volatile中文的意思是易于挥发的。在C语言中,如果一个变量被声明为volatile,则说明这个变量每回都要从内存读到寄存器中,操作完以后, 再将值写回到内存中,编译器(例如gcc)并不将值cache在寄存器中。对于多线程或SMP系统,要注意使用volatile。
int wait = TRUE;


thread1() thread2()
{ {
while (wait) { wait = FALSE;
sleep(1000); ...
} }
}

当 编译器看到thread1调用sleep的时候,它认为sleep是一个外部模块的函数,不可能改变wait的值,所以它"可能"就进行代码优化,将 wait的值cache在寄存器中,以提高效率。假如另外一个线程thread2开始运行,将wait改成FALSE,如果CPU没有预测到wait变量 的依赖关系的话,thread1就会一直sleep下去。在这种情况下,volatile可以强制编译器不将wait缓存到寄存器中。注意,一般情况下, CPU是能检测到wait被改过了,从而将寄存器刷新,但是这和CPU的体系结构有关,Linux是能支持多种体系结构的,所以经常使用 volatile。
有网友问为什么在do_timer()函数中,有一句(*(unsigned long *)&jiffies)++,为什么不直接用jiffies++呢? 这个问题和两个原因有关:
* jiffiers是volatile变量
* CPU可能会重新排序指令


我编了3个小程序来测试,

jiffier1.c


int main()
{
unsigned long jiffiers = 777;


jiffiers++;
return 0;
}

gcc -S jiffer1.c,生成的汇编为

.file "jiffier1.c"
.version "01.01"
gcc2_compiled.:
.text
.align 4
.globl main
.type main,@function
main:
pushl %ebp
movl %esp, %ebp
subl , %esp
movl 7, -4(%ebp)
leal -4(%ebp), %eax
incl (%eax)
movl , %eax
leave
ret
.Lfe1:
.size main,.Lfe1-main
.ident "GCC: (GNU) 2.96 20000731 (Red Hat Linux 7.3 2.96-110)"


从汇编中,可以看到jiffiers的值是直接在内存里加一的。

jiffier2.c


int main()
{
volatile unsigned long jiffiers = 777;
jiffiers++;
return 0;
}

生成的汇编为

.file "jiffier2.c"
.version "01.01" gcc2_compiled.:
.text
.align 4
.globl main
.type main,@function
main:
pushl %ebp
movl %esp, %ebp
subl , %esp
movl 7, -4(%ebp)
movl -4(%ebp), %eax
incl %eax
movl %eax, -4(%ebp)
movl , %eax
leave
ret
.Lfe1:
.size main,.Lfe1-main
.ident "GCC: (GNU) 2.96 20000731 (Red Hat Linux 7.3 2.96-110)"

当jiffiers被声明为volatile时,在加一之前,会先从内存中将值读到寄存器%eax中, 直接都寄存器操作,完了以后不做cache,将值写回内存。它需要3条指令才完成了加一 操作,

movl -4(%ebp), %eax
incl %eax
movl %eax, -4(%ebp)


也就是说,jiffiers++并不是原子操作,在多处理器环境中,
问题就出来了,jiffers有可能被加了两次,内存中却只是加了1。
另外的原因是这3条指令执行的时候,可能会被重新排序,从而破坏了操作的原子性。
当然我们也可以直接调用汇编指令加一,但是do_timers函数是独立于体系结构的,
所以Linux使用的一种最简单的方法,也就是(*(unsigned long *)&jiffiers)++
来解决这些问题。

jiffier3.c


int main()
{
volatile unsigned long jiffiers = 777;


(*(unsigned long *)&jiffiers)++;


return 0;
}

生成的汇编为
.file "jiffier3.c"
.version "01.01" gcc2_compiled.:
.text
.align 4
.globl main
.type main,@function
main:
pushl %ebp
movl %esp, %ebp
subl , %esp
movl 7, -4(%ebp)
leal -4(%ebp), %eax
incl (%eax)
movl , %eax
leave
ret
.Lfe1: .size main,.Lfe1-main
.ident "GCC: (GNU) 2.96 20000731 (Red Hat Linux 7.3 2.96-110)"

可 以看出来,生成的指令和jiffer1.c的一模一样,也就是说, (*(unsigned long *)&jiffiers)++将volatile的jiffiers转换成一般的内存变量, 避免了用寄存器做cache,从而保证了jiffiers加一操作的原子性。
Linux一开始也是直接用jiffiers++的,到后来的版本才改成(*(unsigned long *)&jiffiers)++, 可想而知写一个牢固的OS是不容易的,要解决的隐含细节非常多。
Good Luck, Linux!