iOS虛擬內存管理

虛擬內存概述

虛擬內存是一種允許操作系統避開設備的物理RAM限制的內存管理機制。虛擬內存管理器爲每個進程創建一個邏輯地址空間或者虛擬內存地址空間,並且將它分配爲相同大小的內存塊,可稱爲頁。處理器與內存管理單元MMU維持一個頁表來映射程序邏輯地址空間到計算機RAM的硬件地址。當程序的代碼訪問內存中的一個地址時,MMU利用頁表將指定的邏輯地址轉換爲真實的硬件內存地址,這種轉換自動發生並且對於運行的應用是透明的。

就程序而言,在它邏輯地址空間的地址永遠可用。然而,當應用訪問一個當前並沒有在物理RAM中的內存頁的地址時,就會發生頁錯誤。當這種情況發生時,虛擬內存系統調用一個專用的頁錯誤處理器來立即響應錯誤。頁錯誤處理器停止當前執行的代碼,定位到物理內存的一個空閒頁,從磁盤加載包含必要數據的頁,同時更新頁表,之後返回對程序代碼的控制,程序代碼就可以正常訪問內存地址了,這個過程被稱爲分頁。

如果在物理內存中沒有空閒頁,頁錯誤處理器必須首先釋放一個已經存在的頁從而爲新頁提供空間,如何釋放頁由系統平臺決定系統。在OS X,虛擬內存系統常常將頁寫入備份存儲,備份存儲是一個基於磁盤的倉庫,包含了給定進程內存頁的拷貝。將數據從物理內存移到備份存儲被稱爲頁面換出;將數據從備份存儲移到物理內存被稱爲頁面換入。在iOS,沒有備份存儲,所以頁永遠不會換出到磁盤,但是隻讀頁仍可以根據需要從磁盤換入。

在OS X 和iOS中,頁大小爲4kb。因此,每次頁錯誤發生時,系統會從磁盤讀取4kb。當系統花費過度的時間處理頁錯誤並且讀寫頁,而並不是執行代碼時,會發生磁盤震盪(disk thrashing)。

無論頁換出/換入,磁盤震盪會降低性能。因爲它強迫系統花費大量時間進行磁盤的讀寫。從備份存儲讀取頁花費相當長的時間, 並且比直接從RAM讀取要慢很多。如果系統從磁盤讀取另一個頁之前,不得不將一個頁寫入磁盤時,性能影響會更糟。

虛擬內存的限制

在iOS開發的過程中,難免手動去申請內存,目前大多數的移動設備都是ARM64的設備,即使用的是64位尋址空間,而且在iOS上 通過malloc申請的內存只是虛擬內存,不是真正的物理內存,那麼在iOS設備上爲什麼會出現申請了2-3G就會出現申請失敗呢?

當申請分配一個超大的內存時,iOS系統會按照 nano_zone 和 scalable_zone 的設計理念進行內存的申請,申請原理如下:

  • 小於1k的走 tiny_malloc

  • 小於15k或者127k的走 small_malloc (視不同設備內存上限而不同)

  • 剩下的走 large_malloc

由於我們分配的非常大,我們可以確定我們的邏輯是落入 large_malloc 中。需要特別注意的是: large_malloc 分配內存的基本單位是一頁大小,而對於其他的幾種分配方式,則不是必須按照頁大小進行分配。

由於 large_malloc 這個函數本身並沒有特殊需要注意的地方,我們直接關注其真正分配內存的地方,即 allocate_pages ,如下所示:

從上不難看出,如果分配失敗,就是提示報錯。而 mach_vm_map 則是整個內存的分配核心。

概括來說, vm_map代表就是一個進程運行時候涉及的虛擬內存, pmap 代表的就是和具體硬件架構相關的物理內存。(這裏我們暫時先不考慮 submap 這種情況)。

vm_map 本身是進程(或者從Mach內核的角度看是task的地址分佈圖)。這個地址分佈圖維護着一個 雙向列表 ,列表的每一項都是 vm_entry_t ,代表着虛擬地址上連續的一個範圍。而 pmap 這個結構體代表了個硬件相關的內存轉換:即利用 pmap 這個結構體來描述抽象的物理地址訪問和使用。

進程(任務)的創建

對於在iOS上的進程創建和加載執行Mach-O過程,有必要進行一個簡單的介紹,在類UNIX系統本質上是不會無緣無故創建出一個 進程的,基本上必須通過 fork 的形式來創建。無論是用戶態調用 posix 相關的API還是別的API,最終落入內核是均是通過函數fork_create_child 來創建屬於Mach內核的任務。實現如下:

要注意的就是Mach內核裏面沒有進程的概念,只有任務,進程是屬於BSD之上的抽象。它們之間的聯繫就是通過指針建立,child_proc->task = child_task 。

fork 出來的進程像是一個空殼,需要利用這個進程殼去執行科執行文件編程有意義的程序進程。從XNU上看,可執行文件的類型有如下分類:

常用的通常是 Mach-o 文件:

上面的代碼基本上都是在對文件進行各種檢查,然後分配一個預使用的進程殼,之後使用 load_machfile 加載真正的二進制文件。

利用 pmap_create 創建硬件相關的物理內存抽象。利用 vmap_create 創建虛擬內存的地址圖。ARM64下的頁是16k一個虛擬頁對應一個物理頁。

這裏需要重點關注 vm_map_create 0 和vm_compute_max_offset(result->is64bit) ,代表着當前任務分配的虛擬內存地址的上下限, vm_compute_max_offset 函數實現如下:

pmap_max_offset 函數實現如下:

這裏的關鍵點代碼是:

max_offset_ret 這個值就代表了我們任務對應的 vm_map_t 的最大地址範圍,比如說這裏是8.375GB。

虛擬內存分配的限制

之前提到了 large_malloc 會走入到最後的 vm_map_enter ,那麼我們來看看 vm_map_enter 的實現:

  • 注意點1:基本上就是檢查頁的權限等,iOS上不允許可寫和可執行並存。

  • 剩下的就是作各種前置檢查。

如果上述代碼不夠清晰明瞭,如下這段代碼可以更加的簡潔:

  • 整個這段代碼的意思是,就是要我們要找個一個比我們這個 start 地址大的 vm_entry_t。最終的目的是爲了在兩個已經存在 vm_entry_t 之間嘗試插入一個能包含從 start 到 start + size 的新的 vm_entry_t 。

  • 如果沒找到的話,就嘗試利用 vm_map_lookup_entry 找一個 preceding 我們地址的的 vm_entry_t 。

當找到了一個滿足 start 地址條件的 vm_entry_t 後,剩下就是要滿足分配大小 size 的需求了。

判斷 start + size 是不是可以正好插入在 vm_entry_t 代表的地址範圍的空隙內,如果一直遍歷到最後的任務地址上限都找不到,那就說明不存在我們需求的連續的虛擬內存空間用於作分配了。

總結

除了本文說明的虛擬內存分配的連續性限制以外,虛擬內存作爲堆內存分配的一種,在佈局範圍上也有限制。更多詳細的信息可參考如下鏈接。

作者簡介

張永超,TalkingData資深研發工程師,負責TalkingData SDK的研發工作,對於IOS有深入的瞭解,並且對於機器學習有不錯的瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章