CMA 工作原理:分配与释放机制、与伙伴系统的交互、迁移类型与页面迁移
连续内存分配器(CMA)是 Linux 内核为解决碎片化环境下大块连续内存分配难题而设计的核心机制。它并非一个独立的内存分配器,而是与伙伴系统深度协作的预留内存管理框架。理解 CMA 的工作原理,需要从分配与释放的生命周期、与伙伴系统的交互协议、迁移类型以及页面迁移策略四个维度展开。
2.1 CMA 区域的生命周期:分配与释放机制
CMA 区域在系统启动时通过设备树或内核参数预留,但预留的内存并非立即被独占。CMA 的核心设计哲学是“预留但不浪费”:当没有 DMA 或驱动申请大块连续内存时,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 预留内存的占位管理 |
当伙伴系统进行页面分配时,分配器会按照以下优先级顺序遍历迁移类型链表:
- 首先尝试从请求类型对应的链表分配;
- 若失败,则从
MIGRATE_CMA链表“偷取”页面(仅当请求类型为 MOVABLE 时); - 若仍失败,则触发页面 compaction 或直接回收。
这种设计确保了 CMA 区域的内存始终处于“可回收”状态:普通可移动页面可以临时占用 CMA 区域,但当驱动发起 CMA 分配请求时,这些页面必须被迁移出去。
2.3 页面迁移:CMA 分配的核心引擎
页面迁移(page migration)是 CMA 实现“预留但不浪费”的关键技术。当驱动调用 cma_alloc() 时,如果目标区域已被可移动页面占用,内核必须将这些页面迁移到其他位置,腾出连续物理空间。
CMA 的页面迁移流程包含以下步骤:
- 锁定目标区域: 获取 CMA 区域锁,防止并发分配。
- 扫描页面状态: 遍历目标范围内的所有页面,检查是否被锁定、是否属于文件页或匿名页。
- 隔离页面: 将可移动页面从 LRU 链表或文件页缓存中摘除,防止分配器再次使用。
- 分配新页: 从伙伴系统的非 CMA 区域分配新的页面作为迁移目标。
- 复制数据: 对于匿名页,复制页内容并更新页表映射;对于文件页,标记为脏页并等待回写。
- 释放原页: 将原页面归还伙伴系统,标记为 MIGRATE_CMA 类型。
以下 SVG 图展示了 CMA 分配触发页面迁移的完整流程:
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 分配的核心引擎,涉及隔离、分配新页、复制数据和释放原页四个步骤,迁移代价取决于页面类型。