Linux Kernel CMPXCHG函数分析

时间:2022-04-22
本文章向大家介绍Linux Kernel CMPXCHG函数分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

最近看到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)实现:

01./* TODO: You should use modern GCC atomic instruction builtins instead of this. */  
02.#include <stdint.h>  
03.#define cmpxchg( ptr, _old, _new ) {   
04.  volatile uint32_t *__ptr = (volatile uint32_t *)(ptr);     
05.  uint32_t __ret;                                       
06.  asm volatile( "lock; cmpxchgl %2,%1"             
07.    : "=a" (__ret), "+m" (*__ptr)                  
08.    : "r" (_new), "0" (_old)                       
09.    : "memory");                   
10.  );                                               
11.  __ret;                                           
12.}  
/* 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的内嵌汇编格式是

01.asm ( assembler template  
02.    : output operands                   (optional)  
03.    : input operands                    (optional)  
04.    : clobbered registers list          (optional)  
05.    );  
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排他地使用内存。