linux内存管理——内存大小起始地址的解析与修改

Posted 正在起飞的蜗牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内存管理——内存大小起始地址的解析与修改相关的知识,希望对你有一定的参考价值。

1、前言

(1)本文是以hi3516dv300芯片的uboot和内核源码进行讲解,uboot版本是2016.11,内核版本是4.9.37;
(2)uboot没有采用设备树技术,还是传统的tag传参;内核采用了设备树技术,镜像是zImage-dtb格式;
(3)下面的源码都是摘抄自dv300芯片的uboot和内核;

2、linux内核获取内存信息的来源

(1)设备树中可以通过"/memory"节点来指定内存的起始地址、大小等信息;
(2)uboot启动内核时,可以通过ATAG_MEM类型的struct tag结构体向内核传递内存起始地址、大小等信息;
(3)uboot启动内核时,可以通过ATAG_CMDLINE类型的struct tag结构体向内核传递内存起始地址、大小等信息;
内存信息生效的优先级:ATAG_CMDLINE类型的tag > ATAG_MEM类型的tag > 设备树"/memory"节点

3、hi3516dv300芯片的内存地址范围

(1)通过查阅数据手册可知《Hi3516DV300 专业型 Smart IP Camera SoC 用户指南》,dv300芯片的内存地址范围是0x8000_0000-0xFFFF_FFFF,最大能支持2G内存;
(2)海思芯片把内存分为mmz内存和os内存,最终linux系统能管理的内存是小于实际接的内存大小,因为要分一部分内存作为mmz;

4、设备树中指定内存大小

/ 
	······
	memory 
		device_type = "memory";
		reg = <0x82000000 0x20000000>;
	;
	
	······
;

(1)memory节点是设备树中专门用来指定内存起始地址、大小等信息,在内核启动时会被解析;
(2)上面的含义:内核的内存起始地址是0x82000000(2G),内存大小是0x20000000(512M);

5、uboot启动内核的BOOTM_ENABLE_MEMORY_TAGS传参

static void setup_memory_tags(bd_t *bd)

	int i;

	//内存地址可能不连续,分为好几块内存,这里用循环来设置每块内存的信息
	//可能存在多个ATAG_MEM类型的tag
	for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) 
		params->hdr.tag = ATAG_MEM;	//标明这是内存信息的tag
		params->hdr.size = tag_size (tag_mem32);

		params->u.mem.start = bd->bi_dram[i].start;	//内存的起始地址
		params->u.mem.size = bd->bi_dram[i].size;	//内存的大小

		params = tag_next (params);
	

当uboot中定义了BOOTM_ENABLE_MEMORY_TAGS宏时,会传递给内核ATAG_MEM类型的tag参数,内存具体的start和size分析uboot的源码可知;

6、uboot启动内核的BOOTM_ENABLE_CMDLINE_TAG传参

static void setup_commandline_tag(bd_t *bd, char *commandline)

	char *p;

	if (!commandline)
		return;

	/* eat leading white space */
	for (p = commandline; *p == ' '; p++);

	/* skip non-existent command lines so the kernel will still
	 * use its default command line.
	 */
	if (*p == '\\0')
		return;

	//构建ATAG_CMDLINE类型的tag
	params->hdr.tag = ATAG_CMDLINE;
	params->hdr.size =
		(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;

	strcpy (params->u.cmdline.cmdline, p);

	params = tag_next (params);

当uboot中定义了BOOTM_ENABLE_CMDLINE_TAG宏时,会传递给内核ATAG_CMDLINE类型的tag参数,具体bootarg分析uboot源码可知;

7、内核启动时设置内存信息的流程解析

7.1、内存信息的设置信息

//设备树dts文件
/ 
	······
	memory 
		device_type = "memory";
		reg = <0x82000000 0x20000000>;
	;
	
	······
;

//内核启动参数
~ # cat /proc/cmdline 
mem=1408M console=ttyS0,115200 root=/dev/mmcblk0p7 rootfstype=squashfs rootwait

//uboot的ATAG_MEM类型的struct tag
这里直接告诉结论:内存起始地址是0x8000_0000,内存大小是0x20000000

(1)如果设备树memory节点生效:内存起始地址是0x82000000,大小是0x20000000(512M);
(2)如果启动参数生效:内存起始地址是0x80000000,大小是1408M;(内存起始地址0x80000000可以分析内核源码)
(3)如果ATAG_MEM类型的tag生效:内存起始地址是0x80000000,大小是0x20000000(512M);

7.2、内核解析内存信息的流程

(1)将设备树dts文件编译成dtb格式文件,接续到zImage镜像后面构成zImage-dtb;
(2)uboot构建好构建ATAG_CMDLINE类型和ATAG_MEM类型的tag,启动内核,将tag的内存地址告诉内核;
(3)内核在解压缩阶段会解析tag传参,其中会用ATAG_MEM类型的tag替换dtb的"/memory"节点的reg属性值,用ATAG_CMDLINE类型的tag替换dtb的"/chosen"节点的bootargs属性值;
(4)解析dtb的"/memory"节点,向内核注册内存信息;
(5)解析dtb的"/chosen"节点的bootargs属性值,保存到boot_command_line变量;
(6)解析boot_command_line变量中的内存信息,向内核注册内存信息;这里注册时会覆盖掉第四步解析"/memory"节点时设置的内存信息;

7.3、内核解析内存信息的相关函数调用

start_kernel()
	setup_arch()
		setup_machine_fdt()
			early_init_dt_scan_nodes()
				of_scan_flat_dt()	//遍历dtb数据中的节点,找到memory节点
					early_init_dt_scan_chosen()	//解析"/chosen"节点的bootargs属性值,保存到boot_command_line变量
					
					early_init_dt_scan_memoryc()	//解析memory节点
						of_get_flat_dt_prop()	//获取memroy节点的reg属性值,也就是内存的起始地址、范围
						early_init_dt_add_memory_arch()
							memblock_add(base, size)
								memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0)	//向内核注册内存起始地址、范围
			

	parse_early_param()	//解析boot_command_line变量
		parse_early_options()
			parse_args()
				parse_one()	//将bootargs中的每项设置都单独解析出来
					do_early_param()
						early_mem()	//解析"mem=1408M"
							arm_add_memory()	
								memblock_add()	
									memblock_add_range()	//向内核注册内存起始地址、范围

(1)设备树的节点解析参考博客:《linux内核启动阶段对设备树的解析》
(2)boot_command_line变量的解析参考博客:《内核启动参数cmdline详解》
(3)uboot给内核的tag传参,参考博客:《uboot以tag方式给内核传参》
(4)不采用设备树时内核对tag的解析参考博客:《内核中对uboot传参tags的校验》

7.4、memblock_add()函数

int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)

	printk("memblock_add: [%#016llx-%#016llx], flags %#02lx, %pF @@@@@@@\\n",
		     (unsigned long long)base,
		     (unsigned long long)base + size - 1,
		     0UL, (void *)_RET_IP_);

	return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);

(1)memblock_add()函数是内存提供的注册内存信息的接口,传入内存的起始地址(物理地址)和大小;
(2)memblock_add()函数内部有打印,默认是不打印的,放开打印帮助调试,我这里是直接改成printk;

7.5、内核启动打印分析

······
 - 80000000 ,  20000000
memblock_add: [0x00000080000000-0x0000009fffffff], flags 0x0, early_init_dt_scan_memory+0x16c/0x190 

······
memblock_add: [0x00000080000000-0x000000d7ffffff], flags 0x0, arm_add_memory+0x174/0x19c 
······

(1)从内核打印可以看出内核总共设置两次内存信息,根据上面流程分析,第一次是解析"/memory"节点时设置的,第二次是解析bootargs的"mem=1408M"时设置的;
(2)第一次设置:内存启动地址是0x80000000,内存大小是0x20000000,刚好符合uboot的ATAG_MEM类型的tag参数;
(3)第二次设置:内存起始地址是0x80000000,内存大小是0xd8000000 - 0x80000000 = 1408M,刚好符合bootargs的"mem=1408M";

7.6、内核实际内存信息

~ # free
             total         used         free       shared      buffers
Mem:       1419980       132504      1287476          448        23700
-/+ buffers:             108804      1311176
Swap:            0            0            0

通过free命令可知,内核实际识别到的内存大概是1408M(识别到1408M,但是内核可能保留一部分内存不对外显示),证实我们之前的结论是bootargs的"mem=1408M"生效;

以上是关于linux内存管理——内存大小起始地址的解析与修改的主要内容,如果未能解决你的问题,请参考以下文章

清晰讲解Linux内核,连续内存分配与非连续内存分配(图例解析)

嵌入式linux学习笔记1—内存管理MMU之虚拟地址到物理地址的转化

《Linux内核设计与实现》读书笔记- 内存管理

操作系统---内存管理(上)

操作系统---内存管理(上)

Windows内存管理和linux内存管理