>>>问题:
在/kernel/sched.c中有一句:
...
(*(unsigned long *)&jiffies)++
这么理解妥否: 取jiffies地址,强行转换为unsigned long型的指针,此地址指向的目标变量进行++操作。
为何要这样绕圈子,不就是jiffies++操作吗?
>>>回应:
by 硅谷农民 on Friday June 07 2002 @ 02:08PM EDT
有网友问为什么在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 $4, %esp
movl $777, -4(%ebp)
leal -4(%ebp), %eax
incl (%eax)
movl $0, %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 $4, %esp
movl $777, -4(%ebp)
movl -4(%ebp), %eax
incl %eax
movl %eax, -4(%ebp)
movl $0, %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 $4, %esp
movl $777, -4(%ebp)
leal -4(%ebp), %eax
incl (%eax)
movl $0, %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!
test1.c
**************************
int i = 0; /*注意这里没有volatile*/
int main()
{
while( i==0 );
return 0;
}
**************************
首先编译没有优化的版本:
gcc -o test1 test1.c
objdump -d test1
0804841c <main>:
804841c: 55 push %ebp
804841d: 89 e5 mov %esp,%ebp
804841f: 90 nop
8048420: 83 3d 8c 95 04 08 00 cmpl $0x0,0x804958c
8048427: 74 f7 je 8048420 <main+0x4>
8048429: b8 00 00 00 00 mov $0x0,%eax
804842e: 5d pop %ebp
804842f: c3 ret
注意,生成的代码是直接用内存中的i和0比较的
这在多线访问时没有问题
接下来看看优化的版本
gcc -o test1 -O2 test1.c
objdump -d test1
0804841c <main>:
804841c: 55 push %ebp
804841d: 89 e5 mov %esp,%ebp
804841f: a1 8c 95 04 08 mov 0x804958c,%eax
8048424: 85 c0 test %eax,%eax
8048426: 74 fc je 8048424 <main+0x8> /*注意跳转的地址*/
8048428: 31 c0 xor %eax,%eax
804842a: 5d pop %ebp
804842b: c3 ret
看到了吧,将i的值装到了eax中,而且以后的循环中不会重新取i的值
如果进入循环时点值为0,即使i的值以后被其他的线程改变了,这个循环
“永远”也退不出来了。
下面在i的定义前加一个volatile修饰
test2.c
**************************
volatile int i = 0;
int main()
{
while( i==0 );
return 0;
}
**************************
gcc -o test2 -O2 test2.c
objdump -d test2
0804841c <main>:
804841c: 55 push %ebp
804841d: 89 e5 mov %esp,%ebp
804841f: 90 nop
8048420: a1 8c 95 04 08 mov 0x804958c,%eax
8048425: 85 c0 test %eax,%eax
8048427: 74 f7 je 8048420 <main+0x4> /*注意跳转的地址*/
8048429: 31 c0 xor %eax,%eax
804842b: 5d pop %ebp
804842c: c3 ret
这回不同了,每次循环都会到内存中将i的值重新装入寄存器eax并进行新的比较。
这样,在多线的环境下就没有问题了