[转帖]什么是内存屏障? Why Memory Barriers ?
要了解如何使用memory barrier,最好的方法是明白它为什么存在。CPU硬件设计为了提高指令的执行速度,增设了两个缓冲区(store buffer, invalidate queue)。这个两个缓冲区可以避免CPU在某些情况下进行不必要的等待,从而提高速度,但是这两个缓冲区的存在也同时带来了新的问题。下面我们一步步来分析说明
1. cache一致性问题
-
Cache 一致性问题出现的原因是在一个多处理器系统中,每个处理器核心都有独占的Cache 系统(比如一级 Cache 和二级 Cache),而导致一个内存块在系统中同时可能有多个备份,从而引起访问时的不一致性问题。
-
-
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]。常见的消息类型:
-
read: 包含要读取的CACHE-LINE的物理地址
-
read response: 包含READ请求的数据,要么由内存满足要么由cache满足
-
invalidate: 包含要invalidate的cache-line的物理地址,所有其他cache必须移除相应的数据项
-
invalidate ack: 回复消息
-
read invalidate: 包含要读取的cache-line的物理地址,同时使其他cache移除该数据。需要read response和invalidate ack消息
-
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
试考虑如下代码:
-
a = 1;
-
b = a + 1;
-
assert(b == 2);
假设初始时a和b的值都是0,a处于CPU1-cache中,b处于CPU0-cache中。如果按照下面流程执行这段代码:
- CPU 0 starts executing the a = 1
- CPU 0 looks “a” up in the cache, and finds that it is missing
- CPU 0 therefore sends a “read invalidate” message in order to get exclusive ownership of the cache line containing “a”.
- CPU 0 records the store to “a” in its store buffer.
- CPU 1 receives the “read invalidate” message, and responds by transmitting the cache line and removing that cacheline from its cache.
- CPU 0 starts executing the b = a + 1.
- CPU 0 receives the cache line from CPU 1, which still has a value of zero for “a”.
- CPU 0 loads “a” from its cache, finding the value zero.
- 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.
- 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).
- 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
请看下面的代码:
-
void foo(void)
-
{
-
a = 1;
-
b = 1;
-
}
-
-
void bar(void)
-
{
-
while (b == 0) continue;
-
assert(a == 1);
-
}
假设变量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这类关系。
解决办法是:使用硬件设计者提供的“内存屏障”来修改代码:
-
void foo(void)
-
{
-
a = 1;
-
smp_mb();
-
b = 1;
-
}
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消息,而不必等待相应的行真正变成无效状态。于是乎出现了下面的组织架构:

但是,此种方法也存在如下问题:
-
void foo(void)
-
{
-
a = 1;
-
smp_mb();
-
b = 1;
-
}
-
-
void bar(void)
-
{
-
while (b == 0) continue;
-
assert(a == 1);
-
}
- 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:
-
void bar(void)
-
{
-
while (b == 0) continue;
-
smp_mb();
-
assert(a == 1);
-
}
此处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。
上述例子完整的代码变为:
-
void foo(void)
-
{
-
a = 1;
-
smp_wmb();/*CPU1要使用该值,因此需要及时更新处理store buffer中的数据*/
-
b = 1;
-
}
-
-
void bar(void)
-
{
-
while (b == 0) continue;
-
smp_rmb();/*由于CPU0修改了a值,使用此值时及时处理Invalidation Queue中的消息*/
-
assert(a == 1);
-
}
[转帖]什么是内存屏障? Why Memory Barriers ?的更多相关文章
- 内存屏障(Memory barrier)-- 转发
本文例子均在 Linux(g++)下验证通过,CPU 为 X86-64 处理器架构.所有罗列的 Linux 内核代码也均在(或只在)X86-64 下有效. 本文首先通过范例(以及内核代码)来解释 Me ...
- JVM内存模型、指令重排、内存屏障概念解析
在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...
- [面试]volatile类型修饰符/内存屏障/处理器缓存
volatile类型修饰符 本篇文章的目的是为了自己梳理面试知识点, 在这里做一下笔记. 绝大部分内容是基于这些文章的内容进行了copy+整理: 1. http://www.infoq.com/cn/ ...
- JVM内存模型、指令重排、内存屏障概念解析(转载)
在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...
- C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI
目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...
- CPU缓存和内存屏障
CPU性能优化手段 - 缓存 为了提高程序的运行性能, 现代CPU在很多方面对程序进行了优化例如: CPU高速缓存, 尽可能的避免处理器访问主内存的时间开销, 处理器大多会利用缓存以提高性能 多级缓存 ...
- volatile关键字?MESI协议?指令重排?内存屏障?这都是啥玩意
一.摘要 三级缓存,MESI缓存一致性协议,指令重排,内存屏障,JMM,volatile.单拿一个出来,想必大家对这些概念应该有一定了解.但是这些东西有什么必然的联系,或者他们之间究竟有什么前世今生想 ...
- 多线程 - 内存屏障和cpu缓存
CPU性能优化 - 缓存 为了提高程序运行的性能,现代CPU在很多方面会对程序进行优化.CPU的处理速度是很快的,内存的速度次之,硬盘速度最慢.在cpu处理内存数据中,内存运行速度太慢,就会拖累cpu ...
- Java 内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题.Java编译器也会根据内存屏障的规则禁止重排序. 内 ...
- [SPDK/NVMe存储技术分析]006 - 内存屏障(MB)
在多核(SMP)多线程的情况下,如果不知道CPU乱序执行的话,将会是一场噩梦,因为无论怎么进行代码Review也不可能发现跟内存屏障(MB)相关的Bug.内存屏障分为两类: 跟编译有关的内存屏障: 告 ...
随机推荐
- MySQL|MySQL执行计划
使用explain关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的,分析你的查询语句或是表结构的性能瓶颈. explain执行计划包含的信息 每列的内容 列 含义 ...
- 马某 说c# 不开源,他是蠢还是坏?
马某在视频 计算机主流开发语言的现状和未来3-5年的发展前景--Java.Golang.Python.C\C#\C++.JS.前端.AI.大数据.测试.运维.网络安全 点评各种语言,其中说到C# 的时 ...
- 40. 干货系列从零用Rust编写负载均衡及代理,websocket的实现
wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代 ...
- ChatGPT的中转站(欧派API) oupuapi,不扶墙也能上楼
开启智能生活新篇章:oupo中转站(欧派)--引领人工智能大模型的枢纽 在人工智能技术日新月异的今天,我们荣幸地向您推介oupo中转站(欧派)--这一汇聚各类顶尖人工智能大模型的平台.它不仅为技术研发 ...
- static、final、private是否可以修饰抽象方法?
1.static和abstract:是不能共存的.static是为了方便调用,abstract是为了给子类重写,没有方法体. 2.final和abstract:是互相冲突的,final修饰的方法不能重 ...
- SSH默认端口从22修改为其他端口
1.在终端中使用root权限登录到您的Linux服务器. 2.打开终端,并使用适合您的文本编辑器(如vi.nano等)打开SSH配置文件.例如,通过运行以下命令之一: vi /etc/ssh/sshd ...
- .Net 系列:Attribute特性的高级使用及自定义验证实现
一.特性是什么?特性有什么用? 特性(Attribute)是用于在运行时传递程序中各种元素(比如类.方法.结构.枚举.组件等)的行为信息的声明性标签. 您可以通过使用特性向程序添加声明性信息.一个声明 ...
- 加快云原生技术转型, 智能调度登陆华为云DevOps: 增速,节源
摘要:本文将探讨智能资源调度在华为云DevOps上的应用与实践. 本文分享自华为云社区<加快云原生技术转型, 智能调度登陆华为云DevOps: 增速,节源>,作者: DevAI. 1. 背 ...
- 企业诊断屋:二手车交易平台 APP 如何用 AB 测试赋能业务
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 2023年汽车行业新车市场低靡,由新车降价引发的车辆价格波动很快传导到二手车市场,二手车的交易也受到了冲击,收车验 ...
- 如何在跨平台的环境中创建可以跨平台的后台服务,它就是 Worker Service。
一.简介 最近,有一个项目要使用 Windows 服务,来做为一个软件项目的载体.我想了想,都已经到了跨平台的时代了,会不会有替换 Windows 服务的技术出现呢?于是,在网络上疯狂的搜索了一番,真 ...