1 分頁機制 在虛擬記憶體中,頁表是個映射表的概念, 即從進程能理解的線性地址(linear address)映射到存儲器上的物理地址(phisical address). 很顯然,這個頁表是需要常駐記憶體的東西, 以應對頻繁的查詢映射需要(實際上,現代支持VM的處理器都有一個叫TLB的硬體級頁表緩存部 ...
1 分頁機制
在虛擬記憶體中,頁表是個映射表的概念, 即從進程能理解的線性地址(linear address)映射到存儲器上的物理地址(phisical address).
很顯然,這個頁表是需要常駐記憶體的東西, 以應對頻繁的查詢映射需要(實際上,現代支持VM的處理器都有一個叫TLB的硬體級頁表緩存部件,本文不討論)。
1.1 為什麼使用多級頁表來完成映射
但是為什麼要使用多級頁表來完成映射呢?
用來將虛擬地址映射到物理地址的數據結構稱為頁表, 實現兩個地址空間的關聯最容易的方式是使用數組, 對虛擬地址空間中的每一頁, 都分配一個數組項. 該數組指向與之關聯的頁幀,但這會引發一個問題, 例如, IA-32體繫結構使用4KB大小的頁, 在虛擬地址空間為4GB的前提下, 則需要包含100萬項的頁表. 這個問題在64位體繫結構下, 情況會更加糟糕. 而每個進程都需要自身的頁表, 這回導致系統中大量的所有記憶體都用來保存頁表.
設想一個典型的32位的X86系統,它的虛擬記憶體用戶空間(user space)大小為3G,並且典型的一個頁表項(page table entry, pte)大小為4 bytes,每一個頁(page)大小為4k bytes。那麼這3G空間一共有(3G/4k=)786432個頁面,每個頁面需要一個pte來保存映射信息,這樣一共需要786432個pte!
如何存儲這些信息呢?一個直觀的做法是用數組來存儲,這樣每個頁能存儲(4k/4=)1K個,這樣一共需要(786432/1k=)768個連續的物理頁面(phsical page)。而且,這隻是一個進程,如果要存放所有N個進程,這個數目還要乘上N! 這是個巨大的數目,哪怕記憶體能提供這樣數量的空間,要找到連續768個連續的物理頁面在系統運行一段時間後碎片化的情況下,也是不現實的。
為減少頁表的大小並容許忽略不需要的區域, 電腦體繫結構的涉及會將虛擬地址分成多個部分. 同時虛擬地址空間的大部分們區域都沒有使用, 因而頁沒有關聯到頁幀, 那麼就可以使用功能相同但記憶體用量少的多的模型: 多級頁表
但是新的問題來了, 到底採用幾級頁表合適呢?
1.2 32位系統中2級頁表
從80386開始, intel處理器的分頁單元是4KB的頁, 32位的地址空間被分為3部分
單元 | 描述 |
---|---|
頁目錄表Directory | 最高10位 |
頁中間表Table | 中間10位 |
頁內偏移 | 最低12位 |
即頁表被劃分為頁目錄表Directory和頁中間表Tabl兩個部分
此種情況下, 線性地址的轉換分為兩步完成.
第一步, 基於兩級轉換表(頁目錄表和頁中間表), 最終查找到地址所在的頁幀
第二步, 基於偏移, 在所在的頁幀中查找到對應偏移的物理地址
使用這種二級頁表可以有效的減少每個進程頁表所需的RAM的數量. 如果使用簡單的一級頁表, 那將需要高達2^20
個頁表, 假設每項4B, 則共需要占用2^20 * 4B = 4MB
的RAM來表示每個進程的頁表. 當然我們並不需要映射所有的線性地址空間(32位機器上線性地址空間為4GB), 內核通常只為進程實際使用的那些虛擬記憶體區請求頁表來減少記憶體使用量.
1.3 64位系統中的分頁
正常來說, 對於32位的系統兩級頁表已經足夠了, 但是對於64位系統的電腦, 這遠遠不夠.
首先假設一個大小為4KB的標準頁. 因為1KB覆蓋2^10
個地址的範圍, 4KB覆蓋2^12
個地址, 所以offset欄位需要12位.
這樣線性地址空間就剩下64-12=52位分配給頁中間表Table和頁目錄表Directory. 如果我們現在決定僅僅使用64位中的48位來定址(這個限制其實已經足夠了, 2^48=256TB, 即可達到256TB的定址空間). 剩下的48-12=36位被分配給Table和Directory欄位. 即使我們現在決定位兩個欄位各預留18位, 那麼每個進程的頁目錄和頁表都包含2^18
個項, 即超過256000個項.
基於這個原因, 所有64位處理器的硬體分頁系統都使用了額外的分頁級別. 使用的級別取決於處理器的類型
平臺名稱 | 頁大小 | 定址所使用的位數 | 分頁級別數 | 線性地址分級 |
---|---|---|---|---|
alpha | 8KB | 43 | 3 | 10 + 10 + 10 + 13 |
ia64 | 4KB | 39 | 3 | 9 + 9 + 9 + 12 |
ppc64 | 4KB | 41 | 3 | 10 + 10 + 9 + 12 |
sh64 | 4KB | 41 | 3 | 10 + 10 + 9 + 12 |
x86_64 | 4KB | 48 | 4 | 9 + 9 + 9 + 9 + 12 |
1.4 Linux中的分頁
層次話的頁表用於支持對大地址空間快速, 高效的管理. 因此linux內核堆頁表進行了分級.
前面我們提到過, 對於32位系統中, 兩級頁表已經足夠了. 但是64位需要更多數量的分頁級別.
為了同時支持適用於32位和64位的系統, Linux採用了通用的分頁模型. 在Linux-2.6.10版本中, Linux採用了三級分頁模型. 而從2.6.11開始普遍採用了四級分頁模型.
目前的內核的記憶體管理總是假定使用四級頁表, 而不管底層處理器是否如此.
單元 | 描述 |
---|---|
頁全局目錄 | Page GlobalDirectory |
頁上級目錄 | Page Upper Directory |
頁中間目錄 | Page Middle Directory |
頁表 | Page Table |
頁內偏移 | Page Offset |
Linux不同於其他的操作系統, 它把電腦分成獨立層(體繫結構無關)/依賴層(體繫結構相關)兩個層次. 對於頁面的映射和管理也是如此. 頁表管理分為兩個部分, 第一個部分依賴於體繫結構, 第二個部分是體繫結構無關的. 所有數據結構幾乎都定義在特定體繫結構的文件中. 這些數據結構的定義可以在頭文件arch/對應體系/include/asm/page.h
和arch/
對應體系/include/asm/pgtable.h
中找到. 但是對於AMD64和IA-32已經統一為一個體繫結構. 但是在處理頁表方面仍然有很多的區別, 因為相關的定義分為兩個不同的文件arch/x86/include/asm/page_32.h
和arch/x86/include/asm/page_64.h
, 類似的也有pgtable_xx.h .
2 頁表
Linux內核通過四級頁表將虛擬記憶體空間分為5個部分(4個頁表項用於選擇頁, 1個索引用來表示頁內的偏移). 各個體繫結構不僅地址長度不同, 而且地址字拆分的方式也不一定相同. 因此內核使用了巨集用於將地址分解為各個分量.
其他內容請參照博主的另外兩篇博客, 我就不羅嗦了
深入理解電腦系統-之-記憶體定址(五)–頁式存儲管理, 詳細講解了傳統的頁式存儲管理機制
深入理解電腦系統-之-記憶體定址(六)–linux中的分頁機制, 詳細的講解了Linux內核分頁機制的實現機制
3 Linux分頁機制的演變
3.1 Linux的頁表實現
由於程式存在局部化特征, 這意味著在特定的時間內只有部分記憶體會被頻繁訪問,具體點,進程空間中的text段(即程式代碼), 堆, 共用庫,棧都是固定在進程空間的某個特定部分,這樣導致進程空間其實是非常稀疏的, 於是,從硬體層面開始,頁表的實現就是採用分級頁表的方式,Linux內核當然也這麼做。所謂分級簡單說就是,把整個進程空間分成區塊,區塊下麵可以再細分,這樣在記憶體中只要常駐某個區塊的頁表即可,這樣可以大量節省記憶體。
3.2 Linux最初的二級頁表
Linux最初是在一臺i386機器上開發的,這種機器是典型的32位X86架構,支持兩級頁表
一個32位虛擬地址如上圖劃分。當在進行地址轉換時,結合在CR3寄存器中存放的頁目錄(page directory, PGD)的這一頁的物理地址,再加上從虛擬地址中抽出高10位叫做頁目錄表項(內核也稱這為pgd)的部分作為偏移, 即定位到可以描述該地址的pgd;
從該pgd中可以獲取可以描述該地址的頁表的物理地址,再加上從虛擬地址中抽取中間10位作為偏移, 即定位到可以描述該地址的pte;
在這個pte中即可獲取該地址對應的頁的物理地址, 加上從虛擬地址中抽取的最後12位,即形成該頁的頁內偏移, 即可最終完成從虛擬地址到物理地址的轉換。
從上述過程中,可以看出,對虛擬地址的分級解析過程,實際上就是不斷深入頁表層次,逐漸定位到最終地址的過程,所以這一過程被叫做page talbe walk。
至於這種做法為什麼能節省記憶體,舉個更簡單的例子更容易明白。比如要記錄16個球場的使用情況,每張紙能記錄4個場地的情況。採用4+4+4+4,共4張紙即可記錄,但問題是球場使用得很少,有時候一整張紙記錄的4個球場都沒人使用。於是,採用4 x 4方案,即把16個球場分為4組,同樣每張紙剛好能記錄4組情況。這樣,使用一張紙A來記錄4個分組球場情況,當某個球場在使用時,只要額外使用多一張紙B來記錄該球場,同時,在A上記錄”某球場由紙B在記錄”即可。這樣在大部分球場使用很少的情況下,只要很少的紙即困記錄,當有球場被使用,有需要再用額外的紙來記錄,當不用就擦除。這裡一個很重要的前提就是:局部性。
3.3 Linux的三級頁表
當X86引入物理地址擴展(Pisycal Addrress Extension, PAE)後,可以支持大於4G的物理記憶體(36位),但虛擬地址依然是32位,原先的頁表項不適用,它實際多4 bytes被擴充到8 bytes,這意味著,每一頁現在能存放的pte數目從1024變成512了(4k/8)。相應地,頁表層級發生了變化,Linus新增加了一個層級,叫做頁中間目錄(page middle directory, PMD), 變成:
欄位 | 描述 |
---|---|
cr3 | 指向一個PDPT |
PGD | 指向PDPT中4個項中的一個 |
PMD | 指向頁目錄中512項中的一個 |
PTE | 指向頁表中512項中的一個 |
page offset | 4KB頁中的偏移 |
實際的page table walk依然類似,只不過多了一級
現在就同時存在2級頁表和3級頁表,在代碼管理上肯定不方便。巧妙的是,Linux採取了一種抽象方法:所有架構全部使用3級頁表: 即PGD -> PMD -> PTE。那隻使用2級頁表(如非PAE的X86)怎麼辦?
辦法是針對使用2級頁表的架構,把PMD抽象掉,即虛設一個PMD表項。這樣在page table walk過程中,PGD本直接指向PTE的,現在不了,指向一個虛擬的PMD,然後再由PMD指向PTE。這種抽象保持了代碼結構的統一。
3.4 Linux的四級頁表
硬體在發展,3級頁表很快又捉襟見肘了,原因是64位CPU出現了, 比如X86_64, 它的硬體是實實在在支持4級頁表的。它支持48位的虛擬地址空間1。如下:
欄位 | 描述 |
---|---|
PML4 | 指向一個PDPT |
PGD | 指向PDPT中4個項中的一個 |
PMD | 指向頁目錄中512項中的一個 |
PTE | 指向頁表中512項中的一個 |
page offset | 4KB頁中的偏移 |
Linux內核針為使用原來的3級列表(PGD->PMD->PTE),做了折衷。即採用一個唯一的,共用的頂級層次,叫PML4[2]。這個PML4沒有編碼在地址中,這樣就能套用原來的3級列表方案了。不過代價就是,由於只有唯一的PML4, 定址空間被局限在(239=)512G, 而本來PML4段有9位, 可以支持512個PML4表項的。現在為了使用3級列表方案,只能限制使用一個, 512G的空間很快就又不夠用了,解決方案呼之欲出。
在2004年10月,當時的X86_64架構代碼的維護者Andi Kleen提交了一個叫做4level page tables for Linux的PATCH系列,為Linux內核帶來了4級頁表的支持。在他的解決方案中,不出意料地,按照X86_64規範,新增了一個PML4的層級, 在這種解決方案中,X86_64擁一個有512條目的PML4, 512條目的PGD, 512條目的PMD, 512條目的PTE。對於仍使用3級目錄的架構來說,它們依然擁有一個虛擬的PML4,相關的代碼會在編譯時被優化掉。 這樣,就把Linux內核的3級列表擴充為4級列表。這系列PATCH工作得不錯,不久被納入Andrew Morton的-mm樹接受測試。
不出意外的話,它將在v2.6.11版本中釋出。但是,另一個知名開發者Nick Piggin提出了一些看法,他認為Andi的Patch很不錯,不過他認為最好還是把PGD作為第一級目錄,把新增加的層次放在中間,並給出了他自己的Patch:alternate 4-level page tables patches。Andi更想保持自己的PATCH, 他認為Nick不過是玩了改名的游戲,而且他的PATCH經過測試很穩定,快被合併到主線了,不宜再折騰。
不過Linus卻表達了對Nick Piggin的支持,理由是Nick的做法conceptually least intrusive。畢竟作為Linux的扛把子,穩定對於Linus來說意義重大。
最終,不意外地,最後Nick Piggin的PATCH在v2.6.11版本中被合併入主線。在這種方案中,4級頁表分別是:PGD -> PUD -> PMD -> PTE。