4. 缓存一致性核心概念
在上一章中,我们已经理解了CPU缓存与DMA之间的基本矛盾:CPU为了性能会把数据缓存在本地,而DMA控制器直接访问物理内存,两者看到的数据可能不一致。要真正掌握DMA与共享内存的性能优化,必须先理解现代多核处理器中缓存一致性的底层机制。本章将围绕四个核心概念展开:多核缓存一致性、硬件一致性域、非一致性内存访问(NUMA),以及设备与CPU共享数据时的可见性问题。
一、多核缓存一致性:MESI协议与总线监听
在多核系统中,每个CPU核心都有自己的私有L1/L2缓存,多个核心可能同时缓存同一块物理内存。如果核心A修改了某个缓存行,核心B的缓存副本就会过期。硬件通过缓存一致性协议来保证所有核心看到的内存视图一致,最经典的是MESI协议。
MESI将每个缓存行的状态分为四种:
- M(Modified,已修改):本核心独占且数据已修改,与内存不一致,其他核心无副本。
- E(Exclusive,独占):本核心独占且数据与内存一致,其他核心无副本。
- S(Shared,共享):多个核心共享该缓存行,数据与内存一致。
- I(Invalid,无效):本核心的副本已失效,需要重新从内存或其他核心获取。
当核心A要写入一个处于S状态的缓存行时,它必须先通过总线向其他核心发送“失效”消息,将其他核心的副本置为I,然后自己进入M状态。这个过程称为总线监听(snooping)或缓存行窃取。它保证了写操作的全局可见性,但也带来了总线流量和延迟。
二、硬件一致性域:谁和谁一致
并非所有硬件组件都处于同一个一致性域中。所谓一致性域(Coherency Domain),是指一组共享同一套缓存一致性协议的硬件单元。在典型的ARM SoC中,CPU集群内部通过CCI(Cache Coherent Interconnect)或CMN保持一致,但DMA控制器、PCIe设备、部分GPU往往位于一致性域之外。
这就引出了一个重要区分:
| 类型 | 是否参与缓存一致性 | 典型代表 | 软件需要做什么 |
|---|---|---|---|
| 一致性设备 | 是,硬件自动维护 | 支持ACE/CHI的DMA、部分集成GPU | 通常只需内存屏障,无需手动cache flush |
| 非一致性设备 | 否,硬件不维护 | 传统PCIe DMA、USB控制器 | 必须手动dma_sync_single_for_device/cpu |
在Linux中,判断设备是否一致,可以查看设备树中的dma-coherent属性,或使用dma_alloc_coherent()分配一致性内存。对于非一致性设备,驱动必须显式调用DMA同步API。
三、非一致性内存访问(NUMA)
NUMA(Non-Uniform Memory Access)是另一个影响缓存一致性和性能的重要概念。在多路服务器中,每个CPU插槽连接自己的本地内存,访问本地内存的延迟远低于访问其他插槽的内存。虽然NUMA主要影响的是内存访问延迟,但它与缓存一致性密切相关:跨插槽的缓存一致性维护需要经过片间互连,开销更大。
在NUMA系统中,如果DMA设备连接到某个节点,而缓冲区分配在另一个节点的内存上,就会产生跨节点流量,显著降低吞吐量。Linux提供了NUMA感知的分配接口:
// 在指定NUMA节点上分配DMA一致性内存
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// 查询设备所属NUMA节点
int node = dev_to_node(dev);
// 使用numa_alloc_onnode分配本地内存
void *local = numa_alloc_onnode(size, node);
优化原则是:让设备、CPU和缓冲区尽量处于同一NUMA节点,减少跨节点一致性流量。
四、设备与CPU共享数据时的可见性问题
这是本章最核心的实践问题。当CPU和DMA设备共享一块缓冲区时,存在两个方向的可见性风险:
- CPU写、设备读:CPU写入的数据可能还在缓存中(M状态),尚未写回内存。设备直接读内存会读到旧数据。必须在启动DMA前执行
dma_sync_single_for_device(),将缓存写回或使设备可见。 - 设备写、CPU读:设备写入内存后,CPU缓存中可能还保留着旧的缓存行(S或E状态)。CPU读取时会命中旧缓存,看不到设备的新数据。必须在DMA完成后执行
dma_sync_single_for_cpu(),使CPU缓存失效。
下面这张图展示了CPU缓存、内存与DMA设备之间的可见性路径与同步点:
图中清晰展示了:CPU缓存与内存之间需要同步,DMA设备直接访问内存,而同步API是连接两者的桥梁。对于一致性设备,硬件会自动处理缓存行状态,软件只需内存屏障;对于非一致性设备,必须手动调用同步API。
五、常见误区与复盘
误区二:只做一次同步就够。 如果CPU和DMA交替读写同一缓冲区,每次交接都必须同步,不能只在初始化时同步一次。
误区三:忽略NUMA节点。 在双路服务器上,设备与缓冲区跨节点会导致性能下降30%以上,应使用
dev_to_node()和NUMA感知分配。