作者:Enrico Martignetti
译者:zhouws/kkindof qq5771067
原文:http://www.opening-windows.com/download/apcinternals/2009-05/windows_vista_apc_internals.pdf
APC基础概念
APCs作为windows提供的一种机制,它能让一个线程从正常的执行流程转到执行其它代码。另外,apc的一个重要的特点就是当被执行的时候,是可以指定一个线程作为它的运行环境的。
APCs很少有官方文档化的介绍:内核使用的API是没有公开的,而且它们的内部原理只有部分作了介绍。关于 apc,最有趣的是它们和windows的线程dispatch联系的特别紧密,所以,通过分析apc的机制,我们可以从侧面更好的理解windows的内核功能特性。
一些讲解windows 内核机制的书本经常有提到一个这样的情况,apc是通过软中断来派发执行的。在这里就引出了另一个问题:操作系统是怎么保证这个中断会在一个特定的线程中执行呢?换言之也就是怎么保证一个APC在特定的线程中执行的呢?而且软中断是可以中断到任何的线程中的。对这个问题,我们下回分解。
根据APC类型的不同,通过APC执行的代码会执行在一个合适的irql level,没错,这个irql level就叫apc level.
那么,这些APC用来干什么的呢?
- I/O manager这个windows组件可以使用一个APC来完成一次i/o请求,并且可以在当初发起 这个i/o请求的线程中完成
- 当要结束一个进程的时候,一个特殊的apc就能派上用场了,因为它能插入到一个正在执行中的进程
- 当使用windows api像QueueUserAPC,ReadFileEx/WriteFileEx(异步IO方式)时,这些实现的内部就是通过APC来完成的
APC进程环境
通常来说,一个线程的执行环境就是当初那个创建它的那个原始进程(这里我们可以叫原始环境),但是,也有可能一个线程attach到另一个进程中去,也就是说,在这种情况下这个线程它就会执行在它attach的那个进程环境中(这里我们可以叫attach环境)
在管理APC的时候,WINDOWS也支持了这种场景:执行apc代码的线程可以运行在一个原始环境中,也可以在attach环境中。
Windows使用内核结构_KAPC_STATE来维护将要执行的APC的状态,结构如下:
kd> dt nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x010 Process : Ptr32 _KPROCESS
+0x014 KernelApcInProgress : UChar
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : Uchar
windows内核里有一个很重要的内核结构_KTHREAD,在这个结构里面有2个这样的_KAPC_STATE结构成员,分别叫ApcState和SaveApcState,这2个也是传说中的APC环境,当指定APC要在当前线程运行时的环境中运行时,ApcState是用来保存这类APC环境的,不管这个线程是否有attach到其它进程,ApcState包含着当前进程环境信息,也就是可以派发的那些,SavedApcState 保存的APC信息表示不是当前进程可派发的,而且也需要等待其它时候才能使用,例如当一个线程attach到一个不是它原始的进程的时候,而你又指定一个APC需要在目标线程的原始环境中才能执行时,这个时候,这些APC就需要保存到SavedApcState中,同时需要等待线程从attac环境切回原始环境,即回到原始进程环境是时才有可能被派发。
从上面的解释可以看出,当一个线程attach到其它进程时,attach之前的ApcState 会被copy到SavedApcState结构中去,同时会重新初始化。当线程Detach出来时,就会把apc信息从SavedApcState中copy回来到ApcState中,同时SavedApcState就清空了。从这里我们也可以看出,内核分发APC时,是从ApcState中查找可派发的APC的
在线程结构体_KTHREAD中,也存在着一个指针数组,叫ApcStatePointer,这里面的元素保存的是ApcState和SavedApcState的地址,内核中时刻维护着这个数组,保证这个数组的第一个元素永远指向的apc环境是线程的原始环境,而第二个是指向attach环境。
例如,当一个线程没有attach到其它进程的时候,这个线程正在执行的进程环境就是当前激活的那个,因此,这个线程的当前执行环境就是保存在ApcState中,同时ApcStatePointer[0]也是指向这个ApcState.
最后,在_KTHREAD结构中还有一个成员叫ApcStateIndex保存着一个索引,这个索引会指向当前使用的环境,当没attach时会指向ApcState,反之指向SavedApcState。所以,如果一个线程attach到其它进程时,这个线程的原始环境就会保存回SaveApcState中,那ApcStatePointer[0]也会指向SavedApcState,因为第一个元素永远指向着它的原始环境,接着,ApcStatepointer[1]指向ApcState,因为它永远指向着被atached的进程的环境。所以ApcStateIndex就会被设置成1,这是因为apcStatePointer[1]必须指向的是当前使用的进程环境
当我们要使用一个APC的时候,我们可以指定把这个APC放在ApcStatePointer指向的任何一个环境中执行,如果是原始环境还是attach环境,还是指定当前真正使用的环境(要么是原始环境要么是attach环境)
APC类型
在Windows操作系统中,APC一共有3种类型:
- 特殊模式的内核APC(下方叫SK apc)
这种APC执行在内核模式下,同时它的irql是apc level.这种APC是真正可以打断一个正在运行的线程,并转向执行其它内核模式函数的(这个函数就是APC中的KernelRoutine),而且这个打断的行为是异步的.
当一个线程由于调用了下面几类函数KeWaitforSingleObject,KeWaitForMutipleObjects,KeWaitforMutexObject或者KeDelayExecution而进入了wait状态时,如果给这个线程投递一个sk apc的话,这个线程就会被unwait出来,
并且会去执行这个sk apc的KernelRoutine,但在执行完这个Routine后,它又会重新进入wait状态.
通常来说,sk apc总会被派发执行的只要它的目标线程正在执行同时irql降到低于apc level一般是passive level时。但有时候 apc也会被disable掉的,例如当线程_KTHREAD的SpecialApcDisable字段不为0时,这种情况下
,所有的类型的APC都会被disable,包括sk apc.
- 普通模式的内核APC
这种APC也是执行在内核模式下,但它的运行时irql level是passive level。和sk apc一样,这种APC也可以打断正在运行的线程,同时能把线程从wait状态unwait过来。但是这种APC的执行条件有一定的限制,具体细节后面分解。
- 用户模式APC
这种APC只会执行用户模式的代码,并且执行的要求条件比内核APC更多限制,如这种APC只会在目标线程将要进入alertable类型的等待状态时才会被派发运行(不过特殊的情况下不需要,后面会讲解)。
那这个alertable类型的等待状态什么情况下会发生呢,一般线程调用下面几个函数,并且参数传递的是Alertable = true and WaitMode = User时,就会进入这种状态.KeWaitForSingleObject, KeWaitForMultipleObjects,
KeWaitForMutexObject, orKeDelayExecutionThread
因此,正常情况下用户模式APC并不会打断一个线程的执行流程,它们更像一种可以放入队列的工作项:即在任何时候可以把这个工作项放到一个线程中,同时这个线程决定什么时候来执行这个工作项.