Linux内存管理-1-物理内存初始化
在服务器上⼀般都会插很多条内存。但Linux内核在刚启动的时候是不了解这些硬件信息的。内核必须对整个系统 中的内存进⾏初始化,以⽅便后⾯的使⽤。具体过程包括物理内存检测、memblock初期分配器创建、内存NUMA 之node节点信息获取、⻚管理机制初始化、向伙伴系统交接可⽤内存范围等。这⼀章中我们围绕这些知识点来进 ⾏讲解。还是按照惯例,让我们带着⼏个问题开始本章的学习。
1)内核是通过什么⼿段来识别可⽤内存硬件范围的?
2)内核管理物理内存都使⽤了哪些技术⼿段?
3)为什么 free -m命令展示的总内存⽐dmidecode中输出的要少,少了的这些内存跑哪⾥去了?
Linux 内核给我们使⽤的内存看起来“并不⾜量”。拿我⼿头的⼀台虚拟机来举例(物理机原理⼀样),通过 dmidecode 命令查看到这台服务器是⼀条 16384 MB 的内存。、
但是使⽤ free 查看的时候,Linux 却告诉我们只有 15773 MB 可⽤。
那16384 和 15773 中间差的这 611 MB 跑哪⼉去了?
4)内核是怎么知道某个内存地址范围是属于哪个NUMA node节点的呢?
学习完本章后你将对这些物理内存管理问题会有深⼊理解。
1.1 固件程序介绍
但其实操作系统在刚刚启动的时候,对内存的可⽤地址范围、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
在这个规范中,定义了计算机硬件和操作系统之间的接⼝,包含的主要内容有计算机硬件配置描述、设备通信⽅ 式、电源功能管理等功能。在计算机启动过程中,固件负责着硬件⾃检、初始化硬件设备、加载操作系统引导程 序,将控制权转移到操作系统并提供接⼝供操作系统读取硬件信息。操作系统所需要的内存等硬件信息都是通过固 件来获取的。
1.2 物理安装内存检测
在操作系统启动时要做的⼀件重要的事情就是探测可⽤物理内存的地址范围。在固件ACPI接⼝规范中定义了探测内 存的物理分布规范。内核请求中断号 15H,并设置操作码为 E820 H。然后固件就会向内核报告可⽤的物理内存地 址范围。因为操作码是 E820,所以这个获取机制也被常称为 E820。
下⾯是具体的内核代码。内核在启动的时候也有个 main 函数。在 main 函数中会调⽤ detect_memory ,物理内 存安装检测就是在这个函数开始处理的。
// file:arch/x86/boot/main.c
void main(void)
{
...
/* Detect memory layout */
detect_memory();
...
}
//file:arch/x86/boot/main.c
void detect_memory(void)
{
detect_memory_e820();
detect_memory_e801();
detect_memory_88();
}
真正的探测操作是在 detect_memory_e820 中完成的。detect_memory_e820 函数发出 15 中断并处理所有结 果,把内存地址范围保存到 boot_params.e820_table 对象中,相关源码如下。
// file:arch/x86/boot/memory.c
static void detect_memory_e820(void)
{
int count = 0;
struct biosregs ireg, oreg;
struct boot_e820_entry *desc = boot_params.e820_table;
static struct boot_e820_entry buf; /* static so it is zeroed */
initregs(&ireg);
ireg.ax = 0xe820;
ireg.cx = sizeof(buf);
ireg.edx = SMAP;
ireg.di = (size_t)&buf;
/*
* Note: at least one BIOS is known which assumes that the
* buffer pointed to by one e820 call is the same one as
* the previous call, and only changes modified fields. Therefore,
* we use a temporary buffer and copy the results entry by entry.
*
* This routine deliberately does not try to account for
* ACPI 3+ extended attributes. This is because there are
* BIOSes in the field which report zero for the valid bit for
* all ranges, and we don't currently make any use of the
* other attribute bits. Revisit this if we see the extended
* attribute bits deployed in a meaningful way in the future.
*/
do {
intcall(0x15, &ireg, &oreg);
ireg.ebx = oreg.ebx; /* for next iteration... */
/* BIOSes which terminate the chain with CF = 1 as opposed
to %ebx = 0 don't always report the SMAP signature on
the final, failing, probe. */
if (oreg.eflags & X86_EFLAGS_CF)
break;
/* Some BIOSes stop returning SMAP in the middle of
the search loop. We don't know exactly how the BIOS
screwed up the map at that point, we might have a
partial map, the full map, or complete garbage, so
just return failure. */
if (oreg.eax != SMAP) {
count = 0;
break;
}
*desc++ = buf;
count++;
} while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_table));
boot_params.e820_entries = count;
}
// file: arch/x86/include/uapi/asm/setup_data.h
/*
* The E820 memory region entry of the boot protocol ABI:
*/
struct boot_e820_entry {
__u64 addr;
__u64 size;
__u32 type;
} __attribute__((packed));
// struct boot_params在linux源码中有多个头文件进行了定义
// file: arch/x86/include/uapi/asm/bootparam.h
/* The so-called "zeropage" */
struct boot_params {
struct screen_info screen_info; /* 0x000 */
struct apm_bios_info apm_bios_info; /* 0x040 */
...
struct boot_e820_entry e820_table[E820_MAX_ENTRIES_ZEROPAGE]; /* 0x2d0 */
...
} __attribute__((packed));
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
/*
* The whole array of E820 entries:
*/
struct e820_table {
__u32 nr_entries;
struct e820_entry entries[E820_MAX_ENTRIES];
};
// file:arch/x86/include/asm/e820/types.h
/*
* A single E820 map entry, describing a memory range of [addr...addr+size-1],
* of 'type' memory type:
*
* (We pack it because there can be thousands of them on large systems.)
*/
struct e820_entry {
u64 addr;
u64 size;
enum e820_type type;
} __attribute__((packed));
// file:arch/x86/include/asm/e820/types.h
/*
* These are the E820 types known to the kernel:
*/
enum e820_type {
E820_TYPE_RAM = 1,
E820_TYPE_RESERVED = 2,
E820_TYPE_ACPI = 3,
E820_TYPE_NVS = 4,
E820_TYPE_UNUSABLE = 5,
E820_TYPE_PMEM = 7,
...
};
在内核启动的后⾯的过程中,会把 boot_params.e820_table 中的数据拷⻉到这个全局的 e820_table 中。并把它 打印出来。具体是在 e820__memory_setup 函数中处理的。
// file:arch/x86/kernel/e820.c
void __init e820__memory_setup(void)
{
char *who;
/* This is a firmware interface ABI - make sure we don't break it: */
BUILD_BUG_ON(sizeof(struct boot_e820_entry) != 20);
who = x86_init.resources.memory_setup();
memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));
memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));
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 对物理内存的检测结果。
在dmsg的输出结果中,输出的最后⼀列为 usable 是实际可⽤的物理内存地址范围。被标记为 reserved 的内存不 能被分配使⽤,可能是内存启动时⽤来保存内核的⼀些关键数据和代码,也可能没有实际的物理内存映射到这个范 围。也建议⼤家使⽤ dmesg 查看下你的 Linux 对物理内存的探测结果。