计算机学习微信公众号(jsj_xx) 1 前言 内存屏障是搞软件的需要面对的一个涉及硬件cpu的问题,很多人困惑不解。本文是我们对linux内核内存屏障的理解,参考linux内核(4.0版本)的Documentation/memory-barriers.txt。 2 内容 主要内容如下:
好,让我们开始遐想(本文需要借助想象力,否则。。。)吧! 2.1 内存访问的抽象模型 如下图,多个cpu共同访问一个内存的场景(模型): 我们的理解是:只要能保证程序逻辑正确执行,cpu和complier(为了提升性能)怎么个乱序(优化)都行! 举例说明,如下:
cpu1有2条指令,cpu2也有2条指令,共4条指令,总共有24种执行顺序(其实就是4的阶乘),够多了吧!(更可怕的是,这还是建立在一个重要假设之下的:假设cpu上执行顺序和其它cpu感知的是一样的!否则。。。) 就这个例子而言,我们只关注x和y组成的可能结果,不外乎4种(其实就是2*2):
再举个例子:
只关注Q和D组成的结果的话,会出现“Q == 3”的结果么?cpu2这里有数据依赖性(或者说程序逻辑性):必须先取Q,再取*Q!这样看,cpu2肯定是先执行“Q = P”,所以一定不会出现“Q == 3”的结果了。 再看一个例子:
A是地址端口寄存器,D是数据端口寄存器。很明显,此时必须先放地址,再读数据,我们肯定认为只能这一种顺序,但是谁也保证不了! 至此,我们停顿下来,做个分析。貌似很乱了,软件根本搞不定,感觉是个硬件问题啊,那就让硬件做些限制(保证)吧!(cpu必须得做一些前提保证,否则软件世界大乱。。。) 前面说了,cpu会按照自己的(优化)顺序去执行指令,但一定不能违反一个大准则:程序本身的逻辑顺序。它会做如下保证: 1)有依赖的,保证保持现有顺序。 比如下面指针使用的例子:
smp_read_barrier_depends()一般为空,也就是说大部分的cpu是(不需要特殊处理)保证这种顺序的:因为得保持程序自身的依赖关系! 2)保证对重叠操作的处理保序 所谓重叠操作就是对同一内存地址的连续处理。 比如:
对地址X的处理顺序的两条指令就是重叠操作,cpu会保证现有顺序。 3)对毫无关系的指令,cpu保证你猜不出顺序!比如:
这样的指令序列,会有几种可能的顺序?6种(3的阶乘)顺序,哪种都可能! 4)cpu保证对重叠部分可能合并,可能覆盖! 比如:
此时可能有:
再比如:
此时可能有:
可见,重叠时,cpu可能会合并(又可能导致覆盖)。 cpu能做出以上保证,算给软件稍微(一点点)减负了。 特别需要注意的是位域结构:一般地,位域操作不是线程安全的!就是说,对同一个结构体内的不同位域成员(即使连续定义的成员)的多线程访问是无法保证线程安全的:
我们来仔细分析这个问题,先看跟位域相关的一个memory location定义:
所谓memory location,指的是一个标量类型对象或一个最大的连续非0长度位域组。特别地,0长度位域会单独霸占一个memory location,从而隔离出memory location! 那memory location到底对线程安全有何影响?对同一memory location的访问(包括更新)不是线程安全的;对不同memory location的访问(包括更新)则是线程安全的。 更具体地讲,对于一个结构体(各个字段的类型,可能是位域,也可能不是)而言,我们总结如下几个要点:
综上,要使访问位域线程安全化,可以采用锁,也可以在两个位域之间插入0长度位域(虽然有点浪费空间)。 好了,我们这次就讲到这里。总之,每个控制主体(compiler、各个cpu、程序逻辑本身)都会有自己所期望的顺序,那如何协调呢?下次开始讲什么是内存屏障。。。(未完待续) 关于我们 新浪微博(@NP等不等于P) 计算机学习微信公众号(jsj_xx) 原创技术文章,感悟计算机,透彻理解计算机! |
|