要了解如何使用memory barrier,最好的方法是明白它为什么存在。CPU硬件设计为了提高指令的执行速度,增设了两个缓冲区(store buffer, invalidate queue)这个两个缓冲区可以避免CPU在某些情况下进行不必要的等待,从而提高速度,但是这两个缓冲区的存在也同时带来了新的问题。下面我们一步步来分析说明

1. cache一致性问题


  1. Cache 一致性问题出现的原因是在一个多处理器系统中,每个处理器核心都有独占的Cache 系统(比如一级 Cache 和二级 Cache),而导致一个内存块在系统中同时可能有多个备份,从而引起访问时的不一致性问题。
  2. Cache 一致性问题的根源是因为存在多个处理器独占的 Cache,而不是多个处理器。它的限制条件比较多:多核,独占 Cache,Cache 写策略。当其中任一个条件不满足时便不存在cache一致性问题。

为了保证在多处理器的环境下cache一致性,需要通过某种手段来保证cache的一致性。

解决 Cache 一致性问题的机制有两种:基于目录的协议(Directory-based protocol)和总线窥探协议(Bus snooping protocol)。 目前被多个厂家广泛使用的协议是MESI协议。它是从总线窥探协议中衍生而来的一种协议。

2. cache一致性协议:MESI

MESI 协议是 Cache line 四种状态的首字母的缩写,分别是修改(Modified)态、独占(Exclusive)态、共享(Shared)态和失效(Invalid)态。 Cache 中缓存的每个 Cache Line 都必须是这四种状态中的一种。

  • 修改态(Modified),如果该 Cache Line 在多个 Cache 中都有备份,那么只有一个备份能处于这种状态,并且“dirty”标志位被置上。拥有修改态 Cache Line 的 Cache 需要在某个合适的时候把该 Cache Line 写回到内存中。但是在写回之前,任何处理器对该 Cache Line在内存中相对应的内存块都不能进行读操作。 Cache Line 被写回到内存中之后,其状态就由修改态变为共享态。
  • 独占态(Exclusive),和修改状态一样,如果该 Cache Line 在多个 Cache 中都有备份,那么只有一个备份能处于这种状态,但是“dirty”标志位没有置上,因为它是和主内存内容保持一致的一份拷贝。如果产生一个读请求,它就可以在任何时候变成共享态。相应地,如果产生了一个写请求,它就可以在任何时候变成修改态。
  • 共享态(Shared),意味着该 Cache Line 可能在多个 Cache 中都有备份,并且是相同的状态,它是和内存内容保持一致的一份拷贝,而且可以在任何时候都变成其他三种状态。
  • 失效态(Invalid),该 Cache Line 要么已经不在 Cache 中,要么它的内容已经过时。一旦某个Cache Line 被标记为失效,那它就被当作从来没被加载到 Cache 中

MESI使用消息传递的方式在上述几种状态之间切换,具体转换过程参见[1]。常见的消息类型:


  1. read: 包含要读取的CACHE-LINE的物理地址
  2. read response: 包含READ请求的数据,要么由内存满足要么由cache满足
  3. invalidate: 包含要invalidate的cache-line的物理地址,所有其他cache必须移除相应的数据项
  4. invalidate ack: 回复消息
  5. read invalidate: 包含要读取的cache-line的物理地址,同时使其他cache移除该数据。需要read response和invalidate ack消息
  6. writeback:包含要写回的数据和地址,该状态将处于modified状态的lines写回内存,为其他数据腾出空间

3. Store buffer的引入

虽然该MESI协议可以保证数据的一致性,但是在某种情况下并不高效。举例来说,如果CPU0要更新一个处于CPU1-cache中的数据,那么它必须等待 cache-line从CPU1-cache传递到CPU0-cache,然后再执行写操作。cache之间的传递需要花费大量的时间,比执行一个简单的 操作寄存器的指令高出几个数量级。而事实上,花费这个时间根本毫无意义,因为不论从CPU1-cache传递过来的数据是什么,CPU0都会覆盖它。为了 解决这个问题,硬件设计者引入了store buffer,该缓冲区位于CPU和cache之间,当进行写操作时,CPU直接将数据写入store buffer,而不再等待另一个CPU的消息。但是这个设计会导致一个很明显的错误情况。

3.1 引入store buffer后出现的问题1

试考虑如下代码:


  1. a = 1;
  2. b = a + 1;
  3. assert(b == 2);

假设初始时a和b的值都是0,a处于CPU1-cache中,b处于CPU0-cache中。如果按照下面流程执行这段代码:

  1. CPU 0 starts executing the a = 1
  2. CPU 0 looks “a” up in the cache, and finds that it is missing
  3. CPU 0 therefore sends a “read invalidate” message in order to get exclusive ownership of the cache line containing “a”.
  4. CPU 0 records the store to “a” in its store buffer.
  5. CPU 1 receives the “read invalidate” message, and responds by transmitting the cache line and removing that cacheline from its cache.
  6. CPU 0 starts executing the b = a + 1.
  7. CPU 0 receives the cache line from CPU 1, which still has a value of zero for “a”.
  8. CPU 0 loads “a” from its cache, finding the value zero.
  9. CPU 0 applies the entry from its store buffer to the newly arrived cache line, setting the value of “a” in its cache to one.
  10. CPU 0 adds one to the value zero loaded for “a” above, and stores it into the cache line containing “b” (which we will assume is already owned by CPU 0).
  11. CPU 0 executes assert(b == 2), which fails.

出现问题的原因是我们有两份”a”的拷贝,一份在cache-line中,一份在store buffer中。硬件设计师的解决办法是“store forwarding”,当执行load操作时,会同时从cache和store buffer里读取。也就是说,当进行一次load操作,如果store-buffer里有该数据,则CPU会从store-buffer里直接取出数 据,而不经过cache。因为“store forwarding”是硬件实现,我们并不需要太关心。

3.2 引入store buffer后出现的问题2

请看下面的代码:


  1. void foo(void)
  2. {
  3. a = 1;
  4. b = 1;
  5. }
  6. void bar(void)
  7. {
  8. while (b == 0) continue;
  9. assert(a == 1);
  10. }

假设变量a在CPU1-cache中,b在CPU0-cache中。CPU0执行foo(),CPU1执行bar()。

程序执行的顺序如下:

  • CPU 0 执行a = 1。缓存行不在CPU0的缓存中,因此CPU0将“a”的新值放到存储缓冲区,并发送一个“读使无效”消息
  • CPU 1 执行while (b == 0) continue,但是包含“b”的缓存行不在缓存中,它发送一个“读”消息。
  • CPU 0 执行b = 1,它已经在缓存行中有“b”的值了(换句话说,缓存行已经处于“modified”或者“exclusive”状态),因此它存储新的“b”值在它的缓存行中。
  • CPU 0 接收到“读”消息,并且发送缓存行中的最新的“b”的值到CPU1,同时将缓存行设置为“shared”状态
  • CPU 1 接收到包含“b”值的缓存行,并将其值写到它的缓存行中
  • CPU 1 现在结束执行while (b == 0) continue,因为它发现“b”的值是1,它开始处理下一条语句。
  • CPU 1 执行assert(a == 1),并且,由于CPU 1 工作在旧的“a”的值,因此验证失败。
  • CPU 1 接收到“读使无效”消息, 并且发送包含“a”的缓存行到CPU0,同时使它的缓存行变成无效。但是已经太迟了。
  • CPU 0 接收到包含“a”的缓存行,将且将存储缓冲区的数据保存到缓存行中,这使得CPU1验证失败。

就是说,可能出现这类情况,b已经赋值了,但是a还没有,所以出现了b = 1, a = 0的情况。对于这类问题,硬件设计者也爱莫能助,因为CPU无法知道变量之间的关联关系。所以硬件设计者提供了memory barrier指令,让软件来告诉CPU这类关系。

解决办法是:使用硬件设计者提供的“内存屏障”来修改代码:


  1. void foo(void)
  2. {
  3. a = 1;
  4. smp_mb();
  5. b = 1;
  6. }

smp_mb()指令可以迫使CPU在进行后续store操作前刷新store-buffer。以上面的程序为例,增加memory barrier之后,就可以保证在执行b=1的时候CPU0-store-buffer中的a已经刷新到cache中了,此时CPU1-cache中的a 必然已经标记为invalid。对于CPU1中执行的代码,则可以保证当b==0为假时,a已经不在CPU1-cache中,从而必须从CPU0- cache传递,得到新值“1”

4. Invalidation Queue的引入

store buffer一般很小,所以CPU执行几个store操作就会填满, 这时候CPU必须等待invalidation ACK消息(得到invalidation ACK消息后会将storebuffer中的数据存储到cache中,然后将其从store buffer中移除),来释放store buffer缓冲区空间。

“Invalidation ACK”消息需要如此长的时间,其原因之一是它们必须确保相应的缓存行实际变成无效了。如果缓存比较忙的话,这个使无效操作可能被延迟。例如,如果CPU密集的装载或者存储数据,并且这些数据都在缓存中。另外,如果在一个较短的时间内,大量的“使无效”消息到达,一个特定的CPU会忙于处理它们。这会使得其他CPU陷于停顿。但是,在发送应答前,CPU 不必真正的使无效缓存行。它可以将使无效消息排队。并且它明白,在发送更多的关于该缓存行的消息前,需要处理这个消息。

一个带Invalidation Queue的CPU可以迅速应答一个Invalidation Ack消息,而不必等待相应的行真正变成无效状态。于是乎出现了下面的组织架构:

但是,此种方法也存在如下问题:


  1. void foo(void)
  2. {
  3. a = 1;
  4. smp_mb();
  5. b = 1;
  6. }
  7. void bar(void)
  8. {
  9. while (b == 0) continue;
  10. assert(a == 1);
  11. }
  • CPU0执行a=1。因为cache-line是shared状态,所以新值放到store-buffer里,并传递invalidate消息来通知CPU1
  • CPU1执行 while(b==0) continue;但是b不再CPU1-cache中,所以发送read消息
  • CPU1接受到CPU0的invalidate消息,将其排队,然后返回ACK消息
  • CPU0接收到来自CPU1的ACK消息,然后执行smp_mb(),将a从store-buffer移到cache-line中。(内存屏蔽在此处生效了)
  • CPU0执行b=1;因为已经包含了该cache-line,所以将b的新值写入cache-line
  • CPU0接收到了read消息,于是传递包含b新值的cache-line给CPU1,并标记为shared状态
  • CPU1接收到包含b的cache-line
  • CPU1继续执行while(b==0) continue;因为为假所以进行下一个语句
  • CPU1执行assert(a==1),因为a的旧值依然在CPU1-cache中,断言失败
  • 尽管断言失败了,但是CPU1还是处理了队列中的invalidate消息,并真的invalidate了包含a的cache-line,但是为时已晚

出现问题的原因是,当CPU排队某个invalidate消息后,并做错了应答Invalidate Ack, 但是在它还没有处理这个消息之前,就再次读取了位于cache中的数据,该数据此时本应该已经失效,但由于未处理invalidate消息导致使用错误。

解决方法是在bar()中也增加一个memory barrier:


  1. void bar(void)
  2. {
  3. while (b == 0) continue;
  4. smp_mb();
  5. assert(a == 1);
  6. }

此处smp_mb()的作用是处理“Invalidate Queues”中的消息,于是在执行assert(a==1)时,CPU1中的包含a的cache-line已经无效了,新的值要重新从CPU0-cache中读取

从这两个例子可以看出:

smp_mb();既可以用来处理storebuffer中的数据,也可以用来处理Invalidation Queue中的Invalid消息。实际上,memory barrier确实可以细分为“write memory barrier(wmb)”和“read memory barrier(rmb)”。rmb只处理Invalidate Queues,wmb只处理store buffer。

上述例子完整的代码变为:


  1. void foo(void)
  2. {
  3. a = 1;
  4. smp_wmb();/*CPU1要使用该值,因此需要及时更新处理store buffer中的数据*/
  5. b = 1;
  6. }
  7. void bar(void)
  8. {
  9. while (b == 0) continue;
  10. smp_rmb();/*由于CPU0修改了a值,使用此值时及时处理Invalidation Queue中的消息*/
  11. assert(a == 1);
  12. }
文章知识点与官方知识档案匹配,可进一步学习相关知识
CS入门技能树Linux入门初识Linux30025 人正在系统学习中

[转帖]什么是内存屏障? Why Memory Barriers ?的更多相关文章

  1. 内存屏障(Memory barrier)-- 转发

    本文例子均在 Linux(g++)下验证通过,CPU 为 X86-64 处理器架构.所有罗列的 Linux 内核代码也均在(或只在)X86-64 下有效. 本文首先通过范例(以及内核代码)来解释 Me ...

  2. JVM内存模型、指令重排、内存屏障概念解析

    在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...

  3. [面试]volatile类型修饰符/内存屏障/处理器缓存

    volatile类型修饰符 本篇文章的目的是为了自己梳理面试知识点, 在这里做一下笔记. 绝大部分内容是基于这些文章的内容进行了copy+整理: 1. http://www.infoq.com/cn/ ...

  4. JVM内存模型、指令重排、内存屏障概念解析(转载)

    在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...

  5. C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

    目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...

  6. CPU缓存和内存屏障

    CPU性能优化手段 - 缓存 为了提高程序的运行性能, 现代CPU在很多方面对程序进行了优化例如: CPU高速缓存, 尽可能的避免处理器访问主内存的时间开销, 处理器大多会利用缓存以提高性能 多级缓存 ...

  7. volatile关键字?MESI协议?指令重排?内存屏障?这都是啥玩意

    一.摘要 三级缓存,MESI缓存一致性协议,指令重排,内存屏障,JMM,volatile.单拿一个出来,想必大家对这些概念应该有一定了解.但是这些东西有什么必然的联系,或者他们之间究竟有什么前世今生想 ...

  8. 多线程 - 内存屏障和cpu缓存

    CPU性能优化 - 缓存 为了提高程序运行的性能,现代CPU在很多方面会对程序进行优化.CPU的处理速度是很快的,内存的速度次之,硬盘速度最慢.在cpu处理内存数据中,内存运行速度太慢,就会拖累cpu ...

  9. Java 内存屏障

    内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题.Java编译器也会根据内存屏障的规则禁止重排序. 内 ...

  10. [SPDK/NVMe存储技术分析]006 - 内存屏障(MB)

    在多核(SMP)多线程的情况下,如果不知道CPU乱序执行的话,将会是一场噩梦,因为无论怎么进行代码Review也不可能发现跟内存屏障(MB)相关的Bug.内存屏障分为两类: 跟编译有关的内存屏障: 告 ...

随机推荐

  1. AES算法在网络安全中的应用:如何守护数据宝藏?

    摘要:高级加密标准(AES)是美国国家标准与技术研究所(NIST)用于加密电子数据的规范.本文从历史.算法原理.性能优势和应用等方面全面介绍了AES算法,旨在帮助读者更好地理解这一广泛应用的对称加密算 ...

  2. JavaFx之横向布局左右两侧对齐(十九)

    JavaFx之横向布局左右两侧对齐(十九) 横向布局HBox在子节点A.B中添加<HBox HBox.hgrow="ALWAYS"></HBox> 即可做到 ...

  3. Spring 多线程的事务处理

    问题起因 Spring 的 JDBC 相关的依赖库已经提供了对 JDBC 类事务处理的统一解决方案,在正常情况下,我们只需要在需要添加事务的业务处理方法上加上 @Transactional 注解即可开 ...

  4. 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 09.第二个游戏规则:玩家重生

    斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 本文对应课程15章,60 - Refining Player Respawns. 在本篇文章中,将会为游戏新增加 ...

  5. hystrix的熔断降级

    hystrix的熔断降级 结合Feign使用 1.A服务通过B服务的唯-标识,从Nacos获取到可调用列表. 2.使用feigh中的Http发起远程请求. 3.超过默认配置的时限,抛出异常,结束该线程 ...

  6. jQuery模糊匹配checkbox全选 value实现checkbox部分或全部全选

    本文章总结jQuery实现checkbox三种情况的全选功能 第一种:等值全选,也称name的等值全选,通过checkbox的名称name实现. 第二种:模糊全选,也称id模糊全选,通过checkbo ...

  7. 基于jdk自带httpserver开发的最小完整MVC框架

    基于jdk自带httpserver开发的最小完整MVC框架 410kb级的完整MVC:solon(83k) + jdkhttp(27k) + enjoy(227k) + snack3(73k) DEM ...

  8. CountDownLatch、CyclicBarrier 使用区别

    主要区别 CountDownLatch:所有子线程完成后,再执行主线程 CyclicBarrier: 所有子线程就绪后,再执行子线程 CountDownLatch 所有子线程完成后,再执行主线程 多线 ...

  9. vue 基础学习 一

    1. vue 使用快速入门三步走 (1) 新建 HTML 页面,引入 Vue.js文件 <!DOCTYPE html> <html> <head> <meta ...

  10. CMake 禁用 MSVC 编译警告 C4819

    warning C4819:该文件包含不能在当前代码页(936)中表示的字符.请将该文件保存为 Unicode 格式以防止数据丢失 if (win32) add_complie_options(/W4 ...