Linux分頁機制之概述--Linux內存管理(六)

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.harch/對應體系/include/asm/pgtable.h中找到. 但是對於AMD64和IA-32已經統一爲一個體繫結構. 但是在處理頁表方面仍然有很多的區別, 因爲相關的定義分爲兩個不同的文件arch/x86/include/asm/page_32.harch/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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章