1.1 固件程序介绍与物理内存安装检测
固件程序介绍
Linux 内核给我们使用的内存看起来“并不足量”。拿我手头的一台虚拟机来举例(物理机原理一样),通过 dmidecode 命令查看到这台服务器是一条 16384 MB 的内存。
但是使用 free 查看的时候,Linux 却告诉我们只有 15773 MB 可用。
那 16384 和 15773 中间差的这 611 MB 跑哪儿去了?
4)内核是怎么知道某个内存地址范围是属于哪个 NUMA node 节点的呢?
学习完本章后你将对这些物理内存管理问题会有深入理解。
1.1 固件程序介绍
内存从硬件上看到的是连接在主板上的一根根有着金手指的硬件。内核需要识别到这些内存才可以进行后面的使用。但其实操作系统在刚刚启动的时候,对内存的可用地址范围、NUMA分组信息都是一无所知。在计算机的体系结构中,除了操作系统和硬件外,中间还存在着一层固件(firmware)。
# dmidecode
Memory Device
Total Width: Unknown
Data Width: Unknown
Size: 16384 MB
Manufacturer: QEMU
......
# free -m
total used free shared buff/cache available
Mem: 15773 794 13708 54 1270 14688
Swap: 0 0 0固件是位于主板上的使用 SPI Nor Flash 存储着的软件。起着在硬件和操作系统中间承上启下的作用。它对外提供接口规范是高级配置和电源接口(ACPI,Advanced Configuration and Power Interface)。其第一个版本 ACPI 1.0 是 1997 年的时候由英特尔、微软和东芝公司共同推出的。截至书稿写作时最新的版本是 2022 年 8 月发布的 6.5 版本。在 UEFI 论坛里可以下载到最新的规范文档:https://uefi.org/sites/default/files/resources/ACPI_Spec_6_5_Aug29.pdf。
在这个规范中,定义了计算机硬件和操作系统之间的接口,包含的主要内容有计算机硬件配置描述、设备通信方式、电源功能管理等功能。在计算机启动过程中,固件负责着硬件自检、初始化硬件设备、加载操作系统引导程序,将控制权转移到操作系统并提供接口供操作系统读取硬件信息。操作系统所需要的内存等硬件信息都是通过固件来获取的。
固件的作用
固件位于主板,使用 SPI Nor Flash 存储,是硬件与操作系统之间的桥梁。它通过 ACPI 接口规范向操作系统提供硬件信息。
1.2 物理内存安装检测
在操作系统启动时要做的一件重要的事情就是探测可用物理内存的地址范围。在固件 ACPI 接口规范中定义了探测内存的物理分布规范。内核请求中断号 15H,并设置操作码为 E820 H。然后固件就会向内核报告可用的物理内存地址范围。因为操作码是 E820,所以这个获取机制也被常称为 E820。
下面是具体的内核代码。内核在启动的时候也有个 main 函数。在 main 函数中会调用 detect_memory,物理内存安装检测就是在这个函数开始处理的。
真正的探测操作是在 detect_memory_e820 中完成的。detect_memory_e820 函数发出 15 中断并处理所有结果,把内存地址范围保存到 boot_params.e820_table 对象中,相关源码如下。
//file: arch/x86/boot/main.c
void main(void)
{
detect_memory();
...
}
//file: arch/x86/boot/memory.c
void detect_memory(void)
{
detect_memory_e820();
...
}
//file: arch/x86/boot/memory.c
static void detect_memory_e820(void)
{
struct boot_e820_entry *desc = boot_params.e820_table;
initregs(&ireg);
ireg.ax = 0xe820;
...
do {
intcall(0x15, &ireg, &oreg);
...
*desc++ = buf;
count++;
} while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_table));
boot_params.e820_entries = count;
}boot_params 只是一个中间启动过程数据。物理内存这么重要的数据还是应该单独存起来。所以,还专门有一个 e820_table 全局数据结构。
在内核启动的后面过程中,会把 boot_params.e820_table 中的数据拷贝到这个全局的 e820_table 中。并把它打印出来。具体是在 e820__memory_setup 函数中处理的。
关于保存过程我们就不细看了。我们重点来了解下内存检测结果打印,这个对我们比较有用。
//file: arch/x86/kernel/e820.c
static struct e820_table e820_table_init __initdata;
struct e820_table *e820_table __refdata = &e820_table_init;
//file: arch/x86/include/asm/e820/types.h
struct e820_table {
__u32 nr_entries;
struct e820_entry entries[E820_MAX_ENTRIES];
};
//file: arch\x86\kernel\e820.c
void __init e820__memory_setup(void)
{
// 保存 boot_params.e820_table 保存到全局 e820_table 中
char *who;
who = x86_init.resources.memory_setup();
...
// 打印内存检测结果
pr_info("BIOS-provided physical RAM map:\n");
e820__print_table(who);
}
//file: arch\x86\kernel\e820.c
void __init e820__print_table(char *who)
{
int i;
for (i = 0; i < e820_table->nr_entries; i++) {
pr_info("%s: [mem %#018Lx-%#018Lx] ",
who,
e820_table->entries[i].addr,
e820_table->entries[i].addr + e820_table->entries[i].size - 1);
e820_print_type(e820_table->entries[i].type);
pr_cont("\n");
}
}内核启动过程中的输出信息通过 dmesg 命令来查看。比如我手头的某台机器启动时输入的日志如下,详细地展示了 BIOS 对物理内存的检测结果。
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-provided physical RAM map:
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000000000000000-
0x000000000009ffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000000a0000-
0x00000000000fffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000000000100000-
0x000000002fffffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000000030000000-
0x0000000030041fff] ACPI NVS
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000000030042000-
0x0000000075daffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000000075db0000-
0x0000000075ffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000000076000000-
0x00000000a4d52fff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000a4d53000-
0x00000000a6bf7fff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000a6bf8000-
0x00000000a6d49fff] ACPI data
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000a6d4a000-
0x00000000a7241fff] ACPI NVS
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000a7242000-
0x00000000a816cfff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000a816d000-
0x00000000abffffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000ac000000-
0x00000000afffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000b4000000-
0x00000000b5ffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000be000000-
0x00000000bfffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000c8000000-
0x00000000c9ffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000f4000000-
0x00000000f5ffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x00000000fe000000-
0x00000000ffffffff] reserved
查看内存映射
使用
dmesg命令可以查看系统启动时的 BIOS E820 内存映射信息,它展示了每个内存区域的起始地址、结束地址和类型(usable、reserved、ACPI NVS、ACPI data 等)。
[Image 64 on Page 10]