自旋锁、互斥锁和信号量
自旋鎖
Linux內(nèi)核中最常見的鎖是自旋鎖(spin lock)。自旋鎖最多只能被一個(gè)可執(zhí)行線程持有。如果一個(gè)執(zhí)行線程試圖獲得一個(gè)已經(jīng)被持有的自旋鎖,那么該線程就會(huì)一直進(jìn)行忙循環(huán)——旋轉(zhuǎn)——等待鎖重新可用。要是鎖未被爭(zhēng)用,請(qǐng)求鎖的執(zhí)行線程便能立刻得到它,繼續(xù)執(zhí)行。在任意時(shí)間,自旋鎖都可以防止多于一個(gè)的執(zhí)行線程同時(shí)進(jìn)入臨界區(qū)。同一個(gè)鎖可以用在多個(gè)位置。例如,對(duì)于給定數(shù)據(jù)的所有訪問都可以得到保護(hù)和同步。
自旋鎖相當(dāng)于上廁所時(shí),在門外等待的過程。如果你到在廁所門外,發(fā)現(xiàn)里面沒有人,就可以推開門進(jìn)入廁所。如果你到了廁所門口發(fā)現(xiàn)門是關(guān)著的(里面有人),就必須在門口等待(此時(shí)你很著急),不斷地檢查廁所是否為空。當(dāng)廁所為空時(shí),你就可以進(jìn)入了。正是因?yàn)橛辛碎T(相當(dāng)于自旋鎖),才允許一次只有一個(gè)人(相當(dāng)于執(zhí)行線程)進(jìn)入廁所里(相當(dāng)于臨界區(qū))。
一個(gè)被爭(zhēng)用的自旋鎖使得請(qǐng)求它的線程在等待鎖重新可用時(shí)自旋(特別浪費(fèi)處理器時(shí)間),這種行為是自旋鎖的要點(diǎn)。所以自旋鎖不應(yīng)該被長時(shí)間的持有。事實(shí)上,這點(diǎn)正是使用自旋鎖的初衷:在段期間內(nèi)進(jìn)行輕量級(jí)加鎖。還可以采取另外的方式來處理對(duì)鎖的爭(zhēng)用:讓請(qǐng)求線程睡眠,直到鎖重新可用時(shí)再喚醒它。這樣處理器就不必循環(huán)等待,可以去執(zhí)行其他代碼。這也會(huì)帶來一定的開銷——這里有兩次明顯的上下文切換,被阻塞的線程要換出和換入,與實(shí)現(xiàn)自旋鎖的少數(shù)幾行代碼相比,上下文切換當(dāng)然有較多的代碼。因此,持有自旋鎖的時(shí)間最好小于完成兩次上下文切換的耗時(shí)。當(dāng)然我們大多數(shù)人都不會(huì)無聊到去測(cè)量上下文切換的耗時(shí),所以我們讓持有自旋鎖的時(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在同一時(shí)刻最多被一個(gè)執(zhí)行線程持有,所以一個(gè)時(shí)刻只能有一個(gè)線程位于臨界區(qū)內(nèi),這就為多處理器機(jī)器提供了防止并發(fā)訪問所需的保護(hù)機(jī)制。注意:在單處理器機(jī)器上,編譯的時(shí)候并不會(huì)加入自旋鎖。它僅僅被當(dāng)做一個(gè)設(shè)置內(nèi)核搶占機(jī)制是否被啟用的開關(guān)。如果禁止內(nèi)核搶占,那么在編譯時(shí)自旋鎖會(huì)被完全剔除出內(nèi)核。
注意:自旋鎖是不可遞歸的
Linux內(nèi)核實(shí)現(xiàn)的自旋鎖是不可遞歸的,這點(diǎn)不同于自旋鎖在其他操作系統(tǒng)中的實(shí)現(xiàn)。所以如果你試圖得到一個(gè)你正持有的鎖,你必須自旋,等待你自己釋放這個(gè)鎖。由于你處于自旋忙等待,所以你永遠(yuǎn)沒有機(jī)會(huì)釋放鎖,于是你被自己鎖死了。
自旋鎖可能帶來的問題
(1)死鎖。試圖遞歸地獲得自旋鎖必然會(huì)引起死鎖:例如遞歸程序的持有實(shí)例在第二個(gè)實(shí)例循環(huán),以試圖獲得相同自旋鎖時(shí),就不會(huì)釋放此自旋鎖。所以,在遞歸程序中使用自旋鎖應(yīng)遵守下列策略:遞歸程序決不能在持有自旋鎖時(shí)調(diào)用它自己,也決不能在遞歸調(diào)用時(shí)試圖獲得相同的自旋鎖。此外如果一個(gè)進(jìn)程已經(jīng)將資源鎖定,那么,即使其它申請(qǐng)這個(gè)資源的進(jìn)程不停地瘋狂“自旋”,也無法獲得資源,從而進(jìn)入死循環(huán)。
(2)過多占用CPU資源。如果不加限制,由于申請(qǐng)者一直在循環(huán)等待,因此自旋鎖在鎖定的時(shí)候,如果不成功,不會(huì)睡眠,會(huì)持續(xù)的嘗試,單CPU的時(shí)候自旋鎖會(huì)讓其它process動(dòng)不了。因此,一般自旋鎖實(shí)現(xiàn)會(huì)有一個(gè)參數(shù)限定最多持續(xù)嘗試次數(shù)。超出后,自旋鎖放棄當(dāng)前time slice,等下一次機(jī)會(huì)。
自旋鎖的操作
spin_lock_init():可以使用該方法來初始化動(dòng)態(tài)創(chuàng)建的自旋鎖(此時(shí)你只有一個(gè)指向spinlock_t類型的指針,沒有它的實(shí)體)。
spin_try_lock():試圖獲得某個(gè)特定的自旋鎖,如果該鎖已經(jīng)被爭(zhēng)用,那么該方法會(huì)立即返回一個(gè)非0值,而不會(huì)自旋等待鎖被釋放;如果成功地獲得了這個(gè)自旋鎖,該函數(shù)返回0。同理,spin_is_locked()方法用于檢查特定的鎖當(dāng)前是否已被占用,如果被占用,返回非0值;否則返回0。該方法只做判斷,并不實(shí)際占用。
標(biāo)準(zhǔn)的自旋鎖操作的完整列表:
| spin_lock_init(lock) | 初始化自旋鎖,將自旋鎖設(shè)置為1,表示有一個(gè)資源可用。 |
| 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()”,這個(gè)調(diào)用的功能是“關(guān)搶占”(在spin_unlock中會(huì)重新開啟搶占功能)。從中可以看出,使用自旋鎖保護(hù)的區(qū)域是工作在非搶占的狀態(tài);即使獲取不到鎖,在“自旋”狀態(tài)也是禁止搶占的。了解到這,我想咱們應(yīng)該能夠理解為何自旋鎖保護(hù) 的代碼不能睡眠了。試想一下,如果在自旋鎖保護(hù)的代碼中間睡眠,此時(shí)發(fā)生進(jìn)程調(diào)度,則可能另外一個(gè)進(jìn)程會(huì)再次調(diào)用spinlock保護(hù)的這段代碼。而我們 現(xiàn)在知道了即使在獲取不到鎖的“自旋”狀態(tài),也是禁止搶占的,而“自旋”又是動(dòng)態(tài)的,不會(huì)再睡眠了,也就是說在這個(gè)處理器上不會(huì)再有進(jìn)程調(diào)度發(fā)生了,那么死鎖自然就發(fā)生了。
由此可見,這兩者之間只有一個(gè)差別:是否調(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ū),此時(shí)來了一個(gè)中斷(interrupt),該中斷也運(yùn)行在和進(jìn)程A相同的CPU上,并且在該中斷處理程序中恰巧也會(huì)spin_lock(&lock)試圖獲取同一個(gè)鎖。由于是在同一個(gè)CPU上被中斷,進(jìn)程A會(huì)被設(shè)置為TASK_INTERRUPT狀態(tài),中斷處理程序無法獲得鎖,會(huì)不停的忙等,由于進(jìn)程A被設(shè)置為中斷狀態(tài),schedule()進(jìn)程調(diào)度就無法再調(diào)度進(jìn)程A運(yùn)行,這樣就導(dǎo)致了死鎖!但是如果該中斷處理程序運(yùn)行在不同的CPU上就不會(huì)觸發(fā)死鎖。因?yàn)樵诓煌腃PU上出現(xiàn)中斷不會(huì)導(dǎo)致進(jìn)程A的狀態(tài)被設(shè)為TASK_INTERRUPT,只是換出。當(dāng)中斷處理程序忙等被換出后,進(jìn)程A還是有機(jī)會(huì)獲得CPU,執(zhí)行并退出臨界區(qū)。所以在使用spin_lock時(shí)要明確知道該鎖不會(huì)在中斷處理程序中使用。
自旋鎖可以使用在中斷處理程序中(此處不能使用信號(hào)量,因?yàn)樗鼈儠?huì)導(dǎo)致睡眠)。在中斷處理程序中使用自旋鎖時(shí),一定要在獲取鎖之前,先禁止本地中斷(在當(dāng)前處理器上的中斷請(qǐng)求),否則,中斷處理程序會(huì)打斷正在持有的鎖的內(nèi)核代碼,有可能會(huì)試圖去爭(zhēng)用這個(gè)已經(jīng)被持有的自旋鎖。這樣一來,中斷處理程序就會(huì)自旋,等待該鎖重新可用,但是鎖的持有者在這個(gè)中斷處理程序執(zhí)行完畢前不可能運(yùn)行(雙重請(qǐng)求死鎖)。注意,需要關(guān)閉的只是當(dāng)前處理器上的中斷。如果中斷發(fā)生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會(huì)妨礙鎖的持有者(在不同處理器上)最終釋放鎖。
內(nèi)核提供的禁止中斷同時(shí)請(qǐng)求鎖的接口: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()對(duì)指定的鎖解鎖,然后讓中斷恢復(fù)到加鎖前的狀態(tài)。所以即使中斷最初是被禁止的,代碼也不會(huì)錯(cuò)誤地激活它們,相反,會(huì)繼續(xù)讓它們禁止。注意,flags變量看起來像是由數(shù)值傳遞的,這是因?yàn)檫@些鎖函數(shù)有些部分是通過宏的方式實(shí)現(xiàn)的。在單處理器系統(tǒng)上,雖然在編譯時(shí)拋棄掉了鎖機(jī)制,但在上面的例子中仍需要關(guān)閉中斷,以禁止中斷處理程序訪問共享數(shù)據(jù)。加鎖和解鎖分別可以禁止和允許內(nèi)核搶占。
由此可見,自旋鎖比較適用于鎖使用者保持鎖時(shí)間比較短的情況。正是由于自旋鎖使用者一般保持鎖時(shí)間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠(yuǎn)高于互斥鎖。信號(hào)量和讀寫信號(hào)量適合于保持時(shí)間較長的情況,它們會(huì)導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用,而自旋鎖適合于保持時(shí)間非常短的情況,它可以在任何上下文使用。
自旋鎖為什么廣泛用于內(nèi)核
自旋鎖是一種輕量級(jí)的互斥鎖,可以更高效的對(duì)互斥資源進(jìn)行保護(hù)。自旋鎖本來就只是一個(gè)很簡單的同步機(jī)制,在SMP之前根本就沒這個(gè)東西,一切都是Event之類的同步機(jī)制,這類同步機(jī)制都有一個(gè)共性就是:一旦資源被占用都會(huì)產(chǎn)生任務(wù)切換,任務(wù)切換涉及很多東西的(保存原來的上下文,按調(diào)度算法選擇新的任務(wù),恢復(fù)新任務(wù)的上下文,還有就是要修改cr3寄存器會(huì)導(dǎo)致cache失效)這些都是需要大量時(shí)間的,因此用Event之類來同步一旦涉及到阻塞代價(jià)是十分昂貴的,而自旋鎖的效率就遠(yuǎn)高于互斥鎖。
總結(jié)自旋鎖在不同CPU下工作的特點(diǎn):
(1)單CPU非搶占內(nèi)核下:自旋鎖會(huì)在編譯時(shí)被忽略(因?yàn)閱蜟PU且非搶占模式情況下,不可能發(fā)生進(jìn)程切換,時(shí)鐘只有一個(gè)進(jìn)程處于臨界區(qū)(自旋鎖實(shí)際沒什么用了)。
(2)單CPU搶占內(nèi)核下:自選鎖僅僅當(dāng)作一個(gè)設(shè)置搶占的開關(guān)(因?yàn)閱蜟PU不可能有并發(fā)訪問臨界區(qū)的情況,禁止搶占就可以保證臨街區(qū)唯一被擁有)。
(3)多CPU下:此時(shí)才能完全發(fā)揮自旋鎖的作用,自旋鎖在內(nèi)核中主要用來防止多處理器中并發(fā)訪問臨界區(qū),防止內(nèi)核搶占造成的競(jìng)爭(zhēng)。
?
POSIX提供的與自旋鎖相關(guān)的函數(shù)
使用自旋鎖時(shí)要注意:由于自旋時(shí)不釋放CPU,因而持有自旋鎖的線程應(yīng)該盡快釋放自旋鎖,否則等待該自旋鎖的線程會(huì)一直在哪里自旋,這就會(huì)浪費(fèi)CPU時(shí)間。
持有自旋鎖的線程在sleep之前應(yīng)該釋放自旋鎖以便其他線程可以獲得該自旋鎖。內(nèi)核編程中,如果持有自旋鎖的代碼sleep了就可能導(dǎo)致整個(gè)系統(tǒng)掛起。(下面會(huì)解釋)使用任何鎖都需要消耗系統(tǒng)資源(內(nèi)存資源和CPU時(shí)間),這種資源消耗可以分為兩類:1.建立鎖所需要的資源? 2.當(dāng)線程被阻塞時(shí)所需要的資源。
int pthread_spin_init(pthread_spinlock_t*lock,int pshared);初始化spin lock,當(dāng)線程使用該函數(shù)初始化一個(gè)未初始化或者被destroy過的spin lock有效。該函數(shù)會(huì)為spin lock申請(qǐng)資源并且初始化spin lock為unlocked狀態(tài)。有關(guān)第二個(gè)選項(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è)置第二個(gè)參數(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)用或者解鎖一個(gè)沒有l(wèi)ock的spin lock這樣的行為都是undefined的。?
信號(hào)量
Linux中的信號(hào)量是一種睡眠鎖。如果有一個(gè)任務(wù)試圖獲得一個(gè)不可用(已經(jīng)被占用)的信號(hào)量時(shí),信號(hào)量會(huì)將其推進(jìn)一個(gè)等待隊(duì)列,然后讓其睡眠。這時(shí)處理器能重獲自由,從而去執(zhí)行其他代碼。當(dāng)持有的信號(hào)量可用(被釋放)后,處于等待隊(duì)列中的那個(gè)任務(wù)將被喚醒,并獲得該信號(hào)量。例如:當(dāng)某個(gè)人到了門前(鑰匙在門外,進(jìn)去房間的人持有鑰匙),此時(shí)房間(臨界區(qū))里沒有人,于是他就進(jìn)入房間并關(guān)上了門。最大的差異在于當(dāng)另外一個(gè)人想進(jìn)入房間,但無法進(jìn)入時(shí),這家伙不是在徘徊,而是把自己的名字寫在一個(gè)列表中,然后去打盹。當(dāng)里面的人離開房間(釋放鑰匙)時(shí),就在門口查看一下列表。如果列表上有名字,他就對(duì)第一個(gè)名字仔細(xì)檢查,并叫醒那個(gè)人讓他進(jìn)入房間,在這種方式中,鑰匙(相當(dāng)于信號(hào)量)確保一次只有一個(gè)人(相當(dāng)于執(zhí)行線程)進(jìn)入房間(臨界區(qū))。這就比自旋鎖提供了更好的處理器利用率,因?yàn)闆]有把時(shí)間花費(fèi)在忙等待上,但是,信號(hào)量比自旋鎖有更大的開銷。可以從信號(hào)量的睡眠特性中得出以下結(jié)論:
(1)由于爭(zhēng)用信號(hào)量的進(jìn)程在等待鎖重新變?yōu)榭捎脮r(shí)會(huì)睡眠,所以信號(hào)量適用于鎖會(huì)被長時(shí)間持有的情況。?
(2)如果鎖被短時(shí)間持有時(shí),此時(shí)不建議使用信號(hào)量。因?yàn)樗摺⒕S護(hù)、等待隊(duì)列以及喚醒等操作,其所花費(fèi)的開銷可能要比鎖持有的全部時(shí)間還要長。
(3)由于執(zhí)行線程在鎖被爭(zhēng)用時(shí)會(huì)睡眠,所以只能在進(jìn)程上下文中才能獲取信號(hào)量鎖,因?yàn)樵谥袛嗌舷挛闹惺遣荒苓M(jìn)行調(diào)度的。
(4)可以在持有信號(hào)量時(shí)去睡眠,因?yàn)楫?dāng)其他進(jìn)程試圖獲得同一信號(hào)量時(shí)不會(huì)因此而死鎖(因?yàn)樵撨M(jìn)程也只是去睡眠而已,而前一個(gè)進(jìn)程最終會(huì)繼續(xù)執(zhí)行)。
(5)占用信號(hào)量的同時(shí)不能占用自旋鎖。因?yàn)樵谀愕却盘?hào)量時(shí)可能會(huì)睡眠,而在持有自旋鎖時(shí)是不允許睡眠的。
在使用信號(hào)量的大多數(shù)時(shí)候,選擇余地并不大。往往在需要和用戶空間同步時(shí),當(dāng)代碼需要睡眠,此時(shí)使用信號(hào)量是唯一的選擇。由于不受睡眠的限制,使用信號(hào)量通常來說更加容易一些。信號(hào)量不同于自旋鎖,它不會(huì)禁止內(nèi)核搶占,所以持有信號(hào)量的代碼可以被搶占。這意味著信號(hào)量不會(huì)對(duì)調(diào)度的等待時(shí)間帶來負(fù)面影響。
用戶搶占在以下情況下產(chǎn)生: 從系統(tǒng)調(diào)用返回用戶空間 從中斷處理程序返回用戶空間 內(nèi)核搶占會(huì)發(fā)生在: 當(dāng)從中斷處理程序返回內(nèi)核空間的時(shí)候,且當(dāng)時(shí)內(nèi)核具有可搶占性 當(dāng)內(nèi)核代碼再一次具有可搶占性的時(shí)候(如:spin_unlock時(shí)) 如果內(nèi)核中的任務(wù)顯示的調(diào)用schedule()計(jì)數(shù)信號(hào)量和二值信號(hào)量
信號(hào)量同時(shí)允許的持有者數(shù)量可以在聲明信號(hào)量時(shí)指定。這個(gè)值成為使用者數(shù)量或簡單的叫數(shù)量。通常情況下,信號(hào)量和自旋鎖一樣,在一個(gè)時(shí)刻僅允許有一個(gè)鎖的持有者。這時(shí)計(jì)數(shù)等于1,這樣的信號(hào)量被稱為二值信號(hào)量(因?yàn)樗蛘哂梢粋€(gè)任務(wù)特有,或者根本沒有任務(wù)持有它)或者稱為互斥信號(hào)量(因?yàn)樗鼜?qiáng)制進(jìn)行互斥)。另一方面,初始化時(shí)也可以把數(shù)量設(shè)置為大于1的非0值。這種情況,信號(hào)量被稱為計(jì)數(shù)信號(hào)量,它允許在一個(gè)時(shí)刻最多有count個(gè)鎖持有者。計(jì)數(shù)信號(hào)量不能用來進(jìn)行強(qiáng)制互斥,因?yàn)樗试S多個(gè)執(zhí)行線程同時(shí)訪問臨界區(qū)。相反,這種信號(hào)量用來對(duì)特定代碼加以限制,內(nèi)核中使用它的機(jī)會(huì)不多。在使用信號(hào)量時(shí),基本上用到的都是互斥信號(hào)量(計(jì)數(shù)等于1的信號(hào)量)。
信號(hào)量是一種常見的鎖機(jī)制,它支持兩個(gè)原子操作P()和V(),后來的系統(tǒng)把這兩個(gè)操作分別叫做down()和up(),Linux也遵從這種叫法。down()操作通過對(duì)信號(hào)量計(jì)數(shù)減1來請(qǐng)求獲得一個(gè)信號(hào)量。如果結(jié)果是0或者大于0,獲得信號(hào)量鎖,任務(wù)就可以進(jìn)入臨界區(qū)。如果結(jié)果是負(fù)數(shù),任務(wù)會(huì)被放入等待隊(duì)列,處理器執(zhí)行其他任務(wù)。該函數(shù)如同一個(gè)動(dòng)詞,降低(down)一個(gè)信號(hào)量就等于獲取該信號(hào)量。相反,當(dāng)臨界區(qū)中的操作完成后,up()操作用來釋放信號(hào)量,該操作也被稱作提升信號(hào)量,因?yàn)樗鼤?huì)增加信號(hào)量的計(jì)數(shù)值。如果在該信號(hào)量上的等待隊(duì)列不為空,那么處于隊(duì)列中等待的任務(wù)在被喚醒的同時(shí)會(huì)獲得該信號(hào)量。
創(chuàng)建信號(hào)量和初始化信號(hào)量
信號(hào)量的實(shí)現(xiàn)是與體系結(jié)構(gòu)相關(guān)的,具體實(shí)現(xiàn)定義在文件<asm/semaphore.h>中。struct semaphore類型用來表示信號(hào)量。可以通過以下方式靜態(tài)地聲明信號(hào)量——其中name是信號(hào)量變量名,count是信號(hào)量的使用數(shù)量:
struct semaphore name; sema_init(&name, count);創(chuàng)建更為普通的互斥信號(hào)量可以使用以下快捷方式:
static DECLARE_MUTEX(name);//Linux 2.6.36以后,將#define DECLARE_MUTEX(name)改成了#define DEFINE_SEMAPHORE(name)更常見的情況是,信號(hào)量作為一個(gè)數(shù)據(jù)結(jié)構(gòu)的一部分動(dòng)態(tài)創(chuàng)建。此時(shí),只有指向該動(dòng)態(tài)創(chuàng)建的信號(hào)量的間接指針,可以使用如下函數(shù)來對(duì)其進(jìn)行初始化:
sema_init(sem, count); ??//sem是指針,count是信號(hào)量的使用者數(shù)量。與前面情況類似,初始化一個(gè)動(dòng)態(tài)創(chuàng)建的互斥信號(hào)量時(shí),使用如下函數(shù):
init_MUTEX(sem);?
互斥體(互斥鎖)
內(nèi)核中唯一允許睡眠的鎖是信號(hào)量。多數(shù)用戶使用信號(hào)量只使用計(jì)數(shù)1,說白了是把其作為一個(gè)互斥的排他鎖使用。信號(hào)量的用途更通用,沒有多少使用限制。這點(diǎn)使得信號(hào)量適合用于那些較為復(fù)雜的、未明情況下的互斥訪問,比如內(nèi)核與用戶空間復(fù)雜的交互行為。但這也意味著簡單的鎖定而使用信號(hào)量并不方便,并且信號(hào)量也缺乏強(qiáng)制的規(guī)則來行使任何形式的自動(dòng)調(diào)試,即便受限的調(diào)試也不可能。為了找到一個(gè)更簡單睡眠鎖,內(nèi)核開發(fā)者們引入了互斥體(mutex)。互斥體這個(gè)稱謂所指的是任何可以睡眠的強(qiáng)制互斥鎖,比如使用計(jì)數(shù)是1的信號(hào)量。但是在Linux內(nèi)核2.6.34中,互斥體這個(gè)稱謂也用于一種實(shí)現(xiàn)互斥的特定睡眠鎖。也就是說,互斥體是一種互斥信號(hào)。
mutex在內(nèi)核中對(duì)應(yīng)數(shù)據(jù)結(jié)構(gòu)mutex,其行為和使用計(jì)數(shù)為1的信號(hào)量類似,但操作接口更簡單,實(shí)現(xiàn)也更為高效,而且使用限制更強(qiáng)。靜態(tài)定義mutex,你需要做:
DEFINE_MUTEX(name); 動(dòng)態(tài)初始化mutex: mutex_init(&mutex);對(duì)互斥鎖加鎖和解鎖:
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*) ???如果鎖已被爭(zhēng)用,則返回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ù)是以動(dòng)態(tài)方式創(chuàng)建互斥鎖的,參數(shù)attr指定了新建互斥鎖的屬性。如果參數(shù)attr為空,則使用默認(rèn)的互斥鎖屬性,默認(rèn)屬性為快速互斥鎖 。互斥鎖的屬性在創(chuàng)建鎖的時(shí)候指定,在LinuxThreads實(shí)現(xiàn)中僅有一個(gè)鎖類型屬性,不同的鎖類型在試圖對(duì)一個(gè)已經(jīng)被鎖定的互斥鎖加鎖時(shí)表現(xiàn)不同。pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經(jīng)被占據(jù)時(shí)返回EBUSY而不是掛起等待。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 初始化一個(gè)快速鎖的宏定義 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)識(shí)符*/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)識(shí)符指針NULL, //默認(rèn)屬性pthread_function1, //運(yùn)行函數(shù)NULL); //無參數(shù)if (ret != 0){perror ("pthread_1_create");}ret = pthread_create( &pt_2, //線程標(biāo)識(shí)符指針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); /*釋放互斥鎖*//*注意,這里以防線程的搶占,以造成一個(gè)線程在另一個(gè)線程sleep時(shí)多次訪問互斥資源,所以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); /*釋放互斥鎖*//*注意,這里以防線程的搶占,以造成一個(gè)線程在另一個(gè)線程sleep時(shí)多次訪問互斥資源,所以sleep要在得到互斥鎖后調(diào)用*/sleep (1);}pthread_exit ( NULL ); }Linux下編譯時(shí)需要加 -lpthread注意第一個(gè)字母是大寫,windows C語言中單位是毫秒(ms)。 Sleep (500); 就是到這里停半秒,然后繼續(xù)向下執(zhí)行。 包含在#include <windows.h>頭文件在Linux C語言中 sleep的單位是秒(s) sleep(5);//停5秒 包含在 #include <unistd.h>頭文件mutex的簡潔性和高效性源于相比使用信號(hào)量更多的受限性。它不同于信號(hào)量,其使用場(chǎng)景相對(duì)而言更嚴(yán)格、更定向了。
(1)任何時(shí)刻中只有一個(gè)任務(wù)可以持有mutex,也就是說,mutex的使用計(jì)數(shù)永遠(yuǎn)是1。
(2)給mutex上鎖者必須負(fù)責(zé)給其再解鎖——你不能再上下文中鎖定一個(gè)mutex,而在另一個(gè)上下文中給它解鎖。這個(gè)限制使得mutex不適合內(nèi)核同用戶空間復(fù)雜的同步場(chǎng)景。最常使用的方式是:在同一個(gè)上下文中上鎖和解鎖。
(3)遞歸地上鎖和解鎖是不允許的。也就是說,你不能遞歸地持有同一個(gè)鎖,同樣你也不能再去解一個(gè)已經(jīng)解開的mutex。
(4)當(dāng)持有一個(gè)mutex時(shí),進(jìn)程不可以退出。
(5)mutex不能再中斷或者下半部中使用,即使使用mutex_trylock()也不行。整個(gè)中斷處理流程被分為兩個(gè)部分,中斷處理程序?yàn)樯习氩俊6掳氩康娜蝿?wù)是執(zhí)行與中斷處理密切相關(guān)但中斷處理程序本身不執(zhí)行的工作。
(6)mutex只能通過官方API管理:它不可被拷貝、手動(dòng)初始化或者重復(fù)初始化。
從實(shí)現(xiàn)原理上來講,Mutex屬于sleep-waiting類型的鎖。例如在一個(gè)雙核的機(jī)器上有兩個(gè)線程(線程A和線程B),它們分別運(yùn)行在Core0和Core1上。假設(shè)線程A想要通過 pthread_mutex_lock操作去得到一個(gè)臨界區(qū)的鎖,而此時(shí)這個(gè)鎖正被線程B所持有,那么線程A就會(huì)被阻塞(blocking),Core0 會(huì)在此時(shí)進(jìn)行上下文切換(Context Switch)將線程A置于等待隊(duì)列中,此時(shí)Core0就可以運(yùn)行其他的任務(wù)(例如另一個(gè)線程C)而不必進(jìn)行忙等待。而Spin lock則不然,它屬于busy-waiting類型的鎖,如果線程A是使用pthread_spin_lock操作去請(qǐng)求鎖,那么線程A就會(huì)一直在 Core0上進(jìn)行忙等待并不停的進(jìn)行鎖請(qǐng)求,直到得到這個(gè)鎖為止。
?
信號(hào)量和互斥體
互斥體和信號(hào)量很相似,內(nèi)核中兩者共存會(huì)令人混淆。所幸,它們的標(biāo)準(zhǔn)使用方式都有簡單的規(guī)范:除非mutex的某個(gè)約束妨礙你使用,否則相比信號(hào)量要優(yōu)先使用mutex。如果你所寫的是很底層的代碼,才會(huì)需要使用信號(hào)量。如果發(fā)現(xiàn)不能滿足其約束條件,且沒有其他別的選擇時(shí),再考慮選擇信號(hào)量。
自旋鎖和互斥體
對(duì)于自旋鎖來說,它只需要消耗很少的資源來建立鎖;隨后當(dāng)線程被阻塞時(shí),它就會(huì)一直重復(fù)檢查看鎖是否可用了,也就是說當(dāng)自旋鎖處于等待狀態(tài)時(shí)它會(huì)一直消耗CPU時(shí)間。
對(duì)于互斥鎖來說,與自旋鎖相比它需要消耗大量的系統(tǒng)資源來建立鎖;隨后當(dāng)線程被阻塞時(shí),線程的調(diào)度狀態(tài)被修改,并且線程被加入等待線程隊(duì)列;最后當(dāng)鎖可用 時(shí),在獲取鎖之前,線程會(huì)被從等待隊(duì)列取出并更改其調(diào)度狀態(tài);但是在線程被阻塞期間,它不消耗CPU資源。
因此自旋鎖和互斥鎖適用于不同的場(chǎng)景。自旋鎖適用于那些僅需要阻塞很短時(shí)間的場(chǎng)景,而互斥鎖適用于那些可能會(huì)阻塞很長時(shí)間的場(chǎng)景。還有一點(diǎn)是在中斷上下文中只能使用自旋鎖,而在任務(wù)睡眠時(shí)只能使用互斥體。
?
參考:
《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++线程基本函数
- 下一篇: 可重入锁(递归锁) 互斥锁属性设置