2.5 核心中的并發
随着多核筆記本電腦時代的到來,對稱多處理器(SMP)的使用不再被限于高科技使用者。SMP和核心搶占是多線程執行的兩種場景。多個線程能夠同時操作共享的核心資料結構,是以,對這些資料結構的通路必須被串行化。
接下來,我們會讨論并發通路情況下保護共享核心資源的基本概念。我們以一個簡單的例子開始,并逐漸引入中斷、核心搶占和SMP等複雜概念。
2.5.1 自旋鎖和互斥體
通路共享資源的代碼區域稱作臨界區。自旋鎖(spinlock)和互斥體(mutex,mutual exclusion的縮寫)是保護核心臨界區的兩種基本機制。我們逐個分析。
自旋鎖可以確定在同時隻有一個線程進入臨界區。其他想進入臨界區的線程必須不停地原地打轉,直到第1個線程釋放自旋鎖。注意:這裡所說的線程不是核心線程,而是執行的線程。
下面的例子示範了自旋鎖的基本用法:
- #include <linux/spinlock.h>
- spinlock_t mylock = SPIN_LOCK_UNLOCKED; /* Initialize */
- /* Acquire the spinlock. This is inexpensive if there
- * is no one inside the critical section. In the face of
- * contention, spinlock() has to busy-wait.
- */
- spin_lock(&mylock);
- /* ... Critical Section code ... */
- spin_unlock(&mylock); /* Release the lock */
與自旋鎖不同的是,互斥體在進入一個被占用的臨界區之前不會原地打轉,而是使目前線程進入睡眠狀态。如果要等待的時間較長,互斥體比自旋鎖更合适,因為自旋鎖會消耗CPU資源。在使用互斥體的場合,多于2次程序切換時間都可被認為是長時間,是以一個互斥體會引起本線程睡眠,而當其被喚醒時,它需要被切換回來。
是以,在很多情況下,決定使用自旋鎖還是互斥體相對來說很容易:
(1) 如果臨界區需要睡眠,隻能使用互斥體,因為在獲得自旋鎖後進行排程、搶占以及在等待隊列上睡眠都是非法的;
(2) 由于互斥體會在面臨競争的情況下将目前線程置于睡眠狀态,是以,在中斷處理函數中,隻能使用自旋鎖。(第4章将介紹更多的關于中斷上下文的限制。)
下面的例子示範了互斥體使用的基本方法:
- #include <linux/mutex.h>
- /* Statically declare a mutex. To dynamically
- create a mutex, use mutex_init() */
- static DEFINE_MUTEX(mymutex);
- /* Acquire the mutex. This is inexpensive if there
- * is no one inside the critical section. In the face of
- * contention, mutex_lock() puts the calling thread to sleep.
- */
- mutex_lock(&mymutex);
- /* ... Critical Section code ... */
- mutex_unlock(&mymutex); /* Release the mutex */
為了論證并發保護的用法,我們首先從一個僅存在于程序上下文的臨界區開始,并以下面的順序逐漸增加複雜性:
(1) 非搶占核心,單CPU情況下存在于程序上下文的臨界區;
(2) 非搶占核心,單CPU情況下存在于程序和中斷上下文的臨界區;
(3) 可搶占核心,單CPU情況下存在于程序和中斷上下文的臨界區;
(4) 可搶占核心,SMP情況下存在于程序和中斷上下文的臨界區。
舊的信号量接口
互斥體接口代替了舊的信号量接口(semaphore)。互斥體接口是從-rt樹演化而來的,在2.6.16核心中被融入主線核心。
盡管如此,但是舊的信号量仍然在核心和驅動程式中廣泛使用。信号量接口的基本用法如下:
- #include <asm/semaphore.h> /* Architecture dependent header */
- /* Statically declare a semaphore. To dynamically
- create a semaphore, use init_MUTEX() */
- static DECLARE_MUTEX(mysem);
- down(&mysem); /* Acquire the semaphore */
- /* ... Critical Section code ... */
- up(&mysem); /* Release the semaphore */
信号量可以被配置為允許多個預定數量的線程同時進入臨界區,但是,這種用法非常罕見。
1. 案例1:程序上下文,單CPU,非搶占核心
這種情況最為簡單,不需要加鎖,是以不再贅述。
2. 案例2:程序和中斷上下文,單CPU,非搶占核心
在這種情況下,為了保護臨界區,僅僅需要禁止中斷。如圖2-4所示,假定程序上下文的執行單元A、B以及中斷上下文的執行單元C都企圖進入相同的臨界區。
![]() |
(點選檢視大圖)圖2-4 程序和中斷上下文進入臨界區 |
由于執行單元C總是在中斷上下文執行,它會優先于執行單元A和B,是以,它不用擔心保護的問題。執行單元A和B也不必關心彼此會被互相打斷,因為核心是非搶占的。是以,執行單元A和B僅僅需要擔心C會在它們進入臨界區的時候強行進入。為了實作此目的,它們會在進入臨界區之前禁止中斷:
- Point A:
- local_irq_disable(); /* Disable Interrupts in local CPU */
- /* ... Critical Section ... */
- local_irq_enable(); /* Enable Interrupts in local CPU */
但是,如果當執行到Point A的時候已經被禁止,local_irq_enable()将産生副作用,它會重新使能中斷,而不是恢複之前的中斷狀态。可以這樣修複它:
- unsigned long flags;
- Point A:
- local_irq_save(flags); /* Disable Interrupts */
- /* ... Critical Section ... */
- local_irq_restore(flags); /* Restore state to what it was at Point A */
不論Point A的中斷處于什麼狀态,上述代碼都将正确執行。
3. 案例3:程序和中斷上下文,單CPU,搶占核心
如果核心使能了搶占,僅僅禁止中斷将無法確定對臨界區的保護,因為另一個處于程序上下文的執行單元可能會進入臨界區。重新回到圖2-4,現在,除了C以外,執行單元A和B必須提防彼此。顯而易見,解決該問題的方法是在進入臨界區之前禁止核心搶占、中斷,并在退出臨界區的時候恢複核心搶占和中斷。是以,執行單元A和B使用了自旋鎖API的irq變體:
- unsigned long flags;
- Point A:
- /* Save interrupt state.
- * Disable interrupts - this implicitly disables preemption */
- spin_lock_irqsave(&mylock, flags);
- /* ... Critical Section ... */
- /* Restore interrupt state to what it was at Point A */
- spin_unlock_irqrestore(&mylock, flags);
我們不需要在最後顯示地恢複Point A的搶占狀态,因為核心自身會通過一個名叫搶占計數器的變量維護它。在搶占被禁止時(通過調用preempt_disable()),計數器值會增加;在搶占被使能時(通過調用preempt_enable()),計數器值會減少。隻有在計數器值為0的時候,搶占才發揮作用。
4. 案例4:程序和中斷上下文,SMP機器,搶占核心
現在假設臨界區執行于SMP機器上,而且你的核心配置了CONFIG_SMP和CONFIG_PREEMPT。
到目前為止讨論的場景中,自旋鎖原語發揮的作用僅限于使能和禁止搶占和中斷,時間的鎖功能并未被完全編譯進來。在SMP機器内,鎖邏輯被編譯進來,而且自旋鎖原語確定了SMP安全性。SMP使能的含義如下:
- unsigned long flags;
- Point A:
- /*
- - Save interrupt state on the local CPU
- - Disable interrupts on the local CPU. This implicitly disables preemption.
- - Lock the section to regulate access by other CPUs
- */
- spin_lock_irqsave(&mylock, flags);
- /* ... Critical Section ... */
- /*
- - Restore interrupt state and preemption to what it
- was at Point A for the local CPU
- - Release the lock
- */
- spin_unlock_irqrestore(&mylock, flags);
在SMP系統上,擷取自旋鎖時,僅僅本CPU上的中斷被禁止。是以,一個程序上下文的執行單元(圖2-4中的執行單元A)在一個CPU上運作的同時,一個中斷處理函數(圖2-4中的執行單元C)可能運作在另一個CPU上。非本CPU上的中斷處理函數必須自旋等待本CPU上的程序上下文代碼退出臨界區。中斷上下文需要調用spin_lock()/spin_unlock():
- spin_lock(&mylock);
- /* ... Critical Section ... */
- spin_unlock(&mylock);
除了有irq變體以外,自旋鎖也有底半部(BH)變體。在鎖被擷取的時候,spin_lock_bh()會禁止底半部,而spin_unlock_bh()則會在鎖被釋放時重新使能底半部。我們将在第4章讨論底半部。
-rt樹
實時(-rt)樹,也被稱作CONFIG_PREEMPT_RT更新檔集,實作了核心中一些針對低延時的修改。該更新檔集可以從www.kernel.org/pub/linux/kernel/projects/rt下載下傳,它允許核心的大部分位置可被搶占,但是用互斥體代替了一些自旋鎖。它也合并了一些高精度的定時器。數個-rt功能已經被融入了主線核心。詳細的文檔見http://rt.wiki.kernel.org/。
為了提高性能,核心也定義了一些針對特定環境的特定的鎖原語。使能适用于代碼執行場景的互斥機制将使代碼更高效。下面來看一下這些特定的互斥機制。