2021-04-04 CPU缓存一致性 MESI协议

一 CPU以及缓存和高速缓存结构

1.1 CPU结构

我们知道CPU主要功能,一是控制,一是运算。主要包括寄存器、控制单元、运算单元和中断系统,主要架构如下:
在这里插入图片描述

控制单元:主要负责分析和解释指令
算数逻辑单元:也就是CPU的运算或者执行单元,负责计算
寄存器:有多种类型,包括地址寄存器、数据寄存器和控制寄存器等等,数据寄存器:保存数据和操作数的寄存器;地址寄存器:保存地址;指令寄存器:存放指令的寄存器

1.2 寄存器和高速缓存和写缓冲区比较

相同点:都可以是CPU这边的硬件,其中高速缓存可以在处理器和MMU之间,被称为逻辑缓存;也可以在MMU和物理内存之间,也叫做物理缓存。

1.2.1 寄存器(register)

寄存器是一种硬件,主要是存储当前需要操作的数据、地址和指令和状态

1.2.2 高速缓存(cache)

高速缓存也是硬件,主要缓存从主内存读取的缓存行(cache line),以便于降低主存和CPU之间的频繁交互,和减小CPU和主存之间的速度差异,从而提升系统性能。现在的CPU一般有L1,L2和L3三级缓存,缓存也是烧录在CPU上的。

1.3 缓存行(Cache Line)

CPU并不是按字节访问内存的,而是基于缓存行cache line进行数据换入换出,CPU首先从高速缓存获取数据,如果没有命中,则会主存获取一个cache line大小字节数据,并且放入到高速缓存中。缓存行是缓存CPU和主存交换数据的最小单位,目前主流CPU的Cache Line大小为64字节,如果L1 Cache是256字节,则可以放4个缓存行,L2 cache是1M,则可以放256个Cache Line。
简而言之,CPU如果没有在高速缓存获取到数据,则最终会向内存获取一块64字节的数据,拿回来放在高速缓存中,这样符合局部性原则,获取的数据就是缓存行。然后CPU就不必花太多时间在读取数据,提升性能。

二 MESI协议(缓存一致性协议)

2.1 Cache的写策略

2.1.1 策略一:缓存命中,写通(write through)

每次处理器修改了cache中的内容,则立即更新到主存。即更新了缓存也需要立即更新到主存

2.1.2 策略二:缓存命中,写回(write back)

处理器修改了cache中的内容,并不会立即写入,而是等待这个cache line因为某种原因需要从cache中移除或者删除,这时候才会更新到内存。即更新高速缓存,并不一定立即更新到主存。
写通因为存在大量的或者频繁的内存访问,会影响效率,如果使用这种策略一般都会结合写缓冲器(write buffer)进行缓冲。大多数的处理其都是采用的写回策略。

2.1.3 缓存未命中, 写分配缓存(write allocate)

如果CPU写数据未命中缓存,这时候先从主存读取数据,然后将数据放入缓存中,此时的数据是旧的数据。然后再通过写回法更新缓存中数据。

2.1.4 缓存未命中, 写未分配缓存(no write allocate)

如果CPU写数据未命中缓存,直接将数据写入主存,并不会写入缓存,只有读取这个数据的缓存没有才去读取这个数据。

2.2 Cache Coherence(缓存一致性)

假设内存有一个数据data = 10, 处理器是多核的,有4和core1、core2、core3、core4。现在假设data = 10, 已经缓存到了四个核对应的缓存中,但是core2修改data = 20, 这时候根据不同的cache写策略是不同的处理。
如果是写通机制:则会将数据更新到缓存,并且更新主存;而且数据在总线,其他CPU因为总线嗅探机制,可以知晓更新的数据,所以这种方式不存在缓存一致性问题,但是效率低。
如果是写回处理:这个数据只是写入了core2对应的cache中,数据并没有走总线,其他核无法通过总线嗅探机制感知到修改的数据,其他核是不知道data已经发生了变化。

2.3 MESI协议简介

2.3.1 总线嗅探机制和MESI协议之间的关系

我们知道,在多核CPU的情况下,CPU与CPU之间存在数据可见性的问题,解决方案有总线嗅探机制或者基于目录实现的缓存一致性。那么总线嗅探我们知道主要包含两种协议:一是写无效;一种是写更新。但是写更新因为会占用大量的总线流量,所以写失效使用的更多,那么MESI就是一个基于写失效的缓存一致性协议。

2.3.2 MESI协议以及状态

2.3.2.1 什么是MESI协议

MESI协议是Modified、Exclusive、Shared、Invalid的缩写,表示修改、独占、共享和无效四种状态。缓存行是处理器和主存读写数据的最小单位,这4种状态就是在缓存行上的。
我们知道一般情况下,缓存行就包括四个部分:有效标记位(valid)、脏标记位(dirty)、tag部分和block数据部分。如果是写通的话(write through)可能不包括dirty脏标记位,而是需要一个write buffer。如果是MESI协议,还会增加一个标记位,表示当前缓存行是什么状态, 缓存行状态总共有4种状态: M(01)、E(02)、S(03)、I(04),如图示:
在这里插入图片描述

另外,并不是每一个CPU厂商都是按照MESI协议实现的,有的有一些变化。比如MOESI协议,当某一个行的缓存行中的变量被修改,和内存数据不一样,则其他的核可以拷贝这个变量等等。

2.3.2.2 MESI协议状态描述

我们从数据有效、数据是否发生修改、数据是否存在多个cache中三个维度来描述MESI协议的状态:
在这里插入图片描述

2.3.2.2.1 独占

E: 表示数据有效;这个数据所在的缓存行只有自己读了,其他缓存没有;读取的数据只是在本地缓存
在这里插入图片描述

2.3.2.2.2 共享

S: 数据有效; 没有修改;多个缓存有这数据(多个数据所在缓存行都有这个数据)

在这里插入图片描述

2.3.2.2.3 修改

M: 数据有效;但是修改了cache; 这种状态只是存在于本地缓存

2.3.2.2.4 失效

就是收到别的CPU失效请求,则将自己的本地缓存行设置为失效

2.4 MESI协议的请求以及简单的流程图

2.4.1 MESI协议的消息类型

2.4.1.1读请求消息(read)

处理器向总线发送读请求,读取其他CPU cache数据或者主存数据

2.4.1.2读响应消息(read response)

其他CPU的缓存控制器会不断嗅探总线,当嗅探到总线上传播的读请求,如果其他CPU有的话则返回响应;如果其他CPU都没有就从下一个缓存或者主存加载,然后返回读响应

2.4.1.3失效请求消息(invalidate)

缓存控制器向总线发送失效请求,其他CPU的缓存中有这个数据的缓存行将会被置为失效状态

2.4.1.4失效响应请求(invalidate acknowledge)

其他处理器嗅探到失效请求,根据不同情况处理:

情况一:其余CPU有对应缓存行,都是处于共享(S)或者处于独占状态(E)
他们将自己缓存中对应的缓存的数据失效,然后返回invalidate ack响应消息

情况二:其余CPU有对应缓存行,都是处于修改(M)修改状态
首先:会发送write back消息,将消息写入下一个缓存或者主存,然后状态置为独占(E)
然后:响应其他CPU发送过来的invalidate失效消息

2.4.1.5 读失效请求消息(read invalidate)

向总线发送发送读失效请求,其他CPU需要同时进行读响应,也需要进行失效响应,并且根据收集失效响应的数量决定是否本地更新。这里还是需要根据具体的情况处理:

情况一:其余CPU有对应缓存行,都是处于共享(S)或者处于独占状态(E)
他们将自己缓存中对应的缓存的数据失效,然后返回读响应消息和失效响应消息

情况二:其余CPU有对应缓存行,都是处于修改(M)修改状态
首先:会发送write back消息,将消息写入下一个缓存或者主存,然后状态置为独占(E)
然后:响应CPU发送过来的invalidate失效消息

2.4.1.6 回写请求消息(write back)

修改了某一个缓存行的数据,但是状态是修改状态(M),这时候需要先把缓存行写入下一个缓存或者主存

2.4.2 MESI协议请求流程状态转换图

在这里插入图片描述

如图所示:修改(M)状态的缓存行是不能直接转换为共享(S)状态的,共享状态的前提是要读取的数据的缓存行在其他CPU中存在,且状态是E或者S

如图所示:共享(S)状态的缓存行是不能直接转换为修改(M)状态的,共享状态如果是本地写,则转化为M;如果是远程写,则会转化为I

2.5 MESI某些状态转换详细流程

最开始的CPU缓存情况如图所示:
在这里插入图片描述

2.5.1 读取所有核心都不会命中的缓存行的数据,都是处于invalid状态

在这里插入图片描述

#1 CPU1读取变量a数据
#2 缓存控制器检查变量a的地址是否存在缓存行
#3 因为没有在缓存行,于是将读请求交给了总线, 总线会广播这个读请求(read), 如果其他核心缓存控制器从总线上监听到读请求,则检查本地缓存是否有这个数据副本,都没有这个数据副本则不会响应读请求,则从主存读取数据
#4 主存读取变量a所在的主存数据块,数据块缓存在CPU高速缓存行中
#5 因为此时是刚读取的数据,其他核心还没有这个缓存行,所以缓存行状态是独占(E)

2.5.2 本地核心读取其他核心存在的数据

在这里插入图片描述

#1 CPU2也读取变量a,通过变量a地址,没有命中缓存
#2缓存控制器发送读请求到总线
#3 总线广播读请求
#4 CPU1缓存控制器监听到了请求,则自身状态转化为共享(S), 状态 然后响应这个请求,返回出去的缓存行状态的当然也是共享(S) 状态。因此不必向主存发起请求。

2.5.3 本地核心修改数据

在这里插入图片描述

#1 CPU1进行了一个本地写,将int a = 10,此时缓存行状态置为修改(M)状态
#2 并且向总线发出write invalidate失效请求
#3 其他核心的缓存控制器监听到write invalidate失效请求,则将本地缓存对应的缓存行状态置为失效(I)状态
#4 注意此时并不会更新立即更新主存

2.5.4 核心读取失效状态的数据

在这里插入图片描述
在这里插入图片描述

#1 CPU2读取变量a
#2 缓存控制器找到对应的缓存行,发现状态是失效的, 缓存控制器则向总线发起read读请求
#3 总线广播CPU2的读请求,CPU1缓存控制器监听到这个读请求,发现自己有这个缓存行,且状态是M的。状态这时候先把状态是修改(M)的缓存行数据写入到主存,然后状态更新为独占(E)
#4 然后CPU1将缓存行状态更新为S(共享), 作为读响应返回
#5 总线广播读响应,CPU2和CPU3的缓存监测到读响应,会更新缓存行,把对应无效的缓存行状态变为有效

2.5.5 核心写失效状态的数据

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#1 CPU2要更新或者修改数据a,缓存命中但是是缓存行是失效状态
#2 缓存控制器发送read invalidate消息到总线,总线传播read invalidate消息
#3 各个CPU缓存控制器检查本地缓存有无对应的缓存行,如果其他CPU没有或者都是S或者是E则返回读响应,然后各个CPU缓存将对应的缓存行置为无效(I); 如果有CPU的缓存行状态是修改状态(M), 则需要先将M写入下一个缓存或者主存,发送write back消息到总线,然后自己这状态置为E;然后在返回读响应和失效响应
#4 然后CPU2根据读响应先将缓存行对应数据更新,状态置为E,然后CPU2将缓存行更新缓存,缓存行状态置为M

三 MESI协议存在哪些问题

情况一:发送invalidate 或者 read invalidate消息,需要同步等待其他CPU invalidate ack返回
情况二:发送write back消息,将该缓存行数据写入下一个缓存或者主存中去。此时该缓存行的CPU不能干其他事情,比如不能执行其他指令,只能同步等待返回结果才能执行下一步操作
情况三:缓存控制器收到invalidate消息需要将对应缓存行状态失效,当前CPU也需要等待修改状态完成才能干其他事情

四 MESI协议优化

4.1 store buffer 和 invalidate queue

在这里插入图片描述

为什么引入store buffer?
我们知道MESI协议在缓存读写的时候存在的一些效率问题,比如同步等待。那么针对这个情况,针对需要写请求,提供一个缓冲区(store buffer),指令放入到store buffer则认为写成功,CPU可以继续干其他事情,而不用同步等待。除非store buffer满了,才会继续同步等待。

为什么引入无效队列?
#1 对于接受invalidate消息,需要等待缓存被更新才能返回invalidate ack,影响性能
#2 store buffer因为空间也不大,如果满了依然还是会等待足够数量的invalidate ack响应返回才能执行下一个指令,即还是需要同步等待

4.1.1 store buffer

#1 先将数据写入store buffer
#2 然后发送invalidate消息到总线,将其他CPU对应的数据如果是E或者S则置为I
#3 等待所有的CPU返回invalidate ack 响应,则将数据从store buffer取出来,然后将本地缓存对应的缓存行置为修改(M)状态

4.1.2 invalidate queue

#1 缓存控制器收到其他CPU的invalidate消息,并不立即更新缓存状态为失效状态
#2 而是将这个请求放入到invalidate queue无效队列中,然后就返回invalidate ack响应
#3 当某个时机,比如当前本地缓存要读取数据,先从无效队列中获取,如果存在则将对应的缓存行置为失效(I)状态,此时再进行读取,就需要从其他CPU或者主存进行远程读了

4.2 引入store buffer 和invalidate queue存在哪些问题?

4.2.1 store buffer 引起的乱序或者有序性问题

问题:缓存行更新延迟导致可见性问题,并且对于有依赖关系的读写指令可能发生乱序的问题
CPU会把数据放在store buffer, 然后缓存控制器发送invalidate请求到总线,然后CPU就认为写成功了,则去执行其他指令了。缓存控制器等待足够数量的invalidate ack返回,然后再对对应的缓存行操作。正是因为延迟执行了缓存行操作,导致缓存中的数据并不是最新的,如果所以很有可能存在之前的缓存行还没有写,后续的CPU需要写新的数据,但是新数据是需要依赖之前的数据,导致从缓存读取的是旧数据,并不是最新的数据。

4.2.1.1 写读重排序

当前指令是写读指令,且读依赖于写,但是读先于写发生,导致顺序错乱。如代码所示:
在这里插入图片描述

#1 CPU需要修改a的值,假设a所在缓存行是S状态的,当CPU把a放入store buffer, 然后发送invalidate消息,然后CPU就认为写成功了
#2 CPU继续执行第二条指令,需要把a变量值赋给变量b,此时CPU会从缓存读取变量a
#3 但是由于之前发送的a修该还未收到足够数量的invalidate ack,所以缓存控制器还没有更新a的值以及对应的缓存行状态,此时还是处于S状态,所以直接进行本地读取, 所以产生了写读重排序问题(StoreLoad)
#4 所以导致b的值是有问题,期望是10可最后是0

4.2.1.2 写写重排序问题

当前后指令是写写指令的时候,后面的写先于前面的写发生,导致写写重排序问题。比如:
在这里插入图片描述

#1 假设a是S状态,先执行a >> 2操作,即修改完了之后,把数据a放入store buffer,然后缓存控制器发送invalidate消息到总线,CPU认为写完成
#2 CPU继续执行下一个指令 c+=a;
#3 但是此前的写a发送invalidate 消息还没有足够数量的invalidate ack返回,所以缓存中a变量并没有修改
#4 但是此时c+=a,会读取缓存a变量,所以就相当于c+=a先于a>>2执行了,所以产生了写写重排序问题(StoreStore)

4.2.2 invalidate queue无效队列存在缓存失效时间不确定的问题

当缓存控制器监听到总线上的invalidate消息是,加入到invalidate queue,就返回invalidate ack,这样提升了效率,无需等待处理器失效缓存行之后才返回。但是invalidate queue中的消息何时执行失效并不是确定的,很有可能现在本身需要读取一个更新的数据,但是因为还没有是当前缓存的状态更新,导致读取的还是旧值,比如:
在这里插入图片描述

#1 收到CPU1发送的invalidate 消息,需要把a所在缓存行状态置为失效(I),然后放入到无效队列中
#2 b需要读取a的值,缓存中此时a所在缓存行状态并不是失效的,所以还是读取以前旧值
#3 按照预期的情况是,a所在缓存行是失效的,需要发送remote read消息,从其他CPU或者主存读取,获取最新的值
#4 所以这里依然产生了写读重排序问题(StoreLoad)

五 内存屏障(Memory Barrier)解决重排序问题

因为store buffer和 invalidate queue会导致重排序问题,那么不要store buffer和 invalidate queue不就可以禁止重排序问题,这样肯定没问题,但是效率低下,所以还是需要store buffer和 invalidate queue来优化性能。如果可以有一种机制可以根据需要来禁止重排序就好了,内存屏障就是这样的一个机制。在Linux中提供了三种内存屏障函数,即lfence(读串行化)、sfence(写串行化)、mfence(读写都串行化)

JVM里面主要包括四种屏障:LoadLoad、LoadStore、StoreStore、StoreLoad屏障。

5.1 LoadLoad 读读屏障

LoadLoad: 读读屏障,禁止LoadLoad重排序,确保LoadLoad之前任何的读操作都必须在之后的Load前面。比如Load 1; Load2; LoadLoad; Load 3, 必须在Load 3之前先执行Load1和Load2。

5.2 StoreStore 写写屏障

StoreStore: 写写屏障,禁止写写重排序,确保在StoreStore之前的任何写操作都会在屏障后的写操作之前写入(等待屏障之前所有写操作收到了足够数量的invalidate ack响应,然后更新缓存,清空store buffer, 再进行写操作)。比如Store1;Store2;Store3;StoreStore;Store4;Store5;确保StoreStore之后的Store4;Store5执行前,StoreStore之前的Store1;Store2;Store3;已经完全执行完毕,更新了缓存和状态。

5.3 StoreLoad 读写屏障

StoreLoad: 写读屏障,禁止写读重排序指令,确保在StoreLoad之前的所有写操作全部完成才会进行后面的读(等待屏障之前所有写操作收到了足够数量的invalidate ack响应,然后更新缓存,清空store buffer, 再进读操作)。比如Store1;Store2;StoreLoad;Load1, 必须确保执行Load1之前的所有写操作Store1和Store2全部执行完了。

5.4 LoadStore 读写屏障

LoadStore: 读写屏障,禁止LoadStore重排序,确保屏障之前任何一个读的数据都会在屏障后任意一个写操作之前被读取。比如Load 1;Load 2; LoadStore; Store 1, Store 2,必须确保在执行LoadStore 屏障后的Store 1和 Store 2之前,先要执行Load 1和Load 2, 读取的结果更新缓存。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值