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