Linux的原子操作與同步機制
Linux的原子操作與同步機制
轉載自:https://www.cnblogs.com/fanzhidongyzby/p/3654855.html
並發問題
現代操作系統支持多任務的並發,並發在提高計算資源利用率的同時也帶來了資源競爭的問題。例如C語言語句“count++;”在未經編譯器優化時生成的匯編代碼為。
當操作系統內存在多個進程同時執行這段代碼時,就可能帶來並發問題。
假設count變量初始值為0。進程1執行完“mov eax, [count]”後,寄存器eax內保存了count的值0。此時,進程2被調度執行,搶占了進程1的CPU的控制權。進程2執行“count++;”的匯編代碼,將累加後的count值1寫回到內存。然後,進程1再次被調度執行,CPU控制權回到進程1。進程1接著執行,計算count的累加值仍為1,寫回到內存。雖然進程1和進程2執行了兩次“count++;”操作,但是count實際的內存值為1,而不是2!
單處理器原子操作
解決這個問題的方法是,將“count++;”語句翻譯為單指令操作。
Intel x86指令集支持內存操作數的inc操作,這樣“count++;”操作可以在一條指令內完成。因為進程的上下文切換是在總是在一條指令執行完成後,所以不會出現上述的並發問題。對於單處理器來說,一條處理器指令就是一個原子操作。
多處理器原子操作
但是在多處理器的環境下,例如SMP架構,這個結論不再成立。我們知道“inc [count]”指令的執行過程分為三步:
1)從內存將count的數據讀取到cpu。
2)累加讀取的值。
3)將修改的值寫回count內存。
這又回到前面並發問題類似的情況,只不過此時並發的主題不再是進程,而是處理器。
Intel x86指令集提供了指令前綴lock用於鎖定前端串行總線(FSB),保證了指令執行時不會受到其他處理器的幹擾。
使用lock指令前綴後,處理器間對count內存的並發訪問(讀/寫)被禁止,從而保證了指令的原子性。
x86原子操作實現
Linux的源碼中x86體系結構原子操作的定義文件為。
linux2.6/include/asm-i386/atomic.h
文件內定義了原子類型atomic_t,其僅有一個字段counter,用於保存32位的數據。
typedef struct { volatile int counter; } atomic_t;
其中原子操作函數atomic_inc完成自加原子操作。
/** * atomic_inc - increment atomic variable * @v: pointer of type atomic_t * * Atomically increments @v by 1. */ static __inline__ void atomic_inc(atomic_t *v) { __asm__ __volatile__( LOCK "incl %0" :"=m" (v->counter) :"m" (v->counter)); }
其中LOCK宏的定義為。
#ifdef CONFIG_SMP #define LOCK "lock ; " #else #define LOCK "" #endif
可見,在對稱多處理器架構的情況下,LOCK被解釋為指令前綴lock。而對於單處理器架構,LOCK不包含任何內容。
arm原子操作實現
在arm的指令集中,不存在指令前綴lock,那如何完成原子操作呢?
Linux的源碼中arm體系結構原子操作的定義文件為。
linux2.6/include/asm-arm/atomic.h
其中自加原子操作由函數atomic_add_return實現。
static inline int atomic_add_return(int i, atomic_t *v) { unsigned long tmp; int result; __asm__ __volatile__("@ atomic_add_return\n" "1: ldrex %0, [%2]\n" " add %0, %0, %3\n" " strex %1, %0, [%2]\n" " teq %1, #0\n" " bne 1b" : "=&r" (result), "=&r" (tmp) : "r" (&v->counter), "Ir" (i) : "cc"); return result; }
上述嵌入式匯編的實際形式為。
1: ldrex [result], [v->counter] add [result], [result], [i] strex [temp], [result], [v->counter] teq [temp], #0 bne 1b
ldrex指令將v->counter的值傳送到result,並設置全局標記“Exclusive”。
add指令完成“result+i”的操作,並將加法結果保存到result。
strex指令首先檢測全局標記“Exclusive”是否存在,如果存在,則將result的值寫回counter->v,並將temp置為0,清除“Exclusive”標記,否則直接將temp置為1結束。
teq指令測試temp值是否為0。
bne指令temp不等於0時跳轉到標號1,其中字符b表示向後跳轉。
示意圖如下:
整體看來,上述匯編代碼一直嘗試完成“v->counter+=i”的操作,直到temp為0時結束。
使用ldrex和strex指令對是否可以保證add指令的原子性呢?假設兩個進程並發執行“ldrex+add+strex”操作,當進程1執行ldrex後設定了全局標記“Exclusive”。此時切換到進程2,執行ldrex前全局標記“Exclusive”已經設定,ldrex執行後重復設定了該標記。然後執行add和strex指令,完成累加操作。再次切換回進程1,接著執行add指令,當執行strex指令時,由於“Exclusive”標記被進程2清除,因此不執行傳送操作,將temp設置為1。後繼teq指令測定temp不等於0,則跳轉到起始位置重新執行,最終完成累加操作!可見ldrex和strex指令對可以保證進程間的同步。多處理器的情況與此相同,因為arm的原子操作只關心“Exclusive”標記,而不在乎前端串行總線是否加鎖。
在ARMv6之前,swp指令就是通過鎖定總線的方式完成原子的數據交換,但是影響系統性能。ARMv6之後,一般使用ldrex和strex指令對代替swp指令的功能。
自旋鎖中的原子操作
Linux的源碼中x86體系結構自旋鎖的定義文件為。
linux2.6/include/asm-i386/spinlock.h
其中__raw_spin_lock完成自旋鎖的加鎖功能
#define __raw_spin_lock_string "\n1:\t" "lock ; decb %0\n\t" "jns 3f\n" "2:\t" "rep;nop\n\t" "cmpb $0,%0\n\t" "jle 2b\n\t" "jmp 1b\n" "3:\n\t" static inline void __raw_spin_lock(raw_spinlock_t *lock) { __asm__ __volatile__( __raw_spin_lock_string :"=m" (lock->slock) : : "memory"); }
上述代碼的實際匯編形式為:
1: lock decb [lock->slock] jns 3 2: rep nop cmpb $0, [lock->slock] jle 2 jmp 1 3:
示意圖如下:
其中lock->slock字段初始值為1,執行原子操作decb後值為0。符號位為0,執行jns指令跳轉到3,完成自旋鎖的加鎖。
當再次申請自旋鎖時,執行原子操作decb後lock->slock值為-1。符號位為1,不執行jns指令。進入標簽2,執行一組nop指令後比較lock->slock是否小於等於0,如果小於等於0回到標簽2進行循環(自旋)。否則跳轉到標簽1重新申請自旋鎖,直到申請成功。
自旋鎖釋放時會將lock->slock設置為1,這樣保證了其他進程可以獲得自旋鎖。
信號量中的原子操作
Linux的源碼中x86體系結構信號量的定義文件為。
linux2.6/include/asm-i386/semaphore.h
信號量的申請操作由函數down實現。
/* * This is ugly, but we want the default case to fall through. * "__down_failed" is a special asm handler that calls the C * routine that actually waits. See arch/i386/kernel/semaphore.c */ static inline void down(struct semaphore * sem) { might_sleep(); __asm__ __volatile__( "# atomic down operation\n\t" LOCK "decl %0\n\t" /* --sem->count */ "js 2f\n" "1:\n" LOCK_SECTION_START("") "2:\tlea %0,%%eax\n\t" "call __down_failed\n\t" "jmp 1b\n" LOCK_SECTION_END :"=m" (sem->count) : :"memory","ax"); }
實際的匯編代碼形式為:
lock decl [sem->count] js 2 1: <========== another section ==========> 2: lea [sem->count], eax call __down_failed jmp 1
信號量的sem->count一般初始化為一個正整數,申請信號量時執行原子操作decl,將sem->count減1。如果該值減為負數(符號位為1)則跳轉到另一個段內的標簽2,否則申請信號量成功。