1 臨界區(qū)
1.1簡(jiǎn)介
在早期計(jì)算機(jī)系統(tǒng)中,只有一個(gè)任務(wù)進(jìn)程在執(zhí)行,并不存在資源的共享與競(jìng)爭(zhēng)。隨著技術(shù)和需求的飛速發(fā)展,單個(gè)CPU通過(guò)時(shí)間分片在一段時(shí)間內(nèi)同時(shí)處理多個(gè)任務(wù)進(jìn)程,當(dāng)多個(gè)進(jìn)程對(duì)共享資源進(jìn)行并發(fā)訪問(wèn),就引起了進(jìn)程間的競(jìng)態(tài)。這其中包括了我們所熟知的SMP(對(duì)稱(chēng)多處理器結(jié)構(gòu))系統(tǒng),多核間的相互競(jìng)爭(zhēng),單CPU中斷和進(jìn)程間的相互搶占等諸多問(wèn)題。
更具體的說(shuō),對(duì)某段代碼而言,可能會(huì)在程序中多次被執(zhí)行(多線程便是典型的場(chǎng)景),每次執(zhí)行的過(guò)程我們稱(chēng)作代碼的執(zhí)行路徑,當(dāng)兩個(gè)或多個(gè)代碼路徑要競(jìng)爭(zhēng)共同的資源的時(shí)候,該代碼段就是臨界區(qū)如圖1所示

為了保護(hù)共享資源不被同時(shí)訪問(wèn),Linux內(nèi)核中提供了各式各樣的同步鎖機(jī)制,包括:原子操作、自旋鎖、信號(hào)量、互斥量等等,除了原子操作無(wú)論哪種鎖機(jī)制都并非是免費(fèi)的午餐,加鎖操作伴隨著用戶(hù)態(tài)到內(nèi)核態(tài)切換、進(jìn)程上下文切換等高消耗過(guò)程。
1.2 用戶(hù)態(tài)與內(nèi)核態(tài)切換
為了集中管理,減少有限資源的訪問(wèn)和使用沖突,CPU設(shè)置了多個(gè)特權(quán)級(jí)別,就Intel x86架構(gòu)的CPU來(lái)說(shuō)一共有0~3四個(gè)特權(quán)級(jí),0級(jí)最高,3級(jí)最低,硬件上在執(zhí)行每條指令時(shí)都會(huì)對(duì)指令所具有的特權(quán)級(jí)做相應(yīng)的檢查,相關(guān)的概念有 CPL、DPL和RPL,這里不再過(guò)多闡述。為了安全考慮,Linux系統(tǒng)分為內(nèi)核態(tài)和用戶(hù)態(tài),分別運(yùn)行在內(nèi)核空間和用戶(hù)空間,對(duì)應(yīng)的使用了0級(jí)特權(quán)級(jí)和3級(jí)特權(quán)級(jí)。
內(nèi)核態(tài)的程序可以執(zhí)行特權(quán)指令,操作系統(tǒng)本身也在其中運(yùn)行。
用戶(hù)態(tài)則不允許直接訪問(wèn)操作系統(tǒng)的核心數(shù)據(jù)、設(shè)備等關(guān)鍵資源,必須先通過(guò)系統(tǒng)調(diào)用或者中斷進(jìn)入內(nèi)核態(tài)才可以訪問(wèn),當(dāng)系統(tǒng)調(diào)用或中斷返回時(shí),重新回到用戶(hù)空間運(yùn)行。
由用戶(hù)態(tài)切換到內(nèi)核態(tài)的步驟主要包括:
1)從當(dāng)前進(jìn)程的描述符中提取其內(nèi)核棧的ss0及esp0信息。
2)使用ss0和esp0指向的內(nèi)核棧將當(dāng)前進(jìn)程的cs,eip,eflags,ss,esp信息保存起來(lái),這個(gè)過(guò)程也完成了由用戶(hù)棧到內(nèi)核棧的切換過(guò)程,同時(shí)保存了被暫停執(zhí)行的程序的下一條指令。
3)將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應(yīng)的寄存器,開(kāi)始執(zhí)行中斷處理程序,這時(shí)就轉(zhuǎn)到了內(nèi)核態(tài)的程序執(zhí)行了。
簡(jiǎn)單來(lái)說(shuō)用戶(hù)態(tài)與內(nèi)核態(tài)切換一般都需要保存用戶(hù)程序得上下文(context), 在進(jìn)入內(nèi)核得時(shí)候需要保存用戶(hù)態(tài)得寄存器,在內(nèi)核態(tài)返回用戶(hù)態(tài)得時(shí)候會(huì)恢復(fù)這些寄存器得內(nèi)容,相對(duì)而言,這是一個(gè)很大的開(kāi)銷(xiāo)。
1.3 進(jìn)程上下文切換
上下文切換的定義,http://www.linfo.org/context_switch.html 此文中已做了詳細(xì)的說(shuō)明,只提煉以下幾個(gè)關(guān)鍵要點(diǎn):
1)進(jìn)程上下文切換可以描述為kernel執(zhí)行下面的操作
a. 掛起一個(gè)進(jìn)程,并儲(chǔ)存該進(jìn)程當(dāng)時(shí)寄存器和程序計(jì)數(shù)器的狀態(tài)
b. 從內(nèi)存中恢復(fù)下一個(gè)要執(zhí)行的進(jìn)程,恢復(fù)該進(jìn)程原來(lái)的狀態(tài)到寄存器,返回到其上次暫停的執(zhí)行代碼然后繼續(xù)執(zhí)行
2)上下文切換只能發(fā)生在內(nèi)核態(tài),所以還會(huì)觸發(fā)用戶(hù)態(tài)與內(nèi)核態(tài)切換
2. Linux鎖機(jī)制
2.1 自旋鎖
自旋鎖的實(shí)現(xiàn)是為了保護(hù)一段短小的臨界區(qū)操作代碼,保證這個(gè)臨界區(qū)的操作是原子的,從而避免并發(fā)的競(jìng)爭(zhēng)。在Linux內(nèi)核中,自旋鎖通常用于包含內(nèi)核數(shù)據(jù)結(jié)構(gòu)的操作,你可以看到在許多內(nèi)核數(shù)據(jù)結(jié)構(gòu)中都嵌入有spinlock,這些大部分就是用于保證它自身被操作的原子性,在操作這樣的結(jié)構(gòu)體時(shí)都經(jīng)歷這樣的過(guò)程:上鎖-操作-解鎖。如果內(nèi)核控制路徑發(fā)現(xiàn)自旋鎖“開(kāi)著”(可以獲?。瞳@取鎖并繼續(xù)自己的執(zhí)行。相反,如果內(nèi)核控制路徑發(fā)現(xiàn)鎖由運(yùn)行在另一個(gè)CPU上的內(nèi)核控制路徑“鎖著”,就在原地“旋轉(zhuǎn)”,反復(fù)執(zhí)行一條緊湊的循環(huán)檢測(cè)指令,直到鎖被釋放。 自旋鎖是循環(huán)檢測(cè)“忙等”,即等待時(shí)內(nèi)核無(wú)事可做(除了浪費(fèi)時(shí)間),進(jìn)程在CPU上保持運(yùn)行,所以它保護(hù)的臨界區(qū)必須小,且操作過(guò)程必須短。不過(guò),自旋鎖通常非常方便,因?yàn)楹芏鄡?nèi)核資源只鎖極短的時(shí)間片段,所以等待自旋鎖的釋放不會(huì)消耗太多CPU的時(shí)間。
2.2.1 自旋鎖需要做的工作
從保證臨界區(qū)訪問(wèn)原子性的目的來(lái)考慮,自旋鎖應(yīng)該阻止在代碼運(yùn)行過(guò)程中出現(xiàn)的任何并發(fā)干擾。這些“干擾”包括:
中斷,包括硬件中斷和軟件中斷 (僅在中斷代碼可能訪問(wèn)臨界區(qū)時(shí)需要) 這種干擾存在于任何系統(tǒng)中,一個(gè)中斷的到來(lái)導(dǎo)致了中斷例程的執(zhí)行,如果在中斷例程中訪問(wèn)了臨界區(qū),原子性就被打破了。所以如果在某種中斷例程中存在訪問(wèn)某個(gè)臨界區(qū)的代碼,那么就必須用spinlock保護(hù)。對(duì)于不同的中斷類(lèi)型(硬件中斷和軟件中斷)對(duì)應(yīng)于不同版本的自旋鎖實(shí)現(xiàn),其中包含了中斷禁用和開(kāi)啟的代碼。但是如果你保證沒(méi)有中斷代碼會(huì)訪問(wèn)臨界區(qū),那么使用不帶中斷禁用的自旋鎖API即可。
內(nèi)核搶占(僅存在于可搶占內(nèi)核中) 在2.6以后的內(nèi)核中,支持內(nèi)核搶占,并且是可配置的。這使UP系統(tǒng)和SMP類(lèi)似,會(huì)出現(xiàn)內(nèi)核態(tài)下的并發(fā)。這種情況下進(jìn)入臨界區(qū)就需要避免因搶占造成的并發(fā),所以解決的方法就是在加鎖時(shí)禁用搶占(preempt_disable(); ),在開(kāi)鎖時(shí)開(kāi)啟搶占(preempt_enable();注意此時(shí)會(huì)執(zhí)行一次搶占調(diào)度)
其他處理器對(duì)同一臨界區(qū)的訪問(wèn) (僅SMP系統(tǒng)) 在SMP系統(tǒng)中,多個(gè)物理處理器同時(shí)工作,導(dǎo)致可能有多個(gè)進(jìn)程物理上的并發(fā)。這樣就需要在內(nèi)存加一個(gè)標(biāo)志,每個(gè)需要進(jìn)入臨界區(qū)的代碼都必須檢查這個(gè)標(biāo)志,看是否有進(jìn)程已經(jīng)在這個(gè)臨界區(qū)中。這種情況下檢查標(biāo)志的代碼也必須保證原子和快速,這就要求必須精細(xì)地實(shí)現(xiàn),正常情況下每個(gè)構(gòu)架都有自己的匯編實(shí)現(xiàn)方案,保證檢查的原子性。
根據(jù)上的介紹,我們很容易知道自旋鎖的操作包括:
中斷控制(僅在中斷代碼可能訪問(wèn)臨界區(qū)時(shí)需要)
搶占控制(僅存在于可搶占內(nèi)核中需要)
自旋鎖標(biāo)志控制 (僅SMP系統(tǒng)需要)
中斷控制是按代碼訪問(wèn)臨界區(qū)的不同而在編程時(shí)選用不同的變體,有些API中有,有些沒(méi)有。
而搶占控制和自旋鎖標(biāo)志控制依據(jù)內(nèi)核配置(是否支持內(nèi)核搶占)和硬件平臺(tái)(是否為SMP)的不同而在編譯時(shí)確定。如果不需要,相應(yīng)的控制代碼就編譯為空函數(shù)。 對(duì)于非搶占式內(nèi)核,由自旋鎖所保護(hù)的每個(gè)臨界區(qū)都有禁止內(nèi)核搶占的API,但是為空操作。由于UP系統(tǒng)不存在物理上的并行,所以可以閹割掉自旋的部分,剩下?lián)屨己椭袛嗖僮鞑糠旨纯伞?/p>
有些人會(huì)以為自旋鎖的自旋檢測(cè)可以用for實(shí)現(xiàn),這種想法“Too young, too simple, sometimes naive”!你可以在理論上用C去解釋?zhuān)侨绻胒or,起碼會(huì)有如下兩個(gè)問(wèn)題:
1)你如何保證在SMP下其他處理器不會(huì)同時(shí)訪問(wèn)同一個(gè)的標(biāo)志呢?(也就是標(biāo)志的獨(dú)占訪問(wèn))
2)必須保證每個(gè)處理器都不會(huì)去讀取高速緩存而是真正的內(nèi)存中的標(biāo)志(可以實(shí)現(xiàn),編程上可以用volitale)要根本解決這個(gè)問(wèn)題,需要在芯片底層實(shí)現(xiàn)物理上的內(nèi)存地址獨(dú)占訪問(wèn),并且在實(shí)現(xiàn)上使用特殊的匯編指令訪問(wèn)。請(qǐng)看參考資料中對(duì)于自旋鎖的實(shí)現(xiàn)分析。以arm為例,從存在SMP的ARM構(gòu)架指令集開(kāi)始(V6、V7),采用LDREX和STREX指令實(shí)現(xiàn)真正的自旋等待。
2.2.2 自旋鎖變體的使用規(guī)則
不論是搶占式UP、非搶占式UP還是SMP系統(tǒng),只要在某類(lèi)中斷代碼可能訪問(wèn)臨界區(qū),就需要控制中斷,保證操作的原子性。所以這個(gè)和模塊代碼中臨界區(qū)的訪問(wèn)還有關(guān)系,是否可能在中斷中操作臨界區(qū),只有程序員才知道。所以自旋鎖API中有針對(duì)不同中斷類(lèi)型的自旋鎖變體:
不會(huì)在任何中斷例程中操作臨界區(qū)
static inline void spin_lock(spinlock_t *lock)
static inline void spin_unlock(spinlock_t *lock)
如果在軟件中斷中操作臨界區(qū):
static inline void spin_lock_bh(spinlock_t *lock)
static inline void spin_unlock_bh(spinlock_t *lock)
bh代表bottom half,也就是中斷中的底半部,因內(nèi)核中斷的底半部一般通過(guò)軟件中斷(tasklet等)來(lái)處理而得名。
如果在硬件中斷中操作臨界區(qū):
static inline void spin_lock_irq(spinlock_t *lock)
static inline void spin_unlock_irq(spinlock_t *lock)
如果在控制硬件中斷的時(shí)候需要同時(shí)保存中斷狀態(tài):
spin_lock_irqsave(lock, flags)
static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
這些情況描訴似乎有點(diǎn)簡(jiǎn)單,我在網(wǎng)上找到了一篇使用規(guī)則((轉(zhuǎn))自旋鎖(spinlock) 解釋得經(jīng)典,透徹),非常詳細(xì)。我稍作修改,轉(zhuǎn)載如下:
獲得自旋鎖和釋放自旋鎖有好幾個(gè)版本,因此讓讀者知道在什么樣的情況下使用什么版本的獲得和釋放鎖的宏是非常必要的。
如果被保護(hù)的共享資源只在進(jìn)程上下文訪問(wèn)和軟中斷(包括tasklet、timer)上下文訪問(wèn),那么當(dāng)在進(jìn)程上下文訪問(wèn)共享資源時(shí),可能被軟中斷打斷,從而可能進(jìn)入軟中斷上下文來(lái)對(duì)被保護(hù)的共享資源訪問(wèn),因此對(duì)于這種情況,對(duì)共享資源的訪問(wèn)必須使用spin_lock_bh和spin_unlock_bh來(lái)保護(hù)。當(dāng)然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bh和spin_unlock_bh是最恰當(dāng)?shù)?,它比其他兩個(gè)快。
如果被保護(hù)的共享資源只在兩個(gè)或多個(gè)tasklet或timer上下文訪問(wèn),那么對(duì)共享資源的訪問(wèn)僅需要用spin_lock和spin_unlock來(lái)保護(hù),不必使用_bh版本,因?yàn)楫?dāng)tasklet或timer運(yùn)行時(shí),不可能有其他tasklet或timer在當(dāng)前CPU上運(yùn)行。
如果被保護(hù)的共享資源只在一個(gè)tasklet或timer上下文訪問(wèn),那么不需要任何自旋鎖保護(hù),因?yàn)橥粋€(gè)tasklet或timer只能在一個(gè)CPU上運(yùn)行,即使是在SMP環(huán)境下也是如此。實(shí)際上tasklet在調(diào)用tasklet_schedule標(biāo)記其需要被調(diào)度時(shí)已經(jīng)把該tasklet綁定到當(dāng)前CPU,因此同一個(gè)tasklet決不可能同時(shí)在其他CPU上運(yùn)行。timer也是在其被使用add_timer添加到timer隊(duì)列中時(shí)已經(jīng)被幫定到當(dāng)前CPU,所以同一個(gè)timer絕不可能運(yùn)行在其他CPU上。當(dāng)然同一個(gè)tasklet有兩個(gè)實(shí)例同時(shí)運(yùn)行在同一個(gè)CPU就更不可能了。
如果被保護(hù)的共享資源只在一個(gè)軟中斷(tasklet和timer除外)上下文訪問(wèn),那么這個(gè)共享資源需要用spin_lock和spin_unlock來(lái)保護(hù),因?yàn)橥瑯拥能浿袛嗫梢酝瑫r(shí)在不同的CPU上運(yùn)行。
如果被保護(hù)的共享資源在兩個(gè)或多個(gè)軟中斷上下文訪問(wèn),那么這個(gè)共享資源當(dāng)然更需要用spin_lock和spin_unlock來(lái)保護(hù),不同的軟中斷能夠同時(shí)在不同的CPU上運(yùn)行。
如果被保護(hù)的共享資源在軟中斷(包括tasklet和timer)或進(jìn)程上下文和硬中斷上下文訪問(wèn),那么在軟中斷或進(jìn)程上下文訪問(wèn)期間,可能被硬中斷打斷,從而進(jìn)入硬中斷上下文對(duì)共享資源進(jìn)行訪問(wèn),因此,在進(jìn)程或軟中斷上下文需要使用spin_lock_irq和spin_unlock_irq來(lái)保護(hù)對(duì)共享資源的訪問(wèn)。
而在中斷處理句柄中使用什么版本,需依情況而定,如果只有一個(gè)中斷處理句柄訪問(wèn)該共享資源,那么在中斷處理句柄中僅需要spin_lock和spin_unlock來(lái)保護(hù)對(duì)共享資源的訪問(wèn)就可以了。因?yàn)樵趫?zhí)行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進(jìn)程打斷。
但是如果有不同的中斷處理句柄訪問(wèn)該共享資源,那么需要在中斷處理句柄中使用spin_lock_irq和spin_unlock_irq來(lái)保護(hù)對(duì)共享資源的訪問(wèn)。
在使用spin_lock_irq和spin_unlock_irq的情況下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具體應(yīng)該使用哪一個(gè)也需要依情況而定,如果可以確信在對(duì)共享資源訪問(wèn)前中斷是使能的,那么使用spin_lock_irq更好一些。因?yàn)樗萻pin_lock_irqsave要快一些,但是如果你不能確定是否中斷使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因?yàn)樗鼘⒒謴?fù)訪問(wèn)共享資源前的中斷標(biāo)志而不是直接使能中斷。
當(dāng)然,有些情況下需要在訪問(wèn)共享資源時(shí)必須中斷失效,而訪問(wèn)完后必須中斷使能,這樣的情形使用spin_lock_irq和spin_unlock_irq最好。
spin_lock用于阻止在不同CPU上的執(zhí)行單元對(duì)共享資源的同時(shí)訪問(wèn)以及不同進(jìn)程上下文互相搶占導(dǎo)致的對(duì)共享資源的非同步訪問(wèn),而中斷失效和軟中斷失效卻是為了阻止在同一CPU上軟中斷或中斷對(duì)共享資源的非同步訪問(wèn)。
2.2.3 自旋鎖使用及注意事項(xiàng)
自旋鎖使用如下;
//1.分配自旋鎖
spinlock_t lock;
//2.初始化自旋鎖
spin_lock_init(&lock);
//3.訪問(wèn)臨界區(qū)之前獲取鎖:
spin_lock(&lock); //獲取自旋鎖,立即返回,如果沒(méi)有獲取鎖,將進(jìn)行忙等待
或者
spin_trylock(&lock); //獲取鎖,返回true,否則返回false,所以這個(gè)函數(shù)一定要對(duì)返回值進(jìn)行判斷!
//4 .訪問(wèn)臨界區(qū)
//5.釋放自旋鎖
spin_unlock(&lock);
自旋鎖的注意事項(xiàng):
自旋鎖使CPU處于忙等狀態(tài),因此臨界區(qū)執(zhí)行時(shí)間應(yīng)該盡量短;
自旋鎖是不可重入的;
-
自旋鎖保護(hù)的臨界區(qū)不應(yīng)該有睡眠操作:
1)對(duì)于開(kāi)中斷的自旋鎖來(lái)說(shuō),睡眠操作可能發(fā)生如下兩種情況:
a. 死鎖:任務(wù)A獲得自旋鎖之后睡眠,接著又發(fā)生了中斷,而中斷處理程序內(nèi)部又打算獲取同一個(gè)自旋鎖,則此時(shí)會(huì)發(fā)生自死鎖 —— 自旋鎖是不可重入的。
b. CPU浪費(fèi):倘若中斷處理程序內(nèi)部沒(méi)有獲取同一個(gè)自旋鎖的操作,則理論上可以產(chǎn)生調(diào)度。假設(shè)進(jìn)程B打算獲取CPU的控制權(quán),但由于此時(shí)是關(guān)搶占的(因?yàn)檫M(jìn)程A還沒(méi)有解自旋鎖,此時(shí)依舊處于自旋鎖的臨界區(qū)中),導(dǎo)致進(jìn)程B無(wú)法運(yùn)行。也就是說(shuō)CPU將無(wú)法運(yùn)行任何程序,一直處于無(wú)事可做的狀態(tài),造成CPU的浪費(fèi)。2)對(duì)于順帶關(guān)中斷的自旋鎖來(lái)說(shuō),顯而易見(jiàn)在臨界區(qū)內(nèi)使不能睡眠的,因?yàn)閱拘岩粋€(gè)睡眠的進(jìn)程依賴(lài)于調(diào)度器,而調(diào)度器是通過(guò)時(shí)鐘中斷來(lái)判斷合適喚醒進(jìn)程的,倘若在關(guān)閉中斷的時(shí)候進(jìn)程睡眠,則調(diào)度器將再也無(wú)法收到時(shí)鐘中斷(因?yàn)殚_(kāi)中斷的操作也是由該進(jìn)程控制的),從而永遠(yuǎn)都無(wú)法喚醒睡眠的進(jìn)程。也就是說(shuō)該進(jìn)程將處于睡死狀態(tài)。
簡(jiǎn)單來(lái)說(shuō),自旋鎖的初衷就是:在短期間內(nèi)進(jìn)行輕量級(jí)的鎖定。一個(gè)被爭(zhēng)用的自旋鎖使得請(qǐng)求它的線程在等待鎖重新可用的期間進(jìn)行自旋(特別浪費(fèi)處理器時(shí)間),所以自旋鎖不應(yīng)該被持有時(shí)間過(guò)長(zhǎng)。如果需要長(zhǎng)時(shí)間鎖定的話, 最好使用信號(hào)量。
2.3 信號(hào)量
信號(hào)量是用來(lái)協(xié)調(diào)不同進(jìn)程間的數(shù)據(jù)對(duì)象的,而最主要的應(yīng)用是共享內(nèi)存方式的進(jìn)程間通信。本質(zhì)上,信號(hào)量是一個(gè)計(jì)數(shù)器,它用來(lái)記錄對(duì)某個(gè)資源(如共享內(nèi)存)的存取狀況。一般說(shuō)來(lái),為了獲得共享資源,進(jìn)程需要執(zhí)行下列操作:
1) 測(cè)試控制該資源的信號(hào)量。
2) 若此信號(hào)量的值為正,則允許進(jìn)行使用該資源。進(jìn)程將信號(hào)量減1。
3) 若此信號(hào)量為0,則該資源目前不可用,進(jìn)程進(jìn)入睡眠狀態(tài),直至信號(hào)量值大于0,進(jìn)程被喚醒,轉(zhuǎn)入步驟(1)。
4) 當(dāng)進(jìn)程不再使用一個(gè)信號(hào)量控制的資源時(shí),信號(hào)量值加1。如果此時(shí)有進(jìn)程正在睡眠等待此信號(hào)量,則喚醒此進(jìn)程。
維護(hù)信號(hào)量狀態(tài)的是Linux內(nèi)核操作系統(tǒng)而不是用戶(hù)進(jìn)程。我們可以從頭文件/usr/src/linux/include/linux/sem.h 中看到內(nèi)核用來(lái)維護(hù)信號(hào)量狀態(tài)的各個(gè)結(jié)構(gòu)的定義。信號(hào)量是一個(gè)數(shù)據(jù)集合,用戶(hù)可以單獨(dú)使用這一集合的每個(gè)元素。要調(diào)用的第一個(gè)函數(shù)是semget,用以獲得一個(gè)信號(hào)量ID。Linux2.6.26下定義的信號(hào)量結(jié)構(gòu)體:
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
從以上信號(hào)量的定義中,可以看到信號(hào)量底層使用到了spin lock的鎖定機(jī)制,這個(gè)spinlock主要用來(lái)確保對(duì)count成員的原子性的操作(count–)和測(cè)試(count > 0)。
2.3.1 信號(hào)量的P操作
- void down(struct semaphore *sem);
- int down_interruptible(struct semaphore *sem);
- int down_trylock(struct semaphore *sem);
函數(shù)1表示當(dāng)信號(hào)申請(qǐng)不到時(shí)會(huì)進(jìn)程會(huì)休眠;對(duì)于函數(shù)(2)來(lái)說(shuō),它表示如果當(dāng)進(jìn)程因申請(qǐng)不到信號(hào)量而進(jìn)入睡眠后,能被信號(hào)打斷,這里所說(shuō)的信號(hào)是指進(jìn)程間通信的信號(hào),比如我們的Ctrl+C,但這時(shí)候這個(gè)函數(shù)的返回值不為0;
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);
spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
對(duì)此函數(shù)的理解:在保證原子操作的前提下,先測(cè)試count是否大于0,如果是說(shuō)明可以獲得信號(hào)量,這種情況下需要先將count--,以確保別的進(jìn)程能否獲得該信號(hào)量,然后函數(shù)返回,其調(diào)用者開(kāi)始進(jìn)入臨界區(qū)。如果沒(méi)有獲得信號(hào)量,當(dāng)前進(jìn)程利用struct semaphore 中wait_list加入等待隊(duì)列,開(kāi)始睡眠。
對(duì)于需要休眠的情況,在__down_interruptible()函數(shù)中,會(huì)構(gòu)造一個(gè)struct semaphore_waiter類(lèi)型的變量(struct semaphore_waiter定義如下:
struct semaphore_waiter
{
struct list_head list;
struct task_struct *task;
int up;
};
將當(dāng)前進(jìn)程賦給task,并利用其list成員將該變量的節(jié)點(diǎn)加入到以sem中的wait_list為頭部的一個(gè)列表中,假設(shè)有多個(gè)進(jìn)程在sem上調(diào)用down_interruptible,則sem的wait_list上形成的隊(duì)列如下圖:

(注:將一個(gè)進(jìn)程阻塞,一般的經(jīng)過(guò)是先把進(jìn)程放到等待隊(duì)列中,接著改變進(jìn)程的狀態(tài),比如設(shè)為T(mén)ASK_INTERRUPTIBLE,然后調(diào)用調(diào)度函數(shù)schedule(),后者將會(huì)把當(dāng)前進(jìn)程從cpu的運(yùn)行隊(duì)列中摘下)
函數(shù)(3)試圖去獲得一個(gè)信號(hào)量,如果沒(méi)有獲得,函數(shù)立刻返回1而不會(huì)讓當(dāng)前進(jìn)程進(jìn)入睡眠狀態(tài)。
2.3.2 信號(hào)量的V操作
void up(struct semaphore *sem);
原型如下:
void up(struct semaphore *sem)
{
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
spin_unlock_irqrestore(&sem->lock, flags);
}
如果沒(méi)有其他線程等待在目前即將釋放的信號(hào)量上,那么只需將count++即可。如果有其他線程正因?yàn)榈却撔盘?hào)量而睡眠,那么調(diào)用__up.
__up的定義:
static noinline void __sched __up(struct semaphore *sem)
{
struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
list_del(&waiter->list);
waiter->up = 1;
wake_up_process(waiter->task);
}
這個(gè)函數(shù)首先獲得sem所在的wait_list為頭部的鏈表的第一個(gè)有效節(jié)點(diǎn),然后從鏈表中將其刪除,然后喚醒該節(jié)點(diǎn)上睡眠的進(jìn)程。 由此可見(jiàn),對(duì)于sem上的每次down_interruptible調(diào)用,都會(huì)在sem的wait_list鏈表尾部加入一新的節(jié)點(diǎn)。對(duì)于sem上的每次up調(diào)用,都會(huì)刪除掉wait_list鏈表中的第一個(gè)有效節(jié)點(diǎn),并喚醒睡眠在該節(jié)點(diǎn)上的進(jìn)程。
2.3.3 信號(hào)量的使用
//1.分配信號(hào)量對(duì)象
struct semaphore sema;
//2.初始化為互斥信號(hào)量
init_MUTEX(&sema);
或者:
DECLARE_MUTEX(sema);
//3.訪問(wèn)臨界區(qū)之前獲取信號(hào)量
down(&sema);
//如果獲取信號(hào)量,立即返回
//如果信號(hào)量不可用,進(jìn)程將在此休眠,并且休眠的狀態(tài)是 [ 不可中斷的休眠狀態(tài) TASK_UNINTERRUPTIBLE] !
或者
down_interruptible(&sema);
//如果信號(hào)量不可用,進(jìn)程將進(jìn)入 [ 可中斷的休眠狀態(tài) TASK_INTERRUPTIBLE ],如果返回0表示正常獲取信號(hào),如果返回非0,表示接受到了信號(hào)
down_trylock();
//獲取信號(hào),如果信號(hào)量不可用,返回非0,如果信號(hào)量可用,返回0;不會(huì)引起休眠,可以在中斷上下文使用。返回值也要做判斷!
//4.訪問(wèn)臨界區(qū):臨界區(qū)可以休眠
//5.釋放信號(hào)量
up(&sema);
//不僅僅釋放信號(hào)量,然后喚醒休眠的進(jìn)程,讓這個(gè)進(jìn)程去獲取信號(hào)量來(lái)訪問(wèn)臨界區(qū)
2.4 互斥量
互斥體實(shí)現(xiàn)了“互相排斥”(mutual exclusion)同步的簡(jiǎn)單形式(所以名為互斥體(mutex))。互斥體禁止多個(gè)線程同時(shí)進(jìn)入受保護(hù)的代碼“臨界區(qū)”(critical section)。因此,在任意時(shí)刻,只有一個(gè)線程被允許進(jìn)入這樣的代碼保護(hù)區(qū)。任何線程在進(jìn)入臨界區(qū)之前,必須獲取(acquire)與此區(qū)域相關(guān)聯(lián)的互斥體的所有權(quán)。如果已有另一線程擁有了臨界區(qū)的互斥體,其他線程就不能再進(jìn)入其中。這些線程必須等待,直到當(dāng)前的屬主線程釋放(release)該互斥體。
Linux 2.6.26中mutex的定義:
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
struct thread_info *owner;
const char *name;
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
對(duì)比前面的struct semaphore,struct mutex除了增加了幾個(gè)作為debug用途的成員變量外,和semaphore幾乎長(zhǎng)得一樣。但是mutex的引入主要是為了提供互斥機(jī)制,以避免多個(gè)進(jìn)程同時(shí)在一個(gè)臨界區(qū)中運(yùn)行。
如果靜態(tài)聲明一個(gè)count=1的semaphore變量,可以使用DECLARE_MUTEX(name),DECLARE_MUTEX(name)實(shí)際上是定義一個(gè)semaphore,所以它的使用應(yīng)該對(duì)應(yīng)信號(hào)量的P,V函數(shù).
如果要定義一個(gè)靜態(tài)mutex型變量,應(yīng)該使用DEFINE_MUTEX
如果在程序運(yùn)行期要初始化一個(gè)mutex變量,可以使用mutex_init(mutex),mutex_init是個(gè)宏,在該宏定義的內(nèi)部,會(huì)調(diào)用__mutex_init函數(shù)。
#define mutex_init(mutex) \
do { \
static struct lock_class_key __key; \
\
__mutex_init((mutex), #mutex, &__key); \
} while (0)
void __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key)
{
atomic_set(&lock->count, 1);
spin_lock_init(&lock->wait_lock);
INIT_LIST_HEAD(&lock->wait_list);
debug_mutex_init(lock, name, key);
}
從__mutex_init的定義可以看出,在使用mutex_init宏來(lái)初始化一個(gè)mutex變量時(shí),應(yīng)該使用mutex的指針型。
mutex上的P,V操作:void mutex_lock(struct mutex *lock)和void __sched mutex_unlock(struct mutex *lock)
從原理上講,mutex實(shí)際上是count=1情況下的semaphore,所以其PV操作應(yīng)該和semaphore是一樣的。但是在實(shí)際的Linux代碼上,出于性能優(yōu)化的角度,并非只是單純的重用down_interruptible和up的代碼。以ARM平臺(tái)的mutex_lock為例,實(shí)際上是將mutex_lock分成兩部分實(shí)現(xiàn):fast path和slow path,主要是基于這樣一個(gè)事實(shí):在絕大多數(shù)情況下,試圖獲得互斥體的代碼總是可以成功獲得。所以Linux的代碼針對(duì)這一事實(shí)用ARM V6上的LDREX和STREX指令來(lái)實(shí)現(xiàn)fast path以期獲得最佳的執(zhí)行性能。
mutux底層支持:
Linux:底層的pthread mutex采用futex(2)(fast userspace mutex)實(shí)現(xiàn),不必每次加鎖、解鎖都陷入系統(tǒng)調(diào)用(從用戶(hù)態(tài)切換到內(nèi)核態(tài))。
futex(2):快速用戶(hù)態(tài)互斥鎖(fast userspace mutex),在非競(jìng)態(tài)(或非鎖爭(zhēng)用,表示申請(qǐng)即能立即拿到鎖而不用等待)的情況下,futex操作完全在用戶(hù)態(tài)下進(jìn)行,內(nèi)核態(tài)只負(fù)責(zé)處理競(jìng)態(tài)(或鎖爭(zhēng)用,表示申請(qǐng)了但有其它線程提前拿到了鎖,需要等待鎖被釋放后才能拿到)下的操作(調(diào)用相應(yīng)的系統(tǒng)調(diào)用來(lái)仲裁),futex有兩個(gè)主要的函數(shù):
futex_wait(addr, val) //檢查*addr == val ?,若相等則將當(dāng)前線程放入等待隊(duì)列addr中隨眠(陷入到內(nèi)核態(tài)等待喚醒),否則調(diào)用失敗并返回錯(cuò)誤(依舊處于用戶(hù)態(tài))
futex_wake(addr, val) //喚醒val個(gè)處于等待隊(duì)列addr中的線程(從內(nèi)核態(tài)回到用戶(hù)態(tài)
因此采用futex(2)的互斥鎖不必每次加解鎖都從用戶(hù)態(tài)切換到內(nèi)核態(tài),效率較高.
Windows:底層的CRITICAL_SECTION嵌入了一小段自旋鎖,如果不能立即拿到鎖,它先會(huì)自旋一小段時(shí)間,如果還拿不到,才掛起當(dāng)前線程.
2.4.1 互斥量的使用
pthread mutex接口函數(shù):
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
3. 各種鎖的區(qū)別
3.1 信號(hào)量/互斥體和自旋鎖的區(qū)別
信號(hào)量/互斥體允許進(jìn)程睡眠屬于睡眠鎖,自旋鎖則不允許調(diào)用者睡眠,而是讓其循環(huán)等待,所以有以下區(qū)別應(yīng)用 :
- 信號(hào)量和讀寫(xiě)信號(hào)量適合于保持時(shí)間較長(zhǎng)的情況,它們會(huì)導(dǎo)致調(diào)用者睡眠,因而自旋鎖適合于保持時(shí)間非常短的情況
- 自旋鎖可以用于中斷,不能用于進(jìn)程上下文(會(huì)引起死鎖)。而信號(hào)量不允許使用在中斷中,而可以用于進(jìn)程上下文
- 自旋鎖保持期間是搶占失效的,自旋鎖被持有時(shí),內(nèi)核不能被搶占,而信號(hào)量和讀寫(xiě)信號(hào)量保持期間是可以被搶占的
另外需要注意的是:
- 信號(hào)量鎖保護(hù)的臨界區(qū)可包含可能引起阻塞的代碼,而自旋鎖則絕對(duì)要避免用來(lái)保護(hù)包含這樣代碼的臨界區(qū),因?yàn)樽枞馕吨M(jìn)行進(jìn)程的切換,如果進(jìn)程被切換出去后,另一進(jìn)程企圖獲取本自旋鎖,死鎖就會(huì)發(fā)生。
- 在你占用信號(hào)量的同時(shí)不能占用自旋鎖,因?yàn)樵谀愕却盘?hào)量時(shí)可能會(huì)睡眠,而在持有自旋鎖時(shí)是不允許睡眠的。
3.2 信號(hào)量和互斥體之間的區(qū)別
概念上的區(qū)別:
信號(hào)量:是進(jìn)程間(線程間)同步用的,一個(gè)進(jìn)程(線程)完成了某一個(gè)動(dòng)作就通過(guò)信號(hào)量告訴別的進(jìn)程(線程),別的進(jìn)程(線程)再進(jìn)行某些動(dòng)作。有二值和多值信號(hào)量之分。
互斥鎖:是線程間互斥用的,一個(gè)線程占用了某一個(gè)共享資源,那么別的線程就無(wú)法訪問(wèn),直到這個(gè)線程離開(kāi),其他的線程才開(kāi)始可以使用這個(gè)共享資源。可以把互斥鎖看成二值信號(hào)量。
上鎖時(shí):
信號(hào)量: 只要信號(hào)量的value大于0,其他線程就可以sem_wait成功,成功后信號(hào)量的value減一。若value值不大于0,則sem_wait阻塞,直到sem_post釋放后value值加一。
互斥鎖: 只要被鎖住,其他任何線程都不可以訪問(wèn)被保護(hù)的資源。如果沒(méi)有鎖,獲得資源成功,否則進(jìn)行阻塞等待資源可用。
使用場(chǎng)所: 信號(hào)量主要適用于進(jìn)程間通信,當(dāng)然,也可用于線程間通信。而互斥鎖只能用于線程間通信。