linux内存管理笔记(十)--- memblock分配器

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;
	}
}
  1. 如果memblock算法管理内存为空的时候,则将当前空间添加进去就可以了
  2. 如果memblock算法管理内存不为空,则先检查是否存在内存重叠的情况,如果有的话,则剔除该重叠部分,然后将其余非重叠部分添加进去
  3. 如果出现region[]数据空间不够的情况,则通过memblock_double_array添加新的region空间即可
  4. 如果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();
}
  1. 将内核代码段设置为reserved类型memblock,其中的init段会在free_initmem中返回给内核
  2. 将内核的initrd段设置为reserved类型memblock
  3. 将swapper_pg_dir页目录的16K地址空间也设置为reserved类型memblock
  4. 将DTB本身区域设置为reserved类型memblock
  5. 将dtb中的reserved-memory区域设置为reserved类型memblock,根据dtb中的memreserve信息, 调用memblock_reserve
  6. 会预留内存并准备给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();                                         
} 
  1. initial_boot_params实际上就是fdt对应的虚拟地址。在early_init_dt_verify中设定的。如果系统中都没有有效的fdt,那么没有什么可以scan的
  2. 分析fdt中的 /memreserve/ fields ,进行内存的保留
  3. 保留每一个/memreserve/ fields定义的memory region,底层是通过memblock_reserve接口函数实现的
  4. 对fdt中的每一个节点调用__fdt_scan_reserved_mem函数,进行reserved-memory节点的扫描,之后调用fdt_init_reserved_mem函数进行内存预留的动作

设备树中标记为reserved的内存保留区域可以通过以下两种方式来配置

  1. dts开头的位置通过/memreserve字段标记,由于它不属于任何节点,例如这种方式

    /dts-v1/;
    
    /memreserve/ 0x00000000 0x00400000;
    
    #include "axm55xx.dtsi"
    #include "axm5516-cpus.dtsi"
    
  2. 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中。

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值