聊一聊Linux虛擬內存技術
以存儲單元為單位來管理顯然不現實,因此Linux把虛存空間分成若干個大小相等的存儲分區,Linux把這樣的分區叫做 頁。為了換入、換出的方便,物理內存也就按頁的大小分成若干個塊。由于物理內存中的塊空間是用來容納虛存頁的容器,所以物理內存中的塊叫做 頁框。頁與頁框是Linux實現虛擬內存技術的基礎。
虛擬內存的頁、物理內存的頁框及頁表
在Linux中,頁與頁框的大小一般為4KB。當然,根據系統和應用的不同,頁與頁框的大小也可有所變化。
物理內存和虛擬內存被分成了頁框與頁之后,其存儲單元原來的地址都被自然地分成了兩段,并且這兩段各自代表著不同的意義:高位段分別叫做頁框碼和頁碼,它們是識別頁框和頁的編碼;低位段分別叫做頁框偏移量和頁內偏移量,它們是存儲單元在頁框和頁內的地址編碼。下圖就是兩段虛擬內存和物理內存分頁之后的情況:
為了使系統可以正確的訪問虛存頁在對應頁框中的映像,在把一個頁映射到某個頁框上的同時,就必須把頁碼和存放該頁映像的頁框碼填入一個叫做 頁表的表項中。這個頁表就是之前提到的映射記錄表。一個頁表的示意圖如下所示:
頁模式下,虛擬地址、物理地址轉換關系的示意圖如下所示:
也就是說:處理器遇到的地址都是虛擬地址。虛擬地址和物理地址都分成頁碼(頁框碼)和偏移值兩部分。在由虛擬地址轉化成物理地址的過程中,偏移值不變。而頁碼和頁框碼之間的映射就在一個映射記錄表——頁表中。
請頁與交換
虛存頁面到物理頁框的映射叫做頁面的 加載。
當處理器試圖訪問一個虛存頁面時,首先到頁表中去查詢該頁是否已映射到物理頁框中,并記錄在頁表中。如果在,則MMU會把頁碼轉換成頁框碼,并加上虛擬地址提供的頁內偏移量形成物理地址后去訪問物理內存;如果不在,則意味著該虛存頁面還沒有被載入內存,這時MMU就會通知操作系統:發生了一個頁面訪問錯誤(頁面錯誤),接下來系統會啟動所謂的“請頁”機制,即調用相應的系統操作函數,判斷該虛擬地址是否為有效地址。
如果是 有效的地址,就從虛擬內存中將該地址指向的頁面讀入到內存中的一個空閑頁框中,并在頁表中添加上相對應的表項,***處理器將從發生頁面錯誤的地方重新開始運行;如果是 無效的地址,則表明進程在試圖訪問一個不存在的虛擬地址,此時操作系統將終止此次訪問。
當然,也存在這樣的情況:在請頁成功之后,內存中已沒有空閑物理頁框了。這是,系統必須啟動所謂地 交換機制,即調用相應的內核操作函數,在物理頁框中尋找一個當前不再使用或者近期可能不會用到的頁面所占據的頁框。找到后,就把其中的頁移出,以裝載新的頁面。對移出頁面根據兩種情況來處理:如果該頁未被修改過,則刪除它;如果該頁曾經被修改過,則系統必須將該頁寫回輔存。
系統請頁的處理過程如下所示:
為了公平地選擇將要從系統中拋棄的頁面,Linux系統使用 最近最少使用(LRU)頁面的衰老算法。這種策略根據系統中每個頁面被訪問的頻率,為物理頁框中的頁面設置了一個叫做 年齡的屬性。頁面被訪問的次數越多,則頁面的年齡最小;相反,則越大。而年齡較大的頁面就是待換出頁面的***候選者。
快表
在系統每次訪問虛存頁時,都要在內存的所有頁表中尋找該頁的頁框,這是一個很費時間的工作。但是,人們發現,系統一旦訪問了某一個頁,那么系統就會在一段時間內穩定地工作在這個頁上。所以,為了提高訪問頁表的速度,系統還配備了一組正好能容納一個頁表的 硬件寄存器,這樣當系統再訪問虛存時,就首先到這組硬件寄存器中去訪問,系統速度就快多了。這組存放當前頁表的寄存器叫做 快表。
總之,使用虛擬存儲技術時,處理器必須配備一些硬件來承擔內存管理的一部分任務。承擔內存管理任務的硬件部分叫做存儲管理單元MMU。存儲管理單元MMU的工作過程如下圖所示:
頁的共享
在多程序系統中,常常有多個程序需要共享同一段代碼或數據的情況。在分頁管理的存儲器中,這個事情很好辦:讓多個程序共享同一個頁面即可。
具體的方法是:使這些相關程序的虛擬空間的頁面在頁表中指向內存中的同一個頁框。這樣,當程序運行并訪問這些相關頁面時,就都是對同一個頁框中的頁面進行訪問,而該頁框中的頁就被這些程序所共享。下圖是3個程序共享一個頁面的例子:
頁的保護
由上可知,頁表實際上是由虛擬空間轉到物理空間的入口。因此,為了保護頁面內容不被沒有該頁面訪問權限的程序所破壞,就應在頁表的表項中設置一些訪問控制字段,用于指明對應頁面中的內容允許何種操作,從而禁止非法訪問。
下圖是頁表項中存放控制信息的一種可能的形式:
注意:其中的PCD位表示著是否允許高速緩存(cache)。
如果程序對一個頁試圖進行一個該頁控制字段所不允許的操作,則會引起操作系統的一次中斷——非法訪問中斷,并拒絕這種操作,從而保護該頁的內容不被破壞。
多級頁表
需要注意的是,頁表是操作系統創建的用于內存管理的表格。因此,一個程序在運行時,其頁表也要存放到內存空間。如果一個程序只需要一個頁表,則不會有什么問題。但如果,程序的虛擬空間很大的話,就會出現一個比較大的問題。
比如:一個程序的虛擬空間為4GB,頁表以4KB為一頁,那么這個程序空間就是1M頁。為了存儲這1M頁的頁指針,那么這個頁表的長度就相當大了,對內存的負擔也很大了。所以,***對頁表也進行分頁存儲,在程序運行時只把需要的頁復制到內存,而暫時不需要的頁就讓它留在輔存中。為了管理這些頁表頁,還要建立一個 記錄頁表頁首地址的頁目錄表,于是單級頁表就變成了二級頁表。二級頁表的地址轉換如下圖所示:
當然,如果程序的虛擬空間更大,那么也可以用三級頁表來管理。為了具有通用性,Linux系統使用了三級頁表結構:頁目錄(Page Directory,PGD)、中間頁目錄(Page Middle Directory,PMD)、頁表(Page Table,PTE)。
Linux的頁表結構
為了通用,Linux系統使用了三級頁表結構:頁目錄、中間頁目錄和頁表。PGD為 ***頁表,是一個pgdt數據類型的數組,每個數組元素指向一個中間頁目錄;PMD為 二級頁表,是一個pmdt數據結構的數組,每個數組元素指向一個頁表;PTE則是 頁表,是一個pte_t數據類型的數組,每個元素中含有物理地址。
為了應用上的靈活,Linux使用一系列的宏來掩蓋各種平臺的細節。用戶可以在配置文件 config中根據自己的需要對頁表進行配置,以決定是使用三級頁表還是使用二級頁表。
在系統編譯時,會根據配置文件 config中的配置,把目錄 include/asm符號連接到具體CPU專用的文件目錄中。例如,對于i386CPU,該目錄符號會連接到include/asm-i386,并在文件pgable-2level-defs.h中定義了二級頁表的基本結構,如下圖:
其中還定義了:
- #define PGDIR_SHIFT 22 //PGD在線性地址中的起始地址為bit22
- #define PTRS_PER_PGD 1024 //PGD共有1024個表項
- #define PTRS_PER_PTE 1024 //PTE共有1024個表項
- #endif
在文件include/asm-i386/pgtable.h中定義了頁目錄和頁表項的數據結構,如下:
- typedof struct { unsigned long pte_low; } pte_t ; //頁表中的物理地址,頁框碼
- typedof struct { unsigned long pgd; } pgd_t ; //指向一個頁表
- typedof struct { unsigned long pgprot; } pgprot_t ; //頁表中的各個狀態信息和訪問權限
從定義可知,它們都是只有一個 長整型類型(32位)的結構體。
注意:如上文的“頁的保護”部分,頁框碼代表物理地址,只需要高20位就夠了(因為頁框的長度為4KB,因此頁內偏移12位)。而后12位可以存放各個狀態信息和訪問權限。但是Linux并沒有這樣做,反而重新定義了一個結構體來存放,通過“或”運算來將兩者結合。