1. 程式人生 > >linux裝置驅動第五篇:驅動中的併發與竟態

linux裝置驅動第五篇:驅動中的併發與竟態

綜述

在上一篇介紹了linux驅動的除錯方法,這一篇介紹一下在驅動程式設計中會遇到的併發和竟態以及如何處理併發和競爭。

首先什麼是併發與竟態呢?併發(concurrency)指的是多個執行單元同時、並行被執行。而併發的執行單元對共享資源(硬體資源和軟體上的全域性、靜態變數)的訪問則容易導致競態(race conditions)。可能導致併發和竟態的情況有:

  • SMP(Symmetric Multi-Processing),對稱多處理結構。SMP是一種緊耦合、共享儲存的系統模型,它的特點是多個CPU使用共同的系統匯流排,因此可訪問共同的外設和儲存器。 
  • 中斷。中斷可 打斷正在執行的程序,若中斷處理程式訪問程序正在訪問的資源,則競態也會發生。中斷也可能被新的更高優先順序的中斷打斷,因此,多箇中斷之間也可能引起併發而導致競態。
  • 核心程序的搶佔。linux是可搶佔的,所以一個核心程序可能被另一個高優先順序的核心程序搶佔。如果兩個程序共同訪問共享資源,就會出現竟態。

以上三種情況只有SMP是真正意義上的並行,而其他都是巨集觀上的並行,微觀上的序列。但其都會引發對臨界共享區的競爭問題。而解決競態問題的途徑是保證對共享資源的互斥訪問,即一個執行單元在訪問共享資源的時候,其他的執行單元被禁止訪問。那麼linux核心中如何做到對對共享資源的互斥訪問呢?在linux驅動程式設計中,常用的解決併發與竟態的手段有訊號量與互斥鎖,Completions 機制,自旋鎖(spin lock),以及一些其他的不使用鎖的實現方式。下面一一介紹。

訊號量與互斥鎖

訊號量其實就是一個整型值,其核心是一個想進入臨界區的程序將在相關訊號量上呼叫 P; 如果訊號量的值大於零, 這個值遞減 1 並且程序繼續. 相反,,如果訊號量的值是 0 ( 或更小 ), 程序必須等待直到別人釋放訊號量. 解鎖一個訊號量通過呼叫 V 完成; 這個函式遞增訊號量的值,,並且, 如果需要, 喚醒等待的程序。而當訊號量的初始值為1的時候,就變成了互斥鎖。 訊號量的典型使用形式:
//宣告訊號量
struct semaphore sem;

//初始化訊號量
void sema_init(struct semaphore *sem, int val)
    //常用下面兩種形式
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
    //以下是初始化訊號量的快捷方式,最常用的
DECLARE_MUTEX(name)    //初始化name的訊號量為1
DECLARE_MUTEX_LOCKED(name) //初始化訊號量為0

//常用操作
DECLARE_MUTEX(mount_sem);
down(&mount_sem); //獲取訊號量
...
critical section    //臨界區
...
up(&mount_sem);    //釋放訊號量
常用的down操作還有
// 類似down(),因為down()而進入休眠的程序不能被訊號打斷,而因為down_interruptible()而進入休眠的程序能被訊號打斷, 
// 訊號也會導致該函式返回,此時返回值非0
int down_interruptible(struct semaphore *sem);
// 嘗試獲得訊號量sem,若立即獲得,它就獲得該訊號量並返回0,否則,返回非0.它不會導致呼叫者睡眠,可在中斷上下文使用
int down_trylock(struct semaphore *sem);

Completions 機制

完成量(completion)提供了一種比訊號量更好的同步機制,它用於一個執行單元等待另一個執行單元執行完某事。
</pre></div><div><pre name="code" class="cpp">// 定義完成量
struct completion my_completion;
 
// 初始化completion
init_completion(&my_completion);
 
// 定義和初始化快捷方式:
DECLEAR_COMPLETION(my_completion);
 
// 等待一個completion被喚醒
void wait_for_completion(struct completion *c);
 
// 喚醒完成量
void cmplete(struct completion *c);
void cmplete_all(struct completion *c);

自旋鎖

若一個程序要訪問臨界資源,測試鎖空閒,則程序獲得這個鎖並繼續執行;若測試結果表明鎖扔被佔用,程序將在一個小的迴圈內重複“測試並設定”操作,進行所謂的“自旋”,等待自旋鎖持有者釋放這個鎖。自旋鎖與互斥鎖類似,但是互斥鎖不能用在可能睡眠的程式碼中,而自旋鎖可以用在可睡眠的程式碼中,典型的應用是可以用在中斷處理函式中。自旋鎖的相關操作:
// 定義自旋鎖 
spinlock_t spin; 
 
// 初始化自旋鎖
spin_lock_init(lock);
 
// 獲得自旋鎖:若能立即獲得鎖,它獲得鎖並返回,否則,自旋,直到該鎖持有者釋放
spin_lock(lock); 
 
// 嘗試獲得自旋鎖:若能立即獲得鎖,它獲得並返回真,否則立即返回假,不再自旋
spin_trylock(lock); 
 
// 釋放自旋鎖: 與spin_lock(lock)和spin_trylock(lock)配對使用
spin_unlock(lock); 
 
  自旋鎖的使用:
// 定義一個自旋鎖
spinlock_t lock;
spin_lock_init(&lock);
 
spin_lock(&lock);  // 獲取自旋鎖,保護臨界區
...  // 臨界區
spin_unlock();  // 解鎖

自旋鎖持有期間核心的搶佔將被禁止。自旋鎖可以保證臨界區不受別的CPU和本CPU內的搶佔程序打擾,但是得到鎖的程式碼路徑在執行臨界區的時候還可能受到中斷和底半部(BH)的影響。為防止這種影響,需要用到自旋鎖的衍生:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

其他的一些選擇

以上是linux驅動程式設計中經常用到的鎖機制,下面講一些核心中其他的一些實現。

不加鎖演算法

有時, 你可以重新打造你的演算法來完全避免加鎖的需要.。許多讀者/寫者情況 -- 如果只有一個寫者 -- 常常能夠在這個方式下工作.。如果寫者小心使資料結構,由讀者所見的,是一直一致的,,有可能建立一個不加鎖的資料結構。在linux核心中就有一個通用的無鎖的環形緩衝實現,具體內容參考<linux/kfifo.h>。

原子變數與位操作

原子操作指的是在執行過程中不會被別的程式碼路徑所中斷的操作。原子變數與位操作都是原子操作。以下是其相關操作介紹。
// 設定原子變數的值
void atomic_set(atomic_t *v, int i);  // 設定原子變數的值為i
atomic_t v = ATOMIC_INIT(0);  // 定義原子變數v,並初始化為0
 
// 獲取原子變數的值
atomic_read(atomic_t *v);  // 返回原子變數的值
 
// 原子變數加/減
void atomic_add(int i, atomic_t *v);  // 原子變數加i
void atomic_sub(int i, atomic_t *v);  // 原子變數減i
 
// 原子變數自增/自減
void atomic_inc(atomic_t *v);  // 原子變數增加1
void atomic_dec(atomic_t *v);  // 原子變數減少1
 
// 操作並測試:對原子變數進行自增、自減和減操作後(沒有加)測試其是否為0,為0則返回true,否則返回false
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
 
// 操作並返回: 對原子變數進行加/減和自增/自減操作,並返回新的值
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
  位原子操作:
// 設定位
void set_bit(nr, void *addr);  // 設定addr地址的第nr位,即將位寫1
 
// 清除位
void clear_bit(nr, void *addr);  // 清除addr地址的第nr位,即將位寫0
 
// 改變位
void change_bit(nr, void *addr);  // 對addr地址的第nr位取反
 
// 測試位
test_bit(nr, void *addr); // 返回addr地址的第nr位
 
// 測試並操作:等同於執行test_bit(nr, void *addr)後再執行xxx_bit(nr, void *addr)
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);

seqlock(順序鎖)

使用seqlock鎖,讀執行單元不會被寫執行單元阻塞,即讀執行單元可以在寫執行單元對被seqlock鎖保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫執行單元完成寫操作,寫執行單元也不需要等待所有讀執行單元完成讀操作才去進行寫操作。寫執行單元之間仍是互斥的。若讀操作期間,發生了寫操作,必須重新讀取資料。seqlock鎖必須要求被保護的共享資源不含有指標。
// 獲得順序鎖
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh()
 
// 釋放順序鎖
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh()
 
// 寫執行單元使用順序鎖的模式如下:
write_seqlock(&seqlock_a);
...  // 寫操作程式碼塊
write_sequnlock(&seqlock_a);
  讀執行單元操作:
// 讀開始:返回順序鎖sl當前順序號
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags)
 
// 重讀:讀執行單元在訪問完被順序鎖sl保護的共享資源後需要呼叫該函式來檢查,在讀訪問期間是否有寫操作。若有寫操作,重讀
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
 
// 讀執行單元使用順序鎖的模式如下:
do{
    seqnum = read_seqbegin(&seqlock_a);
    // 讀操作程式碼塊 
    ...
}while(read_seqretry(&seqlock_a, seqnum));

讀取-拷貝-更新(RCU)

讀取-拷貝-更新(RCU) 是一個高階的互斥方法,在合適的時候可以取得非常高的效率。RCU可以看作讀寫鎖的高效能版本,相比讀寫鎖,RCU的優點在於既允許多個讀執行單元同時訪問被保護的資料,又允許多個讀執行單元和多個寫執行單元同時訪問被保護的資料。但是RCU不能替代讀寫鎖,因為如果寫比較多時,對讀執行單元的效能提高不能彌補寫執行單元導致的損失。由於平時應用較少,所以不做多說。

小結

以上就是linux驅動程式設計中涉及的併發與競態的內容,下面做一個簡單的小結。 現在的處理器基本上都是SMP型別的,而且在新的核心版本中,基本上都支援搶佔式的操作,在linux中很多程式都是可重入的,要保護這些資料,就得使用不同的鎖機制。而鎖機制的基本操作過程其實大同小異的,宣告變數,上鎖,執行臨界區程式碼,然後再解鎖。不同點在於,可以重入的限制不同,有的可以無限制重入,有的只允許異種操作重入,而有的是不允許重入操作的,有的可以在可睡眠程式碼中使用,有的不可以在可睡眠程式碼中使用。而在考慮不同的鎖機制的使用時,也要考慮CPU處理的效率問題,對於不同的程式碼長度,不同的程式碼執行時間,選擇一個好的鎖對CPU的良好使用有很大的影響,否則將造成浪費。  之前在linux裝置驅動第三篇:寫一個簡單的字元裝置驅動中介紹了簡單的字元裝置驅動,下一篇將介紹一些字元裝置驅動中得高階操作。

第一時間獲得部落格更新提醒,以及更多技術資訊分享,歡迎關注個人微信公眾平臺:程式設計師互動聯盟(coder_online),掃一掃下方二維碼或搜尋微訊號coder_online即可關注,閱讀android,chrome等多種熱門技術文章。