2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_9614673.html

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"
  1. 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中。

阅读全文