併發的定義是:多個執行單元同時、並行的執行。併發會導致競態:併發的執行單元對共享資源的訪問。以下幾種情況會發生競態:
- 對稱多處理器(SMP)的多個 CPU。
- 單個 CPU 內部的多個進程。
- 中斷與進程之間。
解決競態的途徑是保證對共享資源的互斥訪問:一個執行單元訪問共享資源的時候,其他的執行單元被禁止訪問。我們常用的互斥機制有:
中斷屏蔽
、原子操作
、自旋鎖
、信號量
。
一、中斷屏蔽
因爲 Linux 的異步 IO、進程調度等很多操作都是通過中斷來進行的。所以,最簡單的避免競態的方法就是在進入臨界區之前屏蔽系統的中斷。但是同樣,因爲中斷有這樣重要的作用,長時間的屏蔽中斷是很危險的。另外,中斷屏蔽只能解決上述三種競態情況中的後兩種,對於第一種競態是無法解決的。所以中斷屏蔽通常和自旋鎖搭配使用。
中斷屏蔽的使用方法是:
C
local_irq_disable() //屏蔽中斷
...
critical section /*臨界區*/
...
local_irq_enable()
中斷屏蔽通常是和自旋鎖聯合使用的。
二、原子操作
有時候,共享的資源可能正好是一個整數值或者是位操作。內核提供了一種原子的整數類型(位類型)。相應的操作如下:
整形原子操作
C
void atomic_set(atomic_t *v, int i); //設置原子變量 v 的值爲 i
atomic_t v = ATOMIC_INIT(0); //初始化原子變量 v 的值爲 0
atomic_read(atomic_t *v); //讀取原子變量 v 的值
void atomic_add(int i, atomic_t *v); //原子變量 v 的值加 i
void atomic_sub(int i, atomic_t *v); //原子變量 v 的值減 i
void atomic_inc(atomic_t *v); //原子變量 v 的值自加1
void atomic_dec(atomic_t *v); //原子變量 v 的值自減1
int atomic_inc_and_test(atomic_t *v); //原子變量 v 的值自加1,並測試是否等於0
int atomic_dec_and_test(atomic_t *v); //原子變量 v 的值自減1,並測試是否等於0
int atomic_sub_and_test(int i, atomic_t *v); //原子變量 v 的值減 i,並測試是否等於0
int atomic_add_and_return(int i, atomic_t *v); //原子變量 v 的值加 i,並返回值
int atomic_sub_and_return(int i, atomic_t *v); //原子變量 v 的值減 i,並返回值
int atomic_inc_and_return(atomic_t *v); //原子變量 v 的值自加1,並返回值
int atomic_dec_and_return(atomic_t *v); //原子變量 v 的值自減1,並返回值
atomic_t
的數據只能通過上述的函數進行訪問。位原子操作
C
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位
int test_and_set_bit(nr, void *addr); //返回addr地址的第nr位並置該位爲1
int test_and_clear_bit(nr, void *addr); //返回addr地址的第nr位並置該位爲0
int test_and_change_bit(nr, void *addr);//返回addr地址的第nr位並反置該位
三、自旋鎖
自旋鎖是一個互斥設備,他只能有兩個值:鎖定和解鎖。爲了獲取一個自旋鎖,程序先執行一個原子操作,測試相關的位,如果鎖可用,則鎖定,代碼進入臨界區;如果鎖不可用,代碼則進入循環測試直到該鎖可用。
自旋鎖的初始化:
可以使用兩種方法進行自旋鎖的初始化:
編譯時使用:spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
運行時使用:void spin_lock_init(spinlock_t *lock);
獲取鎖
獲取鎖使用下面的函數:
C
void spin_lock(spinlock_t *lock);
需要注意的是,自旋鎖的等待是不可中斷的,一旦調用了該函數,在獲取鎖之前將一直處於自旋狀態。如果不想阻塞等待可以使用非阻塞版本的獲取鎖:
C
void spin_trylock(spinlock_t *lock);
該函數在成功獲取鎖的情況下返回非零值,在未獲取鎖的情況下返回0釋放鎖
釋放鎖的函數如下:
C
void spin_unlock(spinlock_t *lock);
這個函數一般與spin_lock
和spin_trylock
搭配使用。自旋鎖和中斷
自旋鎖可以保證臨界區不受當前CPU和其他CPU的搶佔進程打擾,即能解決前面提到的競態中的前兩種,但是依舊可能受到中斷的影響,所以自旋鎖有下列衍生:
C
void spin_lock_irq(spinlock_t *lock); //在獲取自旋鎖之前禁止中斷
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); //在獲取鎖之前屏蔽中斷,並將相應的中斷狀態保存在 flags 中
void spin_lock_bh(spinlock_t *lock); //在獲取鎖之前屏蔽軟件中斷,但保持硬件中斷
當然還有與上面幾個函數一一對應的 unlock 函數,不再詳細描述。讀寫自旋鎖
讀寫自旋鎖是對自旋鎖的擴展,它允許多個讀操作併發執行,但是只能有一個寫單元。相應的函數如下:
C
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; //靜態初始化讀寫自旋鎖
rwlock_init(rwlock_t *my_rwlock); //動態初始化讀寫自旋鎖
void read_lock(rwlock_t *my_rwlock); //獲取讀鎖
void read_unlock(rwlock_t *my_rwlock); //釋放讀鎖
void write_lock(rwlock_t *my_rwlock); //獲取寫鎖
void write_unlock(rwlock_t *my_rwlock); //釋放寫鎖
讀寫自旋鎖也有相應的中斷衍生版本。順序鎖
對於資源較小,頻繁被讀取但是很少寫入的資源,可以使用順序鎖。順序鎖的讀執行單元不會被寫執行單元阻塞。
順序鎖的初始化類似於自旋鎖:
C
seqlock_t my_seqlock = SEQLOCK_UNLOCKED; //靜態初始化讀寫自旋鎖
seqlock_t_init(seqlock_t *my_seqlock); //動態初始化讀寫自旋鎖
對於寫單元來說,相應的獲取鎖和釋放鎖的機制和自旋鎖一致,不再詳說,詳細說下讀單元的執行。
C
unsigned int seq;
do {
seq = read_seqbegin(&my_seqlock);
/*相應的操作*/
} while (read_seqretry(&the_lock, seq))
read_seqbegin
會返回當前順序鎖的順序號,read_seqretry
會檢查當前的順序號是否改變。
通常不能使用順序鎖來保護數據結構中含有指針的數據。讀-拷貝-更新
RCU
(read-copy-update,讀-拷貝-更新)是基於原理命名的,在這裏不再詳細介紹
四、信號量
信號量和自旋鎖類似,只有得到信號量的進程才能執行臨界區代碼。但是與自旋鎖不同的是,當獲取不到信號量的時候,進程不會原地打轉而是會進入休眠等待狀態。
信號量的定義和初始化
有三種方法可以初始化一個信號量:
C
void sema_init(struct semaphore *sem, int val); //初始化信號量 sem,並將 sem 的值設爲 val
void init_MUTEX(struct semaphore *sem); //初始化信號量 sem 爲0
void init_MUTEX_LOCKED(struct semaphore *sem); //初始化信號量 sem 爲1
DECLARE_MUTEX(name); //聲明並初始化一個名爲 name 的信號量爲0
DECLARE_MUTEX_LOCKED(name); //聲明並初始化一個名爲 name 的信號量爲1
對於含有LOCKED
的初始化方法,信號量的初始狀態就是鎖定的。信號量的獲取
信號量的獲取使用下列方式:
C
void down(struct semaphore *sem); //會導致睡眠,不能被信號打斷
int down_interruptible(struct semaphore *sem); //會導致睡眠,並能被信號打斷
int down_trylock(struct semaphore *sem); //不導致睡眠
使用down_interruptible
函數被信號打斷和使用down_trylock
函數未獲取信號量,函數會返回非零值。否則返回0。信號量的釋放
C
void up(struct semaphore *sem);
該函數會釋放信號量,喚醒等待者。完成量
completion (完成量) 用於一個執行單元等待另一個執行單元執行完成某事。相應的使用方法如下:
C
struct completion my_completion;
init_completion(&my_completion); //定義並初始化 completion
/*
DECLARE_COMPLETION(my_completion); //另一種創建 completion 的方法
*/
void wait_for_completion(struct completion *c); //等待 completion 被喚醒
...
void complete(struct completion *c); //喚醒一個等待 c 的執行單元
void complete_all(struct completion *c); //喚醒所有等待 c 的執行單元
INIT_COMPLETION(struct completion my_completion); //用於重新初始化一個信號量
void completion_and_exit(struct completion *c, long retval); //
對於completion_and_exit
的用法還有不清楚的地方,等驗證完成後再來補充。讀寫信號量
讀寫信號量類似與讀寫自旋鎖,使用方法如下:
C
struct rw_semaphore my_rws; //定義讀寫信號量
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);
void down_write_trylock(struct rw_semaphore *sem); //寫信號量獲取
void up_write(struct rw_semaphore *sem); //寫信號量釋放
五、互斥體
互斥體簡單實現了互斥的功能:
C
struct mutex my_mutex;
mutex_init(&my_mutex); //初始化互斥體
void inline __sched mutex_lock(struct mutex *lock);
int __sched mutex_lock_interruptible(struct mutex *lock);
int __sched mutex_trylock(struct mutext *lock); //獲取互斥體
void __sched mutex_unlock(struct mutext *lock); //釋放互斥體