Linux 同步方法剖析

來源:互聯網
上載者:User

本文討論了 Linux 核心中可用的大量同步或鎖定機制。這些機製為 2.6.23 版核心的許多可用方法提供了應用程式介面(API)。但是在深入學習 API 之前,首先需要明白將要解決的問題。

並發和鎖定

當存在並發特性時,必須使用同步方法。當在同一時間段出現兩個或更多進程並且這些進程彼此互動(例如,共用相同的資源)時,就存在並發 現象。

在單一處理器(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_savelocal_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_returnatomic_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 );

原子 API 原型
原子操作依賴於架構,可以在 ./linux/include/asm-/atomic.h 中找到。

自旋鎖

自旋鎖是使用忙等待鎖來確保互斥鎖的一種特殊方法。如果鎖可用,則擷取鎖,執行互斥鎖動作,然後釋放鎖。如果鎖不可用,線程將忙等待該鎖,直到其可用為止。忙等待看起來效率低下,但它實際上比將線程休眠然後當鎖可用時將其喚醒要快得多。

自旋鎖只在 SMP 系統中才有用,但是因為您的代碼最終將會在 SMP 系統上運行,將它們添加到 UP 系統是個明智的做法。

自旋鎖有兩種可用的形式:完全鎖(full lock)和讀寫鎖。 首先看一下完全鎖。

首先通過一個簡單的聲明建立一個新的自旋鎖。這可以通過調用 spin_lock_init 進行初始化。清單 5 中顯示的每個變數都會實現相同的結果。

清單 5. 建立和初始化自旋鎖

                spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;... or ...DEFINE_SPINLOCK( my_spinlock );... or ...spin_lock_init( &my_spinlock );

定義了自旋鎖之後,就可以使用大量的鎖定變數了。每個變數用於不同的上下文。

清單 6 中顯示了 spin_lockspin_unlock 變數。這是一個最簡單的變數,它不會執行中斷禁用,但是包含全部的記憶體壁壘(memory barrier)。這個變數假定中斷處理常式和該鎖之間沒有互動。

清單 6. 自旋鎖 lock 和 unlock 函數

spin_lock( &my_spinlock );

// critical section

spin_unlock( &my_spinlock );

接下來是 irqsaveirqrestore 對,如清單 7 所示。spin_lock_irqsave 函數需要自旋鎖,並且在本地處理器(在 SMP 情形中)上禁用中斷。spin_unlock_irqrestore 函數釋放自旋鎖,並且(通過 flags 參數)恢複中斷。

清單 7. 自旋鎖變數,其中禁用了本地 CPU 中斷

                spin_lock_irqsave( &my_spinlock, flags );// critical sectionspin_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 所示。

清單 8. 自旋鎖函數實現 bottom-half 互動

                spin_lock_bh( &my_spinlock );// critical sectionspin_unlock_bh( &my_spinlock );

讀/寫鎖

在許多情形下,對資料的訪問是由大量的讀和少量的寫操作來完成的(讀取資料比寫入資料更常見)。讀/寫鎖的建立就是為了支援這種模型。這個模型有趣的地方在於允許多個線程同時訪問相同資料,但同一時刻只允許一個線程寫入資料。如果執行寫操作的線程持有此鎖,則臨界段不能由其他線程讀取。如果一個執行讀操作的線程持有此鎖,那麼多個讀線程都可以進入臨界段。清單 9 示範了這個模型。

清單 9. 讀/寫自旋鎖函數

                rwlock_t my_rwlock;rwlock_init( &my_rwlock );write_lock( &my_rwlock );// critical section -- can read and writewrite_unlock( &my_rwlock );read_lock( &my_rwlock );// critical section -- can read onlyread_unlock( &my_rwlock );

根據對鎖的需求,還針對 bottom half 和插斷要求(IRQ)對讀/寫自旋鎖進行了修改。顯然,如果您使用的是原版的讀/寫鎖,那麼按照標準自旋鎖的用法使用這個自旋鎖,而不區分讀線程和寫線程。

核心互斥鎖

在核心中可以使用互斥鎖來實現訊號量行為。核心互斥鎖是在原子 API 之上實現的,但這對於核心使用者是不可見的。互斥鎖很簡單,但是有一些規則必須牢記。同一時間只能有一個任務持有互斥鎖,而且只有這個任務可以對互斥鎖進行解鎖。互斥鎖不能進行遞迴鎖定或解鎖,並且互斥鎖可能不能用於互動上下文。但是互斥鎖比當前的核心訊號量選項更快,並且更加緊湊,因此如果它們滿足您的需求,那麼它們將是您明智的選擇。

可以通過 DEFINE_MUTEX 宏使用一個操作建立和初始化互斥鎖。這將建立一個新的互斥鎖並初始化其結構。可以在 ./linux/include/linux/mutex.h 中查看該實現。

                DEFINE_MUTEX( my_mutex );

互斥鎖 API 提供了 5 個函數:其中 3 個用於鎖定,一個用於解鎖,另一個用於測試互斥鎖。首先看一下鎖定函數。在需要立即鎖定以及希望在互斥鎖不可用時掌握控制的情形下,可以使用第一個函數 mutex_trylock。該函數如清單 10 所示。

清單 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 中顯示了這兩種調用。

清單 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 示範了這些函數。

清單 12. 用 mutex_is_locked 測試互斥鎖鎖

                mutex_unlock( &my_mutex );if (mutex_is_locked( &my_mutex ) == 0) {  // Mutex is unlocked}

互斥鎖 API 存在著自身的局限性,因為它是基於原子 API 的。但是其效率比較高,如果能滿足你的需要,還是可以使用的。

大核心鎖(Big kernel lock)

最後看一下大核心鎖(BLK)。它在核心中的用途越來越小,但是仍然有一些保留下來的用法。BKL 使多處理器 Linux 成為可能,但是細粒度(finer-grained)鎖正在慢慢取代 BKL。BKL 通過 lock_kernelunlock_kernel 函數提供。要獲得更多資訊,請查看 ./linux/lib/kernel_lock.c。

結束語

Linux 效能非凡,其鎖定方法也一樣。原子鎖不僅提供了一種鎖定機制,同時也提供了算術或 bitwise 操作。自旋鎖提供了一種鎖定機制(主要應用於 SMP),而且讀/寫自旋鎖允許多個讀線程且僅有一個寫線程獲得給定的鎖。最後,互斥鎖是一種新的鎖定機制,提供了一種構建在原子之上的簡單 API。不管你需要什麼,Linux 都會提供一種鎖定方案保護您的資料。

原文出處(點擊此處)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.