Linux的原子操做與同步機制

併發問題html

現代操做系統支持多任務的併發,併發在提升計算資源利用率的同時也帶來了資源競爭的問題。例如C語言語句「count++;」在未經編譯器優化時生成的彙編代碼爲。前端

當操做系統內存在多個進程同時執行這段代碼時,就可能帶來併發問題。linux

假設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。atom

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,不然申請信號量成功。

標籤2被編譯到另外一個段內,進入標籤2後,執行lea指令取出sem->count的地址,放到eax寄存器做爲參數,而後調用函數__down_failed表示信號量申請失敗,進程加入等待隊列。最後跳回標籤1結束信號量申請。

信號量的釋放操做由函數up實現。

/*

 * Note! This is subtle. We jump to wake people up only if

 * the semaphore was negative (== somebody was waiting on it).

 * The default case (no contention) will result in NO

 * jumps for both down() and up().

 */

static inline void up(struct semaphore * sem)

{

    __asm__ __volatile__(

       "# atomic up operation\n\t"

       LOCK "incl %0\n\t"     /* ++sem->count */

       "jle 2f\n"

       "1:\n"

       LOCK_SECTION_START("")

       "2:\tlea %0,%%eax\n\t"

       "call __up_wakeup\n\t"

       "jmp 1b\n"

       LOCK_SECTION_END

       ".subsection 0\n"

       :"=m" (sem->count)

       :

       :"memory","ax");

}

實際的彙編代碼形式爲。

lock   incl sem->count

jle     2

1:

<========== another section ==========>

2:

lea    [sem->count], eax

call   __up_wakeup

jmp    1

釋放信號量時執行原子操做incl將sem->count加1,若是該值小於等於0,則說明等待隊列有阻塞的進程須要喚醒,跳轉到標籤2,不然信號量釋放成功。

標籤2被編譯到另外一個段內,進入標籤2後,執行lea指令取出sem->count的地址,放到eax寄存器做爲參數,而後調用函數__up_wakeup喚醒等待隊列的進程。最後跳回標籤1結束信號量釋放。

 

總結

本文經過對操做系統併發問題的討論研究操做系統內的原子操做的實現原理,並討論了不一樣體系結構下Linux原子操做的實現,最後描述了Linux操做系統如何利用原子操做實現常見的進程同步機制,但願對你有所幫助。

 

相關文章
相關標籤/搜索