3. CMA与DMA的关系:DMA对物理连续性的要求与CMA的支撑机制
在嵌入式Linux系统中,DMA(Direct Memory Access)是高性能外设与内存之间进行批量数据传输的核心机制。DMA控制器通常不具备MMU(内存管理单元)的页表转换能力,因此它要求传输缓冲区在物理地址空间上必须是连续的。这一硬性约束使得系统在长时间运行后,面对内存碎片化严重的场景,难以分配满足DMA需求的大块连续物理内存。CMA(Contiguous Memory Allocator)正是为解决这一矛盾而设计的——它在系统初始化阶段预留一块连续的物理内存区域,并在运行时通过页面迁移技术,为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页面迁移到其他位置,从而“腾出”连续的物理空间。
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() 时,内核内部的调用链如下:
- dma_alloc_coherent() → 根据设备dma_mask选择合适的内存区域
- dma_alloc_attrs() → 解析分配属性(是否要求连续、是否可迁移)
- dma_direct_alloc() → 尝试从常规伙伴系统分配(可能失败)
- cma_alloc() → 从CMA区域分配连续物理页(核心步骤)
- cma_alloc_contig_range() → 执行页面迁移,腾出连续空间
- 返回物理连续页框 → 建立内核虚拟映射 → 返回虚拟地址和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密集 |
3.5 多DMA外设共享CMA区域的调度策略
当系统中存在多个DMA外设(如SD卡、以太网、GPU、USB控制器)时,它们共享同一个CMA区域。内核通过互斥锁和等待队列来协调并发分配请求:
并发分配时的关键调度逻辑:
- 互斥锁(cma_mutex):确保同一时刻只有一个分配请求在执行页面迁移,防止多个迁移操作互相干扰。
- 等待队列:当CMA区域正在被迁移时,其他DMA外设的分配请求会进入等待队列,直到当前迁移完成。
- 优先级反转:高优先级DMA外设(如音频)可能被低优先级外设(如SD卡)的迁移操作阻塞,需要通过
dma_alloc_attrs()的DMA_ATTR_PRIORITY属性来提升优先级。 - 超时机制:如果迁移时间过长(默认10秒),内核会触发警告并可能返回分配失败,驱动需要处理这种异常情况。
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的协同关系,是嵌入式系统内存性能优化的关键基础。