4. Cache 一致性基础:CPU Cache 结构与一致性协议

在 DMA 与设备共享内存的场景中,Cache 一致性是决定数据正确性的核心问题。CPU 通过多级 Cache 加速内存访问,但设备(如 DMA 控制器)直接读写物理内存,绕过了 CPU Cache。若 Cache 与内存中的数据不一致,轻则数据错误,重则系统崩溃。本节从硬件结构出发,深入剖析 Cache 的工作机制与一致性协议。

4.1 CPU Cache 层次结构(L1 / L2 / L3)

现代多核处理器通常采用三级 Cache 架构,以平衡速度、容量与成本:

层级 位置 典型容量 延迟(CPU 周期) 共享范围
L1 Cache 每个核心私有 32 KB ~ 64 KB(指令 + 数据分离) 2 ~ 4 周期 单核心
L2 Cache 每个核心私有 256 KB ~ 512 KB 10 ~ 20 周期 单核心
L3 Cache 所有核心共享 2 MB ~ 32 MB 30 ~ 50 周期 所有核心
关键认知:L1 速度最快但容量最小,L3 容量最大但延迟最高。DMA 缓冲区通常映射为“非缓存”或“一致性”内存,以避免与 L1/L2 私有 Cache 产生冲突。

下图展示了一个四核处理器的典型 Cache 拓扑结构:

四核处理器 Cache 拓扑结构 Core 0 L1 Cache (32KB) L2 Cache (256KB) Core 1 L1 Cache (32KB) L2 Cache (256KB) Core 2 L1 Cache (32KB) L2 Cache (256KB) Core 3 L1 Cache (32KB) L2 Cache (256KB) L3 Cache (共享, 8MB) 主内存 (DDR4/DDR5)

4.2 Cache Line:数据交换的最小单元

Cache 与内存之间的数据交换并非以字节为单位,而是以 Cache Line(缓存行)为基本单元。Cache Line 的大小通常为 64 字节(x86/ARM 主流架构)。

  • 空间局部性利用:当 CPU 访问一个地址时,其相邻的 64 字节数据会被一次性加载到 Cache Line 中。
  • 对齐要求:DMA 缓冲区若与 Cache Line 边界未对齐,可能导致一个 Cache Line 包含部分 DMA 数据和部分 CPU 数据,引发“伪共享”(False Sharing)问题。
  • 一致性粒度:Cache 一致性协议以 Cache Line 为粒度进行状态跟踪和同步。
驱动调试要点:在分配 DMA 缓冲区时,务必使用 dma_alloc_coherent() 或确保缓冲区地址按 Cache Line 大小对齐。若手动管理一致性,需调用 dma_map_single() 并指定正确的 DMA 方向。

下图展示了 Cache Line 在 CPU 与内存之间的数据搬运过程:

Cache Line 数据搬运示意(64 字节粒度) CPU Core Cache (L1/L2) 主内存 Cache Line (64B) Cache Line (64B) 读/写 64 字节块

4.3 Cache 写策略:Write-through 与 Write-back

CPU 写入数据时,Cache 与内存的同步方式决定了性能与一致性复杂度。两种主流写策略对比如下:

写策略 写入 Cache 时 写入内存时 性能 一致性复杂度 典型应用
Write-through 立即写入 Cache 同时立即写入内存 较低(每次写操作都访问内存) 低(内存始终最新) 早期处理器、对一致性要求严格的设备寄存器
Write-back 仅写入 Cache,标记为 Dirty 延迟写入(Cache Line 被替换或显式刷回时) 高(减少内存访问次数) 高(需协议保证一致性) 现代通用处理器(x86/ARM)
DMA 陷阱:现代 Linux 系统默认使用 Write-back 策略。当 DMA 设备向内存写入数据时,若 CPU Cache 中仍持有该内存区域的“脏”Cache Line,CPU 将读到旧数据。反之,CPU 写入数据后若未刷回 Cache,DMA 设备将读到旧数据。这是驱动开发中最常见的一致性 Bug。

下图对比了两种写策略的数据流:

Write-through vs Write-back 数据流对比 Write-through CPU 写操作 Cache 更新 内存同步更新 Write-back CPU 写操作 Cache 更新 (Dirty) 延迟刷回内存 Dirty 标志位 = 1

4.4 Cache 一致性协议:MESI

MESI 协议是维护多核 Cache 一致性的经典协议,定义了每个 Cache Line 的四种状态:

状态 全称 该 Cache Line 是否有效 数据是否与内存一致 其他核心中是否有副本
M Modified(已修改) 是 否(数据仅在本 Cache,内存已过时) 无
E Exclusive(独占) 是 是(与内存一致) 无
S Shared(共享) 是 是(与内存一致) 有(一个或多个核心持有副本)
I Invalid(无效) 否 — —
协议核心思想:当某个核心要写入一个 Cache Line 时,必须确保其他核心中该 Cache Line 的副本全部失效(Invalid)。写入后,该 Cache Line 变为 Modified 状态,其他核心若读取则必须从修改后的核心获取最新数据(Cache-to-Cache 传输)。

下图展示了 MESI 协议的状态转换流程:

MESI 协议状态转换图 M Modified E Exclusive S Shared I Invalid 写回 / 窥探失效 本地写操作 远程读请求 读缺失(独占) 读缺失(共享) 窥探失效 其他共享者失效

4.5 一致性协议对 DMA 的影响

MESI 协议虽然保证了多核 CPU 之间的 Cache 一致性,但 DMA 设备不在 MESI 协议的管理范围内。设备直接访问物理内存,不会发送“窥探”请求给 CPU Cache。因此,驱动开发者必须手动处理以下场景:

  1. CPU 写 → DMA 读:CPU 写入数据到缓冲区后,必须执行 Cache 刷回(Clean/Flush)操作,将 Dirty Cache Line 写回内存,否则 DMA 设备读到的是旧数据。
  2. DMA 写 → CPU 读:DMA 设备向内存写入数据后,CPU 必须使相关 Cache Line 失效(Invalidate),否则 CPU 可能读到 Cache 中的陈旧副本。
  3. 伪共享(False Sharing):当两个核心分别操作同一 Cache Line 中的不同变量时,MESI 协议会频繁触发状态切换,导致性能下降。DMA 缓冲区应避免与频繁访问的 CPU 数据共享同一 Cache Line。
最佳实践:在 Linux 驱动中,使用 dma_alloc_coherent() 分配的内存默认是非缓存的(或启用硬件一致性协议如 IOMMU/SMMU),无需手动维护一致性。若使用 dma_map_single() 进行流式 DMA 映射,则必须根据 DMA 方向调用 dma_sync_single_for_cpu() 和 dma_sync_single_for_device() 来管理 Cache。

下图总结了 DMA 与 Cache 一致性的关键交互路径:

DMA 与 Cache 一致性交互路径 CPU Core 读/写 Cache CPU Cache L1/L2/L3 主内存 DMA 缓冲区 DMA 设备 直接内存访问 刷回/失效 DMA 读写 ⚠ 关键:DMA 设备不参与 M