4. 内存管理子系统初始化:memblock分配器的诞生

在系统启动早期,内核面临一个“先有鸡还是先有蛋”的困境:伙伴系统(Buddy System)需要内存管理结构(如 struct page)来管理物理内存,但这些结构本身又需要分配内存来存放。memblock 分配器正是为了解决这一矛盾而诞生的——它是一个早期、简单、静态的物理内存分配器,在伙伴系统就绪之前提供临时的内存管理服务。

核心概念:memblock 分配器不依赖任何内核内存管理基础设施,它直接操作物理内存映射表,通过“预留”和“分配”两个基本操作来管理启动阶段的内存使用。

4.1 为什么需要 memblock?

内核启动顺序决定了内存管理子系统的初始化必须分阶段进行:

阶段 分配器 特点 依赖条件
1. 汇编启动 无(直接使用物理地址) 极简,无管理 无
2. 早期C初始化 memblock 静态、线性、不可回收 仅需物理内存映射信息
3. 伙伴系统就绪 Buddy + Slab 动态、可回收、支持碎片管理 需要 struct page 数组

memblock 分配器在阶段 2 中扮演了桥梁角色:它负责分配存放 struct page 数组所需的内存,以及早期驱动、设备树、页表等关键数据结构的内存。

4.2 memblock 的核心数据结构

memblock 使用两个核心数组来跟踪物理内存:可用内存区域(memory)和预留内存区域(reserved)。每个区域由 struct memblock_region 描述:

struct memblock_region {
    phys_addr_t    base;    // 物理基地址
    phys_addr_t    size;    // 区域大小
    enum memblock_flags flags; // 属性标志(如可热插拔)
};

struct memblock_type {
    unsigned long        cnt;      // 当前区域数量
    unsigned long        max;      // 最大区域数量
    struct memblock_region *regions; // 区域数组
    char                 *name;    // 类型名称
};

struct memblock {
    bool                   bottom_up;  // 分配方向:从低地址还是高地址
    phys_addr_t            current_limit; // 分配上限
    struct memblock_type   memory;     // 可用物理内存
    struct memblock_type   reserved;   // 已预留内存
};
关键设计:memblock 不合并相邻的可用区域,也不回收已分配的内存。它只维护一个“已分配/未分配”的简单视图,所有复杂的内存管理逻辑都留给后续的伙伴系统。

4.3 memblock 初始化流程

memblock 的初始化发生在 setup_arch() 函数中,大致分为以下步骤:

  1. 解析固件提供的内存信息(如 UEFI 内存映射、设备树 /memory 节点)
  2. 调用 memblock_add() 将可用物理内存区域加入 memblock.memory
  3. 调用 memblock_reserve() 标记内核镜像、页表、initrd 等已占用的区域
  4. 设置 memblock.current_limit 限制分配范围(通常为 DMA 区域上限)
  5. 调用 memblock_alloc() 分配 struct page 数组所需的内存
注意:memblock 的 regions 数组本身也需要内存。在早期,这个数组使用静态定义的 INIT_MEMBLOCK_REGIONS(通常为 128 个条目),如果启动过程中区域数量超过这个值,memblock 会尝试动态扩展——但此时它只能依赖自身来分配扩展所需的内存,形成一种“自举”过程。

4.4 memblock 分配与预留操作

memblock 提供两组核心 API:

操作 API 函数 说明
添加可用内存 memblock_add(base, size) 将一段物理内存标记为可用
预留内存 memblock_reserve(base, size) 标记一段内存为已占用,不可再分配
分配内存 memblock_alloc(size, align) 从可用区域分配对齐后的物理内存
分配(指定上限) memblock_alloc_range(size, align, start, end) 在指定地址范围内分配内存
释放内存 memblock_free(phys_addr, size) 将预留的内存重新标记为可用
查询信息 memblock_phys_mem_size() 返回总的可用物理内存大小

4.5 memblock 分配策略:自底向上 vs 自顶向下

memblock 支持两种分配方向,由 memblock.bottom_up 标志控制:

  • 自底向上(bottom-up):从低地址开始分配,适用于大多数架构,有利于 DMA 设备访问。
  • 自顶向下(top-down):从高地址开始分配,在某些架构(如 x86)的早期阶段使用,以避免覆盖低地址区域的固件数据。

分配算法非常直接:遍历 memblock.memory 中的区域,找到第一个满足大小和对齐要求的空闲区间(即该区间不与任何 memblock.reserved 区域重叠),然后将其标记为预留。

4.6 memblock 与 DMA 的早期关联

在设备驱动加载的上下文中,memblock 与 DMA 的关联体现在两个方面:

  1. DMA 区域限制:memblock.current_limit 通常被设置为 DMA 区域的物理上限(如 16MB 或 4GB),确保早期分配的内存都在 DMA 可访问范围内。
  2. DMA 页表分配:某些架构在启动早期使用 memblock 分配 DMA 页表所需的物理内存。
驱动开发者视角:在 probe 函数中调用 dma_alloc_coherent() 时,如果伙伴系统尚未完全初始化(极少数情况),内核会回退到 memblock 分配器。但通常情况下,驱动加载时伙伴系统已经就绪,memblock 已完成其历史使命。

4.7 memblock 的生命周期与退出

memblock 分配器在伙伴系统初始化完成后被废弃,但它的数据结构并不会被销毁——它们被保留用于调试和内存信息查询。转换过程发生在 mm_init() 函数中:

// mm/page_alloc.c
void __init memblock_free_all(void)
{
    unsigned long pages, total_pages = 0;
    struct memblock_region *reg;

    // 遍历所有 memblock 中的可用区域
    for_each_memblock(memory, reg) {
        pages = memblock_free_memory_pages(reg);
        total_pages += pages;
    }

    // 将释放的页添加到伙伴系统
    pr_info("Freeing %lu pages to buddy system\n", total_pages);
}

此后,所有内存分配请求都通过伙伴系统和 slab 分配器处理,memblock 只作为只读的信息源存在。

4.8 memblock 初始化全景图

下面的 SVG 图展示了 memblock 分配器在内核启动过程中的位置及其与前后阶段的关系:

memblock 分配器初始化全景图 阶段1:汇编启动 无分配器,直接使用物理地址 阶段2:memblock 分配器 静态、线性、不可回收 管理 memory + reserved 区域 阶段3:伙伴系统 动态、可回收 基于 struct page 管理 memblock 内部结构 memblock.memory 可用物理内存区域 memblock.reserved 已预留内存区域 memblock_alloc() / memblock_reserve() 分配/预留操作接口 memblock 分配的关键对象 struct page 数组 内核页表 设备树 (DTB) initramfs 早期 DMA 缓冲区 memblock 的桥梁作用 在伙伴系统就绪前,memblock 为关键数据结构提供临时内存分配服务 伙伴系统初始化完成后,memblock 将内存移交给伙伴系统,自身进入只读状态

4.9 关键代码路径示例

以下是一个简化的 memblock 初始化代码路径,展示了从固件信息到 memblock 区域建立的过程:

// arch/arm64/mm/init.c
void __init arm64_memblock_init(void)
{
    // 1. 从设备树获取物理内存信息
    early_init_fdt_scan_reserved_mem();

    // 2. 添加可用内存
    memblock_add(__pa(_text), (u64)(_end - _text));

    // 3. 预留内核镜像自身
    memblock_reserve(__pa(_text), (u64)(_end - _text));

    // 4. 预留设备树
    memblock_reserve(__pa(initial_boot_params),
                     fdt_totalsize(initial_boot_params));

    // 5. 预留 initrd
    if (initrd_start)
        memblock_reserve(__pa(initrd_start), initrd_end - initrd_start);

    // 6. 设置 DMA 区域限制
    if (IS_ENABLED(CONFIG_ZONE_DMA))
        memblock_set_current_limit(arm64_dma_phys_limit);
}

4.10 总结:memblock 的设计哲学

设计原则 具体体现 对驱动开发的影响
极简 只维护两个区域列表,无碎片管理 早期驱动分配内存时无需考虑释放
自举 自身数据结构可动态扩展 保证即使复杂系统也能完成初始化
临时性 伙伴系统就绪后即被取代 驱动 probe 时通常已使用伙伴系统
DMA 感知 通过 current_limit 限制分配范围 确保早期分配的内存对 DMA 友好

memblock 分配器的诞生解决了内核启动过程中的“鸡生蛋”问题,它为后续伙伴系统的建立铺平了道路,也为早期设备驱动(如中断控制器、定时器)提供了必要的内存支持。理解 memblock 的工作原理,有助于深入把握内核内存初始化的完整脉络,以及在驱动开发中遇到早期内存分配问题时的调试思路。