Linux内核深度解析
上QQ阅读APP看书,第一时间看更新

3.6.2 memblock分配器

1.数据结构

memblock分配器使用的数据结构如下:

    include/linux/memblock.h
    struct memblock {
          bool bottom_up;  /* 是从下向上的方向? */
          phys_addr_t current_limit;
          struct memblock_type memory;
          struct memblock_type reserved;
    #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
          struct memblock_type physmem;
    #endif
    };

成员bottom_up表示分配内存的方式,值为真表示从低地址向上分配,值为假表示从高地址向下分配。

成员current_limit是可分配内存的最大物理地址。

接下来是3种内存块:memory是内存类型(包括已分配的内存和未分配的内存), reserved是预留类型(已分配的内存), physmem是物理内存类型。物理内存类型和内存类型的区别是:内存类型是物理内存类型的子集,在引导内核时可以使用内核参数“mem=nn[KMG]”指定可用内存的大小,导致内核不能看见所有内存;物理内存类型总是包含所有内存范围,内存类型只包含内核参数“mem=”指定的可用内存范围。


内存块类型的数据结构如下:

    include/linux/memblock.h
    struct memblock_type {
          unsigned long cnt;         /* 区域数量 */
          unsigned long max;         /* 已分配数组的大小 */
          phys_addr_t total_size;    /* 所有区域的长度 */
          struct memblock_region *regions;
          char *name;
    };

内存块类型使用数组存放内存块区域,成员regions指向内存块区域数组,cnt是内存块区域的数量,max是数组的元素个数,total_size是所有内存块区域的总长度,name是内存块类型的名称。


内存块区域的数据结构如下:

    include/linux/memblock.h
    struct memblock_region {
          phys_addr_t base;
          phys_addr_t size;
          unsigned long flags;
    #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
          int nid;
    #endif
    };
    /* memblock标志位的定义.*/
    enum {
        MEMBLOCK_NONE      = 0x0,   /* 无特殊要求 */
        MEMBLOCK_HOTPLUG   = 0x1,   /* 可热插拔区域 */
        MEMBLOCK_MIRROR    = 0x2,   /* 镜像区域 */
        MEMBLOCK_NOMAP     = 0x4,   /* 不添加到内核直接映射 */
    };

成员base是起始物理地址,size是长度,nid是节点编号。成员flags是标志,可以是MEMBLOCK_NONE或其他标志的组合。

(1)MEMBLOCK_NONE表示没有特殊要求的区域。

(2)MEMBLOCK_HOTPLUG表示可以热插拔的区域,即在系统运行过程中可以拔出或插入物理内存。

(3)MEMBLOCK_MIRROR表示镜像的区域。内存镜像是内存冗余技术的一种,工作原理与硬盘的热备份类似,将内存数据做两个复制,分别放在主内存和镜像内存中。

(4)MEMBLOCK_NOMAP表示不添加到内核直接映射区域(即线性映射区域)。

2.初始化

源文件“mm/memblock.c”定义了全局变量memblock,把成员bottom_up初始化为假,表示从高地址向下分配。

ARM64内核初始化memblock分配器的过程是:

(1)解析设备树二进制文件中的节点“/memory”,把所有物理内存范围添加到memblock. memory,具体过程参考3.6.3节。

(2)在函数arm64_memblock_init中初始化memblock。

函数arm64_memblock_init的主要代码如下:

    start_kernel() ->setup_arch() -> arm64_memblock_init()
   arch/arm64/mm/init.c
    1   void __init arm64_memblock_init(void)
    2   {
    3    const s64 linear_region_size = -(s64)PAGE_OFFSET;
    4
    5    fdt_enforce_memory_region();
    6
    7    memstart_addr = round_down(memblock_start_of_DRAM(),
    8                       ARM64_MEMSTART_ALIGN);
    9
    10   memblock_remove(max_t(u64, memstart_addr + linear_region_size,
    11              __pa_symbol(_end)), ULLONG_MAX);
    12   if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {
    13        /* 确保memstart_addr严格对齐 */
    14        memstart_addr = round_up(memblock_end_of_DRAM() - linear_region_size,
    15                         ARM64_MEMSTART_ALIGN);
    16        memblock_remove(0, memstart_addr);
    17   }
    18
    19   if (memory_limit ! = (phys_addr_t)ULLONG_MAX) {
    20        memblock_mem_limit_remove_map(memory_limit);
    21        memblock_add(__pa_symbol(_text), (u64)(_end - _text));
    22   }
    23
    24   …
    25   memblock_reserve(__pa_symbol(_text), _end - _text);
    26   …
    27
    28   early_init_fdt_scan_reserved_mem();
    29   …
    30   }

第5行代码,调用函数fdt_enforce_memory_region解析设备树二进制文件中节点“/chosen”的属性“linux, usable-memory-range”,得到可用内存的范围,把超出这个范围的物理内存范围从memblock.memory中删除。

第7行和第8行代码,全局变量memstart_addr记录内存的起始物理地址。

第10~17行代码,把线性映射区域不能覆盖的物理内存范围从memblock.memory中删除。

第19~22行代码,设备树二进制文件中节点“/chosen”的属性“bootargs”指定的命令行中,可以使用参数“mem”指定可用内存的大小。如果指定了内存的大小,那么把超过可用长度的物理内存范围从memblock.memory中删除。因为内核镜像可以被加载到内存的高地址部分,并且内核镜像必须是可以通过线性映射区域访问的,所以需要把内核镜像占用的物理内存范围重新添加到memblock.memory中。

第25行代码,把内核镜像占用的物理内存范围添加到memblock.reserved中。

第28行代码,从设备树二进制文件中的内存保留区域(memory reserve map,对应设备树源文件的字段“/memreserve/”)和节点“/reserved-memory”读取保留的物理内存范围,添加到memblock.reserved中。

3.编程接口

memblock分配器对外提供的接口如下。

(1)memblock_add:添加新的内存块区域到memblock.memory中。

(2)memblock_remove:删除内存块区域。

(3)memblock_alloc:分配内存。

(4)memblock_free:释放内存。

为了兼容bootmem分配器,memblock分配器也实现了bootmem分配器提供的接口。如果开启配置宏CONFIG_NO_BOOTMEM, memblock分配器就完全替代了bootmem分配器。

4.算法

memblock分配器把所有内存添加到memblock.memory中,把分配出去的内存块添加到memblock.reserved中。内存块类型中的内存块区域数组按起始物理地址从小到大排序。


函数memblock_alloc负责分配内存,把主要工作委托给函数memblock_alloc_range_nid,算法如下。

(1)调用函数memblock_find_in_range_node以找到没有分配的内存块区域,默认从高地址向下分配。

函数memblock_find_in_range_node有两层循环,外层循环从高到低遍历memblock.memory的内存块区域数组;针对每个内存块区域M1,执行内层循环,从高到低遍历memblock.reserved的内存块区域数组。针对每个内存块区域M2,目标区域是内存块区域M2和前一个内存块区域之间的区域,如果目标区域属于内存块区域M1,并且长度大于或等于请求分配的长度,那么可以从目标区域分配内存。

(2)调用函数memblock_reserve,把分配出去的内存块区域添加到memblock.reserved中。


函数memblock_free负责释放内存,只需要把内存块区域从memblock.reserved中删除。