2021-08-03
關鍵字:競態解決方案、同步
驅動開發中共有四種方式可以解決併發競態問題:
1、原子變量;
2、自旋鎖;
3、信號量;
4、完成量;
原子變量的功能是通過硬件來操作變量的值,使該變量的值在更替過程中是原子式的,解決了在內核中因調度導致某變量在變值中途被打斷從而影響到最終結果的情況。
自旋鎖是一種區域保護型的鎖。原子變量只能保護一個整型變量,但自旋鎖卻可以保護一片區域。自旋鎖是一種“忙等待”鎖,當條件不滿足時會在原地打轉而不會交出CPU執行權。
信號量的功能與自旋鎖一樣,都是區域保護型鎖。所不同的是信號量並非“忙等待”型鎖。拿不到鎖資源的線程不會原地打轉,而是將CPU執行權讓度出去並進入休眠狀態。適用於耗時較長的場景。
完成量與信號量的功能或說用途是一樣的。只不過完成量的效率會比信號量高。如果有需要在多線程之間控制執行順序的需求,優先選擇完成量而不是信號量。
1、原子變量
原子變量的定義在以下頭文件中:
kernel/include/linux/types.h
原子變量的類型定義如下所示:
typedef struct { int counter; } atomic_t;
需要強調的是:雖然原子類型定義看起來與普通結構體無異,但必須通過 atomic.h 中提供的接口來實現增加、減少及查詢需求。
原子變量相關的接口如下所示:
1、初始化原子變量。
atomic_t counter = ATOMIC_INIT(0);
原子變量在聲明的時候必須初始化。且初始化宏不能在函數中執行。初始化宏中的參數即是想初始化成的整數值。
2、增加。
static inline void atomic_inc(atomic_t* atm); static inline void atomic_add(int val, atomic_t* atm);
第一個函數表示指定原子變量自增1個值。
第二個函數則是增加指定的值。
還可以使用宏來改變原子變量的值:
#define atomic_set(atm, val)
這裏的宏參數要傳原子變量的地址。
3、減少。
static inline void atomic_dec(atomic_t* atm); static inline void atomic_sub(int val, atomic_t* atm);
與增加類似,分別用於自減及減去指定值。
同樣可以用宏來改變值:
#define atomic_set(atm, val)
參數要傳原子變量的地址。
4、查詢。
#define atomic_read(atm)
參數要傳原子變量的地址。
2、自旋鎖
自旋鎖因爲是“忙等待”型鎖,當在加鎖時因此鎖正被其它線程使用的話會一直在原地打轉,因此在一定程度上會“浪費”CPU的執行權,所以程序在佔用自旋鎖時不宜佔用過久。如果確實有需要長時間佔用鎖資源的應選擇“信號量”鎖替代。
自旋鎖相關的原型定義位於以下所示頭文件中:
kernel/include/linux/spinlock.h
kernel/include/linux/spinlock_types.h
自旋鎖的類型定義如下:
typedef struct spinlock {
...
} spinlock_t;
自旋鎖的使用流程也比較簡單,主要步驟如下所示:
1、定義自旋鎖變量;
2、初始化自旋鎖變量;
3、加鎖;
4、執行需要被保護的代碼;
5、解鎖;
6、結束;
上述步驟涉及到的接口如下所示:
//step 1 spinlock_t mylock; //step 2 spin_lock_init(&mylock); //step 3 spin_lock(&mylock); //step 4 code block //step 5 spin_unlock(&mylock);
3、信號量
信號量與自旋鎖最大的區別就是信號量在拿不到鎖時有可能會進入睡眠狀態,因此信號量不能用在中斷處理程序中。另外,線程的睡眠與喚醒是比較消耗計算資源的,只有在區域需要被保護的時間足夠長時才宜選擇信號量,否則頻繁的睡眠上下文切換會更耗資源,此種情況下自旋鎖會更好一點。
信號量相關的類型定義位於下述頭文件中:
kernel/include/linux/semaphore.h
內核信號量類型原型如下所示:
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };
lock變量無須理會,調用接口改變信號量鎖狀態時系統會自行更改其狀態。
count變量的值有三種狀態:
1、爲0時表示此信號量正被其它線程使用,但是wait_list爲空;
2、小於0時與第1種狀態類型,不過此時 wait_list 中有值;
3、大於0時表示此信號量當前爲空閒狀態。
根據count變量值的不同可以將信號量分爲兩種:
1、二值信號量;
2、計數信號量;
二值信號量是將count值初始化爲1時。計數信號量則是將count值初始化爲大於1的值,說白了就是同時允許count個線程訪問此鎖。
信號量的使用方式與自旋鎖並無不同,定義變量、初始化、加鎖解鎖。
//聲明變量 struct semaphore sema; //初始化 static inline void sema_init(struct semaphore* sem, int val); //或者可用宏來初始化。初始化爲空閒狀態 #define init_MUTEX(sem) //初始化爲鎖定狀態 #define init_MUTEX_LOCKED(sem) //加鎖 void down(struct semaphore* sem); int down_interruptible(strut semaphore* sem); //此函數可以在中斷上下文中使用 //解鎖 void up(struct semaphore* sem);
如果加鎖的時候信號量無空閒資源,則調用加鎖的線程可能會進入睡眠狀態。
4、完成量
完成量的定義位於以下文件中:
kernel/include/linux/completion.h
完成量用以下結構體來描述:
struct completion { unsigned int done; wait_queue_head_t wait; };
成員done表示此完成量是否可用。當完成量的值爲0時表示此完成量正被其它線程佔用。
成員wait用於存放等待該完成量的進程信息。通常此鏈表中的進程都處於睡眠狀態。
與信號量類似,完成量的使用也分以下幾個步驟:
1、聲明變量;
2、初始化;
3、申請完成量;
4、釋放完成量;
步驟對應的函數接口如下:
//step 1 struct completion cpl; //step 2 static inline void init_completion(struct completion* x); #define DECLARE_COMPLETION(work) struct completion work = COMPLETION_INITIALIZER(work) //step 3 void __sched wait_for_completion(struct completion* x); //step 4 void complete(struct completion* x); void complete_all(struct completion* x);
如果申請完成量時被申請的完成量中done成員的值爲0,則調用申請完成量的線程可能會進入睡眠狀態。