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


    一、前言

    同样的,本文是内存初始化文章的一份补充文档,希望能够通过这样的一份文档,细致的展示在初始化阶段,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 "); 
                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的设备树定义可以参考Documentationdevicetreeindings eserved-memory eserved-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的数组,我们就知道了操作系统需要管理的所有系统内存的布局情况。

  • 相关阅读:
    ios设备new Date('2019-07-26 11:00:00')报错
    vue图片压缩(不失真)
    ios微信端网页点击右上角复制链接或在浏览器打开,得不到当前页地址(动态路由)
    ios打开网页,输入框获取焦点键盘弹起,关闭键盘,界面下方空白不回弹
    vue-cli打包后vendor.js文件太大怎么办
    ios点击有300毫秒延迟,输入框必须重压或长按才能获取焦点唤起软键盘
    ios微信端上下滑动页面,若触摸的是input区域,页面内不滚动,整个页面被拖动了
    vue-cli打包优化之分析工具webpack-bundle-analyzer
    vue引入fastclick设置输入框type="number"报错Failed to execute 'setSelectionRange' on 'HTMLInputElement': The input element's type ('number') does not support selection.的解决办法
    js/vue图片压缩
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8447580.html
Copyright © 2020-2023  润新知