在看WDF框架的时候,看到一段代码注释:
NTSTATUS
__fastcall
FxPkgIo::DispatchStep2(
__inout MdIrp Irp,
__in_opt FxIoInCallerContext* IoInCallerCtx,
__in_opt FxIoQueue* Queue
)
{
//
// The request inserted into the queue can be retrieved and processed
// by an arbitrary thread. So we need to make sure that such a thread doesn't
// get suspended and deadlock the driver and potentially the system by
// entering critical region.The KeEnterCriticalRegion temporarily disables
// the delivery of normal kernel APCs used to suspend a thread.
// Kernel APCs queued to this thread will get executed when we leave the
// critical region.
//
if (Mx::MxGetCurrentIrql() <= APC_LEVEL) {
Mx::MxEnterCriticalRegion();
inCriticalRegion = TRUE;
}
WDF框架使用进入临界区的方法,使得当前线程无法被挂起。对此,我难以理解。翻了一下<Windows内核情景分析>5.14节关于KeSuspendThread的注释:
if (!Thread->SuspendApc.Inserted)
{
/* Not inserted, insert it */
//创建线程时由KeInitializeThread设置APC为PiSuspendThreadNormalRoutine
Thread->SuspendApc.Inserted = TRUE;
KiInsertQueueApc(&Thread->SuspendApc, IO_NO_INCREMENT);
}
而Thread->SuspendApc在线程初始化时,设置为:
KeInitializeApc(&Thread->SuspendApc,
Thread,
OriginalApcEnvironment,
PsSuspendThreadKernelRoutine,PiSuspendThreadRundownRoutine,
PiSuspendThreadNormalRoutine,KernelMode,NULL);
PiSuspendThreadNormalRoutine内部仅仅是KeWaitForSingleObject。因此当IRQL退到APC_LEVEL并执行先前插入的APC时,会使得线程的执行被阻塞。
了解了线程挂起的背景后,再来看看如何阻止线程被挂起:
1).
#define KeEnterCriticalRegion() \
{ \
PKTHREAD _Thread = KeGetCurrentThread(); \
ASSERT(_Thread == KeGetCurrentThread()); \
ASSERT((_Thread->KernelApcDisable <= 0) && \
(_Thread->KernelApcDisable != -32768)); \
_Thread->KernelApcDisable--; \
}
上面ASSERT语句反映了Thread->KernelApcDisable的值<=0。当线程进入临界区后,执行完_Thread->KernelApcDisable--;Thread->KernelApcDisable的值最多等于-1,不可能等于0.
2).当IRQL降到DISPATCH_LEVEL以下时,OS会调用KiDeliverApc,来执行APC。然而,执行APC必须满足一定的条件Thread->KernelApcDisable==0:
while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
{
/* Lock the APC Queue */
KiAcquireApcLockAtApcLevel(Thread, &ApcLock);
/* Check if the list became empty now */
if (IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
{
/* It is, release the lock and break out */
KiReleaseApcLock(&ApcLock);
break;
}
/* Get the next Entry */
ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
NormalRoutine = Apc->NormalRoutine; //PiSuspendThreadNormalRoutine
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext; //NULL
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
/* Special APC */
if (!NormalRoutine)
{
...
}
else
{
/* Normal Kernel APC, make sure it's safe to deliver */
if ((Thread->ApcState.KernelApcInProgress) ||
(Thread->KernelApcDisable)) //<----------------Thread->KernelApcDisable==0,才会跳过这个if语句
{
/* Release lock and return */
KiReleaseApcLock(&ApcLock);
goto Quickie;
}
/* Dequeue the APC */
RemoveEntryList(ApcListEntry);
Apc->Inserted = FALSE;
/* Go back to APC_LEVEL */
KiReleaseApcLock(&ApcLock);
/* Call the Kernel APC */
KernelRoutine(Apc,
&NormalRoutine,
&NormalContext,
&SystemArgument1,
&SystemArgument2);
...
}
}
Quickie:
}
但是由于1)的作用,破坏了APC执行的条件,因此APC无法执行,进一步说,线程无法调用KeWaitforSingleObject而一直无法被挂起。
另外,以前看代码时,忽略了一个细节:当IRQL从APC_LEVEL将到PASSIVE_LEVEL时,如果有APC请求,OS会短暂的提升IRQL并执行APC:
VOID
NTAPI
KiCheckForKernelApcDelivery(VOID)
{
KIRQL OldIrql;
if (KeGetCurrentIrql() == PASSIVE_LEVEL)
{
KeRaiseIrql(APC_LEVEL, &OldIrql);
KiDeliverApc(KernelMode, 0, 0);
KeLowerIrql(PASSIVE_LEVEL);
}
else
{
KeGetCurrentThread()->ApcState.KernelApcPending = TRUE;
HalRequestSoftwareInterrupt(APC_LEVEL);
}
}
总结一下:
Thread有3种方式进入APC_LEVEL:
- Call KeAcquireFastMutex
- Delivery a APC
- KeRaiseIrql (一般不用)等。
当使用Fast Mutex进入APC_LEVEL后,对于其它线程,若要获取此Mutex,则会被设置为等待状态。对于线程自己而言,微软说:
Code paths that are protected by a fast mutex run at IRQL=APC_LEVEL, thus disabling delivery of all APCs and preventing the thread from suspension.
即:阻止响应任何APC,而且线程不能被挂起(suspend),为什么不能被挂起?因为操作系统实现线程挂起的方式,就是Delivery APC,在APC的回调函数里面等待一个信号量(这个是我查阅WRK中找到的答案)。由于运行在APC_LEVEL(KeAcquireFastMutex的作用)上,会disabling delivery of all APCs(不响应同一IRQL级别发出的投递APC请求)。
参考: IRQL中断请求级别及APC_LEVEL讨论