zoukankan      html  css  js  c++  java
  • Sword 内存屏障-读写屏障指令

    写屏障指令
        cpu为了优化指令的执行效率,引入了store buffer(forwarding),而又因此导致了指令执行顺序的变化。

    要保证这种顺序一致性,无法靠硬件优化,需要在软件层面支持,cpu提供了写屏障(write memory barrier)指令,

    Linux操作系统将写屏障指令封装成了smp_wmb()函数,cpu执行smp_mb()的思路是,会先把当前store buffer中的数据刷到cache之后,

    再执行屏障后的"写入操作",该思路有两种实现方式: 一是简单地刷store buffer,但如果此时远程cache line没有返回,则需要等待,

    二是将当前store buffer中的条目打标,然后将屏障后的"写入操作"也写到store buffer中,cpu继续干其他的事,当被打标的条目全部刷到cache line,

    之后再刷后面的条目,以第二种实现逻辑为例,我们看看以下代码执行过程:

    void foo() {
        a = 1;
        smp_wmb()
        b = 1;
    }
    void bar() {
        while (b == 0) continue;
        assert(a == 1)
    }
    cpu1执行while(b == 0),由于cpu1的cache中没有b,发出Read b消息。
    cpu0执行a
    =1,由于cpu0的cache中没有a,因此它将a(当前值1)写入到store buffer并发出Read Invalidate a消息。
    cpu0看到smp_wmb()内存屏障,它会标记当前store buffer中的所有条目(即a
    =1被标记)。
    cpu0执行b
    =1,尽管b已经存在在cache中(Exclusive),但是由于store buffer中还存在被标记的条目,因此b不能直接写入,只能先写入store buffer中。
    cpu0收到Read b消息,将cache中的b(当前值0)返回给cpu1,将b写回到内存,并将cache line状态改为Shared。
    cpu1收到包含b的cache line,继续while (b
    == 0)循环。
    cpu1收到Read Invalidate a消息,返回包含a的cache line,并将本地的cache line置为Invalid。
    cpu0收到cpu1传过来的包含a的cache line,然后将store buffer中的a(当前值1)刷新到cache line,并且将cache line状态置为Modified。
    由于cpu0的store buffer中被标记的条目已经全部刷新到cache,此时cpu0可以尝试将store buffer中的b
    =1刷新到cache,
    但是由于包含b的cache line已经不是Exclusive而是Shared,因此需要先发Invalidate b消息。
    cpu1收到Invalidate b消息,将包含b的cache line置为Invalid,返回Invalidate ACK。
    cpu1继续执行while(b
    == 0),此时b已经不在cache中,因此发出Read消息。
    cpu0收到Invalidate ACK,将store buffer中的b
    =1写入Cache。
    cpu0收到Read消息,返回包含b新值的cache line。
    cpu1收到包含b的cache line,可以继续执行while(b
    == 0),终止循环,然后执行assert(a == 1),此时a不在其cache中,因此发出Read消息。
    cpu0收到Read消息,返回包含a新值的cache line。
    cpu1收到包含a的cache line,断言为真。

    Invalid Queue
    引入了store buffer,再辅以store forwarding,写屏障,看起来好像可以自洽了,然而还有一个问题没有考虑: store buffer的大小是有限的,

    所有的写入操作发生cache missing(数据不再本地)都会使用store buffer,特别是出现内存屏障时,

    后续的所有写入操作(不管是否cache missing)都会挤压在store buffer中(直到store buffer中屏障前的条目处理完),因此store buffer很容易会满,

    当store buffer满了之后,cpu还是会卡在等对应的Invalidate ACK以处理store buffer中的条目。因此还是要回到Invalidate ACK中来,

    Invalidate ACK耗时的主要原因是cpu要先将对应的cache line置为Invalid后再返回Invalidate ACK,一个很忙的cpu可能会导致其它cpu都在等它回Invalidate ACK。

    解决思路还是化同步为异步: cpu不必要处理了cache line之后才回Invalidate ACK,而是可以先将Invalid消息放到某个请求队列Invalid Queue,

    然后就返回Invalidate ACK。CPU可以后续再处理Invalid Queue中的消息,大幅度降低Invalidate ACK响应时间。
    加入了invalid queue之后,cpu在处理任何cache line的MSEI状态前,都必须先看invalid queue中是否有该cache line的Invalid消息没有处理。

    另外,它也再一次破坏了内存的一致性。请看代码:

    //假设a, b的初始值为0,a在cpu0,cpu1中均为Shared状态,b在cpu0独占(Exclusive状态),cpu0执行foo,cpu1执行bar
    void foo() {
        a = 1;
        smp_wmb()
        b = 1;
    }
    void bar() {
        while (b == 0) continue;
        assert(a == 1)
    }
    cpu0执行a=1,由于其有包含a的cache line,将a写入store buffer,并发出Invalidate a消息。
    cpu1执行while(b
    == 0),它没有b的cache,发出Read b消息。
    cpu1收到cpu0的Invalidate a消息,将其放入Invalidate Queue,返回Invalidate ACK。
    cpu0收到Invalidate ACK,将store buffer中的a
    =1刷新到cache line,标记为Modified。
    cpu0看到smp_wmb()内存屏障,但是由于其store buffer为空,因此它可以直接跳过该语句。
    cpu0执行b
    =1,由于其cache独占b,因此直接执行写入,cache line标记为Modified。 cpu0收到cpu1发的Read b消息,将包含b的cache line写回内存并返回该cache line,本地的cache line标记为Shared。
    cpu1收到包含b(当前值1)的cache line,结束while循环。
    cpu1执行assert(a
    == 1),由于其本地有包含a旧值的cache line,读到a初始值0,断言失败。
    cpu1这时才处理Invalid Queue中的消息,将包含a旧值的cache line置为Invalid。

    问题在于第9步中cpu1在读取a的cache line时,没有先处理Invalid Queue中该cache line的Invalid操作,其实cpu还提供了读屏障指令,Linux将其封装成smp_rmb()函数,将该函数插入到bar函数中,就像这样:

    void foo() {
        a = 1;
        smp_wmb()
        b = 1;
    }
    void bar() {
        while (b == 0) continue;
        smp_rmb()
        assert(a == 1)
    }

    和smp_wmb()类似,cpu执行smp_rmb()的时,会先把当前invalidate queue中的数据处理掉之后,再执行屏障后的"读取操作"。

    备注

    smp_rmb(): 在invalid queue的数据被刷完之后再执行屏障后的读操作。
    smp_wmb(): 在store buffer的数据被刷完之后再执行屏障后的写操作。
    smp_mb(): 同时具有读屏障和写屏障功能。
  • 相关阅读:
    OpenFeign添加日志信息
    SpringCloud openFeign远程调用超时解决办法
    SpringCloud多个接口标注@FeignClient报错
    Maven依赖管理更新
    Spring Data Jpa中getOne和FindOne的区别
    Restful API规范解析
    Jpa查询部分字段的方法
    使用Jpa报错之Unable to locate Attribute with the the given name [***] on this ManagedType
    IDEA 安装三方插件的方法
    shiro重新赋值权限
  • 原文地址:https://www.cnblogs.com/zhanggaofeng/p/15202435.html
Copyright © 2011-2022 走看看