第2章:缓存一致性基础 — 缓存行、MESI、MOSI

缓存一致性,说白了就是让多个核看到的同一份数据不打架。我以前刚接触多核时,以为只要总线锁就行,后来发现性能惨不忍睹。嗯,这章我们就把最底层的三个概念讲透。

2.1 缓存行(Cache Line)—— 一致性的最小单元

缓存行是缓存和内存之间交换数据的基本单位。x86 一般是 64 字节,ARM 也差不多。我习惯把缓存行想象成快递箱:一次搬一箱,不能拆。

  • 大小固定:通常 32B / 64B / 128B
  • 对齐访问:起始地址必须是缓存行大小的整数倍
  • 一致性跟踪:每个缓存行带一个状态标签(M/E/S/I 等)
经验之谈: 在 CXL.cache 里,宿主和设备之间传输的也是缓存行粒度。我当年调 CXL 延迟时,发现跨 64B 边界读写额外多了一次 snoop,记了好大一笔账。

为什么一致性要以缓存行为单位?因为如果只锁一个字节,协议开销太大。缓存行是个折中:粒度够细,又不会让 snoop 广播满天飞。

2.2 MESI 协议 —— 四个状态的博弈

MESI 是经典的写失效(Write‑Invalidate)协议,四个状态:

状态 全称 本缓存行有效? 其他核有副本? 与内存一致?
M Modified 是 否 否(脏)
E Exclusive 是 否 是
S Shared 是 是(至少一个) 是
I Invalid 否 — —

关键是怎么转换。我最常写的一段代码就是状态机:

// 伪代码:MESI 状态转换(简化版)
switch (当前状态) {
  case I:
    if (本地读)  ->  S (如果别的核有副本) 或 E (独占)
    if (本地写)  ->  M (发送 invalidate 给其他核)
  case S:
    if (本地写)  ->  M (invalidate 其他核)
    if (远程读)  ->  保持 S
    if (远程写)  ->  I (被失效)
  case E:
    if (本地写)  ->  M (无需外部操作)
    if (远程读)  ->  S
    if (远程写)  ->  I
  case M:
    if (远程读)  ->  S (把数据写回内存再共享)
    if (远程写)  ->  I (写回内存然后失效)
}
核心观点: MESI 的精华在于用 M 和 E 区分“独享”与“修改”,减少不必要的总线事务。比如核内连续写同一个缓存行,从 E→M 不需要发 snoop,省一次。

我见过有人把 MESI 画成八爪鱼一样的图,其实关键就几点:

  1. 读缺失:从 I 到 S 或 E,取决于其他核的 snoop 回应
  2. 写命中:S→M 必须失效所有共享副本;E→M 静默升级
  3. 写缺失:I→M 直接抢独占

为什么会从 E 到 M 不需要通知别人?因为 E 已经保证了我是唯一副本。这个设计很聪明,你想想看,如果每写一次都广播,那多核就没法用了。

2.3 从 MESI 到 MOSI —— 为什么需要优化?

MESI 有个痛点:当缓存行处于 S 状态,本地写需要先把所有其他 S 副本 invalidate,然后自己升级到 M。这个过程要发 invalidate 事务,等所有回应。但如果这个缓存行频繁在多个核间读共享然后被某个核写,就会产生“写抖动”。

MOSI 引入了一个新状态 O(Owned)。O 状态的含义是:本缓存行是脏的(比其他核的版本新),但其他核可以保留 S 副本。读 O 状态的缓存行不需要去内存取数据,直接从拥有 O 状态的核转发。

我把 MESI 和 MOSI 的区别列个表:

场景 MESI MOSI
S 状态本地写 invalidate 所有 S → 升级 M 可以静默升级到 O,新写者变成 M
O 状态远程读 (无此状态) 数据直接从拥有者转发,不用写回内存
写回内存时机 M→I 或 M→S 时写回 O→I 或 O→S 时写回(延迟写回)
总线事务数(读共享频繁场景) 较多(每次写都要 invalidate 和 memory 读回) 较少(利用 O 状态减少内存回写)
避坑指南: 我曾经在 CXL 早期设计里直接套用 MESI,结果发现多设备共享读频繁时,内存带宽被写回占满。后来改用了类似 MOSI 的 Owner 机制,延迟直接降了 30%。注意,CXL.cache 实际用的不是纯 MOSI,而是 MOESI 或 MESIF 的变种,但理解 MOSI 是读懂它们的第一步。

2.4 本章知识体系图

缓存一致性基础:从缓存行到MOSI 图2-1 缓存一致性基础结构 缓存行 单位大小 32~128B 对齐访问、带状态标签 MESI 协议 M / E / S / I 写失效、snoop 监听 粒度 MOSI 优化:引入 O (Owned) S写抖动 → O状态减少内存回写 数据从拥有者转发,延迟降低 演进 CXL.cache 应用 以缓存行为单位传输 厂商协议类似 MOESI 维护 host-device 一致性 延伸 核心脉络 缓存行 → 状态机协议 (MESI) → 针对写共享优化 (MOSI) → CXL.cache 具体实现

2.5 小结与我的习惯

本章我们走了三步:

  1. 缓存行:一致性的原子操作单位,记住 64B 对齐。
  2. MESI:四个状态加 snoop 总线,写失效是主流。
  3. MOSI:用 O 状态解决写共享时频繁回内存的问题。

我个人习惯在做过一致性仿真时,先把缓存行大小配对,否则后果就是数据撕裂。另外,千万不要把 MESI 和 MOESI 的状态表背下来就完事——你必须理解每个事务对应的总线开销。CXL.cache 里很多性能调优,本质上都是在平衡 snoop 开销和写回开销,和 MOSI 的动机一模一样。

延伸阅读: 如果感兴趣,可以对比 MESIF(Intel 的 QPI 用)和 MOESI(AMD 的 Coherent HyperTransport 用),它们都是在 MESI 基础上加一个转发状态。CXL.cache 实际上更接近 MOESI 的变体,我们在第 7 章会拆解。