2. 计算机体系结构基础

要真正理解 Linux 内核中的 DMA 与缓存一致性编程,必须先回到计算机体系结构层面:CPU 如何通过多级缓存访问内存,设备如何通过总线访问内存,DMA 控制器又如何在不占用 CPU 逐字节搬运的情况下完成大块数据迁移。本节把这些基础概念串成一条完整的“数据通路”,为后续编写一致性 DMA 缓冲区、处理 cacheline 对齐与内存屏障打下基础。

一、CPU 缓存层次:速度、容量与一致性的起点

现代 CPU 不会直接、频繁地访问主存,而是通过多级缓存(L1/L2/L3)降低访存延迟。典型结构是:L1 分为指令缓存 I-Cache 与数据缓存 D-Cache,容量约 32KB~64KB,延迟仅几个周期;L2 通常每核私有,容量 256KB~1MB;L3 多核共享,容量可达数十 MB。主存 DRAM 延迟可达上百个周期。缓存以 cacheline 为最小管理单位,常见为 64 字节。

关键点在于:CPU 读写内存时,实际读写的是缓存行。若设备或 DMA 直接修改了主存,而 CPU 缓存中仍保留旧副本,就会产生“缓存不一致”。这正是 DMA 编程中必须处理 cache 同步的根本原因。

二、内存模型:从强顺序到弱顺序

内存模型描述的是“一个 CPU 的写操作,何时对另一个 CPU 或设备可见”。x86 属于较强的 TSO 模型,普通读写基本保持顺序;ARM、RISC-V、PowerPC 等属于弱内存模型,编译器和 CPU 都可能重排访存指令。因此在内核中,描述符写入后必须使用 wmb()、dma_wmb() 等屏障,确保设备看到的是完整、有序的数据。

核心结论:缓存一致性解决“数据副本是否最新”,内存屏障解决“访问顺序是否可见”,两者在 DMA 编程中缺一不可。

三、总线与设备交互:CPU、内存、设备如何对话

CPU、内存与设备通过总线/互连结构连接。传统平台使用前端总线与北桥,现代 SoC 普遍采用 AXI、ACE、CHI 等片上互连。设备访问内存通常有两种方式:

  • PIO(Programmed I/O):CPU 逐字节/逐字读写设备寄存器或 FIFO,简单但占用 CPU。
  • DMA(Direct Memory Access):设备或 DMA 控制器直接在内存与设备之间搬运数据,CPU 只负责配置与收尾。

设备与 CPU 之间还通过中断、MSI/MSI-X 消息、doorbell 寄存器等机制交互。理解这些交互路径,才能判断数据何时真正到达内存、何时可以安全释放缓冲区。

四、DMA 控制器工作原理:一次传输的完整生命周期

DMA 控制器本质上是一个“受 CPU 委托的内存搬运引擎”。以常见的描述符式 DMA 为例,其工作流程如下:

  1. CPU 在内存中准备好源缓冲区、目的缓冲区和 DMA 描述符。
  2. CPU 将描述符物理地址写入 DMA 控制器的寄存器,并启动通道。
  3. DMA 控制器通过总线发起读请求,从内存读取数据。
  4. DMA 控制器将数据写入设备 FIFO 或另一段内存。
  5. 传输完成后,DMA 控制器置位完成标志并触发中断。
  6. CPU 进入中断处理,检查状态、清理描述符、唤醒等待队列。

在这个过程中,CPU 缓存可能仍持有旧数据,因此需要根据 DMA 方向执行 dma_map_single()、dma_sync_single_for_cpu()、dma_sync_single_for_device() 等操作,保证设备与 CPU 看到一致的内存视图。

CPU 缓存层次 · 总线 · DMA 控制器 · 设备交互 CPU 核心 L1 D-Cache / I-Cache L2 Cache(每核私有) L3 Cache(多核共享) 主存 DRAM DMA 缓冲区 / 描述符环 cacheline 64B 对齐区域 物理地址空间 DMA 控制器 通道寄存器 / 描述符指针 源地址 → 目的地址 完成标志 / 中断产生 外设 / 设备 设备 FIFO / 寄存器 中断 / MSI-X 消息 一致性关键点 • cacheline 对齐与填充 • dma_map / dma_sync 方向 • 内存屏障保证顺序可见 读写 配置 搬运数据 中断 DMA 直访内存 同步 数据流:CPU 配置 DMA → DMA 经总线读写 DRAM → 设备收发 → 中断通知 CPU → CPU 执行 cache 同步与屏障 缓存一致性保证副本最新,内存屏障保证访问顺序,二者共同决定 DMA 数据是否安全可见

五、案例场景:网卡收包中的 DMA 与缓存

以网卡接收数据为例:网卡通过 DMA 将报文写入内核预分配的 RX 环形缓冲区,然后触发中断。CPU 在中断下半部读取描述符,发现状态为“已完成”,再通过 dma_sync_single_for_cpu() 使 CPU 缓存失效或同步,从而读取到网卡写入的最新报文。若缺少这一步,CPU 可能读到 cacheline 中的旧数据,表现为“收包成功但内容错误”或“协议栈解析异常”。

/* 简化示意:DMA 映射与同步 */
dma_addr_t dma_handle;
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* 设备写入后,CPU 读取前同步 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
/* 读取数据 ... */
dma_sync_single_for_device(dev, dma_handle, size, DMA_FROM_DEVICE);

六、常见误区与复盘清单

误区 后果 正确做法
认为 CPU 与设备看到同一份内存 读到旧 cacheline,数据错乱 按 DMA 方向执行 map/sync
忽略 cacheline 对齐 相邻数据被意外失效或写回 缓冲区按 64B 对齐并填充
描述符写完立即启动 DMA 设备读到未写完的描述符 使用 dma_wmb() 保证顺序
在中断中直接释放 DMA 缓冲区 设备仍在使用,导致踩内存 确认完成标志后再回收
学习建议:把“CPU 缓存—内存—总线—DMA—设备”画成一张数据通路图,每次调试 DMA 问题时,先问三个问题:数据现在在哪?谁拥有最新副本?访问顺序是否可见?