Linux 設備驅動中必須解決的一個問題是多個進程對共享資源的并發訪問,并發的訪問會導致競態。
中斷屏蔽、原子操作、自旋鎖和信號量都是解決并發問題的機制。中斷屏蔽很少單獨被使用,原子操作只能針對整數進行,因此自旋鎖和信號量應用最為廣泛。
自旋鎖會導致死循環,鎖定期間不允許阻塞,因此要求鎖定的臨界區小。信號量允許臨界區阻塞,可以適用于臨界區大的情況。
讀寫自旋鎖和讀寫信號量分別是放寬了條件的自旋鎖和信號量,它們允許多個執行單元對共享資源的并發讀。
中斷屏蔽
訪問共享資源的代碼區域稱為臨界區( critical sections),在單 CPU 范圍內避免競態的一種簡單而省事的方法是在進入臨界區之前屏蔽系統的中斷。中斷屏蔽將使得中斷與進程之間的并發不再發生,而且,由于 Linux 內核的進程調度等操作都依賴中斷來實現,內核搶占進程之間的并發也得以避免了。
local_irq_disable(); /* 屏蔽中斷 */ ... critical section /* 臨界區*/ ... local_irq_enable(); /* 開中斷 */
但是由于 Linux 的異步 I/O、進程調度等很多重要操作都依賴于中斷,長時間屏蔽中斷是很危險的;而且中斷屏蔽只對本 CPU 內的中斷有效,因此也并不能解決 SMP 多 CPU 引發的競態。在實際應用中并不推薦直接使用,適宜與下文的自旋鎖結合使用。
原子操作
Linux 內核提供了一系列函數來實現內核中的原子操作,這些函數又分為兩類,分別針對位和整型變量進行原子操作。它們的共同點是在任何情況下操作都是原子的,內核代碼可以安全地調用它們而不被打斷。
整型原子操作
設置原子變量的值
#include void atomic_set(atomic_t *v, int i); /* 設置原子變量的值為 i */atomic_t v = ATOMIC_INIT(0); /* 定義原子變量 v 并初始化為 0 */
獲取原子變量的值
int 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, return true */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);/* 操作完結果 <0, return true */int atomic_add_negative(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);
位原子操作
位原子操作相當快,一般只需一個機器指令,不需關中斷。
set/clear/toggle
#include /* 更改指針addr所指數據的第nr位 */void set_bit(nr, void *addr);void clear_bit(nr, void *addr);void change_bit(nr, void *addr);
test
int test_bit(nr, void *addr); /* 返回第nr位 */
測試并操作
/* 操作第nr位,并返回操作前的值 */int test_and_set_bit(nr, void *addr);int test_and_clear_bit(nr, void *addr);int test_and_change_bit(nr, void *addr);
自旋鎖(spinlock)
自旋鎖(spinlock)是一種典型的對臨界資源進行互斥訪問的手段,其名稱來源于它的工作方式。為了獲得一個自旋鎖, 在某 CPU 上運行的代碼需先執行一個原子操作,該操作測試并設置( test-and-set) 某個內存變量,由于它是原子操作,所以在該操作完成之前其他執行單元不可能訪問這個內存變量。如果測試結果表明鎖已經空閑,則程序獲得這個自旋鎖并繼續執行; 如果測試結果表明鎖仍被占用,程序將在一個小的循環內重復這個“ 測試并設置” 操作,即進行所謂的“ 自旋”,通俗地說就是“在原地打轉”。 當自旋鎖的持有者通過重置該變量釋放這個自旋鎖后,某個等待的“測試并設置” 操作向其調用者報告鎖已釋放。
Basic
定義/初始化
#include /* 靜態初始化 */spinlock_t my_lock = SPIN_LOCK_UNLOCKED;/* 動態初始化 */void spin_lock_init(spinlock_t *lock);
獲取/釋放
/* 基本操作 */void spin_lock(spinlock_t *lock);void spin_unlock(spinlock_t *lock);/* 保存中斷狀態并關閉 == spin_lock() + local_irq_save() */void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);void spin_unlock_irqsave(spinlock_t *lock, unsigned long flags);/* 忽略操作前中斷狀態 */void spin_lock_irq(spinlock_t *lock);void spin_unlock_irq(spinlock_t *lock);/* 關閉中斷底部(即關閉軟件中斷,打開硬件中斷,詳見后續中斷的講解) */void spin_lock_bh(spinlock_t *lock);void spin_unlock_bh(spinlock_t *lock);/* 非阻塞獲取,成功返回非0 */int spin_trylock(spinlock_t *lock);int spin_trylock_bh(spinlock_t *lock);
Reader/Writer Spinlocks
粒度更小,可多Reader同時讀,但Writer只能單獨,且讀與寫不能同時,適用于寫很少讀很多的情況。
定義/初始化
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);void write_unlock(rwlock_t *lock);void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);void write_unlock_irq(rwlock_t *lock);void write_unlock_bh(rwlock_t *lock);
seqlock
順序鎖(seqlock)是對讀寫鎖的一種優化,采用了重讀機制,讀寫不相互阻塞。
定義/初始化
#include seqlock_t lock1 = SEQLOCK_UNLOCKED; /* 靜態 */seqlock_t lock2;seqlock_init(&lock2); /* 動態 */
讀
/* 讀之前先獲取個順序號,讀完與當前順序號對比,如不一致則重讀 */unsigned int seq;do { seq = read_seqbegin(&the_lock); /* Do what you need to do */} while read_seqretry(&the_lock, seq);/* 如果這個鎖可能會出現在中斷程序中獲取,則在這里應使用關中斷版本 */unsigned int read_seqbegin_irqsave(seqlock_t *lock,unsigned long flags);int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq,unsigned long flags);
寫
void write_seqlock(seqlock_t *lock);void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);void write_seqlock_irq(seqlock_t *lock);void write_seqlock_bh(seqlock_t *lock);int write_tryseqlock(seqlock_t *lock);void write_sequnlock(seqlock_t *lock);void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);void write_sequnlock_irq(seqlock_t *lock);void write_sequnlock_bh(seqlock_t *lock);
RCU(Read-Copy-Update)
對于被 RCU 保護的共享數據結構,讀執行單元不需要獲得任何鎖就可以訪問它,因此讀執行單元沒有任何同步開銷。使用 RCU 的寫執行單元在訪問它前需首先拷貝一個副本,然后對副本進行修改,最后使用一個回調機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據,這個時機就是所有引用該數據的 CPU 都退出對共享數據的操作的時候。寫執行單元的同步開銷則取決于使用的寫執行單元間同步機制。RCU在驅動中很少使用,這里暫不詳述。
注意事項
自旋鎖實際上是忙等鎖,當鎖不可用時, CPU 一直循環執行“測試并設置”該鎖直到可用而取得該鎖, CPU 在等待自旋鎖時不做任何有用的工作,僅僅是等待。 因此,只有在占用鎖的時間極短的情況下,使用自旋鎖才是合理的。 當臨界區很大,或有共享設備的時候,需要較長時間占用鎖,使用自旋鎖會降低系統的性能。
自旋鎖可能導致系統死鎖。引發這個問題最常見的情況是遞歸使用一個自旋鎖,即如果一個已經擁有某個自旋鎖的 CPU 想第二次獲得這個自旋鎖,則該 CPU 將死鎖。
自旋鎖鎖定期間不能調用可能引起進程調度而導致休眠的函數。如果進程獲得自旋鎖之后再阻塞, 如調用 copy_from_user()、 copy_to_user()、 kmalloc()和 msleep()等函數,則可能導致內核的崩潰。
信號量 semaphore
使用方式和自旋鎖類似,不同的是,當獲取不到信號量時,進程不會原地打轉而是進入休眠等待狀態。
定義/初始化
#include struct semaphore sem;void sema_init(struct semaphore *sem, int val);/* 通常我們將val的值置1,即使用互斥模式 */DECLARE_MUTEX(name);DECLARE_MUTEX_LOCKED(name);void init_MUTEX(struct semaphore *sem);void init_MUTEX_LOCKED(struct semaphore *sem);
獲得信號量
void down(struct semaphore * sem); /* 信號量減1, 會導致睡眠,因此不能在中斷上下文使用 */int down_interruptible(struct semaphore * sem); /* 與down不同的是,進入睡眠后的進程可被打斷返回非0 */ int down_trylock(struct semaphore * sem); /* 非阻塞版本,獲得返回0,不會導致睡眠,可在中斷上下文使用 */
釋放信號量
void up(struct semaphore * sem);
Reader/Writer Semaphores
讀寫信號量與信號量的關系與讀寫自旋鎖和自旋鎖的關系類似,讀寫信號量可能引起進程阻塞,但它可允許 N 個讀執行單元同時訪問共享資源, 而最多只能有 1 個寫執行單元。因此,讀寫信號量是一種相對放寬條件的粒度稍大于信號量的互斥機制。
定義/初始化
#include struct rw_semaphore;void init_rwsem(struct rw_semaphore *sem);
讀
void down_read(struct rw_semaphore *sem);int 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);
完成量 completion
輕量級,用于一個執行單元等待另一個執行單元執行完某事。
定義/初始化
#include /* 靜態 */DECLARE_COMPLETION(name);/* 動態 */struct completion my_completion;init_completion(struct completion *c);INIT_COMPLETION(struct completion c); /* 重新初始化已經定義并使用過的 completion */
等待完成
void wait_for_completion(struct completion *c);
完成信號
void complete(struct completion *c); /* 喚醒1個 */void complete_all(struct completion *c); /* 喚醒所有waiter */void complete_and_exit(struct completion *c, long retval); /* call complete() and exit(retval) */
?
評論
查看更多