Linux設備驅動程序學習筆記5——併發和競態

 
1、 併發及其管理
競態通常作爲對資源的共享訪問結果而產生。
資源共享的硬規則:在單個執行線程之外共享硬件或軟件資源的任何時候,因爲另外一個線程可能產生對該資源的不一致觀察,因此必須顯式地管理對該資源的訪問。
一個重要的規則:當內核代碼創建了一個可能和其他內核部分共享的對象時,該對象必須在還有其他組件引用自己時保持存在(並正確工作)。
2、 信號量和互斥體
臨界區:在任意給定的時刻,代碼只能被一個線程執行;
進入休眠:當一個linux進程到達某個時間點,此時它不能進行任何處理時,它將進入休眠(或“阻塞”)狀態,這將把處理器讓給其他執行線程直到將來它能夠繼續完成自己的處理爲止。
互斥體:當信號量用於互斥時(即避免多個進行同時在一個臨界區中運行),信號量的值應初始化爲1。這種信號量在任何給定時刻只能由單個進程或線程擁有。
Linux內核中幾乎所有的信號量均用於互斥。
a)         Linux信號量的實現
信號量包含在<asm/semaphore>中;
信號量的創建:void sema_init(struct semaphore *sem, int val);
互斥體的初始化:
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);
int down_interruptible(struct semaphore *sem);
iInt down_trylock(struct semaphore *sem);
void up(struct semaphore *sem);
注:down減小信號量的值,並在必要時一直等待;down_interruptible完成相同的工作,但操作是可中斷的,對其的正確使用需要始終檢查返回值,並作出相應的響應;down_trylock永遠不會休眠,若信號量在調用時不可獲取,它會立即返回一個非零值。
如果在擁有一個信號量時發生錯誤,必須在將錯誤狀態返回給調用者之前釋放該信號量。
正確使用鎖定機制的關鍵是:明確指定需要保護的資源,並確保每一個對這些資源的訪問使用正確的鎖定。
b)        讀取者/寫入者信號量
讀取者/寫入者信號量包含在<linux/rwsem.h>中;
初始化:void init_rwsem(struct rw_semaphore *sem);
相關函數:
void down_read(struct rw_semaphore *sem);
iInt down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
void down_write(struct rw_semaphore *sem);
void down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
注:down_read可能會將調用進程置於不可中斷的休眠;down_read_trylock不會在讀取訪問不可獲得時等待;它在授予訪問時返回非零,其他情況下返回零。
當某個快速改變獲得了寫入者鎖,而其後是更長時間的只讀訪問,可在結束脩改之後調用downgrade_write,來允許其他讀取者的訪問。
最好在很少需要寫訪問且寫入者只會短期擁有信號量的時候使用rwsem。
3、 Completion
Completion是一種輕量級的機制,它允許一個線程告訴另一個線程某個工作已經完成。
其包含在<linux/completion.h>中;
創建:
DECLARE_COMPLETION(my_completion);
struct completion my_completion;
init_completion(my_completion);
等待completion:void wait_for_completion(struct completion *c);
調用的函數:
void complete(struct completion *c);
void complete_all(struct completion *c);
complete喚醒一個等待線程;complete_all喚醒所有等待線程;
如果使用了complete_all,則必須在重複使用該結構之前重新初始化它。
初始化:INIT_COMPLETION(struct completion c);
Completion機制的典型使用是模塊退出時的內核線程終止。
void complete_and_exit(struct completion *c, long retval);通過調用complete並調用當前線程的exit函數而發出completion事件信號。
4、 自旋鎖
a)         定義:一個自旋鎖是一個互斥設備,它只能有兩個值:“鎖定”和“解鎖”。它通常實現爲某個整數值中的單個位。它可在不能休眠的代碼中使用。
b)        自旋鎖API介紹
其包含在頭文件<linux/spinlock.h>;
編譯時初始化: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);
c)        自旋鎖和原子上下文
自旋鎖的核心原則:任何擁有自旋鎖的代碼都必須是原子的。它不能休眠,事實上,它不能因爲任何原因放棄處理器,除了服務中斷以外(某些情況下此時也不能放棄處理器)。
爲了避免因爲中斷造成的死鎖,需要在擁有自旋鎖時禁止中斷(僅在本地CPU上)。
自旋鎖的另一個重要規則:自旋鎖必須在可能的最短時間內擁有。
d)        自旋鎖函數
四個鎖定自旋鎖的函數:
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
spin_lock_irqsave會在獲得自旋鎖之前禁止中斷(只在本地處理器上),先前中斷狀態保存在flags中;
spin_lock_irq用於能夠確保沒有任何其他代碼禁止本地處理器的中斷(即能夠確保在釋放自旋鎖時應該啓用中斷);
spin_lock_bh在獲得鎖之前禁止軟件中斷,保持硬件中斷打開;
四個釋放自旋鎖的函數:
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
非阻塞的自旋鎖操作:
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
注:這兩個函數成功返回非0值,否則返回0。對於禁止中斷的情況,沒有對應的“try”版本。
e)         讀取者/寫入者自旋鎖
兩種聲明和初始化方式:
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_irqsave(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
注:沒有read_trylock函數。
對於寫入者有如下函數:
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);
5、 鎖陷阱
a)         不明確的規則
創建一個可被並行訪問的對象時,應同時定義用來控制訪問的鎖。
若某個獲得鎖的函數要調用其他同樣試圖獲取這個鎖的函數,代碼就會死鎖。
通常,內部的靜態函數可通過這種方式(假定調用者已獲取相關鎖)編寫,而提供給外部調用的函數則必須顯示地處理鎖定。在編寫假定調用者已處理了鎖定的內部函數時,應該顯式說明這種假定。
b)        鎖的順序規則
在必須獲取多個鎖時,應始終以相同的順序獲得。
兩個規則:若必須獲取一個局部鎖和一個屬於內核更中心位置的鎖,應該首先獲取自己的局部鎖;若擁有信號量和自旋鎖的組合,則必須首先獲得信號量,在擁有自旋鎖時調用down(可導致休眠)是個嚴重的錯誤。
最後的辦法是避免出現需要多個鎖的情況。
c)        細粒度鎖和粗粒度鎖的對比
通常的規則:應該在最初使用粗粒度的鎖,除非有真正的原因相信競爭會導致問題。
Lockmeter工具可度量內核花費在鎖上的時間。
6、 除了鎖之外的辦法
a)         免鎖算法
經常用於免鎖的生產者/消費者任務的數據結構之一是循環緩衝區,其可用於多讀取者/單個寫入者情況。
b)        原子變量
內核提供了一種原子的整數類型,稱爲atomic_t,定義在<asm/atomic.h>;
在atomic_t變量中不能記錄大於24位的整數。
atomic_t變量必須通過相應函數訪問。
初始化:void atomic_set(atomic_t *v, int i); atomic_t v =ATOMIC_INIT(0);
獲取值:int atomic_read(atomic_t *v);
加法:void atomic_add(int i, atomic_t *v);
減法:void atomic_sub(int i, atomic_t *v);
遞增:void atomic_inc(atomic_t *v);
遞減:void atomic_dec(atomic_t *v);
執行特定操作並測試結果:
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,返回true,否則返回false。
將整數變量i累加到v:int atomic_add_negative(int i, atomic_t *v);返回值在結果爲負時爲true,否則爲false。
類似於atomic_add及其變種:
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);
將院子變量傳遞給需要整型參數的函數,會出現編譯錯誤。
只有原子變量的數目是原子的,atomic_t變量才能工作。需要多個atomic_t變量的操作,仍需要某種類型的鎖。
c)        位操作
原子位操作非常快,只要底層硬件允許,這種操作就可以使用單個機器指令來執行,並且不需要禁止中斷。這些函數依賴於具體的架構,因此在<asm/bitops.h>中聲明。
設置第nr位:void set_bit(nr, void *addr);
清除第nr位:void clear_bit(nr, void *addr);
切換第nr位:void change_bit(nr, void *addr);
返回指定位的當前值:test_bit(nr, void *addr);
返回第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);
d)        Seqlock
Seqlock用於要保護的資源很小,很簡單,會頻繁被訪問而且寫入訪問很少發生且必須快速時。
Seqlock通常不能用於保護包含有指針的數據結構,因爲在寫入者修改該數據結構的同時,讀取者可能會追隨一個無效的指針。
Seqlock在<linux/seqlock.h>中定義。
其初始化有兩種方式:
seqlock_t lock1 = SEQLOCK_UNLOCKED;
seqlock_t lock2;
seqlock_init(&lock2);
讀取鎖相關函數:
unsigned int read_seqbegin(seqlock_t *lock);
int read_seqretry(seqlock_t *lock, unsigned int seq);
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry_irqstore(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);
void write_sequnlock(seqlock_t *lock);
void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
如果write_tryseqlock可以獲得自旋鎖,它也會返回非零值。
e)         讀取-複製-更新(read-copy-update,RCU)
RCU針對經常發生讀取而很少寫入的情形。在需要修改該數據結構時,寫入線程首先複製,然後修改副本,之後用新的版本替代相關指針。
其包含在<linux/rcupdate.h>中。
RCU很少用於驅動程序。
相關函數:
void rcu_read_lock();
void rcu_read_unlock();
void call_rcu(struct rec_head *head, void (*func)(void *arg), void *arg);
call_rcu準備用於安全釋放受RCU保護的資源的回調函數,該函數將在所有的處理器被調度後運行。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章