第2章:缓存一致性基础 — 缓存行、MESI、MOSI
缓存一致性,说白了就是让多个核看到的同一份数据不打架。我以前刚接触多核时,以为只要总线锁就行,后来发现性能惨不忍睹。嗯,这章我们就把最底层的三个概念讲透。
2.1 缓存行(Cache Line)—— 一致性的最小单元
缓存行是缓存和内存之间交换数据的基本单位。x86 一般是 64 字节,ARM 也差不多。我习惯把缓存行想象成快递箱:一次搬一箱,不能拆。
- 大小固定:通常 32B / 64B / 128B
- 对齐访问:起始地址必须是缓存行大小的整数倍
- 一致性跟踪:每个缓存行带一个状态标签(M/E/S/I 等)
为什么一致性要以缓存行为单位?因为如果只锁一个字节,协议开销太大。缓存行是个折中:粒度够细,又不会让 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 画成八爪鱼一样的图,其实关键就几点:
- 读缺失:从 I 到 S 或 E,取决于其他核的 snoop 回应
- 写命中:S→M 必须失效所有共享副本;E→M 静默升级
- 写缺失: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 状态减少内存回写) |
2.4 本章知识体系图
2.5 小结与我的习惯
本章我们走了三步:
- 缓存行:一致性的原子操作单位,记住 64B 对齐。
- MESI:四个状态加 snoop 总线,写失效是主流。
- MOSI:用 O 状态解决写共享时频繁回内存的问题。
我个人习惯在做过一致性仿真时,先把缓存行大小配对,否则后果就是数据撕裂。另外,千万不要把 MESI 和 MOESI 的状态表背下来就完事——你必须理解每个事务对应的总线开销。CXL.cache 里很多性能调优,本质上都是在平衡 snoop 开销和写回开销,和 MOSI 的动机一模一样。