CMA 工作原理:分配与释放机制、与伙伴系统的交互、迁移类型与页面迁移

连续内存分配器(CMA)是 Linux 内核为解决碎片化环境下大块连续内存分配难题而设计的核心机制。它并非一个独立的内存分配器,而是与伙伴系统深度协作的预留内存管理框架。理解 CMA 的工作原理,需要从分配与释放的生命周期、与伙伴系统的交互协议、迁移类型以及页面迁移策略四个维度展开。

2.1 CMA 区域的生命周期:分配与释放机制

CMA 区域在系统启动时通过设备树或内核参数预留,但预留的内存并非立即被独占。CMA 的核心设计哲学是“预留但不浪费”:当没有 DMA 或驱动申请大块连续内存时,CMA 区域的内存可以被伙伴系统用于可移动页面的常规分配;只有当驱动真正需要连续内存时,内核才触发迁移,将当前占用 CMA 区域的页面移走,腾出连续空间。

关键设计原则: CMA 区域的内存属于 MIGRATE_CMA 迁移类型,伙伴系统在分配可移动页面时优先使用该区域,但在分配不可移动页面时必须跳过 CMA 区域。这种隔离机制保证了 CMA 区域始终可以被回收和迁移。

CMA 分配与释放的完整流程如下表所示:

阶段 触发条件 核心操作 返回值
预留 系统启动,解析设备树或内核参数 调用 cma_declare_contiguous() 创建 CMA 区域,将其加入伙伴系统的 MIGRATE_CMA 链表 struct cma *
分配 驱动调用 dma_alloc_coherent() 或 cma_alloc() 在 CMA 区域内查找空闲连续块;若被占用则触发页面迁移;返回物理地址 物理页框号或 NULL
释放 驱动调用 dma_free_coherent() 或 cma_release() 将页面归还 CMA 区域,重新标记为 MIGRATE_CMA 类型,可供伙伴系统再次使用 void
销毁 模块卸载或系统关闭 释放 CMA 描述符,将页面完全交回伙伴系统 void

以下是一个典型的 CMA 分配调用链代码示例,展示了驱动如何通过标准 DMA API 触发 CMA 分配:

/* 驱动分配 CMA 内存的典型路径 */
static int my_driver_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    dma_addr_t dma_handle;
    void *cpu_addr;
    size_t size = SZ_2M;  /* 请求 2MB 连续内存 */

    /* 触发 CMA 分配,内部调用 cma_alloc() */
    cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
    if (!cpu_addr) {
        dev_err(dev, "CMA allocation failed\n");
        return -ENOMEM;
    }

    /* 使用连续内存进行 DMA 操作 */
    // ...

    /* 释放 CMA 内存 */
    dma_free_coherent(dev, size, cpu_addr, dma_handle);
    return 0;
}
工程提示: 驱动应优先使用 dma_alloc_coherent() 而非直接调用 cma_alloc(),因为前者会自动处理 DMA 掩码、缓存一致性以及 IOMMU 映射,而后者仅返回裸物理地址,需要驱动自行管理映射。

2.2 与伙伴系统的交互:MIGRATE_CMA 迁移类型

CMA 与伙伴系统的交互通过 迁移类型(migrate type) 实现。伙伴系统将物理页面按迁移类型分组,每种类型对应一个独立的空闲链表。CMA 区域引入了一种特殊的迁移类型——MIGRATE_CMA,其行为与标准类型有显著差异。

下表对比了 CMA 相关迁移类型的行为:

迁移类型 分配策略 是否允许从 CMA 区域分配 典型用途
MIGRATE_UNMOVABLE 优先从非 CMA 区域分配 否 内核核心代码、页表
MIGRATE_MOVABLE 优先从 CMA 区域分配 是 用户空间匿名页、文件页
MIGRATE_RECLAIMABLE 优先从非 CMA 区域分配 否 文件系统元数据、dentry 缓存
MIGRATE_CMA 仅用于 CMA 区域内部 是(仅限 MOVABLE 页面) CMA 预留内存的占位管理

当伙伴系统进行页面分配时,分配器会按照以下优先级顺序遍历迁移类型链表:

  1. 首先尝试从请求类型对应的链表分配;
  2. 若失败,则从 MIGRATE_CMA 链表“偷取”页面(仅当请求类型为 MOVABLE 时);
  3. 若仍失败,则触发页面 compaction 或直接回收。

这种设计确保了 CMA 区域的内存始终处于“可回收”状态:普通可移动页面可以临时占用 CMA 区域,但当驱动发起 CMA 分配请求时,这些页面必须被迁移出去。

避坑指南: 如果驱动在 CMA 分配期间持有不可移动页面的锁,或者 CMA 区域被不可移动页面污染(例如由于迁移类型配置错误),将导致 CMA 分配长时间阻塞甚至失败。务必确保 CMA 区域只被 MIGRATE_MOVABLE 类型的页面占用。

2.3 页面迁移:CMA 分配的核心引擎

页面迁移(page migration)是 CMA 实现“预留但不浪费”的关键技术。当驱动调用 cma_alloc() 时,如果目标区域已被可移动页面占用,内核必须将这些页面迁移到其他位置,腾出连续物理空间。

CMA 的页面迁移流程包含以下步骤:

  1. 锁定目标区域: 获取 CMA 区域锁,防止并发分配。
  2. 扫描页面状态: 遍历目标范围内的所有页面,检查是否被锁定、是否属于文件页或匿名页。
  3. 隔离页面: 将可移动页面从 LRU 链表或文件页缓存中摘除,防止分配器再次使用。
  4. 分配新页: 从伙伴系统的非 CMA 区域分配新的页面作为迁移目标。
  5. 复制数据: 对于匿名页,复制页内容并更新页表映射;对于文件页,标记为脏页并等待回写。
  6. 释放原页: 将原页面归还伙伴系统,标记为 MIGRATE_CMA 类型。

以下 SVG 图展示了 CMA 分配触发页面迁移的完整流程:

CMA 分配与页面迁移流程 驱动调用 cma_alloc() 检查 CMA 区域空闲块 空闲充足:直接分配 页面被占用:触发迁移 是 否 隔离页面(isolate) 分配新页(migrate_pages) 复制数据 + 更新映射 释放原页,归还 CMA 返回连续物理地址 迁移关键条件 页面必须属于 MOVABLE 类型 目标区域不能有锁竞争

2.4 迁移类型与页面迁移的协同

CMA 的成功依赖于迁移类型与页面迁移机制的紧密协同。迁移类型决定了哪些页面可以占用 CMA 区域,而页面迁移则负责在需要时将这些页面移走。两者共同构成了 CMA 的“软预留”模型。

以下表格总结了不同迁移类型在 CMA 上下文中的行为差异:

迁移类型 能否占用 CMA 区域 CMA 分配时是否需要迁移 迁移代价
MIGRATE_MOVABLE 是 是 低(仅复制页表或文件页引用)
MIGRATE_UNMOVABLE 否(被伙伴系统阻止) 不适用 极高(无法迁移)
MIGRATE_RECLAIMABLE 否(被伙伴系统阻止) 不适用 中(可回收但不可迁移)
内核实现细节: 在 mm/page_alloc.c 中,__rmqueue_cma_fallback() 函数负责从 CMA 区域“偷取”页面给 MOVABLE 分配请求。当 CMA 分配发生时,cma_alloc() 调用 alloc_contig_range(),后者通过 start_isolate_page_range() 隔离目标页面,然后调用 migrate_pages() 执行实际迁移。

页面迁移的代价直接影响 CMA 分配延迟。对于文件页,迁移仅需更新页缓存中的引用;对于匿名页,则需要复制整个页内容并更新所有进程的页表。因此,驱动在调用 CMA 分配时应尽量在系统空闲时进行,避免在中断上下文或持有锁时触发迁移。

优化建议: 对于延迟敏感的驱动,可以在系统初始化阶段通过 cma_alloc() 预分配连续内存并保持持有,避免运行时触发页面迁移。但需注意这会降低 CMA 区域的内存利用率,属于“用空间换时间”的权衡。

2.5 本章小结

CMA 的工作原理围绕“预留-借用-迁移-归还”的闭环设计:

  • 分配与释放机制 通过 cma_alloc() 和 cma_release() 实现,驱动应优先使用 DMA API 间接调用。
  • 与伙伴系统的交互 通过 MIGRATE_CMA 迁移类型完成,伙伴系统仅在分配 MOVABLE 页面时允许使用 CMA 区域。
  • 迁移类型 决定了页面能否占用 CMA 区域,MIGRATE_UNMOVABLE 和 MIGRATE_RECLAIMABLE 被严格禁止进入 CMA 区域。
  • 页面迁移 是 CMA 分配的核心引擎,涉及隔离、分配新页、复制数据和释放原页四个步骤,迁移代价取决于页面类型。