深入淺出iOS系統內核(3)— 內存管理

本文參考《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守護程序。

參考資料

https://www.amazon.com/Mac-OS-iOS-Internals-Apples/dp/1118057651/ref=sr_1_2?ie=UTF8&qid=1331298923&sr=8-2

https://developer.apple.com/library/content/documentation/Darwin/Conceptual/KernelProgramming/About/About.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容