Linux 内核是如何检测可用物理内存地址范围的?
大家好,我是飞哥!
内存从硬件上,看到的是一根根有着金手指的硬件。但内核是如何能够识别到主板上安装的内存的呢?我们有没有办法来查看到内核的这个识别过程?
我们今天就来看下内核对物理内存条的检测过程。
一、固件程序介绍
内存从硬件上看到的是连接在主板上一根根有着金手指的硬件。内核需要识别到这些内存才可以进行后面的使用。但其实操作系统在刚刚启动的时候,对内存的可用地址范围、NUMA分组信息都是一无所知。
好在在计算机的体系结构中,除了操作系统和硬件外,中间还存在着一层固件( firmware)。
固件是位于主板上的使用 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。
在这个规范中,定义了计算机硬件和操作系统之间的接口,包含的主要内容有计算机硬件配置描述、设备通信方式、电源功能管理等功能。在计算机启动过程中,固件负责着硬件自检、初始化硬件设备、加载操作系统引导程序,将控制权转移到操作系统并提供接口供操作系统读取硬件信息。
操作系统所需要的内存等硬件信息就是通过固件来获取的。
二、从固件读取内存布局数据
操作系统启动时要做的一件重要的事情就是探测可用物理内存的地址范围。在固件 ACPI 接口规范中定义了探测内存的物理分布规范。
具体的过程是内核请求中断号 15H,并设置操作码为 E820 H。然后固件就会向内核报告可用的物理内存地址范围。因为操作码是 E820,所以这个获取机制也被常称为 E820。
下面是具体的内核代码。内核在启动的时候也有个 main 函数。在 main 函数中会调用 detect_memory ,物理内存安装检测就是在这个函数开始处理的。
//file:arch/x86/boot/main.c
void main(void)
{
detect_memory();
...
}
// file:arch/x86/boot/memory.c
void detect_memory(void)
{
detect_memory_e820();
...
}
真正的探测操作是在 detect_memory_e820 中完成的。detect_memory_e820 函数发出 15 中断并处理所有结果,把内存地址范围保存到 boot_params.e820_table 对象中。
//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;
}
三、保存 e820 全局数据并打印
在上面一节中,把内存地址范围数据保存到 boot_params 中了。但 boot_params 只是一个中间启动过程数据。内存这么重要的数据还是应该单独存起来。所以,还专门有一个 e820_table 全局数据结构。
//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];
};
在内核启动的后面的过程中,会把 boot_params.e820_table 中的数据拷贝到这个全局的 e820_table 中。并把它打印出来。具体是在 e820__memory_setup 函数中处理的。
//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");
}
}
内核启动过程中的输出的信息通过 dmseg 命令来查看。比如我的这台机器启动时输入的日志如下,详细地展示了 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
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000104fefffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000104ff00000-0x000000104fffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000001050000000-0x000000204fefffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000204ff00000-0x000000204fffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000002050000000-0x000000304fefffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000304ff00000-0x000000304fffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000003050000000-0x000000404f2fffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000404f300000-0x000000404fffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000004050000000-0x000000504fefffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000504ff00000-0x000000504fffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000005050000000-0x000000604fefffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000604ff00000-0x000000604fffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000006050000000-0x000000704fefffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000704ff00000-0x000000704fffffff] reserved
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x0000007050000000-0x000000804fefffff] usable
Dec 23 04:53:10 kernel: [ 0.000000] BIOS-e820: [mem 0x000000804ff00000-0x000000804fffffff] reserved
......
总结
一台服务器上会被插入多条内存。但操作系统在刚开始的时候,对内存的可用地址范围、NUMA分组信息都是一无所知每条需要被编排进某个地址范围然后才能被 CPU 使用。
Linux 操作系统在启动的时候会向固件程序 发起 15 号中断,并设置操作码为 E820 H。然后固件上报内存地址范围,内核会把它保存并打印出来。
使用 dmseg 可以查看到 Linux 在启动时对内存探测过程的记录。其中输出的最后一列为 usable 是实际可用的物理内存地址范围。被标记为 reserved 的内存不能被分配使用,可能是内存启动时用来保存内核的一些关键数据和代码,也可能没有映射到实际的物理内存。
如果你感兴趣的话,可以使用 dmesg 查看下你的 Linux 对物理内存的探测结果。
有了这个 e820_table 表示的可用物理内存地址范围后,后面内核会通过自己的初期分配器 memblock、以及伙伴系统两种机制将所有可用的物理内存管理起来。
其中 memblock 管理方式比较粗放,仅仅用于内核启动时。启动好了后 memblock 会把可用物理页交接给伙伴系统。伙伴系统管理的比较精细,会按 4KB、8KB、16KB 等各种不同大小的空闲块来管理。
当我们的应用程序执行过程中,发现对应虚拟地址对应物理内存还没有分配的时候,会触发缺页中断。在缺页中断中向伙伴系统申请指定大小的物理内存页面。
这样我们就把从物理内存的可用地址探测到最后的应用程序物理内存申请,这中间的执行过程就都串起来理解了。以上内容源自飞哥新书《深入理解Linux进程与内存》!
最后和大家同步下知识星球中视频内容的最新进展,目前硬件原理、内存管理、进程管理、容器原理五大部分更新完了。累计1900分钟视频,目录如下:
接下来更新文件系统、性能观测、性能优化等几个大部分。另外我最近也在准备 eBPF 相关内容,后面计划在这块再做一些分享。
想加入的同学长按识别下方二维码即可加入,目前优惠后是 299 一年。