自旋锁、互斥锁和信号量
自旋鎖
Linux內(nèi)核中最常見的鎖是自旋鎖(spin lock)。自旋鎖最多只能被一個可執(zhí)行線程持有。如果一個執(zhí)行線程試圖獲得一個已經(jīng)被持有的自旋鎖,那么該線程就會一直進(jìn)行忙循環(huán)——旋轉(zhuǎn)——等待鎖重新可用。要是鎖未被爭用,請求鎖的執(zhí)行線程便能立刻得到它,繼續(xù)執(zhí)行。在任意時間,自旋鎖都可以防止多于一個的執(zhí)行線程同時進(jìn)入臨界區(qū)。同一個鎖可以用在多個位置。例如,對于給定數(shù)據(jù)的所有訪問都可以得到保護(hù)和同步。
自旋鎖相當(dāng)于上廁所時,在門外等待的過程。如果你到在廁所門外,發(fā)現(xiàn)里面沒有人,就可以推開門進(jìn)入廁所。如果你到了廁所門口發(fā)現(xiàn)門是關(guān)著的(里面有人),就必須在門口等待(此時你很著急),不斷地檢查廁所是否為空。當(dāng)廁所為空時,你就可以進(jìn)入了。正是因?yàn)橛辛碎T(相當(dāng)于自旋鎖),才允許一次只有一個人(相當(dāng)于執(zhí)行線程)進(jìn)入廁所里(相當(dāng)于臨界區(qū))。
一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用時自旋(特別浪費(fèi)處理器時間),這種行為是自旋鎖的要點(diǎn)。所以自旋鎖不應(yīng)該被長時間的持有。事實(shí)上,這點(diǎn)正是使用自旋鎖的初衷:在段期間內(nèi)進(jìn)行輕量級加鎖。還可以采取另外的方式來處理對鎖的爭用:讓請求線程睡眠,直到鎖重新可用時再喚醒它。這樣處理器就不必循環(huán)等待,可以去執(zhí)行其他代碼。這也會帶來一定的開銷——這里有兩次明顯的上下文切換,被阻塞的線程要換出和換入,與實(shí)現(xiàn)自旋鎖的少數(shù)幾行代碼相比,上下文切換當(dāng)然有較多的代碼。因此,持有自旋鎖的時間最好小于完成兩次上下文切換的耗時。當(dāng)然我們大多數(shù)人都不會無聊到去測量上下文切換的耗時,所以我們讓持有自旋鎖的時間應(yīng)盡可能的短就可以了。
自旋鎖的實(shí)現(xiàn)和體系結(jié)構(gòu)密切相關(guān),代碼往往通過匯編實(shí)現(xiàn)。這些與體系結(jié)構(gòu)相關(guān)的代碼定義在文件<asm/spinlock.h>中,實(shí)際需要用到的接口定義在文件<linux/spinlock.h>中。本文參考的書籍是Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn),其討論的是2.6.34內(nèi)核版本。自旋鎖的基本使用方式如下:
DEFINE_SPINLOCK(mr_lock); spin_lock(&mr_lock); /*臨界區(qū)...*/ spin_unlock(&mr_lock);因?yàn)樽孕i在同一時刻最多被一個執(zhí)行線程持有,所以一個時刻只能有一個線程位于臨界區(qū)內(nèi),這就為多處理器機(jī)器提供了防止并發(fā)訪問所需的保護(hù)機(jī)制。注意:在單處理器機(jī)器上,編譯的時候并不會加入自旋鎖。它僅僅被當(dāng)做一個設(shè)置內(nèi)核搶占機(jī)制是否被啟用的開關(guān)。如果禁止內(nèi)核搶占,那么在編譯時自旋鎖會被完全剔除出內(nèi)核。
注意:自旋鎖是不可遞歸的
Linux內(nèi)核實(shí)現(xiàn)的自旋鎖是不可遞歸的,這點(diǎn)不同于自旋鎖在其他操作系統(tǒng)中的實(shí)現(xiàn)。所以如果你試圖得到一個你正持有的鎖,你必須自旋,等待你自己釋放這個鎖。由于你處于自旋忙等待,所以你永遠(yuǎn)沒有機(jī)會釋放鎖,于是你被自己鎖死了。
自旋鎖可能帶來的問題
(1)死鎖。試圖遞歸地獲得自旋鎖必然會引起死鎖:例如遞歸程序的持有實(shí)例在第二個實(shí)例循環(huán),以試圖獲得相同自旋鎖時,就不會釋放此自旋鎖。所以,在遞歸程序中使用自旋鎖應(yīng)遵守下列策略:遞歸程序決不能在持有自旋鎖時調(diào)用它自己,也決不能在遞歸調(diào)用時試圖獲得相同的自旋鎖。此外如果一個進(jìn)程已經(jīng)將資源鎖定,那么,即使其它申請這個資源的進(jìn)程不停地瘋狂“自旋”,也無法獲得資源,從而進(jìn)入死循環(huán)。
 (2)過多占用CPU資源。如果不加限制,由于申請者一直在循環(huán)等待,因此自旋鎖在鎖定的時候,如果不成功,不會睡眠,會持續(xù)的嘗試,單CPU的時候自旋鎖會讓其它process動不了。因此,一般自旋鎖實(shí)現(xiàn)會有一個參數(shù)限定最多持續(xù)嘗試次數(shù)。超出后,自旋鎖放棄當(dāng)前time slice,等下一次機(jī)會。
自旋鎖的操作
spin_lock_init():可以使用該方法來初始化動態(tài)創(chuàng)建的自旋鎖(此時你只有一個指向spinlock_t類型的指針,沒有它的實(shí)體)。
spin_try_lock():試圖獲得某個特定的自旋鎖,如果該鎖已經(jīng)被爭用,那么該方法會立即返回一個非0值,而不會自旋等待鎖被釋放;如果成功地獲得了這個自旋鎖,該函數(shù)返回0。同理,spin_is_locked()方法用于檢查特定的鎖當(dāng)前是否已被占用,如果被占用,返回非0值;否則返回0。該方法只做判斷,并不實(shí)際占用。
標(biāo)準(zhǔn)的自旋鎖操作的完整列表:
|   spin_lock_init(lock)  |   初始化自旋鎖,將自旋鎖設(shè)置為1,表示有一個資源可用。  | 
|   spin_is_locked(lock)  |   如果自旋鎖被置為1(未鎖),返回0,否則返回1。  | 
|   spin_unlock_wait(lock)  |   等待直到自旋鎖解鎖(為1),返回0;否則返回1。  | 
|   spin_trylock(lock)  |   嘗試鎖上自旋鎖(置0),如果原來鎖的值為1,返回1,否則返回0。  | 
|   spin_lock(lock)  |   循環(huán)等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。  | 
|   spin_unlock(lock)  |   將自旋鎖解鎖(置為1)。  | 
|   spin_lock_irqsave(lock, flags)  |   循環(huán)等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。關(guān)中斷,將狀態(tài)寄存器值存入flags。  | 
|   spin_unlock_irqrestore(lock, flags)  |   將自旋鎖解鎖(置為1)。開中斷,將狀態(tài)寄存器值從flags存入狀態(tài)寄存器。  | 
|   spin_lock_irq(lock)  |   循環(huán)等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。關(guān)中斷。  | 
|   spin_unlock_irq(lock)  |   將自旋鎖解鎖(置為1)。開中斷。  | 
|   spin_unlock_bh(lock)  |   將自旋鎖解鎖(置為1)。開啟底半部的執(zhí)行。  | 
|   spin_lock_bh(lock)  |   循環(huán)等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。阻止軟中斷的底半部的執(zhí)行。  | 
spin_lock和spin_lock_irq的區(qū)別
(1)spin_lock
 spin_lock 的實(shí)現(xiàn)關(guān)系為:spin_lock ->?raw_spin_lock ->?_raw_spin_lock ->?__raw_spin_lock ,而__raw_spin_lock?的實(shí)現(xiàn)為:
(2)spin_lock_irq
spin_lock_irq 的實(shí)現(xiàn)關(guān)系為:spin_lock_irq ->?raw_spin_lock_irq ->?_raw_spin_lock_irq ->?__raw_spin_lock_irq,而__raw_spin_lock_irq 的實(shí)現(xiàn)為:
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock) {local_irq_disable();preempt_disable();spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }注意:“preempt_disable()”,這個調(diào)用的功能是“關(guān)搶占”(在spin_unlock中會重新開啟搶占功能)。從中可以看出,使用自旋鎖保護(hù)的區(qū)域是工作在非搶占的狀態(tài);即使獲取不到鎖,在“自旋”狀態(tài)也是禁止搶占的。了解到這,我想咱們應(yīng)該能夠理解為何自旋鎖保護(hù) 的代碼不能睡眠了。試想一下,如果在自旋鎖保護(hù)的代碼中間睡眠,此時發(fā)生進(jìn)程調(diào)度,則可能另外一個進(jìn)程會再次調(diào)用spinlock保護(hù)的這段代碼。而我們 現(xiàn)在知道了即使在獲取不到鎖的“自旋”狀態(tài),也是禁止搶占的,而“自旋”又是動態(tài)的,不會再睡眠了,也就是說在這個處理器上不會再有進(jìn)程調(diào)度發(fā)生了,那么死鎖自然就發(fā)生了。
由此可見,這兩者之間只有一個差別:是否調(diào)用local_irq_disable()函數(shù),即是否禁止本地中斷。這兩者的區(qū)別可以總結(jié)為:在任何情況下使用spin_lock_irq都是安全的。因?yàn)樗冉贡镜刂袛?#xff0c;又禁止內(nèi)核搶占。spin_lock比spin_lock_irq速度快,但是它并不是任何情況下都是安全的。
舉例來說明:進(jìn)程A中調(diào)用了spin_lock(&lock)然后進(jìn)入臨界區(qū),此時來了一個中斷(interrupt),該中斷也運(yùn)行在和進(jìn)程A相同的CPU上,并且在該中斷處理程序中恰巧也會spin_lock(&lock)試圖獲取同一個鎖。由于是在同一個CPU上被中斷,進(jìn)程A會被設(shè)置為TASK_INTERRUPT狀態(tài),中斷處理程序無法獲得鎖,會不停的忙等,由于進(jìn)程A被設(shè)置為中斷狀態(tài),schedule()進(jìn)程調(diào)度就無法再調(diào)度進(jìn)程A運(yùn)行,這樣就導(dǎo)致了死鎖!但是如果該中斷處理程序運(yùn)行在不同的CPU上就不會觸發(fā)死鎖。因?yàn)樵诓煌腃PU上出現(xiàn)中斷不會導(dǎo)致進(jìn)程A的狀態(tài)被設(shè)為TASK_INTERRUPT,只是換出。當(dāng)中斷處理程序忙等被換出后,進(jìn)程A還是有機(jī)會獲得CPU,執(zhí)行并退出臨界區(qū)。所以在使用spin_lock時要明確知道該鎖不會在中斷處理程序中使用。
自旋鎖可以使用在中斷處理程序中(此處不能使用信號量,因?yàn)樗鼈儠?dǎo)致睡眠)。在中斷處理程序中使用自旋鎖時,一定要在獲取鎖之前,先禁止本地中斷(在當(dāng)前處理器上的中斷請求),否則,中斷處理程序會打斷正在持有的鎖的內(nèi)核代碼,有可能會試圖去爭用這個已經(jīng)被持有的自旋鎖。這樣一來,中斷處理程序就會自旋,等待該鎖重新可用,但是鎖的持有者在這個中斷處理程序執(zhí)行完畢前不可能運(yùn)行(雙重請求死鎖)。注意,需要關(guān)閉的只是當(dāng)前處理器上的中斷。如果中斷發(fā)生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。
內(nèi)核提供的禁止中斷同時請求鎖的接口:spin_lock_irqsave的實(shí)現(xiàn)關(guān)系spin_lock_irqsave------>__raw_spin_lock_irqsave
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) {unsigned long flags;local_irq_save(flags);preempt_disable();spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);/** On lockdep we dont want the hand-coded irq-enable of* do_raw_spin_lock_flags() code, because lockdep assumes* that interrupts are not re-enabled during lock-acquire:*/ #ifdef CONFIG_LOCKDEPLOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); #elsedo_raw_spin_lock_flags(lock, &flags); #endifreturn flags; }使用方法:
DEFINE_SPINLOCK(mr_lock); unsigned long flags; spin_lock_irqsave(&mr_lock, flags); /*臨界區(qū)*/ spin_unlock_irqrestore(&mr_lock, flags);函數(shù)spin_lock_irqsave()保存中斷的當(dāng)前狀態(tài),并禁止本地中斷,所以再去獲取指定的鎖。反過來spin_unlock_irqrestore()對指定的鎖解鎖,然后讓中斷恢復(fù)到加鎖前的狀態(tài)。所以即使中斷最初是被禁止的,代碼也不會錯誤地激活它們,相反,會繼續(xù)讓它們禁止。注意,flags變量看起來像是由數(shù)值傳遞的,這是因?yàn)檫@些鎖函數(shù)有些部分是通過宏的方式實(shí)現(xiàn)的。在單處理器系統(tǒng)上,雖然在編譯時拋棄掉了鎖機(jī)制,但在上面的例子中仍需要關(guān)閉中斷,以禁止中斷處理程序訪問共享數(shù)據(jù)。加鎖和解鎖分別可以禁止和允許內(nèi)核搶占。
由此可見,自旋鎖比較適用于鎖使用者保持鎖時間比較短的情況。正是由于自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠(yuǎn)高于互斥鎖。信號量和讀寫信號量適合于保持時間較長的情況,它們會導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用,而自旋鎖適合于保持時間非常短的情況,它可以在任何上下文使用。
自旋鎖為什么廣泛用于內(nèi)核
自旋鎖是一種輕量級的互斥鎖,可以更高效的對互斥資源進(jìn)行保護(hù)。自旋鎖本來就只是一個很簡單的同步機(jī)制,在SMP之前根本就沒這個東西,一切都是Event之類的同步機(jī)制,這類同步機(jī)制都有一個共性就是:一旦資源被占用都會產(chǎn)生任務(wù)切換,任務(wù)切換涉及很多東西的(保存原來的上下文,按調(diào)度算法選擇新的任務(wù),恢復(fù)新任務(wù)的上下文,還有就是要修改cr3寄存器會導(dǎo)致cache失效)這些都是需要大量時間的,因此用Event之類來同步一旦涉及到阻塞代價(jià)是十分昂貴的,而自旋鎖的效率就遠(yuǎn)高于互斥鎖。
總結(jié)自旋鎖在不同CPU下工作的特點(diǎn):
(1)單CPU非搶占內(nèi)核下:自旋鎖會在編譯時被忽略(因?yàn)閱蜟PU且非搶占模式情況下,不可能發(fā)生進(jìn)程切換,時鐘只有一個進(jìn)程處于臨界區(qū)(自旋鎖實(shí)際沒什么用了)。
(2)單CPU搶占內(nèi)核下:自選鎖僅僅當(dāng)作一個設(shè)置搶占的開關(guān)(因?yàn)閱蜟PU不可能有并發(fā)訪問臨界區(qū)的情況,禁止搶占就可以保證臨街區(qū)唯一被擁有)。
(3)多CPU下:此時才能完全發(fā)揮自旋鎖的作用,自旋鎖在內(nèi)核中主要用來防止多處理器中并發(fā)訪問臨界區(qū),防止內(nèi)核搶占造成的競爭。
?
POSIX提供的與自旋鎖相關(guān)的函數(shù)
使用自旋鎖時要注意:由于自旋時不釋放CPU,因而持有自旋鎖的線程應(yīng)該盡快釋放自旋鎖,否則等待該自旋鎖的線程會一直在哪里自旋,這就會浪費(fèi)CPU時間。
持有自旋鎖的線程在sleep之前應(yīng)該釋放自旋鎖以便其他線程可以獲得該自旋鎖。內(nèi)核編程中,如果持有自旋鎖的代碼sleep了就可能導(dǎo)致整個系統(tǒng)掛起。(下面會解釋)使用任何鎖都需要消耗系統(tǒng)資源(內(nèi)存資源和CPU時間),這種資源消耗可以分為兩類:1.建立鎖所需要的資源? 2.當(dāng)線程被阻塞時所需要的資源。
int pthread_spin_init(pthread_spinlock_t*lock,int pshared);初始化spin lock,當(dāng)線程使用該函數(shù)初始化一個未初始化或者被destroy過的spin lock有效。該函數(shù)會為spin lock申請資源并且初始化spin lock為unlocked狀態(tài)。有關(guān)第二個選項(xiàng)是這么說的:If?the?Thread?Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_SHARED,the implementation shall permit the spin lock to be operated upon by any thread that has access to the memory?where?the?spin lock is allocated,even if it is allocated in memory that is shared by multiple processes.If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_PRIVATE,or if?the option is not supported,the spin lock shall only be operated upon by threads created within the same?process?as?the thread that initialized the spin lock.If threads of differing processes attempt to operate on such a spin lock,the behav‐ior is undefined.
所以,如果初始化spin lock的線程設(shè)置第二個參數(shù)為PTHREAD_PROCESS_SHARED,那么該spin lock不僅被初始化線程所在的進(jìn)程中所有線程看到,而且可以被其他進(jìn)程中的線程看到,PTHREAD_PROESS_PRIVATE則只被同一進(jìn)程中線程看到。如果不設(shè)置該參數(shù),默認(rèn)為后者。
int pthread_spin_destroy(pthread_spinlock_t*lock); 銷毀spin lock,作用和mutex的相關(guān)函數(shù)類似。 int pthread_spin_lock(pthread_spinlock_t*lock); 加鎖函數(shù),不過這么一點(diǎn)值得注意:EBUSY?A thread currently holds the lock。These functions shall not return an error code of[EINTR]. int pthread_spin_trylock(pthread_spinlock_t*lock); 試圖獲取指定的鎖 int pthread_spin_unlock(pthread_spinlock_t*lock); 解鎖函數(shù)。不是持有鎖的線程調(diào)用或者解鎖一個沒有l(wèi)ock的spin lock這樣的行為都是undefined的。?
信號量
Linux中的信號量是一種睡眠鎖。如果有一個任務(wù)試圖獲得一個不可用(已經(jīng)被占用)的信號量時,信號量會將其推進(jìn)一個等待隊(duì)列,然后讓其睡眠。這時處理器能重獲自由,從而去執(zhí)行其他代碼。當(dāng)持有的信號量可用(被釋放)后,處于等待隊(duì)列中的那個任務(wù)將被喚醒,并獲得該信號量。例如:當(dāng)某個人到了門前(鑰匙在門外,進(jìn)去房間的人持有鑰匙),此時房間(臨界區(qū))里沒有人,于是他就進(jìn)入房間并關(guān)上了門。最大的差異在于當(dāng)另外一個人想進(jìn)入房間,但無法進(jìn)入時,這家伙不是在徘徊,而是把自己的名字寫在一個列表中,然后去打盹。當(dāng)里面的人離開房間(釋放鑰匙)時,就在門口查看一下列表。如果列表上有名字,他就對第一個名字仔細(xì)檢查,并叫醒那個人讓他進(jìn)入房間,在這種方式中,鑰匙(相當(dāng)于信號量)確保一次只有一個人(相當(dāng)于執(zhí)行線程)進(jìn)入房間(臨界區(qū))。這就比自旋鎖提供了更好的處理器利用率,因?yàn)闆]有把時間花費(fèi)在忙等待上,但是,信號量比自旋鎖有更大的開銷??梢詮男盘柫康乃咛匦灾械贸鲆韵陆Y(jié)論:
(1)由于爭用信號量的進(jìn)程在等待鎖重新變?yōu)榭捎脮r會睡眠,所以信號量適用于鎖會被長時間持有的情況。?
(2)如果鎖被短時間持有時,此時不建議使用信號量。因?yàn)樗?、維護(hù)、等待隊(duì)列以及喚醒等操作,其所花費(fèi)的開銷可能要比鎖持有的全部時間還要長。
(3)由于執(zhí)行線程在鎖被爭用時會睡眠,所以只能在進(jìn)程上下文中才能獲取信號量鎖,因?yàn)樵谥袛嗌舷挛闹惺遣荒苓M(jìn)行調(diào)度的。
(4)可以在持有信號量時去睡眠,因?yàn)楫?dāng)其他進(jìn)程試圖獲得同一信號量時不會因此而死鎖(因?yàn)樵撨M(jìn)程也只是去睡眠而已,而前一個進(jìn)程最終會繼續(xù)執(zhí)行)。
(5)占用信號量的同時不能占用自旋鎖。因?yàn)樵谀愕却盘柫繒r可能會睡眠,而在持有自旋鎖時是不允許睡眠的。
在使用信號量的大多數(shù)時候,選擇余地并不大。往往在需要和用戶空間同步時,當(dāng)代碼需要睡眠,此時使用信號量是唯一的選擇。由于不受睡眠的限制,使用信號量通常來說更加容易一些。信號量不同于自旋鎖,它不會禁止內(nèi)核搶占,所以持有信號量的代碼可以被搶占。這意味著信號量不會對調(diào)度的等待時間帶來負(fù)面影響。
用戶搶占在以下情況下產(chǎn)生: 從系統(tǒng)調(diào)用返回用戶空間 從中斷處理程序返回用戶空間 內(nèi)核搶占會發(fā)生在: 當(dāng)從中斷處理程序返回內(nèi)核空間的時候,且當(dāng)時內(nèi)核具有可搶占性 當(dāng)內(nèi)核代碼再一次具有可搶占性的時候(如:spin_unlock時) 如果內(nèi)核中的任務(wù)顯示的調(diào)用schedule()計(jì)數(shù)信號量和二值信號量
信號量同時允許的持有者數(shù)量可以在聲明信號量時指定。這個值成為使用者數(shù)量或簡單的叫數(shù)量。通常情況下,信號量和自旋鎖一樣,在一個時刻僅允許有一個鎖的持有者。這時計(jì)數(shù)等于1,這樣的信號量被稱為二值信號量(因?yàn)樗蛘哂梢粋€任務(wù)特有,或者根本沒有任務(wù)持有它)或者稱為互斥信號量(因?yàn)樗鼜?qiáng)制進(jìn)行互斥)。另一方面,初始化時也可以把數(shù)量設(shè)置為大于1的非0值。這種情況,信號量被稱為計(jì)數(shù)信號量,它允許在一個時刻最多有count個鎖持有者。計(jì)數(shù)信號量不能用來進(jìn)行強(qiáng)制互斥,因?yàn)樗试S多個執(zhí)行線程同時訪問臨界區(qū)。相反,這種信號量用來對特定代碼加以限制,內(nèi)核中使用它的機(jī)會不多。在使用信號量時,基本上用到的都是互斥信號量(計(jì)數(shù)等于1的信號量)。
信號量是一種常見的鎖機(jī)制,它支持兩個原子操作P()和V(),后來的系統(tǒng)把這兩個操作分別叫做down()和up(),Linux也遵從這種叫法。down()操作通過對信號量計(jì)數(shù)減1來請求獲得一個信號量。如果結(jié)果是0或者大于0,獲得信號量鎖,任務(wù)就可以進(jìn)入臨界區(qū)。如果結(jié)果是負(fù)數(shù),任務(wù)會被放入等待隊(duì)列,處理器執(zhí)行其他任務(wù)。該函數(shù)如同一個動詞,降低(down)一個信號量就等于獲取該信號量。相反,當(dāng)臨界區(qū)中的操作完成后,up()操作用來釋放信號量,該操作也被稱作提升信號量,因?yàn)樗鼤黾有盘柫康挠?jì)數(shù)值。如果在該信號量上的等待隊(duì)列不為空,那么處于隊(duì)列中等待的任務(wù)在被喚醒的同時會獲得該信號量。
創(chuàng)建信號量和初始化信號量
信號量的實(shí)現(xiàn)是與體系結(jié)構(gòu)相關(guān)的,具體實(shí)現(xiàn)定義在文件<asm/semaphore.h>中。struct semaphore類型用來表示信號量??梢酝ㄟ^以下方式靜態(tài)地聲明信號量——其中name是信號量變量名,count是信號量的使用數(shù)量:
struct semaphore name; sema_init(&name, count);創(chuàng)建更為普通的互斥信號量可以使用以下快捷方式:
static DECLARE_MUTEX(name);//Linux 2.6.36以后,將#define DECLARE_MUTEX(name)改成了#define DEFINE_SEMAPHORE(name)更常見的情況是,信號量作為一個數(shù)據(jù)結(jié)構(gòu)的一部分動態(tài)創(chuàng)建。此時,只有指向該動態(tài)創(chuàng)建的信號量的間接指針,可以使用如下函數(shù)來對其進(jìn)行初始化:
sema_init(sem, count); ??//sem是指針,count是信號量的使用者數(shù)量。與前面情況類似,初始化一個動態(tài)創(chuàng)建的互斥信號量時,使用如下函數(shù):
init_MUTEX(sem);?
互斥體(互斥鎖)
內(nèi)核中唯一允許睡眠的鎖是信號量。多數(shù)用戶使用信號量只使用計(jì)數(shù)1,說白了是把其作為一個互斥的排他鎖使用。信號量的用途更通用,沒有多少使用限制。這點(diǎn)使得信號量適合用于那些較為復(fù)雜的、未明情況下的互斥訪問,比如內(nèi)核與用戶空間復(fù)雜的交互行為。但這也意味著簡單的鎖定而使用信號量并不方便,并且信號量也缺乏強(qiáng)制的規(guī)則來行使任何形式的自動調(diào)試,即便受限的調(diào)試也不可能。為了找到一個更簡單睡眠鎖,內(nèi)核開發(fā)者們引入了互斥體(mutex)。互斥體這個稱謂所指的是任何可以睡眠的強(qiáng)制互斥鎖,比如使用計(jì)數(shù)是1的信號量。但是在Linux內(nèi)核2.6.34中,互斥體這個稱謂也用于一種實(shí)現(xiàn)互斥的特定睡眠鎖。也就是說,互斥體是一種互斥信號。
mutex在內(nèi)核中對應(yīng)數(shù)據(jù)結(jié)構(gòu)mutex,其行為和使用計(jì)數(shù)為1的信號量類似,但操作接口更簡單,實(shí)現(xiàn)也更為高效,而且使用限制更強(qiáng)。靜態(tài)定義mutex,你需要做:
DEFINE_MUTEX(name); 動態(tài)初始化mutex: mutex_init(&mutex);對互斥鎖加鎖和解鎖:
mutex_lock(&mutex); /*臨界區(qū)*/ mutex_unlock(&mutex);Mutex方法:
mutex_lock(struct mutex*) ??????為指定的mutex上鎖,如果鎖不可用則睡眠 mutex_unlock(struct mutex*) ????為指定的mutex解鎖 mutex_trylock(struct mutex*) ???試圖獲取指定的mutex,成功返回1;否則,返回0 mutex_is_lock(struct mutex*) ???如果鎖已被爭用,則返回1;否則返回0C語言的多線程編程中,互斥鎖的初始化
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex) int pthread_mutex_unlock(pthread_mutex_t *mutex);pthread_mutex_init()?函數(shù)是以動態(tài)方式創(chuàng)建互斥鎖的,參數(shù)attr指定了新建互斥鎖的屬性。如果參數(shù)attr為空,則使用默認(rèn)的互斥鎖屬性,默認(rèn)屬性為快速互斥鎖 ?;コ怄i的屬性在創(chuàng)建鎖的時候指定,在LinuxThreads實(shí)現(xiàn)中僅有一個鎖類型屬性,不同的鎖類型在試圖對一個已經(jīng)被鎖定的互斥鎖加鎖時表現(xiàn)不同。pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經(jīng)被占據(jù)時返回EBUSY而不是掛起等待。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 初始化一個快速鎖的宏定義 pthread_mutex_lock(&mutex); /*中間代碼*/ pthread_mutex_unlock(&mutex); 函數(shù)成功執(zhí)行后,互斥鎖被初始化為未鎖住態(tài)。互斥鎖例子:
#include <stdlib.h> #include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h>/*全局變量*/ int sum = 0; /*互斥量 */ pthread_mutex_t mutex; /*聲明線程運(yùn)行服務(wù)程序*/ void* pthread_function1 (void*); void* pthread_function2 (void*);int main (void) {/*線程的標(biāo)識符*/pthread_t pt_1 = 0;pthread_t pt_2 = 0;int ret = 0;/*互斥初始化*/pthread_mutex_init (&mutex, NULL);/*分別創(chuàng)建線程1、2*/ret = pthread_create( &pt_1, //線程標(biāo)識符指針NULL, //默認(rèn)屬性pthread_function1, //運(yùn)行函數(shù)NULL); //無參數(shù)if (ret != 0){perror ("pthread_1_create");}ret = pthread_create( &pt_2, //線程標(biāo)識符指針NULL, //默認(rèn)屬性pthread_function2, //運(yùn)行函數(shù)NULL); //無參數(shù)if (ret != 0){perror ("pthread_2_create");}/*等待線程1、2的結(jié)束*/pthread_join (pt_1, NULL);pthread_join (pt_2, NULL);printf ("main programme exit!\n");return 0; }/*線程1的服務(wù)程序*/ void* pthread_function1 (void*a) {int i = 0;printf ("This is pthread_1!\n");for( i=0; i<3; i++ ){pthread_mutex_lock(&mutex); /*獲取互斥鎖*//*臨界資源*/sum++;printf ("Thread_1 add one to num:%d\n",sum);pthread_mutex_unlock(&mutex); /*釋放互斥鎖*//*注意,這里以防線程的搶占,以造成一個線程在另一個線程sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖后調(diào)用*/sleep (1);}pthread_exit ( NULL ); }/*線程2的服務(wù)程序*/ void* pthread_function2 (void*a) {int i = 0;printf ("This is pthread_2!\n");for( i=0; i<5; i++ ){pthread_mutex_lock(&mutex); /*獲取互斥鎖*//*臨界資源*/sum++;printf ("Thread_2 add one to num:%d\n",sum);pthread_mutex_unlock(&mutex); /*釋放互斥鎖*//*注意,這里以防線程的搶占,以造成一個線程在另一個線程sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖后調(diào)用*/sleep (1);}pthread_exit ( NULL ); }Linux下編譯時需要加 -lpthread注意第一個字母是大寫,windows C語言中單位是毫秒(ms)。 Sleep (500); 就是到這里停半秒,然后繼續(xù)向下執(zhí)行。 包含在#include <windows.h>頭文件在Linux C語言中 sleep的單位是秒(s) sleep(5);//停5秒 包含在 #include <unistd.h>頭文件mutex的簡潔性和高效性源于相比使用信號量更多的受限性。它不同于信號量,其使用場景相對而言更嚴(yán)格、更定向了。
(1)任何時刻中只有一個任務(wù)可以持有mutex,也就是說,mutex的使用計(jì)數(shù)永遠(yuǎn)是1。
(2)給mutex上鎖者必須負(fù)責(zé)給其再解鎖——你不能再上下文中鎖定一個mutex,而在另一個上下文中給它解鎖。這個限制使得mutex不適合內(nèi)核同用戶空間復(fù)雜的同步場景。最常使用的方式是:在同一個上下文中上鎖和解鎖。
(3)遞歸地上鎖和解鎖是不允許的。也就是說,你不能遞歸地持有同一個鎖,同樣你也不能再去解一個已經(jīng)解開的mutex。
(4)當(dāng)持有一個mutex時,進(jìn)程不可以退出。
(5)mutex不能再中斷或者下半部中使用,即使使用mutex_trylock()也不行。整個中斷處理流程被分為兩個部分,中斷處理程序?yàn)樯习氩俊6掳氩康娜蝿?wù)是執(zhí)行與中斷處理密切相關(guān)但中斷處理程序本身不執(zhí)行的工作。
(6)mutex只能通過官方API管理:它不可被拷貝、手動初始化或者重復(fù)初始化。
從實(shí)現(xiàn)原理上來講,Mutex屬于sleep-waiting類型的鎖。例如在一個雙核的機(jī)器上有兩個線程(線程A和線程B),它們分別運(yùn)行在Core0和Core1上。假設(shè)線程A想要通過 pthread_mutex_lock操作去得到一個臨界區(qū)的鎖,而此時這個鎖正被線程B所持有,那么線程A就會被阻塞(blocking),Core0 會在此時進(jìn)行上下文切換(Context Switch)將線程A置于等待隊(duì)列中,此時Core0就可以運(yùn)行其他的任務(wù)(例如另一個線程C)而不必進(jìn)行忙等待。而Spin lock則不然,它屬于busy-waiting類型的鎖,如果線程A是使用pthread_spin_lock操作去請求鎖,那么線程A就會一直在 Core0上進(jìn)行忙等待并不停的進(jìn)行鎖請求,直到得到這個鎖為止。
?
信號量和互斥體
互斥體和信號量很相似,內(nèi)核中兩者共存會令人混淆。所幸,它們的標(biāo)準(zhǔn)使用方式都有簡單的規(guī)范:除非mutex的某個約束妨礙你使用,否則相比信號量要優(yōu)先使用mutex。如果你所寫的是很底層的代碼,才會需要使用信號量。如果發(fā)現(xiàn)不能滿足其約束條件,且沒有其他別的選擇時,再考慮選擇信號量。
自旋鎖和互斥體
對于自旋鎖來說,它只需要消耗很少的資源來建立鎖;隨后當(dāng)線程被阻塞時,它就會一直重復(fù)檢查看鎖是否可用了,也就是說當(dāng)自旋鎖處于等待狀態(tài)時它會一直消耗CPU時間。
 對于互斥鎖來說,與自旋鎖相比它需要消耗大量的系統(tǒng)資源來建立鎖;隨后當(dāng)線程被阻塞時,線程的調(diào)度狀態(tài)被修改,并且線程被加入等待線程隊(duì)列;最后當(dāng)鎖可用 時,在獲取鎖之前,線程會被從等待隊(duì)列取出并更改其調(diào)度狀態(tài);但是在線程被阻塞期間,它不消耗CPU資源。
因此自旋鎖和互斥鎖適用于不同的場景。自旋鎖適用于那些僅需要阻塞很短時間的場景,而互斥鎖適用于那些可能會阻塞很長時間的場景。還有一點(diǎn)是在中斷上下文中只能使用自旋鎖,而在任務(wù)睡眠時只能使用互斥體。
?
參考:
《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》
https://www.cnblogs.com/kuliuheng/p/4064680.html
https://www.cnblogs.com/aaronLinux/p/5890924.html
https://blog.csdn.net/wh_19910525/article/details/11536279
https://blog.csdn.net/freeelinux/article/details/53695111
總結(jié)
以上是生活随笔為你收集整理的自旋锁、互斥锁和信号量的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: C/C++线程基本函数
 - 下一篇: 可重入锁(递归锁) 互斥锁属性设置