内存屏障
这部分的内容,主要来自于1的学习笔记。
为什么需要内存屏障
因为下面几个原因,导致需要内存屏障:
编译器优化代码
CPU乱序执行
内存一致性
内存合并操作
内存预读取
分支预测
PS: 在1的GUARANTEES部分,提到了编译器和CPU会保证顺序的情况--访问的变量有前后依赖的情况。
而内存屏障提供了一种方法,让代码的执行顺序能按照我们写的来。
PS: 实际上应该是内存屏障和编译屏障一起提供保障。
内存屏障的种类
通常我们看到的种类有:
Write memory barrier
Read memory barrier
General memory barrier
这几个的定义可以用一句话概括:
all the LOAD/STORE operations specified before the barrier will appear to happen before all the LOAD/STORE operations specified after the barrier with respect to the other components of the system.
以及隐含变体:
ACQUIRE operation
RELEASE operation
这两个我感觉以RCU里面的subscribe/publish来理解可能容易点。
RELEASE对应的是publish,而ACQUIRE对应的是subscribe。之前我一直认为RELEASE是释放的意思,但感觉这里解释为发布更为合适。
Address Dependency Barrier(Historical)
我们先看一个例子:
这里面有个很神奇的情况,就是从CPU2的角度看P已经赋值为&B,但是B还是2。(这种情况会发生在split cache的机器上)
具体解决方法是使用READ_ONCE(),因为当前的READ_ONCE()里隐藏了address dependency。
这里需要关注的是,如果没有隐藏的address dependency,会影响rcu的功能。 我们可以把P看作一个全局指针,B是一个新的版本。当CPU2认为P已经更新到B这个版本时,如果看到的B里内容不是最新的,那就有问题了。
Control Dependency
这部分主要是因为编译器会对if这样的判断语句做优化,导致代码不按照我们的预期执行。
从形式上看,又可以分成两类:
load-load control dependency
load-store control dependency
load-load
比如这样的情况
两个READ_ONCE之间没有地址上的依赖,一个是从a读,一个是从b读,所以CPU可以打乱两者的顺序。 所以正确的写法是
PS: 如果是单线程纯内存访问,不加barrier可能也没问题。最后还是要判断q是不是非空,才会赋值到p。但是如果是访问设备寄存器的话,就必须加barrier了。
load-store
写的情况稍微好些,因为之间有一定的地址依赖:
PS: 其实我好像没有看出来有依赖,但意思就是能。而且必须是先读后写。
另外重要的是READ_ONCE和WRITE_ONCE是必须要的。
后面还有几个编译器能预测出if结果的例子,这里就不展开了。
SMP Barrier Pairing
这一部分主要关注的是多个CPU访问同一段内存的情况,这个问题是由内存一致性引入的。
为了解决这个问题,通常需要内存屏障成对出现。
例如:
CPU1/CPU2中各自要加上屏障,才能保证CPU2上读到B==2后,A等于1。
内核中显式屏障
这个又分成两种:
compiler barrier
cpu memory barrier
Compiler Barrier
barrier()
READ_ONCE()/WRITE_ONCE()
不过READ_ONCE()/WRITE_ONCE()还有点cpu memory barrier的作用。
比如这个例子:
说是可以防止a1的值不会比a[0]的旧,而仅仅是编译器屏障,是不能保证cpu不会乱序执行的。
另外还有个例子:
作者说下面的代码
会被编译器优化成
所以以后像while/if这种判断里面,最好是不要做赋值操作了。
或者也会因为寄存器不够用,将上面的代码优化成:
这样如果有另一个线程在do_something_with()前更改了a,那就不是我们想要的行为了。
所以这种情况需要写成:
CPU memory barrier
内核中其中基本的内存屏障:
General
mb()
smp_mb()
Write
wmb()
smp_wmb()
Read
rmb()
smp_rmb()
Address Dependency
READ_ONCE()
除了Address Dependency, 都隐含了编译器屏障。
内核中隐含屏障
除了显式内存屏障,内核中有些接口隐含调用了内存屏障:
锁
睡眠/唤醒
调度
哪里需要用到内存屏障?
代码执行顺序重排,在下面几个情况会产生问题:
interprocessor interaction
atomic operation
accessing device
interrupt
内存模型模拟
在1中,提到了一个内存模型的模拟器herd7,以及内核中相关验证代码在tools/memory-model目录。
另外还找到一篇文章简述了herd7的使用。
一些例子
Double-checked locking
内核文档 Documentation/litmus-tests/locking/DCL-broken.litmus 写了一个很有意思的问题。
对应还有解释在tools/memory-model/Documentation/locking.txt。
下面这段代码是有问题的
因为在上面标出来的两对代码可能会被重排,所以需要用smp_load_acquire()/smp_store_release()修复。
参考资料
Last updated