为了解决 CPU 与内存、I/O设备之间读取速度不一致的问题引入了多级缓存,但同时带了数据不一致的问题,为了解决这种数据不一致性问题,又引入了缓存一致性协议,其中,MESI 协议是其中最著名的一种。
1 缓存写策略
缓存写策略是指在多核处理器系统中,缓存对主存进行写操作时采用的策略。
- 从缓存和内存的更新关系来看,缓存写策略可分为写回(Write Back)和写直达(Write Through),其中:
- 写回
- 概念:每次写操作,先写入缓存,但不立即写入内存,仅当缓存行被替换(也就是缓存行失效)时,才将数据写回主存
- 特点:
- 当 CPU 修改缓存数据时,数据只写入缓存,而不是立即写入主存
- 只有当缓存行被替换/失效时,才将修改后的数据写回主存
- 优点:
- 写操作速度比较快
- 适合写操作密集的应用,因为减少了对主存的访问次数
- 缺点:
- 数据一致性较差,主存的数据和缓存中的数据不一致,当其他处理器或者设备读取主存时可能得到过期的数据
- 硬件实现复杂,需要额外的机制来管理缓存行的状态,用来决定何时将数据写回主存
- 写直达
- 概念:每次写操作,同时将数据写入缓存和主存
- 特点:
- 当 CPU 修改缓存数据时,立即将数据同步写入主存
- 保持缓存和主存之间数据一致性
- 优点:
- 数据一致性好
- 实现简单,硬件设计容易
- 缺点:
- 写操作速度较慢,因为每次需要将数据同步到主存
- 不适合写操作密集的应用
- 从写缓存时各CPU之间的更新策略来看,缓存写策略可分为:写更新(Write Update)、写无效(Write Invalidate),其中:
- 写更新
- 概念:每次缓存写入新的值,该处理器必须发起一次总线请求,通知其他处理器更新他们,其他处理器接收到通知后,检查其缓存中是否包含相同的数据
- 优点:
- 其他处理器能够立即获取最新的值
- 缺点:
- 每次写操作都需要通过总线来通知其他处理器,这样会占用很多总线带宽
- 写无效
- 概念:每次缓存写入新的值,都将其他处理器缓存中对应的值置位无效。
- 优点:
- 多次写操作只需要发一次总线事件,第一次写操作已经将其他处理器缓存中对应的缓存行置为无效,之后的写不必要更新状态,这样可以节省总线带宽
- 缺点:
- 当其他处理器需要访问该缓存行时,发现缓存行以无效,必须从主存中重新加载新的数据
- 从写缓存时数据是否被加载来看,缓存写策略可分为:写分配(Write Allocate)、写不分配(Not Write Allocate),其中:
- 写分配
- 概念:当 CPU 尝试写入一个不在缓存中的数据时,先将数据块从主存加载到缓存中,然后在缓冲中进行写操作
- 优点:
- 提高缓存命中率,当数据加载到缓存中,后续对同一数据的读写可以直接命中缓存
- 性能较好,结合写回策略时,减少对主存的访问次数
- 缺点:
- 初始写操作延迟较大,需要先将数据从主存中读取到缓存中
- 可能导致缓存污染,对于一些不常用的数据也被加载到缓存中,占用宝贵的缓存空间
- 写不分配
- 概念:当 CPU 尝试写入一个不在缓存中的数据时,直接将数据写入到主存,不将数据块加载到缓存
- 优点:
- 避免在不经常访问的数据上浪费缓存空间
- 性能较差
- 缺点:
- 缓存命中率低
2 MESI 缓存一致性协议
MESI 协议是一个基于失效的缓存一致性协议,是支持写回缓存策略的最常用协议,同时为了解决多个核心之间的数据传播问题,提出了总线嗅探策略,即:所有的读写请求都通过总线广播给所有处理器,然后让各个处理器去嗅探这些请求,再根据本地的状态进行相应。MESI 是 Modified、Exclusive、Shared、Invalid 的简称,它定义了每个缓存行的四种状态。
缓存行(Cache Line)是缓存和主存之间数据交换的最小单位,每个缓存行包含了 Flag、Tag 和 Data 三部分,通常 Data 大小为 64 字节,但不同型号的 CPU 的 Flag 和 Tag 的大小可能不同。
2.1 四种状态
MESI 每种状态及其对应的监听事件如下:
- Modified:简写 M,修改
- 描述:该状态表示当前缓存行有效,但数据被修改了,和主存中的数据不一致,数据仅存在当前核的缓存中
- 监听事件:如果其他 CPU 核需要读取主存中对应的数据,该缓存行必须先回写到主存,并且状态置为 S。
- Exclusive:简写 E,独占
- 描述:该状态表示当前缓存行有效,缓存和内存中的数据一致,数据仅存在当前核的缓存中,即,当前缓存是唯一持有该数据的副本且与主存一致
- 监听事件:缓存行必须监听其他缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要置为 S 状态。如果当前核写数据时,则置为 M 状态。
- Shared:简写 S,共享
- 描述:该状态表示当前缓存行有效,缓存和内存中的数据一致,数据存在多个核的缓存中
- 监听事件:缓存行必须监听其他核的缓存使该缓存行无效或者独享该缓存行的请求,并且将该缓存置为 I 状态
- Invalid:简写 I,无效
- 描述:当前缓存行是无效的
- 监听事件:无
2.2 状态切换
假设这样一个场景,你有一个2核的 CPU,编号分别是:CPU0、CPU1,并且内存中有1条数据,编号data0,对应的内存地址分别是 address0。
场景1:CPU0 的数据状态为 M
| 事件 | 行为 | 下一个状态 |
|---|---|---|
| local read | CPU0 读取本地缓存中的数据,状态不会改变 | M |
| local write | CPU0 修改本地缓存中的数据,状态不会改变 | M |
| remote read | CPU1 读取数据,CPU0 将数据写回到内存并将状态置为 S,CPU1 拿到数据后也将状态置为 S | S |
| remote write | CPU1 修改数据,CPU0 将数据写回到内存并将状态置为 I,CPU1 拿到数据后进行修改并将状态置为M | I |
场景2:CPU0 的数据状态为 S
| 事件 | 行为 | 下一个状态 |
|---|---|---|
| local read | CPU0 读取本地缓存中的数据,状态不会改变 | S |
| local write | CPU0 修改本地缓存中的数据,将数据写回到内存并将状态置为 M,CPU1 如果持有数据则将其置为 I | M |
| remote read | CPU1 读取数据,CPU0 中的数据状态不会变化,CPU1 中的数据状态也不会变化 | S |
| remote write | CPU1 修改数据,将数据写回到内存并将状态置为 M,CPU0 中的状态置为 I | I |
场景3:CPU0 的数据状态为 E
| 事件 | 行为 | 下一个状态 |
|---|---|---|
| local read | CPU0 读取本地缓存中的数据,状态不会改变 | E |
| local write | CPU0 修改本地缓存中的数据,状态置为 M | M |
| remote read | CPU1 读取数据,CPU0、CPU1 都将数据状态置为 S | S |
| remote write | CPU1 修改数据,将数据写回到内存并将状态置为 M,CPU0 中的状态置为 I | I |
场景4:CPU0 的数据状态为 I,表示当前数据失效或者不存在数据
| 事件 | 行为 | 下一个状态 |
|---|---|---|
| local read | 如果 CPU1 没有数据,则 CPU0 获取到数据后状态置为 E;如果 CPU1 有数据且数据状态为 M,则需要 CPU1 现将数据同步到内存并将其状态置为 S,CPU 0 获取到数据后将状态置为 S;如果 CPU1 有数据且数据状态为 E 或 S,则 CPU0 从内存读取数据,CPU0、CPU1 将状态置为 S。 | E 或者 S |
| local write | 如果 CPU1 没有数据,则 CPU0 从内存获取数据后修改数据并将其状态置为 M;如果 CPU1 有数据且状态为 M,则 CPU1 需要将数据写回到内存,CPU0 获取数据后修改数据并将其状态置为 M,CPU1 将数据状态置为 I;如果 CPU1 有数据且数据状态为 E 或者 S,则 CPU0 从内存读取数据后修改数据并将状态置为 M,CPU1 将数据状态置为 I | M |
| remote read | 当前数据失效,远程读不会改变现有状态 | I |
| remote write | 当前数据失效,远程写不会改变现有状态 | I |
2.3 存在的问题与优化策略
MESI 缓存一致性协议保障了多颗 CPU 内核之间同内存之间的数据一致性,但它本身也存在一些问题,主要在于当 CPU 内核中的数据状态发生变化时,需要通过总线通知并等待其他 CPU 核的响应,然后修改本身 Cache Line 的状态,这对于 CPU 来讲是很耗时的操作,会大大降低 CPU 的处理性能。为了解决上述问题,引入了 Store Buffer 和 Invalidate Queue,对应的架构示意图如下图所示。
Store Buffer:Store Buffer 位于 CPU 和其缓存之间,当 CPU0 需要写数据时,把需要写的值丢到 Store Buffer 中,然后再去执行其他任务,而无需等待其他 CPU 的响应结果。当其他 CPU 给出了响应结果后,CPU0 再将 Store Buffer 中的数据写入到内存中。当 CPU0 需要读取数据时,优先在 Store Buffer 确认是否有数据,如果有,则返回,如果没有,则从缓存中读取。
Invalidate Queue:当 CPU0 接收到 Invalidate (使无效)的消息后,并不会立即响应,而是将消息存储到 Invalidate Queue 中并立即返回 Invalidate Ack 消息,然后 CPU0 在合适的时机执行 Invalidate 操作。
计算机的演进就是一部反复挖坑、填坑的发展史
引入 Store Buffer、Invalidate Queue 机制也带来了一些其他问题,主要有2个:Store Buffer 何时写回内存没有保证、Invalidate Queue 何时执行没有保证。为了解决这些问题,尽可能释放 CPU 的处理能力,引入了“内存屏障”(Memory Barrier)。
内存屏障包括了_写屏障_和_读屏障_,其中写屏障是告诉 CPU,在执行这之后的指令之间需要将 Store Buffer 中保存的指令刷新到内存中;读屏障是告诉 CPU,在执行这之后的任何加载数据指令之前,执行所有已经在 Invalidate Queue 中的失效操作的指令。
不同操作系统有不同的内存屏障的实现,通过在合适的位置调用内存屏障指令,程序人员可以及时的让 CPU 执行 Store Buffer 或者 Invalidate Queue,保障程序的正确性。