5.2.4 第二次启动分页管理
回到init_memory_mapping()函数,之后nr_range次调用kernel_physical_mapping_init(),nr_range为map_range数组的总的元素数,前面总共执行了两次save_mr,所以nr_range为2,分别是0~1<<9和2MB~end>>12的map_range结构,代表4k和2MB两种形式的内存页表。不过为了方便起见,我们暂时忽略2MB页面大小的体系,只分析传统4k页面体系。
238unsigned long __init 239kernel_physical_mapping_init(unsigned long start, 240 unsigned long end, 241 unsigned long page_size_mask) 242{ 243 int use_pse = page_size_mask == (1<<PG_LEVEL_2M); 244 unsigned long last_map_addr = end; 245 unsigned long start_pfn, end_pfn; 246 pgd_t *pgd_base = swapper_pg_dir; 247 int pgd_idx, pmd_idx, pte_ofs; 248 unsigned long pfn; 249 pgd_t *pgd; 250 pmd_t *pmd; 251 pte_t *pte; 252 unsigned pages_2m, pages_4k; 253 int mapping_iter; 254 255 start_pfn = start >> PAGE_SHIFT; 256 end_pfn = end >> PAGE_SHIFT; 257 258 /* 259 * First iteration will setup identity mapping using large/small pages 260 * based on use_pse, with other attributes same as set by 261 * the early code in head_32.S 262 * 263 * Second iteration will setup the appropriate attributes (NX, GLOBAL..) 264 * as desired for the kernel identity mapping. 265 * 266 * This two pass mechanism conforms to the TLB app note which says: 267 * 268 * "Software should not write to a paging-structure entry in a way 269 * that would change, for any linear address, both the page size 270 * and either the page frame or attributes." 271 */ 272 mapping_iter = 1; 273 274 if (!cpu_has_pse) 275 use_pse = 0; 276 277repeat: 278 pages_2m = pages_4k = 0; 279 pfn = start_pfn; 280 pgd_idx = pgd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 281 pgd = pgd_base + pgd_idx; 282 for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) { 283 pmd = one_md_table_init(pgd); 284 285 if (pfn >= end_pfn) 286 continue; 287#ifdef CONFIG_X86_PAE 288 pmd_idx = pmd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 289 pmd += pmd_idx; 290#else 291 pmd_idx = 0; 292#endif 293 for (; pmd_idx < PTRS_PER_PMD && pfn < end_pfn; 294 pmd++, pmd_idx++) { 295 unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET; 296 297 /* 298 * Map with big pages if possible, otherwise 299 * create normal page tables: 300 */ 301 if (use_pse) { 302 unsigned int addr2; 303 pgprot_t prot = PAGE_KERNEL_LARGE; 304 /* 305 * first pass will use the same initial 306 * identity mapping attribute + _PAGE_PSE. 307 */ 308 pgprot_t init_prot = 309 __pgprot(PTE_IDENT_ATTR | 310 _PAGE_PSE); 311 312 addr2 = (pfn + PTRS_PER_PTE-1) * PAGE_SIZE + 313 PAGE_OFFSET + PAGE_SIZE-1; 314 315 if (is_kernel_text(addr) || 316 is_kernel_text(addr2)) 317 prot = PAGE_KERNEL_LARGE_EXEC; 318 319 pages_2m++; 320 if (mapping_iter == 1) 321 set_pmd(pmd, pfn_pmd(pfn, init_prot)); 322 else 323 set_pmd(pmd, pfn_pmd(pfn, prot)); 324 325 pfn += PTRS_PER_PTE; 326 continue; 327 } 328 pte = one_page_table_init(pmd); 329 330 pte_ofs = pte_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET); 331 pte += pte_ofs; 332 for (; pte_ofs < PTRS_PER_PTE && pfn < end_pfn; 333 pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) { 334 pgprot_t prot = PAGE_KERNEL; 335 /* 336 * first pass will use the same initial 337 * identity mapping attribute. 338 */ 339 pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR); 340 341 if (is_kernel_text(addr)) 342 prot = PAGE_KERNEL_EXEC; 343 344 pages_4k++; 345 if (mapping_iter == 1) { 346 set_pte(pte, pfn_pte(pfn, init_prot)); 347 last_map_addr = (pfn << PAGE_SHIFT) + PAGE_SIZE; 348 } else 349 set_pte(pte, pfn_pte(pfn, prot)); 350 } 351 } 352 } 353 if (mapping_iter == 1) { 354 /* 355 * update direct mapping page count only in the first 356 * iteration. 357 */ 358 update_page_count(PG_LEVEL_2M, pages_2m); 359 update_page_count(PG_LEVEL_4K, pages_4k); 360 361 /* 362 * local global flush tlb, which will flush the previous 363 * mappings present in both small and large page TLB's. 364 */ 365 __flush_tlb_all(); 366 367 /* 368 * Second iteration will set the actual desired PTE attributes. 369 */ 370 mapping_iter = 2; 371 goto repeat; 372 } 373 return last_map_addr; 374} |
通过作者的注释, 可以了解到这个函数的作用是把整个物理内存地址都映射到从内核空间的开始地址,即从0xc0000000的整个内核空间中,直到物理内存映射完毕为止。这个函数比较长, 而且用到很多关于内存管理方面的宏定义,理解了这个函数, 就能大概理解内核是如何建立页表的,将这个抽象的模型完全的理解。
函数开始定义了4个变量pgd_t *pgd, pmd_t *pmd, pte_t *pte, pfn;pgd指向一个目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址pfn是页框号被初始为0。注意这个页全局目录地址swapper_pg_dir,在文件arch/x86/kernel/head_32.S第一次定义后,就再也没变过,前面find_early_table_space函数只是为页表分配新的空间,然后在这里为页表项赋值。看到280行,有个pgd_index宏,确定从页全局目录的哪个位置开始设置内核临时页表:
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
我们得到的pgd_idx是768,也就是从虚拟地址0xC0000000处开始映射,定位主内核页全局目录的起始项pgd=768,也是内核要从目录表中第768个表项开始进行设置。从768到1024这个256个表项被linux内核设置成内核目录项,低768个目录项被用户空间使用。pgd = pgd_base + pgd_idx; pgd便指向了第768个表项。
然后函数开始一个循环即开始填充从768到1024这256个目录项的内容。one_md_table_init()函数根据pgd找到指向的pmd表。279行pfn = start_pfn,也就是为0,物理地址从0x0000 0000开始,起始页框号(page frame number)为pfn,因为我们只分析4k的那个map_range结构,这个结构的start是0,end是max_low_pfn<<PAGE_SHIFT,也就是所有能使用页面的最后一个页面的地址。那么传递给kernel_physical_mapping_init的就是这样两个参数再加一个mask。对这样一个范围,0~max_low_pfn,函数282~352行代码就设置页表和页目录的值。
285行的if (pfn >= end_pfn)很关键,前面讲了max_low_pfn代表着整个物理内存一共有多少页框。当pfn大于max_low_pfn的时候,表明内核已经把整个物理内存都映射到了系统空间中, 所以剩下有没被填充的表项就直接忽略了。因为内核已经可以映射整个物理空间了, 没必要继续填充剩下的表项。
紧接293行第2个for循环,在linux的3级映射模型中,是要设置pmd表的, 但在2级映射中忽略, 只循环一次,直接进行页表pte的设置。
315行,is_kernel_text函数根据前面提到的addr来判断addr线性地址是否属于内核代码段,它同样在mm/init.c中定义:
static inline int is_kernel_text(unsigned long addr)
{
if (addr >= PAGE_OFFSET && addr <= (unsigned long)__init_end)
return 1;
return 0;
}
__init_end是个内核符号,咱们很熟悉了,在内核链接的时候生成的,表示内核代码段的终止地址。如果address属于内核代码段, 那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性:
#define _PAGE_KERNEL_EXEC /
(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED)
#define _PAGE_KERNEL /
(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_NX)
283行,直接返回pgd(pud、pmd和pgd指向同一个页目录项,怀疑这句话的同学去看看one_md_table_init函数的内容就知道了);295行计算第pfn个页框对应的内核空间的线性地址;321行填写一个页目录项pmd(pgd),并填写该目录项所对应的页表的所有项;346行为页目录项pmd分配页表pte,将该页表pte的物理地址写入pmd中,并初始化页表pte的每个页表项。最后通过set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,然户在用set_pte宏把页表项值写到页表项里。
其实调用early_ioremap_page_table_range_init()也实现了类似的功能。那么,调用完毕kernel_physical_mapping_init之后,4k和2MB两种分页体系的页全局目录和页表就通过set_pmd和set_pte等咱们熟悉的动作建立完成了,涵盖了物理地址从0到max_low_pfn<<PAGE_SHIFT,接下来使用load_cr3设置存器cr3的值为页全局目录的首地址swapper_pg_dir。
将控制swapper_pg_dir送入控制寄存器cr3. 每当重新设置cr3时, CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分。现在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从这条指令以后就扩大了系统空间中有映射区域的大小,使整个映射覆盖到整个物理内存(高端内存)除外。实际上此时swapper_pg_dir中已经改变的目录项很可能还在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。
最后,init_memory_mapping返回了所映射页的数量值,并将值保存在 max_low_pfn_mapped中。而32位x86体系中,max_pfn_mapped的值也为max_low_pfn_mapped。