6、設備驅動中的併發控制(一)

  在 Linux 設備驅動中必需要解決的一個問題是多個進程對共享資源的訪問,併發的訪問會致使競態。編程

6.1 併發與競態

  併發(Concurrency)指的是多個執行單元同時、並行的執行,而併發的執行單元對共享資源(硬件資源和軟件上的全局變量、靜態變量等)的訪問則很容易致使競態(Race Conditions)。安全

  在 Linux 內核中,競態主要發生於以下幾種狀況:架構

  • 對稱多處理器(SMP)的多個 CPU
    • SMP 是一種耦合的、共享存儲的系統模型,它的特色是多個 CPU 使用共同的系統總線,所以可訪問共同的外設和存儲器。
    • SMP 體系架構以下圖:
    • 在 SMP 的狀況下,兩個核的競態可能發生與 CPU0 的進程與 CPU1 的進程之間、CPU0 的進程與 CPU1 的中斷之間以及 CPU0 的中斷與 CPU1 的中斷之間,下圖中任何一條線鏈接的兩個實體都有核間併發可能性。
  • 單 CPU 內進程與搶佔它的進程
    • Linux 2.6 之後的內核支持內核搶佔調度,一個進程在內核執行的時候可能耗完了本身的時間片,也可能被另外一個高優先級進程打斷,進程與搶佔它的進程訪問共享資源的狀況相似於 SMP 的多個 CPU。
  • 中斷(硬中斷、軟中斷、Tasklet、底半部)與進程之間
    • 中斷能夠打斷正在執行的進程,若是中斷服務程序訪問進程正在訪問的資源,則競態會發生。
    • 中斷可悲新的更高優先級的中斷打斷,多箇中斷之間自己也可能引發併發而致使競態,但在 Linux 2.6.35 取消了中斷嵌套。

  解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問是指一個執行單元在訪問共享資源的時候,其餘的執行單元被禁止訪問。併發

  訪問共享資源的代碼區域稱爲臨界區,臨界區須要被以某種互斥機制加以保護。中斷屏蔽、原子操做、自旋鎖、信號量、互斥體等是 Linux 設備驅動中可採用的互斥途徑。函數

6.2 中斷屏蔽

  中斷屏蔽適用於單 CPU 範圍內的競態,即在進入臨界區以前屏蔽系統的中斷,可是在驅動編程中不值得推薦這麼作,驅動一般要考慮平臺特色而不假定本身在單核上運行。性能

  中斷屏蔽使得中斷與進程之間的併發再也不發生,並且因爲 Linux 內核的進程調度等操做都依賴中斷來實現,內核搶佔進程之間的併發也能夠避免了。測試

  中斷屏蔽使用的函數:優化

  

   

   

  注意:不建議單獨使用中斷屏蔽,它適合與自旋鎖配合使用。spa

6.3 原子操做

  原子操做可保證對一個整型數據的修改是排他性的。Linux 內核提供了一系列函數來實現內核中的原子操做,這些函數分爲兩類:分別針對位或整型進行原子操做。3d

  位和整型變量的原子操做都依賴於底層 CPU 的原子操做,這些函數都與 CPU 架構密切相關。

 6.3.1 整型原子操做

     

       

 6.3.2 位原子操做

    

     

       

 6.3.3 例子-使用原子操做

  

  原子變量初始化爲 1,當打開設備的時候,斷定原子變量減 1 以後,是否爲 0,爲 0 返回 TRUE(1),代表打開成功; 非0,返回 FALSE(0),表示當前設備正在打開狀態。

 6.4 自旋鎖

6.4.1 基本介紹

  自旋鎖(Spin lock) 是一種典型的對臨界資源進行互斥訪問的手段,其名稱來源於它的工做方式。爲了得到一個自旋鎖,在某 CPU 上執行的代碼需先執行一個原子操做,該操做測試並設置某個內存變量。因爲它是原子操做,因此在它的操做完成前其餘執行單元不可能訪問這個內存變量。若是測試結果代表鎖已經空閒,則程序得到這個自旋鎖並繼續執行;若是測試結果代表鎖仍被佔用,程序將在一個小的循環內重複這個「測試並設置」的操做,即進行所謂的「自旋」。當自旋鎖的持有者經過重置該變量釋放這個自旋鎖後,某個等待的「測試並設置」操做向其調用者報告鎖已釋放。

  Linux 中與自旋鎖相關的操做主要有如下四種:

 1 1.定義自旋鎖
 2 spinlock_t lock;
 3 2.初始化自旋鎖
 4 spin_lock_init(lock);    ///< 該宏用於動態初始化自旋鎖
 5 3.得到自旋鎖
 6 spin_lock(lock);        ///< 該宏用於得到自旋鎖 lock, 
 7                         ///< 若是可以當即得到, 它就立刻返回
 8                         ///< 不然, 它將在那裏自旋,直到該自旋鎖的保持者釋放
 9                         or
10 spin_trylock(lock);        ///< 該宏嘗試得到自旋鎖 lock
11                         ///< 若是可以當即得到, 它得到鎖並返回 true
12                         ///< 不然當即返回 false, 實際上再也不「原地打轉」
13 4.釋放自旋鎖
14 spin_unlock(lock);        ///< 必須與 spin_lock 和 spin_trylock 配對使用

  自旋鎖主要針對 SMP 或單 CPU 但內核可搶佔的狀況,對於單 CPU和內核不支持搶佔的狀況,自旋鎖退化爲空操做。

  自旋鎖和中斷結合使用:

1 spin_lock_irq() = spin_lock() + local_irq_disable();
2 spin_unlock_irq() = spin_unlock() + local_irq_enable();
3 spin_lock_irqsave() = spin_lock() + local_irq_save();
4 spin_unlock_irqrestore() = spin_unlock() + local_irq_restore();
5 spin_lock_bh() = spin_lock() + local_bh_disable();
6 spin_unlock_bh() = spin_unlock() + local_bh_enable();

  在多核編程中,若是進程和中斷可能訪問同一片臨界資源,通常須要在進程上下文調用上面的自旋鎖和中斷結合使用的函數,在中斷上下文再調用 spin_lock() 等函數,這是防止當前的核被其餘核的中斷打斷,防止核間併發。

  • 自旋鎖使用中須要注意的問題:
    • 自旋鎖其實是忙等鎖,當鎖不可用時,CPU 一直循環執行 「測試並設置」 該鎖直到可用而取得該鎖,CPU 在等待自旋鎖時不作任何有用的工做,僅僅是等待。所以,只有在佔用鎖的時間極短的時候,使用自旋鎖才合理。若臨界區很大,須要長時間佔用鎖,使用自旋鎖會下降系統的性能。
    • 自旋鎖可能致使系統死鎖。這種狀況通常都是遞歸使用一個自旋鎖。
    • 在自旋鎖鎖按期間不能調用可能引發進程調度的函數。
    • 在單核狀況下編程,也應該認爲本身的 CPU 是多核的。

   使用例子:

 1 /** 定義文件打開次數計數 */
 2 int xxx_count = 0;
 3 
 4 static int xxx_open(........)
 5 {
 6     ...
 7     spin_lock(&xxx_lock);
 8     if(xxx_count) {    /** 已經打開 */
 9         spin_unlock(&xxx_lock);
10         return -EBUSY;
11     }
12     xxx_count++;    ///< 增長使用計數
13     spin_unlock(&xxx_lock);
14     ...
15 
16     return 0;   ///< 成功
17 }
18 
19 static int xxx_release(.........)
20 {
21     spin_lock(&xxx_lock);
22     xxx_count--;    ///< 減小使用計數
23     spin_unlock(&xxx_lock);
24     return 0;
25 }

6.4.2 讀寫自旋鎖

   自旋鎖的衍生鎖讀寫自旋鎖(rwlock)可容許讀的併發。在寫操做方面,只能最多有 1 個寫進程,在讀操做方面,同時能夠有多個讀執行單元。固然,讀和寫不能同時進行。

 1 1.定義自旋鎖
 2 rwlock_t lock;
 3 2.初始化自旋鎖
 4 rwlock_init(lock);    ///< 該宏用於動態初始化自旋鎖
 5 3.讀鎖定
 6 void read_lock(rwlock_t *lock);
 7 void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
 8 void read_lock_irq(rwlock_t *lock);
 9 void read_lock_bh(rwlock_t *lock);
10 4.讀解鎖
11 void read_unlock(rwlock_t *lock);
12 void read_unlock_irqsave(rwlock_t *lock, unsigned long flags);
13 void read_unlock_irq(rwlock_t *lock);
14 void read_unlock_bh(rwlock_t *lock);
15 5.寫鎖定
16 void write_lock(rwlock_t *lock);
17 void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
18 void write_lock_irq(rwlock_t *lock);
19 void write_lock_bh(rwlock_t *lock);
20 void write_trylock(rwlock_t *lock);
21 6.寫解鎖
22 void write_unlock(rwlock_t *lock);
23 void write_unlock_irqsave(rwlock_t *lock, unsigned long flags);
24 void write_unlock_irq(rwlock_t *lock);
25 void write_unlock_bh(rwlock_t *lock);

6.4.3 順序鎖

  順序鎖(seqlock)是對讀寫鎖的一種優化,若使用順序鎖,讀執行單元不會被寫執行單元阻塞,即讀執行單元在寫執行單元對被順序鎖保護的共享資源進行寫操做的時候仍然能夠繼續讀,而沒必要等待寫執行單元完成寫操做,寫執行單元也不須要等待全部讀執行單元完成讀操做纔去進行寫操做。但,寫執行單元之間是互斥的。

  若讀執行單元在讀操做期間,寫執行單元已經發生了寫操做,那麼,讀執行單元必須從新讀取數據,以便確保獲得的數據是完整的。所以,在這種狀況下,讀端可能反覆讀屢次一樣的區域才能獲取到完整的數據。

  在內核中,寫執行單元涉及的順序操做以下:

 1 1.得到順序鎖
 2 void write_seqlock(seqlock_t *sl);
 3 void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags);
 4 void write_seqlock_bh(seqlock_t *sl);
 5 void write_seqlock_irq(seqlock_t *sl);
 6 2.釋放順序鎖
 7 void write_sequnlock(seqlock_t *sl);
 8 void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags);
 9 void write_sequnlock_bh(seqlock_t *sl);
10 void write_sequnlock_irq(seqlock_t *sl);

  讀執行單元涉及的操做以下:

1 1.讀開始
2 unsigned read_seqbegin(const seqlock_t *sl);
3 unsigned read_seqbegin_irqsave(const seqlock_t *sl, unsigned long flags); ///< 4.0 以後內核已捨棄
4 2.重讀
5 unsigned read_seqretry(const seqlock_t *sl, unsigned start);
6 unsigned read_seqretry_irqsave(const seqlock_t *sl, unsigned long flags); ///< 4.0 以後內核已捨棄

6.4.4 讀-複製-更新

   RCU(Read-Copy-Update,讀-複製-更新),它容許多個讀執行單元同時訪問被保護的數據,又容許多個讀執行單元和多個寫執行單元同時訪問被保護的數據。但RCU 不能替代讀寫鎖,對讀執行單元的

 

 11】讀鎖定
 2 void rcu_read_lock(void);
 3 void rcu_read_lock_bh(void);
 42】讀解鎖
 5 void rcu_read_unlock(void);
 6 void rcu_read_unlock_bh(void);
 7 /** 使用 RCU 進行讀模式 */
 8 rcu_read_lock();
 9 ... ///< 讀臨界區
10 rcu_read_unlock();
113】同步 RCU
12 /** 
13  *    此函數由 RCU 寫執行單元調用,它將阻塞寫執行單元,
14  *    直到 CPU 上全部的已經存在的讀執行單元完成讀臨界區,寫執行單元才能夠執行下一步 
15  *    此函數並不須要等待後續讀臨界區的完成
16  */
17 void synchronize_rcu(void);
184】掛接回調
19 /** 
20  *    此函數由 RCU 寫執行單元調用,它不阻塞寫執行單元,可在中斷上下文和軟中斷中使用
21  *    該函數把 func 掛接到 RCU 的回調函數鏈上,而後當即返回
22  *    掛接回調函數會在全部的已經存在的讀執行單元完成讀臨界區後被執行
23  */
24 void call_rcu(struct rcu_head *head, rcu_callback_t func);
25 
26 /** 給 RCU 保護的指針賦一個新值 */
27 #define rcu_assign_pointer(p, v);
28 
29 /**
30  *    讀端使用此宏獲取一個 RCU 保護的指針,以後既能夠安全的引用它
31  *     通常須要在 rcu_read_lock 和 rcu_read_unlock 保護的區間引用這個指針
32  */
33 #define rcu_dereference(p);
34 
35 /**
36  *    讀端使用此宏獲取一個 RCU 保護的指針,以後並不引用它
37  *     只關心指針的值,不關心指針指向的內容
38  *     例如可使用此宏判斷指針是否爲 NULL
39  */
40 #define rcu_access_pointer(p);
相關文章
相關標籤/搜索