虚拟地址空间与物理地址空间完整映射

setup bootmem

物理内存都添加到系统之后,会调用setup_bootmem对整个物理内存进行整理,主要的工作就是remove掉一些no-map区域(不归内核管理),同时保留一些关键区域,如内核镜像区,dtb中reserved的内存节点。

上图中,浅绿色的就是reserved部分,不能被分配使用,而剩下的部分就可以通过调用上小章节中的函数去使用内存了。

void __init setup_bootmem(void)
{
    struct memblock_region *reg;
    phys_addr_t mem_size = 0;
    phys_addr_t vmlinux_end = __pa(&_end);
    phys_addr_t vmlinux_start = __pa(&_start);

    /* Find the memory region containing the kernel */
    for_each_memblock(memory, reg) {
        phys_addr_t end = reg->base + reg->size;

        if (reg->base <= vmlinux_end && vmlinux_end <= end) {
            mem_size = min(reg->size, (phys_addr_t)-PAGE_OFFSET);

            /*
             * Remove memblock from the end of usable area to the
             * end of region
             */
            if (reg->base + mem_size < end)
                memblock_remove(reg->base + mem_size,
                        end - reg->base - mem_size);
        }
    }
    BUG_ON(mem_size == 0);

    /* Reserve from the start of the kernel to the end of the kernel */
    memblock_reserve(vmlinux_start, vmlinux_end - vmlinux_start);
    ①将内核镜像地址空间添加到&memblock.reserved。
    set_max_mapnr(PFN_DOWN(mem_size));
    max_low_pfn = PFN_DOWN(memblock_end_of_DRAM());

#ifdef CONFIG_BLK_DEV_INITRD
    setup_initrd();
#endif /* CONFIG_BLK_DEV_INITRD */

    /*
     * Avoid using early_init_fdt_reserve_self() since __pa() does
     * not work for DTB pointers that are fixmap addresses
     */
    memblock_reserve(dtb_early_pa, fdt_totalsize(dtb_early_va));
    ②将dtb的空间添加到&memblock.reserved
early_init_fdt_scan_reserved_mem();
③遍历设备树的节点,将/memreserve/,reserved-memory节点添加到&memblock.reserved
    memblock_allow_resize();
    memblock_dump_all();

    for_each_memblock(memory, reg) {
        unsigned long start_pfn = memblock_region_memory_base_pfn(reg);
        unsigned long end_pfn = memblock_region_memory_end_pfn(reg);

        memblock_set_node(PFN_PHYS(start_pfn),
                  PFN_PHYS(end_pfn - start_pfn),
                  &memblock.memory, 0);
    }
}  

下面是调用memblock_dump_all的信息:
MEMBLOCK configuration:
memory size = 0x000000000fe00000 reserved size = 0x000000000091288b
memory.cnt  = 0x1
memory[0x0] [0x0000000080200000-0x000000008fffffff], 0x000000000fe00000 bytes flags: 0x0
reserved.cnt  = 0x3
reserved[0x0] [0x0000000080000000-0x000000008003ffff], 0x0000000000040000 bytes flags: 0x0
reserved[0x1] [0x0000000080200000-0x0000000080ad133b], 0x00000000008d133c bytes flags: 0x0
reserved[0x2] [0x0000000082200000-0x000000008220154e], 0x000000000000154f bytes flags: 0x0

小结:
(1)系统通过memblock以数组memory type的方式记录物理内存空间,数组中每一个内存区域描述了一段内存信息,包括base,size,node id等。
(2)在memblock信息中,已经被使用或者被内核定义需要保留的区域,会存储在reserved 数组中。
(3)memory type数组中并不是代表整个内核系统的内存空间,因为部分驱动会保留一段内存区域供自己单独使用,其在dts中具有no-map属性的reserved-memory节点,不会由内核创建地址映射。
(4)可以通过内核调试节点/sys/kernel/debug/memblock进行查询相关信息

完整映射

在前面部分只完成了kernel、dtb,部分临时页表的临时映射,也就是说对于内核来说使能了MMU后也只能访问kernel,dtb,部分临时页表。在前面阶段,内核也只知道kernel,dtb的物理位置,对于其他的内存位置内核是不清楚的。通过memblock机制后,内核系统已经对物理内存布局信息比较清楚了,那么就需要对内存完成最后的映射。主要的工作是,对此前的kernel、dtb等映射重新做一次调整,统一使用swapper_pg_dir根目录页表,同时对于新纳入memblock中的内存建立映射,填充好对应的页表。

static void __init setup_vm_final(void)
{
    uintptr_t va, map_size;
    phys_addr_t pa, start, end;
    struct memblock_region *reg;

    /* Set mmu_enabled flag */
    mmu_enabled = true;

    /* Setup swapper PGD for fixmap */
    create_pgd_mapping(swapper_pg_dir, FIXADDR_START,
               __pa(fixmap_pgd_next),
               PGDIR_SIZE, PAGE_TABLE);
    ① 将fixmap的映射的页表从early_pg_dir切换为swapper_pg_dir。
    /* Map all memory banks */
    for_each_memblock(memory, reg) {
        start = reg->base;
        end = start + reg->size;

        if (start >= end)
            break;
        if (memblock_is_nomap(reg))
            continue;
        if (start <= __pa(PAGE_OFFSET) &&
            __pa(PAGE_OFFSET) < end)
            start = __pa(PAGE_OFFSET);

        map_size = best_map_size(start, end - start);
        for (pa = start; pa < end; pa += map_size) {
            va = (uintptr_t)__va(pa);
            create_pgd_mapping(swapper_pg_dir, va, pa,
                       map_size, PAGE_KERNEL_EXEC);
        }
    }
②遍历memblock中的内存,对所有的内存进行映射,根目录页表为swapper_pg_dir,二级页表和PTE页表会使用memblock_phys_alloc进行分配内存,对已经分配的内存访问会先反向映射到fixmap地址空间,通过fixmap进行访问页表。
    /* Clear fixmap PTE and PMD mappings */
    clear_fixmap(FIX_PTE);
    clear_fixmap(FIX_PMD);

    /* Move to swapper page table */
    csr_write(CSR_SATP, PFN_DOWN(__pa(swapper_pg_dir)) | SATP_MODE);
local_flush_tlb_all();
③将swapper_pg_dir的根目录写到satp寄存器,这样就完成了整个物理内存到虚拟地址空间的映射,所有的物理内存空间都可以进行访问了。
}

到此,内核通过memblock知道物理内存的布局信息,虚拟地址翻译到物理内存所需要的页表都填充好了,所有要访问的物理内存都映射虚拟地址空间,所有的物理内存都可以通过虚拟地址正常访问了。内存下一阶段的初始化会涉及到Node、Zone以及伙伴系统等,这里就先不阐述。