本节思考:
在系统启动时,
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, ®);
size = dt_mem_next_cell(dt_root_size_cells, ®);
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
初始化
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 小结