作者 | Gustavo Duarte
譯者 | qhwdw ? ? ? ? ? 共計翻譯:86 篇 貢獻時間:122 天
在學習了行程的 虛擬地址佈局[1] 之後,讓我們回到核心,來學習它管理使用者記憶體的機制。這裡再次使用 Gonzo:
Linux kernel mm_struct
Linux 行程在核心中是作為行程描述符 task_struct[2] (LCTT 譯註:它是在 Linux 中描述行程完整資訊的一種資料結構)的實體來實現的。在 task_struct 中的 mm[3] 域指向到記憶體描述符,mm_struct[4] 是一個程式在記憶體中的執行摘要。如上圖所示,它儲存了起始和結束記憶體段,行程使用的物理記憶體頁面的 數量[5](RSS 常駐記憶體大小 )、虛擬地址空間使用的 總數量[6]、以及其它片斷。 在記憶體描述符中,我們可以獲悉它有兩種管理記憶體的方式:虛擬記憶體區域集和頁面表。Gonzo 的記憶體區域如下所示:
Kernel memory descriptor and memory areas
每個虛擬記憶體區域(VMA)是一個連續的虛擬地址範圍;這些區域絕對不會重疊。一個 vm_area_struct[7] 的實體完整地描述了一個記憶體區域,包括它的起始和結束地址,flags[8] 決定了訪問許可權和行為,並且 vm_file[9] 域指定了對映到這個區域的檔案(如果有的話)。(除了記憶體對映段的例外情況之外,)一個 VMA 是不能匿名對映檔案的。上面的每個記憶體段(比如,堆、棧)都對應一個單個的 VMA。雖然它通常都使用在 x86 的機器上,但它並不是必需的。VMA 也不關心它們在哪個段中。
一個程式的 VMA 在記憶體描述符中是作為 mmap[10] 域的一個連結串列儲存的,以起始虛擬地址為序進行排列,並且在 mm_rb[11] 域中作為一個 紅黑樹[12] 的根。紅黑樹允許核心透過給定的虛擬地址去快速搜尋記憶體區域。在你讀取檔案 /proc/pid_of_process/maps
時,核心只是簡單地讀取每個行程的 VMA 的連結串列並顯示它們[13]。
在 Windows 中,EPROCESS[14] 塊大致類似於一個 task_struct 和 mm_struct 的結合。在 Windows 中模擬一個 VMA 的是虛擬地址描述符,或稱為 VAD[15];它儲存在一個 AVL 樹[16] 中。你知道關於 Windows 和 Linux 之間最有趣的事情是什麼嗎?其實它們只有一點小差別。
4GB 虛擬地址空間被分配到頁面中。在 32 位樣式中的 x86 處理器中支援 4KB、2MB、以及 4MB 大小的頁面。Linux 和 Windows 都使用大小為 4KB 的頁面去對映使用者的一部分虛擬地址空間。位元組 0-4095 在頁面 0 中,位元組 4096-8191 在頁面 1 中,依次類推。VMA 的大小 必須是頁面大小的倍數 。下圖是使用 4KB 大小頁面的總數量為 3GB 的使用者空間:
4KB Pages Virtual User Space
處理器透過檢視頁面表去轉換一個虛擬記憶體地址到一個真實的物理記憶體地址。每個行程都有它自己的一組頁面表;每當發生行程切換時,使用者空間的頁面表也同時切換。Linux 在記憶體描述符的 pgd[17] 域中儲存了一個指向行程的頁面表的指標。對於每個虛擬頁面,頁面表中都有一個相應的頁面表條目(PTE),在常規的 x86 頁面表中,它是一個簡單的如下所示的大小為 4 位元組的記錄:
x86 Page Table Entry (PTE) for 4KB page
Linux 透過函式去 讀取[18] 和 設定[19] PTE 條目中的每個標誌位。標誌位 P 告訴處理器這個虛擬頁面是否在物理記憶體中。如果該位被清除(設定為 0),訪問這個頁面將觸發一個頁面故障。請記住,當這個標誌位為 0 時,內核可以在剩餘的域上做任何想做的事。R/W 標誌位是讀/寫標誌;如果被清除,這個頁面將變成只讀的。U/S 標誌位表示使用者/超級使用者;如果被清除,這個頁面將僅被核心訪問。這些標誌都是用於實現我們在前面看到的只讀記憶體和核心空間保護。
標誌位 D 和 A 用於標識頁面是否是“髒的”或者是已被訪問過。一個臟頁面表示已經被寫入,而一個被訪問過的頁面則表示有一個寫入或者讀取發生過。這兩個標誌位都是粘滯位:處理器只能設定它們,而清除則是由核心來完成的。最終,PTE 儲存了這個頁面相應的起始物理地址,它們按 4KB 進行整齊排列。這個看起來不起眼的域是一些痛苦的根源,因為它限制了物理記憶體最大為 4 GB[20]。其它的 PTE 域留到下次再講,因為它是涉及了物理地址擴充套件的知識。
由於在一個虛擬頁面上的所有位元組都共享一個 U/S 和 R/W 標誌位,所以記憶體保護的最小單元是一個虛擬頁面。但是,同一個物理記憶體可能被對映到不同的虛擬頁面,這樣就有可能會出現相同的物理記憶體出現不同的保護標誌位的情況。請註意,在 PTE 中是看不到執行許可權的。這就是為什麼經典的 x86 頁面上允許程式碼在棧上被執行的原因,這樣會很容易導致挖掘出棧緩衝上限溢位漏洞(可能會透過使用 return-to-libc[21] 和其它技術來找出非可執行棧)。由於 PTE 缺少禁止執行標誌位說明瞭一個更廣泛的事實:在 VMA 中的許可權標誌位有可能或可能不完全轉換為硬體保護。核心只能做它能做到的,但是,最終的架構限制了它能做的事情。
虛擬記憶體不儲存任何東西,它只是簡單地 對映 一個程式的地址空間到底層的物理記憶體上。物理記憶體被當作一個稱之為物理地址空間的巨大塊而由處理器訪問。雖然記憶體的操作涉及到某些[22]匯流排,我們在這裡先忽略它,並假設物理地址範圍從 0 到可用的最大值按位元組遞增。物理地址空間被核心進一步分解為頁面幀。處理器並不會關心幀的具體情況,這一點對核心也是至關重要的,因為,頁面幀是物理記憶體管理的最小單元。Linux 和 Windows 在 32 位樣式下都使用 4KB 大小的頁面幀;下圖是一個有 2 GB 記憶體的機器的例子:
Physical Address Space
在 Linux 上每個頁面幀是被一個 描述符[23] 和 幾個標誌[24] 來跟蹤的。透過這些描述符和標誌,實現了對機器上整個物理記憶體的跟蹤;每個頁面幀的具體狀態是公開的。物理記憶體是透過使用 Buddy 記憶體分配[25] (LCTT 譯註:一種記憶體分配演演算法)技術來管理的,因此,如果一個頁面幀可以透過 Buddy 系統分配,那麼它是未分配的(free)。一個被分配的頁面幀可以是匿名的、持有程式資料的、或者它可能處於頁面快取中、持有資料儲存在一個檔案或者塊裝置中。還有其它的異形頁面幀,但是這些異形頁面幀現在已經不怎麼使用了。Windows 有一個類似的頁面幀號(Page Frame Number (PFN))資料庫去跟蹤物理記憶體。
我們把虛擬記憶體區域(VMA)、頁面表條目(PTE),以及頁面幀放在一起來理解它們是如何工作的。下麵是一個使用者堆的示例:
Physical Address Space
藍色的矩形框表示在 VMA 範圍內的頁面,而箭頭表示頁面表條目對映頁面到頁面幀。一些缺少箭頭的虛擬頁面,表示它們對應的 PTE 的當前標誌位被清除(置為 0)。這可能是因為這個頁面從來沒有被使用過,或者是它的內容已經被交換出去了。在這兩種情況下,即便這些頁面在 VMA 中,訪問它們也將導致產生一個頁面故障。對於這種 VMA 和頁面表的不一致的情況,看上去似乎很奇怪,但是這種情況卻經常發生。
一個 VMA 像一個在你的程式和核心之間的合約。你請求它做一些事情(分配記憶體、檔案對映、等等),核心會回應“收到”,然後去建立或者更新相應的 VMA。 但是,它 並不立刻 去“兌現”對你的承諾,而是它會等待到發生一個頁面故障時才去 真正 做這個工作。內核是個“懶惰的傢伙”、“不誠實的人渣”;這就是虛擬記憶體的基本原理。它適用於大多數的情況,有一些類似情況和有一些意外的情況,但是,它是規則是,VMA 記錄 約定的 內容,而 PTE 才反映這個“懶惰的核心” 真正做了什麼。透過這兩種資料結構共同來管理程式的記憶體;它們共同來完成解決頁面故障、釋放記憶體、從記憶體中交換出資料、等等。下圖是記憶體分配的一個簡單案例:
Example of demand paging and memory allocation
當程式透過 brk()[26] 系統呼叫來請求一些記憶體時,核心只是簡單地 更新[27] 堆的 VMA 並給程式回覆“已搞定”。而在這個時候並沒有真正地分配頁面幀,並且新的頁面也沒有對映到物理記憶體上。一旦程式嘗試去訪問這個頁面時,處理器將發生頁面故障,然後呼叫 do_page_fault()[28]。這個函式將使用 find_vma()[29] 去 搜尋[30] 發生頁面故障的 VMA。如果找到了,然後在 VMA 上進行許可權檢查以防範惡意訪問(讀取或者寫入)。如果沒有合適的 VMA,也沒有所嘗試訪問的記憶體的“合約”,將會給行程傳回段故障。
當找到[31]了一個合適的 VMA,核心必須透過查詢 PTE 的內容和 VMA 的型別去處理[32]故障。在我們的案例中,PTE 顯示這個頁面是 不存在的[33]。事實上,我們的 PTE 是全部空白的(全部都是 0),在 Linux 中這表示虛擬記憶體還沒有被對映。由於這是匿名 VMA,我們有一個完全的 RAM 事務,它必須被 do_anonymous_page()[34] 來處理,它分配頁面幀,並且用一個 PTE 去對映故障虛擬頁面到一個新分配的幀。
有時候,事情可能會有所不同。例如,對於被交換出記憶體的頁面的 PTE,在當前(Present)標誌位上是 0,但它並不是空白的。而是在交換位置仍有頁面內容,它必須從磁碟上讀取並且透過 do_swap_page()[35] 來載入到一個被稱為 major fault[36] 的頁面幀上。
這是我們透過探查內核的使用者記憶體管理得出的前半部分的結論。在下一篇文章中,我們透過將檔案載入到記憶體中,來構建一個完整的記憶體框架圖,以及對效能的影響。
via: http://duartes.org/gustavo/blog/post/how-the-kernel-manages-your-memory/
作者:Gustavo Duarte[38] 譯者:qhwdw 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出