分布式缓存一致性性,什么是分布式缓存一致性性

感觉有人把访存次序consistency和缓存一致性coherency搞混了。我从硬件角度说说他们到底是什么。
首先我有一些假设,每个核拥有独立的一级缓存,他们之间通过总线相连并一直保持开启和一致性。为了简单起见我把二三级省略了。
缓存一致性,排名第一的答案解释的很好了,我只加一点,就是它是针对同一缓存行的地址的。有些变量处于相同缓存行的不同字节,我们仍然认为他们是一个地址。
而访存次序定义了访存指令执行的次序。再详细一点,访存可以分成两部分,一部分是请求request,一部分是完成completion,这两步是可以拆开的。并且,请求的次序和完成的次序可以是打乱的。例如read A, read B可以拆分成read
A req, read B req, read A comp, read B comp或者read A req, read A comp, read B req, read B comp,又或者read B req, read A req, read A comp, read B comp或者read B req, read B comp, read A req, read A comp。&
我们先看一个核的情况。当连续的两个读read A, read B被发到总线,我们想保持请求或完成有序,那么就插入内存壁垒指令mbar。内存壁垒指令又可以分为两类,一类是弱壁垒mbar0,一类是强壁垒mbar1。强壁垒是说,我必须等第一个读完成了,才能发第二个。也就是read
A req, read A comp, mbar1, read B req, read B comp。弱壁垒只需要保证请求有先后次序就行,read A req, mbar0, read B req, read A comp, read B comp或者read A req, mbar0, read B req, read B comp, read A comp。它们对应的指令都是read A, mbar, read B。
在之后的部分,我假设mbar都是强壁垒,而不加mbar的时候,请求和完成次序完全是乱的,这样便于分析。
我们在一个核上引入写,write A, mbar, read B。当地址A=B,此时能保证读到的是写入的值。不加这个mbar我就没法保证,加了缓存也没用,因为写可能都没发生。
当A和B不相等,也有特殊的用途。假设我访问的是设备的寄存器,先要设一个寄存器让他开始工作,然后再去读另一个寄存器看看状态,这时候就必须要壁垒指令,否则读状态的时候设备还没开始工作。当然,此时一般对于设备的缓存是禁止的,并且不可推测执行,防止额外的访问。例外的情况是有些处理器如powerpc,关掉缓存就默认是强制有序,所以不需要壁垒指令。
当我们把访存次序把缓存一致性结合考虑,可以假设在C和D两个核上,同时循环跑write A coreC, read B coreC和write A coreD, read B coreD。核C执行write A coreC时,总线会广播到核D,看看是不是有同样的地址,做一系列操作。具体过程请参考mesi协议,我就不详细写了。有人说每次都这样广播岂不是很浪费时间,没错。优化方法之一是把缓存在总线里存个小副本,只保留状态和源地址,这样就不必总是到另外一个核去查了。这时候不管单核和双核,读写次序都是乱的,完全没法做任何保证。唯一能保证的是缓存一致性。
然后我们改下代码,加入壁垒write A coreC, mbar, read B coreC和write A coreD, mbar, read B coreD。这时候核之间次序没保证,每个核上的有次序保证。可能发生的序列是write
A coreC, write A coreD, read B coreD, read B coreC.
假设地址A和B相同,此时核C读到的可能是核D写入的值,而不是他自己写入的。此时缓存一致性还是没有问题,但是却未必是我们想要的结果。
所以在多核时,哪怕使用了内存壁垒指令,也没法保证读写的原子性。解决办法有两个,一个是软件锁,一个是原子操作。原子操作我看到过的有两种,一种是总线收到锁请求时,直接封掉整个总线,同时只有一个核能访问。这样效率很低。还有个方法是把锁的请求发送到对端设备,比如内存控制器,让他禁止别的核的访问,而总线依然可以运行,这样效率就高不少,我看到过的数据,减少10倍时间。
软件锁我说一个自旋锁,因为它能用一个硬件机制来完美模拟。当使用特殊指令对一个地址写入值,相应缓存行上会做一个特殊标记,表示还没有别的核去写这行缓存。然后下条指令读这个行,如果标记没变,说明写和读之间没有人打扰,那么就拿到锁了。如果变了,那么回到写的过程重新获取锁。当然,过程中可能还是需要壁垒指令来保证次序。
更复杂的情况,我们可以把访存的请求和完成,弱壁垒和强壁垒同时考虑,来达到最大的访问效率。缓存一致性也可以单向来用,形成snoop.
希望能有所帮助。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:13183次
排名:千里之外
原创:13篇
(1)(1)(1)(1)(9)拒绝访问 | stor- | 百度云加速
请打开cookies.
此网站 (stor-) 的管理员禁止了您的访问。原因是您的访问包含了非浏览器特征(389cccdd-ua98).
重新安装浏览器,或使用别的浏览器303112345678910111213141516171819202122232425273031123456789
阅读排行榜
评论排行榜主从DB与cache一致性 - 简书
主从DB与cache一致性
转载:本文主要讨论这么几个问题:(1)数据库主从延时为何会导致缓存数据不一致(2)优化思路与方案一、需求缘起上一篇《缓存架构设计细节二三事》中有一个小优化点,在只有主库时,通过“串行化”的思路可以解决缓存与数据库中数据不一致。引发大家热烈讨论的点是“在主从同步,读写分离的数据库架构下,有可能出现脏数据入缓存的情况,此时串行化方案不再适用了”,这就是本文要讨论的主题。二、为什么数据会不一致为什么会读到脏数据,有这么几种情况:(1)单库情况下,服务层的并发读写,缓存与数据库的操作交叉进行
虽然只有一个DB,在上述诡异异常时序下,也可能脏数据入缓存:1)请求A发起一个写操作,第一步淘汰了cache,然后这个请求因为各种原因在服务层卡住了(进行大量的业务逻辑计算,例如计算了1秒钟),如上图步骤12)请求B发起一个读操作,读cache,cache miss,如上图步骤23)请求B继续读DB,读出来一个脏数据,然后脏数据入cache,如上图步骤34)请求A卡了很久后终于写数据库了,写入了最新的数据,如上图步骤4这种情况虽然少见,但理论上是存在的,后发起的请求B在先发起的请求A中间完成了。(2)主从同步,读写分离的情况下,读从库读到旧数据在数据库架构做了一主多从,读写分离时,更多的脏数据入缓存是下面这种情况:
1)请求A发起一个写操作,第一步淘汰了cache,如上图步骤12)请求A写数据库了,写入了最新的数据,如上图步骤23)请求B发起一个读操作,读cache,cache miss,如上图步骤34)请求B继续读DB,读的是从库,此时主从同步还没有完成,读出来一个脏数据,然后脏数据入cache,如上图步45)最后数据库的主从同步完成了,如上图步骤5这种情况请求A和请求B的时序是完全没有问题的,是主动同步的时延(假设延时1秒钟)中间有读请求读从库读到脏数据导致的不一致。那怎么来进行优化呢?三、不一致优化思路有同学说“那能不能先操作数据库,再淘汰缓存”,这个是不行的,在的文章中介绍过。出现不一致的根本原因:(1)单库情况下,服务层在进行1s的逻辑计算过程中,可能读到旧数据入缓存(2)主从库+读写分离情况下,在1s钟主从同步延时过程中,可能读到旧数据入缓存既然旧数据就是在那1s的间隙中入缓存的,是不是可以在写请求完成后,再休眠1s,再次淘汰缓存,就能将这1s内写入的脏数据再次淘汰掉呢?答案是可以的。写请求的步骤由2步升级为3步:(1)先淘汰缓存(2)再写数据库(这两步和原来一样)(3)休眠1秒,再次淘汰缓存这样的话,1秒内有脏数据如缓存,也会被再次淘汰掉,但带来的问题是:(1)所有的写请求都阻塞了1秒,大大降低了写请求的吞吐量,增长了处理时间,业务上是接受不了的再次分析,其实第二次淘汰缓存是“为了保证缓存一致”而做的操作,而不是“业务要求”,所以其实无需等待,用一个异步的timer,或者利用消息总线异步的来做这个事情即可:
写请求由2步升级为2.5步:(1)先淘汰缓存(2)再写数据库(这两步和原来一样)(2.5)不再休眠1s,而是往消息总线esb发送一个消息,发送完成之后马上就能返回这样的话,写请求的处理时间几乎没有增加,这个方法淘汰了缓存两次,因此被称为“缓存双淘汰”法。这个方法付出的代价是,缓存会增加1次cache miss(代价几乎可以忽略)。而在下游,有一个异步淘汰缓存的消费者,在接收到消息之后,asy-expire在1s之后淘汰缓存。这样,即使1s内有脏数据入缓存,也有机会再次被淘汰掉。上述方案有一个缺点,需要业务线的写操作增加一个步骤,有没有方案对业务线的代码没有任何入侵呢,是有的,这个方案在中也提到过,通过分析线下的binlog来异步淘汰缓存:
业务线的代码就不需要动了,新增一个线下的读binlog的异步淘汰模块,读取到binlog中的数据,异步的淘汰缓存。提问:为什么上文总是说1s,这个1s是怎么来的?回答:1s只是一个举例,需要根据业务的数据量与并发量,观察主从同步的时延来设定这个值。例如主从同步的时延为200ms,这个异步淘汰cache设置为258ms就是OK的。四、总结在“异常时序”或者“读从库”导致脏数据入缓存时,可以用二次异步淘汰的“缓存双淘汰”法来解决缓存与数据库中数据不一致的问题,具体实施至少有三种方案:(1)timer异步淘汰(本文没有细讲,本质就是起个线程专门异步二次淘汰缓存)(2)总线异步淘汰(3)读binlog异步淘汰==【完】==回【冗余】回【一致】回【缓存】
梦幻人生--用技术来点缀自己的人生
github:/wangjianghua123
陈捷的博客
梦康的博客
https://mengkang.net/
涛哥博客 http://www.tiyee.net/
张华伟博客 https://geekweb.me/
周至博客 /
数据库内核月报http://mysql.taobao.org/monthly/
全栈技术:/
阿里中间件团队博客:http://jm.taobao.org/您的位置: >>
  参考原文:
  本文是RAD Game Tools程序员Fabian &ryg& Giesen在其博客上发表的《Cache coherency primer》一文的翻译,经作者许可分享至InfoQ中文站。该系列共有两篇,本文系第一篇。
  我计划写一些关于多核场景下数据组织的文章。写了第一篇,但我很快意识到有大量的基础知识我首先需要讲一下。在本文中,我就尝试阐述这些知识。
  缓存(Cache)
  本文是关于CPU缓存的快速入门。我假设你已经有了基本概念,但你可能不熟悉其中的一些细节。(如果你已经熟悉了,你可以忽略这部分。)
  在现代的CPU(大多数)上,所有的内存访问都需要通过层层的缓存来进行。也有些例外,比如,对映射成内存地址的I/O口、写合并(Write-combined)内存,这些访问至少会绕开这个流程的一部分。但这两者都是罕见的场景(意味着绝大多数的用户态代码都不会遇到这两种情况),所以在本文中,我将忽略这两者。
  CPU的读/写(以及取指令)单元正常情况下甚至都不能直接访问内存&&这是物理结构决定的;CPU都没有管脚直接连到内存。相反,CPU和一级缓存(L1 Cache)通讯,而一级缓存才能和内存通讯。大约二十年前,一级缓存可以直接和内存传输数据。如今,更多级别的缓存加入到设计中,一级缓存已经不能直接和内存通讯了,它和二级缓存通讯&&而二级缓存才能和内存通讯。或者还可能有三级缓存。你明白这个意思就行。
  缓存是分&段&(line)的,一个段对应一块存储空间,大小是32(较早的ARM、90年代/2000年代早期的x86和PowerPC)、64(较新的ARM和x86)或128(较新的Power ISA机器)字节。每个缓存段知道自己对应什么范围的物理内存地址,并且在本文中,我不打算区分物理上的缓存段和它所代表的内存,这听起来有点草率,但是为了方便起见,还是请熟悉这种提法。具体地说,当我提到&缓存段&的时候,我就是指一段和缓存大小对齐的内存,不关心里面的内容是否真正被缓存进去(就是说保存在任何级别的缓存中)了。
  当CPU看到一条读内存的指令时,它会把内存地址传递给一级数据缓存(或可戏称为L1D$,因为英语中&缓存(cache)&和&现金(cash)&的发音相同)。一级数据缓存会检查它是否有这个内存地址对应的缓存段。如果没有,它会把整个缓存段从内存(或者从更高一级的缓存,如果有的话)中加载进来。是的,一次加载整个缓存段,这是基于这样一个假设:内存访问倾向于本地化(localized),如果我们当前需要某个地址的数据,那么很可能我们马上要访问它的邻近地址。一旦缓存段被加载到缓存中,读指令就可以正常进行读取。
  如果我们只处理读操作,那么事情会很简单,因为所有级别的缓存都遵守以下规律,我称之为:
基本定律:在任意时刻,任意级别缓存中的缓存段的内容,等同于它对应的内存中的内容。
  一旦我们允许写操作,事情就变得复杂一点了。这里有两种基本的写模式:直写(write-through)和回写(write-back)。直写更简单一点:我们透过本级缓存,直接把数据写到下一级缓存(或直接到内存)中,如果对应的段被缓存了,我们同时更新缓存中的内容(甚至直接丢弃),就这么简单。这也遵守前面的定律:缓存中的段永远和它对应的内存内容匹配。
  回写模式就有点复杂了。缓存不会立即把写操作传递到下一级,而是仅修改本级缓存中的数据,并且把对应的缓存段标记为&脏&段。脏段会触发回写,也就是把里面的内容写到对应的内存或下一级缓存中。回写后,脏段又变&干净&了。当一个脏段被丢弃的时候,总是先要进行一次回写。回写所遵循的规律有点不同。
回写定律:当所有的脏段被回写后,任意级别缓存中的缓存段的内容,等同于它对应的内存中的内容。
  换句话说,回写模式的定律中,我们去掉了&在任意时刻&这个修饰语,代之以弱化一点的条件:要么缓存段的内容和内存一致(如果缓存段是干净的话),要么缓存段中的内容最终要回写到内存中(对于脏缓存段来说)。
  直接模式更简单,但是回写模式有它的优势:它能过滤掉对同一地址的反复写操作,并且,如果大多数缓存段都在回写模式下工作,那么系统经常可以一下子写一大片内存,而不是分成小块来写,前者的效率更高。
  有些(大多数是比较老的)CPU只使用直写模式,有些只使用回写模式,还有一些,一级缓存使用直写而二级缓存使用回写。这样做虽然在一级和二级缓存之间产生了不必要的数据流量,但二级缓存和更低级缓存或内存之间依然保留了回写的优势。我想说的是,这里涉及到一系列的取舍问题,且不同的设计有不同的解决方案。没有人规定各级缓存的大小必须一致。举个例子,我们会看到有CPU的一级缓存是32字节,而二级缓存却有128字节。
  为了简化问题,我省略了一些内容:缓存关联性(cache associativity),缓存组(cache sets),使用分配写(write-allocate)还是非分配写(上面我描述的直写是和分配写相结合的,而回写是和非分配写相结合的),非对齐的访问(unaligned access),基于虚拟地址的缓存。如果你感兴趣,所有这些内容都可以去查查资料,但我不准备在这里讲了。
  一致性协议(Coherency protocols)
  只要系统只有一个CPU核在工作,一切都没问题。如果有多个核,每个核又都有自己的缓存,那么我们就遇到问题了:如果某个CPU缓存段中对应的内存内容被另外一个CPU偷偷改了,会发生什么?
  好吧,答案很简单:什么也不会发生。这很糟糕。因为如果一个CPU缓存了某块内存,那么在其他CPU修改这块内存的时候,我们希望得到通知。我们拥有多组缓存的时候,真的需要它们保持同步。或者说,系统的内存在各个CPU之间无法做到与生俱来的同步,我们实际上是需要一个大家都能遵守的方法来达到同步的目的。
  注意,这个问题的根源是我们拥有多组缓存,而不是多个CPU核。我们也可以这样解决问题,让多个CPU核共用一组缓存:也就是说只有一块一级缓存,所有处理器都必须共用它。在每一个指令周期,只有一个幸运的CPU能通过一级缓存做内存操作,运行它的指令。
  这本身没问题。唯一的问题就是太慢了,因为这下处理器的时间都花在排队等待使用一级缓存了(并且处理器会做大量的这种操作,至少每个读写指令都要做一次)。我指出这一点是因为它表明了问题不是由多核引起的,而是由多缓存引起的。我们知道了只有一组缓存也能工作,只是太慢了,接下来最好就是能做到:使用多组缓存,但使它们的行为看起来就像只有一组缓存那样。缓存一致性协议就是为了做到这一点而设计的。就像名称所暗示的那样,这类协议就是要使多组缓存的内容保持一致。
  缓存一致性协议有多种,但是你日常处理的大多数计算机设备使用的都属于&窥探(snooping)&协议,这也是我这里要讲的。(还有一种叫&基于目录的(directory-based)&协议,这种协议的延迟性较大,但是在拥有很多个处理器的系统中,它有更好的可扩展性。)
  &窥探&背后的基本思想是,所有内存传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(arbitrate):同一个指令周期中,只有一个缓存可以读写内存。窥探协议的思想是,缓存不仅仅在做内存传输的时候才和总线打交道,而是不停地在窥探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其他处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其他处理器马上就知道这块内存在它们自己的缓存中对应的段已经失效。
  在直写模式下,这是很直接的,因为写操作一旦发生,它的效果马上会被&公布&出去。但是如果混着回写模式,就有问题了。因为有可能在写指令执行过后很久,数据才会被真正回写到物理内存中&&在这段时间内,其他处理器的缓存也可能会傻乎乎地去写同一块内存地址,导致冲突。在回写模型中,简单把内存写操作的信息广播给其他处理器是不够的,我们需要做的是,在修改本地缓存之前,就要告知其他处理器。搞懂了细节,就找到了处理回写模式这个问题的最简单方案,我们通常叫做MESI协议(译者注:MESI是Modified、Exclusive、Shared、Invalid的首字母缩写,代表四种缓存状态,下面的译文中可能会以单个字母指代相应的状态)。
  MESI以及衍生协议
  本节叫做&MESI以及衍生协议&,是因为MESI衍生了一系列紧密相关的一致性协议。我们先从原生的MESI协议开始:MESI是四种缓存段状态的首字母缩写,任何多核系统中的缓存段都处于这四种状态之一。我将以相反的顺序逐个讲解,因为这个顺序更合理:
失效(Invalid)缓存段,要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。
共享(Shared)缓存段,它是和主内存内容保持一致的一份拷贝,在这种状态下的缓存段只能被读取,不能被写入。多组缓存可以同时拥有针对同一内存地址的共享缓存段,这就是名称的由来。
独占(Exclusive)缓存段,和S状态一样,也是和主内存内容保持一致的一份拷贝。区别在于,如果一个处理器持有了某个E状态的缓存段,那其他处理器就不能同时持有它,所以叫&独占&。这意味着,如果其他处理器原本也持有同一缓存段,那么它会马上变成&失效&状态。
已修改(Modified)缓存段,属于脏段,它们已经被所属的处理器修改了。如果一个段处于已修改状态,那么它在其他处理器缓存中的拷贝马上会变成失效状态,这个规律和E状态一样。此外,已修改缓存段如果被丢弃或标记为失效,那么先要把它的内容回写到内存中&&这和回写模式下常规的脏段处理方式一样。
  如果把以上这些状态和单核系统中回写模式的缓存做对比,你会发现I、S和M状态已经有对应的概念:失效/未载入、干净以及脏的缓存段。所以这里的新知识只有E状态,代表独占式访问。这个状态解决了&在我们开始修改某块内存之前,我们需要告诉其他处理器&这一问题:只有当缓存段处于E或M状态时,处理器才能去写它,也就是说只有这两种状态下,处理器是独占这个缓存段的。当处理器想写某个缓存段时,如果它没有独占权,它必须先发送一条&我要独占权&的请求给总线,这会通知其他处理器,把它们拥有的同一缓存段的拷贝失效(如果它们有的话)。只有在获得独占权后,处理器才能开始修改数据&&并且此时,这个处理器知道,这个缓存段只有一份拷贝,在我自己的缓存里,所以不会有任何冲突。
  反之,如果有其他处理器想读取这个缓存段(我们马上能知道,因为我们一直在窥探总线),独占或已修改的缓存段必须先回到&共享&状态。如果是已修改的缓存段,那么还要先把内容回写到内存中。
  MESI协议是一个合适的状态机,既能处理来自本地处理器的请求,也能把信息广播到总线上。我不打算讲更多关于状态图的细节以及不同的状态转换类型。如果你感兴趣的话,可以在关于硬件架构的书中找到更多的深度内容,但对于本文来说,讲这些东西有点过了。作为一个软件开发者,你只要理解以下两点,就大有可为:
  第一,在多核系统中,读取某个缓存段,实际上会牵涉到和其他处理器的通讯,并且可能导致它们发生内存传输。写某个缓存段需要多个步骤:在你写任何东西之前,你首先要获得独占权,以及所请求的缓存段的当前内容的拷贝(所谓的&带权限获取的读(Read For Ownership)&请求)。
  第二,尽管我们为了一致性问题做了额外的工作,但是最终结果还是非常有保证的。即它遵守以下定理,我称之为:
MESI定律:在所有的脏缓存段(M状态)被回写后,任意缓存级别的所有缓存段中的内容,和它们对应的内存中的内容一致。此外,在任意时刻,当某个位置的内存被一个处理器加载入独占缓存段时(E状态),那它就不会再出现在其他任何处理器的缓存中。
  注意,这其实就是我们已经讲过的回写定律加上独占规则而已。我认为MESI协议或多核系统的存在根本没有弱化我们现有的内存模型。
  好了,至此我们(粗略)讲了原生MESI协议(以及使用它的CPU,比如ARM)。其他处理器使用MESI扩展后的变种。常见的扩展包括&O&(Owned)状态,它和E状态类似,也是保证缓存间一致性的手段,但它直接共享脏段的内容,而不需要先把它们回写到内存中(&脏段共享&),由此产生了MOSEI协议。还有MERSI和MESIF,这两个名字代表同一种思想,即指定某个处理器专门处理针对某个缓存段的读操作。当多个处理器同时拥有某个S状态的缓存段的时候,只有被指定的那个处理器(对应的缓存段为R或F状态)才能对读操作做出回应,而不是每个处理器都能这么做。这种设计可以降低总线的数据流量。当然你可以同时加入R/F状态和O状态,或者更多的状态。这些都属于优化,没有一种会改变基本定律,也没有一种会改变MESI协议所确保的结果。
  我不是这方面的专家,很有可能有系统在使用其他协议,这些协议并不能完全保证一致性,不过如果有,我没有注意到它们,或者没有看到有什么流行的处理器在使用它们。所以为了达到我们的目的,我们真的就可以假设一致性协议能保证缓存的一致性。不是基本一致,不是&写入一会儿后才能保持一致&&&而是完全的一致。从这个层面上说,除非硬件有问题,内存的状态总是一致的。用技术术语来说,MESI以及它的衍生协议,至少在原理上,提供了完整的(sequential consistency),在C++ 11的内存模型中,这是最强的一种确保内存顺序的模型。这也引出了问题,为什么我们需要弱一点的内存模型,以及&什么时候会用到它们&?
  内存模型
  不同的体系结构提供不同的内存模型。到本文写作的时候为止,ARM和POWER体系结构的机器拥有相对较弱的内存模型:这类CPU在读写指令重排序(reordering)方面有相当大的自由度,这种重排序有可能会改变程序在多核环境下的语义。通过&内存屏障(memory barrier)&,程序可以对此加以限制:&重排序操作不允许越过这条边界&。相反,x86则拥有较强的内存模型。
  我不打算在这里深入到内存模型的细节中,这很容易陷入堆砌技术术语中,而且也超出了本文的范围。但是我想说一点关于&他们如何发生&的内容&&也就是,弱内存模型如何保证正确性(相比较于MESI协议给缓存带来的顺序一致性),以及为什么。当然,一切都归结于性能。
  规则是这样的:如果满足下面的条件,你就可以得到完全的顺序一致性:第一,缓存一收到总线事件,就可以在当前指令周期中迅速做出响应。第二,处理器如实地按程序的顺序,把内存操作指令送到缓存,并且等前一条执行完后才能发送下一条。当然,实际上现代处理器一般都无法满足以上条件:
缓存不会及时响应总线事件。如果总线上发来一条消息,要使某个缓存段失效,但是如果此时缓存正在处理其他事情(比如和CPU传输数据),那这个消息可能无法在当前的指令周期中得到处理,而会进入所谓的&失效队列(invalidation queue)&,这个消息等在队列中直到缓存有空为止。
处理器一般不会严格按照程序的顺序向缓存发送内存操作指令。当然,有乱序执行(Out-of-Order execution)功能的处理器肯定是这样的。顺序执行(in-order execution)的处理器有时候也无法完全保证内存操作的顺序(比如想要的内存不在缓存中时,CPU就不能为了载入缓存而停止工作)。
写操作尤其特殊,因为它分为两阶段操作:在写之前我们先要得到缓存段的独占权。如果我们当前没有独占权,我们先要和其他处理器协商,这也需要一些时间。同理,在这种场景下让处理器闲着无所事事是一种资源浪费。实际上,写操作首先发起获得独占权的请求,然后就进入所谓的由&写缓冲(store buffer)&组成的队列(有些地方使用&写缓冲&指代整个队列,我这里使用它指代队列的一条入口)。写操作在队列中等待,直到缓存准备好处理它,此时写缓冲就被&清空(drained)&了,缓冲区被回收用于处理新的写操作。
  这些特性意味着,默认情况下,读操作有可能会读到过时的数据(如果对应失效请求还等在队列中没执行),写操作真正完成的时间有可能比它们在代码中的位置晚,一旦牵涉到乱序执行,一切都变得模棱两可。回到内存模型,本质上只有两大阵营:
  在弱内存模型的体系结构中,处理器为了开发者能写出正确的代码而做的工作是最小化的,指令重排序和各种缓冲的步骤都是被正式允许的,也就是说没有任何保证。如果你需要确保某种结果,你需要自己插入合适的内存屏障&&它能防止重排序,并且等待队列中的操作全部完成。
  使用强一点的内存模型的体系结构则会在内部做很多记录工作。比如,x86会跟踪所有在等待中的内存操作,这些操作都还没有完全完成(称为&退休(retired)&)。它会把它们的信息保存在芯片内部的MOB(&memory ordering buffer&,内存排序缓冲)。x86作为部分支持乱序执行的体系结构,在出问题的时候能把尚未&退休&的指令撤销掉&&比如发生页错误(page fault),或者分支预测失败(branch mispredict)的时候。我已经在我以前的文章&&中提到过一些细节,以及和内存子系统的一些交互。主旨是x86处理器会主动地监控外部事件(比如缓存失效),有些已经执行完的操作会因为这些事件而被撤销,但不算&退休&。这就是说,x86知道自己的内存模型应该是什么样子的,当发生了一件和这个模型冲突的事,处理器会回退到上一个与内存模型兼容的状态。这就是我在中提到的&清除内存排序机(memory ordering machine clear)&。最后的结果是,x86处理器为内存操作提供了很强的一致性保证&&虽然没有达到完美的顺序一致性。
  无论如何,一篇文章讲这么多已经够了。我把它放在我的博客上。我的想法是将来的文章只要引用它就行了。我们看效果吧。感谢阅读!
其他分类热门文章
其他分类最新文章

我要回帖

更多关于 缓存数据一致性 的文章

 

随机推荐