3. memblock初始化流程:从start_kernel到memblock初始化
memblock 是 Linux 内核早期启动阶段使用的物理内存分配器,其初始化发生在 start_kernel 函数执行初期,早于页分配器(buddy system)的建立。理解 memblock 的初始化流程,是掌握内核内存管理子系统启动过程的关键。
核心要点:memblock 初始化是“从无到有”的过程——内核在无法依赖任何动态内存分配机制时,通过静态定义的数据结构和汇编阶段传递的内存信息,逐步建立起可用的物理内存管理框架。
3.1 初始化起点:start_kernel 中的调用链
memblock 的初始化入口位于 init/main.c 的 start_kernel 函数中。以下是关键调用序列:
// start_kernel 中与 memblock 相关的调用链(简化)
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
// ... 架构无关的早期初始化 ...
setup_arch(&command_line); // 架构相关初始化,包含 memblock 早期设置
// ...
mm_init_cpumask(&init_mm); // 内存描述符初始化
setup_command_line(command_line); // 命令行参数保存
setup_nr_cpu_ids(); // CPU 数量检测
setup_per_cpu_areas(); // per-CPU 区域分配(使用 memblock)
// ...
build_all_zonelists(NULL); // 构建 zonelist(依赖 memblock 内存信息)
page_alloc_init(); // 页分配器初始化
// ...
mem_init(); // 打印内存信息,memblock 统计输出
// ...
mm_init(); // 内存管理子系统完整初始化
// ...
}
上述流程中,setup_arch 是 memblock 初始化的真正起点。不同架构的实现略有差异,但核心逻辑一致。
3.2 关键函数调用链全景
下表列出了从 start_kernel 到 memblock 就绪的核心函数调用链及其职责:
| 调用顺序 | 函数名称 | 所在文件 | 核心职责 |
|---|---|---|---|
| 1 | start_kernel |
init/main.c | 内核启动 C 语言入口,调度所有子系统初始化 |
| 2 | setup_arch |
arch/arm64/kernel/setup.c (以 ARM64 为例) | 架构相关初始化,解析设备树/BIOS 传递的内存信息 |
| 3 | arm64_memblock_init |
arch/arm64/mm/init.c | ARM64 架构的 memblock 初始化封装,调用通用接口 |
| 4 | memblock_add |
mm/memblock.c | 将物理内存区域添加到 memblock.memory 类型 |
| 5 | memblock_reserve |
mm/memblock.c | 将内核镜像、页表等保留区域标记为已分配 |
| 6 | memblock_alloc |
mm/memblock.c | 早期内存分配接口,供后续初始化使用 |
| 7 | memblock_free |
mm/memblock.c | 释放不再需要的早期保留内存 |
架构差异提示:x86 架构使用
setup_memory_map → e820__memblock_setup 路径;ARM32 使用 arm_memblock_init;RISC-V 使用 setup_bootmem。但最终都调用 memblock_add/memblock_reserve 等通用接口。
3.3 memblock 初始化流程 SVG 架构图
下图展示了从 start_kernel 到 memblock 就绪的完整调用链与数据流:
3.4 核心数据结构初始化
memblock 的全局实例 struct memblock 在编译时通过 #define 静态初始化,无需运行时分配:
// mm/memblock.c 中的静态定义
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, // 初始计数为1
.memory.max = INIT_MEMBLOCK_REGIONS,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1,
.reserved.max = INIT_MEMBLOCK_REGIONS,
.reserved.name = "reserved",
.bottom_up = false, // 默认从高地址向下分配
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
关键约束:
INIT_MEMBLOCK_REGIONS 默认值为 128。当添加的内存区域超过此数量时,memblock 会尝试通过 memblock_double_array 动态扩展 region 数组——这本身又需要调用 memblock_alloc,形成“先有鸡还是先有蛋”的问题。内核通过预留一个最小安全区域来解决此循环依赖。
3.5 初始化阶段的内存区域操作
在 setup_arch 执行过程中,memblock 主要完成三类区域操作:
- 添加可用区域(memblock_add):将固件/引导程序报告的可用物理内存范围加入
memblock.memory类型。 - 保留已占用区域(memblock_reserve):将内核镜像、设备树/ACPI 表、页表、initrd 等占用的内存加入
memblock.reserved类型。 - 标记特殊区域(memblock_mark_hotplug / memblock_mark_nomap):标识可热插拔或不可直接映射的区域。
以下是一个典型的 ARM64 平台初始化序列:
// arch/arm64/mm/init.c
void __init arm64_memblock_init(void)
{
// 1. 从设备树获取内存信息并添加
// 实际由 early_init_dt_scan_memory() 调用 memblock_add()
// 2. 保留内核镜像所在区域
memblock_reserve(__pa_symbol(_text), _end - _text);
// 3. 保留 initrd 区域(如果存在)
if (initrd_start)
memblock_reserve(__pa(initrd_start), initrd_end - initrd_start);
// 4. 保留设备树 blob
memblock_reserve(__pa(fdt), fdt_totalsize(fdt));
// 5. 保留 EFI 内存映射表(如果使用 UEFI)
// ...
// 6. 对某些区域应用特殊属性
memblock_mark_nomap(phys, size); // 标记不可直接映射区域
}
3.6 初始化完成后的状态检查
memblock 初始化完成后,可以通过以下方式验证状态:
| 检查方式 | 命令/接口 | 输出示例 |
|---|---|---|
| 内核启动日志 | dmesg | grep -i memblock |
memblock: memory: 0x0000004000000000..0x00000043ffffffff |
| proc 文件系统 | cat /proc/meminfo (早期不可用) |
MemTotal / MemFree 等统计 |
| 调试接口 | memblock_dump_all() 内核函数 |
打印所有 memory 和 reserved 区域 |
| sysfs 节点 | /sys/devices/system/memory/ |
内存块信息(需内核配置) |
调试技巧:在早期启动阶段,可以通过
early_param("memblock=debug", ...) 或 memblock=debug 内核参数启用 memblock 的详细调试输出,跟踪每次 add/reserve/alloc/free 操作。
3.7 初始化流程中的常见问题
- 内存区域重叠:不同固件接口报告的内存范围可能重叠,memblock 会自动合并相邻区域,但交叉重叠可能导致计数异常。
- 保留区域遗漏:如果内核镜像或关键数据结构未被正确保留,后续 memblock_alloc 可能覆盖这些区域,导致内核崩溃。
- region 数组溢出:当内存区域碎片化严重时,128 个初始 region 可能不够用,触发
memblock_double_array失败会导致系统挂起。 - current_limit 设置不当:某些架构需要限制分配范围(如 DMA 区域),若
memblock_set_current_limit调用时机错误,可能导致分配越界。