4. memblock_add 与 memblock_reserve:内存添加与预留接口的底层实现
在 Linux 内核早期启动阶段,memblock 分配器负责管理物理内存的初始布局。memblock_add() 和 memblock_reserve() 是两个最核心的接口:前者向分配器注册可用物理内存区域,后者将特定区域标记为已占用,防止被后续分配覆盖。理解这两个接口的底层实现,是掌握内核内存初始化流程的关键。
4.1 接口语义与调用场景
两个接口在语义上存在本质区别,调用时机也截然不同:
| 接口 | 语义 | 典型调用者 | 底层操作 |
|---|---|---|---|
memblock_add(phys_addr_t base, phys_addr_t size) |
向 memblock 注册一段可用物理内存 | 体系结构启动代码、setup_arch() |
将区域插入 memblock.memory 类型数组 |
memblock_reserve(phys_addr_t base, phys_addr_t size) |
将一段物理内存标记为预留,不可分配 | 内核镜像加载、页表、dtb 区域保护 | 将区域插入 memblock.reserved 类型数组 |
memblock_add() 定义"哪些内存可用",memblock_reserve() 定义"哪些内存已被占用"。分配器在 memblock_alloc() 时,只会从 memblock.memory 中排除 memblock.reserved 的区域。
4.2 核心数据结构:memblock_type
两个接口最终操作的是同一个全局变量 memblock,其内部维护两个 memblock_type 数组:
struct memblock {
bool bottom_up; /* 分配方向:true 从低地址开始 */
phys_addr_t current_limit; /* 分配上限地址 */
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; /* 区域数组指针 */
char *name; /* 调试用名称 */
};
每个 memblock_region 描述一段连续的物理地址区间:
struct memblock_region {
phys_addr_t base; /* 起始物理地址 */
phys_addr_t size; /* 区域大小 */
enum memblock_flags flags; /* 属性标志,如 MEMBLOCK_NOMAP */
};
4.3 memblock_add() 实现流程
memblock_add() 的核心逻辑是向 memblock.memory 列表插入一个新区域,并自动执行合并相邻区域操作。其简化实现路径如下:
- 参数校验:检查 base 和 size 是否对齐、是否溢出。
- 调用底层添加函数:
memblock_add_range(&memblock.memory, base, size, ...)。 - 区域插入与合并:遍历现有 regions 数组,找到插入位置;如果新区与已有区域相邻或重叠,则合并为一个连续区域。
- 数组扩容:如果 regions 数组已满,调用
memblock_double_array()动态扩展容量。 - 更新 total_size:累加新增的有效内存大小。
4.4 memblock_reserve() 实现流程
memblock_reserve() 的实现与 memblock_add() 高度对称,区别在于操作的目标是 memblock.reserved 列表:
int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
phys_addr_t end = base + size - 1;
/* 确保地址不溢出 */
if (end < base)
return -EOVERFLOW;
/* 调用底层添加函数,目标为 reserved 列表 */
return memblock_add_range(&memblock.reserved, base, size, ...);
}
预留操作同样支持区域合并。例如,连续两次调用 memblock_reserve(0x1000, 0x1000) 和 memblock_reserve(0x2000, 0x1000),如果中间无间隙,最终 reserved 列表中将只包含一个 [0x1000, 0x3000) 的合并区域。
4.5 区域合并与数组扩容的 SVG 流程图
下图展示了 memblock_add() 在插入新区间时,如何执行区域合并与数组扩容决策:
4.6 数组动态扩容机制
当 memblock.memory 或 memblock.reserved 的 regions 数组容量不足时,memblock_double_array() 会被触发。该函数执行以下步骤:
- 计算新容量:将当前 max 值翻倍(至少为
INIT_MEMBLOCK_REGIONS的 2 倍)。 - 分配新内存:调用
memblock_alloc_base()从 memblock 自身分配一块新内存用于存放更大的 regions 数组。 - 数据拷贝:将旧数组内容逐项拷贝到新数组。
- 指针切换:更新
memblock_type->regions指向新数组,释放旧数组(如果旧数组不是静态初始化的)。
memblock_double_array() 可能递归调用 memblock 分配函数,导致死循环。内核通过 memblock_can_resize 标志和早期静态数组规避此问题。
4.7 调试与故障定位技巧
在实际开发中,memblock_add() 和 memblock_reserve() 相关故障通常表现为内存区域丢失或预留冲突。以下是常用的调试手段:
| 调试方法 | 命令/接口 | 适用场景 |
|---|---|---|
| 打印 memblock 状态 | memblock_dump_all() |
查看所有 memory 和 reserved 区域 |
| 检查区域重叠 | memblock_overlaps_region() |
验证新区域是否与已有区域冲突 |
| 跟踪调用栈 | 在 memblock_add_range() 中添加 WARN_ON() |
定位非法调用者 |
| 验证预留有效性 | memblock_is_region_reserved() |
确认某地址是否已被预留 |
memblock_dump_all() 发现 memblock.memory 中缺少了 [0x80000000, 0x90000000) 区域。进一步排查发现,设备树中 reg 属性配置错误,导致 early_init_dt_add_memory_arch() 未能正确调用 memblock_add()。修正设备树后问题解决。
4.8 接口调用顺序与初始化流程
在系统启动过程中,memblock_add() 和 memblock_reserve() 的调用顺序至关重要:
start_kernel()
└─ setup_arch()
├─ 解析设备树 / e820 表
├─ memblock_add() // 注册所有可用物理内存
├─ 内核镜像解压与重定位
├─ memblock_reserve() // 保护内核镜像所在区域
├─ 页表初始化
├─ memblock_reserve() // 保护页表内存
├─ 其他早期驱动初始化
│ └─ memblock_reserve() // 保护硬件专用内存
└─ paging_init()
└─ 最终 memblock 分配器移交给 buddy
如果 memblock_reserve() 在 memblock_add() 之前被调用,预留区域将无法正确记录,因为 memblock.memory 中尚未包含该地址范围。内核在 memblock_add_range() 中会检查这种情况并返回错误。
4.9 总结
memblock_add()和memblock_reserve()共享同一套底层插入合并逻辑,区别仅在于操作的目标列表不同。- 区域合并机制有效减少了 regions 数组中的碎片条目,提升了后续分配效率。
- 数组动态扩容是 memblock 分配器从静态初始化到动态管理的桥梁,但需注意递归分配风险。
- 调试时优先使用
memblock_dump_all()输出完整状态,结合调用栈定位异常调用者。