1. 程式人生 > >Linux核心4級頁表的演進

Linux核心4級頁表的演進

Linux記憶體管理中core VM程式碼中,關於頁表(page tables)管理的程式碼是個重點,是虛擬記憶體(Virtual Memory, VM)的基石,本文探討Linux的頁表實現及發展過程。

頁表概覽

在虛擬記憶體中,頁表是個對映表的概念, 即從程序能理解的線性地址(linear address)對映到儲存器上的實體地址(phisical address)。很顯然,這個頁表是需要常駐記憶體的東西, 以應對頻繁的查詢對映需要(實際上,現代支援VM的處理器都有一個叫TLB的硬體級頁表快取部件,本文不討論)。

設想一個典型的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個連續的物理頁面在系統執行一段時間後碎片化的情況下,也是不現實的。

Linux的頁表實現

上面這種理論上的討論顯然不是實際情況。由於程式存在區域性化特徵, 這意味著在特定的時間內只有部分記憶體會被頻繁訪問,具體點,程序空間中的text段(即程式程式碼), 共享庫都是固定在程序空間的某個特定部分,這樣導致程序空間其實是非常稀疏的, 於是,從硬體層面開始,頁表的實現就是採用分級頁表的方式,Linux核心當然也這麼做。所謂分級簡單說就是,把整個程序空間分成區塊,區塊下面可以再細分,這樣在記憶體中只要常駐某個區塊的頁表即可,這樣可以大量節省記憶體

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在記錄"即可。這樣在大部分球場使用很少的情況下,只要很少的紙即困記錄,當有球場被使用,有需要再用額外的紙來記錄,當不用就擦除。這裡一個很重要的前提就是:區域性性

Linux的三級頁表

當X86引入實體地址擴充套件(Pisycal Addrress Extension, PAE)後,可以支援大於4G的實體記憶體(36位),但虛擬地址依然是32位,原先的頁表項不適用,它實際多4 bytes被擴充到8 bytes,這意味著,每一頁現在能存放的pte數目從1024變成512了(4k/8)。相應地,頁表層級發生了變化,Linus新增加了一個層級,叫做頁中間目錄(page middle directory, PMD), 變成:

  1. 312920110
  2. +-----+---------------+--------------+--------------+
  3. | PGD | PMD | PTE | page offset |
  4. +-----+---------------+--------------+--------------+

實際的page table walk依然類似,只不過多了一級。

現在就同時存在2級頁表和3級頁表,在程式碼管理上肯定不方便。巧妙的是,Linux採取了一種抽象方法:所有架構全部使用3級頁表: 即PGD -> PMD -> PTE。那隻使用2級頁表(如非PAE的X86)怎麼辦?

辦法是針對使用2級頁表的架構,把PMD抽象掉,即虛設一個PMD表項。這樣在page table walk過程中,PGD本直接指向PTE的,現在不了,指向一個虛擬的PMD,然後再由PMD指向PTE。這種抽象保持了程式碼結構的統一。

Linux的四級頁表

硬體在發展,3級頁表很快又捉襟見肘了,原因是64位CPU出現了, 比如X86_64, 它的硬體是實實在在支援4級頁表的。它支援48位的虛擬地址空間[1](不過Linux核心最開始只使用47位)。如下:

  1. 47382920110
  2. +------+------------+------------+------------+--------------+
  3. | PML4 | PGD | PMD | PTE | page offset |
  4. +------+------------+------------+------------+--------------+

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

參考:

  • 程式碼:
    • include/asm-generic/pgtable-nopud.h
    • include/asm-generic/pgtable-nopmd.h
    • arch/x86/include/asm/pgtable-2level*.h
    • arch/x86/include/asm/pgtable-3level*.h
    • arch/x86/include/asm/pgtable_64*.h
    * * * * * * 全文完 * * * * * *

[1]: 一開始就實現全64位的虛擬空間,一是實際用不到這麼大的空間,二是技術實現上也有困難,所以首先推出X86_64架構CPU的AMD只只支援48位(256TB),但虛擬地址仍使用64位,只不過對高16位作位擴充套件,跟第48位值一樣。這樣方便以後擴充套件,另外一個副作用是:自然地分出使用者與核心態兩個空間。不多詳述。

[2]: Page Map Layer 4, 這是首先引入X86_64 CPU的AMD的叫法。