kernel併發控制:自旋鎖、互斥體、中斷屏蔽

 

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)鑑於自旋鎖與信號量的上述特點,一般而言:

  • 自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用;信號量適合於保持時間較長的情況,且只能在進程上下文使用。

  • 如果被保護的共享資源只在進程上下文訪問,則可以以信號量來保護該共享資源,如果對共享資源的訪問時間非常短,自旋鎖也是好的選擇。

  • 但是,如果被保護的共享資源需要在中斷上下文訪問(包括底半部、軟中斷),就必須使用自旋鎖。

 

 

 

發佈了165 篇原創文章 · 獲贊 114 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章