Linux 设备驱动的并发控制
?Linux 設備驅動中必須要解決的一個問題是多個進程對共享的資源的并發訪問,并發的訪問會導致競態,即使是經驗豐富的驅動工程師也常常設計出包含并發問題bug 的驅動程序。
一、基礎概念
1、Linux 并發相關基礎概念
a -- 并發(concurrency):并發指的是多個執行單元同時、并發被執行,而并發的執行單元對共享資源(硬件資源和軟件上的全局變量、靜態變量等)的訪問則很容易導致競態(race condition);
b -- 競態(race condition)?:競態簡單的說就是兩個或兩個以上的進程同時訪問一個資源,同時引起資源的錯誤;
c -- 臨界區(Critical Section):每個進程中訪問臨界資源的那段代碼稱為臨界區;
d -- 臨界資源 :一次僅允許一個進程使用的資源稱為臨界資源;多道程序系統中存在許多進程,它們共享各種資源,然而有很多資源一次只能供一個進程使用;
? ? ? 在宏觀上并行或者真正意義上的并行(這里為什么是宏觀意義的并行呢?我們應該知道“時間片”這個概念,微觀上還是串行的,所以這里稱為宏觀上的并行),可能會導致競爭; 類似兩條十字交叉的道路上運行的車。當他們同一時刻要經過共同的資源(交叉點)的時候,如果沒有交通信號燈,就可能出現混亂。在linux 系統中也有可能存在這種情況:
2、并發產生的場合
a -- 對稱多處理器(SMP)的多個CPU
? ? ???SMP 是一種共享存儲的系統模型,它的特點是多個CPU使用共同的系統總線,因此可訪問共同的外設和儲存器,這里可以實現真正的并行;
b -- 單CPU內進程與搶占它的進程
? ? ? ?一個進程在內核執行的時候有可能被另一個高優先級進程打斷;
c -- 中斷和進程之間
? ? ? ?中斷可以打斷正在執行的進程,如果中斷處理函數程序訪問進程正在訪問的資源,則競態也會發生;
3、解決競態問題的途徑
? ? ? 解決競態問題的途徑最重要的是保證對共享資源的互斥訪問,所謂互斥訪問是指一個執行單元在訪問共享資源的時候,其他的執行單元被禁止訪問。
? ? ? Linux 設備中提供了可采用的互斥途徑來避免這種競爭。主要有原子操作,信號量,自旋鎖。
? ? ?那么這三種有什么相同的地方,有什么區別呢?適用什么不同的場合呢?會帶來什么邊際效應?要徹底弄清楚這些問題,要從其所處的環境來進行細化分類處理。是UP(單CPU)還是SMP(多CPU);是搶占式內核還是非搶占式內核;是在中斷上下文不是進程上下文。似交通信號燈一樣的措施來避免這種競爭。
? ? 先看一下三種并發機制的簡單概念:
?原子鎖:原子操作不可能被其他的任務給調開,一切(包括中斷),針對單個變量。
?自旋鎖:使用忙等待鎖來確保互斥鎖的一種特別方法,針對是臨界區。
?信號量:包括一個變量及對它進行的兩個原語操作,此變量就稱之為信號量,針對是臨界區。
二、并發處理途徑詳解
1、中斷屏蔽
? ? ? 在單CPU范圍內避免靜態的一種簡單而省事的方法是在進入臨界區之前屏蔽系統的中斷,這項功能可以保證正在執行的內核執行路徑不被中斷處理程序所搶占,防止某些競爭條件的發生。具體而言
a --?中斷屏蔽將使得中斷和進程之間的并發不再發生;
b --?由于Linux內核的進程調度等操作都依賴中斷來實現,內核搶占進程之間的并發也得以避免;
中斷屏蔽的使用方法:
[cpp]?view plaincopy
但是要注意:
a --?中斷對系統正常運行很重要,長時間屏蔽很危險,有可能造成數據丟失乃至系統崩潰,所以中斷屏蔽后應盡可能快的執行完畢。
b -- 宜與自旋鎖聯合使用。
? ? ? ?所以,不建議使用中斷屏蔽。
2、原子操作
? ? ??原子操作(分為原子整型操作和原子位操作)就是絕不會在執行完畢前被任何其他任務和時間打斷,不會執行一半,又去執行其他代碼。原子操作需要硬件的支持,因此是架構相關的,其API和原子類型的定義都在include/asm/atomic.h中,使用匯編語言實現。
在linux中,原子變量的定義如下:
typedef struct {volatile int counter;} atomic_t;
? ??關鍵字volatile用來暗示GCC不要對該類型做數據優化,所以對這個變量counte的訪問都是基于內存的,不要將其緩沖到寄存器中。存儲到寄存器中,可能導致內存中的數據已經改變,而寄存其中的數據沒有改變。 ?
原子整型操作:
1)定義atomic_t變量:?
#define ATOMIC_INIT(i) ( (atomic_t) { (i) } )
atomic_t v = ATOMIC_INIT(0); ? ?//定義原子變量v并初始化為0
2)設置原子變量的值:
#define atomic_set(v,i) ((v)->counter = (i)) void atomic_set(atomic_t *v, int i);//設置原子變量的值為i
3)獲取原子變量的值:
#define atomic_read(v) ((v)->counter + 0) atomic_read(atomic_t *v);//返回原子變量的值
4)原子變量加/減:
static __inline__ void atomic_add(int i, atomic_t * v); //原子變量增加i
static __inline__ void atomic_sub(int i, atomic_t * v); //原子變量減少i
5)原子變量自增/自減:
#define atomic_inc(v) atomic_add(1, v); //原子變量加1 #define atomic_dec(v) atomic_sub(1, v); //原子變量減1
6)操作并測試:
//這些操作對原子變量執行自增,自減,減操作后測試是否為0,是返回true,否則返回false
#define atomic_inc_and_test(v) (atomic_add_return(1, (v)) == 0) static inline int atomic_add_return(int i, atomic_t *v)
原子操作的優點編寫簡單;缺點是功能太簡單,只能做計數操作,保護的東西太少。下面看一個實例:
[cpp]?view plaincopy3、自旋鎖
自旋鎖是專為防止多處理器并發而引入的一種鎖,它應用于中斷處理等部分。對于單處理器來說,防止中斷處理中的并發可簡單采用關閉中斷的方式,不需要自旋鎖。
自旋鎖最多只能被一個內核任務持有,如果一個內核任務試圖請求一個已被爭用(已經被持有)的自旋鎖,那么這個任務就會一直進行忙循環——旋轉——等待鎖重新可用(忙等待,即當一個進程位于其臨界區內,任何試圖進入其臨界區的進程都必須在進入代碼連續循環)。要是鎖未被爭用,請求它的內核任務便能立刻得到它并且繼續進行。自旋鎖可以在任何時刻防止多于一個的內核任務同時進入臨界區,因此這種鎖可有效地避免多處理器上并發運行的內核任務競爭共享資源。
1)自旋鎖的使用:
spinlock_t spin; //定義自旋鎖
spin_lock_init(lock); //初始化自旋鎖
spin_lock(lock); //成功獲得自旋鎖立即返回,否則自旋在那里直到該自旋鎖的保持者釋放
spin_trylock(lock); //成功獲得自旋鎖立即返回真,否則返回假,而不是像上一個那樣"在原地打轉"
spin_unlock(lock);//釋放自旋鎖
下面是一個實例:
[cpp]?view plaincopy
? ? ? ?自旋鎖主要針對SMP或單CPU但內核可搶占的情況,對于單CPU和內核不支持的搶占的系統,自旋鎖退化為空操作(因為自旋鎖本身就需進行內核搶占)。在單CPU和內核可搶占的系統中,自旋鎖持有期間內核的搶占將被禁止。由于內核可搶占的單CPU系統的行為實際很類似于SMP系統,因此,在這樣的單CPU系統中使用自旋鎖仍十分重要。
? ? ? 盡管用了自旋鎖可以保證臨界區不受別的CPU和本CPU內的搶占進程打擾,但是得到鎖的代碼路徑在執行臨界區的時候,還可能受到中斷和底半部的影響。為了防止這種影響。為了防止影響,就需要用到自旋鎖的衍生。
2)注意事項:
a -- 自旋鎖是一種忙等待。它是一種適合短時間鎖定的輕量級的加鎖機制。
b -- 自旋鎖不能遞歸使用。自旋鎖被設計成在不同線程或者函數之間同步。這是因為,如果一個線程在已經持有自旋鎖時,其處于忙等待狀態,則已經沒有機會釋放自己持有的鎖了。如果這時再調用自身,則自旋鎖永遠沒有執行的機會了,即造成“死鎖”。
【自旋鎖導致死鎖的實例】
1)a進程擁有自旋鎖,在內核態阻塞的,內核調度進程b,b也要或得自旋鎖,b只能自旋,而此時搶占已經關閉了,a進程就不會調度到了,b進程永遠自旋。
2)進程a擁有自旋鎖,中斷來了,cpu執行中斷,中斷處理函數也要獲得鎖訪問共享資源,此時也獲得不到鎖,只能死鎖。
3)內核搶占
? ? ? 內核搶占是上面提到的一個概念,不管當前進程處于內核態還是用戶態,都會調度優先級高的進程運行,停止當前進程;當我們使用自旋鎖的時候,搶占是關閉的。
4)自旋鎖有幾個重要的特性:
a -- 被自旋鎖保護的臨界區代碼執行時不能進入休眠。
b -- 被自旋鎖保護的臨界區代碼執行時是不能被被其他中斷中斷。
c -- 被自旋鎖保護的臨界區代碼執行時,內核不能被搶占。
? ? ? ?從這幾個特性可以歸納出一個共性:被自旋鎖保護的臨界區代碼執行時,它不能因為任何原因放棄處理器。?
4、信號量
linux中,提供了兩種信號量:一種用于內核程序中,一種用于應用程序中。這里只講屬前者
信號量和自旋鎖的使用方法基本一樣。與自旋鎖相比,信號量只有當得到信號量的進程或者線程時才能夠進入臨界區,執行臨界代碼。信號量和自旋鎖的最大區別在于:當一個進程試圖去獲得一個已經鎖定的信號量時,進程不會像自旋鎖一樣在遠處忙等待。
信號量是一種睡眠鎖。如果有一個任務試圖獲得一個已被持有的信號量時,信號量會將其推入等待隊列,然后讓其睡眠。這時處理器獲得自由去執行其它代碼。當持有信號量的進程將信號量釋放后,在等待隊列中的一個任務將被喚醒,從而便可以獲得這個信號量。
1)信號量的實現:
在linux中,信號量的定義如下:
struct semaphore {spinlock_t lock; //用來對count變量起保護作用。
unsigned int count; // 大于0,資源空閑;等于0,資源忙,但沒有進程等待這個保護的資源;小于0,資源不可用,并至少有一個進程等待資源。
struct list_head wait_list; //存放等待隊列鏈表的地址,當前等待資源的所有睡眠進程都會放在這個鏈表中。
};
2)信號量的使用:
static inline void sema_init(struct semaphore *sem, int val); //設置sem為val
#define init_MUTEX(sem) sema_init(sem, 1) //初始化一個用戶互斥的信號量sem設置為1 #define init_MUTEX_LOCKED(sem) sema_init(sem, 0) //初始化一個用戶互斥的信號量sem設置為0 定義和初始化可以一步完成:
DECLARE_MUTEX(name); //該宏定義信號量name并初始化1
DECLARE_MUTEX_LOCKED(name); //該宏定義信號量name并初始化0
? 當信號量用于互斥時(即避免多個進程同是在一個臨界區運行),信號量的值應初始化為1。這種信號量在任何給定時刻只能由單個進程或線程擁有。在這種使用模式下,一個信號量有時也稱為一個“互斥體(mutex)”,它是互斥(mutual exclusion)的簡稱。Linux內核中幾乎所有的信號量均用于互斥。
使用信號量,內核代碼必須包含<asm/semaphore.h> 。
3)獲取(鎖定)信號量:
void down(struct semaphore *sem); int down_interruptible(struct semaphore *sem); int down_killable(struct semaphore *sem);
4)釋放信號量
void up(struct semaphore *sem);
下面看一個實例:
[cpp]?view plaincopy
三、自旋鎖與信號量的比較
| ? | 信號量 | 自旋鎖 |
| 1、開銷成本 | 進程上下文切換時間 | 忙等待獲得自旋鎖時間 |
| 2、特性 | a -- 導致阻塞,產生睡眠 b --?進程級的(內核是代表進程來爭奪資源的) | a -- 忙等待,內核搶占關閉 b --?主要是用于CPU同步的 |
| 3、應用場合 | 只能運行于進程上下文 | 還可以出現中斷上下文 |
| 4、其他 | 還可以出現在用戶進程中 | 只能在內核線程中使用 |
從以上的區別以及本身的定義可以推導出兩都分別適應的場合。只考慮內核態
后記:除了上述幾種廣泛使用的的并發控制機制外,還有中斷屏蔽、順序鎖(seqlock)、RCU(Read-Copy-Update)等等,做個簡單總結如下圖:
總結
以上是生活随笔為你收集整理的Linux 设备驱动的并发控制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EMC相关标准
- 下一篇: Linux 字符设备驱动结构(四)——