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\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内存初始化(三) 内存布局的更多相关文章
- linux文件系统 - 初始化(三)
执行init程序 一.目的 内核加载完initrd文件后,为挂载磁盘文件系统做好了必要的准备工作,包括挂载了sysfs.proc文件系统,加载了磁盘驱动程序驱动程序等.接下来,内核跳转到用户空间的in ...
- 三星S5-PV210内存初始化
一.S5PV210时钟系统 时钟:一定频率的电信号. 时钟系统:基于CMOS工艺的高性能处理器时钟系统,集成PLL可以从内部触发,比从外部触发更快且更准确,能有效地避免一些与信号完整性相关的问题. ...
- 第二章 Java内存区域与内存溢出异常
运行时数据区域: 程序计数器(Program Counter Register):当前线程执行码行号指示器,属于线程私有内存.字节码解释器工作时就是通过调整这个计数器的值来选取下一条需要执行字节码指令 ...
- Linux内存初始化(四) 创建系统内存地址映射
一.前言 经过内存初始化代码分析(一)和内存初始化代码分析(二)的过渡,我们终于来到了内存初始化的核心部分:paging_init.当然本文不能全部解析完该函数(那需要的篇幅太长了),我们只关注创建系 ...
- Linux内存初始化(一)
一.前言 一直以来,我都非常着迷于两种电影拍摄手法:一种是慢镜头,将每一个细节全方位的展现给观众.另外一种就是快镜头,多半是反应一个时代的变迁,从非常长的时间段中,截取几个典型的snapshot,合成 ...
- Linux进程地址空间 && 进程内存布局[转]
一 进程空间分布概述 对于一个进程,其空间分布如下图所示: 程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码. 初始化过的数据(Data):在程序运行初已经对变量进行初始 ...
- Linux内存初始化
start_kernel -> setup_arch 在这个函数中我们主要看这几个函数. machine_specific_memory_setup max_low_pfn = setup_me ...
- Linux内存描述之内存区域zone--Linux内存管理(三)
1 内存管理域zone 为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个 ...
- Linux内存初始化【转】
转自:http://www.cnblogs.com/super-king/p/3291120.html start_kernel -> setup_arch 在这个函数中我们主要看这几个函数. ...
随机推荐
- Android中Dialog对话框的调用及监听
Android中经常会需要在Android界面上弹出一些对话框提示用户,比如App的退出的时候都会有各种框来挽留你的心,支付宝的时候输入密码的密码框,非常常见及其实用的功能,类似于JS中的alter, ...
- C#异常处理及心得
C sharp中的异常用于处理系统级和应用程序级的错误状态,它是一种结构化.统一的类型安全的处理机制.c#的异常 机制非常类似于c++的异常处理机制,但是还是有一些重要的区别: 1,在 C# 中,所有 ...
- 一步步教你如何在 Visual Studio 2013 上使用 Github
介绍 我承认越是能将事情变简单的工具我越会更多地使用它.尽管我已经知道了足够的命令来使用Github,但我宁愿它被集成到IDE中.在本教程中,我会告诉你使用Visual Studio 2013如何实现 ...
- MongoDB学习笔记(一)--基础
Insert MongoD ...
- svn自助改动password(PHP脚本实现)
#创建脚本文件夹 mkdir -p /var/www/svn/svntools #创建apache配置文件 touch /etc/httpd/conf.d/alias.conf #输入下面内容: Al ...
- 修改linux的文件时,如何快速找到要修改的内容并修改
修改linux系统下的文件时,如果文件内容很多,不容易找到需要修改的内容,下面详细介绍linux系统下如何快速修改文件. 工具/原料 linux系统 方法/步骤 在linux系统下,找到需 ...
- Android studio 2.0--android增量更新的那些事
用了这么久的AS 2.0预览版本号.4.7日谷歌最终公布了android studio 2.0正式版,小编当日便下载了.玩了一下.感觉第二次build编译明显快了,并且好像并没有又一次部署apk.经过 ...
- ArcGIS按字段属性分割文件
在ArcGIS中我们有时需要将一个文件安字段的属性分割成多个文件,网友总结了几乎所有的方法:http://blog.sina.com.cn/s/blog_4e87fb4c0100fcjh.html , ...
- 【转】TCP/IP详解学习笔记(二)
TCP/IP详解学习笔记(5)-IP选路,动态选路,和一些细节 1.静态IP选路 1.1.一个简单的路由表 选路是IP层最重要的一个功能之一.前面的部分已经简单的讲过路由器是通过何种规则来根据IP数据 ...
- npm之使用淘宝源
暂时使用: npm --registry https://registry.npm.taobao.org i express 永久使用: npm conf set registry https://r ...