1. 课程导入

在嵌入式、服务器、存储与网络设备中,CPU 与外部设备之间的数据搬运量往往远超纯计算量。如果每一次数据搬运都由 CPU 逐字节读写,系统吞吐会被迅速拖垮。DMA(Direct Memory Access,直接内存访问)正是为解决这一问题而生的硬件机制:它让外设在不占用 CPU 指令周期的情况下,直接与系统内存交换数据。但 DMA 带来高性能的同时,也引入了 Linux 内核中最容易出错、最难调试的一类问题——缓存一致性。

本课程的目标不是让你背下 API,而是让你建立一套可落地的工程判断力:看到一段 DMA 代码,能判断它是否存在缓存一致性风险;遇到数据错乱、偶发丢包、校验失败,能沿着 DMA 与缓存的信号链定位根因;在性能优化时,知道该用流式映射还是相干映射,该在何处插入屏障与同步。

一、核心概念:从一次数据搬运说起

要理解 DMA 与缓存一致性,先要理解三个角色的关系:CPU、Cache、DMA 控制器。CPU 访问内存时,数据会先进入 Cache;而 DMA 控制器通常绕过 CPU,直接访问物理内存(DRAM)。这就产生了一个经典矛盾:CPU 看到的是 Cache 中的副本,DMA 看到的是 DRAM 中的原始数据,两者可能不一致。

  • DMA 地址:设备使用的总线地址,需经 IOMMU 或直接映射转换为物理地址。
  • 一致性映射(Coherent Mapping):如 dma_alloc_coherent(),分配的内存保证 CPU 与设备看到相同数据,适合长期共享的控制结构。
  • 流式映射(Streaming Mapping):如 dma_map_single(),用于一次性传输,需要显式同步,性能更高但更易出错。
  • Cache 维护操作:clean(写回)、invalidate(失效)、flush(两者兼有),是软件保证一致性的关键手段。
  • 内存屏障:dma_wmb()、dma_rmb() 保证描述符写入顺序对设备可见。
核心判断:DMA 缓冲区是否被 Cache 缓存?若被缓存,CPU 与设备谁先写、谁后读,决定了必须执行 clean 还是 invalidate。

二、整体知识框架:一张图看清全貌

下图给出本课程的知识骨架:从硬件信号链出发,经过 Linux DMA API 层,落到缓存一致性策略与性能优化手段,最终形成可复用的实战路线。图中同时标出了 CPU、Cache、DMA 控制器与内存之间的数据路径,以及一致性维护的插入点。

DMA 与缓存一致性知识框架与实战路线 硬件信号链 CPU 读写内存 / 触发 DMA Cache clean / invalidate DMA 控制器 总线主设备 DRAM 物理内存 一致性维护插入点 Linux DMA 软件层 DMA API dma_map / dma_sync dma_alloc_coherent DMA 引擎驱动 描述符 / 通道 / 中断 dmaengine 框架 设备驱动 缓冲区管理 同步时机选择 IOMMU / SMMU 地址转换 隔离与保护 一致性策略 相干映射 长期共享,无需手动同步 流式映射 一次性传输,需显式同步 同步原语:dma_sync_single_for_cpu / for_device clean → 设备读;invalidate → CPU 读 性能优化与实战 减少同步次数 批量传输 / 合并描述符 对齐与边界 Cache line 对齐避免伪共享 调试手段:ftrace / dma-debug / perf 定位映射泄漏、同步缺失与性能热点

三、方法流程:一次安全 DMA 传输的标准动作

无论使用哪种 DMA 框架,一次安全的流式 DMA 传输都应遵循固定顺序。下面以字符设备发送缓冲区为例,给出典型流程与关键代码骨架。

/* 1. 分配并填充缓冲区(CPU 写) */
buf = kmalloc(len, GFP_KERNEL);
memcpy(buf, src, len);

/* 2. 流式映射,方向为 TO_DEVICE */
dma_addr = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) { /* 错误处理 */ }

/* 3. 确保 CPU 写入对设备可见(clean cache) */
dma_sync_single_for_device(dev, dma_addr, len, DMA_TO_DEVICE);

/* 4. 启动 DMA 传输,等待完成中断 */
start_dma(dma_addr, len);

/* 5. 传输完成后解除映射 */
dma_unmap_single(dev, dma_addr, len, DMA_TO_DEVICE);

接收方向则相反:先映射为 DMA_FROM_DEVICE,DMA 完成后调用 dma_sync_single_for_cpu() 使 CPU 看到设备写入的新数据,再读取缓冲区。顺序错误是导致“偶发读到旧数据”的首要原因。

四、典型场景与误区复盘

考虑一个网卡收包场景:驱动为每个描述符分配 2KB 缓冲区,DMA 写入后 CPU 读取协议头。若在 DMA 完成中断中忘记调用 dma_sync_single_for_cpu(),CPU 可能从 Cache 中读到上一次的旧包内容,表现为“随机丢包”或“协议解析错误”。这类问题在 x86 上可能因 Cache 行为不同而难以复现,但在 ARM 平台上极易暴露。

常见误区 现象 正确做法
映射后不调用 sync 设备读到旧数据或 CPU 读到旧数据 按方向调用 for_device / for_cpu
缓冲区未按 Cache line 对齐 相邻数据被意外 clean/invalidate 使用对齐分配或填充至 cache line 边界
在中断中频繁 map/unmap CPU 占用高,吞吐下降 预分配相干缓冲区或复用映射
忽略 dma_mapping_error IOMMU 映射失败后访问非法地址 每次映射后检查返回值
不要假设“x86 上没问题,ARM 上也没问题”。不同架构的 Cache 策略、IOMMU 行为与内存序模型差异巨大,DMA 代码必须在目标平台上验证。
学习本课程时,建议准备一块支持 DMA 的开发板(如树莓派、i.MX、RK 系列),配合 dma-debug 与 ftrace 观察真实的映射与同步行为,比只读代码更有效。

接下来的章节将沿着“硬件信号链 → DMA API → 一致性策略 → 性能优化 → 实战排障”这条主线逐步展开。你将学会用 dma_alloc_coherent() 构建共享环形缓冲区,用 dma_sync_* 精确控制同步点,用 dma-debug 捕获映射泄漏,并最终能独立分析一个真实驱动中的 DMA 性能瓶颈与一致性缺陷。