精华内容
下载资源
问答
  • mesi
    千次阅读 多人点赞
    2022-02-21 16:17:10

    前言:

    • JVM不是真实存在的,只是一个抽象的概念。volatile关键字底层也是借助MESI缓存一致性协议和内存屏障得以实现有序性和可见性的。
    • MESI是计算机底层的协议,所有支持高并发的编程语言的底层都是基于MESI协议来保证并发安全的,所以MESI协议更有助于我们去理解底层原理,知其所以然!
    • 本期围绕着,什么是(Who),为何来(How),是什么(What),这三点内容来进行讲解该协议。

    1.什么是(Who):

    MESI(Modified Exclusive Shared Or Invalid)协议是基于Invalidate的高速缓存一致性协议,并且是支持回写高速缓存的最常用协议之一。 它也被称为伊利诺伊州协议(由于其在伊利诺伊大学厄巴纳 - 香槟分校的发展)。用于解决缓存一致的问题。


    2.为何来(How):

    2.1缓存不一致带来的后果

    在这里插入图片描述
    如上图,数据加载的流程如下:(从内存到寄存器)

    1. 将程序和数据从硬盘加载到内存中

    2. 将程序和数据从内存加载到缓存中(目前多三级缓存,数据加载顺序:L3->L2->L1)

    3. CPU将缓存中的数据加载到寄存器中,并进行运算

    4. CPU会将数据刷新回缓存,并在一定的时间周期之后刷新回内存

    现在的CPU基本都是多核CPU,服务器更是提供了多CPU的支持,而每个核心也都有自己独立的缓存,当多个核心同时操作多个线程对同一个数据进行更新时,如果核心2在核心1还未将更新的数据刷回内存之前读取了数据,并进行操作,就会造成程序的执行结果造成随机性的影响,举个例子如下:

    如果由两个cpu同事开始读取了int i =0,然后同同时执行如下语句,会出现如下情况:

    ......
    int i = 0;
    i++;
    ......
    

    刚开始,i初始化为0,假设有两个线程A,B,

    1. A线程在CPU0上进行执行,从主存加载i变量的数值到缓存,然后从缓存中加载到寄存器中,在寄存器中执行i+1操作,得到i的值为1,此时得到i等于1的值还存放在CPU0的缓存中;
    2. 由于线程A计算i等于1的值还存放在缓存中,还没有刷新会内存,此时线程B执行在CPU1上,从内存中加载i的值,此时i的值还是0,然后进行i+1操作,得到i的值为1,存到CPU1的缓存中,
    3. A,B线程得到的值都是1,在一定的时间周期之后刷新回内存
      4.写回内存后,两次i++操作之后,其值还是1;
      可以看到虽然我们做了两次++i操作,但是只进行了一次加1操作,这就是缓存不一致带来的后果。

    2.2解决方法:

    在先前的解决方案中,大致有两种思路:
    总线加锁的方式:

    • 先前大佬们提供了一种总线加锁的方式,而总线加锁是对整个内存进行加锁,在一个核心对一个数据进行修改的过程中,其他的核心也无法修改内存中的其他数据,这样对导致CPU处理性能严重下降。

    总线加锁的方式导致CPU性能严重下降,此时我们提出了缓存一致性协议(MESI):

    • 缓存一致性协议提供了一种高效的内存数据管理方案,它只会对单个缓存行(缓存行是缓存中数据存储的基本单元)的数据进行加锁,不会影响到内存中其他数据的读写。
    • cache line,缓存行是为了简化与RAM之间的通信,高速缓存控制器是针对数据块,而不是字节进行操作的。从程序设计的角度讲,高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块。

    因此,我们引入了缓存一致性协(MESI)议来对内存数据的读写进行管理。


    3.是什么(What)

    3.1数据在缓存中的四种状态:

    MESI的英文全程为:Modified Exclusive Shared Or Invalid,有四种状态,分别对应其英文单词,如下:

    状态具体描述状态所在缓存对应的CPU是否独占数据cache line 是否是最新数据对数据的写入
    M: 被修改(Modified)该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的数据需要在未来的某个时间点(允许其它CPU读取主存中相应数据之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。可以
    E: 独享的(Exclusive)该数据只被缓存在该CPU的缓存行中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。可以
    S: 共享的(Shared)该状态意味着该数据可能被多个CPU缓存读取,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改自己的缓存行中的数据时,数据对应的缓存行状态变成Modified状态,其它CPU中该缓存行变成无效状态(Invalid)。可以
    I: 无效的(Invalid)该缓存是无效的(可能有其它CPU修改了该缓存行)。否 (无数据)无数据无数据,不可以

    3.2MESI的六种消息(请求消息和响应消息)

    cpu接收响应消息的顺序决定了其他cpu感知到的当前线程的执行顺序

    1. read:(请求消息)
      “read” 消息用来获取指定物理地址上的 cache line(如果在缓存中,从缓存中取,不在缓存中则从内存中取) 数据。
    2. read response:(响应消息)
      "read response"消息包含先前“read”消息请求的数据。此“read response”消息可能来自内存或其他CPU的缓存。
    3. invalidate:(请求消息)
      Invalidate。该消息将其他 CPU cache 中指定的数据设置为失效。该消息携带物理地址,其他 CPU cache 在收到该消息后,必须进行匹配,发现在自己的 cache line 中有该地址的数据,那么就将其从 cahe line 中移除,并响应 Invalidate Acknowledge 回应。
    4. invalidate acknowledge:(响应消息)
      该消息用做回应 Invalidate 消息。
    5. read invalidate:(请求消息)
      该消息中带有物理地址,用来说明想要读取哪一个 cache line 中的数据,同时指示其他缓存删除数据。可以看作是 read + Invalidate 消息的组合,“read invalidate”消息需要“read response”和一组“invalidate acknowledge”消息作为应答。
    6. writeback:
      “writeback”消息包含要写回内存的地址和数据(也可能是沿途“窥探”到其他cpu的缓存中)。该消息用在 modified 状态的 cache line 被置换时发出,用来将最新的数据写回 memory 或其他下一级 cache 中。

    3.3MESI四种状态通过六种消息进行转换(利用3.1与3.2章节的知识点)

    在这里插入图片描述
    上图的转换详细说明:

    • a
      cache 通过 writeback 将数据回写到 memory 或者下一级 cache 中。这时候状态由 modified 变成了 exclusive 。
    • b
      cpu 直接将数据写入 cache line ,导致状态变为了 modified 。
    • c
      CPU 收到一个 read invalidate 消息,该消息中带有物理地址,用来说明想要读取哪一个 cache line 中的数据,同时指示其他缓存删除数据。此时 CPU 必须将对应 cache line 设置成 invalid 状态 , 并且响应一个 read response 消息和 invalidate acknowledge 消息。
    • d
      CPU 需要执行一个原子的 readmodify-write 操作,并且其 cache 中没有缓存数据。这时候 CPU 就会在总线上发送一个 read invalidate 消息来请求数据,并试图独占该数据。CPU 可以通过收到的 read response 消息获取到数据,并等待所有的 invalidate acknowledge 消息,然后将状态设置为 modifie 。
    • e
      CPU需要执行一个原子的readmodify-write操作,并且其local cache中有read only的缓存数据(cacheline处于shared状态),这时候,CPU就会在总线上发送一个invalidate请求其他cpu清空自己的local copy,以便完成其独自霸占对该数据的所有权的梦想。同样的,该cpu必须收集所有其他cpu发来的invalidate acknowledge之后才能更改状态为 modified。
    • f
      在本cpu独自享受独占数据的时候,其他的cpu发起read请求,希望获取数据,这时候,本cpu必须以其local cacheline的数据回应,并以read response回应之前总线上的read请求。这时候,本cpu失去了独占权,该cacheline状态从Modified状态变成shared状态(有可能也会进行写回的动作)。
    • g
      这个迁移和f类似,只不过开始cacheline的状态是exclusive,cacheline和memory的数据都是最新的,不存在写回的问题。总线上的操作也是在收到read请求之后,以read response回应。
    • h
      需要发送invalidate以通知其他cpu相应数据将要失效,并等待其他cpu的回应消息(invalidate acknowledge)。
    • i
      其他的CPU进行一个原子的read-modify-write操作,但是,数据在本cpu的cacheline中,因此,其他的那个CPU会发送read invalidate,请求对该数据以及独占权。本cpu回送read response”和“invalidate acknowledge”,一方面把数据转移到其他cpu的cache中,另外一方面,清空自己的cacheline。
    • j
      cpu想要进行write的操作但是数据不在local cache中,因此,该cpu首先发送了read invalidate启动了一次总线transaction。在收到read response回应拿到数据,并且收集所有其他cpu发来的invalidate acknowledge之后(确保其他cpu没有local copy),完成整个bus transaction。当write操作完成之后,该cacheline的状态会从Exclusive状态迁移到Modified状态。
    • k
      本CPU执行读操作,发现local cache没有数据,因此通过read发起一次bus transaction,来自其他的cpu local cache或者memory会通过read response回应,从而将该 cache line 从Invalid状态迁移到shared状态。
    • l
      当cache line处于shared状态的时候,说明在多个cpu的local cache中存在副本,因此,这些cacheline中的数据都是read only的,一旦其中一个cpu想要执行数据写入的动作,必须先通过invalidate获取该数据的独占权,而其他的CPU会以invalidate acknowledge回应,清空数据并将其cacheline从shared状态修改成invalid状态。

    该篇已完结
    后续将在写一篇博文介绍在Java语言中某个关键字的底层是如何用到MESI协议以及内存屏障的。
    author:YuShiwen
    更多相关内容
  • 5-3JMM-CPU缓存一致性协议MESI.mp4
  • MESI MESI是Modified、Exclusive、Shared、Invalid四个单词的首字母缩写,表示缓存行的4种状态。 Modified 缓存行被对应的CPU核心修改之后就会处于Modified状态,并且保证该缓存行不会出现在任何其他CPU的缓存中。...


    故事还得从一个矛盾说起。

    摩尔定律告诉我们:大约每18个月会将芯片的性能提高一倍。芯片的这种飞速发展直接导致了芯片的指令执行速度与内存读取速度之间的巨大鸿沟。

    举个例子,CPU在1纳秒之内可以执行几十条指令,但是从内存中读取一条数据就需要花费几十纳秒。这种数量级的差异便是计算机中的一个主要矛盾:

    CPU日益增长的对数据快速读取的需要和I/O设备读取速度不平衡不充分的发展之间的矛盾

    而CPU运行所需要的指令和数据都存储在低速的内存中,人们无法容忍让CPU这样宝贵的高速设备进行漫长的等待。

    计算机科学领域的任何问题都可以通过增加一个中间层来解决。所以需要一个比内存更快的存取设备做缓冲,尽量做到和CPU一样快,这样就不需要每次都从低速的内存中获取数据了。

    于是引入了高速缓存。

    1. 高速缓存

    img

    我们已经知道为什么需要高速缓存了。那么什么是高速缓存?它为什么就比内存快?既然这么快,为什么不直接当成内存用?

    别急,我一点点解释。

    1.1. 什么是高速缓存Cache

    我们最熟悉的内存是一种动态随机访问存储器(Dynamic RAM,DRAM),存储器中每个存储单元由配对出现的晶体管和电容器构成,每隔一段时间,固定要对DRAM刷新充电一次,否则内部的数据就会消失。

    而高速缓存是一种静态随机访问存储器(Static RAM,SRAM),不需要刷新电路就能保存它内部存储的数据,这就是静态的含义,因此SRAM的存储性能非常高!工作速度在纳秒级别,勉强能跟得上CPU的运算速度。

    但是SRAM的缺点就是集成度低,相同容量的内存可以设计成较小的体积,但是SRAM却需要更大的体积;而且,SRAM这玩意儿巨贵!这就是不能直接把它当内存用的原因。

    越靠近CPU核心地带的设备越需要强悍的性能,可是容量如果太小又帮不上太大的忙。如果一个中间层(一层高速缓存)不能高效解决问题,那就多来几个中间层。目前CPU的解决思路一般是以量取胜,比如同时设置L1L2L3三级缓存。

    在缓存容量上,通常是内存 > L3 > L2 > L1,容量越小速度越快。其中L1L2是由每个CPU核心独享的,L3缓存是由所有CPU核心共享的。CPU的架构见下图:

    现代CPU架构

    需要特别说明的是,L1缓存又分为了L1d数据缓存(L1 Data)和L1i指令缓存(L1 Instruct),上图为了完整性一并画出了,本文中的高速缓存一律指数据缓存。

    为了接下来方便讲解,我们把三级缓存模型简化为一级缓存模型,毕竟道理都是相通的嘛。看一下简化之后的图。

    简化的高速缓存架构

    1.2. 缓存行

    说完了什么是Cache,接下来我们来看看Cache里装的到底是什么?

    这不是废话嘛,肯定装的是数据啊。没错,是从内存中获取到的数据,但是数据的单位呢?CPU每次只把需要的数据从内存中读取到Cache就行了吗?肯定不是,我们想一下,只把需要的一个数据从内存中读到Cache,CPU再从Cache中继续读这个数据进行处理,Cache的存在完全就是多此一举,还不如直接从内存读数据呢。

    所以要想让Cache充分发挥作用,必须让它做点“多余”的事情。因此从内存中获取数据的时候,我们把包含目标数据的一整块内存数据都放入Cache中。别小看这个动作,它有个科学的解释,叫做空间局部性

    位置相邻的数据常常会在相近的时间内被访问

    根据空间局部性原理,如果目标数据相邻的数据被访问,CPU就不需要再从内存中获取了,这种直接从Cache中获取到目标数据的行为叫做“缓存命中”,极大地提高了CPU的工作效率。如果Cache里边没有,就称为Cache Miss,CPU需要再等待几十个指令周期从内存中把这一整块内存数据读入Cache。

    给存储“一整块内存数据”的地方起个名字,叫「缓存行」(Cache Line)。

    Cache是由缓存行组成的,缓存行是CPU高速缓存和内存交互的最小单元。在X86架构中,缓存行的大小是64个字节,大小和CPU具体型号有关。本文只关注缓存行的抽象概念,不涉及具体的缓存行大小。


    接下来,终于要进入本文的正式部分了。

    img

    我一直认为,计算机的演进就是一部在挖坑和填坑之间反复横跳的发展史。对这一点的理解会随着本文的后续讲述逐渐加深。比如高速缓存Cache很好地解决了CPU与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,我来举个例子。

    2. 伪共享问题

    我们到目前为止说的都是CPU从Cache中read数据,但是总得有write的时候吧。既然有了Cache,肯定就得先把值write到Cache中,再更新到内存里啊。那么,问题来了。

    2.1. 什么是伪共享

    伪共享问题

    数据XYZ同处于一个缓存行内,Core0Core1同时加载了该缓存行到Cache中,此时Core0修改了该缓存行中的XX1,如果此时Core1也想修改YY1该怎么办呢?

    由于缓存行是Cache和内存之间交互的最小单元,所以Core0根本不知道Core1修改的是缓存中的Y还是X,所以为了防止造成并发问题,最好的办法就是让Core1中的该缓存行失效,重新加载。这就是伪共享问题。

    伪共享问题的定义:当多核心修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享

    2.2. 解决伪共享

    既然问题是由多个变量共享一个缓存行导致的,那就让Y变量独享一个缓存行就好了。

    缓存行填充

    最简单的方法就是通过代码手动进行字节填充,拿早期的LinkedTransferQueue中的部分源码举个例子,注意看注释内容:

    static final class PaddedAtomicReference<T> extends AtomicReference<T> {
        // 追加15个对象引用,一个对象引用占据4个字节
        // 加上继承自父类的value,共64字节,正好占一个缓存行
        Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
        PaddedAtomicReference(T r) {
            super(r);
        }
    }
    
    //父类
    public class AtomicReference<V> implements java.io.Serializable {
        private volatile V value;
        public AtomicReference(V initialValue) {
            value = initialValue;
        }
    }
    

    此外,JDK 8开始,提供了一个sun.misc.Contended注解来解决伪共享问题,加上这个注解的类会自动补齐缓存行。

    稍微扯远了一些,我们回到上方的动图。Core0修改了缓存行中的X,我们说当前最合适的处理办法就是让Core1中的缓存行失效,否则就会出现缓存一致性问题。伪共享问题其实就是解决缓存一致性问题的副作用。只不过本文中我单独把这个问题列了出来。

    为了解决缓存一致性问题,CPU天然支持了总线锁的功能。

    3. 总线锁

    顾名思义就是,锁住Bus总线。通过处理器发出lock指令,总线接受到指令后,其他处理器的请求就会被阻塞,直到此处理器执行完成。这样,处理器就可以独占共享内存的使用。

    但是,总线锁有一个非常大的缺点,一旦某个处理器获取总线锁,其他处理器都只能阻塞等待,多处理器的优势就无法发挥

    于是,经过发展、优化,又产生了缓存锁。

    4. 缓存锁

    缓存锁:不需锁定总线,维护本处理器内部缓存和其他处理器缓存的一致性。相比总线锁,会提高cpu利用率。

    但是缓存锁也不是万能,有些场景和情况依然必须通过总线锁才能完成。

    缓存锁其实是一种实现的效果,它是通过缓存一致性协议来实现的,可能有的读者也听说过Snoopy嗅探协议,我举个例子帮助大家理解这三个概念。

    总线锁&缓存锁&嗅探协议

    假如村里有一个单人公厕,一条蜿蜒大道与公厕相连,大道旁边住着A、B、C、D四个人,每个人要上厕所必须经过主干道。

    我们再设置一点前提,假设每个人都不想到了厕所门口的时候才知道厕所已经被人占用了。

    为了合理使用厕所,保证每次只有一个人进入厕所,并且不会出现其他人在厕所门口等待的情况,ABCD四个人聚在一起开会讨论,协商出了一条约定。

    当有人去上厕所的时候,其他人在家老实呆着,不要去上厕所!

    四个人纷纷拍着自己大腿叫绝。他们商议出来了一个听起来确实能解决问题,但是实际上内容非常空洞的一个协议。

    因为他们不知道现在有谁正在占用厕所,更不知道谁正在前往厕所的路上。

    其中A灵机一动,想出了一个办法。可以在每家和主干道的岔路口设置一个监测设备,当有人上厕所经过岔路口的时候监测设备就提醒其他三个人已经有人去厕所了,老实在家等着吧。

    如此一来,就达成了一种给厕所添加锁的一种效果,这种效果就相当于上文提到的缓存锁。4人商议出来的协议就相当于缓存一致性协议,A提出来的方法实现了协议,相当于Snoopy嗅探。

    如果大家读到这里在思考例子中的厕所究竟表示内存还是缓存行,我劝大家赶紧止住。在生活中找到和计算机科学中非常贴切的例子是非常非常困难的。这个例子只是简单说明一下缓存锁和锁存一致性协议以及Snoopy嗅探协议之间的关系罢了,不要深究!

    自然,缓存一致性协议就是我们接下来的主角了。

    5. 缓存一致性协议

    每个处理器共享同一个主内存,并且都有自己的高速缓存。如果多个处理器都对同一块主内存区域进行更改,将导致各自的的缓存数据不一致。那同步到主内存时该以谁的缓存数据为准呢?

    缓存一致性协议就是为了解决这个问题提出的,这类协议有MSIMESIMOSI等。

    我们以应用最广泛的MESI为例进行介绍。

    5.1. MESI

    MESIModifiedExclusiveSharedInvalid四个单词的首字母缩写,表示缓存行的4种状态。

    • Modified

    缓存行被对应的CPU核心修改之后就会处于Modified状态,并且保证该缓存行不会出现在任何其他CPU的缓存中。即使有,也是Invalid状态,需要从内存或其他Cache中重新读取。

    因此,处于Modified状态的缓存行可以说是被相应CPU核心独占的。由于该缓存行拥有该数据的唯一最新副本,因此该缓存行最终负责将其写回内存或将其传递给其他CPU。

    Modified的缓存行

    • Exclusive

    Exclusive状态就非常好理解了,意味着独占、排他,和Modified状态非常类似。唯一不同的一点就是这个缓存行还没有被CPU核心修改,这也说明内存中的内容依然是最新的。即便如此,一旦某个缓存行处于该状态,就意味着其他CPU核心不能拥有该缓存行的副本。

    Exclusive缓存行

    • Shared

    处于Shared状态的缓存行意味着同时出现在了一个或多个CPU Cache中,且多个CPU Cache的缓存行和内存中的数据一致。CPU核心不能在未与其他核心“协商”的情况下,修改其Cache中的该缓存行。至于什么是“协商”,下文会讲到。

    Shared缓存行

    • Invalid

    处于Invalid状态的缓存行不包含任何数据,只是被打上了Invalid状态的标签而已。其他CPU修改了缓存行,就会导致本CPU中的该缓存行失效为Invalid状态。当有新数据被放入Cache中时,会被优先放入Invalid状态的缓存行中,避免置换出其他有用的缓存导致Cache Miss。

    Invalid缓存行

    以上4种状态之间的跃迁离不开各个CPU核心之间的协作,比如某个数据被同时缓存在多个CPU核心的Cache中,此时这些缓存行的状态是Shared,假如Core0对缓存行做了write操作,为了避免缓存数据的不一致性,其他CPU核心需要将对应的缓存行状态设置为Invalid状态。那么其他CPU核心是怎么知道Core0修改了缓存行呢?换个问法,Core0怎么让其他核心知道自己修改了缓存行呢?

    人有人言,兽有兽语。CPU核心之间的沟通也有自己的一套“黑话”,称为缓存一致性消息

    5.2. CPU之间的“黑话”

    消息分为请求和响应两类。

    处理器在进行数据读写的时候会往总线(Bus)中发请求消息,同时每个处理器核心还会嗅探(Snoop)总线中由其他处理器发出的请求消息并在一定条件下往总线中回复响应消息。

    • Read

    Read消息表示要读取某个缓存行,同时会携带目标缓存行对应的物理地址。

    • Read Response

    是对Read消息的反馈,反馈的内容就是发送Read消息的CPU核心请求的目标缓存行。Read Response可能来自于内存,也可能来自其他CPU核心。

    比如,如果被请求的目标缓存行不存在于任何CPU Cache中,那么只能从内存中获取;如果被请求的目标缓存行恰好被其中一个CPU修改,此时该缓存行为Modified状态,意味着该缓存行目前是最新数据,那么理应让其他同样需要该缓存行的CPU核心获取到该最新数据,更进一步,自然理应由该CPU核心把该缓存行的内容反馈给发出Read消息的CPU核心。

    Read Response

    • Invalidate

    Invalidate的含义是使某个缓存行失效,拥有该缓存行的其他CPU核心需要删除该缓存行中的数据,并对发出Invalidate消息的核心做出反馈。

    • Invalidate Acknowledge

    这就是上面提到的Invalidate消息的反馈,意味着发出此消息的CPU核心已经将Invalidate消息的目标缓存行中的数据清除。

    Invalidate

    如果有多个CPU同时发出Invalidate消息怎么办?答案是总线裁决。首先占用消息总线的CPU核心获胜,其他核心只能乖乖清空自己的缓存行,并向其发出Invalidate Acknowledge反馈。

    • Read Invalidate

    Read Invalidate相当于Read + Invalidate,既要读取某个缓存行信息,又要让属于其他CPU核心的此缓存行失效。同样,Read Invalidate也需要收到反馈,只不过此反馈既包含1条Read Response,又包含多条(如果其他CPU核心也拥有目标缓存行的话)Invalidate Acknowledge

    • Writeback

    Writeback消息包含要写回内存的地址和数据,通常指的是Modified状态的数据,这样Cache就可以根据需要弹出处于Modified状态的缓存行,以便为其他数据腾出空间。

    这俩消息很简单,就不画图浪费你们的流量了。。。

    5.3. MESI状态跃迁示例

    CPU之间通过缓存一致性消息的传递,才有了缓存行在MESI四种状态之间的跃迁。

    img

    如上图,每两个状态之间都可能会发生状态越迁,是不是感觉很复杂?

    如果之前的内容我给你解释地很清楚的话,就很容易想明白每个状态之间的跃迁场景了。为了不影响接下来的讲解,我把每种场景解释放在了文章最后(见附录1),需要的读者读完文章之后可以翻阅一下(即使不看也不会影响接下来的阅读哦)。

    还有一个在线的网站可以帮助你更好地理解MESI协议(见附录2),你可以站在CPU的角度发出指令,网站以动态方式展示缓存行的状态变换,强烈建议阅读完文章之后大家试一下。

    截至目前,文章都是围绕Cache展开的,高速缓存的引入极大地提高了计算机的整体运行效率。但在某些特殊情况下,CPU的性能表现却是非常糟糕。

    6. 不能让CPU闲着

    考虑这么一个场景,CPU 0CPU 1 同时拥有某个缓存行,两个缓存行都处于Shared状态,CPU 0想对自己的缓存行执行write操作,必须先发送Invalidate消息让CPU 1中的缓存行失效。如下图所示:

    CPU闲下来了

    由于CPU 0必须等到CPU 1反馈了Invalidate Acknowledge之后才能确保自己可以操作缓存行,所以从发出Invalidate直到收到Invalidate Acknowledge的这段时间,CPU 0一直处于闲置状态。

    CPU是何等宝贵的资源,让它闲着是不可能的,绝对不可能的!

    硬件工程师为了解决这个问题,引入了Store Buffers

    6.1. 引入Store Buffers

    img

    工程师在CPU和Cache之间添加了一个中间层——Store Buffer。当CPU 0想执行write指令时,先把想要write的值写入到Store Buffer中,然后再继续执行其他任务,无需傻傻地等待CPU 1。直到CPU 1传回反馈之后,CPU 0再将Store Buffer中的最新值写入到缓存行中。

    计算机的发展就是不断挖坑、填坑的过程。Store Buffers的引入解决了CPU闲置的问题,如果事情发展到现在就完美了该有多好,然而又引出了3个新问题。

    6.2. Store Buffers引起的问题1

    img

    看一下上图左侧的代码,其中ab的初始值为0,在大多数时候,最后的断言会为True。

    之所以说大多数时候,因为左侧的代码在某个场景下可能会出现不符合我们预期的情况(断言为False)。如果要证明,我们只需要举出一个反例即可,因此我们进一步假设含有变量a的缓存行已经存在于CPU 1的Cache中,含有变量b的缓存行已经存在于CPU 0的Cache中。

    下面我们根据引入Store Buffers之后的CPU架构来执行上面的代码,CPU 0 和CPU 1 的操作顺序如下图所示:

    img

    1. CPU 0 执行 a = 1;
    2. CPU 0 首先从自己的Cache中查找a,发现没有;
    3. CPU 0 发送Read Invalidate消息来获取含有a的缓存行,并通知其他CPU,“老子要用,你们都给我销毁!”;
    4. CPU 0 在Store Buffer中记录下自己想赋给a的值,即a = 1。此时CPU 0并不会阻塞,会继续向下执行,但是在时间线的发展上,紧接着是CPU 1的操作,见第5步;
    5. CPU 1收到来自CPU 0的Read Invalidate消息,于是把自己包含a的缓存行返回给CPU 0,并且把自己的缓存行状态设置为Invalid
    6. CPU 0 开始执行b = a + 1
    7. CPU 0 收到来自CPU 1 的缓存行,并放到自己的缓存行中,其中a的值为0;此时CPU 0 的缓存行中的ab的状态都是Exclusive,因为这些缓存行都由CPU 0 独占;
    8. CPU 0 从缓存行中读取a,此时值为0;
    9. CPU 0 根据自己之前在Store Buffer中存放的a = 1来更新自己Cache中的a,设置为1;
    10. CPU 0 在第8步获取的a值的基础上+ 1(这一步不需要重新从缓存行中读取数据,因为读取的动作在第8步中已经做了),并更新自己缓存行中的b;此时包含b的缓存行的状态为Modified
    11. CPU 0 执行断言操作,发现断言为False。

    再给大家补充一个动图:

    img

    这确实是一件非常违反直觉的事情,我们本来以为CPU就是完全按照代码的顺序执行的(至少最终结果应该表现地像CPU是完全按照代码的顺序执行的一样),我们认为b的最终结果就应该是2。

    出现这个问题的原因是CPU 0 运行过程中出现了a的两份数据拷贝,一份是在Store Buffer中,一份是在Cache中。为了不让软件工程师疯掉,继续保持软件代码的直观性,硬件工程师又引入了Store Forwarding来解决这个问题。

    6.3. 引入Store Forwarding

    每个CPU在执行数据加载操作时都直接使用Store Buffer中的内容,而无需从Cache中获取,如下图所示。

    img

    请注意上图和原来图片的区别,上图中的Store Buffer中的数据可以直接被CPU读取。对应到上面的CPU 0 的操作步骤,就是第8步直接从Store Buffer中读取最新的a,而不是从Cache中读取,这样整个程序的最终断言结果就是True!

    总之,引发的第1个问题,硬件工程师通过引入Store Forwarding为我们解决了。

    6.4. Store Buffers引起的问题2

    在多个CPU并发处理情况下也可能会导致代码运行出现问题。

    同样也是举一个极端一点的例子。见下图左侧的代码,其中ab的初始值为0,进一步假设含有变量a的缓存行已经存在于CPU 1的Cache中,含有变量b的缓存行已经存在于CPU 0的Cache中。CPU 0 执行foo方法,CPU 1 执行bar方法。正常情况下,bar方法中的断言结果应该为True。

    img

    然而,我们按照下图中的执行顺序操作一遍之后,断言却是False!

    img

    1. CPU 0 执行a = 1,首先从自己的Cache查找啊,发现没有;
    2. CPU 0 将a的新值1写入到自己的Store Buffer中;
    3. CPU 0 发送Read Invalidate消息(从发出这个消息到CPU 1 接收到,期间又运行了非常多的步骤,见下方GIF图);
    4. CPU 1 执行while (b == 0) continue,发现b不在自己的Cache中,于是发送Read消息;
    5. CPU 0 执行b = 1,由于b已经存在于自己的Cache中了,所以直接将Cache中的b修改为1,并修改包含b的缓存行的状态为Modified
    6. CPU 0 收到来自第4步CPU 1 发出的Read消息,由于当前自己拥有的b是最新版本的,所以CPU 0 把含有b的缓存行返回给CPU 1,同时修改自己的缓存行状态为Shared
    7. CPU 1 收到来自CPU 0 的b缓存行数据,放到自己的Cache中,并设置为Shared状态;
    8. CPU 1 结束while循环,因为此时的b值已经是1了;
    9. CPU 1 执行assert(a == 1),由于Cache中的a值是0(此时还没收到来自CPU 0 的Read Invalidate消息,因此CPU 1 有理由认为自己的数据就是合法的),因此断言结果为False;
    10. CPU 1 终于收到来自CPU 0 的Read Invalidate消息了,虽然已经晚了(当然CPU压根不知道自己的这个消息接收的时机并不合适),但是还得按照约定把自己的a设置为Invalid状态,并且给CPU 0 发送Invalidate Acknowledge以及Read Response反馈;
    11. CPU 0 收到CPU 1 的反馈,利用Store Buffer中的值更新a

    至此,流程全部结束,再送给大家一个GIF。

    img

    我们分析一下结果不符合我们预期的原因。

    Store Buffer的加入导致Read Invalidate的发送是一个异步操作,异步可能导致的结果就是CPU 1 接收到CPU 0 的Read Invalidate消息太晚了,导致在Cache中的实际操作顺序是b = 1,最后才是a = 1,就好像写操作被重排序了一样,这就是CPU的乱序执行

    如果没有看懂上面一段就再看一下图片中的CPU 0 Cache的时间线演化。

    很多人看到「乱序执行」唯恐避之不及,它当初可是为了提高CPU的工作效率而诞生的,而且在大多数情况下并不会导致什么错误,只是在多处理器(smp)并发执行的时候可能会出现问题,于是便有了下文。

    也就是说,如果在第5步CPU 0 修改b之前,我们强制让CPU 0先完成对a的修改就可以了。

    为了解决这样的问题,CPU提供了一些操作指令,来帮助我们避免这样的问题,就是大名鼎鼎的内存屏障(Memory Barrier,mb)。

    6.5. 内存屏障

    我们稍微修改一下foo方法,在b = 1之前添加一条内存屏障指令smp_mb()

    内存屏障

    多说一点,smp的全称是Symmetrical Multi-Processing(对称多处理)技术,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。

    为什么要特意加上smp呢?因为即便现代处理器会乱序执行,但在单个CPU上,指令能通过指令队列顺序获取指令并执行,结果利用队列顺序返回寄存器,这使得程序执行时所有的内存访问操作看起来像是按程序代码编写的顺序执行的,因此没必要使用内存屏障(前提是不考虑编译器的优化的情况)。

    内存屏障听起来很高大上,但是对于软件开发者而言其实非常简单,总结一句话就是:

    在内存屏障语句之后的所有针对Cache的写操作开始之前,必须先把Store Buffer中的数据全部刷新到Cache中。

    如果你看明白了我上面说的Store Buffer,这句话是不是贼好懂呢?换个角度再翻译一下,就是一定要保证存到Store Buffer中的数据有序地刷新到Cache中,这样就可以避免发生指令重排序了。

    如何保证有序呢?

    最简单的方式就是让CPU傻等,CPU 0 在执行第5步之前必须等着CPU 1给出反馈,直到清空自己的Store Buffer,然后才能继续向下执行。

    啥?又让CPU闲着?一切让CPU闲置的方法都是馊主意!

    还有一个办法就是让数据在Store Buffer中排队,谁先进入就必须先刷新谁,后边的必须等着!

    这样一来,本来可以直接写入Cache的操作(比如待操作的数据已经存在于自己的Cache中了)也必须先存到Store Buffer,然后依序进行刷新

    img

    应用内存屏障之后的操作步骤就不给大家再写一遍了,相信大家能够想清楚。

    总之,引发的第2个问题,我们通过使用内存屏障解决了。

    6.6. Store Buffers引起的问题3

    Store Buffer的容量通常很小,如果CPU此时需要对多个数据执行write操作,碰巧这些数据都不在该CPU的Cache中,那么该CPU只能发送对应的Read Invalidate指令了,同时新数据写入Store Buffer,非常容易导致Store Buffer空间被占满。

    一旦Store Buffer被占满,CPU就只能干等着目标CPU完成Read Invalidate操作,并且返给自己Invalidate Acknowledge,当前CPU才能逐步将Store Buffer中的值刷新到Cache,腾出空间,然后继续执行。

    CPU又又又闲下来了!所以我们肯定又得找个办法来解决这个问题。

    出现这个问题的主要原因在于Invalidate Acknowledge的反馈速度太慢了!

    因为CPU太老实了,它只有在确认自己的缓存行被设置为Invalid状态之后才会发送Invalidate Acknowledge。如果Cache的其他操作太频繁,“设置缓存行为Invalid状态”这个动作本身都会被延迟执行,更何况Invalidate Acknowledge的反馈动作呢,得等到猴年马月啊!

    上面的GIF图中为了表现出「反馈慢」这种情况,我特意把Invalidate消息的发送速度设置地很慢,其实消息地发送速度非常快,只是CPU处理Invalidate消息的速度太慢了而已,望悉知。

    如果不想等,想直接获取操作结果,你想到了什么?

    没错,是异步!

    实现方式就是再加一层消息队列——Invalidate Queues

    6.7. 引入Invalidate Queues

    如下图,我们的硬件架构又升级了。在每个CPU的Cache之上,又设置了一个Invalidate Queue

    这样一来,收到Invalidate消息的CPU核心会把Invalidate消息直接存储到Invalidate Queue中,然后立即返回Invalidate Acknowledge,不需要再等着缓存行被实际设置成Invalid状态再发送,极大地提高了反馈速度。

    你可能会问,万一Invalidate Queues中的Invalidate消息最终执行失败,但是Acknowledge消息已经返回了,这该怎么办呢?

    好问题!答案是,我不知道。我们就当作硬件工程师绝对不会留下这个bug就是了。

    Invalidate Queue

    Invalidate Queue填了Store Buffer容量太小的坑,接下来看看它自己又挖了什么坑吧。

    6.7.1. Invalidate Queue引发的问题

    这个坑比较严重,很有可能直接干翻缓存屏障,再次引发乱序执行的问题。

    老样子,还是先准备一下翻车的环境。如下图,我们假设变量aCPU 0CPU 1 共享,为Shared状态;变量bCPU 0 独占,为Exclusive状态;CPU 0CPU 1 分别执行foobar方法。

    img

    我们按照下图中的执行顺序操作一遍。

    img

    1. CPU 0 执行a = 1,因为CPU 0 的Cache中已经有a了,状态为Shared,因此不能直接修改,需要发送Invalidate(不是Read Invalidate,因为自己有a)消息使其他缓存行失效;
    2. CPU 0 把试图修改的a的最新值1放入Store Buffer
    3. CPU 0 发送Invalidate消息;
    4. CPU 1 执行while(b == 0) continue;发现b不在自己的Cache中,于是发送Read消息来获取b
    5. CPU 1 收到来自CPU 0 的Invalidate消息,把该消息放入Invalidate Queue中(并没有立即让a失效),等候处理,然后立刻返回Anknowledge
    6. CPU 0 收到Acknowledge消息,认为CPU 1 已经把a值设置为Invalid了,于是放心地把Store Buffer中的数据刷新到自己的Cache中,此时CPU 0 Cache中的a1,状态为Modified;然后就可以直接越过smp_mb()内存屏障,因为现在Store Buffer中的数据已经空了,满足内存屏障的约束条件。
    7. CPU 0 执行b = 1,因为其独占了b,所以可以直接在Cache中修改b的值,此时b缓存行的状态为Modified
    8. CPU 0 收到来自CPU 1 的Read消息,将修改之后的b缓存行返回,并修改自己Cache中的b缓存行的状态为Shared
    9. CPU 1 收到包含b的缓存行数据,放在自己的Cache中,此时CPU 1 的Cache同时拥有了ab
    10. CPU 1 结束执行while(b == 0) continue;因为此时CPU 1 读到的b已经是1了;
    11. CPU 1 开始执行assert(a == 1),CPU 1 从自己的Cache读到a0,断言为False。
    12. CPU 1 开始处理Invalidate Queue队列,令Cache中的a失效,但是为时已晚!

    至此流程全部结束,再上个GIF。

    img

    问题很明显出在第11步,这就是臭名昭著著名的可见性问题CPU 0 修改了a的值,CPU 1 却不知道或者说知道的太晚!如果在第11步读取a的值之前就赶紧刷新Invalidate Queue中的消息,让a失效就好了,这样CPU 1 就不得不重新Read,得到的结果自然就是1了。

    原因搞明白了,怎么解决呢?内存屏障再一次闪亮登场!

    6.7.2. 内存屏障的另一个功能

    上文已经解释了内存屏障的功能,再抄一遍加深印象:

    1.在内存屏障语句之后的所有针对Cache的写操作开始之前,必须先把Store Buffer中的数据全部刷新到Cache中。

    其实内存屏障还有另一个功能:

    2.在内存屏障语句之后的所有针对Cache的读操作开始之前,必须先把Invalidate Queue中的数据全部作用到Cache中。

    使用缓存屏障之后的代码就变成了这个样子:

    img

    bar方法在assert之前添加了内存屏障,意味着在获取a的值之前,所有在Invalidate Queue中的Invalidate消息必须作用到Cache中。

    至此,我们再次用内存屏障解决了可见性问题。

    问题还没有结束…

    7. 读内存屏障 & 写内存屏障

    内存屏障有两个功能,在foo方法中实际发挥作用的是功能1,功能2并没有派上用场;同理,在bar方法中实际发挥作用的是功能2,功能1并没有派上用场。于是很多不同型号的CPU架构(不是所有)将内存屏障功能分为了读内存屏障写内存屏障,具体如下。

    • smp_mb(全内存屏障,包含读和写全部功能)
    • smp_rmb(read memory barrier,仅包含功能2)
    • smp_wmb(write memory barrier,仅包含功能1)

    上文已经解释地挺清楚了,因此就不再重复介绍smp_rmbsmp_wmb的作用了。直接看修改之后的代码吧。

    img

    8. 总结

    计算机的演进就是一部反复挖坑、填坑的发展史。

    为了解决内存和CPU之间速度差异过大的问题,引入了高速缓存Cache,结果导致了缓存一致性问题;

    为了达到缓存一致的效果,CPU之间需要沟通啊,于是又设计了各种消息传递,结果消息传递导致了CPU的偶尔闲置;

    为了不让CPU停下来,硬件工程师加入了写缓冲——Store Buffer,这一下子带来了3个问题!

    第一个问题比较简单,通过引入Store Forwarding解决了;

    第二个问题是操作重排序问题,我们又引入了内存屏障的第一个大招;

    第三个问题是由于Store Buffer空间限制导致CPU又闲下来了,于是又设计了Invalidate Queues,然后又导致了乱序执行和可见性问题;

    通过使用内存屏障的全部大招终于解决了乱序执行和可见性问题,又引出了大招伤害性过强的问题,于是又拆分成了更细粒度的读屏障写屏障。。。。。。

    9. 后记

    问题其实还有很多,比如各种不同CPU结构是怎么实现内存屏障的?可想而知,每个人都有每个人的想法,不同CPU的实现指定也不一样,甚至可能拆分地更细或者更粗。不过这些与大部分软件开发者(包括我)都没有什么关系了,更多问题还是留给芯片开发者以及操作系统开发者吧。

    不要纠结于具体的实现细节,把文章中的大部分搞懂已经能帮助我们理解很多问题了。如果还想知道的更多,可以看看附录3中的第1篇文章。

    10. 附录1——MESI跃迁场景解析

    img

    (a):通过Writeback消息把被修改过的缓存行刷新至内存,但是CPU的Cache仍然保留该数据;

    (b):CPU修改了只保存在当前Cache中的缓存行;

    ©:CPU收到了Read Invalidate消息,该消息的目标正是当前处于M状态(被修改了)的缓存行。CPU不得不使自己的缓存行失效,并把该缓存行数据携同Read Response以及Invalidate Acknowledge消息返回;

    (d):CPU执行了一个原子操作,该操作包含读和写两个子操作,并且不可分割。CPU首先发送Read Invalidate消息,收到Read Response消息之后立刻对数据进行更新,至此便完成了该原子操作。

    (e):和d大致相同。CPU执行了一个原子操作,该操作包含读和写两个子操作,并且不可分割。CPU首先发送Invalidate消息,收到Invalidate Acknowledge消息之后立刻对数据进行更新,至此便完成了该原子操作。

    (f):当前CPU修改了一个缓存行数据,接着其他CPU核心对当前CPU的该缓存行发出Read消息,当前CPU将该缓存行数据随Read Response消息反馈给其他CPU核心。至于该过程会不会涉及到缓存行数据刷新到内存,那就不一定了。

    (g):当前CPU独占了一个未经修改的缓存行,其他CPU对当前CPU的该缓存行发出Read消息,当前CPU将该缓存行随Read Response消息反馈给其他CPU,并将缓存行的状态由Exclusive改为Shared

    (h):多个CPU共享某个缓存行,其中一个CPU对其他CPU发出Invalidate消息,该CPU收到其他所有拥有该缓存行的CPU的Invalidate Acknowledge消息之后,将该缓存行状态切换为Exclusive;或者其他CPU自己清空了该缓存行(比如为其他数据腾出空间)导致该CPU独占该缓存行,同样会发生这种状态转换。

    (i):其他CPU对当前CPU独占的一个缓存行发出一个Read Invalidate消息,当前CPU将该缓存行设置为Invalid,并发送Read Response以及Invalidate Acknowledge反馈;

    (j):CPU对不在自己Cache的一个数据进行写操作,因此发出Read Invalidate消息,收到一条Read Response(可能来自其他Cache,也可能来自内存)以及所有拥有该缓存行的CPU的Invalidate Acknowledge反馈(可能压根没有)之后,缓存行被当前CPU独占;

    (k):CPU读取某个自己Cache中不存在的数据,于是发出Read消息,收到Read Response(该消息一定来自于其他CPU)之后,缓存行的状态由Invalid变为了Shared

    (l):当前CPU和其他CPU共享了一个缓存行,突然有一个其他CPU向当前CPU发来一条Invalidate消息,当前缓存行只能默默把自己的缓存行设置为Invalidate,并回复Invalidate Acknowledge

    11. 附录2——MESI在线网站使用

    地址:https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm

    img

    见上图,网站主要分为3部分

    1. 内存数据

    内存中保存了4个数据,初始值为0,地址分别为a0a1a2a3

    1. 高速缓存

    显示 CPU 缓存的变量数据和 MESI 协议状态。该高速缓存能容纳2个缓存行数据,所有缓存行的初始状态为I(Invalid)。

    1. CPU核心

    共有3个 CPU,每个 CPU 都有各自的 Cache,CPU 操作分为「读」和「写」,这部分是我们可以手动操作的部分。

    举个例子:

    1. CPU0执行read a0,于是通过各种bus总线将内存中地址为a0的数据读入缓存行内,由于目前只有CPU0独占该缓存行,所以状态变为Exclusive
    2. CPU1执行read a0,又通过各种bus总线将a0的数据读到自己的缓存行内,此时CPU0和CPU1的缓存行都变为Shared状态;
    3. CPU1执行write a0,将地址为a0的数据+1后写回内存,同时向CPU0发出Invalidate信号,导致CPU0将其缓存行置为Invalid状态;此时CPU1独占缓存行,因此缓存行为Exclusive状态;
    4. CPU1执行read a1,类似第1步,通过各种bus总线将内存中地址为a1的数据读入缓存行内,由于目前只有CPU1独占该缓存行,所以状态为Exclusive;此时CPU1的Cache被占满;
    5. 最后CPU1执行read a2,由于CPU1的Cache已经被占满了,只能弹出a0,存入a2,此时a2状态为Exclusive

    img

    12. 附录3——参考文献

    [1] Paul E. McKenney. Memory Barriers: a Hardware View for Software Hackers.

    [2] barrier和smp_mb

    [3] 内存屏障和volatile语义

    [4] 解密内存屏障

    [5] MESI在线网站

    [6] 看懂这篇,才能说了解并发底层技术

    [7] Why Memory Barriers中文翻译(下)

    [8] [MESI与内存屏障](

    展开全文
  • Mesi协议与内存屏障

    2021-10-14 19:00:21
    Mesi协议和内存屏障都是和计算机并发相关的概念。 什么是Mesi协议 计算机存储分层 多核cpu带来的问题 而随着CPU的发展,CPU逐渐发展成了多核,CPU可以同时使用多个核心控制器执行线程任务,当然CPU处理同时...

    Mesi协议和内存屏障都是和计算机并发相关的概念。

    什么是Mesi协议

    计算机存储分层

    多核cpu带来的问题

    而随着CPU的发展,CPU逐渐发展成了多核,CPU可以同时使用多个核心控制器执行线程任务,当然CPU处理同时处理线程任务的速度也越来越快了,但随之也产生了一个问题,多核CPU每个核心控制器工作的时候都会有自己独立的CPU缓存,每个核心控制器都执行任务的时候都是操作的自己的CPU缓存,CPU1与CPU2它们之间的缓存是相互不可见的。

    解决这个问题的根本其实就是需要一种机制来保证一个人修改了内存数据后另外几个缓存了该共享变量的人可以感知到,那么就可以保证各个缓存之间的数据一致性了。

    总线锁模型

    如果想要每个CPU的缓存数据一致,那么最直接的办法就是同时只允许一个人修改内存的数据,当前面一个人操作结束之后,然后 通知其它缓存了该共享变量的缓存,通过这种串行化的方式加上通知机制来保证各个缓存之间的数据一致性,这也就是总线锁的思路。

    总线锁模型带来的问题就是效率较低,一个指令周期只允许一个cpu进行内存的读写操作。

    Mesi协议解决的问题

    与总线锁模型类似,Mesi协议也是一种缓存一致性协议,但更高效。

    在cpu读写数据时,需要确保同一数据在多个cache中是一致的。在写之前必须先让其他 CPU cache 中的该数据失效,之后才可以安全的写数据。

    Mesi协议简介

    cache line的四种状态

    M :  modified(独占且已更改的数据)

    E : exclusive(独占且未更改的数据)

    S :  shared(共享数据,只读)

    I : invalid(无效数据)


    CPU是否独占数据

    cacheline数据

    memory数据
     

    直接写数据
     

    modified
     


     

    最新
     

    最新

    可以
     

    exclusive 


     

    最新
     

    最新

    可以

    shared 


     

    最新

    最新

    不可以
     

    invalid
     

    否(无数据)
     

    无数据
     

    最新

    无数据

     Mesi协议消息

    1. Read。"read" 消息用来获取指定物理地址上的 cache line 数据。
    2. Read Response。该消息携带了 “read” 消息所请求的数据。read response 可能来自于 memory 或者是其他 CPU cache。
    3. Invalidate。该消息将其他 CPU cache 中指定的数据设置为失效。该消息携带物理地址,其他 CPU cache 在收到该消息后,必须进行匹配,发现在自己的 cache line 中有该地址的数据,那么就将其从 cahe line 中移除,并响应 Invalidate Acknowledge 回应。
    4. Invalidate Acknowledge。该消息用做回应 Invalidate 消息。
    5. Read Invalidate。该消息中带有物理地址,用来说明想要读取哪一个 cache line 中的数据。这个消息还有 Invalidate 消息的效果。其实该消息是 read + Invalidate 消息的组合,发送该消息后 cache 期望收到一个 read response 消息。
    6. Writeback。 该消息带有地址和数据,该消息用在 modified 状态的 cache line 被置换时发出,用来将最新的数据写回 memory 或其他下一级 cache 中。

    状态转化图

     

    1. cache 通过 writeback 将数据回写到 memory 或者下一级 cache 中。这时候状态由 modified 变成了 exclusive 。
    2. cpu 直接将数据写入 cache line ,导致状态变为了 modified 。
    3. CPU 收到一个 read invalidate 消息,此时 CPU 必须将对应 cache line 设置成 invalid 状态 , 并且响应一个 read response 消息和 invalidate acknowledge 消息。
    4. CPU 需要执行一个原子的 readmodify-write 操作,并且其 cache 中没有缓存数据。这时候 CPU 就会在总线上发送一个 read invalidate 消息来请求数据,并试图独占该数据。CPU 可以通过收到的 read response 消息获取到数据,并等待所有的 invalidate acknowledge 消息,然后将状态设置为 modifie 。
    5. CPU需要执行一个原子的readmodify-write操作,并且其local cache中有read only的缓存数据(cacheline处于shared状态),这时候,CPU就会在总线上发送一个invalidate请求其他cpu清空自己的local copy,以便完成其独自霸占对该数据的所有权的梦想。同样的,该cpu必须收集所有其他cpu发来的invalidate acknowledge之后才能更改状态为 modified。
    6. 在本cpu独自享受独占数据的时候,其他的cpu发起read请求,希望获取数据,这时候,本cpu必须以其local cacheline的数据回应,并以read response回应之前总线上的read请求。这时候,本cpu失去了独占权,该cacheline状态从Modified状态变成shared状态(有可能也会进行写回的动作)。
    7. 这个迁移和f类似,只不过开始cacheline的状态是exclusive,cacheline和memory的数据都是最新的,不存在写回的问题。总线上的操作也是在收到read请求之后,以read response回应。
    8. 如果cpu认为自己很快就会启动对处于shared状态的cacheline进行write操作,因此想提前先霸占上该数据。因此,该cpu会发送invalidate敦促其他cpu清空自己的local copy,当收到全部其他cpu的invalidate acknowledge之后,transaction完成,本cpu上对应的cacheline从shared状态切换exclusive状态。还有另外一种方法也可以完成这个状态切换:当所有其他的cpu对其local copy的cacheline进行写回操作,同时将cacheline中的数据设为无效(主要是为了为新的数据腾些地方),这时候,本cpu坐享其成,直接获得了对该数据的独占权。
    9. 其他的CPU进行一个原子的read-modify-write操作,但是,数据在本cpu的cacheline中,因此,其他的那个CPU会发送read invalidate,请求对该数据以及独占权。本cpu回送read response”和“invalidate acknowledge”,一方面把数据转移到其他cpu的cache中,另外一方面,清空自己的cacheline。
    10. cpu想要进行write的操作但是数据不在local cache中,因此,该cpu首先发送了read invalidate启动了一次总线transaction。在收到read response回应拿到数据,并且收集所有其他cpu发来的invalidate acknowledge之后(确保其他cpu没有local copy),完成整个bus transaction。当write操作完成之后,该cacheline的状态会从Exclusive状态迁移到Modified状态。
    11. 本CPU执行读操作,发现local cache没有数据,因此通过read发起一次bus transaction,来自其他的cpu local cache或者memory会通过read response回应,从而将该 cache line 从Invalid状态迁移到shared状态。
    12. 当cache line处于shared状态的时候,说明在多个cpu的local cache中存在副本,因此,这些cacheline中的数据都是read only的,一旦其中一个cpu想要执行数据写入的动作,必须先通过invalidate获取该数据的独占权,而其他的CPU会以invalidate acknowledge回应,清空数据并将其cacheline从shared状态修改成invalid状态。

    内存屏障

    复杂的CPU架构

    store buffer

    当cpu执行一条write操作时,需要先发送一条invalidate消息,其他所有的 CPU 在收到这个 Invalidate 消息之后,需要将自己 CPU local cache 中的该数据从 cache 中清除,并且发送消息 acknowledge 告知 CPU 0。CPU 0 在收到所有 CPU 发送的 ack 消息后会将数据写入到自己的 local cache 中。

    这里就产生了性能问题:当 CPU 0 在等待其他 CPU 的 ack 消息时是处于停滞的(stall)状态,大部分的时间都是在等待消息。为了提高性能就引入的 Store Buffer。

    store buffer 的目的是让 CPU 不再操作之前进行漫长的等待时间,而是将数据先写入到 store buffer 中,CPU 无需等待可以继续执行其他指令,等到 CPU 收到了 ack 消息后,再从 store buffer 中将数据写入到 local cache 中。

    Store Forward

    Store Buffer 的确提高了CPU的资源利用率,不过优化了带来了新的问题。在新数据存储在Store Buffer里时,如果此时有一条read指令,若仍旧从cache中读取数据时,读到的是旧的数据。要解决这个问题就必须要求CPU读取数据时得先看Store Buferes里面有没有,如果有则直接读取Store Buferes里的值,如果没有才能读取自己缓存里面的数据,这也就是所谓的“Store Forward”。

    Invalidate Queue

    CPU其实不需要完成invalidate操作就可以回送acknowledge消息,这样,就不会阻止发生invalidate请求的那个CPU进入无聊的等待状态。CPU可以buffer这些invalidate message(放入Invalidate Queues),然后直接回应acknowledge,表示自己已经收到请求,随后会慢慢处理。

    一旦将一个invalidate(例如针对变量a的cacheline)消息放入CPU的Invalidate Queue,实际上该CPU就等于作出这样的承诺:在处理完该invalidate消息之前,不会发送任何相关(即针对变量a的cacheline)的MESI协议消息。

    内存屏障保障缓存的一致性

    内存屏障可以简单的认为它就是用来禁用我们的CPU缓存优化的,使用了内存屏障后,写入数据时候会保证所有的指令都执行完毕,这样就能保证修改过的数据能即时的暴露给其他的CPU。在读取数据的时候保证所有的“无效队列”消息都已经被读取完毕,这样就保证了其他CPU修改的数据消息都能被当前CPU知道,然后根据Invalid消息判断自己的缓存是否处于无效状态,这样就读取数据的时候就能正确的读取到最新的数据。

    写屏障(write memory barrier)

    当CPU执行load memory barrier指令的时候,强制其后的store指令,一定是在写屏障之前的所有store指令完成之后,才允许执行。为了达到这个目标有两种方法:方法一就是让CPU stall,直到完成了清空了store buffer(也就是把store buffer中的数据写入cacheline了)。方法二是让CPU可以继续运行,不过需要在store buffer中做些文章,也就是要记录store buffer中数据的顺序,在将store buffer的数据更新到cacheline的操作中,严格按照顺序执行,即便是后来的store buffer数据对应的cacheline已经ready,也不能执行操作,要等前面的store buffer值写到cacheline之后才操作。

    读屏障(load memory barrier)

    当CPU执行load memory barrier指令的时候,对当前Invalidate Queue中的所有的entry进行标注,这些被标注的项次被称为marked entries,而随后CPU执行的任何的load操作都需要等到Invalidate Queue中所有marked entries完成对cacheline的操作之后才能进行。

    小结

    Memory barrier其实是解决复杂(优化后)cpu架构带来的新问题。它连同Mesi协议一起保障并发程序中数据缓存的一致性。

    后续的问题

    1、mutex的实现方式

    2、memory order

    参考文献

    1、带你了解缓存一致性协议 MESI

    2、内存屏障(Memory Barrier)究竟是个什么鬼? - 知乎

    3、并发理论基础:并发问题产生的三大根源 - 知乎

    4、并发基础理论:缓存可见性问题、MESI协议、内存屏障 - 知乎

     

     

     

    展开全文
  • 缓存一致性协议-MESI

    千次阅读 2022-02-17 17:58:26
    MESI MESI协议是一个基于失效的缓存一致性协议,是支持写回(write-back)缓存的最常用协议。也称作伊利诺伊协议 (Illinois protocol,因为是在伊利诺伊大学厄巴纳-香槟分校被发明的[1])。与写穿(write through)...

    背景

    带有高速缓存的CPU执行计算的流程

    1. 程序以及数据被加载到主内存
    2. 指令和数据被加载到CPU的高速缓存
    3. CPU执行指令,把结果写到高速缓存
    4. 高速缓存中的数据写回主内存

    高速缓存的数据结构

    高速缓存的底层数据结构其实是一个拉链散列表的结构,就是有很多的bucket,每个bucket挂了很多的cache entry,每个 cache entry 由三个部分组成: tagcache lineflag

    • cache line :缓存的数据,可以包含多个变量的值
    • tag :指向了这个缓存数据在主内存的数据的地址
    • flag :标识了缓存行的状态,具体状态划分见下边MESI协议

    怎么在高速缓存中定位到这个变量呢?

    在处理器读写高速缓存的时候,实际上会根据变量名执行一个内存地址解码的操作,解析出来三个东西。 index , tagofferset

    • index :用于定位到拉链散列表中的某个 bucket
    • tag :用于定位 cache entry
    • offerset :用于定位一个变量在 cache line 中的位置

    由于CPU的运算速度超越了1级缓存的数据I\O能力,CPU厂商引入了多级的缓存结构。

    多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。

    问题

    每个核都有自己私有的 L1,、L2 缓存。那么多线程编程时, 另外一个核的线程想要访问当前核内 L1、L2 缓存行的数据, 该怎么办呢?

    有人说可以通过第 2 个核直接访问第 1 个核的缓存行,这是当然是可行的,但这种方法不够快。跨核访问需要通过 Memory Controller(内存控制器,是计算机系统内部控制内存并且通过内存控制器使内存与 CPU 之间交换数据的重要组成部分),典型的情况是第 2 个核经常访问第 1 个核的这条数据,那么每次都有跨核的消耗。更糟的情况是,有可能第 2 个核与第 1 个核不在一个插槽内,况且 Memory Controller 的总线带宽是有限的,扛不住这么多数据传输。所以,CPU 设计者们更偏向于另一种办法: 如果第 2 个核需要这份数据,由第 1 个核直接把数据内容发过去,数据只需要传一次。

    那么什么时候会发生缓存行的传输呢?答案很简单:当一个核需要读取另外一个核的脏缓存行时发生。但是前者怎么判断后者的缓存行已经被弄脏(写)了呢?这就需要了解MESI 协议了。

    MESI

    MESI协议是一个基于失效的缓存一致性协议,是支持写回(write-back)缓存的最常用协议。也称作伊利诺伊协议 (Illinois protocol,因为是在伊利诺伊大学厄巴纳-香槟分校被发明的[1])。与写穿(write through)缓存相比,回写缓冲能节约大量带宽。总是有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss)且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主存的事务数量。这极大改善了性能。[2]

    MESI协议缓存状态

    MESI 是指4种状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:

    状态描述
    M 修改 (Modified)该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
    E 独享、互斥 (Exclusive)该Cache line有效,缓存行内容和内存中的一样,而且其它处理器都没有这行数据;
    S 共享 (Shared)该Cache line有效,缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝
    I 无效 (Invalid)该Cache line无效。

    这四个状态是如何转换的?

    MESI状态转换

    • 初始:一开始时,缓存行没有加载任何数据,所以它处于 I 状态。
    • 本地写(Local Write):如果本地处理器写数据至处于 I 状态的缓存行,则缓存行的状态变成 M。
    • 本地读(Local Read):如果本地处理器读取处于 I 状态的缓存行,很明显此缓存没有数据给它。此时分两种情况:
      • (1) 其它处理器的缓存里也没有此行数据,则从内存加载数据到此缓存行后,再将它设成 E 状态,表示只有我一家有这条数据,其它处理器都没有;
      • (2) 其它处理器的缓存有此行数据,则将此缓存行的状态设为 S 状态。(备注:如果处于M状态的缓存行,再由本地处理器写入/读出,状态是不会改变的)
    • 远程读(Remote Read):假设我们有两个处理器 c1 和 c2,如果 c2 需要读另外一个处理器 c1 的缓存行内容,c1 需要把它缓存行的内容通过内存控制器 (Memory Controller) 发送给 c2,c2 接到后将相应的缓存行状态设为 S。在设置之前,内存也得从总线上得到这份数据并保存。
    • 远程写(Remote Write):其实确切地说不是远程写,而是 c2 得到 c1 的数据后,不是为了读,而是为了写。也算是本地写,只是 c1 也拥有这份数据的拷贝,这该怎么办呢?c2 将发出一个 RFO (Request For Owner) 请求,它需要拥有这行数据的权限,其它处理器的相应缓存行设为 I,除了它自已,谁不能动这行数据。这保证了数据的安全,同时处理 RFO 请求以及设置I的过程将给写操作带来很大的性能消耗。

    cache line

    缓存行通常是 64 字节(译注:本文基于 64 字节,其他长度的如 32 字节等不是本文讨论的重点),并且它有效地引用主内存中的一块地址。

    一个 Java 的 long 类型是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。所以,如果你访问一个 long 数组,当数组中的一个值被加载到缓存中,它会额外加载另外 7 个,以致你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。而如果你在数据结构中的项在内存中不是彼此相邻的(如链表),你将得不到免费缓存加载所带来的优势,并且在这些数据结构中的每一个项都可能会出现缓存未命中。

    伪共享(False Sharing)

    如果存在这样的场景,有多个线程操作不同的成员变量,但是相同的缓存行,这个时候会发生什么?

    上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。

    但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态(失效态)。

    轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。从前一篇我们知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。

    表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

    如何避免伪共享?

    其中一个解决思路,就是让不同线程操作的对象处于不同的缓存行即可。

    那么该如何做到呢?那就是缓存行填充(Padding) 。

    一条缓存行有 64 字节,而 Java 程序的对象头对象头(mark word)固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 对象处于不同的缓存行,就避免了伪共享( 64 位系统超过缓存行的 64 字节也无所谓,只要保证不同线程不操作同一缓存行就可以)。

    例如:Baidu UID-generator 的作法:

    /*
     * Copyright (c) 2017 Baidu, Inc. All Rights Reserve.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package com.baidu.fsg.uid.utils;
    
    import java.util.concurrent.atomic.AtomicLong;
    
    /**
     * 该类表示 用AtomicLong 来进行填充 ,以避免伪共享问题
     * 
     * CPU cache line 一般为 64 字节,以下是填充后的 cache line 示例:<br>
     * 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
     * 
     * @author yutianbao
     */
    public class PaddedAtomicLong extends AtomicLong {
        private static final long serialVersionUID = -3415778863941386253L;
    
        /** Padded 6 long (48 bytes) */
        public volatile long p1, p2, p3, p4, p5, p6 = 7L;
    
        /**
         * Constructors from {@link AtomicLong}
         */
        public PaddedAtomicLong() {
            super();
        }
    
        public PaddedAtomicLong(long initialValue) {
            super(initialValue);
        }
    
        /**
         * 为防止清理未使用的填充引用而进行 的 GC 优化
         */
        public long sumPaddingToPreventOptimization() {
            return p1 + p2 + p3 + p4 + p5 + p6;
        }
    
    }
    

    这里的程序对 32位的没有问题,但64位的填充感觉就不对了

    在32和64位系统中,冗余变量填充所需的个数不一样。在32位系统中,Cache Line的长度为32字节,Java对象头所占据字节数分别为“Mark Word(4字节)”,“指向类的指针(4字节)”,“数组长度(4字节,只有数组对象才有该部分)”

    所以 是不是可以用 5个 long 1、1个int 来填充,或者 使用 @contended 注解解决

    参考:

    展开全文
  • 【Cache篇】MESI协议

    2022-05-01 22:20:53
    在这里我们介绍MESI协议(Write-Once总线监听协议),MESI这四个字母分别代表Modify、Exclusive、Shared和Invalid。Cache Line的状态必须是这四个中的一种。前三种状态均是数据有效下的状态。Cache Line有两个标志-...
  • 39 | MESI协议:如何让多核CPU的高速缓存保持一致?
  • volatile和MESI协议之间的关系 刚接触MESI协议时容易产生误解,以为是volatile关键字触发MESI机制来保证变量的可见性,实际没有这一层因果关系。下面简单总结下我对两者关系的一些理解: 1.MESI保障了多核场景的...
  • MESI 协议

    2021-12-06 16:51:29
    MESI 协议是高速缓存一致性协议,是为了解决多 cpu 、并发环境下,多个 cpu 缓存不一致问题而提出的协议。 缓存行在任何时刻一定处于四个状态之一: Modified: 缓存行已经被修改,但是没有被写回主存; Exclusive:...
  • MESI 消息 MESI 协议中,缓存行状态的切换依赖消息的传递,MESI 有以下几种消息: Read: 读取某个地址的数据。 Read Response: Read 消息的响应。 Invalidate: 请求其他 CPU invalid 地址对应的缓存行。 Invalidate...
  • MESI 缓存一致性协议

    千次阅读 2021-11-15 16:19:14
    通过例子来介绍 MESI 协议1.MESI 场景2.MESI 协议下,执行步骤3.MESI协议失效问题 场景再现 场景:   服务器有2个线程t1、t2在跑。都对 x=1 分别+1,期望最终结果:x = 3   问题分析:   首先会将 x=1 加载到...
  • JMM1.来谈谈JMM2.volatile和synchronized关键字2.1volatile是如何保证可见性的??2.2 volatile是如何保证有序性的呢??哪些情况不能重排序??三级目录 1.来谈谈JMM 我们都知道,为了提高CPU的运行效率,我们会...不了解MESI
  • MESI —— 内存缓存一致性性协议 注意,MESI协议只能保证并发编程中的可见性,并未解决原子性和有序性的问题,所以只靠MESI协议是无法完全解决多线程中的所有问题。 1. cache line的四种状态 M : "modified" E : ...
  • 图解MESI(缓存一致性协议)

    千次阅读 2021-12-27 15:16:01
    MESI(缓存一致性协议)
  • MESI电子商务 MESI项目 介绍 项目管理: Trello 返回: Java SPRING-Thymeleaf 前: HTML CSS Boostrap JS 备份管理: GitHub SGBD: MySQL 开发服务器 可访问suivante:
  • CPU缓存一致性协议MESI

    2021-05-28 06:40:56
    MESI是通过锁定缓存行来失效脏数据的,缓存行锁定是原子的,即一个缓存行是MESI锁定的最小单元,所以就无法保证能同时锁定多个缓存行,因为其他CPU有可能会竞争这个锁 ,即无法原子性的锁定多个缓存行 。...
  • Java内存模型&&MESI协议

    2021-06-23 23:24:04
    CPU缓存一致性协议MESI 目录多线程并发编程的三个特性实现缓存的出现缓存不一致MESI协议volatileJava内存模型 多线程并发编程的三个特性实现 多线程并发编程中主要围绕着三个特性实现。 可见性 可见性 是指当多个...
  • MESI的概念此处不再累赘,有兴趣的可以搜索 store buffer 引入store buffer是为了将同步改为异步 引入store forwarding技术是为了让CPU可以直接从store buffer里加载数据 但是因此可能会发生乱序情况,譬如a在...
  • 2.3.2 MESI协议以及状态 2.3.2.1 什么是MESI协议 MESI协议是Modified、Exclusive、Shared、Invalid的缩写,表示修改、独占、共享和无效四种状态。缓存行是处理器和主存读写数据的最小单位,这4种状态就是在缓存行上...
  • 这样看来的MESI是可以保证缓存一致性,也间接保证了可见性,那为什么需要volatile 关键字来保证可见性
  • 缓存一致性协议(MESI) - 简书存储器层次结构中,最快速的就是cpu一级别 在目前主流的计算机中,cpu执行计算的主要流程如图所示: 数据加载的流程如下: 将程序和数据从硬盘加载到内存中将程序和数据从内存加......
  • 处理器上有一套完整的协议,来保证缓存的一致性,比较经典的就是 MESI 协议,其实现方法是在 CPU 缓存中保存一个标记位,以此来标记四种状态。另外,每个 Core 的 Cache 控制器不仅知道自己的读写操作,也监听其它 ...
  • MESI缓存一致性协议详解 1、CPU为何要有高速缓存 CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。 带有高速缓存的CPU执行计算的流程 程序以及数据被加载到主内存 指令和数据被加载到CPU的...
  • 【Java锁体系】八、MESI缓存一致性协议讲解 MESI是一种广泛使用的写回策略的缓存一致性协议。 8.1 MESI协议中的状态 M:Modified被修改 E:Exclusive独享的 S:Shared共享的 I:Invalid无效的 M: 被修改...
  • MESI协议.xmind

    2021-08-07 10:59:56
    MESI协议.xmind
  • java多线程和高并发系列三 & 缓存一致性协议MESI
  • 万字长文深入剖析缓存一致性协议(MESI),内存屏障

    千次阅读 多人点赞 2021-04-04 21:37:17
    缓存一致性协议,内存屏障 计算机基本硬件组成 总线 I/O设备 主存储器 CPU 高速缓存存储器 简介 局部性原理 具体结构 缓存一致性协议 MESI 协议状态迁移 MESI协议消息 Store Buffer / Invalidate Queue Store Buffer...
  • CPU缓存一致性协议:MESI

    千次阅读 2022-03-26 20:10:40
    今天在看《架构解密》的时候,看到一段介绍CPU缓存...MESI MESI协议是基于Invalidate的高速缓存一致性协议,并且是支持回写高速缓存的最常用协议之一。 它也被称为伊利诺伊州协议(由于其在伊利诺伊大学厄巴纳 - 香
  • 3.7 通用缓存一致性协议 - MESI MESI协议:是一个基于写嗅探-写失效的缓存一致性协议,它一共是缓存行的4个状态的缩写: M 已修改Modified:表示与主存中的的值不一致,被修改过。 E 独占Exclusive:缓存行仅仅只在...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 9,266
精华内容 3,706
关键字:

mesi

友情链接: Sort.rar