天天看點

核心是如何管理記憶體的

本片是轉的站内人翻譯的國外的部落格。英文原連結為:

http://duartes.org/gustavo/blog/post/how-the-kernel-manages-your-memory/

之是以發這個,是因為這對于後來講解page cache和buffer cache的關系是很有意義的。如果部落格的讀者想在buffer cache和page cache的關系上搞透,那麼我建議你要看完本篇、翻譯的page cache的介紹和翻譯的buffer cache以及最後我的總結篇。

在仔細審視了程序的虛拟位址布局之後,讓我們把目光轉向核心以及其管理使用者記憶體的機制。再次從gonzo圖示開始:

核心是如何管理記憶體的

Linux程序在核心中是由task_struct的執行個體來表示的,即程序描述符。task_struct的mm字段指向記憶體描述符(memory descriptor),即mm_struct,一個程式的記憶體的執行期摘要。它存儲了上圖所示的記憶體段的起止位置,程序所使用的實體記憶體頁的數量(rss表示Resident Set Size),虛拟記憶體空間的使用量,以及其他資訊。我們還可以在記憶體描述符中找到用于管理程式記憶體的兩個重要結構:虛拟記憶體區域集合(the set of virtual memory areas)及頁表(page table)。Gonzo的記憶體區域如下圖所示:

核心是如何管理記憶體的

每一個虛拟記憶體區域(簡稱VMA)是一個連續的虛拟位址範圍;這些區域不會交疊。一個vm_area_struct的執行個體完備的描述了一個記憶體區域,包括它的起止位址,決定通路權限和行為的标志位,還有vm_file字段,用于指出被映射的檔案(如果有的話)。一個VMA如果沒有映射到檔案,則是匿名的(anonymous)。除memory mapping 段以外,上圖中的每一個記憶體段(如:堆,棧)都對應于一個單獨的VMA。這并不是強制要求,但在x86機器上經常如此。VMA并不關心它在哪一個段。

一個程式的VMA同時以兩種形式存儲在它的記憶體描述符中:一個是按起始虛拟位址排列的連結清單,儲存在mmap字段;另一個是紅黑樹,根節點儲存在mm_rb字段。紅黑樹使得核心可以快速的查找出給定虛拟位址所屬的記憶體區域。當你讀取檔案/proc/pid_of_process/maps時,核心隻須簡單的周遊指定程序的VMA連結清單,并列印出每一項來即可。

在Windows中,EPROCESS塊可以粗略的看成是task_struct和mm_struct的組合。VMA在Windows中的對應物時虛拟位址描述符(Virtual Address Descriptor),或簡稱VAD;它們儲存在平衡樹中(AVL tree)。你知道Windows和Linux最有趣的地方是什麼嗎?就是這些細小的不同點。

4GB虛拟位址空間被分割為許多頁(page)。x86處理器在32位模式下所支援的頁面大小為4KB,2MB和4MB。Linux和Windows都使用4KB大小的頁面來映射使用者部分的虛拟位址空間。第0-4095位元組在第0頁,第4096-8191位元組在第1頁,以此類推。VMA的大小必須是頁面大小的整數倍。下圖是以4KB分頁的3GB使用者空間:

核心是如何管理記憶體的

處理器會依照頁表(page table)來将虛拟位址轉換到實體記憶體位址。每個程序都有屬于自己的一套頁表;一旦程序發生了切換,使用者空間的頁表也會随之切換。Linux在記憶體描述符的pgd字段儲存了一個指向程序頁表的指針。每一個虛拟記憶體頁在頁表中都有一個與之對應的頁表項(page table entry),簡稱PTE。它在普通的x86分頁機制下,是一個簡單的4位元組記錄,如下圖所示:

核心是如何管理記憶體的

Linux有一些函數可以用于讀取或設定PTE中的每一個标志。P位告訴處理器虛拟頁面是否存在于(present)實體記憶體中。如果是0,通路這個頁将觸發頁故障(page fault)。記住,當這個位是0時,核心可以根據喜好,随意的使用其餘的字段。R/W标志表示讀/寫;如果是0,頁面就是隻讀的。U/S标志表示使用者/管理者;如果是0,則這個頁面隻能被核心通路。這些标志用于實作隻讀記憶體和保護核心空間。

D位和A位表示資料髒(dirty)和通路過(accessed)。髒表示頁面被執行過寫操作,通路過表示頁面被讀或被寫過。這兩個标志都是粘滞的:處理器隻會将它們置位,之後必須由核心來清除。最後,PTE還儲存了對應該頁的起始實體記憶體位址,對齊于4KB邊界。PTE中的其他字段我們改日再談,比如實體位址擴充(Physical Address Extension)。

虛拟頁面是記憶體保護的最小單元,因為頁内的所有位元組都共享U/S和R/W标志。然而,同樣的實體記憶體可以被映射到不同的頁面,甚至可以擁有不同的保護标志。值得注意的是,在PTE中沒有對執行許可(execute permission)的設定。這就是為什麼經典的x86分頁可以執行位于stack上的代碼,進而為黑客利用堆棧溢出提供了便利(使用return-to-libc和其他技術,甚至可以利用不可執行的堆棧)。PTE缺少不可執行(no-execute)标志引出了一個影響更廣泛的事實:VMA中的各種許可标志可能會也可能不會被明确的轉換為硬體保護。對此,核心可以盡力而為,但始終受到架構的限制。

虛拟記憶體并不存儲任何東西,它隻是将程式位址空間映射到底層的實體記憶體上,後者被處理器視為一整塊來通路,稱作實體位址空間(physical address space)。對實體記憶體的操作還與總線有點聯系,好在我們可以暫且忽略這些并假設實體位址範圍以位元組為機關遞增,從0到最大可用記憶體數。這個實體位址空間被核心分割為一個個頁幀(page frame)。處理器并不知道也不關心這些幀,然而它們對核心至關重要,因為頁幀是實體記憶體管理的最小單元。Linux和Windows在32位模式下,都使用4KB大小的頁幀;以一個擁有2GB RAM的機器為例:

核心是如何管理記憶體的

在Linux中,每一個頁幀都由一個描述符和一些标志所跟蹤。這些描述符合在一起,記錄了計算機内的全部實體記憶體;可以随時知道每一個頁幀的準确狀态。實體記憶體是用buddy memory allocation技術來管理的,是以如果一個頁幀可被buddy 系統配置設定,則它就是可用的(free)。一個被配置設定了的頁幀可能是匿名的(anonymous),儲存着程式資料;也可能是頁緩沖的(page cache),儲存着一個檔案或塊裝置的資料。還有其他一些古怪的頁幀使用形式,但現在先不必考慮它們。Windows使用一個類似的頁幀編号(Page Frame Number簡稱PFN)資料庫來跟蹤實體記憶體。

讓我們把虛拟位址區域,頁表項,頁幀放到一起,看看它們到底是怎麼工作的。下圖是一個使用者堆的例子:

核心是如何管理記憶體的

藍色矩形表示VMA範圍内的頁,箭頭表示頁表項将頁映射到頁幀上。一些虛拟頁并沒有箭頭;這意味着它們對應的PTE的存在位(Present flag)為0。形成這種情況的原因可能是這些頁還沒有被通路過,或者它們的内容被系統換出了(swap out)。無論那種情況,對這些頁的通路都會導緻頁故障(page fault),即使它們處在VMA之内。VMA和頁表的不一緻看起來令人奇怪,但實際經常如此。

一個VMA就像是你的程式和核心之間的契約。你請求去做一些事情(如:記憶體配置設定,檔案映射等),核心說”行”,并建立或更新适當的VMA。但它并非立刻就去完成請求,而是一直等到出現了頁故障才會真正去做。核心就是一個懶惰,騙人的敗類;這是虛拟記憶體管理的基本原則。它對大多數情況都适用,有些比較熟悉,有些令人驚訝,但這個規則就是這樣:VMA記錄了雙方商定做什麼,而PTE反映出懶惰的核心實際做了什麼。這兩個資料結構共同管理程式的記憶體;都扮演着解決頁故障,釋放記憶體,換出記憶體(swapping memory out)等等角色。讓我們看一個簡單的記憶體配置設定的例子:

核心是如何管理記憶體的

當程式通過brk()系統調用請求更多的記憶體時,核心隻是簡單的更新堆的VMA,然後說搞好啦。其實此時并沒有頁幀被配置設定,新的頁也并沒有出現于實體記憶體中。一旦程式試圖通路這些頁,處理器就會報告頁故障,并調用do_page_fault()。它會通過調用find_vma()去搜尋哪一個VMA含蓋了産生故障的虛拟位址。如果找到了,還會根據VMA上的通路許可來比對檢查通路請求(讀或寫)。如果沒有合适的VMA,也就是說記憶體通路請求沒有與之對應的合同,程序就會被處以段錯誤(Segmentation Fault)的罰單。

當一個VMA被找到後,核心必須處理這個故障,方式是察看PTE的内容以及VMA的類型。在我們的例子中,PTE顯示了該頁并不存在。事實上,我們的PTE是完全空白的(全為0),在Linux中意味着虛拟頁還沒有被映射。既然這是一個匿名的VMA,我們面對的就是一個純粹的RAM事務,必須由do_anonymous_page()處理,它會配置設定一個頁幀并生成一個PTE,将出故障的虛拟頁映射到那個剛剛配置設定的頁幀上。

事情還可能有些不同。被換出的頁所對應的PTE,例如,它的Present标志是0但并不是空白的。相反,它記錄了頁面内容在交換系統中的位置,這些内容必須從磁盤讀取出來并通過do_swap_page()加載到一個頁幀當中,這就是所謂的major fault。

至此我們走完了”核心的使用者記憶體管理”之旅的前半程。

繼續閱讀