原子操作、訊號量、讀寫訊號量和自旋鎖的區別與聯絡
阿新 • • 發佈:2019-02-09
一.為什麼核心需要同步方法
併發指的是多個執行單元同時,並行被執行,而併發的執行單元對共享資源(硬體資源和軟體上的全域性變數,靜態變數等)的訪問則很容易導致競態。
主要競態發生如下:
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)); //判斷是否需要重讀
併發指的是多個執行單元同時,並行被執行,而併發的執行單元對共享資源(硬體資源和軟體上的全域性變數,靜態變數等)的訪問則很容易導致競態。
主要競態發生如下:
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)); //判斷是否需要重讀