一、Cache 一致性问题回顾
1.1 核心问题
多处理器系统中每个 CPU 有自己的 cache,当多个 CPU 持有同一地址的副本时,一个 CPU 修改了数据,其他 CPU cache 中的副本就会变成过时数据。如何保证所有 CPU 看到的 memory 视角一致?
在多核系统中,每个核心独立使用自己的 cache,但共享主存。当某个核心修改了共享数据后,其他核心 cache 中的副本就变成了脏数据——类似于共享存储系统中,一个节点修改了数据而其他节点并未感知的情形。
1.2 两种基本思路
在 cache 一致性问题上,有两种基本思路:
| 方案 | 描述 | 代价 |
|---|---|---|
| 目录协议(Directory-based) | 建立一个集中目录,记录每个数据块在哪些 cache 中有副本,谁访问了谁修改了 | 需要大量内存存放目录信息 |
| 监听协议(Snooping-based) | 让硬件来监听总线上的消息,不需要软件维护目录 | 总线成为瓶颈 |
评估任一方案时都应首先考虑代价。目录协议会消耗相当大的内存空间来存放目录,同时还需要在 cache 中额外存储目录信息——这与页表和快表(TLB)的关系类似。任何提出”加一个目录”的方案,都意味着需要额外的存储开销和访问开销。
1.3 硬件设计的现实约束
在硬件行业中,”能不改就不改”是一个重要的工程原则:
- 流片成本极高:一次流片失败可能意味着上亿美元的损失
- 验证成本极高:硬件无法像软件那样随意 patch,出错后无法通过补丁修复
- 参考设计的价值:实际硬件设计大量依赖成熟参考设计,每一次修改都需要严格的验证
二、监听协议(Snooping Protocol)
2.1 基本思想
监听协议的核心思想是:每个 cache 自己监听总线上的事务,根据总线上的消息判断自己手里的数据是否还有效、是否需要更新。这将对一致性维护的负担从软件转移到了硬件上——各 CPU 自行监听总线变化并做出响应,软件工程师无需直接管理缓存一致性。
2.2 总线作为广播媒介
在基于总线的多处理器系统中,总线是所有事务的广播媒介:
- 每个处理器访问 memory 时会产生总线事务(transaction)
- 每个 cache 的 snoop 控制器监听总线上的所有事务
- 总线控制器负责串行化(serialization)写操作——多个处理器同时发起写请求时,由总线控制器进行仲裁,确保写操作按序执行
在 cache 一致性协议的设计中,cache 控制器不仅需要服务本地 CPU 的读写请求,还必须持续监听总线上的其他事务——包括其他 CPU 的读写操作、数据失效通知和 cache 间数据传输请求。
2.3 监听协议中的两种策略
在监听协议框架下,当一个处理器写数据时,有两种通知方式:
| 写更新(Write-update) | 写失效(Write-invalidate) | |
|---|---|---|
| 行为 | 写了以后马上广播,让所有持有者更新 | 写第一个字节时就广播”作废”,让所有人标记失效 |
| 优点 | 别人读的时候直接 hit,延迟低 | 总线事务少,省电,对连续多次写友好 |
| 缺点 | 连续写时广播太多,总线拥挤 | 别人需要读的时候要多做一次 transfer |
| 适用场景 | 一写多读(如 GPU、神经网络推理) | 通用 CPU,模式复杂多变 |
两种策略各有取舍:写更新策略在广播时产生更多总线流量但读延迟低;写失效策略减少了总线事务但增加了读缺失时的传输开销。在计算机体系结构中,每种方案都有其代价——需要根据实际需求来取舍。
2.4 为什么写失效是主流
在通用 CPU 上,写失效协议更常见:
- 总线事务少:对同一块的多个字连续写时,写更新需要对每个字都广播;写失效只需要广播一次
- 省电:总线每次传输都消耗能量,在功耗墙面前很重要
- 通用 CPU 负载复杂:操作系统上各种任务混合,数据没有明显特征,写失效更平衡
写更新策略在特定领域仍然活跃。在 GPU 内部、专用 AI 芯片中,数据流以流水线方式从前往后写,写更新就很合适。选择哪种策略应根据实际应用的数据访问模式决定。
2.5 总线事务(Bus Transaction)
监听协议中总线上的基本事务包括:
| 事务 | 含义 | 说明 |
|---|---|---|
| Bus Read | 总线上有处理器读缺失,需要去 memory 拿数据 | 最基本的读请求 |
| Bus Read Exclusive | 读进来并即将修改(read with intent to modify) | 告诉别人”我要改了,你们作废” |
| Bus Write Back / Flash | 脏数据块被替换掉,需要写回 memory | 写回策略才需要 |
| Cache-to-cache Transfer | 数据直接在 cache 之间传输 | 比从 memory 读更快 |
在支持 cache-to-cache transfer 的系统中,当一份数据已在其他 cache 中时,可以直接从该 cache 获取而不是访问主存,这比从 memory 到 cache 的传输要快得多。
2.6 Cache 控制器的角色
在多处理器一致性协议中,cache 控制器职责大增:
- 服务 CPU:处理来自本处理器的 load/store 请求
- 监听总线:监听总线上别人的读写和失效通知
- 发起总线事务:自己也要在总线上申请、发失效消息、做 transfer
在使用写回策略时,主存中的数据不一定是最新的——最新版本的数据可能存在于某个 cache 中而非主存,因此 cache 控制器在响应读请求时需要判断数据的最新副本在何处。
三、MSI 协议
3.1 三个状态
MSI 协议(Modified, Shared, Invalid)是监听协议中最基础的缓存一致性协议,为每个 cache block 定义三个状态:
| 状态 | 缩写 | 含义 |
|---|---|---|
| Modified(已修改) | M | 只有当前 cache 持有,已被修改,主存中是旧数据,当前 cache 是唯一的”新鲜版本” |
| Shared(共享) | S | 当前 cache 有一份,可能其他 cache 也有,与主存一致 |
| Invalid(无效) | I | 这个数据块没有进入 cache,或被别人失效了 |
M 态的核心含义是:当前 cache 拥有唯一的最新副本。其他处理器需要该数据时,必须从当前 cache 获取,而不能从主存读取。
3.2 MSI 状态机
MSI 协议的核心是有限状态机(FSM),每个 cache block 根据三类事件发生状态迁移:
三类事件:
- Processor 操作:本处理器发出的 read / write(hit 或 miss)
- Bus 操作:总线上观察到的 bus read / bus read exclusive / bus write back
- Block replacement:cache block 被替换掉
状态迁移要点:
- M 态:只有自己有,读写都是 hit,不产生总线事务。如果总线上别人要读(bus read),则把自己数据写回 memory 并 transfer 过去,变为 S。如果被替换掉,写回 memory 变为 I。
- S 态:读是 hit,不产生总线事务。写则需要先发 bus read exclusive(通知别人失效),然后进入 M 态。如果总线上有别人写(bus read exclusive),自己的数据失效进入 I。S 态允许多个 cache 共享同一份数据——一个 cache 发起读请求时,其他持有 S 态副本的 cache 不做排斥。
- I 态:读则 miss,发 bus read 取数据,进入 S(如果有别人持有)或进入 M(如果没有别人持有且需要写)。写则 miss,发 bus read exclusive 取数据并修改,进入 M。
3.3 MSI 协议详解:典型场景
以下通过一个具体例子(P1 和 P2,数据 A1)展示完整的 MSI 协议运作流程:
场景:memory 中 A1 = 10,两个处理器 P1 和 P2
- P1 写 10 到 A1(write miss):
- 总线上发 bus read exclusive
- P1 cache 将 A1 读入,状态变为 M(独占、已修改)
- 此时 P1 cache 中 A1 = 10,memory 中仍是旧值
- P1 再读 A1(read hit):
- M 态下直接 hit,不产生总线事务
- 状态仍为 M
- P2 读 A1(read miss):
- 总线上发 bus read
- P1 监听到后:将数据写回 memory,从 M 变为 S
- P2 cache 读入 A1 = 10,状态为 S
- 此时 P1 和 P2 都是 S(共享一致)
- P2 写 20 到 A1(write hit on S):
- P2 是 S 态,写需要先通知别人失效
- 总线上发 bus read exclusive(通知 P1 作废)
- P1 的 A1 变为 I(失效)
- P2 变为 M,A1 = 20
- memory 中 A1 仍为 10(等到 P2 cache block 被替换时才写回)
- P2 再写 A2(A2 与 A1 在同一 cache block 中):
- 因为 A1 和 A2 在同一个 cache line 中
- P2 是 M 态,写 A2 也是 hit,不产生总线事务
- P2 cache 中 A2 = 40(原来是 25)
核心规则:M 态下读写都不产生总线事务。一旦有处理器发起 bus read,M 态持有者需要写回 memory、传递数据给请求者,然后降级为 S。一旦有处理器发起 bus read exclusive,S 态持有者直接作废,M 态持有者传输数据并写回 memory 后作废。
四、MSI 协议的问题:虚假共享(False Sharing)
4.1 问题场景
MSI 协议有一个严重的问题:失效粒度过大。因为 cache coherence 是以 cache line(整个 block) 为单位的,失效操作是对整个 block 做的,而不是对单个字。
例如:假设 A1 和 A2 在同一个 cache block 中。P1 和 P2 都持有这个 block(都是 S 态)。现在 P1 修改了 A2,P2 那边的 A1 还能用吗?答案是不能——因为失效是按 block 来的,P1 一改 A2,P2 整个 block 都被 invalidate,P2 的 A1 虽然没被改也失效了。
4.2 无谓开销
这种场景带来了不必要的总线传输开销:
- P1 和 P2 共享 block,A1 = 5,A2 = 5(S 态)
- P1 改 A2 为 6,发 bus read exclusive
- P2 整个 block 被 invalidate(A1 和 A2 都不能用了)
- P2 要读 A1(本来还是 5),发现 I 了,必须做 transfer
- P1 把整个 block transfer 给 P2,P1 变成 S
- 后续又有处理器修改,如此反复
4.3 动机
这个例子揭示了 MSI 协议的一个不足:即使数据没被修改,只要同一个 block 中另一个数据被写了,整个 block 都会被作废,导致不必要的总线传输。这引出了对 MSI 的改进——MESI 协议。
五、MESI 协议
5.1 新增 Exclusive 状态
MESI 协议(Modified, Exclusive, Shared, Invalid)在 MSI 的基础上增加了 E(Exclusive,独占) 状态。
| 状态 | 含义 |
|---|---|
| Modified | 只有当前 cache 持有,已被修改,与主存不一致 |
| Exclusive | 只有当前 cache 持有,但还没修改,与主存一致 |
| Shared | 当前 cache 有一份,可能其他 cache 也有,与主存一致 |
| Invalid | 数据无效 |
E 状态的关键洞察是:当从主存读进一个数据时,如果只有当前 cache 持有、且数据与主存一致,那它就是 E 态而不是 S 态。此时如果处理器要写这个数据,直接从 E 变为 M,不需要在总线上发消息通知别人作废——因为根本没有别人持有这个数据。
5.2 E 态的价值
E 态的引入解决了 MSI 的一个痛点:
- MSI 的问题:处理器从主存读数据时,不知道别人有没有,只能进入 S 态。等到要写的时候,即使是唯一持有者,也要发 bus read exclusive 通知”作废”——但实际上根本没有人有。
- MESI 的改进:读数据时,如果确保没有别人持有,进入 E 态。从 E 态写数据时,直接变为 M,不需要产生总线事务,省掉一次总线操作。
5.3 Bus Read Exclusive 的作用
在 MESI 协议中,bus read exclusive 是一个关键的总线事务。它的含义是”我要读进来并且要修改”。
- 如果当前是 M 态,观察到 bus read exclusive → 说明别人要写这个数据 → 自己先把最新数据 transfer 过去、写回 memory,然后变 I
- 如果当前是 S 态,观察到 bus read exclusive → 说明别人要写 → 自己的副本直接变 I
- 如果当前是 E 态,观察到 bus read exclusive → 把数据 transfer 过去,变 I
bus read exclusive 与普通 bus read 的关键区别在于多了一个 “X”(exclusive),它向总线上的其他 cache 传达的信号不是”共享”,而是”我要独占并修改”,因此其他 cache 的响应是作废而非保持共享。
5.4 何时给 Memory 写回
MESI 协议中 memory 的更新时机:协议选择在 flash(cache block 被替换)操作时更新 memory。
需要注意到实际上所谓的”memory”可能本身就是一层 L3 cache——在层次化存储中,对上级 cache 而言的”主存”,对下级而言仍是 cache。因此数据从 memory 来还是从另一个 cache transfer 过来,逻辑上是一致的——S 态保证了 cache 数据和 memory 一致。
5.5 状态机核心要点
设计 MESI 状态机的基本方法:
- 先画出最简单的系统:两个处理器 + 各自的 cache + 总线 + memory
- 列出三类事件:处理器读、处理器写、总线上的事务
- 逐一分析每个状态遇到读怎么办、遇到写怎么办、遇到总线事务怎么办
对于读请求:如果是 read hit,无论当前是什么状态,都不会改变状态也不会产生总线操作。如果是 read miss,则根据当前状态按协议规则走——发 bus read 或 bus read exclusive 获取数据并转换到相应状态。
六、总结
6.1 核心框架
整个 cache 一致性的核心框架由三个要素构成:两个 CPU 带各自 cache,用总线连起来,底下是 memory。在这个框架中,核心问题是:数据在 cache 之间怎么传送?数据在 cache 和 memory 之间怎么传送?一个 cache 的数据修改后如何使其他 cache 的副本作废?作废时要不要写回 memory?
6.2 与之前知识的联系
本节内容与此前知识的联系:
- write through vs write back:写时马上更新 memory 还是等替换时再更新 → 一致性协议中对应”何时更新 memory”
- write update vs write invalidate:写时通知大家更新还是通知大家作废 → 一致性协议的两种策略
- cache 的 tag/valid/dirty 位:一致性协议用更多状态位(M/S/I/E)来管理
设计中没有绝对的最优方案:如果应用特点是”一写多读”,写更新更合适;如果各种访问模式都有,写失效更省总线带宽。选择应基于实际负载的数据访问模式。