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 用于跟踪页面是否可合并。

下图展示了伙伴系统在分配和释放过程中的分裂与合并操作:

伙伴系统分裂与合并示意图 伙伴系统:分裂与合并流程 初始空闲块 (order 3, 32KB) 请求 order 1 (4KB) order 2 (16KB) - 继续分裂 order 2 (16KB) - 空闲 order 1 (8KB) - 分配 order 1 (8KB) - 空闲 释放 order 1 合并为 order 2 (16KB)

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 工作流程

  1. 预留: 系统启动时或通过设备树预留一块物理内存(如 cma=64M 内核参数)。
  2. 借用: 预留区域被加入 MIGRATE_CMA 迁移类型,用户空间的可移动页面可以正常分配和使用这些页面。
  3. 分配: 驱动调用 dma_alloc_coherent() 或 cma_alloc() 请求连续内存。
  4. 迁移: CMA 管理器扫描预留区域,将正在使用的可移动页面迁移到其他位置。
  5. 锁定: 迁移完成后,预留区域变为连续空闲块,分配给驱动使用。
  6. 释放: 驱动释放内存后,页面重新回到 MIGRATE_CMA 池,可被用户空间再次借用。
CMA 分配与迁移流程 CMA 连续内存分配流程 阶段 1:CMA 预留区域 (MIGRATE_CMA),被用户可移动页面占用 驱动请求连续内存 阶段 2:CMA 管理器扫描并迁移占用页面到其他区域 阶段 3:预留区域变为连续空闲块,分配给驱动 阶段 4:驱动使用连续内存进行 DMA 操作,完成后释放

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 映射失败,从而提升系统的稳定性和可靠性。