windows核心情景分析---執行緒同步
基於同步物件的等待、喚醒機制:
一個執行緒可以等待一個物件或多個物件而進入等待狀態(也叫睡眠狀態),另一個執行緒可以觸發那個等待物件,喚醒在那個物件上等待的所有執行緒。
一個執行緒可以等待一個物件或多個物件,而一個物件也可以同時被N個執行緒等待。這樣,執行緒與等待物件之間是多對多的關係。他們之間的等待關係由一個佇列和一個‘等待塊’來控制,等待塊就是執行緒與等待目標物件之間的紐帶。
WaitForSingleObject可以等待那些“可等待物件”,哪些物件是‘可等待’的呢?程序、執行緒、作業、檔案物件、IO完成埠、可等待定時器、互斥、事件、訊號量等,這些都是‘可等待’物件,可用於WaitForSingleObject等函式。
‘可等待’物件又分為‘可直接等待物件’和‘可間接等待物件’
互斥、事件、訊號量、程序、執行緒這些物件由於內部結構中的自第一個欄位是DISPATCHER_HEADER結構(可以看成是繼承了DISPATCHER_HEADER),因此是可直接等待的。而檔案物件不帶這個結構,但檔案物件內部有一個事件物件,因此,檔案物件是‘可間接等待物件’。
比如:訊號量就是一種可直接等待物件,它的結構如下:
Struct KSEMAPHORE
{
DISPATCHER_HEADER Header;//公共頭
LONG Limit;//最大訊號量個數
}
Struct DISPATCHER_HEADER
{
…
LONG SignalState;//訊號狀態量(>0表示有訊號,<=0表示無訊號)
LIST_ENTRY WaitListHead;//等待塊佇列
…
}
WaitForSingleObject內部最終呼叫下面的系統服務
NTSTATUS
NtWaitForSingleObject(IN HANDLE ObjectHandle,//直接或間接可等待物件的控制代碼
IN BOOLEAN Alertable,//表示本次等待操作是否可被吵醒(即被強制喚醒)
IN PLARGE_INTEGER TimeOut OPTIONAL)//超時
{
PVOID Object, WaitableObject;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
LARGE_INTEGER SafeTimeOut;
NTSTATUS Status;
if ((TimeOut) && (PreviousMode != KernelMode))
{
_SEH2_TRY
{
SafeTimeOut = ProbeForReadLargeInteger(TimeOut);
TimeOut = &SafeTimeOut;
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
Status = ObReferenceObjectByHandle(ObjectHandle,SYNCHRONIZE,NULL,PreviousMode,
&Object,NULL);
if (NT_SUCCESS(Status))
{
//得到那個物件的‘可直接等待物件’DefaultObject
WaitableObject = OBJECT_TO_OBJECT_HEADER(Object)->Type->DefaultObject;
if (IsPointerOffset(WaitableObject))//if DefaultObject是個偏移,不是指標
{
//加上偏移值,獲得內部的‘可直接等待物件’
WaitableObject = (PVOID)((ULONG_PTR)Object + (ULONG_PTR)WaitableObject);
}
_SEH2_TRY
{
Status = KeWaitForSingleObject(WaitableObject,//這個函式只能等待‘直接等待物件’
UserRequest,PreviousMode,Alertable,TimeOut);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
}
_SEH2_END;
ObDereferenceObject(Object);
}
return Status;
}
#define IsPointerOffset(Ptr) ((LONG)(Ptr) >= 0)
如上,每個物件的物件型別都有一個預設的可直接等待物件,要麼直接指向物件,要麼是個偏移值。
如果是個偏移值,那麼DefaultObject值的最高位為0,否則為1。
NtWaitForSingleObject可以等待直接的、間接的可等待物件,然而KeWaitForSingleObject只能等待真正的可直接等待物件,所以,必須將間接可等待物件轉換為直接可等待物件,而每個物件的可直接等待物件記錄在其物件型別的DefaultObject欄位中。
下面這個函式是重點。這個函式很不好理解,我也是認真看了好幾遍才有所明白,這個函式本身邏輯量比較大,函式也較長。重要的是對喚醒原因的理解。(把WaitStatus理解為喚醒原因就好了)
NTSTATUS //返回值表示本次睡眠的喚醒原因
//注意下面的函式只能在DISPATCH_LEVEL以下呼叫,否則藍屏。(除非Timeout!=NULL && *Timeout==0)
KeWaitForSingleObject(IN PVOID Object,//要等待的‘可直接等待物件’
IN KWAIT_REASON WaitReason,//執行緒上次被切出原因
IN KPROCESSOR_MODE WaitMode,//表示這是來自使用者模式/核心模式的等待請求
IN BOOLEAN Alertable,//表示本次等待操作是否可以被強制喚醒
IN PLARGE_INTEGER Timeout OPTIONAL)//超時值
{
PKTHREAD Thread = KeGetCurrentThread();
PKMUTANT CurrentObject = (PKMUTANT)Object;//其實意思是(DISPATCHER_HEADER*)Object
PKWAIT_BLOCK WaitBlock = &Thread->WaitBlock[0];
//內建的第4個等待塊固定用於定時器(TIMER_WAIT_BLOCK是索引3)
PKWAIT_BLOCK TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];
PKTIMER Timer = &Thread->Timer;//複用這個超時定時器
NTSTATUS WaitStatus;//其實表示‘上次喚醒原因’
BOOLEAN Swappable;//核心棧是否可換到外存
LARGE_INTEGER DueTime, NewDueTime, InterruptTime;
PLARGE_INTEGER OriginalDueTime = Timeout;
ULONG Hand = 0;
//首次等待時需從WaitStart處開始,加鎖,提升irql至DISPATCH_LEVEL
if (!Thread->WaitNext) goto WaitStart;
Thread->WaitNext = FALSE;//復位
KxSingleThreadWait();//這是一個巨集,主要用來構造當前執行緒的等待塊連結串列
for (;;)//首次迴圈、以後每次臨時喚醒切回來時的入口
{
Thread->Preempted = FALSE;//表示因等待而主動放棄的cpu,不是被搶佔
//if進入等待態前的原irql是APC_LEVEL,每次切回來時會自動在KiSwapContextInternal執行掉所有核心APC。if進入等待態前的原irql是APC_LEVEL,則用下面的語句手動執行掉所有核心APC。總之,確保每次切回來時,總是會先執行掉所有核心APC
if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) &&
(Thread->WaitIrql == PASSIVE_LEVEL))
{
KiReleaseDispatcherLock(Thread->WaitIrql);//執行掉所有Pending中的核心APC
}
//每輪迴圈中(即每次切回來時),測試是否條件成熟,可以退出睡眠。可以退出的3種條件為:
1、 所等待的物件有訊號了
2、 可被強制喚醒了
3、 超時了
Else
{
//先檢測所等待的物件是否有訊號了,不過互斥物件需要特殊判斷。
if (CurrentObject->Header.Type == MutantObject)
{
//if等待的互斥物件有訊號了,或者當前執行緒本身就是該互斥物件的擁有者(一個執行緒可以反覆多次等待同一個互斥物件的)
if ((CurrentObject->Header.SignalState > 0) ||
(Thread == CurrentObject->OwnerThread))
{
if (CurrentObject->Header.SignalState != (LONG)MINLONG)
{
KiSatisfyMutantWait(CurrentObject, Thread);
WaitStatus = Thread->WaitStatus;//喚醒原因
goto DontWait;//退出函式
}
Else 丟擲異常…
}
}
//普通的等待物件只要DISPATCHER_HEADER頭部中的SignalState > 0就表示有訊號了
else if (CurrentObject->Header.SignalState > 0)
{
KiSatisfyNonMutantWait(CurrentObject);//遞減訊號狀態量計數
WaitStatus = STATUS_WAIT_0;//喚醒原因為‘真喚醒’
goto DontWait; //退出函式
}
//若所等等待的物件沒有訊號,就檢查當前狀態是否可被強制喚醒
WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);
if (WaitStatus != STATUS_WAIT_0)//if 可以強制喚醒了,就break退出迴圈,退出函式
break;
//如果無法強制喚醒,就再檢測是否已經等待超時了
if (Timeout)//if使用者提供了一個超時值,就檢測超時情況
{
InterruptTime.QuadPart = KeQueryInterruptTime();
if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
{
WaitStatus = STATUS_TIMEOUT;//喚醒原因為‘超時’
goto DontWait; //退出函式
}
Timer->Header.Inserted = TRUE;
}
//如果所有條件都不滿足,就需要進入睡眠(首輪檢測)或繼續進入睡眠(每次臨時喚醒切回來時的檢查)
//將等待塊掛入目標等待物件內部的等待塊連結串列中
InsertTailList(&CurrentObject->Header.WaitListHead,&WaitBlock->WaitListEntry);
if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);
Thread->State = Waiting;//將執行緒標記為等待狀態(即睡眠狀態)
KiAddThreadToWaitList(Thread, Swappable);//加入到當前cpu的等待執行緒連結串列中
if (Timeout)
KxInsertTimer(Timer, Hand);
else
KiReleaseDispatcherLockFromDpcLevel();
KiSetThreadSwapBusy(Thread);//標記執行緒正在進行切換
WaitStatus = KiSwapThread(Thread, KeGetCurrentPrcb());//關鍵。切出執行緒,進入睡眠
-------------------------------華麗的分割線--------------------------------------
//上面函式的返回值表示執行緒喚醒切回來的原因(大體分為三種)【臨時、強制、真喚醒】
1、 臨時喚醒。指被其他執行緒發來的核心APC臨時喚醒,要求執行緊急APC任務
2、 強制喚醒。指被其他執行緒發來的強制喚醒要求喚醒/被髮來的使用者APC強制喚醒
3、 真喚醒。指所等待的物件真的有訊號了而被喚醒
if (WaitStatus != STATUS_KERNEL_APC)//if喚醒原因不是臨時喚醒,就退出函數了
return WaitStatus;
//下面的程式碼,是當被臨時喚醒回來後,需要繼續執行的。臨時喚醒回來後,需要重新計算剩餘超時值、執行核心APC,繼續進入下輪迴圈,測試是否可以退出睡眠。
if (Timeout)//重新計算剩餘超時值
Timeout = KiRecalculateDueTime(OriginalDueTime,&DueTime,&NewDueTime);
}
WaitStart://首次開始進入等待時,從這兒開始,提升irql
Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
KxSingleThreadWait();//構造好執行緒的等待塊連結串列
KiAcquireDispatcherLockAtDpcLevel();
}
KiReleaseDispatcherLock(Thread->WaitIrql);//break方式退出到這兒
return WaitStatus;
DontWait:
KiReleaseDispatcherLockFromDpcLevel();
KiAdjustQuantumThread(Thread);//調整時間片
return WaitStatus;//返回本次睡眠的喚醒原因
}
如上,這個函式是一個迴圈測試,概念上有點類似忙式等待。首先,第一輪迴圈,從WaitStart處開始,提升irql(目的是防止被切換),然後回到for迴圈開頭處,測試條件。如果所要等待的物件本來就有訊號,那麼第一輪迴圈時,就不用進入睡眠了,直接退出函式。否則,就切出執行緒,讓出cpu,構造好執行緒的等待塊連結串列,並將等待塊(此處意指執行緒自身)掛入目標等待物件的等待塊佇列中(可理解為等待者執行緒佇列)。然後,當以後條件成熟,喚醒回來時,檢測喚醒原因。若是被臨時喚醒的,就繼續進入下輪迴圈測試等待條件,否則即可退出函式,退出睡眠了。
下面的等待塊結構是執行緒的等待機制核心,它即用來掛入執行緒的等待塊連結串列,也用來掛入等待物件的佇列中。當掛入前者時,等待塊就可理解為一個等待物件,當掛入後者時,就可理解為一個等待者執行緒。
typedef struct _KWAIT_BLOCK
{
LIST_ENTRY WaitListEntry;//用來掛入目標物件的等待者執行緒佇列
struct _KTHREAD *Thread;//所屬執行緒
PVOID Object;//要等待的目標物件
struct _KWAIT_BLOCK *NextWaitBlock;//下一個等待塊(用來掛入執行緒的等待塊連結串列)
USHORT WaitKey;//本等待塊是所屬執行緒的第幾個等待物件
UCHAR WaitType;//WaitAll/WaitAny
volatile UCHAR BlockState;
} KWAIT_BLOCK, *PKWAIT_BLOCK, *PRKWAIT_BLOCK;
上面的函式中牽涉到一個重要巨集和幾個子函式,我們看。
#define KxSingleThreadWait()
Thread->WaitBlockList = WaitBlock;//等待塊連結串列
WaitBlock->WaitKey = 0;//即等待塊的索引
WaitBlock->Object = Object;//要等待的目標物件
WaitBlock->WaitType = WaitAny; //只等待一個物件的話就固定是WaitAny
Thread->WaitStatus = 0; //復位喚醒原因
if (Timeout) //if給定了超時值
{
KxSetTimerForThreadWait(Timer, *Timeout, &Hand);//設定好定時器物件
DueTime.QuadPart = Timer->DueTime.QuadPart;
WaitBlock->NextWaitBlock = TimerBlock; //定時器物件等待塊也掛入連結串列中
TimerBlock->NextWaitBlock = WaitBlock;//單迴圈連結串列
Timer->Header.WaitListHead.Flink = &TimerBlock->WaitListEntry;
Timer->Header.WaitListHead.Blink = &TimerBlock->WaitListEntry;
}
else
{
WaitBlock->NextWaitBlock = WaitBlock; //單迴圈連結串列
}
Thread->Alertable = Alertable;//執行緒睡眠模式,是否可被提醒(即強制喚醒)
Thread->WaitMode = WaitMode;//來自使用者還是核心模式的等待請求
Thread->WaitReason = WaitReason; //上次執行緒切換原因
Thread->WaitListEntry.Flink = NULL;
Swappable = KiCheckThreadStackSwap(Thread, WaitMode);//檢測本執行緒的核心棧是否可以換到外存
Thread->WaitTime = KeTickCount.LowPart;//記錄上次切出時間
如上,這個巨集的主要功能就是用來構造好該執行緒的等待塊連結串列,以及一些其它亂七八糟的工作,上面的函式KiCheckThreadStackSwap用來檢測本執行緒的核心棧是否可以置換到外存
BOOLEAN KiCheckThreadStackSwap(IN PKTHREAD Thread,IN KPROCESSOR_MODE WaitMode)
{
if ((WaitMode == UserMode) && (Thread->EnableStackSwap) &&
(Thread->Priority >= (LOW_REALTIME_PRIORITY + 9)))
{
return TRUE;
}
else
{
return FALSE;
}
}
如上,超級實時類的執行緒在處理來自使用者模式的等待請求時,核心棧可以置換到外存。
下面的函式用來線上程被臨時喚醒後,測試執行緒的狀態是否可被強制喚醒。
NTSTATUS
KiCheckAlertability(IN PKTHREAD Thread,
IN BOOLEAN Alertable,//本次睡眠操作是否支援強制喚醒
IN KPROCESSOR_MODE WaitMode)//來自哪個模式的等待請求
{
if (Alertable)//if本次睡眠操作支援強制喚醒(即可被強制喚醒)
{
if (Thread->Alerted[WaitMode])//if 真收到了來自那個模式的強制喚醒要求
{
Thread->Alerted[WaitMode] = FALSE;//復位
return STATUS_ALERTED;//喚醒原因設為強制喚醒
}
//若沒收到其他執行緒發來對應模式的強制喚醒要求,但收到了使用者APC
else if ((WaitMode != KernelMode) &&
(!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
{
Thread->ApcState.UserApcPending = TRUE;
return STATUS_USER_APC;//標記為被使用者APC給強制喚醒了
}
//只要收到了核心模式的強制喚醒要求,就可被強制喚醒(不必模式匹配)
else if (Thread->Alerted[KernelMode])
{
Thread->Alerted[KernelMode] = FALSE;
return STATUS_ALERTED; //喚醒原因設為強制喚醒
}
}
//即使本次睡眠不支援強制喚醒,但其他執行緒發來的使用者APC仍可強制喚醒本執行緒
else if ((WaitMode != KernelMode) && (Thread->ApcState.UserApcPending))
{
return STATUS_USER_APC; //標記為被使用者APC給強制喚醒了
}
return STATUS_WAIT_0;
}
如上:當執行緒的本次睡眠支援強制喚醒的情況下,可被對應模式或核心模式的強制喚醒要求給喚醒,即使不支援強制喚醒,也會被其他執行緒發來的使用者APC給強制搞醒。
關於模式匹配記住下面一點。來自核心模式的強制喚醒要求可以強制喚醒來自核心模式、使用者模式的等待請求,而來自使用者模式的強制喚醒要求只能強制喚醒來自使用者模式的等待請求。這就好比:核心空間的程式可以訪問核心空間的程式碼和資料,而使用者空間的程式只能訪問使用者空間的程式碼和資料。這樣記就容易了。
執行緒的睡眠其工作之一就是執行緒切換。執行緒一被切出了,要麼進入就緒態,要麼進入等待態。因等待物件而引起的執行緒切換,將使執行緒處於等待態。等待態與就緒態的本質區別就是處於就緒態的執行緒直接掛入就緒佇列,隨時等候被排程執行,處於等待態的執行緒,則要將自己掛入cpu的等待佇列,掛入各個目標等待
物件的等待執行緒佇列,然後一直等待別的執行緒觸發等待物件,喚醒自己,重新進入就緒態或執行態。
#define KiAddThreadToWaitList(Thread, Swappable)
{
if (Swappable) //為什麼要滿足這個條件,我也搞不清
InsertTailList(&KeGetCurrentPrcb()->WaitListHead, &Thread->WaitListEntry);
}
當一個執行緒處於睡眠態時,最典型的喚醒原因就是所等待的物件有了訊號。當一個等待物件有了訊號時,系統(指別的執行緒)會呼叫下面的函式嘗試喚醒在該等待物件上等待的所有執行緒。
VOID FASTCALL
KiWaitTest(IN PVOID ObjectPointer,//目標等待物件
IN KPRIORITY Increment)//喚醒執行緒後的優先順序增量(以便儘快得到排程執行)
{
PLIST_ENTRY WaitEntry, WaitList;
PKWAIT_BLOCK WaitBlock;
PKTHREAD WaitThread;
PKMUTANT FirstObject = ObjectPointer;
NTSTATUS WaitStatus;
WaitList = &FirstObject->Header.WaitListHead;
WaitEntry = WaitList->Flink;
//遍歷等待者執行緒佇列,嘗試喚醒所有執行緒,直到訊號狀態量分配完畢
while ((FirstObject->Header.SignalState > 0) && (WaitEntry != WaitList))
{
WaitBlock = CONTAINING_RECORD(WaitEntry, KWAIT_BLOCK, WaitListEntry);
WaitThread = WaitBlock->Thread;
WaitStatus = STATUS_KERNEL_APC;//模擬給那個等待者執行緒傳送核心apc而臨時喚醒它
if (WaitBlock->WaitType == WaitAny)//WaitAnt型別的話,肯定滿足分配了
{
WaitStatus = (NTSTATUS)WaitBlock->WaitKey;//喚醒原因改為‘真喚醒’,此處即索引
KiSatisfyObjectWait(FirstObject, WaitThread);//分配資源,遞減物件的訊號狀態量計數
}
KiUnwaitThread(WaitThread, WaitStatus, Increment);//關鍵函式
WaitEntry = WaitList->Flink;//下一個執行緒
}
}
如上,這個函式會在每一個等待物件變成有訊號時,嘗試喚醒所有在該物件上等待的所有執行緒。注意,若佇列中的某個等待塊型別是WaitAny時,那麼,那個等待者執行緒必然真的滿足了等待條件,所以需要將喚醒原因改為‘真喚醒’型別。反之,若那個執行緒的等待型別是WaitAll,也即它還要等待其他物件,那麼,就模擬給它傳送核心apc的方式(其實沒傳送),臨時喚醒它,進入下輪迴圈,繼續測試它所等待的其他物件,這一點務必要注意。
下面的巨集用於當滿足分配條件時,分配訊號狀態量給指定執行緒。
#define KiSatisfyObjectWait(Object, Thread)
{
if ((Object)->Header.Type == MutantObject) //互斥物件要特殊處理
{
(Object)->Header.SignalState--;//遞減訊號狀態量計數(此處為擁有計數)
if ((Object)->Header.SignalState == 0)//if擁有計數==0
{
(Object)->OwnerThread = Thread;
Thread->KernelApcDisable = Thread->KernelApcDisable - (Object)->ApcDisable;
if ((Object)->Abandoned)//如果該互斥物件是因為原擁有者執行緒意外終止了而讓出的
{
(Object)->Abandoned = FALSE;//復位
Thread->WaitStatus = STATUS_ABANDONED;//喚醒原因
}
//插入本執行緒獲得的所有互斥物件連結串列中
InsertHeadList(Thread->MutantListHead.Blink, &(Object)->MutantListEntry);
}
}
//如果是‘自動復位’型事件,觸發物件後,又立馬復位它的狀態
else if (((Object)->Header.Type & TIMER_OR_EVENT_TYPE) == EventSynchronizationObject)
(Object)->Header.SignalState = 0;
else if ((Object)->Header.Type == SemaphoreObject)
(Object)->Header.SignalState--;//遞減訊號狀態量計數(此處為真的訊號量計數)
}
熟悉Win32 多執行緒程式設計的朋友想必不用解釋這段程式碼了吧。
只是要提醒一下,不管是什麼同步物件,其內部的SignalState表示訊號狀態量計數,當該值<=0時表示無訊號,>0時表示有訊號。其實這個欄位本用於訊號量的,不過所有的同步物件都可以看做是SignalState只有0和1兩種情況的‘特殊訊號量’。
下面的這個函式可以說是執行緒的等待喚醒機制的核心,其功能用來喚醒指定執行緒
VOID FASTCALL
KiUnwaitThread(IN PKTHREAD Thread,//目標執行緒
IN LONG_PTR WaitStatus,//喚醒原因
IN KPRIORITY Increment)//喚醒後的優先順序增量,以便喚醒後儘快得到排程執行
{
KiUnlinkThread(Thread, WaitStatus);//將所有等待塊脫鏈
Thread->AdjustIncrement = (SCHAR)Increment;//上次優先順序調整增量
Thread->AdjustReason = AdjustUnwait;//調整原因
KiReadyThread(Thread);//‘就緒化’指定執行緒(也即轉入就緒佇列或者置為搶佔者執行緒)
}
任意一個執行緒都可以呼叫這個函式喚醒目標執行緒。喚醒原因大體分為:臨時喚醒、強制喚醒、真喚醒
每當插入一個apc的時候,將呼叫下面的函式
NTSTATUS KiInsertQueueApc(…)
{
…….
if (Thread != KeGetCurrentThread())
{
if (ApcMode == KernelMode)//若要給其他執行緒插入一個核心apc
{
Thread->ApcState.KernelApcPending = TRUE;
if (Thread->State == Running)
{
RequestInterrupt = TRUE;
}
else if ((Thread->State == Waiting) && (Thread->WaitIrql == PASSIVE_LEVEL) &&
!(Thread->SpecialApcDisable) && (!(Apc->NormalRoutine) ||
(!(Thread->KernelApcDisable)&&!(Thread->ApcState.KernelApcInProgress))))
{
Status = STATUS_KERNEL_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//臨時喚醒目標執行緒
}
…….
}
else if ((Thread->State == Waiting) && (Thread->WaitMode == UserMode) &&
((Thread->Alertable) || (Thread->ApcState.UserApcPending)))
{
Thread->ApcState.UserApcPending = TRUE;
Status = STATUS_USER_APC;
KiUnwaitThread(Thread, Status, PriorityBoost);//插入使用者APC,強制喚醒目標執行緒
}
……
}
看到沒,插入核心apc的時候可能會臨時喚醒目標執行緒,插入使用者apc的時候可能會強制喚醒目標執行緒。
VOID FASTCALL
KiUnlinkThread(IN PKTHREAD Thread, IN NTSTATUS WaitStatus)
{
PKWAIT_BLOCK WaitBlock;
PKTIMER Timer;
//關鍵。喚醒原因記錄線上程結構的這個欄位中,KiSwapContext返回的就是這個欄位值
Thread->WaitStatus |= WaitStatus;
WaitBlock = Thread->WaitBlockList;
//讓各個等待塊脫離各自等待物件的佇列
do
{
RemoveEntryList(&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != Thread->WaitBlockList);
//脫離cpu的等待執行緒佇列
if (Thread->WaitListEntry.Flink) RemoveEntryList(&Thread->WaitListEntry);
Timer = &Thread->Timer;
if (Timer->Header.Inserted) KxRemoveTreeTimer(Timer);
if (Thread->Queue) Thread->Queue->CurrentCount++;
}
注意,上面的這個函式只是脫離各個等待物件的等待塊佇列。執行緒自己的等待塊佇列還在。
現在看一下KeWaitForSingleObject那個函式內部呼叫的KiSwapThread。注意是KiSwapThread不是KiSwapContext。
LONG FASTCALL
KiSwapThread(IN PKTHREAD CurrentThread,IN PKPRCB Prcb)
{
BOOLEAN ApcState = FALSE;
KIRQL WaitIrql;//上次切出時的irql
LONG_PTR WaitStatus;//上次喚醒原因
PKTHREAD NextThread;
KiAcquirePrcbLock(Prcb);
NextThread = Prcb->NextThread;//當前的搶佔者執行緒
if (NextThread)
{
Prcb->NextThread = NULL;
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
NextThread = KiSelectReadyThread(0, Prcb);//排程處一個
if (NextThread)
{
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
else
{
InterlockedOr((PLONG)&KiIdleSummary, Prcb->SetMember);
NextThread = Prcb->IdleThread;//使用空轉執行緒
Prcb->CurrentThread = NextThread;
NextThread->State = Running;
}
}
KiReleasePrcbLock(Prcb);
WaitIrql = CurrentThread->WaitIrql;//記錄上次切出時的irql
MiSyncForContextSwitch(NextThread);
ApcState = KiSwapContext(CurrentThread, NextThread);
----------------------------------------華麗的分割線---------------------------------------
if (ApcState)//切回來後,例行執行核心apc
{
KeLowerIrql(APC_LEVEL);
KiDeliverApc(KernelMode, NULL, NULL);
ASSERT(WaitIrql == PASSIVE_LEVEL);
}
KeLowerIrql(WaitIrql);
WaitStatus = CurrentThread->WaitStatus;//關鍵。返回該執行緒上次喚醒的原因
return WaitStatus;
}
相信到此為止,大家理解了執行緒如何等待單個物件的機制了吧?一個執行緒可以同時等待對個物件,我們看。
NTSTATUS
NTAPI
KeWaitForMultipleObjects(IN ULONG Count,//陣列元素個數
IN PVOID Object[],//等待物件陣列
IN WAIT_TYPE WaitType,//WaitAll/AitAny
IN KWAIT_REASON WaitReason,//上次切換原因
IN KPROCESSOR_MODE WaitMode,//來自使用者模式/核心模式的等待請求
IN BOOLEAN Alertable,//本次等待是否可提醒(指是否可被強制喚醒)
IN PLARGE_INTEGER Timeout OPTIONAL,//超時
OUT PKWAIT_BLOCK WaitBlockArray OPTIONAL)//等待塊陣列
{
PKMUTANT CurrentObject;
PKWAIT_BLOCK WaitBlock;
PKTHREAD Thread = KeGetCurrentThread();
PKWAIT_BLOCK TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];//定時器等待塊是固定的
PKTIMER Timer = &Thread->Timer;
NTSTATUS WaitStatus = STATUS_SUCCESS;
BOOLEAN Swappable;
PLARGE_INTEGER OriginalDueTime = Timeout;
LARGE_INTEGER DueTime, NewDueTime, InterruptTime;
ULONG Index, Hand = 0;
if (!WaitBlockArray)//沒提供就使用內建的等待塊陣列(3個等待塊+一個定時器等待塊)
WaitBlockArray = &Thread->WaitBlock[0];
if (!Thread->WaitNext) goto WaitStart;//首輪迴圈從WaitStart處開始
Thread->WaitNext = FALSE;
KxMultiThreadWait();//關鍵。與等待單個物件時使用的巨集不相同
for (;;)
{
Thread->Preempted = FALSE;
if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) &&
(Thread->WaitIrql < APC_LEVEL))
{
KiReleaseDispatcherLock(Thread->WaitIrql);
}
else
{
Index = 0;
if (WaitType == WaitAny)
{
do
{
CurrentObject = (PKMUTANT)Object[Index];
if (CurrentObject->Header.Type == MutantObject)
{
if ((CurrentObject->Header.SignalState > 0) ||
(Thread == CurrentObject->OwnerThread))
{
if (CurrentObject->Header.SignalState != (LONG)MINLONG)
{
KiSatisfyMutantWait(CurrentObject, Thread);
WaitStatus = Thread->WaitStatus | Index;
goto DontWait;//只要滿足一個就退出函式
}
else
{
KiReleaseDispatcherLock(Thread->WaitIrql);
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
}
}
else if (CurrentObject->Header.SignalState > 0)
{
KiSatisfyNonMutantWait(CurrentObject);
WaitStatus = Index;
goto DontWait; //只要滿足一個就退出函式
}
Index++;
} while (Index < Count);
}
Else //WaitAll
{
do
{
CurrentObject = (PKMUTANT)Object[Index];
if (CurrentObject->Header.Type == MutantObject)
{
if ((Thread == CurrentObject->OwnerThread) &&
(CurrentObject->Header.SignalState == (LONG)MINLONG))
{
KiReleaseDispatcherLock(Thread->WaitIrql);
ExRaiseStatus(STATUS_MUTANT_LIMIT_EXCEEDED);
}
else if ((CurrentObject->Header.SignalState <= 0) &&
(Thread != CurrentObject->OwnerThread))
{
break;//只要任有一個物件不滿足,就要繼續進入等待狀態
}
}
else if (CurrentObject->Header.SignalState <= 0)
{
break; //只要任有一個物件不滿足,就要繼續進入等待狀態
}
Index++;
} while (Index < Count);
if (Index == Count)//if 所有物件都有訊號了
{
WaitBlock = WaitBlockArray;
do
{
CurrentObject = (PKMUTANT)WaitBlock->Object;
KiSatisfyObjectWait(CurrentObject, Thread);
WaitBlock = WaitBlock->NextWaitBlock;
} while(WaitBlock != WaitBlockArray);
WaitStatus = Thread->WaitStatus;//喚醒原因為‘真喚醒’型別
goto DontWait;
}
}
WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);
if (WaitStatus != STATUS_WAIT_0) break;
if (Timeout)
{
InterruptTime.QuadPart = KeQueryInterruptTime();
if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
{
WaitStatus = STATUS_TIMEOUT;
goto DontWait;
}
Timer->Header.Inserted = TRUE;
WaitBlock->NextWaitBlock = TimerBlock;
}
WaitBlock = WaitBlockArray;
do
{
CurrentObject = WaitBlock->Object;
InsertTailList(&CurrentObject->Header.WaitListHead,&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
} while (WaitBlock != WaitBlockArray);
if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);
Thread->State = Waiting;
KiAddThreadToWaitList(Thread, Swappable);
KiSetThreadSwapBusy(Thread);
if (Timeout)
KxInsertTimer(Timer, Hand);
else
KiReleaseDispatcherLockFromDpcLevel();
WaitStatus = KiSwapThread(Thread, KeGetCurrentPrcb());
----------------------------------------華麗的分割線---------------------------------------
//if 喚醒原因不是臨時喚醒,直接退出整個函式,退出睡眠狀態
if (WaitStatus != STATUS_KERNEL_APC) return WaitStatus;
//否則,若是臨時喚醒回來的,則進入下一輪迴圈,繼續去測試等待物件的訊號情況
if (Timeout)
Timeout = KiRecalculateDueTime(OriginalDueTime,&DueTime,&NewDueTime);
}
WaitStart:
Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
KxMultiThreadWait();
KiAcquireDispatcherLockAtDpcLevel();
}
KiReleaseDispatcherLock(Thread->WaitIrql);
return WaitStatus;
DontWait:
KiReleaseDispatcherLockFromDpcLevel();
KiAdjustQuantumThread(Thread);
return WaitStatus;
}
這段程式碼我想不用多解釋了吧。唯一需要注意的是使用了不同的巨集:
#define KxMultiThreadWait()
Thread->WaitBlockList = WaitBlockArray;
Index = 0;
//構造好本執行緒的等待塊連結串列
do
{
WaitBlock = &WaitBlockArray[Index];
WaitBlock->Object = Object[Index];
WaitBlock->WaitKey = (USHORT)Index;//關鍵
WaitBlock->WaitType = WaitType;//所有等待塊的等待型別都相同
WaitBlock->Thread = Thread;
WaitBlock->NextWaitBlock = &WaitBlockArray[Index + 1];
Index++;
} while (Index < Count);
WaitBlock->NextWaitBlock = WaitBlockArray; //單迴圈連結串列
Thread->WaitStatus = STATUS_WAIT_0;
if (Timeout)
{
TimerBlock->NextWaitBlock = WaitBlockArray;
KxSetTimerForThreadWait(Timer, *Timeout, &Hand);
DueTime.QuadPart = Timer->DueTime.QuadPart;
InitializeListHead(&Timer->Header.WaitListHead);
}
Thread->Alertable = Alertable;//是否可被強制喚醒
Thread->WaitMode = WaitMode;//來自使用者模式/核心模式的等待請求
Thread->WaitReason = WaitReason;//上次被切原因
Thread->WaitListEntry.Flink = NULL;
Swappable = KiCheckThreadStackSwap(Thread, WaitMode);
Thread->WaitTime = KeTickCount.LowPart;//記錄上次被切時間
弄懂了執行緒的等待喚醒機制後,下面我們看各種具體等待物件(又叫同步物件)的原理:
同步物件:(互斥、事件、訊號量、自旋鎖)
訊號量的原理:
typedef struct _KSEMAPHORE {
DISPATCHER_HEADER Header;//公共頭部
LONG Limit;//訊號量的最大訊號個數
} KSEMAPHORE, *PKSEMAPHORE;
NTSTATUS
NtCreateSemaphore(OUT PHANDLE SemaphoreHandle,//返回訊號量物件的控制代碼
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,//訊號量的名稱及其他屬性
IN LONG InitialCount,//訊號量的初始訊號個數
IN LONG MaximumCount)//支援的最大訊號個數
{
PKSEMAPHORE Semaphore;
HANDLE hSemaphore;
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
NTSTATUS Status;
if (PreviousMode != KernelMode)
{
_SEH2_TRY
{
ProbeForWriteHandle(SemaphoreHandle);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
_SEH2_YIELD(return _SEH2_GetExceptionCode());
}
_SEH2_END;
}
if ((MaximumCount <= 0) || (InitialCount < 0) || (InitialCount > MaximumCount))
return STATUS_INVALID_PARAMETER;
//訊號量也是一種核心物件
Status = ObCreateObject(PreviousMode,ExSemaphore