Linux 的内存管理是操作系统核心功能之一,负责高效、安全地分配和回收物理内存资源,同时为应用程序提供抽象的虚拟内存空间.其复杂性源于现代计算机系统的多样化需求、硬件架构的差异以及性能优化的权衡. 涵盖了诸如:
kswapd
守护进程).mmap
动态扩展.等等方面的问题, 本节我们从最基本的物理内存布局探测入手, 在本系列中逐步分析这些内容
对于一个操作系统, 在启动之初有两个非常关键的问题
内存在硬件上的表现为内存条, 以及内存条上的内存颗粒. 从软件角度来看可以理解为是一大块连续的数组, 每个数组元素占 1 字节. 内存总大小等信息作为设备的关键信息,应该在硬件启动初期就由CPU获得并存储,操作系统只需要通过CPU的相关协定读取即可,这个协定就是BIOS中断
在x86芯片中,探测物理内存布局用的BIOS中断向量是0x15,根据ax寄存器值的不同,有三种常见的方式:0xE820,0xE801和0x88.
其中,0xE820 是主探测接口,0xE801、0x88 作为 0xE820 接口的补充.
// arch/x86/boot/main.c
void main() {
// ...
/* Detect memory layout */
detect_memory();
// ...
}
在 detect_memory() 函数中,依次调用了 detect_memory_e820() ,detect_memory_e801() 以及 detect_memory_88() 函数,每个函数对应着上文介绍的一种接口协议
// arch/x86/boot/memory.c
void detect_memory(void) {
detect_memory_e820(); /* 使用e820 BIOS中断获取物理内存布局 */
detect_memory_e801(); /* 使用e801 BIOS中断获取物理内存布局 */
detect_memory_88(); /* 使用88 BIOS中断获取物理内存布局 */
}
前文提到三种探测方式的区别在于根据 AX 寄存器值的不同.其中 e820 需要设置 AX 向量号为 0xe820
,
该接口返回已安装的内存映射以及为 BIOS 保留的物理内存区域.每次调用该 API,只会返回一段物理内存的信息,信息中会指示内存类型.为了获取完整的内存映射,需要多次调用该接口.
该接口通过寄存器传参,共有 5 个参数:
寄存器 | 描述 |
---|---|
EAX | 功能码,值为 E820 . |
EBX | 首次调用时,必须设置为 0.当调用后,EBX 中包含下次运行的物理地址,如果该值为 0,说明完成探测. |
ES:DI | Buffer 指针,BIOS 会将探测结果填充到指针指向的 Buffer 中. |
ECX | 以字节为单位的 Buffer 大小,最小为 20 字节. |
EDX | 签名,ASCII 码 "SMAP" ,用来验证调用者. |
接口的输出结果,也保存在 5 个寄存器中:
寄存器 | 描述 |
---|---|
CF | 状态寄存器 EFLAGS 的 CF 标志位,用来指示请求是否出错.当 CF 为 0 时,指示未发生错误. |
EAX | 签名,ASCII 码 "SMAP" . |
ES:DI | Buffer 指针,与输入一致. |
ECX | Buffer 大小,BIOS 返回数据的大小. |
EBX | 指示是否需要继续查询.当该值为 0 时,表示已查询到最后一段内存. |
简而言之, 参数需要传入一个 buffer, BIOS内存探测的结果会保存在 buffer 中(ecx)NOTE
需要连续调用, 直到 ebx == 0
我们来结合这部分的代码看一下
// arch/x86/boot/memory.c
#define SMAP 0x534d4150 /* ASCII "SMAP" */
// Input:
// AX = E820h
// EAX = 0000E820h
// EDX = 534D4150h ('SMAP')
// EBX = continuation value or 00000000h to start at beginning of map
// ECX = size of buffer for result, in bytes (should be >= 20 bytes)
// ES:DI -> buffer for result (see #00581)
// int 0x15
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;
do {
intcall(0x15, &ireg, &oreg);
ireg.ebx = oreg.ebx; /* for next iteration... */
if (oreg.eflags & X86_EFLAGS_CF)
break;
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;
}
初始化阶段将参数 ireg 的 ax 设置为 e820
标记这是一个 BIOS E820 的中断. 并将结果保存在 buf 中, edx 设置为 ASCII 的 "SMAP"
调用 intcall 0x15 获取结果, 循环调用直到 ireg.ebx 的值为 0, 或者超过 e820_table 的数组大小
相比E820中断,E801的输出和输出就简单了不少, 输入只需要传入一个 AX 值
寄存器 | 描述 |
---|---|
AX | 功能码,值为 E801 . |
接口的输出结果,也保存在 5 个寄存器中:
寄存器 | 描述 |
---|---|
CF | 状态寄存器 EFLAGS 的 CF 标志位,用来指示请求是否出错.当 CF 为 0 时,指示未发生错误. |
AX | 在 1MB 到 16 MB 之间的内存,以 KB 为单位.最大为 0x3C00,即 15 MB 内存. |
BX | 在 16 MB 到 4 GB 之间的内存,以 64 KB 为单位 |
CX | 同AX |
DX | 同BX |
为什么 AX 的最大值是 0x3C00 ?这是由于历史原因导致的.在 80286 时代,ISA 总线由 8 位扩展到了 24 位,24 位的地址线最大寻址空间为 16M,而15 MB ~ 16 MB 的空间要用于 ISA 设备的内存映射,不能自由使用,这段内存也被称为 "ISA Memory Hole"
static void detect_memory_e801(void)
{
struct biosregs ireg, oreg;
initregs(&ireg);
ireg.ax = 0xe801;
intcall(0x15, &ireg, &oreg);
if (oreg.eflags & X86_EFLAGS_CF)
return;
/* Do we really need to do this? */
if (oreg.cx || oreg.dx) {
oreg.ax = oreg.cx;
oreg.bx = oreg.dx;
}
if (oreg.ax > 15*1024) {
return; /* Bogus! */
} else if (oreg.ax == 15*1024) {
boot_params.alt_mem_k = (oreg.bx << 6) + oreg.ax;
} else {
/*
* This ignores memory above 16MB if we have a memory
* hole there. If someone actually finds a machine
* with a memory hole at 16MB and no support for
* 0E820h they should probably generate a fake e820
* map.
*/
boot_params.alt_mem_k = oreg.ax;
}
}
代码中有一段很有意思的注释(高亮部分), 这里的两个字段 cx 和 dx 的设计可能是由于历史原因
由于 ax 的最大值为 15MB,如果大于 15MB,说明出错了,返回 -1;如果 ax 等于 15MB,说明内存大于 16MB,需要计算 bx 寄存器中的内存.由于 bx 中的内存单位为 64KB,所以左移 6 位转换成 KB,再加上 ax 的值,就得到以 KB 为单位的总内存大小,并将结果保存到 boot_params.alt_mem_k 中;如果 ax 小于 15 MB,说明内存不超过 16 MB,ax 的值就是内存总大小.
相比E820,E88中断可以说非常简单了. 输入只有 AH
寄存器 | 描述 |
---|---|
AH | 功能码,值为 88 . |
输出只需要读取 AX 即可
寄存器 | 描述 |
---|---|
CF | 状态寄存器 EFLAGS 的 CF 标志位,用来指示请求是否出错.当 CF 为 0 时,指示未发生错误. |
AX | >1MB, 以KB为单位 |
这是一个比较原始的接口. 该接口返回 1 MB 以上的连续内存值,但是由于返回的是 16 位值(以 KB 为单位),因此其能返回的最大值会略低于 64 MB.也就是说,该接口会返回 1MB ~ 64 MB 之间的内存
static void detect_memory_88(void)
{
struct biosregs ireg, oreg;
initregs(&ireg);
ireg.ah = 0x88;
intcall(0x15, &ireg, &oreg);
boot_params.screen_info.ext_mem_k = oreg.ax;
}
0x88 接口探测的是 1MB ~ 64 MB 之间的内存.探测完成后,将结果保存到 boot_params.screen_info.ext_mem_k 中
对比上面三种内存探测方式, 可以看到 e820 探测结果最为重要, 几乎涵盖了系统使用到的大部分内存, 其中 buf 对于的结构体字段如下, 分别对应起始地址, 大小和内存类型
struct boot_e820_entry {
__u64 addr;
__u64 size;
__u32 type;
} __attribute__((packed));
其中内存类型包括以下几种:
其中,ACPI reclaimable memory(ACPI 可回收内存)中保存着 ACPI 表(ACPI tables),当 ACPI 表使用完成后,这部分内存就是可用的,所以被称为可回收内存.在分配物理内存时,内存类型 2、4、5(Reserved,ACPI Non-Volatile-Sleeping,bad )不应被分配.
由于该接口返回的是未排序的内存列表,可能包含不可用的或重叠的内存区域,所以需要对该列表进行后期处理.
你可以在启动时看到对应的内存区间检测输出结果
就是在检测后由下面的函数负责输出
static void __init e820_print_type(enum e820_type type)
{
switch (type) {
case E820_TYPE_RAM: /* Fall through: */
case E820_TYPE_RESERVED_KERN: pr_cont("usable"); break;
case E820_TYPE_RESERVED: pr_cont("reserved"); break;
case E820_TYPE_SOFT_RESERVED: pr_cont("soft reserved"); break;
case E820_TYPE_ACPI: pr_cont("ACPI data"); break;
case E820_TYPE_NVS: pr_cont("ACPI NVS"); break;
case E820_TYPE_UNUSABLE: pr_cont("unusable"); break;
case E820_TYPE_PMEM: /* Fall through: */
case E820_TYPE_PRAM: pr_cont("persistent (type %u)", type); break;
default: pr_cont("type %u", type); break;
}
}
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");
}
}
答:通过BIOS 0x15中断,常见有E820,E801和E88子中断号.
答:不是的,只有内存类型为usable的才能被操作系统所使用.
memblock 子系统主要用于引导过程中的物理内存管理, 在操作系统内核早期的启动阶段(即尚未进入 mm_core_init)时, 此时内核尚未完全初始化和建立内存管理器. 此时内核的主要工作只是简单的读取并处理一些系统硬件的信息. 一旦内核初始化完成,memblock 子系统的功能通常会被更高级的内存管理机制所取代,如 buddy allocator(伙伴系统)或 slab allocator(SLAB 系统)
memblock 的功能主要包括
/**
* struct memblock - memblock allocator metadata
* @bottom_up: is bottom up direction? 用于判断记录的内存是否从底部往顶部增长
* @current_limit: physical address of the current allocation limit 当前内存管理器管理的物理地址上限
* @memory: usable memory regions 操作系统可用内存,即E820探测物理布局时,flags为usable的内存区域
* @reserved: reserved memory regions 在boot阶段保留的内存,包括E820探测物理布局时,flags为reserved的内存区域,boot阶段分配出去的内存区域
*/
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
/**
* struct memblock_type - collection of memory regions of certain type
* @cnt: number of regions 记录的内存区域(memblock_region)的数量
* @max: size of the allocated array 最多能使用的内存区域数,当预留的内存区域不足时,管理器会扩展
* @total_size: size of all regions 所有内存区域的内存之和
* @regions: array of regions 内存区域数组,每一项代表usable或保留的内存区域
* @name: the memory type symbolic name 内存管理器类型的名称,例如"memory","reserved"等
*/
struct memblock_type {
unsigned long cnt;
unsigned long max;
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
/**
* struct memblock_region - represents a memory region
* @base: base address of the region 内存区域的起始地址,类型为u64或u32,表示64位/32位架构的支持最大地址长度
* @size: size of the region 内存区域的大小
* @flags: memory region attributes 内存区域的类型表示,有四种类型:MEMBLOCK_NONE(普通内存),MEMBLOCK_HOTPLUG(可热拔插内存),MEMBLOCK_MIRROR(镜像内存),MEMBLOCK_NOMAP(非内核直接映射内存),相同类型的相邻内存,条件合适时可以被合并
* @nid: NUMA node id 暂时略去与NUMA相关的内容
*/
struct memblock_region {
phys_addr_t base;
phys_addr_t size;
enum memblock_flags flags;
#ifdef CONFIG_NUMA
int nid;
#endif
};
/**
* enum memblock_flags - definition of memory region attributes
* @MEMBLOCK_NONE: no special request
* @MEMBLOCK_HOTPLUG: memory region indicated in the firmware-provided memory
* map during early boot as hot(un)pluggable system RAM (e.g., memory range
* that might get hotunplugged later). With "movable_node" set on the kernel
* commandline, try keeping this memory region hotunpluggable. Does not apply
* to memblocks added ("hotplugged") after early boot.
* @MEMBLOCK_MIRROR: mirrored region
* @MEMBLOCK_NOMAP: don't add to kernel direct mapping and treat as
* reserved in the memory map; refer to memblock_mark_nomap() description
* for further details
* @MEMBLOCK_DRIVER_MANAGED: memory region that is always detected and added
* via a driver, and never indicated in the firmware-provided memory map as
* system RAM. This corresponds to IORESOURCE_SYSRAM_DRIVER_MANAGED in the
* kernel resource tree.
*/
enum memblock_flags {
MEMBLOCK_NONE = 0x0, /* No special request */
MEMBLOCK_HOTPLUG = 0x1, /* hotpluggable region */
MEMBLOCK_MIRROR = 0x2, /* mirrored region */
MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */
MEMBLOCK_DRIVER_MANAGED = 0x8, /* always detected via a driver */
};
操作系统通过BIOS 0x15中断,常见有E820、E801和E88子中断号获取设备总内存大小, 内存类型为usable的才能被操作系统所使用
memblock 子系统主要用于引导过程中的物理内存管理