3. CMA与DMA的关系:DMA对物理连续性的要求与CMA的支撑机制

在嵌入式Linux系统中,DMA(Direct Memory Access)是高性能外设与内存之间进行批量数据传输的核心机制。DMA控制器通常不具备MMU(内存管理单元)的页表转换能力,因此它要求传输缓冲区在物理地址空间上必须是连续的。这一硬性约束使得系统在长时间运行后,面对内存碎片化严重的场景,难以分配满足DMA需求的大块连续物理内存。CMA(Contiguous Memory Allocator)正是为解决这一矛盾而设计的——它在系统初始化阶段预留一块连续的物理内存区域,并在运行时通过页面迁移技术,为DMA驱动提供稳定、可靠的大块连续缓冲区。

核心矛盾: 虚拟内存可以映射非连续的物理页,但DMA硬件直接访问物理总线地址,无法处理分散的物理页框。因此,DMA驱动必须从物理连续的内存区域中申请缓冲区。

3.1 DMA对物理连续性的硬性要求

DMA传输的最小粒度通常是Cache Line(32/64字节),但缓冲区大小往往从几KB到数MB不等。DMA控制器在传输过程中,通过递增物理地址来访问下一个数据单元。如果物理地址不连续,DMA控制器将访问到错误的地址空间,导致数据损坏或系统崩溃。

DMA传输类型 典型缓冲区大小 物理连续性要求 碎片化影响
SD/MMC 控制器 4KB ~ 2MB 严格连续 高碎片化时分配失败
USB 控制器 (EHCI/XHCI) 64KB ~ 8MB 严格连续 长时间运行后分配延迟增大
GPU/显示控制器 4MB ~ 32MB 严格连续 大块连续内存稀缺
以太网 DMA (GMAC) 1.5KB ~ 64KB 严格连续 小碎片累积导致分配失败
音频 I2S/PCM 4KB ~ 128KB 严格连续 实时性要求下分配延迟敏感
避坑提示: 不要认为虚拟地址连续就等同于物理地址连续。使用 vmalloc() 或 kmap() 得到的虚拟连续区域,其物理页框可能是分散的,不能直接用于DMA传输。必须使用 dma_alloc_coherent() 或 dma_map_single() 等DMA API来确保物理连续性。

3.2 CMA如何为DMA提供稳定的大块连续内存

CMA的工作机制可以概括为三个关键阶段:预留、隔离、迁移。系统启动时,CMA从整个物理内存中划分出一块连续的物理区域(通过内核启动参数 cma= 或设备树配置)。这块区域在大多数时间被内核用于可移动页面的分配(如匿名页、文件缓存),但当DMA驱动发起连续内存申请时,CMA会执行页面迁移,将当前占用该区域的非DMA页面迁移到其他位置,从而“腾出”连续的物理空间。

CMA为DMA提供连续内存的工作流程 阶段1:系统启动预留 cma=size@start 或 DTS配置 阶段2:运行时隔离 CMA区域标记为MIGRATE_CMA 阶段3:DMA申请触发 dma_alloc_coherent() 调用 页面迁移子流程(核心机制) 扫描CMA区域 查找可移动页面 迁移到其他区域 页表更新+TLB刷新 释放连续物理块 合并为连续区域 返回 DMA缓冲区 最终结果:DMA驱动获得物理连续缓冲区 物理地址连续 | 大小满足需求 | 无碎片化干扰 关键参数 cma=size@start kernel/dma/contiguous.c 性能指标 迁移延迟:100μs ~ 10ms 成功率:>99.9%(配置合理时)

3.3 CMA与DMA API的协同调用链

在Linux内核中,DMA驱动通常通过以下标准API来申请连续内存,这些API底层会优先尝试从CMA区域分配:

/* 典型DMA缓冲区申请路径 */
#include <linux/dma-mapping.h>

/* 1. 一致性DMA缓冲区(同时保证cache一致性) */
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* 2. 流式DMA映射(需要手动管理cache一致性) */
dma_addr_t dma_handle = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);

/* 3. 分散/聚集DMA(非连续物理页,但DMA支持SG模式) */
int sg_len = dma_map_sg(dev, sglist, nents, DMA_BIDIRECTIONAL);

当调用 dma_alloc_coherent() 时,内核内部的调用链如下:

  1. dma_alloc_coherent() → 根据设备dma_mask选择合适的内存区域
  2. dma_alloc_attrs() → 解析分配属性(是否要求连续、是否可迁移)
  3. dma_direct_alloc() → 尝试从常规伙伴系统分配(可能失败)
  4. cma_alloc() → 从CMA区域分配连续物理页(核心步骤)
  5. cma_alloc_contig_range() → 执行页面迁移,腾出连续空间
  6. 返回物理连续页框 → 建立内核虚拟映射 → 返回虚拟地址和DMA地址
最佳实践: 在驱动中使用 dma_alloc_coherent() 时,建议在 probe() 阶段预分配缓冲区,而不是在每次I/O操作时动态申请。这样可以避免运行时页面迁移带来的延迟抖动,尤其适用于音频、视频等实时性要求高的场景。

3.4 CMA区域配置对DMA性能的影响

CMA区域的大小和位置直接影响DMA缓冲区的分配成功率和延迟。以下是不同配置策略的对比:

配置策略 CMA大小 分配成功率 迁移延迟 适用场景
保守型 总内存的1%~3% 中等(80%~90%) 低(100μs~1ms) 嵌入式设备,内存紧张
均衡型 总内存的5%~10% 高(95%~99%) 中(500μs~5ms) 多媒体设备,多DMA外设
激进型 总内存的15%~25% 极高(>99.9%) 高(1ms~20ms) 高性能计算,GPU/VPU密集
避坑提示: CMA区域并非越大越好。过大的CMA区域会减少普通可分配内存的总量,导致系统在非DMA场景下出现内存压力。同时,过大的CMA区域在迁移时会产生更长的延迟,可能影响实时任务的调度。建议根据实际DMA缓冲区峰值需求,预留1.5~2倍的安全余量。

3.5 多DMA外设共享CMA区域的调度策略

当系统中存在多个DMA外设(如SD卡、以太网、GPU、USB控制器)时,它们共享同一个CMA区域。内核通过互斥锁和等待队列来协调并发分配请求:

多DMA外设共享CMA区域的并发调度 SD/MMC 控制器 申请 2MB 缓冲区 以太网 GMAC 申请 64KB 缓冲区 GPU 显示控制器 申请 8MB 缓冲区 USB XHCI 控制器 申请 1MB 缓冲区 CMA 核心调度器 互斥锁保护 等待队列管理 页面迁移引擎 碎片整理触发 分配超时处理 (kernel/dma/contiguous.c) CMA 物理内存区域 [ 连续物理地址空间 ] 基地址:0x4C00_0000 大小:64MB 当前使用:42MB 剩余连续块:3块 (最大连续块:16MB)

并发分配时的关键调度逻辑:

  • 互斥锁(cma_mutex):确保同一时刻只有一个分配请求在执行页面迁移,防止多个迁移操作互相干扰。
  • 等待队列:当CMA区域正在被迁移时,其他DMA外设的分配请求会进入等待队列,直到当前迁移完成。
  • 优先级反转:高优先级DMA外设(如音频)可能被低优先级外设(如SD卡)的迁移操作阻塞,需要通过 dma_alloc_attrs() 的 DMA_ATTR_PRIORITY 属性来提升优先级。
  • 超时机制:如果迁移时间过长(默认10秒),内核会触发警告并可能返回分配失败,驱动需要处理这种异常情况。
工程经验: 在多媒体SoC中,建议为实时性要求高的DMA外设(如显示控制器、音频I2S)单独配置专用的CMA区域,通过设备树中的 shared-dma-pool 属性实现隔离。这样可以避免非实时外设的迁移操作影响关键任务的DMA传输。

3.6 CMA分配失败时的降级策略

尽管CMA设计为高成功率分配器,但在极端内存压力下仍可能失败。驱动开发者需要实现降级策略:

/* CMA分配失败时的降级处理示例 */
void *safe_dma_alloc(struct device *dev, size_t size, dma_addr_t *dma_handle)
{
    void *buf;

    /* 1. 首选:从CMA分配(物理连续) */
    buf = dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL);
    if (buf)
        return buf;

    /* 2. 降级:从伙伴系统分配(可能触发直接回收) */
    buf = dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL | __GFP_RETRY_MAYFAIL);
    if (buf)
        return buf;

    /* 3. 紧急降级:使用分散/聚集DMA(需要硬件支持SG模式) */
    if (dev->dma_parms && dev->dma_parms->max_segment_size >= size) {
        /* 使用dma_map_sg()进行分散/聚集传输 */
        pr_warn("CMA allocation failed, falling back to SG DMA\n");
        return NULL; /* 实际应返回SG列表 */
    }

    /* 4. 最终降级:返回错误,由上层应用处理 */
    pr_err("DMA buffer allocation failed for size %zu\n", size);
    return NULL;
}
调试技巧: 通过 /sys/kernel/debug/cma/ 目录下的调试接口,可以实时查看CMA区域的使用情况、碎片程度和分配延迟。在开发阶段,建议开启 CONFIG_CMA_DEBUG 和 CONFIG_CMA_DEBUGFS 内核配置选项,以便快速定位分配失败的原因。

3.7 本章小结

CMA与DMA的关系可以概括为:DMA硬件对物理连续性的硬性需求,驱动了CMA机制的产生;CMA通过预留、隔离、迁移三个核心步骤,为DMA驱动提供了稳定的大块连续内存保障。在实际工程中,开发者需要根据系统内存总量、DMA外设数量和实时性要求,合理配置CMA区域的大小和位置,并在驱动层实现完善的降级策略,以应对极端情况下的分配失败。理解CMA与DMA的协同关系,是嵌入式系统内存性能优化的关键基础。