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() 等屏障,确保设备看到的是完整、有序的数据。
三、总线与设备交互: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 为例,其工作流程如下:
- CPU 在内存中准备好源缓冲区、目的缓冲区和 DMA 描述符。
- CPU 将描述符物理地址写入 DMA 控制器的寄存器,并启动通道。
- DMA 控制器通过总线发起读请求,从内存读取数据。
- DMA 控制器将数据写入设备 FIFO 或另一段内存。
- 传输完成后,DMA 控制器置位完成标志并触发中断。
- CPU 进入中断处理,检查状态、清理描述符、唤醒等待队列。
在这个过程中,CPU 缓存可能仍持有旧数据,因此需要根据 DMA 方向执行 dma_map_single()、dma_sync_single_for_cpu()、dma_sync_single_for_device() 等操作,保证设备与 CPU 看到一致的内存视图。
五、案例场景:网卡收包中的 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 缓冲区 | 设备仍在使用,导致踩内存 | 确认完成标志后再回收 |