cmpxchg是X86比较交换指令,这个指令在各大底层系统实现的原子操作和各种同步原语中都有广泛的使用,比如linux内核,JVM,GCC编译器等,cmpxchg就是比较交换指令,了解cmpxchg之前先了解原子操作。
intel P6以及最新系列处理器保证了以下操作是原子的:1.读写一个字节。2.读写16位对齐的字。3.读写32位对齐的双字。4.读写64位对齐的四字。5.读写16位,32位,64位在cache line内的未对齐的字。所以普通的load store指令都是原子的。cache一致性协议保证了不可能有两个cpu同时写一个内存。对于cmpxchg这种比较交换指令肯定不是原子的,intel是CISC复杂指令集架构,在内部流水线执行的时候,肯定会将cmpxchg指令翻译成几条微码执行(对比ARM精简指令集)。所以英特尔对于一些指令提供了LOCK前缀来保证这个指令的原子性。Intel 64和IA-32处理器提供LOCK#信号,该信号在某些关键存储器操作期间自动置位,以锁定系统总线或等效链路。当该输出信号被断言时,来自其他处理器或总线代理的用于控制总线的请求被阻止。对于Intel386,Intel486和Pentium处理器,明确锁定的指令将导致LOCK#信号的置位。硬件设计人员有责任在系统硬件中使用LOCK#信号来控制处理器之间的存储器访问。对于P6和更新的处理器系列,如果被访问的存储区域在处理器内部高速缓存,则LOCK#信号通常不被断言;相反,锁定仅应用于处理器的缓存。对于Intel486和Pentium处理器,LOCK#信号在LOCK操作期间始终在总线上置位,即使被锁定的存储器区域缓存在处理器中也是如此。所以这个性能会降低很多,导致其它cpu不能访问内存。对于P6和更新的处理器系列,如果在LOCK操作期间被锁定的存储器区域被高速缓存在执行LOCK操作作为回写存储器并且完全包含在高速缓存行中的处理器中,则处理器可能不会断言总线上的LOCK#信号。相反,它将在内部修改内存位置并允许其缓存一致性机制,以确保操作以原子方式执行。此操作称为“缓存锁定”。缓存一致性机制自动阻止缓存相同内存区域的两个或多个处理器同时修改该区域中的数据。
为了更清楚理解cmxchg,需要同时看ARM和x86两种架构下的实现一个RISC,一个CISC,linux内核提供了两种架构下的实现。linux内核的原子变量定义如下:
代码语言:javascript复制//原子变量
typedef struct {
volatile int counter; //volatile禁止编译器把变量缓冲到寄存器
} atomic_t;
先看ARM架构下,ARM架构是精简指令集,没有提供cmpxchg这种复杂指令,和其它所有RISC架构一样提供了LL/SC(链接加载,条件存储)操作,这个操作是很多原子操作的基础。ARMv8指令是LDXRSTXR,ARMv7指令是LDREXSTREX,大同小异,都属于独占访问,需要有local monitor和global monitor配合使用。这两条指令一般需要成对出现。ldrex是从内存取出数据放到寄存器,然后监视器将此地址标记为独占,strex会先测试是否是当前cpu的独占,如果是则存储成功返回0,如果不是则存储失败返回1。例如cpu0将地址m标记为独占,在strex执行前,线程被调出了,cpu1调用ldrex会清除cpu0的独占,而将自己标记为独占,然后执行strxr,然后cpu0的线程重新被调度,此时执行strex会失败,因为自己的独占位被清除了。这样也会导致后进入ldrex的线程可能比先进入的先执行。标记为独占的地址调用strex后都会清除独占标志。
代码语言:javascript复制/**
* 比较ptr->counter和old的值如果相等,则ptr->counter = new,并且返回old,否则ptr->counter不变
* 返回ptr->counter
*/
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new)
{
unsigned long oldval, res;
smp_mb(); //内存屏障,保证cmpxchg不会在屏障前执行
do {
__asm__ __volatile__("@ atomic_cmpxchgn"
"ldrex %1, [%2]n" //独占访问,监视器会将此地址标志独占并且将ptr->counter给oldvalue
"mov %0, #0n" //res = 0
"teq %1, %3n" //测试oldvalue是否和old相等也就是ptr->counter和old
//独占访问成功并且如果相等则把new赋值给ptr->counter,否则不执行这条指令
"strexeq %0, %4, [%2]n"
: "=&r" (res), "=&r" (oldval)
: "r" (&ptr->counter), "Ir" (old), "r" (new)
: "cc");
} while (res); //while res是因为strexeq指令是独占访存指令从,此时可能未标记访存,而res为1
smp_mb();//内存屏障,保证cmpxchg不会在屏障后执行
return oldval;
}
x86架构也是类似:
代码语言:javascript复制/*
* 根据size大小比较交换字节,字或者双字,如果返回old则交换成功,否则交换失败
*/
static inline unsigned long __cmpxchg(volatile void *ptr, unsigned long old,
unsigned long new, int size)
{
unsigned long prev;
switch (size) {
case 1:
__asm__ __volatile__(LOCK_PREFIX "cmpxchgb �,%2"
: "=a"(prev)
: "q"(new), "m"(*__xg(ptr)), "0"(old)
: "memory");
return prev;
case 2:
__asm__ __volatile__(LOCK_PREFIX "cmpxchgw %w1,%2"
: "=a"(prev)
: "r"(new), "m"(*__xg(ptr)), "0"(old)
: "memory");
return prev;
//eax = old,比较%2 = ptr->counter和eax是否相等,如果相等则ZF置位,并把%1 = new赋值
//给ptr->counter,返回old值,否则ZF清除,并且将ptr->counter赋值给eax
case 4:
__asm__ __volatile__(LOCK_PREFIX "cmpxchgl %1,%2"
: "=a"(prev)
: "r"(new), "m"(*__xg(ptr)), "0"(old) //0表示eax = old
: "memory");
return prev;
}
return old;
}
在cmpxchg指令前加了lock前缀,保证在进行操作的时候,不会让其它cpu操作同一个内存。使得整个操作保持原子性。对比来看虽然X86只用了一条指令,但是处理器内部肯定将这条指令转成了类RISC的微码。