内存管理 | 内存初始化【转】

转自:https://zhuanlan.zhihu.com/p/355205941

介绍完内存初始化过程中最为重要的一个数据结构后,我们就正式开始跟着代码从start_kernel一步一步了解内存初始化的整个流程。我们再次借用初始化第一章节的代码流程图。

内存管理 | 内存初始化【转】

 

setup_arch

setup_arch是一个特定于体系结构的设置函数。

setup_machine_fdt

内存管理 | 内存初始化【转】

 

void __init setup_arch(char **cmdline_p)
{
    /* 
     * 重要数据结构,内核通过machine_desc结构来控制系统体系架构相关部分的初始化 
     * machine_desc机构提的成员包含了体系架构相关部分的几个最重要的初始化函数
     * 包括map_io、init_irq、init_machine、phys_io、timer等
     */
    const struct machine_desc *mdesc;
    ...
    mdesc = setup_machine_fdt(__atags_pointer);
    ...
}

setup_machine_fdt函数用于获取内核前期初始化所需的bootargs,cmdline等系统引导参数 。

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
    ...
    early_init_dt_scan_nodes();
    ...
}

void __init early_init_dt_scan_nodes(void)
{
    /* Retrieve various information from the /chosen node */
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

    /* Initialize {size,address}-cells info */
    of_scan_flat_dt(early_init_dt_scan_root, NULL);

    /* Setup memory, calling early_init_dt_add_memory_arch */
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

early_init_dt_scan_nodes函数中通过of_scan_flat_dt函数扫描整个设备树,实际动作是在回调函数中完成的。early_init_dt_scan_chosen是对chosen节点的操作,主要是将节点下的bootargs属性的字符串拷贝到boot_command_line指向的内存中。early_init_dt_scan_root是根据节点的#address-cells属性和#size-cells属性初始化全局变量dt_root_size_size_cells和dt_root_addr_cells,如果没有设置属性的话这里就使用默认值。early_init_dt_scan_memory是对内存的初始化。

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data)
{
    ...
    base = dt_mem_next_cell(dt_root_addr_cells, &reg);
    size = dt_mem_next_cell(dt_root_size_cells, &reg);
    early_init_dt_add_memory_arch(base, size);
}

对于dt_root_addr_cells和dt_root_size_cells的使用,我们可以看出根节点的#address-cells属性和#size-cells属性都是用来描述内存地址和大小的,得到每块内存的起始地址和大小后,再调用early_init_dt_add_memory_arch函数。

void __init early_init_dt_add_memory_arch(u64 base, u64 size)
{
    ...
    /* Add the chunk to the MEMBLOCK list */
    if (add_mem_to_memblock) {
        if (validate_mem_limit(base, &size))
            memblock_add(base, size);
    }
}

在比较完内核对地址和大小的一系列要求后,最后调用memblock_add将内存块加入内存。

setup_dma_zone

setup_dma_zone传递的参数是mdesc,根据mdesc->dma_zone_size设置DMA区域的大小arm_dma_zone_size和DMA区域的结束地址arm_dma_limit。

adjust_lowmem_bounds

void __init adjust_lowmem_bounds(void)
{
    ...
    vmalloc_limit = (u64)(uintptr_t)vmalloc_min - PAGE_OFFSET + PHYS_OFFSET;

    /*
     * The first usable region must be PMD aligned. Mark its start
     * as MEMBLOCK_NOMAP if it isn't
     * 四级页表结构,PMD对应中间页目录
     */
    for_each_memblock(memory, reg) {
        if (!memblock_is_nomap(reg)) {
            if (!IS_ALIGNED(reg->base, PMD_SIZE)) {
                phys_addr_t len;

                len = round_up(reg->base, PMD_SIZE) - reg->base;
                memblock_mark_nomap(reg->base, len);
            }
            break;
        }
    }

    for_each_memblock(memory, reg) {
        phys_addr_t block_start = reg->base;
        phys_addr_t block_end = reg->base + reg->size;

        if (memblock_is_nomap(reg))
            continue;

        if (reg->base < vmalloc_limit) {
            if (block_end > lowmem_limit)

                lowmem_limit = min_t(u64,
                             vmalloc_limit,
                             block_end);
             /* 
              * 找到第一个非pmd对齐的页面,然后将memblock_limit指向该页面。 
              * 这取决于将限制向下舍入为pmd对齐,此限制发生在此函数的末尾。 
              * 使用此算法,几乎任何存储体的开始或结束都可以不按PMD对齐。 
              * 唯一的例外是存储体0的开始必须是部分对齐的,
              * 因为否则在映射存储体0的开始时就需要分配内存,
              * 这是在映射任何可用内存之前发生的。
              */
            if (!memblock_limit) {
                if (!IS_ALIGNED(block_start, PMD_SIZE))
                    memblock_limit = block_start;
                else if (!IS_ALIGNED(block_end, PMD_SIZE))
                    memblock_limit = lowmem_limit;
            }

        }
    }

    arm_lowmem_limit = lowmem_limit;

    high_memory = __va(arm_lowmem_limit - 1) + 1;

    if (!memblock_limit)
        memblock_limit = arm_lowmem_limit;
    ...
}

adjust_lowmem_bounds负责为lowmem/highmem设置边界。它需要先进行系统设置,然后才能进行内存块保留。在进行内存块保留时,也可以从系统中删除内存。低内存/高内存边界和内存尾部可能会受到删除操作的影响,但删除后内存并未重新计算。虽然在某些系统上,这种情况是无害的,而在其他系统上,这可能导致将错误的范围传递给主内存分配器。所以在完成所有保留后,需要通过重新计算lowmem/highmem边界来更正此问题。这也就是为什么adjust_lowmem_bounds函数会被调用两次。

arm_memblock_init

void __init arm_memblock_init(const struct machine_desc *mdesc)
{
    /* Register the kernel text, kernel data and initrd with memblock. */
    /* 预留内核镜像内存,其中包括.text,.data,.init */
    memblock_reserve(__pa(KERNEL_START), KERNEL_END - KERNEL_START);

    arm_initrd_init();

    /* 
     * 预留vector page内存
     * 如果CPU支持向量重定向(控制寄存器的V位),则CPU中断向量被映射到这里。
     */
    arm_mm_memblock_reserve();

    /* reserve any platform specific memblock areas */
    if (mdesc->reserve)
        mdesc->reserve();  //预留架构相关的内存,这里包括内存屏障和安全ram

    early_init_fdt_reserve_self();  //预留设备树自身加载所占内存
    early_init_fdt_scan_reserved_mem();  //初始化设备树扫描reserved-memory节点预留内存

    /* reserve memory for DMA contiguous allocations */
    dma_contiguous_reserve(arm_dma_limit);  //内核配置参数或命令行参数中预留的DMA连续内存

    arm_memblock_steal_permitted = false;
    memblock_dump_all();
}

由arm_memblock_init函数可以看出设置保留内存的4种方法:

  1. machine的reserve接口中设置:mdesc->reserve();
  2. 设备树reserved-memory节点中设置;
  3. 配置文件:Device Drivers> Generic Driver Options> DMA Contiguous Memory Allocator;
  4. 启动参数添加字段 mem=size;

paging_init

void __init paging_init(const struct machine_desc *mdesc)
{
    void *zero_page;

    /* 
     * prepare_page_table()
     * 在内存使用之前,需要首先清理页表信息。
     * 在prepare_page_table函数中对三段地址使用pmd_clear来清理一级页表的内容
     * 0~MODULES_VADDR,MODULES_VADDR~PAGE_OFFSET,arm_lowmem_limit~VMALLOC_START
     */
    prepare_page_table();
    /*
     * map_lowmem()
     * 将lowmem部分的一级页表即PGD页表填充初始化。
     * 1MB对齐部分的物理内存会被初始化PGD中,不足1MB的会通过PTE来映射。
     * 对此,boot阶段初始化的页表就被覆盖了。
     */
    map_lowmem();
    memblock_set_current_limit(arm_lowmem_limit);
    dma_contiguous_remap();
    early_fixmap_shutdown();
    devicemaps_init(mdesc);
    kmap_init();
    tcm_init();

    top_pmd = pmd_off_k(0xffff0000);

    /* allocate the zero page. */
    zero_page = early_alloc(PAGE_SIZE);

    bootmem_init();

    empty_zero_page = virt_to_page(zero_page);
    __flush_dcache_page(NULL, empty_zero_page);

    /* Compute the virt/idmap offset, mostly for the sake of KVM */
    kimage_voffset = (unsigned long)&kimage_voffset - virt_to_idmap(&kimage_voffset);
}

paging_init主要完成初始化内核的分页机制,通过对boot阶段页表的覆盖,并填充新的一级页表,这样我们的虚拟内存空间就初步建立,并可以完成物理地址到虚拟地址的映射工作了。

在paging_init中最为重要的函数要数bootmem_init(),接下来我们来详细介绍一下bootmem_init。

void __init bootmem_init(void)
{
    unsigned long min, max_low, max_high;

    memblock_allow_resize();
    max_low = max_high = 0;

    /* 通过find_linits找出物理内存开始帧号、结束帧号和NORMAL区域的结束帧号 */
    find_limits(&min, &max_low, &max_high);

    early_memtest((phys_addr_t)min << PAGE_SHIFT,
              (phys_addr_t)max_low << PAGE_SHIFT);

    /*
     * Sparsemem tries to allocate bootmem in memory_present(),
     * so must be done after the fixed reservations
     */
     /* 
      * 遍历所有memory region,每个memory region分成1G大小的section,并设置section在位
      * 函数中调用memory_present函数:
      *     sparse_index_init(section,nid):
      *     遍历所有的section,为其分配“struct mem_section”实例,需要注意
      *     1.如果memory region不是按照section对齐的,那么最后一个section会有空洞,即没有对应的物理页
      *     2.SECTIONS_PER_ROOT即一个物理页面可以存放多少“struct mem_section”实例,由于目前内存是按照物理页面来管理的,
      *       所以一次会分配一个物理页面来存放“struct mem_section”实例,称为一个ROOT,
      *       如果“struct mem_section”实例很多的话,可能需要分配多个物理页面。
      *     ms->section_mem_map = sparse_encode_early_nid(nid) | SECTION_MARKED_PRESENT:
      *     用来设置section在位
      */

    arm_memory_present();

    /*
     * sparse_init() needs the bootmem allocator up and running.
     */
     /* 初始化section机制
      * 初始化mem_section数组使之与每一个section映射 
      * 初始化section_mem_map与page映射
      */
    sparse_init();

    /*
     * Now free the memory - free_area_init_node needs
     * the sparse mem_map arrays initialized by sparse_init()
     * for memmap_init_zone(), otherwise all PFNs are invalid.
     */
    zone_sizes_init(min, max_low, max_high);

    /*
     * This doesn't seem to be used by the Linux memory manager any
     * more, but is used by ll_rw_block.  If we can get rid of it, we
     * also get rid of some of the stuff above as well.
     */
    min_low_pfn = min;
    max_low_pfn = max_low;
    max_pfn = max_high;
}

bootmem_init函数中提到了一个新的机制——Sparsemem Memory Model。之前在基础知识中我们学过,内存的最基本单位是page,但在Sparse Memory模型中,section是管理内存online/offline的最小内存单元。在添加此模型的补丁中,作者描述了该模型的几大优势:

  1. 对于有内存空洞的设备,减少系统struct page使用的内存
  2. 内存热插拔系统需要使用
  3. 在NUMA系统上支持内存的重叠

section就是几个page组合而成,比page更大一些的内存区域,但又比node的范围要小。这样整个系统的物理内存就被分成一个个section,并由mem_section结构体表示。而这个结构体中保存了该section范围的struct page结构体的地址。

bootmem_init函数中另一个重要的函数是zone_sizes_init, 先看以下zone_sizes_init的函数调用图:

内存管理 | 内存初始化【转】

 

  1. calculate_node_totalpages:从名字就可以看出这个函数是用来统计node结点中的页面数的,而统计方法如图所示:

内存管理 | 内存初始化【转】

 

  1. free_area_init_core:主要完成struct pglist_data结构中的字段函数初始化,比如初始化pglist_data内部使用的锁和队列,并初始化它所管理的各个zone。

build_all_zonelists

pagin_init完成了分页机制的初始化,然后bootmem_init完成了内存结点和内存域的初始化工作,此时,数据结构已经基本准备完毕,之后要做的就是将所有节点的内存域都链入到zonelists中,方便后面内存分配的工作。

build_all_zonelists中将大部分内存相关的工作都交给了__build_all_zonelists,后者又对系统中的各个NUMA结点分别调用了build_zonelists。

static void __build_all_zonelists(void *data)
{
    int nid;
    int __maybe_unused cpu;
    pg_data_t *self = data;
    static DEFINE_SPINLOCK(lock);

    spin_lock(&lock);

#ifdef CONFIG_NUMA
    memset(node_load, 0, sizeof(node_load));
#endif

    /*
     * This node is hotadded and no memory is yet present.   So just
     * building zonelists is fine - no need to touch other nodes.
     */
    if (self && !node_online(self->node_id)) {
        build_zonelists(self);
    } else {
        for_each_online_node(nid) {
            pg_data_t *pgdat = NODE_DATA(nid);

            build_zonelists(pgdat);
        }
        ...
    }

    spin_unlock(&lock);
}

for_each_online_node遍历了系统中所有的活动结点。如果是UMA系统只有一个结点,build_zonelists只调用了一次,就对所有的内存创建了内存域列表。NUMA系统调用该函数的次数等于结点的个数,每次调用都会对一个不同的结点生成内存域数据。build_zonelists传入的参数是一个指向pgdat_t实例的指针参数,该数据结构包含了结点的所有信息。由于UMA和NUMA架构下结点的层次结构有很大的区别,因此,内核分别提供了两套不同的build_zonelists接口。但大体实现方法都是通过for循环遍历所有结点并加入到zonelists中。

参考资料

 

上一篇:简述下8种常见SQL错误用法?


下一篇:ASP.NET Core - 在ActionFilter中使用依赖注入