Chinaunix首页 | 论坛 | 博客
  • 博客访问: 238194
  • 博文数量: 46
  • 博客积分: 1410
  • 博客等级: 上尉
  • 技术积分: 622
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-17 20:07
文章分类

全部博文(46)

文章存档

2011年(1)

2010年(23)

2009年(22)

我的朋友

分类: 嵌入式

2010-03-27 19:43:49

内存屏障

        由于编译器的优化和缓存的使用,导致对内存的写入操作不能及时地反映出来,也就是说当完成对内存的写入操作之后,读取出来的有可能是旧的内容。我们把这种 现象称为内存屏障(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 }

         在内核中context_switch()负责从当前进程切换到另外一个进程环境中,但是由于当前进程需要修改某些内核数据结 构,这些修改需要及时地反映到另外一个进程中。因此在这里插入barrier(),这样gcc 会采取必要的措施,以保证内存变量和对应的寄存器的一致性。

需要主意的是,这个动作是在编译期发生的。

本文摘自《独辟蹊 径品内核:Linux内核源代码导读》

阅读(1715) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~