最近在看Linux的进程调度,其中进程描述符task_struct中有个变量state。这个变量是这样定义的: volatile long state;

由于之前不了解volatile关键字的用法,所以这里对volatile深究了一下。

<!-- more -->

一. volatile关键字

volatile是一个类型修饰符(type specifier)。

在程序设计中,尤其是在C语言、C++、C#和Java语言中,使用volatile关键字声明的变量或对象通常拥有和优化和(或)多线程相关的特殊属性。通常,volatile关键字用来阻止(伪)编译器对那些它认为变量的值不能“被代码本身”改变的代码上执行任何优化。 在C环境中,volatile关键字的真实定义和适用范围经常被误解,加之C++、C#和Java都从C中神秘地“继承”了volatile,在这些编程语言中,因此这些语言中volatile的用法和语义大相径庭。

二. 从比赛中引出问题

在正式ACM/ICPC比赛开始之前,参赛选手都会做一项测试OJ系统各项边界的工作,比如测试提交代码最大可以开多少的数组,能大概接受多少时间复杂度的算法…

比如:

#include <stdio.h>
int main()
{
    int i;
    for (i=0; i<1000000000; i++);
    return 0;
}

显然,这段程序是在测试OJ能不能接受$O(10^9)$的算法,提交OJ返回的信息(Accept or Time Limit Exceed)来判断能否接受。

通常情况下,这段代码提交上去都会被接受。

因为在大多数OJ系统中,OJ在用gcc编译代码时,会自动加上-O2优化。结果就是,代码第5行的for循环被编译器优化掉了,最后不会被真正执行。

这个情况在测试可以开多大数组中也会出现:编译器认为你开的数组没有被使用,所以就自动给优化了。

于是通常的解决办法是,在循环中加一些赋值或其他的操作,来防止编译器自动优化。

三. 从实例中分析问题

编写Linux信号处理程序遇到的一个问题。

代码:

#include <stdio.h>
#include <signal.h>
int a;
void func(int signo)
{
	printf("SIGNO: %d\n",signo);
	a=1;
	printf("%d\n",a);
}
int main()
{
	a=0;
	signal(SIGINT,func);
	while (a != 1);
	return 0;
}

这段程序设置进程接受到SIGINT信号(摁下ctrl+c)时的动作是调用func函数。

之后程序便进入一个循环,知道a的值被赋为1时,结束。

可以看看实际效果。

</img>

当使用gcc编译不开启O3编译优化选项时,程序执行效果和预想的效果是一样的。

当使用gcc编译开启O3编译优化选项时,摁下ctrl+c时,程序并没有终止,而是继续循环了下去…最后是发送SIGQUIT信号(摁下ctrl+\)才停止的。

使用gcc -S t.c命令生产t.s文件,t.s文件里面可以看到t.c通过gcc产生的汇编代码。 File t.s:

	.file	"t.c"
	.comm	a,4,4
	.section	.rodata
.LC0:
	.string	"SIGNO: %d\n"
.LC1:
	.string	"%d\n"
	.text
	.globl	func
	.type	func, @function
func:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$24, %esp
	movl	8(%ebp), %eax
	movl	%eax, 4(%esp)
	movl	$.LC0, (%esp)
	call	printf
	movl	$1, a
	movl	a, %eax
	movl	%eax, 4(%esp)
	movl	$.LC1, (%esp)
	call	printf
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	func, .-func
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$16, %esp
	movl	$0, a
	movl	$func, 4(%esp)
	movl	$2, (%esp)
	call	signal
	nop
.L3:
	movl	a, %eax
	cmpl	$1, %eax
	jne	.L3
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2"
	.section	.note.GNU-stack,"",@progbits

第38行开始是main函数代码段。

可以看到第53行到第56行是while循环代码段。

第54行,把a传送到eax寄存器;

第55行,比较数值1和eax寄存器中保存的值,即a;

第56行,jne:若不相等则跳转到.L3;

所以在汇编代码中,这个while循环是老老实实地执行了的。

再使用gcc -S -O3 t.c -o t2.s 命令生产t2.s文件,t2.s文件里面可以看到t.c通过gcc并且经过O3编译优化之后产生的汇编代码。为了节省篇幅,就放从main之后的代码段。 File t2.s

...
main:
.LFB26:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$16, %esp
	movl	$func, 4(%esp)
	movl	$2, (%esp)
	movl	$0, a
	call	signal
	cmpl	$1, a
	je	.L4
.L6:
	jmp	.L6
	.p2align 4,,7
	.p2align 3
.L4:
	xorl	%eax, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE26:
	.size	main, .-main
	.comm	a,4,4
	.ident	"GCC: (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2"
	.section	.note.GNU-stack,"",@progbits

第15行,对比了a和1的大小;

第16行,je:相等则跳转到.L4。因为a=2,所以显然这里的跳转不会执行。

第18行,jmp:无条件跳转到L6。相当于是在不断地回跳,即死循环。

由此可知到,编译器并不知道a的值是会发生变化的。

四. 使用volatile解决问题

volatile关键字就是要告诉编译器,这个变量时经常改变的,而且编译时不要进行代码优化,所以每次读写的时候都要到它所在的地址去读取。 如:

volatile int a;

这样写,编译器就不会再盲目地把原来的那个循环“优化” <im1g src=“http://bcs.duapp.com/blogbuk/volatile2.jpg”/>