You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

105 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 39 | MESI协议如何让多核CPU的高速缓存保持一致
你平时用的电脑应该都是多核的CPU。多核CPU有很多好处其中最重要的一个就是它使得我们在不能提升CPU的主频之后找到了另一种提升CPU吞吐率的办法。
不知道上一讲的内容你还记得多少上一节我们讲到多核CPU里的每一个CPU核都有独立的属于自己的L1 Cache和L2 Cache。多个CPU之间只是共用L3 Cache和主内存。
我们说CPU Cache解决的是内存访问速度和CPU的速度差距太大的问题。而多核CPU提供的是在主频难以提升的时候通过增加CPU核心来提升CPU的吞吐率的办法。我们把多核和CPU Cache两者一结合就给我们带来了一个新的挑战。因为CPU的每个核各有各的缓存互相之间的操作又是各自独立的就会带来[**缓存一致性**](https://en.wikipedia.org/wiki/Cache_coherence)Cache Coherence的问题。
![](https://static001.geekbang.org/resource/image/07/41/0723f72f3016fede96b545e2898c0541.jpeg)
## 缓存一致性问题
那什么是缓存一致性呢我们拿一个有两个核心的CPU来看一下。你可以看这里这张图我们结合图来说。
![](https://static001.geekbang.org/resource/image/a6/da/a6146ddd5c78f2cbc1af56b0ee3292da.jpeg)
在这两个CPU核心里1号核心要写一个数据到内存里。这个怎么理解呢我拿一个例子来给你解释。
比方说iPhone降价了我们要把iPhone最新的价格更新到内存里。为了性能问题它采用了上一讲我们说的写回策略先把数据写入到L2 Cache里面然后把Cache Block标记成脏的。这个时候数据其实并没有被同步到L3 Cache或者主内存里。1号核心希望在这个Cache Block要被交换出去的时候数据才写入到主内存里。
如果我们的CPU只有1号核心这一个CPU核那这其实是没有问题的。不过我们旁边还有一个2号核心呢这个时候2号核心尝试从内存里面去读取iPhone的价格结果读到的是一个错误的价格。这是因为iPhone的价格刚刚被1号核心更新过。但是这个更新的信息只出现在1号核心的L2 Cache里而没有出现在2号核心的L2 Cache或者主内存里面。**这个问题就是所谓的缓存一致性问题1号核心和2号核心的缓存在这个时候是不一致的。**
为了解决这个缓存不一致的问题,我们就需要有一种机制,来同步两个不同核心里面的缓存数据。那这样的机制需要满足什么条件呢?我觉得能够做到下面两点就是合理的。
第一点叫**写传播**Write Propagation。写传播是说在一个CPU核心里我们的Cache数据更新必须能够传播到其他的对应节点的Cache Line里。
第二点叫**事务的串行化**Transaction Serialization事务串行化是说我们在一个CPU核心里面的读取和写入在其他的节点看起来顺序是一样的。
第一点写传播很容易理解。既然我们数据写完了自然要同步到其他CPU核的Cache里。但是第二点事务的串行化可能没那么好理解我这里仔细解释一下。
我们还拿刚才修改iPhone的价格来解释。这一次我们找一个有4个核心的CPU。1号核心呢先把iPhone的价格改成了5000块。差不多在同一个时间2号核心把iPhone的价格改成了6000块。这里两个修改都会传播到3号核心和4号核心。
![](https://static001.geekbang.org/resource/image/fe/6c/fe0c449e71800c5ad2e4b84af0d6e46c.jpeg)
然而这里有个问题3号核心先收到了2号核心的写传播再收到1号核心的写传播。所以3号核心看到的iPhone价格是先变成了6000块再变成了5000块。而4号核心呢是反过来的先看到变成了5000块再变成6000块。虽然写传播是做到了但是各个Cache里面的数据是不一致的。
事实上我们需要的是从1号到4号核心都能看到相同顺序的数据变化。比如说都是先变成了5000块再变成了6000块。这样我们才能称之为实现了事务的串行化。
事务的串行化,不仅仅是缓存一致性中所必须的。比如,我们平时所用到的系统当中,最需要保障事务串行化的就是数据库。多个不同的连接去访问数据库的时候,我们必须保障事务的串行化,做不到事务的串行化的数据库,根本没法作为可靠的商业数据库来使用。
而在CPU Cache里做到事务串行化需要做到两点第一点是一个CPU核心对于数据的操作需要同步通信给到其他CPU核心。第二点是如果两个CPU核心里有同一个数据的Cache那么对于这个Cache数据的更新需要有一个“锁”的概念。只有拿到了对应Cache Block的“锁”之后才能进行对应的数据更新。接下来我们就看看实现了这两个机制的MESI协议。
## 总线嗅探机制和MESI协议
要解决缓存一致性问题首先要解决的是多个CPU核心之间的数据传播问题。最常见的一种解决方案呢叫作**总线嗅探**Bus Snooping。这个名字听起来你多半会很陌生但是其实特很好理解。
这个策略本质上就是把所有的读写请求都通过总线Bus广播给所有的CPU核心然后让各个核心去“嗅探”这些请求再根据本地的情况进行响应。
总线本身就是一个特别适合广播进行数据传输的机制所以总线嗅探这个办法也是我们日常使用的Intel CPU进行缓存一致性处理的解决方案。关于总线这个知识点我们会放在后面的I/O部分更深入地进行讲解这里你只需要了解就可以了。
基于总线嗅探机制其实还可以分成很多种不同的缓存一致性协议。不过其中最常用的就是今天我们要讲的MESI协议。和很多现代的CPU技术一样MESI协议也是在Pentium时代被引入到Intel CPU中的。
MESI协议是一种叫作**写失效**Write Invalidate的协议。在写失效协议里只有一个CPU核心负责写入数据其他的核心只是同步读取到这个写入。在这个CPU核心写入Cache之后它会去广播一个“失效”请求告诉所有其他的CPU核心。其他的CPU核心只是去判断自己是否也有一个“失效”版本的Cache Block然后把这个也标记成失效的就好了。
相对于写失效协议,还有一种叫作**写广播**Write Broadcast的协议。在那个协议里一个写入请求广播到所有的CPU核心同时更新各个核心里的Cache。
写广播在实现上自然很简单但是写广播需要占用更多的总线带宽。写失效只需要告诉其他的CPU核心哪一个内存地址的缓存失效了但是写广播还需要把对应的数据传输给其他CPU核心。
![](https://static001.geekbang.org/resource/image/4e/59/4ed6d05049cbbc8603346f617206cd59.jpeg)
MESI协议的由来呢来自于我们对Cache Line的四个不同的标记分别是
* M代表已修改Modified
* E代表独占Exclusive
* S代表共享Shared
* I代表已失效Invalidated
我们先来看看“已修改”和“已失效”这两个状态比较容易理解。所谓的“已修改”就是我们上一讲所说的“脏”的Cache Block。Cache Block里面的内容我们已经更新过了但是还没有写回到主内存里面。而所谓的“已失效“自然是这个Cache Block里面的数据已经失效了我们不可以相信这个Cache Block里面的数据。
然后我们再来看“独占”和“共享”这两个状态。这就是MESI协议的精华所在了。无论是独占状态还是共享状态缓存里面的数据都是“干净”的。这个“干净”自然对应的是前面所说的“脏”的也就是说这个时候Cache Block里面的数据和主内存里面的数据是一致的。
那么“独占”和“共享”这两个状态的差别在哪里呢这个差别就在于在独占状态下对应的Cache Line只加载到了当前CPU核所拥有的Cache里。其他的CPU核并没有加载对应的数据到自己的Cache里。这个时候如果要向独占的Cache Block写入数据我们可以自由地写入数据而不需要告知其他CPU核。
在独占状态下的数据如果收到了一个来自于总线的读取对应缓存的请求它就会变成共享状态。这个共享状态是因为这个时候另外一个CPU核心也把对应的Cache Block从内存里面加载到了自己的Cache里来。
而在共享状态下因为同样的数据在多个CPU核心的Cache里都有。所以当我们想要更新Cache里面的数据的时候不能直接修改而是要先向所有的其他CPU核心广播一个请求要求先把其他CPU核心里面的Cache都变成无效的状态然后再更新当前Cache里面的数据。这个广播操作一般叫作RFORequest For Ownership也就是获取当前对应Cache Block数据的所有权。
有没有觉得这个操作有点儿像我们在多线程里面用到的读写锁。在共享状态下,大家都可以并行去读对应的数据。但是如果要写,我们就需要通过一个锁,获取当前写入位置的所有权。
整个MESI的状态可以用一个有限状态机来表示它的状态流转。需要注意的是对于不同状态触发的事件操作可能来自于当前CPU核心也可能来自总线里其他CPU核心广播出来的信号。我把对应的状态机流转图放在了下面你可以对照着[Wikipedia里面MESI的内容](https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE),仔细研读一下。
![](https://static001.geekbang.org/resource/image/fa/d1/fa98835c78c879ab69fd1f29193e54d1.jpeg)
[图片来源](https://commons.wikimedia.org/wiki/File:Diagrama_MESI.GIF)
## 总结延伸
好了关于CPU Cache的内容我们介绍到这里就结束了。我们来总结一下。这一节我们其实就讲了两块儿内容一个是缓存一致性另一个是MESI协议。
想要实现缓存一致性关键是要满足两点。第一个是写传播也就是在一个CPU核心写入的内容需要传播到其他CPU核心里。更重要的是第二点保障事务的串行化才能保障我们的数据是真正一致的我们的程序在各个不同的核心上运行的结果也是一致的。这个特性不仅在CPU的缓存层面很重要在数据库层面更加重要。
之后我介绍了基于总线嗅探机制的MESI协议。MESI协议是一种基于写失效的缓存一致性协议。写失效的协议的好处是我们不需要在总线上传输数据内容而只需要传输操作信号和地址信号就好了不会那么占总线带宽。
MESI协议是已修改、独占、共享以及已失效这四个缩写的合称。独占和共享状态就好像我们在多线程应用开发里面的读写锁机制确保了我们的缓存一致性。而整个MESI的状态变更则是根据来自自己CPU核心的请求以及来自其他CPU核心通过总线传输过来的操作信号和地址信息进行状态流转的一个有限状态机。
## 推荐阅读
大部分计算机组成或者体系结构的教科书都没有提到缓存一致性问题。不过最近有一本国人写的计算机底层原理的书《大话计算机》里面的6.9章节比较详细地讲解了多核CPU的访问存储数据的一致性问题很值得仔细读一读。
## 课后思考
今天我们所讲的MESI缓存一致性协议其实是对于MSI缓存一致性协议的一个优化。你可以通过搜索引擎研究一下什么是MSI协议以及MESI相对于MSI协议究竟做了哪些优化
欢迎把你研究的结果写在留言区和大家分享。如果有收获,也欢迎你把这篇文章分享给你的朋友,和他一起学习和进步。