Linux内核源码分析之setup_arch (三)

  • A+
所属分类:linux技术
摘要

在 Linux内核源码分析之setup_arch (二) 中介绍了当前启动阶段的内存分配函数memblock_alloc,该内存分配函数在本篇将要介绍paging_init中用于页表和内存的分配,paging_init函数大致流程如下图所示。


1. 前言

Linux内核源码分析之setup_arch (二) 中介绍了当前启动阶段的内存分配函数memblock_alloc,该内存分配函数在本篇将要介绍paging_init中用于页表和内存的分配,paging_init函数大致流程如下图所示。

Linux内核源码分析之setup_arch (三)

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_KERNEL  addr = ((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中即可完成映射。

Linux内核源码分析之setup_arch (三)
/* 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进行映射。

Linux内核源码分析之setup_arch (三)

2.5 kmap_init

这个函数非常简单,把大小为2MB的区间[PKMAP_BASE,PAGE_OFFSET]映射到内核空间。

/* arch/arm/mm/mmu.c */ static void __init kmap_init(void) { #ifdef CONFIG_HIGHMEM  pkmap_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的分析将在下一篇中给出。