Linux核心原始碼之自旋鎖的實現
1 Linux核心同步
Linux核心中有許多共享資源,這些共享資源是核心中程序都有機會訪問到的。核心對其中一些共享資源的訪問是獨佔的,因此需要提供機制對共享資源進行保護,確保任意時刻只有一個程序在訪問共享資源。自旋鎖就是一種共享資源保護機制,確保同一時刻只有一個程序能訪問到共享資源。
2 普通自旋鎖
核心中提供的普通自旋鎖API為spin_lock()何spin_unlock(),使用的方法為:
spin_lock();
...臨界區...
spin_unlock();
核心保證spin_lock()和spin_unlock()之間的臨界區程式碼在任意時刻只會由一個CPU進行訪問,並且當前CPU訪問期間不會發生程序切換,當前程序也不會進入睡眠,這個在後面還會進一步分析。
3 單處理器(UP)普通自旋鎖
之前講過,自旋鎖的功能有兩點,一是臨界區程式碼任意時刻由一個CPU進行訪問,二是當前CPU訪問期間不會發生程序切換。對於單處理器來說第一個問題就不存在了,因為只有一個CPU,不存在多處理器訪問的問題。因此單處理器自旋鎖只要保證CPU對臨界區程式碼訪問期間不發生程序切換就行了。知道了這一點後讓我們來看看單處理器普通自旋鎖使用的資料結構和API的實現程式碼。以下程式碼和資料結構使用的核心版本是Linux-2.6.24,特此註明。
3.1 資料結構
普通自旋鎖的資料型別為spinlock_t:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} spinlock_t;
可以看出,去掉了編譯配置選項之後其實spinlock_t裡只剩下了型別為raw_spinlock_t的資料結構raw_lock:
typedef struct { } raw_spinlock_t;
這裡讀者可能要有疑問了,問什麼raw_spinlock_t是個空的資料結構呢,這就和下面將要說到的單處理器普通自旋鎖的實現了,因為單處理器的實現根本就不需要對資料結構進行處理。
3.2 API的實現
先看spin_lock()的實現:
#define spin_lock(lock) _spin_lock(lock) //lock資料型別為*spinlock_t,雖然沒有用到-_-。
#define _spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)
#define __acquire(x) (void)0
其中preempt_disable()的工作是關閉核心搶佔,__acquire(lock)是空操作,(void)(lock)是簡單的資料轉型操作,防止編譯器對lock未使用報警。看到這裡,讀者應該就明白了,在單處理器中spin_lock()所做的工作僅僅是關閉核心搶佔而已,僅此而已,這就保證了在執行期間不會發生程序搶佔,從而也就保證了臨界區裡的程式碼只有當前程序才會訪問到,在當前程序釋放臨界區之前都不會有別的程序能夠訪問。
再來看看spin_unlock()的實現:
#define spin_unlock(lock) _spin_unlock(lock)
#define _spin_unlock(lock) __UNLOCK(lock)
#define __UNLOCK(lock) \
do { preempt_enable(); __release(lock); (void)(lock); } while (0)
#define __release(x) (void)0
看懂了上面的spin_lock(),spin_unlock()所做的工作也就很清楚了。spin_unlock()和spin_lock()所做的工作是相反的,spin_lock()關閉了核心搶佔,則spin_unlock()開啟核心搶佔。也就是說在關閉了核心搶佔後,程序進入臨界區,由於核心搶佔已經關閉,因此當前程序不會被其他程序所搶佔。完成相應任務後開啟核心搶佔,釋放臨界區,此時其他程序就可以搶佔CPU從而訪問臨界區了。也就是說,普通自旋鎖執行過程就是 關閉核心搶佔->訪問臨界區程式碼->開啟核心搶佔。
3.3 普通自旋鎖存在的風險
雖然普通自旋鎖通過關閉核心搶佔獨佔CPU資源阻止了了其餘程序訪問臨界區資源,但是還有一種特殊的情況。關閉核心搶佔只是組織了其餘程序對CPU的搶佔,但是並不能阻止中斷程式對CPU的搶佔,如果中斷服務程式想要訪問臨界區的話就有可能造成資源的併發訪問,從而導致中斷結束後進程訪問的資源被改變了,從而導致錯誤。為此,Linux核心提供了更加安全的自旋鎖API spin_lock_irqsave()和spin_unlock_irqrestore()。
spin_lock_irqsave()先將處理器狀態指令暫存器IF的內容儲存起來,然後通過cli指令關閉中斷,然後再執行和spin_lock()相同的步驟。
spin_unlock_irqrestore()先執行和spin_unlock()相同的步驟,即開啟核心搶佔,然後通過sti指令開啟中斷,最後之前儲存的值恢復到處理器狀態指令暫存器IF中。
之前講過在執行由自旋鎖保護的臨界區程式碼時不允許進入睡眠狀態,不僅臨界區內的程式碼不允許進入睡眠狀態,臨界區內程式碼所呼叫的程式碼也不允許進入睡眠狀態。首先,自旋鎖一般只在需要短時間訪問共享資源是才會使用,一般都是馬上就能完成的任務,不需要進入睡眠等待什麼資源。其次,進入臨界區之後提供的中斷和核心搶佔都已經關閉了,基於系統時鐘中斷的程序切換也就失效了,也就是說進入睡眠狀態之後可能就會永遠的睡下去了,因為沒有激勵訊號來把進入睡眠的程序喚醒。但是有一個特例,那就是kmalloc函式,當分配失敗時它可以進入睡眠狀態。總而言之,執行由自旋鎖保護的臨界區程式碼時,不允許程式進入睡眠;同時臨界區應該是短時間就可以完成的任務,因為在多處理器架構中自旋鎖會進行忙等待,白白佔用CPU資源,接下來就講解多處理器架構中自旋鎖的實現。
4 多處理器(SMP)普通自旋鎖
之前講過,自旋鎖要保證任意時刻只有一個CPU執行在臨界區內,同時執行在臨界區內的CPU不允許進行程序切換。在單處理器中通過關閉核心搶佔保證了程序對資源的訪問不被打擾,在多處理器中情況就要麻煩一些了,因為還要應付來自其他處理器的干擾。在多處理器中也是通過關閉核心搶佔來保證對臨界區的訪問不受執行在當前CPU上的其餘程序的打擾,那麼怎麼應付來自其他處理器的干擾呢,帶著這個問題讓我們接著往下看。
4.1 資料結構
多處理器中spinlock_t的定義和單處理器中的一樣,這裡就不再贅述了,不同的是raw_spinlock_t不再是空資料結構了:
typedef struct {
unsigned int slock;
} raw_spinlock_t;
由此可以看出,在多處理其中普通自旋鎖其實是使用了一個整數作為計數器,自旋鎖初始化的時候會將計數器的值設為1,表示自旋鎖當前可用。下面來看自旋鎖API的實現。
4.2 API的實現
#define spin_lock(lock) _spin_lock(lock) //lock資料型別為*spinlock_t,雖然沒有用到-_-。
void __lockfunc _spin_lock(spinlock_t *lock)
{
preempt_disable();//關搶佔
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);//空操作
LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);
}
LOCK_CONTENDED是個巨集定義,在當前lock為普通自旋鎖時,會以lock為引數執行_raw_spin_lock()函式,_raw_spin_lock()定義如下:
#define _raw_spin_lock(lock) __raw_spin_lock(&(lock)->raw_lock);
__raw_spin_lock()的實現是跟體系結構相關的,下面來看看在x86裡面的實現:
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
asm volatile(
"\n1:\t"
LOCK_PREFIX " ; decl %0\n\t"
"jns 2f\n"
"3:\n"
"rep;nop\n\t"
"cmpl $0,%0\n\t"
"jle 3b\n\t"
"jmp 1b\n"
"2:\t" : "=m" (lock->slock) : : "memory");
}
指令字首LOCK_PREFIX表示執行這條指令時將匯流排鎖住,不讓其他處理器方位,以此來保證這條指令執行的“原子性”,%0表示lock->slock,第一句話表示將lock->slock減一。第二句話進行判斷,如果減一之後大於或等於零則表示加鎖成功,則調到標號2處,程式碼2後面沒有繼續執行的程式碼了,因此會返回。如果減一之後小於零,則表示之前已經有程序進行了加鎖操作,則跳到標號3處執行,將lock->slock與0進行比較,如果小於零則再次跳到3處執行,即迴圈執行標號3處的指令。直到加鎖者釋放鎖將lock->slock設為1,此時會跳到標號1處進行加鎖操作。
與 __raw_spin_lock()相對應的解鎖函式是 __raw_spin_unlock(),他的作用是將lock->slock的值設為1,僅此而已。只有加鎖者將lock->slock設為1之後其他在忙等待的CPU才能進行加鎖,結合之前的__raw_spin_lock()應該不難理解。
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
asm volatile("movl $1,%0" :"=m" (lock->slock) :: "memory");
}
5 總結
單處理器自旋鎖的工作流程是:關閉核心搶佔->執行臨界區程式碼->開啟核心搶佔。更加安全的單處理器自旋鎖工作流程是:儲存IF暫存器->關閉當前CPU中斷->關閉核心搶佔->執行臨界區程式碼->開啟核心搶佔->開啟當前CPU中斷->恢復IF暫存器。
多處理器自旋鎖的工作流程是:關閉核心搶佔->(忙等待->)獲取自旋鎖->執行臨界區程式碼->釋放自旋鎖->開啟核心搶佔。更加安全的多處理器自旋鎖工作流程是:儲存IF暫存器->關閉當前CPU中斷->關閉核心搶佔->(忙等待->)獲取自旋鎖->執行臨界區程式碼->釋放自旋鎖->開啟核心搶佔->開啟當前CPU中斷->恢復IF暫存器。