IMX6ULL學習--Linux併發與競爭及解決機制

併發與競爭

  • Linux 系統是個多任務操作系統,會存在多個任務同時訪問同一片內存區域,這些任務可能會相互覆蓋這段內存中的數據,造成內存數據混亂。
  • 產生原因:
    • 多線程併發訪問
    • 搶佔式併發訪問
    • 中斷程序併發訪問
    • SMP(多核)核間併發訪問
      因此要保護共享資源,阻止併發訪問,就要保證臨界區(共享數據段)是原子訪問的,即訪問是最基礎的訪問,不可拆分。

解決機制

爲了解決併發訪問對臨界區的破壞,提出了多種解決機制。針對不同的臨界區數據結構和併發情況,採取不同的措施。

原子操作

原子操作是指不能再進行拆分的操作,一般用於變量或者位操作。
原子操作API

  • 變量原子操作API
  • 位原子操作API

變量原子操作API

  • 原子整形定義
    定義原子操作整形數據。
typedef struct {
	int counter;
	} atomic_t;

如果要使用原子操作API,就必須先定義atomic_t的變量。

atomic_t b = ATOMIC_INIT(0); // 定義原子操作變量b 並賦值爲0
  • 原子變量操作API
函數 描述
ATOMIC_INIT(init 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,如果結果爲負就返回真,否則返回假
  • Demo
atomic_t v =ATOMIC_INIT(0);		/* 定義原子變量,初始化爲0 */
atomic_set(&v,10); 	/* 設置v = 10 */
atomic_read(&v); 	/* 讀取v的值,返回10 */
atomic_inc(&v);		/* v自加1 */

位原子操作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訪問臨界區時,禁止其他線程訪問臨界區。因此就有了加鎖機制,對臨界區上鎖,只有獲得鎖的線程可以訪問臨界區。
針對不同的情況,產生了多種加鎖機制。

自旋鎖

自旋鎖特徵

  • 特點
    當線程A獲得鎖訪問臨界區,在釋放鎖之前,線程B請求鎖不成功,只能一直請求鎖,不可以去處理其他事情,也不能休眠。直到A釋放鎖。
  • 缺點
    等待自旋鎖線程會一直自旋,浪費資源,所以自旋鎖持有時間不能過長,適用於輕量級加鎖。

自旋鎖使用

  • 定義自旋鎖
typedef struct spinlock {
	union {
			struct raw_spinlock rlock;
			#ifdef CONFIG_DEBUG_LOCK_ALLOC
			# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
			 struct {
					u8 __padding[LOCK_PADSIZE];
					struct lockdep_map dep_map;
		 		};
	 		#endif
		};
} 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函數,否則會導致死鎖現象。比如:線程A獲取鎖,禁止內核搶佔,如果此時A進入了休眠,A就放棄了CPU使用權,線程B獲取使用權開始運行,但是線程B也要獲取鎖,但是鎖未被A釋放,會一直處於自旋狀態,死鎖就產生了。

  • 中斷與自旋鎖
    中斷可以使用自旋鎖,但是在獲取鎖之前一定要禁止本地中斷,也就是CPU中斷。否則也可能導致死鎖。比如:線程A獲取鎖期間中斷髮生,中斷服務函數中也要獲取鎖,此時就會死鎖。因此在A獲取鎖之前要先禁止中斷,釋放鎖之後要恢復中斷狀態。
    相關API:
函數 描述
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。

  • Demo
DEFINE_SPINLOCK(lock) /* 定義並初始化一個鎖 */
 
 /* 線程 A */
void functionA (){
	unsigned long flags; /* 中斷狀態 */
	spin_lock_irqsave(&lock, flags) /* 獲取鎖 */
	/* 臨界區 */
	spin_unlock_irqrestore(&lock, flags) /* 釋放鎖 */
}

/* 中斷服務函數 */
void irq() {
	spin_lock(&lock) /* 獲取鎖 */
	 /* 臨界區 */
	spin_unlock(&lock) /* 釋放鎖 */
}

讀寫自旋鎖

讀寫鎖只需要保證讀和寫不同時發生就行,同一時間只能有一個人獲取寫操作權限,但是可以多人併發讀取。當某個數據結構符合讀/寫或者生產者/消費者模型的時候,就可以使用讀寫鎖。

  • 定義讀寫鎖
typedef struct {
 arch_rwlock_t raw_lock;
} rwlock_t;
  • 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) 打開下半部,並釋放讀鎖。

順序鎖

順序鎖在讀寫鎖的基礎上衍生而來的,使用讀寫鎖的時候讀操作和寫操作不能同時進行。使用順序鎖的話可以允許在寫的時候進行讀操作,也就是實現同時讀寫,但是不允許同時進行併發的寫操作。雖然順序鎖的讀和寫操作可以同時進行,但是如果在讀的過程中發生了寫操作,最好重新進行讀取,保證數據完整性。順序鎖保護的資源不能是指針,因爲如果在寫操作的時候可能會導致指針無效,而這個時候恰巧有讀操作訪問指針的話就可能導致意外發生,比如讀取野指針導致系統崩潰。

  • 定義順序鎖
typedef struct {
 	struct seqcount seqcount;
 	spinlock_t lock; 
 } seqlock_t;
  • API
函數 描述
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) 讀結束以後調用此函數檢查在讀的過程中有沒有對資源進行寫操作,如果有的話就要重讀

自旋鎖使用注意事項

  • 線程持有鎖時間不能太久,否則會降低CPU性能,如果時間較長建議使用其他併發處理方式。
  • 自旋鎖臨界區內不能調用會導致休眠的API,否則會死鎖。
  • 自旋鎖不能遞歸申請,否則會死鎖。
  • 編寫驅動時,考慮到驅動可移植性,將其當做多核SOC編寫驅動。

信號量

特點

  • 因爲信號量可以使等待資源線程進入休眠狀態,因此適用於那些佔用資源比較久的場合。
  • 因此信號量不能用於中斷中,因爲信號量會引起休眠,中斷不能休眠。
  • 如果共享資源的持有時間比較短,那就不適合使用信號量了,因爲頻繁的休眠、切換線程引起的開銷要遠大於信號量帶來的那點優勢。

使用

  • 定義
struct semaphore {
 	raw_spinlock_t lock;
 	unsigned int count;
 	struct list_head wait_list;
};
  • API
    |函數| 描述 |
    |–|--|
    |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)| 釋放信號量|

  • Demo

struct semaphore sem; /* 定義信號量 */
sema_init(&sem, 1); /* 初始化信號量 */
down(&sem); /* 申請信號量 */
/* 臨界區 */
up(&sem); /* 釋放信號量 */

互斥體

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章