最近看到Linux Kernel cmpxchg的代码,对实现很不理解。上网查了内嵌汇编以及Intel开发文档,才慢慢理解了,记录下来以享和我一样困惑的开发者。其实cmpxchg实现的原子操作原理早已被熟知:
cmpxchg(void* ptr, int old, int new),如果ptr和old的值一样,则把new写到ptr内存,否则返回ptr的值,整个操作是原子的。在Intel平台下,会用lock cmpxchg来实现,这里的lock个人理解是锁住内存总线,这样如果有另一个线程想访问ptr的内存,就会被block住。
好了,让我们来看Linux Kernel中的cmpxchg(网上找来的,我自己机器上没找到对应的头文件,据说在include/asm-i386/cmpxchg.h)实现:
/* TODO: You should use modern GCC atomic instruction builtins instead of this. */ #include <stdint.h> #define cmpxchg( ptr, _old, _new ) { volatile uint32_t *__ptr = (volatile uint32_t *)(ptr); uint32_t __ret; asm volatile( "lock; cmpxchgl %2,%1" : "=a" (__ret), "+m" (*__ptr) : "r" (_new), "0" (_old) : "memory"); ); __ret; }
主要要看懂内嵌汇编,c的内嵌汇编格式是
asm ( assembler template : output operands (optional) : input operands (optional) : clobbered registers list (optional) );
output operands和inpupt operands指定参数,它们从左到右依次排列,用‘,‘分割,编号从0开始。以cmpxchg汇编为例,(__ret)对应0,(*__ptr)对应1,(_new)对应2,(_old)对应3,如果在汇编中用到"%2",那么就是指代_new,"%1"指代(*__ptr)。
"=a"是说要把结果写到__ret中,而且要使用eax寄存器,所以最后写结果的时候是的操作是mov eax, ret (eax==>__ret)。"r" (_new)是要把_new的值读到一个通用寄存器中使用。
在cmpxchg中,注意"0"(_old),这个是困惑我的地方,它像告诉你(_old)和第0号操作数使用相同的寄存器或者内存,即(_old)的存储在和0号操作数一样的地方。在cmpxchg中,就是说_old和__ret使用一样的寄存器,而__ret使用的寄存器是eax,所以_old也用eax。
明白了这些,再来看cmpxchgl,在Intel开发文档上说:
0F B1/r CMPXCHG r/m32, r32 MR Valid Valid* Compare EAX with r/m32. If equal, ZF is set
and r32 is loaded into r/m32. Else, clear ZF
and load r/m32 into EAX.
翻译一下:
比较eax和目的操作数(第一个操作数)的值,如果相同,ZF标志被设置,同时源操作数(第二个操作)的值被写到目的操作数,否则,清ZF标志,并且把目的操作数的值写回eax。
好了,把上面这句话套在cmpxchg上就是:
比较_old和(*__ptr)的值,如果相同,ZF标志被设置,同时_new的值被写到(*__ptr),否则,清ZF标志,并且把(*__ptr)的值写回_old。
很明显,符合我们对cmpxchg的理解。
另:Intel开发手册上说lock就是让CPU排他地使用内存。