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 列表插入一个新区域,并自动执行合并相邻区域操作。其简化实现路径如下:

  1. 参数校验:检查 base 和 size 是否对齐、是否溢出。
  2. 调用底层添加函数:memblock_add_range(&memblock.memory, base, size, ...)。
  3. 区域插入与合并:遍历现有 regions 数组,找到插入位置;如果新区与已有区域相邻或重叠,则合并为一个连续区域。
  4. 数组扩容:如果 regions 数组已满,调用 memblock_double_array() 动态扩展容量。
  5. 更新 total_size:累加新增的有效内存大小。
合并规则:当新区间的 base 落在已有区间 [base_i, base_i + size_i) 范围内,或新区间与已有区间边界紧邻(无间隙),则合并为一个更大的连续区间。合并后,被覆盖的旧区间会被移除。

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() 在插入新区间时,如何执行区域合并与数组扩容决策:

memblock_add 区域合并与扩容流程 memblock_add 区域合并与数组扩容决策流程 memblock_add(base, size) 遍历 memblock.memory.regions 判断:新区间与已有区间 是否相邻或重叠? 是 合并相邻区域 否 插入新区间 判断:regions 数组是否已满? (cnt == max) 是 memblock_double_array() 否 直接写入数组 返回 0 (成功) 合并后 cnt 可能减少

4.6 数组动态扩容机制

当 memblock.memory 或 memblock.reserved 的 regions 数组容量不足时,memblock_double_array() 会被触发。该函数执行以下步骤:

  1. 计算新容量:将当前 max 值翻倍(至少为 INIT_MEMBLOCK_REGIONS 的 2 倍)。
  2. 分配新内存:调用 memblock_alloc_base() 从 memblock 自身分配一块新内存用于存放更大的 regions 数组。
  3. 数据拷贝:将旧数组内容逐项拷贝到新数组。
  4. 指针切换:更新 memblock_type->regions 指向新数组,释放旧数组(如果旧数组不是静态初始化的)。
潜在风险:在 memblock 分配器自身尚未完全初始化时,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() 确认某地址是否已被预留
实战案例:某 ARM64 平台在启动时内核崩溃,通过 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() 输出完整状态,结合调用栈定位异常调用者。