面試必備進程同步機制--內核自旋鎖

進程(線程)間的同步機制是面試時的常見問題,因此準備用一個系列來好好整理下用戶態與內核態的各類同步機制。本文就之內核空間的一種基礎同步機制--- 自旋鎖開始好了

自旋鎖是什麼

自旋鎖就是一個二狀態的原子(atomic)變量:php

  • unlocked
  • locked

自旋

當任務A但願訪問被自旋鎖保護的臨界區(Critical Section),它首先須要這個自旋鎖當前處於unlocked狀態,而後它會去嘗試獲取(acquire)這個自旋鎖(將這個變量狀態修改成locked),linux

若是在這以後有另外一個任務B一樣但願去訪問這段這段臨界區,那麼它必需要等到任務A釋放(release)掉自旋鎖才行,在這以前,任務B會一直等待此處,不段嘗試獲取(acquire),也就是咱們說的自旋在這裏。面試

自旋鎖有什麼特色

若是被問到這個問題,很多人可能根據上面的定義也能總結出來了:函數

  • "保護臨界區"
  • "一直忙等待,直到鎖被其餘人釋放"
  • "適合用在等待時間很短的場景中"

說錯了嗎?固然沒有!而且這些的確都是自旋鎖的特色,那麼更多呢 ?ui

幾個基本概念

爲何內核須要引入自旋鎖?回答這個問題以前我想先簡單引入如下幾個基本概念:atom

UP & SMP

UP表示單處理器,SMP表示對稱多處理器(多CPU)。一個處理器就視爲一個執行單元,在任何一個時刻,只能運行在一個進程上下文或者中斷上下文裏。spa

SMP

中斷(interrupt)

中斷能夠發生在任務的指令過程當中,若是中斷處於使能,會從任務所處的進程上下文切換到中斷上下文,在中斷上下文中進行所謂的中斷處理(ISR)。.net

isr

內核中使用 local_irq_disable()或者local_irq_save(&flags)來去使能中斷。二者的區別是後者會將當前的中斷使能狀態先保存到flags中。線程

相反,內核使用local_irq_enale()來無條件的使能中斷,而使用local_irq_restore(&flags)來恢復以前的中斷狀態。3d

不管是開中斷仍是關中斷的函數都有local前綴, 這表示開關中斷的只在當前CPU生效。

內核態搶佔(preempt)

搶佔,通俗的理解就是內核調度時,高優先級的任務從低優先的任務中搶到CPU的控制權,開始運行,其中又分爲用戶態搶佔內核態搶佔, 本文須要關心的是內核態搶佔

早期版本(比2.6更早的)的內核仍是非搶佔式內核,也就是說當高優先級任務就緒時,除非低優先級任務主動放棄CPU(好比阻塞或者主動調用Schedule觸發調度),不然高優先級任務是沒有機會運行的。

而在此以後,內核可配置爲搶佔式內核(默認),在一些時機(好比說中斷處理結束,返回內核空間時),會觸發從新調度,此時高優先級的任務能夠搶佔原來佔用CPU的低優先級任務。

4

須要特別指出的是,搶佔一樣須要中斷處於打開狀態!

void __sched notrace preempt_schedule(void)
{
    struct thread_info *ti = current_thread_info();

    /*
     * If there is a non-zero preempt_count or interrupts are disabled,
     * we do not want to preempt the current task. Just return..
     */
    if (likely(ti->preempt_count || irqs_disabled()))
        return;

上面代碼中的 preempt_count表示當前任務是否可被搶佔,0表示能夠被搶佔,而大於0表示不能夠。而irqs_disabled用來看中斷是否關閉。

內核中使用preemt_disbale()來禁止搶佔,使用preempt_enable()來使能可搶佔。

單處理器上臨界區問題

對於單處理器來講,因爲任何一個時刻只會有一個執行單元,所以不存在多個執行單元同時訪問臨界區的狀況。可是依然存在下面的情形須要保護

Case 1 任務上下文搶佔

低優先級任務A進入臨界區,但此時發生了調度(好比發生了中斷, 而後從中斷中返回),高優先級任務B開始運行訪問臨界區。

5
解決方案:進入臨界區前禁止搶佔就行了。這樣即便發生了中斷,中斷返回也只能回到任務A.

Case 2 中斷上下文搶佔

任務A進入臨界區,此時發生了中斷,中斷處理函數中也去訪問修改臨界區。當中斷處理結束時,返回任務A的上下文,但此時臨界區已經變了!

6

解決方案:進入臨界區前禁止中斷(順便說一句,這樣也順便禁止了搶佔)

Case 3 多處理器上臨界區問題

除了單處理器上的問題以外,多處理上還會面臨一種須要保護的情形

其餘CPU訪問

任務A運行在CPU_a上,進入臨界區前關閉了中斷(本地),而此時運行在CPU_b上的任務B仍是能夠進入臨界區!沒有人能限制它

7

解決方案:任務A進入臨界區前持有一個互斥結構,阻止其餘CPU上的任務進入臨界區,直到任務A退出臨界區,釋放互斥結構。

這個互斥結構就是自旋鎖的來歷。因此本質上,自旋鎖就是爲了針對SMP體系下的同時訪問臨界區而發明的!

內核中的自旋鎖實現

接下來,咱們來看一下內核中的自旋鎖是如何實現的,個人內核版本是4.4.0

定義

內核使用spinlock結構表示一個自旋鎖,若是不開調試信息的話,這個結構就是一個·raw_spinlock·:

typedef struct spinlock {
    union {
        struct raw_spinlock rlock;
        // code omitted
    };
} spinlock_t;

raw_spinlock這個結構展開, 能夠看到這是一個體系相關的arch_spinlock_t結構

typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
    // code omitted
} raw_spinlock_t;

本文只關心常見的x86_64體系來講,這種狀況下上述結構可展開爲

typedef struct qspinlock {
    atomic_t    val;
} arch_spinlock_t;

上面的結構是SMP上的定義,對於UParch_spinlock_t就是一個空結構

typedef struct { } arch_spinlock_t;

啊,自旋鎖就是一個原子變量(修改這個變量會LOCK總線,所以能夠避免多個CPU同時對其進行修改)

API

內核使用spin_lock_init來進行自旋鎖的初始化

# define raw_spin_lock_init(lock)                \
    do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)
    
#define spin_lock_init(_lock)                \
do {                            \
    spinlock_check(_lock);                \
    raw_spin_lock_init(&(_lock)->rlock);        \
} while (0)

最終val會設置爲0 (對於UP,不存在這個賦值)

內核使用spin_lockspin_lock_irq或者spin_lock_irqsave 完成加鎖操做;使用 spin_unlockspin_unlock_irq或者spin_unlock_irqsave完成對應的解鎖。

spin_lock / spin_unlock

static inline void spin_lock(spinlock_t *lock)
{
    raw_spin_lock(&lock->rlock);
}

對於UP,raw_spin_lock最後會展開爲_LOCK

# define __acquire(x) (void)0

#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

能夠看到,它就是單純地禁止搶佔。這是上面Case 1的解決辦法

而對於SMP, 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_acquire在沒設置CONFIG_DEBUG_LOCK_ALLOC時是空操做, 因此關鍵的語句是最後一句,將其展開後是

#define LOCK_CONTENDED(_lock, try, lock) \
    lock(_lock)

因此,真正生效的是

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
    __acquire(lock);
    arch_spin_lock(&lock->raw_lock);
}

__acquire並不重要。而arch_spin_lock定義在include/asm-generic/qspinlock.h.這裏會檢查val,若是當前鎖沒有被持有(值爲0),那麼就經過原子操做將其修改成1並返回。

不然就調用queued_spin_lock_slowpath一直自旋。

#define arch_spin_lock(l)        queued_spin_lock(l)

static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
    u32 val;

    val = atomic_cmpxchg(&lock->val, 0, _Q_LOCKED_VAL);
    if (likely(val == 0))
        return;
    queued_spin_lock_slowpath(lock, val);
}

以上就是spin_lock()的實現過程,能夠發現除了咱們熟知的等待自旋操做以外,它會在以前先調用preempt_disable禁止搶佔,不過它並無禁止中斷,也就是說,它能夠解決前面說的Case 1Case 3

Case 2仍是有問題!

使用這種自旋鎖加鎖方式時,若是本地CPU發生了中斷,在中斷上下文中也去獲取該自旋鎖,這就會致使死鎖

所以,使用spin_lock()須要保證知道該鎖不會在該CPU的中斷中使用(其餘CPU的中斷沒問題)

解鎖時成對使用的spin_unlock基本就是加鎖的逆向操做,在設置了val從新爲0以後,使能搶佔。

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    spin_release(&lock->dep_map, 1, _RET_IP_);
    do_raw_spin_unlock(lock);
    preempt_enable();
}

spin_lock_irq / spin_unlock_irq

這裏咱們就只關注SMP的情形了,相比以前的spin_lock中調用__raw_spin_lock, 這裏多出的一個操做的就是禁止中斷。

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);
}

前面說過,實際禁止中斷的時候也就不會發生搶佔了,那麼這裏其實使用preemt_disable禁止搶佔是個有點多餘的動做。

關於這個問題,能夠看如下幾個鏈接的討論
CU上的討論
Stackoverflow上的回答
linux DOC

對於的解鎖操做是spin_unlock_irq會調用__raw_spin_unlock_irq。相比前一種實現方式,多了一個local_irq_enable

static inline void __raw_spin_unlock_irq(raw_spinlock_t *lock)
{
    spin_release(&lock->dep_map, 1, _RET_IP_);
    do_raw_spin_unlock(lock);
    local_irq_enable();
    preempt_enable();
}

這種方式也就解決了Case 2

spin_lock_irqsave / spin_unlock_irqsave

spin_lock_irq還有什麼遺漏嗎?它沒有遺漏,但它最後使用local_irq_enable打開了中斷,若是進入臨界區前中斷原本是關閉,那麼經過這一進一出,中斷居然變成打開的了!這顯然不合適!

所以就有了spin_lock_irqsave和對應的spin_unlock_irqsave.它與上一種的區別就在於加鎖時將中斷使能狀態保存在了flags

static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
    unsigned long flags;

    local_irq_save(flags);   // 保存中斷狀態到flags
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    do_raw_spin_lock_flags(lock, &flags);
    
    return flags;
}

而在對應的解鎖調用時,中斷狀態進行了恢復,這樣就保證了在進出臨界區先後,中斷使能狀態是不變的。

static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock,
                        unsigned long flags)
{
    spin_release(&lock->dep_map, 1, _RET_IP_);
    do_raw_spin_unlock(lock);
    local_irq_restore(flags);   // 從 flags 恢復
    preempt_enable();
}

總結

  • 內核自旋鎖的主要用於SMP系統上的臨界區保護,而且在UP系統上也有簡化的實現
  • 內核自旋鎖與搶佔中斷的關係密切
  • 內核自旋鎖在內核有多個API,實際使用時能夠靈活使用。
相關文章
相關標籤/搜索