本文參考《Mac OS X and iOS Internals: To the Apple’s Core》 by Jonathan Levin
文章內容主要是閱讀這本書的讀書筆記,建議讀者掌握《操作系統》,了解現代操作系統的技術特點,再閱讀本文可以事半功倍。
雖然iOS系統內核使用極簡的微內核架構,但內容依然十分龐大,所以會分
系統架構、進程調度、內存管理和文件系統四個部分進行闡述。
操作系統管理所有的硬件資源,操作系統內核管理最核心的資源CPU和內存。上一篇闡述了Mach通過進程管理CPU,本文主要闡述XNU和Mach如何高效的管理內存
1 內存分配
- 基于棧的內存分配:通常由編譯器處理,因為棧中填充的通常是程序的自動變量
- 基于堆的內存分配:用于動態內存分配,只限于用戶態使用,在內層面,既沒有用戶對也沒有棧的存在。
1.1 alloca 棧分配
按照傳統,棧一般都是保存自動變量,正常情況棧由系統管理,但是在iOS中某些情況下,程序員也可以選擇用棧來動態分配內存,方法是使用鮮為人知的alloca( ) 這個函數的原型和malloc( )是一樣的,區別在于這個函數返回的指針是棧上的地址而不是堆中的地址。
從實現角度,alloca( )從兩方面優于malloc( )
- 在棧中非配空間只不過是簡單的修改棧指針寄存器,時間消耗低,不用擔心頁面錯誤
- 當分配空間的函數返回時,棧中分配的空間會自動釋放,解決內存地址泄露問題
但是棧空間通常比堆空間受限很多,所以alloca( )非常適合名稱較短的函數中對小空間的分配
1.2 堆分配
堆是由C語言運行時維護的用戶態數據結構,通過堆的使用,程序可以不用直接在頁面的層次處理內存分配。Darwin的libC 采用了一個基于分配區域(allocation zone)的特殊分配算法
2 BSD內存管理
在iOS中內存的管理是由在Mach層中進行的,BSD只是對Mach接口進行了POSIX封裝,方便用戶態進程調用。
XNU內存管理的核心機制是虛擬內存管理,在Mach 層中進行的,Mach 控制了分頁器,并且向用戶態導出了各種 vm_ 和 mach_vm_ 消息接口。 為方便用戶態進程使用BSD對Mach 調用進行了封裝,通過current_map( ) 獲得當前的Mach 內存映射,最后再調用底層的Mach 函數。
2.1 MALLOC 和 zone
BSD 的malloc 系列函數<bsd/sys/malloc.h> 頭文件中。函數名為_MALLOC、_FREE、_REALLOC、_MALLOC_ZONE、_FREE_ZONE
2.2 mcache 和 slab 分配器
mcache機制是BSD 提供的基于緩存的非常高效的內存分配方法。默認實現基于mach zone,通過mach zone提供預分配好的緩存內存。
mcache具有可擴展架構,可以使用任何后端 slab 分配器。
使用mcache 機制的主要優點是速度:內存分配和維護是在每一個 CPU 自有的cache中進行的,因此可以映射到CPU的物理cache,從而極大地提升訪問速度。
2.4 內存壓力
Mach VM層支持VM pressure 的機制,這個機制是可用RAM量低到危險程度的處置,下面我們會詳細講,這里不展開。
當RAM量低到危險時,Mach的pageout 守護程序會查詢一個進程列表,查詢駐留頁面數,然后向駐留頁面數最高的進程發送NOTE_VM_PRESSURE ,會在進程隊列中發出一個事件。被選中的進程會響應這個壓力通知,iOS中的運行時會調用 didReceiveMemoryWarning 方法。
然而有些時候這些操作沒有效果,當內存壓力機制失敗之后,** 非常時間要用非常手段 **, Jetsam機制介入。
2.3 Jestam/Memorystatus
當進程不能通過釋放內存緩解內存壓力時,Jestam機制開始介入。這是iOS 實現的一個低內存清理的處理機制。也稱為Memorystatus,這個機制有點類似于Linux的“Out-of-Memory”殺手,最初的目的就是殺掉消耗太多內存的進程。Memorystatus維護了兩個列表:
- 快照列表:保存系統中所有進程的狀態以及消耗的內存頁面數
- 優先級列表:保存要殺掉的備選進程
在iOS的用戶態可以通過 sysctl(2)查詢這些列表,優先級列表可以在用戶態進行設置。
2.3 進程休眠
在iOS 5中,Jestsam/Memorystatus 和默認的freezer 結合在一起,實現了對進程的冷凍而不是殺死。通過這種方式可以提供更好的用戶體驗,因為數據不會丟失,而且當內存情況好轉時進程可以安全恢復。(感謝@易步指出本段錯誤)
用戶態也可以通過pid_suspend( ) 和 pid_resume( )控制進程的休眠。
iOS 定義了 pid_hibernate,通過發送kern_hibernation_wakeup信號喚醒kernel_hibernation_thread 線程,這個線程專用于對進程冷凍操作。
實際的進程休眠操作是由jestsam_hibernate_top_proc 完成的,這個函數通過task_freeze冷凍底層的任務。
冷凍操作需要遍歷任務的vm_map,然后將vm_map 傳遞給默認的 freezer。
3 Mach 虛擬內存 virtual memory,VM
VM是Darwin系統內存管理的核心機制。
3.1 VM架構
VM 機制主要通過內存對象(memory object)和分頁器(pager)的形式管理內存。
Mach 虛擬內存的實現非常全面而且通用。這部分由兩個層次構成:一層是和硬件相關的部分,另一層構建在這一層之上和硬件無關的公共層。OS X 和 iOS 使用的幾乎一樣的底層機制,硬件無關層以及之上的BSD 層中的機制都是一樣的。
3.1.1 VM系統全貌
Mach 的 VM子系統可以說是和其要管理的內存一樣復雜和充滿了各種細節。然后從高層次看,可以看到兩個層次:
- 虛擬內存的層次
- 物理內存的層次
3.1.2 虛擬內存層
虛擬內存這一層完全以一種機器無關的方式來管理虛擬內存。這一層通過幾種關鍵的抽象表示虛擬內存:
vm_map
表示任務地址空間內的一個或多個虛擬內存區域。每一個區域都是有一個獨立的條目 vm_map_entry 表示。這些條目由一個雙向鏈表vm_map_links維護。vm_map_entry
這是關鍵的數據結構,盡管只有在包含這個結構的映射的上下文中才會訪問到這個結構。每一個vm_map_entry 都表示了虛擬內存中一塊連續的區域(region)。每一個這樣的區域都可以通過指定的訪問保護權限進行保護(和虛擬內存頁面采用同樣的權限)。任務之間可以共享區域。vm_map_entry
通常指向一個vm_object,但是也可以指向一個嵌套的vm_map,即子映射(submap)。vm_object
用于將vm_map_entry 和實際支撐的內存關聯起來。這個數據結構包含一個vm_page 的鏈表,還包含一個用于訪問正確分頁器的Mach 端口(稱為memory_object),通過這個分頁器進行頁面的獲取或清理操作。vm_page
vm_page 真正表示了vn_object 或部分vm_object(由vm_object中的偏移量表示)。
vm_page 可以有多種狀態:駐留內存、交換出、加密、干凈和臟等。
Mach 允許使用多個分頁器。事實上,默認就存在3~4個分頁器。Mach 的分頁器以外部實體的形式存在:是專業的任務,有點類似于其他系統上的內核交換(kernel-swapping)線程。Mach 的設計允許分頁器和內核任務隔離開,設置允許用戶態任務作為分頁器。類似地,底層的后備存儲也可以駐留在磁盤交換文件中(通過OS X 中的 default_pager 處理),可以映射到一個文件(由vnode_pager處理),可以是一個設備(由device_pager 處理)。注意:在Mach 中,每一個分頁器處理的都是屬于這個分頁器的頁面的請求,但是這些請求必須通過pageout 守護程序發出。這些守護程序(實際上就是內核線程)維護內核的頁面表,并且判定哪些頁面需要被清除出去。因此,這些守護程序維護的分頁策略和分頁器實現的分頁操作是分開的。
3.1.3 物理內存層
物理內存的頁面處理的是虛擬內存到物理內存的映射,因為虛擬內存中的內容最終總要存儲在某個地方。這一層面只有一個抽象,那就是pmap,不過這個抽象非常重要,因為提供了機器無關的接口。這個接口隱藏了底層平臺的細節,底層的細節需要在處理器層次進行分頁操作,其中要處理的對象包括硬件頁表項(page table entry,PTE)、翻譯查找表(translation lookaside buffer,TLB)等。
每一個Mach 任務都要自己的虛擬內存空間,任務的struct task 中的 map 字段保存的就是這個虛擬內存空間。
vm_page_entry 中最關鍵的元素是vm_map_object,這是一個聯合體,既可以包含另一個vm_map(作為子映射),也可以包含一個vm_object_t(由于這是一個聯合體,所以具體的內容需要用布爾字段is_sub_map 來判斷)。vm_object 是一個巨大的數據結構,其中包含了處理底層虛擬內存所需要的所有數據。vm_object的數據結構中的大部分字段都是用位表示的標志。這些字段表示了底層的內存狀態(聯動、物理連續和持久化等狀態)和一些計數器(引用計數、駐留計數和聯動計數等)。不過有3個字段需要特別注意:
memq:vm_page 對象的鏈表,每一項都表示一個駐留內存的虛擬內存頁面。盡管一個對象可以表示一個單獨的頁面,但是多數情況下一個對象可以包含多個頁面,所以每一個頁面關聯到一個對象時都會有一個偏移值
page:memory_object 對象,這是指向分頁器的Mach 端口。分頁器將未駐留內存的頁面關聯到后備存儲,后備存儲可以是內存映射的文件、設備和交換文件,后備存儲保存了沒有駐留內存的頁面。換句話說,分頁器(可以有多個)負責將數據從后備存儲移入內存以及將數據從內存移出到后備存儲。分頁器對于虛擬內存子系統來說極為重要
internal:vm_page 中眾多標志位之一,如果這個位為真,那么表示這個對象是由內核內部使用的。這個標志位的值決定了對象中的頁面會進入哪一個pageout隊列
3.2 Mach 物理內存管理
盡管內核和用戶空間一樣,基本上只在虛擬地址空間內操作,但是虛擬內存最終還是要翻譯為物理地址的。機器的RAM 實際上是虛擬內存中開的窗口,允許程序訪問虛擬內存是有限的,而且通常是不連續的區域,這些區域的上線就是機器上安裝的內存。而虛擬內存中其他部分則要么延遲分配,要么共享,要么被交換到外部存儲中,外部存儲通常是磁盤。
然而虛擬內存和具體的底層架構相關。盡管虛擬內存和物理內存的概念在所有架構上本周都是一樣的,但是具體的實現細節則各有千秋。XNU 構建與Mach 的物理內存抽象層之上,這個的抽象層成為pmap。pmap 從設計上對物理內存提供了一個統一的接口,屏蔽了架構相關的區別。這對于XNU來說非常有用,因為XNU支持的物理內存的架構包括以前的PowerPC,現在主要是Intel,然后在iOS 中還支持ARM。
3.2.1 pmap 的 API
Mach 的pmap 層邏輯上由一下兩個子層構成:
- 機器無關層
提供了一組基本上和及其無關的API。只要求及其支持基本的虛擬內存分頁的概念。VM層只考慮pamp_t 并傳遞這個類型的數據即可,pmap_t 是一個指向struct pmap 是指針,實際上是一個void 指針 - 機器相關層
將pmap綁定到一個具體的實現,處理底層敬愛個的各種細節
3.3 Mach Zone
Mach Zone的概念相當于Linux的內存緩存(memory cache)和Windows 的Pool。Zone 是一種內存區域,用于快速分配和是否頻繁使用的固定大小的對象。Zone的API是內核內部使用的,在用戶態不能訪問。Mach中Zone的使用非常廣泛。
3.3.1 Mach Zone 的結構
所有的zone 內存實際上都是在調用zinit( )時預先分配好的(zinit( )通過底層內存分配器kernel_memory_allocate( )分配內存)zalloc( )實際上是對REMOVE_FROM_ZONE 宏的封裝,作用是返回zone的空閑列表中的下一個元素(如果zone已滿,則調用kernel_memory_allocate( )分配這個zone在定義的alloc_size字節)。zfree( ) 使用的是相反功能的宏 ADD_TO_ZONE。這兩個函數都會執行合理數量的參數檢查,不過這些檢查幫助不大:過去zone分配相關的bug已經導致了數據可以被黑客利用的內存損壞。zalloc( ) 最重要的客戶是內核中的kalloc( ),這個函數從kalloc.*系列zone中分配內存。BSD的mcache機制也會從自己的zone中分配內存。BSD內核zone也是如此,BSD內核zone直接構建與Mach的zone之上。
3.5 Mach 分頁器
進程的內存需求早晚會超過可用的RAM,系統必須有一種方法能夠將不活動的頁面備份起來,并且從RAM中刪除,騰出更多的RAM給活動的頁面使用,至少暫時能夠滿足活動頁面的需求。在其他操作系統中,這個工作專門是由專門的內核線程完成的。在Mach 中,這些專門的任務稱為分頁器(pager),分頁器可以是內核線程,設置建議是外部的用戶態服務程序。
Mach分頁器是一個內存管理器,負責將虛擬內存備份到某個特定類型的后備存儲中。當內存容量不足,內存頁面需要被交換出內存是,后備存儲保存內存頁面的內容:當換出的內存頁面需要被使用時,將內存的頁面恢復到RAM中。只有“臟”頁面才需要進行上述的換出和換入,因為“臟”頁面是在內存中修改過的頁面,要從RAM中剔除時必須保存到磁盤中防止數據丟失。
要注意的是,這里提到的分頁器僅僅實現了各自負責的內存對象的分頁操作,這些分頁器不會控制系統的分頁策略。分頁策略是有vm_pageout 守護線程負責的。
3.4.1 分頁器的類型
iOS 和 OS X 中XNU 包含的分頁器種類都是一樣的。下表是XNU中的內存分頁器的多種類型:
內存分頁器 | 用途 |
---|---|
Default | 負責內存交換的通用接口 |
VNode | 內存映射文件 |
Device | 概念類似VNode,通過IO接口將數據交換給外設 |
Swapfile | 阻止直接映射交換文件的企圖 |
Apple-protected | 實現Apple代碼加密機制 |
Default Freezer | 在物理內存較少切沒有真正交換文件的系統上,應用程序在后臺時不需要真正的運行,當用戶將應用切入后臺,系統將應用冷凍,在應用恢復時解凍 |
3.6 分頁策略管理
3.6.1 Pageout 守護程序
pageout 守護程序其實不是一個真的守護程序,而是一個線程。而且不是一般的線程:當kernel_bootstarp_thread( ) 完成內核初始化工作并且沒有其他事情可做時,就調用vm_pageout( ) 成為了pageour 守護程序, vm_pageout( ) 永遠不返回。這個線程管理頁面交換的策略,判斷哪些頁面需要寫回到其后備存儲。
vm_pageout線程
vm_pageout( ) 函數講kernel_bootstrap_thread 線程轉變為pageout 守護程序,這個函數實際上重新設置了這個線程。設置完成后,調用vm_pageout_continute( ),這個函數周期性地喚醒并執行vm_page_scan( ),維護4個頁面表(稱為頁面隊列)。系統中的每一個vm_page 都通過pageq字段綁定這4個隊列中的一個:
vm_page_queue_active
最近活躍且駐留在內存中的頁面vm_page_queue_inactive
最近不活躍的頁面,因此這些頁面是頁面換出的備選頁面。根據這些頁面的使用情況,可能會被換出,也可能會被重新激活vm_page_queue_free
空閑頁面表。這些頁面曾經是非活躍的頁面,但是被清理出去了(頁面換出)vm_page_queue_speculative
這些頁面是通過預讀策略投機映射的頁面,這些頁面是不活躍的,但是很可能很快會變為活躍頁面
垃圾回收線程
垃圾回收線程(vm_pageout_garbage_collect( ))偶爾會被vm_pageout_scan( ) 通過其續體喚醒。垃圾回收機制線程處理4個方面的垃圾回收工作:
srack_collect( )
內核棧中的頁面consider_machine_collect( )
回收機器相關的頁面consider_buffer_cache_collect( )
如果確定定義了這個函數則調用這個函數。調用這通過vm_set_buffer_cleanup_callout( ) 定義這個函數。BSD 層在bufinit( ) 函數中注冊了buffer_cache_gc( ) 函數consider_zone_gc( )
zone 相關的垃圾回收
3.6.2 處理頁錯誤
vm_pageout( ) 守護程序處理的只是交換的一個方向,從物理內存換出到后備存儲。而另外一個方向是頁面換入,則是發生在頁面錯誤的時候處理的。這個邏輯非常復雜,簡化為一下步驟:
- 如果陷阱的原因是頁錯誤,那么機器級別的線程處理程序調用vm_fault( )
- vm_fault( ) 函數調用 vm_pageout_fault( )處理實際發生錯誤的頁面,并且從后備存儲中將這個頁面返回
- PMAP_ENTER( ) 將頁面插入任務的pmap
頁錯誤有很多種,上述只是其中一種,其他類型的也錯誤還包括:
- 非法訪問
訪問應該沒有映射到進程地址空間(即任務的vm_map)的地址。解引用應該野指針時通常會發生這種錯誤。發生這種錯誤時進程會收到SIGSEGV信號 - 頁面保護錯誤
訪問應該映射的地址,但是頁面的保護掩碼拒絕請求的訪問 - 寫時復制(copy-on-write)
頁面可以被標記可讀,因此如果任務試圖寫入頁面時,會捕捉到這個錯誤,在重新嘗試寫入操作之前可以將這個頁面復制出來
總結
VM系統是Mach中最重要最復雜而且最不好理解的子系統。Mach的內存管理核心是分頁器,分頁器允許將虛擬內存擴展到各種后背存儲介質上:交換文件、內存映射文件、設備、甚至遠程主機。
iOS中提高內存使用率的Freezer,以及處理內存耗盡的pageout守護程序。