2. 计算机体系结构基础

要理解 Linux 内核 DMA 的性能与缓存一致性问题,必须先回到计算机体系结构层面:CPU 并不是直接、顺序地访问主存,而是经过多级缓存、写缓冲、乱序执行和一致性协议;设备也不是“另一个 CPU”,它通过总线或互连发起 DMA 事务,绕过 CPU 直接读写内存。本节把这些基础模型串起来,为后续的 DMA 映射、屏障和一致性维护打底。

一、CPU 缓存层次:为什么 DMA 会“看不见”数据

现代 CPU 通常采用三级缓存结构:L1 分为指令与数据缓存,L2 为每核私有或共享,L3 为多核共享,最后才是 DRAM。缓存以 cache line 为最小单位,常见为 64 字节。CPU 读写内存时,实际先命中缓存;若未命中,才从下一级或主存填充。关键点在于:CPU 的视角是“缓存视角”,而 DMA 的视角是“内存视角”。

当 CPU 写数据到缓冲区后,数据可能仍停留在 L1/L2 的脏行中,尚未写回 DRAM。此时若启动设备 DMA 读取该缓冲区,设备从内存读到的可能是旧数据。反之,设备 DMA 写入内存后,CPU 若命中旧缓存行,读到的仍是旧值。这就是缓存一致性问题的根源,也是 DMA 编程中必须处理 cache invalidate/clean 的原因。

核心结论:DMA 与 CPU 之间的数据可见性,不是“内存是否写对”,而是“缓存行是否被正确清理或失效”。

二、内存屏障:控制可见性与顺序

即使缓存被正确维护,编译器和 CPU 仍可能重排访存顺序。内存屏障用于约束这种重排,保证特定顺序的可见性。常见类型包括:

  • 编译器屏障:barrier(),阻止编译器跨屏障重排访存。
  • CPU 内存屏障:mb()、rmb()、wmb(),分别约束全序、读序、写序。
  • DMA 专用屏障:dma_wmb()、dma_rmb(),用于设备与 CPU 之间的描述符/数据顺序。

典型场景是“先写数据,再写描述符,最后敲门铃通知设备”。若没有 dma_wmb(),描述符可能先于数据到达设备,设备读到未初始化数据。正确顺序如下:

/* 1. 填充数据缓冲区 */
buf[0] = 0xAA;
buf[1] = 0xBB;

/* 2. 确保数据写入对设备可见 */
dma_wmb();

/* 3. 写描述符并通知设备 */
desc->addr = dma_addr;
desc->len  = 2;
writel(desc_dma, dev->doorbell);

三、总线事务与设备访问内存的基本模型

设备访问内存通常通过 PCIe、AXI、AHB 等总线或互连发起。一次 DMA 事务可理解为:设备发出读/写请求,经 IOMMU 或直接映射到物理地址,访问 DRAM 或缓存,再返回响应。与 CPU 访问不同,设备通常不参与 CPU 缓存一致性协议,除非平台支持硬件一致性(如 ARM CCI、PCIe ATS/PASID 等)。

因此,设备访问内存的基本模型可分为三类:

模型一致性行为软件责任典型平台
非一致性 DMA设备绕过缓存直接访问内存手动 clean/invalidate 缓存多数嵌入式 ARM、部分 x86
硬件一致性 DMA设备与 CPU 共享一致性域通常只需屏障,无需手动刷缓存支持 CCI/CCN 的 ARM SoC
IOMMU 映射 DMA设备使用 IOVA,经 IOMMU 翻译建立/销毁映射,处理缺页与隔离服务器、虚拟化环境

下图给出 CPU 缓存层次、内存屏障与设备 DMA 路径的关系示意:

CPU 核心 L1 D-Cache L1 I-Cache L2 Cache(私有/共享) L3 Cache 多核共享 cache line 64B 一致性协议维护 DRAM 主存 DMA 目标缓冲区 物理地址空间 设备可见内存 DMA 设备 网卡 / NVMe / GPU 发起总线事务 IOMMU / 互连 IOVA → 物理地址 隔离与映射 读/写 填充/写回 DMA 请求 地址翻译 数据搬运 内存屏障 dma_wmb() / dma_rmb() 保证数据与描述符顺序 顺序约束 非一致性 DMA:CPU 写后需 clean 缓存,设备写后需 invalidate 缓存; 一致性 DMA:硬件维护一致性,软件主要关注屏障与映射生命周期。

四、案例与误区复盘

假设一个网卡驱动要发送一帧数据。错误做法是:CPU 填完缓冲区后直接写门铃,没有 dma_wmb(),也没有在非一致性平台上 clean 缓存。结果设备可能读到旧数据或未写完的描述符,表现为“偶发丢包”或“校验和错误”。正确做法是:填充数据 → dma_wmb() → 写描述符 → 再 dma_wmb() → 敲门铃;若平台非一致性,还需在启动 DMA 前对缓冲区执行 dma_cache_clean() 或使用 dma_map_single() 等 API 完成映射与缓存维护。

常见误区:认为“调用了 dma_alloc_coherent() 就万事大吉”。实际上,一致性内存只保证 CPU 与设备看到一致的数据,不保证描述符与数据的先后顺序,也不替代内存屏障。

另一个误区是把 mb() 当成万能屏障。在 DMA 场景中,应优先使用 dma_wmb()/dma_rmb(),它们针对设备可见性做了更精确的约束,避免不必要的全序开销。理解缓存层次、屏障语义、总线事务和一致性模型,是后续编写高性能、可移植 DMA 代码的基础。