?? 漫談兼容內(nèi)核之十五:windows線程的等待、喚醒機制.txt
字號:
漫談兼容內(nèi)核之十五:Windows線程的等待/喚醒機制
[align=center] 毛德操[/align]
對于任何一個現(xiàn)代的操作系統(tǒng),進程間通信都是不可或缺的。
共享內(nèi)存區(qū)顯然可以用作進程間通信的手段。兩個進程把同一組物理內(nèi)存頁面分別映射到各自的用戶空間,然后一個進程往里面寫,另一個進程就可以讀到所寫入的內(nèi)容。所以,共享內(nèi)存區(qū)天然就是一種進程間通信機制。但是這又是很原始的手段,因為這里有個讀出方如何知道共享區(qū)的內(nèi)容已經(jīng)被寫入方改變的問題。輪詢,或者定期輪詢,當(dāng)然也是個辦法,但是一般而言效率畢竟太低。所以,這里需要有個能夠?qū)νㄐ烹p方的活動加以有效協(xié)調(diào)的機制,這就是“進程間同步”機制。進程間同步本身也是一種進程間通信(因為涉及信息的交換),當(dāng)然也是一種原始的進程間通信,但同時又是更高級的進程間通信機制的基石。
所以,在談?wù)撏ㄐ艡C制之前,應(yīng)該先考察一下進程間同步機制。在Linux中,這就是進程的睡眠/喚醒機制,或者說阻塞/解阻塞機制,體現(xiàn)為信息的接收方(進程)在需要讀取信息、而發(fā)送方(進程)尚未向其發(fā)送之時就進入睡眠,到發(fā)送方向其發(fā)送信息時則加以喚醒。在Windows中,這個過程的原理是一樣的,只是名稱略有不同,稱為“等待/喚醒”,表現(xiàn)形式上也有些不同。
在Windows中,進程間通信必須憑籍著某個已打開的“對象(Object)”才能發(fā)生(其實Linux中也是一樣,只是沒有統(tǒng)一到“對象”這個概念上)。我們不妨把這樣的對象想像成某類貨品的倉庫,信息的接受方試圖向這個倉庫領(lǐng)貨。如果已經(jīng)到貨,那當(dāng)然可以提了就走,但要是尚未到貨就只好等待,到一邊歇著去(睡眠),直至到了貨才把它(喚醒)叫回來提貨。Windows專門為此過程提供了兩個系統(tǒng)調(diào)用,一個是NtWaitForSingleObject(),另一個是NtWaitForMultipleObjects()。后者是前者的推廣、擴充,使得一個線程可以同時在多個對象上等待。
于是,在Windows應(yīng)用程序中,當(dāng)一個線程需要從某個對象“提貨”、即獲取信息時,就通過系統(tǒng)調(diào)用NtWaitForSingleObject()實現(xiàn)在目標(biāo)對象上的等待,當(dāng)前線程因此而被“阻塞”、即進入睡眠狀態(tài),直至所等待的條件得到滿足時才被喚醒。
[code]NTSTATUS STDCALL
NtWaitForSingleObject(IN HANDLE ObjectHandle,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER TimeOut OPTIONAL)
{
. . . . . .
PreviousMode = ExGetPreviousMode();
if(TimeOut != NULL && PreviousMode != KernelMode)
{
_SEH_TRY
{
ProbeForRead(TimeOut, sizeof(LARGE_INTEGER), sizeof(ULONG));
/* make a copy on the stack */
SafeTimeOut = *TimeOut;
TimeOut = &SafeTimeOut;
}
_SEH_HANDLE
{
Status = _SEH_GetExceptionCode();
}
_SEH_END;
if(!NT_SUCCESS(Status))
{
return Status;
}
}
Status = ObReferenceObjectByHandle(ObjectHandle, SYNCHRONIZE, NULL,
PreviousMode, &ObjectPtr, NULL);
. . . . . .
if (!KiIsObjectWaitable(ObjectPtr))
{
DPRINT1("Waiting for object type '%wZ' is not supported\n",
&BODY_TO_HEADER(ObjectPtr)->ObjectType->TypeName);
Status = STATUS_HANDLE_NOT_WAITABLE;
}
else
{
Status = KeWaitForSingleObject(ObjectPtr, UserRequest,
PreviousMode, Alertable, TimeOut);
}
ObDereferenceObject(ObjectPtr);
return(Status);
}[/code]
參數(shù)ObjectHandle和TimeOut的作用不言自明。另一個參數(shù)Alertable是個布爾量,表示是否允許本次等待因用戶空間APC而中斷,或者說被“警醒”。警醒與喚醒是不同的,喚醒是因為所等待的條件得到了滿足(倉庫到了貨),而警醒是因為別的原因(與倉庫無關(guān))。
我們知道,Windows的系統(tǒng)調(diào)用函數(shù)既可以從用戶空間通過自陷指令int 0x2e加以調(diào)用,也可以在內(nèi)核中直接加以調(diào)用。如果是從用戶空間調(diào)用,而且又有以指針形式傳遞的參數(shù),那就需要從用戶空間讀取這些指針?biāo)傅膬?nèi)容。但是,這些指針?biāo)柑幍?虛存)頁面是否有映射呢?這是沒有保證的。如果沒有映射,那么在訪問時就會發(fā)生“頁面錯誤”異常。另一方面,既然讀不到調(diào)用參數(shù),原定的操作也就無法繼續(xù)下去了。為此,代碼中把對于目標(biāo)是否可讀的測試ProbeForRead()以及參數(shù)內(nèi)容的復(fù)制放在_SEH_TRY{}中,并且設(shè)置好“頁面錯誤”異常處理的向量,使得一旦發(fā)生“頁面錯誤”異常就執(zhí)行_SEH_HANDLE{}中的操作。這是Windows的“結(jié)構(gòu)化出錯處理”即SHE機制的一部分,以后還要有專文介紹。由于篇幅的關(guān)系,以后在系統(tǒng)調(diào)用的程序中就不再列出這些代碼了。
NtWaitForSingleObject()中實質(zhì)性的操作只有兩個。一是ObReferenceObjectByHandle(),就是通過已打開對象的Handle獲取指向該目標(biāo)對象(數(shù)據(jù)結(jié)構(gòu))的指針。第二個操作就是KeWaitForSingleObject(),這是下面要講的。不過,并非對于所有的對象都可以執(zhí)行這個函數(shù),有的對象是“可等待”的,有的對象卻是“不可等待”的,所以先要通過一個函數(shù)KiIsObjectWaitable()加以檢驗。這樣,一言以蔽之,NtWaitForSingleObject()的作用就是對可等待目標(biāo)對象的數(shù)據(jù)結(jié)構(gòu)執(zhí)行KeWaitForSingleObject()。
那么什么樣的對象才是可等待的呢?看一下這個函數(shù)的代碼就知道了:
[code]BOOL inline FASTCALL KiIsObjectWaitable(PVOID Object)
{
POBJECT_HEADER Header;
Header = BODY_TO_HEADER(Object);
if (Header->ObjectType == ExEventObjectType ||
Header->ObjectType == ExIoCompletionType ||
Header->ObjectType == ExMutantObjectType ||
Header->ObjectType == ExSemaphoreObjectType ||
Header->ObjectType == ExTimerType ||
Header->ObjectType == PsProcessType ||
Header->ObjectType == PsThreadType ||
Header->ObjectType == IoFileObjectType) {
return TRUE;
} else {
return FALSE;
}
}[/code]
可見,所謂“可等待”的對象包括進程、線程、Timer、文件,以及用于進程間通信的對象Event、Mutant、Semaphore,還有用于設(shè)備驅(qū)動的IoCompletion。這IoCompletion屬于設(shè)備驅(qū)動框架,所以KeWaitForSingleObject()既是進程間通信的重要一環(huán),同時也是設(shè)備驅(qū)動框架的一個重要組成部分。
注意這里(取自ReactOS)關(guān)于對象數(shù)據(jù)結(jié)構(gòu)的處理是很容易讓人摸不著頭腦的,因而需要加一些說明。首先,每個進程的“打開對象表”是由Handle表項構(gòu)成的,是一個HANDLE_TABLE_ENTRY結(jié)構(gòu)指針數(shù)組。而HANDLE_TABLE_ENTRY數(shù)據(jù)結(jié)構(gòu)中有個指針指向另一個數(shù)據(jù)結(jié)構(gòu)(而且這個指針的低位又被用于一些標(biāo)志位),可是這個數(shù)據(jù)結(jié)構(gòu)并非具體對象的數(shù)據(jù)結(jié)構(gòu),而是一個通用的OBJECT_HEADER數(shù)據(jù)結(jié)構(gòu):
[code]typedef struct _OBJECT_HEADER
/*
* PURPOSE: Header for every object managed by the object manager
*/
{
UNICODE_STRING Name;
LIST_ENTRY Entry;
LONG RefCount;
LONG HandleCount;
BOOLEAN Permanent;
BOOLEAN Inherit;
struct _DIRECTORY_OBJECT* Parent;
POBJECT_TYPE ObjectType;
PSECURITY_DESCRIPTOR SecurityDescriptor;
/*
* PURPOSE: Object type
* NOTE: This overlaps the first member of the object body
*/
CSHORT Type;
/*
* PURPOSE: Object size
* NOTE: This overlaps the second member of the object body
*/
CSHORT Size;
} OBJECT_HEADER, *POBJECT_HEADER;[/code]
緊隨在OBJECT_HEADER后面的才是具體對象的數(shù)據(jù)結(jié)構(gòu)的正身、即Body。所以O(shè)BJECT_HEADER和Body合在一起才構(gòu)成一個對象的完整的數(shù)據(jù)結(jié)構(gòu)。但是,當(dāng)傳遞一個對象的數(shù)據(jù)結(jié)構(gòu)指針時,所傳遞的指針卻既不是指向其正身,又不是指向其OBJECT_HEADER,而是指向其OBJECT_HEADER結(jié)構(gòu)中的字段Type。宏定義HEADER_TO_BODY說明了這一點:
[code]#define HEADER_TO_BODY(objhdr) \
(PVOID)((ULONG_PTR)objhdr + sizeof(OBJECT_HEADER) \
- sizeof(COMMON_BODY_HEADER))[/code]
就是說,具體對象數(shù)據(jù)結(jié)構(gòu)的起點是objhdr加上OBJECT_HEADER的大小、再減去COMMON_BODY_HEADER的大小。而COMMON_BODY_HEADER定義為:
[code]typedef struct
{
CSHORT Type;
CSHORT Size;
} COMMON_BODY_HEADER, *PCOMMON_BODY_HEADER;[/code]
顯然這就是OBJECT_HEADER中的最后兩個字段。那么具體對象的數(shù)據(jù)結(jié)構(gòu)又是什么樣的呢?我們以Semaphore的數(shù)據(jù)結(jié)構(gòu)KSEMAPHORE為例:
[code]typedef struct _KSEMAPHORE {
DISPATCHER_HEADER Header;
LONG Limit;
} KSEMAPHORE;[/code]
它的第一個成分是一個DISPATCHER_HEADER數(shù)據(jù)結(jié)構(gòu),但是卻看不到Type和Size這兩個字段,也看不到COMMON_BODY_HEADER。我們進一步看DISPATCHER_HEADER的定義:
[code]typedef struct _DISPATCHER_HEADER {
UCHAR Type;
UCHAR Absolute;
UCHAR Size;
UCHAR Inserted;
LONG SignalState;
LIST_ENTRY WaitListHead;
} DISPATCHER_HEADER, *PDISPATCHER_HEADER;[/code]
與COMMON_BODY_HEADER相比較,我們確實看到這里有Type和Size,但是中間卻又夾著別的字段。但是,仔細(xì)觀察,就可看出在COMMON_BODY_HEADER中Type的類型是16位的CSHORT,而在這里是8位的UCHAR,而且下面Absolute的類型也是UCHAR。這就清楚了,原來COMMON_BODY_HEADER中(以及OBJECT_HEADER中)的Type雖然是16位的,實際上卻只用了其低8位,而在DISPATCHER_HEADER中則將其高8位用作Absolute。編譯器在分配空間時是由低到高(地址)分配的,所以Type是低8位而Absolute是高8位。同樣的道理也適用于Size和Inserted。這樣的安排當(dāng)然使代碼的可讀性變得很差,筆者尚不明白為什么非得要這么干。另一方面,不同的對象有不同的數(shù)據(jù)結(jié)構(gòu),所以代碼中有關(guān)對象指針的類型一般總是PVOID,這似乎也合理。但是既然第一個成分總是DISPATCHER_HEADER,那為什么不用PDISPATCHER_HEADER呢?那樣至少也可以改善一些可讀性。
回到NtWaitForSingleObject()的代碼,我們需要進一步往下看KeWaitForSingleObject()的代碼。不過在此之前先得考察一下有關(guān)的數(shù)據(jù)結(jié)構(gòu)。
首先,執(zhí)行這個函數(shù)的主體是個線程,而所等待的又是通過一個對象傳遞的信息,就一定要有個數(shù)據(jù)結(jié)構(gòu)把這二者連系起來,這就是KWAIT_BLOCK數(shù)據(jù)結(jié)構(gòu):
[code]typedef struct _KWAIT_BLOCK
/*
* PURPOSE: Object describing the wait a thread is currently performing
*/
{
LIST_ENTRY WaitListEntry;
struct _KTHREAD* Thread;
struct _DISPATCHER_HEADER *Object;
struct _KWAIT_BLOCK* NextWaitBlock;
USHORT WaitKey;
USHORT WaitType;
} KWAIT_BLOCK, *PKWAIT_BLOCK;[/code]
這里的Thread和Object都是指針。前者指向一個KTHREAD數(shù)據(jù)結(jié)構(gòu),代表著正在等待的線程;后者指向一個對象的數(shù)據(jù)結(jié)構(gòu),雖然指針的類型是DISPATCHER_HEADER*,但是如上所述這是不管什么對象的數(shù)據(jù)結(jié)構(gòu)中的第一個成分,所以指向這個數(shù)據(jù)結(jié)構(gòu)也就是指向了它所在對象的數(shù)據(jù)結(jié)構(gòu)。此外,結(jié)構(gòu)中的成分WaitListEntry顯然是用來把這個數(shù)據(jù)結(jié)構(gòu)掛入某個(雙鏈)隊列的,同時指針NextWaitBlock也是用來維持一個(單鏈)隊列。這是因為一個“等待塊”即KWAIT_BLOCK數(shù)據(jù)結(jié)構(gòu)可能同時出現(xiàn)在兩個隊列中。首先,多個線程可能在同一個對象上等待,每個線程為此都有一個等待塊,從而形成特定目標(biāo)對象的等待隊列,這就是由WaitListEntry維持的隊列。這樣,對于一個具體的對象而言,其等待隊列中的每個等待塊都代表著一個線程。同時,一個線程又可能同時在多個對象上等待,因而又可能有多個等待塊。對于這個線程而言,每個等待塊都代表著一個不同的對象,這些等待塊則通過NextWaitBlock構(gòu)成一個隊列。其余字段的作用以后就會明白。
既然等待是具體線程的行為,線程數(shù)據(jù)結(jié)構(gòu)中就得有相應(yīng)的安排,KTHREAD結(jié)構(gòu)中與此有關(guān)的成分如下:
[code]typedef struct _KTHREAD
{
/* For waiting on thread exit */
DISPATCHER_HEADER DispatcherHeader; /* 00 */
. . . . . .
LONG WaitStatus; /* 50 */
KIRQL WaitIrql; /* 54 */
CHAR WaitMode; /* 55 */
UCHAR WaitNext; /* 56 */
UCHAR WaitReason; /* 57 */
PKWAIT_BLOCK WaitBlockList; /* 58 */
LIST_ENTRY WaitListEntry; /* 5C */
ULONG WaitTime; /* 64 */
CHAR BasePriority; /* 68 */
UCHAR DecrementCount; /* 69 */
UCHAR PriorityDecrement; /* 6A */
CHAR Quantum; /* 6B */
KWAIT_BLOCK WaitBlock[4]; /* 6C */
PVOID LegoData; /* CC */
. . . . . .
} KTHREAD;[/code]
首先我們注意到這里有個結(jié)構(gòu)數(shù)組WaitBlock[4],這就是KWAIT_BLOCK數(shù)據(jù)結(jié)構(gòu)座落所在,需要時就在這里就地取材。之所以是個數(shù)組,是因為有時候需要同時在多個對象上等待,這就是KeWaitForMultipleObjects()的目的,有點類似于Linux中的select()。此時KWAIT_BLOCK指針WaitBlockList指向本線程的等待塊隊列。如前所述,這個隊列中的每個等待塊都代表著一個對象。WaitStatus則是狀態(tài)信息,在結(jié)束等待時反映著結(jié)束的原因。
下面我們就來看KeWaitForSingleObject()的代碼。
[code][NtWaitForSingleObject() > KeWaitForSingleObject()]
NTSTATUS STDCALL
KeWaitForSingleObject(PVOID Object,
KWAIT_REASON WaitReason,
KPROCESSOR_MODE WaitMode,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout)
{
PDISPATCHER_HEADER CurrentObject;
PKWAIT_BLOCK WaitBlock;
PKWAIT_BLOCK TimerWaitBlock;
PKTIMER ThreadTimer;
PKTHREAD CurrentThread = KeGetCurrentThread();
NTSTATUS Status;
NTSTATUS WaitStatus;
. . . . . .
/* Check if the lock is already held */
if (CurrentThread->WaitNext) {
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -