1. 鎖的由來,?
學(xué)習(xí)linux的時(shí)候,肯定會遇到各種和鎖相關(guān)的知識,,有時(shí)候自己學(xué)好了一點(diǎn),,感覺半桶水的自己已經(jīng)可以華山論劍了,又突然冒出一個新的知識點(diǎn),,我看到新知識點(diǎn)的時(shí)候,,有時(shí)間也是一臉的懵逼,,在大學(xué)開始寫單片機(jī)的跑裸機(jī)代碼,完全不懂這個鎖在操作系統(tǒng)里面是什么鬼,,從單片機(jī)到嵌入式Linux,,還有一個多任務(wù)系統(tǒng),不懂的同學(xué)建議百度看看,。
2. 什么是并發(fā)和競態(tài),?
在早期的Linux內(nèi)核中,并發(fā)源相對較少,。內(nèi)核不支持對稱多處理器(SMP)系統(tǒng),,唯一導(dǎo)致并發(fā)問題的原因是中斷。 隨著處理器的CPU核越來越多,,這要求系統(tǒng)對事件迅速做出響應(yīng),。為適應(yīng)現(xiàn)代硬件和應(yīng)用的需求,Linux內(nèi)核已經(jīng)發(fā)展到可以同時(shí)進(jìn)行更多事情的地步,。這種演變帶來了更大的可伸縮性,。但是,這也大大復(fù)雜化了內(nèi)核編程的任務(wù),。設(shè)備驅(qū)動程序員現(xiàn)在必須從一開始就將并發(fā)性考慮到他們的設(shè)中,,而且他們需要深刻的理解并發(fā)問題,并利用內(nèi)核提供的工具處理這類問題,。
Spinlocks and Atomic Context
Imagine for a moment that your driver acquires a spinlockand goes
about its business within its critical section. Somewhere in the
middle, your driver loses the processor.Perhaps it has called a
function (copy_from_user, say) that puts the process to sleep. Or,
perhaps, kernel preemption kicks in, and a higher-priority process
pushes your code aside. Your code is now holding a lockthat it will
not release any time in the foreseeable future. If some other thread
tries to obtain the same lock, it will, in the best case, wait
(spinning in the processor) for a very long time. In the worst
case,the system could deadlock entirely. Most readers would agree that
this scenario is best avoided. Therefore, the core rule that applies
to spinlocks is that any code must, while holding a spinlock, be
atomic.It cannot sleep; in fact, it cannot relinquish the processor
for any reason except toservice interrupts (and sometimes not even
then).
2.1 并發(fā)與競態(tài)概念
上面扯淡完了進(jìn)入正題 什么是并發(fā): 并發(fā)是指多個執(zhí)行任務(wù)同時(shí),、并行被執(zhí)行。 什么是競態(tài): 字面意思是競爭,,并發(fā)的執(zhí)行單元對共享資源(硬件資源和軟件上的全局變量,,靜態(tài)變量等)的訪問容易發(fā)生競態(tài)。 舉例一個字符設(shè)備的缺陷: 對于一個虛擬的字符設(shè)備驅(qū)動,,假設(shè)一個執(zhí)行單元A對其寫入300個字符'a’,,而另一個執(zhí)行單元B對其寫入300個字符'b’,第三個執(zhí)行單元讀取所有字符,。如果A,、B被順序串行執(zhí)行那么C讀出的則不會出錯,但如果A,、B并發(fā)執(zhí)行,,那結(jié)果則是我們不可料想的。 競態(tài)發(fā)生的情況 對稱多處理器(SMP)的多個CPU: SMP是一種緊耦合,、共享存儲的系統(tǒng)模型,,它的特點(diǎn)是多個CPU使用共同的系統(tǒng)總線,因此可以訪問共同的外設(shè)和存儲器,。 單CPU內(nèi)進(jìn)程與搶占它的進(jìn)程: Linux 2.6的內(nèi)核支持搶占調(diào)度,,一個進(jìn)程在內(nèi)核執(zhí)行的時(shí)候可能被另一高優(yōu)先級進(jìn)程打斷,。 中斷(硬中斷、軟中斷,、tasklet,、低半部)與進(jìn)程之間:中斷可以打斷正在執(zhí)行的進(jìn)程,處理中斷的程序和被打斷的進(jìn)程間也可能發(fā)生競態(tài),。 競態(tài)的解決辦法 解決競態(tài)問題的途徑是保證對共享資源的互斥訪問,。訪問共享資源的代碼區(qū)域稱為臨界區(qū),臨界區(qū)要互斥機(jī)制保護(hù),。Linux設(shè)備驅(qū)動中常見的互斥機(jī)制有以下方式:中斷屏蔽,、原子操作、自旋鎖和信號量等,。
Most readers would agree that this scenario is best avoided.
Therefore, the core rule that applies to spinlocks is that any code
must, while holding a spinlock, be atomic.It cannot sleep; in fact, it
cannot relinquish the processor for any reason except toservice
interrupts (and sometimes not even then).
對上面死鎖理解不夠深入的,可以細(xì)細(xì)評味這段英文,。
3. 討論下死鎖
死鎖的問題是開發(fā)中稍不小心就可能遇到的,,在SMP系統(tǒng)里面,如果有一個CPU被死鎖了,,還有其他CPU可以繼續(xù)運(yùn)行,,就像一個車子,有一個輪子爆胎了,,理論上還是可以跑的,,就是開不快,或者開快的話就會容易掛逼,。
3.1 多進(jìn)程調(diào)度導(dǎo)致死鎖
之前的文章https://mp.weixin.qq.com/s/au-ZXnTgpOO3mYRyKir01w 出現(xiàn)以下四種情況會產(chǎn)生死鎖: 1,,相互排斥。一個線程或進(jìn)程永遠(yuǎn)占有共享資源,,比如,,獨(dú)占該資源。 2,,循環(huán)等待,。例如,進(jìn)程A在等待進(jìn)程B,,進(jìn)程B在等待進(jìn)程C,,而進(jìn)程C又在等待進(jìn)程A。 3,,部分分配,。資源被部分分配,例如,,進(jìn)程A和B都需要訪問一個文件,,同時(shí)需要用到打印機(jī),,進(jìn)程A得到了這個文件資源,進(jìn)程B得到了打印機(jī)資源,,但兩個進(jìn)程都不能獲得全部的資源了,。 4,缺少優(yōu)先權(quán),。一個進(jìn)程獲得了該資源但是一直不釋放該資源,,即使該進(jìn)程處于阻塞狀態(tài)。 具體使用的場景會更加復(fù)雜,,要需要按實(shí)際分析,,對號入座~
3.2 單線程導(dǎo)致死鎖
單線程導(dǎo)致死鎖的情況一般是由于調(diào)用了引起阻塞的函數(shù),比如(copy_from_user(),、copy_to_ser(),、和kmalloc()),阻塞后進(jìn)行系統(tǒng)調(diào)度,,調(diào)度的過程中有可能又調(diào)用了之前獲取鎖的函數(shù),,這樣必然導(dǎo)致死鎖。 還有一種就是自旋鎖函數(shù)在沒有釋放鎖馬上又進(jìn)行申請同一個自旋鎖,,這樣的低級問題也是會導(dǎo)致自旋鎖,。
4. 互斥鎖和自旋鎖、信號量的區(qū)別,?
互斥鎖和互斥量 在我的理解里沒啥區(qū)別,,不同叫法。廣義上講可以值所有實(shí)現(xiàn)互斥作用的同步機(jī)制,。狹義上講指的就是mutex這種特定的二元鎖機(jī)制,。互斥鎖的作用就是互斥,,mutual exclusive,,是用來保護(hù)臨界區(qū)(critical section)的 。所謂臨界區(qū)就是代碼的一個區(qū)間,,如果兩個線程同時(shí)執(zhí)行就有可能出問題,,所以需要互斥鎖來保護(hù)。
信號量(semaphore) 是一種更高級的同步機(jī)制,,mutex(互斥鎖) 可以說是 semaphore(信號量) 在僅取值0/1時(shí)的特例,。Semaphore可以有更多的取值空間,用來實(shí)現(xiàn)更加復(fù)雜的同步,,而不單單是線程間互斥,。 自旋鎖 是一種 互斥鎖 的實(shí)現(xiàn)方式而已,相比一般的互斥鎖會在等待期間放棄cpu,,自旋鎖(spinlock) 則是不斷循環(huán)并測試鎖的狀態(tài),,這樣就一直占著cpu,。所以相比于自旋鎖和信號量,在申請鎖失敗的話,,自旋鎖會不斷的查詢,,申請線程不會進(jìn)入休眠,信號量和互斥鎖如果申請鎖失敗的話線程進(jìn)入休眠,,如果申請鎖被釋放后會喚醒休眠的線程,。 同步鎖 好像沒啥特殊說法,你可以理解為能實(shí)現(xiàn)同步作用的都可以叫同步鎖,,比如信號量,。最后,不要鉆這些名詞的牛角尖,,更重要的是理解這些東西背后的原理,,叫什么名字并沒有什么好說的。這些東西在不同的語言和平臺上又有可能會有不同的叫法,,其實(shí)本質(zhì)上就這么回事,。
5. 如何解決競態(tài)引起的問題?
上面我們已經(jīng)分析了競態(tài)產(chǎn)生的原因,、發(fā)生的情況以及解決辦法,下面我們對常見的解決辦法一一分析,。
1. 中斷屏蔽
1.基本概念:在單CPU中避免競態(tài)的一種簡單方法是在進(jìn)入臨界區(qū)之前屏蔽系統(tǒng)的中斷,。由于linux的異步I/O、進(jìn)程調(diào)度等很多內(nèi)容都依靠中斷,,所以我們應(yīng)該盡快的執(zhí)行完臨界區(qū)的代碼,,換句話就是臨界區(qū)代碼應(yīng)該盡量少。
2.具體操作: linux內(nèi)核提供了下面具體方法
Local_irq_disable();//屏蔽中斷 Local_irq_enable();//打開中斷 Local_irq_save(flags);//禁止中斷并保存當(dāng)前cpu的中斷位信息
2.原子操作
1.基本概念:原子操作指在執(zhí)行過程中不會被別的代碼中斷的操作,。
2.具體操作:linux內(nèi)核提供了一系列的函數(shù)來實(shí)現(xiàn)內(nèi)核中的原子操作,,這些操作分為兩類,一類是整型原子操作,,另一類是位原子操作,,其都依賴底層CPU的原子操作實(shí)現(xiàn),所以這些函數(shù)與CPU架構(gòu)有密切關(guān)系,。
1) 整型原子操作
atomic_t v = ATOMIC_INIT(0);//定義原子變量v并初始化為0 void atomic_set(atomic_t *v, int i);//設(shè)置原子變量值為i
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); int atomic_dec_and_test(atomic_t *v); int atomic_sub_and_test(int i,atomic_t *v); /*上述三個函數(shù)對原子變量v自增,、自減和減操作(沒有加)后測試其是否為0,,如果為0返回true,否則返回false*/
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); /*上述函數(shù)對原子變量v進(jìn)行自增,、自減,、加,、減操作,并返回新的值*/
2) 位原子操作
void set_bit(nr,void *addr);//設(shè)置addr地址的第nr位,,即向該位寫入1,。
void clear_bit(nr,void *addr);//清除addr地址的第nr位,即向該位寫入0,。
void change_bit(nr,void *addr);//對addr地址的第nr取反
int test_bit(nr,void *addr);//返回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); /*上述函數(shù)等同于執(zhí)行test_bit后,,再執(zhí)行xxx_bit函數(shù)*/
3. 自旋鎖
1.基本概念: 自旋鎖是一種對臨界資源進(jìn)行互斥訪問的手段。 2.工作原理: 為獲得自旋鎖,,在某CPU上運(yùn)行的代碼需先執(zhí)行一個原子操作,,該操作測試并設(shè)置某個內(nèi)存變量,由于其為原子操作,,所以在該操作完成之前其他執(zhí)行單元不可能訪問這個內(nèi)存變量,,如果測試結(jié)果表明已經(jīng)空閑,則程序獲得這個自旋鎖并繼續(xù)執(zhí)行,,如果測試結(jié)果表明該鎖仍被占用,,程序?qū)⒃谝粋€小的循環(huán)內(nèi)重復(fù)這個“測試并設(shè)置”操作,即進(jìn)行所謂的“自旋”,,通俗的說就是在“原地打轉(zhuǎn)”,。 3.具體操作: linux內(nèi)核中與自旋鎖相關(guān)的操作主要有:
spinlock_t lock;
spin_lock_init(lock);
spin_lock(lock);//獲得自旋鎖lock spin_trylock(lock);//嘗試獲取lock如果不能獲得鎖,返回假值,,不在原地打轉(zhuǎn),。
spin_unlock(lock);//釋放自旋鎖
為保證我們執(zhí)行臨界區(qū)代碼的時(shí)候不被中斷等影響我們的自旋鎖又衍生了下面的內(nèi)容
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_disable()
4.使用注意事項(xiàng):
- 1)自旋鎖實(shí)質(zhì)是忙等鎖,因此在占用鎖時(shí)間極短的情況下,,使用鎖才是合理的,,反之則會影響系統(tǒng)性能。
- 2)自旋鎖可能導(dǎo)致系統(tǒng)死鎖,。
- 3)自旋鎖鎖定期間不能調(diào)用可能引起進(jìn)程調(diào)度的函數(shù),。
4.讀寫自旋鎖
1.基本概念: 為解決自旋鎖中不能允許多個單元并發(fā)讀的操作,衍生出了讀寫自旋鎖,,其不允許寫操作并發(fā),,但允許讀操作并發(fā)。
2.具體操作: linux內(nèi)核中與讀寫自旋鎖相關(guān)的操作主要有:
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;//靜態(tài)初始化 rwlock_init(&my_rwlock);//動態(tài)初始化
read_unlock_irqrestore();
write_unlock_irqrestore();
5.順序鎖
1.基本概念: 順序鎖是對讀寫鎖的一種優(yōu)化,,如果使用順序鎖,,讀執(zhí)行單元在寫執(zhí)行單元對被順序鎖保護(hù)的共享資源進(jìn)行寫操作時(shí)仍然可以繼續(xù)讀,不必等待寫執(zhí)行單元的完成,,寫執(zhí)行單元也不需等待讀執(zhí)行單元完成在進(jìn)行寫操作,。 2.注意事項(xiàng): 順序鎖保護(hù)的共享資源不含有指針,因?yàn)樵趯憟?zhí)行單元可能使得指針失效,但讀執(zhí)行單元如果此時(shí)訪問該指針,,將導(dǎo)致oops,。 3.具體操作: linux內(nèi)核中與順序鎖相關(guān)的操作主要有:
write_sequnlock_irqrestore();
read_seqbegin_irqsave();//local_irq_save + read_seqbegin
4)讀執(zhí)行單元重讀
read_seqretry_irqrestore ();
6. RCU(讀-拷貝-更新)
1.基本概念: RCU可以看做是讀寫鎖的高性能版本,相比讀寫鎖,,RCU的優(yōu)點(diǎn)在于即允許多個讀執(zhí)行單元同時(shí)訪問被保護(hù)數(shù)據(jù),,又允許多個讀執(zhí)行單元和多個寫執(zhí)行單元同時(shí)訪問被保護(hù)的數(shù)據(jù)。 2.注意事項(xiàng): RCU不能代替讀寫鎖,。 3.具體操作: linux內(nèi)核中與RCU相關(guān)的操作主要有:
synchronize_rcu ();//由RCU寫執(zhí)行單元調(diào)用 synchronize_sched();//可以保證中斷處理函數(shù)處理完畢,,不能保證軟中斷處理結(jié)束
有關(guān)RCU的操作還有很多,大家可以參考網(wǎng)絡(luò),。
7. 信號量
1.基本概念: 信號量用于保護(hù)臨界區(qū)的常用方法與自旋鎖類似,,但不同的是當(dāng)獲取不到信號量時(shí),進(jìn)程不會原地打轉(zhuǎn)而是進(jìn)入休眠等待狀態(tài),。 2.具體操作: linux內(nèi)核中與信號量相關(guān)的操作主要有:
Struct semaphore sem;
void sema_init(struct semaphore *sem, int val);//初始化sem為val,,當(dāng)然還有系統(tǒng)定義的其他宏初始化,這里不列舉
void down(struct semaphore *sem);//獲得信號量sem,,其會導(dǎo)致睡眠,,并不能被信號打斷 int down_interruptible(struct semaphore *sem);//進(jìn)入睡眠可以被信號打斷 int down_trylock(struct semaphore *sem);//不會睡眠
void up(struct semaphore *sem);//釋放信號量,喚醒等待進(jìn)程
注:當(dāng)信號量被初始為0時(shí),,其可以用于同步,。
8.Completion用于同步
1.基本概念: linux中的同步機(jī)制。
2.具體操作: linux內(nèi)核中與Completion相關(guān)的操作主要有:
struct completion *my_completion;
void init_completion(struct completion *x);
void wait_for_completion(struct completion *);
void complete(struct completion *);//喚醒一個 void complete_all(struct completion *);//喚醒該Completion的所有執(zhí)行單元
9.讀寫信號量
1.基本概念: 與自旋鎖和讀寫自旋鎖的關(guān)系類似 2.具體操作: linux內(nèi)核中與讀寫信號量相關(guān)的操作主要有:
up_read ();
up_write();
10.互斥體
1.基本概念: 用來實(shí)現(xiàn)互斥操作
2.具體操作: linux內(nèi)核中與互斥體相關(guān)的操作主要有:
void mutex_lock(struct mutex *lock); int mutex_lock_interruptible(struct mutex *lock); int mutex_lock_killable(struct mutex *lock);
void mutex_unlock(struct mutex *lock);
|