Linux并发与竞争介绍(原子操作、自旋锁、信号量、互斥体)
目錄
- 并發與競爭
- 并發與競爭簡介
- 保護內容是什么
- 原子操作
- 原子操作簡介
- 原子整形操作API函數(atomic_t 結構體)
- 原子位操作API 函數
- 自旋鎖
- 自旋鎖簡介
- 自旋鎖API函數
- 線程與線程
- 線程與中斷(獲取鎖之前關閉中斷)
- 其他類型的鎖(讀寫鎖、順序鎖)
- 自旋鎖使用注意事項
- 信號量
- 信號量簡介
- 信號量API 函數
- 互斥體
- 互斥體簡介
- 互斥體API 函數
Linux 是一個多任務操作系統,肯定會存在多個任務共同操作同一段內存或者設備的情況,多個任務甚至中斷都能訪問的資源叫做共享資源,就和共享單車一樣。在驅動開發中要注意對共享資源的保護,也就是要處理對共享資源的并發訪問。比如共享單車,大家按照誰掃誰騎走的原則來共用這個單車,如果沒有這個并發訪問共享單車的原則存在,只怕到時候為了一輛單車要打起來了。在Linux 驅動編寫過程中對于并發控制的管理非常重要,本章我們就來學習一下如何在Linux 驅動中處理并發。
并發與競爭
并發與競爭簡介
并發就是多個“用戶”同時訪問同一個共享資源,比如你們公司有一臺打印機,你們公司的所有人都可以使用。現在小李和小王要同時使用這一臺打印機,都要打印一份文件。小李要打印的文件內容如下:
我叫小李 電話:123456 工號:16小王要打印的內容如下:
我叫小王 電話:678910 工號:20這兩份文檔肯定是各自打印出來的,不能相互影響。當兩個人同時打印的話如果打印機不做處理的話可能會出現小李的文檔打印了一行,然后開始打印小王的文檔,這樣打印出來的文檔就錯亂了,可能會出現如下的錯誤文檔內容:
我叫小王 電話:123456 工號:20可以看出,小王打印出來的文檔中電話號碼錯誤了,變成小李的了,這是絕對不允許的。如果有多人同時向打印機發送了多份文檔,打印機必須保證一次只能打印一份文檔,只有打印完成以后才能打印其他的文檔。
Linux 系統是個多任務操作系統,會存在多個任務同時訪問同一片內存區域,這些任務可能會相互覆蓋這段內存中的數據,造成內存數據混亂。針對這個問題必須要做處理,嚴重的話可能會導致系統崩潰。現在的Linux 系統并發產生的原因很復雜,總結一下有下面幾個主要原因:
- ①、多線程并發訪問,Linux 是多任務(線程)的系統,所以多線程訪問是最基本的原因。
- ②、搶占式并發訪問,從2.6 版本內核開始,Linux 內核支持搶占,也就是說調度程序可以在任意時刻搶占正在運行的線程,從而運行其他的線程。
- ③、中斷程序并發訪問,這個無需多說,學過STM32 的同學應該知道,硬件中斷的權利可是很大的。
- ④、SMP(多核)核間并發訪問,現在ARM 架構的多核SOC 很常見,多核CPU 存在核間并發訪問。
并發訪問帶來的問題就是競爭,學過FreeRTOS 和UCOS 的同學應該知道臨界區這個概念,所謂的臨界區就是共享數據段,對于臨界區必須保證一次只有一個線程訪問,也就是要保證臨界區是原子訪問的。我們都知道,原子是化學反應不可再分的基本微粒,這里的原子訪問就表示這一個訪問是一個步驟,不能再進行拆分。如果多個線程同時操作臨界區就表示存在競爭,我們在編寫驅動的時候一定要注意避免并發和防止競爭訪問。很多Linux 驅動初學者往往不注意這一點,在驅動程序中埋下了隱患,這類問題往往又很不容易查找,導致驅動調試難度加大、費時費力。所以我們一般在編寫驅動的時候就要考慮到并發與競爭,而不是驅動都編寫完了然后再處理并發與競爭。
保護內容是什么
前面一直說要防止并發訪問共享資源,換句話說就是要保護共享資源,防止進行并發訪問。那么問題來了,什么是共享資源?現實生活中的公共電話、共享單車這些是共享資源,我們都很容易理解,那么在程序中什么是共享資源?也就是保護的內容是什么?我們保護的不是代碼,而是數據!某個線程的局部變量不需要保護,我們要保護的是多個線程都會訪問的共享數據。
一個整形的全局變量a 是數據,一份要打印的文檔也是數據,雖然我們知道了要對共享數據進行保護,那么怎么判斷哪些共享數據要保護呢?找到要保護的數據才是重點,而這個也是難點,因為驅動程序各不相同,那么數據也千變萬化,一般像全局變量,設備結構體這些肯定是要保護的,至于其他的數據就要根據實際的驅動程序而定了。
當我們發現驅動程序中存在并發和競爭的時候一定要處理掉,接下來我們依次來學習一下Linux 內核提供的幾種并發和競爭的處理方法。
原子操作
原子操作簡介
首先看一下原子操作,原子操作就是指不能再進一步分割的操作,一般原子操作用于變量或者位操作。假如現在要對無符號整形變量a 賦值,值為3,對于C 語言來講很簡單,直接就是:
a = 3但是C 語言要先編譯為成匯編指令,ARM 架構不支持直接對寄存器進行讀寫操作,比如要借助寄存器R0、R1 等來完成賦值操作。假設變量a 的地址為0X3000000,“a=3”這一行C語言可能會被編譯為如下所示的匯編代碼:
1 ldr r0, =0X30000000 /* 變量a地址*/ 2 ldr r1, = 3 /* 要寫入的值*/ 3 str r1, [r0] /* 將3寫入到a變量中*/示例代碼47.2.1.1 只是一個簡單的舉例說明,實際的結果要比示例代碼復雜的多。從上述代碼可以看出,C 語言里面簡簡單單的一句“a=3”,編譯成匯編文件以后變成了3 句,那么程序在執行的時候肯定是按照示例代碼47.2.1.1 中的匯編語句一條一條的執行。假設現在線程A要向a 變量寫入10 這個值,而線程B 也要向a 變量寫入20 這個值,我們理想中的執行順序如圖47.2.1.1 所示:
按照圖47.2.1.1 所示的流程,確實可以實現線程A 將a 變量設置為10,線程B 將a 變量設置為20。但是實際上的執行流程可能如圖47.2.1.2 所示:
按照圖47.2.1.2 所示的流程,線程A 最終將變量a 設置為了20,而并不是要求的10!線程B 沒有問題。這就是一個最簡單的設置變量值的并發與競爭的例子,要解決這個問題就要保證示例代碼47.2.1.2 中的三行匯編指令作為一個整體運行,也就是作為一個原子存在。Linux 內核提供了一組原子操作API 函數來完成此功能,Linux 內核提供了兩組原子操作API 函數,一組是對整形變量進行操作的,一組是對位進行操作的,我們接下來看一下這些API 函數。
原子整形操作API函數(atomic_t 結構體)
Linux 內核定義了叫做atomic_t 的結構體來完成整形數據的原子操作,在使用中用原子變量來代替整形變量,此結構體定義在include/linux/types.h 文件中,定義如下:
175 typedef struct { 176 int counter; 177 } atomic_t;如果要使用原子操作API 函數,首先要先定義一個atomic_t 的變量,如下所示:
atomic_t a; //定義a也可以在定義原子變量的時候給原子變量賦初值,如下所示:
atomic_t b = ATOMIC_INIT(0); //定義原子變量b 并賦初值為0可以通過宏ATOMIC_INIT 向原子變量賦初值。
原子變量有了,接下來就是對原子變量進行操作,比如讀、寫、增加、減少等等,Linux 內核提供了大量的原子操作API 函數,如表47.2.2.1 所示:
| ATOMIC_INIT(int i) | 定義原子變量的時候對其初始化。 |
| int atomic_read(atomic_t *v) | 讀取v 的值,并且返回。 |
| void atomic_set(atomic_t *v, int i) | 向v 寫入i 值。 |
| void atomic_add(int i, atomic_t *v) | 給v 加上i 值。 |
| void atomic_sub(int i, atomic_t *v) | 從v 減去i 值。 |
| void atomic_inc(atomic_t *v) | 給v 加1,也就是自增。 |
| void atomic_dec(atomic_t *v) | 從v 減1,也就是自減 |
| int atomic_dec_return(atomic_t *v) | 從v 減1,并且返回v 的值。 |
| int atomic_inc_return(atomic_t *v) | 給v 加1,并且返回v 的值。 |
| int atomic_sub_and_test(int i, atomic_t *v) | 從v 減i,如果結果為0 就返回真,否則返回假 |
| int atomic_dec_and_test(atomic_t *v) | 從v 減1,如果結果為0 就返回真,否則返回假 |
| int atomic_inc_and_test(atomic_t *v) | 給v 加1,如果結果為0 就返回真,否則返回假 |
| int atomic_add_negative(int i, atomic_t *v) | 給v 加i,如果結果為負就返回真,否則返回假 |
如果使用64 位的SOC 的話,就要用到64 位的原子變量,Linux 內核也定義了64 位原子結構體,如下所示:
typedef struct {long long counter; } atomic64_t;相應的也提供了64 位原子變量的操作API 函數,這里我們就不詳細講解了,和表47.2.1.1中的API 函數有用法一樣,只是將“atomic_”前綴換為“atomic64_”,將int 換為long long。如果使用的是64 位的SOC,那么就要使用64 位的原子操作函數。Cortex-A7 是32 位的架構,所以本書中只使用表47.2.2.1 中的32 位原子操作函數。原子變量和相應的API 函數使用起來很簡
單,參考如下示例:
原子位操作API 函數
位操作也是很常用的操作,Linux 內核也提供了一系列的原子位操作API 函數,只不過原子位操作不像原子整形變量那樣有個atomic_t 的數據結構,原子位操作是直接對內存進行操作,API 函數如表47.2.3.1 所示:
| void set_bit(int nr, void *p) | 將p 地址的第nr 位置1。 |
| void clear_bit(int nr,void *p) | 將p 地址的第nr 位清零。 |
| void change_bit(int nr, void *p) | 將p 地址的第nr 位進行翻轉。 |
| int test_bit(int nr, void *p) | 獲取p 地址的第nr 位的值。 |
| int test_and_set_bit(int nr, void *p) | 將p 地址的第nr 位置1,并且返回nr 位原來的值。 |
| int test_and_clear_bit(int nr, void *p) | 將p 地址的第nr 位清零,并且返回nr 位原來的值。 |
| int test_and_change_bit(int nr, void *p) | 將p 地址的第nr 位翻轉,并且返回nr 位原來的值。 |
自旋鎖
自旋鎖簡介
原子操作只能對整形變量或者位進行保護,但常用的設備結構體變量不是整型變量,我們對于結構體中成員變量的操作也要保證原子性,在線程A 對結構體變量使用期間,應該禁止其他的線程來訪問此結構體變量,這些工作原子操作都不能勝任,需要本節要講的鎖機制,在Linux內核中就是自旋鎖。
當一個線程要訪問某個共享資源的時候首先要先獲取相應的鎖,鎖只能被一個線程持有,只要此線程不釋放持有的鎖,那么其他的線程就不能獲取此鎖。對于自旋鎖而言,如果自旋鎖正在被線程A 持有,線程B 想要獲取自旋鎖,那么線程B就會處于忙循環-旋轉-等待狀態,線程B不會進入休眠狀態或者說去做其他的處理,而是會一直傻傻的在那里“轉圈圈”的等待鎖可用。
比如現在有個公用電話亭,一次肯定只能進去一個人打電話,現在電話亭里面有人正在打電話,相當于獲得了自旋鎖。此時你到了電話亭門口,因為里面有人,所以你不能進去打電話,相當于沒有獲取自旋鎖,這個時候你肯定是站在原地等待,你可能因為無聊的等待而轉圈圈消遣時光,反正就是哪里也不能去,要一直等到里面的人打完電話出來。終于,里面的人打完電話出來了,相當于釋放了自旋鎖,這個時候你就可以使用電話亭打電話了,相當于獲取到了自旋鎖。
自旋鎖的“自旋”也就是“原地打轉”的意思,“原地打轉”的目的是為了等待自旋鎖可以用,可以訪問共享資源。把自旋鎖比作一個變量a,變量a=1 的時候表示共享資源可用,當a=0的時候表示共享資源不可用。現在線程A 要訪問共享資源,發現a=0(自旋鎖被其他線程持有),那么線程A 就會不斷的查詢a 的值,直到a=1。
從這里我們可以看到自旋鎖的一個缺點:那就等待自旋鎖的線程會一直處于自旋狀態,這樣會浪費處理器時間,降低系統性能,所以自旋鎖的持有時間不能太長。所以自旋鎖適用于短時期的輕量級加鎖,如果遇到需要長時間持有鎖的場景那就需要換其他的方法了,這個我們后面會講解。
Linux 內核使用結構體spinlock_t 表示自旋鎖,結構體定義如下所示:
64 typedef struct spinlock { 65 union { 66 struct raw_spinlock rlock; 67 68 #ifdef CONFIG_DEBUG_LOCK_ALLOC 69 # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) 70 struct { 71 u8 __padding[LOCK_PADSIZE]; 72 struct lockdep_map dep_map; 73 }; 74 #endif 75 }; 76 } spinlock_t;在使用自旋鎖之前,肯定要先定義一個自旋鎖變量,定義方法如下所示:
spinlock_t lock; //定義自旋鎖定義好自旋鎖變量以后就可以使用相應的API 函數來操作自旋鎖。
自旋鎖API函數
最基本的自旋鎖API 函數如表47.3.2.1 所示:
| DEFINE_SPINLOCK(spinlock_t lock) | 定義并初始化一個自選變量。 |
| int spin_lock_init(spinlock_t *lock) | 初始化自旋鎖。 |
| void spin_lock(spinlock_t *lock) | 獲取指定的自旋鎖,也叫做加鎖。 |
| void spin_unlock(spinlock_t *lock) | 釋放指定的自旋鎖。 |
| int spin_trylock(spinlock_t *lock) | 嘗試獲取指定的自旋鎖,如果沒有獲取到就返回0 |
| int spin_is_locked(spinlock_t *lock) | 檢查指定的自旋鎖是否被獲取,如果沒有被獲取就返回非0,否則返回0。 |
線程與線程
表47.3.2.1 中的自旋鎖API 函數適用于多核SMP或支持搶占的單CPU下線程之間的并發訪問,也就是用于線程與線程之間,被自旋鎖保護的臨界區一定不能調用任何能夠引起睡眠和阻塞的API 函數,否則的話會可能會導致死鎖現象的發生。自旋鎖會自動禁止搶占,也就說當線程A得到鎖以后會暫時禁止內核搶占。
如果線程A 在持有鎖期間進入了休眠狀態,那么線程A 會自動放棄CPU 使用權。線程B 開始運行,線程B 也想要獲取鎖,但是此時鎖被A 線程持有,而且內核搶占還被禁止了!線程B 無法被調度出去,那么線程A 就無法運行,鎖也就無法釋放,死鎖發生了!
線程與中斷(獲取鎖之前關閉中斷)
表47.3.2.1 中的API 函數用于線程之間的并發訪問,如果此時中斷也要插一腳,中斷也想訪問共享資源,那該怎么辦呢?首先可以肯定的是,中斷里面可以使用自旋鎖,但是在中斷里面使用自旋鎖的時候,在獲取鎖之前一定要先禁止本地中斷(也就是本CPU 中斷,對于多核SOC來說會有多個CPU 核),否則可能導致鎖死現象的發生,如圖47.3.2.1 所示:
在圖47.3.2.1 中,線程A 先運行,并且獲取到了lock 這個鎖,當線程A 運行functionA 函數的時候中斷發生了,中斷搶走了CPU 使用權。右邊的中斷服務函數也要獲取lock 這個鎖,但是這個鎖被線程A 占有著,中斷就會一直自旋,等待鎖有效。但是在中斷服務函數執行完之前,線程A 是不可能執行的,線程A 說“你先放手”,中斷說“你先放手”,場面就這么僵持著,死鎖發生!
最好的解決方法就是獲取鎖之前關閉本地中斷,Linux 內核提供了相應的API 函數,如表47.3.2.2 所示:
| void spin_lock_irq(spinlock_t *lock) | 禁止本地中斷,并獲取自旋鎖。 |
| void spin_unlock_irq(spinlock_t *lock) | 激活本地中斷,并釋放自旋鎖。 |
| void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | 保存中斷狀態,禁止本地中斷,并獲取自旋鎖。 |
| void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 將中斷狀態恢復到以前的狀態,并且激活本地中斷,釋放自旋鎖。 |
使用spin_lock_irq/spin_unlock_irq 的時候需要用戶能夠確定加鎖之前的中斷狀態,但實際上內核很龐大,運行也是“千變萬化”,我們是很難確定某個時刻的中斷狀態,因此不推薦使用spin_lock_irq/spin_unlock_irq。建議使用spin_lock_irqsave/ spin_unlock_irqrestore,因為這一組函數會保存中斷狀態,在釋放鎖的時候會恢復中斷狀態。一般在線程中使用spin_lock_irqsave/ spin_unlock_irqrestore,在中斷中使用spin_lock/spin_unlock,示例代碼如下所示:
1 DEFINE_SPINLOCK(lock) /* 定義并初始化一個鎖*/ 2 3 /* 線程A */ 4 void functionA (){ 5 unsigned long flags; /* 中斷狀態*/ 6 spin_lock_irqsave(&lock, flags) /* 獲取鎖*/ 7 /* 臨界區*/ 8 spin_unlock_irqrestore(&lock, flags) /* 釋放鎖*/ 9 } 10 11 /* 中斷服務函數*/ 12 void irq() { 13 spin_lock(&lock) /* 獲取鎖*/ 14 /* 臨界區*/ 15 spin_unlock(&lock) /* 釋放鎖*/ 16 }下半部(BH)也會競爭共享資源,有些資料也會將下半部叫做底半部。關于下半部后面的章節會講解,如果要在下半部里面使用自旋鎖,可以使用表47.3.2.3 中的API 函數:
| void spin_lock_bh(spinlock_t *lock) | 關閉下半部,并獲取自旋鎖。 |
| void spin_unlock_bh(spinlock_t *lock) | 打開下半部,并釋放自旋鎖。 |
其他類型的鎖(讀寫鎖、順序鎖)
在自旋鎖的基礎上還衍生出了其他特定場合使用的鎖,這些鎖在驅動中其實用的不多,更多的是在Linux 內核中使用,本節我們簡單來了解一下這些衍生出來的鎖。
1、讀寫自旋鎖
現在有個學生信息表,此表存放著學生的年齡、家庭住址、班級等信息,此表可以隨時被修改和讀取。此表肯定是數據,那么必須要對其進行保護,如果我們現在使用自旋鎖對其進行保護。每次只能一個讀操作或者寫操作,但是,實際上此表是可以并發讀取的。只需要保證在修改此表的時候沒人讀取,或者在其他人讀取此表的時候沒有人修改此表就行了。也就是此表
的讀和寫不能同時進行,但是可以多人并發的讀取此表。像這樣,當某個數據結構符合讀/寫或生產者/消費者模型的時候就可以使用讀寫自旋鎖。
讀寫自旋鎖為讀和寫操作提供了不同的鎖,一次只能允許一個寫操作,也就是只能一個線程持有寫鎖,而且不能進行讀操作。但是當沒有寫操作的時候允許一個或多個線程持有讀鎖,可以進行并發的讀操作。Linux 內核使用rwlock_t 結構體表示讀寫鎖,結構體定義如下(刪除了條件編譯):
typedef struct { arch_rwlock_t raw_lock; } rwlock_t;讀寫鎖操作API 函數分為兩部分,一個是給讀使用的,一個是給寫使用的,這些API 函數如表47.3.3.1 所示:
| DEFINE_RWLOCK(rwlock_t lock) | 定義并初始化讀寫鎖 |
| void rwlock_init(rwlock_t *lock) | 初始化讀寫鎖。 |
| void read_lock(rwlock_t *lock) | 獲取讀鎖。 |
| void read_unlock(rwlock_t *lock) | 釋放讀鎖。 |
| void read_lock_irq(rwlock_t *lock) | 禁止本地中斷,并且獲取讀鎖。 |
| void read_unlock_irq(rwlock_t *lock) | 打開本地中斷,并且釋放讀鎖。 |
| void read_lock_irqsave(rwlock_t *lock,unsigned long flags) | 保存中斷狀態,禁止本地中斷,并獲取讀鎖。 |
| void read_unlock_irqrestore(rwlock_t *lock,unsigned long flags) | 將中斷狀態恢復到以前的狀態,并且激活本地中斷,釋放讀鎖。 |
| void read_lock_bh(rwlock_t *lock) | 關閉下半部,并獲取讀鎖。 |
| void read_unlock_bh(rwlock_t *lock) | 打開下半部,并釋放讀鎖。 |
| void write_lock(rwlock_t *lock) | 獲取寫鎖。 |
| void write_unlock(rwlock_t *lock) | 釋放寫鎖。 |
| void write_lock_irq(rwlock_t *lock) | 禁止本地中斷,并且獲取寫鎖。 |
| void write_unlock_irq(rwlock_t *lock) | 打開本地中斷,并且釋放寫鎖。 |
| void write_lock_irqsave(rwlock_t *lock,unsigned long flags) | 保存中斷狀態,禁止本地中斷,并獲取寫鎖。 |
| void write_unlock_irqrestore(rwlock_t *lock,unsigned long flags) | 將中斷狀態恢復到以前的狀態,并且激活本地中斷,釋放讀鎖。 |
| void write_lock_bh(rwlock_t *lock) | 關閉下半部,并獲取讀鎖。 |
| void write_unlock_bh(rwlock_t *lock) | 打開下半部,并釋放讀鎖。 |
2、順序鎖
順序鎖在讀寫鎖的基礎上衍生而來的,使用讀寫鎖的時候讀操作和寫操作不能同時進行。使用順序鎖的話可以允許在寫的時候進行讀操作,也就是實現同時讀寫,但是不允許同時進行并發的寫操作。雖然順序鎖的讀和寫操作可以同時進行,但是如果在讀的過程中發生了寫操作,最好重新進行讀取,保證數據完整性。順序鎖保護的資源不能是指針,因為如果在寫操作的時
候可能會導致指針無效,而這個時候恰巧有讀操作訪問指針的話就可能導致意外發生,比如讀取野指針導致系統崩潰。Linux 內核使用seqlock_t 結構體表示順序鎖,結構體定義如下:
關于順序鎖的API 函數如表47.3.3.2 所示:
| DEFINE_SEQLOCK(seqlock_t sl) | 定義并初始化順序鎖 |
| void seqlock_ini seqlock_t *sl) | 初始化順序鎖。 |
| void write_seqlock(seqlock_t *sl) | 獲取寫順序鎖。 |
| void write_sequnlock(seqlock_t *sl) | 釋放寫順序鎖。 |
| void write_seqlock_irq(seqlock_t *sl) | 禁止本地中斷,并且獲取寫順序鎖 |
| void write_sequnlock_irq(seqlock_t *sl) | 打開本地中斷,并且釋放寫順序鎖。 |
| void write_seqlock_irqsave(seqlock_t *sl,unsigned long flags) | 保存中斷狀態,禁止本地中斷,并獲取寫順序鎖。 |
| void write_sequnlock_irqrestore(seqlock_t *sl,unsigned long flags) | 將中斷狀態恢復到以前的狀態,并且激活本地中斷,釋放寫順序鎖。 |
| void write_seqlock_bh(seqlock_t *sl) | 關閉下半部,并獲取寫讀鎖。 |
| void write_sequnlock_bh(seqlock_t *sl) | 打開下半部,并釋放寫讀鎖。 |
| unsigned read_seqbegin(const seqlock_t *sl) | 讀單元訪問共享資源的時候調用此函數,此函數會返回順序鎖的順序號。 |
| unsigned read_seqretry(const seqlock_t *sl,unsigned start) | 讀結束以后調用此函數檢查在讀的過程中有沒有對資源進行寫操作,如果有的話就要重讀 |
自旋鎖使用注意事項
綜合前面關于自旋鎖的信息,我們需要在使用自旋鎖的時候要注意一下幾點:
- ①、因為在等待自旋鎖的時候處于“自旋”狀態,因此鎖的持有時間不能太長,一定要短,否則的話會降低系統性能。如果臨界區比較大,運行時間比較長的話要選擇其他的并發處理方式,比如稍后要講的信號量和互斥體。
- ②、自旋鎖保護的臨界區內不能調用任何可能導致線程休眠的API 函數,否則的話可能導致死鎖。
- ③、不能遞歸申請自旋鎖,因為一旦通過遞歸的方式申請一個你正在持有的鎖,那么你就必須“自旋”,等待鎖被釋放,然而你正處于“自旋”狀態,根本沒法釋放鎖。結果就是自己把自己鎖死了!
- ④、在編寫驅動程序的時候我們必須考慮到驅動的可移植性,因此不管你用的是單核的還是多核的SOC,都將其當做多核SOC來編寫驅動程序。
信號量
信號量簡介
大家如果有學習過FreeRTOS 或者UCOS 的話就應該對信號量很熟悉,因為信號量是同步的一種方式。Linux 內核也提供了信號量機制,信號量常常用于控制對共享資源的訪問。
相比于自旋鎖,信號量可以使線程進入休眠狀態,比如A 與B、C 合租了一套房子,這個房子只有一個廁所,一次只能一個人使用。某一天早上A 去上廁所了,過了一會B 也想用廁所,因為A 在廁所里面,所以B 只能等到A 用來了才能進去。B 要么就一直在廁所門口等著,等A 出來,這個時候就相當于自旋鎖。B 也可以告訴A,讓A 出來以后通知他一下,然后B 繼續回房間睡覺,這個時候相當于信號量。
可以看出,使用信號量會提高處理器的使用效率,畢竟不用一直傻乎乎的在那里“自旋”等待。但是,信號量的開銷要比自旋鎖大,因為信號量使線程進入休眠狀態以后會切換線程,切換線程就會有開銷。總結一下信號量的特點:
- ①、因為信號量可以使等待資源線程進入休眠狀態,因此適用于那些占用資源比較久的場合。
- ②、因此信號量不能用于中斷中,因為信號量會引起休眠,中斷不能休眠。
- ③、如果共享資源的持有時間比較短,那就不適合使用信號量了,因為頻繁的休眠、切換線程引起的開銷要遠大于信號量帶來的那點優勢。
信號量有一個信號量值,相當于一個房子有10 把鑰匙,這10 把鑰匙就相當于信號量值為10。因此,可以通過信號量來控制訪問共享資源的訪問數量,如果要想進房間,那就要先獲取一把鑰匙,信號量值減1,直到10 把鑰匙都被拿走,信號量值為0,這個時候就不允許任何人進入房間了,因為沒鑰匙了。如果有人從房間出來,那他要歸還他所持有的那把鑰匙,信號量值加1,此時有1 把鑰匙了,那么可以允許進去一個人。相當于通過信號量控制訪問資源的線程數,在初始化的時候將信號量值設置的大于1,那么這個信號量就是計數型信號量,計數型信號量不能用于互斥訪問,因為它允許多個線程同時訪問共享資源。
如果要互斥的訪問共享資源那么信號量的值就不能大于1,此時的信號量就是一個二值信號量。
信號量API 函數
Linux 內核使用semaphore 結構體表示信號量,結構體內容如下所示:
struct semaphore {raw_spinlock_t lock;unsigned int count;struct list_head wait_list; };要想使用信號量就得先定義,然后初始化信號量。有關信號量的API 函數如表47.4.2.1 所示:
| DEFINE_SEAMPHORE(name) | 定義一個信號量,并且設置信號量的值為1。 |
| void sema_init(struct semaphore *sem, int val) | 初始化信號量sem,設置信號量值為val。 |
| void down(struct semaphore *sem) | 獲取信號量,因為會導致休眠,因此不能在中斷中使用。 |
| int down_trylock(struct semaphore *sem); | 嘗試獲取信號量,如果能獲取到信號量就獲取,并且返回0。如果不能就返回非0,并且不會進入休眠。 |
| int down_interruptible(struct semaphore *sem) | 獲取信號量,和down 類似,只是使用down 進入休眠狀態的線程不能被信號打斷。而使用此函數進入休眠以后是可以被信號打斷的。 |
| void up(struct semaphore *sem) | 釋放信號量 |
信號量的使用如下所示:
struct semaphore sem; /* 定義信號量*/ sema_init(&sem, 1); /* 初始化信號量*/ down(&sem); /* 申請信號量*/ /* 臨界區*/ up(&sem); /* 釋放信號量*/互斥體
互斥體簡介
在FreeRTOS 和UCOS 中也有互斥體,將信號量的值設置為1 就可以使用信號量進行互斥訪問了,雖然可以通過信號量實現互斥,但是Linux 提供了一個比信號量更專業的機制來進行互斥,它就是互斥體—mutex。互斥訪問表示一次只有一個線程可以訪問共享資源,不能遞歸申請互斥體。在我們編寫Linux 驅動的時候遇到需要互斥訪問的地方建議使用mutex。Linux 內核使用mutex 結構體表示互斥體,定義如下(省略條件編譯部分):
struct mutex {/* 1: unlocked, 0: locked, negative: locked, possible waiters */atomic_t count;spinlock_t wait_lock; };在使用mutex 之前要先定義一個mutex 變量。在使用mutex 的時候要注意如下幾點:
- ①、mutex 可以導致休眠,因此不能在中斷中使用mutex,中斷中只能使用自旋鎖。
- ②、和信號量一樣,mutex 保護的臨界區可以調用引起阻塞的API 函數。
- ③、因為一次只有一個線程可以持有mutex,因此,必須由mutex 的持有者釋放mutex。并且mutex 不能遞歸上鎖和解鎖。
互斥體API 函數
有關互斥體的API 函數如表47.5.2.1 所示:
| DEFINE_MUTEX(name) | 定義并初始化一個mutex 變量。 |
| void mutex_init(mutex *lock) | 初始化mutex。 |
| void mutex_lock(struct mutex *lock) | 獲取mutex,也就是給mutex 上鎖。如果獲取不到就進休眠。 |
| void mutex_unlock(struct mutex *lock) | 釋放mutex,也就給mutex 解鎖。 |
| int mutex_trylock(struct mutex *lock) | 嘗試獲取mutex,如果成功就返回1,如果失敗就返回0。 |
| int mutex_is_locked(struct mutex *lock) | 判斷mutex 是否被獲取,如果是的話就返回1,否則返回0。 |
| int mutex_lock_interruptible(struct mutex *lock) | 使用此函數獲取信號量失敗進入休眠以后可以被信號打斷。 |
互斥體的使用如下所示:
1 struct mutex lock; /* 定義一個互斥體*/ 2 mutex_init(&lock); /* 初始化互斥體*/ 3 4 mutex_lock(&lock); /* 上鎖*/ 5 /* 臨界區*/ 6 mutex_unlock(&lock); /* 解鎖*/關于Linux 中的并發和競爭就講解到這里,Linux 內核還有很多其他的處理并發和競爭的機制,本章我們主要講解了常用的原子操作、自旋鎖、信號量和互斥體。以后我們在編寫Linux驅動的時候就會頻繁的使用到這幾種機制,希望大家能夠深入理解這幾個常用的機制。
總結
以上是生活随笔為你收集整理的Linux并发与竞争介绍(原子操作、自旋锁、信号量、互斥体)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android 颜色渲染(九) Port
- 下一篇: LightOJ 1422 Hallowe