1. 中斷屏蔽(關中斷)
在單 CPU 範圍内避免競态的一種簡單方法是在進入臨界區之前屏蔽系統的中斷。
CPU 一般都具備屏蔽中斷和打開中斷的功能,這項功能可以保證正在執行的核心執行路徑不被中斷處理程式所搶占,防止某些競态條件的發生。具體而言,中斷屏蔽将使得中斷與程序之間的并發不再發生,而且,由于 Linux 核心的程序排程等操作都依賴中斷來實作,核心搶占程序之間的并發也就得以避免了。
由于 Linux 系統的異步 I/O、程序排程等很多重要操作都依賴于中斷,中斷對于核心的運作非常重要,在屏蔽中斷期間所有的中斷都無法得到處理,是以長時間屏蔽中斷是很危險的,有可能造成資料丢失甚至系統崩潰。這就要求在屏蔽了中斷之後,目前的核心執行路徑應當盡快地執行完臨界區的代碼。
local_irq_disable()和 local_irq_enable()都隻能禁止和使能本 CPU 内的中斷, 是以,并不能解決 SMP 多 CPU 引發的競态。是以,單獨使用中斷屏蔽通常不是一種值得推薦的避免競态的方法,它适宜與自旋鎖聯合使用。
如果隻是想禁止中斷的底半部,應使用 local_bh_disable(), 使能底半部應該調用 local_bh_enable()。
2. 自旋鎖
自旋鎖(spin lock)是一個典型的對臨界資源的互斥手段,它的名稱來源于它的特性。為了獲得一個自旋鎖,在某CPU上運作的代碼需先執行一個原子操作,該操作測試并設定(test-and-set)某個記憶體變量,由于它是原子操作,是以在該操作完成之前其它CPU不可能通路這個記憶體變量。如果測試結果表明鎖已經空閑,則程式獲得這個自旋鎖并繼續執行。如果測試結果表明鎖仍被占用,程式将在一個小的循環内重複這個“測試并設定(test-and-set)”操作,即開始“自旋”。最後,鎖的所有者通過重置該變量釋放這個自旋鎖,于是,某個等待的test-and-set操作向其調用者報告鎖已釋放。
了解自旋鎖最簡單的方法是把它作為一個變量看待,這個變量把一個臨界區或者标記為“我目前在另一個CPU上運作,請稍等一會”,或者标記為“我目前不在運作,可以被使用”。如果1号CPU首先進入該例程,它就擷取該自旋鎖;當2号CPU試圖進入同一個例程時,該自旋鎖告訴它自己已為1号CPU所持有,需等到1号CPU釋放自己後才能進入。
自旋鎖主要針對SMP或單CPU且核心可搶占的情況,對于單CPU且核心不可搶占的系統自旋鎖退化為空操作。
盡管自旋鎖可以保證臨界區不受别的CPU和本CPU的搶占程序打擾,但是得到鎖的代碼路徑在執行臨界區的時候還可能受到中斷和底半部影響,此時應該使用 自旋鎖的衍生操作。
驅動工程師應謹慎使用自旋鎖, 而且在使用中還要特别注意如下幾個問題。
1) 自旋鎖實際上是忙等鎖, 當鎖不可用時, CPU一直循環執行“測試并設定”該鎖直到可用而取得該鎖, CPU在等待自旋鎖時不做任何有用的工作, 僅僅是等待。 是以, 隻有在占用鎖的時間極短的情況下,使用自旋鎖才是合理的。 當臨界區很大, 或有共享裝置的時候, 需要較長時間占用鎖, 使用自旋鎖會降低系統的性能。
2) 自旋鎖可能導緻系統死鎖。 引發這個問題最常見的情況是遞歸使用一個自旋鎖, 即如果一個已經擁有某個自旋鎖的CPU想第二次獲得這個自旋鎖, 則該CPU将死鎖。
3) 在自旋鎖鎖定期間不能調用可能引起程序排程的函數。 如果程序獲得自旋鎖之後再阻塞, 如調用copy_from_user() 、 copy_to_user() 、 kmalloc() 和msleep() 等函數, 則可能導緻核心的崩潰。
4) 在單核情況下程式設計的時候, 也應該認為自己的CPU是多核的, 驅動特别強調跨平台的概念。 比如, 在單CPU的情況下, 若中斷和程序可能通路同一臨界區, 程序裡調用spin_lock_irqsave() 是安全的, 在中斷裡其實不調用spin_lock() 也沒有問題, 因為spin_lock_irqsave() 可以保證這個CPU的中斷服務程式不可能執行。 但是, 若CPU變成多核, spin_lock_irqsave() 不能屏蔽另外一個核的中斷, 是以另外一個核就可能造成并發問題。 是以, 無論如何, 我們在中斷服務程式裡也應該調用spin_lock() 。
3. 互斥體
互斥體實作了“互相排斥”(mutual exclusion)同步的簡單形式(是以名為互斥體(mutex))。互斥體禁止多個線程同時進入受保護的代碼“臨界區”(critical section)。是以,在任意時刻,隻有一個線程被允許進入這樣的代碼保護區。
任何線程在進入臨界區之前,必須擷取(acquire)與此區域相關聯的互斥體的所有權。如果已有另一線程擁有了臨界區的互斥體,其他線程就不能再進入其中。這些線程必須等待,直到目前的屬主線程釋放(release)該互斥體。
新的Linux核心傾向于直接使用互斥體,而不是信号量作為互斥。
4. 自旋鎖和互斥體的差別
自旋鎖和互斥鎖都是解決互斥問題的基本手段,這兩種所的差別:
1)互斥鎖和自旋鎖屬于不同層次的互斥手段,前者的實作依賴于後者,在互斥體本身的實作上,為了保證互斥體結構存取的原子性,需要自旋鎖來互斥,是以自旋鎖屬于更底層的操作。
2)互斥鎖是程序級别的,用于對各程序之間對資源的互斥,雖然也在核心中,但是該核心執行路徑是以程序的身份,代表程序來争奪資源的,如果競争失敗,會發生程序上下文的切換,目前程序進入睡眠狀态,CPU将運作于其他程序。由于程序上下文切換開銷比較大,是以程序占用資源時間較長時用互斥鎖才是比較好的選擇。
3)當要保護的臨界區通路時間很短時,用自旋鎖是非常友善的,因為它可以節省上下文切換的開銷。但是CPU如果得不到自旋鎖會忙等執行臨界區解鎖為止,是以要求鎖不能再臨界區長時間停留。
由此, 可以總結出自旋鎖和互斥體選用的3項原則。
1) 當鎖不能被擷取到時, 使用互斥體的開銷是程序上下文切換時間, 使用自旋鎖的開銷是等待擷取自旋鎖(由臨界區執行時間決定) 。 若臨界區比較小, 宜使用自旋鎖, 若臨界區很大, 應使用互斥體。
2) 互斥體所保護的臨界區可包含可能引起阻塞(或睡眠)的代碼, 而自旋鎖則絕對要避免用來保護包含這樣代碼的臨界區。
3) 互斥體存在于程序上下文, 是以, 如果被保護的共享資源需要在中斷或軟中斷情況下使用, 則在互斥體和自旋鎖之間隻能選擇自旋鎖。 當然, 如果一定要使用互斥體, 則隻能通過mutex_trylock() 方式進行, 不能擷取就立即傳回以避免阻塞。
Linux核心中解決并發控制的最常用方法是自旋鎖與信号量(絕大多數時候作為互斥體使用)。
1)自旋鎖與信号量"類似而不類",類似說的是它們功能上的相似性,"不類"指代它們在本質和實作機理上完全不一樣,不屬于一類。
2)自旋鎖不會引起調用者睡眠,如果自旋鎖已經被别的執行單元保持,調用者就一直循環檢視是否該自旋鎖的保持者已經釋放了鎖,"自旋"就是"在原地打轉"。而信号量則引起調用者睡眠,它把程序從運作隊列上拖出去,除非獲得鎖。這就是它們的"不類"。
3)但是,無論是信号量,還是自旋鎖,在任何時刻,最多隻能有一個保持者,即在任何時刻最多隻能有一個執行單元獲得鎖。這就是它們的"類似"。
4)鑒于自旋鎖與信号量的上述特點,一般而言:
- 自旋鎖适合于保持時間非常短的情況,它可以在任何上下文使用;信号量适合于保持時間較長的情況,且隻能在程序上下文使用。
- 如果被保護的共享資源隻在程序上下文通路,則可以以信号量來保護該共享資源,如果對共享資源的通路時間非常短,自旋鎖也是好的選擇。
- 但是,如果被保護的共享資源需要在中斷上下文通路(包括底半部、軟中斷),就必須使用自旋鎖。