從 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) 就可以重用代碼了。