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 的原因。
二、内存屏障:控制可见性与顺序
即使缓存被正确维护,编译器和 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 填完缓冲区后直接写门铃,没有 dma_wmb(),也没有在非一致性平台上 clean 缓存。结果设备可能读到旧数据或未写完的描述符,表现为“偶发丢包”或“校验和错误”。正确做法是:填充数据 → dma_wmb() → 写描述符 → 再 dma_wmb() → 敲门铃;若平台非一致性,还需在启动 DMA 前对缓冲区执行 dma_cache_clean() 或使用 dma_map_single() 等 API 完成映射与缓存维护。
另一个误区是把 mb() 当成万能屏障。在 DMA 场景中,应优先使用 dma_wmb()/dma_rmb(),它们针对设备可见性做了更精确的约束,避免不必要的全序开销。理解缓存层次、屏障语义、总线事务和一致性模型,是后续编写高性能、可移植 DMA 代码的基础。