程序頁表與核心頁表:頁表的初始化
摘要:linux剛剛加電啟動時,如何從真實模式進入保護模式?啟動分頁機制的前提是什麼?如何保證分頁機制之前和之後通過實地址和虛擬地址都能訪問到同一個實體地址呢?核心頁表是如何進行初始化的?使用者程序不能訪問核心的資料是在初始化的哪個階段決定的?這些內容,都牽扯到linu的程序頁表和核心頁表,以及核心頁表的初始化。本文也主要為你解答這些疑問.
1.程序頁表:
關鍵資料結構:
PAGE_OFFSET: 0xc000000
程序地址空間以0xc0000000(由巨集PAGE_OFFSET定義)分割成兩個部分,程序執行在使用者態,產生的地址小於0xc0000000;程序執行在核心態,產生的地址大於0xc0000000.但是,在某些情況下,核心為了訪問資料必須訪問使用者態線性地址空間。
程序地址空間,其中的核心態部分對於所有的程序都是一樣的,等於主核心頁全域性目錄的相應表項。
2.核心頁表
核心維護著自己的頁表,駐留在所謂的主核心頁全域性目錄中。我們將在此處解釋:核心如何初始化自己的頁表。
1)第一階段:核心映象剛剛裝入記憶體,CPU處於真實模式,分頁功能尚未開啟,核心建立一個有限的地址空間,128K,僅僅將核心裝入RAM並初始化核心資料。
2)第二階段:核心充分利用剩餘的RAM建立頁表,下面,我們將詳細討論這個頁表的建立過程。
2.1臨時核心頁表
關鍵資料結構:
swapper_pg_dir:臨時頁全域性目錄對應的虛擬地址。
pg0:第一個頁所在的實體地址
臨時頁全域性目錄在編譯核心過程中靜態初始化,而臨時頁全域性目錄存放在swapper_pg_dir之中。臨時頁表從pg0變數處開始存放。這裡,我們假設核心使用的段,臨時頁表和128KB的記憶體初始化資料可以存放在前8M的RAM之中。為了對映這8M,我們需要用到2個頁表。
開啟分頁的首要任務是確保真實模式和保護模式下都能對前8M進行定址(參考其中有關控制暫存器CR3的部分)。就是說,從0x0000000到0x007fffff的線性地址,從0xc0000000到0xc07fffff均可對映到實體地址範圍:0x00000000到0x007fffff。
是startup_32()來初始化的。它的等價程式碼(這段程式碼見於2.4.0核心,2.6核心以後不是這樣)如下:
98 movl $swapper_pg_dir-__PAGE_OFFSET,%eax
99 movl %eax,%cr3 /* set the page table pointer.. */
100 movl %cr0,%eax
101 orl $0x80000000,%eax
102 movl %eax,%cr0 /* ..and set paging (PG) bit */
其中,swapper_pg_dir是一個數組變數的名稱;核心通過將swapper_pg_dir的所有項都填充為0.除了0、1,ox300(十進位制768),0x301(十進位制769)除外。這四項按照下列方式進行初始化:
* 0和0x300設定位pg0的實體地址,1和0x301設定成pg1的地址
* 四項的present、R/W,U/S置位
* 四項的accessed、dirty、pcd和pagesize位置零
2.2RAM小於896M時候的最終核心頁表
關鍵資料結構:
關鍵函式:
巨集__pa和__va分別進行相應區域的實體地址和線性地址之間的轉換:位於核心空間的轉換
由核心頁表所提供的最終對映必須把從0xc0000000開始的線性地址對映到從0開始的實體地址。其中巨集__pa和__va分別進行相應區域的實體地址和線性地址之間的轉換。
主核心全域性目仍然在swapee_page_dir變數中,它由paging_init()函式進行初始化:
444 void __init paging_init(void)
445 {
446 pagetable_init();
447
448 __asm__( "movl %%ecx,%%cr3\n" ::"c"(__pa(swapper_pg_dir)));
449
450 #if CONFIG_X86_PAE
451 /*
452 * We will bail out later - printk doesnt work right now so
453 * the user would just see a hanging kernel.
454 */
455 if (cpu_has_pae)
456 set_in_cr4(X86_CR4_PAE);
457 #endif
458
459 __flush_tlb_all();
460
461 #ifdef CONFIG_HIGHMEM
462 kmap_init();
463 #endif
464 {
465 unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0};
466 unsigned int max_dma, high, low;
467
468 max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
469 low = max_low_pfn;
470 high = highend_pfn;
471
472 if (low < max_dma)
473 zones_size[ZONE_DMA] = low;
474 else {
475 zones_size[ZONE_DMA] = max_dma;
476 zones_size[ZONE_NORMAL] = low - max_dma;
477 #ifdef CONFIG_HIGHMEM
478 zones_size[ZONE_HIGHMEM] = high - low;
479 #endif
480 }
481 free_area_init(zones_size);
482 }
483 return;
484 }
函式執行過程如下:
1)使用pagetable_init()建立頁表項
2)將swapper_pg_dir的實體地址寫入cr3
3)如果CPU編譯核心的時候支援PAE,則將CR4控制暫存器的PAE置位
4)呼叫__flush_tlb_all()使得所有的TLB無效
pagetable_init()執行的操作依賴於RAM容量和CPU模型,對全域性目錄的初始化程式碼等價如下:
314 static void __init pagetable_init (void)
315 {
316 unsigned long vaddr, end;
317 pgd_t *pgd, *pgd_base;
318 int i, j, k;
319 pmd_t *pmd;
320 pte_t *pte;
321
322 /*
323 * This can be zero as well - no problem, in that case we exit
324 * the loops anyway due to the PTRS_PER_* conditions.
325 */
326 end = (unsigned long)__va(max_low_pfn*PAGE_SIZE);
327
328 pgd_base = swapper_pg_dir;
329 #if CONFIG_X86_PAE
330 for (i = 0; i < PTRS_PER_PGD; i++) {
331 pgd = pgd_base + i;
332 __pgd_clear(pgd);
333 }
334 #endif
335 i = __pgd_offset(PAGE_OFFSET);
336 pgd = pgd_base + i;
337
338 for (; i < PTRS_PER_PGD; pgd++, i++) {
339 vaddr = i*PGDIR_SIZE;
340 if (end && (vaddr >= end))
341 break;
342 #if CONFIG_X86_PAE
343 pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
344 set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
345 #else
346 pmd = (pmd_t *)pgd;
347 #endif
348 if (pmd != pmd_offset(pgd, 0))
349 BUG();
350 for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {
351 vaddr = i*PGDIR_SIZE + j*PMD_SIZE;
352 if (end && (vaddr >= end))
353 break;
354 if (cpu_has_pse) {
355 unsigned long __pe;
356
357 set_in_cr4(X86_CR4_PSE);
358 boot_cpu_data.wp_works_ok = 1;
359 __pe = _KERNPG_TABLE + _PAGE_PSE + __pa(vaddr);
360 /* Make it "global" too if supported */
361 if (cpu_has_pge) {
362 set_in_cr4(X86_CR4_PGE);
363 __pe += _PAGE_GLOBAL;
364 }
365 set_pmd(pmd, __pmd(__pe));
366 continue;
367 }
368
369 pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
370 set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte)));
371
372 if (pte != pte_offset(pmd, 0))
373 BUG();
374
375 for (k = 0; k < PTRS_PER_PTE; pte++, k++) {
376 vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE;
377 if (end && (vaddr >= end))
378 break;
379 *pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);
380 }
381 }
382 }
383
384 /*
385 * Fixed mappings, only the page table structure has to be
386 * created - mappings will be set by set_fixmap():
387 */
388 vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
389 fixrange_init(vaddr, 0, pgd_base);
390
391 #if CONFIG_HIGHMEM
392 /*
393 * Permanent kmaps:
394 */
395 vaddr = PKMAP_BASE;
396 fixrange_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);
397
398 pgd = swapper_pg_dir + __pgd_offset(vaddr);
399 pmd = pmd_offset(pgd, vaddr);
400 pte = pte_offset(pmd, vaddr);
401 pkmap_page_table = pte;
402 #endif
403
404 #if CONFIG_X86_PAE
405 /*
406 * Add low memory identity-mappings - SMP needs it when
407 * starting up on an AP from real-mode. In the non-PAE
408 * case we already have these mappings through head.S.
409 * All user-space mappings are explicitly cleared after
410 * SMP startup.
411 */
412 pgd_base[0] = pgd_base[USER_PTRS_PER_PGD];
413 #endif
414 }
由startup_32()函式建立的實體記憶體前8M的恆等轉化在這種對映不再必要的時候,需要呼叫zap_low_mappings()進行撤銷。
2.3當RAM在896M和4096M之間的最終核心頁表
這種情況下,並不把RAM全部對映到核心地址空間。linux在初始化階段將把一個具有896MB的視窗對映到核心線性地址空間。如果一個程式需要對896M以上的地址進行定址,那麼就必須把線性地址對映到對應的RAM,這意味這修改某些頁表項的值。核心使用與前一種情況相同的程式碼來初始化頁全域性目錄。
2.4當RAM大於4096MB時候的最終核心頁表
此時,線性地址只有1G和RAM大於1G,此處的對映就可能涉及到PAE和高階記憶體,詳細可以參考高階記憶體。此時,linux僅僅對映前896M的RAM,剩下的不進行對映(剩下的就用於存放使用者資料了)。與前兩種的主要差異在於,此時,採用三級分頁模型,程式碼如下:
。。。。。。頁全域性目錄的前三項與使用者線性地址空間對應,核心使用一個空頁(empty_zero_page)進行初始化,第四項,採用頁中間目錄的地址進行初始化,改頁中間目錄是通過呼叫alloc_bootmem_low_pages()獲得的。頁中間目錄的前448項用RAM的實體地址填充。
然後頁全域性目錄的第四項被拷貝到第一項中,這樣好為線性地址空間的前896M中的第實體記憶體對映做映象。為了完成對SMP系統的初始化,這個對映是必須的,初始化完成以後,核心呼叫zap-low_mappings()清楚對應的頁表項。