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同时修改同一块内存 数据竞争,最终结果不可预测
DMA与缓存一致性问题示意图 CPU Cache (L1/L2) 物理内存 DMA控制器 外设设备 ⚠ CPU通过Cache读写,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 无需额外同步
Streaming vs 一致性DMA映射决策流程 DMA传输需求 传输频率? 高频短时传输 Streaming DMA映射 低频长时共享 一致性DMA映射

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 两种映射机制的选择策略

DMA映射机制选择决策树 DMA传输场景 共享时间? 长期共享 dma_alloc_coherent 短期传输 dma_map_single 💡 选择原则:描述符环、控制结构用一致性映射;数据缓冲区用流式映射 流式映射性能更好,但需要开发者手动管理同步

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。在错误路径中也要保证成对释放。