# Linux 内核内存屏障原理三
# 内存屏障使用例子
写屏障
首先我们先来看看写屏障,我们知道该屏障只对内存访问的写操作控制顺序。看如下写内存操作序列:
CPU 1
=======================
STORE A = 1
STORE B = 2
STORE C = 3
<write barrier>
STORE D = 4
STORE E = 5
2
3
4
5
6
7
8
当上述夹杂着内存屏障的写操作按上述顺序向内存提交时,在拥有缓存一致性的系统中,将会存在该顺序:
{ STORE D, STORE E} 写操作集将永远发生在 { STORE A,STORE B, STORE C } 写操作集之后,也即:其他 CPU 看到 D或者E 的值为 4 或者 5时,A、B、C的值一定为 1,2,3(这里忽略 数据依赖屏障和读屏障哈 ,假如 不需要读屏障便能保证读取最新值!!!!)。但,各位一定要注意:由于 A、B、C 和 D、E 之间不存在写屏障,所以, A、B、C 可以乱序写出,同理 D、E 也是,所以我们这里将 屏障前的 ABC写入打包看成一个写操作,屏障后的 DE写入打包看成一个写操作,于是,读者可以这么理解 :
STORE A,B,C
<write barrier>
STORE D,E
2
3
于是,我们得到如下描述:
# 数据依赖屏障
我们接着看数据依赖屏障,该屏障用于两个相邻的 加载操作,保证依赖顺序。来看如下例子:
CPU 1 CPU 2
======================= =======================
{ B = 7; X = 9; Y = 8; C = &Y } // 初始值
STORE A = 1
STORE B = 2
<write barrier>
STORE C = &B LOAD X
STORE D = 4 LOAD C ( 此时获取到 C 为 B的地址 )
LOAD *C ( 然后读取 B 地址处的值 )
2
3
4
5
6
7
8
9
CPU 1 使用了写屏障,保证了 { C、D } 集 和 { A、B } 集 的写出顺序。但,若没有数据依赖屏障的干预,CPU 2 还是可能读取到错误的 C 值,尽管CPU 1 已经使用了写屏障:
在上述的例子中,CPU 2 可能观测到 B的旧值 7,尽管 CPU 2 读取到了最新的 C 值为 指向 B 的地址。那么为了让 CPU 2 读取到最新的 B 的值 2 ,那么我们可以使用数据依赖屏障:
CPU 1 CPU 2
======================= =======================
{ B = 7; X = 9; Y = 8; C = &Y }
STORE A = 1
STORE B = 2
<write barrier>
STORE C = &B LOAD X
STORE D = 4 LOAD C
<data dependency barrier>
LOAD *C
2
3
4
5
6
7
8
9
10
那么描述图如下:
# 读屏障
接着我们来看读屏障的例子,我们知道读屏障仅仅对加载操作保证顺序。看如下执行序列:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 } // 初始时变量值
STORE A=1
<write barrier>
STORE B=2
LOAD B
LOAD A
2
3
4
5
6
7
8
若我们不使用读屏障,那么CPU 2 或许读取到 A 和 B 值得任意值(新值或者旧值),尽管 CPU 1 使用 写屏障按照顺序 将 A 和 B 的最新值写入了内存:
为了避免这种现象,我们可以使用如下的读屏障:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<write barrier>
STORE B=2
LOAD B
<read barrier>
LOAD A
2
3
4
5
6
7
8
9
此时,很完美: CPU 1 按照顺序写出 A 和 B的修改值, CPU 2 读屏障保证读取到 A 和 B的最新值:
为了更完整的理解读屏障,我们来看如下代码,此时在读屏障的两边都加载了变量 A:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<write barrier>
STORE B=2
LOAD B
LOAD A [第一次加载 A]
<read barrier>
LOAD A [第二次加载 A]
2
3
4
5
6
7
8
9
10
此时,即使代码中我们编写的执行顺序为 : 两次加载 A 都在加载 B变量之后,但此时仍然会出现读取到不同的值的现象,不难看出,由于第一次读取时,没有读屏障保证看到 A 的最新值,所以读取到了 A的旧值,但第二次读取发生在屏障之后,此时读取到了A的最新值。
当然,也有可能 CPU 2 在读屏障之前观察到了 A 的最新值(比如:cache bank 空闲,直接更新成功)此时将会出现如下顺序:
从上述的例子中,我们很容易得出结论:读屏障保证了 第二次读取的 A的值一定为最新值,但并不保证 读屏障之前的读取,能获取到最新值。所以自然出现 : B = 2 但 第一次读取 A = 1 或者 A = 0 的现象。
# 读屏障对预加载的影响
现代 cpu 通常会对 内存数据的加载操作 进行预加载:也就是说,CPU 可以提前预读指令,当发现一些加载指令,那么就会找到一些内存访问总线空闲的时间点,来对这些加载数据的指令进行预操作,尽管它们实际上还没有在指令执行流开始执行。这样的预加载充分利用了总线的空闲周期,大幅度提升了CPU的性能,但是,也带来了一些奇怪的现象。
这样的预加载结果可能是CPU实际上并不需要这个值,比如我们在执行分支判断时,由于判断失败,此时根本不需要分支内部已经加载的数据,在这种情况下,CPU可以选择丢弃这个值,也可以进行缓存,当以后需要使用时不需要重新加载。
来看如下指令流:
CPU 1 CPU 2
======================= =======================
LOAD B
DIVIDE 执行除法指令,对于CPU而言,相对耗时,所以此时内存访问总线空闲
DIVIDE
LOAD A
2
3
4
5
6
那么对于 CPU 1而言,将会执行如下流程:
那么,我们来看下,若我们在加载A之前放置一个读屏障,看如下执行流:
CPU 1 CPU 2
======================= =======================
LOAD B
DIVIDE
DIVIDE
<read barrier>
LOAD A
2
3
4
5
6
7
此时,该读屏障将导致 CPU 重新评估 预加载的 A 变量值是否应该被使用 ------ 若未被修改,那么直接使用:
那么,若 A变量被修改了呢?来看如下执行流: