Linux内核启动早期内存管理:memblock分配器从入门到实战

1. 内核启动早期的内存管理挑战

当Linux内核刚开始启动时,系统处于一个特殊状态——完整的内存管理系统尚未初始化。在这个阶段,内核需要一种简单可靠的方式来管理物理内存,这就是memblock分配器诞生的背景。

传统bootmem分配器已经逐渐被memblock取代,主要原因在于memblock提供了更灵活的内存区域管理方式。它通过两个核心数据结构来跟踪内存状态:

  • memory类型:记录所有可用的物理内存区域
  • reserved类型:记录已被分配或预留的内存区域

这种设计使得memblock能够:

  • 处理非连续内存区域
  • 支持NUMA架构
  • 提供内存热插拔基础支持
  • 精确控制内存分配位置
// memblock核心数据结构示例
struct memblock {
    bool bottom_up;  // 分配方向
    phys_addr_t current_limit; // 当前分配限制
    struct memblock_type memory; // 可用内存区域
    struct memblock_type reserved; // 预留内存区域
};

2. memblock的核心工作机制

2.1 内存区域管理策略

memblock采用region-based的内存管理方式,每个内存区域由以下属性定义:

属性说明典型值
base起始物理地址0x80000000
size区域大小0x20000000
flags内存特性标志HOTPLUG/MIRROR等
nidNUMA节点ID0-7

内存添加时会自动执行以下操作:

  1. 检查新区域与现有区域的重叠情况
  2. 合并相邻或重叠的区域
  3. 更新总内存大小统计

2.2 内存分配算法

memblock提供两种分配策略:

  • 自顶向下分配:从高地址向低地址分配
  • 自底向上分配:从低地址向高地址分配

分配过程主要考虑以下因素:

  • 指定的NUMA节点要求
  • 内存对齐要求
  • 分配方向设置
  • 当前分配地址限制
// 典型的内存分配调用链
memblock_alloc() → memblock_alloc_internal() → memblock_find_in_range_node()

2.3 关键API分类

memblock提供的接口可分为四大类:

  1. 内存区域管理

    • memblock_add(): 添加可用内存区域
    • memblock_reserve(): 预留内存区域
    • memblock_remove(): 移除内存区域
  2. 内存分配释放

    • memblock_alloc(): 分配内存
    • memblock_free(): 释放内存
  3. 内存区域遍历

    • for_each_memblock(): 遍历特定类型区域
    • for_each_free_mem_range(): 遍历空闲内存
  4. 辅助功能

    • memblock_set_current_limit(): 设置分配上限
    • memblock_bottom_up(): 设置分配方向

3. memblock在ARM64启动流程中的应用

3.1 内存初始化阶段

在ARM64架构中,memblock的初始化主要经历以下步骤:

  1. 设备树解析:扫描"memory"节点,获取物理内存信息
  2. EFI内存映射处理(如果启用)
  3. 特殊区域标记:如nomap、hotplug等标志
  4. 内核镜像区域预留
// 设备树内存解析示例流程
early_init_dt_scan_memory()
    → early_init_dt_add_memory_arch()
        → memblock_add()

3.2 内存布局调整

内核会根据以下因素调整最终内存布局:

  • 线性映射区域大小限制
  • 内存大小参数(mem=xxx)
  • 初始化ramdisk位置
  • crashkernel保留区域
  • DMA连续内存需求

典型调整操作包括:

memblock_remove()  // 移除不可用区域
memblock_cap_memory_range() // 限制内存范围
memblock_reserve() // 保留关键区域

3.3 页表构建阶段

在paging_init()中,memblock发挥关键作用:

  1. 为内核镜像建立映射
  2. 为线性映射区域分配页表内存
  3. 处理特殊内存区域(如nomap)
  4. 释放初始页表占用的内存

4. memblock高级使用技巧

4.1 调试与问题排查

当memblock出现问题时,可以使用以下调试手段:

  1. 打印当前内存布局

    memblock_dump_all();
    
  2. 检查特定地址状态

    if (memblock_is_reserved(addr)) {
        // 处理预留区域
    }
    
  3. 内存测试

    early_memtest(phys_addr, size);
    

4.2 性能优化建议

  1. 合理设置分配方向:根据内存使用模式选择bottom-up或top-down
  2. 尽早设置内存限制:避免不必要的内存扫描
  3. 合并内存添加操作:减少region合并开销
  4. 预分配关键内存:如页表所需内存

4.3 与后续内存管理器的交接

memblock最终会将内存移交给伙伴系统,关键步骤包括:

  1. 释放可用内存到伙伴系统

    memblock_free_all()
        → __free_pages_core()
    
  2. 保留memblock元数据:用于pfn_valid()等检查

  3. 释放memblock自身内存:通过memblock_discard()

5. 实战案例:自定义内存分配策略

5.1 实现特定区域优先分配

有时需要优先从特定内存区域分配,可通过以下方式实现:

phys_addr_t my_alloc_in_zone(phys_addr_t size, phys_addr_t align, 
                           phys_addr_t zone_start, phys_addr_t zone_end)
{
    phys_addr_t addr;
    
    // 首先尝试在指定区域分配
    addr = memblock_alloc_range(size, align, zone_start, zone_end);
    if (addr)
        return addr;
        
    // 失败则尝试全范围分配
    return memblock_alloc(size, align);
}

5.2 处理特殊内存设备

对于需要特殊处理的内存设备(如持久内存),可以:

  1. 在设备树中标记特殊标志
  2. 启动时通过memblock_mark_*()标记区域
  3. 在驱动中检查内存标志
  4. 提供定制化的分配接口

5.3 内存热插拔支持

memblock为内存热插拔提供基础支持:

  1. 标记热插拔区域

    memblock_mark_hotplug(base, size);
    
  2. 热插拔事件处理

    // 添加新内存区域
    memblock_add_node(base, size, nid);
    
    // 移除内存区域 
    memblock_remove(base, size);
    

6. 常见问题解决方案

问题1:内存分配失败

可能原因及解决方案:

  • 内存不足 → 检查memblock布局,确认可用内存
  • 分配限制过严 → 调整current_limit
  • 对齐要求过高 → 降低对齐值或预分配

问题2:内存区域碎片化

优化建议:

  • 合并相邻的预留区域
  • 调整分配顺序
  • 尽早预留大块内存

问题3:NUMA节点分配不均

解决方法:

  • 明确指定nid参数
  • 检查NUMA初始化代码
  • 平衡各节点预留内存

7. 性能调优实战

通过一个实际优化案例展示如何改进memblock使用:

场景:嵌入式设备启动时内存分配耗时过长

分析步骤:

  1. 测量memblock操作耗时
  2. 发现频繁的小块分配
  3. 确认存在大量region合并操作

优化方案:

  1. 预分配关键数据结构所需内存
  2. 调整内存添加顺序减少合并
  3. 设置合理的bottom_up参数

效果:启动时间减少15%,内存分配更加集中

// 优化后的内存初始化代码示例
void optimized_memblock_init(void)
{
    // 首先添加所有大块内存
    memblock_add(0x80000000, 0x20000000);
    
    // 预分配关键内存
    void *ptr = memblock_alloc(0x100000, PAGE_SIZE);
    
    // 设置分配策略
    memblock_set_bottom_up(true);
}

对于内核开发者而言,深入理解memblock的工作机制能够帮助解决启动阶段的各类内存问题。在实际项目中,我曾遇到一个棘手的内存冲突问题——某硬件模块无法正常工作,最终发现是因为其使用的内存区域未被正确预留。通过添加适当的memblock_reserve()调用,问题得以解决。这提醒我们,在开发早期就要充分考虑各模块的内存需求,并在启动流程中做好相应的预留工作。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐