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