分享

理解内存屏障(一)

 mzsm 2015-05-29
作者:新浪微博(@NP等不等于P
计算机学习微信公众号(jsj_xx)
1 前言

内存屏障是搞软件的需要面对的一个涉及硬件cpu的问题,很多人困惑不解。本文是我们对linux内核内存屏障的理解,参考linux内核(4.0版本)的Documentation/memory-barriers.txt

2 内容

主要内容如下:

  • 2.1 内存访问的的抽象模型

  • 2.2 什么是内存屏障

  • 2.3 内核中的显式和隐式的内存屏障

  • 2.4 cpu间的锁和屏障的关系

  • 2.5 哪里需要使用屏障

  • 2.6 内核中io屏障的作用

  • 2.7 执行有序的最小假想模型

  • 2.8 cpu cache对屏障的影响

  • 2.9 alpha cpu

  • 2.10 一个环形缓冲区的使用样例

好,让我们开始遐想(本文需要借助想象力,否则。。。)吧!

2.1 内存访问的抽象模型

如下图,多个cpu共同访问一个内存的场景(模型):


我们的理解是:只要能保证程序逻辑正确执行,cpu和complier(为了提升性能)怎么个乱序(优化)都行!

举例说明,如下:

CPU 1CPU 2

==============================

{ A == 1; B == 2 }

A = 3;x = B;

B = 4;y = A;

cpu1有2条指令,cpu2也有2条指令,共4条指令,总共有24种执行顺序(其实就是4的阶乘),够多了吧!(更可怕的是,这还是建立在一个重要假设之下的:假设cpu上执行顺序和其它cpu感知的是一样的!否则。。。)

就这个例子而言,我们只关注x和y组成的可能结果,不外乎4种(其实就是2*2):

x == 2, y == 1

x == 2, y == 3

x == 4, y == 1

x == 4, y == 3

再举个例子:

CPU 1CPU 2

==============================

{ A == 1, B == 2, C = 3, P == &A, Q == &C }

B = 4;Q = P;

P = &BD = *Q;

只关注Q和D组成的结果的话,会出现“Q == 3”的结果么?cpu2这里有数据依赖性(或者说程序逻辑性):必须先取Q,再取*Q!这样看,cpu2肯定是先执行“Q = P”,所以一定不会出现“Q == 3”的结果了。

再看一个例子:

*A = 5;

x = *D;

A是地址端口寄存器,D是数据端口寄存器。很明显,此时必须先放地址,再读数据,我们肯定认为只能这一种顺序,但是谁也保证不了

至此,我们停顿下来,做个分析。貌似很乱了,软件根本搞不定,感觉是个硬件问题啊,那就让硬件做些限制(保证)吧!(cpu必须得做一些前提保证,否则软件世界大乱。。。)

前面说了,cpu会按照自己的(优化)顺序去执行指令,但一定不能违反一个大准则:程序本身的逻辑顺序。它会做如下保证:

1)有依赖的,保证保持现有顺序。

比如下面指针使用的例子:

ACCESS_ONCE(Q) = P; smp_read_barrier_depends(); D = ACCESS_ONCE(*Q);

smp_read_barrier_depends()一般为空,也就是说大部分的cpu是(不需要特殊处理)保证这种顺序的:因为得保持程序自身的依赖关系!

2)保证对重叠操作的处理保序

所谓重叠操作就是对同一内存地址的连续处理。

比如:

a = ACCESS_ONCE(*X); ACCESS_ONCE(*X) = b;

对地址X的处理顺序的两条指令就是重叠操作,cpu会保证现有顺序。

3)对毫无关系的指令,cpu保证你猜不出顺序!比如:

X = *A; Y = *B; *D = Z;

这样的指令序列,会有几种可能的顺序?6种(3的阶乘)顺序,哪种都可能!

4)cpu保证对重叠部分可能合并,可能覆盖!

比如:

X = *A; Y = *(A + 4);

此时可能有:

X = LOAD *A; Y = LOAD *(A + 4);

Y = LOAD *(A + 4); X = LOAD *A;

{X, Y} = LOAD {*A, *(A + 4) };

再比如:

X = *A; Y = *(A + 4);

此时可能有:

STORE *A = X; STORE *(A + 4) = Y;

STORE *(A + 4) = Y; STORE *A = X;

STORE {*A, *(A + 4) } = {X, Y};

可见,重叠时,cpu可能会合并(又可能导致覆盖)。

cpu能做出以上保证,算给软件稍微(一点点)减负了。

特别需要注意的是位域结构:一般地,位域操作不是线程安全的!就是说,同一个结构体内的不同位域成员(即使连续定义的成员)的多线程访问是无法保证线程安全的:

Do not attempt to use bitfields to synchronize parallel algorithms.

我们来仔细分析这个问题,先看跟位域相关的一个memory location定义:

memory location

either an object of scalar type, or a maximal sequence

of adjacent bit-fields all having nonzero width

所谓memory location,指的是一个标量类型对象或一个最大的连续非0长度位域组。特别地,0长度位域会单独霸占一个memory location,从而隔离出memory location!

那memory location到底对线程安全有何影响?对同一memory location的访问(包括更新)不是线程安全的;对不同memory location的访问(包括更新)则是线程安全的。

更具体地讲,对于一个结构体(各个字段的类型,可能是位域,也可能不是)而言,我们总结如下几个要点:

  • 位域类型和非位域类型之间的并发访问(包括更新),是线程安全(两个线程分别访问其中一个类型字段)的。

  • 此结构体内部和该结构体的嵌套子结构体之间的位域字段,是线程安全的。

  • 位域之间如果有0长度位域分割,则是线程安全的。

  • 位域之间如果被一个非位域分割,则是线程安全的。

  • 位域之间所有的位域都是非0位域,则是线程不安全的。

综上,要使访问位域线程安全化,可以采用锁,也可以在两个位域之间插入0长度位域(虽然有点浪费空间)。

好了,我们这次就讲到这里。总之,每个控制主体(compiler、各个cpu、程序逻辑本身)都会有自己所期望的顺序,那如何协调呢?下次开始讲什么是内存屏障。。。(未完待续)

关于我们

新浪微博(@NP等不等于P

计算机学习微信公众号(jsj_xx)

原创技术文章,感悟计算机,透彻理解计算机!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多