01-內核的互斥與同步概述

 本系列文章主要講述內核中的互斥與同步操作,主要包括內核中的鎖機制,信號量和互斥體,講述了基礎概念和常用的API函數接口和代碼示例,詳細目錄如下:
01 - 內核中的互斥與同步概述
02 - 原子變量應用示例
03 - 自旋鎖應用示例
04 - 信號量的應用示例
05 - 互斥量的應用示例


本文摘錄自《嵌入式Linux驅動開發教程》一書。

1. 原子變量

 如果一個變量的操作是原子性的,即不能再被分割,類似於在彙編代碼也只要一條指令就能完成那麼對於這樣的變量就根本不需要考慮併發帶來的影響。

typedef struct {
	int counter;
} atomic_t;

 由上可知,原子變量其實是一個整型變量。需要說明的是對於非整型變量不能使用原子變量來操作。但是在能夠使用原子變量時就應該儘量使用原子變量而不要使用複雜的鎖機制,因爲相比於鎖機制他的開銷小。主要API接口如下:

int atomic_read(const atomic_t * v)				// 讀取原子變量的值
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
int atomic_add_return(int i, atomic_t * v)		// 加 _return 表示要返回修改後的值
int atomic_sub_return(int i, atomic_t * v)		
int atomic_add_negative(int i, atomic_t * v)	// 加 _negative 表示當結果爲負時返回真

void atomic_inc(atomic_t * v)					// 自加1
void atomic_dec(atomic_t * addr)				// 自減1
int atomic_inc_and_test(atomic_t * v)			// 加 _test 表示當結果爲0時返回真
int atomic_dec_and_test(atomic_t * v)
int atomic_sub_and_test(i, v)

void atomic_xchg(atomic_t * v, int new)			// 交換數據

2. 自旋鎖

 自旋鎖:在訪問共享資源之前,首先要獲得自旋鎖,訪問共享資源之後解鎖,其他內核路徑如果沒有競爭到鎖,只能忙等待,所以自旋鎖是一種忙等鎖,最多隻能被一個線程持有。
 內核中自旋鎖的類型是 spinlock_t,相關的API如下:

void spin_lock_init(spinlock_t * lock)		// 初始化自旋鎖,在使用自旋鎖之前必須初始化

void spin_lock(spinlock_t * lock)			// 獲取自旋鎖,如果不能獲取自旋鎖則進行忙等待
void spin_lock_irq(spinlock_t * lock)		// 獲取自旋鎖並禁止中斷
void spin_lock_irqsave(spinlock_t * lock, unsigned long flags)	// 獲取自旋鎖並禁止中斷,保存中斷屏蔽狀態到flags中
void spin_lock_bh(spinlock_t * lock)		// 獲取自旋鎖並禁止下半部

int spin_trylock(spinlock_t * lock)			// 嘗試獲取自旋鎖(非阻塞獲取自旋鎖),即使不能獲取也立即返回,返回值爲0表示成功獲取自旋鎖否則表示沒有獲得自旋鎖
int spin_trylock_bh(spinlock_t * lock)
int spin_trylock_irq(spinlock_t * lock)

void spin_unlock(spinlock_t * lock)			// 釋放自旋鎖
void spin_unlock_irq(spinlock_t * lock)		// 釋放自旋鎖並激活中斷
void spin_unlock_bh(spinlock_t * lock)		
void spin_unlock_irqrestore(spinlock_t * lock, unsigned long flags)	// 釋放自旋鎖並讓本地中斷恢復到之前狀態

3. 讀寫鎖 (讀共享,寫獨佔)

 在併發的方式中有讀——讀併發,讀——寫併發和寫——寫併發三種,顯然一般的讀操作並不會修改它的值,因此讀和讀之間是允許併發的。但是使用自旋鎖讀操作也會被加鎖從而阻止了另外一個讀操作,爲了提高併發的效率必須要提高鎖的粒度以允許讀和讀之間的併發。因此內核中提供了一種允許讀和讀併發的鎖,叫讀寫鎖,其數據類型是 rwlock_t,常用的API如下:

rwlock_init(lock)				// 初始化自旋鎖,在使用自旋鎖之前必須初始化

read_lock(lock)					// 獲取讀鎖
write_lock(lock)				// 獲取寫鎖

read_lock_irq(lock)				// 獲取讀鎖並關閉中斷
read_lock_irqsave(lock, flags)	// 獲取讀鎖並禁止中斷,保存中斷屏蔽狀態到flags中
read_lock_bh(lock)

write_lock_irq(lock)
write_lock_irqsave(lock, flags)
write_lock_bh(lock)

read_unlock(lock) 
write_unlock(lock)

read_unlock_irq(lock)
read_unlock_irqrestore(lock, flags)	// 讀解鎖並恢復中斷
read_unlock_bh(lock)

write_unlock_irq(lock)
write_unlock_irqrestore(lock, flags)
write_unlock_bh(lock)

 讀寫鎖的使用也需經歷定義、初始化、加鎖和解鎖的過程。當一個內核路徑在獲取變量的值時,如果另一條路徑也要獲取變量的值,則讀鎖可以正常獲得,從而另一條路徑也能獲取變量的值;但如果有一個寫正在進行,則不管是讀鎖還是寫鎖都不能正常獲得只有當寫鎖釋放後纔可以。
 注意:讀寫鎖需要比spinlocks更多的訪問原子內存操作,如果讀臨界區不是很大,最好別使用讀寫鎖。讀寫鎖比較適合鏈表等數據結構,特別是查找遠多於修改的情況。RCU比讀寫鎖更適合遍歷list,但需要更關注細節。目前kernel社區正在努力用RCU代替讀寫鎖。

4. 順序鎖

 自旋鎖不允許讀和讀之間的併發,讀寫鎖則更進了一步,允許讀和讀之間的併發,順序鎖則又更進了一步允許讀和寫之間的併發。
 爲了實現這一需求,順序鎖在讀時不上鎖,也就意味着在讀的期間允許寫,但是在讀之前先讀取一個順序值,讀操作完成後再次讀取順序值,如果兩者相等說明在讀的過程中沒有發生寫操作否則要重新讀。顯然寫操作要上鎖並要更新順序值。
 順序所特別適合讀很多而寫比較少的場合,否則由於反覆的讀操作也不一定能夠獲取較高的效率。順序鎖的數據類型是 seqlock_t,其類型定義如下:

typedef struct {
	struct seqcount seqcount;
	spinlock_t lock;
} seqlock_t;

 顯然順序鎖使用了自旋鎖的機制,並且有一個順序值 seqcount,主要API如下:

seqlock_init(x)		// 初始化順序鎖
unsigned read_seqbegin(const seqlock_t * sl)	// 讀之前獲取順序值,函數返回順序值
unsigned read_seqretry(const seqlock_t * sl, unsigned start)	// 讀之後驗證順序值是否變化,返

void write_seqlock(seqlock_t * sl)		// 寫之前加鎖
void write_seqlock_irq(seqlock_t * sl)
void write_seqlock_irqsave(lock, flags)
void write_seqlock_bh(seqlock_t * sl)

void write_sequnlock(seqlock_t * sl)	// 寫之後解鎖
void write_sequnlock_irq(seqlock_t * sl)
void write_sequnlock_irqrestore(seqlock_t * sl, unsigned long flags)
void write_sequnlock_bh(seqlock_t * sl)

示例:
int i = 5;
unsigned long flags;

seqlock_t lock;				/* 定義一個順序鎖 */
seqlock_init(&lock);		/* 初始化順序鎖 */

int v;
unsigned start;
do
{
	start = read_seqbegin(&lock);			/* 讀之前獲得順序值 */
	v = i;
} while ( read_seqretry(&lock, start) );	/* 讀完之後檢查順序值是否發生變化,變化則要重讀 */

write_seqlock_irqsave(&lock, flags);	/* 寫之前獲取順序鎖              */
i++;
write_sequnlock_irqrestore(&lock, flags);	/* 寫之後釋放順序鎖         */

5. 信號量

 前面討論的鎖機制都有一個限制,那就是在鎖獲得期間不能調用調度器,即不能引起進程切換。但是內核中很多函數都可能會觸發對調度器的調用,這給驅動開發帶來了一定的麻煩。另外我們也知道對於忙等鎖來說,當臨界的代碼執行的比較長的時候會降低系統的效率,爲此內核中專門提供了一種叫信號量的機制來取消這一限制,他的數據類型定義如下:

struct semaphore {
	raw_spinlock_t		lock;
	unsigned int		count;
	struct list_head	wait_list;
};

 可以看到,他有一個 count 成員,是用來記錄信號量資源情況的,當count的值不爲0時是可以獲得信號量的,當count的值爲0時信號量就不能被獲取,這也說明信號量可以同時被多個進程所持有。我們還看到一個wait_list成員,不難猜想當信號量不能獲取時當前的進程就應該休眠了。最後lock成員在提示我們信號量在底層其實是使用了自旋鎖的機制。常用的API接口如下:

void sema_init(struct semaphore * sem, int val)			// 初始化信號量,val是給count成員的初值這樣就有val個進程可以同時獲得信號量
void down(struct semaphore * sem)						// 獲取信號量(count值減1),當信號量的值不爲0時,可以立即獲取信號量,否則進程休眠
int down_interruptible(struct semaphore * sem)			// 同down,但是能被信號喚醒
int down_trylock(struct semaphore * sem)				// 非阻塞獲取信號量,不能獲取立即返回,返回0表示成功獲取,返回1表示獲取失敗
int down_timeout(struct semaphore * sem, long timeout)	// 同down,但是在timeout個時鐘週期內如果還沒有獲取信號量則超時返回。返回0表示成功獲取信號量,返回負值表示超時
void up(struct semaphore * sem)							// 釋放信號量(count加1),如果有進程等待信號量則進程被喚醒

 1. 信號量可以被多個進程所持有,當給信號量賦初值1時,信號量成爲二值信號量,也成爲互斥信號量。
 2. 如果不能獲取信號量,則進程休眠,調度其他的進程執行不會忙等待。
 3. 信號量的獲取可能會引起進程切換,不能用在中斷上下文中。
 4. 信號量開銷比較大,在不違背自旋鎖的使用規則下應該優先使用自旋鎖。

 相比於自旋鎖,信號量可以使線程進入休眠狀態,比如 A 與 B、C 合租了一套房子,這個房子只有一個廁所,一次只能一個人使用。某一天早上 A 去上廁所了,過了一會 B 也想用廁所,因爲 A 在廁所裏面,所以 B 只能等到 A 用來了才能進去。B 要麼就一直在廁所門口等着,等 A 出來,這個時候就相當於自旋鎖。B 也可以告訴 A,讓 A 出來以後通知他一下,然後 B 繼續回房間睡覺,這個時候相當於信號量。可以看出,使用信號量會提高處理器的使用效率,畢竟不用一直傻乎乎的在那裏“自選”等待。但是,信號量的開銷要比自旋鎖大,因爲信號量使線程進入休眠狀態以後會切換線程,切換線程就會有開銷。總結一下信號量的特點:
 ①、因爲信號量可以使等待資源線程進入休眠狀態,因此適用於那些佔用資源比較久的場合。
 ②、因此信號量不能用於中斷中,因爲信號量會引起休眠,中斷不能休眠。
 ③、如果共享資源的持有時間比較短,那就不適合使用信號量了,因爲頻繁的休眠、切換線程引起的開銷要遠大於信號量帶來的那點優勢。

 信號量的使用如下所示:

struct semaphore sem; /* 定義信號量 */
sema_init(&sem, 1)/* 初始化信號量 */

down(&sem); /* 申請信號量 */
/* 臨界區 */
up(&sem); /* 釋放信號量 */

6. 互斥量

 信號量除了不能用於中斷的上下文,還有一個缺點就是不是很智能。在獲取信號量的代碼中只要信號量的值爲0,進程就馬上休眠了。但是更一般的情況是,在不用等待很長的時間後信號量馬上就可以獲得,那麼信號量的操作就要經歷使進程先休眠再被喚醒的一個漫長過程。可以在信號量不能獲取的時候稍微耐心等待一小段時間,如果在這段時間能夠獲取信號量,那麼獲取信號量的操作就可以立即返回,否則再將進程休眠也不遲。
 爲了實現這種比較智能化的信號量,內核提供了另外一種專門用於互斥的高效率信號量,也就是互斥量,也叫互斥體,類型爲 struct mutex,相關的API如下:

mutex_init(mutex)						// 初始化互斥量
void mutex_lock(struct mutex * lock)	// 獲取互斥量
int mutex_lock_interruptible(struct mutex * lock)	
int mutex_trylock(struct mutex * lock)	// 非阻塞獲取互斥量,返回1表示成功
void mutex_unlock(struct mutex * lock)	// 釋放互斥量

在使用 mutex 之前要先定義一個 mutex 變量。在使用 mutex 的時候要注意如下幾點:
 ①、mutex 可以導致休眠,因此不能在中斷中使用 mutex,中斷中只能使用自旋鎖。
 ②、和信號量一樣,mutex 保護的臨界區可以調用引起阻塞的 API 函數。
 ③、因爲一次只有一個線程可以持有 mutex,因此,必須由 mutex 的持有者釋放 mutex。並且 mutex 不能遞歸上鎖和解鎖。

7. RCU機制

 RCU(Read-Copy Update)機制即 讀——複製——更新。RCU機制對共享內存的訪問機制是通過指針來進行性的。讀者通過對該指針解引用來獲取想要的數據;寫着在發起寫訪問操作的時候,並不是去寫以前共享資源內存而是另起爐竈,重新分配一片內存空間,複製以前的數據到新開闢的內存空間,然後修改新分配
的內存空間裏面的內容;當寫結束後,等待所有的讀者完成了對原有內存空間的讀取後,將讀的指針更新
指向新的內存空間,之後的讀操作將會得到更新後的數據。
 常用API接口如下:

void rcu_read_lock(void)	// 讀者進入臨界區
rcu_dereference(p)			// 讀者用於獲取共享資源的內存區指針
void rcu_read_unlock(void)	// 讀者退出臨界區
rcu_assign_pointer(p, v)	// 用新指針更新老指針
void synchronize_rcu(void)	// 等待之前的讀者完成讀操作

8. 完成量

 同步是指內核中的執行路徑要按照一定的順序來進行。同步可以用信號量來表示,例如對於一個ADC設備來說,可以先初始化一個值爲0的信號量,做轉換操作的執行路徑先用down來獲取這個信號量,如果在這之前沒有採集到數據那麼做轉換的操作就會休眠,當採集數據完成之後調用up釋放信號量那麼做轉換的操作將會被喚醒,這就保證了採樣和轉換的同步。
 內核中轉成提供了一個完成量來實現該操作,完成量的結構類型定義如下:

struct completion {
	unsigned int done;
	wait_queue_head_t wait;
};

 變量done表示是否完成的狀態,是一個計數值,爲0表示未完成,wait是一個等待隊列頭。當done爲0時進程阻塞,當內核中的其他路徑使done的值大於0時,負責喚醒被阻塞在這個完成量上的進程。主要API如下:

void init_completion(struct completion * x)
void wait_for_completion(struct completion * x)
int wait_for_completion_interruptible(struct completion * x)
unsigned long wait_for_completion_timeout(struct completion * x, unsigned long timeout)
bool try_wait_for_completion(struct completion * x)
void complete(struct completion * x)
void complete_all(struct completion * x)
發佈了57 篇原創文章 · 獲贊 65 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章