这是一个从C语言就有的关键字,但是我们一直没有涉及到,就是因为其实这个 volatile 关键字是和信号挂钩的,要了解它必须知道了信号的知识,现在我们对信号有了基本的认知,下面我们来看一段代码:
// signal.c
#include <stdio.h>
#include <signal.h>
int quit = 1; // 定义一个全局变量
void handler(int signo)
{
printf("%d号信号,正在被捕捉!\n", signo);
printf("quit: %d", quit);
quit = 0; // 将全局变量改为0
printf("-> %d", quit);
}
int main()
{
signal(SIGINT, handler);
while(quit == 1); // 若quit为1则一直循环
printf("main正常退出!\n");
return 0;
}
// makefile
my_signal : signal.c
gcc -o $@ $^
.PHONY : clean
clean:
rm -f my_signal
可以看到运行结果很正常,符合我们的预期,当我们使用 ctrl+c 的时候信号捕捉后将 quit 从 1 变成了 0,这看起来没什么啊,但是这个时候我们在 makefile 中对 gcc 选项添加一个 -O3 进行一下优化,看看结果如何:

这是一个什么现象,很奇怪,明明 quit 已经被改为 0 了,为啥还是会继续循环不退出呢❓❓❓
这其实和我们的编译器优化级别是有关系的,编译器进行优化之后,特别是 -O3,优化的非常厉害,此时当执行到 main 中的 while(quit == 1) 的时候,编译器发现这是一个会一直循环的语句,所以将其放到寄存器中,也就是拷贝一份 quit 数据放到寄存器,这和我们现在全局变量 quit 其实是不一样的,当前的全局变量其实还在物理内存中,而循环体中的 quit 已经在寄存器中了。
并且为了提高效率,编译器不会让物理内存反复去取寄存器中的 quit,这也就导致了我们在 handler 中修改了全局变量 quit 之后,其实是在物理内存进行修改,但是本质上并不会影响到寄存器中的 quit,那么此时循环体还是继续不断的循环!我们也能看到 handler 中打印这个全局变量 quit 的时候其实已经是 0 了,也可以看出它们是不同的变量!
而这都是编译器优化的结果,那如果我们不想让这件事情发生,因为这是因为编译器的问题导致出现与我们代码的本意不相符的问题,那该怎么办❓❓❓
很明显,我们就要使用 volatile 关键字来帮忙,利用 volatile 来修饰 quit 变量,表示该变量保持内存可见性!
volatile int quit = 1;
原子性:一个操作不会被打断,要么一次完成,要么不做。
可见性:一个资源被修改后,是否对其他线程是立即可见的(一个变量的修改存在一个过程,将数据从内存加载的cpu寄存器,进行运算,完毕后交还内存,但是这个过程在代码优化中可能会被编译器优化,将数据放入寄存器,则后续运算只从寄存器取数据,就节省了从内存获取数据的时间)
有序性:简单理解,程序按照写代码的先后顺序执行,就是有序的。(编译器有时候会为了提高程序效率进行代码优化,进行指令重排,来提高效率,而有序性就是禁止指令重排)
而 volatile 关键字的作用是,防止编译器过度优化,因此具备 可见性 与 有序性 的功能。