MESI协议详解:状态转换与总线操作
缓存一致性协议里,MESI 是绕不开的经典。说白了,它就是给每个缓存行贴了个标签——Modified、Exclusive、Shared、Invalid。这四个状态决定了数据能不能用、该不该写回、要不要通知别人。我最早接触这玩意儿是在调一套四路 AMD Opteron 系统,当时总线上莫名其妙出现大量 BusRdX,查了半天才发现是状态机实现有个 corner case 没处理好。嗯,咱们今天就把这事掰开说透。
状态定义
- M – Modified:缓存行只在这一个核里有,且被修改过(脏数据)。内存里的副本已经过时,该行写回内存前不能让别人读。
- E – Exclusive:只在这一个核里有,但没修改过(干净数据)。内存副本是最新的,别人可以申请读,但读后状态会变成 S。
- S – Shared:多个核都有这个缓存行,且所有副本都和内存一致。谁都不能乱写,写之前必须先拿到独占权。
- I – Invalid:该缓存行无效,啥数据都不算数。下次访问必须重新从总线或内存读取。
我个人习惯把这四个状态想象成“房间权限牌”:M 是主人独占且改过家具,E 是主人独占但没动过,S 是合租房大家都看得到,I 是退租了。你想想看,总线上喊一嗓子,大家就得按这个规则换牌。
状态转换:一张表说清所有路径
| 当前状态 | 本地事件 | 总线操作 | 下一状态 | 总线信号说明 |
|---|---|---|---|---|
| I | 本地读 (PrRd) | BusRd | E(无人共享) 或 S(有人共享) | 请求读缓存行;嗅探器返回是否共享 |
| I | 本地写 (PrWr) | BusRdX | M | 请求独占写;其他核收到后必须失效自己的副本 |
| E | 本地读 (PrRd) | 无 | E | 命中,无需总线事务 |
| E | 本地写 (PrWr) | 无 | M | 直接本地修改,总线不参与 |
| E | 远端读 (BusRd) | — | S | 本地提供数据,状态降级为共享 |
| E | 远端写 (BusRdX) | — | I | 本地提供数据并失效自己 |
| S | 本地读 (PrRd) | 无 | S | 命中,直接返回 |
| S | 本地写 (PrWr) | BusUpgr | M | 通知其他核失效;若总线需要写回,先 Flush |
| S | 远端读 (BusRd) | — | S | 不提供数据(由内存提供) |
| S | 远端写 (BusRdX) | — | I | 收到失效信号 |
| M | 本地读 (PrRd) | 无 | M | 命中,直接返回 |
| M | 本地写 (PrWr) | 无 | M | 本地写,状态不变 |
| M | 远端读 (BusRd) | Flush + 提供数据 | S | 将脏数据写回内存,并共享给请求方 |
| M | 远端写 (BusRdX) | Flush + 提供数据 | I | 写回内存,然后失效 |
这表我建议你贴在工作台旁边。调试的时候遇到总线风暴,对着表一查,基本能定位是哪个核在反复做 BusRdX 导致颠簸。我在项目里就碰过一次,S 状态下连续 PrWr 触发 BusUpgr,但设计时忘了处理 Flush 争用,结果挂死……嗯,后面会讲到怎么避坑。
总线操作:节点间的喊话
MESI 的通信全靠 4 种总线事务。我习惯把它们理解成“喊话内容”:
- BusRd:读请求。“我要这块数据,谁有?”——其他核如果状态是 M,要先把脏数据写回内存,并且自己降成 S;如果状态是 E 或 S,直接共享,但不用写回。
- BusRdX:读并独占。“我要写这块数据,所有人都给我失效!”——不管谁来,看到这个信号必须立即把自己副本置为 I。如果本核是 M,还要先 Flush 写回。
- BusUpgr:升级请求。“我本来有 S 状态,现在想写,你们失效吧。”——注意这个事务不携带数据,只是失效通知。当时有个同事问我为什么不用 BusRdX?因为 S 状态本地已经有数据,不需要内存再发一次,节省带宽。
- Flush:写回。“我这儿有脏数据,现在把它刷到内存。”——一般由 M 状态的核在被 BusRd/BusRdX 打断时发起,Flush 完成后状态降级为 S 或 I。
状态转换图
一个典型流程:首次写入共享行
假设 Core A 和 Core B 都缓存了地址 X,状态都是 S。现在 Core A 要写 X:
- Core A 检测到本地写命中,但状态是 S,不能直接写。
- Core A 发出 BusUpgr 事务,通知所有其他核失效地址 X。
- Core B 嗅探到 BusUpgr,将 X 的行状态置为 I,并返回 ACK。
- Core A 收到所有 ACK(保证没有其他核还持有 S 或 E 状态),将本地状态改为 M,并执行写入。
- 此时只有 Core A 的数据是最新的,内存还是旧值。
这个流程里没有数据从内存搬动,只有失效广播。所以我常说 BusUpgr 是 MESI 协议里最优雅的设计——用最小的总线带宽换取一致性。但优雅也意味着风险:如果总线事务队列处理不当,ACK 丢失或超时,系统直接挂死。
性能关键:避免不必要的 BusRdX 和 Flush
核心指标:一次 BusRdX 的代价大约是 BusUpgr 的 1.5~2 倍,因为它要额外读一次内存。Flush 更贵,因为它要启动写回。所以优化 MESI 性能的秘诀就是:
- 减少 M→S 的降级(增加伪共享感知的数据布局)
- 让 S→M 尽量走 BusUpgr,不走 BusRdX
- 避免频繁的远端读导致 M 状态反复 Flush
我记得有一次优化一个分布式数据库引擎,发现 cache miss 率正常,但总线利用率异常高。抓包一看,全是 BusRdX 代替 BusUpgr——原因是一个函数里的原子操作指令用了非标准的缓存行锁定,导致协议降级为“直写+失效”。改为普通的 compare-and-swap 配合 BusUpgr 后,总线上事务数降了 40%。所以说,有时候问题不在协议本身,而在编译器或 API 的用法上。