?? 漫談兼容內核之十五:windows線程的等待、喚醒機制.txt
字號:
漫談兼容內核之十五:Windows線程的等待/喚醒機制
[align=center] 毛德操[/align]
對于任何一個現代的操作系統,進程間通信都是不可或缺的。
共享內存區顯然可以用作進程間通信的手段。兩個進程把同一組物理內存頁面分別映射到各自的用戶空間,然后一個進程往里面寫,另一個進程就可以讀到所寫入的內容。所以,共享內存區天然就是一種進程間通信機制。但是這又是很原始的手段,因為這里有個讀出方如何知道共享區的內容已經被寫入方改變的問題。輪詢,或者定期輪詢,當然也是個辦法,但是一般而言效率畢竟太低。所以,這里需要有個能夠對通信雙方的活動加以有效協調的機制,這就是“進程間同步”機制。進程間同步本身也是一種進程間通信(因為涉及信息的交換),當然也是一種原始的進程間通信,但同時又是更高級的進程間通信機制的基石。
所以,在談論通信機制之前,應該先考察一下進程間同步機制。在Linux中,這就是進程的睡眠/喚醒機制,或者說阻塞/解阻塞機制,體現為信息的接收方(進程)在需要讀取信息、而發送方(進程)尚未向其發送之時就進入睡眠,到發送方向其發送信息時則加以喚醒。在Windows中,這個過程的原理是一樣的,只是名稱略有不同,稱為“等待/喚醒”,表現形式上也有些不同。
在Windows中,進程間通信必須憑籍著某個已打開的“對象(Object)”才能發生(其實Linux中也是一樣,只是沒有統一到“對象”這個概念上)。我們不妨把這樣的對象想像成某類貨品的倉庫,信息的接受方試圖向這個倉庫領貨。如果已經到貨,那當然可以提了就走,但要是尚未到貨就只好等待,到一邊歇著去(睡眠),直至到了貨才把它(喚醒)叫回來提貨。Windows專門為此過程提供了兩個系統調用,一個是NtWaitForSingleObject(),另一個是NtWaitForMultipleObjects()。后者是前者的推廣、擴充,使得一個線程可以同時在多個對象上等待。
于是,在Windows應用程序中,當一個線程需要從某個對象“提貨”、即獲取信息時,就通過系統調用NtWaitForSingleObject()實現在目標對象上的等待,當前線程因此而被“阻塞”、即進入睡眠狀態,直至所等待的條件得到滿足時才被喚醒。
[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]
參數ObjectHandle和TimeOut的作用不言自明。另一個參數Alertable是個布爾量,表示是否允許本次等待因用戶空間APC而中斷,或者說被“警醒”。警醒與喚醒是不同的,喚醒是因為所等待的條件得到了滿足(倉庫到了貨),而警醒是因為別的原因(與倉庫無關)。
我們知道,Windows的系統調用函數既可以從用戶空間通過自陷指令int 0x2e加以調用,也可以在內核中直接加以調用。如果是從用戶空間調用,而且又有以指針形式傳遞的參數,那就需要從用戶空間讀取這些指針所指的內容。但是,這些指針所指處的(虛存)頁面是否有映射呢?這是沒有保證的。如果沒有映射,那么在訪問時就會發生“頁面錯誤”異常。另一方面,既然讀不到調用參數,原定的操作也就無法繼續下去了。為此,代碼中把對于目標是否可讀的測試ProbeForRead()以及參數內容的復制放在_SEH_TRY{}中,并且設置好“頁面錯誤”異常處理的向量,使得一旦發生“頁面錯誤”異常就執行_SEH_HANDLE{}中的操作。這是Windows的“結構化出錯處理”即SHE機制的一部分,以后還要有專文介紹。由于篇幅的關系,以后在系統調用的程序中就不再列出這些代碼了。
NtWaitForSingleObject()中實質性的操作只有兩個。一是ObReferenceObjectByHandle(),就是通過已打開對象的Handle獲取指向該目標對象(數據結構)的指針。第二個操作就是KeWaitForSingleObject(),這是下面要講的。不過,并非對于所有的對象都可以執行這個函數,有的對象是“可等待”的,有的對象卻是“不可等待”的,所以先要通過一個函數KiIsObjectWaitable()加以檢驗。這樣,一言以蔽之,NtWaitForSingleObject()的作用就是對可等待目標對象的數據結構執行KeWaitForSingleObject()。
那么什么樣的對象才是可等待的呢?看一下這個函數的代碼就知道了:
[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,還有用于設備驅動的IoCompletion。這IoCompletion屬于設備驅動框架,所以KeWaitForSingleObject()既是進程間通信的重要一環,同時也是設備驅動框架的一個重要組成部分。
注意這里(取自ReactOS)關于對象數據結構的處理是很容易讓人摸不著頭腦的,因而需要加一些說明。首先,每個進程的“打開對象表”是由Handle表項構成的,是一個HANDLE_TABLE_ENTRY結構指針數組。而HANDLE_TABLE_ENTRY數據結構中有個指針指向另一個數據結構(而且這個指針的低位又被用于一些標志位),可是這個數據結構并非具體對象的數據結構,而是一個通用的OBJECT_HEADER數據結構:
[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后面的才是具體對象的數據結構的正身、即Body。所以OBJECT_HEADER和Body合在一起才構成一個對象的完整的數據結構。但是,當傳遞一個對象的數據結構指針時,所傳遞的指針卻既不是指向其正身,又不是指向其OBJECT_HEADER,而是指向其OBJECT_HEADER結構中的字段Type。宏定義HEADER_TO_BODY說明了這一點:
[code]#define HEADER_TO_BODY(objhdr) \
(PVOID)((ULONG_PTR)objhdr + sizeof(OBJECT_HEADER) \
- sizeof(COMMON_BODY_HEADER))[/code]
就是說,具體對象數據結構的起點是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中的最后兩個字段。那么具體對象的數據結構又是什么樣的呢?我們以Semaphore的數據結構KSEMAPHORE為例:
[code]typedef struct _KSEMAPHORE {
DISPATCHER_HEADER Header;
LONG Limit;
} KSEMAPHORE;[/code]
它的第一個成分是一個DISPATCHER_HEADER數據結構,但是卻看不到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,但是中間卻又夾著別的字段。但是,仔細觀察,就可看出在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。這樣的安排當然使代碼的可讀性變得很差,筆者尚不明白為什么非得要這么干。另一方面,不同的對象有不同的數據結構,所以代碼中有關對象指針的類型一般總是PVOID,這似乎也合理。但是既然第一個成分總是DISPATCHER_HEADER,那為什么不用PDISPATCHER_HEADER呢?那樣至少也可以改善一些可讀性。
回到NtWaitForSingleObject()的代碼,我們需要進一步往下看KeWaitForSingleObject()的代碼。不過在此之前先得考察一下有關的數據結構。
首先,執行這個函數的主體是個線程,而所等待的又是通過一個對象傳遞的信息,就一定要有個數據結構把這二者連系起來,這就是KWAIT_BLOCK數據結構:
[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數據結構,代表著正在等待的線程;后者指向一個對象的數據結構,雖然指針的類型是DISPATCHER_HEADER*,但是如上所述這是不管什么對象的數據結構中的第一個成分,所以指向這個數據結構也就是指向了它所在對象的數據結構。此外,結構中的成分WaitListEntry顯然是用來把這個數據結構掛入某個(雙鏈)隊列的,同時指針NextWaitBlock也是用來維持一個(單鏈)隊列。這是因為一個“等待塊”即KWAIT_BLOCK數據結構可能同時出現在兩個隊列中。首先,多個線程可能在同一個對象上等待,每個線程為此都有一個等待塊,從而形成特定目標對象的等待隊列,這就是由WaitListEntry維持的隊列。這樣,對于一個具體的對象而言,其等待隊列中的每個等待塊都代表著一個線程。同時,一個線程又可能同時在多個對象上等待,因而又可能有多個等待塊。對于這個線程而言,每個等待塊都代表著一個不同的對象,這些等待塊則通過NextWaitBlock構成一個隊列。其余字段的作用以后就會明白。
既然等待是具體線程的行為,線程數據結構中就得有相應的安排,KTHREAD結構中與此有關的成分如下:
[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]
首先我們注意到這里有個結構數組WaitBlock[4],這就是KWAIT_BLOCK數據結構座落所在,需要時就在這里就地取材。之所以是個數組,是因為有時候需要同時在多個對象上等待,這就是KeWaitForMultipleObjects()的目的,有點類似于Linux中的select()。此時KWAIT_BLOCK指針WaitBlockList指向本線程的等待塊隊列。如前所述,這個隊列中的每個等待塊都代表著一個對象。WaitStatus則是狀態信息,在結束等待時反映著結束的原因。
下面我們就來看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) {
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -