引导内存分配器原理


引导内存分配器原理

内存分配node ,每个 node 有多个zone,每个zone 有很多个page 组成。

bootmem分配器

在内核初始化的过程中需要分配内存,内核提供临时的引导内存分配器,在页分配器和块分配器初始化完成之后,把空闲的物理页交给页分配器关联,丢弃引导内存分配器。

  1. bootmem 分配器定义的数据结构,内核源码如下
typedef struct bootmem_data {
    unsigned long node_min_pfn;//起始物理页号
    unsigned long node_low_pfn;//结束物理页号
    void *  node_bootmem_map;//指向一个位图,每一个物理页对应一位,如果物理页被分配,把对应的位置设置为1
    unsigned  long last_end_off;//上次分配的内存块的结束为止后面一个字节的偏移
  unsigned long hint_idx;//是上次分配的内存块的结束为止后面物理页位置中索引下次优先考虑从这个物理页开始分配
  struct list_head list;//用来创建双向链表
} bootmem_data_t;
  1. 每个内存节点有一个bootmem_data 实例
struct bootmem_data;
typedef struct pglist_data {
 struct zone node_zones[MAX_NR_ZONES];//内存区域数组
    struct zonelist node_zonelists[MAX_ZONELISTS];//备用区域列表
    int nr_zones;//该节点包含的内存区域数量
#ifdef CONFIG_FLAT_NODE_MEM_MAP // 除了稀疏内存模型以外
    struct page *node_mem_map ;//页描述符数组
#ifdef CONFIG_PAGE_EXTENSION
    struct page_ext *node_page_ext;//页的拓展属性
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
    struct bootmem_data *bdata;//引导bootmem分配器
#endif
}
  1. bootmem 分配器的算法
    1. 只把低端内存添加到bootmem分配器,低端内存是可以直接映射到内核虚拟地址空间的物理内存;
    2. 使用一个位图记录那些物理页被分配,如果物理页被分配,把这个物理页对应的位设置为1;
    3. 采用最先适配算法,扫描位图,找到第一个足够大的空闲内存块
    4. 为了支持分配小于一页的内存块,记录上次分配的内存块的结束为止后面一个字节的偏移和后面一页的索引,下次分配时,从上次分配的位置后面开始。如果上次分配的最后一个物理页剩余空间足够,可以直接在这个物理页上分配内存。
    5. bootmem分配器对外提供分配内存函数alloc_bootmem ,释放内存函数是free_bootmem.ARM 64架构不使用bootmem分配器,但是其他处理器架构还在使用bootmem分配器
  2. memblock 分配器数据结构

    物理内存类型和内存类型区别:内存类型时物理内存类型的子集,在引导内核时可以使用内核参数mem = nn[KMG],指定可用内存的大小,导致内核不能看见所有的内存,物理内在类型总是包含所有内存范围,内存类型只包含内核参数’mem=’,指定可用的内在范围。

    //memblock分配器使用的数据结构源码:
    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
    }
    //内存块类型的数据结构
    struct memblock_type {
        unsigned long cnt; //当前管理集合中记录的内存区域个数
        unsigned long max; //当前管理集合记录的内存区域的最大个数,最大值是INIT_PHYSMEM_REGIONS
        phys_addr_t total_size;//所有内存块区域的总长度
        struct memblock_region *regions;//执行内存区域结构的指针
        char *name;//内存块类型的名称

    }
    //内存块的数据结构
    enum {
        // 表示没有特殊要求的区域
        MEMBLOCK_NONE = 0x0; //普通
      // 表示可以热插拔的区域,即在系统运行过程中可以插入和拔出的物理内存
        MEMBLOCK_HOTPLUG = 0x1;//热插拔
     // 表示镜像的区域,将内存数据做两个复制,分配放在在主内存和镜像内存中
        MEMBLOCK_MIRROR = 0x2;
     // 表示不添加到内核直接映射区域(即线性映射区域)
        MEMBLOCK_NOMAP = 0x4;
    }
    struct memblock_region {
        phys_addr_t base; //起始的物理地址
        phys_addr_t size; //长度
        unsigned long flags; //成员flags 标志
    #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
        int nid;//节点编号
    #endif
    }
  1. ARM64内核初始化memblock 分配器流程

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

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

    1. 解析设备树二进制文件的节点/memory,把所有物理内存范围添加到memblock。
    2. 在函数arm64_memblock_init 中初始化memblock。
    3. 解析设备树二进制文件节点

      fdt_enforce_memory_region();

    4. 全局变量memstart_addr记录内存的起始物理地址。

      memstart_addr = round_down(memblock_start_of_DRAM(),ARM64_MEMSTART_ALIGN);

    5. 把线性映射区域不能覆盖的物理内存范围从memblock删除。

      memblock_remove(max_t(u64,memstart_addr + linear_region_size,__pa_symbol(end)),ULLONG_MAX);//

    f.

    //设备树二进制文件中字节指定的命令行中,可以使用mem指定可用内存大小
    //如果指定内存的大小,那么把超过可用长度的物理内存范围从memblock中删除。
    if(memory_limit != (phys_addr_t)UULONG_MAX) {
        memblock_mem_limit_remove_map(memory_limit);
        memblock_add(__pa_symbol(_text),(u64)(_end - _text));
    }
   g. 把内核镜像占用物理内存范围添加到memblock 中,


memblock_reserve(__pa_symbol(_text),_end - _text);


 h. 从设备树二进制文件中的内存保留区域,对应设备源文件字段和节点。


early_init_fdt_scn_reserved_mem();


1. memblock分配器编程接口


    memblock_add:添加新的内存块区域到memblock.memory中;
    memblock_remove:删除内存块区域;
    memblock_alloc:分配内存;
    memblock_free:释放内存;


    1. 插入一块可用的物理内存,其中base 指向要添加内存块的起始物理地址;size指向要添加内存块的大小。


        int __init_memblock memblock_add(phys_addr_t base,phys_addr_t size){
            phys_addr_t end = base + size -1;
            memblock_dbg("memblock_add:[%pa-%pa] %pF\n",&base,&end,(void *)_RET_IP_);
            //函数直接调用memblock_add_range() 函数将内存区块添加到memblock.memory
            return memblock_add_range(&memblock.memory,base,size,MAX_NUMNODES,0);
        }


    2. type 指向内存区,由上面调用的函数,这里指向预留内存区,base 指向新加入的内存块的基础地址;size 用来指向新加入的内核块的长度;nid指向NUMA,flags 指向新加入内存块对应的flags


        int __init_memblock memblock_add_range(struct memblock_type *type,phys_addr_t base,phys_addr_t size,int nid,unsigned long flags)


    3. 遍历该内存区内的所有内存区块,每遍历到一个内存区块,函数会将新的内存区块和该内存区块进行比较。


        for_each_memblock_type(type,rgn)
        /* insert the remaining portion*/
        if(base < end) {
            nr_new++;
            /*
                type 指向内存区
                index 指向内存区链表索引
                base 指向内存区块的基地址
                size 指向内存区块的长度
                nid  指向NUMA
                flags 指向内存区块标志 
            */
            if(insert)
                memblock_insert_region(type,idx,base,end - base,nid,flags);
        }
        /*
        此参数用于指定有没有新的内存区块需要加入到内存区块链表
        */
        if(!nr_new)
            return 0;


    4. 从可用物理内存区中删除一块可用物理内存

        base 指向需要删除物理内存的起始物理地址;

        size 指向需要删除物理内存的大小;


        int __init_memblock memblock_remove(phys_addr_t base,phys_addr_t size){
            return memblock_remove_range(&memblock.memory,base,size); 
        }


    5. 函数计算处删除物理内存的终止物理地址之后,直接调用下面此函数来删除指定的物理内存渔区

        type: 指明要从哪一块内存区域删除物理内存

        base:指向需要删除物理内存的起始物理地址

        size:指向需要删除物理内存大小


        static int __init_memblock memblock_remove_range(struct memblock_type *type,phys_addr_t base,phys_addr_t size)



    6. 要删除的内存区可能与内存区内的内存块存在重叠部分,对于重叠部分需要调用下面这个函数来删除物理内存区从内存区中独立出来


        ret=memblock_isolate_range(type,base,size,&start_rgn,&end_rgn);
        /*
        然后,记录这些重叠的内存区的索引号,直接调用下面的函数将这些索引对应的内存区域从内存区中删除
        */
        for(i = end_rgn -1; i>= start_rgn; i--)
            memblock_remove_region(type,i);
        return 0;


    7. 从指定地址之前分配物理内存


        phys_addr_t __init memblock_alloc(phys_addr_t size,phys_addr_t align)
        {
            return memblock_alloc_base(size,align,MEMBLOCK_ALLOC_ACCESSIBLE);
        }


    8. size 指明我们要分配物理内存大小

        align 对齐方式

        max_addr 指明最大可分配物理地址


        phys_addr_t __init memblock_alloc_base(phys_addr_t size ,phys_addr_t align,phys_addr_t max_addr)
        {
            phys_addr_t alloc;
            //调用此函数获得一块可用物理内存区块,如果找到物理内存区块的起始地址为0 
            alloc = __memblock_alloc_base(size,align,max_addr);
            if(alloc == 0 )
                panic("ERROR:Failed to allocate %pa bytes below %pa.\n",&size,&max_addr):

            return alloc;
        }

        phys_addr_t __init __memblock_alloc_base(phys_addr_t size,phys_addr_t align,phys_addr_t max_addr)
        {
            return memblock_alloc_base_nid(size,align,max_addr,NUMA_NO_NODE,MEMBLOCK_NONE);
        }
        //分配所需要内存
        /*
        size 指明我们要分配物理内存大小
        align 对齐方式
         max_addr 指明最大可分配物理地址
        */
        static phys_addr_t __init memblock_alloc_base_nid(phys_addr_t size,phys_addr_t align,phys_addr_t max_addr,int nid,ulong flags)
        {
            return memblock_alloc_range_nid(size,align,0,max_addr,nid,flags);
        }
        /*
        size 指明我们要分配物理内存大小
        align 分配区块的对齐大小
         start 分配区域的起始物理地址
         end  分配区域的终止物理地址
         nid  指向NUMA 节点号
         flags 分配内存区块的标志
        */
        static phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size,phys_addr_t align,phys_addr_t start,phys_addr_t end,int nid,ulong flags)



    9. 从预留的内存区域中删除一块预留的内存区块,

        base  指向要删除预留内存的起始物理地址

        size 指向要删除预留内存的大小


        int __init_memblock memblock_free(phys_addr_t base,phys_addr_t size){

            phys_addr_t end = base + size - 1;

            //函数计算出要删除物理内存的终止物理地址
            kmemleak_free_part_phys(base,size);

            return memblock_remove_range(&memblock.reserved,base,size);
        }



    memblock 内存分配器原理

    主要维护两种内存:

    1. 第一种内存是系统可用的物理内存,即系统实际含有的物理内存,其值从DTS中进行配置,通过 uboot实际探测之后,传入到内核
    2. 第二种内存是内核预留到操作系统的内存,这部分内存作为特殊功能使用,不能作为共享内存使用。