天天看点

线程警惕和APC

在内部,Windows NT内核有时使用线程警惕(thread alert)来唤醒线程。这种方法使用APC(异步过程调用)来唤醒线程去执行某些特殊例程。用于生成警惕和APC的支持例程没有输出给WDM驱动程序开 发者使用。但是,由于DDK文档和头文件中有大量地方引用了这个概念,所以我想在这里谈一下。 当某人通过调用KeWaitXxx例程阻塞一个线程时,需要指定一个布尔参数,该参数表明等待是否是警惕的(alertable)。一个警惕的 等待可以提前完成,即不用满足任何等待条件或超时,仅由于线程警惕。线程警惕起源于用户模式的native API函数NtAlertThread。如果因为警惕等待提前终止,则内核返回特殊的状态值STATUS_ALERTED。 APC机制使操作系统能在特定线程上下文中执行一个函数。APC的异步含义是,系统可以有效地中断目标线程以执行一个外部例程。APC的动作有点类似于硬件中断使处理器从任何当前代码突然跳到ISR的情形,它是不可预见的。 APC来自三种地方:用户模式、内核模式,和特殊内核模式。用户模式代码通过调用Win32 API函数QueueUserAPC请求一个用户模式APC。内核模式代码通过调用一个未公开的函数请求一个APC,而且该函数在DDK头文件中没有原 型。某些逆向工程师可能已经知道该例程的名称以及如何调用它,但该函数的确是仅用于内部,所以我不在这里讨论它。系统把APC排入一个特殊线程直到和适的 执行条件出现。和适的执行条件要取决于APC的类型,如下: 特殊的内核APC被尽可能快地执行,既只要APC_LEVEL级上有可调度的活动。在很多情况下,特殊的内核APC甚至能唤醒阻塞的线程。

普通的内核APC仅在所有特殊APC都被执行完,并且目标线程仍在运行,同时该线程中也没有其它内核模式APC正执行时才执行。

用户模式APC在所有内核模式APC执行完后才执行,并且仅在目标线程有警惕属性时才执行。

如果系统唤醒线程去提交一个APC,则使该线程阻塞的等待原语函数将返回特殊状态值STATUS_KERNEL_APC或STATUS_USER_APC。 APC与I/O请求

内核使用APC概念有多种目的。我仅解释APC与执行I/O操作之间的关系。在某些场合,当用户模式程序在一个句柄 上执行同步的ReadFile操作时,Win32子系统就调用一个名为NtReadFile(尽管未公开,但已经被广泛了解)的内核模式例程。该函数创建 并提交一个IRP到适当的设备驱动程序,而驱动程序通常返回STATUS_PENDING以指出操作未完成。NtReadFile然后向ReadFile 也返回这个状态代码,于是ReadFile调用NtWaitForSingleObject函数,这将使应用程序在那个用户模式句柄指向的文件对象上等 待。NtWaitForSingleObject接着调用KeWaitForSingleObject以执行一个非警惕的用户模式的等待,在文件对象内部 的一个事件对象上等待。 当设备驱动程序最后完成了读操作时,它调用IoCompleteRequest函数,该函数接下来排队一个特殊的内核模式APC。该APC例程 然后调用KeSetEvent函数使文件对象进入信号态,因此应用程序被释放并得以继续执行。有时,I/O请求被完成后还需要执行一些其它任务,如缓冲区 复制,而这些操作又必须发生在请求线程的地址上下文中,因此会需要其它种类的APC。如果请求线程不处于警惕性的等待状态,则需要内核模式APC。如果在 提交APC时线程并不适合运行,则需要特殊的APC。实际上,APC例程就是用于唤醒线程的机制。 内核模式例程也能调用NtReadFile函数。但驱动程序应该调用ZwReadFile函数替代,它使用与用户模式程序一样的系统服务接口到 达NtReadFile(注意,NtReadFile函数未公开给设备驱动程序使用)。如果你遵守DDK的限定调用ZwReadFile函数,那么你向 NtReadFile的调用与用户模式中的调用几乎没有什么不同,仅有两处不同。第一,ZwReadFile函数更小,并且任何等待都将在内核中完成。另 一个不同之处是,如果你调用了ZwCreateFile函数并指定了同步操作,则I/O管理器将自动等待你的读操作直到完成。这个等待可以是警惕的也可以 不是,取决于你在ZwCreateFile调用中指定的实际选项。 如何指定Alertable和WaitMode参数

现在你已经有足够的背景资料了解等待原语中的Alertable和WaitMode 参数。作为一个通用规则,你绝不要写同步响应用户模式请求的代码,仅能为确定的I/O控制请求这样做。一般说来,最好挂起长耗时的操作(从派遣例程中返回 STATUS_PENDING代码)而以异步方式完成。再有,你不要一上来就调用等待原语。线程阻塞仅适合设备驱动程序中的某几个地方使用。下面几段介绍 了这几个地方。 内核线程 有时,当你的设备需要周期性循检时,你需要创建自己的内核模式线程。 1)处理PnP请求 有几个PnP请求需要你在驱动程序这边同步处理。换句话说,你把这些请求传递到低级驱动程序并等待它们完成。你将调用 KeWaitForSingleObject函数并在内核模式中等待,这是由于PnP管理器是在内核模式线程的上下文中调用你的驱动程序。另外,如果你需 要执行作为处理PnP请求一部分的辅助请求时,例如,与USB设备通信,你应在内核模式中等待。 2)处理其它I/O请求 当你正在处理其它种类的I/O请求时,并且你知道正运行在一个非任意线程上下文中时,那么你在行动前必须仔细考虑,如果你确信那个线程可以被阻塞,你应该 在调用者所处于的处理器模式中等待。在多数情况下,你可以利用IRP中的RequestorMode域。此外,你还可以调用 ExGetPreviousMode来确定前一个处理器模式。如果你在用户模式中等待,并允许用户程序调用QueueUserAPC提前终止等待,你应该 执行一个警惕性等待。 3)我最后要提到的情况是,在用户模式中等待并要允许用户模式APC打断,你应使用警惕性等待。 *)底线是:使用非警惕性等待,除非你知道不这样做的原因。

转自http://hi.baidu.com/fanzier/blog/item/877b86546d4798183b2935c1.html

继续阅读