内存管理

  • Post author:
  • Post category:其他


本节思考:

在系统启动时,

ARM Linux

内核如何知道系统中有多大的内存空间?



32bit Linux

内核中,用户空间和内核空间的比例通常是

3:1

,可以修改成

2:2

吗?

物理内存页面如何添加到伙伴系统中,是一页一页添加,还是以

2

的几次幂来加入呢?

在内存管理的上下文中, 初始化(

initialization

)可以有多种含义.在许多

CPU

上,必须显式设置适用于

Linux

内核的内存模型.在

x86_32

上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器.

在初始化过程中, 还必须建立内存管理的数据结构,以及很多事务.因为内核在内存管理完全初始化之前就需要使用内存.

在系统启动过程期间,使用了额外的简化的内存管理模块,然后在初始化完成后,将旧的模块丢弃掉.

对相关数据结构的初始化是从全局启动函数

start_kernel

中开始的,该函数在加载内核并激活各个子系统之后执行.

由于内存管理是内核一个非常重要的部分,因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化.

现在大部分计算机使用

DDR



Dual Data Rate SDRAM

)的存储设备,

DDR

包括

DDR3L



DDR4L



LPDDR3/4

等。


DDR

初始化一般在

BIOS



boot loader

中,

BIOS



boot loader



DDR

大小传给内核,因此从

Linux

内核角度看其实就是一段物理内存空间。



1. 内存管理概述

分层描述的话,内存空间可以分为

3

个层次,分别是用户空间层、内核空间层和硬件层。如图2.1。

图2.1 内存管理框图:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

用户空间和内核空间的接口是系统调用,因此内核空间层首先需要处理这些内存管理相关的系统调用,例如

sys_brk



sys_mmap



sys_madvise

等。

接下来就包括

VMA

管理、缺页中断管理、匿名页面、

page cache

、页面回收、反向映射、

slab

分配器、页表管理等模块了。

最下面是硬件层,包括处理器的

MMU



TLB



cache

部件,以及板载的物理内存,例如

LPDDR



DDR

首先,需要知道整个用户和内核空间是如何划分的(

3:1



2:2

),然后从

Node->Zone->Page

的层级进行初始化,直到内存达到可用状态。

关于

Nodes



Zones



Pages

三者之间的关系,《

ULVMM



Figure 2.1

介绍,虽然

zone_mem_map

一层已经被替代,但是仍然反映了他们之间的层级树形关系。


pg_data_t

对应一个

Node



node_zones

包含了不同

Zone



Zone

下又定义了

per_cpu_pageset

,将

page



cpu

绑定。

在这里插入图片描述



2. 内存大小



1.2.1 DTS上报


ARM Linux

, 所有的设备的相关属性描述都采用

DTS(Device Tree Source)

方式.

总结:


ARM Linux

中,各种设备的相关属性描述(!!!)都采用

DTS

方式(!!!)呈现。

DTS



device tree source

,最早由

PowerPC

等其他体系结构使用的

FDT(Flattened Device Tree)

转变的,

ARM Linux

社区自

2011

年被

Linus

公开批评后全面支持

DTS



ARM Vexpress

平台中,内存的定义在

vexpress-v2p-ca9.dts

文件中。该

DTS

文件定义了内存的起始地址为

0x60000000

,大小为

0x40000000

,即

1GB

大小内存空间。

// [arch/arm/boot/dts/vexpress-v2p-ca9.dts]
//  http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm/boot/dts/vexpress-v2p-ca9.dts#L65
 
memory@60000000 {
    device_type = "memory";
    reg = <0x60000000 0x40000000>;
};


ARM64

平台类似, 起始地址为

0x80000000

, 大小

0x80000000

(

2GB

).

// http://elixir.free-electrons.com/linux/v4.13.11/source/arch/arm64/boot/dts/arm/vexpress-v2f-1xv7-ca53x2.dts#L61
memory@80000000 {
	device_type = "memory";
	reg = <0 0x80000000 0 0x80000000>; /* 2GB @ 2GB */
};

内核启动中,需要解析这些

DTS

文件,在

early_init_dt_scan_memory()

函数中。

代码调用关系是:

start_kernel()->setup_arch()->setup_machine_fdt()->early_init_dt_scan_nodes()->of_scan_flat_dt

(遍历

Nodes

)->

early_init_scan_memory

(初始化单个内存

node

)。

[drivers/of/fdt.c]
void __init early_init_dt_scan_nodes(void)
{
	/* Retrieve various information from the /chosen node */
	of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
 
	/* Initialize {size,address}-cells info */
	of_scan_flat_dt(early_init_dt_scan_root, NULL);
 
	/* Setup memory, calling early_init_dt_add_memory_arch */
	of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

最终

early_init_dt_scan_nodes()

调用了

early_init_dt_scan_memory

函数读取

DTS

的信息并初始化内存信息.

[drivers/of/fdt.c]
 
/**
 * early_init_dt_scan_memory - Look for and parse memory nodes
 */
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;
	bool hotpluggable;
 
	/* We are scanning "memory" nodes only */
	if (type == NULL) {
		/*
		 * The longtrail doesn't have a device_type on the
		 * /memory node, so look for the node called /memory@0.
		 */
		if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
			return 0;
	} else if (strcmp(type, "memory") != 0)
		return 0;
 
	reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
	if (reg == NULL)
		reg = of_get_flat_dt_prop(node, "reg", &l);
	if (reg == NULL)
		return 0;
 
	endp = reg + (l / sizeof(__be32));
	hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL);
 
	pr_debug("memory scan node %s, reg size %d,\n", uname, l);
 
	while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
		u64 base, size;
 
		base = dt_mem_next_cell(dt_root_addr_cells, &reg);
		size = dt_mem_next_cell(dt_root_size_cells, &reg);
 
		if (size == 0)
			continue;
		pr_debug(" - %llx ,  %llx\n", (unsigned long long)base,
		    (unsigned long long)size);
 
		early_init_dt_add_memory_arch(base, size);
 
		if (!hotpluggable)
			continue;
 
		if (early_init_dt_mark_hotplug_memory_arch(base, size))
			pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n",
				base, base + size);
	}
 
	return 0;
}



1.2.2 ACPI上报



3. 物理内存映射



4.

zone

初始化



5. 空间划分



32bit Linux

中 ,一共能使用的虚拟地址空间是

4GB

,用户空间和内核空间的划分通常是按照

3:1

来划分,也可以按照

2:2

来划分。

[arch/arm/Kconfig]
choice
    prompt "Memory split"
    depends on MMU
    default VMSPLIT_3G
    help 
      Select the desired split between kernel and user memory.
    
      If you are not absolutely sure what you are doing, leave this 
      option alone!
    
    config VMSPLIT_3G
        bool "3G/1G user/kernel split"
    config VMSPLIT_3G_OPT
        depends on !ARM_LPAE
        bool "3G/1G user/kernel split (for full 1G low memory)"
    config VMSPLIT_2G                     ### !!!  
        bool "2G/2G user/kernel split"
    config VMSPLIT_1G
        bool "1G/3G user/kernel split"
endchoice
    
config PAGE_OFFSET
    hex
    default PHYS_OFFSET if !MMU
    default 0x40000000 if VMSPLIT_1G
    default 0x80000000 if VMSPLIT_2G
    default 0xB0000000 if VMSPLIT_3G_OPT
    default 0xC0000000



ARM Linux

中有一个配置选项“

memory split

”,可以用于调整内核空间和用户空间的大小划分。

通常使用“

VMSPLIT_3G

”选项,用户空间大小是

3GB

, 内核空间大小是

1GB

,那么

PAGE_OFFSET

描述内核空间的偏移量就等于

0xC0000000

(等于

2^31+2^30=3GB

)。

也可以选择“

VMSPLIT_2G

”选项,这时内核空间和用户空间的大小都是

2GB

,

PAGE_OFFSET

就等于

0x80000000

(等于

2^31=2GB

)。

这样配置的结果就是生成的

autoconf.h

(

include/generated/autoconf.h

)定义了

#define CONFIG_PAGE_OFFSET 0xC0000000

内核中通常会使用

PAGE_OFFSET

这个宏来计算内核线性映射中虚拟地址和物理地址的转换。

[arch/arm/include/asm/memory.h]
 
/* PAGE_OFFSET - the virtual address of the start of the kernel image */
#define PAGE_OFFSET  UL(CONFIG_PAGE_OFFSET)

线性映射的物理地址等于虚拟地址

vaddr

减去

PAGE_OFFSET

(

0xC000_0000

)再加上

PHYS_OFFSET

(在部分

ARM

系统中该值为

0

)。

[arch/arm/include/asm/memory.h]
 
static inline phys_addr_t __virt_to_phys_nodebug(unsigned long x)
{
    return (phys_addr_t)x - PAGE_OFFSET + PHYS_OFFSET;
}
 
static inline unsigned long __phys_to_virt(phys_addr_t x)
{
    return x - PHYS_OFFSET + PAGE_OFFSET;
}



6. 物理内存初始化



7 小结



版权声明:本文为sunshineywz原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。