天天看點

linux自旋鎖和互斥體

2.5 核心中的并發

随着多核筆記本電腦時代的到來,對稱多處理器(SMP)的使用不再被限于高科技使用者。SMP和核心搶占是多線程執行的兩種場景。多個線程能夠同時操作共享的核心資料結構,是以,對這些資料結構的通路必須被串行化。

接下來,我們會讨論并發通路情況下保護共享核心資源的基本概念。我們以一個簡單的例子開始,并逐漸引入中斷、核心搶占和SMP等複雜概念。

2.5.1 自旋鎖和互斥體

通路共享資源的代碼區域稱作臨界區。自旋鎖(spinlock)和互斥體(mutex,mutual exclusion的縮寫)是保護核心臨界區的兩種基本機制。我們逐個分析。

自旋鎖可以確定在同時隻有一個線程進入臨界區。其他想進入臨界區的線程必須不停地原地打轉,直到第1個線程釋放自旋鎖。注意:這裡所說的線程不是核心線程,而是執行的線程。

下面的例子示範了自旋鎖的基本用法:

  1. #include <linux/spinlock.h> 
  2. spinlock_t mylock = SPIN_LOCK_UNLOCKED; /* Initialize */  
  3. /* Acquire the spinlock. This is inexpensive if there  
  4.  * is no one inside the critical section. In the face of  
  5.  * contention, spinlock() has to busy-wait.  
  6.  */  
  7. spin_lock(&mylock);  
  8. /* ... Critical Section code ... */  
  9. spin_unlock(&mylock); /* Release the lock */ 

與自旋鎖不同的是,互斥體在進入一個被占用的臨界區之前不會原地打轉,而是使目前線程進入睡眠狀态。如果要等待的時間較長,互斥體比自旋鎖更合适,因為自旋鎖會消耗CPU資源。在使用互斥體的場合,多于2次程序切換時間都可被認為是長時間,是以一個互斥體會引起本線程睡眠,而當其被喚醒時,它需要被切換回來。

是以,在很多情況下,決定使用自旋鎖還是互斥體相對來說很容易:

(1) 如果臨界區需要睡眠,隻能使用互斥體,因為在獲得自旋鎖後進行排程、搶占以及在等待隊列上睡眠都是非法的;

(2) 由于互斥體會在面臨競争的情況下将目前線程置于睡眠狀态,是以,在中斷處理函數中,隻能使用自旋鎖。(第4章将介紹更多的關于中斷上下文的限制。)

下面的例子示範了互斥體使用的基本方法:

  1. #include <linux/mutex.h> 
  2. /* Statically declare a mutex. To dynamically  
  3.    create a mutex, use mutex_init() */  
  4. static DEFINE_MUTEX(mymutex);  
  5. /* Acquire the mutex. This is inexpensive if there  
  6.  * is no one inside the critical section. In the face of  
  7.  * contention, mutex_lock() puts the calling thread to sleep.  
  8.  */  
  9. mutex_lock(&mymutex);  
  10. /* ... Critical Section code ... */  
  11. mutex_unlock(&mymutex);      /* Release the mutex */ 

為了論證并發保護的用法,我們首先從一個僅存在于程序上下文的臨界區開始,并以下面的順序逐漸增加複雜性:

(1) 非搶占核心,單CPU情況下存在于程序上下文的臨界區;

(2) 非搶占核心,單CPU情況下存在于程序和中斷上下文的臨界區;

(3) 可搶占核心,單CPU情況下存在于程序和中斷上下文的臨界區;

(4) 可搶占核心,SMP情況下存在于程序和中斷上下文的臨界區。

舊的信号量接口

互斥體接口代替了舊的信号量接口(semaphore)。互斥體接口是從-rt樹演化而來的,在2.6.16核心中被融入主線核心。

盡管如此,但是舊的信号量仍然在核心和驅動程式中廣泛使用。信号量接口的基本用法如下:

  1. #include <asm/semaphore.h>  /* Architecture dependent header */  
  2. /* Statically declare a semaphore. To dynamically  
  3.    create a semaphore, use init_MUTEX() */  
  4. static DECLARE_MUTEX(mysem);  
  5. down(&mysem);    /* Acquire the semaphore */  
  6. /* ... Critical Section code ... */  
  7. up(&mysem);      /* Release the semaphore */ 

信号量可以被配置為允許多個預定數量的線程同時進入臨界區,但是,這種用法非常罕見。

1. 案例1:程序上下文,單CPU,非搶占核心

這種情況最為簡單,不需要加鎖,是以不再贅述。

2. 案例2:程序和中斷上下文,單CPU,非搶占核心

在這種情況下,為了保護臨界區,僅僅需要禁止中斷。如圖2-4所示,假定程序上下文的執行單元A、B以及中斷上下文的執行單元C都企圖進入相同的臨界區。

linux自旋鎖和互斥體
(點選檢視大圖)圖2-4 程序和中斷上下文進入臨界區

由于執行單元C總是在中斷上下文執行,它會優先于執行單元A和B,是以,它不用擔心保護的問題。執行單元A和B也不必關心彼此會被互相打斷,因為核心是非搶占的。是以,執行單元A和B僅僅需要擔心C會在它們進入臨界區的時候強行進入。為了實作此目的,它們會在進入臨界區之前禁止中斷:

  1. Point A:      
  2.   local_irq_disable();  /* Disable Interrupts in local CPU */  
  3.   /* ... Critical Section ...  */  
  4.   local_irq_enable();   /* Enable Interrupts in local CPU */ 

但是,如果當執行到Point A的時候已經被禁止,local_irq_enable()将産生副作用,它會重新使能中斷,而不是恢複之前的中斷狀态。可以這樣修複它:

  1. unsigned long flags;  
  2. Point A:  
  3.   local_irq_save(flags);     /* Disable Interrupts */  
  4.   /* ... Critical Section ... */  
  5.   local_irq_restore(flags);  /* Restore state to what it was at Point A */ 

不論Point A的中斷處于什麼狀态,上述代碼都将正确執行。

3. 案例3:程序和中斷上下文,單CPU,搶占核心

如果核心使能了搶占,僅僅禁止中斷将無法確定對臨界區的保護,因為另一個處于程序上下文的執行單元可能會進入臨界區。重新回到圖2-4,現在,除了C以外,執行單元A和B必須提防彼此。顯而易見,解決該問題的方法是在進入臨界區之前禁止核心搶占、中斷,并在退出臨界區的時候恢複核心搶占和中斷。是以,執行單元A和B使用了自旋鎖API的irq變體:

  1. unsigned long flags;  
  2. Point A:  
  3.   /* Save interrupt state.  
  4.    * Disable interrupts - this implicitly disables preemption */  
  5.   spin_lock_irqsave(&mylock, flags);  
  6.   /* ... Critical Section ... */  
  7.   /* Restore interrupt state to what it was at Point A */  
  8.   spin_unlock_irqrestore(&mylock, flags); 

我們不需要在最後顯示地恢複Point A的搶占狀态,因為核心自身會通過一個名叫搶占計數器的變量維護它。在搶占被禁止時(通過調用preempt_disable()),計數器值會增加;在搶占被使能時(通過調用preempt_enable()),計數器值會減少。隻有在計數器值為0的時候,搶占才發揮作用。

4. 案例4:程序和中斷上下文,SMP機器,搶占核心

現在假設臨界區執行于SMP機器上,而且你的核心配置了CONFIG_SMP和CONFIG_PREEMPT。

到目前為止讨論的場景中,自旋鎖原語發揮的作用僅限于使能和禁止搶占和中斷,時間的鎖功能并未被完全編譯進來。在SMP機器内,鎖邏輯被編譯進來,而且自旋鎖原語確定了SMP安全性。SMP使能的含義如下:

  1. unsigned long flags;  
  2. Point A:  
  3.   /*  
  4.     - Save interrupt state on the local CPU  
  5.     - Disable interrupts on the local CPU. This implicitly disables preemption.  
  6.     - Lock the section to regulate access by other CPUs  
  7.    */  
  8.   spin_lock_irqsave(&mylock, flags);  
  9.   /* ... Critical Section ... */  
  10.   /*  
  11.     - Restore interrupt state and preemption to what it  
  12.       was at Point A for the local CPU  
  13.     - Release the lock  
  14.    */  
  15.   spin_unlock_irqrestore(&mylock, flags); 

在SMP系統上,擷取自旋鎖時,僅僅本CPU上的中斷被禁止。是以,一個程序上下文的執行單元(圖2-4中的執行單元A)在一個CPU上運作的同時,一個中斷處理函數(圖2-4中的執行單元C)可能運作在另一個CPU上。非本CPU上的中斷處理函數必須自旋等待本CPU上的程序上下文代碼退出臨界區。中斷上下文需要調用spin_lock()/spin_unlock():

  1. spin_lock(&mylock);  
  2. /* ... Critical Section ... */  
  3. spin_unlock(&mylock); 

除了有irq變體以外,自旋鎖也有底半部(BH)變體。在鎖被擷取的時候,spin_lock_bh()會禁止底半部,而spin_unlock_bh()則會在鎖被釋放時重新使能底半部。我們将在第4章讨論底半部。

-rt樹

實時(-rt)樹,也被稱作CONFIG_PREEMPT_RT更新檔集,實作了核心中一些針對低延時的修改。該更新檔集可以從www.kernel.org/pub/linux/kernel/projects/rt下載下傳,它允許核心的大部分位置可被搶占,但是用互斥體代替了一些自旋鎖。它也合并了一些高精度的定時器。數個-rt功能已經被融入了主線核心。詳細的文檔見http://rt.wiki.kernel.org/。

為了提高性能,核心也定義了一些針對特定環境的特定的鎖原語。使能适用于代碼執行場景的互斥機制将使代碼更高效。下面來看一下這些特定的互斥機制。

繼續閱讀