1. 程式人生 > 實用技巧 >Linux中的spinlock機制[四] - API的使用【轉】

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, taskletworkqueue三種,由於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就可以避免的死鎖,更多死鎖的場景和應對辦法,將交由下文討論。

參考:

位元組島 - 自旋鎖spin_lock、spin_lock_irq以及spin_lock_irqsave的區別

原創文章,轉載請註明出處。