綜述
并發(fā)問題是編程中經(jīng)常遇到的難題暮顺,我們需要學(xué)會針對并發(fā)產(chǎn)生的競態(tài)進(jìn)行編程
一、信號量和互斥體
Linux上信號量的實現(xiàn)
1.信號量:本質(zhì)上是一個整數(shù)值蝙茶,它和一對函數(shù)聯(lián)合使用,這對函數(shù)通常稱為P和V,希望進(jìn)入臨界區(qū)的進(jìn)程將在相關(guān)信號量上調(diào)用P油猫,如果信號量大于0,該值減1柠偶,進(jìn)程可以進(jìn)入臨界區(qū)代碼運行情妖;相反睬关,如果信號量的值<=0,進(jìn)程必須等待其他人釋放該信號量。解鎖信號量調(diào)用V函數(shù)毡证,該函數(shù)增加信號量的值电爹,并在必要時喚醒等待的進(jìn)程。
當(dāng)信號量用于互斥時(即避免多個進(jìn)程同時在一個臨界區(qū)中運行)
信號量應(yīng)初始化為1
這種信號量在任何時刻只能由單個進(jìn)程或者線程擁有料睛,這種模式下丐箩,
信號量也成為互斥體
2.聲明和初始化
void sema_init(strcut semaphore *sem,int val);
sem代表信號量,val代表初始值恤煞,一般初始化為1.
DECLARE_MUTEX(name):一個名稱為name的信號量被初始化為1
DECLARE_MUTEX_LOCKED(name):一個名稱為name的信號量被初始化為0
void init_MUTEX(stuct semaphore *sem):
void init_MUTEX_LOCKED(stuct semaphore *sem):
LINUX2.6版本后已經(jīng)被遺棄 無法使用
Linux中的P函數(shù)是down(),該函數(shù)會減少信號量的值屎勘,必要時會一直等待。
void down(struct semaphore *sem)
減少信號量的值 并在必要時一直等待居扒,操作不可中斷
void down_interruptible(struct semaphore *sem)
完成和down相同的工作概漱,但是操作是可中斷,通常推薦使用苔货,如果操作被中斷犀概,該函數(shù)返回非零值,而調(diào)用者不會擁有該信號夜惭,使用時注意檢查返回值姻灶,并且做出相應(yīng)的操作。
int down_trylock(struct semaphore *sem)
永遠(yuǎn)不會休眠诈茧,如果信號量在調(diào)用時不可獲得产喉,該函數(shù)會立即返回一個非零值。
當(dāng)一個函數(shù)成功調(diào)用上述的down函數(shù)敢会,就稱為該線程擁有(獲得曾沈、拿到)了該信號量,這樣該線程就被賦予訪問由該信號量保護(hù)的臨界區(qū)的權(quán)利鸥昏,當(dāng)互斥操作完成后塞俱,必須返回該信號量,即調(diào)用V函數(shù)
void up(strcut semaphore *sem);
調(diào)用后吏垮,調(diào)用者不在擁有該信號量障涯。
使用信號量實例
步驟1:定義信號量
在自己的定義的結(jié)構(gòu)體加入semaphore *sem;//互斥信號量
步驟2:初始化信號量
sema_init(sem,1);
默認(rèn)初始化信號量sem的值為1
步驟3:
在要保護(hù)的資源調(diào)用dowm_interruptible();
if(dowm_interruptible(&sem))
retrun -ERESTARTSYS;
//這里是要保護(hù)資源的代碼
out:
up(&sem);
在函數(shù)調(diào)用up最后釋放信號量
strcut hello_dev{
int val;
semaphore *sem;//互斥信號量
}
static hello_init()
{
//初始化信號量
sema_init(dev->sem,1);
}
/*讀取寄存器設(shè)備 val的值*/
static ssize_t hello_read(struct file *filp,char __user *buf,
size_t count,loff_t *f_ops) {
ssize_t err = 0;
struct hello_dev *dev = filp->private_data;
/*同步訪問*/
if(down_interruptible(&(dev->sem)));
return -ERESTARTSYS;
if(count < sizeof(dev->val)){
goto out;
}
/*將寄存器val的值拷貝到用戶提供的緩存區(qū)*/
if(copy_to_user(buf,&(dev->val),sizeof(dev->val))){
err = -EFAULT;
goto out;
}
out:
up(&(dev->sem));
return err;
}
讀取者/寫入者信號量
許多任務(wù)可分為:
1.只需要讀取受保護(hù)的數(shù)據(jù)(多個進(jìn)程和線程可以同時并發(fā)訪問)
2.寫入受保護(hù)的數(shù)據(jù)
為此,Linux提供了特殊的信號量"rwsem"(或者reader/writer semaphore),rwsem使用很少膳汪,偶爾有用
相關(guān)定義包含在<linux/rwsem.h>頭文件中
struct rw_semaphore *sem;
初始化:
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);
void downgrade_write(struct rw_semaphore *sem);
downgrade_write允許其他讀取者訪問
完成量
completion是一種輕量級的機制遗嗽,它允許一個線程告訴另一個線程某個工作已經(jīng)完成了粘我,包含在<linux/completion.h>中
1.創(chuàng)建和初始化completion
DECLARE_COMPLETION(my_com);
或者動態(tài)創(chuàng)建和初始化
strcut completion my_com;
inti_completion(&my_com);
2.等待completion
void wait_for_completion(strcut completion *c);
注意,該函數(shù)執(zhí)行一個非中斷等待痹换,如果代碼調(diào)用了該函數(shù)且沒人能完成該信號量征字,則會產(chǎn)生一個不可殺進(jìn)程都弹!
3.喚醒completion
void completion(struct completion *c);
void complete_all(struct completion *c);
快速重新初始化某個復(fù)用的completion
INIT_COMPLETION(struct completion c);
自旋鎖
信號量在互斥中是非常有用的工具,內(nèi)核還提供另一種工具--自旋鎖匙姜。
和信號量不同缔杉,自旋鎖可以在休眠的代碼中使用,如中斷處理例程搁料,正確使用的情況下或详,自旋鎖性能比信號量好!
自旋鎖API
初始化
自旋鎖相關(guān)定義包含在頭文件<linux/spinlock.h>中
編譯時初始化
strcuvt spinlock_t my_lock;
my_lock = SPIN_LOCK_UNLOCKED;
或者
運行時初始化
void spin_lock_init(spinlock_t *lock);
進(jìn)入臨界區(qū)之前郭计,必須調(diào)用以下函數(shù)獲取鎖
void spin_lock(spinlock_t *lock);
注意:所以自旋鎖本質(zhì)上都是不可中斷的霸琴,一旦調(diào)用了spin_lock,在獲取鎖之前一直處于自旋狀態(tài)
釋放鎖函數(shù)
void spin_unlock(spinlock_t *lock);
注意:為了避免
在中斷例程自旋時昭伸,非中斷代碼將沒有機會釋放這種個自旋鎖梧乘,導(dǎo)致處理器將永遠(yuǎn)自旋下去的情況
我們需要在擁有自旋鎖時禁止中斷(僅本地CPU上),下面的函數(shù)可以實現(xiàn)用于禁止中斷的自旋鎖函數(shù)庐杨。
另一個重要原則:自旋轉(zhuǎn)必須的在盡可能短的時間內(nèi)擁有选调!
自旋鎖函數(shù)
void spin_lock(spinlock_t *lock):允許中斷的自旋鎖
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags)
在獲得自旋鎖之前禁止中斷(只在本地cup上),先前中斷報錯在flags中
void spin_lock_irq(spinlock_t *lock)
如果我們能確保沒有任何其他代碼禁止本地處理器的中斷,則可以使用spin_lock_irq灵份,而無需跟蹤標(biāo)志仁堪!
void spin_lock_bh(spinlock_t *lock)
在獲得鎖之前禁止軟件中斷,允許硬件中斷打開
該函數(shù)可以安全的避免死鎖問題填渠,還能服務(wù)硬件中斷
釋放鎖函數(shù)
void spin_unlock(spinlock_t *lock):
void spin_unlock_irqrestore(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);
兩個函數(shù)在成功獲取自旋鎖時弦聂,返回非零值,失敗時返回零氛什,對應(yīng)禁止中斷的情況沒有對應(yīng)的try版本
讀取者/寫入者自旋鎖
和rwsem信號量很相似莺葫,但是可能會造成讀取者饑餓,導(dǎo)致性能變低枪眉!
注意防止死鎖情況
死鎖1:當(dāng)某個獲得鎖的函數(shù)要調(diào)用其他同樣試圖獲取這個鎖的函數(shù)捺檬,代碼就會死鎖,無論是信號量還是自旋鎖贸铜,都不允許鎖擁有者第二次獲得這個鎖
死鎖2:線程1擁有鎖1堡纬,線程2擁有鎖2,這時候萨脑,當(dāng)這兩個線程都試圖獲取另外線程的的鎖時隐轩,這兩個線程將處于死鎖狀態(tài)
最好的辦法是避免同時需要多個鎖的情況
鎖之外的辦法
1.免鎖算法
常用于免鎖算法的生產(chǎn)者/消費者任務(wù)的數(shù)據(jù)結(jié)構(gòu)就是循環(huán)緩沖區(qū)饺饭!
2.原子變量
共享資源是一個簡單的整數(shù)型時渤早,內(nèi)核提供了一種原子的整數(shù)類型
稱為atomic_t 定義在<asm/atomic.h>中
初始化
void atomic_set(atomic *v,int t);
或者
atomic_t v = ATOMIC_INIT(0);初始化為0
還有讀寫函數(shù),運算操作函數(shù)瘫俊,位操作函數(shù)就不一一列舉了
3.seqlock
當(dāng)要保護(hù)的資源很小鹊杖、很簡單悴灵、會被頻繁讀取訪問且寫入訪問很少發(fā)生且必須快速時,就可以使用內(nèi)核提供的seqlock
允許讀取者自由訪問骂蓖,但是需要讀取者檢測是否和寫入者沖突
seqlock通常不能包含在含有指針的數(shù)據(jù)結(jié)構(gòu)中积瞒,因為在寫入者修改數(shù)據(jù)結(jié)構(gòu)的同時,讀取者可能會追隨一個無效的指針登下。
seqlock定義在<linux/seqlock.h>
初始化方法2種
1.seqlock_t lock1 = SEQLOCK_UNLOCKED;
2.seqlock_t lock2;
seqlock_init(&lock2);
讀取時會訪問通過一個(無符號的)整數(shù)順序值而進(jìn)入臨界區(qū)茫孔,在退出時,該順序值和當(dāng)前值比較被芳,如果不相等缰贝,必須重試讀取訪問。
unsigned int seq;
do {
seq = read_seqbegin(&the_lock);
/*完成需要做的工作*/
}while read_seqretry(&the_lock,seq);
如果在中斷處理例程中使用seqlock,則應(yīng)該使用IRQ安全的版本
unsigned int read_seqbegin_irqsave(seqlock_t *lock,unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,
unsigned long flags);
寫入者必須進(jìn)入由seqlock包含的臨界區(qū)時獲得一個互斥鎖畔濒,因此要調(diào)用以下函數(shù):
void write_sequnlock(seqlock_t *lock);
還有其他常見自旋鎖的變種函數(shù)
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_irqrestore(seqlock_t *lock,unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
另外
如果write_tryseqlock()可以獲得自旋鎖剩晴,它也會返回非0值。
讀取-復(fù)制-更新(read-copy-update,RCU)
RCU是一種高級的互斥機制侵状,正確使用下赞弥,也可獲得很高的性能,但很少在驅(qū)動程序中使用趣兄。
RCU針對經(jīng)常發(fā)生讀取而很少寫入的情形做了優(yōu)化绽左,被保護(hù)的資源應(yīng)通過指針訪問,而對這些資源的引用必須由原子代碼擁有艇潭!
#include <linux/rcupdate.h>
使用讀取-復(fù)制-更新(RCU)機制是需要包含的頭文件
void rcu_read_lock();
void rcu_read_unlock();
獲取對受RCU保護(hù)資源的讀取訪問的宏
void call_rcu(srcut rcu_head head,void (func)(void *arg),void *arg);
準(zhǔn)備用于安全示范受RCU保護(hù)的資源的回調(diào)函數(shù)妇菱,該函數(shù)將在所有的處理器被調(diào)度后運行!