Linux設備驅動中的併發控制總結

併發(concurrency)指的是多個執行單元同時、並行被執行。而併發的執行單元對共享資源(硬件資源和軟件上的全局、靜態變量)的訪問則容易導致競態(race conditions)。
SMP是一種緊耦合、共享存儲的系統模型,它的特點是多個CPU使用共同的系統總線,因此可訪問共同的外設和存儲器。
進程與搶佔它的進程訪問共享資源的情況類似於SMP的多個CPU.
中斷可打斷正在執行的進程,若中斷處理程序訪問進程正在訪問的資源,則競態也會發生。中斷也可能被新的更高優先級的中斷打斷,因此,多箇中斷之間也可能引起併發而導致競態。上述併發的發生情況除了SMP是真正的並行以外,其他的都是“宏觀並行、微觀串行”的,但其引發的實質問題和SMP相似。解決競態問題的途徑是保證對共享資源的互斥訪問,即一個執行單元在訪問共享資源的時候,其他的執行單元被禁止訪問。
訪問共享資源的代碼區域成爲臨界區(critical sections),臨界區需要以某種互斥機制加以保護。中斷屏蔽、原子操作、自旋鎖和信號量等是Linux設備驅動中可採用的互斥途徑。

中斷屏蔽的使用方法爲:

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

在屏蔽了中斷後,當前的內核執行路徑應當儘快執行完臨界區代碼。上述兩個函數都只能禁止和使能本CPU內的中斷,不能解決SMP多CPU引發的競態。

local_irq_save(flags) 除禁止中斷的操作外,還保存目前CPU的中斷位信息;

local_irq_restore(flags) 進行的是local_irq_save(flags)相反的操作;

若只想禁止中斷的底半部,應使用local_bh_disable(), 使能被local_bh_disable()禁止的底半部應調用local_bh_enable()

原子操作指的是在執行過程中不會被別的代碼路徑所中斷的操作。

整型原子操作:

// 設置原子變量的值
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);

原子變量使用實例,使設備只能被一個進程打開:

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;
}

自旋鎖(spin lock)——“在原地打轉”。若一個進程要訪問臨界資源,測試鎖空閒,則進程獲得這個鎖並繼續執行;若測試結果表明鎖扔被佔用,進程將在一個小的循環內重複“測試並設置”操作,進行所謂的“自旋”,等待自旋鎖持有者釋放這個鎖。

自旋鎖的相關操作:

// 定義自旋鎖
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()

注意:自旋鎖實際上是忙等待,只有在佔用鎖的時間極短的情況下,使用自旋鎖纔是合理的

自旋鎖可能導致死鎖:遞歸使用一個自旋鎖或進程獲得自旋鎖後阻塞。

自旋鎖使用實例,使設備只能被最多一個進程打開:

int xxx_count = 0; // 定義文件打開次數計數
 
static int xxx_open(struct inode *inode, struct file *filp)
{
...
spinlock(&xxx_lock);
if(xxx_count); // 已經打開
{
spin_unlock(&xxx_lock);
return - EBUSY;
}
xxx_count++; // 增加使用計數
spin_unlock(&xxx_lock);
...
return 0; // 成功
}
 
static int xxx_release(struct inode *inode, struct file *filp)
{
...
spinlock(&xxx_lock);
xxx_count--; // 減少使用計數
spin_unlock(&xxx_lock);
return 0;
}

讀寫自旋鎖(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 *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(Read-Copy Update 讀-拷貝-更新)

RCU可看作讀寫鎖的高性能版本,既允許多個讀執行單元同時訪問被保護的數據,又允許多個讀執行單元和多個寫執行單元同時訪問被保護的數據

但是RCU不能替代讀寫鎖。因爲如果寫操作比較多時,對讀執行單元的性能提高不能彌補寫執行單元導致的損失。因爲使用RCU時,寫執行單元之間的同步開銷會比較大,它需要延遲數據結構的釋放,複製被修改的數據結構,它也必須使用某種鎖機制同步並行的其他寫執行單元的修改操作。

RCU操作:

// 讀鎖定
rcu_read_lock()
rcu_read_lock_bh()
 
// 讀解鎖
rcu_read_unlock()
rcu_read_unlock_bh()
 
// 使用RCU進行讀的模式如下:
rcu_read_lock()
... // 讀臨界區
rcu_read_unlock()

rcu_read_lock() 和rcu_read_unlock()實質是禁止和使能內核的搶佔調度:

#define rcu_read_lock() preempt_disable()
#define rcu_read_unlock() preempt_enable()
 

rcu_read_lock_bh()、rcu_read_unlock_bh()定義爲:

#define rcu_read_lock_bh() local_bh_disable()
#define rcu_read_unlock_bh() local_bh_enable()

同步RCU

synchronize_rcu()

由RCU寫執行單元調用,保證所有CPU都處理完正在運行的讀執行單元臨界區。

信號量的使用

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

信號量的操作:

// 定義信號量
struct semaphore sem;
// 初始化信號量:
// 初始化信號量,並設置sem的值爲val
void sema_init(struct semaphore *sem, int val);
// 初始化一個用於互斥的信號量,sem的值設置爲1。等同於sema_init(struct semaphore *sem, 1)
void init_MUTEX(struct semaphore *sem);
 
// 等同於sema_init(struct semaphore *sem, 0)
void init_MUTEX_LOCKED(struct semaphore *sem);
 
// 下面兩個宏是定義並初始化信號量的“快捷方式”:
DECLEAR_MUTEX(name)
DECLEAR_MUTEX_LOCKED(name)
// 獲得信號量:
// 用於獲得信號量,它會導致睡眠,不能在中斷上下文使用
void down(struct semaphore *sem);
// 類似down(),因爲down()而進入休眠的進程不能被信號打斷,而因爲down_interruptible()而進入休眠的進程能被信號打斷,
// 信號也會導致該函數返回,此時返回值非0
void down_interruptible(struct semaphore *sem);
// 嘗試獲得信號量sem,若立即獲得,它就獲得該信號量並返回0,否則,返回非0.它不會導致調用者睡眠,可在中斷上下文使用
int down_trylock(struct semaphore *sem);
// 使用down_interruptible()獲取信號量時,對返回值一般會進行檢查,若非0,通常立即返回-ERESTARTSYS,如:
if(down_interruptible(&sem))
{
return - ERESTARTSYS;
}
// 釋放信號量
// 釋放信號量sem, 喚醒等待者
void up(struct semaphore *sem);
// 信號量一般這樣被使用:
DECLARE_MUTEX(mount_sem);
down(&mount_sem); // 獲取信號量,保護臨界區
...
critical section // 臨界區
...
up(&mount_sem); // 釋放信號量
 

Linux自旋鎖和信號量鎖採用的“獲取鎖-訪問臨界區-釋放鎖”的方式存在於幾乎所有的多任務操作系統之中。

用信號量實現設備只能被一個進程打開的例子:

static DECLEAR_MUTEX(xxx_lock) // 定義互斥鎖
 
static int xxx_open(struct inode *inode, struct file *filp)
{
...
if(down_trylock(&xxx_lock)) // 獲得打開鎖
return - EBUSY; // 設備忙
...
return 0; // 成功
}
 
static int xxx_release(struct inode *inode, struct file *filp)
{
up(&xxx_lock); // 釋放打開鎖
return 0;
}

信號量用於同步

若信號量被初始化爲0,則它可以用於同步,同步意味着一個執行單元的繼續執行需等待另一執行單元完成某事,保證執行的先後順序。

完成量用於同步

完成量(completion)提供了一種比信號量更好的同步機制,它用於一個執行單元等待另一個執行單元執行完某事。

completion相關操作:

// 定義完成量
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);

自旋鎖和信號量的選擇

當鎖不能被獲取時,使用信號量的開銷是進程上下文切換時間Tsw,使用自旋鎖的開銷是等待獲取自旋鎖(由臨界區執行時間決定)Tcs,若Tcs較小,應使用自旋鎖,若Tcs較大,應使用信號量。

信號量保護的臨界區可包含可能引起阻塞的代碼,而自旋鎖則絕對要避免用來保護包含這樣代碼的臨界區。因爲阻塞意味着要進行進程切換,若進程被切換出去後,另一個進程企圖獲取本自旋鎖,死鎖就會發生。

信號量存在於進程上下文,因此,若被保護的共享資源需要在中斷或軟中斷情況下使用,則在信號量和自旋鎖之間只能選擇自旋鎖。若一定要使用信號量,則只能通過down_trylock()方式進行,不能獲取就立即返回避免阻塞。

讀寫信號量

讀寫信號量與信號量的關係與讀寫自旋鎖和自旋鎖的關係類似,讀寫信號量可能引起進程阻塞,但它可允許N個讀執行單元同事訪問共享資源,而最多只能有一個寫執行單元

讀寫自旋鎖的操作:

// 定義和初始化讀寫信號量
struct rw_semaphore my_res; // 定義
void init_rwsem(struct rw_semaphore *sem); // 初始化
 
// 讀信號量獲取
void down_read(struct rw_semaphore *sem);
void down_read_trylock(struct rw_semaphore *sem);
 
// 讀信號量釋放
void up_read(struct rw_semaphore *sem);
 
// 寫信號量獲取
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
 
// 寫信號量釋放
void up_write(struct rw_semaphore *sem);
 
// 讀寫信號量的使用:
rw_semaphore rw_sem; // 定義
init_rwsem(&rw_sem); // 初始化
 
// 讀時獲取信號量
down_read(&rw_sem);
... // 臨街資源
up_read(&rw_sem);
 
// 寫時獲取信號量
down_write(&rw_sem);
... // 臨界資源
up_writer(&rw_sem);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章