一、什么是自旋鎖
一直以為自旋鎖也是用於多線程互斥的一種鎖,原來不是!
自旋鎖是專為防止多處理器並發(實現保護共享資源)而引入的一種鎖機制。自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。但是兩者在調度機制上略有不同。對於互斥鎖,如果資源已經被占用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那里看是否該自旋鎖的保持者已經釋放了鎖,“自旋”一詞就是因此而得名。自旋鎖在內核中大量應用於中斷處理等部分(對於單處理器來說,防止中斷處理中的並發可簡單采用關閉中斷的方式,即在標志寄存器中關閉/打開中斷標志位,不需要自旋鎖)。
自旋鎖的初衷就是:在短期間內進行輕量級的鎖定。一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用的期間進行自旋(特別浪費處理器時間),所以自旋鎖不應該被持有時間過長。如果需要長時間鎖定的話, 最好使用信號量。
自旋鎖只有在內核可搶占或SMP(多處理器)的情況下才真正需要,在單CPU且不可搶占的內核下,自旋鎖的所有操作都是空操作。
二、自旋鎖的缺陷
自旋鎖是一種比較低級的保護數據結構或代碼片段的原始方式,這種鎖可能存在兩個問題:
(1)死鎖。試圖遞歸地獲得自旋鎖必然會引起死鎖:例如遞歸程序的持有實例在第二個實例循環,以試圖獲得相同自旋鎖時,就不會釋放此自旋鎖。所以,在遞歸程序中使用自旋鎖應遵守下列策略:遞歸程序決不能在持有自旋鎖時調用它自己,也決不能在遞歸調用時試圖獲得相同的自旋鎖。此外如果一個進程已經將資源鎖定,那么,即使其它申請這個資源的進程不停地瘋狂“自旋”,也無法獲得資源,從而進入死循環。
(2)過多占用cpu資源。如果不加限制,由於申請者一直在循環等待,因此自旋鎖在鎖定的時候,如果不成功,不會睡眠,會持續的嘗試,單cpu的時候自旋鎖會讓其它process動不了。因此,一般自旋鎖實現會有一個參數限定最多持續嘗試次數。超出后,自旋鎖放棄當前time slice,等下一次機會。
由此可見,自旋鎖比較適用於鎖使用者保持鎖時間比較短的情況。正是由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。信號量和讀寫信號量適合於保持時間較長的情況,它們會導致調用者睡眠,因此只能在進程上下文使用,而自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用。
三、Linux環境下的自旋鎖
自旋鎖的實現基於共享變量。一個線程通過給共享變量設置一個值來獲取鎖,其他等待線程查詢共享變量是否為0來確定鎖現是否可用,然后在忙等待的循環中“自旋”直到鎖可用為止。自旋鎖的狀態值為1表示解鎖狀態,說明有1個資源可用;0或負值表示加鎖狀態,0說明可用資源數為0。Linux內核為通用自旋鎖提供了API函數初始化、測試和設置自旋鎖。API函數功能說明如下表所示:
宏定義 | 功能說明 |
spin_lock_init(lock) | 初始化自旋鎖,將自旋鎖設置為1,表示有一個資源可用。 |
spin_is_locked(lock) | 如果自旋鎖被置為1(未鎖),返回0,否則返回1。 |
spin_unlock_wait(lock) | 等待直到自旋鎖解鎖(為1),返回0;否則返回1。 |
spin_trylock(lock) | 嘗試鎖上自旋鎖(置0),如果原來鎖的值為1,返回1,否則返回0。 |
spin_lock(lock) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。 |
spin_unlock(lock) | 將自旋鎖解鎖(置為1)。 |
spin_lock_irqsave(lock, flags) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。關中斷,將狀態寄存器值存入flags。 |
spin_unlock_irqrestore(lock, flags) | 將自旋鎖解鎖(置為1)。開中斷,將狀態寄存器值從flags存入狀態寄存器。 |
spin_lock_irq(lock) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。關中斷。 |
spin_unlock_irq(lock) | 將自旋鎖解鎖(置為1)。開中斷。 |
spin_unlock_bh(lock) | 將自旋鎖解鎖(置為1)。開啟底半部的執行。 |
spin_lock_bh(lock) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。阻止軟中斷的底半部的執行。 |
在實際編程中,何時使用spin_lock,何時使用spin_lock_irq呢?這兩者有點區別。
(1)spin_lock
spin_lock 的實現關系為:spin_lock -> raw_spin_lock -> _raw_spin_lock -> __raw_spin_lock ,而__raw_spin_lock 的實現為:
static inline void __raw_spin_lock(raw_spinlock_t *lock) { preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
(2)spin_lock_irq
spin_lock_irq 的實現關系為:spin_lock_irq -> raw_spin_lock_irq -> _raw_spin_lock_irq -> __raw_spin_lock_irq,而__raw_spin_lock_irq 的實現為:
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock) { local_irq_disable(); preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
由此可見,這兩者之間只有一個差別:是否調用local_irq_disable()函數, 即是否禁止本地中斷。這兩者的區別可以總結為:
- 在任何情況下使用spin_lock_irq都是安全的。因為它既禁止本地中斷,又禁止內核搶占。
- spin_lock比spin_lock_irq速度快,但是它並不是任何情況下都是安全的。
舉例來說明:進程A中調用了spin_lock(&lock)然后進入臨界區,此時來了一個中斷(interrupt),該中斷也運行在和進程A相同的CPU上,並且在該中斷處理程序中恰巧也會spin_lock(&lock)試圖獲取同一個鎖。由於是在同一個CPU上被中斷,進程A會被設置為TASK_INTERRUPT狀態,中斷處理程序無法獲得鎖,會不停的忙等,由於進程A被設置為中斷狀態,schedule()進程調度就無法再調度進程A運行,這樣就導致了死鎖!但是如果該中斷處理程序運行在不同的CPU上就不會觸發死鎖。 因為在不同的CPU上出現中斷不會導致進程A的狀態被設為TASK_INTERRUPT,只是換出。當中斷處理程序忙等被換出后,進程A還是有機會獲得CPU,執行並退出臨界區。所以在使用spin_lock時要明確知道該鎖不會在中斷處理程序中使用。