進程(線程)間的同步機制是面試時的常見問題,因此準備用一個系列來好好整理下用戶態與內核態的各類同步機制。本文就之內核空間的一種基礎同步機制---
自旋鎖
開始好了
自旋鎖就是一個二狀態的原子(atomic
)變量:php
unlocked
locked
當任務A
但願訪問被自旋鎖保護的臨界區(Critical Section),它首先須要這個自旋鎖當前處於unlocked
狀態,而後它會去嘗試獲取(acquire
)這個自旋鎖(將這個變量狀態修改成locked
),linux
若是在這以後有另外一個任務B
一樣但願去訪問這段這段臨界區,那麼它必需要等到任務A
釋放(release
)掉自旋鎖才行,在這以前,任務B
會一直等待此處,不段嘗試獲取(acquire
),也就是咱們說的自旋
在這裏。面試
若是被問到這個問題,很多人可能根據上面的定義也能總結出來了:函數
說錯了嗎?固然沒有!而且這些的確都是自旋鎖的特色,那麼更多呢 ?ui
爲何內核須要引入自旋鎖?回答這個問題以前我想先簡單引入如下幾個基本概念:atom
UP
表示單處理器,SMP
表示對稱多處理器(多CPU
)。一個處理器就視爲一個執行單元,在任何一個時刻,只能運行在一個進程上下文或者中斷上下文裏。spa
中斷能夠發生在任務的指令過程當中,若是中斷處於使能,會從任務所處的進程上下文切換到中斷上下文,在中斷上下文中進行所謂的中斷處理(ISR
)。.net
內核中使用 local_irq_disable()
或者local_irq_save(&flags)
來去使能中斷。二者的區別是後者會將當前的中斷使能狀態先保存到flags
中。線程
相反,內核使用local_irq_enale()
來無條件的使能中斷,而使用local_irq_restore(&flags)
來恢復以前的中斷狀態。3d
不管是開中斷仍是關中斷的函數都有local
前綴, 這表示開關中斷的只在當前CPU
生效。
搶佔
,通俗的理解就是內核調度時,高優先級的任務從低優先的任務中搶到CPU
的控制權,開始運行,其中又分爲用戶態搶佔和內核態搶佔, 本文須要關心的是內核態搶佔。
早期版本(比2.6
更早的)的內核仍是非搶佔式內核,也就是說當高優先級任務就緒時,除非低優先級任務主動放棄CPU(好比阻塞或者主動調用Schedule
觸發調度),不然高優先級任務是沒有機會運行的。
而在此以後,內核可配置爲搶佔式內核(默認),在一些時機(好比說中斷處理結束,返回內核空間時),會觸發從新調度,此時高優先級的任務能夠搶佔原來佔用CPU
的低優先級任務。
須要特別指出的是,搶佔一樣須要中斷處於打開狀態!
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()
來使能可搶佔。
對於單處理器來講,因爲任何一個時刻只會有一個執行單元,所以不存在多個執行單元同時訪問臨界區的狀況。可是依然存在下面的情形須要保護
低優先級任務A
進入臨界區,但此時發生了調度(好比發生了中斷, 而後從中斷中返回),高優先級任務B
開始運行訪問臨界區。
解決方案:進入臨界區前禁止搶佔就行了。這樣即便發生了中斷,中斷返回也只能回到任務A
.
任務A
進入臨界區,此時發生了中斷,中斷處理函數中也去訪問修改臨界區。當中斷處理結束時,返回任務A
的上下文,但此時臨界區已經變了!
解決方案:進入臨界區前禁止中斷(順便說一句,這樣也順便禁止了搶佔)
除了單處理器上的問題以外,多處理上還會面臨一種須要保護的情形
任務A
運行在CPU_a
上,進入臨界區前關閉了中斷(本地),而此時運行在CPU_b
上的任務B
仍是能夠進入臨界區!沒有人能限制它
解決方案:任務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
上的定義,對於UP
,arch_spinlock_t
就是一個空結構
typedef struct { } arch_spinlock_t;
啊,自旋鎖就是一個原子變量(修改這個變量會LOCK
總線,所以能夠避免多個CPU
同時對其進行修改)
內核使用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_lock
、spin_lock_irq
或者spin_lock_irqsave
完成加鎖操做;使用 spin_unlock
、spin_unlock_irq
或者spin_unlock_irqsave
完成對應的解鎖。
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 1
和Case 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(); }
這裏咱們就只關注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_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
,實際使用時能夠靈活使用。