Linux Kernel CMPXCHG函数分析

本文详细解析了Linux内核cmpxchg函数的实现原理,包括内嵌汇编语法、Intel开发文档解释及具体代码分析,揭示了cmpxchg如何通过原子操作实现值的更新或比较交换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近看到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排他地使用内存。

 

原文地址:

http://blog.youkuaiyun.com/penngrove/article/details/44175387

 

### Linux Kernel 中 `cmpxchg` 函数的用途与实现 在 Linux 内核中,`cmpxchg` 是一种原子操作函数,通常用于同步访问共享资源。它基于硬件支持的比较并交换指令来实现,能够在一个单一的操作中完成以下功能: 1. 将目标内存位置的内容与预期值进行比较。 2. 如果两者相等,则将目标内存位置更新为目标值;如果不相等,则不执行任何更改。 这种机制广泛应用于多线程环境下的管理、计数器控制以及其他需要高并发性能的任务场景[^3]。 #### 实现细节 以下是 `cmpxchg` 的典型定义形式及其工作原理说明: ```c static inline unsigned long __cmpxchg(volatile void *ptr, unsigned long old, unsigned long new, int size) { switch (size) { case 1: return __sync_val_compare_and_swap((unsigned char *)ptr, old, new); case 2: return __sync_val_compare_and_swap((unsigned short *)ptr, old, new); case 4: return __sync_val_compare_and_swap((unsigned int *)ptr, old, new); #ifdef CONFIG_64BIT case 8: return __sync_val_compare_and_swap((unsigned long *)ptr, old, new); #endif } } ``` 该代码片段展示了如何通过 GCC 提供的内置函数 `_sync_val_compare_and_swap` 来实现不同大小的数据类型的比较和交换操作。具体来说,`__cmpxchg` 接受四个参数:指向要修改的目标地址 (`ptr`)、期望找到的旧值 (`old`)、希望写入的新值 (`new`) 和数据宽度 (`size`)。根据传入的不同尺寸参数,会选择合适的底层汇编指令来进行实际的比较和替换过程[^4]。 对于 x86 架构而言,其对应的机器码可能类似于下面这样: ```asm lock cmpxchgl %edx,(%eax) ``` 这里,“lock” 前缀确保了当前 CPU 核心独占总线使用权直到整个指令序列结束为止,从而防止其他处理器干扰正在进行中的事务处理流程[^5]。 #### 使用案例分析 为了更好地理解 `cmpxchg` 如何融入到更复杂的系统设计当中去解决现实世界里的问题,我们来看几个具体的例子吧! ##### 自旋初始化 自旋是一种简单的互斥原语,在等待获取期间不会主动放弃CPU时间片而是不断轮询直至成功占有资源为止。下面是简化版的自旋结构体以及相关成员变量声明方式之一: ```c typedef struct spinlock_t { volatile unsigned int slock; } spinlock_t; #define SPIN_LOCK_UNLOCKED { .slock = 0 } void spin_lock(spinlock_t *lock){ while (__sync_lock_test_and_set(&lock->slock, 1)){} } int spin_trylock(spinlock_t *lock){ return !__sync_lock_test_and_set(&lock->slock, 1); } void spin_unlock(spinlock_t *lock){ lock->slock = 0; } ``` 在这个版本里头,当尝试定的时候会调用 `__sync_lock_test_and_set()` 方法设置标志位的同时返回之前的状态值以便判断是否已经有人抢先一步占据了这个对象。解动作则仅仅是简单地把对应字段重置回初始状态即可[^6]。 ##### 参考计数器维护 另一个常见的应用场景涉及到动态分配的对象生命周期跟踪方面的工作——即所谓的引用计数技术。每当新增加一个对该实体的有效引用关系建立起来之后就应当相应增加它的内部统计数值;反之亦然减少次数时也要小心谨慎避免误删仍然处于活跃期内的目标实例。如下所示为一段伪代码表示法描述此类行为模式的大致轮廓特征: ```c struct kref { atomic_t refcount; }; static inline void kref_init(struct kref *krefp){ atomic_set(&(krefp)->refcount ,1 ); } bool kref_get_unless_zero(struct kref *krefp ){ return likely(atomic_inc_not_zero(&(krefp)->refcount )); } void kref_put(struct kref *krefp,void (*release)(struct kref*)){ if(unlikely(!atomic_dec_and_test(&(krefp)->refcount ))){ release(krefp); } } ``` 在这里可以看到每次增减都利用到了类似的原子级别操作手段以保障即使是在高度竞争环境下也能保持一致性约束条件得到满足的要求[^7]。 ### 结论 综上所述,`cmpxchg` 不仅是一个基础构建模块级别的工具集组成部分,而且还是许多高级抽象层面上不可或缺的重要基石构件之一。通过对它的深入学习研究可以帮助开发者们更加高效稳定可靠地开发出高质量软件产品出来。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值