C语言的设计者可能没法预见多核CPU 的诞生与流行,更没法想到多个进程访问共享变量时会遇到那么多的时序问题。
volatile 类型的声明让编译器每次都从内存中读取该数值,以防止外设(通过IOMMU)或其他CPU(通过共享内存)改变了该变量,而其他程序没法及时感知。毕竟编译器无法知道该段内存将被共享与否。
Linux 内核中常用的三个宏,临时将该变量用volatile 指针指向,实现直接对该变量内存的访问
define ACCESS_ONCE(var) (*((volatile typeof(var) *)&(var)))
define READ_ONCE(var) ({ typeof(var) _v; _v = ACCESS_ONCE(var); _v; })
define WRITE_ONCE(var, val) (*((volatile typeof(val) *)(&(var))) = (val))
但这些宏在遇到一些积极的优化时可能会遇到问题,甚至在较新的GCC 也无法避免,下面是刚遇到的一个例子(该进程等待其他CPU 上的程序重置var 为0 才继续运行):
asm volatile ("nop\n\tnop\n\tnop\n\tnop\n\t"); // fast location in assembly
while (get_value(var) != 0) {
ACCESS_ONCE(get_value(var));
}
实际上程序在这里却发生了死锁,通过gdb 反汇编这段代码会发现问题。
0x000000000001d228 <+2552>: nop
0x000000000001d229 <+2553>: nop
0x000000000001d22a <+2554>: nop
0x000000000001d22b <+2555>: nop
0x000000000001d22c <+2556>: mov 0x60(%r15),%rax
0x000000000001d230 <+2560>: movslq 0xc(%r15),%rdx
0x000000000001d234 <+2564>: mov 0x260(%rax),%rax
0x000000000001d23b <+2571>: lea (%rax,%rdx,8),%rax
0x000000000001d23f <+2575>: cmpq $0x0,(%rax)
0x000000000001d243 <+2579>: je 0x1d2a8 <swTask+2680>
0x000000000001d245 <+2581>: nopl (%rax)
0x000000000001d248 <+2584>: mov (%rax),%rdx
0x000000000001d24b <+2587>: jmp 0x1d248 <swTask+2584>
这段生成汇编在最后两行就是死循环,在gcc 9.4.0 -O1 和-O2 都可以复现该问题。