Linux内存屏障浅析
根据该文章整理 https://blog.csdn.net/myxmu/article/details/8035025
1 解决的问题
内存屏障主要解决了单处理器下的乱序问题和多处理器下的内存同步问题。
2 为什么会乱序
现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。相反,流水线是并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段,而两条加法指令在“执行”阶段就只能串行工作。
相比于串行+阻塞的方式,流水线像这样并行的工作,效率是非常高的。
然而,这样一来,乱序可能就产生了。比如一条加法指令原本出现在一条除法指令的后面,但是由于除法的执行时间很长,在它执行完之前,加法可能先执行完了。再比如两条访存指令,可能由于第二条指令命中了cache而导致它先于第一条指令完成。一般情况下,指令乱序并不是CPU在执行指令之前刻意去调整顺序。CPU总是顺序的去内存里面取指令,然后将其顺序的放入指令流水线。但是指令执行时的各种条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。
指令流水线除了在资源不足的情况下会卡住之外(如前所述的一个加法器应付两条加法指令的情况),指令之间的相关性也是导致流水线阻塞的重要原因。
CPU的乱序执行并不是任意的乱序,而是以保证程序上下文因果关系为前提的。有了这个前提,CPU执行的正确性才有保证。比如:
a++;
b=f(a);
c--;
由于b=f(a)这条指令依赖于前一条指令a++的执行结果,所以b=f(a)将在“执行”阶段之前被阻塞,直到a++的执行结果被生成出来;而c--跟前面没有依赖,它可能在b=f(a)之前就能执行完。(注意,这里的f(a)并不代表一个以a为参数的函数调用,而是代表以a为操作数的指令。C语言的函数调用是需要若干条指令才能实现的,情况要更复杂些。)
像这样有依赖关系的指令如果挨得很近,后一条指令必定会因为等待前一条执行的结果,而在流水线中阻塞很久,占用流水线的资源。而编译器的乱序,作为编译优化的一种手段,则试图通过指令重排将这样的两条指令拉开距离,以至于后一条指令进入CPU的时候,前一条指令结果已经得到了,那么也就不再需要阻塞等待了。比如将指令重排为:
a++; c--; b=f(a);
相比于CPU的乱序,编译器的乱序才是真正对指令顺序做了调整。但是编译器的乱序也必须保证程序上下文的因果关系不发生改变。
3 乱序的后果
乱序执行,有了“保证上下文因果关系”这一前提,一般情况下是不会有问题的。因此,在绝大多数情况下,我们写程序都不会去考虑乱序所带来的影响。但是,有些程序逻辑,单纯从上下文是看不出它们的因果关系的。比如:
*addr=5;
val=*data;
从表面上看,addr和data是没有什么联系的,完全可以放心的去乱序执行。但是如果这是在某某设备驱动程序中,这两个变量却可能对应到设备的地址端口和数据端口。并且,这个设备规定了,当你需要读写设备上的某个寄存器时,先将寄存器编号设置到地址端口,然后就可以通过对数据端口的读写而操作到对应的寄存器。那么这么一来,对前面那两条指令的乱序执行就可能造成错误。
对于这样的逻辑,我们姑且将其称作隐式的因果关系;而指令与指令之间直接的输入输出依赖,也姑且称作显式的因果关系。CPU或者编译器的乱序是以保持显式的因果关系不变为前提的,但是它们都无法识别隐式的因果关系。再举个例子:
obj->data = xxx;
obj->ready = 1;
当设置了data之后,记下标志,然后在另一个线程中可能执行:
if (obj->ready) do_something(obj->data);
虽然这个代码看上去有些别扭,但是似乎没错。不过,考虑到乱序,如果标志被置位先于data被设置,那么结果很可能就杯具了。因为从字面上看,前面的那两条指令其实并不存在显式的因果关系,乱序是有可能发生的。
总的来说,如果程序具有显式的因果关系的话,乱序一定会尊重这些关系;否则,乱序就可能打破程序原有的逻辑。这时候,就需要使用屏障来抑制乱序,以维持程序所期望的逻辑。
4 屏障的作用
内存屏障主要有:读屏障、写屏障、通用屏障、优化屏障、几种。
以读屏障为例,它用于保证读操作有序。屏障之前的读操作一定会先于屏障之后的读操作完成,写操作不受影响,同属于屏障的某一侧的读操作也不受影响。
类似的,写屏障用于限制写操作。而通用屏障则对读写操作都有作用。而优化屏障则用于限制编译器的指令重排,不区分读写。前三种屏障都隐含了优化屏障的功能。
比如:
tmp = ttt;
*addr = 5;
mb();
val = *data;
有了内存屏障就了确保先设置地址端口,再读数据端口。而至于设置地址端口与tmp的赋值孰先孰后,屏障则不做干预。
内存屏障可以在隐式因果关系的场景中,保证因果关系逻辑正确。
5 多处理器情况
前面只是考虑了单处理器指令乱序的问题,而在多处理器下,除了每个处理器要独自面对上面讨论的问题之外,当处理器之间存在交互的时候,同样要面对乱序的问题。
一个处理器(记为a)对内存的写操作并不是直接就在内存上生效的,而是要先经过自身的cache。另一个处理器(记为b)如果要读取相应内存上的新值,先得等a的cache同步到内存,然后b的cache再从内存同步这个新值。而如果需要同步的值不止一个的话,就会存在顺序问题。再举前面的一个例子:
<CPU-a> <CPU-b>
obj->data = xxx;
wmb(); if (obj->ready)
obj->ready = 1; do_something(obj->data);
前面也说过,必须要使用屏障来保证CPU-a不发生乱序,从而使得ready标记置位的时候,data一定是有效的。但是在多处理器情况下,这还不够。data和ready标记的新值可能以相反的顺序更新到CPU-b上!
其实这种情况在大多数体系结构下并不会发生,不过内核文档memory-barriers.txt举了alpha机器的例子。alpha机器可能使用分列的cache结构,每个cache列可以并行工作,以提升效率。而每个cache列上面缓存的数据是互斥的(如果不互斥就还得解决cache列之间的一致性),于是就可能引发cache更新不同步的问题。
假设cache被分成两列,而CPU-a和CPU-b上的data和ready都分别被缓存在不同的cache列上。
首先是CPU-a更新了cache之后,会发送消息让其他CPU的cache来同步新的值,对于data和ready的更新消息是需要按顺序发出的。如果cache只有一列,那么指令执行的顺序就决定了操作cache的顺序,也就决定了cache更新消息发出的顺序。但是现在假设了有两个cache列,可能由于缓存data的cache列比较繁忙而使得data的更新消息晚于ready发出,那么程序逻辑就没法保证了。不过好在SMP下的内存屏障在解决指 令乱序问题之外,也将cache更新消息乱序的问题解决了。只要使用了屏障,就能保证屏障之前的cache更新消息先于屏障之后的消息被发出。
然后就是CPU-b的问题。在使用了屏障之后,CPU-a已经保证data的更新消息先发出了,那么CPU-b也会先收到data的更新消息。不过同样,CPU-b上缓存data的cache列可能比较繁忙,导致对data的更新晚于对ready的更新。这里同样会出问题。
所以,在这种情况下,CPU-b也得使用屏障。CPU-a上要使用写屏障,保证两个写操作不乱序,并且相应的两个cache更新消息不乱序。CPU-b上则需要使用读屏障,保证对两个cache单元的同步不乱序。可见,SMP下的内存屏障一定是需要配对使用的。
因此上面的例子应该改写成:
<CPU-a> <CPU-b>
obj->data = xxx; if (obj->ready)
wmb(); rmb();
obj->ready = 1; do_something(obj->data);
CPU-b上使用的读屏障还有一种弱化版本,它不保证读操作的有序性,叫做数据依赖屏障。顾名思义,它是在具有数据依赖情况下使用的屏障,因为有数据依赖(也就是之前所说的显式的因果关系),所以CPU和编译器已经能够保证指令的顺序。
再举个例子:
<CPU-a> <CPU-b>
init(newval); p = data;
<write barrier> <data dependency barrier>
data = &newval; val = *p;
这里的屏障就可以保证:如果data指向了newval,那么newval一定是初始化过的。
Linux内存屏障浅析的更多相关文章
- Linux内存管理原理
本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址又叫线性地址.linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)(在用户态,内核态逻 ...
- Linux内存管理原理【转】
转自:http://www.cnblogs.com/zhaoyl/p/3695517.html 本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址 ...
- Windows内存管理和linux内存管理
windows内存管理 windows 内存管理方式主要分为:页式管理,段式管理,段页式管理. 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页:页式管理把内存空间按照页的大小划分成片或 ...
- 深入理解Linux内存分配
深入理解Linux内存分配 为了写一个用户层程序,你也许会声明一个全局变量,这个全局变量可能是一个int类型也可能是一个数组,而声明之后你有可能会先初始化它,也有可能放在之后用到它的时候再初始化.除此 ...
- 转 Linux内存管理原理
Linux内存管理原理 在用户态,内核态逻辑地址专指下文说的线性偏移前的地址Linux内核虚拟3.伙伴算法和slab分配器 16个页面RAM因为最大连续内存大小为16个页面 页面最多16个页面,所以1 ...
- Linux并发与同步专题 (1)原子操作和内存屏障
关键词:. <Linux并发与同步专题 (1)原子操作和内存屏障> <Linux并发与同步专题 (2)spinlock> <Linux并发与同步专题 (3) 信号量> ...
- Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁
在上一篇博文中笔者讨论了关于原子操作和自旋锁的相关内容,本篇博文将继续锁机制的讨论,包括内存屏障.读写自旋锁以及顺序锁的相关内容.下面首先讨论内存屏障的相关内容. 三.内存屏障 不知读者是是否记得在笔 ...
- 大话Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁
大话Linux内核中锁机制之内存屏障.读写自旋锁及顺序锁 在上一篇博文中笔者讨论了关于原子操作和自旋锁的相关内容,本篇博文将继续锁机制的讨论,包括内存屏障.读写自旋锁以及顺序锁的相关内容.下面首先讨论 ...
- linux内核同步之每CPU变量、原子操作、内存屏障、自旋锁【转】
转自:http://blog.csdn.net/goodluckwhh/article/details/9005585 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[-] 一每 ...
随机推荐
- Redis持久化存储(三)
redis高级特性-发布订阅消息服务功能 Pub/Sub 订阅,取消订阅和发布实现了发布/订阅消息范式(引自wikipedia),发送者(发布者)不是计划发送消息给特定的接收者(订阅者).而是发布的消 ...
- 【Linux常见命令】route命令
route - show / manipulate the IP routing table route命令用于显示和操作IP路由表. route命令用来显示并设置Linux内核中的网络路由表,rou ...
- 【Linux常见命令】cat命令
cat - concatenate files and print on the standard output cat 命令用于连接文件并打印到标准输出设备上. 用法: 1. cat file 查看 ...
- 命令替换、权限、chmod、特殊权限
命令替换 把字符串里面的命令先执行再把该字符串输出,与PHP的""里面的变量被执行一样. $(COMMAND) `COMMAND` [root@jiakang ~]# echo & ...
- Hadoop学习笔记(一)——安装与配置
操作系统:Ubuntu 15.04; Hadoop version: 2.6.4 参考文献: Ubuntu上搭建Hadoop环境(单机模式+伪分布模式)
- 面向对象第四单元(UML)总结
OO第四单元 一.总结本单元两次作业的架构设计 第一次作业 架构 第一次作业只有类图,所以全部的UmlElement都可以放在MyUmlInteraction中进行存储.计算和查找.对于类图来说,可以 ...
- C语言入门经典题目及其答案
写在开始: 我叫风骨散人,名字的意思是我多想可以不低头的自由生活,可现实却不是这样.家境贫寒,总得向这个世界低头,所以我一直在奋斗,想改变我的命运给亲人好的生活,希望同样被生活绑架的你可以通过自己的努 ...
- bzoj 4152[AMPPZ2014]The Captain
bzoj 4152[AMPPZ2014]The Captain 给定平面上的n个点,定义(x1,y1)到(x2,y2)的费用为min(|x1-x2|,|y1-y2|),求从1号点走到n号点的最小费用. ...
- 《Docker从入门到跑路》之网络模型介绍
Bridge模式 当我们安装完docker后,启动Docker daemon,就会在主机上看到一个docker0的网桥,默认在此主机上启动的容器都会连接到这个网桥上.虚拟网桥的工作方式和物理交换机的工 ...
- 首次使用AWS服务器EC2
AWS有一年的免费套餐,这个便宜我得占. 申请的时候需要填写银行卡,AWS暂不支持储蓄卡,只好绑信用卡了. 创建EC2实例之后,下一个要解决的问题就是远程root访问. 1. 修改安全组设置 2. s ...