缓存一致性协议MESI
参考大佬博客:https://www.cnblogs.com/yanlong300/p/8986041.html
带有高速cache的CPU计算执行流程:
- 程序以及数据被加载到Memory。
- 指令和数据被加载到CPU的cache。
- CPU执行完指令将数据还给cache。
- cache通过BUS将数据返回给Memory。
MSEI:
现在多核处理器下,会存在多个CPU的cache,如何保证缓存内部数据的一致呢,引入MSEI。
Cache line:缓存存储数据的单元。
MSEI为缓存行的4种状态的首字母,缓存行用2个bit来保存状态。
多核协作的状态转换例子:
假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。
1.单核读取:cache a试图从Memory中得到x的值(remote read)。
Memory中的x->bus->cache a(该cache line置为E)。
-
双核读取:CPU A和CPU B先后想要读取Memory的x。
Memory x->bus->cache a(cache line置为E)。
Memory x->(cache a检测到地址冲突,将E->S)bus->cache b(cache line置为S)。 -
修改数据
cache a想要将x改为1。
cache a(该cache line状态S->M)->bus->cache b(该cache line状态S->I)。
cache a将x改为1。 -
同步数据
cache b想要同步cache a的数据。
cache b发起remote read->bus->cache a(将该cache line同步到cache b和Memory)->cache a(该cache line状态M->S),cache b(该cache line状态I->S)。
MESI产生的问题以及解决
每次本地缓存修改以后,等待各个cache进行invalid的时间,对于CPU的浪费是极大的:
因此引入了store buffer的机制,即将要改变的值先存在store buffer中,让CPU先做别的事情,等所有invalid都确认了,再正式修改缓存的值。
CPU缓存的乱序问题:
我们假设a,b在主存中均为0,CPU0执行foo,CPU1执行bar,进行模拟:
- CPU0先执行a=1,缓存不在CPU0中,因此CPU0将新的a存入缓存并发送read invalidate给CPU1。
- CPU1执行while (n==0) continue。但是缓存中不包含b,因此发送read消息给CPU0。
- CPU0执行b=1。此时CPU0可能已经缓存了b=1,因此直接将新值赋予缓存(请注意为什么说可能已经缓存了b=1,因为CPU0和CPU1除了这两个函数以外,可能在其它地方也需要缓存a和b)。
- CPU0接受到了CPU1的read消息,并将缓存中的b发到了CPU1,并将缓存行设置为share。
- CPU1得到了b将其存入缓存。
- CPU1结束执行while (n==0) continue。
- CP1执行assert(a==1),而由于CPU1缓存了老的a=0,因此assert失败。
- 此时CPU1得到了CPU0的read invalidate,并将缓存中的a设置为1,但为时已晚。
总结:问题出在a的read invalidate在被CPU1接受之前,CPU1已经继续进行了包含a的论断。
我们通过memory barrier来解决这个问题:
- CPU0先执行a=1,缓存不在CPU0中,因此CPU0将新的a存入SB并发送read invalidate给CPU1。//注意放入SB而不是缓存
- CPU1执行while (n==0) continue。但是缓存中不包含b,因此发送read消息给CPU0。
- CPU1接受到CPU0的read invalidate,将其放入队列并立刻响应。
- CPU0收到了CPU1的立刻恢复,继续执行smp_mb,将a从SB移入缓存。
- CPU0执行b=1。此时CPU0可能已经缓存了b=1,因此直接将新值赋予缓存。
- CPU0接受到了CPU1的read消息,并将缓存中的b发到了CPU1,并将缓存行设置为share。
- CPU1得到了b将其存入缓存。
- CPU1结束执行while (n==0) continue。
- CPU1这时执行smp_mb,必须等待其处理队列中的所有invalidation事件。
- CPU1处理了invalidate消息,并且将a从缓存中清除。
- CP1执行assert(a==1),由于a不在cache 1中,因此向cache 0发送read请求。
- CPU0响应read,将最新的a发出。
- CPU1接受到最新的a,因此assert不触发。
总结:
将a=1先放入SB,等待read invalidation被加入到CPU1的处理队列并返回响应后,再将a=1放入缓存;
在CPU1进行a的assert之前,对处理队列的消息进行清空,及时发现了a已经过期,对其进行重新获取。