3、DMA与缓存一致性:DMA传输中的一致性问题、Streaming DMA映射与一致性DMA映射、dma_alloc_coherent与dma_map_single详解
3.1 DMA传输中的缓存一致性问题
DMA(Direct Memory Access)控制器直接访问物理内存,而CPU通过缓存(Cache)访问数据。当DMA与CPU共享同一块内存区域时,若缓存与内存中的数据不一致,将导致数据损坏或逻辑错误。这是驱动开发中最常见且最隐蔽的问题之一。
核心矛盾:CPU通过Cache读写数据(写回策略),DMA直接读写物理内存(不经过Cache)。两者看到的“内存视图”不同步。
3.1.1 一致性问题场景分析
| 场景 | 操作顺序 | 问题描述 |
|---|---|---|
| CPU写 → DMA读 | CPU写入数据 → 启动DMA从内存读取 | CPU写入的数据可能仍在Cache中,未写回内存,DMA读到旧数据 |
| DMA写 → CPU读 | DMA写入数据到内存 → CPU读取该内存 | CPU可能从Cache中读到旧数据,而非DMA刚写入的新数据 |
| CPU与DMA同时写 | CPU和DMA同时修改同一块内存 | 数据竞争,最终结果不可预测 |
3.2 Streaming DMA映射与一致性DMA映射
Linux内核提供两种DMA映射机制来解决缓存一致性问题:Streaming DMA映射(流式映射)和一致性DMA映射(Coherent DMA映射)。两者适用场景不同,开发者需根据数据传输模式选择。
3.2.1 两种映射机制对比
| 特性 | Streaming DMA映射 | 一致性DMA映射 |
|---|---|---|
| API函数 | dma_map_single / dma_unmap_single |
dma_alloc_coherent / dma_free_coherent |
| 缓存策略 | 临时同步(Cache Flush/Invalidate) | 永久禁止Cache(页表属性设置) |
| 适用场景 | 短时、频繁的DMA传输(如网络包、块设备I/O) | 长时、共享的控制结构(如描述符环、状态寄存器) |
| 内存来源 | 任意已分配的内存(kmalloc、栈、全局变量) | 专用分配的一致性内存区域 |
| 性能开销 | 每次映射/取消映射时执行Cache维护操作 | 分配时一次性设置,后续无额外开销 |
| 同步要求 | 必须按方向调用 dma_sync_single_for_cpu/device |
无需额外同步 |
3.3 dma_alloc_coherent 详解
dma_alloc_coherent 用于分配一致性DMA内存。该函数返回的虚拟地址和物理地址(或DMA总线地址)指向的内存区域,对CPU和DMA控制器而言始终保持一致。
3.3.1 函数原型与参数
#include <linux/dma-mapping.h>
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
| 参数 | 说明 |
|---|---|
dev |
设备指针,用于关联DMA域。不可为NULL |
size |
分配大小(字节),必须为2的幂次对齐(部分架构要求) |
dma_handle |
输出参数,返回DMA总线地址(给DMA控制器使用) |
flag |
GFP标志,如 GFP_KERNEL、GFP_ATOMIC |
3.3.2 典型使用模式
struct device *dev = &pdev->dev;
dma_addr_t dma_handle;
void *cpu_addr;
size_t size = 4096;
/* 分配一致性DMA内存 */
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
dev_err(dev, "Failed to allocate coherent DMA memory\n");
return -ENOMEM;
}
/* 使用cpu_addr访问内存(CPU侧),使用dma_handle编程DMA控制器 */
/* ... */
/* 释放一致性DMA内存 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
实现原理:内核在页表中将这段内存的Cache属性设置为“不可缓存”(Uncacheable)或“写合并”(Write Combine),从而保证CPU访问时直接操作物理内存,与DMA控制器看到的视图一致。
3.3.3 注意事项
- 内存对齐:分配的内存默认按页对齐,适合描述符环等结构
- 原子上下文:中断上下文必须使用
GFP_ATOMIC - 内存大小:避免分配过大内存(通常不超过几MB),否则可能耗尽一致性内存池
- 释放匹配:
dma_free_coherent必须传入与分配时完全相同的size、cpu_addr、dma_handle
3.4 dma_map_single 详解
dma_map_single 用于Streaming DMA映射,将已有的内存缓冲区映射到DMA地址空间,并在映射过程中执行必要的Cache维护操作。
3.4.1 函数原型与参数
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
| 参数 | 说明 |
|---|---|
dev |
设备指针 |
cpu_addr |
CPU虚拟地址(必须来自 kmalloc、栈、全局变量等) |
size |
映射大小(字节) |
dir |
数据传输方向(见下表) |
3.4.2 数据传输方向枚举
| 方向常量 | 含义 | Cache操作 |
|---|---|---|
DMA_TO_DEVICE |
CPU → 设备(CPU写,DMA读) | 映射时Flush Cache(写回内存) |
DMA_FROM_DEVICE |
设备 → CPU(DMA写,CPU读) | 取消映射时Invalidate Cache(丢弃Cache行) |
DMA_BIDIRECTIONAL |
双向传输 | 映射时Flush,取消映射时Invalidate |
DMA_NONE |
调试用途 | 无操作 |
3.4.3 完整使用流程
/* 1. 分配或获取CPU缓冲区 */
void *buffer = kmalloc(BUF_SIZE, GFP_KERNEL);
if (!buffer)
return -ENOMEM;
/* 2. 映射到DMA地址空间 */
dma_addr_t dma_addr = dma_map_single(dev, buffer, BUF_SIZE, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "DMA mapping failed\n");
kfree(buffer);
return -EIO;
}
/* 3. 编程DMA控制器,使用dma_addr启动传输 */
/* ... */
/* 4. 等待DMA传输完成 */
/* ... */
/* 5. 取消映射(释放DMA地址空间并同步Cache) */
dma_unmap_single(dev, dma_addr, BUF_SIZE, DMA_TO_DEVICE);
/* 6. 释放CPU缓冲区 */
kfree(buffer);
3.4.4 手动同步操作
当DMA传输与CPU访问交错进行时,需使用 dma_sync_single_for_cpu 和 dma_sync_single_for_device 手动同步。
/* CPU准备读取DMA写入的数据 */
dma_sync_single_for_cpu(dev, dma_addr, BUF_SIZE, DMA_FROM_DEVICE);
/* 此时可以安全读取buffer */
process_data(buffer);
/* CPU完成写入,准备交给DMA读取 */
dma_sync_single_for_device(dev, dma_addr, BUF_SIZE, DMA_TO_DEVICE);
/* 启动下一次DMA传输 */
常见错误:在
dma_map_single 之后直接访问 cpu_addr 指向的缓冲区,而不调用 dma_sync_single_for_cpu。这会导致CPU读到过时的Cache数据,或DMA读到未写回内存的数据。
3.5 两种映射机制的选择策略
3.6 调试技巧与常见陷阱
3.6.1 调试方法
| 方法 | 操作 | 适用场景 |
|---|---|---|
| 内核配置 | 开启 CONFIG_DMA_API_DEBUG |
检测DMA API使用错误(如重复映射、大小不匹配) |
| 打印地址 | 打印 cpu_addr 和 dma_handle 对比 |
确认映射是否正确,地址是否在合理范围 |
| Cache刷新验证 | 在关键点添加 dma_sync_single_for_cpu |
排查数据不一致问题 |
| 内存毒化 | 映射前填充 0xdeadbeef,传输后检查 |
验证DMA是否真的写入了预期数据 |
3.6.2 常见陷阱
- 栈变量映射:不能对栈上的局部变量调用
dma_map_single,因为栈可能不在DMA可访问的地址范围 - 映射后修改缓冲区:Streaming映射后,在取消映射前修改缓冲区内容,必须重新同步
- 方向错误:
DMA_TO_DEVICE和DMA_FROM_DEVICE混淆会导致Cache操作方向错误 - 内存泄漏:
dma_map_single后忘记dma_unmap_single,会耗尽DMA地址空间 - 大小不匹配:映射和取消映射时的
size必须一致,否则部分Cache行未被正确同步
黄金法则:每次
dma_map_single 必须对应一次 dma_unmap_single;每次 dma_alloc_coherent 必须对应一次 dma_free_coherent。在错误路径中也要保证成对释放。