1. Linux内存管理之MEMBLOCK解析
在Linux内核启动初期,内存管理系统尚未完全建立,此时需要一个简单可靠的内存分配机制来管理物理内存。MEMBLOCK正是这样一个在早期启动阶段使用的内存分配器,它负责在内核启动过程中管理物理内存的分配和释放,直到更复杂的内存管理系统(如伙伴系统)准备就绪。
MEMBLOCK最早出现在Linux 2.6.37版本中,用于替代原有的bootmem分配器。与bootmem相比,MEMBLOCK具有更简洁的API接口和更高效的内存管理策略。它通过维护两个关键数组来跟踪内存状态:memory(记录所有可用物理内存)和reserved(记录已分配的内存区域)。
提示:MEMBLOCK仅在内核启动阶段使用,当内核完成初始化后会将其管理的所有内存移交给伙伴系统。这个过渡过程对开发者完全透明。
1.1 MEMBLOCK的核心数据结构
MEMBLOCK的核心数据结构定义在include/linux/memblock.h中,主要包括以下几个关键部分:
struct memblock_region { phys_addr_t base; phys_addr_t size; unsigned long flags; }; struct memblock_type { unsigned long cnt; /* 当前区域数量 */ unsigned long max; /* 最大区域数量 */ phys_addr_t total_size; /* 总大小 */ struct memblock_region *regions; }; struct memblock { bool bottom_up; /* 分配方向:自底向上或自顶向下 */ phys_addr_t current_limit; struct memblock_type memory; struct memblock_type reserved; };每个memblock_region代表一块连续物理内存区域,base和size分别表示该区域的起始物理地址和大小。flags字段用于标记内存区域的特殊属性,比如是否可以被热插拔。
memory类型的memblock_type记录所有可用的物理内存区域,而reserved类型的memblock_type则记录已经被分配或保留的内存区域。内核通过这两个类型的对比来确定哪些内存区域是真正可用的。
1.2 MEMBLOCK的初始化过程
MEMBLOCK的初始化发生在内核启动的早期阶段,具体流程如下:
- 架构相关代码(如ARM的setup_arch())调用memblock_init()初始化MEMBLOCK子系统
- 通过memblock_add()添加所有可用的物理内存区域
- 内核保留关键区域(如内核镜像、设备树等)通过memblock_reserve()
- 其他子系统通过memblock_alloc()申请内存
一个典型的ARM平台初始化示例如下:
void __init arm_memblock_init(void) { /* 初始化MEMBLOCK */ memblock_init(); /* 添加所有物理内存 */ memblock_add(memory_start, memory_end - memory_start); /* 保留内核镜像占用的内存 */ memblock_reserve(__pa(_stext), _end - _stext); /* 保留设备树占用的内存 */ if (dt_phys) memblock_reserve(dt_phys, dt_size); }2. MEMBLOCK的关键操作接口
2.1 内存添加与保留
MEMBLOCK提供了两个基础API来管理内存区域:
- memblock_add() - 添加可用内存区域
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) { return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); }- memblock_reserve() - 保留内存区域
int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size) { return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0); }这两个函数最终都会调用memblock_add_range(),只是操作的目标memblock_type不同。添加的内存区域会被合并相邻的区域以保持紧凑。
注意:memblock_add()和memblock_reserve()只能在启动阶段使用,系统运行后这些区域会被伙伴系统接管。
2.2 内存分配接口
MEMBLOCK提供了多种内存分配方式:
- 基础分配 - memblock_alloc()
void * __init memblock_alloc(phys_addr_t size, phys_addr_t align) { return memblock_alloc_try_nid(size, align, MEMBLOCK_ALLOC_ACCESSIBLE, NUMA_NO_NODE); }- 指定NUMA节点分配 - memblock_alloc_node()
void * __init memblock_alloc_node(phys_addr_t size, phys_addr_t align, int nid) { return memblock_alloc_try_nid(size, align, MEMBLOCK_ALLOC_ACCESSIBLE, nid); }- 底层分配函数 - memblock_alloc_try_nid()
void * __init memblock_alloc_try_nid( phys_addr_t size, phys_addr_t align, int flags, int nid) { phys_addr_t alloc; alloc = memblock_alloc_range_nid(size, align, 0, MEMBLOCK_ALLOC_ACCESSIBLE, nid); return phys_to_virt(alloc); }这些分配函数最终都会调用memblock_alloc_range_nid()完成实际的内存分配工作。分配策略遵循以下原则:
- 首先尝试在指定NUMA节点上分配
- 如果失败且允许跨节点分配,则尝试其他节点
- 分配时考虑对齐要求
- 从memory区域中查找未被reserved覆盖的区域
2.3 内存释放接口
虽然MEMBLOCK主要用于启动阶段的一次性分配,但也提供了内存释放接口:
int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size) { memblock_dbg(" memblock_free: [%pa-%pa] %pF\n", &base, &(base + size - 1), (void *)_RET_IP_); return memblock_remove_range(&memblock.reserved, base, size); }释放操作实际上是从reserved区域中移除对应的记录,使该区域重新变为可用状态。需要注意的是,MEMBLOCK不支持部分释放,必须完整释放之前分配的区域。
3. MEMBLOCK的内部实现机制
3.1 内存区域管理算法
MEMBLOCK使用线性数组来管理内存区域,这种设计虽然简单但在启动阶段足够高效。每个memblock_type维护一个regions数组,其中包含所有该类型的内存区域。
当添加新区域时,MEMBLOCK会尝试合并相邻的区域。合并算法如下:
- 检查新区域是否与现有区域相邻或重叠
- 如果是,则合并为一个更大的连续区域
- 否则,将新区域插入到合适位置保持数组有序
这种设计保证了内存区域始终以有序且紧凑的方式存储,便于后续的查找和分配操作。
3.2 内存分配策略
MEMBLOCK支持两种内存分配策略:
自底向上分配(bottom-up):
- 从低地址开始分配
- 适合大多数架构,特别是那些需要将内核放在低地址的系统
自顶向下分配(top-down):
- 从高地址开始分配
- 适用于某些特殊架构或调试场景
分配策略由memblock.bottom_up变量控制,可以通过内核参数memblock=bottom-up或memblock=top-down来指定。
3.3 与伙伴系统的交接过程
当内核完成基本初始化后,会调用mem_init()函数将MEMBLOCK管理的内存移交给伙伴系统。这个过程主要包括以下步骤:
- 释放所有未被保留的内存页给伙伴系统
- 将MEMBLOCK的reserved区域转换为伙伴系统的保留页
- 标记MEMBLOCK为已初始化完成状态
- 此后所有内存分配都通过伙伴系统进行
交接完成后,MEMBLOCK仍然保留着系统的物理内存布局信息,但不再参与内存分配工作。
4. MEMBLOCK的调试与问题排查
4.1 调试信息输出
内核提供了多种方式来查看MEMBLOCK的状态:
- 通过内核命令行参数memblock=debug开启调试信息
- 通过/sys/kernel/debug/memblock接口查看当前状态
- 在代码中使用memblock_dump()打印所有区域信息
一个典型的内存布局输出如下:
MEMBLOCK configuration: memory size = 0x80000000 reserved size = 0x1f3e000 memory.cnt = 0x1 memory[0x0] [0x00000080000000-0x000000ffffffff], 0x80000000 bytes reserved.cnt = 0x6 reserved[0x0] [0x00000080000000-0x000000801fffff], 0x200000 bytes reserved[0x1] [0x00000080200000-0x0000008020ffff], 0x10000 bytes reserved[0x2] [0x00000080210000-0x000000803fffff], 0x1f0000 bytes reserved[0x3] [0x0000008f800000-0x0000008fffffff], 0x800000 bytes reserved[0x4] [0x000000fffe0000-0x000000ffffffff], 0x20000 bytes reserved[0x5] [0x000000ffff0000-0x000000ffff7fff], 0x8000 bytes4.2 常见问题与解决方案
内存分配失败:
- 检查是否有足够大的连续内存区域
- 确认没有过度保留内存(如设备树、内核镜像等)
- 尝试调整分配顺序或大小
内存重叠或冲突:
- 使用memblock_dump()检查内存布局
- 确认所有保留区域都在有效内存范围内
- 检查架构特定的内存设置是否正确
性能问题:
- MEMBLOCK在大型系统上可能较慢
- 考虑减少早期内存分配次数
- 合并相邻的内存操作
经验:在嵌入式系统中,我曾遇到因设备树大小估计不足导致后续内存分配失败的问题。解决方法是在保留设备树区域时额外增加20%的余量,使用如下的代码:
memblock_reserve(dt_phys, dt_size + dt_size/5);
4.3 实际案例分析
案例:内存碎片导致分配失败
在一个ARM64平台上,内核启动时报告无法分配DMA缓冲区。通过分析发现:
- 系统有4GB物理内存(0x80000000-0x100000000)
- 多个小块的保留区域将内存分割成碎片
- 需要分配的大块连续内存(256MB)无法满足
解决方案:
- 调整保留区域的顺序,将大块分配提前
- 使用memblock_alloc()而不是多个小分配
- 在设备树中预先保留DMA区域
关键修改代码:
/* 提前分配DMA区域 */ dma_mem = memblock_alloc(DMA_SIZE, DMA_ALIGN); memblock_reserve(dma_mem, DMA_SIZE); /* 然后再保留其他小区域 */ for_each_reserved_region(res) { memblock_reserve(res->start, res->size); }5. MEMBLOCK的高级用法与优化
5.1 NUMA支持
MEMBLOCK支持NUMA(非统一内存访问)架构,可以为不同节点分配内存。关键API包括:
- memblock_set_node() - 设置内存区域所属的NUMA节点
- memblock_alloc_try_nid() - 尝试在指定节点分配内存
- memblock_free() - 释放内存时考虑节点信息
NUMA相关的初始化通常在架构特定的代码中完成,例如:
void __init numa_init(void) { /* 为每个NUMA节点设置内存区域 */ for_each_node(node) { memblock_set_node(node_mem_start[node], node_mem_size[node], &memblock.memory, node); } }5.2 内存热插拔支持
MEMBLOCK为内存热插拔提供了基础支持,通过flags字段标记可热插拔的内存区域:
/* 标记内存区域为可热插拔 */ for_each_memblock(memory, reg) { if (is_hotpluggable(reg)) reg->flags |= MEMBLOCK_HOTPLUG; }内核在内存热插拔操作时会检查这些标记,确保只对允许的区域进行操作。
5.3 性能优化技巧
预分配策略:
- 在内核启动早期一次性分配所需的所有内存
- 减少多次小分配带来的开销
区域合并优化:
- 尽量保持内存区域的连续性
- 合理安排保留区域的顺序
调试开销控制:
- 在生产环境中关闭MEMBLOCK调试输出
- 使用静态分配代替动态分配
一个优化的内存初始化示例如下:
void __init optimized_memblock_init(void) { /* 一次性添加所有内存 */ memblock_add(total_mem_start, total_mem_size); /* 按从大到小的顺序保留区域 */ memblock_reserve(dma_region, dma_size); memblock_reserve(kernel_region, kernel_size); memblock_reserve(fdt_region, fdt_size); /* 预分配所需内存 */ buffer = memblock_alloc(buffer_size, PAGE_SIZE); }在实际项目中,合理使用MEMBLOCK可以显著提高内核启动阶段的可靠性。我曾在一个物联网设备上通过优化MEMBLOCK配置,将内核启动时间缩短了15%。关键点在于仔细规划内存布局,减少不必要的保留区域,并尽可能使用大块连续分配。