linux之 内存管理(2)-kernel 启动时 内存初始化 memblock

一、内核启动过程中,内存管理的变化

可以把linux内核的内存管理分三个阶段:

阶段起点终点描述
第一阶段系统启动bootmem或者memblock初始化完成此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束
第二阶段bootmem或者memblock初始化完buddy完成前引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束
第三阶段buddy初始化完成系统停止运行可以用cache和buddy分配内存

start_kernel是如何初始化系统的内存,只截取出其中与内存管理初始化相关的部分, 如下所示:

asmlinkage __visible void __init start_kernel(void)
{

    /*  设置特定架构的信息
     *  同时初始化memblock  */
    setup_arch(&command_line);
    mm_init_cpumask(&init_mm);

    setup_per_cpu_areas();

    /*  初始化内存结点和内段区域  */
    build_all_zonelists(NULL, NULL);
    page_alloc_init();


    /*
     * These use large bootmem allocations and must precede
     * mem_init();
     * kmem_cache_init();
     */
    mm_init();

    kmem_cache_init_late();

    kmemleak_init();
    setup_per_cpu_pageset();

    rest_init();
}

 

函数功能
setup_arch是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器
mm_init_cpumask初始化CPU屏蔽字
setup_per_cpu_areas函数(查看定义)给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.在SMP系统中, setup_per_cpu_areas初始化源代码中(使用per_cpu宏)定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本. 此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本 在非SMP系统中这是一个空操作
build_all_zonelists建立并初始化结点和内存域的数据结构
mm_init建立了内核的内存分配器, 其中通过mem_init停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统) 然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器
kmem_cache_init_late在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器slab, slob, slub都会定义此函数
kmemleak_initKmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 /sys/kernel/debug/kmemleak中, Kmemcheck能够帮助定位大多数内存错误的上下文
setup_per_cpu_pageset初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中.

先 介绍 memblock; 

二、获取内存大小

在arm linux下,设备相关属性的描述都是通过dts文件来完成的。linux在启动过程中,会解析这些dts文件,从而获取相关设备属性信息。 下面这段就是dts里很典型的一种描述内存信息的内容:

memory@200000000 {
        device_type = "memory";
        reg = <0x2 0x0 0x2 0x0>;
    };

“memory"节点描述物理内存布局,“@”后面的设备地址用来区分名字相同的节点,如果节点有属性“reg”,那么设备地址必须是属性“reg”的第一个地址。如果有多块内存,可以使用多个“memory”节点来描述,也可以使用一个“memory”节点的属性“reg”的地址/长度列表来描述。

属性“reg”定义物理内存范围,值是一个地址/长度列表。这里前面两个数表示了起始地址,0x2是高32位,紧接着的0表示低32位,所以这里的起始地址就是0x200000000,同理后面的两个数字表示内存大小,大小就是0x200000000,说明内存大小总共是8G。

那么这个解析过程是怎样的呢?这个主要内核代码的early_init_dt_scan_memory函数中, 该函数的调用链为:

start_kernel()->setup_arch()->setup_machine_fdt()->early_init_dt_scan()->early_init_dt_scan_nodes()->early_init_dt_scan_memory()

看一下这个函数:

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,

                     int depth, void *data)

{

    const char *type = of_get_flat_dt_prop(node, "device_type", NULL);

    const __be32 *reg, *endp;

    int l;

    bool hotpluggable;

    if (type == NULL || strcmp(type, "memory") != 0)
        return 0;


    reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);

    if (reg == NULL)
        reg = of_get_flat_dt_prop(node, "reg", &l);

    if (reg == NULL)
        return 0;


    endp = reg + (l / sizeof(__be32));

    hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL);

    pr_debug("memory scan node %s, reg size %d,\n", uname, l);

    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {

        u64 base, size;

        base = dt_mem_next_cell(dt_root_addr_cells, &reg);

        size = dt_mem_next_cell(dt_root_size_cells, &reg);


        if (size == 0)
            continue;

        pr_debug(" - %llx ,  %llx\n", (unsigned long long)base,

            (unsigned long long)size);


        early_init_dt_add_memory_arch(base, size);


        if (!hotpluggable)

            continue;

        if (early_init_dt_mark_hotplug_memory_arch(base, size))

            pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n",

                base, base + size);

    }

    return 0;

}

函数一开始就过滤掉type不是"memory"的属性,然后解析"memory"属性,获取到起始地址和大小,然后加入到memblock系统中。

memblock系统

        可能大家都知道linux内核的内存管理采用的是伙伴系统管理方法,但是是初始化的过程中,也会涉及到内存的分配,此时内核提供了临时的引导内存分配器,在完成初始化之后,把空闲的物理页交给也分配器管理,再丢弃引导内存分配器。目前的内核使用的内核引导分配器就是memblock。所以上面会将memory加入到memblock中。

数据结构

memblock用到的主要数据结构如下:

struct memblock {

    bool bottom_up;  

    phys_addr_t current_limit;

    struct memblock_type memory;

    struct memblock_type reserved;

};

总体结构看,memblock管理了两段区域:memblock.memory和memblock.reserved。所有物理上可用的内存区域都会被添加到memblock.memory。而被分配或者被系统占用的区域则会添加到memblock.reserved。 bottom_up表示分配内存的方式,值为真表示从低地址向上分配,否则从高地址向下分配。 current_limit表示可分配内存的最大物理地址。

memblock_type的结构如下:

enum memblock_flags {

    MEMBLOCK_NONE       = 0x0,  /* No special request */

    MEMBLOCK_HOTPLUG    = 0x1,  /* hotpluggable region */

    MEMBLOCK_MIRROR     = 0x2,  /* mirrored region */

    MEMBLOCK_NOMAP      = 0x4,  /* don't add to kernel direct mapping */

};

struct memblock_region {

    phys_addr_t base;

    phys_addr_t size;

    enum memblock_flags flags;

#ifdef CONFIG_NEED_MULTIPLE_NODES
    int nid;
#endif
};

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是内存块类型的名称。

初始化

setup_arch()->arm64_memblock_init();

arm64的memblock初始化位于arch/arm64/mm/init.c。初始化过程,首先如上节先将物理内存加入到memblock.memory中,然后在函数arm64_memblock_init中初始化memblock。

//start_kernel() ->setup_arch() ->arm64_memblock_init()
void __init arm64_memblock_init(void)

{

    const s64 linear_region_size = BIT(vabits_actual - 1);

    fdt_enforce_memory_region();

    memblock_remove(1ULL << PHYS_MASK_SHIFT, ULLONG_MAX);

    memstart_addr = round_down(memblock_start_of_DRAM(),

                   ARM64_MEMSTART_ALIGN);


    memblock_remove(max_t(u64, memstart_addr + linear_region_size,

            __pa_symbol(_end)), ULLONG_MAX);

    if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {

        /* ensure that memstart_addr remains sufficiently aligned */

        memstart_addr = round_up(memblock_end_of_DRAM() - linear_region_size,

                     ARM64_MEMSTART_ALIGN);

        memblock_remove(0, memstart_addr);

    }


    if (IS_ENABLED(CONFIG_ARM64_VA_BITS_52) && (vabits_actual != 52))

        memstart_addr -= _PAGE_OFFSET(48) - _PAGE_OFFSET(52);


    if (memory_limit != PHYS_ADDR_MAX) {

        memblock_mem_limit_remove_map(memory_limit);

        memblock_add(__pa_symbol(_text), (u64)(_end - _text));

    }



    if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {

        u64 base = phys_initrd_start & PAGE_MASK;

        u64 size = PAGE_ALIGN(phys_initrd_start + phys_initrd_size) - base;


        if (WARN(base < memblock_start_of_DRAM() ||

             base + size > memblock_start_of_DRAM() +

                       linear_region_size,

            "initrd not fully accessible via the linear mapping -- please check your bootloader ...\n")) {

            phys_initrd_size = 0;

        } else {

            memblock_remove(base, size); 

            memblock_add(base, size);

            memblock_reserve(base, size);

        }

    }


    if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {

        extern u16 memstart_offset_seed;

        u64 range = linear_region_size -

                (memblock_end_of_DRAM() - memblock_start_of_DRAM());


        if (memstart_offset_seed > 0 && range >= ARM64_MEMSTART_ALIGN) {

            range /= ARM64_MEMSTART_ALIGN;

            memstart_addr -= ARM64_MEMSTART_ALIGN *

                     ((range * memstart_offset_seed) >> 16);

        }

    }

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

    if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {

        /* the generic initrd code expects virtual addresses */

        initrd_start = __phys_to_virt(phys_initrd_start);

        initrd_end = initrd_start + phys_initrd_size;

    }

    early_init_fdt_scan_reserved_mem();

    if (IS_ENABLED(CONFIG_ZONE_DMA)) {

        zone_dma_bits = ARM64_ZONE_DMA_BITS;

        arm64_dma_phys_limit = max_zone_phys(ARM64_ZONE_DMA_BITS);

    }


    if (IS_ENABLED(CONFIG_ZONE_DMA32))

        arm64_dma32_phys_limit = max_zone_phys(32);

    else

        arm64_dma32_phys_limit = PHYS_MASK + 1;


    reserve_crashkernel();
    reserve_elfcorehdr();

    high_memory = __va(memblock_end_of_DRAM() - 1) + 1;

    dma_contiguous_reserve(arm64_dma32_phys_limit);

}

函数主要做了以下事情:

(1)全局变量memstart_addr记录内存的起始物理地址;

(2)把线性映射区域不能覆盖的物理内存范围从memblock.memory中删除;

(3)dts文件中节点“/chosen”的属性“bootargs”指定的命令行中,可以使用参数“mem”指定可用内存的大小。如果指定了内存的大小,那么把超过可用长度的物理内存范围从memblock.memory中删除;

(4)把内核镜像占用的物理内存范围添加到memblock.reserved中;

(5)从dts文件中的内存保留区域(memory reserve map,对应设备树源文件的字段“/memreserve/”)和节点“/reserved-memory”读取保留的物理内存范围,添加到memblock.reserved中。

API接口

memblock_add

int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)

{
    phys_addr_t end = base + size - 1;
    return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);

}

static int __init_memblock memblock_add_range(struct memblock_type *type,

                phys_addr_t base, phys_addr_t size,

                int nid, enum memblock_flags 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;


repeat:
    base = obase;

    nr_new = 0;

    for_each_memblock_type(idx, type, rgn) {

        phys_addr_t rbase = rgn->base;

        phys_addr_t rend = rbase + rgn->size;


        if (rbase > base) {

            nr_new++;

            if (insert)

                memblock_insert_region(type, idx++, base,

                               rbase - base, nid,

                               flags);

        }

        base = min(rend, end);

    }


    if (base < end) {

        nr_new++;

        if (insert)

            memblock_insert_region(type, idx, base, end - base,

                           nid, flags);

    }


    if (!insert) {

        while (type->cnt + nr_new > type->max)

            if (memblock_double_array(type, obase, size) < 0)

                return -ENOMEM;

        insert = true;

        goto repeat;

    } else {

        memblock_merge_regions(type);

        return 0;

    }

}

注意add的过程是按照区间从小到大的顺序加入的,如果有重叠的情况,会进行合并。

memblock_remove

删除内存块区域

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

static int __init_memblock memblock_remove_range(struct memblock_type *type,
                      phys_addr_t base, phys_addr_t size)
{
    int start_rgn, end_rgn;
    int i, ret;

    ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);
    if (ret)
        return ret;

    for (i = end_rgn - 1; i >= start_rgn; i--)
        memblock_remove_region(type, i);
    return 0;
}

static void __init_memblock memblock_remove_region(struct memblock_type *type, unsigned long r)
{
    type->total_size -= type->regions[r].size;
    memmove(&type->regions[r], &type->regions[r + 1],
        (type->cnt - (r + 1)) * sizeof(type->regions[r]));
    type->cnt--;

    if (type->cnt == 0) {
        WARN_ON(type->total_size != 0);
        type->cnt = 1;
        type->regions[0].base = 0;
        type->regions[0].size = 0;
        type->regions[0].flags = 0;
        memblock_set_region_node(&type->regions[0], MAX_NUMNODES);
    }
}

memblock_alloc

分配主要通过函数memblock_alloc_range_nid完成

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,

                    bool exact_nid)

{

    enum memblock_flags flags = choose_memblock_flags();

    phys_addr_t found;


    found = memblock_find_in_range_node(size, align, start, end, nid,

                        flags);


    if (nid != NUMA_NO_NODE && !exact_nid) {

        found = memblock_find_in_range_node(size, align, start,

                            end, NUMA_NO_NODE,

                            flags);

        if (found && !memblock_reserve(found, size))

            goto done;

    }

}

static phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size,

                    phys_addr_t align, phys_addr_t start,

                    phys_addr_t end, int nid,

                    enum memblock_flags flags)

{

    phys_addr_t kernel_end, ret;



    start = max_t(phys_addr_t, start, PAGE_SIZE);

    end = max(start, end);

    kernel_end = __pa_symbol(_end);

    if (memblock_bottom_up() && end > kernel_end) {

        phys_addr_t bottom_up_start;

        bottom_up_start = max(start, kernel_end);

        ret = __memblock_find_range_bottom_up(bottom_up_start, end,

                              size, align, nid, flags);
    }



    return __memblock_find_range_top_down(start, end, size, align, nid,

                          flags);

}

static phys_addr_t __init_memblock

__memblock_find_range_bottom_up(phys_addr_t start, phys_addr_t end,

                phys_addr_t size, phys_addr_t align, int nid,

                enum memblock_flags flags)

{

    phys_addr_t this_start, this_end, cand;

    u64 i;

    for_each_free_mem_range(i, nid, flags, &this_start, &this_end, NULL) {

        this_start = clamp(this_start, start, end);

        this_end = clamp(this_end, start, end);



        cand = round_up(this_start, align);

        if (cand < this_end && this_end - cand >= size)

            return cand;

    }

    return 0;

}

        函数从高到底遍历memblock.memory的内存块区域数组,针对这个内存区域M1,从高到低遍历memblock.reserved的内存块区域数组,针 对每个内存块区域M2,目标区域是内存块区域M2和前一个内存块区 域之间的区域,如果目标区域属于内存块区域M1,并且长度大于或等 于请求分配的长度,那么可以从目标区域分配内存。 然后,调用函数memblock_reserve,把分配出去的内存块区域添加到memblock.reserved中。

在将内存加入memblock系统后,会首先建立页表,然后再初始化zone。映射这部分在之前页表建立的文章中已经讲过,这里不再涉及。 在分析zone初始化之前,需要先了解下linux物理内存的组织结构。这部分先留待下一篇。

三、memblock的初始化(arm64架构)

start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.

void __init setup_arch(char **cmdline_p)
{
    /*  初始化memblock  */
    arm64_memblock_init( );

    /*  分页机制初始化  */
    paging_init();

    bootmem_init();
}
流程描述
arm64_memblock_init初始化memblock内存分配器
paging_init初始化分页机制
bootmem_init初始化内存管理

其中arm64_memblock_init就完成了arm64架构下的memblock的初始化

arm64_memblock_init(),上面已经分析过了。

paging_init():

第一步是为 swapper_pg_dir 建立映射表,

map_kernel(pgdp) ,内核细粒度映射,对各个内核段进行映射;map_mem(pgdp) 线性映射;

map_mem(pgdp)-> _map_memblock(),会对memblock 建立页表。

paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响。

具体可以参考内存管理(1)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值