一次ACCESS_ONCE 导致的bug

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 都可以复现该问题。