Linux内核使用伙伴系统管理内存,那么在伙伴系统之前,内核使通过memblock来管理。在系统启动阶段,使用memblock记录物理内存的使用情况,首先我们知道在内核启动后,对于内存,分成好几块
- 内存中的某些部分使永久分配给内核的,例如代码段和数据段,ramdisk和dtb占用的空间等,是系统内存的一部分,不能被侵占,也不参与内存的分配,称之为静态内存
- GPU/camera/多核共享的内存都需要预留大量连续内存,这部分内存平时不使用,但是必须为各个应用场景预留,这样的内存称之为预留内存
- 内存其余的部分,是需要内核管理的内存,称之为动态内存
那么memblock就是将以上内存按功能划分为若干内存区,使用不同的类型存放在memory和reserved的两个集合中,memory即为动态内存,而resvered包括静态内存等。
1. memblock介绍
memblock的算法实现是,它将所有的状态都保持在一个全局变量__initdata_memblock中,算法的初始化以及内存的申请释放都是在将内存块的状态做变更。那么从数据结构入手,__initdata_memblock是一个memblock结构体,其定义如下:
struct memblock {
bool bottom_up; /* is bottom up direction? */
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 | 表示分配器的分配方式,true表示从低地址向高地址分配,false则相反 |
current_limit | 来表示用来限制alloc的内存申请 |
memory | 表示可用可分配的内存 |
reserved | 表示已经分配出去了的内存 |
memory和reserved是很关键的一个数据结构,memblock算法的内存初始化和申请释放都是围绕着他们展开工作。
往下看看memory和reserved的结构体memblock_type定义:
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid;
#endif
};
struct memblock_type {
unsigned long cnt; /* number of regions */
unsigned long max; /* size of the allocated array */
phys_addr_t total_size; /* size of all regions */
struct memblock_region *regions;
};
定义 | 含义 |
---|---|
cnt | 表示当前状态(memory/reserved)的内存块可用数 |
max | 可支持的最大数 |
total_size | 当前状态(memory/reserved)的空间大小,也就是内存总大小空间 |
regions | 用于保存内存块信息的结构(包括基址、大小和标记等) |
内核中memblock实例,定义了初始值,其定义如下
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, /* empty dummy entry */
.memory.max = INIT_MEMBLOCK_REGIONS,
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1, /* empty dummy entry */
.reserved.max = INIT_MEMBLOCK_REGIONS,
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
.physmem.regions = memblock_physmem_init_regions,
.physmem.cnt = 1, /* empty dummy entry */
.physmem.max = INIT_PHYSMEM_REGIONS,
#endif
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
它初始化了部分成员,其定义如下
- bottom_up.定义了内存申请方式,从高地址向低地址
- current_limit:alloc的内存限制为0xFFFFFFFF
- 同时通过全局定义为memblock的算法管理器中的memory和reserved准备了内存空间
2. 获取物理内存大小
内核是如何知道内存的信息呢,这部分是由bootloader完成,然后使用fdt或者atag等方式传递给内核,然后内核解析其中的memory节点获取物理内存地址和大小,通过DTB获取物理属性,然后解析并添加到memblock子系统中。
memory {
reg = <0x80000000 0x20000000>;
};
根据上面的dts,在start_kernel–>setup_arch–>setup_machine_fdt–>early_init_dt_scan_nodes–>of_scan_flat_dt( 遍历Nodes )–>early_init_dt_scan_memory(初始化单个内存Node),这部分的分析已经在设备树详解(四)kernel的解析W中详细介绍过了,结果是从DTS解析出base size分别是0x80000000 0x20000000,后根据解析出的base/size,调用early_init_dt_add_memory_arch–>memblock_add–>memblock_add_range将解析出的物理内存加入到memblock子系统中,里首先看一下memblock_add_range()的函数实现:
int __init_memblock memblock_add_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size,
int nid, unsigned long flags)
{
bool insert = false;
phys_addr_t obase = base;
phys_addr_t end = base + memblock_cap_size(base, &size);
int idx, nr_new;
struct memblock_region *rgn;
if (!size)
return 0;
/* special case for empty array */
if (type->regions[0].size == 0) { ------------------ (1)
WARN_ON(type->cnt != 1 || type->total_size);
type->regions[0].base = base;
type->regions[0].size = size;
type->regions[0].flags = flags;
memblock_set_region_node(&type->regions[0], nid);
type->total_size = size;
return 0;
}
repeat:
base = obase;
nr_new = 0;
for_each_memblock_type(type, rgn) { ------------------ (2)
phys_addr_t rbase = rgn->base;
phys_addr_t rend = rbase + rgn->size;
if (rbase >= end)
break;
if (rend <= base)
continue;
if (rbase > base) {
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
WARN_ON(nid != memblock_get_region_node(rgn));
#endif
WARN_ON(flags != rgn->flags);
nr_new++;
if (insert)
memblock_insert_region(type, idx++, base,
rbase - base, nid,
flags);
}
/* area below @rend is dealt with, forget about it */
base = min(rend, end);
}
/* insert the remaining portion */
if (base < end) {
nr_new++;
if (insert)
memblock_insert_region(type, idx, base, end - base,
nid, flags);
}
if (!nr_new)
return 0;
/*
* If this was the first round, resize array and repeat for actual
* insertions; otherwise, merge and return.
*/
if (!insert) {
while (type->cnt + nr_new > type->max)
if (memblock_double_array(type, obase, size) < 0) ------------------ (3)
return -ENOMEM;
insert = true;
goto repeat;
} else {
memblock_merge_regions(type); ------------------ (4)
return 0;
}
}
- 如果memblock算法管理内存为空的时候,则将当前空间添加进去就可以了
- 如果memblock算法管理内存不为空,则先检查是否存在内存重叠的情况,如果有的话,则剔除该重叠部分,然后将其余非重叠部分添加进去
- 如果出现region[]数据空间不够的情况,则通过memblock_double_array添加新的region空间即可
- 如果region[]数据空间够的情况,则通过memblock_merge_regions把紧凑的空间合并即可
memblock_add_range函数的功能为将传入参数给定的内存块(以起始物理地址和长度表示)添加到memory或者reserved region中,这两种region 都用数组方式存储数据,在初始阶段会分配一个给定大小的数组,然后在本函数中添加memblock region。所添加的各region数组都以物理地址从低到高的顺序排列。实际测试的打印信息为
- memblock_add将0x00000080000000-0x0000009fffffff内存加入到memblock memory region
- 将内核代码段地址加入到0x00000080200000-0x000000810e8eeb加入到memblock reserve
- 将fdt地址加入到0x00000088000000-0x00000088014303加入到memblock reserve
OF: fdt:Machine model: Freescale i.MX6 ULL 14x14 EVK Board
memblock_add: [0x00000080000000-0x0000009fffffff] flags 0x0 early_init_dt_scan_memory+0xe4/0xf4
memblock_reserve: [0x00000080200000-0x000000810e8eeb] flags 0x0 arm_memblock_init+0x44/0x1b0
memblock_reserve: [0x00000080003000-0x00000080007fff] flags 0x0 arm_memblock_init+0x154/0x1b0
memblock_reserve: [0x00000088000000-0x00000088014303] flags 0x0 early_init_fdt_reserve_self+0x3c/0x44
memblock_reserve: [0x0000008c000000-0x0000009fffffff] flags 0x0 memblock_alloc_range_nid+0x70/0x88
3. 记录系统预留内存
对于系统预留内存,包括静态内存(内核image,ramdisk,fdt等占用空间),以及camera,display等作系统预留的大量连续内存。另外像手机平台,也需要为多核留一些空间,比如为通信核预留的modem取悦,这部分都是永久分配出去。因此正式因为这部分有特殊的用途或者正在使用,不会进入伙伴系统或被memblock分配器再次分配。对于32位的系统,采用arm_memblock_init进行初始化,而对于64采用arm64_memblock_init,我们来看一下这个做了些什么
void __init arm_memblock_init(const struct machine_desc *mdesc)
{
/* Register the kernel text, kernel data and initrd with memblock. */
#ifdef CONFIG_XIP_KERNEL ----------------------(1)
memblock_reserve(__pa(_sdata), _end - _sdata);
#else
memblock_reserve(__pa(_stext), _end - _stext);
#endif
#ifdef CONFIG_BLK_DEV_INITRD ----------------------(2)
/* FDT scan will populate initrd_start */
if (initrd_start && !phys_initrd_size) {
phys_initrd_start = __virt_to_phys(initrd_start);
phys_initrd_size = initrd_end - initrd_start;
}
initrd_start = initrd_end = 0;
if (phys_initrd_size &&
!memblock_is_region_memory(phys_initrd_start, phys_initrd_size)) {
pr_err("INITRD: 0x%08llx+0x%08lx is not a memory region - disabling initrd\n",
(u64)phys_initrd_start, phys_initrd_size);
phys_initrd_start = phys_initrd_size = 0;
}
if (phys_initrd_size &&
memblock_is_region_reserved(phys_initrd_start, phys_initrd_size)) {
pr_err("INITRD: 0x%08llx+0x%08lx overlaps in-use memory region - disabling initrd\n",
(u64)phys_initrd_start, phys_initrd_size);
phys_initrd_start = phys_initrd_size = 0;
}
if (phys_initrd_size) {
memblock_reserve(phys_initrd_start, phys_initrd_size);
/* Now convert initrd to virtual addresses */
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
}
#endif
arm_mm_memblock_reserve(); ----------------------(3)
/* reserve any platform specific memblock areas */
if (mdesc->reserve)
mdesc->reserve();
early_init_fdt_reserve_self(); ----------------------(4)
early_init_fdt_scan_reserved_mem(); ----------------------(5)
/* reserve memory for DMA contiguous allocations */
dma_contiguous_reserve(arm_dma_limit); ----------------------(6)
arm_memblock_steal_permitted = false; ----------------------(7)
memblock_dump_all();
}
- 将内核代码段设置为reserved类型memblock,其中的init段会在free_initmem中返回给内核
- 将内核的initrd段设置为reserved类型memblock
- 将swapper_pg_dir页目录的16K地址空间也设置为reserved类型memblock
- 将DTB本身区域设置为reserved类型memblock
- 将dtb中的reserved-memory区域设置为reserved类型memblock,根据dtb中的memreserve信息, 调用memblock_reserve
- 会预留内存并准备给CMA使用。在有些体系架构下(例如,ARM),需要完成一些体系相关的工作
下面我们对重点看一下early_init_fdt_scan_reserved_mem这个这个函数怎么将dtb的节点属性来完成reserved。
void __init early_init_fdt_scan_reserved_mem(void)
{
int n;
u64 base, size;
if (!initial_boot_params) -------------------(1)
return;
/* Process header /memreserve/ fields */
for (n = 0; ; n++) {
fdt_get_mem_rsv(initial_boot_params, n, &base, &size); -------------------(2)
if (!size)
break;
early_init_dt_reserve_memory_arch(base, size, 0); -------------------(3)
}
of_scan_flat_dt(__fdt_scan_reserved_mem, NULL); -------------------(4)
fdt_init_reserved_mem();
}
- initial_boot_params实际上就是fdt对应的虚拟地址。在early_init_dt_verify中设定的。如果系统中都没有有效的fdt,那么没有什么可以scan的
- 分析fdt中的 /memreserve/ fields ,进行内存的保留
- 保留每一个/memreserve/ fields定义的memory region,底层是通过memblock_reserve接口函数实现的
- 对fdt中的每一个节点调用__fdt_scan_reserved_mem函数,进行reserved-memory节点的扫描,之后调用fdt_init_reserved_mem函数进行内存预留的动作
设备树中标记为reserved的内存保留区域可以通过以下两种方式来配置
- dts开头的位置通过/memreserve字段标记,由于它不属于任何节点,例如这种方式
/dts-v1/;
/memreserve/ 0x00000000 0x00400000;
#include "axm55xx.dtsi"
#include "axm5516-cpus.dtsi"
- reserved-memory节点保留内存,以下为某dts中保留内存节点的一段。由dts可见它可以定义多个保留内存块属性,每个属性包含一块要保留的内存,在初始化reserved region时,可以通过解析dtb中该节点的各个属性,然后填充reserved region。在这个例子中,保留内存属性还包含了no-map标志,它表示这段内存不要放入线性映射区,因此需要从memory region中移除它们,例如这种方式
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
nss@40000000 {
reg = <0x40000000 0x1000000>;
no-map;
};
smem@41000000 {
reg = <0x41000000 0x200000>;
no-map;
};
};
no-map”属性决定向reserved region添加内存区,还是从memory region移除内存区,二者差别在于内核不会给”no-map”属性的内存区建立内存映射,即该内存区不在动态内存管理范围
对于本开发板其memory和reserved如下
[ 0.000000] memory[0x0] [0x00000080000000-0x0000009fffffff], 0x20000000 bytes flags: 0x0
[ 0.000000] reserved[0x0] [0x00000080003000-0x00000080007fff], 0x5000 bytes flags: 0x0
[ 0.000000] reserved[0x1] [0x00000080200000-0x000000810e8eeb], 0xee8eec bytes flags: 0x0
[ 0.000000] reserved[0x2] [0x00000088000000-0x00000088014303], 0x14304 bytes flags: 0x0
[ 0.000000] reserved[0x3] [0x0000008c000000-0x0000009fffffff], 0x14000000 bytes flags: 0x0
4. memblock API
几乎所有的 memblock 相关的 APIs 都在文件:mm/memblock.c 中进行了实现,最为常见的有
/*
* 1. 基本接口
*/
// 向memory区中添加内存区域.
memblock_add(phys_addr_t base, phys_addr_t size)
// 向memory区中删除区域.
memblock_remove(phys_addr_t base, phys_addr_t size)
// 申请内存
memblock_alloc(phys_addr_t size, phys_addr_t align)
// 释放内存
memblock_free(phys_addr_t base, phys_addr_t size)
/*
* 2. 查找 & 遍历
*/
// 在给定的范围内找到未使用的内存
phys_addr_t memblock_find_in_range(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align)
// 反复迭代 memblock
for_each_mem_range(i, type_a, type_b, nid, flags, p_start, p_end, p_nid)
/*
* 3. 获取信息
*/
// 获取内存区域信息
phys_addr_t get_allocated_memblock_memory_regions_info(phys_addr_t *addr);
// 获取预留内存区域信息
phys_addr_t get_allocated_memblock_reserved_regions_info(phys_addr_t *addr);
/*
* 4. 打印
*/
#define memblock_dbg(fmt, ...) \
if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
5. memblock调试
如果需要了解memblock的详细分配流程,可以通过在bootargs中加入“memblock=debug”。
在内核启动后,通过/proc/kmsg查看调试信息。
查看内存地址范围和reserved区域可以通过:
/sys/kernel/debug/memblock/memory
/sys/kernel/debug/memblock/reserved
6. 总结
此时memblock的初始化工作已经基本完成了,memblock管理算法将可用可分配的内存在memblock.memory进行管理,已分配的内存在memblock.reserved进行管理,只要内存加入到reserved里面就表示内存已经被申请了。所以,内存申请的时候,仅把被申请到的内存加入到reserved中,并不会在memblock.memory里面有关的删去操作,对于申请和释放的操作都集中在memblock.reserved中。