Linux内核启动早期内存管理:memblock分配器从入门到实战
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等 |
| nid | NUMA节点ID | 0-7 |
内存添加时会自动执行以下操作:
- 检查新区域与现有区域的重叠情况
- 合并相邻或重叠的区域
- 更新总内存大小统计
2.2 内存分配算法
memblock提供两种分配策略:
- 自顶向下分配:从高地址向低地址分配
- 自底向上分配:从低地址向高地址分配
分配过程主要考虑以下因素:
- 指定的NUMA节点要求
- 内存对齐要求
- 分配方向设置
- 当前分配地址限制
// 典型的内存分配调用链
memblock_alloc() → memblock_alloc_internal() → memblock_find_in_range_node()
2.3 关键API分类
memblock提供的接口可分为四大类:
-
内存区域管理
- memblock_add(): 添加可用内存区域
- memblock_reserve(): 预留内存区域
- memblock_remove(): 移除内存区域
-
内存分配释放
- memblock_alloc(): 分配内存
- memblock_free(): 释放内存
-
内存区域遍历
- for_each_memblock(): 遍历特定类型区域
- for_each_free_mem_range(): 遍历空闲内存
-
辅助功能
- memblock_set_current_limit(): 设置分配上限
- memblock_bottom_up(): 设置分配方向
3. memblock在ARM64启动流程中的应用
3.1 内存初始化阶段
在ARM64架构中,memblock的初始化主要经历以下步骤:
- 设备树解析:扫描"memory"节点,获取物理内存信息
- EFI内存映射处理(如果启用)
- 特殊区域标记:如nomap、hotplug等标志
- 内核镜像区域预留
// 设备树内存解析示例流程
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发挥关键作用:
- 为内核镜像建立映射
- 为线性映射区域分配页表内存
- 处理特殊内存区域(如nomap)
- 释放初始页表占用的内存
4. memblock高级使用技巧
4.1 调试与问题排查
当memblock出现问题时,可以使用以下调试手段:
-
打印当前内存布局
memblock_dump_all(); -
检查特定地址状态
if (memblock_is_reserved(addr)) { // 处理预留区域 } -
内存测试
early_memtest(phys_addr, size);
4.2 性能优化建议
- 合理设置分配方向:根据内存使用模式选择bottom-up或top-down
- 尽早设置内存限制:避免不必要的内存扫描
- 合并内存添加操作:减少region合并开销
- 预分配关键内存:如页表所需内存
4.3 与后续内存管理器的交接
memblock最终会将内存移交给伙伴系统,关键步骤包括:
-
释放可用内存到伙伴系统
memblock_free_all() → __free_pages_core() -
保留memblock元数据:用于pfn_valid()等检查
-
释放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 处理特殊内存设备
对于需要特殊处理的内存设备(如持久内存),可以:
- 在设备树中标记特殊标志
- 启动时通过memblock_mark_*()标记区域
- 在驱动中检查内存标志
- 提供定制化的分配接口
5.3 内存热插拔支持
memblock为内存热插拔提供基础支持:
-
标记热插拔区域
memblock_mark_hotplug(base, size); -
热插拔事件处理
// 添加新内存区域 memblock_add_node(base, size, nid); // 移除内存区域 memblock_remove(base, size);
6. 常见问题解决方案
问题1:内存分配失败
可能原因及解决方案:
- 内存不足 → 检查memblock布局,确认可用内存
- 分配限制过严 → 调整current_limit
- 对齐要求过高 → 降低对齐值或预分配
问题2:内存区域碎片化
优化建议:
- 合并相邻的预留区域
- 调整分配顺序
- 尽早预留大块内存
问题3:NUMA节点分配不均
解决方法:
- 明确指定nid参数
- 检查NUMA初始化代码
- 平衡各节点预留内存
7. 性能调优实战
通过一个实际优化案例展示如何改进memblock使用:
场景:嵌入式设备启动时内存分配耗时过长
分析步骤:
- 测量memblock操作耗时
- 发现频繁的小块分配
- 确认存在大量region合并操作
优化方案:
- 预分配关键数据结构所需内存
- 调整内存添加顺序减少合并
- 设置合理的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()调用,问题得以解决。这提醒我们,在开发早期就要充分考虑各模块的内存需求,并在启动流程中做好相应的预留工作。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)