linux裝置驅動中的併發控制

時光漫步LH發表於2015-05-01

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

  • 對稱多處理器(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); //設定原子變數值為i
atomic_t v = ATOMIC_INIT(0); //定義原子變數v,初始化為0

2.獲取原子變數的值

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()方式進行,不能獲取就立即返回以避免阻塞。

相關文章