C、作業系統學習筆記

Yang1492955186752發表於2017-12-13

背景:由於基礎比較差,所以最近在惡補C語言以及其他基礎學科例如:作業系統之類的基礎學科。然後結合筆記和自己的理解寫了一點C和OC的聯絡。

首先從最基礎的開始-指標 C語言的精華就在於指標(pointer),接下來我們看圖說話 貼一段示範程式碼:

1.png

剛開始我們宣告瞭一個int型別的指標變數但是沒有初始化,然後給a賦值,這時候我們執行程式會爆記憶體洩露的問題。這是因為初始化a的時候系統預設a這個指標變數指向0x0這個地址(IDE為Xcode,編譯器為LLVM),這時候向*a所指向的地方進行賦值是也就是向0x0這個地址賦值,這樣的行為被編譯器預設是非法的,假如是使用的其他的編譯器,如果編譯器不認定這種行為非法,可能會產生各種詭異的情況,因為沒有初始化過的指標物件有可能指向記憶體的任何區域,冒然的對所指向的地址進行賦值可能會引發程式崩潰。 這時候我們需要先對指標變數進行初始化操作如圖:

2.png
然後進行列印:

3.png

因此我們可以得出結論:b作為指標變數,它存的值與a在記憶體中地址是一致的,通過'*'b對指標變數所存的地址進行取值也就是2。然後我們列印指標b的地址 &b,結果與之前的相差8個位元組,這說明指標b也是一個變數,它在記憶體中也是有地址的,所儲存的值就是所指向變數的地址。

由列印結果引出下面的討論:由於我們宣告的是本地變數,所以變數被分配到棧裡,由編譯器進行生命週期的管理(ARC就是編譯器特性),在棧裡存放的是函式的引數值,本地變數的值。由於移動端的裝置屬於暫存器計算機,一切運算都是在暫存器中完成,記憶體是無法進行執行的,下文的rsp和rbp是指標暫存器,當一個函式被執行的時候,首先會把本地變數和所需引數壓入棧內,然後通過rsp(棧頂指標)和rbp(基地址指標)進行操作,當函式被呼叫時,rbp先入棧,儲存當前棧的狀態值,用來後面恢復本棧的狀態,然後將rsp裝入rbp更新棧的底部,然後切換到下一個函式(也就是下一個棧),然後將rsp減去所需空間大小,抬高棧頂。在函式執行完畢後會先pop區域性變數和引數,然後根據棧底的rbp指標找到下一個函式的入口以及其他本地變數和引數。在OC中我們所說的作用域就是函式執行的範圍,本地變數執行完後會自動銷燬是因為在棧內被pop了,導致引用計數減1。而全域性變數靜態變數都在記憶體的靜態區域,所以不需要程式設計師進行管理。接下來我們看另一種列印圖:

4.png

我們可以發現全域性變數和靜態變數在同一片區域,本地變數,成員變數在棧區,但是兩者的地址差距非常大,這是為什麼呢?首先我們的清楚成員變數本質是ivar,而每個OC物件都是struct objc_object,在這個結構體的內部有一個isa指標,這個isa指標指向struct objc_class。這個結構體應該大家都熟悉:


struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;             // formerly cache pointer and vtable
class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
//...
};
複製程式碼

superclass是指向metaClass的指標。 cache是這個類的方法快取,每當呼叫方法的時候會現在cache裡查詢,沒有就新增到快取中,提高命中率。 bits 給類分配空間的標誌。 data 存的就是方法列表,成員變數等資料,通過查詢可以看到ivars的結構體。

struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};
複製程式碼

通過查詢這個結構體裡的first結構體

struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
//...
};
複製程式碼

我們發現了offset,而且是一個指標,它記錄了這個ivar,所以當我們列印成員變數地址的時候會首先根據struct objc_object這個結構體裡的isa指標找到成員變數所在的類的struct objc_class,runtime根據類物件在記憶體中的地址然後加上offset就可以找到成員變數了。因此我們可以根據Extension在編譯期新增成員變數,runtime會通過修改offset來更新成員變數的偏移距。那麼為什麼不能在程式執行期間動態新增成員變數呢?那是因為在編譯期,編譯期就會根據給定的成員變數計算空間大小並佈局,假如可以動態的新增成員變數,就會導致runtime在訪問已建立出來的子類的時候出現無法識別的情況,因為佈局改變,runtime通過offset查詢成員變數的時候就無法保證準確性。 那麼為什麼runtime可以動態新增方法呢? 1.因為方法列表struct objc_method_list **methodLists 這個結構體沒有isa指標,所以沒有指向一個固定的記憶體區域。 2.蘋果給這個結構體宣告瞭一個二階指標,因此決定了外界可以動態的修改方法。 3.這個方法列表的資料結構是一個連結串列,由於Category可以“覆蓋”類的原方法,但是經檢測證明原方法還在只不過位於Category方法的後一個,因此可以推測應該是單向連結串列,當新增一個Category方法對原方法進行覆蓋時,首先runtime會遍歷這個連結串列,直到找到這個方法為止,每個方法也是一個結構體,

typedef struct method_t{
const char *name;
struct   method_t *  next;
//...
}
複製程式碼

當需要插入的這個方法前面時,首先判斷方法名與目標方法名是否相等: 若相等,先建立節點a(也就是struct method_t),然後使前一個節點的next指標指向a,然後使a->next指向原方法的節點。這樣子保證Category的方法能一直在原方法前面,runtime呼叫同名方法根據next指標一個個查詢,直到發現同名方法為止。

然後我們來看一下作業系統。 首先提出一點,作業系統中,執行緒的切換其本質是棧的切換。 然後又分為使用者級執行緒和核心級執行緒,每一個使用者級執行緒維護一個棧,每個核心機執行緒維護一套棧。當使用者級執行緒相互切換的時候,是通過一個對映表將ESP切換到記憶體不同位置,保證目標棧的唯一性。當使用者級執行緒切換到核心級執行緒是系統通過呼叫fork從而將使用者級執行緒切換到核心級執行緒,這時候函式呼叫就是在核心棧中進行的。

5.png

我們iOS上所用到的GCD就是基於核心級(XNU)執行緒實現的,因此普通程式碼級別的執行緒排程是無論如何都比不上核心級執行緒排程來的快,因為程式碼級別的執行緒一般都要在系統級別上實現。並且蘋果已經將GCD的APi優化的足夠高了,以至於GCD的API中關於block的迴圈引用問題都不需要我們考慮(因為Block只被DispatchQueue管理)。因為基於GCD 的block不是直接加入DispatchQueue中,而是先加入DispatchContinuation這個dispatch_continuation_t這個結構體中,然後再加入DispatchQueue這個FIFO的佇列。該DispatchContinuation用於儲存block所屬的的dispatchGroup和其他資訊,在執行GCD追加的block的時候,libdispatch(一個C函式庫)從DispatchQueue取到DispatchContinuation,然後呼叫pthread_workqueue_np,將queue本身以及回撥函式傳遞給引數,然後XNU核心級執行緒根據系統狀態判斷是否生成執行緒。

後面我會繼續學習,假如有合適的機會還是會寫筆記的。

相關連結: 網易雲課堂-C語言程式設計進階

網易雲課堂-資料結構

網易雲課堂-作業系統

Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理

相關文章