深入淺出~Linux裝置驅動中的並發控制

來源:互聯網
上載者:User

標籤:

並發和競爭發生在兩類體系中:

  •     對稱式多處理器(SMP)的多個CPU
  •     核心可搶佔的單CPU系統

  訪問共用資源的代碼地區稱為臨界區critical sections,臨界區需要以某種互斥機制加以保護。在驅動程式中,當多個線程同時訪問相同的資源critical sections時(驅動程式中的全域變數是一種典型的共用資源),可能會引發"競態",因此我們必須對共用資源進行並發控制。Linux核心中解決並發控制的方法又中斷屏蔽、原子操作、自旋鎖、訊號量。(後面為主要方式)

 

中斷屏蔽:

  使用方法

local_irq_disable()  //屏蔽中斷...critical section        //臨界區...local_irq_enable()   //開中斷

  local_irq_disable/enable只能禁止/使能本CPU內的中斷,不能解決SMP多CPU引發的競態,故不推薦使用,其適宜於自旋鎖聯合使用。

 

原子操作:  

  原子操作是一系列的不能被打斷的操作。linux核心提供了一系列的函數來實現核心中的原子操作,這些函數分為2類,分別針對位和整型變數進行原子操作。

實現整型原子操作的步驟如下:

1.定義原子變數並設定變數值

void atomic_set(atomic_t *v , int i); //設定原子變數值為iatomic_t v = ATOMIC_INIT(0); //定義原子變數v,初始化為0

2.擷取原子變數的值

atomic_read(atomic_t *v);

3.原子變數加減操作

void atomic_add(int i,atomic_t *v);//原子變數加ivoid atomic_sub(int i ,atomic_t *v);//原子變數減i

4.原子變數自增/自減

void atomic_inc(atomic_t *v);//自增1void atomic_dec(atomic_t *v);//自減1

5.操作並測試:對原子變數執行自增、自減後(沒有加)測試其是否為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);

6.操作並返回

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)

 

下面來舉一個執行個體,是原子變數使用執行個體,使裝置只能被一個進程開啟:

static atomic_t xxx_available = ATOMIC_INIT(1);  // 定義原子變數 static int xxx_open(struct inode *inode, struct file *filp){    ...    if(!atomic_dec_and_test(&xxx_available))    {        atomic_inc(&xxx_availble);        return - EBUSY;  // 已經開啟    }    ...    return 0;  // 成功} static int xxx_release(struct inode *inode, struct file *filp){    atomic_inc(&xxx_available);  // 釋放裝置    return 0;}

  

我要著重談一下:

自旋鎖VS訊號量

  從嚴格意義上來說,訊號量和自旋鎖屬於不同層次的互斥手段,前者的實現依賴於後者,在多CPU中需要自旋鎖來互斥。訊號量是進程級的,用於多個進程之間對資源的互斥,雖然也在核心中,但是該核心執行路徑是以進程的身份,代表進程來爭奪資源的。如果競爭失敗,會切換到下個進程,而當前進程進入睡眠狀態,因此,當進程佔用資源時間較長時,用訊號量是較好的選擇。

       當所要保護的臨界訪問時間比較短時,用自旋鎖是非常方便的,因為它節省了環境切換的時間。但是CPU得不到自旋鎖是,CPU會原地打轉,直到其他執行單元解鎖為止,所以要求鎖不能在臨界區裡停留時間過長。

  自旋鎖的操作步驟:

1.定義自旋鎖spinlock_t lock;2.初始化自旋鎖spin_lock_init(lock);這是個宏,它用於動態初始化自旋鎖lock;3.獲得自旋鎖spin_lock(lock);該宏用於加鎖,如果能夠立即獲得鎖,它就能馬上返回,否則,他將自旋在那裡,直到該自旋鎖的保持者釋放。spin_trylock(lock);能夠獲得,則返回真,否則返回假,實際上是不在原地打轉而已。4.釋放自旋鎖spin_unlock(lock);

 

  自旋鎖持有期間核心的搶佔將被禁止。 自旋鎖可以保證臨界區不受別的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()

 注意:自旋鎖實際上是忙等待,只有在佔用鎖的時間極短的情況下,使用自旋鎖才是合理的自旋鎖可能導致死結:遞迴使用一個自旋鎖或進程獲得自旋鎖後阻塞。

例子:

spinlock_t lock;spin_lock_init(&lock);spin_lock(&lock);  //擷取自旋鎖,保護臨界區。。。。臨界區spin_unlock(&lock);//釋放自旋鎖

  自旋鎖不關心鎖定的臨界區究竟是如何執行的。不管是讀操作還是寫操作,實際上,對共用資源進行讀取的時候是應該可以允許多個執行單元同時訪問的,那麼這樣的話,自旋鎖就有了弊端。於是便衍生出來一個讀寫鎖。它保留了自旋的特性,但在對操作上面可以允許有多個單元進程同時操作。當然,讀和寫的時候不能同時進行。

  現在又有問題了,如果我第一個進程寫共用資源,第二個進程讀的話,一旦寫了,那麼就讀不到了,可能寫的東西比較多,但是第二個進程讀很小,那麼能不能第一個進程寫的同時,我第二個進程讀呢?
當然可以,那麼引出了順序鎖的概念。都是一樣的操作。

 

  讀寫自旋鎖(rwlock)允許讀的並發。在寫操作方面,只能最多有一個寫進程,在讀操作方面,同時可以有多個讀執行單元。當然,讀和寫也不能同時進行。

// 定義和初始化讀寫自旋鎖rwlock_t my_rwlock = RW_LOCK_UNLOCKED;  // 靜態初始化rwlock_t my_rwlock;rwlock)init(&my_rwlock);  // 動態初始化 // 讀鎖定:在對共用資源進行讀取之前,應先調用讀鎖定函數,完成之後調用讀解鎖函數void read_lock(rwlock_t *lock);void read_lock_irqsave(rwlock_t *lock, unsigned long flags);void read_lock_irq(rwlock_t *lock);void read_lock_bh(rwlock_t *lock); // 讀解鎖void read_unlock(rwlock_t *lock);void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);void read_unlock_irq(rwlock_t *lock);void read_unlock_bh(rwlock_t *lock); // 寫鎖定:在對共用資源進行寫之前,應先調用寫鎖定函數,完成之後調用寫解鎖函數void write_lock(rwlock_t *lock);void write_lock_irqsave(rwlock_t *lock, unsigned long flags);void write_lock_irq(rwlock_t *lock);void write_lock_bh(rwlock_t *lock);int write_trylock(rwlock_t *lock); // 寫解鎖void write_unlock(rwlock_t *lock);void write_unlock_irqsave(rwlock_t *lock, unsigned long flags);void write_unlock_irq(rwlock_t *lock);void write_unlock_bh(rwlock_t *lock);

  讀寫自旋鎖一般用法:

rwlock_t lock;  // 定義rwlockrwlock_init(&lock);  // 初始化rwlock // 讀時擷取鎖read_lock(&lock);...  // 臨界資源read_unlock(&lock); // 寫時擷取鎖write_lock_irqsave(&lock, flags);...  // 臨界資源write_unlock_irqrestore(&lock, flags);

  

順序鎖(seqlock):

  順序鎖是對讀寫鎖的一種最佳化,若使用順序鎖,讀與寫操作不阻塞,只阻塞同種操作,即讀與讀/寫與寫操作。

  寫執行單元的操作順序如下:

//獲得順序鎖void write_seqlock(seqlock_t *s1);int write_tryseqlock(seqlock_t *s1);write_seqlock_irqsave(lock, flags)write_seqlock_irq(lock)write_seqlock_bh(lock)//釋放順序鎖void write_sequnlock(seqlock_t *s1);write_sequnlock_irqrestore(lock, flags)write_sequnlock_irq(lock)write_sequnlock_bh(lock)

  讀執行單元的操作順序如下:

//讀開始unsinged read_seqbegin(const seqlock_t *s1);read_seqbegin_irqsave(lock, flags)//重讀,讀執行單元在訪問完被順序鎖s1保護的共用資源後需要調用該函數來檢查在讀操作器件是否有寫操作,如果有,讀執行單元需要從新讀一次。int reead_seqretry(const seqlock_t *s1, unsigned iv);read_seqretry_irqrestore(lock, iv, flags)

  

  RCU(Read-Copy Update 讀-拷貝-更新)可看作讀寫鎖的高效能版本,既允許多個讀執行單元同時訪問被保護的資料,又允許多個讀執行單元和多個寫執行單元同時訪問被保護的資料。但是RCU不能替代讀寫鎖。因為如果寫操作比較多時,對讀執行單元的效能提高不能彌補寫執行單元導致的損失。因為使用RCU時,寫執行單元之間的同步開銷會比較大,它需要延遲資料結構的釋放,複製被修改的資料結構,它也必須使用某種鎖機制同步並行的其他寫執行單元的修改操作。

  具體操作:略

 

訊號量的使用

    訊號量(semaphore)與自旋鎖相同,只有得到訊號量才能執行臨界區代碼,但,當擷取不到訊號量時,進程不會原地打轉而是進入休眠等待狀態。

相同點:只有得到訊號量的進程才能執行臨界區的代碼。(linux自旋鎖和訊號量鎖採用的都是“獲得鎖-訪問臨界區-釋放鎖”,可以稱為“互斥三部曲”,實際存在於幾乎所有多任務作業系統中)

不同點:當擷取不到訊號量時,進程不會原地打轉而是進入休眠等待狀態。

    訊號量的操作:

//訊號量的結構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的訊號量為1DECLARE_MUTEX_LOCKED(name) //初始化訊號量為0//常用操作DECLARE_MUTEX(mount_sem);down(&mount_sem); //擷取訊號量...critical section    //臨界區...up(&mount_sem);    //釋放訊號量

  訊號量用於同步時只能喚醒一個執行單元,而完成量(completion)用於同步時可以喚醒所有等待的執行單元。

 

 自旋鎖與互斥鎖的選擇

  • 當鎖 不能被擷取到時,使用訊號量的開銷是進程環境切換時間Tsw,使用自旋鎖的開始是等待擷取自旋鎖的時間Tcs,若Tcs比較小,則應使用自旋鎖,否則應使用訊號量
  • 訊號量鎖保護的臨界區可以包含引起阻塞的代碼,而自旋鎖則卻對要避免使用包含阻塞的臨界區代碼,否則很可能引發鎖陷阱
  • 訊號量存在於進程上下文,因此,如果被保護的共用資源需要在中斷或非強制中斷情況下使用,則在訊號量和自旋鎖之間只能選擇自旋鎖。當然,如果一定要使用訊號量,則只能通過down_trylock()方式進行,不能擷取就立即返回以避免阻塞。

 

  著作權,轉載請註明轉載地址:http://www.cnblogs.com/lihuidashen/p/4435979.html

深入淺出~Linux裝置驅動中的並發控制

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.