深入解析Mac OS X & iOS 操作系統(tǒng) 學習筆記(五)

可執(zhí)行文件

在UNIX中,任何文件都可以通過簡單的 chmod +x 命令標記為可執(zhí)行文件。但是不能保證文件可執(zhí)行,這個標記只是告訴操作系統(tǒng)內核將這個文件讀入內存,然后尋找一個頭簽名,又稱為“魔數(magic)”,據此可以確定精確的可執(zhí)行格式。

OS X支持的可執(zhí)行格式:

  • 解釋器腳本格式:一種特殊形式的二進制文件格式,這種文件指向的文件才是真正得到執(zhí)行的文件
  • 通用二進制格式:這種文件包含一個簡單的文件頭,文件頭后面依次拷貝了每一種支持架構的二進制文件,因此通用二進制往往非常龐大,因此又叫“胖二進制”
胖二進制文件頭格式.png
  • Mach-O二進制格式:OS X獨有的二進制格式。Mach-O文件頭的主要功能在于加載命令(load command)。加載命令緊跟在文件頭后,文件頭中的兩個字段:ncmds和sizeofncmds 是用于解析加載命令
Mach-O文件頭.png

動態(tài)庫

Mach-O鏡像中有很多對外部的庫和符號的引用。由動態(tài)鏈接器來綁定,動態(tài)鏈接器是內核執(zhí)行LC_DYLINKER 加載命令時啟動的,通常使用的是/usr/lib/dyld 作為動態(tài)鏈接器,鏈接器接管剛創(chuàng)建的進程的控制權,因為內核將進程的入口點設置為鏈接器的入口點

共享庫緩存

共享庫緩存(shard library cache)是dyld 支持的另外一個機制。共享庫緩存指的是一些庫經過預先鏈接,然后保存在磁盤上的一個文件中。共享緩存在iOS 中尤為重要,在iOS 中大部分常用庫都被緩存了

庫的運行時加載

OS X 中提供的運行時動態(tài)庫加載API類似于POSIX提供的API,但是OS X 的實現完全不同:

  • dlopen(const char *path)用于尋找和加載指定路徑的庫或bundle
  • dlopen_preflight(const char *path)是Leopard之后OS X提供的擴展,用于模擬dlopen( )的加載過程,但不是真正加載任何東西
  • dlsym(void *handle, char *sym)用于定位之前通過dlopen( ) 打開的句
    柄中的符號
  • dladdr(char *addr, DL_Info *info)通過bundle名稱或地址addr處的庫填充DL_Info結構體,這個函數和GNU擴展一致
  • 在其他函數發(fā)送錯誤時,dlerror( )用于提供一條錯誤信息

Cocoa 和 Carbon 為dl* 系列函數提供了高級包裝,以及CFBundle/NSBundle 對象,用于加載Mach-O bundle文件

dyld 的特性

  • 兩級名稱空間:指的是符號名稱還包含其所在庫的信息
  • 函數攔截:DYLD_INTERPOSE宏定義允許一個庫將其函數替換一個函數的實現

進程

和其他任何搶占式的多任務操作系統(tǒng)一樣,進程作為一個正在執(zhí)行的程序的是實例是UNIX的一個基本概念。這個實例可以通過Progress ID(進程ID,即PID)來唯一辨識。進程還會將其和父進程的親屬關系保存在父進程ID(Parent Progress ID, PPID)中。父進程可以通過fork(或通過posix_spawn)創(chuàng)建子進程,并且預期子進程會消亡。UNIX進程的生母意義就是運行,然后在運行結束之后返回一個整數。子進程返回的整數由其父進程手機。進程將要返回的值傳遞給exit( 2)系統(tǒng)調用(或者重main( )函數中返回)

進程的生命周期

進程生命周期.png

僵尸狀態(tài):當生命周期超過子進程,但是仍然拋棄自己的子進程轉移到其他事情的父進程會使得其子進程處于半死不活的僵尸狀態(tài)

進程地址空間

用戶態(tài)的一個優(yōu)點在于虛擬內存的隔離,進程獨享一個私有的地址空間,在iOS 上范圍2~3GB,在32為OS X 上為4GB,在64位的OS X 上則為不可想象的16EB

  • 進程入口點
    和所有標準的C語言程序一樣,OS X 也有一個標準的入口點,默認為“main”。不過除了三個標準參數:argc、argv 和envp外,Mach-O 還接受第四個參數名為“Apple”的char **。Cocoa 應用程序的標準入口也是C main( ),不過常見做法是將main( ) 實現為NSApplicationMain( )的包裝,通過NSApplicationMain( )進入Objective-C的編程模型
  • 地址空間布局隨機化
    進程在自己私有的虛擬內存空間中啟動?,F在在大部分操作系統(tǒng)中都采用了一種稱為地址空間布局隨機化(ASLR)技術,進程每一次啟動時,地址空間都會被簡單地隨機化,只是偏移基本的布局,程序文本、數據結構仍然是一樣的,實現方法是通過內核將Mach-O 的段“平移”某個隨機系數
  • 32位地址空間(Intel)
    32位地址空間在4GB大?。?32字節(jié))然而和其他操作系統(tǒng)的不同之處在于整個4GB的地址空間都是在用戶空間訪問的,沒有預留的內核空間。
  • 64位地址空間
    64位地址空間允許高達16EB(16GGB)的巨大地址空間,但是大部分硬件架構只支持48位到52位尋址,由于虛擬地址到物理地址轉換的開銷,Intel 架構只使用了48位的虛擬地址,因為用戶內存空間可以訪問的區(qū)域最高到0x7FFF FFFF FFFF。64位地址空間和傳統(tǒng)的OS X 模型的不同之處在于傳統(tǒng)的OS X 內存模型中內核有自己的地址空間,但是新的地址空間允許更快速的用戶態(tài)/內核態(tài)的切換(共享CR3寄存器,CR3寄存器包含了頁表指針的控制寄存器)
  • 32位地址空間(iOS)
    iOS 的地址空間32位Intel地址空間更為受限,首先和32位 OS X 不同的地方在于iOS 的內核映射在用戶地址空間的0xC000 0000(iOS 3) 或 0x8000 0000(iOS 4 和 5),占用1~2GB的空間。此外,0x3000 0000 以上的空間預留給各種庫和框架

進程內存分配(用戶態(tài))

  • 基于棧的內存分配:通常由編譯器處理,因為棧中填充的通常是程序的自動變量
  • 基于堆的內存分配:用于動態(tài)內存分配,只限于用戶態(tài)使用,在內層面,既沒有用戶對也沒有棧的存在。

alloca( )

盡管棧在傳統(tǒng)上一直都是保存自動變量,但是在某些情況下,程序員也可以選擇用棧來動態(tài)分配內存,方法是使用鮮為人知的alloca( ) 這個函數的原型和malloc( )是一樣的,區(qū)別在于這個函數返回的指針是棧上的地址而不是堆中的地址。
從實現角度,alloca( )從兩方面優(yōu)于malloc( )

  • 在棧中非配空間只不過是簡單的修改棧指針寄存器,時間消耗低,不用擔心頁面錯誤
  • 當分配空間的函數返回時,棧中分配的空間會自動釋放,解決內存地址泄露問題
    但是??臻g通常比堆空間受限很多,所以alloca( )非常適合名稱較短的函數中對小空間的分配

堆分配

堆是由C語言運行時維護的用戶態(tài)數據結構,通過堆的使用,程序可以不用直接在頁面的層次處理內存分配。Darwin的libC 采用了一個基于分配區(qū)域(allocation zone)的特殊分配算法

虛擬內存(系統(tǒng)管理員角度)

  • 頁面生命周期
頁面狀態(tài) 意義
Free(空閑) 物理頁面沒有被任何虛擬內存頁面使用,如果有的話,可以立即回收
Acitve(活躍) 物理頁面當前正用于一個虛擬內存頁面,而且最近被引用過。這個頁面不太可能被交換出去,除非不再有任何非活躍的物理頁面存在了。如果這個頁面近期不會被引用,則會被置于非活躍的狀態(tài)
Inactive(非活躍) 物理頁面當前正用于一個虛擬內存頁面,但是最近沒有被任何進程引用過。這個頁面有可能會在有需要時被交換出去,另外,如果這個頁面在任何時刻被引用了,那么會被重新置于活躍狀態(tài)
Speculative(投機) 頁面被投機映射。通常產生這個狀態(tài)的原因是針對可能的內存需求做了一次猜測的分配,但是還沒有處于活躍狀態(tài)(也不是非活躍狀態(tài),因為有可能很快被訪問)
Wired down(聯動) 物理頁面當前正用于一個虛擬內存頁面。但是不能被交換出去,不論引用狀況如何
物理頁面的狀態(tài)遷移.png
  • vm_stat( ):實用工具,顯示內核內部的虛擬內存計數器

  • Translation faults:頁錯誤計數

  • Pages copy-on-write:因為cow錯誤引發(fā)的頁面復制的次數

  • Pages zero filled:被分配且初始化的頁面數

  • Pageins:從交換空間提取頁面的次數

  • Pageouts:將頁面推出到交換空間的次數

  • sysctl( )
    sysctl( )命令是用于查看和修改內核變量的標準UNIX命令,這條命令也支持管理虛擬內存的設置

  • dynamic-pager()
    OS X 有一個來自Mach的獨特之處在于交換空間不是直接在內核層次管理的而是由專用的用戶進程dynamic-pager處理所有的交換技術,dynamic-pager負責管理磁盤上的交換空間

線程

線程作為最大化利用進程時間片的方法,應運而生:通過使用多個線程,程序的指向可以分割表面上看上去并發(fā)執(zhí)行的子任務。線程之間切換的開銷比較小,只要保存和恢復寄存器即可。多核處理器更是特別和適合線程,因為多個處理器核心共享同樣的cache和ARM,這位線程間的共享虛擬內存提供了基礎

  • POSIX 線程
    POSIX 線程模型實際上是除了Windows(Windows 堅持提供Win32 線程 API)之外所有系統(tǒng)使用的線程的API。 OS X 和 iOS 不僅僅是pthread
  • Grand Central Dispatch
    蘋果宣稱這一套API是對線程的替換,這一套API引入了編程范式的變化,不要從線程和線程函數的角度思考,而是鼓勵開發(fā)者從功能塊(functional block)的角度思考。GCD自己維護了一個底層的線程庫實現,以支持并發(fā)和異步的執(zhí)行模型,減輕開發(fā)者處理并發(fā)的負擔以及減少類似于死鎖之類的潛在錯誤。這種機制也可以處理其他異步的通知類型。GCD還支持異步 I/O。 GCD的另外一項優(yōu)勢是能夠自動地隨著邏輯處理器的個數而擴展。

函數塊中的工作由一下幾個分發(fā)隊列中的某一個完成:

  • 全局分發(fā)隊列:應用程序通過dispatch_get_global_queue( )并指定請求的優(yōu)先級(DISPATCH_QUEUE_PRIORITY_DEFAULT、_LOW、或_HIGH)既可以得到這個隊列
  • 主分發(fā)隊列:和Cocoa 應用程序的運行循環(huán)整合在一起。通過dispatch_get_main_queue( )可以獲得此隊列
  • 自定義隊列:通過調用 dispatch_queue_create( )手工創(chuàng)建隊列,可以用于更好地控制分發(fā)行為。自定義隊列既可以是串行隊列(任務以FIFO的順序進行)也可以是并發(fā)隊列

GCD的API都聲明在<dispatch/dispatch.h>頭文件中,代碼實現在libDispatch.dylib庫中,這是一個libSystem的內部庫。這些API本身都是基于preeathed_workqueue API實現的,而XNU通過workq系統(tǒng)調用也支持這個API。要注意的是:Objective-C 進一步封裝這些API,暴露給用戶的是NSOperation 相關的對象

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容