首页 > 编程知识 正文

Memory Barriers(内存屏障): a Hardware View for Software Hackers 阅读笔记

时间:2023-05-05 22:33:26 阅读:216625 作者:2120

Memory Barriers: a Hardware View for Software Hackers(原文地址:http://www.puppetmastertrading.com/images/hwViewForSwHackers.pdf)是一篇介绍CPU缓存及内存屏障的原理,通过阅读这篇文章使我对CPU缓存工作原理和内存屏障有了新的认识,也让我对java内存模型有了新的理解,一下是本人关于这篇文章的前5章做的总结,限于本人的英语水平和相关知识的欠缺存在很多理解不正确的地方,欢迎大家指出。 1. 缓存结构 1.1 cache line结构(cache line)


- cache line为什么设计成多行两列
多行实际上是硬件hash结构,首先根据内存地址可以直接映射到某一cache line(比如地址0X…000–>0x0,0X…FO1–>OXF)。可以实现快速定位,否则需要逐行匹配,耗费时间

为什么需要设计成两列的模式
如果直接使用单列的硬件hash缓存结构,由于缓存数据经常会出现冲突,剔除原有缓存,造成缓存命中率大大降低。加上一列可以在出现冲突时有备用地址保存冲突元素cache line每一次缓存多大的数据
cache line每一次缓存一块数据,比如cache line大小是64kb,则每次缓存是64kb数据。连续64kb的数据实际上映射到一个缓存块上去,比如0X00000002和0X00000003属于同一个缓存块,cache line其中一个都会缓存另外一个。 1.2 CPU总体缓存架构


缓存总体上由,缓存器,store buffer,invalidate queues三部分组成,其中缓存器用于存储缓存数据项store buffer用于存储cpu写入的数据项,invalidate queues用于保存接收到无效消息时,暂时存储的位置

2. 缓存协议 2.1 缓存状态MESI

缓存的主要状态有如下4种

M:modified状态
表示当前cache line的数据项被修改过(未保存至内存),这种状态下该数据是被该CPU占有的,其他任何CPU中都不会存在有效的相同数据项。E:exclusive状态
表示该数据被本CPU拥有,在其他CPU的缓存中都不存在对应的数据,这种情况下该CPU可以直接修改该数据项,而不需要发送消息S:shared状态
表示该数据被多个CPU所共享(存在于多个CPU缓存中),这种情况下CPU不能直接修改该数据项I:invalid状态
表示该数据无效,即删除,CPU加载该数据时不需要重新加载,不能直接使用 2.2 缓存协议消息

缓存消息主要有如下6种

Read消息
该消息包含读的物理地址,一般用于加载数据Read Respionse消息
该消息包含前面Read消息所请求的数据,可以由其他CPU缓存或者内存发出Invalidate消息
该消息包含无效的物理地址,由某个CPU缓存发出,所有接收到消息的缓存需要移除对应的数据项(置无效)Invalidate Acknowledge消息
在接收到Invalidate消息并移除对应数据后,相应的CPU缓存需要发送此消息Read Invalidate消息
该消息包含读的物理地址,同时让其他CPU缓存移除对应数据。该消息可以接收到一个Read Response消息和一系列Invalidate Acknowledge消息Writeback消息
该消息包含物理地址和需要被会写内存的数据,这个消息允许缓存为存放其他数据清除该数据所占的空间,否则该数据不能被移除

2.3 MESI状态转换

以下转换以CPU0的角度解释
(1)CPU0自身发送消息导致的状态转换

a:m->e
发送writeback消息,将数据写入内存,同时将当前数据项的状态置为Exclusiveb:e->m
不需要发送任何消息,CPU0拥有数据项,直接将数据写入cache linej:i->e
CPU0意识到将要保存一个数据但是不在他的缓存中,CPU0发送read invalidate消息,在接收到一个read response消息和一系列validate acknowledge消息后,改变状态为exclusive,此时CPU0可以通过b保存数据项(也有说加载数据项后,发现其他缓存中不存在该数据,这种解释应该是错的)d:i-m
CPU0执行一个原子性读写操作,直接保存一个数据项但是不在他的缓存中,CPU0发送read invalidate消息,在接收到一个read response消息和一系列validate acknowledge消息后,改变状态为modfiedk:i->s
CPU0发送read消息,加载数据项,得到read response消息后改变状态为sharede:s->m
CPU0执行一个原子性读写操作,直接保存一个数据,但是该数据当前以shared(只读)状态在他的缓存中,CPU0发送invalidate消息,在接收到一系列validate acknowledge消息后,改变状态为modfiedh:s->e
CPU0意识到将要保存一个数据,但是该数据当前以shared(只读)状态在他的缓存中,CPU0发送invalidate消息,在接收到一个read response消息和一系列validate acknowledge消息后,改变状态为exclusive,此时CPU0可以通过b保存数据项;
或者所有其他CPU发送消息writeback将数据回写至内存(i-e可否知道自己单独存储?)

(2)其他CPU发送消息给CPU0导致的状态转换

c:m->i
CPU0接收到read消息,发送read response消息同时将modfied状态的数据项发送出去,改变自身状态置sharedf:m-s
CPU0接收到read消息,发送read response消息同时将modfied状态的数据项发送出去,改变自身状态置sharedi:e->i
其他CPU执行一个原子性读写操作,发送read invalidate消息,CPU0接收到消息后发送read response消息同时将modfied状态的数据项发送出去,然后发送invalidate acknowledge消息并将该数据项置invalidateg:e->s
其他CPU读取一个数据项,发送read消息,CPU0接收消息后发送数据项,同时将其状态置为sharedl:s-i
其他CPU想要保存一个数据项,发送invalidate消息,CPU0接收消息后将该数据项状态置为invalidate 3. store buffer和内存屏障(memory barriers) 3.1 store buffer

(1)store buffer如何工作
CPU0在写入共享数据时,直接将数据写入store buffer中,同时发送invalidate消息,等接收到所有的invalidate acknowledge消息时再将数据存储至cache line中

(2)为什么需要store buffer

虽然加上缓存后可以使数据的读取更加有效率,但是对于数据的存储来说却并不是非常有效率考虑如下:
CPU0存储数据项,发送invalidate消息,此时需要等待接收所有invalidate acknowledge消息才能将数据保存至cache line,这对存储来说是非常耗时的,而且CPU0总是会将数据存储至cache line中

(3)为什么需要让CPU可以从store buffer中加载数据
我们看一个例子,如下:

初始状态 CPU0拥有变量b=0,CPU1拥有变量a=0CPU0执行a=1,cache misssing,发送read invalidate消息(为什么不是invalidate消息?)CPU0直接存储a=1至store buffer中CPU0接收CPU1发送的read response消息(a=0)CPU0从cache line中载入a=0CPU0接收invalidate acknowledge消息,将a=1写入cache lineCPU0执行b=a+1,其中a=0,且CPU0拥有b(状态为exclusive),所有直接写入cache line,并将b状态置为modified,此时b=1CPU0执行assert(b==2)–>false
上述问题可以让CPU0直接加载a时直接读取store buffer中的a 3.2 内存屏障

(1)首先看一个例子

初始状态CPU0拥有b=0,CPU1拥有a=0,CPU0执行foo,CPU1执行barCPU0执行a=1,CPU0缓存中不拥有a,所有将a=1放入store buffer,同时发送read invalidate消息CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息CPU0执行b=1,CPU0拥有b,所以直接写入缓存中(b状态为modfied)CPU0接收到read消息,发送read response消息,同时将b状态编程sharedCPU1接收CPU0发送来的b=1的数据CPU1结束while(b==0)的循环CPU1执行assert(a==1),CPU1中a=0,所以FALSECPU1接收到read invalidate消息,发送a同时将a置invalid状态,此时已经晚了CPU0接收到read response和invalidate acknowledge消息,将store buffer中的a=1存储至缓存中(a状态为modfied)

(2)上述问题的解决方案

使用内存屏障,该内存屏障主要使CPU简单的停止存储数据直到store buffer为空或者使用store buffer存储后续的数据直到先前的数据全部保存至缓存

代码如下(主要加上了内存屏障)

初始状态CPU0拥有b=0,CPU1拥有a=0,CPU0执行foo,CPU1执行barCPU0执行a=1,CPU0缓存中不拥有a,所有将a=1放入store buffer,同时发送read invalidate消息CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息CPU0执行smp_mp(),标记当期的store buffer中的数据项CPU0执行b=1,CPU0拥有b,但是当前store buffer中有被标记的数据项(即上面的a),所有CPU0只能将b=1存储至store bufferCPU0接收到read消息,发送read response消息,同时将b状态编程sharedCPU1接收CPU0发送来的b=0的数据CPU1继续while(b==0)的循环,此时CPU1缓存中的b=0CPU1接收到read invalidate消息,发送a同时将a置invalid状态CPU0接收到read response和invalidate acknowledge消息,将store buffer中的a=1存储至缓存中(a状态为modfied)CPU0的store buffer中只要a一个表标记,a被存储至缓存中,所以此时b也可以存储了,但是现在CPU0中b的状态是shared,所以CPU0发送invalidate消息CPU1接收invalidate消息,发送acknowledge消息CPU0接收acknowledge消息,将b状态置为exclusive,同时可以将store buffer中的b=1写入缓存中CPU1执行while(b==0)但是此时CPU1缓存中不包含b,发送read消息CPU0接收read消息,发送b=1,同时将b状态置为sharedCPU1接收CPU0发送来的b=1的数据CPU1结束while(b==0)的循环CPU1执行assert(a==1),CPU1缓存中此时并不包含a,所以发送read消息,当其接收CPU0返回的a=1消息时,执行assert(a==1)此时正确 4. invalidate queues和内存屏障 4.1 invalidate queues

(1)invalidate queues如何工作
CPU在接收到invalidate消息后,不用等到CPU真正将相应的缓存置为无效状态,CPU可以直接将对应数据加入invalidate queues中,同时直接发送invalidate acknowledge响应,当然在对应的数据被处理前,CPU不能再向其他CPU发送有关该数据的无效消息
(2)为什么需要invalidate queues
因为缓存存储器的容量有限(很小),CPU很容易将其填满(特别是加入了内存屏障的情况下),通过加入invalidate queues可以让其他CPU快速响应acknowledge消息,以将数据从store buffer保存至缓存器中

4.2 内存屏障

(1)首先看一个例子
这个例子还是使用3.2(1)中的代码

初始状态CPU0拥有b=0(独有),存有a=0(shared),CPU1存有a=0(shared),CPU0执行foo,CPU1执行barCPU0执行a=1,CPU0缓存a的状态是shared,将a=1放入store buffer,同时发送invalidate消息CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息CPU0执行b=1,CPU0拥有b,所以直接将其存储至缓存中CPU0接收到read消息,发送read response消息,同时将b状态变成sharedCPU1接收invalidate消息,将其加入invalidate queues,同时直接发送invalidate acknowledge消息(此时CPU1mldyl拥有a)CPU1接收CPU0发送来的b=1的数据CPU1结束while(b==0)的循环CPU1执行assert(a==1),但是此时a的值为0,因为CPU1的缓存中还存在aCPU1处理invalidate queues,将a移除缓存,但是此时已经晚了CPU0接收到invalidate acknowledge消息,保存a=1至缓存中

(2)上述问题的解决方案
使用内存屏障,该内存屏障主要将invalidate queues中的数据项都标记,CPU后续的加载数据都必须等到这些被标记的数据处理完毕
代码如下:

初始状态CPU0拥有b=0(独有),存有a=0(shared),CPU1存有a=0(shared),CPU0执行foo,CPU1执行barCPU0执行a=1,CPU0缓存a的状态是shared,将a=1放入store buffer,同时发送invalidate消息CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息CPU0执行b=1,CPU0拥有b,所以直接将其存储至缓存中CPU0接收到read消息,发送read response消息,同时将b状态变成sharedCPU1接收invalidate消息,将其加入invalidate queues,同时直接发送invalidate acknowledge消息(此时CPU1mldyl拥有a)CPU1接收CPU0发送来的b=1的数据CPU1结束while(b==0)的循环CPU1执行smp_mb内存屏障,将invalidate queues中的数据标记CPU1执行assert(a==1),但是此时a被内存屏障标记,存在于invalidate queues中,所有不能加载a直至invalidate queues中a的消息被处理了CPU1处理invalidate queues,将a移除缓存CPU1此时可以加载a了,但是此时CPU1中并不包含a,所有发送read消息CPU0接收到invalidate acknowledge消息,保存a=1至缓存中CPU0接收read消息,将a=1发送至CPU1CPU1接收CPU0返回的a=1消息,执行assert(a==1)此时正确 5. 读写内存屏障

上述例子中的内存屏障会同时处理store buffer和invalidate queues,但是在我们的代码中foo并不需要处理invalidate queues,同样的bar也无需处理store buffer,所以一些CPU架构将两者分开处理,分别是读内存屏障和写内存屏障如下:

写内存屏障主要解决写入数据至缓存存储器时,保证后续的写操作不能再这个写操作之前(也就是内存指令不能重排),这样可以避免CPU0执行a=1(写入store buffer),b=1(直接写入),然后其他CPU读取CPU0数据时,造成读到了b=1的操作,而未得到a=1的导致的结果,通过内存屏障强制a=1发生在b=1之前 读内存屏障主要解决invalidate queues中的数据和缓存中数据的冲突问题,保证CPU读取数据时必须先执行完invalidate queues中的任务,否则CPU0执行a=1,b=1操作时,CPU1可能读取到的a还是a=0(因为a=1发送的无效消息,但是该消息并不直接将CPU1缓存中的a置为无效),但是b=1(直接从CPU0读取

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。