小鱼儿游啊游啊。。。。
全部博文(338)
分类: Android平台
2014-08-08 14:18:43
内存屏障
由于编译器的优化和缓存的使用,导致对内存的写入操作不能及时地反映出来,也就是说当完成对内存的写入操作之后,读取出来的有可能是旧的内容。我们把这种现象称为内存屏障(Memory Barrier) 。
编译器引起的内存屏障
首先让我们来看一个例子,假设有下面这样一段代码:
代码片段2.45 内存屏障示例代码
1 int flag = 0;
2
3 void wait()
4 {
5 while (flag == 0) {
6 sleep(1000);
7 }
8 ......
9 }
10
11 void wakeup()
12 {
13 flag = 1;
14 }
由于编译器的优化,当gcc 发现sleep()函数内部不会修改flag 变量时,它可能把某个寄存器分配给内存变量flag,于是上面的代码编译后可能是这个样子(为了尽量直观易懂,这个例子中采用了C 和汇编代码结合的方式来说明这个问题)。
代码片段2.46 内存屏障示例代码
1
2
3
4
void wait()
{
movl flag, %edx;
5
6
7
8
while (%eax == 0) {
sleep(1000);
}
......
9 }
在这个例子中gcc 为了优化代码,把EDX 分配给内存变量flag,这样可以减少内存访问的次数。假设现在flag 为0,线程进入睡眠状态,当它被唤醒时,会再次判断EDX 的值。在这种情况下,就算另外一个线程在某个时候调用了wakeup()把flag 设置为1,这个睡眠的线程仍然不能跳出while 循环。由此可见编译器的优化带来了副作用。即便是在单CPU 的系统上,也会出现问题。好在我们可以使用volatile 来避免这种情况,因此上面对flag 变量的定义可以修改为:
代码片段2.47 内存屏障示例代码
1 volatile int flag = 0;
这里关键字volatile 的作用是要避免编译器的优化,这样编译器就不会把某个寄存器分配给flag。编译后的代码就是,每一次对flag 的访问都是通过内存访问来进行的,从而避免了这个问题。
另外volatile 常常用于外部设备IO 寄存器访问,考虑下面的例子:
代码片段2.48 内存屏障示例代码
1
2
3
/* 假设0x80 为某一个外部设备寄存器的地址。*/
volatile usigned int *p_status = 0x80;
4
5
6
while (*p_status != ERROR) {
do_something();
}
在这个例子中,由于指针p_status 指向外部设备的某个寄存器,而外部设备随时有可能改变这个寄存器的值,因此也要通过volatile 阻止编译器优化。
通过使用volatile 可以避免编译器在优化时把寄存器分配给不必要的内存变量,从而保证对内存的修改立即反映到相关进程。但是在某些情况下,由于涉及的变量比较多,如果把每一 个变量声明为volatile 显得很烦琐。因此内核中使用另外一个方式来避免编译器优化引起的副作用。其代码如下:
代码片段2.49 节自include/linux/compiler-gcc.h
1 #define barrier() __asm__ __volatile__("": : :"memory")
这条汇编指令指令部分、输出部分、输入部分都为空,唯独损坏部分为"memory",它告诉gcc 内存已经被修改了。当gcc 遇到这条指令时,gcc 会插入必要的指令重新刷新内存和它对应的寄存器。那么这个barrier()如何使用呢?我们来看内核中的一个实际例子。
代码片段2.50 节自kernel/sched.c
static inline void
context_switch(struct rq *rq,
struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
11 ......
12 /* Here we just switch the register state and the stack. */
13 switch_to(prev, next, prev);
14 barrier();
15 finish_task_switch(this_rq(), prev);
16 }