异步过程调用(APC)

来源:互联网 发布:淘宝客服流程图 编辑:程序博客网 时间:2024/04/29 14:49
在Windows NT中,APC被无数次地提到,但在标准Microsoft DDK中却没有说明什么是APC以及应该怎么使用。但是理解APC是理解Windows NT怎么工作的本质。
  
   当然,毫无疑问你们一定知道一些完全支持APC的Win32 API(比如QueueUserApc这个Win32 API函数)。Windows NT平台的Win32 APC抽象是建立在内核中的本地APC支持之上的。
   在1997年5月发行的The NT Insider中我们讨了I/O完成。I/O完成的一个关键原理是I/O完成的“第二场景(second stage)”。第二场景必须在开始I/O的线程上下文中完成。I/O管理器通常使用APC来完成——因为APC的一个关键特征就是运行在特定线程上下文中。注意在知识库文章#Q126416种错误地的把APC描述为运行在任意线程上下文中。
  
  异步过程调用有下面几个有趣的特征:
  
  — 一个APC总是运行在特定线程上下文中。
  — 一个APC运行在OS预定的时刻。
  — 一个APC可以导致当前运行线程的抢占(pre-emption)。
  — 一个 APC例程可以被强占。
  
   对于在Windows NT中运行的线程,操作系统用一个叫做“线程结构(thread structure)”的数据结构来表示。在这个结构中有两个APC队列。其中一个用来存放“用户模式”APC对象,另一个队列则存放“内核模式”APC对象。这些队列的APC对象各自都有常规和特殊这两种特性(flavors)。
   在描述用户模式的APC和内核模式APC的区别之前先来描述一下APC对象的控制对象是个不错的注意。虽然没有在DDK中明确地列出,但是APC对象还是在NTDDK。H中定义了。其声明如下:
  
  typedef struct {
  CSHORT Type;
  CSHORT Size;
  ULONG Spare0;
  struct _KTHREAD *Thread;
  LIST_ENTRY ApcListEntry;
  PKKERNEL_ROUTINE KernelRoutine;
  PKRUNDOWN_ROUTINE RundownRoutine;
  PKNORMAL_ROUTINE NormalRoutine;
  PVOID NormalContext;
  PVOID SystemArgument1;
  PVOID SystemArgument2;
  CCHAR ApcStateIndex;
  KPROCESSOR_MODE ApcMode;
  BOOLEAN Inserted;
  } KAPC, *PKAPC;
  
   从APC对象的声明中,它的许多特征是简单地描述的。例如,APC是线程特定的数据结构,因此APC对象包含一个它所关联的线程的指针。和任何标准Windows NT控制对象一样,APC对象包含一个单链表结构LIST_ENTRY,用来排队APC对象。
   一般来说,一个内核模式APC有一个有效KernelRoutine(…)函数指针,而一个用户模式APC会有一个有效NormalRoutine(…)函数指针。这两种APC对象都可以有一个RundownRoutine(…)函数指针,这个函数每当OS需要丢弃APC对列的内容的时候调用(比如线程退出)。没有这个例程的APC就被简单地删除。在这种情况下,KernelRoutine(…)和NormalRoutine(…)都不会被调用,只会调用RundownRoutine(…)。
   我们在前面提到有两个APC队列,每个队列中可以有普通的和特殊的APC对象。对于内核模式操作,特殊APC确实被频繁使用,I/O管理器就是这样执行I/O完成的。I/O管理器创建一个“特殊”内核模式APC并在这个特定内核模式APC上下文中完成线程特定的I/O部分(比如把结果复制到适当的输出缓冲区)。但是特殊的用户模式APC是不常用的。它们使用在比如线程被终止这种情况。不管怎样,特殊APC总是插入到APC队列前面,在任何普通APC对象之前。这样能确保特殊APC在任何普通APC之前运行。
   对于文件系统来说管理APC的投递是保证文件系统正确行为的基本要素。确实,在Windows NT上开发文件系统会碰到的最复杂的问题之一就是在VM系统和文件系统之间交互的复杂锁定模式。一个FSD的危险是一个APC可能导致一个额外的I/O被触发到文件系统中。
   例如,假设对某个文件X执行的I/O刚好完成。如果同一线程的一个内核APC开始对文件Y进行I/O(这是很常见的情况),就很可能导致死锁。实际上,即使FSD开发者定义了在文件X和文件Y之间的锁定顺序,但是在FSD控制之外写成的代码可能不遵守这个规则。
   要处理这种情况,一个典型的Windows NT文件系统将会调用KeEnterCriticalRegion(…)(曾经叫做FsRtlEnterFileSystem(…),但是在当前版本的Windows NT中被定义为KeEnterCriticalRegion(…))来禁止内核模式APC投递。但是还是允许I/O管理器投递特殊内核模式APC。这些APC是安全的是因为他们不会重入文件系统,因此不会引入任何死锁风险。
   禁止APC投递的另一种方式是提高系统的IRQL到APC_LEVEL。这将禁止任何类型的APC投递。例如,Windows NT内存管理器在某些情况下就在APC_LEVEL等级发出I/O操作。这就确保了在它发出新的paging I/O操作的时候任何APC,特别是I/O完成APC都不会投递。
   Windows NT中的某些同步原语为了确保代码不会重入当前运行线程就提高系统的IRQL到APC_LEVEL。这里值得注意的是快速互斥操作。ExAcquireFastMutex(…) 提高系统的IRQL到APC_LEVEL,当驱动程序调用ExReleaseFastMutex(…)的时候降低IRQL。因此,当拥有快速互斥体的时候这个线程的所有APC都不能投递。
   用户模式APC的打开和禁止不能使用和管理内核模式APC一样的方法。相反,内核在某些情况下检查用户APC队列,包括线程等待一个事件发生的时候,比如KeWaitForSingleObject(…)以及它的参数。触发APC投递的另一个一般事件是退出一个系统服务调用。无论如何,任何使用APC的代码不能依赖现在的系统行为。相反,代码对数据结构访问必须进行同步。
   尽管APC对象用在整个操作系统中,怎样创建一个APC对象就从来没有在DDK中文档化。虽然有几个例程说明他们是支持APC函数的,但是在DDK中却没有使用这些例程的示例。例如,函数ZwReadFile(…)接受一个函数指针和上下文参数。名字提示了他们实际上是APC例程。不幸的是,DDK在这一点却相当简单,仅仅声明“设备驱动程序和中间层驱动程序应当把这个指针设置为NULL”。
   虽然这个警告是正确的,但是这也给文件系统或者文件服务器这样的最高层驱动程序怎样使用这些参数一点指导。通常这就导致使用事件而不是使用APC例程,而这样就导致了相应的高负荷和低性能的系统特征。当然,就像平台SDK文档中清晰说明的:“注意ReadFileEx(…), SetWaitableTimer(…), 和 WriteFileEx(…)函数使用APC作为完成通知回调机制”,Win32使用这个功能来实现重叠I/O。
   对于Win32应用程序来说要使用APC比内核层应用程序简单很多。他们可以简单地调用QueueUserApc,传一个函数,一个线程句柄以及上下文参数。假定他们有适当许可,操作系统将构建一个APC对象并插入到目标线程的用户APC队列中。
   尽管内核模式应用程序不能直接创建APC对象,但是Microsoft为文件系统驱动程序提供了一个操作来确保代码执行在一个特定进程上下文中。这个关键的例程是KeAttachProcess(…) 和 KeDetachProcess(…)。尽管这些函数不允许指定一个特定线程上下文,但是他们能确保给定进程的资源可用,特别是地址空间。这些函数原形如下。他们在NTIFS。H中定义了。
  
  NTKERNELAPI VOID KeAttachProcess (IN PRKPROCESS Process);
  NTKERNELAPI VOID KeDetachProcess (VOID);
  
   为了强行切换到特定进程地址空间,文件系统驱动程序可能使用KeAttachProcess(…)。从这个函数返回的时候,线程就执行在在KeAttachProcess(…)中指定进程的地址空间中。文件系统接着可以操作被附加(attached)的进程中的数据。操作完成的时候线程调用KeDetachProcess(…)来恢复进程上下文。
  
   通常应当避免使用KeAttachProcess(…) 和 KeDetachProcess(…)。尽管这些函数允许文件系统进入指定进程上下文,但是却很昂贵。许多文件系统使用这个函数都是不必要的。但是他们能够以直接的方式帮助文件系统在地址空即之间复制数据。
  
   综上,Windows NT使用异步过程调用来在一个已知线程上下文中运行任意过程。通过为每个线程维护一个APC对象队列并周期性检查这个队列来确定是否有工作要做。APC确实是Windows NT处理象I/O完成这样的基本系统操作的基础。对于大多数驱动程序,APC都是不存在的。对于文件系统驱动程序,适当地控制APC投递是控制正确行为的本质。

异步过程调用

异步过程调用(APC)为用户程序和系统代码提供了一种在特殊用户线程的描述表(一个特殊的进程地址空间)中执行代码的方法。APC在特殊线程描述表中执行,使用专用队列,它们以低于2的IRQL运行,所受的限制和DPC有很大的不同。APC例程可以获得资源(对象)、等待对象句柄、导致页错误以及调用系统服务。

APC也是由内核控制对象描述,称为APC对象。等待执行的APC在由内核管理的APC队列中。APC队列与DPC队列的不同在于:DPC队列是系统范围的;而APC队列是特定于线程的—每个线程都有自己的APC队列。当内核被要求对APC排队时,内核将APC插入到将要执行APC例程的线程的APC队列中。内核依次请求APC级的软件中断,并当线程最终开始运行时执行APC。

有两种APC,用户态APC和核心态APC。核心态APC在线程描述表中运行并不需要得到目标线程的“允许”,而用户态APC则需要得到目标线程的“允许”。核心态APC可以中断线程及执行过程,而不需要线程的干预或同意。

执行体使用核心态APC来执行必须在特定线程的地址空间(在描述表中)中完成的操作系统工作。例如,可以使用核心态APC命令一个线程停止执行可中断的系统服务,或记录在线程地址空间中的异步I/0操作的结果。环境子系统使用核心态的APC将线程挂起或终止自身的运行,或者得到或设置它的用户态执行描述表。POSIX子系统使用核心态APC来模仿POSIX信号到POSIX进程的发送。

设备驱动程序也使用核心态APC。例如,如果启动了一个I/0操作并且线程进入等待状态,则另一个进程中的另一个线程就可以被调度而去运行。当设备完成传输数据时,I/0系统必须以某种方式重新进入到启动IlO系统线程的描述表中,以便它能够来执行这个动作。

几个Win32 APl,例如ReadiEx、WriteFileEx和QueueUserAPC,使用用户态APC。例如ReadFileEx和WriteFileEx函数允许调用者指定I/O操作完成时将被调用的完成例程。该完成例程是通过把APC排队到发出I/O操作的线程来实现的。然而,。在对APC排队时,对完成例程的回调是没有必要的,因为仅当线程在“可报警等待状态”(alterablewait state)时,用户态APC才被传送给线程。线程可以通过等待对象句柄并且指定它的等待是可报警的(使用Win32 WaitForMultipleObects函数)进入等待状态,也可以通过直接测试它是否有一个挂起的APC(使用SleepEx)进入等待状态。在两种情况下,如果用户态APC是挂起的,内核将中断(报警)线程,将控制转交给APC例程,并在APC例程执行完成后,继续线程执行;如果发送APC的线程
处于等待状态,在APC例程执行完成后,等待被重新发出或重新执行。如果等待仍没有解决,线程将返回到等待状态,但此时它将位于它正在等待的对象列表的末尾