蛻變成蝶~Linux設備驅動中的併發控制

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

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

  訪問共享資源的代碼區域稱爲臨界區critical sections,臨界區須要以某種互斥機制加以保護。在驅動程序中,當多個線程同時訪問相同的資源critical sections時(驅動程序中的全局變量是一種典型的共享資源),可能會引起"競態",所以咱們必須對共享資源進行併發控制。Linux內核中解決併發控制的方法又中斷屏蔽、原子操做、自旋鎖、信號量。(後面爲主要方式)node

 

中斷屏蔽: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); //設置原子變量值爲i
atomic_t v = ATOMIC_INIT(0); //定義原子變量v,初始化爲0

2.獲取原子變量的值
ui

atomic_read(atomic_t *v);

3.原子變量加減操做

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

4.原子變量自增/自減

void atomic_inc(atomic_t *v);//自增1
void 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;  // 定義rwlock
rwlock_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的信號量爲1
DECLARE_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

相關文章
相關標籤/搜索