前言
在单核电脑中,处理问题要简单的多。对内存和硬件的要求,各种方面的考虑没有在多核的情况下复杂。电脑中,CPU的运行计算速度是非常快的,而其他硬件比如IO,网络、内存读取等等,跟cpu的速度比起来是差几个数量级的。而不管任何操作,几乎是不可能都在cpu中完成而不借助于任何其他硬件操作。所以协调cpu和各个硬件之间的速度差异是非常重要的,要不然cpu就一直在等待,浪费资源。而在多核中,不仅面临如上问题,还有如果多个核用到了同一个数据,如何保证数据的一致性、正确性等问题,也是必须要解决的。
目前基于高速缓存的存储交互很好的解决了cpu和内存等其他硬件之间的速度矛盾,多核情况下各个处理器(核)都要遵循一定的诸如MSI、MESI等协议来保证内存的各个处理器高速缓存和主内存的数据的一致性。
一、CPU与内存
我们知道,CPU计算的数据需要从主存获取,计算结果需要回写到主存,而CPU的处理效率是远远高于内存的(存在几个数量级的差距) , 如何避免cpu长时间等待内存的读写结果造成的效率浪费呢?聪明的人类想了妙招:在CPU和内存之间加入了高速缓存处理,下面我们来聊下CPU的三级缓存。
CPU的三级缓存
现代计算机系统都加入了一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。在CPU中加入缓存是一种高效的解决方案,这样整个内存储器(缓存+内存)就变成了既有缓存的高速度,又有内存的大容量的存储系统了。缓存对CPU的性能影响很大,主要是因为CPU的数据交换顺序和CPU与缓存间的带宽引起的。
下图是一个典型的存储器层次结构,我们可以看到一共使用了三级缓存
为追求速度上的极致,现代计算机CPU缓存通常设计为三级缓存:L3 cache, L2cache, L1cache,其中L1,L2为每个内核独享,L3为多个内核共享,高速缓存越接近CPU速度越快,同时容量也越小。缓存又可以分为指令缓存和数据缓存,指令缓存用来缓存程序的代码,详见下图:
说明:
L1 Cache,一级缓存,本地core的缓存,分成32K的数据缓存L1d和32k指令缓存L1i,访问L1需要3cycles,耗时大约1ns;
L2 Cache,二级缓存,本地core的缓存,被设计为L1缓存与共享的L3缓存之间的缓冲,大小为256K,访问L2需要12cycles,耗时大约3ns;
L3 Cache,三级缓存,在同插槽的所有core共享L3缓存,分为多个2M的段,访问L3需要38cycles,耗时大约12ns;
什么是cache line(缓存行)
cache line是cache与内存数据交换的最小单位,可以简单的理解为CPU Cache中的最小缓存单位。根据操作系统一般是32byte或64byte。假设我们有一个512字节的一级缓存,那么按照64B的缓存单位大小来算,这个一级缓存所能存放的缓存个数就是512/64 = 8个。在MESI协议中,状态可以是M、E、S、I,地址则是cache line中映射的内存地址,数据则是从内存中读取的数据。
工作方式: 当CPU从cache中读取数据的时候,会比较地址是否相同,如果相同则检查cache line的状态,再决定该数据是否有效,无效则从主存中获取数据,或者根据一致性协议发生一次cache-to–chache的数据推送(参见MESI协议,文章最后的链接);
工作效率: 当CPU能够从cache中拿到有效数据的时候,消耗几个CPU cycle,如果发生cache miss,则会消耗几十上百个CPU cycle
二、缓存一致性问题
现代计算机一般支持多CPU,每个CPU支持多核,超线程技术使得每核可同时运行多个线程,如果同一个变量i=0,有两个线程执行i++方法,线程1把i从内存中读取进缓存,而现在线程2也把i读取进缓存,两个线程执行完i++后,线程1写回内存,i = 1,线程2也写回内存i = 1,两次++结果最终值为1,这就是著名的缓存一致性问题。
缓存一致性问题解决方案
1 总线锁
cpu和内存在主板上面,他们是分布在两个不同的插槽上面,cpu访问内存,他要通过主板上面的总线,才能去访问内存条。 当一个CPU对其缓存中的数据进行操作的时候,往总线中发送一个Lock信号(即在消息总线上加一把锁,就能保证一致性)。其他处理器的请求将会被阻塞,那么该处理器可以独占共享内存。总线锁相当于把CPU和内存之间的通信锁住了,所以这种方式会导致CPU的性能下降,所以出现了另外一种方式,就是缓存锁。
2 缓存一致性协议:MESI
缓存一致性:在多核CPU中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题。而一致性协议正是用于保证多个CPU cache之间缓存共享数据的一致。硬件的缓存一致性协议比较多,其中比较经典的应该就是MESI嗅探协议 了,它的方法是在CPU缓存中保存一个标记位,这个标记为有四种状态:
- M(Modified) 修改缓存:当前CPU缓存已经被修改,表示已经和内存中的数据不一致了
- I(Invalid) 失效缓存:说明CPU的缓存已经不能使用了
- E(Exclusive) 独占缓存:当前cpu的缓存和内存中数据保持一直,而且其他处理器没有缓存该数据
- S(Shared) 共享缓存:数据和内存中数据一致,并且该数据存在多个cpu缓存中
cache操作
MESI协议中,每个cache的控制器不仅知道自己的操作(local read和local write),每个核心的缓存控制器通过总线监听即“嗅探机制”(snooping) 也知道其他CPU中cache的操作(remote read和remote write),再确定自己cache中共享数据的状态是否需要调整。
- local read(LR):读本地cache中的数据;
- local write(LW):将数据写到本地cache;
- remote read(RR):其他核心发生read;
- remote write(RW):其他核心发生write;
说明: CPU的读取会遵循几个原则:
(1)如果缓存的状态是I,那么就从内存中读取,否则直接从缓存读取
(2)如果缓存处于M或者E的CPU 嗅探到其他CPU有读的操作,就把自己的缓存写入到内存,并把自己的状态设置为S
(3)只有缓存状态是M或E的时候,CPU才可以修改缓存中的数据,修改后,缓存状态变为S
(4)每次CPU修改了cache中的数据,不会立即更新到内存,而是等到cache line在某一个必须或合适的时机才会更新到内存中;
重点:状态转换和cache操作。
如上文内容所述,MESI协议中cache line数据状态有4种,引起数据状态转换的CPU cache操作也有4种,因此要理解MESI协议,就要将这16种状态转换的情况讨论清楚。
初始场景: 在最初的时候,所有CPU中都没有数据,某一个CPU发生读操作,此时必然发生cache miss,数据从主存中读取到当前CPU的cache,状态为E(独占,只有当前CPU有数据,且和主存一致),此时如果有其他CPU也读取数据,则状态修改为S(共享,多个CPU之间拥有相同数据,并且和主存保持一致),如果其中某一个CPU发生数据修改,那么该CPU中数据状态修改为M(拥有最新数据,和主存不一致,但是以当前CPU中的为准),其他拥有该数据的核心通过缓存控制器监听到remote write行文,然后将自己拥有的数据的cache line状态修改为I(失效,和主存中的数据被认为不一致,数据不可用应该重新获取)。
状态转换动态演示:https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm
(1)modify
场景:当前CPU中数据的状态是modify,表示当前CPU中拥有最新数据,虽然主存中的数据和当前CPU中的数据不一致,但是以当前CPU中的数据为准;
-
LR:此时如果发生local read,即当前CPU读数据,直接从cache中获取数据,拥有最新数据,因此状态不变;
-
LW:直接修改本地cache数据,修改后也是当前CPU拥有最新数据,因此状态不变;
-
RR:因为本地内存中有最新数据,当本地cache控制器监听到总线上有RR发生的时,必然是其他CPU发生了读主存的操作,此时为了保证一致性,当前CPU应该将数据写回主存,而随后的RR将会使得其他CPU和当前CPU拥有共同的数据,因此状态修改为S;
-
RW:同RR,当cache控制器监听到总线发生RW,当前CPU会将数据写回主存,因为随后的RW将会导致主存的数据修改,因此状态修改成I;
(2)exclusive
场景:当前CPU中的数据状态是exclusive,表示当前CPU独占数据(其他CPU没有数据),并且和主存的数据一致;
-
LR:从本地cache中直接获取数据,状态不变;
-
LW:修改本地cache中的数据,状态修改成M(因为其他CPU中并没有该数据,因此不存在共享问题,不需要通知其他CPU修改cache line的状态为I);
-
RR:本地cache中有最新数据,当cache控制器监听到总线上发生RR的时候,必然是其他CPU发生了读取主存的操作,而RR操作不会导致数据修改,因此两个CPU中的数据和主存中的数据一致,此时cache line状态修改为S;
-
RW:同RR,当cache控制器监听到总线发生RW,发生其他CPU将最新数据写回到主存,此时为了保证缓存一致性,当前CPU的数据状态修改为I;
(3)shared
场景:当前CPU中的数据状态是shared,表示当前CPU和其他CPU共享数据,且数据在多个CPU之间一致、多个CPU之间的数据和主存一致;
-
LR:直接从cache中读取数据,状态不变;
-
LW:发生本地写,并不会将数据立即写回主存,而是在稍后的一个时间再写回主存,因此为了保证缓存一致性,当前CPU的cache line状态修改为M,并通知其他拥有该数据的CPU该数据失效,其他CPU将cache line状态修改为I;
-
RR:状态不变,因为多个CPU中的数据和主存一致;
-
RW:当监听到总线发生了RW,意味着其他CPU发生了写主存操作,此时本地cache中的数据既不是最新数据,和主存也不再一致,因此当前CPU的cache line状态修改为I;
(4)invalid
场景:当前CPU中的数据状态是invalid,表示当前CPU中是脏数据,不可用,其他CPU可能有数据、也可能没有数据;
- LR:因为当前CPU的cache line数据不可用,因此会发生读内存,此时的情形如下。
A. 如果其他CPU中无数据则状态修改为E;
B. 如果其他CPU中有数据且状态为S或E则状态修改为S;
C.如果其他CPU中有数据且状态为M,那么其他CPU首先发生RW将M状态的数据写回主存并修改状态为S,随后当前CPU读取主存数据,也将状态修改为S;
- LW:因为当前CPU的cache line数据无效,因此发生LW会直接操作本地cache,此时的情形如下。
A. 如果其他CPU中无数据,则将本地cache line的状态修改为M;
B. 如果其他CPU中有数据且状态为S或E,则修改本地cache,通知其他CPU将数据修改为I,当前CPU中的cacheline状态修改为M;
C. 如果其他CPU中有数据且状态为M,则其他CPU首先将数据写回主存,并将状态修改为I,当前CPU中的cacheline转台修改为M;
-
RR:监听到总线发生RR操作,表示有其他CPU读取内存,和本地cache无关,状态不变;
-
RW:监听到总线发生RW操作,表示有其他CPU写主存,和本地cache无关,状态不变;
文章参考:
https://www.cnblogs.com/igoodful/p/9493156.html
https://blog.csdn.net/m18870420619/article/details/82431319
https://mp.weixin.qq.com/s/gPdxtXx17b7M8jjnc_-SGw