为什么需要缓存一致
目前主流电脑的 CPU 都是多核心的,多核心的有点就是在不能提升 CPU 主频后,通过增加核心来提升 CPU 吞吐量。每个核心都有自己的 L1 Cache 和 L2 Cache,只是共用 L3 Cache 和主内存。每个核心操作是独立的,每个核心的 Cache 就不是同步更新的,这样就会带来缓存一致性(Cache Coherence)的问题。
举个例子,如图:
有 2 个 CPU,主内存里有个变量x=0
。CPU A 中有个需要将变量x
加1
。CPU A 就将变量x
加载到自己的缓存中,然后将变量x
加1
。因为此时 CPU A 还未将缓存数据写回主内存,CPU B 再读取变量x
时,变量x
的值依然是0
。
这里的问题就是所谓的缓存一致性问题,因为 CPU A 的缓存与 CPU B 的缓存是不一致的。
如何解决缓存一致性问题
通过在总线加 LOCK 锁的方式
在锁住总线上加一个 LOCK 标识,CPU A 进行读写操作时,锁住总线,其他 CPU 此时无法进行内存读写操作,只有等解锁了才能进行操作。
该方式因为锁住了整个总线,所以效率低。
缓存一致性协议 MESI
该方式对单个缓存行的数据进行加锁,不会影响到内存其他数据的读写。
在学习 MESI 协议之前,简单了解一下总线嗅探机制(Bus Snooping)。要对自己的缓存加锁,需要通知其他 CPU,多个 CPU 核心之间的数据传播问题。最常见的一种解决方案就是总线嗅探。
这个策略,本质上就是把所有的读写请求都通过总线广播给所有的 CPU 核心,然后让各个核心去“嗅探”这些请求,再根据本地的情况进行响应。MESI 就是基于总线嗅探机制的缓存一致性协议。
MESI 协议的由来是对 Cache Line 的四个不同的标记,分别是:
状态 | 状态 | 描述 | 监听任务 |
---|---|---|---|
Modified | 已修改 | 该 Cache Line 有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中 | Cache Line 必须时刻监听所有试图读该 Cache Line 相对于主存的操作,这种操作必须在缓存将该 Cache Line 写回主存并将状态改为 S 状态之前,被延迟执行 |
Exclusive | 独享,互斥 | 该 Cache Line 有效,数据和内存中的数据一直,数据只存在于本 Cache | Cache Line 必须监听其他缓存读主存中该 Cache Line 的操作,一旦有这种操作,该 Cache Line 需要改为 S 状态 |
Shared | 共享的 | 该 Cache Line 有效,数据和内存中的数据一直,数据存在于很多个 Cache 中 | Cache Line 必须监听其他 Cache Line 使该 Cache Line 无效或者独享该 Cache Line 的请求,并将 Cache Line 改为 I 状态 |
Invalid | 无效的 | 该 Cache Line 无效 | 无 |
整个 MESI 的状态,可以用一个有限状态机来表示它的状态流转。需要注意的是,对于不同状态触发的事件操作,可能来自于当前 CPU 核心,也可能来自总线里其他 CPU 核心广播出来的信号。我把各个状态之间的流转用表格总结了一下:
当前状态 | 事件 | 行为 | 下个状态 |
---|---|---|---|
M | Local Read | 从 Cache 中读,状态不变 | M |
M | Local Write | 修改 cache 数据,状态不变 | M |
M | Remote Read | 这行数据被写到内存中,使其他核能使用到最新数据,状态变为 S | S |
M | Remote Write | 这行数据被写入内存中,其他核可以获取到最新数据,由于其他 CPU 修改该条数据,则本地 Cache 变为 I | I |
当前状态 | 事件 | 行为 | 下个状态 |
---|---|---|---|
E | Local Read | 从 Cache 中读,状态不变 | E |
E | Local Write | 修改数据,状态改为 M | M |
E | Remote Read | 数据和其他 CPU 共享,变为 S | S |
E | Remote Write | 数据被修改,本地缓存失效,变为 I | I |
当前状态 | 事件 | 行为 | 下个状态 |
---|---|---|---|
S | Local Read | 从 Cache 中读,状态不变 | S |
S | Local Write | 修改数据,状态改为 M,其他 CPU 的 Cache Line 状态改为 I | M |
S | Remote Read | 数据和其他 CPU 共享,状态不变 | S |
S | Remote Write | 数据被修改,本地缓存失效,变为 I | I |
当前状态 | 事件 | 行为 | 下个状态 |
---|---|---|---|
I | Local Read | 1. 如果其他 CPU 没有这份数据,直接从内存中加载数据,状态变为 E; 2. 如果其他 CPU 有这个数据,且 Cache Line 状态为 M,则先把 Cache Line 中的内容写回到主存。本地 Cache 再从内存中读取数据,这时两个 Cache Line 的状态都变为 S; 3. 如果其他 Cache Line 有这份数据,并且状态为 S 或者 E,则本地 Cache Line 从主存读取数据,并将这些 Cache Line 状态改为 S |
E 或者 S |
I | Local Write | 1. 先从内存中读取数据,如果其他 Cache Line 中有这份数据,且状态为 M,则现将数据更新到主存再读取,将 Cache Line 状态改为 M; 2. 如果其他 Cache Line 有这份数据,且状态为 E 或者 S,则其他 Cache Line 状态改为 I |
M |
I | Remote Read | 数据和其他 CPU 共享,状态不变 | S |
I | Remote Write | 数据被修改,本地缓存失效,变为 I | I |