首页 > 系统服务 > 详细

Linux内存初始化(三) 内存布局

时间:2018-02-13 23:28:49      阅读:27      评论:0      收藏:0      [点我收藏+]

标签:也会   mat   注册   情况   align   else if   mod   而在   ech   

一、前言

同样的,本文是内存初始化文章的一份补充文档,希望能够通过这样的一份文档,细致的展示在初始化阶段,Linux 4.4.6内核如何从device tree中提取信息,完成内存布局的任务。具体的cpu体系结构选择的是ARM64。

 

二、memory type region的构建

memory type是一个memblock模块(内核初始化阶段的内存管理模块)的术语,memblock将内存块分成两种类型:一种是memory type,另外一种是reserved type,分别用数组来管理系统中的两种类型的memory region。本小节描述的是系统如何在初始化阶段构建memory type的数组。

1、扫描device tree

在完成fdt内存区域的地址映射之后(fixmap_remap_fdt),内核会对fdt进行扫描,以便完成memory type数组的构建。具体代码位于setup_machine_fdt--->early_init_dt_scan--->early_init_dt_scan_nodes中:

void __init early_init_dt_scan_nodes(void)
{
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); ------(1)
    of_scan_flat_dt(early_init_dt_scan_root, NULL); 
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);-------------(2)
}

(1)of_scan_flat_dt函数是用来scan整个device tree,针对每一个node调用callback函数,因此,这里实际上是针对设备树中的每一个节点调用early_init_dt_scan_chosen函数。之所以这么做是因为device tree blob刚刚完成地址映射,还没有展开,我们只能使用这种比较笨的办法。这句代码主要是寻址chosen node,并解析,将相关数据放入到boot_command_line。

(2)概念同上,不过是针对memory node进行scan。

2、传统的命令行参数解析

int __init early_init_dt_scan_chosen(unsigned long node, const char *uname, int depth, void *data)
{
    int l;
    const char *p;

    if (depth != 1 || !data ||
        (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
        return 0; -------------------------------(1)

    early_init_dt_check_for_initrd(node); --------------------(2)

    /* Retrieve command line */
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
        strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE)); ------------(3)


#ifdef CONFIG_CMDLINE
#ifndef CONFIG_CMDLINE_FORCE
    if (!((char *)data)[0])
#endif
        strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif /* CONFIG_CMDLINE */ -----------------------(4)


    return 1;
}

(1)上面我们说过,early_init_dt_scan_chosen会为device tree中的每一个node而调用一次,因此,为了效率,不是chosen node的节点我们必须赶紧闪人。由于chosen node是root node的子节点,因此其depth必须是1。这里depth不是1的节点,节点名字不是"chosen"或者chosen@0和我们毫无关系,立刻返回。

(2)解析chosen node中的initrd的信息

(3)解析chosen node中的bootargs(命令行参数)并将其copy到boot_command_line。

(4)一般而言,内核有可能会定义一个default command line string(CONFIG_CMDLINE),如果bootloader没有通过device tree传递命令行参数过来,那么可以考虑使用default参数。如果系统定义了CONFIG_CMDLINE_FORCE,那么系统强制使用缺省命令行参数,bootloader传递过来的是无效的。

3、memory node解析

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; 
    if (type == NULL) {
        if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
            return 0;
    } else if (strcmp(type, "memory") != 0)
        return 0; -----------------------------(1)

    reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
    if (reg == NULL)
        reg = of_get_flat_dt_prop(node, "reg", &l);---------------(2)
    if (reg == NULL)
        return 0;

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

    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
        u64 base, size;

        base = dt_mem_next_cell(dt_root_addr_cells, ?);
        size = dt_mem_next_cell(dt_root_size_cells, ?); ----------(4)

        early_init_dt_add_memory_arch(base, size);--------------(5)
    }

    return 0;
}

(1)如果该memory node是root node的子节点的话,那么它一定是有device_type属性并且其值是字符串”memory”。不是的话就可以返回了。不过node没有定义device_type属性怎么办?大部分的平台都可以直接返回了,除了PPC32,对于这个平台,如果memory node是更深层次的节点的话,那么它是没有device_type属性的,这时候可以根据node name来判断。当然,目标都是一致的,不是自己关注的node就赶紧闪人。

(2)该memory node的物理地址信息保存在"linux,usable-memory"或者"reg"属性中(reg是我们常用的)

(3)l / sizeof(__be32)是reg属性值的cell数目,reg指向第一个cell,endp指向最后一个cell。

(4)memory node的reg属性值其实就是一个数组,数组中的每一个entry都是base address和size的二元组。解析reg属性需要两个参数,dt_root_addr_cells和dt_root_size_cells,这两个参数分别定义了root节点的子节点(比如说memory node)reg属性中base address和size的cell数目,如果等于1,基地址(或者size)用一个32-bit的cell表示。对于ARMv8,一般dt_root_addr_cells和dt_root_size_cells等于2,表示基地址(或者size)用两个32-bit的cell表示。

注:dt_root_addr_cells和dt_root_size_cells这两个参数的解析在early_init_dt_scan_root中完成。

(5)针对该memory mode中的每一个memory region,调用early_init_dt_add_memory_arch向系统注册memory type的内存区域(实际上是通过memblock_add完成的)。

4、解析memory相关的early option

setup_arch--->parse_early_param函数中会对early options解析解析,这会导致下面代码的执行:

static int __init early_mem(char *p)
{

    memory_limit = memparse(p, &p) & PAGE_MASK;

    return 0;
}
early_param("mem", early_mem);

在过去,没有device tree的时代,mem这个命令行参数传递了memory bank的信息,内核根据这个信息来创建系统内存的初始布局。在ARM64中,由于强制使用device tree,因此mem这个启动参数失去了本来的意义,现在它只是定义了memory的上限(最大的系统内存地址),可以限制DTS传递过来的内存参数。

 

三、reserved type region的构建

保留内存的定义主要在fixmap_remap_fdt和arm64_memblock_init函数中进行,我们会按照代码顺序逐一进行各种各样reserved type的memory region的构建。

1、保留fdt占用的内存,代码如下:

void *__init fixmap_remap_fdt(phys_addr_t dt_phys)
{……

    memblock_reserve(dt_phys, size);

……}

fixmap_remap_fdt主要是为fdt建立地址映射,在该函数的最后,顺便就调用memblock_reserve保留了该段内存。

2、保留内核和initrd占用的内容,代码如下:

void __init arm64_memblock_init(void)
{
    memblock_enforce_memory_limit(memory_limit); ----------------(1)
    memblock_reserve(__pa(_text), _end - _text);------------------(2)
#ifdef CONFIG_BLK_DEV_INITRD
    if (initrd_start)
        memblock_reserve(__virt_to_phys(initrd_start), initrd_end - initrd_start);------(3)
#endif
……}

(1)我们前面解析了DTS的memory节点,已经向系统加入了不少的memory type的region,当然reserved memory block也会有一些,例如DTB对应的memory就是reserved。memory_limit可以对这些DTS的设定给出上限,memblock_enforce_memory_limit函数会根据这个上限,修改各个memory region的base和size,此外还将大于memory_limit的memory block(包括memory type和reserved type)从列表中删掉。

(2)reserve内核代码、数据区等(_text到_end那一段,具体的内容可以参考内核链接脚本)

(3)保留initital ramdisk image区域(从initrd_start到initrd_end区域)

3、通过early_init_fdt_scan_reserved_mem函数来分析dts中的节点,从而进行保留内存的动作,代码如下:

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的,return,走人。

(2)分析fdt中的 /memreserve/ fields ,进行内存的保留。在fdt的header中定义了一组memory reserve参数,其具体的位置是fdt base address + off_mem_rsvmap。off_mem_rsvmap是fdt header中的一个成员,如下:

struct fdt_header {
……
    fdt32_t off_mem_rsvmap;------/memreserve/ fields offset
……};

fdt header中的memreserve可以定义多个,每个都是(address,size)二元组,最后以0,0结束。

(3)保留每一个/memreserve/ fields定义的memory region,底层是通过memblock_reserve接口函数实现的。

(4)对fdt中的每一个节点调用__fdt_scan_reserved_mem函数,进行reserved-memory节点的扫描,之后调用fdt_init_reserved_mem函数进行内存预留的动作,具体参考下一小节描述。

4、解析reserved-memory节点的内存,代码如下:

static int __init __fdt_scan_reserved_mem(unsigned long node, const char *uname,
                      int depth, void *data)
{
    static int found;
    const char *status;
    int err;

    if (!found && depth == 1 && strcmp(uname, "reserved-memory") == 0) { -------(1)
        if (__reserved_mem_check_root(node) != 0) {
            pr_err("Reserved memory: unsupported node format, ignoring\n"); 
            return 1;
        }
        found = 1; ---------------------------------(2)
        return 0;
    } else if (!found) { 
        return 0; ----------------------------------(3)
    } else if (found && depth < 2) { -------------------------(4)
        return 1;
    }

    status = of_get_flat_dt_prop(node, "status", NULL); ----------------(5)
    if (status && strcmp(status, "okay") != 0 && strcmp(status, "ok") != 0)
        return 0;

    err = __reserved_mem_reserve_reg(node, uname); ----------------(6)
    if (err == -ENOENT && of_get_flat_dt_prop(node, "size", NULL))
        fdt_reserved_mem_save_node(node, uname, 0, 0); ---------------(7)

    /* scan next node */
    return 0;
}

(1)found 变量记录了是否搜索到一个reserved-memory节点,如果没有,我们的首要目标是找到一个reserved-memory节点。reserved-memory节点的特点包括:是root node的子节点(depth == 1),node name是"reserved-memory",这可以过滤掉一大票无关节点,从而加快搜索速度。

(2)reserved-memory节点应该包括#address-cells、#size-cells和range属性,并且#address-cells和#size-cells的属性值应该等于根节点对应的属性值,如果检查通过(__reserved_mem_check_root),那么说明找到了一个正确的reserved-memory节点,可以去往下一个节点了。当然,下一个节点往往是reserved-memory节点的subnode,也就是真正的定义各段保留内存的节点。更详细的关于reserved-memory的设备树定义可以参考Documentation\devicetree\bindings\reserved-memory\reserved-memory.txt文件。

(3)没有找到reserved-memory节点之前,of_scan_flat_dt会不断的遍历下一个节点,而在__fdt_scan_reserved_mem函数中返回0表示让搜索继续,如果返回1,表示搜索停止。

(4)如果找到了一个reserved-memory节点,并且完成了对其所有subnode的scan,那么是退出整个reserved memory的scan过程了。

(5)如果定义了status属性,那么要求其值必须要是ok或者okay,当然,你也可以不定义该属性(这是一般的做法)。

(6)定义reserved memory有两种方法,一种是静态定义,也就是定义了reg属性,这时候,可以通过调用__reserved_mem_reserve_reg函数解析reg的(address,size)的二元数组,逐一对每一个定义的memory region进行预留。实际的预留内存动作可以调用memblock_reserve或者memblock_remove,具体调用哪一个是和该节点是否定义no-map属性相关,如果定义了no-map属性,那么说明这段内存操作系统根本不需要进行地址映射,也就是说这块内存是不归操作系统内存管理模块来管理的,而是归于具体的驱动使用(在device tree中,设备节点可以定义memory-region节点来引用在memory node中定义的保留内存,具体可以参考reserved-memory.txt文件)。

(7)另外一种定义reserved memory的方法是动态定义,也就是说定义了该内存区域的size(也可以定义alignment或者alloc-range进一步约定动态分配的reserved memory属性,不过这些属性都是option的),但是不指定具体的基地址,让操作系统自己来分配这段memory。

5、预留reserved-memory节点的内存

device tree中的reserved-memory节点及其子节点静态或者动态定义了若干的reserved memory region,静态定义的memory region起始地址和size都是确定的,因此可以立刻调用memblock的模块进行内存区域的预留,但是对于动态定义的memory region,__fdt_scan_reserved_mem只是将信息保存在了reserved_mem全局变量中,并没有进行实际的内存预留动作,具体的操作在fdt_init_reserved_mem函数中,代码如下:

void __init fdt_init_reserved_mem(void)
{
    int i;

    __rmem_check_for_overlap(); -------------------------(1)

    for (i = 0; i < reserved_mem_count; i++) {--遍历每一个reserved memory region
        struct reserved_mem *rmem = &reserved_mem[i];
        unsigned long node = rmem->fdt_node;
        int len;
        const __be32 *prop;
        int err = 0;

        prop = of_get_flat_dt_prop(node, "phandle", &len);---------------(2)
        if (!prop)
            prop = of_get_flat_dt_prop(node, "linux,phandle", &len);
        if (prop)
            rmem->phandle = of_read_number(prop, len/4);

        if (rmem->size == 0)----------------------------(3)
            err = __reserved_mem_alloc_size(node, rmem->name,
                         &rmem->base, &rmem->size);
        if (err == 0)
            __reserved_mem_init_node(rmem);--------------------(4)
    }
}

(1)检查静态定义的 reserved memory region之间是否有重叠区域,如果有重叠,这里并不会对reserved memory region的base和size进行调整,只是打印出错信息而已。

(2)每一个需要被其他node引用的node都需要定义"phandle", 或者"linux,phandle"。虽然在实际的device tree source中看不到这个属性,实际上dtc会完美的处理这一切的。

(3)size等于0的memory region表示这是一个动态分配region,base address尚未定义,因此我们需要通过__reserved_mem_alloc_size函数对节点进行分析(size、alignment等属性),然后调用memblock的alloc接口函数进行memory block的分配,最终的结果是确定base address和size,并将这段memory region从memory type的数组中移到reserved type的数组中。当然,如果定义了no-map属性,那么这段memory会从系统中之间删除(memory type和reserved type数组中都没有这段memory的定义)。

(4)保留内存有两种使用场景,一种是被特定的驱动使用,这时候在特定驱动的初始化函数(probe函数)中自然会进行处理。还有一种场景就是被所有驱动或者内核模块使用,例如CMA,per-device Coherent DMA的分配等,这时候,我们需要借用device tree的匹配机制进行这段保留内存的初始化动作。有兴趣的话可以看看RESERVEDMEM_OF_DECLARE的定义,这里就不再描述了。

6、通过命令行参数保留CMA内存

arm64_memblock_init--->dma_contiguous_reserve函数中会根据命令行参数进行CMA内存的保留,本文暂不描述之,留给CMA文档吧。

 

四、总结

物理内存布局是归于memblock模块进行管理的,该模块定义了struct memblock memblock这样的一个全局变量保存了memory type和reserved type的memory region list。而通过这两个memory region的数组,我们就知道了操作系统需要管理的所有系统内存的布局情况。

Linux内存初始化(三) 内存布局

标签:也会   mat   注册   情况   align   else if   mod   而在   ech   

原文:https://www.cnblogs.com/alantu2018/p/8447580.html

(0)
(0)
   
举报
评论 一句话评论(0
0条  
登录后才能评论!
© 2014 bubuko.com 版权所有 鲁ICP备09046678号-4
打开技术之扣,分享程序人生!
             

鲁公网安备 37021202000002号