本文是從IBM Developerworks上轉載的,的確是很不錯的技術網站,有很多關於linux的文章。這篇討論了 Linux 核心中可用的大量同步或鎖定機制,這些機製為2.6.23版核心的許多可用方法提供了應用程式介面(API)。之前用的類似semTake/Give等函數都是基於這些機制的,當然,在有資料的情況下當然要深入學習一下。
並發和鎖定
當存在並發特性時,必須使用同步方法。當在同一時間段出現兩個或更多進程並且這些進程彼此互動(例如,共用相同的資源)時,就存在並發現象。
在單一處理器(uniprocessor,UP)主機上可能發生並發,在這種主機中多個線程共用同一個 CPU 並且搶佔(preemption)建立競態條件。搶佔 通過臨時中斷一個線程以執行另一個線程的方式來實現 CPU 共用。競態條件 發生在兩個或更多線程操縱一個共用資料項目時,其結果取決於執行的時間。在多處理器(MP)電腦中也存在並發,其中每個處理器中共用相同資料的線程同時執行。注意在 MP 情況下存在真正的並行(parallelism),因為線程是同時執行的。而在 UP 情形中,並行是通過搶佔建立的。兩種模式中實現並發都較為困難。
Linux 核心在兩種模式中都支援並發。核心本身是動態,而且有許多建立競態條件的方法。Linux 核心也支援多處理(multiprocessing),稱為對稱式多處理(SMP)。但是這個對我有點複雜了,考慮以後再找資料寫一下。
臨界段概念是為解決競態條件問題而產生的,一個臨界段是一段不允許多路訪問的受保護的代碼。這段代碼可以操縱共用資料或共用服務(例如硬體外圍裝置)。臨界段操作時堅持互斥鎖(mutual exclusion)原則(當一個線程處於臨界段中時,其他所有線程都不能進入臨界段)。
臨界段中需要解決的一個問題是死結條件。考慮兩個獨立的臨界段,各自保護不同的資源。每個資源擁有一個鎖,在本例中稱為 A 和 B。假設有兩個線程需要訪問這些資源,線程 X 擷取了鎖 A,線程 Y 擷取了鎖 B。當這些鎖都被持有時,每個線程都試圖佔有其他線程當前持有的鎖(線程 X 想要鎖 B,線程 Y 想要鎖 A)。這時候線程就被死結了,因為它們都持有一個鎖而且還想要其他鎖。一個簡單的解決方案就是總是按相同次序擷取鎖,從而使其中一個線程得以完成。還需要其他解決方案檢測這種情形。表 1 定義了此處用到的一些重要的並發術語。
表1、並發中的重要定義
Linux 同步方法
接下來就是關於 Linux 支援並發和互斥鎖的各種方法。在以前,互斥鎖是通過禁用中斷來提供的,但是這種形式的鎖定效率比較低(現在在核心中仍然存在這種用法)。這種方法也不能進行擴充,而且不能保證其他處理器上的互斥鎖。
在以下關於鎖定機制的討論中,我們首先看一下原子運算子,它可以保護簡單變數(計數器和位元遮罩(bitmask))。然後介紹簡單的自旋鎖和讀/寫鎖,它們構成了一個 SMP 架構的忙等待鎖(busy-wait lock)覆蓋。最後,我們討論構建在原子 API 上的核心互斥鎖。
原子操作
Linux 中最簡單的同步方法就是原子操作。原子 意味著臨界段被包含在 API 函數中。不需要額外的鎖定,因為 API 函數已經包含了鎖定。由於 C 不能實現原子操作,因此 Linux 依靠底層架構來提供這項功能。各種底層架構存在很大差異,因此原子函數的實現方法也各不相同。一些方法完全通過組合語言來實現,而另一些方法依靠 c 語言並且使用 local_irq_save 和 local_irq_restore 禁用中斷。這裡還需要提一下舊的鎖定方式,在核心中實現鎖定的一種不太好的方法是通過禁用本地 CPU 的硬中斷。這些函數均可用並且仍得到使用(有時用於原子運算子),但我們並不推薦使用。local_irq_save 常式禁用中斷,而 local_irq_restore 恢複以前啟用過的中斷。這些常式都是可重新進入的(reentrant),也就是說它們可以在其他常式上下文中被調用。
當需要保護的資料非常簡單時,例如一個計數器,原子運算子是種理想的方法。儘管原理簡單,原子 API 提供了許多針對不同情形的運算子。下面是一個使用此 API 的樣本。
要聲明一個原子變數(atomic variable),首先聲明一個 atomic_t 類型的變數。這個結構包含了單個 int 元素。接下來,需確保您的原子變數使用 ATOMIC_INIT 符號常量進行了初始化。在清單 1 的情形中,原子計數器被設定為 0。也可以使用 atomic_set function 在運行時對原子變數進行初始化。
清單 1. 建立和初始化原子變數 atomic_t my_counter ATOMIC_INIT(0);... or ...atomic_set( &my_counter, 0 );
原子 API 支援一個涵蓋許多用例的富函數集。可以使用 atomic_read 讀取原子變數中的內容,也可以使用 atomic_add 為一個變數添加指定值。最常用的操作是使用 atomic_inc 使變數遞增。也可用減號運算子,它的作用與相加和遞增操作相反。清單 2. 示範了這些函數。
清單 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所示。
清單 3. Operate-and-test 原子函數 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 所示。
清單 4. 位元遮罩原子函數 unsigned long my_bitmask;atomic_clear_mask( 0, &my_bitmask );atomic_set_mask( (1<<24), &my_bitmask );
自旋鎖
自旋鎖是使用忙等待鎖來確保互斥鎖的一種特殊方法。如果鎖可用,則擷取鎖,執行互斥鎖動作,然後釋放鎖。如果鎖不可用,線程將忙等待該鎖,直到其可用為止。忙等待看起來效率低下,但它實際上比將線程休眠然後當鎖可用時將其喚醒要快得多。
自旋鎖只在 SMP 系統中才有用,但是因為您的代碼最終將會在 SMP 系統上運行,將它們添加到 UP 系統是個明智的做法。