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, "[email protected]") != 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"或者[email protected]和我们毫无关系,立刻返回。

(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, "[email protected]") != 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的数组,我们就知道了操作系统需要管理的所有系统内存的布局情况。

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

时间: 2024-11-06 09:41:01

Linux内存初始化(三) 内存布局的相关文章

linux文件系统 - 初始化(三)

一.目的 内核加载完initrd文件后,为挂载磁盘文件系统做好了必要的准备工作,包括挂载了sysfs.proc文件系统,加载了磁盘驱动程序驱动程序等.接下来,内核跳转到用户空间的init程序,由init完成创建磁盘设备文件.加载磁盘文件系统.从rootfs切换到磁盘根文件系统等工作. 由于在不同的linux发行版中,init的实现方式差异很大,不能将所有的发行版都分析一遍,因此本文选取ubuntu12.04发行版来描述如何从rootfs切换到磁盘根文件系统. 二.创建磁盘设备文件 init程序使

Linux内存初始化(一)

一.前言 一直以来,我都非常着迷于两种电影拍摄手法:一种是慢镜头,将每一个细节全方位的展现给观众.另外一种就是快镜头,多半是反应一个时代的变迁,从非常长的时间段中,截取几个典型的snapshot,合成在十几秒的镜头中,可以让观众很快的了解一个事物的发展脉络.对应到技术层面,慢镜头有点类似情景分析,把每一行代码都详细的进行解析,了解技术的细节.快镜头类似数据流分析,勾勒一个过程中,数据结构的演化.本文采用了快镜头的方法,对内存初始化部分进行描述,不纠缠于具体函数的代码实现,只是希望能给大家一个概略

Linux内存初始化(二)identity mapping和kernel image mapping

一.前言 本文没有什么框架性的东西,就是按照__create_page_tables代码的执行路径走读一遍,记录在初始化阶段,内核是如何创建内核运行需要的页表过程.想要了解一些概述性的.框架性的东西可以参考内存初始化文档. 本文的代码来自ARM64,内核版本是4.4.6,此外,阅读本文最好熟悉ARMv8中翻译表描述符的格式. 二.create_table_entry 这个宏定义主要是用来创建一个中间level的translation table中的描述符.如果用linux的术语,就是创建PGD.

[转帖]linux下CPU、内存、IO、网络的压力测试,硬盘读写速度测试,Linux三个系统资源监控工具

linux下CPU.内存.IO.网络的压力测试,硬盘读写速度测试,Linux三个系统资源监控工具 https://blog.51cto.com/hao360/1587165 linux_python关注0人评论57974人阅读2014-12-06 20:17:16 一.对CPU进行简单测试: 1.通过bc命令计算特别函数 例:计算圆周率 echo "scale=5000; 4*a(1)" | bc -l -q MATH LIBRARY        If bc is invoked w

[转载]linux段页式内存管理技术

原始博客地址: http://blog.csdn.net/qq_26626709/article/details/52742470 一.概述 1.虚拟地址空间 内存是通过指针寻址的,因而CPU的字长决定了CPU所能管理的地址空间的大小,该地址空间就被称为虚拟地址空间,因此32位CPU的虚拟地址空间大小为4G,这和实际的物理内存数量无关.Linux内核将虚拟地址空间分成了两部分: 一部分是用户进程可用的,这部分地址是地址空间的低地址部分,从0到TASK_SIZE,称为用户空间 一部分是由内核保留使

Linux与JVM的内存关系分析(转)

引言 在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约600m,Linux自身使用大约800m.从表面上,物理内存应该是足够使用的:但实际运行的情况是,会发生大量使用SWAP(说明物理内存不够使用了),如下图所示.同时,由于SWAP和GC同时发生会致使JVM严重卡顿,所以我们要追问:内存究竟去哪儿了? 要分析这个问题,理解JVM和操作系统之间的内存关系非常重要.接下来主要就Linux与JVM之间的内存关系进行一

转: 关于Linux与JVM的内存关系分析

转自: http://tech.meituan.com/linux-jvm-memory.html Linux与JVM的内存关系分析 葛吒2014-08-29 10:00 引言 在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约600m,Linux自身使用大约800m.从表面上,物理内存应该是足够使用的:但实际运行的情况是,会发生大量使用SWAP(说明物理内存不够使用了),如下图所示.同时,由于SWAP和GC同时

转: Linux与JVM的内存关系分析

Linux与JVM的内存关系分析 引言 在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约600m,Linux自身使用大约800m.从表面上,物理内存应该是足够使用的:但实际运行的情况是,会发生大量使用SWAP(说明物理内存不够使用了),如下图所示.同时,由于SWAP和GC同时发生会致使JVM严重卡顿,所以我们要追问:内存究竟去哪儿了? 要分析这个问题,理解JVM和操作系统之间的内存关系非常重要.接下来主要就Li

Linux与JVM的内存关系分析

引言 在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约600m,Linux自身使用大约800m.从表面上,物理内存应该是足够使用的:但实际运行的情况是,会发生大量使用SWAP(说明物理内存不够使用了),如下图所示.同时,由于SWAP和GC同时发生会致使JVM严重卡顿,所以我们要追问:内存究竟去哪儿了? 要分析这个问题,理解JVM和操作系统之间的内存关系非常重要.接下来主要就Linux与JVM之间的内存关系进行一