1. 程式人生 > >原子操作、訊號量、讀寫訊號量和自旋鎖的區別與聯絡

原子操作、訊號量、讀寫訊號量和自旋鎖的區別與聯絡

一.為什麼核心需要同步方法
併發指的是多個執行單元同時,並行被執行,而併發的執行單元對共享資源(硬體資源和軟體上的全域性變數,靜態變數等)的訪問則很容易導致競態。
主要競態發生如下:
1.對稱多處理器(SMP)多個CPU
 SMP是一種緊耦合,共享儲存的系統模型,它的特點是多個CPU使用共同的系統匯流排,因此可訪問共同的外設和儲存器。
 
2.單CPU內程序與搶佔它的程序
Linux2.6核心支援搶佔排程,一個程序在核心執行的時候被另一高優先順序的程序打斷,程序與搶佔它的程序訪問共享資源的情況類似於SMP

3.中斷(硬中斷,軟中斷,Tasklet,底半部)與程序之間
中斷可以打斷正在執行的程序,如果中斷處理程式訪問程序正在訪問的資源,則競態也會發生。

此外,中斷也有可能被新的更高階優先順序中斷中斷,因此,多箇中斷之間本身也可能引起競態。

那是不是就沒有辦法呢?當然不是,記住linux開源的力量是無窮的。
解決競態問題的途徑是保證對共享資源的互斥訪問,互斥訪問就是一個執行單元在訪問的時候,其他執行單元禁止訪問。
訪問共享資源的程式碼區域成為臨界區,臨界區需要互斥機制加以保護。
中斷遮蔽,原子操作,自旋鎖和訊號量等是linux互斥操作的途徑。



 
二.核心的同步方法分類
1.中斷遮蔽
中斷遮蔽使得中斷與程序之間的併發不再發生,linux核心程序排程等操作都依賴中斷實現,核心搶佔程序之間的併發就可以避免了。
但是,由於linux核心的非同步I/O,程序排程等都依賴中斷,所以長時間遮蔽中斷很危險。
且中斷遮蔽只能禁止本CPU內的中斷,不能解決SMP引發的競態。
因此中斷遮蔽不是值得推薦的方法,它適宜與自旋鎖聯合使用。

2.原子操作
原子操作保證所有指令以原子方式執行(執行過程不能被中斷)。
例:
執行緒1                                     執行緒2
increment(2->3)                           ...
...                                       increment(3->4)
兩個操作不可能併發訪問同一個變數,絕對不可能引起競態。

2.1>針對原子整數操作只能對atomic_t型別的資料處理。
定義在<asm/atomic.h>  typedef struct { int counter; } atomic_t;
使用atomic_t而不是int型別確保編譯器不對相應的值進行優化,使得原子操作接受正確的地址。強型別匹配。


原子整數操作API簡介:

atomic_read(atomic_t * v);            該函式對原子型別的變數進行原子讀操作,它返回原子型別的變數v的值。

atomic_set(atomic_t * v, int i);      該函式設定原子型別的變數v的值為i。

void atomic_add(int i, atomic_t *v);   該函式給原子型別的變數v增加值i。

atomic_sub(int i, atomic_t *v);        該函式從原子型別的變數v中減去i。

int atomic_sub_and_test(int i, atomic_t *v);
該函式從原子型別的變數v中減去i,並判斷結果是否為0,如果為0,返回真,否則返回假。

void atomic_inc(atomic_t *v);          該函式對原子型別變數v原子地增加1。

void atomic_dec(atomic_t *v);          該函式對原子型別的變數v原子地減1。

int atomic_dec_and_test(atomic_t *v);
該函式對原子型別的變數v原子地減1,並判斷結果是否為0,如果為0,返回真,否則返回假。

int atomic_inc_and_test(atomic_t *v);
該函式對原子型別的變數v原子地增加1,並判斷結果是否為0,如果為0,返回真,否則返回假。

int atomic_add_negative(int i, atomic_t *v);
該函式對原子型別的變數v原子地增加I,並判斷結果是否為負數,如果是,返回真,否則返回假。

注:原子整數的操作主要用途實現計數器。



2.2>原子位操作
位操作函式是對普通函式記憶體地址進行操作,它的引數是一個指標和一個位號。

原子位操作API簡介:
void set_bit(int nr, void *addr)        原子設定addr所指的第nr位

void clear_bit(int nr, void *addr)      原子的清空所指物件的第nr位

void change_bit(nr, void *addr)         原子的翻轉addr所指的第nr位

int test_bit(nr, void *addr)            原子的返回addr位所指物件nr位

int test_and_set_bit(nr, void *addr)    原子設定addr所指物件的第nr位,並返回原先的值

int test_and_clear_bit(nr, void *addr)  原子清空addr所指物件的第nr位,並返回原先的值

int test_and_change_bit(nr, void *addr)  原子翻轉addr所指物件的第nr位,並返回原先的值

如:標誌暫存器EFLSGS的系統標誌,用於控制I/O訪問,可遮蔽硬體中斷。共32位,不同的位代表不同的資訊,
對其中資訊改變都是通過位操作實現的。


3.自旋鎖

3.1>自旋鎖
如果執行單元(一般針對執行緒)申請自旋鎖已經被別的執行單元佔用,申請者就一直迴圈在那裡看是否該執行單元釋放了自旋鎖。
其作用是為了解決某項資源的互斥使用。雖然它的效率比互斥鎖高,但是它也有些不足之處:
    1、自旋鎖一直佔用CPU,他在未獲得鎖的情況下,一直執行自旋,所以佔用著CPU。所以自旋瑣不應該長時間持有。
    2、在用自旋鎖時有可能造成死鎖,當遞迴呼叫時有可能造成死鎖,呼叫有些其他函式也可能造成死鎖,如 copy_to_user()、
    copy_from_user()、kmalloc()等。
    
自旋鎖主要針對SMP,在單CPU中它僅僅設定核心搶佔機制的是否啟用的開關。 在核心不支援搶佔的系統中,自旋鎖退為空操作。
儘管自旋瑣可以保證臨界區不受別的CPU和本程序的搶佔程序打擾,但執行臨界區還受到中斷和底半部(bh)的影響。所以與中斷遮蔽
聯絡使用。

自旋鎖的API:
spin_lock_init(spinlock_t *x);   //自旋鎖在真正使用前必須先初始化
   
  獲得自旋鎖:spin_lock(x);   //只有在獲得鎖的情況下才返回,否則一直“自旋”
                           spin_trylock(x);  //如立即獲得鎖則返回真,否則立即返回假
      釋放鎖:spin_unlock(x);
   
結合以上有以下程式碼段:

    spinlock_t lock;        //定義一個自旋鎖
    spin_lock_init(&lock);
    spin_lock(&lock);   
    臨界區
    spin_unlock(&lock);   //釋放鎖
   

spin_lock_irqsave(lock, flags)
該巨集獲得自旋鎖的同時把標誌暫存器的值儲存到變數flags中並失效本地中//斷。相當於:spin_lock()+local_irq_save()
spin_unlock_irqrestore(lock, flags)
該巨集釋放自旋鎖lock的同時,也恢復標誌暫存器的值為變數flags儲存的//值。它與spin_lock_irqsave配對使用。
相當於:spin_unlock()+local_irq_restore()

spin_lock_irq(lock)
該巨集類似於spin_lock_irqsave,只是該巨集不儲存標誌暫存器的值。相當         //於:spin_lock()+local_irq_disable()
spin_unlock_irq(lock)
該巨集釋放自旋鎖lock的同時,也使能本地中斷。它與spin_lock_irq配對應用。相當於: spin_unlock()+local_irq+enable()

spin_lock_bh(lock)
該巨集在得到自旋鎖的同時失效本地軟中斷。相當於:  //spin_lock()+local_bh_disable()
spin_unlock_bh(lock)
該巨集釋放自旋鎖lock的同時,也使能本地的軟中斷。它與spin_lock_bh配對//使用。相當於:spin_unlock()+local_bh_enable()


3.2>讀寫自旋鎖
讀寫自旋鎖為讀和寫分別提供了不同的鎖。一個或多個讀任務併發的持有讀寫鎖;用於寫的鎖最多隻能被一個寫任務持有,
此時不能併發的讀操作。

注:讀鎖和寫鎖要完全分割在程式碼的分支中
read_lock(&mr_rwlock)
write_lock(&mr_rwlock)  將會帶來死鎖,因為寫鎖會不斷自旋。
讀寫自旋鎖相當於自旋鎖的讀的一種優化,具有自旋鎖的特點,如:不宜長時間持有鎖等。
關於讀寫自旋鎖的API函式自己分析,其中讀寫各有自己的API函式。

3.3>順序鎖
順序鎖是對讀寫鎖的一種優化,讀執行單元絕不會被寫執行單元阻塞。讀執行單元可以在寫執行單元對被順序鎖保護的共享資源
進行寫操作時還可以繼續讀,而不必等待寫執行單元完成寫操作,寫執行單元也不需要等待所有讀執行單元完成讀操作才進行寫操作。

注:順序鎖要求被保護的共享資源不含有指標,因為寫執行單元可能使指標失效,讀執行單元正在訪問該資源,導致Oops。

重要:讀執行單元訪問共享資源時,如果有寫操作執行完成,讀執行單元就需要重新進行讀操作。
do{
  seqnum=read_seqbegin(&seqlock_a);
  //讀執行程式碼
  ...
  }while(read_seqretry(&seqlock_a,seqnum));      //判斷是否需要重讀