文章目录
- 概述
- [MESI 协议的基本原理](#MESI 协议的基本原理)
- 工作机制
- [写缓冲区 & 失效队列](#写缓冲区 & 失效队列)
-
- [写缓冲区(Store Buffer)](#写缓冲区(Store Buffer))
- [失效队列(Invalidation Queue)](#失效队列(Invalidation Queue))
- Java内存模型
- 应用场景
- 结论
- 参考资料
概述
随着整个世界的发展,CPU、内存以及各种I/O设备不断升级迭代,计算机性能有了很大的提升。原先那种简单的计算机架构也随之升级。
程序是存储在磁盘,然后读取进内存中的,内存吞吐率虽然得到很大的提升,但是相对于处理器来讲,仍然非常慢。处理器要从内存中直接读取数据都要花大概几百个时钟周期,在这几百个时钟周期内,处理器除了等待什么也不能做。
随着处理器技术的发展,现代计算机系统越来越多地采用多核处理器架构来提高性能。但是仅仅是提升CPU性能是不够的,如果内存和磁盘的处理性能没有跟上,就意味着整体的计算效率取决于最慢的设备,比如磁盘。为了平衡三者之间的速度差异,最大化利用CPU,硬件层面、操作系统层面、编译器层面也都做出了很多的优化:
- CPU增加了高速缓存
- 操作系统增加了进程、线程
- 优化编译器的指令,更好的利用CPU的高速缓存
然而,多核处理器带来了新的挑战,尤其是在处理共享内存一致性方面。每一种优化都会带来相应的问题。
如果core0在core1还未将更新的数据写回内存之前就读取了数据,并进行了操作,就会造成数据错误。
通常,CPU的L1缓存分为指令缓存(instruction cache)和数据缓存(data cache)。
为了解决这一问题,各种缓存一致性协议应运而生,如MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等。其中,MESI 协议是一种广泛应用的缓存一致性协议,用于解决多 cpu 、并发环境下,共享内存不一致问题。
本文将深入探讨 MESI 协议的基本原理、工作机制以及在多核处理器架构中的应用。
MESI 协议的基本原理
概念
MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议。
CPU操作缓存的单位是缓存行(cache line)。MESI协议就是对缓存行的状态进行标记处理,通过这些状态的切换来管理缓存数据的。
CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示):
其中MESI分别代表缓存行数据所处的四种状态,通过这四种状态的切换来管理缓存数据,四种状态分别为Modefiy、Exclusive、Shared和Invalid,具体含义如下所示:
协议状态
MESI 协议是一种基于状态转移的缓存一致性协议,它通过维护缓存行的状态来确保多个处理器之间的一致性。MESI 协议定义了四种状态:
状态 | 名称 | 描述 |
---|---|---|
M | 修改(Modefiy) | 该缓存行有效, 数据被修改了,但未同步回内存。即数据只存在于本缓存行中,和内存中的数据不一样 |
E | 独占(Exclusive) | 该缓存行有效,数据未被修改,和内存中的数据一致,并且数据只存在本缓存行中 |
S | 共享(Shared) | 该缓存行有效,数据未被修改,和内存中的数据一致,并且数据同时存在于其他缓存中 |
I | 无效(Invalid) | 该缓存行数据无效,数据已过时。 |
- 在E和S状态下,缓存行的数据是有效的,任何读取操作可以直接使用;
- 在M和I状态下,缓存行的数据是dirty(和内存的数据可能不一致)。在读取或写入数据时,需要先将其它核心已修改的数据写回内存,再从内存读取;
- 在S和I状态,没有获得缓存行数据的独占权(锁)。想要修改数据时不能直接修改,而是要先向所有核心广播 RFO(Request For Ownership)请求 ,将其它核心的缓存行置为I,等到获得回应 ACK 后才算获得缓存行数据的独占权。
- 在M和E状态下,核心已经获得了缓存行数据的独占权(锁)。在修改数据时不需要向总线发送广播,能够减轻总线的通信压力。
CPU感知其他CPU的行为(比如读、写某个缓存行)就是是通过嗅探(Snoop)其他CPU发出的请求消息完成的,有时CPU也需要针对总线中的某些请求消息进行响应。这被称为"总线嗅探机制"。
事实上,完整的 MESI 协议更复杂,但我们没必要记得这么细。我们只需要记住最关键的 2 点:
-
- 阻止同时有多个核心修改的共享数据:当一个 CPU 核心要求修改数据时,会先广播 RFO 请求获得 Cache 块的所有权,并将其它 CPU 核心中对应的 Cache 块置为已失效状态;
-
- 延迟回写:只有在需要的时候才将数据写回内存,当一个 CPU 核心要求访问已失效状态的 Cache 块时,会先要求其它核心先将数据写回内存,再从内存读取。
MESI 协议在 MSI 的基础上增加了 E(独占)状态,以减少只有一份缓存的写操作造成的总线通信。
MESI 协议有一个非常 nice 的在线体验网站,可以对照本文内容,在网站上操作指令区,并观察内存和缓存的数据和状态变化。
https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESI.htm
工作机制
缓存行状态转移
MESI 协议的核心在于缓存行状态的转移。当处理器需要读取或写入内存中的数据时,会触发状态转移,以确保数据的一致性。
- 当一个处理器请求使用exclusive模式加载load一个缓存行时,其他的处理器会将所有它们自己关于该缓存行的副本都置为invalid。任何一个已修改过自己本地的该对应缓存行的处理器都需要首先将其写回到内存中,之后第一个处理器的load请求才可以被满足。
- 当一个处理器请求使用shared模式加载load一个缓存行时,任何一个以exclusive模式加载该line的处理器都必须将其状态置为shared,并且任何一个已经修改过自己本地对应缓存行的处理器都必须将该line写回主内存,之后第一个处理器的load请求才可以被满足。
- 如果缓存满了,则可能需要驱逐一个缓存行。如果该line是shared或exclusive状态,那么它可以直接简单的被丢弃。但是如果该line被修改过,那么它必须被首先写回内存之后再丢弃。
状态转换图
状态转换表
上述状态转换图等同于下表:
典型的状态转换示例
下面是一些典型的状态转移示例:
- 读操作:
- 如果处理器试图读取的数据处于 S 状态,则直接从缓存读取。
- 如果数据处于 M 或 E 状态,则也可以直接从缓存读取。
- 写操作:
- 如果处理器试图写入的数据处于 E 或 M 状态,则直接写入缓存,并将状态更新为 M。
- 如果数据处于 S 状态,则必须先向其他缓存发送失效消息(Invalidate Message),使其他缓存中的数据失效,然后才能写入本地缓存,并将状态更新为 M。
详细的状态转换说明
写缓冲区 & 失效队列
MESI 协议保证了 Cache 的一致性,但完全地遵循协议会影响性能。因此,现代的 CPU 会在增加写缓冲区和失效队列将 MESI 协议的请求异步化,以提高并行度:
写缓冲区(Store Buffer)
由于在写入操作之前,CPU 核心 1 需要先广播 RFO 请求获得独占权,在其它核心回应 ACK 之前,当前核心只能空等待,这对 CPU 资源是一种浪费。因此,现代 CPU 会采用 "写缓冲区" 机制:写入指令放到写缓冲区后并发送 RFO 请求后,CPU 就可以去执行其它任务,等收到 ACK 后再将写入操作写到 Cache 上。
失效队列(Invalidation Queue)
由于其他核心在收到 RFO 请求时,需要及时回应 ACK。但如果核心很忙不能及时回复,就会造成发送 RFO 请求的核心在等待 ACK。因此,现代 CPU 会采用 "失效队列" 机制:先把其它核心发过来的 RFO 请求放到失效队列,然后直接返回 ACK,等当前核心处理完任务后再去处理失效队列中的失效请求。
事实上,写缓冲区和失效队列破坏了 Cache 的一致性。 举个例子:初始状态变量 a 和变量 b 都是 0,现在 Core1 和 Core2 分别执行这两段指令,最终 x 和 y 的结果是什么?
# Core1 指令
a = 1; // A1
x = b; // A2
# Core2 指令
b = 2; // B1
y = a; // B2
我们知道在未同步的情况下,这段程序可能会有多种执行顺序。不管怎么执行,只要 2 号指令是在 1 号指令后执行的,至少 x 或 y 至少一个有值。但是在写缓冲区和失效队列的影响下,程序还有以意料之外的方式执行:
执行顺序(先不考虑 CPU 超前流水线控制) | 结果 |
---|---|
A1 → A2 → B1 → B2 | x = 0, y = 1 |
A1 → B1 → A1 → B2 | x = 2, y = 1 |
B1 → B2 → A1 → A2 | x = 1, y = 0 |
B1 → A1 → B2 → A2 | x = 2, y = 1 |
A2 → B1 → B2 → A1(A1 与 A2 重排) | x = 0, y = 0 |
Core2 也会出现相同的情况,不再赘述 | x = 0, y = 0 |
可以看到:从内存的视角看,直到 Core1 执行 A3 来刷新写缓冲区,写操作 A1 才算真正执行了。虽然 Core 的执行顺序是 A1 → A2 → B1 → B2,但内存看到的顺序却是 A2 → B1 → B2 → A1,变量 a 写入没有同步给对变量 a 的读取,Cache 的一致性被破坏了。
Java内存模型
Java内存模型( Java Memory Model),简称JMM。
它本身只是一个抽象的概念,并不真实存在。它描述的是和多线程相关的一组规范。通过这组规范,定义了程序中对各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。需要每个JVM 的实现都要遵守这样的规范,有了JMM规范的保障,并发程序运行在不同的虚拟机上时,得到的程序结果才是安全可靠可信赖的。
JMM定义了主内存和线程之间的抽象关系:
应用场景
多核处理器
在多核处理器系统中,MESI 协议是维护共享内存一致性的一种常用方案。通过有效地管理缓存一致性,MESI 协议使得多核处理器能够在执行并发任务时保持数据的一致性。
分布式系统
尽管 MESI 协议最初设计用于单个芯片上的多核处理器,但它的一些原则也可以应用于分布式系统中的缓存一致性管理。
结论
MESI 协议是一种高效的缓存一致性协议,它通过维护缓存行的状态来确保多核处理器系统中的共享内存一致性。通过状态转移机制,MESI 协议能够有效地处理多处理器环境中的数据一致性问题,从而提高了系统的整体性能和可靠性。
最后的补充:
-
- 在 CPU Cache 的三级缓存中,会存在 2 个缓存一致性问题:
- 纵向 - Cache 与内存的一致性问题: 在修改 Cache 数据后,如何同步回内存?
- 横向 - 多核心 Cache 的一致性问题: 在一个核心修改 Cache 数据后,如何同步给其他核心 Cache?
-
- Cache 与内存的一致性问题有 2 个策略:
- 写直达策略: 始终保持 Cache 数据和内存数据一致,在每次写入操作中都会写入内存;
- 写回策略: 只有在脏 Cache 块被替换出去的时候写回内存,减少写回内存的次数;
-
- 多核心 Cache 一致性问题需要满足 2 点特性:
- 写传播(总线嗅探): 每个 CPU 核心的写入操作,需要传播到其他 CPU 核心;
- 事务串行化(总线仲裁): 各个 CPU 核心所有写入操作的顺序,在所有 CPU 核心看起来是一致。