??
字號:
if(PreviousCount) {
_SEH_TRY . . . . . ._SEH_END;
}
}
/* Return Status */
return Status;
}[/code]
此類函數都是先調用ObReferenceObjectByHandle(),以取得指向目標對象數據結構的指針,然后就對此數據結構執行具體對象類型的具體操作,在這里是KeReleaseSemaphore()。
常規的V操作只使信號量加1,可以理解為只提供一張通行證,而KeReleaseSemaphore()則對此作了推廣,可以使信號量加N,即同時提供好幾張通行證,參數ReleaseCount就是這個增量,而PreviousCount則用來返回原來(V操作之前)的信號量數值。這里的常數IO_NO_INCREMENT定義為0,表示不需要提高被喚醒進程的調度優先級。
[code][NtReleaseSemaphore() > KeReleaseSemaphore()]
LONG STDCALL
KeReleaseSemaphore(PKSEMAPHORE Semaphore,
KPRIORITY Increment,
LONG Adjustment,
BOOLEAN Wait)
{
ULONG InitialState;
KIRQL OldIrql;
PKTHREAD CurrentThread;
. . . . . .
/* Lock the Dispatcher Database */
OldIrql = KeAcquireDispatcherDatabaseLock();
/* Save the Old State */
InitialState = Semaphore->Header.SignalState;
/* Check if the Limit was exceeded */
if (Semaphore->Limit < (LONG) InitialState + Adjustment ||
InitialState > InitialState + Adjustment) {
/* Raise an error if it was exceeded */
KeReleaseDispatcherDatabaseLock(OldIrql);
ExRaiseStatus(STATUS_SEMAPHORE_LIMIT_EXCEEDED);
}
/* Now set the new state */
Semaphore->Header.SignalState += Adjustment;
/* Check if we should wake it */
if (InitialState == 0 && !IsListEmpty(&Semaphore->Header.WaitListHead)) {
/* Wake the Semaphore */
KiWaitTest(&Semaphore->Header, Increment);
}
/* If the Wait is true, then return with a Wait and don't unlock the Dispatcher Database */
if (Wait == FALSE) {
/* Release the Lock */
KeReleaseDispatcherDatabaseLock(OldIrql);
} else {
/* Set a wait */
CurrentThread = KeGetCurrentThread();
CurrentThread->WaitNext = TRUE;
CurrentThread->WaitIrql = OldIrql;
}
/* Return the previous state */
return InitialState;
}[/code]
參數Adjustment就是信號量數值的增量,另一個參數Increment如為非0則表示要為被喚醒的線程暫時增加一些調度優先級,使其盡快得到運行的機會。還有個參數Wait的作用下面就會講到。
程序中KeAcquireDispatcherDatabaseLock()的作用是提升程序的運行級別(稱為IRQL,以后在別的漫談中會講到這個問題),以禁止線程調度,直至執行與之配對的函數KeReleaseDispatcherDatabaseLock()為止。這樣,在這兩個函數調用之間就形成了一個“調度禁區”。可是我們從代碼中看到,KeReleaseDispatcherDatabaseLock()之是否執行實際上取決于參數Wait。這是為什么呢?我在上一篇漫談中講到,在Windows中,當一個線程要在某個或某幾個對象上等待某些事態的發生時,有兩個系統調用可資調用,一個是NtWaitForSingleObject(),另一個是NtWaitForMultipleObjects()??墒瞧鋵嵾€有一個,就是NtSignalAndWaitForSingleObject(),只是這個系統調用有些特殊。正如其函數名所示,這個系統調用一方面是“Signal”一個對象,就是對其執行類似于KeReleaseSemaphore()這樣的操作;另一方面自己又立即在另一個對象上等待,類似于執行NtWaitForSingleObject();而且這二者應該是一氣呵成的。這樣就來了問題,如果在KeReleaseSemaphore()一類的函數中一律調用KeReleaseDispatcherDatabaseLock(),然后在NtWaitForSingleObject()中又調用KeAcquireDispatcherDatabaseLock(),那么在此二者之間就有個間隙,在此間隙中是可以發生線程調度的。再說,從程序效率的角度,那樣不必要地(且不說有害)來回折騰,也是不可取的,理應加以優化。像現在這樣有條件地執行KeReleaseDispatcherDatabaseLock(),就避免了這個問題。
對信號量本身的操作倒很簡單,就是改變Semaphore->Header.SignalState的數值。同時,如果有線程在睡眠等待(隊列非空),并且此前信號量的數值是0,那么既然現在退還了若干張通行證(增加了信號量的數值),就可以放幾個正在等待的進程進入臨界區了,所以通過KiWaitTest()喚醒等待中的進程。至于KiWaitTest(),讀者在上一篇漫談中已經看過它的代碼了。注意這里對于睡眠/喚醒的處理與傳統的P/V操作略有些不同。在傳統的P操作中,每執行一次P操作、不管能否進入臨界區、都使信號量的值遞減,所以信號量可以有負值,而且此時其絕對值就是正在睡眠等待的進程的數量。另一方面,當事進程之能否進入臨界區也是按遞減了以后的信號量數值判定的。而在NtWaitForSingleObject()、KiIsObjectSignaled()、KiSatisfyObjectWait()、以及KiWaitTest()的代碼中,則當事進程只有在信號量大于0時才能獲準進入臨界區,這樣的P操作才使信號量的值遞減,否則當事進程就被掛入等待隊列并進入睡眠,因此信號量不會有負值。所以,NtWaitForSingleObject()是變了形的P操作。
如前所述,信號量既可以用來實現臨界區,也可以使進程(線程)之間形成供應者/消費者的關系和互動。所以,雖然從表面上看信號量操作本身并不攜帶數據,但是它為高效的進程間通信提供了同步手段。另一方面,進程間同步也蘊含著信息的交換,也屬于進程間通信的范疇,所以信號量同時又是一種進程間通信機制。
3. 互斥門(Mutant)
互斥門(Mutant,又稱Mutex,實現于內核中稱Mutant,實現于用戶空間稱Mutex)是“信號量”的一個特例和變種。在信號量機制中,如果把信號量的最大值和初始值都設置成1,就成了互斥門。把信號量的最大值和初始值都設置成1,就相當于一共只有一張通行證,自然就只能有一個線程可以進入臨界區;在它退出臨界區之前,別的線程想要進入臨界區就只好在大門口睡眠等候。所謂“互斥”,就是因此而來。我的朋友胡希明老師曾把這樣的臨界區比作列車上的廁所(當時他常坐火車出差,想必屢屢為此所苦),二十多年過去了,當年的學生聚在一起還會因此事津津樂道。
不過,倘若純粹就是兩個參數的事,那就沒有必要另搞一套了。事實上互斥門機制有一些特殊性,下面讀者就會看到。
為互斥門的創建和打開提供了NtCreateMutant()和NtOpenMutant()兩個系統調用,代碼就不用看了。下面是互斥門對象的數據結構:
[code]typedef struct _KMUTANT {
DISPATCHER_HEADER Header;
LIST_ENTRY MutantListEntry;
struct _KTHREAD *RESTRICTED_POINTER OwnerThread;
BOOLEAN Abandoned;
UCHAR ApcDisable;
} KMUTANT, *PKMUTANT, KMUTEX, *PKMUTEX;[/code]
這個數據結構的定義見之于Windows NT的DDK,所以是“正宗”的??梢?,這數據結構就與信號量對象的不同。
跟信號量機制一樣,請求(試圖)通過互斥門進入臨界區的操作就是系統調用NtWaitForSingleObject()或NtWaitForMultipleObjects()。不過,在NtWaitForSingleObject()內部,特別是在判定能否進入臨界區時所調用的函數KiIsObjectSignaled()中,其實是按不同的對象類型分別處置的。我們不妨看一下。
[code][NtWaitForSingleObject() > KeWaitForSingleObject() > KiIsObjectSignaled()]
BOOLEAN inline FASTCALL
KiIsObjectSignaled(PDISPATCHER_HEADER Object, PKTHREAD Thread)
{
/* Mutants are...well...mutants! */
if (Object->Type == MutantObject) {
/*
* Because Cutler hates mutants, they are actually signaled if the Signal State is <= 0
* Well, only if they are recursivly acquired (i.e if we own it right now).
* Of course, they are also signaled if their signal state is 1.
*/
if ((Object->SignalState <= 0 && ((PKMUTANT)Object)->OwnerThread == Thread) ||
(Object->SignalState == 1)) {
/* Signaled Mutant */
return (TRUE);
} else {
/* Unsignaled Mutant */
return (FALSE);
}
}
/* Any other object is not a mutated freak, so let's use logic */
return (!Object->SignalState <= 0);
}[/code]
可見,互斥門在這里是作為一種特殊情況處理的,使KiIsObjectSignaled()返回TRUE、從而允許當事進程進入臨界區的條件之一是SignalState為1。另一個條件表明,只要是互斥門對象當前的“業主(Owner)”,就不受這個限制,即使沒有通行證也可以進入。那么誰是互斥門對象當前的業主呢?那就是當前已經在此臨界區中的線程,這一點讀者看了下面的代碼就會清楚??墒羌热皇且呀浽谂R界區中的線程,怎么又會企圖通過同一個互斥門進入同一個臨界區呢?這意味著一個線程可能遞歸地多次通過同一個互斥門。這個問題先擱一下,等一下再來探討。
在上一篇漫談中,我們看了KeWaitForSingleObject()的代碼,這是NtWaitForSingleObject()的主體,正是這個函數調用了KiIsObjectSignaled()。如果KiIsObjectSignaled()返回TRUE,那就說明當前進程可以領到通行證而進入臨界區,此時需要執行KiSatisfyObjectWait(),一方面是進行“賬面”上的處理,一方面也還有一些附加的操作需要進行,而這些附加的操作是因具體的對象而異的。我們再重溫一下這個函數的代碼。
[code][NtWaitForSingleObject() > KeWaitForSingleObject() > KiSatisfyObjectWait()]
VOID FASTCALL
KiSatisfyObjectWait(PDISPATCHER_HEADER Object, PKTHREAD Thread)
{
/* Special case for Mutants */
if (Object->Type == MutantObject) {
/* Decrease the Signal State */
Object->SignalState--;
/* Check if it's now non-signaled */
if (Object->SignalState == 0) {
/* Set the Owner Thread */
((PKMUTANT)Object)->OwnerThread = Thread;
/* Disable APCs if needed */
Thread->KernelApcDisable -= ((PKMUTANT)Object)->ApcDisable;
/* Check if it's abandoned */
if (((PKMUTANT)Object)->Abandoned) {
/* Unabandon it */
((PKMUTANT)Object)->Abandoned = FALSE;
/* Return Status */
Thread->WaitStatus = STATUS_ABANDONED;
}
/* Insert it into the Mutant List */
InsertHeadList(&Thread->MutantListHead,
&((PKMUTANT)Object)->MutantListEntry);
}
} else if ((Object->Type & TIMER_OR_EVENT_TYPE) == EventSynchronizationObject) {
/* These guys (Syncronization Timers and Events) just get un-signaled */
Object->SignalState = 0;
} else if (Object->Type == SemaphoreObject) {
/* These ones can have multiple signalings, so we only decrease it */
Object->SignalState--;
}
}[/code]
我們只看對于互斥門對象的處理。首先是遞減SignalState,這就是所謂“賬面”上的處理,也是P操作的一部分。由于前面已經通過KiIsObjectSignaled()進行過試探,如果當時的SignalState數值為1,或者說如果當時的臨界區是空的,那么現在的SignalState數值必定變成了0。所以,下面if語句中的代碼是在一個線程首次進入一個互斥門時執行的。這里說的“首次進入”并不是指退出以后又進去那樣的反復進出中的首次,而是指嵌套多次進入中的首次。當一個線程首次順利進入互斥門時,它就成了這個互斥門當前的業主,直至退出;所以把互斥門數據結構中的OwnerThread字段設置成指向當前線程的KTHREAD數據結構。此外,如果Abandoned字段顯示這個互斥門行將被丟棄,則暫時將其改成繼續使用(因為又有線程進來了),但是把這情況記錄在當前線程的KTHREAD數據結構中。互斥門數據結構中的ApcDisable字段表明通過互斥門進入臨界區的線程是否需要關閉APC請求,現在當前進程通過了互斥門,所以要把這信息記錄在它的數據結構中。注意這里是從Thread->KernelApcDisable的數值中減去互斥門的ApcDisable的值,結果為非0(負數)表示關閉APC請求,而ApcDisable的值則非1即0。舉例言之,假定Thread->KernelApcDisable原來是0,而ApcDisable為1,則相減以后的結果為-1,表示關閉APC請求。最后,當前進程既已成為這個互斥門的主人,二者之間就有了連系,所以通過隊列把它們結合起來,這是因為一個線程有可能同時存在于幾個臨界區中。
應該說這里別的都還好理解,成為問題的是為什么要允許嵌套進入互斥門。據“Programming the Microsoft Windows Driver Model”書中說,互斥門的特點之一就是允許嵌套進入,而優點之一則是可以防止死鎖。書中并沒有明確講這二者之間是否存在因果關系,所以我們只能分析和猜測。首先,如過互斥門不允許嵌套進入(在前面的代碼中取消允許當前業主進入的條件),而已經通過互斥門進入臨界區的線程又對同一個互斥門進行P操作,那么肯定是會引起死鎖的。這個線程會因為在P操作中不能通過互斥門而進入睡眠,能喚醒其睡眠的是已經在這個臨界區中的線程(如果它執行V操作的話),可是這正是已經在睡眠等待的那個線程本身,所以就永遠不會被喚醒。反之,有了前面KiIsObjectSignaled()中那樣的安排,即允許互斥門當前的業主遞歸通過,那確實就可以避免由此而導致的死鎖。
可是,為什么要企圖遞歸通過同一個互斥門呢?既然已經通過這個具體的互斥門進入了臨界區,為什么還要再一次試圖進入同一個互斥門呢?應該說,在精心設計和實現的軟件中是不應該有這種情況出現的??墒?,考慮到應用軟件的可重用(reuse),有時候也許會有這種情況。例如,一個線程在臨界區內可能調用某個軟件模塊所提供的操作,而這個軟件模塊可能需要通過NtWaitForMultipleObjects()進入由多個互斥門保護的復合臨界區,可是其中之一就是已經進入的那個互斥門。在這種情況下,對于已經進入的那個互斥門而言,就構成了遞歸進入。當然,我們可以通過修改那個軟件模塊來避免此種遞歸,但這可能又不是很現實。在這樣的條件下,允許遞歸進入不失為一個簡單的解決方案。
還要說明,允許遞歸通過互斥門固然可以防止此種特定形式的死鎖,卻并不是對所有的死鎖都有效。真要防止死鎖,還是得遵守有關的準則,精心設計,精心實現。
讀者也許會問:這里所引的代碼出自ReactOS,所反映的是ReactOS的作者們對Windows互斥門的理解,但是他們的理解是否正確呢?確實,Windows的代碼是不公開的,所以也無從對比??墒牵m然Windows的代碼不公開,它的一些數據結構的定義卻是公開的,這里面就包括KMUTANT,所以前面特地說明了這是來自Windows DDK(其實ReactOS的許多數據結構都可以在DDK中找到)。既然我們知道互斥門允許遞歸進入,又看到KMUTANT中確有OwnerThread這個指針,那么我們就有理由相信ReactOS的這些代碼離“真相”不會太遠。當然,我們還可以、也應該、設計出一些實驗來加以對比、驗證。
再看從臨界區退出并交還“通行證”的操作、即V操作,這就是系統調用NtReleaseMutant()。
[code]NTSTATUS STDCALL
NtReleaseMutant(IN HANDLE MutantHandle, IN PLONG PreviousCount OPTIONAL)
{
PKMUTANT Mutant;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status = STATUS_SUCCESS;
. . . . . .
if(PreviousMode == UserMode && PreviousCount) {
_SEH_TRY . . . . . . _SEH_END;
if(!NT_SUCCESS(Status)) return Status;
}
/* Open the Object */
Status = ObReferenceObjectByHandle(MutantHandle, MUTANT_QUERY_STATE,
ExMutantObjectType, PreviousMode, (PVOID*)&Mutant, NULL);
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -