4. 内存碎片化与防碎片技术:伙伴系统、内存规整、CMA
在长期运行的嵌入式系统或服务器中,内存碎片化是导致大块连续内存分配失败的首要原因。本章深入分析碎片产生的根源,并详细阐述 Linux 内核用于对抗碎片化的三大核心技术:伙伴系统 (Buddy System)、内存规整 (Memory Compaction) 和 连续内存分配器 (CMA)。理解这些机制是编写高性能、高可靠 DMA 驱动的基础。
4.1 内存碎片化的根源与分类
内存碎片化分为两种类型,其产生机制和影响范围不同:
| 碎片类型 | 产生原因 | 典型场景 | 对驱动的影响 |
|---|---|---|---|
| 外部碎片 | 频繁分配和释放不同大小的内存块,导致空闲内存被分割成大量不连续的小块。 | 网络驱动频繁分配/释放 sk_buff;文件系统缓存动态调整。 | 即使总空闲内存足够,也无法分配 4MB 以上的连续物理内存,导致 DMA 映射失败。 |
| 内部碎片 | 分配器返回的内存块大于请求的大小,多余部分被浪费。 | 伙伴系统最小分配粒度通常为 4KB(一个页面),请求 1KB 也会占用 4KB。 | 内存利用率降低,但不会直接导致大块分配失败。 |
驱动开发者视角: 外部碎片是 DMA 驱动的主要敌人。当
dma_alloc_coherent() 或 __get_free_pages() 请求高阶 order(如 order=4,即 64KB)时,如果伙伴系统无法合并出连续页面,就会返回 -ENOMEM。此时系统可能仍有大量空闲内存,但都是碎片化的。
4.2 伙伴系统:内存管理的基石与碎片诱因
伙伴系统是 Linux 页分配器的核心算法,它将物理内存按 2 的幂次方划分为不同大小的块(order 0 到 order MAX_ORDER-1)。
- 分配原理: 请求 order N 时,从对应空闲链表查找;若无,则从 order N+1 分裂一个块,一半分配,一半挂入 order N 链表。
- 释放原理: 释放时检查其“伙伴”(地址相邻且大小相同)是否空闲,若是则合并为更高 order 的块,递归进行。
- 碎片化诱因: 当不同生命周期、不同大小的页面被混合分配时,高阶块的合并被低阶页面阻塞,形成外部碎片。
关键数据结构:
struct free_area 维护每个 order 的空闲链表。struct page 中的 private 字段和 mapcount 用于跟踪页面是否可合并。
下图展示了伙伴系统在分配和释放过程中的分裂与合并操作:
4.3 内存规整:主动碎片整理
内存规整(Compaction)是一种在运行时主动移动已分配页面以创建大块连续内存的技术。它通过扫描内存区域,将可迁移的页面迁移到新的位置,然后释放旧的页面框,从而“压缩”出高阶空闲块。
4.3.1 规整的触发时机
- 直接规整 (Direct Compaction): 分配高阶页面失败时,在返回 -ENOMEM 前尝试一次规整。
- 后台规整 (Kcompactd): 内核线程
kcompactd在后台周期性运行,预防碎片化。 - 手动触发: 通过
/proc/sys/vm/compact_memory写入 1 触发全系统规整。
4.3.2 页面迁移类型
伙伴系统将页面分为不同的迁移类型(MIGRATE_TYPES),以控制碎片化:
| 迁移类型 | 标识 | 行为 | 典型页面 |
|---|---|---|---|
| 不可移动 | MIGRATE_UNMOVABLE | 页面物理地址固定,不能被移动。 | 内核分配的内存(如页表、slab 对象)。 |
| 可回收 | MIGRATE_RECLAIMABLE | 页面内容可以回收(如文件缓存),但不能直接移动。 | 文件缓存页、匿名页(可 swap)。 |
| 可移动 | MIGRATE_MOVABLE | 页面内容可以通过页表重映射迁移到新位置。 | 用户空间匿名页、用户空间映射的文件页。 |
| 预留 | MIGRATE_PCPTYPES | 每 CPU 页框缓存,用于快速分配。 | 热页面。 |
驱动注意事项: 使用
kmalloc() 或 __get_free_pages() 分配的内存默认属于 MIGRATE_UNMOVABLE。如果驱动大量使用这些接口分配长期占用的内存,会严重阻碍内存规整的效果。对于大块 DMA 缓冲区,应优先考虑 CMA 或 dma_alloc_coherent()(后者可能使用可移动页面)。
4.4 CMA:为 DMA 预留的连续内存池
连续内存分配器(Contiguous Memory Allocator, CMA)是解决大块连续物理内存分配问题的最终方案。它预留一块物理内存区域,平时允许可移动页面使用,当驱动需要连续内存时,将这些页面迁移走,腾出连续空间。
4.4.1 CMA 工作流程
- 预留: 系统启动时或通过设备树预留一块物理内存(如
cma=64M内核参数)。 - 借用: 预留区域被加入 MIGRATE_CMA 迁移类型,用户空间的可移动页面可以正常分配和使用这些页面。
- 分配: 驱动调用
dma_alloc_coherent()或cma_alloc()请求连续内存。 - 迁移: CMA 管理器扫描预留区域,将正在使用的可移动页面迁移到其他位置。
- 锁定: 迁移完成后,预留区域变为连续空闲块,分配给驱动使用。
- 释放: 驱动释放内存后,页面重新回到 MIGRATE_CMA 池,可被用户空间再次借用。
4.4.2 设备树配置示例
在设备树中为特定设备预留 CMA 池:
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
/* 全局 CMA 池 */
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x0 0x4000000>; /* 64MB */
alignment = <0x0 0x200000>; /* 2MB 对齐 */
linux,cma-default;
};
/* 设备专用 CMA 池 */
vcodec_cma: vcodec_cma@a0000000 {
compatible = "shared-dma-pool";
reusable;
reg = <0x0 0xa0000000 0x0 0x2000000>; /* 32MB @ 0xa0000000 */
};
};
/* 在设备节点中引用 */
&vcodec {
memory-region = <&vcodec_cma>;
};
4.4.3 CMA 与伙伴系统的对比
| 特性 | 伙伴系统 (高阶分配) | CMA |
|---|---|---|
| 内存来源 | 系统全局内存 | 预留给定的物理区域 |
| 分配成功率 | 受碎片化影响,可能失败 | 几乎 100%(只要预留区域未被过度占用) |
| 分配延迟 | 低(直接查找链表) | 高(需要页面迁移,可能触发 I/O) |
| 内存利用率 | 高(全局共享) | 中等(预留区域平时可被借用) |
| 适用场景 | 小到中等大小的连续内存 | 大块连续内存(如摄像头、GPU、VPU 缓冲区) |
最佳实践: 对于 DMA 驱动,优先使用
dma_alloc_coherent() 或 dma_alloc_noncoherent()。这些 API 会自动尝试从 CMA 分配,如果 CMA 不可用则回退到伙伴系统。不要直接调用 cma_alloc(),除非你有特殊的生命周期管理需求。
4.5 防碎片技术的协同工作
在实际系统中,伙伴系统、内存规整和 CMA 协同工作,形成一个多层次的内存分配与碎片防御体系:
- 第一层:伙伴系统 处理绝大多数小到中等大小的分配请求,通过迁移类型分组减少碎片。
- 第二层:内存规整 在高阶分配失败时主动整理碎片,尝试从现有内存中“挤出”连续空间。
- 第三层:CMA 作为最后手段,通过预留和迁移机制保证大块连续内存的可用性。
理解这三层机制,能够帮助驱动开发者在设计内存分配策略时做出正确选择,避免因内存碎片化导致的 DMA 映射失败,从而提升系统的稳定性和可靠性。