一、前言
同样的,本文是内存初始化文章的一份补充文档,希望能够通过这样的一份文档,细致的展示在初始化阶段,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的数组,我们就知道了操作系统需要管理的所有系统内存的布局情况。