volatile的定义是为了防止编译器对其进行优化。
1、volatile修饰符的用途
(1)用途
用途如下:
1) 外部设备寄存器映射后的内存——因为外部设备寄存器可能会由于外部设备的状态变化而改变,因此映射后的内存需要注明为volatile的;
2)多线程或异步访问的全局变量(原子机制);
3)嵌入式汇编——防止编译器对其优化;
(2)原子性分析
对于用途2)需要进一步分析。
当全局变量由锁保护的时候,该全局变量是不需要volatile的:
1)锁保证了临界区的串行;
2)锁的实现中有内存屏障,保证临界区访问全局变量为最新值。
当没有锁保护的全局变量是需要使用volatile修饰的。
代码如下:
static volatile int counter = 0;
void add_counter(void)
{
++counter;
}
add_counter:
pushl %ebp
movl %esp, %ebp
movl counter, %eax//*
addl $1, %eax//*
movl %eax, counter//*
popl %ebp
ret
编译器会先把counter存入了寄存器eax,然后对eax进行操作,再将eax存入counter。
++counter不是原子性操作.同时有两个线程会修改这个计数器,这时这个counter必须由锁保护!
2、volatile修饰符的原子机制
volatile提供原子性机制:
1)保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值,每次都会从内存中读取。
2)对该变量的修改,并不提供原子性的保证。
(1)全局变量没使用volatile修饰符
counter没加上volatile修饰符的代码:
static int counter = 0;
void add_counter(void)
{
for (; counter != 0x10; ++counter) {
++counter;
}
}
然后看其汇编代码,需要是使用-O优化啊。
[fgao@fgao-vm-fc13 test]$ gcc -S -O test.c
add_counter:
pushl %ebp
movl %esp, %ebp
movl counter, %eax
cmpl $16, %eax
je .L4
.L5:
addl $2, %eax
cmpl $16, %eax
jne .L5
movl %eax, counter
.L4:
popl %ebp
ret
从汇编代码中,可以看出:
这时将counter的值存入eax,然后每次给eax加2,然后使用eax与16比较,来完成C代码中的for循环的工作。
(2)全局变量使用volatile修饰符
counter加上volatile修饰符的代码:
static volatile int counter = 0;
void add_counter(void)
{
for (; counter != 0x10; ++counter) {
++counter;
}
}
依然打开编译器的优化选项-O
[fgao@fgao-vm-fc13 test]$ gcc -S -O test.c
add_counter:
pushl %ebp
movl %esp, %ebp
movl counter, %eax
cmpl $16, %eax
je .L4
.L5:
movl counter, %eax
addl $1, %eax
movl %eax, counter
movl counter, %eax
addl $1, %eax
movl %eax, counter
movl counter, %eax
cmpl $16, %eax
jne .L5
.L4:
popl %ebp
ret
L5为对应C代码中的for循环的汇编实现。通过对比前一个汇编,我们很容易就发现了区别。虽然在执行counter递增的时候,两者都使用了寄存器eax。
每次访问counter的时候,前者会直接使用寄存器eax,而后者(使用volatile的时候)会重新从counter中读取最新值至eax,然后再使用eax。