前言
核心驅動的併發&竟態很容易理解,其解決方法也不能,看看例程就可以了。
對於API,看看核心原始碼和核心文件即可。
原文連結:https://www.cnblogs.com/lizhuming/p/14907262.html
12. 併發&競態
本章內容為驅動基石之一。
驅動只提供功能,不提供策略。
12.1 併發&競態概念
併發:
- 指多個單元同時、並行執行。
- 但是併發執行的單元對共享資源的訪問容易產生競態。
- 單核的併發可以參考 MCU RTOS 多工原理。看似並行,實質序列。不過也存在競態。
併發產生原因(大概):
- 多執行緒併發訪問。
- 搶佔式併發訪問。(linux2.6及高版本的核心為搶佔式核心)
- 中斷程式併發訪問。
- 多核(SMP)核間併發訪問。
競態:
- 指併發的執行單元對共享資源的訪問。
- 競態產生的條件:
- 存在共享資源。
- 對共享資源進行競爭訪問。
12.2 競態解決方法
需要解決競態是因為要保護資料。
確保每個時刻都只有一個執行單元訪問共享資源。
競態解決方法有:
- 原子操作。
- 自旋鎖操作。
- 訊號量操作。
- 互斥體操作。
12.3 原子
參考文件:
- Documentation\atomic_t.txt
- Documentation\atomic_bitops.txt
12.3.1 原子介紹
都知道,在 C 的世界裡,a = 10;
這樣一個簡單的賦值,到了彙編的世界就不止一條語句啦。若此時多執行緒往變數 a 的地址賦值,就可能會產生資料錯誤。
原子操作就是不可分割操作。
注意:原子操作只能對 整型變數 和 位操作 具有保護功能。
12.3.2 原子操作步驟
原子操作:
- 定義原子變數&設定初始值。
- 設定原子變數的值。
- 獲取原子變數的值。
- 原子變數的 加/減。
- 原子變數的 自加/自減。
- 原子變數的 加/減 及返回值。
- 原子變數測試函式。
12.3.3 原子 API
由於函式容易理解,所以就不像以前的筆記一樣詳細列出。
整型原子的操作需要個 atomic_t 結構體。
bit原子的操作只需要一個地址即可,是直接對記憶體操作。
atomic_t 32bit 整型原子變數結構體:
//atomic_t型別結構體
typedef struct
{
int counter;
}atomic_t;
atomic64_t 64bit 整型原子變數結構體:
//atomic64_t 型別結構體
typedef struct
{
long long counter;
}atomic64_t;
整型原子 API 彙總:
API | 描述 |
---|---|
ATOMIC_INIT(int i) | 定義原子變數時候的初始值 |
void atomic_set(atomic_t *v, int i) | 向 v 寫入 i |
void atomic_read(atomic_t *v) | 讀取 v 的值 |
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_add_return(int i, atomic_t *v) | v 加 i ,返回 v 的結果 |
int atomic_sub_return(int i, atomic_t *v) | v 減 i ,返回 v 的結果 |
int atomic_inc_return(int i, atomic_t *v) | v 加 1 ,返回 v 的結果 |
int atomic_dec_return(int i, atomic_t *v) | v 減 1 ,返回 v 的結果 |
int atomic_sub_and_test(int i, atomic_t *v) | v 減 i 後是否為 0 |
int atomic_inc_and_test(atomic_t *v) | v 加 1 後是否為 0 |
int atomic_dec_and_test(atomic_t *v) | v 減 1 後是否為 0 |
int atomic_add_negative(int i, atomic_t *v) | v 加 i 後是否為 負數 |
更多 API(如atomic_dec_unless_positive()、atomic_inc_unless_negative()) 請參考核心原始碼和推薦的文件。
bit原子的操作不需要 atomic_t 結構體,它是直接對 記憶體 操作的。
bit 原子 API 彙總:
API | 描述 |
---|---|
void set_bit(int nr, void *p) | 對地址 p 的第 nr 位置 1 |
void clear_bit(int nr, void *p) | 對地址 p 的第 nr 位置 0 |
void change_bit(int nr, void *p) | 對地址 p 的第 nr 位翻轉 |
int test_bit(int nr, void *p) | 返回地址 p 的第 nr 位的值 |
void test_and_set_bit(int nr, void *p) | 對地址 p 的第 nr 位置 1,並返回原來的 nr 位值 |
void test_and_clear_bit(int nr, void *p) | 對地址 p 的第 nr 位置 0,並返回原來的 nr 位值 |
void test_and_change_bit(int nr, void *p) | 對地址 p 的第 nr 位翻轉,並返回原來的 nr 位值 |
12.4 自旋鎖
12.4.1 自旋鎖介紹
原子操作只能對整型變數或者bit進行保護。而自旋鎖能對一個單元進行保護,是給程式碼段新增一把鎖。
自旋鎖是實現互斥訪問的常用手段。
獲取自旋鎖後再執行程式碼才能被保護起來。
自旋鎖特點:
- 當使用自旋鎖獲取鎖失敗時(即需要訪問的程式碼段被鎖住了),執行緒不休眠,做死迴圈檢測鎖狀態,直至自旋鎖被釋放。
- 簡單,不休眠,可在中斷中使用。
- 使用不當會導致死鎖。如:
- 遞迴獲取鎖:第一次獲取鎖成功,在自旋鎖保護的程式碼段內進行獲取鎖,那便永遠等不到解鎖,導致死鎖。
自旋鎖缺點:
- 死迴圈檢測,佔用系統資源。
- 遞迴獲取鎖後會導致死鎖。
- 同一執行緒不能連續兩次獲取自旋鎖,必須一獲取一釋放。
- 自旋鎖在鎖定期間不能呼叫引起程式排程的函式,否則可能導致系統崩潰。
12.4.2 自旋鎖操作步驟
自旋鎖操作:
- 定義自旋鎖。
- 初始化自旋鎖。
- 獲取自旋鎖。
- 釋放自旋鎖。
自旋鎖使用注意事項:
- 鎖的持有時間要短。因為自旋鎖是不會休眠的,以免其它執行緒獲取鎖等待太久,降低系統效能。
- 自旋鎖保護的臨界區內不能呼叫引起執行緒休眠的 API 函式,否則可能引起死鎖。
- 不能遞迴獲取自旋鎖,否則會導致死鎖。
- 按多核思想程式設計。提高系統可移植性。
12.4.3 自旋鎖 API
spinlock_t 結構體:
typedef struct
{
struct lock_impl internal_lock;
}spinlock_t;
自旋鎖 API 彙總:
API | 描述 |
---|---|
DEFINE_SPINLOCK(spinlock_t lock) | 定義、初始化一個自選變數 |
void spin_lock_init(spinlock_t *lock) | 初始化一個自旋鎖 |
void spin_lock(spinlock_t *lock) | 加鎖,即是獲取一個自旋鎖 |
int spin_trylock(spinlock_t *lock) | 嘗試獲取自旋鎖,不等待,成功返回 true,失敗返回 false |
void spin_unlock(spinlock_t *lock) | 釋放自旋鎖 |
int spin_is_locked(spinlock_t *lock) | 檢查指定自旋鎖是否已經被獲取。若沒有,則返回非0;否則返回 0 |
void spin_lock_irq(spinlock_t *lock) | 獲取自旋鎖並關中斷(防止中斷打斷) |
void spin_unlock_irq(spinlock_t *lock) | 釋放自旋鎖並開中斷 |
spin_lock_irqsave(lock, flags) | 獲取自旋鎖,並儲存中斷狀態到flags。鎖返回時,之前開的中斷,之後也是開的;之前關,之後也是關 |
spin_unlock_irqrestore(lock, flags) | 釋放自旋鎖,並恢復中斷狀態,即是把 flags 值賦值給中斷狀態暫存器。 |
12.4.4 讀寫自旋鎖
普通的自旋鎖是一刀切的,不管訪問者對臨界區的操作是讀還是寫。
但是實際上,很多共享資源都允許多個執行單元同時讀,這是不影響資料的。
所以,讀寫自旋鎖 允許 讀併發,但是不允許 寫併發,且不允許讀寫同時出現。
即有允許以下情景:
- 多讀。
- 一寫。
讀寫自旋鎖 結構體:
typedef struct
{
arch_rwlock_t raw_lock;
}rwlock_t;
讀寫自旋鎖 API:
- 定義&初始化:
API | 描述 |
---|---|
DEFINE_RWLOCK(rwlock_t lock) | 定義、初始化一個自選變數 |
void rwlock_init(rwlock_t *lock) | 初始化一個自旋鎖 |
- 讀鎖 API:
API | 描述 |
---|---|
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) | 開啟下半部,釋放讀自旋鎖 |
- 寫鎖:
- 把前面讀鎖的字首 read_ 改為 write_,即可。
12.4.5 順序鎖
順序鎖 是 讀寫鎖 的一個優化。
讀寫鎖 不允許讀和寫同時出現。有以下前景:
- 多讀。
- 一寫。
順序鎖 允許讀和寫同時出現,但是隻能出現一個寫。有以下前景:
- 多讀。
- 一寫。
- 多讀一寫。
順序自旋鎖 結構體:
typedef struct
{
struct seqcount seqcount;
spinlock_t lock;
}seqlock_t;
順序自旋鎖 API:
- 定義&初始化:
API | 描述 |
---|---|
DEFINE_SEQLOCK(seqlock_t sl) | 定義、初始化一個自選變數 |
void seqlock_init(seqlock_t *sl) | 初始化一個自旋鎖 |
- 讀鎖 API:
- 需要注意的是,寫操作的順序鎖,會對順序號加1-2。若 read_seqretry() 檢測到順序號不一致,則請重新讀去資料。
API | 描述 |
---|---|
unsigned read_seqbegin(const seqlock_t *sl) | 加鎖,並返回獲取到的順序鎖的順序號 |
unsigned read_seqretry(const seqlock_t *sl) | 讀結束後呼叫該函式。用於檢查在讀的過程中是否有對資源進行寫操作,若有,則返回1,建議重新讀去資料。 |
- 寫鎖 API:
API | 描述 |
---|---|
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) | 開啟下半部,釋放讀自旋鎖 |
12.5 訊號量
12.5.1 訊號量概念
學過 RTOS 的都知道訊號量了。可以看做一個全域性計數器。
訊號量常用於同步和互斥。
訊號量的獲取失敗後,執行緒可引入休眠,當訊號量可用時,系統會通知其退出休眠。
12.5.2 訊號量操作
訊號量操作:
- 定義訊號量。
- 初始化訊號量。
- 嘗試獲取訊號量。
- 獲取訊號量。
- 釋放訊號量。
訊號量使用注意事項:
- 適用於佔用資源較長時間的情景。因為訊號量可以引起休眠,佔用系統資源少。若佔用資源時間少的,建議使用 自旋鎖 ,因為不用切換執行緒,系統開銷小。
- 不能用於中斷。同樣是因為訊號量可以引起休眠。不過可以使用 down_interruptible() 函式。
- 保護的臨界區內可呼叫引起阻塞的 API。
12.5.3 訊號量 API
semaphore 結構體:
struct semaphore
{
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
API | 描述 |
---|---|
DEFINE_SEMAPHORE(name) | 定義一個訊號量,並置為 1 |
void sema_init(struct semaphore *sem, int val) | 初始化訊號量,並置為 val |
void down(struct semaphore *sem) | 獲取訊號量。因為訊號量會導致休眠,且不能被訊號打斷,因此不能在中斷中使用該函式 |
int down_trylock(struct semaphore *sem) | 嘗試獲取訊號量,不休眠。成功返回 0,失敗返回 非0 |
void down_interruptible(struct semaphore *sem) | 獲取訊號量。就算導致休眠後,也能被訊號打斷,因此該函式可以在中斷中使用 |
void up(struct semaphore *sem) | 釋放訊號量 |
12.6 互斥體
12.6.1 互斥體概念
互斥體 的佔用其實和 訊號量量值為 1 的效果是一樣的。
但是互斥體的執行效率更高,畢竟,專業的API做專業的事嘛。
12.6.2 互斥體操作
互斥體執行操作:
- 定義互斥體。
- 初始化互斥體。
- 嘗試獲取互斥體。
- 獲取互斥體。
- 釋放互斥體。
互斥體使用注意事項:
- 不能在中斷中使用。因為 mutex 會導致休眠。除非使用函式 int mutex_lock_interruptible。
- 必須由 mutex 持有者釋放。因為一次只有一條執行緒持有。
- 保護的臨界區內可呼叫引起阻塞的 API。
12.6.3 互斥體 API
API | 描述 |
---|---|
DEFINE_MUTEX(name) | 定義並初始化一個 mutex 變數 |
void mutex_init(mutex *lock) | 初始化 mutex |
void mutex_lock(struct mutex *lock) | 加鎖,獲取 mutex |
void mutex_unlock(struct mutex *lock) | 釋放 mutex |
int mutex_trylock(struct mutex *lock) | 嘗試獲取 mutex。成功返回 1,失敗返回 0 |
int mutex_is_locked(struct mutex *lock) | 判斷 mutex 是否被上鎖了。是返回 1,否返回 0 |
void mutex_lock_interruptible(struct mutex *lock) | 加鎖,獲取 mutex。獲取失敗進入休眠後,依然能被訊號打斷。支援在中斷中使用。 |
12.7 完成量
12.7.1 完成量概念
完成量(completion)。
完成量用於一個執行單元等待另一個執行單元。
12.7.2 完成量操作
完成量操作:
- 定義完成量。
- 初始化完成量。
- 等待完成量。
- 喚醒完成量。
12.7.3 完成量 API
完成量結構體:
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
API | 描述 |
---|---|
void complete(struct completion *x) | 喚醒一個等待完成量 x 的執行緒 |
void complete_all(struct completion *x) | 喚醒所有等待完成量 x 的執行緒 |
void wait_for_completion(struct completion *x) | 等待一個完成量 x |
unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout) | 限時等待一個完成量 x |
void init_completion(struct completion *c) | 初始化一個完成量 |
void reinit_completion(struct completion *c) | 重新初始化一個完成量 |