【linux】驅動-12-併發與競態

李柱明發表於2021-06-20


前言

核心驅動的併發&竟態很容易理解,其解決方法也不能,看看例程就可以了。
對於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) 重新初始化一個完成量

相關文章