WDK tips (9.1) 同步机制与锁
這篇文章基本上就是把WDK文檔復(fù)述了一下,算不上原創(chuàng),各位將就著看吧。
在用戶態(tài)的世界很多程序員(特別是*NIX界的人)不喜歡用多線程,認(rèn)為這東西大大增加了程序的復(fù)雜度的同時(shí)帶來(lái)的好處卻不多,他們寧愿用進(jìn)程來(lái)分割任務(wù)。當(dāng)然這是一種很好的設(shè)計(jì)原則,我個(gè)人也持一模一樣的觀點(diǎn)。但是自從多核被炒熱之后這部分內(nèi)容越來(lái)越受關(guān)注,你假裝問(wèn)題不存在已經(jīng)不可能了,借用冠希哥的日歪普歌詞說(shuō)就是:就算忘記你們不可能看不見(jiàn)。而在內(nèi)核態(tài)的世界多線程的傳統(tǒng)由來(lái)已久,因?yàn)閮?nèi)核部分的地址空間多半是共享的,即使是多進(jìn)程架構(gòu)在反映在內(nèi)核部分也就是不同的線程,并且操作硬件的過(guò)程中不被重入也是基本要求。如果你開(kāi)發(fā)過(guò)驅(qū)動(dòng)或者玩過(guò)內(nèi)核,那么同步機(jī)制和鎖你一定已經(jīng)看的很多了。
各種成熟的操作系統(tǒng)內(nèi)核都會(huì)提供非常豐富的鎖與同步機(jī)制來(lái)保證你的代碼和資源不被重入,NT內(nèi)核也不例外。但我發(fā)現(xiàn)驅(qū)動(dòng)(或內(nèi)核)開(kāi)發(fā)人員常用的鎖卻很少,spin lock算一個(gè),event算第二個(gè),了不起加上個(gè)mutex就完了,其他鎖的出鏡率低的可憐。我看過(guò)比較離譜的例子是只要用到鎖的地方就用spin lock,不管合適不合適。這么做想也知道是不對(duì)的,操作系統(tǒng)提供了不同類型的鎖自然是希望你不同情況用不同的,全用spin lock雖然沒(méi)風(fēng)險(xiǎn)但顯然也很低效。本節(jié)列舉了WDK中提供的各種鎖機(jī)制,并試圖指出何時(shí)該用哪種鎖,以及更重要的,何時(shí)不該用那種。先從自旋鎖說(shuō)起。
自旋鎖(spin lock)
自旋鎖基本上算是最簡(jiǎn)單的同步機(jī)制了。我記得上學(xué)那會(huì)兒剛會(huì)寫程序的時(shí)候碰到要同步的情況會(huì)寫類似下面的代碼:
while( i == 0 );i = 0// do something i = 1寫完上面一段代碼只需要幾分鐘,debug卻debug了一天,弄的自己灰頭土臉,被“高手”看了后免不了的又會(huì)被羞辱一通。沒(méi)有一個(gè)靠譜的程序員會(huì)這么做同步,但是不管你信不信,自旋鎖的核心機(jī)制就是上面那段while循環(huán)。所謂的自旋,就是CPU在某條指令上不停的回繞直到條件成立跳出,也就是我們常說(shuō)的忙等。那上面那段代碼也是不停自旋,它有什么問(wèn)題?我們說(shuō)它在多線程環(huán)境下會(huì)出問(wèn)題。假設(shè)上面那兩句編譯完了后變成如下指令:
WAIT:test if i equals 0jump to WAIT if trueset i into 0// do somethingset i into 1又假設(shè)當(dāng)前 i 為1,我們有兩條線程同時(shí)執(zhí)行,產(chǎn)生的指令流如下:
| 時(shí)間 | 線程1 | 線程2 |
| 0 | test if i not 0 | ? |
| 1 | ? | test if i not 0 |
| 2 | set i into 0 | ? |
| 3 | ? | set i into 0 |
| 4 | … | … |
第0條和第1條指令執(zhí)行完后,兩條線程都進(jìn)入了臨界區(qū),很顯然這是一個(gè)bug。這里的問(wèn)題是test指令和set指令是分開(kāi)的,中間會(huì)被重入,而我們不希望這種重入的發(fā)生。一個(gè)正確的實(shí)現(xiàn)要么需要及其復(fù)雜的算法保證,要么需要CPU提供set-and-test指令,在一條指令里面做完test和set兩件事,保證不會(huì)被重入。NT內(nèi)核用的是set-and-test指令,事實(shí)上這種指令在用戶態(tài)也可以使用,就是InterlockedXXX那一堆API.
在多核CPU架構(gòu)里set-and-test指令往往會(huì)把bus鎖住導(dǎo)致并行效率降低,為了減少此類事件的發(fā)生NT內(nèi)核實(shí)現(xiàn)自旋鎖時(shí)用了兩個(gè)循環(huán),大循環(huán)用set-and-test檢測(cè),如果檢測(cè)失敗(也就是獲得鎖失敗)立即進(jìn)入小循環(huán)用普通的test指令檢測(cè),大致的代碼如下:
while( test i not 0 and then set i into 0 ){while( test i not 0 );}另外考慮到檢測(cè)值 i 不是cpu-local的值,兩邊的cpu cache需要不停的同步,每次i修改后參與排隊(duì)的cpu的緩存就會(huì)變得無(wú)效,如果排隊(duì)自旋鎖競(jìng)爭(zhēng)比較激烈的話,頻繁的緩存同步操作會(huì)導(dǎo)致繁重的系統(tǒng)總線和內(nèi)存的流量,從而大大降低了系統(tǒng)整體的性能。又由于各cpu獲得spin lock的時(shí)機(jī)是無(wú)序的,在競(jìng)爭(zhēng)激烈的情況下很有可能會(huì)出現(xiàn)“饑餓”現(xiàn)象。基于這些理由,某些操作系統(tǒng)設(shè)計(jì)了 一種每個(gè)cpu都在自己的local變量上自旋,并能保證按先來(lái)先得順序獲得鎖的的算法,此種自旋鎖稱為queued spinlock,具體信息請(qǐng)查看IBM Developerworks 上的文章,MSDN上也有簡(jiǎn)單的描述。
WDK里的自旋鎖
之前我們說(shuō)過(guò)NT內(nèi)核里有IRQL的概念,在APC 及以上的level線程調(diào)度是停止的,每個(gè)cpu都只可能有一條線程在運(yùn)行。基于這條原則我們可以很容易的看出,在單核系統(tǒng)里自旋鎖完全沒(méi)必要真去自旋,它只需要把IRQL提高到?jīng)]有線程切換的等級(jí)即可,這就是NT內(nèi)核做的。每次獲得自旋鎖的時(shí)候,內(nèi)核會(huì)把當(dāng)前的IRQL提高到DISPATCH_LEVEL(或以上)并保存之前的level,然后根據(jù)處理器數(shù)量做不同的事情:如果是單核,則啥也不做;如果是多核,進(jìn)入自旋的邏輯。釋放鎖之后則作相反的事:把IRQL設(shè)置成之前的level。
WDK提供了三種類型的自旋鎖:普通自旋鎖,普通ISR自旋鎖,以及ISR同步自旋鎖。普通自旋鎖會(huì)把IRQL設(shè)成DISPATCH_LEVEL,而ISR的IRQL一定會(huì)大于DISPATCH_LEVEL,所以如果你在ISR里用普通自旋鎖那么一定會(huì)發(fā)生BSOD。普通自旋鎖使用前必須要先調(diào)用KeInitializeSpinLock進(jìn)行初始化。普通自旋鎖的獲取用KeAcquireSpinLock函數(shù),釋放用KeReleaseSpinLock。這兩個(gè)函數(shù)都會(huì)修改IRQL,如果你很確認(rèn)當(dāng)前的IRQL是DISPATCH_LEVEL,那么可以使用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel函數(shù),這兩個(gè)函數(shù)不會(huì)修改IRQL。普通ISR自旋鎖允許運(yùn)行在DIRQL中的ISR和運(yùn)行在DISPATCH_LEVEL的DPC之間進(jìn)行同步,ISR同步自旋鎖則是允許不同的ISR之間進(jìn)行同步。當(dāng)你調(diào)用IoConnectInterrupt時(shí)有一個(gè)optional parameter叫做SpinLock,如果你傳入的值是NULL,那么IoManager會(huì)自動(dòng)給你生成一個(gè)普通自旋鎖,ISR調(diào)用前自動(dòng)加鎖,調(diào)用后自動(dòng)解鎖;如果傳入的值非0(也就是由你自己制定spin lock),那么該spin lock為ISR同步自旋鎖,它可以為不同DIRQL上的ISR提供同步機(jī)制。值得注意的是,不管是普通ISR自旋鎖還是ISR同步自旋鎖你都不能用KeAcquireSpinLock加鎖,因?yàn)檫@個(gè)函數(shù)會(huì)把IRQL置為DISPATCH_LEVEL。你應(yīng)該使用KeSynchronizeExecution進(jìn)行同步。
與Linux內(nèi)核里的MCS spin lock類似的,NT內(nèi)核里也有排隊(duì)自旋鎖,而且據(jù)我所知,NT內(nèi)核比Linux更早實(shí)現(xiàn)排隊(duì)自旋鎖。排隊(duì)自旋鎖的工作方式如下:每個(gè)處理器上的執(zhí)行線程都有一個(gè)本地的標(biāo)志,通過(guò)該標(biāo)志,所有使用該鎖的處理器(鎖擁有者和等待者)被組織成一個(gè)單向隊(duì)列。當(dāng)一個(gè)處理器想要獲得一個(gè)已被其它處理器持有的排隊(duì)自旋鎖時(shí),它把自己的標(biāo)志放在該 Queued Spinlock 的單向隊(duì)列的末尾。如果當(dāng)前鎖持有者釋放了自旋鎖,則它將該鎖移交到隊(duì)列中位于自己之后的第一個(gè)處理器。同時(shí),如果一個(gè)處理器正在忙等待排隊(duì)自旋鎖,它并不是檢查該鎖自身的狀態(tài),而是檢查針對(duì)自己的標(biāo)志;在隊(duì)列中位于該處理器之前的處理器釋放自旋鎖時(shí)會(huì)設(shè)置這一標(biāo)志,以表明輪到這個(gè)正在等待的處理器了。獲取排隊(duì)自旋鎖的函數(shù)是KeAcquireInStackQueuedSpinLockAtDpcLevel,釋放鎖用KeReleaseInStackQueuedSpinLockFromDpcLevel。照著WDK文檔的說(shuō)法,給xp以后的操作系統(tǒng)寫驅(qū)動(dòng)都應(yīng)該用排隊(duì)自旋鎖而不是舊的自旋鎖。
死鎖
死鎖發(fā)生的條件想必大家都很清楚,避免方法我也沒(méi)必要多說(shuō),之所以在這里還要提這個(gè)話題是因?yàn)樽孕i有一個(gè)很奇葩的特性:即使只有一個(gè)線程一把鎖,它照樣會(huì)死鎖。為方便起見(jiàn)我們假設(shè)自旋鎖的實(shí)現(xiàn)是這樣的:
void lock(){//……// (1)while( test i not 0 and then set i into 0 ){while( test i not 0 );}// (2) }另有一個(gè)遞歸函數(shù)會(huì)調(diào)用lock()函數(shù),函數(shù)返回后釋放鎖,這時(shí)會(huì)發(fā)生什么事?我們看到第一次lock()函數(shù)運(yùn)行到(2)時(shí) i 是一定是0,接著執(zhí)行第二次lock(),那么那段set-and-test代碼一定沒(méi)法跳出,函數(shù)不會(huì)返回,鎖也就永遠(yuǎn)不會(huì)釋放。雖然只有一個(gè)線程一把鎖,卻也出現(xiàn)了典型的死鎖癥狀。這是使用自旋鎖時(shí)特有的問(wèn)題,其他鎖都不會(huì)有這種情況。
轉(zhuǎn)載于:https://www.cnblogs.com/gussing/archive/2012/06/11/2544475.html
總結(jié)
以上是生活随笔為你收集整理的WDK tips (9.1) 同步机制与锁的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 橙光游戏《风与樱之歌》游戏必备物品
- 下一篇: TP-Link TL-WDR4320 无