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中删除。