1. memblock机制概述:memblock在Linux内核启动内存管理中的角色与定位
1.1 为什么需要memblock?——启动阶段的“内存荒原”
Linux内核在启动初期面临一个特殊的矛盾:需要管理物理内存,但自身的内存管理子系统(伙伴系统、slab分配器)尚未初始化完成。此时,内核处于一个“内存荒原”状态——没有页表、没有空闲链表、没有内存节点描述符。任何动态内存分配需求(如设备树解析、临时页表构建、驱动早期探测)都必须依赖一个轻量级、自包含的中间层分配器。
这个中间层就是 memblock(早期内核版本中称为 bootmem,自Linux 3.x起逐步被memblock取代)。它不依赖任何内核数据结构,仅通过三个全局数组(或链表)来跟踪物理内存的占用与空闲状态,为后续伙伴系统的建立提供“干净的”内存布局。
核心定位:memblock是内核启动阶段(从解压后到mm_init完成前)的唯一物理内存分配器,负责记录哪些区域已被占用、哪些区域可供分配,并在伙伴系统就绪后完成自我销毁与移交。
1.2 memblock在启动流程中的时间窗口
memblock的生命周期严格限定在以下时间窗口内:
| 阶段 | 事件 | memblock状态 |
|---|---|---|
| 1. 内核解压后 | start_kernel() → setup_arch() | 初始化memblock数据结构,扫描EFI/DTB内存映射 |
| 2. 早期内存探测 | memblock_add() / memblock_reserve() | 记录所有可用物理内存和已占用区域 |
| 3. 核心子系统初始化 | 页表构建、设备树解析、驱动probe | 提供memblock_alloc()系列分配接口 |
| 4. 伙伴系统就绪 | mm_init() → mem_init() | 将剩余空闲内存移交伙伴系统,memblock数据被释放 |
| 5. 启动完成 | 内核调度器启动 | memblock失效,不再被调用 |
关键时间点:memblock的“退役”发生在
mem_init() 函数中。该函数遍历memblock的空闲区域,调用 __free_pages() 将页面归还给伙伴系统。此后,所有内存分配请求必须通过 alloc_pages() / kmalloc() 完成。
1.3 memblock的核心数据结构
memblock使用三个全局结构体实例来管理内存:
- memblock.memory:记录所有被探测到的物理内存区域(类型为
struct memblock_type)。 - memblock.reserved:记录所有已被内核或驱动占用的区域。
- memblock.physmem:记录物理内存的原始布局(与memory类似,但不可修改,用于调试)。
每个 struct memblock_type 内部维护一个 struct memblock_region 数组:
struct memblock_region {
phys_addr_t base; // 区域起始物理地址
phys_addr_t size; // 区域大小(字节)
enum memblock_flags flags; // 区域属性(如可热插拔、不可缓存等)
int nid; // NUMA节点ID(-1表示不关心)
};
所有区域按 base地址升序排列,相邻区域在满足条件时会自动合并,保持数组紧凑。
1.4 memblock的工作流程全景
下图展示了memblock从初始化到移交的完整生命周期:
1.5 memblock与伙伴系统的关键差异
| 对比维度 | memblock | 伙伴系统 (Buddy) |
|---|---|---|
| 初始化时机 | start_kernel早期,无依赖 | mm_init()之后,依赖memblock |
| 分配粒度 | 任意字节(实际按页对齐) | 2^order页(4KB ~ 4MB) |
| 释放语义 | 不支持单独释放,仅批量移交 | 支持任意顺序释放与合并 |
| 碎片处理 | 无碎片整理,仅区域合并 | 反碎片、迁移类型、compaction |
| 数据结构 | 数组(线性扫描) | 链表 + 位图 + 红黑树 |
| 性能特征 | O(n)扫描,适合小规模 | O(1)分配/释放,适合大规模 |
| 生命周期 | 临时,启动后销毁 | 永久,贯穿内核运行 |
常见误解:memblock分配的内存不会自动释放。如果驱动在启动阶段通过memblock_alloc()申请了内存,但后续没有显式调用memblock_free(),这块内存将一直被标记为“reserved”,即使伙伴系统接管后也不会被回收。这是启动阶段内存泄漏的常见根源。
1.6 memblock的“三权分立”架构
memblock内部采用类似“三权分立”的设计思想,将内存信息的记录、分配、预留三个职责分离:
- memblock_add():向
memblock.memory添加可用物理内存区域。该函数由体系结构代码调用,通常来自EFI内存映射、设备树/memory节点或BIOS e820表。 - memblock_reserve():向
memblock.reserved添加已占用区域。内核镜像自身、initrd、页表、dtb等必须在分配前被预留。 - memblock_alloc():从
memblock.memory中减去memblock.reserved后的空闲区域中分配内存,并将分配结果自动加入memblock.reserved。
这种设计保证了分配操作不会破坏原始内存布局记录,同时允许在任意时刻查询某个物理地址是否已被占用。
1.7 实际调试中的memblock足迹
在嵌入式系统启动故障定位中,memblock相关的典型问题包括:
- 内存区域重叠:不同设备树节点或固件接口报告了重叠的物理地址范围,导致memblock_add()返回错误。
- 预留不足:某些DMA缓冲区或安全监控区域未在memblock_reserve()中声明,被后续分配覆盖。
- 分配失败:memblock_alloc()在指定NUMA节点或地址范围内找不到足够大的连续空闲区域。
- 启动内存泄漏:驱动使用memblock_alloc()后未释放,导致伙伴系统可用内存减少。
调试入口:内核启动参数
memblock=debug 可以开启memblock的详细日志,打印每次add/reserve/alloc操作。这是定位启动阶段内存问题的第一把钥匙。
1.8 小结:memblock的工程定位
memblock不是一种“高性能”分配器,而是一种“高确定性”的启动阶段内存管理方案。它牺牲了分配速度和灵活性,换取了以下关键能力:
- 零依赖:不依赖页表、cache、锁或任何内核子系统。
- 全可见:任何时候都可以遍历所有内存区域,知道“谁占用了哪里”。
- 可审计:所有分配和预留操作都有记录,便于启动阶段内存布局验证。
- 平滑过渡:在伙伴系统就绪后,能够干净地将管理权移交,不留残留。