1、為什么需要自旋鎖
很多時候我們並不能采用其他的鎖,比如讀寫鎖、互斥鎖、信號量等。一方面這些鎖會發生上下文切換,他的時間是不可預期的,對於一些簡單的、極短的臨界區完全是一種性能損耗;
另一方面在中斷上下文是不允許睡眠的,除了自旋鎖以外的其他任何形式的鎖都有可能導致睡眠或者進程切換,這是違背了中斷的設計初衷,會發生不可預知的錯誤。
基於兩點,我們需要自旋鎖,他是不可替代的。
2、為什么自旋鎖會禁止搶占
這一點其實很好理解,當一個 CPU 獲取到一把自旋鎖之后,開始執行臨界區代碼,此時假設他的時間片運轉完畢,進程調度會主動觸發調度將其調走,
執行另一個線程/進程,結果恰巧了這個線程/進程也需要用到該自旋鎖,而上一個線程/進程還在停留在臨界區內未釋放鎖,導致本進程無法獲取到鎖而形成死鎖,
所以自旋鎖為了規避此類情形的出現從而直接禁止對已經開始運行的臨界區設置禁止搶占標志。
3、為什么臨界區禁止睡眠
如果自旋鎖鎖住以后進入睡眠,而此時又不能進行處理器搶占,內核的調取器無法調取其他進程獲得該 CPU,從而導致該 CPU 被掛起;
同時該進程也無法自喚醒且一直持有該自旋鎖,進一步會導致其他使用該自旋鎖的位置出現死鎖。
4、自旋鎖相關源碼
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); }
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); }
spin_lock_irqsave------>__raw_spin_lock_irqsave
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) { unsigned long flags; local_irq_save(flags); // 關閉中斷,並保存中斷的狀態 preempt_disable(); // 禁止內核搶占 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); /* * On lockdep we dont want the hand-coded irq-enable of * do_raw_spin_lock_flags() code, because lockdep assumes * that interrupts are not re-enabled during lock-acquire: */ #ifdef CONFIG_LOCKDEP LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); #else do_raw_spin_lock_flags(lock, &flags); #endif return flags; }
local_irq_save------> arch_local_irq_save
static __always_inline unsigned long arch_local_irq_save(void) { unsigned long flags = arch_local_save_flags(); // 保存中斷狀態 arch_local_irq_disable(); // 關閉中斷 return flags; }
總結:
spin_lock比spin_lock_irq速度快,但是它並不是任何情況下都是安全的。在使用spin_lock時要明確知道該鎖不會在中斷處理程序中使用。
spin_lock_irq在自旋的時候,不會保存當前的中斷標志寄存器,只會在自旋結束后,將之前的中斷打開。
spin_lock_irqsave在鎖返回時,之前開的中斷,之后也是開的;之前關,之后也是關。但是spin_lock_irq則不管之前的開還是關,返回時都是開的。
spin_lock_irq 和 spin_unlock_irq, 如果你確定在獲取鎖之前本地中斷是開啟的,那么就不需要保存中斷狀態,解鎖的時候直接將本地中斷啟用就可以了。
spin_lock和spin_lock_irq在特殊情況下會導致死鎖,spin_lock_irqsave是最安全的。
5、自旋鎖的使用場景
spin_lock 使用場景
首先如果整個臨界區都只位於進程上下文或者工作隊列中,那么只需要采用最為方便的 spin_lock 即可,因為他不會發生中斷搶占鎖的情況,
哪怕中斷搶占進程上下文也不會導致中斷由於申請自旋鎖而導致死鎖。
還有一種情況就是在硬件中斷中可以考慮使用 spin_lock 即可,因為硬件中斷不存在嵌套(未必一定是這樣,與平台有關),
所以只需要簡單的上鎖即可, 可以不需要關閉中斷,保存堆棧等。
spin_lock_irq 使用場景
這個鎖的變種適合在進程上下文/軟中斷 + 硬件中斷這樣的組合中使用,taskset 也是屬於軟中斷的一種,所以也歸在此類。
當然,這種類型的變種同樣適合軟中斷/taskset + 進程上下文的組合,因為關閉了硬件中斷,從源頭就禁止執行軟中斷代碼,
不過,對於這種類型的中斷最好的方式是使用 spin_lock_bh 的方式,因為他只鎖定軟中斷代碼執行,而不關閉硬件中斷,這樣性能損耗更小。
spin_lock_irqsave 使用場景
這種類型的使用方式是最為安全以及便捷的,畢竟不需要考慮會不會發生死鎖的問題(代碼本身引入的死鎖不在此類),
但是他也是性能損耗最大的代碼,能不使用盡量不適用,在高速設備上,自旋鎖已然成為了一種降低性能的瓶頸。
他最好只出現在在需要嘗試 spin_lock 之前無法確定是否已經關閉中斷的代碼才使用,
如果代碼能夠確定在執行鎖之前中斷一定是打開的,那么使用 spin_lock_irq 是更佳的選擇。
spin_lock_bh 使用場景
這種類型的變種是一種比 spin_lock_irq 更輕量的變種,只關閉中斷底半部,其實就是關閉了軟中斷、Taskset 以及 Timer 等的一個搶占能力,
如果開發者確定編寫的代碼臨界區只存在軟中斷/Taskset/Timer + 進程上下文這樣的組合,則最好考慮使用 spin_lock_bh 這樣的鎖來禁止軟中斷進行搶占。
還有就是軟中斷與軟中斷自我搶占臨界區訪問時,也需要使用 spin_lock_bh 以上的中斷鎖,因為有可能軟中斷在執行的過程中,
自己被硬件中斷打斷,然后又執行到同樣的代碼,在別的 CPU 執行還好說,畢竟軟中斷可以在不同的 CPU 上執行同一個中斷函數,
但是假設不幸運行在同一個 CPU 上,則會導致死鎖。Taskset 由於在運行過程中鍾只會運行一個實例,
所以不存在死鎖問題,Taskset 與 Taskset 的鎖競爭只需要使用 spin_lock 即可。
參考引用:
什么情況下使用什么樣的自旋鎖:http://blog.csdn.net/wesleyluo/article/details/8807919
本地中斷概念的理解:http://blog.csdn.net/adaptiver/article/details/6177646
深入理解自旋鎖:http://blog.csdn.net/vividonly/article/details/6594195
自旋鎖和互斥量區別:http://blog.csdn.net/kyokowl/article/details/6294341
自旋鎖的臨界區本地cpu不會發生任何進程調度:http://blog.chinaunix.net/uid-23769728-id-3367773.html
https://www.cnblogs.com/aaronLinux/p/5890924.html
https://zhuanlan.zhihu.com/p/371721341
https://stackoverflow.com/questions/2559602/spin-lock-irqsave-vs-spin-lock-irq