# MSI 协议 —— 从"为什么需要缓存一致性"讲起

> MSI 是缓存一致性最朴素的形态（Modified / Shared / Invalid 三态）。但在讲 MSI 之前，得先想清楚一个更根本的问题：**多核 CPU 为什么非要一套"一致性协议"？不要会怎样？** 想通了这个，才知道 MSI 在解决什么，以及 [MESI](/concepts/cache/mesi.md) 的 E、[MOESI](/concepts/cache/moesi.md) 的 O、[MESIF](/concepts/cache/mesif.md) 的 F 各自又在优化什么。


> 本篇是家族的**问题起点**；协议细节的主文档是 [mesi.md](/concepts/cache/mesi.md)，横向对比见 [comparison.md](/concepts/cache/comparison.md)。

## 一、先看物理拓扑：多核缓存长什么样

讲协议前，必须先看清硬件是怎么连的——协议管的就是这张图里"缓存副本之间"的一致性。一颗现代多核 CPU 的存储层次大致如下：

```plantuml
@startuml
skinparam shadowing false
skinparam componentStyle rectangle
skinparam rectangle {
  BackgroundColor<<core>>  #E3F2FD
  BorderColor<<core>>      #1976D2
  BackgroundColor<<l1>>    #FFF9C4
  BorderColor<<l1>>        #F9A825
  BackgroundColor<<l2>>    #FFE0B2
  BorderColor<<l2>>        #EF6C00
  BackgroundColor<<l3>>    #C8E6C9
  BorderColor<<l3>>        #388E3C
  BackgroundColor<<mem>>   #FFCDD2
  BorderColor<<mem>>       #C62828
}
rectangle "Core 0" <<core>> {
  rectangle "L1 (私有)\n~1ns" <<l1>> as L1_0
  rectangle "L2 (私有)\n~4ns" <<l2>> as L2_0
  L1_0 -down-> L2_0
}
rectangle "Core 1" <<core>> {
  rectangle "L1 (私有)\n~1ns" <<l1>> as L1_1
  rectangle "L2 (私有)\n~4ns" <<l2>> as L2_1
  L1_1 -down-> L2_1
}
rectangle "Core 2" <<core>> {
  rectangle "L1 (私有)" <<l1>> as L1_2
  rectangle "L2 (私有)" <<l2>> as L2_2
  L1_2 -down-> L2_2
}
rectangle "Core 3" <<core>> {
  rectangle "L1 (私有)" <<l1>> as L1_3
  rectangle "L2 (私有)" <<l2>> as L2_3
  L1_3 -down-> L2_3
}
rectangle "L3 / LLC (所有核共享)  ~30ns" <<l3>> as L3
rectangle "主内存 DRAM   ~100ns" <<mem>> as MEM
L2_0 -down-> L3
L2_1 -down-> L3
L2_2 -down-> L3
L2_3 -down-> L3
L3 -down-> MEM
note right of L3
  总线 / 片上互联(ring, mesh)
  各核的私有缓存通过它互相“嗅探”
  ——一致性协议就作用在这一层
end note
@enduml
```

读这张图的三个要点：

1. **L1/L2 是每个核私有的**（黄/橙），**L3(LLC) 和主内存是所有核共享的**（绿/红）。一致性问题**只发生在"私有缓存"这一层**——因为同一份数据会在多个核的 L1/L2 里各留一份副本。
2. **速度差是根本动机**：L1 ~1ns、L2 ~4ns、L3 ~30ns、内存 ~100ns。正因为私有缓存快 100 倍，才值得每核各留一份；代价就是要一套协议维持它们一致。
3. **各核私有缓存靠"互联 + 嗅探"互相通气**：早期是一条共享**总线**，现代是片上 **ring/mesh 互联**（跨插槽则走 [NUMA](/concepts/numa/numa.md) 的 QPI/UPI/Infinity Fabric）。协议的所有"广播、失效、转发"消息都跑在这条线上——**这也是为什么跨核一致性开销大：它要走互联，不是本地命中。**

> 一致性协议(MSI/MESI/…)本质上就是：**给每个核私有缓存里的每个 cache line 标一个状态，让它们通过互联互相监听，保证多份副本对外看起来像一份。** 下面先看数据在这几级缓存间到底怎么流动。

## 二、数据在多级缓存里怎么流动（前置基础）

一致性协议管的是"缓存里的副本"，那副本是怎么进缓存、又怎么变脏的？这些属于**单核缓存机制**，是理解协议的前置基础，本仓库已在 [cache-organization.md](/concepts/cache/cache-organization.md) 里讲透：

- **缓存怎么判命中**：物理地址拆 Tag/Index/Offset、组相联、cache line 元数据——见 [cache-organization.md](/concepts/cache/cache-organization.md)。
- **"快/慢"的单位**：时钟周期 / 总线周期 / 指令周期，以及各级缓存延迟的周期换算——见 [cache-organization.md 第十一节](/concepts/cache/cache-organization.md)。
- **读**：L1→L2→L3→内存 逐级下探、命中即返回、未命中回填；**写**：write-back（只改 L1 标脏）+ write-allocate（不在则先取上来）——见 [cache-organization.md 第十节](/concepts/cache/cache-organization.md)。MSI 里 **M(Modified) 状态**的物理含义"本核缓存比内存新"就来自 write-back。

这里只强调把上面单核流程引向"多核一致性"的**两个分叉**——它们正是 MSI 存在的理由：

- **读的分叉**：如果一条 line 正被**另一个核**以脏状态(M)持有，L3/内存里还是旧值——此时不能直接读内存，必须靠一致性协议从那个核**拿到最新值**（这就是下面第四节 MSI 要解决的，也是跨核读比本地读慢的原因）。
- **写的分叉**：要改一条别的核也持有的行，硬件先发 **RFO(Read For Ownership)** 让其他核的副本失效，独占后才能写——**脏数据 + 抢独占，正是下面一致性问题和 MSI 状态机的来源。**

> 联系伪共享：缓存以 **64B 的 cache line** 为单位维护一致性——两个变量只要 Tag+Index 相同（落在同一条 line），就共享这条 line 的**同一个 MESI 状态**，一个核写它会波及另一个核。这是伪共享的地址级根源（详见 [mesi.md](/concepts/cache/mesi.md) 第五节、[cache-organization.md 第八节](/concepts/cache/cache-organization.md)）。

## 三、根本问题：多核私有缓存导致"数据不一致"

现代 CPU 每个核都有自己的私有 L1/L2 缓存（拓扑图黄/橙块）。数据以 **cache line（64 字节）** 为单位在各级缓存和内存间搬运。问题就出在"私有"：**同一个内存地址 X，可能同时被多个核各缓存了一份副本**。

```plantuml
@startuml
skinparam shadowing false
skinparam componentStyle rectangle
skinparam rectangle {
  BackgroundColor<<core>> #E3F2FD
  BorderColor<<core>>     #1976D2
  BackgroundColor<<mem>>  #FFCDD2
  BorderColor<<mem>>      #C62828
}
rectangle "Core 0 私有缓存\nX = 5" <<core>> as C0
rectangle "Core 1 私有缓存\nX = 5" <<core>> as C1
rectangle "主内存\nX = 5" <<mem>> as MEM
C0 --> MEM
C1 --> MEM
note top of C0 : 变量 X 被两个核各存了一份副本
@enduml
```

## 四、不做一致性会出什么错？（用图看清 bug）

假设**没有**任何一致性机制，核 0 把 X 从 5 改成 100：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  LifeLineBorderColor        #90A4AE
}
participant "Core 0" as C0
participant "Core 1" as C1
participant "主内存" as MEM
note over C0, MEM : 初始 X=5，两核缓存里都是 5
C0 -> C0 : 写 X = 100（只改了自己缓存）
note left of C0 : Core0 缓存: X=100 ✅
C1 -> C1 : 读 X
note right of C1 : Core1 缓存命中，读到 X=5 ❌\n(它根本不知道 Core0 改过)
C0 -> MEM : (稍后)写回 X=100
note over MEM : 内存: X=100，但 Core1 仍看到 5
@enduml
```

**结果：Core1 读到了过期的 5。** 两个核对同一个变量看到不同的值——程序逻辑直接崩坏。典型后果：

- 一个线程更新了标志位 `ready=1`，另一个线程在别的核上永远看到 `ready=0`，死等。
- 计数器 `count++` 在多核上丢更新、算错总数。
- 锁的状态在不同核不一致，两个线程同时进临界区。

> 这就是**缓存一致性问题(cache coherence problem)**。没有硬件保证，多核共享内存的编程模型根本不成立——你写的 `x = 1` 别的核可能永远看不到。

## 五、需要保证什么？—— 一致性的两条底线

一致性协议要让"多份缓存副本对外表现得像唯一一份内存"。具体保证两点：

1. **写传播(write propagation)**：一个核对 X 的写，最终必须让其他核看得到。
2. **写串行化(write serialization)**：所有核看到的"对同一地址的写顺序"必须一致（不能核 A 觉得先写 5 再写 100、核 B 觉得先 100 再 5）。

**怎么实现？两条路线**：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<q>>   #FFF9C4
  BorderColor<<q>>       #F9A825
  BackgroundColor<<inv>> #C8E6C9
  BorderColor<<inv>>     #388E3C
  BackgroundColor<<upd>> #FFCCBC
  BorderColor<<upd>>     #E64A19
}
rectangle "谁改了共享数据，怎么通知别人？" <<q>> as Q
rectangle "写失效 write-invalidate\n『我要写，你们的副本作废』\n→ MSI / MESI / MOESI / MESIF" <<inv>> as INV
rectangle "写更新 write-update\n『我写了新值，推给你们更新』\n→ Dragon / Firefly" <<upd>> as UPD
Q --> INV
Q --> UPD
note bottom of INV : 主流。写者只发一个失效信号(便宜)
note bottom of UPD : 少见。每次写都广播新数据(贵)
@enduml
```

**MSI 及其家族走"写失效" **：某核要写共享行前，先让其他核的副本失效，保证同一时刻"可写的副本"只有一份，天然满足上面两条底线。（写更新路线见 [dragon.md](/concepts/cache/dragon.md)。）

那怎么"通知别人失效"？早期靠**总线嗅探(bus snooping)**：所有核挂在一条共享总线上，每个核都监听总线；谁要读写就在总线上广播，别的核看到与自己相关的地址就据此改自己的状态。MSI 就是这套嗅探机制上最小的状态集。

## 六、MSI 的三种状态

MSI 给每个 cache line（在每个核里）标三态之一：

| 状态 | 全称 | 含义 | 和内存一致? | 可否直接写 |
|------|------|------|:---:|:---:|
| **M** | Modified | 本核改过、是最新值，内存里是旧的；**独此一份** | 否（脏）| 可 |
| **S** | Shared | 一个或多个核持有干净副本，与内存一致，只读 | 是 | 否（要先让别人失效）|
| **I** | Invalid | 本核该行无效/没有 | — | — |

有了这三态，第四节那个 bug 就被堵住了：核 0 要写 X 时，必须先把状态推到 M，**这一步会让核 1 的副本变 I**；核 1 再读 X 时发现自己是 I（失效），只能重新去取核 0 的最新值——不会再读到过期的 5。

## 七、状态机（PlantUML）

先约定几个术语——它们分**处理器侧(Pr)** 和**总线侧(Bus)** 两类信号：

| 缩写 | 英文全称 | 含义 |
|------|----------|------|
| `PrRd` | **Pr**ocessor **Rd**（read）| 本核处理器发起的**读** |
| `PrWr` | **Pr**ocessor **Wr**（write）| 本核处理器发起的**写** |
| `BusRd` | **Bus** **Rd**（read）| 总线上**别的核**发起的读请求（本核嗅探到）|
| `BusRdX` | **Bus** **Rd**（read）e**X**clusive | 别的核发起的**读独占**请求（读了是为了写，即 RFO, Read For Ownership）|
| `BusUpgr` | **Bus** **Upgr**ade | 别的核把自己已有的 S 副本**升级**为 M 的请求（无需重新取数，只需让别人失效）|
| `Flush` | Flush | 把处于 M 的最新数据**写回内存 / 转发**给请求方 |

- **`Pr*` 是"本核主动做"，`Bus*` 是"嗅探到别人做"**——同一条边，站在发起方是 `PrWr`，站在旁观方嗅探到的就是 `BusRdX`/`BusUpgr`。
- 下图中：**实线 = 本核 `Pr*` 操作触发的迁移；带 `[嗅探]` 前缀 = 监听到别人 `Bus*` 操作而被动迁移。** 颜色：<font color=#C62828>红=Modified(脏)</font>、<font color=#388E3C>绿=Shared(干净共享)</font>、<font color=#757575>灰=Invalid(失效)</font>。

```plantuml
@startuml
skinparam shadowing false
hide empty description
skinparam state {
  BackgroundColor<<mod>> #FFCDD2
  BorderColor<<mod>>     #C62828
  BackgroundColor<<sha>> #C8E6C9
  BorderColor<<sha>>     #388E3C
  BackgroundColor<<inv>> #EEEEEE
  BorderColor<<inv>>     #757575
}
state "Modified" as M <<mod>>
state "Shared"   as S <<sha>>
state "Invalid"  as I <<inv>>
[*] --> I
I --> S : PrRd / BusRd 取数
I --> M : PrWr / BusRdX 抢独占
S --> M : PrWr / BusUpgr 让别人失效
S --> S : PrRd 命中
M --> M : PrRd, PrWr 命中
M --> S : [嗅探] BusRd / 先 Flush 再降级
M --> I : [嗅探] BusRdX / Flush 后失效
S --> I : [嗅探] BusRdX 或 BusUpgr
note right of I
  关键局限：即使只有本核读一行、
  没别人共享，也只能进 Shared，
  无法像 MESI 那样进 Exclusive
end note
@enduml
```

一句话：**本核读写让自己往上升（I→S→M）；嗅探到别人的读写把自己往下压（M→S→I）。**

## 八、时序图：多核协作下的状态转移全景

上面的状态机是"一条 cache line 自己的视角"。这里换成**时序图**，把参与的模块——**核A、核B、总线(嗅探/仲裁)、主内存**——都画出来，看同一个地址 X 在多核并发读写时，两个核的 MSI 状态如何一步步迁移。

参与模块与约定：

- **核A / 核B**：各自的私有缓存，括号里标它对 X 的当前 MSI 状态，`→` 表示状态迁移。
- **总线**：所有核挂在上面、互相嗅探的共享通道，也做仲裁（同一时刻只让一个核发起事务）。
- **内存**：X 的最终归属；只有 M 态数据比它新。
- 事务名同前：`BusRd`(读)、`BusRdX`(读独占,意在写)、`BusUpgr`(S→M 升级)、`Flush`(把 M 写回/转发)。

### 场景1：核A 读一个谁都没有的 X（I → S）

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #1976D2
  LifeLineBorderColor        #90A4AE
}
participant "核A\n(I)" as A
participant "总线" as BUS
participant "内存" as M
A -> BUS : PrRd 未命中 → 发 BusRd(X)
BUS -> M : 无人持有 → 去内存取
M --> BUS : 返回 X 的数据
BUS --> A : 数据给 A
note over A : 核A: I → S\n(拿到干净副本)
@enduml
```

### 场景2：核B 也来读同一个 X（S → S，共享）

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #388E3C
  LifeLineBorderColor        #90A4AE
}
participant "核A\n(S)" as A
participant "总线" as BUS
participant "核B\n(I)" as B
participant "内存" as M
B -> BUS : PrRd 未命中 → 发 BusRd(X)
BUS -> A : 嗅探: 有核持有 X 吗?
A --> BUS : 我有(S)，干净，可共享
alt 由内存供数(基础 MSI)
  BUS -> M : 取 X
  M --> B : 数据
else 或由 A 转发(cache-to-cache)
  A --> B : 数据
end
note over A : 核A: S (不变)
note over B : 核B: I → S\n两核现在都是 S
@enduml
```

**这里数据从哪来，是个设计选择**——图里的 `alt` 画了两条路：内存供数 or A 转发(cache-to-cache)。二者都可行，因为 **S 是干净态、内存里的值和缓存副本完全一致**，读它谁给都对。差异在：

| | 内存供数 | cache-to-cache 转发 |
|---|---|---|
| 谁给数据 | 内存控制器从 DRAM 读 | 持 S 副本的核直接从缓存转发 |
| 延迟 | 慢（~200–300 拍，走内存时钟域）| 快（~几十拍，走互联）|
| 内存带宽 | 占用 | 不占用 |
| 复杂度 | 简单 | 复杂——**多个核都持 S 时，谁负责转发？** |

- **基础/教科书 MSI 通常选内存供数**：简单可靠，且没机制解决"多个 S 副本谁应答"的抢答问题，干脆都回内存。
- **现代高性能 CPU 想要 cache-to-cache**（内存太慢），但必须**加状态来指定唯一应答者**：这正是 [MESIF 的 F(Forward)](/concepts/cache/mesif.md)（Intel，指定一个 F 转发、其余 S 沉默）和 [MOESI 的 O(Owned)](/concepts/cache/moesi.md)（AMD，连脏数据也能转发）存在的理由。
- **对比场景4**：读**脏(M)** 数据时没得选，**必须** cache-to-cache（或先写回）——因为内存是旧的，只有 M 核有最新值。所以"从哪供数"的分叉只出现在读**干净(S)** 数据时。

> 一句话：**读干净数据，内存供数和 cache-to-cache 都对；基础 MSI 图省事选内存，真实 CPU 为了快选 cache-to-cache，代价是要加 F/O 状态管"谁转发"。** 这也是理解 MESIF/MOESI 为何存在的入口。

场景2 还留了两个"图上一笔带过、真机上很关键"的问题——**这些协议消息落在哪一级缓存(L1↔L1？L2↔L2？)、cache-to-cache 供数多了互联流量怎么办、广播→点对点到底换了什么**。它们是把逻辑协议对上真实芯片的"硬件落地"话题，对整个协议族通用，已统一收进 [comparison.md 第七节"一致性的硬件落地"](/concepts/cache/comparison.md)。

### 场景3：核A 要写 X（S → M，让核B 失效）—— MSI 的核心

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #C62828
  LifeLineBorderColor        #90A4AE
}
participant "核A\n(S)" as A
participant "总线" as BUS
participant "核B\n(S)" as B
A -> BUS : PrWr → 发 BusUpgr(X)\n(我要写，请大家失效)
BUS -> B : 嗅探到 BusUpgr(X)
B -> B : 作废自己的副本
note over B : 核B: S → I
BUS --> A : 独占许可
A -> A : 本地改 X，标脏
note over A : 核A: S → M\n(现在只有 A 有最新值)
note over A, B : 写传播+串行化达成：\n同一时刻只有 A 能写 X
@enduml
```

上面时序图把总线画成一根"广播线"，是**基础 MSI 的教科书模型**。但你多半会追问两件事，这两点恰好把前面"广播 vs 点对点"的讨论落到 `BusUpgr` 这一条具体消息上：

**追问一：`BusUpgr` 是广播吗？** —— **在基础 MSI/嗅探式模型里是；现代目录式 CPU 上不是。**

- **基础嗅探式（图里画的）**：核A 不知道到底有谁持有 X 的 S 副本，只能往共享总线上**广播** `BusUpgr(X)`，所有核都嗅探到、各自判断"我有没有 X"。这就是广播——发一次、全体听。
- **现代目录式**：LLC 的目录里明确记着"X 现在被哪些核以 S 持有"。核A 的写请求先到目录，目录**只给真正持 S 的那几个核定向发失效(invalidate)**，其余核根本不被打扰。这就是前面说的"广播→点对点"在写路径上的体现——`BusUpgr` 的语义(升 M 前让别人失效)不变，但**投递方式从广播变成了定向**。

**追问二：需要等对端 ACK 吗？** —— **需要。这是一致性正确性的硬要求，不能"发完就写"。**

核A 必须**等到确认所有 S 副本都已失效**，才能把自己升到 M 并动手改 X。为什么？若不等 ACK 就写：核B 的旧 S 副本可能还没作废，此刻核B 去读，就命中了那份**过期数据**——正是第四节要堵的 bug 又回来了。所以写路径上有一次**"失效—确认"握手**：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #C62828
  LifeLineBorderColor        #90A4AE
}
participant "核A\n(S)" as A
participant "目录/互联" as DIR
participant "核B\n(S)" as B
participant "核C\n(S)" as C
A -> DIR : ① BusUpgr(X)：我要写，请持有者失效
note over DIR : 目录知道 X 被 B、C 持有\n→ 只定向发这两家(不广播全体)
DIR -> B : ② invalidate(X)
DIR -> C : ② invalidate(X)
B --> DIR : ③ Inv-Ack（我已作废）
C --> DIR : ③ Inv-Ack（我已作废）
note over B : S → I
note over C : S → I
DIR --> A : ④ 收齐全部 Ack → 授予独占
note over A : 直到这一刻才 S → M\n之前不能改 X！
A -> A : ⑤ 本地改 X，标脏
@enduml
```

要点：

- **ACK 是必须的**：核A 要收齐**所有**被失效核的 `Inv-Ack`（图里 B、C 两份都要到），才拿到独占许可、才能写。少收一份都不行——那份没确认的副本可能还在被别的核读。
- **这也是跨核写为什么慢**：一次 `counter++` 里的写，若这行正被别核共享，硬件要走"发失效 → 等所有 ACK 回来"这一圈**互联往返**，几十上百拍就耗在这。核越多、持有者越多，等的 ACK 越多，越慢——这正是[伪共享](/concepts/cache/mesi.md)让性能塌方的物理原因。
- **优化不是"取消 ACK"，而是"少等 / 异步等" **：**[store buffer](/concepts/memory-ordering/store-buffer.md)** 让核A 把写暂存、不阻塞后续指令，等 ACK 期间继续干别的活（代价是引入内存乱序，要靠内存屏障纠正——详见 [store-buffer.md](/concepts/memory-ordering/store-buffer.md)）；**目录 + 精确失效**把"等全体"缩小成"等真正的几个持有者"。但"最终必须收齐 ACK 才能让写对外可见"这条底线不变。

> 一句话：**`BusUpgr` 在嗅探式里是广播、目录式里是定向；无论哪种，核A 都必须等到所有 S 副本回 `Inv-Ack` 才能升 M 动笔——这次"失效-确认"握手是写传播正确性的硬要求，也是跨核写慢和伪共享的根子。store buffer 能把"等"藏到后台，但藏不掉。**

### 场景4：核B 读被 A 改过的 X（触发 A 的 M → S）

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #6A1B9A
  LifeLineBorderColor        #90A4AE
}
participant "核A\n(M)" as A
participant "总线" as BUS
participant "核B\n(I)" as B
participant "内存\n(旧值)" as M
B -> BUS : PrRd 未命中 → 发 BusRd(X)
BUS -> A : 嗅探: X 你是 M?
A -> A : 是，我这份最新
A -> BUS : Flush：交出最新数据
note over A : 核A: M → S\n(不再独占，降为共享)
BUS -> M : 顺便写回内存(更新旧值)
BUS --> B : 最新数据给 B
note over B : 核B: I → S
note over M : 内存也被刷新为最新\n(不能让 B 读到旧值)
@enduml
```

> 若场景4 里核B 是**要写**（发 `BusRdX` 而非 `BusRd`），则核A 会 `Flush` 后直接 **M → I**（彻底作废，因为 B 马上要独占改写），核B 取到数据后 **I → M**。这就是状态机里 `M --[嗅探 BusRdX]--> I` 那条边的时序展开。

**把四个场景连起来**就是 X 的完整生命周期：`A读(I→S)` → `B读(→都S)` → `A写(A升M、B降I)` → `B再读(A降S、B升S)`……每一步的状态迁移，都能在第七节状态机上找到对应的边——**状态机是"规则"，时序图是"规则在多核上的实际演出"**。

## 九、MSI 的痛点：读后即写，白发一次广播

MSI 只有三态，**分不清"我独占且干净"和"大家共享"**——凡是读进来的行一律标 S。于是"先读、再写"同一行时要发两次总线事务：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #C62828
  LifeLineBorderColor        #90A4AE
}
participant "Core A" as A
participant "总线" as BUS
== MSI：读后写 = 两次总线事务 ==
A -> BUS : ① PrRd → BusRd（读入 X，标记 Shared）
A -> BUS : ② PrWr → BusUpgr（要升 M，广播让别人失效）
note over A, BUS : 可其实根本没别的核持有 X\n→ 第②次广播完全多余
@enduml
```

而**读后即写是极其常见的模式**：`counter++`（读旧值→加一→写回）、"分配内存后初始化再修改"、"取出对象改个字段"……MSI 每次都要为第 ② 步白发一次 `BusUpgr`。

MESI 加一个 **E(Exclusive)** 状态就治好了它：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #388E3C
  LifeLineBorderColor        #90A4AE
}
participant "Core A" as A
participant "总线" as BUS
== MESI：同样场景只要一次 ==
A -> BUS : ① PrRd → BusRd，发现无人共享 → 标记 Exclusive
A -> A   : ② PrWr → E 静默升级为 M，零总线消息
note over A, BUS : E 表示“独占且干净”，\n写它时无需通知任何人
@enduml
```

**E 就是给"独占且干净"的行一条免广播升级的快车道**——这是 MESI 相对 MSI 存在的全部理由。

## 十、家族演进：每个新状态省一种开销

MSI 能"正确工作"，但每种后续协议都为省掉某类总线/内存开销加了一个状态：

| 演进 | 加的状态 | 省掉什么 | 谁用 |
|------|---------|---------|------|
| MSI → [MESI](/concepts/cache/mesi.md) | **E** Exclusive | "独占干净行"首次写的广播 | 通用基础 / Intel 早期 |
| MESI → [MOESI](/concepts/cache/moesi.md) | **O** Owned | 脏行被读时必须先写回内存 | AMD |
| MESI → [MESIF](/concepts/cache/mesif.md) | **F** Forward | 多个共享副本谁应答新读者（抢答/都读内存）| Intel（QPI/UPI）|
| （另一条路）[Dragon](/concepts/cache/dragon.md) | 写更新替代写失效 | ——（不同思路，非主流）| 教学/历史 |

> 一句话：**MSI 是"能用"的最小集；E/O/F 都是为"少发一次总线消息 / 少写一次内存 / 少一次抢答"而加的优化状态。** 完整对比见 [comparison.md](/concepts/cache/comparison.md)。

## 十一、硬件实现：总线协议、时钟域与开销

MSI 这套状态机在**真实芯片**上怎么落地——缓存控制器里的硬件（Tag+状态阵列、本核访问口、嗅探口）、经典嗅探总线的事务阶段、多时钟域(核心/uncore/内存)、以及一致性通信的周期级开销——都属于**通用缓存硬件机制**，已统一收进 [cache-organization.md 第十一节"缓存访问的时钟域与开销"](/concepts/cache/cache-organization.md)。

> 时钟域与开销、总线通信协议、缓存控制器硬件，见 [cache-organization.md 第十一节](/concepts/cache/cache-organization.md)。

## 十二、对开发者的意义

MSI 本身你在真机上碰不到（商用 CPU 都用增强版），但它讲清了**为什么会有伪共享**：既然一致性以 **cache line** 为单位维护，两个核写同一 cache line 里的不同变量，也会互相触发失效——这就是 [mesi.md](/concepts/cache/mesi.md) 第五节讲的伪共享。规避手段（cache line 对齐）对整个协议族通用。

> 相关：主协议与状态机/伪共享 [mesi.md](/concepts/cache/mesi.md)；变种 [moesi.md](/concepts/cache/moesi.md) / [mesif.md](/concepts/cache/mesif.md)；写更新对照 [dragon.md](/concepts/cache/dragon.md)；家族对比 [comparison.md](/concepts/cache/comparison.md)；伪共享观测 `perf c2c` 见 [mesi.md](/concepts/cache/mesi.md) 第六节。

## 十三、结论：MSI 是"对，但慢"

把全篇收成一句话——**MSI 在功能上完全正确，它的问题纯粹是性能。** 分三点讲清"对在哪、慢在哪、慢不影响对"：

### 13.1 正确性：MSI 已经能"正常工作"，没有可见性 bug

MSI 的三态严格满足第五节的两条一致性底线，这就够了：

| 底线 | MSI 怎么保证 | 对应 |
|------|-------------|------|
| **写传播** | 核 A 写 X 前必须升 M，强制让其他核的副本降 I(失效)；别人再读只能重取最新值 | 场景3 的 `BusUpgr` + 失效-ACK 握手 |
| **写串行化** | 总线仲裁 / 目录保证同一时刻只有一个写事务，所有核看到的写顺序一致 | 总线仲裁见 [cache-organization.md 11.3](/concepts/cache/cache-organization.md) |

于是第四节"Core1 读到过期 5"的 bug 被彻底堵死。**在缓存一致性(coherence)这一层，MSI 不会有线程可见性问题**——它和 MESI/MOESI/MESIF 完全等价，后面那些状态没有一个是为"正确性"服务的。

### 13.2 一个必须澄清的边界：coherence ≠ 内存顺序

"没有可见性问题"要限定在**缓存一致性**这一层。跨核的**内存乱序**是另一回事——它由 [store buffer](/concepts/memory-ordering/store-buffer.md) 引入（核把写暂存、不等 ACK 就继续跑），要靠内存屏障纠正。**但那不是 MSI 的责任**：

- MSI 管的是"多份副本对外像一份"（coherence）；
- store buffer / 内存屏障管的是"写何时对外可见、能否重排"（consistency / ordering）。

两层职责不同。准确说法是：**MSI 保证一致性正确，不保证顺序一致性；顺序问题不归 MSI，也不是 MSI 的缺陷。**

### 13.3 唯一的真问题：性能——读后即写白发一次广播

MSI 三态分不清"独占且干净(E)"和"共享(S)"，读进来的行一律标 S。于是**读后即写**（`counter++` 等极常见模式）每次都要为升 M 白发一次 `BusUpgr`——哪怕根本没有别的核持有这行（详见第九节）。E/O/F 全是来省这类开销的（对照表见第十节），**没有一个在修正错误**。

> **一句话结论：MSI 已经能正常工作、线程在一致性层面没有可见性问题，它的短板只是性能（读后即写多一次广播、无法 cache-to-cache 高效转发）。E/O/F 以及目录、嗅探过滤、mesh 互联，全都是在这套"已经正确"的骨架上做性能优化，而非修 bug。**
