4. 缓存一致性基础

在讨论 DMA 性能瓶颈之前,必须先理解 CPU 侧的缓存体系。DMA 之所以复杂,根本原因在于:外设直接读写内存,而 CPU 可能把同一块内存的副本缓存在自己的私有缓存里。如果两者不同步,就会出现“CPU 看到旧数据”或“外设读到旧数据”的问题。本节从缓存层次、缓存行、写策略到 MESI 协议,建立后续分析 DMA 一致性的基础。

一、CPU 缓存层次:为什么需要多级缓存

CPU 执行速度远高于 DRAM 访问速度,二者之间存在数量级的差距。为了弥补这一鸿沟,现代处理器采用多级缓存结构,通常为 L1、L2、L3 三级:

  • L1 缓存:离核心最近,容量最小(通常 32KB~64KB),访问延迟约 4~5 个周期,分为指令缓存(I-Cache)和数据缓存(D-Cache)。
  • L2 缓存:容量较大(256KB~1MB),延迟约 10~20 个周期,通常为每个核心私有。
  • L3 缓存:容量最大(数 MB 到数十 MB),多核共享,延迟约 30~50 个周期,是核间数据共享的关键层级。

越靠近核心,速度越快、容量越小、成本越高。数据以“缓存行”为单位在各级之间搬运,而不是按字节单独搬运。理解这一点,是理解一致性协议的前提。

二、缓存行:一致性的最小单位

缓存行(Cache Line)是缓存与内存之间传输数据的最小单位,x86 和 ARM 上通常为 64 字节。即使 CPU 只读取 1 个字节,也会把包含该字节的整行(64 字节)加载进缓存。

缓存行带来两个重要后果:

  1. 空间局部性收益:连续访问相邻数据时命中率高,性能好。
  2. 伪共享(False Sharing):两个核心分别修改同一缓存行中的不同变量,虽然逻辑上无冲突,但硬件仍会因整行失效而反复同步,导致性能骤降。
在 DMA 场景中,缓存行对齐尤为关键。若 DMA 缓冲区未按 64 字节对齐,一次 cache flush/invalidate 可能波及相邻数据,造成数据损坏或额外开销。

三、写回与写直达:两种写策略

当 CPU 修改缓存中的数据时,何时把新值写回内存,取决于写策略:

策略 行为 优点 缺点
写直达(Write-Through) 每次写缓存的同时立即写内存 内存始终最新,一致性简单 写内存频繁,带宽压力大
写回(Write-Back) 只写缓存并置脏位,淘汰时才写内存 减少内存写次数,性能高 内存可能滞后,需一致性协议维护

现代 CPU 普遍采用写回策略,因此缓存行存在“干净”和“脏”两种状态。脏行意味着内存中的副本已过期,这正是 DMA 读取时可能拿到旧数据的根源。

四、MESI 协议:四状态一致性模型

多核系统中,每个核心都有自己的缓存,同一内存地址可能在多个缓存中留有副本。MESI 协议用四个状态描述缓存行的归属与有效性:

  • M(Modified,已修改):本行被修改且与内存不一致,仅本缓存持有,是唯一有效副本。
  • E(Exclusive,独占):本行与内存一致,且仅本缓存持有。
  • S(Shared,共享):本行与内存一致,多个缓存可同时持有。
  • I(Invalid,无效):本行数据无效,不可使用。

当某核心要写一个处于 S 状态的行时,必须先向其他缓存发送失效请求,使它们转为 I,自己再转为 M。这个过程称为“缓存一致性流量”,会占用总线带宽。DMA 与 CPU 争抢内存带宽时,这类流量会进一步加剧瓶颈。

MESI 状态转换与缓存层次示意 Core 0 L1 / L2 私有 Core 1 L1 / L2 私有 L3 共享缓存 多核共享 主存 DRAM DMA 直接访问 MESI 四状态转换 M 已修改·脏 E 独占·干净 S 共享·干净 I 无效 写回/降级 他核读 失效 读命中 DMA 绕过 CPU 缓存直接访问 DRAM,需软件维护一致性

五、一致性模型与 DMA 的关联

MESI 保证的是“CPU 核间”的一致性,它并不覆盖 DMA 引擎。DMA 通常不经过 CPU 缓存,直接读写 DRAM,因此会出现两类问题:

  • DMA 读(设备→内存):设备写入新数据到 DRAM,但 CPU 缓存中可能仍是旧行。若该行状态为 M 或 S,CPU 会读到旧值。需要在 DMA 传输前 invalidate 对应缓存行。
  • DMA 写(内存→设备):CPU 修改了缓存中的脏行,尚未写回 DRAM,设备却直接从 DRAM 读取,拿到旧数据。需要在 DMA 传输前 flush/clean 对应缓存行。
很多 DMA 数据损坏问题并非驱动逻辑错误,而是遗漏了一次 cache flush 或 invalidate。尤其在非一致性架构(如部分 ARM SoC)上,这类问题更常见。

六、案例与复盘:一次网卡收包异常

某嵌入式平台使用非一致性 DMA,网卡收包后 CPU 读取缓冲区,发现数据前 64 字节正确、后续为旧值。排查发现:驱动只对缓冲区首地址做了一次 invalidate,但缓冲区跨越多个缓存行,且未按 64 字节对齐,导致部分行未被失效。

/* 错误做法:只失效首地址 */
dma_cache_invalidate(rx_buf, sizeof(rx_buf));

/* 正确做法:按缓存行对齐并覆盖整个范围 */
#define CACHE_LINE 64
uintptr_t start = (uintptr_t)rx_buf & ~(CACHE_LINE - 1);
uintptr_t end   = ((uintptr_t)rx_buf + len + CACHE_LINE - 1) & ~(CACHE_LINE - 1);
dma_cache_invalidate((void *)start, end - start);

复盘要点:

  1. DMA 缓冲区必须按缓存行对齐,长度向上取整到缓存行边界。
  2. 方向决定操作:设备写内存用 invalidate,CPU 写内存供设备读用 flush。
  3. 一致性架构(如 x86 常见)由硬件维护,但非一致性架构必须软件显式处理。
  4. 伪共享与缓存行争用会放大 DMA 与 CPU 的带宽冲突,需结合 perf 观察 cache-misses 指标。
核心结论:缓存一致性不是“CPU 内部的事”。只要 DMA 参与,软件就必须明确数据方向、缓存行边界和操作类型,否则性能与正确性都会出问题。