4. 缓存一致性原理

在DMA性能优化中,缓存一致性是绕不开的第一性问题。CPU访问内存时会经过多级缓存,而DMA设备往往直接读写物理内存,如果二者对同一块数据的视图不一致,就会出现“CPU看到旧数据”或“设备读到脏数据”的经典故障。理解MESI协议、硬件一致性域、非一致性设备以及IOMMU/SMMU的职责边界,是判断“该不该刷cache、该不该用dma_alloc_coherent、该不该配iommu”的前提。

一、核心原理:MESI如何维护CPU侧一致性

MESI是缓存行状态机,每个缓存行处于四种状态之一:Modified(已修改,仅本核有且与内存不一致)、Exclusive(独占,仅本核有且与内存一致)、Shared(共享,多核都有且与内存一致)、Invalid(无效)。当某个核要写一个Shared行时,硬件会先发Invalidate消息让其他核失效该行,再进入Modified;当其他核要读一个Modified行时,会触发回写或转发,保证读到最新值。这套机制由总线监听或目录协议在硬件层自动完成,软件无需干预。

关键点在于:MESI维护的是“CPU缓存之间”以及“CPU缓存与内存”的一致性,它的作用域是硬件一致性域(Coherency Domain)。只有位于同一一致性域内的主设备,才能被这套协议覆盖。

二、硬件一致性域与非一致性设备

在ARM体系结构中,一致性域通常由CCI、CCN或CMN等互连总线定义。CPU、部分GPU、部分加速器可以接入该互连,成为“一致性设备”,它们对内存的访问会被硬件自动维护一致性。而大量外设——如传统网卡、USB控制器、部分FPGA——并不接入一致性互连,属于“非一致性设备”。

非一致性设备做DMA时,硬件不会替软件维护缓存。此时必须由驱动显式处理:DMA写内存前,若CPU缓存中有该区域的脏行,需要clean(回写);DMA读内存后,若CPU缓存中可能有旧行,需要invalidate(失效)。Linux提供了两类API:流式映射(dma_map_single/dma_sync_single_for_cpu/device)用于一次性传输,一致性映射(dma_alloc_coherent)则直接分配非缓存或硬件一致的内存,适合长期共享的控制结构。

常见误区:认为“用了dma_alloc_coherent就万事大吉”。它只保证该块内存本身的一致性,若驱动还用普通kmalloc缓冲区做DMA,仍必须走流式映射并正确sync,否则会出现随机数据损坏。

三、IOMMU与SMMU:地址翻译与隔离

IOMMU(x86)与SMMU(ARM)是设备侧的地址翻译单元。它们把设备发出的IOVA(I/O虚拟地址)翻译成物理地址,并提供访问权限检查。对DMA性能优化而言,它们带来三个直接价值:

  • 地址翻译:设备可使用不连续的物理页拼成连续IOVA,缓解大块DMA的物理内存碎片问题。
  • 隔离与安全:限制设备只能访问被映射的区域,防止野指针DMA破坏内核内存。
  • 一致性属性配置:通过页表属性可标记某段映射为non-coherent,配合软件sync;也可在支持时标记为coherent,减少手动维护。

需要强调的是,IOMMU/SMMU本身不“制造”缓存一致性。它解决的是地址与权限问题;一致性仍取决于设备是否在硬件一致性域内,以及驱动是否正确使用DMA API。把两者混为一谈,是排障时最常见的判断错误。

缓存一致性域、非一致性设备与IOMMU/SMMU关系图 硬件一致性域(Coherency Domain) CPU Core 0 L1/L2 Cache CPU Core 1 L1/L2 Cache CPU Core 2 L1/L2 Cache 共享L3 / CCI / CCN 互连 MESI协议在此维护一致性 一致性设备 GPU/加速器 主存 DRAM 物理地址空间 非一致性设备 网卡/USB/FPGA IOMMU / SMMU IOVA→PA 翻译 软件必须显式维护一致性 DMA前:clean 脏缓存行 DMA后:invalidate 旧缓存行 或使用 dma_alloc_coherent 图例说明 一致性域内设备:硬件自动维护MESI 非一致性设备:需软件clean/invalidate IOMMU/SMMU:地址翻译与隔离,不替代一致性维护

四、方法流程:驱动侧的正确处理顺序

以非一致性网卡收包为例,典型流程如下:

/* 1. 分配DMA缓冲区(流式映射) */
dma_addr_t dma_handle;
void *buf = kmalloc(BUF_SIZE, GFP_KERNEL);
dma_handle = dma_map_single(dev, buf, BUF_SIZE, DMA_FROM_DEVICE);

/* 2. 将dma_handle写入网卡描述符,启动DMA */
writel(dma_handle, nic->rx_desc);

/* 3. DMA完成后,CPU读取前必须invalidate */
dma_sync_single_for_cpu(dev, dma_handle, BUF_SIZE, DMA_FROM_DEVICE);
/* 此时CPU可安全读取buf中的新数据 */

/* 4. 若还要把buf交给设备再次DMA,需重新sync for device */
dma_sync_single_for_device(dev, dma_handle, BUF_SIZE, DMA_FROM_DEVICE);

/* 5. 最终解除映射 */
dma_unmap_single(dev, dma_handle, BUF_SIZE, DMA_FROM_DEVICE);
kfree(buf);

若使用dma_alloc_coherent,则分配到的内存通常是非缓存或硬件一致的,CPU与设备可同时访问而无需上述sync,但代价是CPU访问速度可能下降,且大块分配可能失败。因此控制结构用coherent,大数据流用流式映射,是常见的折中策略。

五、案例与复盘:一次“随机丢包”的排障

某ARM平台网卡驱动在压力测试下出现随机丢包,日志无异常。排查发现:驱动用kmalloc分配RX缓冲区,dma_map_single后启动DMA,但DMA完成后直接读取buf,没有调用dma_sync_single_for_cpu。在CPU缓存中残留了旧描述符数据,导致协议栈解析到过期长度字段而丢包。修复方式是在DMA完成中断中补上sync for cpu,问题消失。

复盘要点:非一致性设备的DMA缓冲区,CPU在DMA写之后读之前必须invalidate;CPU在DMA读之前写之后必须clean。顺序反了或漏了,都会产生难以复现的数据错误。

六、关键对比清单

维度一致性设备非一致性设备IOMMU/SMMU
一致性维护硬件自动(MESI)软件显式clean/invalidate不负责一致性
典型APIdma_alloc_coherent可直接用dma_map_single + dma_sync_*iommu_map / dma_map自动走IOVA
性能影响CPU访问可能变慢sync带来额外开销翻译有TLB开销,但可缓解碎片
安全隔离依赖MMU依赖MMU设备侧隔离,防止野DMA
常见误区以为所有设备都一致漏掉sync或方向写反以为IOMMU能替代cache维护