本文翻譯自 How The Kernel Manages Your Memory
在介紹完進(jìn)程中虛擬地址空間的布局后,我們來看一看內(nèi)核是如何管理內(nèi)存的:
內(nèi)核中使用結(jié)構(gòu)體 task_struct 來描述進(jìn)程,其中含有一個(gè) mm_struct 類型的成員 mm,該類型是內(nèi)存管理的主要數(shù)據(jù)結(jié)構(gòu),如上圖所示,它存儲(chǔ)著以下內(nèi)容:
- 每一個(gè)虛擬地址段的起始地址
- 進(jìn)程使用的物理頁面 (Physical Page) 的數(shù)量
- 進(jìn)程使用的虛擬地址空間(Virtual Address Space)的數(shù)量
- 其他額外的信息
還有兩個(gè)和內(nèi)存管理相關(guān)的重要概念:Virtual Memory Area 和 Page Table
每一個(gè) Virtual Memory Area (以下簡稱 VMA)是一段連續(xù)且不重疊的虛擬地址,內(nèi)核用 vm_area_struct 來描述 VMA,它記錄著如下信息:
- VMA 起始地址
- 訪問權(quán)限的標(biāo)記
- 映射文件(如果有的話。沒有映射文件的 VMA 是匿名映射)
在上圖中,每一個(gè) Memory Segment (如 heap,stack 等) 都對應(yīng)著一個(gè) VMA。
一個(gè)進(jìn)程的所有 VMA 都被保存在它的內(nèi)存描述符中的一個(gè)已排序(根據(jù)虛擬地址的起始地址)的鏈表(在mmap字段中)和紅黑樹(mm_rb)中。使用紅黑樹可以使內(nèi)核快速查找一給定地址所在的 VMA 中。通過查看 /proc/pid_of_process/maps 文件,內(nèi)核會(huì)遍歷該進(jìn)程的 VMA 鏈表并打印出來。
在 Windows 中,EPROCESS block 大致是 task_struct 和 mm_struct 的混合體。存儲(chǔ) VMA 的結(jié)構(gòu)在 Windows 中叫 VAD(Virtual Address Descriptor),VAD 存放在一棵 AVL 樹中。
4GB 的虛擬地址空間被分割成 頁(Page),x86 的32位處理器支持 4KB、2MB 和 4MB 大小的頁,Linux 和 Windows 都默認(rèn)使用 4KB 大小的頁。一個(gè) VMA 的大小必須是頁大小的整數(shù)倍。下圖是 3GB 大小用戶空間的 Page 示例圖:
處理器使用頁表(Page Table)來將虛擬地址轉(zhuǎn)換為物理地址,每一個(gè)進(jìn)程都維護(hù)著它自己的頁表,當(dāng)進(jìn)程切換時(shí),對應(yīng)的頁表(用戶空間的)也會(huì)發(fā)生切換。Linux 在內(nèi)存描述符使用 pgd 字段指向該進(jìn)程的頁表,每 一個(gè)虛擬頁面都對應(yīng)著一個(gè)頁表項(xiàng)(Page Table Entry,PTE),在x86機(jī)器上如下所示:
PTE 中有很多 flag,Linux 使用專門的函數(shù)來設(shè)置和讀取這些 flag。
- P 標(biāo)志位表示該頁面是否已經(jīng)映射到物理內(nèi)存,如果該位為0,訪問該頁將觸發(fā)缺頁中斷,Keep in mind that when this bit is zero, the kernel can do whatever it pleases with the remaining fields.
- R/W 標(biāo)志位表示該頁的讀寫屬性,0為只讀;
- U/S 表示該頁的訪問權(quán)限,0表示只能被內(nèi)核訪問。
以上這些標(biāo)志位實(shí)現(xiàn)了內(nèi)存的寫保護(hù)和內(nèi)核態(tài)內(nèi)存空間。
- D - Dirty,表示臟頁面,如果一個(gè)頁面被寫過,它的 D 位被置為 1;
- A - Access,如果該頁面被訪問過(讀或?qū)懀?,它?A 位被置為 1。
這些標(biāo)志位可以被 CPU 設(shè)置,但是只能由內(nèi)核去 clear 。
最后,PTE 存儲(chǔ)著該頁面對應(yīng)的 4K 對齊的起始物理地址。通常頁面的最大映射范圍是 4GB 的物理內(nèi)存,但是可以通過 PTE 的其他位進(jìn)行拓展。
一個(gè)虛擬頁面是內(nèi)存保護(hù)的基本單位,因?yàn)槿绻麖奈锢眄摰慕嵌瓤?,同一個(gè)物理頁可以對應(yīng)多個(gè)虛擬頁,從而有不同的保護(hù)標(biāo)志位。注意 PTE 沒有關(guān)于執(zhí)行權(quán)限的標(biāo)記,所以在經(jīng)典 x86 體系機(jī)中允許棧上的代碼被執(zhí)行,這容易引致緩沖區(qū)溢出攻擊。This lack of a PTE no-execute flag illustrates a broader fact: permission flags in a VMA may or may not translate cleanly into hardware protection. The kernel does what it can, but ultimately the architecture limits what is possible.
虛擬頁面不實(shí)際存儲(chǔ)數(shù)據(jù),它只是將進(jìn)程的地址空間映射到底層的物理內(nèi)存。
在總線上的內(nèi)存操作比較復(fù)雜, 我們可以假設(shè)物理地址區(qū)間是從0到可用內(nèi)存按照字節(jié)自增的。物理地址空間被內(nèi)核以頁大小為單位劃分頁幀(Frame)。CPU不知道頁幀的存在, 但是頁幀是對內(nèi)核卻很重要, 也是內(nèi)核內(nèi)存管理最基本的單元。 Linux和Windows都使用4KB大小的頁幀(32位模式);下圖是一個(gè)擁有2G內(nèi)存的例子:
在 Linux 中使用一個(gè)描述符和幾個(gè)標(biāo)記位來表示頁幀,這些描述符整體描述了整個(gè)的物理內(nèi)存,一個(gè)頁幀的狀態(tài)總是確定且已知的。物理內(nèi)存通過伙伴算法進(jìn)行管理和分配,如果一個(gè)頁楨當(dāng)前可通過伙伴算法被分配,則稱它是 free 的。
一個(gè)已分配的頁楨有兩種狀態(tài):一種是“匿名”的,存放著程序的數(shù)據(jù);另一種情況在 page cache 中,存放著 *文件 *或 *塊設(shè)備 *中的數(shù)據(jù)(暫不考慮其他奇怪的用法)。Windows 使用 PFN (Page Frame Number) 數(shù)據(jù)庫來管理物理內(nèi)存。
下面這個(gè)圖描述了 虛擬內(nèi)存空間(VMA),頁表項(xiàng)和頁楨的關(guān)系:

中間藍(lán)色的方塊表示 VMA 中的 page,它的箭頭表示通過頁表項(xiàng)指向了物理頁楨。其中一部分 page 是沒有箭頭的,這意味著其頁表項(xiàng)中的P(resent)標(biāo)記位被清零,這可能是由于該頁不再被使用或其內(nèi)容已經(jīng)被 swaped out,這兩種情況下訪問該頁都會(huì)導(dǎo)致缺頁中斷。
VMA 像是內(nèi)核和上層應(yīng)用的橋梁,上層應(yīng)用向內(nèi)核請求做一件事(內(nèi)存分配、文件映射等),內(nèi)核會(huì)直接同意,然后修改 VMA,但是內(nèi)核不會(huì)立即去執(zhí)行上層應(yīng)用請求的事,真正的執(zhí)行請求需要一個(gè)缺頁中斷來做。從這個(gè)角度看,內(nèi)核懶惰且不誠實(shí),但這也是 virtual memory 的基準(zhǔn)準(zhǔn)則。VMA 記錄著內(nèi)核已經(jīng)“同意”的事,而頁表項(xiàng)記錄著內(nèi)核已經(jīng)“做成”的事。這兩個(gè)結(jié)構(gòu)是內(nèi)存管理的主要成員。下面是一個(gè)內(nèi)存分配的流程示例:

應(yīng)用程序通過 brk() 系統(tǒng)調(diào)用來申請更多內(nèi)存時(shí)后,內(nèi)核擴(kuò)展 heap VMA,但新擴(kuò)展的部分還沒有對應(yīng)上物理內(nèi)存;當(dāng)應(yīng)用程序訪問新申請的內(nèi)存后,系統(tǒng)調(diào)用 do_page_fault() 會(huì)被調(diào)用,它會(huì)通過 find_vma() 在 VMA 中查找沒有對應(yīng)上物理內(nèi)存的 virtual address,如果找到的話,會(huì)檢查該 VMA 的相關(guān)權(quán)限并進(jìn)行下一步操作*,否則會(huì)造成 Segmentation Fault.
上面說的“下一步操作”即查找頁表項(xiàng),在上圖的情況下,其頁表項(xiàng)的 P 標(biāo)記位會(huì)顯示其 Not Pressent,(實(shí)際上整個(gè)頁表項(xiàng)是 blank 的,即全為0)。由于當(dāng)前 VMA 是匿名的,后面會(huì)通過 do_anonymous_page() 來進(jìn)行頁楨的分配和更新頁表項(xiàng)。
這一步操作也會(huì)有另外一種情況,即頁表項(xiàng)的 P 標(biāo)記位為0,但整個(gè)頁表項(xiàng)不是 blank 的,這意味著它之前被 swaped out,這種情況可以在頁表項(xiàng)中找到其 swap 地址,之后由 do_swap_pages() 來處理。