MESI 协议机制 —— 四态、状态机与总线事务
承 msi("为什么需要一致性"的问题起点)和 mesi(一致性协议族主文档/总纲)。本篇只讲协议本身怎么工作:每个 cache line 的四种状态、两套状态机(本核读写 + 总线嗅探)、以及一次跨核写操作触发的完整总线事务。伪共享这个头号性能坑见 伪共享篇。
一、问题的根源:多核私有缓存
现代 CPU 每个核有自己的 L1/L2 缓存,内存以 cache line(缓存行,通常 64 字节) 为单位在缓存和内存间搬运。同一 cache line 会被多个核各存一份私有副本——一致性问题就出在这里。
完整的物理拓扑(L1/L2 私有、L3/内存共享、总线/互联如何连接各核、各级延迟)和"为什么需要一致性"的问题起点,见 msi 第一节——协议就作用在"私有缓存"这一层。本篇直接从"协议怎么做"讲起。
核心矛盾:同一块内存被多个核各缓存了一份。核 0 改了自己缓存里的 X,核 1 缓存里还是旧的 X——如果放任不管,两个核看到的内存值就不一致了,程序会算错(这个 bug 的完整演示见 msi 第四节)。
缓存一致性协议就是硬件用来保证"多个副本对外看起来始终像一份"的规则。MESI 是最经典的一种:给每个 cache line 标一个状态,用总线嗅探(bus snooping) 让各核互相监听、按状态机迁移。
术语:这类协议叫 write-invalidate(写失效)——某核要写时,先让别人的副本失效,保证同一时刻只有一个"可写"的副本。对立的一类是 write-update(写更新)。两条路线的决策分叉见 msi 第五节,写更新详解见 dragon。MESI/MOESI/MESIF 都属于写失效族。
二、四种状态(MESI)
每个 cache line 在每个核的缓存里独立地处于四态之一:
| 状态 | 全称 | 含义 | 和内存一致? | 其他核可能有副本? | 可否直接写 |
|---|---|---|---|---|---|
| M | Modified(已修改) | 本核改过、是最新值,内存里是旧的;独此一份 | 否(内存脏) | 否 | 可(已拥有) |
| E | Exclusive(独占) | 只有本核缓存了它,且未改、与内存一致 | 是 | 否 | 可(无需通知别人) |
| S | Shared(共享) | 多个核都缓存了,都是干净只读副本 | 是 | 是 | 否(要先让别人失效) |
| I | Invalid(无效) | 本核这行失效/没有,不能用 | — | — | — |
三条关键不变式(协议时刻维持):
- M 和 E 是"独占"态:全系统只有一个核能对某行处于 M 或 E;此时别的核对该行必为 I。
- 写之前必须独占:要写一行,得先把它变到 M(若在 S,先让所有其他副本失效)。
- M 态的数据比内存新:别人要读它,必须先从 M 那个核拿到(write-back 或转发),不能直接读内存。
E 状态存在的意义(MESI 相比更简单的 MSI 的关键优化):如果一个核独占且干净(E),它要改这行时不需要发任何总线消息就能直接升到 M——因为没人有副本,不用通知谁。省下的这次总线广播,在"读后即写"(如 counter++、初始化后修改)的常见模式里非常划算。MSI 里"独占干净行"首次写也要广播,MESI 用 E 把这条免了(两协议的逐拍对比见 msi 第六节)。
三、状态机(PlantUML)
沿用 msi 第七节 的信号约定——分处理器侧(Pr) 和总线侧(Bus) 两类:PrRd/PrWr(本核读/写)、BusRd(嗅探到别核读)、BusRdX(别核读独占/RFO)、BusUpgr(别核把 S 升 M)、Flush(把 M 写回或转发)。完整术语表见 msi 第七节。
配色:、、、。
3.1 本核处理器操作驱动的迁移

3.2 总线嗅探(监听到别人操作)驱动的迁移
每个核同时在监听总线,看到别的核对自己缓存的行发起操作时,被动迁移:

一句话概括状态机:本核读写让自己"往上升"(I→S/E→M);别人的读写通过总线嗅探把自己"往下压"(M→S→I)。
四、一次写操作触发了什么(总线事务时序)
假设核 A、核 B 都缓存了同一行且都是 S,现在核 A 要写:

代价:每次跨核的"写-读"往返都要经过总线广播、失效、Flush、重载。这套通信走 CPU 间互联(和 NUMA 同一套 QPI/UPI/Infinity Fabric)。偶发无所谓,高频发生就是性能灾难——这正是伪共享的机理(详见 伪共享篇)。
五、MESI 与 store buffer / invalidate queue 的衔接
MESI 保证的是"最终一致":核 A 发出失效请求后,要等所有被失效核回 Inv-Ack 才能确认独占。这个"等"字就是性能杀手——写操作被卡住,后面的指令全排队。
硬件的解法是异步化:
- store buffer(写缓冲):核 A 把写暂存进 SB,不等 ACK 就继续执行后续指令——代价是产生了写乱序(本核还没写完,别核就看到了更晚的写),要用内存屏障纠正。
- invalidate queue(失效队列):核 B 收到失效请求后先入队 ACK,不立即停下手头的事去处理——代价是读乱序(本核可能读到已被宣告失效的旧值),用读屏障排空。
这两件"拖延战术"正是 store-buffer 和 invalidate-queue 的主角。MESI 的握手信号(Inv-Ack、BusUpgr)是它们的输入;std::atomic 的 acquire/release 语义则是排空这两个队列的开关(见 atomic)。
一句话总结
MESI = 四态(M 脏独占 / E 干净独占 / S 共享 / I 失效)+ 两套状态机(本核读写"往上升"、总线嗅探"往下压")+ 一次总线事务(失效广播 → Flush → 重载);E 态是相对 MSI 的核心优化,握手"等 ACK"的代价则催生了 store buffer 与 invalidate queue。