十张图详解CPU缓存一致性( 三 )


于是 , 有一个协议基于总线嗅探机制实现了事务串形化 , 也用状态机机制降低了总线带宽压力 , 这个协议就是 MESI 协议 , 这个协议就做到了 CPU 缓存一致性 。
MESI 协议MESI 协议其实是 4 个状态单词的开头字母缩写 , 分别是:

  • Modified , 已修改
  • Exclusive , 独占
  • Shared , 共享
  • Invalidated , 已失效
这四个状态来标记 Cache Line 四个不同的状态 。
「已修改」状态就是我们前面提到的脏标记 , 代表该 Cache Block 上的数据已经被更新过 , 但是还没有写到内存里 。 而「已失效」状态 , 表示的是这个 Cache Block 里的数据已经失效了 , 不可以读取该状态的数据 。
「独占」和「共享」状态都代表 Cache Block 里的数据是干净的 , 也就是说 , 这个时候 Cache Block 里的数据和内存里面的数据是一致性的 。
「独占」和「共享」的差别在于 , 独占状态的时候 , 数据只存储在一个 CPU 核心的 Cache 里 , 而其他 CPU 核心的 Cache 没有该数据 。 这个时候 , 如果要向独占的 Cache 写数据 , 就可以直接自由地写入 , 而不需要通知其他 CPU 核心 , 因为只有你这有这个数据 , 就不存在缓存一致性的问题了 , 于是就可以随便操作该数据 。
另外 , 在「独占」状态下的数据 , 如果有其他核心从内存读取了相同的数据到各自的 Cache, 那么这个时候 , 独占状态下的数据就会变成共享状态 。
那么 , 「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有 , 所以当我们要更新 Cache 里面的数据的时候 , 不能直接修改 , 而是要先向所有的其他 CPU 核心广播一个请求 , 要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态 , 然后再更新当前 Cache 里面的数据 。
我们举个具体的例子来看看这四个状态的转换:
  1. 当 A 号 CPU 核心从内存读取变量 i 的值 , 数据被缓存在 A 号 CPU 核心自己的 Cache 里面 , 此时其他 CPU 核心的 Cache 没有缓存该数据 , 于是标记 Cache Line 状态为「独占」 , 此时其 Cache 中的数据与内存是一致的;
  2. 然后 B 号 CPU 核心也从内存读取了变量 i 的值 , 此时会发送消息给其他 CPU 核心 , 由于 A 号 CPU 核心已经缓存了该数据 , 所以会把数据返回给 B 号 CPU 核心 。 在这个时候 ,A 和 B 核心缓存了相同的数据 , Cache Line 的状态就会变成「共享」 , 并且其 Cache 中的数据与内存也是一致的;
  3. 当 A 号 CPU 核心要修改 Cache 中 i 变量的值 , 发现数据对应的 Cache Line 的状态是共享状态 , 则要向所有的其他 CPU 核心广播一个请求 , 要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态 , 然后 A 号 CPU 核心才更新 Cache 里面的数据 , 同时标记 Cache Line 为「已修改」状态 , 此时 Cache 中的数据就与内存不一致了 。
  4. 如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值 , 由于此时的 Cache Line 是「已修改」状态 , 因此不需要给其他 CPU 核心发送消息 , 直接更新数据即可 。
  5. 如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」 , 发现 Cache Line 状态是「已修改」状态 , 就会在替换前先把数据同步到内存 。
所以 , 可以发现当 Cache Line 状态是「已修改」或者「独占」状态时 , 修改更新其数据不需要发送广播给其他 CPU 核心 , 这在一定程度上减少了总线带宽压力 。
事实上 , 整个 MESI 的状态可以用一个有限状态机来表示它的状态流转 。 还有一点 , 对于不同状态触发的事件操作 , 可能是来自本地 CPU 核心发出的广播事件 , 也可以是来自其他 CPU 核心通过总线发出的广播事件 。 下图即是 MESI 协议的状态图:
十张图详解CPU缓存一致性文章插图
MESI 协议的四种状态之间的流转过程 , 我汇总成了下面的表格 , 你可以更详细的看到每个状态转换的原因:
十张图详解CPU缓存一致性文章插图
总结CPU 在读写数据的时候 , 都是在 CPU Cache 读写数据的 , 原因是 Cache 离 CPU 很近 , 读写性能相比内存高出很多 。 对于 Cache 里没有缓存 CPU 所需要读取的数据的这种情况 , CPU 则会从内存读取数据 , 并将数据缓存到 Cache 里面 , 最后 CPU 再从 Cache 读取数据 。
而对于数据的写入 , CPU 都会先写入到 Cache 里面 , 然后再在找个合适的时机写入到内存 , 那就有「写直达」和「写回」这两种策略来保证 Cache 与内存的数据一致性: