1. 程式人生 > >記憶體知識梳理2. Linux 頁表的建立過程-x86

記憶體知識梳理2. Linux 頁表的建立過程-x86

Linux的定址機制的正常執行機制的正常執行,有賴於在啟動過程中足部建立的核心頁和頁表。本節講述核心頁表的建立過程。

bootloader載入核心映象

作業系統啟動前時啟動bootloader,並用bootloader載入和解壓核心映象到記憶體中。真實模式下bootloader(示例中是grub)不能訪問1MB以上的記憶體,需要暫時開啟保護模式,將保護模式核心放入1MB以上的地址空間。載入完成後的記憶體佈局如圖。
核心載入完成後的記憶體佈局
Bootloader執行完使命後跳轉到核心arch/x86/boot/header.S的_start處開始執行。

準備進入第一次保護模式(記憶體管理:野蠻)

核心彙編程式碼要設定堆疊(ss,esp)和清理bss段,然後跳入C語言的arch/x86/boot/main.c執行

==================== arch/x86/boot/main.c 134 183 ====================
134 void main(void)
135 {
181 /* Do the last things and invoke protected mode */
182 go_to_protected_mode();
183 }

呼叫go_to_protected_mode()進入保護模式(pm.c)。 

==================== arch/x86/boot/pm.c 104 126 ====================
104 void go_to_protected_mode(void)
105 {
121 /* Actual transition to protected mode... */
122 setup_idt();
123 setup_gdt();
126 }

為了進入保護模式,需要先設定gdt,這個時候的gdt為boot_gdt,程式碼段和資料段描述符中的基址都為0,設定完後就開啟保護模式。
==================== arch/x86/boot/pm.c 66 90 ====================
66 static void setup_gdt(void)
67 {           
68     /* There are machines which are known to not boot with the GDT
69        being 8-byte unaligned.  Intel recommends 16 byte alignment. */
70     static const u64 boot_gdt[] __attribute__((aligned(16))) = {
71         /* CS: code, read/execute, 4 GB, base 0 */
72         [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
73         /* DS: data, read/write, 4 GB, base 0 */
74         [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
75         /* TSS: 32-bit tss, 104 bytes, base 4096 */
76         /* We only have a TSS here to keep Intel VT happy;
77            we don't actually use it for anything. */
78         [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
79     };      
80     /* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead
81        of the gdt_ptr contents.  Thus, make it static so it will
82        stay in memory, at least long enough that we switch to the
83        proper kernel GDT. */
84     static struct gdt_ptr gdt;
85             
86     gdt.len = sizeof(boot_gdt)-1;
87     gdt.ptr = (u32)&boot_gdt + (ds() << 4);
88             
89     asm volatile("lgdtl %0" : : "m" (gdt)); //載入段描述符
90 }

其中GDT_ENTRY巨集的定義如下:

==================== arch/x86/include/asm/segment.h 4 11 ====================
4 /* Constructor for a conventional segment GDT (or LDT) entry */
5 /* This is a macro so it can be used in initializers */
6 #define GDT_ENTRY(flags, base, limit)         \
7   ((((base)  & 0xff000000ULL) << (56-24)) |   \
8    (((flags) & 0x0000f0ffULL) << 40) |        \
9    (((limit) & 0x000f0000ULL) << (48-16)) |   \
10   (((base)  & 0x00ffffffULL) << 16) |        \
11   (((limit) & 0x0000ffffULL)))

由於尚未開啟分頁,所以寫入gdtr暫存器的需要是實體地址,上面87行說明了在真實模式下段式定址的過程,將變數的地址加上段地址才是實體地址。

第一次進入保護模式,為了解壓核心(記憶體管理:野蠻)

進入arch/x86/boot/compressed/head_32.S中的startup_32(),用於解壓剩餘的核心。

==================== arch/x86/boot/pm.c 121 125 ====================
121     /* Actual transition to protected mode... */
122     setup_idt();
123     setup_gdt();
124     protected_mode_jump(boot_params.hdr.code32_start,
125             (u32)&boot_params + (ds() << 4));

第二次進入保護模式(第二次設定gdtr)

由於整個vmlinux的編譯連結地址都是從線性地址0xc0000000開始的,有必要重新設定下段定址。這一次設定的原因是在之前的處理過程中,指令地址是從實體地址0x100000開始的,而此時整個vmlinux的編譯連結地址是從虛擬地址0xC0000000開始的,所以需要在這裡重新設定boot_gdt的位置。

===================arch/x86/boot/compressed/head_32.S ====================
34 ENTRY(startup_32)
82         testb  $(1<<6), BP_loadflags(%esi)
83         jnz    1f
84 
85         cli
86         movl   $__BOOT_DS, %eax
87         movl   %eax, %ds
88         movl   %eax, %es
89         movl   %eax, %fs
90         movl   %eax, %gs
91         movl   %eax, %ss
92 1:
102         leal    (BP_scratch+4)(%esi),%esp      //設定棧頂

小結

啟動過程中壓縮核心被搬移到64MB處,然後解壓到1MB開始的實體地址處,(在PC機上,前1MB物理空間包含BIOS資料和圖形卡,不能被核心使用)核心是從第二個MB開始的。假設Linux核心需要小於3MB的RAM,圖示了x86的前3MB實體地址分配。
x86前3MB記憶體分配
核心在編譯過程中都是使用的0xc0000000開始的線性地址,因此需要立即開啟分頁。

開啟第一次分頁(建立臨時核心頁表)

核心中編有一個初始頁全域性目錄initial_page_table,靜態分配好了空間。

662 ENTRY(initial_page_table)
663    .fill 1024,4,0  //填充一個頁面(4k)的空間

有了全域性目錄,還需要分配頁表空間。在連結vmlinux時,有一個BRK段,其開始地址為brk_base(有的版本稱為pg0變數,緊挨著_end)。這個段的作用是保留給使用者通過brk()系統呼叫向核心申請記憶體空間用的,我們先不管它的這個用處,在這個步驟中頁表空間就從brk_base分配。假設我們需要對映8MB空間,則只需要不超過2048個頁表項。 

核心把頁目錄的0和768項指向同樣的頁表、1和769項指向同樣的頁表,使得線性地址0和0xc0000000指向同樣的實體地址,把其餘頁目錄項全部清零,使得其他地址都未對映。來看初始化後的頁表和頁目錄。
初始化後的頁表和頁目錄

設定好頁目錄和頁表項後,將initial_page_table的值賦給CR3,就開啟了頁式定址。

movl $pa(initial_page_table), %eax
movl %eax,%cr3      /* set the page table pointer.. */
movl $CR0_STATE,%eax
movl %eax,%cr0      /* ..and set paging (PG) bit *///開啟分頁機制!
由於在連結中initial_page_table是線性地址,需要經過pa()巨集轉化為實體地址後才能寫入CR3。

這些工作都完成後,就完成了將實體地址0x00000000到核心_end記憶體空間對映到線性地址0x00000000開始和0xc0000000開始的記憶體空間。這樣的話,用邏輯地址0x00000000或者0xc0000000類似的地址都能訪問到實體地址0x00000000開始的空間。

第三次開啟保護模式(第三次設定gdtr)

上面過程中分段模式一直開啟,但gdtr寫入的一直是實體地址,這時候需要將其重新配置為線性地址。

第二次設定分頁(第二次設定cr3)

start_arch()中把initial_page_table複製給swapper_pg_dir,在這以後swapper_pg_dir就一直當做全域性目錄表使用了。隨後設定cr3,棄用以前的initial_page_table。

873     /*
874      * copy kernel address range established so far and switch
875      * to the proper swapper page table
876      */
877     clone_pgd_range(swapper_pg_dir     + KERNEL_PGD_BOUNDARY,              
878             initial_page_table + KERNEL_PGD_BOUNDARY,
879             KERNEL_PGD_PTRS);
880     
881     load_cr3(swapper_pg_dir);

真正的記憶體管理

下面進入第一個真正的核心管理函式:setup_memory_map()

前面在真實模式的main函式中,曾經獲取過實體記憶體結構並將其存放在e820結構體陣列中,這裡利用結構體得到最大物理頁框號max_pfn,與MAXMEM_PFN進行比較取小賦給max_low_pfn,比較結果決定是否需要開啟高階記憶體,如果需要,二者取差得到highmem_pages表示高階記憶體總頁框數。高階記憶體的問題在後續介紹。

1. struct e820map {
2.  __u32 nr_map;
3.  struct e820entry map[E820_X_MAX];//E820MAX定義為128
4.};
5.
6.struct e820entry {
7.    __u64 addr;    /* start of memory segment */
8.    __u64 size;    /* size of memory segment */
9.    __u32 type;    /* type of memory segment */
10.} __attribute__((packed));

小結

我們來總結下現在核心的狀態。

  1. 已經設定了最終gdt,開啟了段式記憶體管理,段基址都是0,因此邏輯地址和線性地址相同。
  2. 已經設定了cr3,開啟了分頁式記憶體管理。最後的全域性目錄表是swapper_pg_dir,頁表還是在__brk_base開始。將實體地址0x00000000~_end的空間對映到虛擬地址(線性地址)0x00000000開始和0xc0000000開始(即全域性目錄同時從0項和767項分配)。因此由虛擬地址0xc0000000開始連結的核心可以正常定址到實體地址上。
  3. 核心通過int 0x15獲取實體記憶體佈局,並存在e820全域性陣列中。
    需要注意的是,這個時候核心基本不能動態分配管理記憶體,唯一動態分配記憶體的方式也僅僅是從brk中分配頁表。

著手建立最終核心頁表

注意這時候swapper_pg_dir裡只對映到了核心所在的空間(上面假設的8MB),這時候需要將所有物理空間都進行對映,即建立最終頁表。建立時分為三種情況:

  • 第一種:RAM小於896MB時,也就是沒有高階記憶體的情況。
    這種情況是把所有的RAM全部對映到0xc0000000開始。由核心頁表所提供的最終對映必須把從0xc0000000開始的線性地址轉化為從0開始的實體地址,主核心頁全域性目錄仍然儲存在swapper_pg_dir變數中。
  • 第二種:RAM在896MB到4GB之間,也就是存在高階記憶體的情況。
    這種情況就不能把所有的RAM全部對映到核心空間了,Linux在初始化階段只是把一個具有896MB的RAM對映到核心線性地址空間。如果一個程式需要對現有RAM的其餘部分定址,那就需要使用896MB~1GB的這段線性空間中的一部分來分時對映到所需的RAM,在下一講中詳述。這時候頁目錄初始化方法和上一種情況相同。
  • 第三種:RAM在4GB以上。
    現代計算機,特別是些高效能的伺服器記憶體遠遠超過4GB,這要求CPU支援實體地址擴充套件(PAE),且核心以PAE支援來編譯。這時儘管PAE處理36位實體地址,但是線性地址依然是32位。如前所述,Linux對映一個896MB的RAM到核心地址空間;剩餘RAM留著不對映,並由下一講中動態重對映來處理。所不同的是使用三級分頁模型。在這種情況下,即使我們的CPU支援PAE,但是也只能有定址能力為64GB的核心頁表,大量的高階記憶體使用動態重對映的方法,對效能產生了很大的挑戰。所以如果要建立更高效能的伺服器,建議改善動態重對映演算法,或者乾脆升級為64位的處理器。

既然要建立對映,按照從前建立臨時頁表的套路,應該要有個全域性目錄表,然後分配若干頁表並初始化完成對映。這裡還使用全域性目錄表swapper_pg_dir,呼叫init_memory_mapping ()函式完成低端記憶體的對映。

低端記憶體對映

它負責對映低端記憶體空也就是0-896MB。而這部分記憶體也分為兩個部分開始對映:

  • 首先對映0-ISA_END_ADDRESS(0x100000)的RAM
  • 接著對映0x100000~896mb的RAM

先重新確定記憶體範圍(對齊,分割、合併等)後,

start_kernel
  -->setup_arch
    --> init_mem_mapping
      -->init_memory_mapping
        -->kernel_physical_mapping_init

來到kernel_physical_mapping_init(),這個才是真正的建立頁表,設定頁表項,進行記憶體對映,注意,這裡沒有定義PAE。

這個函式的作用是將實體記憶體地址都對映到從核心空間開始的地址(0xc0000000),函式從虛擬地址0xc0000000處開始對映,也就是從目錄表中的768項開始設定。從768到1024這256個表項被linux核心設定為核心目錄項,低768個目錄項被使用者空間使用。接下來就是進入迴圈,準備填充從768號全域性目錄表項開始剩餘目錄項的內容。

注意這裡是按照Linux的四級分頁模型進行的,但上級目錄和中級目錄在32位系統中暫不使用。它們都是隻有一個數組元素的陣列。
這裡要繼續分配頁表,每個頁表4KB大小即佔一頁,這裡從已經對映的記憶體中分配一頁記憶體來作為頁表空間。–梯雲縱。

至此,核心低端記憶體頁表建立完畢。

896MB問題

按照固定偏移0xc0000000的方案,高於1GB的實體記憶體其線性地址將超出32位地址範圍。這是一個Linux發展史上的歷史問題,最早的核心設計者沒有預想到記憶體空間可以發展到1GB以上,因此核心就是為1GB實體記憶體設計的。

當出現大容量記憶體後,需要這個問題。另外核心還需要在其線性地址空間內對映外設暫存器空間等I/O空間,為了解決這個問題,在x86處理器平臺上給核心增添了一個經驗值設定:896MB,更高的128MB線性地址空間用於對映高階記憶體以及I/O地址空間。就是說,如果系統中的實體記憶體(包括記憶體孔洞)大於896MB,那麼將前896MB實體記憶體固定對映到核心邏輯地址空間0xC0000000~0xC0000000+896MB(=high_memory)上,而896MB之後的實體記憶體則不按照0xC0000000偏移建立對映,這部分記憶體就叫高階實體記憶體。此時核心線性地址空間high_memory~0xFFFFFFFF之間的128MB空間就稱為高階記憶體線性地址空間。

在嵌入式系統中可以根據具體情況修改896MB這個閾值。比如,MIPS中將這個值設定為0x20000000(512MB),那麼只有當系統中的實體記憶體空間容量大於512MB時,核心才需要配置CONFIG_HIGHMEM選項,使能核心對高階記憶體的分配和對映功能。

高階記憶體的對映方法在下一講中講述。程序對於高階實體記憶體天然地可以通過設定CR3和頁目錄表合法訪問。

固定記憶體對映中的高階記憶體對映

我們看到核心線性地址第四個GB的前最多896MB部分對映系統的實體記憶體。其餘至少128MB的線性地址總是留作他用,例如將IO和BIOS以及實體地址空間對映到在這128M的地址空間內,使得kernel能夠訪問該空間並進行相應的讀寫操作;還用來臨時對映高階記憶體,使得核心能夠訪問高階記憶體。

其中有一段虛擬地址用於固定對映,也就是fixed map。固定對映的線性地址(fix-mapped linear address)是一個固定的線性地址,它所對應的實體地址是人為強制指定的。每個固定的線性地址都對映到一塊實體記憶體頁。固定對映線性地址能夠對映到任何一頁實體記憶體。

每個固定對映的線性地址都由定義於enum fixed_addresses列舉資料結構中的整型索引來表示:

enum fixed_addresses {
    FIX_HOLE,
    FIX_VDSO,
    FIX_DBGP_BASE,
    FIX_EARLYCON_MEM_BASE,
#ifdef CONFIG_PROVIDE_OHCI1394_DMA_INIT
    FIX_OHCI1394_BASE,
#endif
#ifdef CONFIG_X86_LOCAL_APIC
    FIX_APIC_BASE,  /* local (CPU) APIC) -- required for SMP or not */
#endif
#ifdef CONFIG_X86_IO_APIC
    FIX_IO_APIC_BASE_0,
    FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS - 1,
#endif
#ifdef CONFIG_X86_VISWS_APIC
    FIX_CO_CPU, /* Cobalt timer */
    FIX_CO_APIC,    /* Cobalt APIC Redirection Table */
    FIX_LI_PCIA,    /* Lithium PCI Bridge A */
    FIX_LI_PCIB,    /* Lithium PCI Bridge B */
#endif
#ifdef CONFIG_X86_F00F_BUG
    FIX_F00F_IDT,   /* Virtual mapping for IDT */
#endif
#ifdef CONFIG_X86_CYCLONE_TIMER
    FIX_CYCLONE_TIMER, /*cyclone timer register*/
#endif
    FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */
    FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#ifdef CONFIG_PCI_MMCONFIG
    FIX_PCIE_MCFG,
#endif
#ifdef CONFIG_PARAVIRT
    FIX_PARAVIRT_BOOTMAP,
#endif
    FIX_TEXT_POKE1, /* reserve 2 pages for text_poke() */
    FIX_TEXT_POKE0, /* first page is last, because allocation is backward */
    __end_of_permanent_fixed_addresses,
    /*
     * 256 temporary boot-time mappings, used by early_ioremap(),
     * before ioremap() is functional.
     *
     * If necessary we round it up to the next 256 pages boundary so
     * that we can have a single pgd entry and a single pte table:
     */
#define NR_FIX_BTMAPS       64
#define FIX_BTMAPS_SLOTS    4
#define TOTAL_FIX_BTMAPS    (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
    FIX_BTMAP_END =
     (__end_of_permanent_fixed_addresses ^
      (__end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS - 1)) &
     -PTRS_PER_PTE
     ? __end_of_permanent_fixed_addresses + TOTAL_FIX_BTMAPS -
       (__end_of_permanent_fixed_addresses & (TOTAL_FIX_BTMAPS - 1))
     : __end_of_permanent_fixed_addresses,
    FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
    FIX_WP_TEST,
#ifdef CONFIG_INTEL_TXT
    FIX_TBOOT_BASE,
#endif
    __end_of_fixed_addresses
}; 

這些線性地址都位於第四個GB的末端。在初始化階段指定其對映的實體地址。

還是回到init_mem_mapping(void)裡面,當低端記憶體完成分配以後,緊接著還有一個函式early_ioremap_page_table_range_init(),這個函式就是用來建立固定記憶體對映區域的。

518 void __init early_ioremap_page_table_range_init(void)
519 {  
520     pgd_t *pgd_base = swapper_pg_dir;
521     unsigned long vaddr, end;
522    
523     /*
524      * Fixed mappings, only the page table structure has to be
525      * created - mappings will be set by set_fixmap():
526      */
527     vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
528     end = (FIXADDR_TOP + PMD_SIZE - 1) & PMD_MASK;  //unsigned long __FIXADDR_TOP = 0xfffff000;
529     page_table_range_init(vaddr, end, pgd_base);
530     early_ioremap_reset();
531 }

fix_to_virt()函式計算從給定索引開始的常量線性地址:

========== arch/x86/mm/pgtable_32.c 98 98 ===============
unsigned long __FIXADDR_TOP = 0xfffff000;
============ arch/x86/include/asm/fixmap.h 41 41 ============
#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)

==================== arch/x86/include/asm/fixmap.h 185 210 ====================
185 #define __fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT))
195 static __always_inline unsigned long fix_to_virt(const unsigned int idx)
196 {
206 if (idx >= __end_of_fixed_addresses)
207     __this_fixmap_does_not_exist();
208
209 return __fix_to_virt(idx);
210 }

因此,每個固定對映的線性地址都對映一個實體記憶體的頁框。但是各列舉標識的分割槽並不是從低地址往高地址分佈,而是從整個線性地址空間的最後4KB即線性地址0xfffff000向低地址進行分配。在最後4KB空間與固定對映線性地址空間的頂端空留一頁(未知原因),固定對映線性地址空間前面的地址空間叫做vmalloc分配的區域,他們之間也空有一頁。
核心線性地址空間對映
下一講將詳細描述核心線性地址空間分佈圖。

有了這個固定對映的線性地址後,如何把一個實體地址與固定對映的線性地址關聯起來呢,核心使用set_fixmap(idx, phys)set_fixmap_nocache(idx, phys)巨集。這兩個函式都把fix_to_virt(idx)線性地址對應的一個頁表項初始化為實體地址phys(注意,頁目錄地址仍然在swapper_pg_dir中,這裡只需要設定頁表項);不過,第二個函式也把頁表項的PCD標誌置位,因此,當訪問這個頁框中的資料時禁用硬體快取記憶體反過來,clear_fixmap(idx)用來撤消固定對映線性地址idx和實體地址之間的連線。

這個固定地址對映到底拿來做什麼用呢?一般用來代替一些經常用到的指標。我們想想,就指標變數而言,固定對映的線性地址更有效。事實上,間接引用一個指標變數比間接引用一個立即常量地址要多一次記憶體訪問。比如,我們設定一個FIX_APIC_BASE指標,其所指物件之間存在於對應的實體記憶體中,我們通過set_fixmap和clear_fixmap建立好二者的關係以後,就可以直接定址了,沒有必要像指標那樣再去間接一次定址。

最後init_mem_mapping()呼叫load_cr3()重新整理CR3暫存器,__flush_tlb_all()則用於重新整理TLB,由此啟用新的記憶體分頁對映。

至此,核心頁表建立完畢。

因此對於核心來講其對映是個簡單的線性對映,只需要加減一個兩者間的偏移量0xC0000000即可。該值在程式碼中稱為PAGE_OFFSET,在arch/x86/include/asm/page_types.h中定義。同時PAGE_OFFSET也代表使用者空間的上限,所以常數TASK_SIZE就是用它定義的(arch/x86/include/asm/processor.h)。

從核心頁表建立過程可以看出,固定偏移是後續的基礎。如果沒有這個固定偏移,頁式定址就成為一個雞生蛋蛋生雞的問題了,因為程序切換時頁目錄表的基地址CR3是個實體地址,如果核心連這個實體地址也沒辦法計算,那麼就沒有辦法實現頁式定址。實際上核心確實是使用pa()巨集獲得CR3裡的值的。

程序的頁對映在程序啟動過程中確定,具體過程在第三講詳述。

Linux定址的加速

Linux核心使用了一些技術,提高了硬體快取記憶體和TLB的命中效率,實現定址過程的加速。

快取記憶體

快取記憶體按照快取行Cahce Line為單位定址,在Pentium4之前的Intel模型中CacheLine為32位元組,而Pentium 4上緩衝行為128位元組。為了使快取記憶體的命中率達到最優化,核心在決策中考慮體系結構:

  1. 一個數據結構中最常使用的欄位放在資料結構的低偏移部分,以便他們能處於快取記憶體的同一行。
  2. 當為一大組資料結構分配空間時,核心試圖把它們都存放在記憶體中,以便所有快取記憶體行都按照同一方式使用。

80x86的微處理器能夠自動處理快取記憶體的同步,但是有些處理器卻不能。Linux核心為這些不能同步快取記憶體的處理器提供了快取記憶體重新整理介面。

x86體系中當CR3替換時,處理器將所有TLB自動置無效。除此之外處理器不知道TLB快取是否有效,因此不能自動同步自己的TLB快取記憶體。需要由核心處理TLB的重新整理。Linux核心提供了多種獨立於體系結構的TLB巨集,針對不同體系結構選擇是否實現。

TLB

一般來講程序切換意味著要更換活動頁表集,對於過期頁表,核心把新的CR3寫入完成重新整理,但是為了效率期間,有些情況下Linux會避免TLB重新整理。
當兩個使用相同頁表集的普通程序之間切換時。後續程序管理章節講述schedule()函式時候詳述。
當一個普通程序和一個核心執行緒之間切換時。在之後第三講程序地址空間中將會講到,核心執行緒沒有自己的頁表集,它們使用剛剛在CPU上執行過的程序的頁表集。因為核心執行緒基本上只使用核心地址空間的資訊,這些資訊與程序地址空間的高1G線性空間那部分相同。

除了程序切換外,還有一些情況核心需要重新整理TLB中的一些表項。例如在核心為某個使用者態程序分配頁框並將它的實體地址存入頁表項時,它必須重新整理與相應線性地址對應的TLB表項。
為了避免無效重新整理,核心使用一種叫做懶惰TLB(Lazy TLB)模式的技術。其基本思想是,如果有多個CPU在使用相同的頁表,而且必須對這些CPU上的一個TLB表項進行重新整理,那麼在某些情況下正在執行核心執行緒的那些CPU上的重新整理就可以延遲。
當某個CPU正在執行一個核心執行緒時,核心把它設定為懶惰TLB模式,當發出清除TLB表項的請求時,並不立即清除,而是標記對應的程序地址空間無效。只要有一個懶惰TLB模式的CPU用一個不同的頁表集切換到一個普通程序,硬體就自動重新整理,同時把CPU置為非懶惰模式。然而如果切換到一個同樣頁表集的程序,那麼核心需要自己實施重新整理操作。

核心使用一些額外的資料結構實現懶惰TLB。這些資料結構的具體用法涉及到程序排程,不在這裡詳細描述。

==================== arch/x86/include/asm/tlbflush.h 148 154 ====================
148 #define TLBSTATE_OK 1
149 #define TLBSTATE_LAZY   2
150 
151 struct tlb_state {
152     struct mm_struct *active_mm;
153     int state;
154 };
==================== arch/x86/mm/tlb.c 16 17 ====================
16 DEFINE_PER_CPU_SHARED_ALIGNED(struct tlb_state, cpu_tlbstate)
17          = { &init_mm, 0, };

總結與預告

Intel在發展過程中為了相容過去的處理模式,採用了分段加分頁的定址實現方式,使得定址過程異常複雜。但是硬體上的一些設計使得這種冗餘模式保持了一定的效率。
Linux為了提供跨平臺支援,沒有跟隨x86的段式管理意圖,而是使得偏移量等於線性地址。完全依賴分頁機制。同時還提供了一些技術實現定址的加速。
Linux的分頁機制在初始化過程中逐步建立。

下期:
頁框管理、高階記憶體頁框的核心對映、記憶體區管理。

彙編部分的詳細註解亦可參考http://blog.csdn.net/yu616568/article/details/7581919