在學習 Linux® 的過程當中,您或許接觸過併發(concurrency)、臨界段(critical section)和鎖定,但是怎樣在內核中使用這些概念呢?本文討論了 2.6 版內核中可用的鎖定機制,包含原子運算符(atomic operator)、自旋鎖(spinlock)、讀/寫鎖(reader/writer lock)和內核信號量(kernel semaphore)。本文還探討了每種機制最適合應用到哪些地方。以構建安全高效的內核代碼。html
本文討論了 Linux 內核中可用的大量同步或鎖定機制。這些機制爲 2.6.23 版內核的不少可用方法提供了應用程序接口(API)。但是在深刻學習 API 以前。首先需要明確將要解決的問題。java
![]() |
|
併發和鎖定linux
當存在併發特性時,必須使用同步方法。android
當在同一時間段出現兩個或不少其它進程並且這些進程彼此交互(好比。共享一樣的資源)時,就存在併發 現象。shell
在單處理器(uniprocessor。UP)主機上可能發生併發。在這樣的主機中多個線程共享同一個 CPU 並且搶佔(preemption)建立競態條件。搶佔 經過暫時中斷一個線程以運行還有一個線程的方式來實現 CPU 共享。api
競態條件 發生在兩個或不少其它線程操縱一個共享數據項時,其結果取決於運行的時間。在多處理器(MP)計算機中也存在併發,當中每個處理器中共享一樣數據的線程同一時候運行。安全
注意在 MP 狀況下存在真正的並行(parallelism)。因爲線程是同一時候運行的。微信
而在 UP 情形中。並行是經過搶佔建立的。兩種模式中實現併發都較爲困難。網絡
Linux 內核在兩種模式中都支持併發。內核自己是動態的,而且有不少建立競態條件的方法。Linux 內核也支持多處理(multiprocessing),稱爲對稱多處理(SMP)。架構
可以在本文後面的 參考資料 部分學到不少其它關於 SMP 的知識。
臨界段概念是爲解決競態條件問題而產生的。
一個臨界段 是一段不一樣意多路訪問的受保護的代碼。
這段代碼可以操縱共享數據或共享服務(好比硬件外圍設備)。
臨界段操做時堅持相互排斥鎖(mutual exclusion)原則(當一個線程處於臨界段中時,其它所有線程都不能進入臨界段)。
臨界段中需要解決的一個問題是死鎖條件。考慮兩個獨立的臨界段,各自保護不一樣的資源。每個資源擁有一個鎖,在本例中稱爲 A 和 B。若是有兩個線程需要訪問這些資源,線程 X 獲取了鎖 A,線程 Y 獲取了鎖 B。
當這些鎖都被持有時。每個線程都試圖佔有其它線程當前持有的鎖(線程 X 想要鎖 B。線程 Y 想要鎖 A)。
這時候線程就被死鎖了。因爲它們都持有一個鎖而且還想要其它鎖。一個簡單的解決方式就是老是按一樣次序獲取鎖。從而使當中一個線程得以完畢。還需要其它解決方式檢測這樣的情形。表 1 定義了此處用到的一些重要的併發術語。
術語 | 定義 |
---|---|
競態條件 | 兩個或不少其它線程同一時候操做資源時將會致使不一致的結果。 |
臨界段 | 用於協調對共享資源的訪問的代碼段。 |
相互排斥鎖 | 確保對共享資源進行排他訪問的軟件特性。 |
死鎖 | 由兩個或不少其它進程和資源鎖致使的一種特殊情形。將會減小進程的工做效率。 |
![]() ![]() |
![]()
|
假設您瞭解了一些基本理論而且明確了需要解決的問題。接下來將學習 Linux 支持併發和相互排斥鎖的各類方法。在曾經,相互排斥鎖是經過禁用中斷來提供的,但是這樣的形式的鎖定效率比較低(現在在內核中仍然存在這樣的使用方法)。
這樣的方法也不能進行擴展,而且不能保證其它處理器上的相互排斥鎖。
在下面關於鎖定機制的討論中,咱們首先看一下原子運算符,它可以保護簡單變量(計數器和位掩碼(bitmask))。
而後介紹簡單的自旋鎖和讀/寫鎖,它們構成了一個 SMP 架構的忙等待鎖(busy-wait lock)覆蓋。最後。咱們討論構建在原子 API 上的內核相互排斥鎖。
![]() ![]() |
![]()
|
Linux 中最簡單的同步方法就是原子操做。原子 意味着臨界段被包括在 API 函數中。不需要額外的鎖定,因爲 API 函數已經包括了鎖定。因爲 C 不能實現原子操做,所以 Linux 依靠底層架構來提供這項功能。各類底層架構存在很是大差別,所以原子函數的實現方法也各不一樣樣。一些方法全然經過彙編語言來實現,而還有一些方法依靠 c 語言並且使用 local_irq_save
和 local_irq_restore
禁用中斷。
![]() |
|
當需要保護的數據很easy時。好比一個計數器,原子運算符是種理想的方法。雖然原理簡單,原子 API 提供了不少針對不一樣情形的運算符。如下是一個使用此 API 的演示樣例。
要聲明一個原子變量(atomic variable),首先聲明一個 atomic_t
類型的變量。
這個結構包括了單個 int
元素。接下來,需確保您的原子變量使用 ATOMIC_INIT
符號常量進行了初始化。 在清單 1 的情形中,原子計數器被設置爲 0。也可以使用 atomic_set function
在執行時對原子變量進行初始化。
atomic_t my_counter ATOMIC_INIT(0);
... or ...
atomic_set( &my_counter, 0 );
|
原子 API 支持一個涵蓋不少用例的富函數集。可以使用 atomic_read
讀取原子變量中的內容,也可以使用 atomic_add
爲一個變量加入指定值。最常用的操做是使用 atomic_inc
使變量遞增。
也可用減號運算符,它的做用與相加和遞增操做相反。
清單 2. 演示了這些函數。
val = atomic_read( &my_counter ); atomic_add( 1, &my_counter ); atomic_inc( &my_counter ); atomic_sub( 1, &my_counter ); atomic_dec( &my_counter ); |
該 API 也支持更多常用用例,包含 operate-and-test 例程。這些例程贊成對原子變量進行操縱和測試(做爲一個原子操做來運行)。一個叫作 atomic_add_negative
的特殊函數被加入到原子變量中。而後當結果值爲負數時返回真(true)。這被內核中一些依賴於架構的信號量函數使用。
不少函數都不返回變量的值。但兩個函數除外。
它們會返回結果值( atomic_add_return
和 atomic_sub_return
),如清單 3所看到的。
if (atomic_sub_and_test( 1, &my_counter )) { // my_counter is zero } if (atomic_dec_and_test( &my_counter )) { // my_counter is zero } if (atomic_inc_and_test( &my_counter )) { // my_counter is zero } if (atomic_add_negative( 1, &my_counter )) { // my_counter is less than zero } val = atomic_add_return( 1, &my_counter )); val = atomic_sub_return( 1, &my_counter )); |
假設您的架構支持 64 位長類型(BITS_PER_LONG
是 64 的),那麼可以使用 long_t atomic
操做。可以在 linux/include/asm-generic/atomic.h 中查看可用的長操做(long operation)。
原子 API 還支持位掩碼(bitmask)操做。跟前面提到的算術操做不同,它僅僅包括設置和清除操做。不少驅動程序使用這些原子操做,特別是 SCSI。位掩碼原子操做的使用跟算術操做存在細微的區別。因爲當中僅僅有兩個可用的操做(設置掩碼和清除掩碼)。使用這些操做前,需要提供一個值和將要進行操做的位掩碼,如清單 4 所看到的。
unsigned long my_bitmask; atomic_clear_mask( 0, &my_bitmask ); atomic_set_mask( (1<<24), &my_bitmask ); |
![]() |
|
自旋鎖是使用忙等待鎖來確保相互排斥鎖的一種特殊方法。假設鎖可用。則獲取鎖,運行相互排斥鎖動做,而後釋放鎖。
假設鎖不可用,線程將忙等待該鎖。直到其可用爲止。忙等待看起來效率低下,但它實際上比將線程休眠而後當鎖可用時將其喚醒要快得多。
自旋鎖僅僅在 SMP 系統中才實用。但是因爲您的代碼終於將會在 SMP 系統上執行。將它們加入到 UP 系統是個明智的作法。
自旋鎖有兩種可用的形式:全然鎖(full lock)和讀寫鎖。 首先看一下全然鎖。
首先經過一個簡單的聲明建立一個新的自旋鎖。這可以經過調用 spin_lock_init
進行初始化。清單 5 中顯示的每個變量都會實現一樣的結果。
spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED; ... or ... DEFINE_SPINLOCK( my_spinlock ); ... or ... spin_lock_init( &my_spinlock ); |
定義了自旋鎖以後,就可以使用大量的鎖定變量了。
每個變量用於不一樣的上下文。
清單 6 中顯示了 spin_lock
和 spin_unlock
變量。這是一個最簡單的變量,它不會運行中斷禁用,但是包括全部的內存壁壘(memory barrier)。
這個變量假定中斷處理程序和該鎖之間沒有交互。
spin_lock( &my_spinlock ); // critical section spin_unlock( &my_spinlock ); |
接下來是 irqsave
和 irqrestore
對,如清單 7 所看到的。
spin_lock_irqsave
函數需要自旋鎖,並且在本地處理器(在 SMP 情形中)上禁用中斷。spin_unlock_irqrestore
函數釋放自旋鎖,並且(經過 flags
參數)恢復中斷。
spin_lock_irqsave( &my_spinlock, flags ); // critical section spin_unlock_irqrestore( &my_spinlock, flags ); |
spin_lock_irqsave
/spin_unlock_irqrestore
的一個不太安全的變體是 spin_lock_irq
/spin_unlock_irq
。 我建議不要使用此變體。因爲它會若是中斷狀態。
最後,假設內核線程經過 bottom half 方式共享數據,那麼可以使用自旋鎖的還有一個變體。
bottom half 方法可以將設備驅動程序中的工做延遲到中斷處理後執行。這樣的自旋鎖禁用了本地 CPU 上的軟中斷。這可以阻止 softirq、tasklet 和 bottom half 在本地 CPU 上執行。這個變體如清單 8 所看到的。
spin_lock_bh( &my_spinlock ); // critical section spin_unlock_bh( &my_spinlock ); |
![]() ![]() |
![]()
|
在不少情形下,對數據的訪問是由大量的讀和少許的寫操做來完畢的(讀取數據比寫入數據更常見)。讀/寫鎖的建立就是爲了支持這樣的模型。這個模型有趣的地方在於贊成多個線程同一時候訪問一樣數據。但同一時刻僅僅贊成一個線程寫入數據。
假設運行寫操做的線程持有此鎖,則臨界段不能由其它線程讀取。假設一個運行讀操做的線程持有此鎖,那麼多個讀線程都可以進入臨界段。
清單 9 演示了這個模型。
rwlock_t my_rwlock; rwlock_init( &my_rwlock ); write_lock( &my_rwlock ); // critical section -- can read and write write_unlock( &my_rwlock ); read_lock( &my_rwlock ); // critical section -- can read only read_unlock( &my_rwlock ); |
依據對鎖的需求,還針對 bottom half 和中斷請求(IRQ)對讀/寫自旋鎖進行了改動。顯然,假設您使用的是原版的讀/寫鎖,那麼依照標準自旋鎖的使用方法使用這個自旋鎖。而不區分讀線程和寫線程。
![]() ![]() |
![]()
|
在內核中可以使用相互排斥鎖來實現信號量行爲。內核相互排斥鎖是在原子 API 之上實現的。但這對於內核用戶是不可見的。相互排斥鎖很是easy。但是有一些規則必須牢記。同一時間僅僅能有一個任務持有相互排斥鎖。並且僅僅有這個任務可以對相互排斥鎖進行解鎖。相互排斥鎖不能進行遞歸鎖定或解鎖。並且相互排斥鎖可能不能用於交互上下文。但是相互排斥鎖比當前的內核信號量選項更快,並且更加緊湊,所以假設它們知足您的需求。那麼它們將是您明智的選擇。
可以經過 DEFINE_MUTEX
宏使用一個操做建立和初始化相互排斥鎖。這將建立一個新的相互排斥鎖並初始化其結構。可以在 ./linux/include/linux/mutex.h 中查看該實現。
DEFINE_MUTEX( my_mutex );
|
相互排斥鎖 API 提供了 5 個函數:當中 3 個用於鎖定。一個用於解鎖。還有一個用於測試相互排斥鎖。首先看一下鎖定函數。
在需要立刻鎖定以及但願在相互排斥鎖不可用時掌握控制的情形下。可以使用第一個函數 mutex_trylock
。該函數如清單 10 所看到的。
mutex_trylock
得到相互排斥鎖
ret = mutex_trylock( &my_mutex );
if (ret != 0) {
// Got the lock!
} else {
// Did not get the lock
}
|
假設想等待這個鎖。可以調用 mutex_lock
。這個調用在相互排斥鎖可用時返回,不然,在相互排斥鎖鎖可用以前它將休眠。
無論在哪一種情形中。當控制被返回時,調用者將持有相互排斥鎖。最後,當調用者休眠時使用 mutex_lock_interruptible
。
在這樣的狀況下,該函數可能返回 -EINTR
。
清單 11 中顯示了這兩種調用。
mutex_lock( &my_mutex ); // Lock is now held by the caller. if (mutex_lock_interruptible( &my_mutex ) != 0) { // Interrupted by a signal, no mutex held } |
當一個相互排斥鎖被鎖定後,它必須被解鎖。這是由 mutex_unlock
函數來完畢的。這個函數不能從中斷上下文調用。
最後。可以經過調用 mutex_is_locked
檢查相互排斥鎖的狀態。
這個調用實際上編譯成一個內聯函數。假設相互排斥鎖被持有(鎖定),那麼就會返回 1;不然。返回 0。清單 12 演示了這些函數。
mutex_is_locked
測試相互排斥鎖鎖
mutex_unlock( &my_mutex ); if (mutex_is_locked( &my_mutex ) == 0) { // Mutex is unlocked } |
相互排斥鎖 API 存在着自身的侷限性,因爲它是基於原子 API 的。
但是其效率比較高,假設能知足你的需要,仍是可以使用的。
![]() ![]() |
![]()
|
最後看一下大內核鎖(BLK)。它在內核中的用途愈來愈小,但是仍然有一些保留下來的使用方法。BKL 使多處理器 Linux 成爲可能。但是細粒度(finer-grained)鎖正在慢慢代替 BKL。BKL 經過 lock_kernel
和 unlock_kernel
函數提供。要得到不少其它信息。請查看 ./linux/lib/kernel_lock.c。
![]() ![]() |
![]()
|
![]() |
|
Linux 性能非凡,其鎖定方法也同樣。
原子鎖不只提供了一種鎖定機制。同一時候也提供了算術或 bitwise 操做。
自旋鎖提供了一種鎖定機制(主要應用於 SMP),而且讀/寫自旋鎖贊成多個讀線程且僅有一個寫線程得到給定的鎖。
最後。相互排斥鎖是一種新的鎖定機制,提供了一種構建在原子之上的簡單 API。不管你需要什麼,Linux 都會提供一種鎖定方案保護您的數據。
![]() |
||
|
![]() |
M. Tim Jones 是一名嵌入式軟件project師,他是 GNU/Linux Application Programming、AI Application Programming 以及 BSD Sockets Programming from a Multilanguage Perspective 等書的做者。 他的project背景很普遍。從同步宇宙飛船的內核開發到嵌入式架構設計,再到網絡協議的開發。Tim 是位於科羅拉多州 Longmont 的 Emulex Corp. 的一名顧問project師。 |