Linux中的spinlock機制[四] - API的使用【轉】
轉自:https://zhuanlan.zhihu.com/p/90634198
Linux中的spinlock機制[四] - API的使用
蘭新宇 talk is cheap前面文章介紹的spinlock加鎖的實現都是基於的arch_spin_lock()這個函式,但核心程式設計實際使用的通常是spin_lock(),它們中間還隔了好幾層呼叫關係。先來看最外面的一層(程式碼位於/include/linux/spinlock.h):
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)
接下來,_raw_spin_lock()的實現將出現分野。
【關閉排程】
- SMP實現
spinlock通常是用於多核系統(SMP)的,但它同樣也可以用於單核(UP)的場景。針對SMP,_raw_spin_lock()的實現是這樣的(定義在/include/linux/spinlock_api_smp.h):
#ifdef CONFIG_INLINE_SPIN_LOCK
#define _raw_spin_lock(lock) __raw_spin_lock(lock)
#endif
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
...
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
採用inline函式,可以減少函式呼叫的開銷,提高執行速度,但不利於跟蹤除錯,所以核心提供了"CONFIG_INLINE_SPIN_LOCK"這個配置選項供使用者選擇。
越往內層,函式名前面的下劃線"_"越多。可以看到,在最內側的__raw_spin_lock()中,呼叫了preempt_disable()來關閉排程。也就是說,執行在一個CPU上的程式碼使用spin_lock()試圖加鎖之後,基於該CPU的執行緒排程和搶佔就被禁止了,這也體現了spinlock作為"busy loop"形式的鎖的語義。
到了do_raw_spin_lock()這一步,就進入了和架構相關的arch_spin_lock()。
static inline void do_raw_spin_lock(raw_spinlock_t *lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
...
}
- UP實現
再來看下_raw_spin_lock()針對UP系統的實現(程式碼位於/include/linux/spinlock_api_up.h):
#define _raw_spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); ___LOCK(lock); } while (0)
#define ___LOCK(lock) \
do { __acquire(lock); (void)(lock); } while (0)
在UP的環境中,不再需要防止多個CPU對共享變數的同時訪問,所以spin_lock()的作用僅僅是關閉排程,等同於(或者說退化成了)preempt_disable()。
之所以UP系統也支援使用spinlock相關的函式,是因為這樣同一套程式碼可以同時支援UP和SMP的應用,只需要在配置中選擇是否使用"CONFIG_SMP"就可以了。
【關閉中斷】
spin_lock()可以防止執行緒排程,但不能防止硬體中斷的到來,以及隨後的中斷處理函式(hardirq)的執行,這會帶來什麼影響呢?
試想一下,假設一個CPU上的執行緒T持有了一個spinlock,發生中斷後,該CPU轉而執行對應的hardirq。如果該hardirq也試圖去持有這個spinlock,那麼將無法獲取成功,導致hardirq無法退出。在hardirq主動退出之前,執行緒T是無法繼續執行以釋放spinlock的,最終將導致該CPU上的程式碼不能繼續向前執行,形成死鎖(dead lock)。
為了防止這種情況的發生,我們需要使用spin_lock_irq()函式,一個spin_lock()和local_irq_disable()的結合體,它可以在spinlock加鎖的同時關閉中斷。
因為中斷關閉的操作是可以巢狀的,更多的時候我們是使用local_irq_save()來記錄關中斷的狀態,對應地一個更常用的函式就是spin_lock_irqsave()。
static inline unsigned long __raw_spin_lock_irq_save(raw_spinlock_t *lock)
{
unsigned long flags;
local_irq_save(flags);
__raw_spin_lock(lock);
return flags;
}
然而,local_irq_save()只能對本地CPU執行關中斷操作,所以即便使用了spin_lock_irqsave(),如果其他CPU上發生了中斷,那麼這些CPU上的hardirq,也有可能試圖去獲取一個被本地CPU上執行的執行緒T佔有的spinlock。
不過沒有關係,因為此時hardirq和執行緒T執行在不同的CPU上,等到執行緒T繼續執行釋放了這個spinlock,hardirq就有機會獲取到,不至於造成死鎖。
對於UP系統,spin_lock_irqsave()的作用只剩下關閉中斷了(中斷關閉時不會產生時鐘中斷,排程自然也是關閉的),也就退化成了local_irq_save()。
【遮蔽softirq】
如果hardirq不會和執行緒共享變數,是不是就可以直接使用spin_lock()呢?非也,因為在切回被打斷的執行緒之前,還可能會執行對應的softirq函式。如果該softirq可能訪問和執行緒共享的變數,那麼執行緒就應該使用spin_lock_bh(),一個spin_lock()加local_bh_disable()的二合一函式,否則也可能會導致dead lock。
"bh"代表bottom half,而Linux中的bottom half包括softirq, tasklet和workqueue三種,由於workqueue是執行在程序上下文,所以這裡的"bh"只針對softirq和tasklet。
【API的選擇】
如果關閉了中斷,hardirq不會執行,對應的softirq就更不會執行,可見,使用spin_lock_irqsave()無疑是最安全的,但同時也是開銷最大的。
從程式效能的角度出發,在程序上下文中,對於不會和hardirq/softirq共享的變數,應該儘量使用更輕量級的spin_lock()。只會和softirq共享而不會和hardirq共享的,則應該使用spin_lock_bh()。
對於hardirq上下文,因為Linux是不支援hardirq巢狀的(參考這篇文章評論區的討論),在hardirq執行期間,CPU對中斷的響應預設是關閉的,所以可直接使用spin_lock()。
至於softirq上下文,因為有可能被hardirq打斷,針對會和hardirq共享的變數,需使用spin_lock_irqsave()。
總之,在用一個鎖之前,你得清楚有可能和你競爭這個鎖的對手是誰。
至此,就可以解答上文留下的那個問題,即為什麼一個CPU在一種context下,至多試圖獲取(或者說競爭)一個spinlock。執行緒使用spin_lock()試圖獲取spinlock A,此時發生了中斷,如果hardirq獲取spinlock B,那麼該CPU就同時在試圖獲取2個spinlock。
如果hardirq沒有試圖獲取spinlock,執行完後進入了softirq,softirq試圖獲取spinlock B,然後又被另一箇中斷打斷,新的hardirq在執行過程中又試圖獲取spinlock C,那麼該CPU就同時在試圖獲取3個spinlock。
如果再加入nmi,以此類推,一個CPU至多同時試圖獲取4個spinlock。
spin_lock_irqsave()/spin_lock_bh()可以防止hardirq/softirq和執行緒共享變數造成的死鎖,但這只是死鎖可能出現的一種情況,也可以說是僅依靠選擇合適的API就可以避免的死鎖,更多死鎖的場景和應對辦法,將交由下文討論。
參考:
https://www.kernel.org/doc/htmldocs/kernel-locking/index.html
位元組島 - 自旋鎖spin_lock、spin_lock_irq以及spin_lock_irqsave的區別
原創文章,轉載請註明出處。