Linux 記憶體分頁
從 Linux 2.6.11 開始,核心使用了獨立於硬體架構的四級頁表。但支援幾級頁表應該是硬體支援為標準,Linux 如何做到四級頁表的呢?
下面看一段頁表初始的程式碼就知道了。
PKMAP 固定記憶體部分的頁表初始化
首先弄清,以下分析都是建立在配置了大於 1G 記憶體,並且未開啟 PAE 情況下的 X86 架構的一些巨集的值。
一些觀點列出也都預設是以上條件下。
CallStack: page_table_range_init (arch\x86\mm\init_32.c) permanent_kmaps_init (arch\x86\mm\init_32.c) pagetable_init (arch\x86\mm\init_32.c) paging_init (arch\x86\mm\init_32.c) setup_arch (arch\x86\kernel\setup.c) start_kernel (init\main.c) startup_32 (head_32.S) static void __init page_table_range_init(unsigned long start, unsigned long end, pgd_t *pgd_base) { int pgd_idx, pmd_idx; unsigned long vaddr; pgd_t *pgd; pmd_t *pmd; vaddr = start; pgd_idx = pgd_index(vaddr); pmd_idx = pmd_index(vaddr); pgd = pgd_base + pgd_idx; for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd++, pgd_idx++) { pmd = one_md_table_init(pgd); pmd = pmd + pmd_index(vaddr); for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd++, pmd_idx++) { one_page_table_init(pmd); vaddr += PMD_SIZE; } pmd_idx = 0; } }
+------------------------------+
| PGD | PUD | PMD | PTE | PAGE |
+------------------------------+
每一級對應在虛擬地址的偏移,加上掩碼可以計算出相應級的值
PGDIR_SHIFT 22
PUD_SHIFT 22
PMD_SHIFT 22
PAGE_SHIFT 12
相應級對應的虛擬地址中的位數
PGD PUD, PMD PTE PAGE
10, 0, 0, 10, 12
每一級中包含的項數(也是每一級中索引的上限 [0, x))
PTRS_PER_PGD 1024 (2^10)
PTRS_PER_PUD 1 (2^0)
PTRS_PER_PMD 1 (2^0)
PTRS_PER_PTE 1024 (2^10)
比如 PGD 有 10 位,則它可以表示 2^10 個表項, PUD 對應 0 位,則說明頁上層目錄中只有一個目錄項。
當要初始化一段虛擬地址對應的頁表時,首先根據虛擬地址得到其對應的 PGD 陣列的項,pgd_inex(vaddr) 就是做這個工作的, 它是一個巨集,
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
這樣就得到了該虛擬地址對應 PGD 中的項的索引,這樣就得到了頁全域性目錄項,該項中記錄了頁上級目錄的實體地址。
那麼問題來了,X86 只識別兩級頁表,而 Linux 程式碼中分佈管理是以四級頁表實現的,如何實現這一點呢,首先要明確,程式碼總是建立的硬體實現的基礎上,所以說 Linux 的四級頁表其實是虛擬的四級頁表,也就是在程式碼實現上,好像是四級分佈,其實,是用了四級分頁的程式碼,來填充兩級頁表,看 pmd_index 的實現便知道了。
#define pmd_index(address) (((address) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
其實頁目錄的計算都是既定的這些公式,只是規定的位偏移和長度不同而已,取出 PMD 在虛擬地址中對應的值,然後根據該項的取值範圍做掩碼,就可以得到在該級中的項的偏移,由於 PTRS_PER_PMD 為 1,即該項的索引應該小於 1,即只能為 0,但知道了項的索引值也不足以明白如何構建的二級頁表,那接著看下去。
當知道各級的項對應的索引後,就可以初始化整個頁表了,首先外層迴圈填充頁全域性目錄,然後填充每一個頁全域性目錄項對應的 PUD, PMD, 由函式 one_md_table_init 來實現,傳入頁全域性目錄項的虛擬地址,返回頁中間目錄的虛擬地址,因為只相當於相容了三級頁表,所以 PUD 的初始化相當於省略掉了,它的實現只有兩條語句,
pud = pud_offset(pgd, 0);
pmd_table = pmd_offset(pud, 0);
pud_offset 得到 pud 的偏移, #define pud_offset(pgd, start) (pgd), 它直接返回了 pgd 的地址,
pmd_offset 也同樣返回了 pud 的虛擬地址。也就是 pgd 項的地址,然後函式返回。
然後開始初始化頁中間目錄,迴圈設定每一項,其實只有一項,把這些項設定為頁表的值, one_page_table_init 來設定頁表,首先申請一個頁表,然後賦值給相應的 pmd 項,這樣就依次把 page_table_range_init 傳入的這段虛擬地址的頁表給建立起來了。
總結一下,由於該架構只支援二級頁表,所以在計算 PUD 和 PMD 時,都是返回的傳入的上級目錄項虛擬地址,也就是 PGD 的目錄項虛擬地址。
按四級來分,示意圖如下:
PGD PUD PMD PT +-----+ +-----+ +-----+ +-----+ | a |--->| a |-------------->| a |------------>| t0 | +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ | b |------------>| b |-------------->| b | | t1 | +-----+ +-----+ +-----+ +-----+ +-----+ +-----+ | c |--->| c |-------------->| c | | t2 | +-----+ +-----+ +-----+ +-----+ | ... | | ... |
PUD 和 PMD 每個目錄只有一項,並且值與 PGD 相應的表項相同。
由於通過 PGD 項得到 PUD 目錄地址和通過 PUD 得到 PMD 基址時,返回的其實就是上一級目錄項的地址,參見 pud_offset pmd_offset,即 PUD 其實就是 PGD 中的一項,PMD 就是 PUD 相對應的目錄項,即 PGD 中的一項,那麼最終形成的頁表結構應該是:
PGD PT
+-------------+ +-----+ +------+
|a(PUD)(PMD) |-------------->| t0 |-------->| Page |
+-------------+ +-----+ +------+
| b | | t1 |
+-------------+ +-----+
| c | | t2 |
+-------------+ +-----+
| ... | | ... |
即全域性頁目錄頁,也是 PUD, 也是 PMD。
Linux 用巨集配置,來實現了程式碼上的四級頁表,但真實的兩級頁表。那麼當真實是多級頁表時,只需要配置相應級的偏移量(XXX_SHIFT)及對對應各級的目錄項範圍 (PTRS_PER_XXX) 就可以重用程式碼了。