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从初始化到移交的完整生命周期:

memblock生命周期与角色定位 memblock生命周期与角色定位 阶段1:初始化 memblock_add() 阶段2:记录 memory + reserved 阶段3:分配 memblock_alloc() 阶段4:移交 mem_init() 阶段5:退役 memblock数据释放 伙伴系统接管 alloc_pages() 时间窗口 start_kernel → mm_init 约持续100~500ms (取决于硬件复杂度) 核心能力 • 物理地址连续分配 • 支持NUMA节点感知 • 区域合并与分裂

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相关的典型问题包括:

  1. 内存区域重叠:不同设备树节点或固件接口报告了重叠的物理地址范围,导致memblock_add()返回错误。
  2. 预留不足:某些DMA缓冲区或安全监控区域未在memblock_reserve()中声明,被后续分配覆盖。
  3. 分配失败:memblock_alloc()在指定NUMA节点或地址范围内找不到足够大的连续空闲区域。
  4. 启动内存泄漏:驱动使用memblock_alloc()后未释放,导致伙伴系统可用内存减少。
调试入口:内核启动参数 memblock=debug 可以开启memblock的详细日志,打印每次add/reserve/alloc操作。这是定位启动阶段内存问题的第一把钥匙。

1.8 小结:memblock的工程定位

memblock不是一种“高性能”分配器,而是一种“高确定性”的启动阶段内存管理方案。它牺牲了分配速度和灵活性,换取了以下关键能力:

  • 零依赖:不依赖页表、cache、锁或任何内核子系统。
  • 全可见:任何时候都可以遍历所有内存区域,知道“谁占用了哪里”。
  • 可审计:所有分配和预留操作都有记录,便于启动阶段内存布局验证。
  • 平滑过渡:在伙伴系统就绪后,能够干净地将管理权移交,不留残留。