1. 程式人生 > >Linux 記憶體分頁

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