本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
本文章中系統我們假設為x86下的32位系統,暫且不分析64位系統的頁表結構。
linux分頁
linux下采用四級分頁,一個線性地址會分為5個偏移量用於尋址,具體看圖:
雖然有四級,但並不是每一級都會用到,在linux中,對於硬件體系的不同可能會用到二級頁表,三級頁表,四級頁表中的其中一個,如下:
- 64位系統:使用四級分頁或三級分頁,跟硬件有關。
- 未開啟PAE(物理地址擴展)的32位系統:只使用二級分頁,頁上級目錄和頁中間目錄里的值全為0。
- 開啟PAE的32位系統:使用三級分頁,這種情況下被排除在外的是頁上級目錄,也就是頁上級目錄中所有值都為0。
圖中有個cr3,它是一個寄存器,專門用於保存頁全局目錄的基地址,內核的主內核頁全局目錄的基地址保存在swapper_pg_dir全局變量中,但需要使用主內核頁表時系統會把這個變量的值放入cr3寄存器,進程們自己的頁全局目錄基地址保存在自己的進程描述符的pgd中,當進程切換時,進程的頁表也是需要切換的,就是把新的進程的進程描述符的pgd存入cr3中。這些目錄和頁表每一個都是用一個頁框進行保存,比如一個進程有一個頁全局目錄,1024個頁中間目錄,1024個頁表,那系統要為這個進程分配1個頁框用於保存頁全局目錄,1024個頁框用於保存頁中間目錄,1024個頁框用於保存頁表。當然,進程一般情況下是不會需要這么多頁中間目錄和頁表的。
表項
實際上頁全局目錄、頁上級目錄、頁中間目錄、頁表都是保存在一個一個頁框中,我們知道常規情況下頁框大小為4K(特殊情況有2MB、1GB),也就是頁框的布局都是以4K倍數的地址進行排列的,要尋址一個頁框,只需要20位地址就足夠了。這些目錄和頁表中保存的都是表項,頁全局目錄保存的是頁全局目錄項,頁中間目錄保存的是頁中間目錄項,在32位系統中這些項都是32位(20位是所指頁框的基地址,12位是標志位)的,在開啟PAE后會變成64位,這些項保存着很多標志,我們羅列幾個重要的:
- Present標志:為1,所指的頁在內存中,為0,不在。
- 所指的頁框基地址:占20位。
- Accessed標志:每當分頁單元對相應頁框進行尋址時設置。
- Dirty標志:只用於頁表項,每次對一個頁框進行寫操作時設置。
- Read/Write標志:讀寫權限標志。
- User/Supervisor標志:所指的頁的特權級(進程能否訪問)。
- Page size標志:為1表示指的是2MB或4MB的頁框。也就是頁表是2MB或者4MB。
在這些里面,最重要的或許就是所指頁框基地址了,一個頁中間目錄項保存的頁框基地址就是對應的頁表的基地址,而頁表項中保存的頁框基地址,就是頁(用於保存數據)的地址。而Present標志是用於判斷是否發生缺頁異常處理的標志。由於這些標志加上所指的頁框基地址一共32位,一個4K的頁框就能夠保存1024個表項。
物理地址擴展(PAE)
這個技術是用於X86_32位體系下的,因為32位線性地址最多能表示4GB大小的空間,而PAE技術將物理地址線擴大到36條,也就是CPU能夠尋址64GB大小的物理內存。但是物理地址線擴大到36條,但是線性地址還是使用32位,這時候沒辦法用32位的線性地址去表示64GB大小的物理內存。實際上PAE做的就是讓內核有多個“主內核頁全局目錄”,第一個主內核頁全局目錄尋址0~4GB的地址,第二個尋址5~8GB的地址,所以當尋址不同區域的地址時,只需要將不同的“主內核頁全局目錄”基地址存入cr3中。這些多個主內核頁全局目錄被稱為頁目錄指針表(PDPT)。
開啟PAE后,32位系統尋址方式將大大改變:
- 二級分頁會變成三級分頁
- 表項的大小也由原來的32位變成了64位(原來是12位標志+20位頁框基地址,變成12位標志+24位頁框基地址,為什么是24位,因為64GB需要24位才能尋址完所有頁框)。
- 頁框大小將可選擇4K或者2MB,通過修改表項中的Page size標志即可指定所指頁框大小。
- 線性地址表示也變成如下:
內核啟動后內核區域內存布局
一般的,內核啟動會被加載到內存的1MB開始處,而普通配置的內核大小一般小於3MB,也就是說,內核鏡像被加載內存1MB~4MB的地方,而為什么0MB~1MB的內存內核不使用,因為這段內存一般是由BIOS使用和做一些硬件映射的。如下圖:
在里面我們值得注意的就是_end,它在代碼里表明了內核鏡像在內存中的結束地址,頁表的初始化會先初始化未被內核使用的區域,最后再初始化內核使用的區域。
高端內存布局
之前的文章linux內存管理源碼分析 - 頁框分配器中有簡單地描述了高端內存區,在內核的虛擬地址空間的高端內存區中又分為三個區,分別是:非連續內存區、永久內核映射區、固定映射區。
- 非連續內存區是為系統硬件中斷處理和內核模塊生產空間一次性准備用的。
- 永久映射區是給系統底層空間分區和硬件及驅動准備的。
- 固定映射區是為用戶配置和應用軟件運行提供可用空間准備的。
在圖中,high_memory是高端內存區(ZONE_HIGHMEM)起始地址,VMALLOC是非連續內存區。
在內核中,永久內核映射區和固定映射區大小一般都為4MB,也就是分別用一個頁表可以囊括其所包含地址范圍,其他都給非連續內存區使用。不過如果物理內存大小小於896MB的情況下,內核並不會生成高端內存區,只會有ZONE_DMA和ZONE_NORMAL兩個區。
我們知道,內核可使用的線性地址就只有1G大小(0xC0000000 ~ 0xFFFFFFFF),而用於ZONE_DMA和ZONE_NORMAL這兩個區的映射已經花掉了896MB的線性地址空間,最后只剩下128MB用於映射高端內存,如果內存大於1G,比如2G的情況下,高端內存區大小就為1152MB,這個128MB大小的線性地址空間是完全不夠直接映射高端內存的,所以對於高端內存的處理,linux並不會直接映射,而是在需要的時候才進行映射,不需要的時候就釋放映射,回收線性地址。
在初始化頁表時,會對永久內核映射區和固定映射區分別進行初始化,但是都不會對他們進行映射處理,只有在需要使用時才會分配。
臨時內核頁表
臨時內核頁全局目錄是在內核編譯過程中靜態初始化的,臨時頁表是由startup_32()匯編函數進行初始化的,這個臨時頁表專門用於系統啟動階段,也就是系統第一個使用的頁表,它只能讓系統尋址0~8MB這段區間的物理內存,之后會被初始化好的完整頁表代替。這個臨時頁表主要的工作就是讓系統能夠在實模式(不開啟分頁)和保護模式(開啟分頁)下都能夠對內存的前8MB進行尋址。也就是將地址0x00000000到0x007fffff這個區間的線性地址和0xC0000000到0xC07fffff這個區間的線性地址映射到物理地址0x00000000到0x007fffff。其實做法也很簡單,就是將臨時內核頁全局目錄的0x0、0x1、0x301、0x302項初始化好就行了。為什么是這幾項,我們簡單說明一下,在實模式下,也就是沒有開啟分頁的情況下,線性地址0x00000000對應的物理地址就是0x00000000,而0x00000000到0x007fffff這個區間的線性地址就包含在頁全局目錄的0x0和0x1項中。同理,0xC0000000到0xC07fffff通過掩碼獲得的頁全局目錄項就是0x301和0x302。
源碼
在閱讀源碼之前,我們必須對一些全局變量進行說明:
- swapper_pg_dir:主內核頁全局目錄指針,cr3寄存器中保存的內核頁全局目錄地址就是從這個變量而來。
- max_pfn:物理內存中最后一個頁框號。
- max_low_pfn:低端內存中最后一個頁框號。
對於頁表的初始化,內核有一個優先級順序,低端內存(物理內存中保留的前1MB) ->低端內存(內核未使用部分) -> 低端內存(內核使用部分) -> 高端內存(固定映射區) -> 高端內存(永久內核映射區)。
首先,對於低端內存區域的頁表初始化和高端內存固定映射區頁表的初始化都集中在init_mem_mapping(void)函數中,這個函數在start_kernel() -> setup_arch()中:
1 void __init init_mem_mapping(void) 2 { 3 unsigned long end; 4 5 /* 設置了page_size_mask全局變量,這個變量決定了系統中有多少種頁框大小(4K,2M,1G) */ 6 /* 1G大小的頁框只存在於64位系統中 7 * 4K大小的頁框是普通的頁框 8 * 2M大小的頁框是32位內核開啟了PAE后可選擇頁大小為2M 9 */ 10 probe_page_size_mask(); 11 12 /* max_pfn 和 max_low_pfn 都是由BIOS提供獲取 */ 13 #ifdef CONFIG_X86_64 14 15 /* 64位沒有高端內存區 */ 16 end = max_pfn << PAGE_SHIFT; 17 #else 18 end = max_low_pfn << PAGE_SHIFT; 19 #endif 20 21 /* end為低端內存(ZONE_MDA和ZONE_NORMAL)的最大頁框號 */ 22 23 /* the ISA range is always mapped regardless of memory holes */ 24 /* 0 ~ 1MB,一般內核啟動時被安裝在1MB開始處 25 * 這里先初始化 0 ~ 1MB的物理地址 26 */ 27 init_memory_mapping(0, ISA_END_ADDRESS); 28 29 30 if (memblock_bottom_up()) { 31 /* 內核啟動階段使用的內存的結束地址,內核啟動時一般使用物理內存 1MB ~ 4MB 的區域 */ 32 unsigned long kernel_end = __pa_symbol(_end); 33 34 /* 先映射 內核結束地址 ~ ZONE_NORMAL結束地址 這塊物理地址區域,如果是64位,則直接初始化到最后的內存頁框,因為64位沒有高端內存區 */ 35 memory_map_bottom_up(kernel_end, end); 36 /* 再映射 1MB ~ 內核結束地址 這塊物理地址區域 */ 37 memory_map_bottom_up(ISA_END_ADDRESS, kernel_end); 38 } else { 39 memory_map_top_down(ISA_END_ADDRESS, end); 40 } 41 42 #ifdef CONFIG_X86_64 43 if (max_pfn > max_low_pfn) { 44 /* can we preseve max_low_pfn ?*/ 45 max_low_pfn = max_pfn; 46 } 47 #else 48 /* 高端內存區的固定映射區的初始化,只初始化好了頁中間目錄項和頁表,頁表項並沒初始化 */ 49 early_ioremap_page_table_range_init(); 50 #endif 51 /* 將初始化好的內核頁全局目錄地址寫入cr3寄存器 */ 52 load_cr3(swapper_pg_dir); 53 /* 刷新tlb,每次修改了頁表都需要刷新一下,有興趣的可以查查為什么 */ 54 __flush_tlb_all(); 55 56 /* 檢查一下是否有問題 */ 57 early_memtest(0, max_pfn_mapped << PAGE_SHIFT); 58 }
在這個函數的注釋中寫得很清楚了,我們先看init_memory_mapping()。
init_memory_mapping(0, ISA_END_ADDRESS)
1 /* 內核將start ~ end 這段物理地址映射到線性地址上,這個函數僅會映射低端內存區(ZONE_DMA和ZONE_NORMAL),線性地址0xC0000000 對應的物理地址是 0x00000000 */ 2 unsigned long __init_refok init_memory_mapping(unsigned long start, 3 unsigned long end) 4 { 5 /* 用於保存內存段信息,每個段的頁框大小不同,可能有4K,2M,1G三種 */ 6 struct map_range mr[NR_RANGE_MR]; 7 unsigned long ret = 0; 8 int nr_range, i; 9 10 pr_info("init_memory_mapping: [mem %#010lx-%#010lx]\n", 11 start, end - 1); 12 13 /* 清空mr */ 14 memset(mr, 0, sizeof(mr)); 15 16 /* 17 * 根據start和end設置mr數組,並返回個數 18 */ 19 nr_range = split_mem_range(mr, 0, start, end); 20 21 /* 遍歷整個mr,將所有內存段的頁框進行映射,就是將頁框地址寫入對應的頁表中,返回的是最后映射的地址 */ 22 for (i = 0; i < nr_range; i++) 23 ret = kernel_physical_mapping_init(mr[i].start, mr[i].end, 24 mr[i].page_size_mask); 25 26 /* 調整頁框映射的設置,和map_range類似,只是map_range是線性地址的映射數據,這里面是頁框映射的數據 */ 27 add_pfn_range_mapped(start >> PAGE_SHIFT, ret >> PAGE_SHIFT); 28 29 /* 返回最后映射的頁框號 */ 30 return ret >> PAGE_SHIFT; 31 }
繼續看split_mem_range()函數
1 /* 這個函數會根據頁的大小(4K,2M,1G)建立不同的內存段,1G大小的頁框只會在64位系統下使用 */ 2 static int __meminit split_mem_range(struct map_range *mr, int nr_range, 3 unsigned long start, 4 unsigned long end) 5 { 6 unsigned long start_pfn, end_pfn, limit_pfn; 7 unsigned long pfn; 8 int i; 9 10 /* 獲取物理地址end的所在頁框號 */ 11 limit_pfn = PFN_DOWN(end); 12 13 /* head if not big page alignment ? */ 14 /* 物理地址start所在頁框,初始化階段此值為0 */ 15 pfn = start_pfn = PFN_DOWN(start); 16 17 /* 這一部分建立了一個頁框大小為4K的內存段(mr) */ 18 #ifdef CONFIG_X86_32 19 /* 20 * PMD_SIZE保存頁中間目錄可映射區域的大小 21 * PAE禁用: 4M 22 * PAE激活: 2M 23 */ 24 if (pfn == 0) 25 /* 如果pfn為0,也就是開始頁框號是0,那結束頁框號就是4M或者2M */ 26 end_pfn = PFN_DOWN(PMD_SIZE); 27 else 28 /* 如果pfn不為0,以pfn開始(包括pfn),向上找到下一個是PMD_SIZE倍數的頁框號 */ 29 end_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE)); 30 31 /* 以下數值都是二進制表示 32 * round_up(x,y): x: 11010010 y: 1000 結果; 11011000 33 * round_up(x,y): x: 11011010 y: 1000 結果: 11100000 34 * 35 * round_down(x,y): x: 11010010 y: 1000 結果: 11010000 36 * round_down(x,1) x: 11011010 y: 1000 結果: 11011000 37 * 38 */ 39 #else /* CONFIG_X86_64 */ 40 /* 以pfn開始(包括pfn),向上找到下一個是PMD_SIZE倍數的頁框號 */ 41 end_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE)); 42 #endif 43 /* 如果結束頁框號超過了end所在頁框號,那就選取end所在頁框號為結束頁框 */ 44 if (end_pfn > limit_pfn) 45 end_pfn = limit_pfn; 46 /* 第一個內存段的頁框大小為一個PMD_SIZE的大小,4M或者2M */ 47 if (start_pfn < end_pfn) { 48 /* 49 * mr[nr_range].start = start_pfn<<PAGE_SHIFT; 50 * mr[nr_range].end = end_pfn<<PAGE_SHIFT; 51 * mr[nr_range].page_size_mask = 0; 52 * nr_range++; 53 */ 54 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0); 55 /* pfn等於結束頁框號,下個區創建時就會以這個pfn作為起始頁框號*/ 56 pfn = end_pfn; 57 } 58 59 /* 第二個區域,創建大小為2M的頁框內存段,32位下2M的頁框只有在PAE開啟的情況下才會有,這個區不是一定會有的(有的條件是 32位系統 && PAE啟動 && 開啟2M大小頁框) */ 60 /* 以pfn開始(包括pfn),向上找到下一個是PMD_SIZE倍數的頁框號,這里的情況結果一般都是 start_pfn = pfn */ 61 start_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE)); 62 63 #ifdef CONFIG_X86_32 64 /* X86_32位下的處理 */ 65 /* 以limit_pfn開始(包括limit_pfn),向下找到上一個是PMD_SIZE倍數的頁框號,這樣就有可能有第三個段,有可能沒有 */ 66 end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE)); 67 68 #else /* CONFIG_X86_64 */ 69 /* X86_64位下的處理 */ 70 /* 以pfn開始(包括pfn),向上找到下一個是PUD_SIZE倍數的頁框號 */ 71 end_pfn = round_up(pfn, PFN_DOWN(PUD_SIZE)); 72 if (end_pfn > round_down(limit_pfn, PFN_DOWN(PMD_SIZE))) 73 end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE)); 74 #endif 75 76 if (start_pfn < end_pfn) { 77 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 78 page_size_mask & (1<<PG_LEVEL_2M)); 79 pfn = end_pfn; 80 } 81 82 /* X64下會建立一個區域頁框大小為1G的,32位下不會有 */ 83 #ifdef CONFIG_X86_64 84 start_pfn = round_up(pfn, PFN_DOWN(PUD_SIZE)); 85 end_pfn = round_down(limit_pfn, PFN_DOWN(PUD_SIZE)); 86 if (start_pfn < end_pfn) { 87 /* 88 * mr[nr_range].start = start_pfn<<PAGE_SHIFT; 89 * mr[nr_range].end = end_pfn<<PAGE_SHIFT; 90 * mr[nr_range].page_size_mask = page_size_mask & ((1<<PG_LEVEL_2M)|(1<<PG_LEVEL_1G))); 91 * nr_range++; 92 */ 93 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 94 page_size_mask & 95 ((1<<PG_LEVEL_2M)|(1<<PG_LEVEL_1G))); 96 pfn = end_pfn; 97 } 98 99 start_pfn = round_up(pfn, PFN_DOWN(PMD_SIZE)); 100 end_pfn = round_down(limit_pfn, PFN_DOWN(PMD_SIZE)); 101 if (start_pfn < end_pfn) { 102 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 103 page_size_mask & (1<<PG_LEVEL_2M)); 104 pfn = end_pfn; 105 } 106 #endif 107 108 /* 將剩余所有的頁框作為一個新的4K大小頁框的內存段 */ 109 start_pfn = pfn; 110 end_pfn = limit_pfn; 111 nr_range = save_mr(mr, nr_range, start_pfn, end_pfn, 0); 112 113 /* 如果使用的是bootmem分配器的情況下會調整一下幾個段的起始頁框和結束頁框 */ 114 if (!after_bootmem) 115 adjust_range_page_size_mask(mr, nr_range); 116 117 /* 將相鄰兩個頁框大小相等的區合並 */ 118 for (i = 0; nr_range > 1 && i < nr_range - 1; i++) { 119 unsigned long old_start; 120 if (mr[i].end != mr[i+1].start || 121 mr[i].page_size_mask != mr[i+1].page_size_mask) 122 continue; 123 124 /* 前一個區的結束頁框等於后一個區的開始頁框,並且區中頁框大小相等的情況下,合並 */ 125 old_start = mr[i].start; 126 memmove(&mr[i], &mr[i+1], 127 (nr_range - 1 - i) * sizeof(struct map_range)); 128 mr[i--].start = old_start; 129 nr_range--; 130 } 131 132 /* 打印信息 */ 133 for (i = 0; i < nr_range; i++) 134 printk(KERN_DEBUG " [mem %#010lx-%#010lx] page %s\n", 135 mr[i].start, mr[i].end - 1, 136 (mr[i].page_size_mask & (1<<PG_LEVEL_1G))?"1G":( 137 (mr[i].page_size_mask & (1<<PG_LEVEL_2M))?"2M":"4k")); 138 139 /* 返回內存段的數量 */ 140 return nr_range; 141 }
可以看出內存段的數量跟init_mem_mapping()函數中的probe_page_size_mask()函數有很大關系,其實簡單說就是將不同大小的頁框分成一段一段的。
將頁框大小以大小分段后,調用了kernel_physical_mapping_init(),這個函數就是用於做映射了,它會直接修改頁表達到映射目的。
1 /* 將內核的物理地址start到end映射到線性地址上,page_size_mask是頁大小,分別有4K,2MB,1G三種大小 2 * start和end都是物理地址 3 */ 4 unsigned long __init 5 kernel_physical_mapping_init(unsigned long start, 6 unsigned long end, 7 unsigned long page_size_mask) 8 { 9 int use_pse = page_size_mask == (1<<PG_LEVEL_2M); 10 unsigned long last_map_addr = end; 11 unsigned long start_pfn, end_pfn; 12 pgd_t *pgd_base = swapper_pg_dir; 13 int pgd_idx, pmd_idx, pte_ofs; 14 unsigned long pfn; 15 pgd_t *pgd; 16 pmd_t *pmd; 17 pte_t *pte; 18 unsigned pages_2m, pages_4k; 19 int mapping_iter; 20 21 /* 根據start獲取其對應的頁框號,由於頁大小為4KB,所以在地址里占用12位,其余的就是頁框號了,這里就是start右移12位 */ 22 start_pfn = start >> PAGE_SHIFT; 23 /* 根據end獲取其對應的頁框號 */ 24 end_pfn = end >> PAGE_SHIFT; 25 26 /* 設置為1,表示此時是第一次迭代。在這個函數中需要進行兩次迭代,這兩次迭代不同的就是設置的表項屬性不同 */ 27 mapping_iter = 1; 28 29 if (!cpu_has_pse) 30 use_pse = 0; 31 32 repeat: 33 pages_2m = pages_4k = 0; 34 35 /* 等於start地址對應的頁框號 */ 36 pfn = start_pfn; 37 /* 根據頁框號pfn獲取此頁框在頁全局目錄(pgd)項中的偏移量(pgd_idx),注意后面加了個PAGE_OFFSET(0xC0000000),所以這就會讓線性地址0xC0000000與物理地址0x00000000相應 */ 38 pgd_idx = pgd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 39 /* 指向該頁全局目錄項,如果pfn為0 */ 40 pgd = pgd_base + pgd_idx; 41 42 /* 43 * 這里會從pgd的第pgd_idx項向后遍歷所有的頁全局目錄項,直到頁框號pfn大於end_pfn為止 44 * 這里就會將start到end這段線性地址中所有頁框對應的頁表項都遍歷了一遍 45 */ 46 for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) { 47 48 /* 根據頁全局目錄項獲取頁中間目錄所在頁地址 */ 49 pmd = one_md_table_init(pgd); 50 51 if (pfn >= end_pfn) 52 continue; 53 #ifdef CONFIG_X86_PAE 54 /* 根據頁框對應的線性地址獲取相應的頁中間目錄項pmd偏移量 */ 55 pmd_idx = pmd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 56 pmd += pmd_idx; 57 #else 58 /* 在32位未開啟PAE的情況下,pmd是空的 */ 59 pmd_idx = 0; 60 #endif 61 /* PTRS_PER_PMD代表一個頁中間目錄有多少項,對於沒有啟動物理地址擴展的32系統下,其項數為1,其他情況下為512項 */ 62 for (; pmd_idx < PTRS_PER_PMD && pfn < end_pfn; 63 pmd++, pmd_idx++) { 64 /* 獲取頁框號pfn對應的物理地址 */ 65 unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET; 66 67 /* 如果使用了PSE,則頁框大小會變成4MB,但是這里卻是用pages_2m來保存,2MB大小的頁框應該是PAE技術使用的,並不是PSE,這里不太明白,可能PAE代替了PSE */ 68 if (use_pse) { 69 70 unsigned int addr2; 71 72 /* prot設置為PAGE_KERNEL_LARGE,這個值只有在第二次迭代時才會有效 */ 73 pgprot_t prot = PAGE_KERNEL_LARGE; 74 75 /* init_prot是第一次迭代時會設置到對應的頁表項中 */ 76 pgprot_t init_prot = 77 __pgprot(PTE_IDENT_ATTR | 78 _PAGE_PSE); 79 80 pfn &= PMD_MASK >> PAGE_SHIFT; 81 addr2 = (pfn + PTRS_PER_PTE-1) * PAGE_SIZE + 82 PAGE_OFFSET + PAGE_SIZE-1; 83 84 /* 檢查地址是否處於內核啟動所占用的內存區域 */ 85 if (is_kernel_text(addr) || 86 is_kernel_text(addr2)) 87 prot = PAGE_KERNEL_LARGE_EXEC; 88 89 /* 2MB大小的頁框計數器 */ 90 pages_2m++; 91 92 /* 設置頁表項為此頁框,pfn_pte直接將頁框號(pfn)強制轉換為物理地址,而我們也知道pfn有start屏蔽頁大小占用的位數得來,這里也就實現了直接映射(0xC0000000 映射到 0x00000000) */ 93 /* 注意第一次迭代傳入的是init_prot,第二次是prot */ 94 if (mapping_iter == 1) 95 set_pmd(pmd, pfn_pmd(pfn, init_prot)); 96 else 97 set_pmd(pmd, pfn_pmd(pfn, prot)); 98 99 pfn += PTRS_PER_PTE; 100 continue; 101 } 102 103 /* 104 * 以下是建立普通大小的頁表(4K) 105 */ 106 107 /* 根據頁中間目錄項獲取頁表 */ 108 pte = one_page_table_init(pmd); 109 110 /* 根據頁框號獲取頁表項的偏移量 */ 111 pte_ofs = pte_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 112 /* 根據頁表和頁表項的偏移量獲取到該pfn對應的頁框的頁表項 */ 113 pte += pte_ofs; 114 for (; pte_ofs < PTRS_PER_PTE && pfn < end_pfn; 115 pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) { 116 /* 遍歷此此頁表中當前頁表項及其之后的所有頁表項 */ 117 118 /* pgprot_t是一個64位(PAE開啟)或32位(PAE禁止)的數據類型,表示這個頁的保護標志 */ 119 /* 這個值會在第二次迭代時設置到頁框號對應的頁表項中 */ 120 pgprot_t prot = PAGE_KERNEL; 121 122 /* 初始化這個頁的pgprot_t,這個是第一遍迭代時會設置到頁表項中 */ 123 pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR); 124 125 /* 如果該頁框保存着系統的代碼,則設置其標志PAGE_KERNEL_EXEC */ 126 if (is_kernel_text(addr)) 127 prot = PAGE_KERNEL_EXEC; 128 129 /* 4KB大小的頁框計數器 */ 130 pages_4k++; 131 132 if (mapping_iter == 1) { 133 /* 設置頁表項為此頁框,pfn_pte直接將頁框號(pfn)強制轉換為物理地址,而我們也知道pfn有start屏蔽頁大小占用的位數得來,這里也就實現了直接映射(0xC0000000 映射到 0x00000000) */ 134 set_pte(pte, pfn_pte(pfn, init_prot)); 135 /* last_map_addr保存最近映射的地址,就是我們剛映射完的頁框地址 */ 136 last_map_addr = (pfn << PAGE_SHIFT) + PAGE_SIZE; 137 } else 138 /* 第二次迭代時調用到,傳入prot */ 139 set_pte(pte, pfn_pte(pfn, prot)); 140 } 141 } 142 } 143 if (mapping_iter == 1) { 144 /* direct_pages_count[PG_LEVEL_2M] += pages_2m; 這里是做個統計 */ 145 update_page_count(PG_LEVEL_2M, pages_2m); 146 /* direct_pages_count[PG_LEVEL_4K] += pages_4k; 這里也是做個統計 */ 147 update_page_count(PG_LEVEL_4K, pages_4k); 148 149 /* 刷新一下tlb,內核頁表改變了都要刷新一次tlb */ 150 __flush_tlb_all(); 151 152 /* 准備開始第二次迭代,第一次迭代設置到頁表項中的pgprot_t為init_prot變量,第二次迭代設置到頁表項中的是prot變量,它們的值是不同的 */ 153 mapping_iter = 2; 154 /* 開始第二次迭代 */ 155 goto repeat; 156 } 157 /* 最后一次映射的地址(物理地址) */ 158 return last_map_addr; 159 }
看完這里,應該很清楚低端內存是如何直接映射的了。
回到init_mem_mapping()函數,上面分析的init_memory_mapping(0, ISA_END_ADDRESS)函數只映射了0MB ~ 1MB的物理內存,之后在memory_map_bottom_up()中映射低端內存區中剩余的其他物理內存,首先先映射內核結束地址 ~ ZONE_NORMAL最后一個頁框所在地址,然后再映射1MB ~ 內核結束地址。如果沒有高端內存的情況下,則直接一次映射1MB ~ 結束地址。這個過程結束后,所有低端內存區就已經直接映射完畢了。
我們主要看看memory_map_bottom_up()的實現。
memory_map_bottom_up(map_start, map_end)
1 /* 將物理地址map_start ~ map_end 映射到內核區域 */ 2 static void __init memory_map_bottom_up(unsigned long map_start, 3 unsigned long map_end) 4 { 5 unsigned long next, new_mapped_ram_size, start; 6 unsigned long mapped_ram_size = 0; 7 /* step_size need to be small so pgt_buf from BRK could cover it */ 8 unsigned long step_size = PMD_SIZE; 9 10 start = map_start; 11 /* 開始頁框號 */ 12 min_pfn_mapped = start >> PAGE_SHIFT; 13 14 while (start < map_end) { 15 if (map_end - start > step_size) { 16 17 /* 向上找到下一個step_size倍數的頁框號 */ 18 next = round_up(start + 1, step_size); 19 if (next > map_end) 20 next = map_end; 21 } else 22 next = map_end; 23 /* 內核將 start ~ next 這段物理地址經過修正后映射到線性地址上,最后返回映射的大小 */ 24 new_mapped_ram_size = init_range_memory_mapping(start, next); 25 /* 下一個setp_size倍數的頁框號 */ 26 start = next; 27 28 /* 映射成功后,new_mapped_ram_size必定會大於mapped_ram_size(這個初始化是0),會將setp_size << 5,也就是下次一次會映射更多的頁框 */ 29 if (new_mapped_ram_size > mapped_ram_size) 30 step_size = get_new_step_size(step_size); 31 /* 統計已映射內存大小 */ 32 mapped_ram_size += new_mapped_ram_size; 33 } 34 }
核心在init_range_memory_mapping()中
1 /* 內核將start ~ end 這段物理地址映射到線性地址上 */ 2 static unsigned long __init init_range_memory_mapping( 3 unsigned long r_start, 4 unsigned long r_end) 5 { 6 unsigned long start_pfn, end_pfn; 7 unsigned long mapped_ram_size = 0; 8 int i; 9 10 /* 遍歷每一個結點的頁框段,與memblock_region和NUMA有關,還沒研究 */ 11 for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, NULL) { 12 /* start_pfn, r_start, r_end中處於中間的那個數 */ 13 u64 start = clamp_val(PFN_PHYS(start_pfn), r_start, r_end); 14 /* 同上 */ 15 u64 end = clamp_val(PFN_PHYS(end_pfn), r_start, r_end); 16 if (start >= end) 17 continue; 18 19 /* 20 * if it is overlapping with brk pgt, we need to 21 * alloc pgt buf from memblock instead. 22 */ 23 can_use_brk_pgt = max(start, (u64)pgt_buf_end<<PAGE_SHIFT) >= 24 min(end, (u64)pgt_buf_top<<PAGE_SHIFT); 25 /* 又調用到init_memory_mapping,將start ~ end 這段物理地址映射到線性地址上 */ 26 init_memory_mapping(start, end); 27 mapped_ram_size += end - start; 28 can_use_brk_pgt = true; 29 } 30 31 return mapped_ram_size; 32 }
最后又是調用到init_memory_mapping函數進行頁表的修改,這里就不再次說明了。
整個memory_map_bottom_up()函數也說明完了,其實最后也是調用到init_memory_mapping()進行頁表的修改,到這里,整個低端內存的頁表初始化相信也沒什么大的問題了。
高端內存(ZONE_HIGHMEM)固定映射區頁表初始化
固定映射區的頁表初始化也是在init_mem_mapping(void)函數中進行,它的初始化是在低端內存區初始化結束之后,調用early_ioremap_page_table_range_init()進行初始化的。具體看看:
1 /* 固定映射區的初始化,只初始化好了頁中間目錄項和頁表,頁表項並沒初始化 */ 2 void __init early_ioremap_page_table_range_init(void) 3 { 4 pgd_t *pgd_base = swapper_pg_dir; 5 unsigned long vaddr, end; 6 7 /* 固定映射區開始地址 */ 8 vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK; 9 /* 固定映射區結束地址 */ 10 end = (FIXADDR_TOP + PMD_SIZE - 1) & PMD_MASK; 11 /* 初始化內核的頁全局目錄中vaddr到end這個范圍的線性地址 */ 12 page_table_range_init(vaddr, end, pgd_base); 13 /* 重新啟動一下固定映射區 */ 14 early_ioremap_reset(); 15 }
page_table_range_init(vaddr, end, pgd_base)
核心函數,初始化對應的頁表,但是頁表項並不會初始化。
/* 初始化pgd_base指向的頁全局目錄中start到end這個范圍的線性地址,整個函數結束后只是初始化好了頁中間目錄項對應的頁表,但是頁表中的頁表項並沒有初始化 */ 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; pte_t *pte = NULL; /* 計算start到end這段線性地址區域所使用的頁表數,見后面 */ unsigned long count = page_table_range_init_count(start, end); void *adr = NULL; /* 為這些頁表分配連續物理頁框 */ if (count) adr = alloc_low_pages(count); vaddr = start; /* 找到vaddr線性地址對應的頁全局目錄中的偏移量 */ pgd_idx = pgd_index(vaddr); /* 找到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++) { /* 初始化整個頁中間目錄項和頁表,必要時會為不存在的頁表分配頁框,不過頁表初始化后是空的,具體見后面 */ pte = page_table_kmap_check(one_page_table_init(pmd), pmd, vaddr, pte, &adr); vaddr += PMD_SIZE; } pmd_idx = 0; } }
/* page_table_range_init_count */ /* 計算start到end這段線性地址區域所使用的頁表數 */ static unsigned long __init page_table_range_init_count(unsigned long start, unsigned long end) { unsigned long count = 0; #ifdef CONFIG_HIGHMEM int pmd_idx_kmap_begin = fix_to_virt(FIX_KMAP_END) >> PMD_SHIFT; int pmd_idx_kmap_end = fix_to_virt(FIX_KMAP_BEGIN) >> PMD_SHIFT; int pgd_idx, pmd_idx; unsigned long vaddr; if (pmd_idx_kmap_begin == pmd_idx_kmap_end) return 0; vaddr = start; /* 根據線性地址vaddr,計算該地址所對應的頁全局目錄表項的偏移量 */ pgd_idx = pgd_index(vaddr); /* 計算使用的頁表數量 */ for ( ; (pgd_idx < PTRS_PER_PGD) && (vaddr != end); pgd_idx++) { for (; (pmd_idx < PTRS_PER_PMD) && (vaddr != end); pmd_idx++) { if ((vaddr >> PMD_SHIFT) >= pmd_idx_kmap_begin && (vaddr >> PMD_SHIFT) <= pmd_idx_kmap_end) count++; vaddr += PMD_SIZE; } pmd_idx = 0; } #endif return count; }
/* one_md_table_init */ /* 根據頁全局目錄項獲取第一個頁中間目錄所在頁地址,注意頁上級目錄(pud)在32位和一些64位下是為空的, */ static pmd_t * __init one_md_table_init(pgd_t *pgd) { pud_t *pud; pmd_t *pmd_table; #ifdef CONFIG_X86_PAE /* 32位下開啟了PAE的情況,頁上級目錄是空的,但是頁中間目錄需要存在 */ if (!(pgd_val(*pgd) & _PAGE_PRESENT)) { /* 如果該頁中間目錄不存在,這里會分配一個頁框用於這個頁中間目錄 */ pmd_table = (pmd_t *)alloc_low_page(); paravirt_alloc_pmd(&init_mm, __pa(pmd_table) >> PAGE_SHIFT); /* 設置頁全局目錄項的值為此新的頁中間目錄 */ set_pgd(pgd, __pgd(__pa(pmd_table) | _PAGE_PRESENT)); /* 檢查是否設置成功,成功的情況下pud中獲取的第一個pmd應該等於pmd_table */ pud = pud_offset(pgd, 0); BUG_ON(pmd_table != pmd_offset(pud, 0)); return pmd_table; } #endif /* 獲取第一個頁上級目錄 */ pud = pud_offset(pgd, 0); /* 獲取頁上級目錄中第一個頁中間目錄 */ pmd_table = pmd_offset(pud, 0); return pmd_table; }
/* page_table_kmap_check */ /* pte: 頁表,頁中間目錄項pmd對應的頁表 * pmd: 頁中間目錄項 * vaddr: 需要檢查的線性地址 * lastpte: 上一個pte * adr: 連續頁框 */ static pte_t *__init page_table_kmap_check(pte_t *pte, pmd_t *pmd, unsigned long vaddr, pte_t *lastpte, void **adr) { #ifdef CONFIG_HIGHMEM /* * Something (early fixmap) may already have put a pte * page here, which causes the page table allocation * to become nonlinear. Attempt to fix it, and if it * is still nonlinear then we have to bug. */ /* 獲取固定映射區域開始地址在頁中間目錄中的偏移量 */ int pmd_idx_kmap_begin = fix_to_virt(FIX_KMAP_END) >> PMD_SHIFT; /* 獲取固定映射區域結束地址在頁中間目錄中的偏移量 */ int pmd_idx_kmap_end = fix_to_virt(FIX_KMAP_BEGIN) >> PMD_SHIFT; if (pmd_idx_kmap_begin != pmd_idx_kmap_end && (vaddr >> PMD_SHIFT) >= pmd_idx_kmap_begin && (vaddr >> PMD_SHIFT) <= pmd_idx_kmap_end) { pte_t *newpte; int i; /* 這個函數需要在釋放掉bootmem分配器后使用 */ BUG_ON(after_bootmem); newpte = *adr; /* 將頁表復制到adr的頁框中 */ for (i = 0; i < PTRS_PER_PTE; i++) set_pte(newpte + i, pte[i]); /* adr指向下一個頁框 */ *adr = (void *)(((unsigned long)(*adr)) + PAGE_SIZE); paravirt_alloc_pte(&init_mm, __pa(newpte) >> PAGE_SHIFT); /* 修改頁中間目錄項pmd讓其對應的頁表為newpte */ set_pmd(pmd, __pmd(__pa(newpte)|_PAGE_TABLE)); BUG_ON(newpte != pte_offset_kernel(pmd, 0)); /* 刷新tlb */ __flush_tlb_all(); /* 釋放掉pte對應的頁表 */ paravirt_release_pte(__pa(pte) >> PAGE_SHIFT); pte = newpte; } BUG_ON(vaddr < fix_to_virt(FIX_KMAP_BEGIN - 1) && vaddr > fix_to_virt(FIX_KMAP_END) && lastpte && lastpte + PTRS_PER_PTE != pte); #endif /* 返回初始化好的頁表 */ return pte; }
到這里整個固定映射區也初始化完成了。
高端內存永久內核映射區
這塊區域是最后初始化的,而非連續內存區是在分配過程中進行初始化的。其整個過程與固定映射區初始化類似,最后也調用了page_table_range_init()進行初始化。
永久內核映射區初始化代碼在native_pagetable_init() -> paging_init() -> pagetable_init() -> permanent_kmaps_init()中
/* 在pagetable_init中調用,pgd_base的地址是swapper_pg_dir,也就是頁全局目錄地址 * 這個函數初始化了高端內存區中的永久內核映射區,這個區只需要一個頁表就可以概括整個區的線性地址,這個頁表地址保存在 pkmap_page_table 變量中方便使用 */ static void __init permanent_kmaps_init(pgd_t *pgd_base) { unsigned long vaddr; pgd_t *pgd; pud_t *pud; pmd_t *pmd; pte_t *pte; /* kmap/unkmap系統調用是用來映射高端物理內存頁到內核地址空間的api函數 * 他們分配的內核虛擬地址范圍屬於 [PKMAP_BASE,FIXADDR_START],大小是2M或4M的虛擬空間 */ vaddr = PKMAP_BASE; page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base); pgd = swapper_pg_dir + pgd_index(vaddr); pud = pud_offset(pgd, vaddr); pmd = pmd_offset(pud, vaddr); pte = pte_offset_kernel(pmd, vaddr); /* pkmap_page_table保存了頁表地址,之后如果用到永久內核映射區就很方便 */ pkmap_page_table = pte; }