摘要:Windows APC的全称为(asynchronous procedure call)翻译为中文即“异步过程调用”。《Windows APC机制(一)》、《谈谈对APC的一点理解》、《线程的Alertable与User APC》主要阅读了这三篇文章,对APC有了个大概了解:
1) APCs允许用户程序和系统元件在一个进程的地址空间内某个线程的上下文中执行代码。
2) I/O管理器使用APCs来完成一个线程发起的异步的I/O操作。例如:当一个设备驱动调用IoCompleteRequest来通知I/O管理器,它已经结束处理一个异步I/O请求时,I/O管理器排队一个APC到发起请求的线程。然后线程在一个较低IRQL级别,来执行APC。 APC的作用是从系统空间拷贝I/O操作结果和状态信息到线程虚拟内存空间的一个缓冲中。
3) 使用APC可以得到或者设置一个线程的上下文和挂起线程的执行。
谈到APC,不可避免的牵涉到QueueUserAPC函数——“QueueUserAPC函数把一个APC对象加入到指定线程的APC队列中。”从函数名称,也应该能推测到一个线程其实有两个APC队列:用户APC、系统APC。
Windows APC函数是被按照先进先出(FIFO)顺序放置在一个队列Queue上面的。同时,用户APC函数极为特别,它只有在线程处于“可警告alertable的线程等待状态”时才能被线程调用。但是,线程一旦开始调用APC函数,就会一次性将所有APC队列上的函数全部执行完毕。
那么,什么是可警告alertable的线程等待状态?其实就是线程暂时没有重要的事情要做,就叫做这个状态。APC函数一般不会去干扰(中断)线程的运行,从上文中知道,一个线程附带着两个APC队列(用户APC、系统APC),也就相当于这两个队列的APC函数都是由“线程本身”来储备调用的(APC函数就相当于奥运会比赛上的预备选手),只有当线程处于“可警告的线程等待状态”才会去调用APC函数(比赛时只有主将无法上场时,预备选手才会出现)。
如何衡量线程此时是否有重要的事情要做?对于用户模式下,可以调用函数SleepEx、SignalObjectAndWait、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx都可以使目标线程处于alertable等待状态(无重要事情要做),从而让用户模式APCs执行,原因是这些函数最终都是调用了内核中的KeWaitForSingleObject,KeWaitForMultipleObjects,KeWaitForMutexObject,KeDelayExecutionThread等函数。但是这里需要注意的是线程执行Sleep(10)函数时,并不是“可警告alertable的线程等待状态”。想象一个应用场景:客户端程序每隔5分钟就和服务端进行一次通信,实现“心跳”,最简单的就是使用Sleep(5*60*1000)。那么这样一来,这5分钟内,线程就沉睡了,如果这个时候有比较紧急的网络IO事件发生怎么办呢?线程还在沉睡中,因为5分钟时间还未到,所以无法及时处理这些事件。如何解决这个问题呢?那就是使用SleepEx替换Sleep。这个函数比起Sleep就多了一个参数Alertable,表示该线程是“可唤醒的”,就是说,线程虽然等待时间未到,但如果发生一些事件,线程也会及时去处理。这些事件就是:IO完成例程需要执行或者线程有APC需要交付。
对于上述说明,抽取其中的SleepEx作为例子简单介绍:
SleepEx(
_In_ DWORD dwMilliseconds,
_In_ BOOL bAlertable
);
dwMilliseconds:等待时间,以毫秒为单位。如果该值为INFINITE值,则表示无限等待下去;
bAlertable:函数返回方式。如果为FALSE,除非该函数调用超时,否则该函数不返回。在此期间如果IO完成了回调,完成例程也不会被执行。如果为TRUE,当该函数调用超时或者IO完成回调时,该函数都会返回——当调用超时时,该函数返回WAIT_OBJECT_0(亦即0);如果返回IO完成回调才返回的话,则返回值为WAIT_IO_COMPLETION。
接下来,举个APC的实例:
在实例中需要注意三处:①如果APC函数在线程启动前就已经注入了,那么线程将会在启动前——将所有已经注入的APC函数全部执行完毕,才真正执行线程体;②main函数中之所以要使用Sleep(1),是为了让线程跑起来以后再执行APC函数。否则,如果所有APC函数都执行完毕了线程才真正跑起来,这时候进入SleepEx无限等待中,而没有APC例程去触发它。线程将会卡死在SleepEx处。③当线程退出后,再加入APC函数将不会被执行,因为线程体都已经销毁了。
#include <stdio.h>
#include <Windows.h>
#include <process.h>//_beginthreadex
VOID NTAPI before_thread_running(ULONG_PTR param)
{
printf("apc1.\n");
}
VOID NTAPI thread_running(ULONG_PTR param)
{
printf("apc2.\n");
}
VOID NTAPI after_thread_running(ULONG_PTR param)
{
printf("apc3.\n");
}
unsigned int __stdcall sub_thread(void* context)
{
printf("sub_thread begin.\n");
DWORD ret = SleepEx(INFINITE, TRUE);
switch (ret)
{
case WAIT_OBJECT_0: //Time Out
printf("WAIT_OBJECT_0\n");
break;
case WAIT_IO_COMPLETION: //IO完成,强制退出SleepEx
printf("WAIT_IO_COMPLETION\n");
break;
default:
break;
}
printf("sub_thread end.\n");
return 0;
}
int main(int argc, char* argv[])
{
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, sub_thread, NULL, 0, NULL);
QueueUserAPC(before_thread_running, hThread, NULL);//APC queue(FIFO)
Sleep(1);
QueueUserAPC(thread_running, hThread, NULL);
Sleep(1);
QueueUserAPC(after_thread_running, hThread, NULL);
WaitForSingleObject(hThread, INFINITE);//wait sub_thread
CloseHandle(hThread);
return 0;
}
apc1.
sub_thread begin.
apc2.
WAIT_IO_COMPLETION
sub_thread end.
2017-7-28 in Xi'An
————————————————
版权声明:本文为CSDN博主「qingdujun」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qingdujun/article/details/76223282