cmpxchg kernel api
cmpxchg是一个内核x86经常用到的api,功能是用来比较和交换的,带着以下疑问研究此api
- 为什么需要设计成原子性的
- 如何实现原子性
函数设计
函数原型设计如下:
1 | cmpxchg(A, C, B) |
函数实现大体分为三部分
kcsan_mb
, 2019年google新加检测设计,不开启KCSAN无用instrument_atomic_write
当开启KASAN时,在实际的写发生前,通过检查影子内存来判定访问地址和地址内存是否合规arch_cmpxchg
平台强相关,通过汇编部分完成硬件辅助实现原子操作
因此重点研究arch_cmpxchg
1 |
|
看一下汇编的逻辑:
1 | case __X86_CASE_L: \ |
这段内嵌汇编有两个操作数(占位符) ,对应如下
1 | 操作数 操作数说明 c语言表达式 |
因此cmpxchgl %2,%1
可以理解为 cmpxchgl __new, *__ptr
cmxchgl 指令的作用是比较并交换 (寄存器/内存变量)和 (寄存器) 的值,该指令集语法如下
1 | Intel语法: 目的操作数,源操作数 CMPXCHG EBX,ECX ;如果EAX与EBX相等,则ECX送EBX且ZF置1;否则EBX送EAX,且ZF清0 |
这里需要注意
- linux内核用的AT&T汇编语言,包括目前启动时的
arch/x86/boot/header.S
, 早期的linux kernel开始时的bootsect.s和setu.s是用intel的汇编 - inter的汇编只能用在inter的x86上,AT&T是可以在多种cpu上使用的,arm也有自己的汇编
因此这里按照AT&T语法解读为:__new读出给ecx寄存器, *__ptr为目标内存变量,__old作为匹配数,存放到EAX
1 | //__raw_cmpxchg(ptr, old, new, size, lock) |
if(*__ptr == __old) __ptr = __new
if(__ptr != __old) __old = *__ptr
因此api cmpxchg
逻辑可以理解为函数如下
1 | //R=cmpxchg(A, C, B) |
疑问解答
为什么需要设计成原子性?
保证共享内存操作是多处理器下线程同步的为什么不使用锁机制?
锁性能较差,需要减少锁的使用,这一行为称为lockless,即无锁机制硬件辅助是如何保证原子性的?
a. 内嵌汇编使用volatile, 为
__volatile__
别名,告知编译器不会对内嵌汇编进行优化,指令保持原样b. 内嵌汇编时使用lock, 带lock的指令在操作前会锁总线,xchg指令默认就是会锁总线的,保证指令执行的原子性(什么是锁总线?锁总线为什么能保持指令的原子性)
1
2
3
4
5
6
7//arch/x86/include/asm/cmpxchg.h
/*
* Note: no "lock" prefix even on SMP: xchg always implies lock anyway.
* Since this is generally used to protect other memory information, we
* use "asm volatile" and "memory" clobbers to prevent gcc from moving
* information around.
*/c. 内嵌汇编clobbers部分使用memory修饰
memory告知gcc编译器: 1. 不要将这段指令和内嵌汇编前的指令重新排序,也就是在执行内嵌汇编代码之前,它前面的指令都执行完毕 2. 不要将内存变量缓存到寄存器,因为指令中可能用到了内存变量,而这些变量可能会因为其他原因改变,比如*__ptr。 3. 在内嵌汇编之后再使用内嵌汇编中的变量,应该从内存重新装载,而不是使用寄存器的值,因为寄存器的值可能已经发生改变, 比如__old
扩展
编译器优化
volatile修饰变量指的是变量可能被其他线程改变,因此不将该变量存入寄存器,每次读取都从内存读取
锁总线, 锁缓存
memory barrier 内存屏障
缓存一致性
参考
GCC在C语言中内嵌汇编 asm volatile 对编译器优化有很好的描述
深入: