目录与缺失

从MESI和MOESI到目录协议与Cache缺失分类

Posted by CloudingYu on May 27, 2026

一、MSI 协议回顾与问题分析

1.1 MSI 的基本框架

前文介绍的 MSI 协议是 cache 一致性的基础协议。在多处理器系统中,每个 CPU 有自己的 cache,共享同一块 memory。MSI 定义了三种 cache block 状态:

状态 含义 数据特征
Modified (M) 仅当前 cache 持有,已被修改 与 memory 不一致,只有这一份是最新的
Shared (S) 多个 cache 共享,均未被修改 与 memory 一致
Invalid (I) 该块无效/已失效 不能使用

1.2 数据共享与修改的核心逻辑

在这个框架中,有几种典型的场景:

  • 所有缓存都共享(shared):多个 cache 持有同一块,数据一致,各方都可以读
  • 某个缓存修改了(modified):某个 CPU 从 memory 读进数据后做了修改,但还没写回 memory
    • 这个数据变成了”独一无二”的一份——因为它被改过了,所以叫 modified
    • 其他 cache 中的这块数据被标记为 invalid
    • 只有持有 modified 块的那个 cache 拥有最新、最可用的数据

数据 share 的时候各方都一样,各方都可以用。但一旦有人修改了,那这块数据就是独一无二的了——因为被他改过了,所以才叫 modified。

1.3 数据传递的标准流程

当一个 cache 的数据是 modified,另一个 cache 想要读这个数据时,流程是:

  1. 持有 modified 块的 cache 先写回 memory(write-back / flush)
  2. 请求方再从 memory 读上来

小时候看到一道奥数题——已知烧开水的方法:拿壶装水、放炉上、开火烧。问题是”壶里已经有水,怎么烧开水?”答案是把水倒掉——这样就回到了原样的问题,直接调用原来的解法。这个智慧的思路是:与其写新代码,不如把问题转化成已有代码能处理的样子。

在这个场景中:写 memory 的代码本来就有,从 memory 读的代码本来就有,不需要写新代码。将数据先 flush 到 memory,然后对方从 memory 读——两个已有的代码段组合起来就解决了问题。

1.4 伊利诺伊大学研究者的发现

伊利诺伊大学的研究人员做了一个实验——实际去观察总线上发生了多少次通信,每次通信是不是必要的。他们发现了一个问题:

通过实际测量总线事务,可以发现协议中哪些通信是必要的、哪些通信可以被优化。MESI 正是从 MSI 的冗余总线事务中抽象出新的状态。


二、MESI 协议——从 MSI 到 MESI

2.1 动机:S 到 M 的不必要总线事务

在 MSI 协议中有一个问题:当某个 cache 要从 shared 状态进入 modified 状态时(即读进并修改一个 block),按照协议需要先通知所有其他持有者,将它们的数据失效。这会产生一个总线事务(bus transaction)。

但考虑一个常见场景:实际上只有一个 cache 持有这个块(即 shared 的 count = 1)。在这种情况下:

  • 该 cache 不需要通知其他 cache,因为没有其他共享者
  • 广播失效会浪费一次总线事务

某个 cache 读入并修改一个 block,会产生两个总线事务:第一个是 bus read 把数据读进来进入 S 状态,第二个是把其他副本 invalid 然后进入 M 状态。但如果没有其他共享者,第二个事务并不必要。

2.2 解决方案:添加 Exclusive(E)状态

MESI 的改进方式是在 MSI 的状态集合中增加一个更精确的状态,用来区分“只有一个 cache 持有且未修改”和“多个 cache 共享”这两种情况。

思路是:当 shared 的 count = 1(只有一个 cache 持有者,而且数据没有被修改过、与 memory 一致),这个状态就不再叫 shared,而是单独起一个名字——Exclusive(独占)。

状态 含义 与 memory 关系 持有者数量
Modified (M) 已修改 不一致,数据是旧的 1
Exclusive (E) 独占,未修改 一致,数据是新的 1
Shared (S) 共享 一致,数据是新的 >= 2
Invalid (I) 失效 0

2.3 Exclusive 状态的关键特性

Exclusive 状态的核心意义:表示”当前 cache 独有该块,且该块是干净的(clean),未被修改过”。

关键特性:

  • 区分了”一个人的 shared”和”多个人的 shared”
  • 处于 Exclusive 状态的块做更新时,不需要广播——因为只有当前 cache 持有,没有别人需要通知
  • 可以直接从 E 状态变为 M 状态,省掉了一次总线事务

Exclusive 就出现在”读进来还没写”的时候。某个 cache 读入来只要不写,就一直读,数据跟 memory 一样。但没有人跟我 share——所以不能叫 shared;也没被修改过——所以不能叫 modified。这就是 Exclusive。

2.4 E 状态与 M 状态的区别

  Exclusive Modified
数据被修改过? 否(clean) 是(dirty)
与 memory 一致?
回头要更新时 memory 能用吗? 不能(memory 是旧的)
转换为 M 需要通知别人? 不需要(就自己)
memory 中的数据 最新

在 Exclusive 状态下这两个数据是一致的(cache 和 memory),这一点非常重要。因为我回头要更新数据的时候,我得知道 memory 的数据能不能用。Modified 的话我就知道 memory 不能用了。Share 状态呢?Share 状态就是因为 cache 和 memory 数据一样,所以我是可以用 memory 的。

2.5 硬件实现:Wired-OR 信号

为了判断一个块是否只有当前 cache 持有,需要在总线上增加一条 shared signal 线:

  • 使用 wired-OR(线或)实现——只要有一个 cache 的 shared 信号为高,总线就被拉高
  • Wired-OR 不需要经过门电路延迟,直接把金属线连在一起,速度极快
  • 当读取一个块时,如果 shared 信号线没有被拉高(说明没有其他 cache 持有),则该块进入 E 状态;如果被拉高(有人也有),则进入 S 状态

不需要经过门电路做逻辑,就是把线连在一起——只要有一个为 1,其他都能拉上去。这就叫 wired-OR。这是极简的实现方式,速度更快。

2.6 MESI 状态迁移图

MESI 有四个状态,每个状态之间的转换取决于两个事件源:

  • Processor 的操作(来自自己的 CPU):读(read)或写(write)
  • Bus 上的操作(来自其他 CPU/设备):bus read、bus write 等

MESI 的状态迁移可以从事件来源推导:如果多个 cache 都在读同一块,则保持 Shared;如果只有本地 processor 读且总线上没有其他读请求,则可以保持 Exclusive;如果只有本地 cache 持有最新副本并发生写操作,则进入或保持 Modified。

理解每个状态的所有权、是否脏以及是否存在其他共享者,比机械记忆状态图更重要。


三、Cache 到 Cache 数据传输与 MOESI 协议

3.1 动机:能不能直接从 cache 传数据?

在 MESI 协议中,当 P0 的数据是 modified,P1 想读这块数据时,流程是:

  1. P0 先把数据 flush 到 memory(一次写)
  2. P1 再从 memory read(一次读)

这就涉及两次 memory 访问,而 memory 很慢。

直觉想法:能不能让 P0 直接把数据传给 P1?cache 之间传数据快得多——在同一个芯片内部,cache 的速度远快于 memory。

3.2 cache-to-cache 传输的问题

这个想法在只有两个 CPU 时没问题(伊利诺伊大学就用的两个核)。但当 CPU 多了以后就出问题了:

  • P1、P2、P3、P4 都 share 着数据
  • P5 想从 cache 里拷一份——从谁的 cache 拷?
  • 谁离得近?谁不太忙?
  • 需要一个 selection 机制来决定由谁来提供数据

选择过程很复杂。离得近的那个现在可能特别忙,因为有任务一直在跑;不太忙的可能离得远。到底按远近来选还是按忙碌程度来选?还得设计协议和机制。

当多个 cache 共享时,有个 selection 过程。而且总线上的通信量——之前两个核还好,挂四个核时总线上已经很繁忙了。你在用他在用,平白无故要从 cache 往那边拷数据,还要握手确认,这给总线增加了很大压力。最终发现这种实现代价可能高于直接从 memory 获取数据。

3.3 时代变化带来的瓶颈

时代的一粒沙,落到他头上就把他给压垮了。伊利诺伊大学做的时候只有两个核——不是问题。等多了以后就是问题了。

总线挂四个核、八个核、十六个核,总线越来越繁忙:

  • cache-to-cache 传输需要占用总线带宽
  • 需要握手确认、需要 selection
  • 总线成了瓶颈

3.4 MOESI 协议:添加 Owner 状态

面对”不想通过 memory 传输”和”不知道从谁那拷贝”的矛盾,一个新的思路产生了:

指定一个 owner——当数据在多个 cache 之间 share 时,指定其中一个作为”拥有者”(owner),由他来负责:

  • 将数据传给其他需要的 cache(cache-to-cache)
  • 最终将数据写回 memory(在数据不再需要时)

这就是 MOESI 协议中新增的 Owner (O) 状态:

状态 含义 与 memory 关系 关键特性
Modified (M) 已修改 不一致 只有当前 cache 持有
Owner (O) 拥有者 不一致 与其他 cache share,但 memory 是旧的,由 owner 负责最终写回
Exclusive (E) 独占 一致 只有当前 cache 持有
Shared (S) 共享 一致 多个持有,memory 也是新的
Invalid (I) 失效

Owner 状态的特征

  • 多个 cache share 同数据,但 memory 的数据是旧的(cache 和 memory 不一致)
  • 在 share 的这群 cache 中,只有一个被指定为 owner
  • owner 负责:给其他 cache 传递数据 + 最终写回 memory
  • 当数据不再被任何人需要时,owner 负责把 memory 更新

Owner 状态允许多个 cache 共享同一份最新数据,同时暂时不更新 memory。只要在共享者中指定一个 owner 负责后续数据转发和最终写回,就可以减少不必要的 memory 访问。

MOESI 的设计延续了 MESI 的思路:发现协议中仍有可优化的通信模式,就通过增加状态表达更精确的数据所有权关系。


四、监听协议的可扩展性问题与目录协议

4.1 总线监听的根本限制

监听协议的核心瓶颈是总线。所有一致性消息都依赖同一条总线传播,处理器数量增加后,总线带宽和广播开销都会成为限制。

监听协议在 CPU 数量很少时(2-4 个)工作得很好,但随着 CPU 数量增加:

  • 总线带宽有限,挂的 CPU 越多,冲突越多
  • 广播式通信效率越来越低
  • 协议状态越来越复杂(MSI → MESI → MOESI → …),但治标不治本

当系统扩展到数十乃至上百个核心时,继续在监听协议上增加状态无法根本解决总线瓶颈。

4.2 从集中式到分布式:两种共享存储架构

教材中展示了两种共享存储架构:

集中式共享存储(Symmetric Shared Memory)

  • 所有 CPU 通过一条总线连接到一个共享 memory
  • 每个 CPU 有自己的 cache
  • 这就是之前一直讨论的基于总线的监听协议架构
  • 适合 CPU 数量少的场景

分布式共享存储(Distributed Shared Memory)

  • 每个处理器有自己的 memory
  • 通过互联网络(interconnection network)连接
  • 数据通过消息传递来共享
  • 可以扩展支持更多处理器

对称式(Symmetric)的变体

  • 外部有较大的共享 cache(类似 L2/L3 cache)
  • 每个 CPU 有自己的私有 cache
  • 外部 shared cache 分成多个 bank(bank 0, bank 1, bank 2…)
  • 访问不同 bank 不会冲突——提高了带宽

将共享 cache 分成多个 bank,可以让不同访问落在不同 bank 上并行进行,从而提高总带宽并减少冲突。

4.3 目录协议的基本思想

如果所有处理器都挂在同一条总线上,总线会很快成为瓶颈。分布式共享存储转而使用互联网络进行点对点通信。

在分布式共享存储架构中,改用目录协议(Directory Protocol):

  • 维护一个目录,记录哪个 cache 持有哪个数据块的副本
  • 不需要广播——根据目录找到具体持有者,点对点通信
  • 避免了总线广播的瓶颈
  • 但目录本身需要存储空间(相当于之前讲过的”代价”)

4.4 研究者为什么要做分布式

分布式方案并不是集中式方案失效后才被想到的,而是早期核心数量较少时没有立即体现工程必要性。随着核心数增加,分布式目录协议的价值才变得明确。

前瞻性研究的价值在于提前建立可扩展架构:当核心数从少量扩展到更多时,集中式总线会成为瓶颈,而分布式目录协议可以继续扩展。

目录协议体现了体系结构研究中的一个典型模式:在当前规模下看似复杂的方案,可能在未来规模扩大后成为必要的基础机制。


五、多核环境下的 Cache 一致性与缺失分类

5.1 写入缺失与写入更新的非原子性问题

在监听协议中,写入缺失(write miss)和写入更新(write update)都不是原子操作:

写入缺失(Write Miss)

  • 先发现 cache miss
  • 做一次 bus read 把数据读进来
  • 然后做一次 write
  • 这中间可能被其他操作打断

写入更新(Write Update)

  • 写数据 → 更新到 memory → 同时广播让其他 cache 失效或更新
  • 这些步骤中间也有可能被打断

在单个多核心芯片中,可以通过占用总线(bus locking)来实现原子性:某个核心执行原子操作时暂时独占总线,其他核心等待。但随着核心数量增加,这种方式会严重限制并行性。

5.2 在没有总线的系统中

在分布式架构(没有总线)中,确保读写操作不被冲突就需要更复杂的机制——保持严格的读写顺序(ordering),这就是 memory consistency(内存一致性模型)问题。

5.3 总线流量的两个来源

总线上通信的流量主要来自两个方面:

  1. 单处理器 cache 缺失造成的流量:发生 cache miss 时,需要访问 memory 并占用总线
  2. 一致性协议造成的流量:MSI/MESI/MOESI 这些协议本身的通信信号导致的总线流量(invalidation、read 请求、write-back 等)

六、Cache 缺失的四种分类(4 C’s)

6.1 传统的 3C 模型

缺失类型 英文 含义
强制缺失 Compulsory 第一次访问一个块,cache 里不可能有。冷启动时的必然缺失
容量缺失 Capacity cache 太小,放不下所有需要的数据
冲突缺失 Conflict 两个数据映射到同一个 cache 位置,互相替换

6.2 新增第四类:通信缺失(Coherence/Communication Miss)

在多核系统中,处理器之间通信也会导致 cache miss——这就是通信缺失(Communication Miss)。

通信缺失又分为两类:

真共享缺失(True Sharing Miss)

两个处理器确实在访问同一个变量。例如 P1 和 P2 都访问 X1:

  • P1 把 X1 改成 modified → P2 的 X1 被 invalid
  • P2 再访问 X1 → miss,需要把最新数据拿回来
  • 这个缺失是真的由数据共享导致的——数据本身就两个人在用

假共享缺失(False Sharing Miss)

两个处理器访问的是不同的变量,但这些变量恰好落在同一个 cache block 里:

1
2
3
一个 cache block 里放了四个数据:X0, X1, Y0, Y1
P1 在用 X0, X1
P2 在用 Y0, Y1
  • P1 更新了 X0/X1 → P2 的整个 block 被 invalid(包括 Y0, Y1)
  • P2 正在算 Y0、Y1,突然发现自己的数据失效了
  • P2 重新读回来——发现 Y0、Y1 根本没变!只是因为它们在同一个 block 里被”连累”了
  • 过了一会儿 P2 更新 Y0/Y1 → P1 的 X0/X1 也被 invalid

假共享会导致 cache block 在多个处理器之间反复失效。虽然两个处理器访问的变量在逻辑上无关,但它们位于同一个 block 中,因此任一变量被写入都会使整个 block 失效。

如果 cache block 只有一个字,假共享可以避免;但实际系统为了利用内存带宽和空间局部性,通常一次读取多个字节。只要 block 大于单个变量,不相关变量落入同一 block 的情况就可能发生,假共享也就无法完全避免。

6.3 真共享 vs 假共享的区分

  真共享(True Sharing) 假共享(False Sharing)
访问的变量 同一个变量 不同变量,但在同一 block
缺失原因 数据确实被改了 数据没被改,只是 block 被整体 invalid
能否通过缩小 block 解决? 不能——再怎么缩小,数据就是共享的 可以缓解——block 越小越不容易”连累”
能否通过编程解决? 多线程编程的内在需求 可以通过数据布局优化避免

七、例题分析

7.1 题目

P1 和 P2 的 cache 中,X 和 Y 处于不同 cache block 中,且均为 Shared 状态。分析以下一系列访问操作,判断每次 cache 访问的结果是:命中(hit)、真共享缺失(true sharing miss)、假共享缺失(false sharing miss)、还是强制缺失(compulsory)。

(具体操作序列:P1 write X → P2 read Y → P1 read X → P2 write X → P1 write Y → P2 read Y → …)

7.2 关键分析

  • P1 write X(两人都 shared)→ P1 进入 Modified,P2 的 X 被 invalid → 这是命中(P1 自己 cache 里有 X,从 S 变 M)
  • P2 read Y:Y 本来在 P2 的 cache 里是 shared。但 P1 写 X 时——如果 X 和 Y 在同一个 block 里——P2 的整个 block 都失效了。所以 P2 read Y 时发生 miss。数据 Y 本身没被改,是被”连累”的 → 假共享缺失
  • P2 read X:P2 的 X 被 invalid 了(因为 P1 写了 X),数据确实被 P1 改了 → 真共享缺失
  • 后续每次 P1 写都导致 P2 的整个 block invalid,反之亦然

第一次写 X 是命中(自己 cache 里有)。其他某些操作是命中,某些是 true sharing miss(数据真的被对方改了),某些是 false sharing miss(数据没改,只是同在一个块里被 invalid 了)。关键要看:被 invalid 的时候,自己的数据到底有没有被人真的修改过。


八、多核 Cache 性能数据

8.1 OLTP 基准测试

通过 OLTP 基准测试(一种偏重数据库的测试基准),观察随着核数增加,各类缺失的变化:

  • True sharing miss:随着核数增长而增加——多线程间确实会访问共享数据
  • False sharing miss:也在稳步增长——核越多,不同变量落在同一 block 的概率越大
  • Conflict missCapacity missCompulsory miss:变化不大,基本稳定
  • Code miss(指令缺失):随核数略有增加,因为总 cache 量增加导致更多的冷启动

8.2 Block Size 对缺失率的影响

  • 总体上说,block 越大,miss per 1000 instructions 下降——空间局部性被更好地利用
  • false sharing miss 随 block 增大而增加——block 越大,无关变量被”连累”的概率越高
  • 其他类型缺失率随 block 增大基本下降

8.3 工程权衡

  • Processor 数量增加 → 外出访问时间变长
  • Block size 增大 → 空间局部性好但 false sharing 增加
  • 到底怎么调参数?根据实际情况——没有统一的答案

这体现了体系结构设计中的 trade-off:增加 processor 数量、增大 block size、降低缺失率或降低通信开销之间并不存在无代价的统一最优解,参数选择必须依赖目标负载和实现约束。