在多核 CPU 场景下,每个核心都有自己的私有缓存(L1、L2),而它们共享 L3 缓存和主内存。要保证不同核心缓存中的数据一致且不冲突,主要依靠 Cache Coherence Protocol(缓存一致性协议) 。
最经典、应用最广的是 MESI 协议。
1. MESI 协议:缓存行的四种状态
MESI 通过给每个缓存行(Cache Line)标记四个状态来管理:
| 状态 | 名称 | 描述 |
|---|---|---|
| M | Modified (修改) | 该行已被修改,与内存不一致,且只存在于当前核心缓存中。 |
| E | Exclusive (独占) | 该行与内存一致,且只存在于当前核心缓存中。 |
| S | Shared (共享) | 该行与内存一致,且可能存在于多个核心缓存中。 |
| I | Invalid (无效) | 该行数据已失效,不能使用,必须重新从内存或其它缓存读取。 |
2. 核心机制:总线嗅探 (Bus Sniffing)
仅仅有状态是不够的,核心之间必须“交流”。
- 动作:当核心 A 修改了自己缓存中的共享数据时,它会通过总线向外广播。
- 嗅探:核心 B 会持续监听总线。一旦发现总线上有针对自己缓存中某行数据的写操作,核心 B 会立即将该缓存行标记为 Invalid (I) 。
- 结果:下次核心 B 读取该数据时,发现是无效状态,就会被迫从内存或核心 A 那里获取最新的值。
3. 如何避免“冲突”?
多核竞争同一份数据时,硬件通过以下两级保障来防止冲突:
A. 内存屏障 (Memory Barriers)
编译器或 CPU 为了优化性能可能会指令重排。程序员(或底层库)使用内存屏障强制规定:屏障前的读写必须先于屏障后的读写。这确保了逻辑上的先后顺序。
B. 原子操作 (Atomic Operations)
在硬件层面,通过 Lock 前缀指令:
- 总线锁定:早期 CPU 会锁住总线,让其他核心无法访问内存。效率低,因为整个 CPU 停摆了。
- 缓存锁定(现代主流):利用 MESI 协议,如果数据在缓存内,CPU 只锁定该缓存行并标记为独占修改模式。此时其他核心无法同时修改这块内存,保证了操作的原子性。
4. 潜在的问题:伪共享 (False Sharing)
这是高性能编程中常见的“不冲突但低效”的问题。
- 现象:两个无关的变量(比如
A和B)恰好落在同一个缓存行(通常 64 字节)里。 - 后果:核心 1 修改
A,导致核心 2 的缓存行失效;核心 2 修改B,又导致核心 1 的缓存行失效。 - 解决:缓存行填充(Padding) 。在变量之间加入无意义的占位符,让它们分布在不同的缓存行。
总结
多核一致性 = MESI 状态机 + 总线嗅探机制。
为了让你直观理解 MESI 协议 是如何运作的,我们假设有一个变量 x = 10 存储在主内存中。现在有两个 CPU 核心(Core A 和 Core B)都要操作它。
以下是这四种状态随操作变化的完整链路:
1. 初始状态:独占 (Exclusive)
- 动作:Core A 从内存读取
x。 - 结果:Core A 发现没有其他核心加载过这个变量。
- 状态:Core A 的缓存行标记为 E。
- 解释:此时数据是干净的(和内存一致),且 Core A 独占它。
2. 协作状态:共享 (Shared)
- 动作:Core B 也从内存读取
x。 - 结果:Core A 嗅探(Sniffing)到了这个读取请求,并告诉 Core B:“我也存着呢”。
- 状态:Core A 和 Core B 的缓存行都转变为 S。
- 解释:大家都知道这份数据在多个核心里都有,且都与内存一致。
3. 修改状态:修改 (Modified)
-
动作:Core A 想要执行
x = 20。 -
过程:
- Core A 向总线发送一个“我要写这个地址”的信号。
- Core B 嗅探到信号,立即将其缓存行标记为 I (Invalid) 。
- Core A 修改数据。
-
状态:Core A 变为 M,Core B 变为 I。
-
解释:此时 Core A 里的数据是最新的,但主内存里还是旧的 10。Core A 负有将数据写回内存的责任。
4. 强制失效:无效 (Invalid)
-
动作:Core B 现在想读取
x。 -
过程:
- Core B 发现自己的状态是 I(数据不可信)。
- Core B 发起读取请求。
- Core A 拦截该请求,先将最新的
x = 20同步回内存(或直接传给 Core B)。 - Core A 和 Core B 再次回到 S 状态。
-
状态:双端回到 S。
-
解释:这就是“缓存一致性”的体现,Core B 绝不会读到旧的 10。