帶你了解內(nèi)核如何管理內(nèi)存
在學(xué)習(xí)了進(jìn)程的 虛擬地址布局 之后,讓我們回到內(nèi)核,來學(xué)習(xí)它管理用戶內(nèi)存的機(jī)制。這里再次使用 Gonzo:
Linux kernel mm_struct
Linux 進(jìn)程在內(nèi)核中是作為進(jìn)程描述符 task_struct (LCTT 譯注:它是在 Linux 中描述進(jìn)程完整信息的一種數(shù)據(jù)結(jié)構(gòu))的實(shí)例來實(shí)現(xiàn)的。在 task_struct 中的 mm 域指向到內(nèi)存描述符,mm_struct 是一個程序在內(nèi)存中的執(zhí)行摘要。如上圖所示,它保存了起始和結(jié)束內(nèi)存段,進(jìn)程使用的物理內(nèi)存頁面的 數(shù)量(RSS 常駐內(nèi)存大小 )、虛擬地址空間使用的 總數(shù)量、以及其它片斷。 在內(nèi)存描述符中,我們可以獲悉它有兩種管理內(nèi)存的方式:虛擬內(nèi)存區(qū)域集和頁面表。Gonzo 的內(nèi)存區(qū)域如下所示:
Kernel memory descriptor and memory areas
每個虛擬內(nèi)存區(qū)域(VMA)是一個連續(xù)的虛擬地址范圍;這些區(qū)域絕對不會重疊。一個 vmareastruct 的實(shí)例完整地描述了一個內(nèi)存區(qū)域,包括它的起始和結(jié)束地址,flags 決定了訪問權(quán)限和行為,并且 vm_file 域指定了映射到這個區(qū)域的文件(如果有的話)。(除了內(nèi)存映射段的例外情況之外,)一個 VMA 是不能匿名映射文件的。上面的每個內(nèi)存段(比如,堆、棧)都對應(yīng)一個單個的 VMA。雖然它通常都使用在 x86 的機(jī)器上,但它并不是必需的。VMA 也不關(guān)心它們在哪個段中。
一個程序的 VMA 在內(nèi)存描述符中是作為 mmap 域的一個鏈接列表保存的,以起始虛擬地址為序進(jìn)行排列,并且在 mm_rb 域中作為一個 紅黑樹 的根。紅黑樹允許內(nèi)核通過給定的虛擬地址去快速搜索內(nèi)存區(qū)域。在你讀取文件 /proc/pid_of_process/maps
時,內(nèi)核只是簡單地讀取每個進(jìn)程的 VMA 的鏈接列表并顯示它們。
在 Windows 中,EPROCESS 塊大致類似于一個 taskstruct 和 mmstruct 的結(jié)合。在 Windows 中模擬一個 VMA 的是虛擬地址描述符,或稱為 VAD;它保存在一個 AVL 樹 中。你知道關(guān)于 Windows 和 Linux 之間最有趣的事情是什么嗎?其實(shí)它們只有一點(diǎn)小差別。
4GB 虛擬地址空間被分配到頁面中。在 32 位模式中的 x86 處理器中支持 4KB、2MB、以及 4MB 大小的頁面。Linux 和 Windows 都使用大小為 4KB 的頁面去映射用戶的一部分虛擬地址空間。字節(jié) 0-4095 在頁面 0 中,字節(jié) 4096-8191 在頁面 1 中,依次類推。VMA 的大小 必須是頁面大小的倍數(shù) 。下圖是使用 4KB 大小頁面的總數(shù)量為 3GB 的用戶空間:
4KB Pages Virtual User Space
處理器通過查看頁面表去轉(zhuǎn)換一個虛擬內(nèi)存地址到一個真實(shí)的物理內(nèi)存地址。每個進(jìn)程都有它自己的一組頁面表;每當(dāng)發(fā)生進(jìn)程切換時,用戶空間的頁面表也同時切換。Linux 在內(nèi)存描述符的 pgd 域中保存了一個指向進(jìn)程的頁面表的指針。對于每個虛擬頁面,頁面表中都有一個相應(yīng)的頁面表?xiàng)l目(PTE),在常規(guī)的 x86 頁面表中,它是一個簡單的如下所示的大小為 4 字節(jié)的記錄:
x86 Page Table Entry (PTE) for 4KB page
Linux 通過函數(shù)去 讀取 和 設(shè)置 PTE 條目中的每個標(biāo)志位。標(biāo)志位 P 告訴處理器這個虛擬頁面是否在物理內(nèi)存中。如果該位被清除(設(shè)置為 0),訪問這個頁面將觸發(fā)一個頁面故障。請記住,當(dāng)這個標(biāo)志位為 0 時,內(nèi)核可以在剩余的域上做任何想做的事。R/W 標(biāo)志位是讀/寫標(biāo)志;如果被清除,這個頁面將變成只讀的。U/S 標(biāo)志位表示用戶/超級用戶;如果被清除,這個頁面將僅被內(nèi)核訪問。這些標(biāo)志都是用于實(shí)現(xiàn)我們在前面看到的只讀內(nèi)存和內(nèi)核空間保護(hù)。
標(biāo)志位 D 和 A 用于標(biāo)識頁面是否是“臟的”或者是已被訪問過。一個臟頁面表示已經(jīng)被寫入,而一個被訪問過的頁面則表示有一個寫入或者讀取發(fā)生過。這兩個標(biāo)志位都是粘滯位:處理器只能設(shè)置它們,而清除則是由內(nèi)核來完成的。最終,PTE 保存了這個頁面相應(yīng)的起始物理地址,它們按 4KB 進(jìn)行整齊排列。這個看起來不起眼的域是一些痛苦的根源,因?yàn)樗拗屏宋锢韮?nèi)存最大為 4 GB。其它的 PTE 域留到下次再講,因?yàn)樗巧婕傲宋锢淼刂窋U(kuò)展的知識。
由于在一個虛擬頁面上的所有字節(jié)都共享一個 U/S 和 R/W 標(biāo)志位,所以內(nèi)存保護(hù)的最小單元是一個虛擬頁面。但是,同一個物理內(nèi)存可能被映射到不同的虛擬頁面,這樣就有可能會出現(xiàn)相同的物理內(nèi)存出現(xiàn)不同的保護(hù)標(biāo)志位的情況。請注意,在 PTE 中是看不到運(yùn)行權(quán)限的。這就是為什么經(jīng)典的 x86 頁面上允許代碼在棧上被執(zhí)行的原因,這樣會很容易導(dǎo)致挖掘出棧緩沖溢出漏洞(可能會通過使用 return-to-libc 和其它技術(shù)來找出非可執(zhí)行棧)。由于 PTE 缺少禁止運(yùn)行標(biāo)志位說明了一個更廣泛的事實(shí):在 VMA 中的權(quán)限標(biāo)志位有可能或可能不完全轉(zhuǎn)換為硬件保護(hù)。內(nèi)核只能做它能做到的,但是,最終的架構(gòu)限制了它能做的事情。
虛擬內(nèi)存不保存任何東西,它只是簡單地 映射 一個程序的地址空間到底層的物理內(nèi)存上。物理內(nèi)存被當(dāng)作一個稱之為物理地址空間的巨大塊而由處理器訪問。雖然內(nèi)存的操作涉及到某些總線,我們在這里先忽略它,并假設(shè)物理地址范圍從 0 到可用的最大值按字節(jié)遞增。物理地址空間被內(nèi)核進(jìn)一步分解為頁面幀。處理器并不會關(guān)心幀的具體情況,這一點(diǎn)對內(nèi)核也是至關(guān)重要的,因?yàn)椋?strong>頁面幀是物理內(nèi)存管理的最小單元。Linux 和 Windows 在 32 位模式下都使用 4KB 大小的頁面幀;下圖是一個有 2 GB 內(nèi)存的機(jī)器的例子:
Physical Address Space
在 Linux 上每個頁面幀是被一個 描述符 和 幾個標(biāo)志 來跟蹤的。通過這些描述符和標(biāo)志,實(shí)現(xiàn)了對機(jī)器上整個物理內(nèi)存的跟蹤;每個頁面幀的具體狀態(tài)是公開的。物理內(nèi)存是通過使用 Buddy 內(nèi)存分配 (LCTT 譯注:一種內(nèi)存分配算法)技術(shù)來管理的,因此,如果一個頁面幀可以通過 Buddy 系統(tǒng)分配,那么它是未分配的(free)。一個被分配的頁面幀可以是匿名的、持有程序數(shù)據(jù)的、或者它可能處于頁面緩存中、持有數(shù)據(jù)保存在一個文件或者塊設(shè)備中。還有其它的異形頁面幀,但是這些異形頁面幀現(xiàn)在已經(jīng)不怎么使用了。Windows 有一個類似的頁面幀號(Page Frame Number (PFN))數(shù)據(jù)庫去跟蹤物理內(nèi)存。
我們把虛擬內(nèi)存區(qū)域(VMA)、頁面表?xiàng)l目(PTE),以及頁面幀放在一起來理解它們是如何工作的。下面是一個用戶堆的示例:
Physical Address Space
藍(lán)色的矩形框表示在 VMA 范圍內(nèi)的頁面,而箭頭表示頁面表?xiàng)l目映射頁面到頁面幀。一些缺少箭頭的虛擬頁面,表示它們對應(yīng)的 PTE 的當(dāng)前標(biāo)志位被清除(置為 0)。這可能是因?yàn)檫@個頁面從來沒有被使用過,或者是它的內(nèi)容已經(jīng)被交換出去了。在這兩種情況下,即便這些頁面在 VMA 中,訪問它們也將導(dǎo)致產(chǎn)生一個頁面故障。對于這種 VMA 和頁面表的不一致的情況,看上去似乎很奇怪,但是這種情況卻經(jīng)常發(fā)生。
一個 VMA 像一個在你的程序和內(nèi)核之間的合約。你請求它做一些事情(分配內(nèi)存、文件映射、等等),內(nèi)核會回應(yīng)“收到”,然后去創(chuàng)建或者更新相應(yīng)的 VMA。 但是,它 并不立刻 去“兌現(xiàn)”對你的承諾,而是它會等待到發(fā)生一個頁面故障時才去 真正 做這個工作。內(nèi)核是個“懶惰的家伙”、“不誠實(shí)的人渣”;這就是虛擬內(nèi)存的基本原理。它適用于大多數(shù)的情況,有一些類似情況和有一些意外的情況,但是,它是規(guī)則是,VMA 記錄 約定的 內(nèi)容,而 PTE 才反映這個“懶惰的內(nèi)核” 真正做了什么。通過這兩種數(shù)據(jù)結(jié)構(gòu)共同來管理程序的內(nèi)存;它們共同來完成解決頁面故障、釋放內(nèi)存、從內(nèi)存中交換出數(shù)據(jù)、等等。下圖是內(nèi)存分配的一個簡單案例:
Example of demand paging and memory allocation
當(dāng)程序通過 brk() 系統(tǒng)調(diào)用來請求一些內(nèi)存時,內(nèi)核只是簡單地 更新 堆的 VMA 并給程序回復(fù)“已搞定”。而在這個時候并沒有真正地分配頁面幀,并且新的頁面也沒有映射到物理內(nèi)存上。一旦程序嘗試去訪問這個頁面時,處理器將發(fā)生頁面故障,然后調(diào)用 dopagefault()。這個函數(shù)將使用 find_vma() 去 搜索 發(fā)生頁面故障的 VMA。如果找到了,然后在 VMA 上進(jìn)行權(quán)限檢查以防范惡意訪問(讀取或者寫入)。如果沒有合適的 VMA,也沒有所嘗試訪問的內(nèi)存的“合約”,將會給進(jìn)程返回段故障。
當(dāng)找到了一個合適的 VMA,內(nèi)核必須通過查找 PTE 的內(nèi)容和 VMA 的類型去處理故障。在我們的案例中,PTE 顯示這個頁面是 不存在的。事實(shí)上,我們的 PTE 是全部空白的(全部都是 0),在 Linux 中這表示虛擬內(nèi)存還沒有被映射。由于這是匿名 VMA,我們有一個完全的 RAM 事務(wù),它必須被 doanonymouspage() 來處理,它分配頁面幀,并且用一個 PTE 去映射故障虛擬頁面到一個新分配的幀。
有時候,事情可能會有所不同。例如,對于被交換出內(nèi)存的頁面的 PTE,在當(dāng)前(Present)標(biāo)志位上是 0,但它并不是空白的。而是在交換位置仍有頁面內(nèi)容,它必須從磁盤上讀取并且通過 doswappage() 來加載到一個被稱為 major fault 的頁面幀上。
這是我們通過探查內(nèi)核的用戶內(nèi)存管理得出的前半部分的結(jié)論。在下一篇文章中,我們通過將文件加載到內(nèi)存中,來構(gòu)建一個完整的內(nèi)存框架圖,以及對性能的影響。