Linux 併發與競爭

Bathwind_W發表於2024-06-14

Linux 併發與競爭

併發與競爭

Linux 系統是個多工作業系統,會存在多個任務同時訪問同一片記憶體區域,這些任務可能會相互覆蓋這段記憶體中的資料,造成記憶體資料混亂。針對這個問題必須要做處理,嚴重的話可能會導致系統崩潰。現在的 Linux 系統併發產生的原因很複雜,總結一下有下面幾個主要原因:
①、多執行緒併發訪問, Linux 是多工(執行緒)的系統,所以多執行緒訪問是最基本的原因。
②、搶佔式併發訪問,從 2.6 版本核心開始, Linux 核心支援搶佔,也就是說排程程式可以在任意時刻搶佔正在執行的執行緒,從而執行其他的執行緒。
③、中斷程式併發訪問,
④、 SMP(多核)核間併發訪問,現在 ARM 架構的多核 SOC 很常見,多核 CPU 存在核間併發訪問。
所謂的臨界區就是共享資料段,對於臨界區必須保證一次只有一個執行緒訪問,也就是要保證臨界區是原子訪問的,原子訪問就表示這一個訪問是一個步驟,不能再進行拆分。
防止併發訪問共享資源,換句話說就是要保護共享資源,防止進行併發訪問,因為驅動程式各不相同,那麼資料也千變萬化,一般像全域性變數,裝置結構體這些肯定是要保護的,至於其他的資料就要根據實際的驅動程式而定了。

原子操作

原子操作就是指不能再進一步分割的操作,一般原子操作用於變數或者位操作。假如現在要對無符號整形變數 a 賦值,值為 3,對於 C 語言來講很簡單,直接就是:

a = 3

C 語言要先編譯為成彙編指令, ARM 架構不支援直接對暫存器進行讀寫操作,比如要藉助暫存器 R0、 R1 等來完成賦值操作。假設變數 a 的地址為 0X3000000,“a=3”這一行 C語言可能會被編譯為如下所示的彙編程式碼:

示例程式碼 47.2.1.1 彙編示例程式碼
1 ldr r0, =0X30000000 /* 變數 a 地址 */
2 ldr r1, = 3 /* 要寫入的值 */
3 str r1, [r0] /* 將 3 寫入到 a 變數中 */

假設現線上程 A要向 a 變數寫入 10 這個值,而執行緒 B 也要向 a 變數寫入 20 這個值,可能得到的執行順序如下:
在這裡插入圖片描述
但是實際上的執行流程可能如下圖所示:
在這裡插入圖片描述
執行緒 A 最終將變數 a 設定為了 20,而並不是要求的 10!執行緒B 沒有問題。這就是一個最簡單的設定變數值的併發與競爭的例子,要解決這個問題就要保證三行彙編指令作為一個整體執行,也就是作為一個原子存在。 Linux 核心提供了一組原子操作 API 函式來完成此功能, Linux 核心提供了兩組原子操作 API 函式,一組
是對整形變數進行操作的,一組是對位進行操作的。

原子整形操作 API 函式

Linux 核心定義了叫做 atomic_t 的結構體來完成整形資料的原子操作,在使用中用原子變數來代替整形變數,此結構體定義在 include/linux/types.h 檔案中,定義如下:

示例程式碼 47.2.2.1 atomic_t 結構體
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 函式,如表所示:

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 位原子結構體,如下所示:

示例程式碼 47.2.2.2 atomic64_t 結構體
typedef struct {
long long counter;
} atomic64_t;

相應的也提供了 64 位原子變數的操作 API 函式,這裡我們就不詳細講解了,和表 47.2.1.1中的 API 函式有用法一樣,只是將“atomic_”字首換為“atomic64_”,將 int 換為 long long。如果使用的是 64 位的 SOC,那麼就要使用 64 位的原子操作函式。
例子:

示例程式碼 47.2.2.2 原子變數和 API 函式使用
atomic_t v = ATOMIC_INIT(0); /* 定義並初始化原子變零 v=0 */
atomic_set(&v, 10); /* 設定 v=10 */
atomic_read(&v); /* 讀取 v 的值,肯定是 10 */
atomic_inc(&v); /* v 的值加 1, v=11 */

原子位操作 API 函式

位操作也是很常用的操作, Linux 核心也提供了一系列的原子位操作 API 函式,只不過原子位操作不像原子整形變數那樣有個 atomic_t 的資料結構,原子位操作是直接對記憶體進行操作,API 函式如下所示:

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 不會進入休眠狀態或者說去做其他的處理,而是會一直傻傻的在那裡“轉圈圈”的等待鎖可用。自旋鎖的“自旋”也就是“原地打轉”的意思,“原地打轉”的目的是為了等待自旋鎖可以用,可以訪問共享資源。那就等待自旋鎖的執行緒會一直處於自旋狀態,這樣會浪費處理器時間,降低系統效能,所以自旋鎖的持有時間不能太長。所以自旋鎖適用於短時期的輕量級加鎖,如果遇到需要長時間持有鎖的場景那就需要換其他的方法了,
Linux 核心使用結構體 spinlock_t 表示自旋鎖,結構體定義如下所示:

示例程式碼 47.3.1.1 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 函式

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。

自旋鎖API 函式適用於SMP或支援搶佔的單CPU下執行緒之間的併發訪問,
"SMP"是指“對稱多處理”(Symmetric Multiprocessing)系統。在這種系統中,兩個或多個處理器共享同一主記憶體和I/O裝置,並且在作業系統的管理下平等地執行任務。這種架構與非對稱多處理(Asymmetric Multiprocessing,AMP)相對,後者每個處理器或處理器組有專門的任務和可能有自己的記憶體或I/O裝置。
SMP的特點:

  1. 處理器對等:所有處理器都具有相同的功能和對系統資源的訪問能力。
  2. 記憶體共享:所有處理器共享同一實體記憶體,這意味著任何處理器都可以訪問系統中的任何記憶體位置。
  3. 負載平衡:作業系統負責在所有處理器之間平衡負載,以提高效率和效能。

自旋鎖在SMP系統中的應用:
在SMP系統中,由於多個處理器可能同時嘗試訪問共享資源,因此需要有效的同步機制來避免競態條件和確保資料一致性。自旋鎖是一種用於多執行緒同步的低階原語,特別適用於那些鎖持有時間非常短的場景。

自旋鎖的工作原理是:

  • 當執行緒嘗試獲取一個已被其他執行緒持有的鎖時,它將在一個迴圈中不斷檢查鎖的狀態(這個過程稱為“自旋”),直到鎖變為可用狀態。
  • 這意味著執行緒在等待鎖的過程中會消耗CPU資源,因此自旋鎖適用於鎖持有時間短且執行緒不希望在作業系統層面進行上下文切換的情況。

也就是用於執行緒與執行緒之間,被自旋鎖保護的臨界區一定不能呼叫任何能夠引起睡眠和阻塞的API 函式,否則的話會可能會導致死鎖現象的發生。自旋鎖會自動禁止搶佔,也就說當執行緒 A得到鎖以後會暫時禁止核心搶佔。如果執行緒 A 在持有鎖期間進入了休眠狀態,那麼執行緒 A 會自動放棄 CPU 使用權。執行緒 B 開始執行,執行緒 B 也想要獲取鎖,但是此時鎖被 A 執行緒持有,而且核心搶佔還被禁止了!執行緒 B 無法被排程出去,那麼執行緒 A 就無法執行,鎖也就無法釋放,好了,死鎖發生了!
下面看一個例子:
在這裡插入圖片描述
A先獲取鎖,並執行。當執行緒 A 執行 functionA 函式的時候中斷髮生了,中斷搶走了 CPU 使用權。右邊的中斷服務函式也要獲取 lock 這個鎖,但是這個鎖被執行緒 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_unloc
k.

示例程式碼 47.3.2.1 自旋鎖使用示例
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 }

當然可以。讓我們透過一個簡單的網路資料包處理的例子來詳細說明上半部和下半部是如何工作的,以及它們如何分離以提高系統的整體效率。

場景:網路資料包接收處理

假設一個網路裝置(如乙太網卡)接收到一個資料包,這會觸發一個硬體中斷。

上半部(Top Half):

  1. 中斷觸發:網路裝置接收到資料包後,觸發硬體中斷。
  2. 中斷服務例程(ISR)執行
    • 最小處理:ISR被呼叫,它的任務是儘快處理最緊急的事務。在這個例子中,ISR可能會讀取資料包到一個緩衝區,並清除中斷標誌,以便網路裝置可以繼續接收更多資料包。
    • 快速返回:ISR需要快速執行完畢,因為在執行ISR期間,大多數系統會禁止同一裝置的其他中斷,這可能會導致其他重要事件的延遲。

下半部(Bottom Half):

  1. 延遲處理:一旦ISR完成,它會排程一個下半部任務(如tasklet或work queue)來處理不那麼緊急的工作。
  2. 處理資料包
    • 資料處理:下半部任務會處理ISR讀入緩衝區的資料包。這可能包括檢查資料包的完整性、執行路由決策、統計更新等。
    • 轉發/響應:根據資料包的內容,執行相應的動作,如向上層協議棧傳遞資料、生成響應或轉發資料包。
  3. 允許中斷:由於下半部的執行不會阻塞其他中斷,系統可以繼續響應其他硬體中斷,這對於維持裝置響應性至關重要。

為什麼需要下半部?
使用下半部機制的主要原因是減少中斷服務時間和提高系統的併發處理能力。透過將耗時的資料處理任務從ISR中剝離出來,我們保證了中斷處理的快速執行,從而減少了對系統其他部分的干擾。此外,下半部可以在系統較不繁忙時執行,或者在多處理器系統中,可能在另一個CPU上並行執行,這進一步提高了系統的效率和吞吐量。
下半部裡面使用自旋鎖,可以使用表中的 API 函式:

函式 描述
void spin_lock_bh(spinlock_t *lock) 關閉下半部,並獲取自旋鎖。
void spin_unlock_bh(spinlock_t *lock) 開啟下半部,並釋放自旋鎖。

其他型別的鎖

1、讀寫自旋鎖

比如存在一個資料,此表存放著學生的年齡、家庭住址、班級等資訊,此表可以隨時被修改和讀取。自旋鎖對其進行保護。每次只能一個讀操作或者寫操作,此表的讀和寫不能同時進行,但是可以多人併發的讀取此表。像這樣,當某個資料結構符合讀/寫或生產者/消費者模型的時候就可以使用讀寫自旋鎖。
讀寫自旋鎖為讀和寫操作提供了不同的鎖,一次只能允許一個寫操作,也就是隻能一個執行緒持有寫鎖,而且不能進行讀操作。但是當沒有寫操作的時候允許一個或多個執行緒持有讀鎖,可以進行併發的讀操作。 Linux 核心使用 rwlock_t 結構體表示讀寫鎖,結構體定義如下(刪除了條件編譯):

示例程式碼 47.3.3.1 rwlock_t 結構體
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;

讀寫鎖操作 API 函式分為兩部分,一個是給讀使用的,一個是給寫使用的,這些 API 函式:

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、順序鎖

順序鎖的讀和寫操作可以同時進行,但是如果在讀的過程中發生了寫操作,最好重新進行讀取,保證資料完整性。
順序鎖保護的資源不能是指標,因為如果在寫操作的時候可能會導致指標無效,採用指標的時候,指標的不穩定性
當使用順序鎖保護一個指標時,如果在讀操作進行時指標被寫操作修改(例如指向另一個地址或被設為NULL),會出現以下問題:
指標失效:如果寫操作改變了指標的指向,那麼正在進行的讀操作可能會繼續使用舊的指標值訪問已經不再有效或相關的記憶體地址。這可能導致訪問已釋放的記憶體,從而引發程式崩潰或資料損壞。
資料不一致:即使指標仍然有效,指向的資料可能在寫操作中已經被修改。由於讀操作在檢查序列號前後可能會有延遲,它可能在資料結構不一致的狀態下讀取資料。這種情況下,讀操作可能會得到部分更新的資料,導致資料不一致。
Linux 核心使用 seqlock_t 結構體表示順序鎖,結構體定義如下:

示例程式碼 47.3.3.2 seqlock_t 結構體
typedef struct {
struct seqcount seqcount;
spinlock_t lock;
} seqlock_t;
函式 						描述
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 來編寫驅動程式。

訊號量

學過RTOS就知道訊號量的定義和使用場景。下面來直接看訊號量 API 函式:
Linux 核心使用 semaphore 結構體表示訊號量,結構體內容如下所示:

示例程式碼 47.4.2.1 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) 釋放訊號量

訊號量的使用如下所示:

示例程式碼 47.4.2.2 訊號量使用示例
struct semaphore sem; /* 定義訊號量 */
sema_init(&sem, 1); /* 初始化訊號量 */
down(&sem); /* 申請訊號量 */
/* 臨界區 */
up(&sem); /* 釋放訊號量 */

互斥體

在 FreeRTOS 和 UCOS 中也有互斥體,將訊號量的值設定為 1 就可以使用訊號量進行互斥訪問了,雖然可以透過訊號量實現互斥,但是 Linux 提供了一個比訊號量更專業的機制來進行互斥,它就是互斥體—mutex。互斥訪問表示一次只有一個執行緒可以訪問共享資源,不能遞迴申請互斥體。在我們編寫 Linux 驅動的時候遇到需要互斥訪問的地方建議使用 mutex。
使用 mutex 結構體表示互斥體,定義如下(省略條件編譯部分):

示例程式碼 47.5.1.1 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 函式所示:

函式 描述
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)使用此函式獲取訊號量失敗進入休眠以後可以被訊號打斷。
互斥體的使用如下所示:
示例程式碼 47.5.2.1 互斥體使用示例
1 struct mutex lock; /* 定義一個互斥體 */
2 mutex_init(&lock); /* 初始化互斥體 */
3 4
mutex_lock(&lock); /* 上鎖 */
5 /* 臨界區 */
6 mutex_unlock(&lock); /* 解鎖 */

相關文章