📄 漫谈兼容内核之十五:windows线程的等待、唤醒机制.txt
字号:
NextWaitBlock = NextWaitBlock->NextWaitBlock;
}
/* All the objects are signaled, we can satisfy */
DPRINT("Satisfiying a Wait All\n");
WaitEntry = WaitEntry->Blink;
KiSatisifyMultipleObjectWaits(CurrentWaitBlock);
}
/* All waits satisfied, unwait the thread */
DPRINT("Unwaiting the Thread\n");
KiAbortWaitThread(CurrentWaitBlock->Thread,
CurrentWaitBlock->WaitKey, Increment);
SkipUnwait:
/* Next entry */
WaitEntry = WaitEntry->Flink;
}
DPRINT("Done\n");
} [/code]
这个函数是针对一个特定对象的,所以参数就是指向其数据结构的指针(这一次的类型倒是DISPATCHER_HEADER指针)。另一个参数Increment说明是否、以及怎样、对被唤醒线程的优先级作出调整。
程序的代码基本上就是一个while循环,这是对目标对象的等待队列中所有等待块即KWAIT_BLOCK数据结构的循环。额外的条件是目标对象数据结构中的SignalState字段必须大于0,其原因和意义我们后面就会看到。如前所述,队列中的每个KWAIT_BLOCK数据结构都代表着一个因为在这个对象上等待而被阻塞的线程。另一方面,所涉及的每个线程又都可能同时在多个对象上等待,而等待的类型(方式)则有WaitAny和WaitAll两种。如果只是在单一的对象上等待,则我们在前面看到所用的是WaitAny。
如果队列中某个进程的等待方式是WaitAny,那么就对目标对象执行一次KiSatisfyObjectWait(),然后就通过KiAbortWaitThread()将其唤醒、使其结束等待。反之,要是这个线程的等待方式是WaitAll,则要通过另一个while循环扫描该线程的等待块队列,通过KiIsObjectSignaled()检查其所等待的每一个对象是否接受了“信号”(是否到了货)。只要其中有一个对象没有满足条件,这个线程就不满足被唤醒的条件,因而通过“goto SkipUnwait”语句跳过对此线程的处理。反之,如果所等待的所有对象都收到了“信号”,就对这些对象执行KiSatisifyMultipleObjectWaits(),那实际上就是对所有这些对象都执行KiSatisfyObjectWait()。
前面讲过,KiSatisfyObjectWait()的作用是把目标对象上已经接收到的“信号”消耗掉,或者说对已经到货、并且有了“货主”(因而马上就要被领走)的货物进行销账处理。怎么销呢?我们看代码:
[code][KiWaitTest() > 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]
表面上KiSatisfyObjectWait()似乎是个通用的程序,可以适用于任何可等待对象,但是实际上它的内部还是按不同的可等待对象分别加以处理的。我们以“信号量(Semaphore)”为例来说明其操作方式。对于信号量对象,其数据结构中的SignalState表示可用的资源数量,或者可以理解为筹码的数量。每当资源的提供者向此对象提供一个筹码时,就使SignalState加1,而每当资源的使用者消耗一个筹码时就使其减1。由于此时目标对象已经得到了所要求的资源,SignalState已经递加了计数,这里使其递减,就表示把所收到的资源消耗掉了。这也解释了前面KiWaitTest()中的while循环为什么有个条件“Object->SignalState > 0”,因为如果Object->SignalState不大于0就说明资源已经耗尽,再继续循环就入不敷出了。
最后,把已经接收到的资源耗用掉以后,就要通过KiAbortWaitThread()唤醒正在等待此项资源的线程。读者也许会想,既然接收到的资源已经耗用掉,那正在目标进程上等待的线程还能得到什么呢?其实,这里的所谓耗用一般只是“账面”上的,与此相伴随的还有“真金白银”(例如报文),这才是等待中的线程所需要的。打个比方,目标对象得到“进货”以后,账面上就有了“库存”,而KiSatisfyObjectWait()就是把账上的库存销掉,而实际的货物则有待于要货的线程被唤醒以后前去领取。这一点,读者在读了下一篇关于进程间通信的漫谈以后就会更加清楚。
我们接着看KiAbortWaitThread()的代码。注意这程序名中的“Abort”是指等待状态的Abort,而不是线程的Abort。
[code][KiWaitTest() > KiAbortWaitThread()]
VOID FASTCALL
KiAbortWaitThread(PKTHREAD Thread, NTSTATUS WaitStatus, KPRIORITY Increment)
{
PKWAIT_BLOCK WaitBlock;
/* If we are blocked, we must be waiting on something also */
. . . . . .
ASSERT((Thread->State == THREAD_STATE_BLOCKED) ==
(Thread->WaitBlockList != NULL));
/* Remove the Wait Blocks from the list */
DPRINT("Removing waits\n");
WaitBlock = Thread->WaitBlockList;
while (WaitBlock) {
/* Remove it */
DPRINT("Removing Waitblock: %x, %x\n", WaitBlock, WaitBlock->NextWaitBlock);
RemoveEntryList(&WaitBlock->WaitListEntry);
/* Go to the next one */
WaitBlock = WaitBlock->NextWaitBlock;
};
/* Check if there's a Thread Timer */
if (Thread->Timer.Header.Inserted) {
/* Cancel the Thread Timer with the no-lock fastpath */
DPRINT("Removing the Thread's Timer\n");
Thread->Timer.Header.Inserted = FALSE;
RemoveEntryList(&Thread->Timer.TimerListEntry);
}
/* Increment the Queue's active threads */
if (Thread->Queue) {
DPRINT("Incrementing Queue's active threads\n");
Thread->Queue->CurrentCount++;
}
/* Reschedule the Thread */
DPRINT("Unblocking the Thread\n");
PsUnblockThread((PETHREAD)Thread, &WaitStatus, 0);
}[/code]
这是一段简单直白的代码,如果略去对定时器(Timer)的处理以及对Thread->Queue的处理(如前所述,这与设备驱动有关),那就只剩下两件事。第一件事是通过一个while循环将目标线程的所有等待块WaitBlock从其所在的队列中脱离出来。之所以如此是因为目标进程可能同时在多个对象上等待,因此通过不同的WaitBlock挂入了不同对象的队列。不过,对于因执行KeWaitForSingleObject()而被阻塞的线程而言,实际上只挂入了一个对象的队列,所以WaitBlock->NextWaitBlock为0。第二件事就是通过PsUnblockThread()唤醒目标线程,对于熟悉Linux内核的读者这已是了无新意的事了。
但是,等待中的线程被唤醒或警醒以后怎么走,倒是值得一说的。为此我们又要回到PsBlockThread()的代码中。在那里,睡眠中的目标线程被唤醒或警醒后从PsDispatchThreadNoLock()返回,此时目标线程的Thread->WaitStatus记录着返回时的状态,实际上也反映了返回的原因。返回的原因无非就是这么几种:
? ● 条件得到满足而被唤醒,此时的Thread->WaitStatus为表示成功的状态码。
? ● 超时或出错,此时的Thread->WaitStatus为相应的出错代码。
? ● 别的线程对其执行了NtAlertThread()系统调用,此时的Thread->WaitStatus为STATUS_ALERTED。
? ● 因APC请求而警醒,此时的Thread->WaitStatus为STATUS_KERNEL_APC。
从代码中可以看出,从PsBlockThread()返回到KeWaitForSingleObject()时,通过参数Status返回的值就是Thread->WaitStatus。此外,也可能在PsBlockThread()中因发现有APC请求存在而根本就没有进入睡眠,此时返回的Status也是STATUS_KERNEL_APC。于是,当目标线程从PsBlockThread()返回到KeWaitForSingleObject()中时,如果Status为STATUS_KERNEL_APC就说明这是因为被APC请求警醒而返回的,原先的目的并未达到,还须再接再厉,所以才有KeWaitForSingleObject()中的do{}while(TRUE)循环。还要说明,由于APC机制的特殊安排,目标进程从睡眠时醒来时会先执行APC函数,然后才回到原来的轨道而从PsDispatchThreadNoLock()返回到PsBlockThread()。
最后,由于本文实际上也涉及警醒,而不仅仅是唤醒,这里也看一下系统调用NtAlertThread()的代码:
[code]NTSTATUS STDCALL
NtAlertThread (IN HANDLE ThreadHandle)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
PETHREAD Thread;
NTSTATUS Status;
/* Reference the Object */
Status = ObReferenceObjectByHandle(ThreadHandle,
THREAD_SUSPEND_RESUME,
PsThreadType,
PreviousMode,
(PVOID*)&Thread,
NULL);
/* Check for Success */
if (NT_SUCCESS(Status)) {
/*
* Do an alert depending on the processor mode. If some kmode code wants to
* enforce a umode alert it should call KeAlertThread() directly. If kmode
* code wants to do a kmode alert it's sufficient to call it with Zw or just
* use KeAlertThread() directly
*/
KeAlertThread(&Thread->Tcb, PreviousMode);
/* Dereference Object */
ObDereferenceObject(Thread);
}
/* Return status */
return Status;
}
当然,参数ThreadHandle代表着已被打开的目标线程。显然,这里实质性的操作是KeAlertThread()。
[NtAlertThread() > KeAlertThread()]
BOOLEAN STDCALL
KeAlertThread(PKTHREAD Thread, KPROCESSOR_MODE AlertMode)
{
KIRQL OldIrql;
BOOLEAN PreviousState;
/* Acquire the Dispatcher Database Lock */
OldIrql = KeAcquireDispatcherDatabaseLock();
/* Save the Previous State */
PreviousState = Thread->Alerted[AlertMode];
/* Return if Thread is already alerted. */
if (PreviousState == FALSE) {
/* If it's Blocked, unblock if it we should */
if (Thread->State == THREAD_STATE_BLOCKED &&
(AlertMode == KernelMode || Thread->WaitMode == AlertMode) &&
Thread->Alertable) {
DPRINT("Aborting Wait\n");
KiAbortWaitThread(Thread,
STATUS_ALERTED, THREAD_ALERT_INCREMENT);
} else {
/* If not, simply Alert it */
Thread->Alerted[AlertMode] = TRUE;
}
}
/* Release the Dispatcher Lock */
KeReleaseDispatcherDatabaseLock(OldIrql);
/* Return the old state */
return PreviousState;
}[/code]
这段代码就留给读者自己阅读了。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -