How the Kernel Manages Your Memoryhtml
在研究完進程的虛擬地址佈局後,咱們來看看內核是怎麼管理用戶內存的。仍是以gonzo爲例:linux
Linux進程在內核中被實現爲task_struct(即進程描述符)的實例。task_struct的mm域指向mm_struct,即內存描述符,負責管理整個程序的內存。它保存着上圖中各內存段的開始和結束地址,進程使用的物理內存頁數(rss意爲Resident Set Size,即「駐留集大小」),使用的虛擬地址空間總數,等等。在mm_struct中咱們還能找到兩個內存管理結構:虛擬內存區集合,以及頁表。gonzo的內存區見下圖:數據庫
每一個虛擬內存區(VMA)是一個連續虛擬地址空間,這些區域之間沒有重疊。每一個內存區可由vm_area_struct表示,包括它的開始和結束地址、訪問標誌、可能還有表明映射自哪一個文件的vm_file域。沒有映射自文件的VMA是匿名的。上面說的每一個內存段(如堆、棧)都對應一個單獨的VMA,但mmap段例外。雖然標準沒有這樣規定(每一個內存段單獨一個VMA),但x86機器上一般都是這樣的。VMA並不關心它處於哪一個內存段。c#
在進程的mm_struct中,VMA同時保存在mmap域的鏈表(按起始虛擬地址排序)和mm_rb域的紅黑樹中。紅黑樹容許內核快速查找到覆蓋了指定虛擬地址的內存區。當你讀/proc/pid/maps
時,內核就是簡單的沿鏈表將每一個VMA打印出來。緩存
Windows中的EPROCESS塊差很少是task_struct加mm_struct的。Windows中與VMA相對的是VAD(虛擬地址描述符),VAD保存在一個AVL樹中。你知道與Windows和Linux有關的最有趣的事是什麼嗎?就是它們之間的差異竟然這麼小。架構
4GB的虛擬地址空間被分紅了若干個頁(page)。32位的X86處理器支持4KB、2MB和4MB的頁大小。Linux和Windows在用戶空間都使用4KB的頁。0-4095B處於第0頁,4096-8191是第1頁,依次類推。VMA大小必須是頁大小的整數倍。3GB的用戶空間分紅頁就是這樣:ide
處理器用頁表(page table)將虛擬地址轉換爲物理地址。每一個處理器都有本身的頁表集合。每當發生進程切換時,對應的用戶空間的頁表也會切換。Linux將進程頁表的指針保存在mm_struct的pgd成員裏。對應每一個虛擬頁,頁表中都有一個頁表項(PTE),在常見的X86機器上每一個PTE通常是4Byte:函數
Linux有函數能夠讀寫PTE中每一個標誌位。P位表明虛頁是否已處在物理內存中。若是P位爲0,對它的訪問會致使一次缺頁中斷。若是P位爲0,內核能夠任意處置其它位。R/W位表示read/write,爲0表示只讀頁。U/S位表明用戶/內核,爲0表示只能被內核訪問。這些標誌位是用來實現只讀內存,以及保護咱們上面看到的內核空間。佈局
D和A位表明髒位和訪問位。被寫過的頁就是髒頁,而被讀寫過的頁則是訪問過的頁。這兩個位都很詭異:處理器只負責置其爲1,只有內核才能置0。最後,PTE保存了指向這個頁的起始物理地址,按4KB對齊。這個字段的設計很輕率,是某些問題的根源:它將可訪問的物理內存限制爲4GB。其它PTE
字段都是爲將來的擴展準備的,又稱爲PAE(物理地址擴展)。post
虛擬頁是內存保護的單位,由於每一個頁的全部字節共享一個U/S和R/W標誌。但相同的物理內存能夠映射爲不一樣的頁,且可能有着不一樣的保護標誌。注意PTE中沒有關於執行權限的字段。這就是爲何經典的X86分頁機制容許執行棧上的代碼,從而更容易發現棧溢出的漏洞(在不可執行的棧上,經過return-to-libc等手段仍然可能發起攻擊)。PTE缺少可執行標誌這件事代表了一個更普遍意義的真相:VMA裏的權限標誌可能會,也可能不會徹底轉換成硬件的保護。內核作了它能作的,但最終硬件架構限制了它能作到什麼程度。
虛擬內存不保存什麼,它只是把進程的地址空間簡單的映射爲底層的物理內存,後者是由處理器訪問的一大塊空間,稱爲物理地址空間。內存操做總會涉及總線,在這裏咱們能夠忽略總線,假設物理地址的範圍是從0開始直到最大可能的地址,單位是1Byte。物理地址被內核分紅若干個頁幀(page frame)。處理器不用關心頁幀是什麼,但內核很須要,由於頁幀是物理內存管理的基本單元。Linux和Windows在32位下的頁幀大小都爲4KB,下圖是一個2GB內存的機器:
Linux中每一個頁幀都由一個描述符和多個標誌追蹤。這些描述符加起來就能夠追蹤整個機器的物理內存。咱們總能知道每一個頁幀的準確狀態。物理內存使用了夥伴內存分配技術進行管理。若是一個頁幀在夥伴系統中可分配,它就是空閒的。已分配的頁幀多是匿名的,保存程序的數據,或是頁緩存,保存文件或塊設備中的數據。頁幀還有一些其它用途,這裏先不提。Windows有一個差很少的數據庫PFN(頁幀數)來追蹤物理內存。
我們把虛擬內存區、頁表項和頁幀放到一塊兒,看一下它們是怎麼工做的,見下圖:
藍色矩形表明VMA中的頁,箭頭則表明負責將頁映射爲頁幀的頁表項。有些虛擬頁沒有箭頭,這代表對應的PTE的P位是0。多是由於這些頁從沒有被接觸過,或它們的內容已經被換出了。兩種狀況下訪問這些頁都會產生頁錯誤,即便這些頁在VMA中也同樣。VMA和頁表不一致這件事看起來可能很奇怪,但確實常常發生。
VMA像是程序與內核間的一個合同。你要求完成某事(例如分配內存、映射文件等),內核說「好」,以後它建立或更新了相應的VMA。但內核沒有馬上處理請求,它會等到頁錯誤再真正處理請求。內核是一個懶惰的,有欺騙性的容器,這是虛擬內存的基本原則。這項原則在大多數狀況下都適用,有些你們很熟悉,有些則會讓人驚訝。一個規則就是VMA記錄什麼已經被批准了,而PTE則反映了懶惰的內核實際上作了什麼。二者共同管理程序的內存,共同負責解決頁錯誤、釋放內存、換出內存等。下面咱們看一個簡單的內存分配的例子:
當程序經過brk()系統調用申請內存時,內核只是更新了堆的VMA後就返回OK。此時實際上沒有分配任何頁幀,新申請的頁也沒有與物理內存有聯繫。當程序訪問這些頁時,處理器產生頁錯誤,並調用do_page_fault()。do_page_fault會調用find_vma()查找覆蓋了目標地址的VMA。若是找到了,就會進一步檢查VMA的讀寫權限。若是沒找到,則進程會收到段錯誤。
找到VMA時,內核須要經過查看PTE的內容和VMA的類型來處理頁錯誤。在咱們的例子中,PTE顯示該頁未表達。事實上,咱們的PTE是全空的(全是0),在Linux中意思是這個頁尚未被映射過。既然這是一個匿名VMA(不是映射自文件或設備),咱們就有了一個純內存事件,須要調用do_anonymous_page()處理。它會分配頁幀,並讓PTE將產生頁錯誤的虛擬頁與剛分出來的物理頁映射在一塊兒。
有時會有其它狀況發生。例如,若是PTE對應的頁被換出了,它的P位爲0,但並非空的。相反,它會將保存了頁內容的換出位置記下來,以後經過do_swap_page()將其從磁盤中讀出來再載入到某個頁幀中,這稱爲一次major fault。