B07 —— 从 e820 到内存管理初始化:memblock allocator
金句:内核启动的第一步,是搞清楚自己有多少内存可以管理。
内核如何获取内存信息
内核启动时,通过两种方式获取物理内存信息:
1. 直接从 BIOS 获取(legacy)
- 通过 INT 15h E820h 调用
- 返回 e820 内存映射表
2. 从 bootloader 传递(UEFI)
- UEFI bootloader 已经初始化了内存
- 通过 boot_params 结构体传递(Linux Boot Protocol)
Linux 的做法:
- 启动参数 boot_params 包含 e820map
- 内核解析 e820map,构建 memblock
- 最终 memblock 初始化 buddy systemmemblock 的数据结构
memblock 是 Linux 早期使用的内存分配器(现在被简化的 version 取代):
memblock 的核心结构:
struct memblock {
struct memblock_type memory; // 可用物理内存区域
struct memblock_type reserved; // 已分配/保留区域
};
struct memblock_type {
unsigned long cnt; // 区域数量
unsigned long max; // 数组容量
phys_addr_t total_size; // 总大小
struct memblock_region *regions; // 区域数组
};
struct memblock_region {
phys_addr_t base; // 起始地址
phys_addr_t size; // 大小
unsigned long flags; // MEMBLOCK_* 标志
const char *name; // 区域名称
};
flags 取值:
MEMBLOCK_NONE = 0
MEMBLOCK_HOTPLUG = 1(热插拔内存)
MEMBLOCK_MIRROR = 2(镜像内存)
MEMBLOCK_NOMAP = 4(不映射,不能直接使用)memblock 的初始化过程
memblock 初始化流程(Linux 6.1+):
1. early_init_fdt_scan_reserved_mem()
- 扫描 Device Tree(ARM/ARM64)
- 读取 reserved-memory 节点
- 加入 memblock.reserved
2. early_init_fdt_scan_reserved_mem()
- 也适用于 x86(如果通过 FDT 传递)
3. e820__memory_setup() // x86
- 解析 e820 表
- 把 Type=1(可用内存)加入 memblock.memory
- 把 Type=2(保留)加入 memblock.reserved
4. memblock_add()
- 把内存区域加入 memblock.memory
- 合并相邻区域(如果有)
- 更新 total_size
5. memblock_reserve()
- 保留内核代码/数据(0~1MB)
- 保留 initrd(如果有)
- 加入 memblock.reserved
最终 memblock 结构:
memory:可用物理内存区域(DRAM)
reserved:已分配的/保留的区域e820 到 memblock 的转换
e820 表到 memblock 的代码(简化):
void __init e820__memory_setup(void)
{
char *addr = boot_params.e820_table;
struct e820_entry *entry;
int i;
for (i = 0; i < boot_params.e820_entries; i++) {
entry = &addr[i * sizeof(struct e820_entry)];
if (entry->type == E820_TYPE_MEMORY) {
// 可用内存,加入 memblock.memory
memblock_add(entry->addr, entry->size);
} else if (entry->type == E820_TYPE_RESERVED) {
// 保留区域,加入 memblock.reserved
memblock_reserve(entry->addr, entry->size);
}
// E820_TYPE_ACPI 和 E820_TYPE_NVS 需要特殊处理
}
}
e820_entry 的类型:
#define E820_TYPE_MEMORY 1 // 可用物理内存
#define E820_TYPE_RESERVED 2 // 保留/不可用
#define E820_TYPE_ACPI 3 // ACPI 表,可回收
#define E820_TYPE_NVS 4 // ACPI NVS,不可覆盖早期内存分配:bootmem
在 buddy system 初始化之前,内核用 bootmem 分配器来管理早期内存分配:
bootmem 的工作方式:
bootmem 是一个简单的"位图+链表"分配器:
- 维护一个位图,每 bit 对应一个 4KB 页框
- 分配:找到连续的 1(空闲位),标记为 0
- 释放:把对应的位从 0 改回 1
初始化(arch/x86/mm/init.c):
void __init init_bootmem(void)
{
min_low_pfn = 0;
max_pfn = e820__end_of_low_ram_pfn();
// 分配位图,大小 = max_pfn / 8 bytes
bootmem_map = alloc_bootmem_pages(PFN_BYTES(max_pfn));
// 初始化位图,全部置 1(空闲)
memset(bootmem_map, 0xff, PFN_BYTES(max_pfn));
}
bootmem 分配函数:
void * __init alloc_bootmem_pages(unsigned long size)
{
// 找到第一个满足大小的连续空闲区域
// 标记为已用
// 返回物理地址
}bootmem 的问题(为什么要替换成 memblock):
1. 分配算法 O(n)
- 最坏情况扫描整个位图
- 256GB 内存 = 64M 个页框 = 8MB 位图
- 每次分配扫描 8MB,效率低
2. 碎片化
- 多次分配释放后,位图碎片化
- 无法找到大块连续空间
3. 无法保留内存区域
- memblock 可以把区域标记为"保留"
- bootmem 只能分配,不能区分"已分配"和"保留"
替代方案:
- 早期用 bootmem,buddy system 就绪后迁移
- 现代 Linux(5.x+)用更简单的 memblock allocator(无 bootmem)memblock 分配函数
memblock 核心分配函数:
1. memblock_alloc(phys_addr_t size, phys_addr_t align)
- 分配指定大小的内存,返回物理地址
- align:对齐要求
2. memblock_alloc_try_nid()
- 尝试在不同 NUMA 节点分配
- 用于 NUMA 系统的本地内存分配
3. memblock_reserve(phys_addr_t base, phys_addr_t size)
- 保留一段内存区域(标记为已用)
- 内核启动时保留代码和数据
4. memblock_remove(phys_addr_t base, phys_addr_t size)
- 从 memblock.memory 中移除一段
- 用于标记热插拔内存或损坏的内存
实际使用:
// 保留低端 1MB(BIOS 区域)
memblock_reserve(0x0, 0x100000);
// 保留内核镜像(TEXT + DATA)
memblock_reserve(__pa_symbol(_text),
_end - _text);
// 分配 2MB 的 boot page table
page_table = memblock_alloc(PAGE_SIZE * 512, PAGE_SIZE);从 memblock 到 buddy system 的迁移
buddy system 就绪后,bootmem 被废弃:
page_alloc_init() // 初始化伙伴系统
- 读取 memblock.memory 中所有可用区域
- 对每个区域:
1. 调用 free_bootmem_page() 加入 buddy system
2. 逐页释放给伙伴系统
// 伪代码
for each region in memblock.memory:
start_pfn = __phys_to_pfn(region.base);
end_pfn = __phys_to_pfn(region.base + region.size);
for pfn in [start_pfn, end_pfn):
free_bootmem_page(pfn_to_page(pfn));
完成后,buddy system 完全接管物理内存分配:
- 分配:alloc_pages(gfp_mask, order)
- 释放:__free_pages(page, order)
- memblock 可以继续存在(用于早期分配),也可以移除/proc/meminfo 和 /proc/buddyinfo
Linux 查看内存状态:
$ cat /proc/meminfo
MemTotal: 32768176 kB // 总物理内存
MemFree: 23456789 kB // 空闲
MemAvailable: 28901234 kB // 可用(包含缓存)
Buffers: 123456 kB // 块设备缓存
Cached: 5678901 kB // 页缓存
SwapCached: 2345 kB // 交换缓存
$ cat /proc/buddyinfo
Node 0, zone DMA free pages 1234/5678
Node 0, zone DMA32 free pages 23456/34567
Node 0, zone Normal free pages 78901/123456
各列 = Order 0~Order 10 的空闲页数:
- DMA:低端 16MB(Order 0~10)
- DMA32:16MB~4GB(只有 Order 0~9 适用于 32-bit)
- Normal:4GB 以上总结
- memblock:早期内存分配器,存储可用内存和保留区域(memblock.memory + memblock.reserved)
- e820 → memblock:BIOS e820 表(Type=1 加入 memory,Type=2 加入 reserved)→ memblock_add
- bootmem:早期位图分配器(O(n) 扫描),buddy system 就绪后废弃
- memblock_alloc:早期分配函数(buddy 之前),对齐分配
- 迁移:buddy 就绪后,遍历 memblock.memory,逐页 free_bootmem_page 给 buddy
- /proc/meminfo:显示总内存/空闲/可用/缓存;/proc/buddyinfo:显示各 Order 的空闲页数
下篇预告(B08):内存初始化后,内核是怎么管理进程地址空间的?vmalloc、kmalloc、以及用户空间和内核空间的内存布局。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论