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核心的原子變量定義如下:
//原子變量
typedef struct {
volatile int counter; //volatile禁止編譯器把變量緩沖到寄存器
} atomic_t;
複制
先看ARM架構下,ARM架構是精簡指令集,沒有提供cmpxchg這種複雜指令,和其它所有RISC架構一樣提供了LL/SC(連結加載,條件存儲)操作,這個操作是很多原子操作的基礎。ARMv8指令是LDXR\STXR,ARMv7指令是LDREX\STREX,大同小異,都屬于獨占通路,需要有local monitor和global monitor配合使用。這兩條指令一般需要成對出現。ldrex是從記憶體取出資料放到寄存器,然後螢幕将此位址标記為獨占,strex會先測試是否是目前cpu的獨占,如果是則存儲成功傳回0,如果不是則存儲失敗傳回1。例如cpu0将位址m标記為獨占,在strex執行前,線程被調出了,cpu1調用ldrex會清除cpu0的獨占,而将自己标記為獨占,然後執行strxr,然後cpu0的線程重新被排程,此時執行strex會失敗,因為自己的獨占位被清除了。這樣也會導緻後進入ldrex的線程可能比先進入的先執行。标記為獨占的位址調用strex後都會清除獨占标志。
/**
* 比較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_cmpxchg\n"
"ldrex %1, [%2]\n" //獨占通路,螢幕會将此位址标志獨占并且将ptr->counter給oldvalue
"mov %0, #0\n" //res = 0
"teq %1, %3\n" //測試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架構也是類似:
/*
* 根據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 %b1,%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的微碼。