1. 前言
在 Linux内核源码分析之setup_arch (二) 中介绍了当前启动阶段的内存分配函数memblock_alloc,该内存分配函数在本篇将要介绍paging_init中用于页表和内存的分配,paging_init函数大致流程如下图所示。
2. paging_init
2.1 build_mem_type_table
该函数根据具体的CPU架构对静态定义的mem_types数组中定义的属性进行调整。
2.2 prepare_page_table
该函数的作用是把页目录项清零,源码大致如下。首先是把虚拟地址范围[0, MODULES_VADDR]的页目录项清零,如果内核是模块区域以XIP方式运行的,则跳过内核部分的页目录,然后继续对区域[addr,PAGE_OFFSET]的页目录项清零,此时用户空间的页目录项已经全部清零;最后,把除了第一块内存条之外的内核空间[__phys_to_virt(end), VMALLOC_START]对应的页目录项清零。
/* arch/arm/mm/mmu.c */static inline void prepare_page_table(void){unsigned long addr;phys_addr_t end;/* <--(1)--> */for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE)pmd_clear(pmd_off_k(addr));#ifdef CONFIG_XIP_KERNELaddr = ((unsigned long)_etext + PMD_SIZE - 1) & PMD_MASK;#endif/* <--(2)--> */for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE)pmd_clear(pmd_off_k(addr));end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;if (end >= lowmem_limit)end = lowmem_limit;/* <--(3)--> */for (addr = __phys_to_virt(end);addr < VMALLOC_START; addr += PMD_SIZE)pmd_clear(pmd_off_k(addr));}
2.3 map_lowmem
该函数将物理内存地址小于lowmem_limit的内存映射到内核空间,实际的内存映射工作在create_mapping中完成。
/* arch/arm/mm/mmu.c */static void __init map_lowmem(void){...for_each_memblock(memory, reg) {start = reg->base;end = start + reg->size;if (end > lowmem_limit)end = lowmem_limit;if (start >= end)break;map.pfn = __phys_to_pfn(start);map.virtual = __phys_to_virt(start);map.length = end - start;map.type = MT_MEMORY;create_mapping(&map, false);}}
create_mapping函数的大致流程如下图所示,这里需要提一下,linux内核使用的是四级页表,即PGD、PUD、PMD、PTE;而ARM32使用的是二级页表,即PMD、PTE。同时由于内存管理是以页为单位进行的,如果按照ARM硬件MMU的分页机制,一个PMD对应的PTE并不能完全占用完整个页,为了避免内存浪费,会在软件层面上将两个PMD对应的PTE放在一个页内,具体细节可以参考文件arch/arm/include/asm/pgtable-2level.h中的注释部分。最终会调用alloc_init_pte函数对指定范围的内存区域进行映射,其中的early_pte_alloc函数最终也会去调用 Linux内核源码分析之setup_arch (二) 中介绍的memblock_alloc函数来分配内存,最后将PTE所在页写入到PMD中即可完成映射。
/* arch/arm/mm/mmu.c */static void __init alloc_init_pte(...){pte_t *start_pte = early_pte_alloc(pmd);pte_t *pte = start_pte + pte_index(addr);do {set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);pfn++;} while (pte++, addr += PAGE_SIZE, addr != end);early_pte_install(pmd, start_pte, type->prot_l1);}
2.4 devicemaps_init
该函数大致流程如下图所示,首先调用early_alloc分配一个页,然后调用early_trap_init将向量表复制到新的页内,最后调用create_mapping将这个页映射到0xffff0000处,如果mdesc->map_io存在,还会对设备相关的IO进行映射。
2.5 kmap_init
这个函数非常简单,把大小为2MB的区间[PKMAP_BASE,PAGE_OFFSET]映射到内核空间。
/* arch/arm/mm/mmu.c */static void __init kmap_init(void){#ifdef CONFIG_HIGHMEMpkmap_page_table = early_pte_alloc_and_install(pmd_off_k(PKMAP_BASE),PKMAP_BASE, _PAGE_KERNEL_TABLE);#endif}
3. 总结
本文主要介绍了内核启动阶段页表初始化部分的内容,其中,build_mem_type_table负责根据不同CPU架构对mem_types进行调整,prepare_page_table负责将待初始化区域的页目录项清零,然后通过map_lowmem建立低端内存区域的页表映射,最后调用devicemaps_init建立对向量表和设备IO的映射。至此,除了bootmem_init函数没有分析之外,paging_init基本算是分析完了,bootmem_init的分析将在下一篇中给出。