📄
字号:
KeInitializeApc(IN PKAPC Apc,
IN PKTHREAD Thread,
IN KAPC_ENVIRONMENT TargetEnvironment,
IN PKKERNEL_ROUTINE KernelRoutine,
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,
IN KPROCESSOR_MODE Mode,
IN PVOID Context)
{
. . . . . .
/* Set up the basic APC Structure Data */
RtlZeroMemory(Apc, sizeof(KAPC));
Apc->Type = ApcObject;
Apc->Size = sizeof(KAPC);
/* Set the Environment */
if (TargetEnvironment == CurrentApcEnvironment) {
Apc->ApcStateIndex = Thread->ApcStateIndex;
} else {
Apc->ApcStateIndex = TargetEnvironment;
}
/* Set the Thread and Routines */
Apc->Thread = Thread;
Apc->KernelRoutine = KernelRoutine;
Apc->RundownRoutine = RundownRoutine;
Apc->NormalRoutine = NormalRoutine;
/* Check if this is a Special APC, in which case we use KernelMode and no Context */
if (ARGUMENT_PRESENT(NormalRoutine)) {
Apc->ApcMode = Mode;
Apc->NormalContext = Context;
} else {
Apc->ApcMode = KernelMode;
}
}[/code]
这段代码本身很简单,但是有几个问题需要结合前面NtQueueApcThread()的代码再作些说明。
首先,从NtQueueApcThread()传下来的KernelRoutine是KiFreeApcRoutine(),顾名思义这是在为将来释放PKAPC数据结构做好准备,而RundownRoutine是NULL。
其次,参数TargetEnvironment说明要求挂入哪一种环境下的APC队列。实际传下来的值是OriginalApcEnvironment,表示是针对原始环境、即当前线程所属(而不是所挂靠)进程的。注意代码中所设置的是Apc->ApcStateIndex、即PKAPC数据结构中的ApcStateIndex字段,而不是KTHREAD结构中的ApcStateIndex字段。另一方面,ApcStateIndex的值只能是OriginalApcEnvironment或AttachedApcEnvironment,如果所要求的是CurrentApcEnvironment就要从Thread->ApcStateIndex获取当前的环境值。
最后,APC请求的模式Mode是UserMode。但是有个例外,那就是:如果指针NormalRoutine为0,那么实际的模式变成了KernelMode。这是因为在这种情况下没有用户空间APC函数可以执行,唯一将得到执行的是KernelRoutine,在这里是KiFreeApcRoutine()。这里的宏操作ARGUMENT_PRESENT定义为:
[code]#define ARGUMENT_PRESENT(ArgumentPointer) \
((BOOLEAN) ((PVOID)ArgumentPointer != (PVOID)NULL))[/code]
回到NtQueueApcThread()代码中,下一步就是根据Apc->ApcStateIndex、Apc->Thread、和Apc->ApcMode把准备好的KAPC结构挂入相应的队列。根据APC请求的具体情况,有时候要插在队列的前头,一般则挂在队列的尾部。限于篇幅,我们在这里就不看KeInsertQueueApc()的代码了;虽然这段代码中有一些特殊的处理,但都不是我们此刻所特别关心的。
如果跟Linux的Signal机制作一类比,那么NtQueueApcThread()相当于设置Signal处理函数(或中断服务程序)。在Linux里面,Signal处理函数的执行需要受到某种触发,例如收到了别的线程或某个内核成分发来的信号;而执行Signal处理函数的时机则是在CPU从内核返回目标线程的用户空间程序的前夕。可是Windows的APC机制与此有所不同,一般来说,只要把APC请求挂入了队列,就不再需要触发,而只是等待执行的时机。对于用户APC请求,这时机同样也是在CPU从内核返回目标线程用户空间程序的前夕(对于内核APC则有所不同)。所以,在某种意义上,把一个APC请求挂入队列,就同时意味着受到了触发。对于系统调用NtQueueApcThread(),我们可以理解为是把APC函数的设置与触发合在了一起。而对于异步的文件读写,则APC函数的设置与触发是分开的,内核先把APC函数记录在别的数据结构中,等实际的文件读写完成以后才把APC请求挂入队列,此时实际上只是触发其运行。不过那已是属于设备驱动框架的事了。所以,一旦把APC请求挂入队列,就只是等待执行时机的问题了。从这个意义上说,“异步过程调用”还真不失为贴切的称呼。
下面就来看执行APC的时机,那是在(系统调用、中断、或异常处理之后)从内核返回用户空间的途中。
[code]_KiServiceExit:
/* Get the Current Thread */
cli
movl %fs:KPCR_CURRENT_THREAD, %esi
/* Deliver APCs only if we were called from user mode */
testb $1, KTRAP_FRAME_CS(%esp)
je KiRosTrapReturn
/* And only if any are actually pending */
cmpb $0, KTHREAD_PENDING_USER_APC(%esi)
je KiRosTrapReturn
/* Save pointer to Trap Frame */
movl %esp, %ebx
/* Raise IRQL to APC_LEVEL */
movl $1, %ecx
call @KfRaiseIrql@4
/* Save old IRQL */
pushl %eax
/* Deliver APCs */
sti
pushl %ebx
pushl $0
pushl $UserMode
call _KiDeliverApc@12
cli
/* Return to old IRQL */
popl %ecx
call @KfLowerIrql@4
. . . . . .[/code]
这是内核中处理系统调用返回和中断/异常返回的代码。在返回前夕,这里先通过%fs:KPCR_CURRENT_THREAD取得指向当前线程的ETHREAD(从而KTHREAD)的指针,然后依次检查:
● 即将返回的是否用户空间。
● 是否有用户APC请求正在等待执行(KTHREAD_PENDING_USER_APC是 ApcState.KernelApcPending在KTHREAD数据结构中的位移)。
要是通过了这两项检查,执行针对当前线程的APC请求的时机就到了,于是就调用KiDeliverApc()去“投递”APC函数,这跟Linux中对Signal的处理又是十分相似的。注意在调用这个函数的前后还分别调用了KfRaiseIrql()和KfLowerIrql(),这是为了在执行KiDeliverApc()期间让内核的“中断请求级别”处于APC_LEVEL,执行完以后再予恢复。我们现在暂时不关心“中断请求级别”,以后会回到这个问题上。
前面讲过,KTHREAD中有两个KAPC_STATE数据结构,一个是ApcState,另一个是SavedApcState,二者都有APC队列,但是要投递的只是ApcState中的队列。
注意在call指令前面压入堆栈的三个参数,特别是首先压入堆栈的%ebx,它指向(系统空间)堆栈上的“中断现场”、或称“框架”,即CPU进入本次中断或系统调用时各寄存器的值,这就是下面KiDeliverApc()的调用参数TrapFrame。
下面我们看KiDeliverApc()的代码。
[code][KiDeliverApc()]
VOID
STDCALL
KiDeliverApc(KPROCESSOR_MODE DeliveryMode,
PVOID Reserved,
PKTRAP_FRAME TrapFrame)
{
PKTHREAD Thread = KeGetCurrentThread();
. . . . . .
ASSERT_IRQL_EQUAL(APC_LEVEL);
/* Lock the APC Queue and Raise IRQL to Synch */
KeAcquireSpinLock(&Thread->ApcQueueLock, &OldIrql);
/* Clear APC Pending */
Thread->ApcState.KernelApcPending = FALSE;
/* Do the Kernel APCs first */
while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode])) {
/* Get the next Entry */
ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;
Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);
/* Save Parameters so that it's safe to free the Object in Kernel Routine*/
NormalRoutine = Apc->NormalRoutine;
KernelRoutine = Apc->KernelRoutine;
NormalContext = Apc->NormalContext;
SystemArgument1 = Apc->SystemArgument1;
SystemArgument2 = Apc->SystemArgument2;
/* Special APC */
if (NormalRoutine == NULL) {
/* Remove the APC from the list */
Apc->Inserted = FALSE;
RemoveEntryList(ApcListEntry);
/* Go back to APC_LEVEL */
KeReleaseSpinLock(&Thread->ApcQueueLock, OldIrql);
/* Call the Special APC */
DPRINT("Delivering a Special APC: %x\n", Apc);
KernelRoutine(Apc, &NormalRoutine, &NormalContext,
&SystemArgument1, &SystemArgument2);
/* Raise IRQL and Lock again */
KeAcquireSpinLock(&Thread->ApcQueueLock, &OldIrql);
} else {
/* Normal Kernel APC */
if (Thread->ApcState.KernelApcInProgress || Thread->KernelApcDisable)
{
/*
* DeliveryMode must be KernelMode in this case, since one may not
* return to umode while being inside a critical section or while
* a regular kmode apc is running (the latter should be impossible btw).
* -Gunnar
*/
ASSERT(DeliveryMode == KernelMode);
KeReleaseSpinLock(&Thread->ApcQueueLock, OldIrql);
return;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -