New Page 1

驱动程序超级宝典

称它为“驱动程序超级宝典“一点也不过分，因为它是被译为中文的MSDN DDK文档。这个文档不是我翻译的，我只不过将它编辑了一下。翻译它的同志是个高手，非常专业，在此先谢谢他给我们作的工作。
由于时间催促，里面的图片没有抓。如果哪位同志看这本书的时候将图片补上，那将是利国利民的好事。
另：由于春节期间放假，没有开信箱，结果爆满，里面的数据都是乱码，所以，很多同志的Mail没法回。请重新发，抱歉。
如果有任何问题，请发Mail。

zhai_hong_tao@etang.com

翟洪涛
2004.2.6


驱动程序开发环境

为Microsoft® Windows® 2000开发驱动程序至少需要两台机器：一台用于开发，一台用于调试。

如果驱动程序依赖于基本设备，包括高端工作站和服务器，驱动程序也可在一台多处理器计算机上调试和测试。

本章讨论如下主题：

n 1.1 自由构建（Free Build）和检查构建（Checked Build）

n 1.2 调试环境

1.1 自由构建和检查构建

微软的Windows2000驱动程序的测试和调试需要Windows® 2000的自由构建和检查构建。

n 自由构建（或零星构建）是操作系统的终端用户版本，系统和驱动程序以最优化方式构建，不可用调试断言，且调试信息已从二进制码中去除。自由系统和驱动程序更小更快，对内存的需求更小。

n 检查构建用于操作系统和内核模式驱动程序的测试和调试，检查构建包括意外错误检查、参数检查和在自由构建中不可用的调试信息。一个检查的系统或驱动程序能帮助区分和记录驱动程序的某些问题，如内存泄漏或不当的设备配置，这些问题将导致不可预测的后果。

尽管检查构建提供额外的保护，但它比自由构建消耗更多的内存和硬盘空间，且由于下列原因，系统和驱动程序运行得更慢。

n 可执行程序含有符号调试信息。

n 由于参数检查和调试输出，执行了附加的代码（诊断信息）。

新的驱动程序开发通常包括以下步骤：

1．编写驱动程序代码，应包括条件编译标记的调试检查。

2．测试和调试基于操作系统的检查构建的驱动程序的检查构建。

3．测试和调试基于自由构建的驱动程序的自由构建。

4．基于自由构建的驱动程序的调整。

5．使用了检查构建和自由构建的驱动程序和操作系统附加的测试和调试。

6．使用了自由构建的最终的测试和检查。

在驱动程序开发的早期，需要Windows2000检查构建来调试驱动程序，检查构建的附加调试代码保护了驱动程序可能导致的许多错误（比如复发的自旋锁）。

执行调整、最终测试和检查驱动程序应该基于自由构建而完成，自由构建的速度越快越有可能发现竞争条件和其他同步性的问题。

由于自由构建与Windows2000的零星版本相同，最终测试和检查也应该基于自由构建而完成。

驱动程序代码通常包括预处理器符号，该符号允许被编译成自由和检查构建。

DBG标记是一个保留的符号，可用它来决定编译时Windows2000的什么构建在运行，如果Windows2000检查构建运行，设置DBG为1，如果自由构建运行，不定义DBG（或设置为0，如果或者包括头文件wdm.h ，或者包括ntddk.h）。

驱动程序也应该在至少一个多处理器平台和至少一个单处理器平台上测试和调试；且这两个平台应该运行Windows2000的当前版本。

1.2 调试环境

内核模式调试需要一个目标机和一个主机，目标机用来运行驱动程序或另一内核模式的应用程序，主机运行调试程序。

图1.1 显示使用调试驱动程序的典型Windows2000设置。内核调试不需要自由或检查构建的特别结合，从一自由或检查系统调试一自由系统或检查系统是可行的。然而，总体上说，主机没有理由运行较慢的检查构建。

图1.1典型的Windows2000调试设置

参看在线DDK的Debugging Drivers和微软的调试程序使用文献来获得更多细节。


检查驱动程序

在Windows2000上，Driver Verifier是新的可用工具，它执行几项测试和调试内核模式驱动程序的任务。

1.1 Driver Verifier

Driver Verifier是一可帮助监视一个或多个内核模式驱动程序以证实他们没有非法函数调用或引起系统讹误的工具，Driver Verifier在目标驱动程序上执行广泛的测试和检查任务。例如，如果驱动程序以非正当的IRQL使用了内存，不正当地调用或释放自旋锁和内存分配，或者释放内存池时没有首先删除任何定时器，Driver Verifier将发布合适的错误检查。

当未装载驱动程序时，Driver Verifier检查确信驱动程序已经正确地清理了队列、线程和其他项目。

此外，Driver Verifier能够执行以下任何情况：

n 从一个特别内存池分配驱动程序内存请求，该过程测试驱动程序是否访问它的内存分配之外的内存，或者在释放它的分配之后访问内存。

n 通过使内存分页代码无效而给驱动程序极端的内存压力，这个过程揭示了访问分页内存的企图，分页的内存发生在当错误的IRQL或当保留一自旋锁时。

n 池分配的随机失效或其他的内存请求，该过程测试了驱动程序处理低内存状况的能力。

n 未装载的驱动程序检查所有的内存分配，以确信驱动程序没有漏掉内存。

n 从一特别池分配驱动程序的IRP，监视驱动程序I/O以处理其他不一致的行为。

这些能力可以分别激活或禁止，此外，Driver Verifier可同时用于任何数目的驱动程序。

下面的部分解释了驱动程序的工作方面：

n 2.1.1 Driver Verifier的能力

详细描述了Driver Verifier各个作用,它应用于除过图形驱动程序之外的所有内核模式驱动程序。

n 2.1.2 图形驱动程序的Driver Verifier的能力

描述了Driver Verifier对内核模式图形驱动程序的作用，这里内核模式图形驱动程序使用了图形驱动程序接口（GDI）。

n 2.1.3 激活和监视Driver Verifier

解释了怎样启动Driver Verifier，怎样选择所要的功能，怎样选择所要检查的驱动程序，也解释了如何利用Driver Verifier Manager来监视正被检查的驱动程序的行为。

注意：Driver Verifier能够检查任何数目的驱动程序，然而，当特别内存池和I/O检查选项同时运用于一驱动程序时，其效果将更为有效。
1.1.1 Driver Verifier的能力

Driver Verifier有两种能力：一种是一直起作用，另一种只有被选择时才起作用。

以下是对Driver Verifier所有能力的描述：

n 2.1.1.1 自动检查

这些是一直起作用的功能，比如IRQL和内存例程的检查，检查定时器检查释放的内存池，检查驱动程序的正确卸载。

n 2.1.1.2 特别内存池

这个功能使用了一个特别池来测试内存的上溢和下溢，及在内存释放之后的访问。

n 2.1.1.3 强迫IRQL检查

这个功能给驱动程序极端的内存压力来揭示内存分页的故障。

n 2.1.1.4 低资源模拟

这个功能注入随机分配错误和其他被拒决的请求来测试驱动程序在低内存状况下的响应。

n 2.1.1.5 内存池跟踪

这个功能检查当驱动程序被卸载时所有内存分配已经释放。

n 2.1.1.6 I/O检查

这个功能监视驱动程序I/O对非法或不一致的行为的处理。
1.1.1.1 自动检查

不论Driver Verifier检查一个或多个驱动程序，它将执行以下功能，这些功能不受任何Driver Verifier选项的允许和禁止之影响。

IRQL和内存例程的检查

Driver Verifier为下面被禁止的功能而监视所选择的驱动程序。

n 通过调用KeLowerIrql提高IRQL

n 通过调用KeRaiseIrql降低IRQL

n 零内存分配请求

n 在APC_LEVER之上的IRQL分配和释放分页池

n 在DIPATCH_LEVER之上的IRQL分配和释放非分页池

n 尝试释放一个没有从前面的分配里返回的地址。

n 尝试释放一个已被释放的地址。

n 在APC_LEVER之上的在IRQL获取和释放一个快速的互斥体

n 在IRQL而非在DIPATCH_LEVER之上获取和释放一个自旋锁

n 双倍释放一个自旋锁

n 指定一非法或随机（未初始化的）参数给任一API。

如果Driver Verifier没有运行，在所有状况下，这些故障不大可能会引起立即的系统崩溃，如果以上任何故障发生，则Driver Verifier监视驱动程序的行为并发布错误检测0xC4。（参看微软调试程序文档的使用来获得错误检查参数。）

检查被释放的内存池定时器

Driver Verifier检查所有被检查驱动程序所释放的内存池，如果任何定时器保留在该池里，发布错误检测0xC7。（遗忘的定时器能导致最终系统崩溃，这是最难考虑到的。）

检查驱动程序的卸载

当一个正被检查的驱动程序卸载后，Driver Verifier执行几个检查来确信驱动程序已被清空。

特别地，Driver Verifier寻找下面部分：

n 未删除的定时器

n 未定的DPC

n 未删除的辅助列表

n 未删除的工作线程

n 未删除的队列

n 其他类似的资源

诸如以上的问题能潜在地引起系统错误检查在驱动程序卸载后被发布，引起这些错误检测的原因难于判断。当Driver Verifier运行时，这种故障将导致错误检测0xC7在驱动程序卸载之后立即发布。（参看微软的调试程序文件的使用来获得错误检测参数的列表。）

图形驱动程序

当检查一个驱动程序时，这些自动检查没有执行。
1.1.1.2 特别内存池（Special Memory Pool）

内存讹误是一个普通的驱动程序问题,驱动程序错误能在错误出现的长时间后导致崩溃。这些错误中最普通要数访问已被释放的内存，并分配n字节然后是n+1字节。

为发现内存讹误，Driver Verifier能够从一特别池分配驱动程序内存并监视该池防止不正确的访问。

两个特别池定位是可行的，Verify End定位更易于发现访问上溢，Verify Start分配更易发现下溢。（注意：主要的内存讹误是由于上溢，而非下溢。）

当特别内存池运行并且选择了Verify End时，驱动程序请求的每一个内存分配被放置到不同的内存页码上。允许分配适合分页的最高可能地址被返回，这样内存被定位于页末。分页的前面部分用特殊的模式写入，前面的页码和紧邻的页码被标记为不可访问的。

如果驱动程序在分配结束之后试图访问内存，Driver Verifier将立即发现并发布错误检测0xCD，如果驱动程序在缓冲区的开始之前写内存，这将（大概）更改这种模式。当缓冲区被释放，Driver Verifier将发现更改并报告错误检测0xC1。

如果驱动程序在缓冲区释放以后读出或写入，Driver Verifier将报告错误检测0xCC。

当选择Verify Start时，内存缓冲区定位于分页的开始，在这种设置下，下溢立即引起错误检测，而上溢只有当内存被释放时才引起错误检测。这个选项的其他方面与Verify End选项是相同的。

由于驱动程序上溢错误比下溢错误普遍的多，所以Verify End是默认的定位。为改变这种设置，使用全局标记应用程序（Global Flags Utility）。

一个单独的内存分配推翻这种设置，并通过调用ExAllocateWithTagPriority的Priority参数来设置成XxxSpecialPoolOverrun或XxxSpecialPoolUnderrun来选择它的定位。（这个例程不能够激活或去活特别池，或者请求一个特别的内存分配，否则的话，这个内存分配将从普通池得到，只有此种定位才能够被这个例程控制。）

池标记

Driver Verifier将给已选择要检查的驱动程序指定特别池，另外一种使用特别池的方法是指定它到被一个专用 pool tag 标记的内存池。

Global Flags Utility 能够用来将特别池给具有一给定标记的池。

同时通过Driver Verifier和Global Flags Utility来请求特别池是允许的，如果这么做，微软的Windows2000 将试图利用特别池给指定标记的所有的池和来自指定驱动程序所有的池分配请求。

特别池效率

每个来自特别池的分配使用一个不可分页的内存页码和两个具有虚拟地址空间的页码，如果此池被耗尽，内存通过标准的方式分配，直到特别池再次变得可用为止。这样，如果特别内存池（Special Memory Pool）正在使用，不推荐同时检查多驱动程序。

发出了大量小内存请求的单个驱动程序也能够耗尽此池，出现这种情况，给驱动程序内存分配指定池标记且一次使特别池给一个池标记是可以选择的。

特别池的大小随着系统物理内存的增大而增加，理想地，其容量至少1 GB。在x86机器上，当虚拟空间（还有物理空间）被消耗时，引导而没有/3 GB开关也是可选的。增加分页文件达最小/最大数量（通过二，三当中的一个因子）也是一个好主意。

如果特别内存池可用，但是不到95%的所有池分配已经从特别池指定，在驱动程序测试管理器（Driver Verifier Manager）的 Driver Status screen 上将出现一个警告。发生这种情况，你应该检查一个更短的驱动程序列表，通过池标记检查单个池，或者给你的系统增加更多的物理内存。

为确信所有的驱动程序分配已被测试，推荐加强驱动程序较长时间周期。

监视特别池

Driver Verifier Manager的 Global Counters screen 能被用来监视特别池的使用。

如果是 Allocations Succeeded ，特别池计数器等同于 Allocations Succeeded 计数器，于是特别池足够覆盖所有的内存分配。如果 Allocations Succeeded：特别池少于 Allocations Succeeded ，则特别池至少已经被耗尽过一次。

由于特别池没运用于这些计数器，所以计数器不跟踪大小为一页或更多的分配。

内核调试程序扩展！verifier也能够运用于监视特别池使用，它展示了与Driver Verifier Manager相似的信息。欲获取关于调试程序扩展的信息，请参看微软调试程序使用文件。

图形驱动程序

为获取这种选项怎样作用显示于驱动程序和内核模式打印驱动程序，参看图形驱动程序的特别内存池。
1.1.1.3 强迫IRQL检查（Forcing IRQL Checking）

尽管内核模式驱动程序被禁止在高IRQL或保持一种自旋锁时访问分页内存，但如果分页实际上没被修剪，这种动作将不会被注意到。

当 Forcing IRQL Checking 可用时，Driver Verifier将给所选择的驱动程序施加极端的内存压力。不论何时IRQL被抬伸到DISPATCH_LEVEL或更高，或当一自旋锁被请求时，所有的驱动程序的可分页代码和数据（和系统可分页的池、代码、数据）被标记为修剪过。如果驱动程序试图访问任何一个这种内存，Driver Verifier发布一个错误检测。

既然别的驱动程序的IRQL抬伸不会引起这个动作，这个内存压力将不会直接作用于未被选择检查的驱动程序。然而，当一个正在检查的驱动程序抬伸IRQL时，Driver Verifier修剪分页，该分页能在未被检查的驱动程序所使用。当这个选项运行时，未被检查的驱动程序的这种错误可能偶然被捕获。

图形驱动程序

Forcing IRQL Checking 选项不用于图形驱动程序，如被选中，将不起作用。
1.1.1.4 低资源模拟（Low Resources Simulation）

当 Low Resources Simulation 可用时，Driver Verifier将引起驱动程序内存分配的一个随机选择失效，这个过程测试驱动程序对低内存和其他低资源状况下正常反应的能力。

为精确模拟一低内存条件，这些分配故障直到系统启动后的7分钟被注入，因此，在该过程中暴露的任何驱动程序错误将以合法的运行问题对待，而非不切实际的情况。

标记为MUST_SUCCEED的分配请求不服从于这一动作， MUST_SUCCEED池的每页最大值被禁止。

Driver Verifier能同时检查所选择的驱动程序或所有的驱动程序。

图形驱动程序

参看图形驱动程序的 Low Resources Simulation 来获得该选项如何作用于显示驱动程序和内核模式驱动程序的细节。
1.1.1.5 内存池跟踪（Memory Pool Tracking）

Memory Pool Tracking 监视驱动程序所做的内存分配，当驱动程序未装载时，Driver Verifier确保驱动程序所决定的任何分配都已经释放。

不能释放的内存分配（也叫内存泄露）是引起低操作系统执行的通常原因，这些能引起系统池破碎，最终导致系统崩溃。

当这一选项运行时，如一驱动程序没有释放其所有的分配就卸载，Driver Verifier将发布错误检测0xC4（参数1等于0x60）。

如果Driver Verifier发布错误检测的参数1等于0x51，0x52，0x53，0x54或0x59，则该驱动程序已经写入分配之外的内存里，在这种状况下，你应该能够让特别内存池来定位错源。

参看微软调试程序文件的使用来获得所有错误检测参数0xC4的列表。

监视内存池跟踪

Driver Verifier Manager的 Pool Tracking screen 能够用来监视有分页和无分页的池分配。

内核调试程序扩展！verifer2能被用于驱动程序卸载之后未决的内存分配，或当驱动程序运行时跟踪当前的内存分配。这个扩展也表明了池标记，池大小和每一个分配的分配器地址。为更多的调试程序信息，请参看微软的调试程序使用文件。

图形驱动程序

Memory Pool Tracking 选项不适用于图形驱动程序，如被选中，不起作用。
1.1.1.6 I/O检查

Driver Verifier有两个I/O检查构建，1级I/O检查从一特别池分配驱动程序的IRP和监视驱动程序的I/O对各种不当的动作的处理，2级I/O检查执行所有1级的动作，和许多更细更广驱动程序I/O的使用。

2级I/O检查是一更有力的检测驱动程序I/O的使用的方法，然而，这种高级的详细审查占用了更多的内存，且它也能降低操作系统的执行级别。

1级I/O检查

当1级I/O检查可用时，通过 IoAllocateIrp 获得的所有IRP从一特别池分配，且它们的使用受到跟踪。

此外，Driver Verifier检查非法的I/O调用，包括：

n 尝试释放一个非IO_TYPE_IRP类型的IRP

n 传递非法设备对象给 IoCallDriver

n 传递一IRP 给含有非法的状态或仍保留一已取消的例程集的 IoCompleteRequest

n 通过驱动程序调度例程的调用来改变IRQL

n 尝试释放保留关联的一个线程的IRP

n 传递一个设备对象给已经有初始化过的定时器的 IoInitializeTimer

n 传递一个非法的缓冲区给 IoBuildAsynchronousFsdRequest 或 IoBuildDeviceIoControlRequest

n 当一个I/O状态块分配给一极松散的堆栈时，传递一个I/O状态块给一个IRP

n 当一个事件对象分配给一极松散的堆栈时，传递一个事件对象给一个IRP

由于特别IRP池有大小限制，只有当一次使用于一个驱动程序时，I/O检查才最有效。

1级I/O检查失效将引起错误检测0xC9发布，错误检测的第一个参数表明有什么违背发生。欲得一个全部的错误检测0xC9参数的列表，参看微软的调试程序文件。

2级I/O检查

2级I/O检查错误以不同的方式显示：在一个蓝屏上、在一个崩溃的转储文件里和在一个内核调试程序里。

在一个蓝屏上，信息 IO SYSTEM VERIFICATION ERROR 和信息串 WDM DRIVER ERROR XXX 记下了这些错误，这里 XXX 是一个I/O的错误代码。

在一个崩溃的转储文件里，信息 BugCheck 0xC9（DRIVER_VERIFIER_IOMANAGER_VIOLATION）与I/O错误代码一道，记下了这些错误。在此情况下，I/O错误代码以错误检测0xC9的第一个参数出现。

一个内核调试程序里（KD或WinDbg）这些错误以信息 WDM DRIVER ERROR 和一描述的文本信息串出现，当内核调试程序运行时，忽略2级错误和恢复系统操作是可能的。（不可能出现别的错误检测。）

在以上的每一情况下，额外信息（例如驱动程序名和各种可用的指针）也被显示，欲知2级I/O检查错误信息的全面描述，参看微软的调试程序文件中关于错误检测0xC9的部分。

图形驱动程序

I/O检查选项不适用于图形驱动程序，如被选中，不起作用。
1.1.2 Driver Verifier对图形驱动程序的能力

微软的Windows2000内核模式图形驱动程序不直接分配内存池，相反，这些驱动程序使用GDI（图形驱动程序接口）服务例程，该例程由 win32k.sys 提供。

由于这个差异，Driver Verifier对待图形驱动程序与内核模式驱动程序不同。

下面的部分描述了Driver Verifier在显示驱动程序和内核模式打印驱动程序上的作用。

n 2.1.2.1 图形驱动程序的特别内存池

该动作使用一特别池来检测内存的上溢和下溢，还有在其释放后访问内存。

n 2.1.2.2 图形驱动程序的低资源模拟

该动作注入随机的分配故障和其他被拒绝的请求来检测驱动程序在一个低内存状况下的反应。

图形驱动程序的不可用选项

当在检查一个图形驱动程序时，Driver Verifier经常执行的自动检查（IRQL和内存例程的检查，检查定时器释放的内存池，检查驱动程序卸载）并不执行。

强迫IRQL检测、内存池跟踪和I/O检查选项不被用于图形驱动程序，如被选择，没有作用。

注意：Driver Verifier能被设置检查 win32k.sys 自身。然而，这对同时检查的所有图形驱动程序都有影响。为获得更多关于图形驱动程序的具体信息，Driver Verifier应该仅当驱动程序在调查状态下才被检查。

1.1.2.1 图形驱动程序的特别内存池

内存讹误是一个常见的驱动程序问题，驱动程序错误能导致在它们建立起来很长时间后崩溃。这些错误当中最常见的要数访问已释放的内存，并分配n字节然后是n+1字节。

当特别内存池功能应用到图形驱动程序时，EngAllocMem 例程分配的内存将被移出特别内存池，Driver Verifier将监视该池以发现不正确的使用。

两种特别内存池定位是可行的，Verify End定位能更好的发现访问上溢，Verify Start定位能更好的发现访问下溢。（注意：最主要的内存讹误是由于上溢，而非下溢。）

当特别内存池运行且选择Verify End时，驱动程序所请求的每一内存分配被放到分别的各分页上，在每页上允许分配的最高可能地址被返回，以便内存分配到页末。每页前面的部分以特别形式写出。前页与下一页被标记为不可访问。

如果驱动程序在分配之后尝试访问内存，Driver Verifier将立即发现并发布错误检测0xCD。如果驱动程序先于缓冲区开始写入内存，这很有可能改变形式。当缓冲区被释放时，Driver Verifier将发现并报告错误检测0xC1。

如果驱动程序在释放缓冲区之后读或写，Driver Verifier将报告错误检测0xCC。

当Verify Start被选择，内存缓冲区被定位到页的开端，在这种设置下，下溢引起立即的错误检测，而上溢当内存释放时才引起内存检测。这种选项在其他方面与Verify End选项相同。

Verify End是缺省定位，是由于驱动程序上溢错误比下溢错误要普遍的多。为改变这种设置，请使用全局标记应用程序。

池标记

Driver Verifier将给已选择检查的驱动程序分配特别内存池，使用特别内存池的另一个方法是分配它给一个具有特别标记的内存池。

可利用全局标记应用程序致力于将特别池给具有给定标记的池。

同时通过Driver Verifier和全局标记应用程序来请求特别池是允许的，如果这么做，Windows2000将尝试为所有具有指定标记的池和所有指定的驱动程序的池分配请求使用特别池。

特别池效率

特别池的每一个分配使用不可分页内存的一页和有虚拟地址空间的两页。如果池耗尽，内存以标准方法分配，直到特别池再次变得可用为止。这样，如果特别内存池在用，多驱动程序同时被检查则不受推荐。

有大量小内存请求的一个单一驱动程序也会耗尽此池，出现此情况，给驱动程序内存指定池标记和致力于一次给特别池一个池标记将是更可取的。

特别池的大小随系统里的物理内存的大小增长而增长，理想的池大小至少1GB。在x86机器上，当消耗虚拟空间（对物理空间亦同）时，引导而没有/3GB的开关也是优先选取的。增加页面文件的最大/最小量（通过两或三当中的一个因子）也是一个好主意。

如果特别内存池可用，但是不到95%的所有池分配已经从特别池指定，在Driver Verifier管理器的Driver Status screen上将有一个警告。发生这种情况时，你应该检查一个更短的驱动程序列表，通过池标记检查单个池，或者给你的系统增加更多的物理内存。

确信所有的驱动程序分配已被检测，推荐加强驱动程序较长时间周期

非图形驱动程序

要获得这个选项怎样作用其他内核模式驱动程序的细节，参看特别内存池。
1.1.2.2 图形驱动程序的低资源模拟

当低资源模拟可用时，Driver Verifier将引起随机的驱动程序内存分配的选择失效，这个过程检查驱动程序对低内存和其他低资源状况下正常反应的能力。

下面的GDI回调函数易受制于随机失效：

n EngAllocMem

n EngAllocUserMem

n EngCreateBitmap

n EngCreateDeviceSurface

n EngCreateDeviceBitmap

n EngCreatePalette

n EngCreateClip

n EngCreatePath

n EngCreateWnd

n EngCreateDriverObject

n BRUSHOBJ_pvAllocRbrush

n CLIPOBJ_ppoGetPath

为精确模拟一个低内存条件，这些分配故障直到系统启动7分钟后才被注入，因此，任何在该过程中暴露的驱动程序错误将被当做合法的运行时间问题，而非不切实际的现象。

标记为MUST_SUCCEED的分配请求不服从于这一过程， MUST_SUCCEED池每页的最大值被禁止。

这些内存失效也许会引起着色的错误，其表现在不正确的图象和其他输出错误，所以，这个Driver Verifier选项应该用于对一图形驱动程序的健全检测，而当检测正确执行时也应该运行。

Low Resources Simulation 选项也许会在外壳图标的高速缓存里引起讹误，发生此情况，图标的高速缓存必须手工从硬盘删除。

非图形驱动程序

参看 Low Resources Simulation 来了解该选项如何作用其他内核模式驱动程序的细节。
1.1.3 激活和监视Driver Verifier

Driver Verifier由Driver Verifier的图形接口或verifier.exe的命令行激活，特别内存池选项也可通过Global Flags Utility激活成标记的内存池。

Driver Verifier状态的大多数改变（激活、去活、改变选项、或者改变正被检查的驱动程序列表）在重新启动时仍起作用，无需改变或重新编译驱动程序。Driver Verifier在Microsoft® Windows® 2000的自由和检查构建上都能执行。

然而，一些选项在没有重新启动介入时仍能被激活或去活。细节参见Driver Verifier管理器的易变设置窗口或命令 verifier /volatile /flags VALUE。

监视驱动程序行为

Driver Verifier管理器窗口中的全局计数器和池跟踪，可用来监视Driver Verifier的动作和已装载的驱动程序，这些窗口不监视图形驱动程序的检查。

内核调试程序扩展！verifier 也能用于监视和报告关于许多相关的Driver Verifier行为的统计数字。当检查图形驱动程序时，应该以！gdikdx.verifier 扩展代替。关于调试程序扩展的细节，参看微软调试程序的使用文件。
1.1.3.1 检查器（Verifier）命令行

应用程序verifier.exe可用于从命令窗口激活Driver Verifier。

应用程序放在Windows2000的system32目录下，在Windows2000 DDK的tools目录下。

命令行选项包括：

verifier /flags VALUE [/iolevel 2] /all

使用指定的选项检查所有已安装的驱动程序，VALUE 是下面位的一个综合。

位 作用

0x01 特别内存池

0x02 强迫IRQL检查

0x04 低资源模拟

0x08 内存池跟踪

0x10 I/O检查

位也许会自由地结合，VALUE 必须作为一个十进制数输入，十六进制数不被支持。缺省值是11（1+2+8），如果包括/ iolevel 2，I/O检查被设置为 Level 2。（缺省值是Level 1。）

这些设置在下一次引导后生效。

verifier /flags VALUE [/iolevel 2] /driver NAME [NAME2 NAME3….]

检查所列的驱动程序，被空格分开，多个驱动程序通过罗列它们的名字被确定，但是，诸如n*.sys等通配符的值不被支持。象上面的一样，VALUE 和/ iolevel相同。

这些设置在下一次引导后生效。

verifier /reset

它清除了所有的Driver Verifier设置,在下一次引导后，不检查驱动程序。

verifier /volatile /flags VALUE

不同标准的Driver Verifier激活，易变设置立即起作用，VALUE 是下面位的一个结合。

位 作用

0x01 特别内存池

0x02 强迫IRQL检查

0x04 低资源模拟

位也许会自由地结合，VALUE 必须作为一十进制数输入（而非十六进制数）。

仅仅已列的选项能够有多种设置，在一个多种设置方式里，被检查的驱动程序列表不能改变，且所有的多项设置在重新引导时将丢失。

verifier /query

产生Driver Verifier所显示在窗口的当前数据的一个总括，数据包括内存分配的一个枚举、IRQL的增加、自旋锁和其他Driver Verifier选项的相关数据。

verifier /log LOG_FILE_NAME [/interval SECONDS]

产生记录文件来保持内存、IRQL和自旋锁信息。这个文件有以 SECONDS 指定的频率写上去的当前数据，缺省间隔是30秒。

verifier /？

显示帮助信息。
1.1.3.2 Driver Verifier管理器

Driver Verifier管理器是一个图形接口，其允许你激活Driver Verifier并监视它的动作状态。

Driver Verifier管理器有五个不同的标记页，Driver Status、Global Counters 和 PoolTracking 标记用于显示信息和监视正被检查的驱动程序，Settings 标记用来激活和配置Driver Verifier的动作；对此窗口的改变将在下次引导后起作用，Volatile Settings 标记用于立即改变Driver Verifier的动作（在重新引导之前）。

启动Driver Verifier管理器

Driver Verifier 的快捷方式能启动Driver Verifier管理器，该快捷方式安装在Windows2000 DDK里。

在命令窗口运行应用程序 verifier.exe 也能启动Driver Verifier管理器。该文件放在Windows2000的%windir%\system32 目录下，在Windows2000 DDK的 tools 目录里也有。在没有命令行选项时运行 verifier.exe 来激活Driver Verifier管理器。

驱动程序状态窗口

图：

Driver Verifier管理器驱动程序状态窗口

Driver Status 窗口显示哪一些驱动程序正被装载、哪一些驱动程序被检查和哪一些Driver Verifier选项可用。

全局标志部分表明哪一些Driver Verifier选项当前正在使用。

被检查的驱动程序部分列出所有驱动程序，Driver Verifier已被指示检查这些程序，可能的状态值是：

n Loaded

这意味着驱动程序当前已经装载并正被检查。

n Unloaded

这意味着驱动程序自最后一次引导以来被装载和检查过至少一次，但目前没被装载。

n Never Loaded

这意味着指示Driver Verifier来检查这个驱动程序，但驱动程序自最后一次引导以来未被装载，这可能由讹误的驱动程序图形文件所致。

通过点击标题栏上的Driver或Status来分类这个列表。

重设频率部分控制多久刷新状态栏一次，高、中和低指令Driver Verifier以不同的速度刷新该栏，而手工不能自动更新此栏。按钮 Refresh Now 可使状态栏立即被更新。

如果特别内存池可用，但是不到95%的所有池分配已经从特别池指定，窗口上将出现一个警告信息。参看特别内存池或图形驱动程序特别内存池获取详细资料。

全局计数器窗口

图：

Driver Verifier管理器的全局计数器窗口

全局计数器（Global Counters）窗口显示统计数字，这些数字可帮助监视Driver Verifier的行为。

分配计数器对监视与特别内存池相关的统计数字特别有用，这些计数器仅记录标准内核模式驱动程序所使用的池，而不包括图形驱动程序在内，这里Driver Verifier正在检查标准内核模式驱动程序。当然，如果相应的功能不可用，这些计数器当中的一些将被设置为零，举例说，如果低资源模拟没运行，注入计数器的故障将为零。

重设频率部分控制这些计数器多久被更新一次，高、中和低指令Driver Verifier以不同的速度刷新计数器，而手工不能自动更新。按钮 Refresh Now 可使计数器立即更新。

池跟踪窗口

图：

Driver Verifier管理器的池跟踪窗口

池跟踪（Pool Tracking）窗口显示了关于分页和未分页的池分配的信息（包括当前值和峰值）。

此窗口对监视与内存池跟踪有关的统计数字非常有用。

单个计数器部分每次显示一个驱动程序的统计数字，此窗口顶部的下拉框允许你从当前正被检查的所有驱动程序中选择。

在全局计数器部分，不能跟踪的分配计数器显示了当前正被检查的所有驱动程序的未跟踪分配数目。由于特别池不能用于这些计数器，而计数器不跟踪大小为一页或更大的分配。

重设频率部分控制这些计数器多久被更新一次，高、中和低指令Driver Verifier以不同的速度刷新计数器，而手工不能自动更新。按钮 Refresh Now 可使计数器立即被更新。

设置窗口

图：

Driver Verifier管理器的设置窗口

设置（Settings）窗口是Driver Verifier和它的选项的主要控制面板。

当按下 Apply 按钮时，从 Settings 窗口传来的所有改变被写到注册表里。如果你退出Driver Verifier管理器而没有按 Apply，将问你是否想应用或丢弃你的改变。

当再次引导之后，Driver Verifier将遵照窗口上的改变，直到你重新引导，将不会有 Settings 窗口的改变对Driver Verifier的功能起作用。

窗口上当前系统可用的驱动程序部分列出了在最近的一次引导里系统所装载的驱动程序。

Verification Status 栏给出了每个驱动程序的当前状态，可能的检查状态值有：

n Verify Enabled

这意味着驱动程序正被检查，在重新引导后也将继续被检查。

n Verify Disabled

这意味着驱动程序当前没被检查，在重新引导后也不被检查。

n Verify Enabled（Reboot Needed）

这意味着驱动程序当前没被检查，但是用户已请求检查该驱动程序，在重新引导后它将被检查。

n Verify Disabled（Reboot Needed）

这意味着驱动程序正被检查，但是用户已请求结束检查，重新引导后，该驱动程序不再被检查。

你可以从这个列表中选择一个或多个驱动程序，点击 Verify 或 Don’t Verify 按钮，你也可以在驱动程序名右击，并从弹出的菜单控制检查。

在下一次重新引导后检查这些附加的驱动程序窗口，允许你进入当前系统并未装载的驱动程序名。多个名字必须以空格分开。

如果选择检查所有驱动程序，在重新引导后，Driver Verifier将检查所有的驱动程序。当选择这个选项按钮时，驱动程序列表和 Verify 和 Don’t Verify 按钮变成灰色。

检查类型部分允许你选择在重新引导后哪一个Driver Verifier选项可用。参看Driver Verifier能力和图形Driver Verifier能力中关于这些选项的描述。

Preferred Settings 按钮是打开大部分常用选项的快方式（特别内存池、强迫IRQL检测、内存池跟踪和2级I/O检查）它也选择了检查所有驱动程序按钮。

Reset All 按钮将使所有的驱动程序检查不可用。

易变设置窗口

图：

Driver Verifier管理器的易变设置

易变设置（Volatile Settings）窗口允许你对Driver Verifier做立即的改变（而不是在重新引导之后）。

三种Driver Verifier选项——特别内存池、强迫IRQL检测和低资源模拟能从该窗口打开或关闭。当按下 Apply 按钮时，这些选项当前正被检查的每一个驱动程序激活或去活。

Volatile Settings 窗口不能用来改变被检查的驱动程序列表，也不能改变Memory Pool Tracking或I/O检查状态，所有多项设置在重新引导后将丢失。

该窗口上的检查驱动程序和更新频率部分对应驱动程序状态窗口是同一的。
1.1.3.3 全局标记应用程序（Global Flags Utility）

通过调整运行系统的内核设置，改变图像文件的设置，全局标记应用程序 gflags.exe 提供了一种在系统注册表里设置特定键的简单方法。你能通过使用一个GUI或命令行接口设置这些键。

应用程序放在Windows2000 DDK的 bin 目录下面。

全局标记应用程序也能用来配置Driver Verifier的特别内存池，或用特别内存池进行单个内存分配。

图：

全局标记应用程序窗口

为改变特别内存池设置，必须选择目标部分里的系统注册表选项按钮。

内核特别池标记对话框允许设置一定的特别池选项。

如果一池标记被输入，特别池将应用于具有指定的池标记的所有池分配。如果同时通过Driver Verifier使特别内存池可用，有指定标记的所有池和从正被检查的驱动程序所分配的所有池将从特别池分配（易受池的可用性影响）。

Verify Start选项按钮引起特别池的定位集中在下溢检测，Verify End选项则集中在上溢检测。这些按钮控制特别池分配的定位，这些特别池分配由池标记或Driver Verifier完成。

关于特别内存池的使用的细节，参看特别内存池和图形驱动程序的特别内存池。


即插即用和电源管理的介绍

本章描述了与编写支持即插即用和电源管理的驱动程序相关的主要概念，这些信息如下组织：

1.1 什么是即插即用?

1.1.1 PnP组件

1.1.2 PnP的支持级别

1.2 什么是电源管理?

1.2.1 电源管理的最初含义

1.2.2 电源管理的支持级别

1.2.3 系统范围的电源管理总览

1.2.4 电源状态

1.3 设备树

1.4 驱动程序分层和设备对象

1.4.1 驱动程序种类

1.4.1.1 总线驱动程序

1.4.1.2 功能驱动程序

1.4.1.3 过滤器驱动程序

1.4.2 驱动程序分层---一个例子

1.4.3 设备对象种类

1.4.4 设备对象---一个例子

阅读第2章对驱动程序怎样支持PnP和电源管理有一总体印象。阅读第2部分获取关于PnP的详细信息，阅读第3部分获取关于电源管理的详细信息，阅读第4部分获取关于驱动程序和设备安装的详细信息。

1.1 什么是即插即用？

即插即用（PnP）是一个硬件和软件支持的组合，这些软件支持能让系统识别或适应硬件配置的小改变而没用用户的干预。用户可从计算机系统添加或删除设备而不必做笨拙而且混淆的手工配置，也不需要复杂的计算机硬件知识。例如，用户可以连接一便携机并使用连接站键盘、鼠标和监视器而不需要手工改变配置。

PnP需要从硬件设备、系统软件和驱动程序获得支持。硬件工业的最初思想是给容易标识的插件板和基本的系统组件定义标准（比如PnP ISA定义和PC Card标准）。这本DDK文档编制集中在系统软件对PnP的支持和驱动程序怎么使用这个支持来实现PnP。

系统软件对PnP的支持和PnP驱动程序一起提供以下功能：

n 对已安装硬件的自动和动态识别

在最初的系统安装过程中系统软件识别硬件，识别发生在系统引导之间的PnP硬件改变，并对诸如连接或断开的运行时间硬件事件和设备添加或删除作出响应。

n 硬件资源分配（和再分配）

PnP管理器决定由每一设备（例如，输入/输出[I/O]端口、中断请求[IRQ]、直接内存访问[DMA]通道和内存定位）所请求的硬件资源，并适当分配硬件资源。当需要时，PnP管理器重新配置资源分配。例如当新设备被添加到必须正在使用的资源系统中。

PnP设备驱动程序不分配资源，相反，当一个设备被枚举时，设备所请求的资源是被标识过的。在资源分配过程中，PnP管理器找到每个驱动程序的请求。对一个早期的设备而言，资源不是动态配置的，所以，PnP管理器首先给早期的驱动程序分配资源。

n 适当的驱动程序的加载

PnP管理器决定哪一些驱动程序对支持每个设备是必要的，并加载这些驱动程序。

n 驱动程序与PnP系统相互作用的编程接口

接口包括 Io Xxx 例程、PnP IRP、必要的驱动程序入口点和注册表信息。

n 驱动程序和应用程序了解硬件环境变化并采取适当的动作机制

PnP能让驱动程序和用户模式代码登记，并通知一定的硬件事件。

PnP驱动程序是PnP支持的重要部分，作为一个合格的PnP驱动程序必须提供必要的PnP入口点、处理必要的PnP IRP和遵从PnP向导行。欲知更多信息，参见第2章。
1.1.1 PnP组件

图1.1显示了一组同时工作来支持PnP的组件

图1.1 PnP软件组件

PnP管理器有两个部分：内核模式PnP管理器和用户模式PnP管理器。内核模式PnP管理器与OS组件和驱动程序相互作用来配置、管理和维护设备。用户模式PnP管理器与用户安装组件相互作用来配置和安装设备，例如类安装器。用户模式PnP管理器也与一些应用程序相互作用，举例来说，登记一个设备改变通知的应用程序，并当一个设备事件发生时通知应用程序。

PnP驱动程序支持物理的、逻辑的和虚拟的机器设备，WDM驱动程序遵从微软的驱动程序模式并支持微软的Windows2000和Windows98系统上的设备。WDM PnP驱动程序通过严格定义的API和IRP与PnP管理器和其他内核组件通信。参看《Windows2000驱动程序开发参考》卷一获得这些API和IRP的详尽描述。安装了Windows2000的机器也许有一些PnP驱动程序不支持WDM。

所有PnP驱动程序应该支持PnP和电源管理，如果一单个PnP的驱动程序不支持PnP和电源管理，它作为一个系统整体约束PnP驱动程序和电源管理。

参考第4部分关于设备和驱动程序安装的信息，包括INF文件、.cat文件和注册表。
1.1.2 PnP的支持级别

设备支持PnP的范围取决于设备硬件和设备驱动程序里的PnP支持(看表1.1)。

表1.1 对驱动程序/设备综合的PnP能力级

PnP 驱动程序 非PnP驱动程序

PnP设备 完全PnP 无PnP

非PnP设备 可能的部分PnP 无PnP

任何支持PnP的设备在其驱动程序里都应该有PnP支持。

如果一个非PnP设备被一个PnP驱动程序驱动的话，它能够有一些PnP能力。举例来说，一个ISA声卡和一个EISA网卡可手工安装，然后PnP驱动程序能象一个PnP设备一样对待该卡。

如果一驱动程序不支持PnP，它的设备表现为非PnP设备而不管任何硬件PnP支持。一个非PnP驱动程序能够约束整个系统的PnP和电源管理能力。

在OS支持PnP之前编写的早期驱动程序继续如从前一样工作，但没有任何PnP能力。新的驱动程序应该包括PnP支持。

1.2 什么是电源管理？

电源管理是系统范围的、集成的方法来使用和保存电源，包括电源管理的软硬件支持的计算机系统提供以下功能：

n 最小的启动和关机延迟

系统能在低电力状态时休眠，在这种状态下无须完全的重新引导系统就能恢复操作。从用户的角度来看，唤醒一休眠的计算机大致可象开关电视机一样。

n 更强大的电源效率和电池生命期

仅仅当设备传输功能给用户时电源才启用于设备，如果一个设备未使用，按照命令，电源可被断开，而后再加电。

n 更安静的操作

断开未被使用的设备电源可降低噪音，此外，硬件和软件能够管理电流装载和热装载，其结果是计算机休眠时几乎听不到声音。

工业界OnNow Initiative定义了电源管理的硬件和软件需求，细节参看电源管理的工业萌芽。

在Windows2000、Windows98和其他支持电源管理的操作系统里，计算机和其他外部设备被维持在完成手头任务所需的最低可行的电平上，驱动程序与操作系统协作来管理它们的设备电源。如果所有的驱动程序支持电源管理，操作系统能够在系统范围基础上管理电力消耗，这样，电力保存、电力切断和快速恢复，及需要时再次唤醒都可被管理。

这些集成的电源管理方法---包括在操作系统、系统硬件、设备驱动程序和设备硬件里，产生如下的结果：

n 更智能的电源管理决定。在每一电平，被最优通知的组件指导电力使用。

n 更高的可靠性。更好的电源管理决定减少了不适当的时间关机和数据丢失的可能。

n 平台的独立性。操作系统在不同的硬件平台之间使用一种可控制的、单独的方法来管理电源，允许电力在不同的设备上最大保存。

n 更好的设备集成。符合工业界规范的设备驱动程序确保最大的电力保存而不管其硬件平台。

综合而言，这些优点导致保存更多电能和更有效地利用电能，这在以前是不可能的。
1.2.1 电源管理的最初含义

OnNow Initiative描述了电源管理所需的硬件和软件支持。

高级配置和电源管理接口规范的OnNow Initiative部分，描述了一个硬件级接口，该接口能让操作系统以一种一致的、平台独立的方式实现电源管理。

设备类电源管理参考规范对每个普通的设备类是可行的，如声频或通信设备。每一个规范描述了一个设备类的电源管理要求。驱动程序的编写者应该参考这些规范，也可通过微软的网站来获得设备相关的细节。
1.2.2 电源管理的支持级别

支持PnP的驱动程序必须支持电源管理，支持电源管理的驱动程序也必须支持PnP，电源管理和PnP是被集成而互不独立的。

电源管理有两个工作层次：整个系统和单个设备。

电源管理器，是操作系统内核的一部分，管理着整个系统的电源级别。如果系统中所有的驱动程序都支持电源管理，电源管理器能在系统范围的基础上管理电源消耗，不只利用全开或全关的状态，也有多种中间的系统休眠状态。

在操作系统支持支持电源管理之前编写的早期驱动程序能如从前一样继续工作。然而，包括早期驱动程序的系统不能够进入任何中间的系统休眠状态；它们只能象过去一样在全开或全关的状态下操作。

驱动程序处理设备电源管理。一个支持电源管理的驱动程序能够在需要时打开它的设备，不用时关闭设备。如果设备有硬件能力允许这么做，设备能够进入中间的电源状态。当前系统中早期的驱动程序没有影响更新的驱动程序管理它们设备电源的能力。
1.2.3 全系统范围的电源管理的总览

电源管理需要得到系统、设备硬件和系统软件、驱动程序的支持。如在“电源管理的最初含义”部分描述的一样，必要的硬件支持包括在工业规范里。这一部分包括了软件支持，特别地，驱动程序该做什么来遵照操作系统的要求，并正常的管理它们的设备电源。

图1.2 给出了全系统范围的电源管理总览

分别通过API和控制面板，应用程序和用户已经分别地输入了电源管理决定。用户可通过控制面板设置一定的电源选项，包括设备专用的GUID控制选项，这个选项是如果响应的驱动程序支持它时，用户界面将会提供。通过WMI，控制面板通知电源管理器和驱动程序GUID控制里的改变。

电源管理器管理全系统的电源策略，这些策略包括管理系统电源使用的规则。（细节参见第3章第3部分的系统电源管理。）使用从控制面板和得到的信息，电源管理器能够决定什么时候应用程序在使用，或者可能需要使用不同的设备，这样它能够适当调整系统的电源策略。

电源管理器也提供一个驱动程序接口，该接口主要包括 Po Xxx 例程、电源管理IRP和必要的驱动程序入口点。

当电源管理器请求系统电源状态的变化时，驱动程序通过放置它们设备在适当的设备电源状态来响应。此外，驱动程序能发现是否设备闲置，并设置未用的设备处于休眠状态。总线专用的机制报告设备电源能力、设置并报告设备状态及改变设备电源。怎样和何时确切地改变设备电源取决于设备类型和设备硬件的能力。

尽管ACPI硬件意识到了最大地电力储存，该硬件不需要为在驱动程序里生效的电源管理而遵从ACPI规范。
1.2.4 电源状态

电源状态指示了系统或单个设备的电平消耗，也指示了枚举活动的范围。电源管理器设定整个系统的电源状态，而设备驱动程序设置它们单个设备的电源状态。

ACPI规范定义了系统和单个设备的不连续电源状态。系统电源状态被引用为 Sx，这里 x 是0到5之间的一个状态数，设备状态被引用为 Dx，这里 x 是0到3之间的一个状态数。状态数与电力消耗呈反相关：越高的状态数使用越少的电力，状态S0和D0是最高的电力，也是最具功能的，是最完全的状态，状态S5和D3是最低的电力状态并有最长的唤醒时间。

严格定义的电源状态允许不同制造商的许多设备能一致地、可预测地一起工作。举例来说，当电源管理器设置系统为S3状态时，其能依靠支持电源管理器的驱动程序设置它们设备相应的设备状态，并能以一种可测的形式返回工作状态。

1.1 设备树

OS维持一个能保留系统里的设备踪迹的设备树。图1.3给出了系统配置的设备树的一个例子。

设备树保留着存在于系统中的设备信息，当机器引导时，通过使用从驱动程序和其他组件获得的信息，OS构建此树，并当设备被添加或删除时更新此树。设备树是分级的，总线上的设备代表着总线适配器或控制器的“子级”。你可参看设备管理器的使用来了解设备树的设备分级（选择“View”，然后选择“Devices by connection”）。

设备树的每一个节点是一个设备节点（devnode），一个devnode包括设备驱动程序的设备对象加上由OS所保留的内部信息。参看1.4.3部分来获取更多的信息。

图1.3PnP设备树例子

设备树的分级反映了机器里设备附着的结构，当OS管理设备时它使用该分级。例如，如果一用户请求从图1.3所示机器里拔掉USB控制器，PnP管理器从设备树上决定该行为也将导致另外的三个设备被拔掉（USB集线器、操纵杆和镜头）。当PnP管理器就USB控制器询问驱动程序删除控制器是否安全时，PnP管理器同样地就控制器（USB集线器、操纵杆和镜头）的任何子代而查询驱动程序。

除过在设备树里给出的分级关系外，机器设备之间有其他的关系，这里包括删除和抽出的关系。为了更多的信息，参见《Windows2000驱动程序开发参考》的卷一参考页里的IRP_MN_QUERY_DEVICE_RELATIONS。

1.3 驱动程序层次和设备对象

I/O系统提供一个分层的驱动程序体系结构，这一部分从支持PnP和电源管理的驱动程序的角度讨论了体系结构，讨论包括各种驱动程序、驱动程序层次和设备对象。
1.3.1 驱动程序种类

从PnP的角度，有三种驱动程序：

n 总线驱动程序---驱动I/O总线并提供设备间独立的每个插槽功能

n 功能驱动程序---驱动一个单个的设备

n 过滤器驱动程序---过滤一个设备、设备类或总线的I/O请求

在本书中，总线是物理的、逻辑的或虚拟的设备所能附着的任何设备；总线包括传统的总线，如SCSI和PCI，也有并口、串口和i8042端口。

对一个驱动程序编程人员来说，知道不同的PnP驱动程序种类和他们正在编写哪一类驱动程序是重要的。例如，是否一个驱动程序处理每个PnP IRP和怎样处理这个IRP取决于正在编写哪一类的驱动程序（一个总线驱动程序、功能驱动程序还是过滤器驱动程序）。

图1.4 给出了一个设备的总线驱动程序、功能驱动程序和过滤器驱动程序之间的关系。

每个设备典型地有一个父I/O总线的总线驱动程序管理，一个设备的功能驱动程序，零个或多个设备的过滤器驱动程序。需要许多过滤器驱动程序的驱动程序设计不能产生最佳的执行。

图1.4里的驱动程序如下：

1. 总线驱动程序服务于一个总线控制器、适配器和电桥。总线驱动程序是必须的驱动程序；在一个机器上，每个类型的总线有一个驱动程序。微软为大多数通用总线提供了总线驱动程序，IHV和OEM则提供了其他的总线驱动程序。

2. 总线过滤器驱动程序典型地给一总线增加值，它由微软或系统OEM提供，对一个总线来说，可以有任何数目的总线过滤器驱动程序。

3. 低层过滤器驱动程序典型地修改了设备硬件的行为，它们是可选择的且典型地由IHV提供，对一个设备来说，可以有任何数目的低层过滤器驱动程序。

图1.4 可能的驱动程序层次

4. 功能驱动程序是主要的设备驱动程序，一个功能驱动程序典型地由且必须由设备商来编写（除非设备使用原始模式）。

5. 顶层过滤器驱动程序典型地提供设备的增值特征，它们是可选择的且典型地由IHV提供。

下面各部分描述了驱动程序的总体类型（总线驱动程序、功能驱动程序和过滤器驱动程序）。
1.3.1.1 总线驱动程序

总线驱动程序服务一个总线控制器，适配器，或者电桥（参看图1.4），微软为大多数通用总线提供总线驱动程序，例如PCI，PnpISA，SCSI和USB。其他的总线驱动程序由IHV和OEM提供。总线驱动程序是必须的驱动程序；在一个机器里，每一类总线有一个总线驱动程序。如果机器里有不止一个同类的总线，则一个总线驱动程序能服务不止一个总线。

总线驱动程序的主要任务是：

n 枚举其总线上的设备

n 响应PnP和电源管理IRP

n 总线的多路访问（对某些总线）

n 总体上管理其总线上的设备

在枚举过程中，一个总线驱动程序识别它的总线上的设备并为它们产生设备对象。总线驱动程序用来标识相连接的设备方法取决于特别的总线。

总线驱动程序代表其总线上的设备来执行一定的操作，包括访问设备寄存器来物理地改变设备的电源状态。例如，当设备休眠时，总线驱动程序设置设备寄存器来给设备适当的电源状态。

但要注意，总线驱动程序不能够处理其总线上的设备的读和写请求，一个设备的读和写请求由设备功能驱动程序处理（看1.4.1.2部分）。仅仅当设备以原始模式使用时，父总线驱动程序处理设备的读和写。

总线驱动程序为控制器，适配器，或者电桥起着功能驱动程序的作用，并因此为控制器，适配器，或者电桥管理设备电源策略。

总线驱动程序能够作为一个驱动程序/小驱动程序对来执行，即以SCSI端口/微端口对驱动一个SCSI HBA（主机总线适配器）的方法。在这样的驱动程序对里，小驱动程序与次一级驱动程序（它是一个DLL）相链接。
1.3.1.2 功能驱动程序

功能驱动程序是设备的主要驱动程序（参见图1.4）。一个功能驱动程序典型地由设备厂商且必须由设备厂商来编写（除非设备使用原始模式）。PnP管理器为一个设备至少装载一个功能驱动程序，功能驱动程序能服务一个或多个设备。

功能驱动程序为它的设备提供操作接口，典型地，功能驱动程序处理设备的读和写并管理设备电源策略。

一个设备的功能驱动程序能够被作为一个驱动程序/小驱动程序对来执行，例如一个端口/微端口或一个类/微类对。在这样的驱动程序对里，小驱动程序与次一级驱动程序（其是一个DLL）相链接。

如果以原始模式驱动一个设备，它没有功能驱动程序和顶层或低层过滤器驱动程序。所有的原始模型I/O通过总线驱动程序和可选择的总线过滤器驱动程序来实现。
1.3.1.3 过滤器驱动程序

过滤器驱动程序是可选择的驱动程序，该驱动程序给设备增加值或修改设备的行为。过滤器驱动程序能服务于一个或多个设备。

总线过滤器驱动程序

总线过滤器驱动程序典型地给总线添加数值，它由微软或系统OEM提供（看图1.4）。总线过滤器驱动程序是可选择的，对一个总线来说，可以有任何数目的总线过滤器驱动程序。

举例来说，总线过滤器驱动程序能够实现标准总线硬件的特性增强。

对ACPI BIOS所描述的设备，电源管理在每个这样的设备的总线驱动程序之上插入微软提供的一个ACPI过滤程序（总线过滤器驱动程序）。ACPI过滤程序执行设备电源策略，并打开和关闭设备电源，ACPI过滤程序对其他驱动程序是透明的且在非ACPI机器上是没有的。

低层过滤器驱动程序

低层过滤器驱动程序典型地修改设备硬件的行为（看图1.4），它们典型地由IHV提供且是可选择的，对一个设备来说，可以有任何数目的低层滤驱动程序。

低层设备过滤器驱动程序监视和/或修改一个特定设备的I/O请求，典型地，这些过滤程序重新定义了硬件行为来匹配期望的规范。

低层类过滤器驱动程序监视和/或修改一个设备类的I/O请求，例如，通过执行一非线性鼠标运动数据转换，鼠标设备的低层类过滤器驱动程序能够提供加速。

顶层过滤器驱动程序

顶层过滤器驱动程序典型地为一设备提供增值的特征（看图1.4），这种驱动程序通常由IHV提供且是可选择的。一个设备可有任意数目的顶层过滤器驱动程序。

顶层设备过滤器驱动程序为一个特别的设备添加数值。例如，一个键盘的顶层设备过滤器驱动程序能够加强额外的安全检查。

顶层类过滤器驱动程序为一个特别类的所有设备添加数值。
1.3.2 驱动程序层次---一个例子

这一部分描述了USB硬件可能的一套PnP驱动程序来说明PnP驱动程序的层次。

图 1.5给出了一个USB操纵杆的PnP硬件配置的例子，在图 1.5里，USB操纵杆插进一个USB集线器上的一端口。该例中的USB集线器常驻在USB主控制器面板上，且插到USB主控制器面板上的单个端口里，USB主控制器插到一个PCI总线上。从PnP的角度来看，USB集线器、USB主控制器和PCI总线都是总线设备，因为它们每一个都提供端口。操纵杆不是总线设备。

图 1.5 PnP 硬件的USB操纵杆的例子

图 1.6给出了一套驱动程序的例子，这些驱动程序可能为图 1.5 中的USB操纵杆硬件的原因而装载。

开始于图 1.6的底部，堆栈例子里的驱动程序包括：

n 驱动PCI总线的一个PCI驱动程序，这是一个PnP总线驱动程序，PCI总线驱动程序由微软的系统所带。

n USB主控制器的总线驱动程序作为一个类/微类驱动程序对来执行。微软系统带有USB主控制器类和微类驱动程序。

n USB集线器总线驱动程序驱动USB集线器，微软系统提供USB集线器驱动程序。

n 操纵杆设备的三个驱动程序，其中之一是一个类/微类对。

图 1.6 PnP驱动程序层次的例子--- USB操纵杆

功能驱动程序，即USB操纵杆设备的主要驱动程序，是一个HID类驱动程序/HID USB微类驱动程序对。（HID代表“human interface device”）HID USB微类驱动程序支持HID设备的USB专用的语法，其依赖HID类驱动程序DLL对HID驱动程序的总体支持。

功能驱动程序能专用于特定的设备，或在HID状态下，一个功能驱动程序能服务一组设备。在这个例子当中，HID类/HID USB微类驱动程序对服务于一USB总线系统中的任何HID-compliant 设备。一个HID类驱动程序/HID 1394微类驱动程序对将服务于1394总线系统里的任何HID-compliant设备。

功能驱动程序由设备厂商或微软编写。在这个例子当中，功能驱动程序（HID类/HID USB微类驱动程序对）由微软编写。

在该例中，操纵杆设备有两个过滤器驱动程序：一个顶层类过滤程序添加一个宏按钮特征和一个低层设备过滤程序使操纵杆能枚举鼠标设备。

需要过滤操纵杆I/O编写者编写顶层过滤程序，操纵杆厂商则编写低层过滤器驱动程序。

n 内核模式、用户模式HID客户和应用程序不是驱动程序，但为了全面一并给出。
1.3.3 设备对象种类

一个驱动程序为它所控制的每个设备产生设备对象，设备对象代表驱动程序的设备。从PnP的角度看有三种设备对象：

n 物理设备对象（PDO）---代表一个总线驱动程序的总线上的设备

n 功能设备对象（FDO）---代表一个功能驱动程序的设备

n 过滤程序设备对象（Filter DO）---代表一个过滤器驱动程序的设备

这三种设备对象都是DEVICE_OBJECT类型，但是使用方式不同并有不同的设备扩展。

通过产生一设备对象（IoCreateDevice）并将其附着到设备堆栈（IoAttachDeviceToDevice_Stack），驱动程序将其本身添加到处理设备的I/O驱动程序堆栈，IoAttachDeviceToDeviceStack 决定设备堆栈当前的顶层和附着新的设备对象到设备堆栈的顶层。

图1.7给出了设备对象的可能种类，该设备对象可附着于设备堆栈里，表示处理一个设备的I/O请求的驱动程序。

这一部分描述了每一类的设备对象并注意到何时产生该类。参看第2章获得关于在必要的PnP驱动程序例程里产生设备对象的细节信息，要获得PnP设备枚举的更多信息，参见第2部分。

开始于图1.7的底部：

n 总线驱动程序为总线上它所枚举的每个设备产生PDO。

当总线驱动程序枚举其设备时，它为每个子设备产生PDO。总线驱动程序枚举一设备为PnP管理器的 BusRelations 响应一个IRP_MN_QUERY_DEVICE_RELATIONS请求。如果自从最近一次总线驱动程序响应 BusRelations 的查询关系请求以来（或者这是机器被引导以来第一次查询关系）设备已经添加到总线上，则总线驱动程序为每个子设备产生一个PDO。

PDO表示了总线驱动程序的设备，其他内核模式系统组件也和它一样，如电源管理器、PnP管理器和I/O管理器。

一个设备其他的驱动程序附着于PDO顶端的设备对象，但是PDO一直在设备堆栈的底端。

n 可选择的总线过滤器驱动程序为它们过滤的每个设备产生过滤程序DO。

当PnP管理器在 BusRelations 列表里发现一个新设备时，它决定是否有该设备的任何总线过滤器驱动程序。如果是这样的话，对每个这样的驱动程序PnP管理器确保它们被装载（如果需要调用DriverEntry）并调用驱动程序AddDevice例程。如果总线过滤器驱动程序为这个设备过滤操作，过滤器驱动程序产生一个设备对象并附着它到AddDevice例程里的设备堆栈上。如果不止一个总线过滤器驱动程序存在，且与这个设备相关，每个这样的过滤器驱动程序产生并附着于它自己的设备对象。

n 可选择的，低层过滤器驱动程序为它们过滤的每个设备产生过滤程序DO。

如果一可选择的低层过滤器驱动程序由于这个设备的原因而存在，PnP管理器确信在总线驱动程序和任何总线过滤器驱动程序之后装载了这样的驱动程序。PnP管理器调用过滤器驱动程序的AddDevice例程，在它的AddDevice例程里，低层的过滤器驱动程序为设备产生一个过滤程序DO且附着它到设备堆栈里。如果不止一个低层过滤器驱动程序存在，每个这样的驱动程序将产生并附着它自己的过滤程序DO。

n 功能驱动程序为设备产生一个FDO。

PnP管理器确信已安装了设备的功能驱动程序并调用功能驱动程序的AddDevice例程，功能驱动程序产生一个FDO并附着它到设备堆栈里。

n 可选择的，顶层过滤器驱动程序为它们过滤的每个设备产生过滤程序DO。

如果任何可选择的，顶层过滤器驱动程序为设备而存在，PnP管理器确信在功能驱动程序调用它们的AddDevice例程之后被安装，每个这样的过滤器驱动程序附着它的设备对象到设备堆栈。

总之，设备堆栈包括每一驱动程序的设备对象，该驱动程序参与了特定设备的I/O处理。父总线驱动程序有一个PDO，功能驱动程序有一个FDO，每一个可选择的过滤器驱动程序有一个过滤程序DO。

注意到所有的设备---总线适配器/控制器设备和非总线设备---在它们的设备堆栈里有一个PDO和FDO。总线适配器/控制器的PDO由父总线的总线驱动程序产生。例如，如果一个SCSI适配器插入一个PCI总线，PCI总线驱动程序为SCSI适配器产生一个PDO。

如果一个设备正以原始模式使用，则没有功能驱动程序或过滤器驱动程序（没有FDO或过滤程序DO）。此时还有父总线驱动程序的一个PDO和零个或更多总线过滤程序DO。

要获取哪一个驱动程序例程负责产生和附着设备对象的信息，参看第2章。

设备堆栈和一些额外信息构成了一个devnode设备。在一个设备的devnode里，PnP管理器保留诸如是否设备已经启动和哪一个驱动程序，如有，登记通知设备上的改变。内核调试程序！devnode 命令显示了关于一个devnode的信息。
1.3.4 设备对象---一个例子

这一部分描述了由USB硬件的驱动程序产生可能的驱动程序的设备对象来说明PnP设备对象以及它们怎样分层。

图1.8给出了在1.4.2部分里描述的驱动程序例子所产生的设备对象。

图1.8 PnP设备对象分层的例子---USB操纵杆

开始于图1.8的底部，在设备堆栈例子里的设备对象包括：

1. PCI总线的一个PDO和一个FDO

根总线驱动程序枚举内部系统总线（根总线）并为它所发现的每个设备产生一个PDO，这些PDO的其中之一支持PCI总线。（根总线的PDO和FDO没有在图中列出。）

PnP管理器标识PCI驱动程序为PCI总线的功能驱动程序，安装驱动程序（如果还没安装的话），并传递PDO给PCI驱动程序。在它的AddDevice例程里，PCI驱动程序产生PCI总线（IoCreateDevice）的一个FDO并增加FDO到PCI总线的设备堆栈（IoAttachDeviceToDeviceStack）。象PCI总线的功能驱动程序一样，PCI驱动程序产生并附着这个FDO作为它的部分任务。

在该例中，没有PCI总线的过滤器驱动程序。

2. USB主控制器的一个PDO和一个FDO

PnP管理器指示PCI驱动程序来启动它的设备（IRP_MN_START_DEVICE），然后查询它的子代（IRP_MN_QUERY_DEVICE_RELATIONS BusRelations）PCI驱动程序。作为响应，PCI驱动程序枚举其总线上的设备。在这个例子中，PCI驱动程序发现了一个USB主机控制器并产生那个设备的一个PDO。图中的宽箭头表示了USB主机控制器是PCI总线的一个“子代”。PCI驱动程序生成它子设备的PDO，象支持PCI总线的总线驱动程序一样，作为它的部分任务。

PnP管理器标识USB主机控制器微类/类驱动程序对作为USB主机控制器的功能驱动程序并装载该驱动程序对，PnP管理器在适当的时间调用该驱动程序对生成并附着USB主机控制器一个FDO。

在该例中，没有过滤器驱动程序支持USB主机控制器。

3. USB集线器的一个PDO和一个FDO

USB主机控制器枚举它的总线、在单个端口上定位USB集线器，并为集线器生成一个PDO。USB集线器驱动程序生成并且为集线器附加一个FDO。

在该例中，没有USB集线器的过滤器驱动程序。

4. 操纵杆设备的一个PDO、一个FDO和两个过滤程序DO

USB集线器驱动程序枚举它的总线、定位一个HID设备（操纵杆）和生成一个操纵杆PDO。

在该例中，低层过滤器驱动程序已经在注册表中为操纵杆设备而设置，这样，PnP管理器装载过滤器驱动程序。过滤器驱动程序决定相关设备并给设备堆栈生成和附加一个过滤程序DO。

PnP管理器决定了操纵杆设备的功能驱动程序是HID类/微类驱动程序对并装载这些驱动程序。驱动程序对包含一个链接到类驱动程序DLL的一个微类驱动程序；它们一起作为设备的一个功能驱动程序来执行。该类/微类驱动程序对生成一个设备对象、FDO，并把它附加到设备堆栈中。

顶层过滤器驱动程序生成并附加一过滤程序DO到设备堆栈，其方式与低层过滤程序类似。

注意到由父总线驱动程序生成的FDO一直处于一个特定设备的设备堆栈的底部，当驱动程序处理PnP或电源IRP时，它们必须向下通过设备堆栈传递每一个IRP给PDO和其关的总线驱动程序。

图1.9给出了与图1.8相同的设备堆栈，但强调哪一个设备对象由哪一个驱动程序生成和管理。

图1.9从驱动程序角度看到的设备对象层次的例子

一个总线驱动程序跨越多个设备堆栈，一个总线驱动程序为它的总线适配器/控制器生成FDO，并为它的每个子设备生成一个PDO。

第2章 PnP和电源管理必须的驱动程序支持

本章描述了例程和IRP处理，这些在Windows2000和WDM驱动程序里对支持即插即用（PnP）和电源管理是必须的。它覆盖了以下的主题：

2.1 必须的PnP支持的总览

2.2 PnP和电源管理DriverEntry例程

2.3 PnP和电源管理AddDevice例程

2.3.1 编写AddDevice例程的指南

2.4 Dispatch PnP例程

2.5 DispatchPower例程

2.6 PnP和电源管理Unload例程

一些驱动程序通过系统提供的端口或类驱动程序与PnP和电源系统的细节是隔离的,这样的驱动程序无需实现所有的这些机制。举个例子，一个SCSI端口驱动程序屏蔽了一个SCSI微端口驱动程序的许多电源管理和PnP系统的细节，这样一个SCSI微端口驱动程序不需要直接处理电源和PnP IRP。对这些驱动程序，参看驱动程序专用文件来获得必要的PnP支持细节。

参看第一章对PnP和电源管理的概念和术语的介绍。

2.1 必须的PnP支持的总览

一个支持PnP和电源管理的驱动程序必须具有以下功能：

n 一个用来安装驱动程序的INF文件

n 一个.cat(catalog)文件，该文件有驱动程序包的WHQL数字签名

n 一个用于初始化驱动程序的 DriverEntry 例程

n 一个用于初始化设备的AddDevice例程

n 一个DispatchPnP例程，该例程处理PnP操作的IRP，如启动、停止和删除设备

n 一个DispatchPower例程处理电源操作的IRP

n 一个Unload例程删除任何由 DriverEntry 设定的驱动程序专用资源

这些例程在下面的部分中讨论。参见第4部分获得关于INF文件、.cat文件和驱动程序安装的信息。

2.2 PnP和电源管理DriverEntry例程

一个 DriverEntry 例程初始化一个驱动程序，所有的驱动程序必须有一个 DriverEntry 例程。当装载驱动程序时，PnP管理器为每个驱动程序调用一次 DriverEntry。在驱动程序初始化之后，PnP管理器能够调用驱动程序的AddDevice例程来初始化由该驱动程序控制的设备。

DriverEntry 例程定义如下：

NTSTATUS

(*PDRIVER_INITIALIZE) (

IN PDRIVER_OBJECT DriverObject,

IN PUNICODE_STRING RegistryPath

);

只要驱动程序标识了链接程序的初始化例程名，你可以给初始化例程命名 DriverEntry 之外的某个名字。链接程序必须有初始化例程的名字来链接驱动程序的传输地址到OS装载程序里。一个名为 DriverEntry 的驱动程序初始化例程会自动生成该链接。

DriverEntry 例程在一系统线程IRQL PASSIVE_LEVEL的环境里被调用。

一个 DriverEntry 例程能使用注册表来得到它所需要的初始化驱动程序的一些信息， DriverEntry 例程必须在注册表里为另外的驱动程序和/或要用的被保护的子系统设置信息。为获得关于使用注册表的更多信息，请看第4部分。

一个 DriverEntry 例程是可分页的且应该在INIT段以便它将被丢弃。

一个 DriverEntry 例程必须采取以下步骤：

1. 初始化驱动程序调度表

一驱动程序必须为驱动程序AddDevice例程指定入口点、调度（Dispatch）例程、StartIo例程（如有）、Unload例程和其他任何驱动程序入口点。例如，一驱动程序应该用象下面的描述（Xxx是一个标识驱动程序的前缀）的一样，为它的AddDevice、DispatchPnp和DispatchPower例程设置入口点：

:

DriverObject->DriverExtension->AddDevice = XxxAddDevice;

DriverObject->MajorFunction[IRP_MJ_PNP]->AddDevice = XxxDispatchPnp;

DriverObject-> MajorFunction[IRP_MJ_POWER] = XxxDispatchPower;

:

2. 初始化任何其他的全局驱动程序变量和数据结构。

由于一个 DriverEntry 例程运行在一个系统线程IRQL PASSIVE_LEVEL的环境里，只要驱动程序没有控制存有系统分页文件的设备，任何由 ExAllocatePool 分配且专在初始化过程中使用的内存能够从分页的池得到。这样的内存分配必须在 ExFreePool 在 DriverEntry 返回控制之前释放。

3. [可选择的] 在注册表里使用了 Zw Xxx 和 Rtl Xxx 例程来读或者设置独立于设备的值。

4. 保存输入到 DriverEntry 里的RegistryPath参数。

这个参数指向一个被枚举的Unicode信息串，此Unicode信息串指定了驱动程序注册表键的一个路径：\Registry\Machine\System\CurrentControlSet\Services\Drivername。该例程应该保存一个此信息串的拷贝，而不是指针本身。在 DriverEntry 例程返回控制以后，指针不再有效。

5. 返回状态。

如果 DriverEntry 例程返回的信息不是STATUS_SUCCESS，则驱动程序不须保留装载的状态。然而，在初始化失效和返回控制之前，一个 DriverEntry 例程必须做下面的事情：

n 释放由它设置的任何系统资源。

n 记录错误。在内核模式驱动程序设计指南节里看 Error Logging and NTSTATUS Values 来得到更多的信息。

2.3 PnP和电源管理AddDevice例程

在驱动程序的AddDevice例程里，驱动程序创建一个设备对象作为它的I/O设备的目标，并附着设备对象到设备堆栈，设备堆栈包括每一个相关设备驱动程序的一个设备对象。

PnP管理器调用由驱动程序控制的每个设备的一个驱动程序AddDevice例程。在设备被首次枚举时，AddDevice例程在系统初始化时被调用。当系统运行时，任何时候一个新设备被枚举，AddDevice例程被调用。

一个驱动程序的AddDevice例程应该被命名为 Xxx AddDevice，这里 Xxx 是一个标识特定的驱动程序的前缀。在 DriverEntry 过程中，一个驱动程序在 DriverObject_>DriverExtension->AddDevice 里存储了它的AddDevice例程地址：。

在一个系统线程IRQL PASSIVE_LEVEL环境里调用一个AddDevice例程。

AddDevice例程由PnP管理器定义如下：

NTSTATUS

(*PDRIVER_ADD_DEVICE) (

IN PDRIVER_OBJECT DriverObject,

IN PDEVICE_OBJECT PhysicalDeviceObject

);

DriverObject指向代表驱动程序的驱动程序对象，PhysicalDeviceObject指向被添加的 PnP设备的PDO。

在功能或过滤器驱动程序里，一个AddDevice例程应该有以下步骤：

1. 调用 IoCreateDevice 产生一个被添加的设备的设备对象（一个FDO）。

不要指定FDO的一个设备名，命名一个FDO绕过了PnP管理器的安全管理。如果一个用户模式组件需要一个设备的符号链接，须注册一个设备接口（看下一步）。如果一个内核模式组件需要一个早期的设备名，驱动程序必须命名FDO，但并不推荐命名。

在设备特性参数里包括FILE_DEVICE_SECURE_OPEN，这个特性指示I/O管理器来执行安全检查防止设备对象的所有打开请求，包括相对打开和尾部文件名打开。

2. [可选择的]生成一个或多个与设备的符号链接。

调用 IoRegisterDeviceInterface 来注册设备功能并生成一个符号链接，通过该链接应用程序或系统组件能够打开设备。当驱动程序处理IRP_MN_STRAT_DEVICE请求时，驱动程序应该激活接口（看第2部分）。

3. 在设备扩展里存储设备PDO的指针。

PnP管理器给提供一个PDO指针来作为AddDevice的PhysicalDeviceObject参数，驱动程序使用PDO指针调用诸如 IoGetDeviceProperty 的例程。

4. 在设备扩展里定义标志来跟踪设备一定的PnP状态，比如暂停、删除和突然的删除。

例如，定义一个标记说明当设备处于一暂停状态时，到来的IRP应该被保留。如果驱动程序还没有一个列队IRP的机制，将产生一个队列来保留IRP。为更多信息，请看第2部分。

也在设备扩展里分配一个IO_REMOVE_LOCK结构并调用 IoInitializeRemoveLock 来初始化该结构。

5. 如果需要，设置电源管理的DO_POWER_INRUSH或DO_POWER_PAGABLE标记，可分页的驱动程序必须设置DO_POWER_PAGABLE标记。当设备对象产生设备PDO时，设备对象标记典型地由总线驱动程序设置。然而，当高层的驱动程序产生PDO时，高层的驱动程序有时需要改变它们AddDevice例程里的这些标记值。看第1章第3部分的电源管理设置设备对象标记来获取细节。

6. 产生和/或者初始化任何其他的软件资源，驱动程序使用这些软件资源来管理这个设备，比如事件、自旋锁或者其他的对象。（硬件源，比如I/O端口，在稍后配置来响应一个IRP_MN_START_DEVICE请求。）

由于一个AddDevice例程在一个系统线程IRQL PASSIVE_LEVEL环境里运行，所以只要驱动程序没有控制保留有系统分页文件的设备，任何由 ExAllocatePool 分配、且专门在初始化过程中使用的内存从分页池得到。这样的内存分配必须由 ExFreePool AddDevice在返回控制之前释放。

7. 附着设备对象给设备堆栈（IoAttavhDeviceToDeviceStack）。

在TargetDevice参数里给设备的PDO指定一个指针。

存储由 IoAttavhDeviceToDeviceStack 返回的指针，当向下传递到设备堆栈时，这个指针是 IoCallDriver 和 PoCallDriver 的一个必要参数，该指针指向紧邻的低层的支持驱动程序的设备对象。

8. 用一个如下的状态，在FDO或者过滤程序DO里清除DO_DEVICE_INITIALATION标记：FunctionalDeviceObject->Flags &=~DO_DEVICE_INITIALIZING；

9. 准备处理设备（比如IRP_MN_QUERY_RESOURCE_REQUIREMENTS和IRP_MN_STRAT_DEVICE）的PnP IRP。

直到驱动程序接收到一个IRP_MN_STRAT_DEVICE时，它保留由PnP管理器分配给设备的硬件资源列表，才可以启动控制设备。

一个PnP总线驱动程序有一个AddDevice例程，但是，当总线驱动程序用作它的控制器和适配器的功能驱动程序时，该PnP总线驱动程序才被调用。例如，PnP管理器调用USB集线器总线驱动程序的AddDevice例程来添加集线器设备，集线器驱动程序的AddDevice例程不会为一个子集线器调用（一个插入集线器的设备）。
2.3.1 编写AddDevice例程的指南

当编写一个AddDevice例程时，考虑下面的设计指南：

n 如果一个过滤器驱动程序决定它的AddDevice例程调用一个它无须服务的设备时，过滤器驱动程序必须返回STATUS_SUCCESS来允许剩余的设堆栈为设备而被装入。过滤器驱动程序没有生成一个设备对象，也不附着它到设备堆栈；过滤器驱动程序只是返回成功并允许其余的驱动程序添加到堆栈。

n 通常在一个设备对象的设备扩展里，对于它所使用的任何内核定义的对象和执行程序的自旋锁，一个驱动程序也必须提供特定对象的指针存储，这个对象从I/O管理器或其他的系统组件得到。

你也许决定分配另外的系统空间内存来满足驱动程序的需要，比如，长期缓冲区或一个辅助列表。如果是这样的话，一个AddDevice例程能够调用下面的例程：

n 分页或无分页系统空间内存的 ExAllocatePool

n ExInitializePagedLooksideList 或 ExInitializeNPagedLooksideList 来初始化一个分页或无分页的辅助列表

n 如果驱动程序有一个专为设备服务的线程或在任何内核定义的调度程序对象上等待时，它的AddDevice例程必须初始化调度程序对象。该调度程序对象的线程或驱动程序调用适当的 KeInitialize Xxx 支持例程，该支持例程有一个事件指针、信号灯、互斥体和/或定时器等驱动程序提供存储的对象。

由于它在一系统线程的环境里执行，一个AddDevice例程本身能够在一个调度程序对象上等待的一个非零间隔，只要调度程序对象在等待开始之前已经被初始化。

参见内核模式驱动程序设计指南第3章的内核调度程序对象来获得更多信息。

n 如果驱动程序使用任何执行程序的自旋锁或为一个中断自旋锁提供存储，则AddDevice例程必须在传递它到任何别的支持例程之前，用每个这样的自旋锁调用 KeInitializeSpinLock。看内核模式驱动程序设计指南第16章的自旋锁使用来获得更多信息。

n 当调用 IoCreateDevice 时，加强文件打开安全。

在调用 IoCreateDevice 时指定FILE_DEVICE_SECURE_OPEN特性，Windows2000和 Windows NT 4.0 SP5支持这个特性。它指令I/O管理器为所有打开的请求执行设备对象的安全检查。如果FILE_DEVICE_SECURE_OPEN特性没有在设备的类安装器INF或设备的INF上设置，并且驱动程序不执行它们自己打开请求的安全检查，在调用 IoCreateDevice 时厂商应该指定这个标记。

在调用 IoCreateDevice 时，如果一驱动程序设置了FILE_DEVICE_SECURE_OPEN标记，I/O管理器应用设备对象的安全描述符到任何相对的打开或尾部文件名打开。例如，如果FILE_DEVICE_SECURE_OPEN为\Device\foo设置，并且如果\Device\foo仅能够由管理员打开，那么，\Device\foo\abc也能够由管理员打开。然而，I/O管理器阻止一正常用户打开\Device\foo和\Device\foo\abc。

如果一个设备的驱动程序设置了FILE_DEVICE_SECURE_OPEN。PnP管理器传送它给设备的所有设备对象。

2.4 DispatchPnP例程

PnP管理器使用IRP来指导驱动程序启动、停止和删除设备并查询驱动程序的设备，所有的PnP IRP有主要的功能代码IRP_MJ_PNP。

每个PnP驱动程序对处理特定的IRP是必要的，并能够选择地处理其余的IRP。参见《Windows2000驱动程序开发参考》的卷一，来获得关于每种驱动程序（功能驱动程序、过滤器驱动程序和总线驱动程序）哪一个IRP是必要的，哪一个IRP是可选择的。

驱动程序应该在一 Xxx DispatchPnp例程里处理PnP IRP，这里 Xxx 是一个用来标识驱动程序的前缀。在驱动程序初始化它的 DriverEntry 例程过程中，驱动程序在 DriverObject->MajorFunction[IRP_MJ_PNP]里设置它的DispatchPnp例程的地址。通过I/O管理器，PnP管理器调用一个驱动程序的DispatchPnp例程。

支持一个设备的所有驱动程序必须有机会处理设备的PnP IRP，除过在极个别情况下。（即一个功能或过滤器驱动程序允许IRP失效。）阅读第2部分获得关于PnP IRP的信息，包括何时PnP IRP被送出和它们的处理规则。

2.5 DispatchPower例程

电源管理器使用IRP指示驱动程序来改变电源状态，来等待并响应系统唤醒事件，和查询驱动程序的设备。所有的电源IRP有主功能代码IRP_MJ_POWER。

驱动程序应该在一个 Xxx DispatchPower例程里处理电源 IRP，这里 Xxx 是一个用来标识驱动程序的前缀。在 DriverObject->MajorFunction[IRP_MJ_POWER]里，DriverEntry 例程设置驱动程序的DispatchPower例程的地址。当一个电源IRP被送出时，PnP管理器调用驱动程序的DispatchPower例程。

DispatchPower例程应该处理仅有主功能代码IRP_MJ_POWER的IRP，该例程执行下面的任务：

n 如果可能，处理IRP

n 使用 PoCallDriver，传递IRP给设备堆栈里下一个的低层驱动程序

n 如果是一个总线驱动程序，则执行设备上的电源操作请求和实现IRP

一个设备的所有驱动程序必须有机会处理设备的电源IRP，除过在极个别情况下。（即一个功能或过滤器驱动程序被允许使IRP失效。）大多数功能和过滤器驱动程序要么执行一些处理，要么为每个电源IRP设置一个IoCompletion例程，然后，向下传递IRP到下一个的低层驱动程序而不实现它。最后IRP到达总线驱动程序，如果需要，它物理地改变设备的电源状态并完成IRP。

在完成IRP之后，当IRP向下传递到设备堆栈时，I/O管理器调用任何由驱动程序设置的IoCompletion例程。是否一个驱动程序需要设置一个完成例程取决于IRP类型和单个的驱动程序需求。

加电一个设备的电源IRP必须首先由设备堆栈（基础总线驱动程序）里的最低层驱动程序处理，然后连续地被上面的堆栈里的每个驱动程序处理。断电一设备的电源IRP必须首先被顶部设备堆栈里的第一个驱动程序处理，然后连续地被下面的堆栈里的每个驱动程序处理。

可删除的设备的特别处理

在它们的DispatchPower例程里，可删除的设备的驱动程序应该检查是否设备仍旧存在。如果设备已被删除，驱动程序不应该向下传递IRP到下一个低层驱动程序，相反，驱动程序应该作下面的工作：

n 调用 PoStartNextPowerIrp 来开始处理下一个电源IRP

n 设置 Irp->IoStatus.Status 给STATUS_DELETE_PENDING

n 调用 IoCompleteRequest，指定IO_NO_INCREMENT，完成IRP

n 返回STATUS_DELETE_PENDING

2.6 PnP和电源管理Unload例程

一个PnP驱动程序必须有一个Unload例程来删除任何驱动程序专用的资源，如内存、线程和事件等，这些由 DriverEntry 例程产生。如果没有驱动程序专用的资源可以删除，驱动程序必须仍就有一个Unload例程，但是它能够简单地返回。

在所有的设备已被删除之后，可在任何时候调用驱动程序的Unload例程，PnP管理器在系统线程IRQL PASSIVE_LEVEL的环境里调用一个驱动程序的Unload例程。


理解PnP

本指南的第一部分包含一个即插即用（PnP）和电源管理的总览，并介绍了这两种技术中通用的许多术语和概念。本章包含另外的PnP背景、指南和操作信息来帮助驱动程序编写者理解PnP子系统。你必须遵照在此指南描述的和相随的参考手册中的PnP规则；本章给你理解这些规则的参考框架。

在阅读本章之前，你应该明白第一部分中描述的术语和概念（如功能驱动程序、总线驱动程序、FDO、PDO、过滤程序DO、设备堆栈、设备树、devnode和设备父/子关系）。

本章包括下面的部分：

1.1 PnP驱动程序设计指南

1.2 PnP和设备树

1.3 PnP设备状态

1.4 动态添加一个新的PnP设备

1.5 硬件资源

1.6 使用驱动程序里的GUID

1.1 PnP驱动程序设计指南

如第一部分所描述的，PnP提供：

n 自动和动态识别已安装的硬件

n 硬件资源的分配（和再分配）

n 适当的驱动程序安装

n 和PnP系统相互作用的驱动程序的接口

n 驱动程序和应用程序了解硬件环境的改变的机制

为了支持PnP，驱动程序必须遵循下面的指南：

1. 必须遵循以设备为中心的PnP驱动程序模型。

PnP管理器作为一个整体管理系统上的设备，并调用驱动程序服务于这些单个的设备。例如，当驱动程序被装载时，一个PnP驱动程序不应该查找将要服务的潜在的设备。相反，当PnP管理器定位一个将要服务的驱动程序设备时，调用它的AddDevice例程。

PnP管理器为驱动程序将要服务的每个设备调用驱动程序的AddDevice例程，该驱动程序继续服务设备，直到PnP管理器指令它停止服务这个设备并删除它用于设备的数据结构。

这个以设备为中心的模型能够让PnP管理器支持PnP机器上设备的动态到达和出发。

2. 必须遵循该文件里所描述的PnP规则。

PnP硬件必须遵照PnP硬件标准，PnP驱动程序必须遵守PnP软件规则。例如，该文件描述了PnP驱动程序需要的例程和处理PnP IRP的规则。

通过系统提供的端口或类驱动程序，一些驱动程序与PnP细节和电源系统的细节分开，这样的驱动程序不需要实现所有的这些机制。例如，从电源和PnP系统的许多细节，一个SCSI端口驱动程序隔离一个SCSI微端口驱动程序，这样，一个SCSI微端口驱动程序无需直接处理电源和PnP IRP。对这样的驱动程序，看驱动程序专用的文件来获得必要的PnP支持的细节。

3. 无需直接声明硬件资源。

作为代替，一个PnP驱动程序应该给PnP管理器报告一个设备的资源需求，硬件设备资源包括IRQ、I/O端口、DMA通道和设备内存区域，PnP管理器比较设备的资源需求与系统上其他设备需求。PnP管理器声明设备的资源并通知这些源给设备的驱动程序，它在IRP_MN_START_DEVICE IRP里为设备传达信息。

4. 必须是一个模块以便PnP管理器能在需要时调用它的例程。

一个驱动程序例程，如它的 DriverEntry 添加例程和调度例程必须编写以便它们能够在需要时被PnP管理器调用，你不能够假设何时驱动程序例程在怎样的环境里被调用，除过被文档列出的。

5. 应该是灵活的。

举例来说，如果一个设备能够在多于一套硬件资源上操作，驱动程序应该报告PnP管理器所有可能的硬件资源配置，以便PnP管理器在机器上配置设备时能够有最大的灵活性。

1.2 PnP和设备树

如第一部分中所描述的，PnP管理器维护一个叫设备树的信息集来记录机器上的设备踪迹。在系统引导过程中PnP管理器构建此树；当设备被添加或删除时，PnP管理器更新此树。对机器上每个物理的、逻辑的和虚拟的设备来说，设备树包含一个设备节点（devnode）。

设备树是许多PnP管理器的操作系统的中心，你应该知道关于设备树的下面的内容：

n 设备树是分层次的。

一个总线上的设备代表总线适配器或控制器的子代。一个总线设备是其他物理的、逻辑的或虚拟的设备能够附着的任何设备，设备树有它所需要的表示机器上所有设备的足够多的分级。

当PnP管理器管理系统设备时，它使用设备树上层次的信息。例如，当一用户请求删除一个设备时，PnP管理器标识所有的设备子代，由于它们必须一同被删除。

n 设备树由PnP管理器与PnP驱动程序共同构建。

PnP管理器构建并维护设备树，但是，它从驱动程序收集关于设备的信息。例如，PnP

管理器为它的子设备列表询问总线驱动程序，它的子设备使用了一个P_MN_QUERY_DEVICE_RELATIONS IRP。

n 按照工业标准枚举设备。

当PnP管理器询问它总线上设备一个总线驱动程序时，总线驱动程序依照它的总线协议决定它的子代列表。例如，ACPI驱动程序寻找ACPI命名空间，PCI驱动程序查询PCI config空间，USB集线器驱动程序遵守USB总线协议。

n 除过一个总线设备在任何它的子设备之前被配置，驱动程序编写者不能够作任何关于设备树以何种次序被构建的假设。

例如，驱动程序编写者不应该假设总线上的一个设备在其上的另一个设备之前配置。

n 设备树是动态的。

当设备被添加到和删除机器时，PnP管理器（与驱动程序一同）维护了系统上设备的一个当前视图。

1.3 PnP设备状态

在一个PnP系统上，当一个设备被配置、启动、可能的停止来重新平衡资源和可能的删除时，设备在多种PnP状态之间转换。本部分提供了PnP设备状态的高级总览，PnP设备状态提供了驱动程序必须许多的PnP支持的一个路标，本文件的其余部分详细描述了每个状态的转换。

图1.1给出了设备的PnP状态和设备怎样从一状态转换到另一状态。

开始于图1.1的左上端，一个PnP设备物理地存在于系统中，由于或者用户刚好插入设备，或者设备在引导时存在。系统软件还不知道设备。

为了开始设备的软件配置，PnP管理器和父总线驱动程序枚举设备。PnP管理器，可能带有用户模式组件的帮助，标识设备的驱动程序，包括功能驱动程序和任何可选择的过滤器驱动程序。如果还没有装载驱动程序，PnP管理器调用每个驱动程序的 DriverEntry 例程。看第1部分了解关于它的 DriverEntry 例程里初始化一个PnP驱动程序的信息 。看1.4部分获得更多关于报告和枚举一个PnP设备的信息。

一旦一个驱动程序被初始化，它必须准备初始化它的设备，PnP管理器调用驱动程序所控制的每个设备的一个驱动程序的AddDevice例程。参看第1部分关于一个驱动程序的AddDevice例程的信息。

当一个驱动程序从PnP管理器接受到一IRP_MN_START_DEVICE时，驱动程序启动了设备并准备处理设备的I/O请求。看3.1部分获得处理一个IRP_MN_START_DEVICE请求的信息。

图1.1从PnP的角度所看到的设备状态

如果PnP管理器必须重新配置一个现用设备的硬件资源时，它给设备的驱动程序发出一个停止IRP的集合，指令它们临时停止设备。在它重新配置了硬件资源之后，PnP管理器通过发出一个IRP_MN_START_DEVICE请求指令驱动程序重新启动设备，一个引导配置设备的驱动程序能够在设备启动之前接受到IRP_MN_QUERY_STOP_DEVICE和IRP_MN_STOP_DEVICE请求。（这一步在图1.1中没有给出。）看3.2部分获得处理停止IRP的信息。

当一个PnP设备从系统中物理地删除，或者已被删除时，PnP管理器给设备驱动程序发出各种删除IRP指令，命令它们删除设备的软件表示（设备对象等等）。看3.3部分获得处理删除IRP的信息。

在一个驱动程序所有的设备已被删除之后，PnP管理器卸载该驱动程序。（卸载步骤在图1.1中没有给出）看第一部分获得Unload例程的信息。

1.4 动态地添加一个新的PnP设备

这部分描述了当系统配置一个用户已添加到一运行的机器上的PnP设备时，所发生的一系列事件。这个讨论突出了在枚举和配置一新设备时，PnP管理器、总线驱动程序、功能和过滤器驱动程序的任务。

这个讨论的大部分也与配置一个PnP设备有关，在机器引导时该PnP设备已经存在。特别地，在它们的INF里，标记为SERVICE_DEMAND_START的设备的驱动程序以一种基本相同的方式配置，而不管该设备是否被动态添加或者在引导时已经存在。

图1.2给出了配置设备第一步，从用户给机器插入硬件时开始。

图1.2枚举和报告一个PnP设备

下面的注解对应于图1.2中的被圈起来的数字：

1. 用户插入一个PnP设备到PnP总线上的一个空槽。

在这个例子中，用户插入一个PnP USB操纵杆到一个USB主控制器上的集线器，由于USB集线器有子设备与其联接，所以USB集线器是一个PnP总线设备。

2. 总线设备的功能驱动程序决定一新设备在其总线上。

驱动程序怎么决定这种对总线结构的依赖呢？对一些总线，总线功能驱动程序接受到一个新设备的热插拔通知，如果总线不支持热插拔通知，用户必须在对话框或小应用程序里采取适当的动作以便枚举总线。

在该例中，USB总线支持热插拔通知，这样，支持USB总线的功能驱动程序被通知其子设备已经改变。

3. 总线设备的功能驱动程序通知PnP管理器它的一套子设备已被改变。

功能驱动程序通过调用具有一个 BusRelations 类型的 IoInvalidateDeviceRelations 来通知PnP管理器。

4. PnP管理器查询总线驱动程序当前总线上的设备列表。

PnP管理器发送一个IRP_MN_QUERY_DEVICE_RELATIONS IRP到总线的设备堆栈，Parameters.QueryDeviceRelations.Type 是 BusRelations，表明PnP管理器正请求存在于总线上设备的当前列表。

PnP管理器发送IRP 到总线的设备堆栈里的顶端驱动程序，按照PnP IRP的规则， 堆栈里的每个驱动程序处理IRP，如果合适，则向下传递IRP到紧邻的驱动程序。

5. 总线设备的功能驱动程序处理IRP。

参见《Windows2000驱动程序开发参考》卷一参考页IRP_MN_QUERY_DEVICE_RELATIONS来获得关于处理IRP的详细信息。

在此例中，USB集线器驱动程序处理集线器FDO的这个IRP，集线器驱动程序为操纵杆设备生成一个PDO，并在它随IRP返回的子设备列表里包括一个操纵杆PDO的引用指针。

当USB集线器的父总线驱动程序（USB主控制器类/微类驱动程序对）完成IRP，IRP经由任何被集线器驱动程序注册的完成例程返回设备堆栈。

注意到通过请求PnP管理器查询它的子设备的列表，总线功能驱动程序报告其子列表的一个变化，结果的IRP_MN_QUERY_DEVICE_RELATIONS IRP被总线设备的所有驱动程序所发现。典型地，总线功能驱动程序是处理IRP和报告子设备的唯一驱动程序。在一些设备堆栈里，一个总线过滤器驱动程序存在并参与构建 BusRelations 列表。一个例子是ACPI，其作为一个ACPI设备的总线过滤器驱动程序来附着。在一些设备堆栈里，非总线过滤器驱动程序处理IRP_MN_QUERY_DEVICE_RELATIONS IRP，但这不是典型的处理。

在这一点上，PnP管理器有总线上设备的当前列表，然后，PnP管理器判断是否设备是新近到达的或已经被删除。在本例中，有一个新设备，图1.3给出了PnP管理器为新设备生成一个devnode并开始配置设备。

图1.3为一个新的PnP 设备生成一个devnode

下面的注释对应于图1.3中的被圈起来的数字：

6. PnP管理器为总线上任何新的子设备生成devnode。

PnP管理器比较在IRP_MN_QUERY_DEVICE_RELATIONS IRP里返回的 BusRelations 列表与当前记录在PnP 设备树上的子设备列表，该总线的PnP管理器为每个新设备生成一个devnode并为任何已被删除的设备初始化删除处理。

在本例中，有一个新设备（操纵杆），于是PnP管理器为操纵杆生成一个devnode。在这一点上，为操纵杆配置的唯一的驱动程序是父USB集线器总线驱动程序，它生成一个操纵杆PDO。任何可选择的总线过滤器驱动程序也应该存在于设备堆栈里，但为了简单，本例忽略了总线过滤器驱动程序。

如第1部分图中所介绍的，图1.3中两个devnode之间的宽箭头表示了操纵杆devnode是USB集线器的devnode的子代。

7. PnP管理器收集新设备的信息并开始配置设备。

PnP管理器给设备堆栈发送一系列的IRP来收集关于设备的信息，在这一点上，设备堆栈包括由设备父总线驱动程序生成的唯一的PDO和任何可选择的总线过滤器驱动程序的过滤程序DO。于是，总线驱动程序和总线过滤器驱动程序是响应这些IRP唯一的驱动程序。此例中，操纵杆设备堆栈里唯一的驱动程序是父总线驱动程序，即USB集线器驱动程序。

PnP管理器通过给设备堆栈发送IRP来收集有关新设备的信息，这些IRP包括下面的内容：

n IRP_MN_QUERY_ID，下面的每个设备ID分别有一个IRP：

BusQueryDeviceID

BusQueryInstanceID

BusQueryHardwareIDs

BusQueryCompatibleIDs

n IRP_MN_QUERY_CAPABILITIES

n IRP_MN_QUERY_DEVICE_TEXT，下面的每个条目分别有一个IRP：

DeviceTextDescription

DeviceTextLocationInformation

n IRP_MN_QUERY_BUS_INFORMATION

n IRP_MN_QUERY_RESOURCES

n IRP_MN_QUERY_RESOURCE_REQUIREMENTS

为获得处理这些IRP的信息，参看《Windows2000驱动程序开发参考》的卷一参考页中的IRP。

在处理一个新的PnP 设备过程中，PnP管理器发送上面列出的IRP，但不需要按照所列的次序，于是，你不应该作IRP以怎样的次序被发送的假设。而且，你不应该假设PnP管理器仅仅发送上面列出的IRP。

PnP管理器检查注册表来决定是否设备以前已被安装到机器上。PnP管理器为在 Enum 分支下的设备检查一个<enumerator>\<deviceID>的子键。在本例中，设备是新的并必须“从头开始”配置。

8. PnP管理器在注册表里存储关于设备的信息。

注册表 Enum 分支为操作系统组件的使用而维护，并且，其布局易于改变。驱动程序编写者必须使用API系统来释放驱动程序相关的信息，不要直接从一个驱动程序访问 Enum 分支。下面的 Enum 信息仅为调试的目的而列出。

n PnP管理器为键下的设备生成一个子键来支持设备的计数器。

PnP管理器生成一个名叫 HKLM\System\CurrentControlSet\Enum<enumerator>\ <deviceID>的子键，如果它事先并不存在，它生成<enumerator>子键。

一个计数器是一个组件，该组件发现基于PnP 硬件标准的PnP设备。在微软的Windows2000上，一个计数器的任务由PnP总线驱动程序与PnP管理器共同执行。一设备典型地被它的父总线驱动程序枚举，如PCI或PCMCIA。一些设备由总线过滤器驱动程序枚举，如ACPI。

n PnP管理器为该设备的这个实例生成一个子键。

如果 Capabilities.UniqueID 在IRP_MN_QUERY_CAPABILITIES里返回TRUE，设备的单个ID在系统中是独一无二的。如果没有，PnP管理器修改ID以便它在整个系统里是独一无二的。

PnP管理器生成一个名叫 HKLM\System\CurrentControlSet\Enum<enumerator>\ <deviceID>\<instanceID>的子键。

n PnP管理器写关于设备的信息到设备实例的子键。

如果提供PnP管理器给设备，它存储包括下面的信息：

DeviceDesc---从 IRP_MN_QUERY_DEVICE_TEXT而来

Location--- 从IRP_MN_QUERY_DEVICE_TEXT而来

Capabilities--- 从IRP_MN_QUERY_CAPABILITIES而来的标记

UINumber--- 从IRP_MN_QUERY_CAPABILITIES而来

HardwareID--- 从IRP_MN_QUERY_ID而来

CompatibleIDs--- 从IRP_MN_QUERY_ID而来

LogConf\BootConfig--- 从IRP_MN_QUERY_RESOURCES而来

LogConf\BasicConfigVector--- 从IRP_MN_QUERY_RESOURCE_REQUIREMENTS而来

不要在一个驱动程序里直接访问这些注册表键！它们被列出仅出于调试的目的。

在内核模式里，你用 IoGetDeviceProperty 例程或IRP_MN_QUERY_CAPABILITIES IRP访问这个信息。在用户模式里，你通过 SetupDi Xxx 函数（设备安装程序函数）或 CM Xxx 函数（配置管理器函数）访问这个信息。

在这一点上，PnP管理器准备为设备定位功能驱动程序和过滤器驱动程序，如果有的话。（看图1.4。）

图1.4寻找功能和过滤器驱动程序

下面的注释对应于图1.4中被圈起来的数字：

9. 内核模式PnP管理器与用户模式PnP管理器协同一致，也与用户模式安装组件协同一致来发现设备的功能和过滤器驱动程序（如果有的话）。

内核模式PnP管理器排队用户模式PnP管理器的一个事件，并标识一个需要安装的设备。一旦一个有特权的用户登陆，用户模式组件处理发现的驱动程序。参看第4部分第1章获得安装组件和在安装设备时它们的角色信息。

10. 用户模式安装组件指示内核模式PnP管理器来装载功能和过滤器驱动程序。

用户模式组件回调内核模式来使驱动程序装载，并调用驱动程序AddDevice例程。

图1.5 给出了PnP管理器装载驱动程序（如果合适）、调用它们的AddDevice例程和指示驱动程序启动设备。

图1.5调用AddDevice例程并启动新设备。

下面的注释对应于图1.5中的被圈起来的数字：

11. 低层过滤器驱动程序

在功能驱动程序附着到设备堆栈之前，PnP管理器处理任何任何低层过滤器驱动程序。对每个低层过滤器驱动程序而言，如果驱动程序还没装载，PnP管理器调用驱动程序的 DriverEntry 例程。然后，PnP管理器调用驱动程序的AddDevice例程。在它的AddDevice例程里，过滤器驱动程序生成一个过滤程序DO并附着它到设备堆栈（IoAttachDeviceToDeviceStack），一旦它附着它的设备对象到设备堆栈，此驱动程序能够作为此设备的驱动程序。

在USB操纵杆例子中，有设备的一个低层过滤器驱动程序。

12. 功能驱动程序

在任何低层的过滤器驱动程序被附着后，PnP管理器处理功能驱动程序。如果驱动程序还没装载，PnP管理器调用功能驱动程序的 DriverEntry 例程并调用功能驱动程序的AddDevice例程。功能驱动程序生成一个FDO并附着它到设备堆栈。

在本例中，USB操纵杆的功能驱动程序实际上是一对驱动程序：HID类驱动程序和HID微类驱动程序。这两个驱动程序作为功能驱动程序一起工作，该驱动程序对仅生成一个FDO并附着它到设备堆栈。

13. 顶层过滤器驱动程序

在功能驱动程序被附着后，PnP管理器处理任何顶层过滤器驱动程序。

在本例中，有设备的一个顶层过滤器驱动程序。

14. 分配资源和启动设备

PnP管理器给设备分配资源，如果需要，发布一个IRP来启动设备。

n 分配资源

在配置进程的早期，PnP管理器为从设备的父总线驱动程序而来的设备收集硬件资源需求。在全套的驱动程序为设备而装载后，PnP管理器发送一个IRP_MN_FILTER_RESOURCE_ REQUIREMENTS IRP到设备堆栈。在堆栈里的所有驱动程序有机会处理这个IRP并修改设备的资源需求列表，如果需要的话。

如果设备需要，基于设备的需求和当前可用的资源，PnP管理器分配资源到设备。

PnP管理器也许需要重新安排现存的设备的资源分配来满足新设备的需要，这个资源的再分配被叫作“重新平衡”。 在一个重新平衡过程中，现存的设备的驱动程序收到一系列停止和启动IRP，但是，重新平衡对用户必须是透明的。

在这个USB操纵杆的例子中，USB设备不需要硬件资源，于是，PnP管理器设置资源列表为NULL。

n 启动设备（IRP_MN_START_DEVICE）

一旦PnP管理器分配资源给设备，它发送一个IRP_MN_START_DEVICE IRP到设备堆栈来指令驱动程序启动设备。为了获得关于启动设备的详细信息，参见本指南的第3章，并参见《Windows2000驱动程序开发参考》卷一参考页的IRP_MN_START_DEVICE。

在设备启动之后，PnP管理器发送三个另外的IRP到设备的驱动程序：

n IRP_MN_QUERY_CAPABILITIES

在启动IRP成功完成之后，PnP管理器发送另外的IRP_MN_QUERY_CAPABILITIES IRP到设备堆栈。设备的所有的驱动程序都有处理IRP的选项，在当所有的驱动程序被附着且设备启动之后，由于功能和过滤器驱动程序应该需要访问设备来收集能力信息，所以在这个时候，PnP管理器发送这个IRP。

n IRP_MN_QUERY_PNP_DEVICE_STATE

举个例子，这个IRP给一个驱动程序机会，来报告设备不应该在用户界面里显示，例如设备管理器和热插拔小程序。这对存在于系统但在当前配置中不能使用的设备而言是非常有用的，例如一个便携机的游戏端口，当便携机计算机未连接时，该便携计算机是不可用的。

n BusRelations 的IRP_MN_QUERY_DEVICE_RELATIONS

PnP管理器发送这个IRP来决定是否该设备有任何子设备，如果有的话，PnP管理器配置每个子设备。

1.5 硬件资源

硬件资源是可分配的、有地址的总线路径，该路径允许外设和系统处理器相互通信。硬件资源典型地包括I/O端口地址、中断向量和总线相关的内存地址块。在系统能够与一个设备实例通信之前，PnP管理器必须给设备实例分配硬件资源，该设备实例基于哪些资源可用和那些设备实例能够使用的信息。资源被分配给设备树上的（假设代表的设备需要资源且这些资源可用）每一个设备节点，PnP管理器使用列表来跟踪和每个设备节点相关联的硬件资源，它使用两种类型的列表：

n 资源需求列表

设备被典型地设计以便在资源分配的范围内操作。举例来说，一个设备可能仅需要一个中断向量，但是，它能够使用向量范围内的任何一个。对每个设备实例来说，PnP管理器维护一个资源需求列表，这个资源需求列表指定了设备可在其内操作的硬件资源的所有范围。列表的名字起源于以下的事实：即当分配它们给设备时，PnP管理器必须从这个列表选择资源。

内核模式代码使用结构IO_RESOURCE_REQUIREMENTS_LIST（或者当输入到系统例程时，或者响应IRP时）指定了资源需求列表。当输入到PnP配置管理器功能时，通过使用PnP配置管理器结构，用户模式代码指定了资源需求列表。

n 资源列表

当PnP管理器给一设备分配资源时，它通过生成一个已分配给每个设备实例的列表，来跟踪这些分配。这些列表可被称作资源分配列表，但是，那个名字典型地被缩写为资源列表。当设备被从系统添加或删除时，PnP管理器能够改变资源列表项并且资源在随后被再分配。（资源也能够被一个PnP BIOS所分配，并且，安装软件---使用INF文件或者用户输入---能够迫使PnP管理器给一个设备指定特定的资源。）

通过使用结构CM_RESOURCE_LIST（或者当输入到系统例程时，或者响应IRP时），内核模式代码指定了资源列表。当输入到PnP配置管理器功能时，用户模式代码指定了使用PnP配置管理器结构的资源需求列表。

PnP管理器在注册表里存储资源需求列表和资源列表，使用REGEDT32可察看它们。驱动程序能够通过即插即用例程和即插即用的IRP而不直接地访问这些列表，用户模式应用程序能够使用PnP配置管理器功能。（由于存储格式在将来的版本中易于改变，驱动程序和应用程序不应该直接地访问这些使用注册表功能的列表。）
1.5.1 逻辑配置

资源需求列表和资源列表被用来说明逻辑配置，每一个逻辑配置标识了或者可接受的资源范围或者特定设备实例的一套特定资源，且包含或者单个的资源需求列表或者单个的资源列表。此外，每个支持设备实例的逻辑配置属于一个逻辑配置类型，配置类型在下表里列出，几个有相同或不同类型的逻辑配置可能被指定到每个设备实例。
1.5.1.1 支持资源需求列表的逻辑配置类型

基本配置

一个资源需求列表标识了由即插即用的设备提供的资源范围，当一个驱动程序接受到IRP_MN_QUERY_RESOURCE_REQUIREMENTS IRP时，驱动程序应该返回这个列表。（非PnP设备的基本配置能够在一个INF文件里被描述，这时，设备安装软件读INF文件并调用PnP配置管理器功能来生成一个需求列表。）

过滤配置

一个已提供给驱动程序设备堆栈的资源需求列表可能地被修改，且随后被驱动程序堆栈返回，来响应IRP_MN_FILTER_RESOURCE_REQUIREMENTS IRP。PnP管理器使用结果的过滤程序配置作为分配资源的基础。

重载配置

一个资源需求列表重载了基本的配置，典型地，如果设备的INF文件包含一个带有扩展的 LogConfigOverride 的 DDInstall 节，则一个设备安装程序生成一个重载配置。（在《Windows 2000驱动程序开发参考》的卷一里参看 INF LogConfig Directive。）如果它的设备物理地从系统中删除，一个重载配置不被删除。
1.5.1.2 资源列表的逻辑配置类型

引导配置

当系统被引导时，一个资源列表标识了分配给设备实例的资源。（对PnP设备，这是由BIOS提供的配置；对非PnP设备，这些资源应该被卡上的跳线所选择。）当一个驱动程序接受到IRP_MN_QUERY_RESOURCES IRP时，它应该返回这个资源列表。（如果BIOS不能够决定一个设备使用的所有资源时，一个引导配置能够部分的空缺。）如果一个设备被删除或重新启动，PnP管理器能够修改这个列表。对非PnP设备，这个配置类型能够代替一个强迫配置，在这种情况下，它比等价的强迫配置有较低的配置优先级，对每个设备实例来说，只有一个引导配置能够存在。

强迫配置

一个资源列表标识了一个设备实例必须使用的资源，一个强迫配置阻止PnP管理器指定另外的资源给设备实例。一个设备安装程序可能生成一个强迫配置，这种配置基于或者包含在一个INF里或者从用户得到的信息。如果强迫配置的设备物理地从系统中删除，强迫配置不被删除。对每个设备实例来说，只有一个强迫配置存在。

分配的配置

一个资源列表标识了设备实例当前在用的资源。仅有一个分配的配置能够为每一个设备实例而存在。

设备驱动程序负责决定一个PnP兼容的设备的基本配置、过滤配置和引导配置，并返回信息来响应由PnP管理器发送的IRP。（为了更多的信息，请看第2 章。）驱动程序安装软件能够生成重载配置、强迫配置和对非PnP设备的引导配置。PnP管理器维护每个设备实例的分配配置。

当每个配置生成时，一个优先权指定给每个配置，如果PnP管理器发现一个设备实例已被指定了几个同类的逻辑配置，它尽量使用具有最高优先级的一个。如果那个配置导致资源冲突，它尝试具有次优先级的配置。（欲知配置优先权的列表，参看《Windows2000驱动程序开发参考》的卷一中的 Cm_Add_Empty_Log_Conf。）

1.6 在驱动程序里使用GUID

驱动程序和其他系统组件使用GUID（globally unique identifiers）来标识不同的条目，系统组件为诸如设备安装类、PnP事件、WMI事件和静止图象事件等条目定义GUID。你能够定义你自己一些条目的GUID，如设备接口类、定制PnP事件和定制WMI事件。驱动程序和应用程序包含头文件，这些头文件定义了它们使用的GUID。

本节描述了下面的内容：

1.1.1 定义和导出新的GUID

1.1.2 在驱动程序代码里包含GUID

参看平台SDK来获得在用户模式应用程序使用GUID的信息。
1.6.1 定义和导出新的GUID

为驱动程序导出到其他系统组件、驱动程序或者应用程序的条目定义一个新的GUID，举个例子，你为其上的一个设备定义了定制PnP事件的一个新的GUID。为了定义和导出一个新的GUID，你必须做下面的事情：

1. 为GUID选择一个符号名。

选择一个表示了GUID目的的名字。举个例子，操作系统使用这样的名字如GUID_BUS_TYPE_PCI和PARPORT_WMI_ALLOCATE_FREE_COUNTS_GUID。

2. 使用 uuidgen.exe 或 guidgen.exe 为GUID生成一个值。

这些应用程序生成一个独一无二的、代表一个128位值的格式化的信息串，在 uuidgen.exe 里的-s开关输出了格式化为一个C结构的GUID。

3. 在适当的头文件里定义GUID。

使用DEFINE_GUID宏将GUID符号名与它的值相联。（参看例1。）

例1：在一个仅有GUID的头文件里定义一个GUID

:

DEFINE_GUID( GUID_BUS_TYPE_PCMCIA, 0x9343630L, 0xaf9f, 0x11d0,

0x92, 0x9f, 0x00, 0xc0, 0x4f, 0xc3, 0x40, 0xb1 );

DEFINE_GUID( GUID_BUS_TYPE_PCI, 0xc8ebdfb0L, 0xb510, 0x11d0,

0x80, 0xE9, 0x00, 0x00, 0xf8, 0x1e, 0x1b, 0x30 );

:

如果GUID在一个不只有GUID定义的头文件里被定义，你必须采取一个额外的步骤确保GUID在包含头文件的驱动程序里被实例化。DEFINE_GUID陈述必须发生在任何#ifdef语句之外，该语句阻止了多次的引用。否则，如果头文件包括在预编译的头里，GUID在使用头文件的驱动程序里将不被实例化。在一个混合的头文件里参看例2关于GUID的定义。

例2：在一个混合的头文件里定义GUID

#ifndef _NTDDSER_ //这个ex.从一系列 .h的驱动程序文件里得到

#define _NTDDSER_

:

//在这里放置其他的头文件定义

:

#endif // _NTDDSER_

#ifdef DEFINE_GUID // 不要中断驱动程序的编译，这些驱动程序包含

//这个头文件但不需要GUID

//

//在多次引用保护之外放置GUID定义

DEFINE_GUID ( GUID_CLASS_COMPORT, 0x86e0d1e0L, 0x8089, 0x11d0,

0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73 );

DEFINE_GUID ( GUID_SERENUM_BUS_ENUMERATOR, 0x4D36E978, 0xE325,

0x11CE, 0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18 );

:

#endif // DEFINE_GUID

由于DEFINE_GUID定义GUID为一个EXTERN_C变量，在阻止多次调用的语句之外放置一个GUID定义，这个过程没有在一个驱动程序里引起多次GUID实例。只要类型匹配，EXTERN变量的多次声名是被允许的。
1.6.2 在驱动程序代码里包含GUID

为了在内核模式驱动程序里使用GUID，必须做两件事情：

1. 包含头文件initguid.h,它重新定义了DEFINE_GUID宏。

宏initguid.h重新定义了DEFINE_GUID宏来实例化GUID（不要简单地声明一个EXTERN引用）。你应该在驱动程序源文件里包含这个头文件，在那里GUID将被实例化。（用户模式应用程序在包括含有GUID定义的头文件之前包含<objbase.h>）。

2. 包含定义了GUID的头文件。

在包含initguid.h的语句之后，包含有GUID定义的头文件。一个驱动程序可能包含不止一个的含有GUID定义的头文件，包括系统提供的头文件和第三方的头文件。

下面的代码给出了包含GUID语句的序列：

:

//包含系统头文件如wdm.h

#include <initguid.h>

//包含系统和驱动程序专用的头文件，这些头文件包含GUID定义。

：

你将上面的语句放置到一个驱动程序的模块里：即典型的主模块，当上面的语句存在时，驱动程序使用它的符号名来引用一个GUID。


处理即插即用IRP的规则

这一章描述了驱动程序必须作什么来处理即插即用的（PnP）IRP。

参见《Windows2000驱动程序开发参考》的卷一中关于每个PnP IRP的参考内容，参见第3章关于处理启动、停止和删除IRP的扩展信息。

在第1部分中阅读PnP概念、PnP术语和一个PnP驱动程序全面的需求信息，参见本部分的第1章中关于PnP设备状态的描述。

1.1 PnP IRP需求

PnP 管理器使用IRP来指示驱动程序启动、停止和删除设备并查询驱动程序的设备，所有的PnP IRP有主功能码IRP_MJ_PNP。当PnP 管理器发送一个IRP时，它初始化 Irp->IoStatus.Status 为STATUS_NOT_SUPPORTED。

PnP驱动程序必须遵循下面的这些规则：

1. 一个驱动程序必须有一个处理PnP IRP的调度例程。

驱动程序在 DriverEntry 例程的 DriverObject->MajorFunction[IRP_MJ_PNP]里设置例程地址，这个调度例程是一个独立的仅处理PnP IRP的DispatchPnP例程，或者驱动程序能够在单个的调度例程里处理几个IRP_MJ_Xxx代码。

2. 除非一个功能或过滤器驱动程序处理IRP和遇到一个错误（例如资源不足），它必须向下传递PnP IRP到设备堆栈里的下一个驱动程序。

除非其中的一个驱动程序遇到一个错误，一个设备的所有驱动程序必须有机会处理设备的PnP IRP。PnP管理器发送IRP给设备堆栈里顶端的驱动程序，功能和过滤器驱动程序向下传递IRP给下一个驱动程序，而父驱动程序完成IRP。参见2.2节来获得更多信息。

如果驱动程序尝试处理IRP并且遇到一个错误（如资源不足）时，它可能使IRP失效。如果一个驱动程序收到带有它所不能处理的代码的一个IRP时，该驱动程序不能够使IRP失效，它必须向下传递这样一个IRP给下一个驱动程序而无需修改此IRP的状态。

3. 一个驱动程序必须处理特定的PnP IRP和可选地处理其他的PnP IRP。

每一个PnP驱动程序必须处理特定的IRP，比如IRP_MN_REMOVE_DEVICE，且每个驱动程序能够可选地处理其他的IRP。参见《Windows2000驱动程序开发参考》的卷一获得有关信息，即关于对每一类型的驱动程序（功能驱动程序、过滤器驱动程序和总线驱动程序），哪一个IRP是必要的，哪一个IRP是可选的。

一个驱动程序可能使带有一个适当的错误状态的PnP IRP失效，但是一个驱动程序不应该为这样一个IRP返回STATUS_NOT_SUPPORTED。

4. 如果一个驱动程序成功地处理了一个PnP IRP，该驱动程序设置IRP状态为成功，它不依靠堆栈里另外的驱动程序来设置状态。

一个驱动程序设置 Irp->IoStatus.Status 为STATUS_SUCCESS，以便通知PnP管理器驱动程序已经成功地处理了IRP。对某些IRP，一非总线驱动程序也许能够依靠它的父总线驱动程序设置状态为成功。然而，这是一个冒险的实践，为一致性和健壮性起见，对驱动程序已经成功处理的每个PnP IRP，一个驱动程序必须设置IRP状态为成功。

5. 如果一个驱动程序使IRP失效，驱动程序以一个错误状态完成IRP，并且不向下传递IRP到下一个驱动程序。

为了使一个象IRP_MN_QUERY_STOP的IRP失效，一驱动程序设置 Irp->IoStatus.Status 为STATUS_UNSUCCESSFUL。其余的IRP的额外错误状态值包括STATUS_INSUFFICIENT_ RESOURCES和STATUS_INVALID_DEVICE_STATE。

驱动程序没有为它们处理的IRP设置STATUS_NOT_SUPPORTED，这是由PnP管理器设置的初始状态。如果一个IRP在此状态下完成，这意味着没有驱动程序在堆栈里已经处理了IRP；所有的驱动程序只是传递IRP给下一个驱动程序。

6. 一个驱动程序必须在它的调度例程里（在IRP向设备堆栈下面的方向），在一个IoCompletion例程里（在IRP向设备堆栈后上面的方向），或者两个都有，来处理PnP IRP，如IRP的参考页所提及的。

在带有IRP_MN_REMOVE_DEVICE的情况下，一些PnP IRP必须首先被设备堆栈顶层的驱动程序所处理，然后被每一个下一个较底层的驱动程序所处理。在带有IRP_MN_START_DEVICE的情况下，一些IRP必须首先由父总线驱动程序处理，在带有IRP_MN_QUERY_CAPABILITIES的情况下，其余的IRP仍旧按设备堆栈次序和反向次序被处理。参看《Windows2000驱动程序开发参考》的卷一来获得运用每个PnP IRP的规则，看2.3节获得处理PnP IRP的信息，这个PnP IRP必须首先由父总线驱动程序处理。

7. 一个驱动程序必须按IRP设备堆栈次序添加信息给一个IRP，并按IRP反向次序修改或删除信息。

当响应一个PnP查询IRP的信息返回时，一个驱动程序必须遵守这个规范以便能够通过设备的分层的驱动程序使信息被有续地传送。

8. 除过在明确被说明的地方，一个驱动程序不应该依靠以任何特定的次序发送的PnP IRP。

9. 当一个驱动程序发送一个PnP IRP时，它必须发送该IRP到设备堆栈里的顶端驱动程序。

大多数PnP IRP由PnP管理器发送，但是一些能够由驱动程序发送（例如，IRP_MN_QUERY_INTERFACE）。一个驱动程序必须发送一个PnP IRP给设备堆栈顶端的驱动程序，调用 IoGetAttachedDeviceReference 来得到设备堆栈顶端的驱动程序的设备对象的一个指针。

你应该用操作系统的检查构建测试你的驱动程序，Windows2000的检查构建核实一个驱动程序是否遵照上面列出的许多PnP规则。

1.2 沿设备堆栈传递PnP IRP

除非设备堆栈里的一个驱动程序使IRP失效，一个设备的所有驱动程序必须有机会响应一个PnP IRP。（看图2.1。）

没有设备的单个驱动程序能够假设它是唯一可响应一个PnP IRP的驱动程序。例如，一个功能驱动程序响应一个IRP_MN_QUERY_CAPABILITIES请求并完成IRP而没有传递它给下一个低层驱动程序。没有能力被低层驱动程序所支持，比如，一个唯一的实例ID或由父总线驱动程序支持的电源管理能力被报告。

当父总线驱动程序调用 IoCompleteRequest，I/O管理器调用任何由功能驱动程序或过滤器驱动程序注册的IoCompletion例程时，一个PnP IRP沿设备堆栈返回。

图2.1向下传递PnP IRP给设备堆栈

当一个功能或过滤器驱动程序接收到一个PnP IRP时，它必须做下面的工作：

n 如果驱动程序执行动作来响应IRP：

1. 执行适当的动作。

2. 设置适当的 Irp->IoStatus.Status 状态，比如为STATUS_SUCCESS，如果对IRP适当的话，设置 Irp->IoStatus.Information。

3. 用 IoSkipCurrentStackLocation 或 IoCopyCurrentStackToText 确定下一个堆栈位置。如果你设置了一个IoCompletion例程，调用后面的例程。

4. 如有必要，设置一个IoCompletion例程。

5. 不要完成IRP（不要调用 IoCompleteRequest），父总线驱动程序将完成该IRP。

n 如果驱动程序没有执行这个IRP动作，它简单地准备传递IRP给下一个驱动程序：

1. 调用 IoSkipCurrentStackLocation 从IRP删除它的堆栈位置。

2. 不在 Irp->IoStatus 里设置任何域。

3. 不设置一个IoCompletion例程。

4. 不要完成IRP（不要调用 IoCompleteRequest），父总线驱动程序将完成该IRP。

如果一个功能或过滤器驱动程序在以前没有使IRP失效，它传递IRP给下一个具有 IoCallDriver 的低层驱动程序。一个驱动程序有下一个低层驱动程序的一个指针；该指针由高层的驱动程序的添加例程里的 IoAttachDeviceToDeviceStack 调用返回。

在执行任何任务来响应IRP后，父总线驱动程序完成IRP。在总线驱动程序调用 IoCompleteRequest 之后，I/O管理器调用由设备的功能或过滤器驱动程序所注册的任何IoCompletion例程。

1.3 延迟PnP IRP的处理直到低层的驱动程序完成

一些PnP和电源IRP必须首先被一设备的父总线驱动程序处理，然后被设备堆栈里每个紧邻的高层驱动程序处理。例如，父总线驱动程序必须是执行它的设备的启动操作（IRP_MN_START_DEVICE）的第一个驱动程序，紧接着被下一个高层的驱动程序处理。对这样的一个IRP，功能和过滤器驱动程序必须设置一个I/O完成例程，传递IRP给下一个低层驱动程序，并延迟处理IRP的任何行动直到低层的驱动程序已经完成处理IRP。

一个IoCompletion例程能够在IRQL DISPATCH_LEVEL上被调用，但是一个功能或过滤器驱动程序可能需要在IRQL PASSIVE_LEVEL上处理IRP。为了从一个IoCompletion例程返回从属的IRQL，驱动程序能够使用内核事件。驱动程序注册了一个设置了一个内核模式事件的IoCompletion例程，然后，驱动程序在它的DispatchPnP例程里等待事件。当事件被设置时，低层的驱动程序已经完成了IRP，且驱动程序能够处理该IRP。

注意一个驱动程序不应利用这个技巧来等待低层的驱动程序完成一个电源IRP（IRP_MJ_POWER）。在DispatchPower例程里等待一个事件可能引起死锁，该DispatchPower例程在IoCompletion例程里设置。参见第3部分第1章的传递电源IRP来获得更多的信息。

图2.2和图2.3给出了一个驱动程序怎样等待低层的驱动程序来完成一个PnP IRP，该例给出了功能和总线驱动程序必须做什么及它们怎样与PnP管理器和I/O管理器相互作用。

图2.2延迟PnP IRP的处理---第1部分

下面的注释对应图2.2中的被圈起来的数字：

1. PnP管理器调用I/O管理器来发送一个IRP给设备堆栈里顶端的驱动程序。

2. I/O管理器调用顶端驱动程序的DispatchPnP例程。在本例中，设备堆栈里仅有两个驱动程序（功能驱动程序和父总线驱动程序），且功能驱动程序是顶端驱动程序。

3. 功能驱动程序声明并初始化一个内核模式事件，确定下一个低层驱动程序的堆栈位置，并为这个IRP设置一个IoCompletion例程。

功能驱动程序能够利用 IoCopyCurrentStackLocationToNext 来确定堆栈位置。

在调用 IoSetCompletionRoutine 时，功能驱动程序设置 InvokeOnSuccess、InvokeOnError 和 InvokeOnCancel 为TRUE并且传递内核模式事件作为环境参数的部分。

4. 在执行任何处理IRP操作之前，功能驱动程序向下传递IRP到具有 IoCallDriver 的设备堆栈。

5. I/O管理器通过调用驱动程序的DispatchPnP例程发送一个IRP给设备堆栈里的下一个低层驱动程序。

6. 本例中的下一个低层驱动程序是设备堆栈里的最低层的驱动程序，即父总线驱动程序，总线驱动程序执行它的操作来启动设备。总线驱动程序设置 Irp->IoStatus.Status，如果相关于这个IRP则设置 Irp->IoStatus.Information，并通过调用 IoCompleteRequest 完成IRP。

如果总线驱动程序调用其余的驱动程序例程或发送I/O给设备来启动它，总线驱动程序在它的DispatchPnP例程没有完成PnP IRP，相反，它必须用 IoMarkIrpPending 标记IRP待定，并从它的DispatchPnP例程返回STATUS_PENDING。驱动程序以后从另一个驱动程序例程里调用 IoCompleteRequest，可能是一个DPC例程。

图2.3给出了例子的第2部分，在这个例子中，设备堆栈里的高层的驱动程序恢复它们被延迟的IRP处理。

下面的注释对应图2.3中的被圈起来的数字：

7. 当总线驱动程序调用 IoCompleteRequest 时，I/O管理器检查更高驱动程序的堆栈位置并调用它发现的任何IoCompletion例程。例子中，I/O管理器定位并为下一个高层的驱动程序，即功能驱动程序，调用IoCompletion例程。

8. 功能驱动程序的IoCompletion例程设置在环境参数里提供的内核模式事件，并返回STATUS_MORE_PROCESSING_REQUIRED。

IoCompletion例程必须返回STATUS_MORE_PROCESSING_REQUIRED来阻止I/O管理器在此时调用由高层的驱动程序设置的IoCompletion例程，IoCompletion例程使用这个状态阻止完成以便它的DispatchPnP例程能重获控制。当DispatchPnP例程完成IRP时，I/O管理器为这个IRP恢复调用高层的IoCompletion例程。

9. I/O管理器停止完成IRP并返回控制给 IoCompleteRequest 例程，例子中该例程即为总线驱动程序的DispatchPnP例程。

图2.3延迟PnP IRP的处理---第2部分

10. 总线驱动程序从它的带有状态指示IRP处理结果的DispatchPnP例程里返回：或者是STATUS_SUCCESS或者是一个错误状态。

11. IoCallDriver 返回控制给它的调用程序，在本例中即为功能驱动程序的DispatchPnP例程。

12. 功能驱动程序的DispatchPnP例程恢复处理IRP。

如果 IoCallDriver 返回STATUS_PENDING，DispatchPnP例程已经在它的IoCompletion例程调用之前恢复执行。这样，DispatchPnP例程必须等待它的IoCompletion例程通知内核事件。这确保DispatchPnP例程将不继续处理IRP直到所有低层的驱动程序已经完成它为止。

如果设置 Irp->IoStatus.Status 为一个错误，一个低层的驱动程序已经使IRP失效，且功能驱动程序不可以继续处理IRP（除过任何必要的清理）。

13. 一旦低层的驱动程序已成功地完成IRP，功能驱动程序处理IRP。

对首先被父总线驱动程序处理的IRP，总线驱动程序典型地在 Irp->IoStatus.Status 里设置一个成功状态，并有选择地在 Irp->IoStatus.Information 里设置一个值，除非功能和过滤器驱动程序使IRP失效，它们在 IoStatus 里留下值。

功能驱动程序的DispatchPnP例程调用 IoCompleteRequest 来完成IRP，I/O管理器恢复I/O完成处理。例子中，没有过滤器驱动程序在功能驱动程序之上，这样没有更多的IoCompletion例程将被调用。当 IoCompleteRequest 返回对功能驱动程序的DispatchPnP例程的控制时，DispatchPnP例程返回状态。

对一些IRP，如果一个功能或过滤器驱动程序在它的反方向设备堆栈上使IRP失效，PnP管理器通知低层的驱动程序。例如，如果一个功能或过滤器驱动程序失效一个IRP_MN_START_DEVICE，PnP管理器给设备堆栈发送一个IRP_MN_REMOVE_DEVICE。


启动、停止和删除设备

本章描述了何时PnP管理器启动、停止和删除设备及即插即用（PnP）驱动程序必须在它们的DispatchPnP例程做什么来支持这些活动，包括以下的主题：

3.1 启动设备

3.2 为了资源重新平衡而停止设备

3.3 删除设备

参看第2章获得处理PnP IRP的一般规则，参看《Windows2000驱动程序开发参考》的卷一来获得每个PnP IRP的信息。

1.1 启动一设备

PnP管理器发送一个IRP_MN_START_DEVICE给驱动程序，或者启动一个新被枚举的设备，或者重新启动一个存在但因资源重新平衡而停止的设备。

功能和过滤器驱动程序必须设置一个IoCompletion例程，向下传递IRP_MN_START_DEVICE到设备堆栈，并延迟它们的启动操作直到所有的低层驱动程序已经完成IRP。父总线驱动程序，即设备堆栈里的底端驱动程序，在其他的驱动程序访问设备之前必须是执行设备的启动操作的第一个驱动程序。

为保证启动操作的适当时序，微软Windows2000 PnP管理器的延迟暴露设备接口，并且阻塞设备建立的请求直到启动IRP成功。

如果一个设备的驱动程序失效IRP_MN_START_DEVICE，PnP管理器发送一个IRP_MN_REMOVE_DEVICE IRP给设备堆栈（在Windows2000上）。为了响应这个IRP，设备的驱动程序放弃它们的启动操作（如果它们成功的启动了IRP），放弃它们的AddDevice操作，并与设备堆栈分离，PnP管理器标记这样的一个设备为“失败的启动”。

这一部分包括以下的主题：

3.1.1启动功能驱动程序里的一个设备

3.1.2启动过滤器驱动程序里的一个设备

3.1.3启动总线驱动程序里的一个设备

3.1.4启动设备设计注意
1.1.1 启动功能驱动程序里的一个设备

功能驱动程序设置一个IoCompletion例程，向下传递一个IRP_MN_START_DEVICE到设备堆栈，并延迟它们的启动操作直到所有的低层驱动程序已经完成IRP。参见第2章的2.3节获得使用一个内核事件和一个IoCompletion例程来延迟IRP处理的详细信息。

在所有的低层驱动程序已经完成IRP之后DispatchPnP例程重新得到控制时，功能驱动程序为了启动设备而执行它的任务，一个功能驱动程序用如下的过程启动设备：

1. 如果一个低层驱动程序使IRP失效（IoCallDriver 返回的一个错误），不要继续处理IRP，做一些必要的清理并从DispatchPnP例程返回（去本列表的最后一步）。

2. 如果低层驱动程序成功地处理IRP，启动该设备。

启动一个设备的确切步骤因设备不同而变化，这些步骤应该包括映射I/O空间、初始化硬件寄存器、设置设备为D0电源状态和用 IoConnectInterrupt 连接中断。如果该驱动程序在一个IRP_MN_STOP_DEVICE请求之后正重新启动一个设备，该驱动程序可能有设备状态需要保存。

在任何驱动程序能够访问设备之前，该设备应该被加电，参见第3部分第1章关于加电一个设备的更多信息。

如果设备能够被唤醒，它的电源策略所有者（通常为功能驱动程序）应该在它加电设备之后且在它完成IRP_MN_START_DEVICE请求之前发送一个等待/唤醒IRP。想要详细的资料，参见第3部分第4章中关于发送一个等待/唤醒IRP。

3. 在IRP-holding队列中启动IRP。

清空驱动程序定义的HOLD_NEW_REQUESTS标记并且启动IRP-holding队列中的IRP。当首次启动一个设备和在一个查询停止或停止IRP之后重新启动一个设备时，驱动程序应该能够做这些工作。参看3.2.5节获得更多的信息。

4. [可选择的]通过调用 IoSetDeviceInterfaceState，启用设备的接口。

如果有接口，则启动驱动程序以前在AddDevice例程里注册的接口（或者在一个INF里，或者通过另外一个组件，比如协同安装程序）。

在Windows2000上，直到IRP_MN_START_DEVICE IRP完成，PnP管理器才发送设备接口到达的通知，表明设备的所有驱动程序已经完成它们的启动操作。PnP管理器也使任何建立的请求失效，这些请求在设备所有的驱动程序完成启动IRP之前到达。

5. 完成IRP。

功能驱动程序的IoCompletion例程返回STATUS_MORE_PROCESSING_REQUIRED，如2.3节所描述的，这样功能驱动程序的DispatchPnP例程必须调用 IoCompleteRequest 来恢复I/O完成处理。

如果功能驱动程序的启动操作成功，驱动程序设置 Irp->IoStatus.Status 为STATUS_SUCCESS，用一个IO_NO_INCREMENT的优先增强调用 IoCompleteRequest，并从它的DispatchPnP例程返回STATUS_SUCCESS。

如果功能驱动程序在它的启动操作过程里遇到一个错误，驱动程序在IRP里设置错误状态，用IO_NO_INCREMENT调用 IoCompleteRequest，并从它的DispatchPnP例程里返回错误。

如果一个较低层驱动程序使IRP失效（IoCallDriver 返回的一个错误），功能驱动程序用IO_NO_INCREMENT调用 IoCompleteRequest 并从它的DispatchPnP例程里返回 IoCallDriver 错误。功能驱动程序在这种情况下不设置 Irp->IoStatus.Status，这是由于状态已经由以前使IRP失效的低层驱动程序所设置。

当一个功能驱动程序接受到一个IRP_MN_START_DEVICE请求时，它应该在 IrpSp->Parameters.StartDevice.AllocatedResources 和 IrpSp->Parameters.StartDevice.Allocated ResourcesTranslated 上检查结构，该结构描述了PnP管理器已经分别地指定给设备的原始的和转换的资源。驱动程序应该出于一个调试帮助的目的，在设备扩展里保存每个资源列表的一份拷贝。

资源列表是成对的CM_RESOURCE_LIST结构，在这个结构里，每个原始列表的元素对应于转换列表里相同的元素，例如，如果 AllocatedResource.List[0]描述了一个原始的I/O端口范围，那么，AllocatedResourceTranslated.List[0]描述了转换后的同样的范围，每个转换的资源包括一个物理地址和资源类型。

如果分配驱动程序一个转换了的内存资源（CmResourceTypeMemory），它必须调用 MmMapIoSpace 来映射该物理地址到一个虚拟地址，通过该虚拟地址，驱动程序能够访问设备寄存器。对一个以平台独立的方式操作的驱动程序，如果需要的话，它应该检查每个返回的、转换的资源并映射它。

一个功能驱动程序应该做下面的工作来响应一个IRP_MN_START_DEVICE以确保访问所有的设备资源：

1. 拷贝IrpSp->Parameters.StartDevice.AllocatedResources到设备扩展。

2. 拷贝IrpSp->Parameters.StartDevice.AllocatedResourcesTranslated到设备扩展。

3. 在一个循环里，在 AllocatedResourcesTranslated 里检查每个描述符元素，如果描述符资源类型是 CmResourceTypeMemory，调用 MmMapIoSpace，并传递物理地址和转换的资源的长度。

当某一个驱动程序接收到一个IRP_MN_STOP_DEVICE、IRP_MN_REMOVE_DEVICE或者IRP_MN_SURPRISE_REMOVAL请求时，它必须通过在一个相似的循环里调用 MmUnmapIoSpace 来释放映射。如果该驱动程序必须失效IRP_MN_START_DEVICE请求时，它也应该调用 MmUnmapIoSpace。

参见在《内核模式驱动程序设计指南》的第16章里“重新映射总线相关的内存空间地址到虚拟地址”来获得更多的信息。
1.1.2 启动过滤器驱动程序里的一个设备

一个顶层的过滤器驱动程序应该增强功能驱动程序的任何启动活动，一个低层的驱动程序典型地增强了设备特性并可能参与启动设备。
1.1.3 启动总线驱动程序里的一个设备

在一个总线驱动程序的DispatchPnP例程里，它用一个如下的程序启动一个子设备（子代PDO）：

1. 启动设备。

确切的步骤因设备不同而异，举例来说，PCI总线驱动程序对它的映射寄存器编程以便PCI总线上的请求能够实现，PnP ISA总线驱动程序能够使PnP ISA卡可用，这样功能驱动程序能访问它。

2. 完成IRP。

如果总线驱动程序的启动操作成功，驱动程序设置 Irp->IoStatus.Status 为STATUS_SUCCESS，用一个IO_NO_INCREMENT的优先增强调用 IoCompleteRequest，并从它的DispatchPnP例程返回STATUS_SUCCESS。

如果功能驱动程序在其启动操作过程里遇到一个错误，驱动程序在IRP里设置错误状态，用IO_NO_INCREMENT调用 IoCompleteRequest，并从其DispatchPnP例程里返回错误。

如果一个总线驱动程序需要一些时间来启动设备，它能够标记IRP为待定，并返回STATUS_PENDING。
1.1.4 启动设备设计注意

n PnP管理器失效了设备的创始请求直到IRP_MN_START_IRP完成为止，表明设备的所有的驱动程序已经执行了它们的启动操作。

n 由于DispatchPnP例程在一个系统线程的IRQL PASSIVE_LEVEL的环境里运行，只要驱动程序没有控制拥有一个系统分页文件的设备，由 ExAllocatePoolWithTag 分配的、在初始化过程中专用的任何内存能够从分页的池得到。这样一个内存分配必须在返回控制之前由 ExFreePool 释放。

n 甚至在设备启动过程中，一个WDM设备驱动程序的ISR应该能够决定是否它已经被伪中断调用。从 IoConnectInterrupt 调用返回时，在处理IRP_MN_START_DEVICE的代码路径里，如果中断在设备上是允许的，该ISR能够被立即调用。

1.2 为了资源重新平衡而停止一个设备

PnP管理器指示驱动程序停止一个设备以便它能够重新平衡正被设备使用的硬件资源，当一个新的设备被枚举时，这需要已经在使用的资源，重新平衡典型情况下是必要的。本部分有以下的主题：

3.2.1 理解何时停止发布IRP

3.2.2 处理IRP_MN_QUERY_STOP_DEVICE请求

3.2.3 处理IRP_MN_STOP_DEVICE请求

3.2.4 处理IRP_MN_CANCEL_STOP_DEVICE请求

3.2.5 当一个设备被暂停时，保留进入的IRP
1.2.1 理解何时停止发布IRP

图3.1给出了涉及设备的停止和重新启动的序列，参看第1章获得PnP设备状态从枚举到删除的广泛的讨论。

图3.1停止一个设备

下面的注释对应图3.1中的被圈起来的数字：

1. PnP管理器发布一个IRP_MN_QUERY_STOP_DEVICE来询问是否一个设备的驱动程序能够停止该设备并且释放它的硬件资源。

如果设备堆栈里的所有驱动程序返回STATUS_SUCCESS，驱动程序已经设置设备为一状态（停止待定），从该状态，设备能够被快速地停止。

PnP管理器查询所需要的尽量多的设备堆栈来重新平衡必要的资源。

2. PnP管理器发布一个IRP_MN_STOP_DEVICE来停止该设备。

仅仅如果一个设备前面的查询停止IRP成功完成，PnP管理器发送一个停止IRP。为了响应一个停止IRP，驱动程序释放了设备的硬件资源（比如它的I/O端口），并保留需要访问的设备的任何IRP。

3. 在成功地重新平衡资源之后，PnP管理器发布IRP_MN_START_DEVICE请求来重新启动它在重新平衡过程中停止的设备。

4. 否则，PnP管理器通过发送一个IRP_MN_CANCEL_STOP_DEVICE来取消一个查询停止IRP。

为了响应一个IRP_MN_CANCEL_STOP_DEVICE，一个设备的驱动程序返回设备启动的状态，并恢复处理设备的I/O请求。

如果堆栈里的一个驱动程序使请求失效或如果全面的重新平衡操作失败。并且正在取消它所有的查询停止请求，PnP管理器取消设备堆栈的查询停止。当PnP管理器刚好取消了一个设备堆栈的查询停止时，由于附着在已失败查询的驱动程序之上的任何驱动程序有设备处于停止待定状态，所以，PnP管理器发送IRP_MN_CANCEL_STOP_DEVICE请求。当IRP_MN_CANCEL_STOP_DEVICE成功时，驱动程序已经返回设备为启动的状态。

5. 在资源的重新平衡之后，如果一个驱动程序重新启动设备失效，PnP管理器发送删除IRP给设备堆栈（在Windows2000上）。

PnP管理器首先发送一个IRP_MN_SURPRISE_REMOVAL请求，然后发送一个IRP_MN_REMOVE_DEVICE请求，但是仅仅在设备的所有打开处理被关闭之后。

一个PnP设备的硬件资源的重新平衡对应用程序和最终用户必须是透明的，用户可能在操作上经历一个短暂的滞后，但是数据不能够被丢失，当你处理停止IRP时，你必须考虑到这一点。
1.2.2 处理IRP_MN_QUERY_STOP_DEVICE请求

一个IRP_MN_QUERY_STOP_DEVICE请求首先被设备堆栈里的顶端驱动程序所处理，然后被每下一个低层的驱动程序所处理，一个驱动程序在它的DispatchPnP例程里处理停止IRP。

在响应一个IRP_MN_QUERY_STOP_DEVICE时，一个驱动程序必须做下面的工作：

1. 决定是否设备能够被停止，以及它的硬件资源被释放而没有不利的影响。

如果下面的任何选项是真，一个驱动程序必须失效一个查询停止IRP。

n 已经通知一个驱动程序（通过IRP_MN_DEVICE_USAGE_NOTIFICATION）设备在一个分页、休眠或崩溃转储文件的路径里。

n 不能够释放设备的硬件资源。

如果下面的选项是真，一个驱动程序可能使一个查询停止IRP失效。

n 驱动程序不应扔掉扔掉I/O请求和没有排队IRP的机制。

当设备处于停止状态时，一个驱动程序必须保留需要访问设备的IRP。如果一个驱动程序没有排队IRP，它不会允许停止设备，那样的话，必须使一个查询停止使IRP失效。

这个规则的一个例外是设备被允许扔掉I/O，这样的一个设备的驱动程序能够成功查询停止并停止请求而没有排队IRP。

2. 如果设备不能够被停止，使查询停止IRP失效。

设置 Irp->IoStatus.Status 为一适当的错误状态，用IO_NO_INCREMENT调用 IoCompleteRequest，并从驱动程序的DispatchPnP例程里返回错误。不要传递IRP给下一个较低层的驱动程序。

3. 如果设备能够被停止并且驱动程序排队IRP，在设备扩展里设置HOLD_NEW_REQUESTS标记以便随后的IRP将被排队。（参见3.2.5节。）

可选择的，一个设备的驱动程序能够拖延完全地暂停设备直到驱动程序接收到随后的IRP_MN_QUERY_STOP_DEVICE请求。然而，这样的驱动程序必须排队一些请求，当它到达时，这些请求可阻止它们立即地成功停止IRP。直到设备被重新启动，这样的驱动程序必须排队如下的请求：

n IRP_MN_DEVICE_USAGE_NOTIFICATION请求（举个例子，在设备上放置一个分页文件）

n 同步传送的请求

n 生成能够阻止驱动程序成功一个停止IRP的请求

4. 如果设备在进度失效里不能有一个IRP，要确保传递给其余驱动程序例程和给较低层的驱动程序的任何未完成的请求已经完成。

一个驱动程序能够取得这一点的一个方法是使用一个引用计数和一个事件，确保所有请求已经完成：

n 在它的AddDevice例程里，驱动程序在设备扩展里定义一个I/O引用计数并初始化计数为一。

n 也在它的AddDevice例程里，驱动程序用 KeInitializeEvent 生成一个事件并用 KeClearEvent 初始化事件为Not-Signaled状态。

n 它处理一个IRP的每个时刻，驱动程序用 InterlockedIncrement 增大引用计数。

n 它完成一个请求的每个时刻，驱动程序用 InterlockedDecrement 减少引用计数。

如果有一个请求，或者刚好在调用IoCallDriver之后，如果驱动程序使用了请求的非IoCompletion例程，驱动程序在IoCompletion例程里减少引用计数。

n 当驱动程序接收到一个IRP_MN_QUERY_STOP_DEVICE时，它用 InterlockedDecrement 减少了引用计数。如果没有未完成的请求，这将缩减引用计数为零。

n 当引用计数为零时，驱动程序用 KeSetEvent 设置事件，并发送信号以便查询停止代码能够继续。

作为以上程序的一个替换对象，一个驱动程序能够在进程中的任何IRP之后串行化IRP_MN_QUERY_STOP_DEVICE IRP。

5. 执行放置设备在一个停止待定状态里所必要的任何其他步骤。

在一个驱动程序使一查询停止IRP成功之后，它必须准备使一个IRP_MN_STOP_DEVICE成功。

6. 完成IRP。

在一个功能或过滤器驱动程序里：

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS。

n 用 IoSkipCurrentIrpStackLocation 定位下一个堆栈位置，并用 IoCallDriver 传递IRP给下一个较低层驱动程序。

n 当从DispatchPnP例程返回状态时，从 IoCallDriver 传播这个状态。

n 不要完成IRP。

在一个总线驱动程序里：

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS。

然而，如果在总线上的设备使用硬件资源，重新评价总线和子设备的资源需求。如果任何资源需求已经改变，返回STATUS_RESOURCE_REQUIREMENTS_CHANGED代替STATUS_SUCCESS。这个状态表示成功，但是请求PnP管理器在发送一个停止IRP之前重新查询你的资源。

n 用IO_NO_INCREMENT完成IRP（IoCompleteRequest）。

n 从DispatchPnP例程返回。

如果在设备堆栈里的任何一个驱动程序使IRP_MN_QUERY_STOP_DEVICE失效，PnP管理器发送一个IRP_MN_CANCEL_STOP_DEVICE给设备堆栈，这阻止了驱动程序为一个查询停止IRP而需要一个IoCompletion例程，以便侦测是否一个较低层的驱动程序以前使IRP失效过。
1.2.3 处理IRP_MN_STOP_DEVICE请求

一个IRP_MN_STOP_DEVICE请求首先被设备堆栈里的顶层驱动程序所处理，然后被每下一个较低层的驱动程序所处理。一个驱动程序在它的DispatchPnP例程里处理停止IRP。

一个驱动程序以如下程序处理一个IRP_MN_STOP_DEVICE请求：

1. 确保设备被暂停。

如果一个驱动程序没有完全暂停设备来响应IRP_MN_QUERY_STOP请求，它现在必须这样作了。在设备扩展里设置一个HOLD_NEW_REQUESTS标记并执行任何其他必要的操作来暂停设备。

在资源重新平衡操作过程中设备有可能失去电力，因而有可能失去设备状态。设备的驱动程序应该保存任何设备状态信息，并且当它们接收到随后的IRP_MN_START_DEVICE请求时重新保存它。

2. 释放设备的硬件资源。

在一个功能驱动程序里，确切的操作取决于设备和驱动程序，但是能够包括用 IoDisconnectInterrupt 断开一个中断，用 MmUnmapIoSpace 释放一个物理地址范围和释放I/O端口。

如果一个过滤程序或总线驱动程序以前获得设备的任何硬件资源，驱动程序必须释放资源来响应一个IRP_MN_STOP_DEVICE请求。

3. 设置 Irp->IoStatus.Status 为STATUS_SUCCESS。

4. 传递IRP给下一个较低层的驱动程序或者完成该IRP。

n 在一个功能或过滤器驱动程序里，用 IoSkipCurrentIrpStackLocation 确定下一个堆栈位置，用 IoCallDriver 传递IRP给下一个较低层的驱动程序，并象从DispatchPnP例程返回状态一样从 IoCallDriver 返回状态。不要完成IRP。

n 在一个总线驱动程序里，使用 IoCompleteRequest 的IO_NO_INCREMENT完成IRP并从DispatchPnP例程返回。

当设备为了PnP资源重新平衡而停止时，驱动程序不能启动访问设备的任何IRP。驱动程序必须如3.2.5节里描述的一样排队这样的IRP，或者，如果驱动程序不应扔掉I/O请求和不能够实现一个IRP-holding队列，则使它们失效。
1.2.4 处理IRP_MN_CANCEL_STOP_DEVICE请求

一个IRP_MN_CANCEL_STOP_DEVICE请求首先被一个设备的父总线驱动程序所处理，然后被每个下一个更高级的驱动程序所处理。一个驱动程序在它的DispatchPnP例程里处理停止IRP。

为响应一个IRP_MN_CANCEL_STOP_DEVICE请求，一个驱动程序必须返回设备它的启动状态并且恢复正常操作。驱动程序必须成功于一个取消停止IRP。

一个驱动程序用如下的程序处理一个IRP_MN_CANCEL_STOP_DEVICE请求：

1. 延迟重新启动设备直到更低层的驱动程序已经完成它们的重新启动操作。（看第2章2.3节）

2. 在较低层的驱动程序完成之后，返回设备它的被启动的状态。

确切的操作取决于设备和驱动程序。

3. 在IRP-holding队列里启动IRP。

如果当设备处于停止待定状态时驱动程序正保留请求，清除HOLD_NEW_REQUESTS标记并在IRP-holding队列里启动IRP。参看3.2.5节获得更多的信息。

4. 用 IoCompleteRequest 完成IRP。

n 在一个功能或过滤器驱动程序里：

象2.3节里所描述的一样，驱动程序的IoCompletion例程在以前返回STATUS_MORE_PROCESSING_REQUIRED，这样驱动程序的DispatchPnP例程必须调用 IoCompleteRequest 来恢复I/O完成处理。

驱动程序设置 Irp->IoStatus.Status 为STATUS_SUCCESS。用一个IO_NO_INCREMENT的优先增强调用 IoCompleteRequest，并从它的DispatchPnP例程里返回STATUS_SUCCESS。

一个总线驱动程序不应该使这个操作失效，如果一个驱动程序使重新启动IRP失效，设备处于一个不一致的状态，将不能正常操作。

n 在一个父总线驱动程序里：

如果总线驱动程序的重新启动操作成功，通过指定的一个IO_NO_INCREMENT的优先增强，驱动程序设置 Irp->IoStatus.Status 为STATUS_SUCCESS并调用 IoCompleteRequest。总线驱动程序从它的DispatchPnP例程里返回STATUS_SUCCESS。

一个总线驱动程序不应该使这个操作失效过，如果一个驱动程序使重新启动IRP失效，设备处于一个不一致的状态，将不能正常操作。

当设备启动和运行时，驱动程序接收到一个伪取消停止请求。这可能发生，例如，如果一个驱动程序（或者设备堆栈里的一个更高级的驱动程序）使一个IRP_MN_QUERY_STOP_DEVICE请求失效。当一个设备启动和运行时，驱动程序能够安全地使设备的伪取消停止请求成功。
1.2.5 当一个设备被暂停时，保留进入的IRP

当驱动程序的资源正被重新平衡时，一个设备的驱动程序必须暂停设备。在资源重新平衡的过程中，一些驱动程序暂停设备来响应一个IRP_MN_QUERY_STOP_DEVICE请求，其他的驱动程序则拖延暂停设备直到它们接收到IRP_MN_STOP_DEVICE请求。在每一种情况下，当IRP_MN_STOP_DEVICE成功时，必须暂停设备。

驱动程序必须在进程中完成任何设备的IRP并阻止启动任何需要访问设备的新的IRP。

当一个设备被暂停时，为保留IRP，一个驱动程序执行如下的过程：

1. 在它的AddDevice例程里，用象HOLD_NEW_REQUESTS的一个名字在设备扩展里定义一个设备标志。清除标志。

2. 为了保留IRP生成一个FIFO队列。

如果驱动程序已经排队IRP，由于在暂停一个设备之前，驱动程序对完成任何未完成的请求是必要的，所以它能够再次使用相同的队列。

如果驱动程序还没有一个IRP队列，它必须在它的AddDevice例程里生成一个。它生成什么类型的队列取决于驱动程序怎么处理队列。一个驱动程序可能使用一个内部锁定的、双链接的列表和 ExInterlocked..List 例程。

3. 在它的IRP_MN_QUERY_STOP_DEVICE（或IRP_MN_STOP_DEVICE）的DispatchPnP代码里，完成任何未完成的请求，并设置HOLD_NEW_REQUESTS标志。

4. 在一个访问设备的调度例程里，如DispatchWrite或DispatchRead，检查HOLD_NEW_REQUESTS标志是否设置。如果设置的话，驱动程序必须标记IRP为待定并排队它。（参见内核模式驱动程序设计指南获得有关标记IRP为待定的更多信息。）

驱动程序的DispatchPnP例程必须继续处理PnP IRP而不是保留它们，并且DispatchPower例程必须继续处理电源IRP。

5. 在DispatchPnP里，为响应一个启动或取消停止IRP，清除HOLD_NEW_REQUESTS标志并在IRP-holding队列里启动IRP。

这些动作可能是处理这些PnP IRP的最后步骤，例如，为响应一个启动IRP，驱动程序必须首先执行一些操作来启动设备，然后，它能够在IRP-holding队列里启动IRP。

在IRP-holding队列里处理IRP中的错误没有影响从启动或取消停止IRP返回的状态。

参见《Windows2000驱动程序开发参考》的卷二获得关于 ExInterlocked..List 例程和 IoMarkIrpPending 的信息。

1.3 删除一个设备

当设备已经或正在物理地从机器删除时，PnP管理器指令驱动程序删除一个设备的设备对象。当一个用户请求更新一个设备的驱动程序时，PnP管理器也发送一个删除IRP。

下面的部分描述了PnP管理器何时发布删除IRP和驱动程序该做什么响应这些IRP。本部分涵盖以下的主题：

3.3.1理解何时发布删除IRP

3.3.2处理IRP_MN_QUERY_REMOVE_DEVICE请求

3.3.3处理IRP_MN_REMOVE_DEVICE请求

3.3.4处理IRP_MN_CANCEL_REMOVE_DEVICE请求

3.3.5处理IRP_MN_SURPRISE_REMOVAL请求
1.3.1 理解何时发布删除IRP

图3.2给出了涉及删除一个设备的驱动程序里的IRP的典型序列。参见第2部分第1章获得设备状态从枚举到删除的更广泛的讨论。

下面的注释相应于图3.2中的被圈起来的数字：

1. 查询删除

PnP管理器发布一个IRP_MN_QUERY_REMOVE_DEVICE来询问是否一个设备能够被删除而没有使机器崩溃，当一个用户请求更新一个设备的驱动程序时，它也发送这个IRP。

图3.2典型地删除IRP转换

如果设备堆栈里所有的驱动程序返回STATUS_SUCCESS，驱动程序已经使设备为删除待定状态。在这种状态下，驱动程序不可以启动阻止设备被删除的任何操作。

在这个“干净的”删除状况下，在发送一个删除IRP之前，PnP管理器发送一个查询删除IRP。看第五步对“突然的”删除的描述。

尽管它没有在上面的结构图中给出，一个总线驱动程序可能为没有启动的设备接收一个IRP_MN_QUERY_REMOVE_DEVICE。如果一个用户请求动态地删除一个设备，且该设备物理地存在于机器但不可用时，这种情况能够发生。

2. 在成功查询之后删除

PnP管理器发布一个IRP_MN_REMOVE_DEVICE来删除一个设备的驱动程序。

驱动程序必须使这个请求成功，设备的驱动程序执行任何必要的清空，从设备堆栈分离，并删除FDO和任何过滤程序DO。父总线驱动程序保留PDO直到用户物理地从机器删除设备。

注意到驱动程序可能删除IRP之前接收到一个IRP_MN_STOP_DEVICE，但这不是必须的。为了资源的重新平衡，IRP_MN_STOP_DEVICE被用来暂停一个设备；这不是一个向着删除方向的一步。当设备停止时，如果一个用户删除设备硬件，PnP管理器在停止IRP之后的某个时刻发送一个删除IRP，但是停止不是一个删除的先决条件。

3. 重新枚举设备

如果在驱动程序已经删除它们的设备对象之后重新枚举设备的话，PnP管理器调用驱动程序的AddDevice例程并发布一个IRP_MN_START_DEVICE来保存设备。（也看图1.1）

4. 取消一个查询删除

PnP管理器发布一个IRP_MN_CANCEL_REMOVE_DEVICE来取消一个查询删除请求。

为响应一个IRP_MN_CANCEL_REMOVE_DEVICE，驱动程序使设备返回到它的启动状态。

5. 突然删除(Windows2000)

在Windows2000上，如果一个用户从机器拔掉一个设备而没有首先通过Unplug或Eject Hardware应用程序，则PnP管理器发送一个IRP_MN_SURPRISE_REMOVAL IRP。

由于驱动程序没有提前接收到警告，这个状况被称作“突然的”删除。

为响应一个IRP_MN_SURPRISE_REMOVAL IRP，设备的驱动程序使任何未决的I/O失效，并释放为设备所使用的硬件资源。驱动程序必须确保没有组件试图访问这些设备，由于设备不再存在。

所有的驱动程序必须处理一个IRP_MN_SURPRISE_REMOVAL IRP，并必须设置状态为STATUS_SUCCESS。

不能够取消一个IRP_MN_SURPRISE_REMOVAL IRP。

6. 在突然删除之后删除(Windows2000)

当设备的所有打开的处理被关闭时，Windows2000 的PnP管理器发送一个IRP_MN_REMOVE_DEVICE请求给设备的驱动程序。每个驱动程序与设备堆栈分离并删除它的设备对象。

7. 突然删除(Windows98)

在Windows98上，当一个设备被删除而没有警告时，一个驱动程序没有接收到一个IRP_MN_SURPRISE_REMOVAL。Windows98的PnP管理器仅仅发送一个IRP_MN_REMOVE_DEVICE，WDM驱动程序必须有代码来处理两个：即一个IRP_MN_SURPRISE_REMOVAL和一个IRP_MN_REMOVE_DEVICE而没有一个前面的突然删除IRP（Windows98的行为）。这里IRP_MN_SURPRISE_REMOVAL后面跟着一个IRP_MN_REMOVE_DEVICE（Windows2000的突然删除行为）。

8. 在一个失败的启动之后删除(Windows2000)

如果设备的一个驱动程序使IRP_MN_START_DEVICE失效，PnP管理器发送一个IRP_MN_REMOVE_DEVICE请求给设备堆栈。这样一个删除IRP确保设备的所有驱动程序被通知说该设备没有成功地启动。为响应IRP_MN_REMOVE_DEVICE IRP，设备的驱动程序取消它们的启动操作（如果它们成功地启动了IRP）和AddDevice操作。PnP管理器标记这样一个设备为“失败的启动”。

这个行为仅用于Windows2000平台，在Windows98上，PnP管理器发送一个IRP_MN_STOP_DEVICE来响应一个失败的启动。

PnP设备的一个驱动程序能够比图3.2所示的在更多的条件下接收到一个IRP_MN_SURPRISE_REMOVAL。举例而言，一个用户可以给机器插入一个PC卡，然后在设备启动之前删除它。在这样的情况下，在驱动的添加例程被调用之后，而在发布IRP_MN_STOP_DEVICE请求之前，PnP管理器发布一个突然删除IRP。在驱动程序添加例程调用之后的任何时候，PnP设备的一个驱动程序必须准备处理删除IRP。
1.3.2 处理一个IRP_MN_QUERY_REMOVE_DEVICE请求

PnP管理器发送这个IRP来通知驱动程序一个设备将要从机器里被删除并询问是否一个设备能够被删除而不会使机器崩溃。当一个用户请求更新设备的驱动程序时，它也发送这个IRP。

PnP管理器在系统线程环境的IRQL PASSIVE_LEVEL上发送这个IRP。

在发送这个IRP给设备的驱动程序之前，它做下面的工作：

n 通知所有的用户模式应用程序，这些应用程序已注册接收设备上（或者一个相关的设备）的通知。

这包括被注册接收通知的应用程序，它可能在一个设备上、在一个设备的后代上（子代设备、子代之子代等等）或者在一个设备删除的关系上。通过调用 RegisterDeviceNotification，一个应用程序为这样的通知而注册。

为响应这些通知，一个应用程序或者准备设备删除（关闭设备的处理），或者使查询失效。

n 通知所有的内核模式驱动程序，这些驱动程序被注册用于设备上（或者一个相关的设备）的通知。

这包括被注册用于通知的驱动程序，它可能在一个设备上、在一个设备的后代上或者在一个设备删除的关系上。通过用一个 EventCategoryTargetDeviceChange 的事件种类调用 IoRegisterPlugPlayNotification，一个驱动程序为这样的通知所注册。

为响应这些通知，一个驱动程序或者准备设备删除（关闭设备的处理），或者使查询失效。

n 发送IRP_MN_QUERY_REMOVE_DEVICE IRP给设备的后代驱动程序。

n （在Windows2000上）如果一个文件系统被安装在设备上，PnP管理器给文件系统和一些文件系统过滤程序发送一个查询删除请求。如果有设备的打开句柄，文件系统典型地使查询删除请求失效。如果没有的话，文件系统则典型地锁定卷来阻止将来成功地建立。如果一个安装的文件系统不支持一个查询删除请求，PnP管理器使设备的查询删除请求失效。

如果上面所有的步骤都成功，PnP管理器给设备的驱动程序发送IRP_MN_QUERY_ REMOVE_DEVICE。

一个IRP_MN_QUERY_REMOVE_DEVICE请求首先被设备堆栈里的顶层驱动程序所处理，然后被每下一个较低层驱动程序所处理。一个驱动程序在它的DispatchPnP例程里处理删除IRP。

为响应一个IRP_MN_QUERY_REMOVE_DEVICE，一个驱动程序必须作下面的工作：

1. 决定一个设备是否能够从机器中被删除而没有使操作崩溃。

如果下面的任何选项为真的话，一个驱动程序必须使一个查询删除IRP失效：

n 删除设备可能导致数据丢失。

n 一个组件有设备的一个打开句柄。（这仅是Windows98的一个论点，如果在IRP_MN_QUERY_REMOVE_DEVICE完成之后没有打开句柄，Windows2000跟踪打开句柄并使请求失效。）

n 已经通知一个驱动程序（通过一个IRP_MN_DEVICE_USAGE_NOTIFICATION IRP）设备在一个分页的、崩溃的转储或者休眠的文件路径里。

n 驱动程序相对于设备有一个未完成的接口引用，这就是说，驱动程序提供一个接口来响应一个IRP_MN_QUERY_INTERFACE请求并且接口没有被间接访问。

如果设备能够唤醒，驱动程序应该取消等待唤醒并使查询删除请求成功。

2. 如果不能够删除设备，使查询删除IRP失效。

设置 Irp->IoStatus.Status 为一个适当的错误状态（典型地为STATUS_UNSUCCESSFUL），用IO_NO_INCREMENT调用 IoCompleteRequest，并从驱动程序的DispatchPnP例程里返回。不要传递IRP给下一个较低层的驱动程序。

3. 记录设备以前的PnP状态。

当它收到IRP_MN_QUERY_REMOVE_DEVICE请求时，驱动程序应当记录设备PnP状态，因为如果查询被取消（IRP_MN_CANCEL_REMOVE_DEVICE），驱动程序必须使设备返回到那个状态。从前的状态是典型地“启动的”，但也许不是，例如，如果设备被禁用的时候。

4. 完成IRP：

在一个功能或过滤器驱动程序里：

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS。

n 用 IoSkipCurrentIrpStackLocation 定位下一个堆栈位置，并用 IoCallDriver 传递IRP给下一个较低层驱动程序。

n 当从DispatchPnP例程返回状态时，从 IoCallDriver 传播此状态。

n 不要完成IRP。

在一个总线驱动程序里：

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS。

n 用IO_NO_INCREMENT完成IRP（IoCompleteRequest）。

n 从DispatchPnP例程返回。

如果设备堆栈里的任何驱动程序使一个IRP_MN_QUERY_REMOVE_DEVICE失效，PnP管理器发送一个IRP_MN_CANCEL_REMOVE_DEVICE给设备堆栈，这一过程阻止了为一个查询删除IRP而需要一个IoCompletion例程，从而可以检测是否一个低层的驱动程序在以前使IRP失效过。

一旦一个驱动程序使一个IRP_MN_QUERY_REMOVE_DEVICE成功，且它认为设备处于删除待定状态时，驱动程序必须为设备使一些随后的生成请求失效。驱动程序如通常一样处理其余所有的IRP，直到驱动程序接收到一个IRP_MN_CANCEL_REMOVE_DEVICE或IRP_MN_REMOVE_DEVICE为止。
1.3.3 处理一个IRP_MN_REMOVE_DEVICE请求

PnP管理器使用这个IRP来指令驱动程序删除一个设备的软件表示（设备对象等等）。当一个设备已经用一个有序的方式（例如，在Unplug或者Eject Hardware小应用程序里被一个用户初始化）被删除时，通过突然（一个用户从插槽里拔出设备而没有预先的警告），或者用户请求更新驱动程序时，PnP管理器发送这个IRP。

在发送这个IRP到设备的驱动程序之前，PnP管理器做下面的工作：

n 如果有子设备，发送IRP_MN_REMOVE_DEVICE请求给设备的子设备。

n 通知任何用户模式组件和为通知而注册的内核模式驱动程序，设备正在被删除。PnP管理器在处理一个设备时调用任何为对象设备通知而注册的用户模式组件，并调用任何为 EventCategoryTargetDeviceChange 而注册的内核模式驱动程序。

n （在Windows2000上）如果一个文件系统被安装在设备上，PnP管理器给文件系统和任何文件系统过滤程序发送一个删除请求，作为响应，一个文件系统典型地卸下卷。

设备堆栈里的顶层驱动程序处理一个删除IRP并传递它给下一个较低层的驱动程序，一个设备的父总线驱动程序是执行它的删除设备操作的最后一个驱动程序。一个驱动程序在它DispatchPnP例程里的处理删除IRP。

在一个驱动程序为一个IRP_MN_REMOVE_DEVICE请求返回成功之前，它必须保证设备的所有资源已经释放，这个IRP在驱动程序卸载之前可能是最后一个调用。

删除一个设备可能产生删除一系列其他设备的需要，PnP管理器负责协调从顶层向下到根设备级删除另外的设备对象。

这一部分描述了：

3.3.1.1 删除功能驱动程序里的一个设备

3.3.1.2 删除过滤器驱动程序里的一个设备

3.3.1.3 删除总线驱动程序里的一个设备
1.3.3.1 删除功能驱动程序里的一个设备

当删除一个设备时，一个功能驱动程序必须取消它以前执行的用于添加和启动设备的任何操作。这部分讨论包括外设的功能驱动程序和总线设备的功能驱动程序。

一个功能驱动程序在它的DispatchPnP例程里使用一个如下的过程删除一个设备：

1. 这是一个总线设备的一个功能驱动程序吗？

如果是这样的话，可能地删除总线上的设备任何未完成的子PDO。

如果总线驱动程序处理子设备的一个从前的IRP_MN_SURPRISE_REMOVAL请求，但是驱动程序还没有接收到随后的IRP_MN_REMOVE_DEVICE请求时，驱动程序保留子PDO不动。在以后的某个时候，当子设备的所有处理关闭时，PnP管理器将发送子设备的删除IRP，并且，总线驱动程序将在此时删除子PDO。

如果总线驱动程序处理设备一个从前的IRP_MN_REMOVE_DEVICE请求，且驱动程序还没有接收到随后的IRP_MN_SURPRISE_REMOVAL请求时，总线驱动程序删除子PDO。在这种情况下，PnP管理器保证在它发送一个删除IRP给父总线设备之前，任何功能和过滤器驱动程序已经从子设备（FDO和过滤程序DO已经被删除）删除。子PDO可能仍旧存在，这样总线驱动程序必须在它删除总线设备之前删除子PDO。

2. 是否驱动程序已经处理过这个FDO从前的一个IRP_MN_SURPRISE_REMOVAL请求？

如果是这样的话，执行任何尚存的清理并跳到第七步，IoCallDriver。

一个驱动程序在设备扩展里典型地维护一个标记，这个标记说明了是否驱动程序已经为设备处理过一个IRP_MN_SURPRISE_REMOVAL请求。

3. 保证设备不运行。

如果在响应一个前面的IRP_MN_QUERY_REMOVE_DEVICE时设备还不运行的话，驱动程序必须标记设备以便不接收新的请求，并且必须完成在驱动程序里排队的任何请求。驱动程序必须使任何需要访问设备的未完成请求失效。

一个驱动程序能够使用 Io..RemoveLock..例程来枚举待定的I/O，并设置一个事件来表明删除处理能够继续。参见《Windows2000驱动程序开发参考》的卷一获得这些例程的信息。

4. 执行一些掉电操作。

设备的每个驱动程序执行它的掉电操作，如果有，当它接收到IRP_MN_REMOVE_DEVICE IRP时。设备的电源策略所有者，典型地为功能驱动程序，并不发送一个独立的IRP_MN_SET_POWER IRP来设置电源状态为D3。父总线驱动程序典型地使插槽掉电，并当总线驱动程序得到删除IRP时，用 PoSetPowerState 通知电源管理器。看第3部分获得电源管理的更多信息。

5. 用 IoSetDeviceInterfaceState 禁止任何设备接口。

6. 为驱动程序所使用的设备释放任何硬件资源。

确切的操作取决于设备和驱动程序，但是也包括用 IoDisconnectInterrupt 断开一个中断，用 MmUnmapIoSpace 释放物理地址范围，并释放I/O端口。

7. 向下传递IRP_MN_REMOVE_DEVICE请求给下一个驱动程序。

用 IoSkipCurrentIrpStackLocation 为下一个较低层的驱动程序设定堆栈位置，并用 IoCallDriver 传递IRP给下一个驱动程序。

在继续它的删除行为之前，一个驱动程序不是非要等待基本的驱动程序来完成它们的删除操作。

8. 用 IoDetachDevice 从设备堆栈删除设备对象。

给下一个较低层的设备对象如 TargetDevice 参数指定一个指针。驱动程序在它的AddDevice例程里的 IoSkipCurrentIrpStackLocation 调用接收到这样的一个指针。

9. 清除设备专用的分配、内存、事件等等。

10. 用 IodeleteDevice 释放FDO。

11. 从DispatchPnP例程里返回，并从 IoCallDriver 传播返回的状态。

一个功能驱动程序没有为一个删除IRP指定一个IoCompletion例程，也没有完成IRP。删除IRP由父总线驱动程序完成。
1.3.3.2 删除过滤器驱动程序里的一个设备

当删除一个设备时，一个过滤器驱动程序必须取消它执行用于添加和启动设备的任何操作。当删除一个设备时，一个过滤器驱动程序遵从与功能驱动程序基本相同的步骤。
1.3.3.3 删除总线驱动程序里的一个设备

当删除一个子设备（子PDO）时，父总线驱动程序必须取消它执行来添加和启动设备的任何操作。

一个总线驱动程序在它的DispatchPnP例程里用如下的步骤删除一个子设备：

1. 是否驱动程序已经处理过这个PDO的一个从前的请求？

如果是这样的话，执行任何尚存的清理并跳到第四步。

一个驱动程序在设备扩展里典型地维护一个标记，这个标记说明了是否驱动程序已经处理过设备的一个IRP_MN_SURPRISE_REMOVAL请求。

2. 完成在驱动程序里排队等待的任何请求。

3. 从设备删除电源，如果总线驱动程序能够这样做的话，通过调用 PoSetPowerState 通知电源管理器。

总线驱动程序掉电子设备，如果可能的话，通知电源管理器电源状态里设备的改变。总线驱动程序做这些来响应IRP_MN_REMOVE_DEVICE IRP；当设备被删除时，设备的电源策略所有者并不发送一个IRP_MN_SET_POWER IRP。看第3部分获得电源管理的更多信息。

4. 如果总线驱动程序在它最近为 BusRelations 而对IRP_MN_QUERY_DEVICE_RELATIONS请求的响应中报告过这个设备，设备仍旧物理地存在于机器上。在这种状况下，总线驱动程序将：

n 为设备保留PDO直到设备已经物理地删除

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS

n 用 IoCompleteRequest 完成IRP

n 从DispatchPnP例程返回

总线驱动程序继续在随后的枚举里（BusRelations 的IRP_MN_QUERY_DEVICE_ RELATIONS）报告这个设备，直到设备被物理地删除。PnP管理器跟踪是否一个枚举的设备已经被添加和启动。

5. 如果设备以前不包括在总线驱动程序为了 BusRelations 而响应大部分最近的一个IRP_MN_QUERY_DEVICE_RELATIONS请求时，总线驱动程序认为设备将要物理地从机器上删除。在这样的状况下，总线驱动程序做下面的工作：

n 清除设备专用的分配、内存、事件等等

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS

n 用 IoCompleteRequest 完成IRP

n 用 IoDeleteDevice 释放PDO

如果驱动程序过去忽略了它的大多数最近的 BusRelations 列表里的设备，总线驱动程序必须删除PDO。如果一个用户再次把设备插入到机器里，总线驱动程序必须生成一个新的PDO来响应下一个 BusRelations 查询。如果一个总线驱动程序为一个新的设备实例重新利用相同的PDO时，机器将不能正常操作。

n 从DispatchPnP例程返回

如果当PnP管理器发送IRP_MN_REMOVE_DEVICE请求时设备仍旧存在，则总线驱动程序保留PDO。如果后来的某个时候设备物理地从总线里删除，PnP管理器发送另外的一个IRP_MN_REMOVE_DEVICE。在接收到随后的删除IRP之后，总线驱动程序删除设备的PDO。

总线驱动程序必须能够处理已经删除的设备的一个IRP_MN_REMOVE_DEVICE，并且这个设备的PDO标记为删除。为响应这样的一个IRP，总线驱动程序能够使IRP成功或者返回STATUS_NO_SUCH_DEVICE。尽管总线驱动程序以前对 IoDeleteDevice 的调用，由于一些组件仍旧有这个对象的引用，设备的PDO在这种情况下还没有删除。这样，在处理第二个删除IRP过程中，总线驱动程序能够访问PDO。总线驱动程序不可以为了PDO而两次调用 IoDeleteDevice；当I/O系统的引用计数为零时，它删除PDO。

直到总线驱动程序接收到一个设备的一个IRP_MN_REMOVE_DEVICE请求时，它才删除一个子设备的数据结构。一个总线驱动程序可能发现一个设备已经删除并且调用 IoInvalidateDeviceRelations，但是，直到发送一个IRP_MN_REMOVE_DEVICE请求，它才可以删除设备的PDO。
1.3.4 处理一个IRP_MN_CANCEL_REMOVE_DEVICE请求

为响应一个IRP_MN_CANCEL_REMOVE_DEVICE请求，一个设备的驱动程序必须返回设备到这种状态，即驱动程序接收IRP_MN_QUERY_REMOVE_DEVICE请求之前的状态。典型地，驱动程序返回设备为启动的状态。

除过发送一个IRP_MN_CANCEL_REMOVE_DEVICE给设备之外，如果有的话，PnP管理器发送IRP给设备已删除的相关设备。PnP管理器也发送一个取消删除IRP给设备的子代。

在IRP_MN_CANCEL_REMOVE_DEVICE请求完成之后，PnP管理器调用任何 EventCategoryTargetDeviceChange 通知回调。通过调用 IoRegisterPlugPlayNotification，这样的回调注册在设备上。PnP管理器也通过调用 RegisterDeviceNotification 来调用为这样的通知所注册的任何用户模式组件。

一个IRP_MN_CANCEL_REMOVE_DEVICE请求必须首先被一个设备的父总线驱动程序所处理，然后被设备堆栈里每个更高级的驱动程序所处理。一个驱动程序在它的DispatchPnP例程里处理删除IRP。

一个驱动程序在它的DispatchPnP例程里使用一个如下的步骤处理一个IRP_MN_CANCEL_ REMOVE_DEVICE请求：

1. 在一个功能或过滤器驱动程序里，延迟启动设备直到更低层的驱动程序已经完成它们的重新启动操作。

一个功能或过滤器驱动程序设置一个IoCompletion例程，向下传递IRP_MN_CANCEL_ REMOVE_DEVICE到设备堆栈，并且延迟它的启动操作直到更低层的驱动程序已经处理完IRP。（看第2章2.3节。）

2. 在一个更低层的驱动程序完成之后，返回设备到以前的PnP状态。

驱动程序返回设备到在它接收到IRP_MN_QUERY_REMOVE_DEVICE请求之前的状态。典型地，驱动程序返回设备为启动的状态。确切的操作取决于设备和驱动程序。

3. 设置 Irp->IoStatus.Status 为STATUS_SUCCESS和用 IoCompleteRequest 完成IRP。

当与任何PnP IRP一起时，一个总线驱动程序完成IRP。

一个功能或过滤器驱动程序也完成了IRP，在这种状况下，由于驱动程序的IoCompletion例程通过返回STATUS_MORE_PROCESSING_REQUIRED中断了完成处理。

驱动程序必须使这个IRP成功，如果任何驱动程序使这个IRP失效，设备将处于一个不一致的状态。

当设备启动和运行时，一个驱动程序也许会接收到一个伪取消删除请求。这种情况可能发生，例如，如果一个驱动程序（或设备堆栈里的一个更高级的驱动程序）在过去使一个IRP_MN_QUERY_REMOVE_DEVICE请求失效过。当一个设备启动和运行时，一个驱动程序简单地使一个设备的伪取消删除请求成功。
1.3.5 处理一个IRP_MN_SURPRISE_REMOVAL请求

Windows2000 PnP管理器发送这个IRP来通知驱动程序一个设备已经意外地从机器中被删除，并且将不再能够处理I/O（“突然的”删除）。

所有的PnP驱动程序必须处理这个IRP，适当的IRP操作能够使驱动程序和PnP管理器来做下面的事情：

1. 释放分配给设备的硬件资源并使它们对另一个设备可用。

只要一个设备从机器中物理地删除，它的硬件资源就应该被释放。然后，PnP管理器能够重新分配资源给另一个设备，包括相同的设备，这可能是一个用户热插回到机器中的设备。

2. 最小化数据丢失和系统崩溃的风险。

当一个用户删除一个热插拔设备而没有首先使用Unplug或Eject Hardware小应用程序，突然删除能够挂起机器，使机器崩溃，和/或引起数据丢失。

避免这样崩溃最好的方法是对设备厂商来说为热插拔设备实现一个锁定机制。这样一个机制保证设备在一个受控制的形式中删除。PnP管理器警告访问设备的组件正在删除设备，这样它们能够执行有序的清除和删除。

假如一个设备没有一个锁定机制，或者它被附着到另外一个没有锁定的设备上，它的驱动程序必须处理一个IRP_MN_SURPRISE_REMOVAL请求。处理这个IRP没有阻止一个突然的删除崩溃，但是只要删除被检测到且驱动程序处理了IRP，它就通过停止设备的I/O而最小化了影响。

在下面的任一方法中一个突然的删除被检测：

n 如果一个总线有热插拔通知，它将设备已经消失的消息通知给设备的父总线驱动程序，总线驱动程序调用 IoInvalidateDeviceRelations。作为响应，PnP管理器查询它的子设备的总线驱动程序（BusRelations 的IRP_MN_QUERY_DEVICE_RELATIONS）。PnP管理器确定设备不在子设备的新列表里，并且初始化设备的突然删除操作。

n 总线因另外一个的原因被枚举，突然被删除的设备不包括在它的子设备列表里。PnP管理器初始化它的突然删除操作。

n 设备的功能驱动程序判定设备不再存在（例如，由于它的请求重复地超时），总线也许是可枚举的，但是它没有热插拔通知。在这种状况下，功能驱动程序调用 IoInvalidateDeviceState。作为响应，PnP管理器给设备堆栈发送一个IRP_MN_QUERY_PNP_DEVICE_STATE请求。功能驱动程序在PNP_DEVICE_STATE的位屏蔽里设置PNP_DEVICE_FAILED标记来说明设备已经失效。

PnP管理器在系统线程环境的IRQL PASSIVE_LEVEL上发送一个IRP_MN_SURPRISE_ REMOVAL。

在通知用户模式应用程序和其他的内核模式组件之前PnP管理器给驱动程序发送这个IRP。在完成IRP之后，PnP管理器用为设备上这个通知而注册的内核模式组件的GUID_TARGET_DEVICE_REMOVE_COMPLETE，发送一个 EventCategoryTargetDevice-Change 通知。

IRP_MN_SURPRISE_REMOVAL IRP首先被设备堆栈里顶层的驱动程序所处理，然后被每个下一个较低层的驱动程序所处理。所有的PnP驱动程序必须处理这个IRP，并必须设置STATUS_SUCCESS。

在驱动程序的AddDevice例程调用之后，一个PnP设备的驱动程序必须准备随时处理一个IRP_MN_SURPRISE_REMOVAL。

为响应一个IRP_MN_SURPRISE_REMOVAL，一个驱动程序必须做下面的工作：

1. 阻止设备的任何新的I/O。

一个驱动程序应该处理随后的关闭、清除、电源和PnP请求，但是驱动程序必须阻止设备的任何新的I/O。驱动程序使用后续的IRP（如果设备存在，驱动程序将处理这些IRP。）失效，包括关闭、清除和PnP IRP。

一个驱动程序能够在设备扩展里设置一个位来说明设备已经被突然的删除，驱动程序的调度例程应该检查这个位。

2. 失效设备上的未完成的I/O请求。

3. 继续向下传递任何驱动程序没有为设备处理的IRP。

4. 在一个父总线驱动程序里，如果驱动程序能够这样做的话，掉电总线插槽。调用 PoSetPowerState 来通知电源管理器。

5. 用 IoSetDeviceInterfaceState 使设备接口不可用。

6. 释放设备的硬件资源（中断、I/O端口、内存寄存器和DMA通道）。

7. 清除任何设备专用的分配、内存、事件等等。

一个驱动程序可能延迟这样的清除直到它接收到随后的IRP_MN_REMOVE_DEVICE请求，但是如果一个早期的组件有一个不能够被关闭的打开句柄，删除IRP将永不会被发送。

8. 使设备对象附着到设备堆栈。

不分离和删除设备对象直到随后的IRP_MN_REMOVE_DEVICE请求为止。

9. 完成IRP。

在一个功能或过滤器驱动程序里：

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS。

n 用 IoSkipCurrentIrpStackLocation 定位下一个堆栈位置并用 IoCallDriver 传递IRP给下一个较低层驱动程序。

n 当从DispatchPnP例程返回状态时，从 IoCallDriver 传播此状态。

n 不要完成IRP。

在一个总线驱动程序里（该驱动程序处理这个IRP的一个子PDO）：

n 设置 Irp->IoStatus.Status 为STATUS_SUCCESS。

n 用IO_NO_INCREMENT完成IRP（IoCompleteRequest）。

n 从DispatchPnP例程返回。

在这个IRP成功以及设备的所有的打开句柄关闭之后，PnP管理器发送一个IRP_MN_REMOVE_DEVICE请求给设备堆栈。为响应这个删除IRP，驱动程序从堆栈里分离它们的设备对象并删除它们。如果一个早期的组件有一个设备的打开句柄，尽管I/O失效，却让句柄打开，则PnP管理器从不发送删除IRP。

所有的驱动程序应该处理这个IRP，并注意到设备已经物理地从机器中删除。然而，如果一些驱动程序没有处理IRP，它们将不会引起不利的结果。举例而言，一个没有消耗系统硬件资源并留存于一个协议基础的总线上的设备，如USB或1394，由于它没有消耗任何资源，所以不能够与硬件资源连接。由于功能和过滤器驱动程序仅仅通过父总线驱动程序访问设备，在设备已经被删除之后，驱动程序试图访问设备的努力没有危险。由于总线支持删除通知，当设备消失和总线驱动程序使随后所有的访问设备的努力失效时，通知父总线驱动程序。

Windows98 PnP管理器没有发送这个IRP。如果一个用户删除一个设备而没有首先使用适当的用户接口，PnP管理器仅仅发送一个IRP_MN_REMOVE_DEVICE请求给设备的驱动程序。所有的WDM驱动程序必须处理IRP_MN_SURPRISE_REMOVAL和IRP_MN_REMOVE_DEVICE。IRP_MN_REMOVE_DEVICE的代码应该检查是否一个驱动程序接收到一个前期的突然删除IRP，并应该处理这两种情况。


使用PnP通知

在一个即插即用（PnP）的环境里，驱动程序和应用程序需要对机器里的设备配置的变化作出反应。例如，一个应用程序需要知道何时感兴趣的设备已经被添加到机器上，而一个驱动程序需要知道何时一个变化发生在一个特定的设备上。

当特定的PnP事件发生时，PnP管理器提供一个机制来通知驱动程序和应用程序。。本章描述了在内核模式代码里怎样使用PnP通知。用户模式应用程序的编写者应该看平台SDK文档获得关于 RegisterDeviceNotification 功能和相关的功能的信息。

本章包括以下的主题：

1.1 PnP通知总览

1.2 编写PnP通知回调例程指南

1.3 使用PnP设备接口改变通知

1.4 使用PnP目标设备改变通知

1.5 使用PnP硬件profile改变通知

1.6 使用PnP定制通知

参见《Windows2000驱动程序开发参考》的卷一获得当一个设备的电源状态变化时关于使用 PoRegisterDeviceNotify 来为通知注册。为了了解系统状态、时间和策略变化的通知的有关内容，请看内核模式驱动程序设计指南里有关系统定义的回调的信息。

1.1 PnP通知总览

当特定的事件发生在指定的设备上或者在整个系统上时，PnP管理器提供一个机制来通知的驱动程序和应用程序。一个驱动程序能够为下面种类的事件的通知而注册。

n EventCategoryInterfaceChange

当一个驱动程序为设备接口上这个种类的事件而注册时，PnP管理器通知驱动程序下面的事件：

n GUID_DEVICE_INTERFACE_ARRIVAL

表明一个特定类的设备接口已经启用。例如，一个用户给机器添加一个新的硬盘和卷管理器使一个新的卷可用（类“卷”的一个设备接口）。

n GUID_DEVICE_INTERFACE_REMOVAL

表明指定类的一个设备接口已经被禁止。

在《Windows2000驱动程序开发参考》的卷一中参看 IoRegisterDeviceInterface 和相关的例程获得设备接口的更多信息。

n EventCategoryTargetDeviceChange

当一个驱动程序为设备上这类事件注册时，当在设备上发生下面的事件时，PnP管理器通知驱动程序：

n GUID_TARGET_DEVICE_QUERY_REMOVE

表明PnP管理器准备删除设备的驱动程序。几种操作能够引起这个事件，包括一个用户请求从机器上删除指定的设备，或者一个用户发布设备的一个更新驱动程序请求。这个通知请求设备的驱动程序或者支持或者否决到来的删除操作。

n GUID_TARGET_DEVICE_REMOVE_COMPLETE

表明指定的设备已经从机器中删除，或者一个用户正在改变设备的驱动程序。

n GUID_TARGET_DEVICE_REMOVE_CANCELLED

表明指定的设备上的一个到来的删除操作已经被取消。

n GUID_Xxx（定制事件）

表明一个定制事件已经发生在一个指定的设备上。

你能够定义设备的一个定制事件。当驱动程序（或另外的相关组件）通知PnP管理器定制事件已经发生时，PnP管理器通知设备上为目标设备的改变通知而注册的任何组件。

不象设备接口的改变的注册过程，它可被认为是设备接口里一个“被动的”权利，而为目标设备的改变的注册表明设备里一个“主动的”权利。

n EventCategoryHardwareProfileChange

本类包括下面的事件：

n GUID_HWPROFILE_QUERY_CHANGE

表明一个用户已经请求改变机器的硬件profile。PnP管理器使用这个通知询问已注册的组件，是否它能够改变硬件profile而没有中断系统操作。已注册的组件典型地使这些查询请求成功。

n GUID_HWPROFILE_CHANGE_COMPLETE

表明一个机器的硬件profile已经改变。如果一个驱动程序维护profile专用的设置，它应该在一个硬件profile改变之后更新这些设置。

n GUID_HWPROFILE_CHANGE_CANCELLED

表明一个到来的硬件profile改变已经被取消。

为了内核模式组件，PnP通知如下工作：

1. 通过调用 IoRegisterPlugPlayNotification，驱动程序注册一个种类事件的通知。

一个PnP通知的回调例程保留被注册状态直到驱动程序明确地删除注册。

2. 当已注册的种类里有一个事件发生时，PnP管理器调用驱动程序的回调例程。

3. 驱动程序通过调用 IoUnregisterPlugPlayNotification 删除回调注册。

对一个驱动程序来说，在一个关闭的过程中生成一个同步事件或者等待一个同步事件是非法的。

为获得PnP通知的更多信息，看本章里下面的部分：

1.1 编写PnP通知回调例程指南

1.2 使用PnP设备接口改变通知

1.3 使用PnP目标设备改变通知

1.4 使用PnP硬件profile改变通知

1.5 使用PnP定制通知

1.2 编写PnP通知回调例程的指南

PnP管理器在IRQL PASSIVE_LEVEL上调用通知回调例程。

为保证PnP子系统的平稳操作，一个PnP通知回调例程必须遵循下面的指南：

1. 一个通知回调例程不能够阻塞。

2. 一个通知回调例程不能够调用、或者引起对同步例程或任何例程的调用。在这里，同步例程生成PnP事件；这些任何例程则阻塞了等待设备安装或删除。

在一个通知回调过程中调用这样的例程能够引起系统死锁。

举个例子，一个驱动程序不能够在一个通知回调例程里调用 IoReportTargetDeviceChange，相反，它应该调用 IoReportTargetDeviceChangeAsynchronous。

3. 一个通知回调例程应该为它没有明确失效的任何事件返回成功状态。

当一个驱动程序为一个事件种类上的通知注册时，PnP管理器通知驱动程序那个种类里的所有事件，包括当前的和将来的。如果一个驱动程序为它没有处理的事件返回一个错误状态，驱动程序因错误而将冒险使一个新的查询事件失效。

例如，当驱动程序使一个查询通知失效而否决正被处理的事件时，一个驱动程序正确地返回一个错误状态。

4. 一个通知回调例程应该是可分页的代码。

1.3 使用PnP设备接口改变通知

一个驱动程序为 EventCategoryDeviceInterfaceChange 通知而注册，以便当机器上一个特别类的设备接口到达（可用的）或者被删除（不能用的）时，它能够被通知。例如，一个合成的电池驱动程序可能为电池类的设备接口的通知来注册，这样它就能够提供完整可用的电池电力信息给操作系统。

下面的子部分讨论怎样注册设备接口的改变通知和怎样在一个PnP通知回调例程里处理设备接口改变事件。

在《Windows2000驱动程序开发参考》的卷一中参看 IoRegisterDeviceInterface 和相关的例程，来获得设备接口的有关信息。
1.3.1 为设备接口改变通知注册

通过调用 IoRegisterPlugPlayNotification，一个驱动程序为设备接口的到达和删除事件的通知注册。

在Windows2000驱动程序开发参考的卷一中参看 IoRegisterPlugPlayNotification 获得关于调用例程的总体信息，下面的信息应用于设备接口改变通知而调用这个例程：

n 指定 EventCategoryDeviceInterfaceChange 的一个 EventCategory。

n EventCategoryData 必须指向设备接口类的GUID。

一个接口类的GUID在典型情况下带有支持接口的结构、常数等等的头文件里被定义。

n 指定PNPNOTIFY_DEVICE_INTERFACE_INCLUDE_EXISTING_INTERFACES的一个 EventCategoryFlags。

这个标记指示PnP管理器，为指定类的设备接口在将来的到达和离开而注册回调例程，并立即为已经运行的相关设备接口调用回调例程。

一个驱动程序能够调用 IoGetDeviceInterfaces 来得到一个特定类存在的接口的一个列表，然后注册它的回调例程而不用这个标记，但是使用标记变得更容易，且避免了潜在的时钟问题。

n 指定一个驱动程序定义的环境，如果适当的话，PnP管理器将传递它给回调例程。

打开一个设备的句柄来响应设备接口的到达通知的驱动程序，将在设备上为 EventCategoryTargetDeviceChange 事件注册。（参见4.4节。）

一个驱动程序通过用由 IoRegisterPlugPlayNotification 返回的 NotificationEntry 调用 IoUnregisterPlugPlayNotification 来取消通知注册。
1.3.2 处理设备接口改变事件

当一个驱动程序或者一个用户模式组件能够允许或者禁止一个设备接口，PnP管理器调用设备接口类上为 EventCategoryDeviceInterfaceChange 注册的任何通知回调例程。PnP管理器指定GUID_DEVICE_INTERFACE_ARRIVAL或GUID_DEVICE_INTERFACE_REMOVAL的一个 NotificationStructrue.Event。

当处理一个GUID_DEVICE_INTERFACE_ARRIVAL事件时，为了处理新的接口，一个通知回调例程应该执行驱动程序定义的任务。

例如，一个回调例程可能排队一个工作者例程来打开设备。一个回调例程不必直接打开设备。如果接口的提供者引起了阻塞PnP事件，且如果通知回调例程在回调线程里试图打开设备时，它能够引起一个锁死。

一个回调例程可能使它自己的一个设备接口能够响应新设备接口的可用性。

当处理一个GUID_DEVICE_INTERFACE_REMOVAL事件时，当接口启用时，一个通知回调例程应该取消它执行的任何操作。

1.4 使用PnP目标设备改变通知

一个驱动程序在一个设备上为 EventCategoryTargetDeviceChange 通知而注册，这样当驱动程序将要被删除时，驱动程序能够被通知。例如，如果一个驱动打开设备的一个句柄，驱动程序应该在一个设备上为 EventCategoryTargetDeviceChange 通知而注册，这样当PnP管理器需要删除设备时，驱动程序能够关闭它的句柄。

驱动程序也能够为定制通知使用 EventCategoryTargetDeviceChange 通知。（参见4.6节）

下面的子部分讨论了怎样注册目标设备的改变通知，和怎样在一个PnP通知回调例程里处理目标设备改变事件。

4.4.1 注册目标设备改变通知

4.4.2 处理一个GUID_TARGET_DEVICE_QUERY_REMOVE事件

4.4.3 处理一个GUID_TARGET_DEVICE_REMOVE_COMPLETE事件

4.4.4 处理一个GUID_TARGET_DEVICE_REMOVE_CANCELLED事件

参见第3章当PnP管理器发送删除IRP给一个设备的驱动程序时，启动、停止和删除设备的有关描述。
1.4.1 注册目标设备改变通知

通过调用 IoRegisterPlugPlayNotification，一个驱动程序注册了目标设备改变事件的通知。

在《Windows2000驱动程序开发参考》的卷一中阅读 IoRegisterPlugPlayNotification 获得调用例程的总体信息。下面的信息应用于目标设备改变通知的调用例程：

n 指定 EventCategoryTargetDeviceChange 的一个 EventCategory。

n EventCategoryData 必须为了设备指向文件对象，在这个设备上通知被请求。

如果驱动程序的回调例程需要访问文件对象，驱动程序应该在调用 IoRegisterPlugPlayNotification 之前删除文件对象上的一个引用。

如果驱动程序的回调例程不需要访问文件对象，驱动程序不许要引用该对象。

在文件对象关闭之后，驱动程序继续为设备接收通知直到驱动程序删除它的通知注册。举个例子，这个设计允许驱动程序接收GUID_TARGET_DEVICE_REMOVE_CANCELLED事件的通知。

n 指定一个驱动程序定义的、PnP管理器将传递给回调例程的环境。

一个驱动程序可能使用环境参数来维护文件对象当前的状态信息（如是否它已经被关闭/删除）。

一个驱动程序也可能使用环境来存储它最初用来打开设备的路径。在一个取消的删除操作之后，一个驱动程序能够使用这个例程重新打开设备。（参见4.4.4节获得更多信息。）

一个驱动程序通过用由 IoRegisterPlugPlayNotification 返回的 NotificationEntry 调用 IoUnregisterPlugPlayNotification 来删除一个通知注册。如果过去当驱动程序为通知注册时已经删除文件对象上的一个引用，且该引用仍旧未完成，驱动程序必须在它删除注册表之后释放引用。
1.4.2 处理一个GUID_TARGET_DEVICE_QUERY_REMOVE事件

在PnP管理器给一个设备的驱动程序发送一个IRP_MN_QUERY_REMOVE_DEVICE IRP之前，它调用设备上为 EventCategoryTargetDeviceChange 而注册的任何通知回调例程。PnP管理器指定GUID_TARGET_DEVICE_QUERY_REMOVE的一个 NotificationStructrue.Event。

作为这样的一个通知的响应，回调例程决定设备是否可被删除而没有中断系统。

如果不应该删除设备，回调例程返回STATUS_SUCCESSFUL。为响应这个状态，PnP管理器放弃查询删除处理，且设备不会被删除。

如果能够被删除设备，回调例程应该执行任何适当的操作来准备删除设备，如关闭设备上的任何句柄打开（如果可能的话）。如果在设备上句柄维持打开状态，PnP管理器不能够删除设备并放弃查询删除处理。

当成功地处理一个GUID_TARGET_DEVICE_QUERY_REMOVE事件时，一个通知回调例程应该：

n 关闭设备的任何打开句柄。

n 如果驱动程序在文件对象上有一个未完成的引用，间接访问文件对象。

n 保留将来的 EventCategoryTargetDeviceChange 通知的注册状态，由于到来的删除操作也许会取消，所以这是重要的。

关闭设备的一个句柄没有取消为PnP目标设备改变通知而注册一个驱动程序。PnP管理器仍旧能够调用驱动程序的通知回调例程，但是在这样的调用里，在 NotificationStructure 里的文件对象是不合法的。
1.4.3 处理一个GUID_TARGET_DEVICE_REMOVE_COMPLETE事件

PnP管理器在给一个设备的驱动程序发送IRP_MN_REMOVE_DEVICE IRP之前，它调用设备上为 EventCategoryTargetDeviceChange 注册的任何内核模式通知回调例程。PnP管理器指定GUID_TARGET_DEVICE_REMOVE_COMPLETE的一个 NotificationStructrue.Event。

当处理一个GUID_TARGET_DEVICE_REMOVE_COMPLETE事件时，一个通知回调例程应该：

n 删除设备上的通知注册。

设备已经删除，这样驱动程序调用 IoUnregisterPlugPlayNotification 来删除通知注册。

机器中设备可能仍旧物理地存在，但是所有的设备对象已经被删除，且设备是不可用的。

n 如果驱动程序没有接收到一个从前的查询删除通知时，执行突然删除处理。

如果一个驱动程序是突然的删除，PnP管理器给已注册的驱动程序发送一个删除完成通知而不必有一个先前的查询删除通知。在这种状况下，一个驱动程序必须执行任何必要的清除，如关闭设备的任何句柄并删除任何文件对象的未完成引用。
1.4.4 处理一个GUID_TARGET_DEVICE_REMOVE_CANCELLED事件

如果一个IRP_MN_QUERY_REMOVE_DEVICE 请求失效，PnP管理器给设备的驱动程序发送一个IRP_MN_CANCEL_REMOVE_DEVICE IRP。在取消删除IRP成功完成之后，调用设备上为 EventCategoryTargetDeviceChange 注册的任何通知回调例程。PnP管理器指定一个GUID_TARGET_DEVICE_REMOVE_CANCELLED的 NotificationStructrue.Event。

当处理一个GUID_TARGET_DEVICE_REMOVE_CANCELLED事件时，一个通知回调例程应该为目标设备通知注册。

由于驱动程序关闭了以前的注册句柄来响应查询删除通知，驱动程序必须打开一个新的句柄。驱动程序必须做下面的事情：

1. 用 IoUnregisterPlugPlayNotification 删除旧的注册。

2. 打开设备的一个新的句柄。

3. 用 IoRegisterPlugPlayNotification 为新的句柄上的通知而注册。

1.5 使用PnP硬件profile改变通知

一个驱动程序为 EventCategoryTargetDeviceChange 通知注册，以便当机器从一个硬件profile转变为另一个时驱动程序能够被通知，例如，当一个袖珍计算机被连接或断开连接时，一个驱动程序能够使用这个机制被通知。

下面的子部分讨论怎样注册硬件profile改变通知和怎样在一个PnP通知回调例程里处理硬件profile改变事件。

4.3.1 注册硬件profile改变通知

4.3.2 处理硬件profile改变事件
1.5.1 注册硬件profile改变通知

一个驱动程序通过调用 IoRegisterPlugPlayNotification 来注册硬件profile的改变通知。

在《Windows2000驱动程序开发参考》的卷一中参看 IoRegisterPlugPlayNotification 获得调用例程的总体信息。下面的信息应用于为硬件profile改变通知而调用这个例程：

n 指定 EventCategoryHardwareProfileChange 的一个 EventCategory。

n EventCategoryData 必须为NULL。

n 指定一个驱动程序定义的环境，如果适当，PnP管理器将传递它给回调例程。

一个驱动程序通过用由 IoRegisterPlugPlayNotification 返回的 NotificationEntry 调用 IoUnregisterPlugPlayNotification 来删除通知注册。
1.5.2 处理硬件profile改变事件

在一个硬件profile改变的特定时间，PnP管理器调用为 EventCategoryHardwareProfileChange 而注册的回调例程：

n 在机器的硬件profile改变之前，PnP管理器调用已注册的通知回调例程，并指定GUID_HWPROFILE_QUERY_CHANGE的一个 NotificationStructrue.Event。

n 在一个机器的硬件profile改变完成之后，PnP管理器调用已注册的通知回调例程，并指定GUID_HWPROFILE_CHANGE_COMPLETE的一个 NotificationStructrue.Event。

n 如果机器的硬件profile改变取消，PnP管理器调用已注册的通知回调例程，并指定GUID_HWPROFILE_CHANGE_CANCELLED的一个 NotificationStructrue.Event。

对一个GUID_HWPROFILE_QUERY_CHANGE事件，PnP管理器调用用户模式回调例程，然后调用内核模式回调例程。为响应一个GUID_HWPROFILE_QUERY_CHANGE事件，典型情况下，一个驱动程序的通知回调例程返回STATUS_SUCCESS。

对一个GUID_HWPROFILE_CHANGE_COMPLETE事件，PnP管理器调用内核模式回调例程，然后调用用户模式回调例程。为响应这样的一个事件，一个驱动程序的回调例程可能更新它的硬件profile专用设置。

对一个GUID_HWPROFILE_CHANGE_CANCELLED事件，PnP管理器调用内核模式回调例程，然后调用用户模式回调例程。为响应这样的一个事件，典型情况下，一个驱动程序的回调例程返回STATUS_SUCCESS。如果驱动程序执行任何操作来响应GUID_HWPROFILE_QUERY_CHANGE事件，驱动程序取消这些操作来响应取消事件。

1.6 使用PnP定制通知

通知能够使用目标设备改变通知机制的驱动程序设备上的定制事件。

程序编辑器定义了定制事件必须作下面的事情：

1. 为定制事件定义一个新的GUID。

用 uuidgen 或 guidgen 生成，在一个适当的头文件和文档里发布GUID。

2. 编写代码来触发定制事件。

在内核模式里，一个驱动程序用定制GUID和设备的一个PDO指针调用 IoReportTargetDeviceChange，定制事件仅能够从内核模式被触发。

一个驱动程序编写者在一个如下的步骤里使用定制通知：

1. 驱动程序（或应用程序）为定制事件的通知注册。

在内核模式里，一个驱动程序调用 IoRegisterPlugPlayNotification，并为设备上的一个 EventCategoryTargetDeviceChange 注册。

在用户模式里，一个应用程序注册使用 RegisterDeviceNotification。参见SDK平台获得更多的信息。

2. 一个内核模式组件触发定制事件。

3. PnP管理器调用设备上已注册的通知例程。

它先调用被注册的用户模式回调例程，然后调用内核模式回调例程。

4. 当用户模式通知完成时，内核模式驱动程序通知回调例程响应该定制事件。

参看4.2节获得通知回调例程的总体指南，除过这些指南外，一个定制通知回调例程不必从回调例程线程内打开一个设备的句柄。


支持多功能设备

一个多功能设备占用其父总线上的一个位置，但是有不止一个功能。在一个多功能设备里，单个的功能是独立的：它们不能够有启动顺序依赖；一个功能的资源需求不能够按照另外一个功能资源表达（例如，功能1使用I/O端口X和功能2使用端口X+200）；且每个功能必须能够作为一个分割的设备被操作，即使它作为另一个功能驱动程序而被相同的驱动程序所服务。

为了一个多功能设备在微软的Windows2000上正确地配置，设备上的每个功能必须被枚举并且每个功能的资源需求必须与PnP管理器通信。这里也必须有INF文件和每个功能的驱动程序 。负责这些的任务的每个组件依靠设备的父总线的多功能标准，设备遵照标准决定其依赖的范围，且其能力取决于父总线驱动程序。

本章包括下面的部分：

5.1 支持多功能PC Card设备

5.2 支持多功能PCI 设备

5.3 支持其他总线上的多功能设备

5.4 使用系统提供的mf.sys

5.5 为多功能设备建立资源图

在《Windows2000驱动程序开发参考》的卷一第3部分里参看INF文件部分和命令来获得INF文件语义的全面信息。

参看本指南的第1部分获得诸如功能驱动程序、总线驱动程序和它们相关联的设备对象等的PnP术语信息。

本章描述了怎样在Windows2000平台上支持多功能设备。

1.1 支持多功能PC Card设备

PC Card多功能标准说明一个多功能设备为每个功能在属性内存里有一套配置寄存器。这些寄存器允许PCMCIA总线驱动程序，例如，能够使每个功能独立并定义一个不包括在每个功能里的I/O资源的范围。该标准也说明一个多功能设备在属性内存里包含每套配置寄存器的地址。这些地址能够使PCMCIA总线驱动程序对配置寄存器编程。

如果一个16位PC Card设备完全地和正确地执行PC Card多功能标准，这样的一个设备的厂商有最小的INF和驱动程序需求来保证设备在Windows2000上正确配置。参见5.1.1节获得更多的信息。

如果一个16位PC Card设备没有完全执行PC Card多功能标准，厂商必须在INF文件里提供丢失的信息。有两个方法可以使一个PC Card多功能设备可能不执行多功能标准：

1. 设备为每个功能实现一套多功能配置寄存器，但是不包含所有套的寄存器在它的属性内存里的定位。

2. 设备没有为每个功能实现一套多功能配置寄存器。

如果一个设备有以上列出的限制，且如果设备的INF在 DDInstall.LogConfigOverride部分里有必须的信息，PCMCIA总线驱动程序能够编程配置寄存器。阅读下面的部分获得更多的信息：

1.1.2 支持有不完全配置寄存器地址的PC Card

1.1.3 支持有不完全配置寄存器的PC Card

插件总线设备主要地遵循多功能规则，参见支持多功能PC Card设备。
1.1.1 支持遵照多功能标准的PC Card

如果一个16位ISA类型的PC Card设备完全地和正确地执行PC Card多功能标准，这样的一个设备的厂商在Windows2000上能够依靠下面系统提供的组件处理多功能语义的软件方面：

1. 支持多功能设备的一个INF文件。（系统提供的）

PCMCIA总线驱动程序为设备指定了一个设备硬件ID，这引起配置管理器使用系统提供的多功能INF文件（mf.inf）来配置设备。mf.inf 文件指定了类“多功能”和它相关联的GUID（如在 devguid.h 里定义的）。

2. 支持多功能设备的一个功能驱动程序。（系统提供的）

mf.inf 文件指定了系统提供的多功能总线驱动程序（mf.sys）作为支持设备的功能驱动程序。

mf.sys 总线驱动程序枚举设备的功能数目，PCMCIA总线驱动程序读设备上的配置寄存器来决定每个功能的资源需求。

参见5.4节获得使用系统提供的 mf.sys 驱动程序的更多信息。

一个遵照标准的多功能PC Card设备的厂商必须为单个的功能提供下面的支持：

1. 设备的每个功能有一个PnP功能驱动程序。（厂商提供的）

由于多功能总线驱动程序处理多功能语义，因此功能驱动程序和这个功能被包装为单独的设备而使用的驱动程序相同。

2. 设备的每个功能一个INF文件。（厂商提供的）

如果功能被打包为单个的设备的话，INF文件能够是将被使用的相同的文件。INF文件不需要任何特别的多功能语义。

PCMCIA总线驱动程序建立子功能硬件ID

对一个真的多功能PC Card设备，PCMCIA总线驱动程序与 mf.sys 一道，生成子设备功能的硬件ID，这些ID有下面的格式：

<Manufacturer-name>-<Producu-ID-string>-DEV<number>-CRC

在这个格式里，<number>是功能从0开始的数字。

例如，PCMCIA总线驱动程序生成如下的子设备硬件ID：

3COM_Coporation-3C562D/3C563D-DEV0-4893

3COM_Coporation-3C562D/3C563D-DEV1-4893

一个多功能PC Card设备的一个子功能INF文件必须指定硬件ID，该硬件ID由PCMCIA总线驱动程序和 mf.sys 报告。
1.1.2 支持有不完全配置寄存器地址的PC Card

Register Address

如果一个多功能16位PC Card设备有为每个功能的配置寄存器，但是在属性内存里不包含所有寄存器设置（不支持LONGLINK_MFC元组）的指针，这样的设备厂商在Windows2000上能够使用系统提供的多功能总线驱动程序（mf.sys），但是必须提供一个定制的INF文件和单个功能的支持。

这样的设备的厂商能够在Windows2000上为多功能设备使用系统提供的功能驱动程序。

设备的一个定制的INF必须指定 mf.sys 作为设备的功能驱动程序，系统提供的 mf.sys 驱动程序然后将枚举设备的功能。

参看5.4部分获得使用系统提供的 mf.sys 驱动程序的更多信息。

这样的设备的厂商在Windows2000上必须提供下面的设备：

1. 多功能设备的一个定制的INF文件。（厂商提供的）

厂商必须提供一个多功能INF文件，该文件指定 mf.sys 作为多功能总线驱动程序，指定类“多功能”（和在 devguid.h 里定义的相关联的GUID），并提供丢失的配置寄存器地址。在本部分的后面看进一步的信息。

2. 设备的每个功能有一个PnP功能驱动程序。（厂商提供的）

由于多功能总线驱动程序处理多功能语义，当功能被打包为单个的设备的话，功能驱动程序能够与被使用的驱动程序相同。

3. 设备的每个功能有一个INF文件。（厂商提供的）

当功能被打包为单个的设备的话，INF文件能够是相同的文件。INF文件不需要任何特别的多功能语义。

这样一个多功能设备的定制的INF必须包含至少一个 DDInstall.LogConfigOverride表项。重载部分必须包含每个功能的一个 MfCardConfig 表项，这样可以标识每套配置寄存器的位置。

如果由于重载configs存在于INF，INF必须重述由设备指定的所有资源需求。PnP管理器没有使用设备的任何设备资源需求。

使用下面的语法指定 MfCardConfig 表项：

MfCardConfig=ConfigRegBase:ConfigOptions[:IoConfigIndex][(attrs)]

ConfigRegBase

为设备的一个功能指定配置寄存器属性偏移。

ConfigOptions

指定8位PCMCIA配置选项寄存器。

IoConfigIndex

可选择地给支持总线驱动程序的 IOConfig 资源描述符指定一个索引，使用它来为配置I/O基础编程和限定寄存器。该索引是基于0的，这样零代表着 DDInstall.LogConfigOverride节里第一个 IOConfig 表项。

如果在配置选项寄存器里设置一位（0x02），那么一个 IoConfigIndex 必须指定。如果该位被清空，则该参数被忽略。

attrs

可选择地指定一套属性标记，可能的标记包括：

A

在配置和状态寄存器里打开音频。

例如，为包含有一个调制解调器和一个网络适配器的多功能PC Card设备考虑一个定制INF下面的摘录：

; …

[DDInstall.LogConfigOverride]

LogConfig = DDInstall.Override0

[DDInstall.Override]

IOConfig = 3F8-3FF ; Com1

IOConfig = 10@100-FFFF%FFF0 ; NIC I/O

IRQConfig = 3,4,5,7,9,10,11 ; IRQ

MemConfig = 2000@0-FFFFFFFF%FFFFE000 ; Memory Descriptor 0

MemConfig = 1000@0-FFFFFFFF%FFFFE000 ; Memory Descriptor 1

MfCardConfig = 1000:47:0 (A)

MfCardConfig = 1000:47:1

;…

例子给出了两个 MfCardConfig 表项，每一个为设备的每个功能。第一个 MfCardConfig 表项包含下面的信息：

1000(the ConfigRegBase)

指定在卡的属性内存里有一套配置寄存器位于偏移0x1000。在这个例子中，这些寄存器里的信息描述了卡上调制解调器的功能。

47(the ConfigOptions)

为总线驱动程序指定十六进制的值来编程使配置选项寄存器位于ConfigRegBase偏移（0x1000）。

0(the IoConfigIndex)

指定这个功能的I/O资源被列在本部分里的第一个 IOConfig 表项。一个零索引表示第一个表项，在此例中即为“IOConfig=3F8-3FF”。

A(the attrs)

指示总线驱动程序能够为这个功能打开音频，对一个调制解调器来说是典型的。

第二个 MfCardConfig 表项包含关于设备上第二个功能的信息（在这个例子中即为网络适配器），这个表项指出这里有第二套配置寄存器位于偏移0x1080。总线驱动程序将写值为0x47的 ConfigOptions 给这个功能的配置选项寄存器。IoConfigIndex 值为一指令总线驱动程序使用这部分里的第二个 IOConfig 表项（IOConfig=10@100-FFFF%FFF0）编程I/O基础，并限定这个功能的寄存器。

在INF里包括不止一个 DDInstall.LogConfigOverride N节，将指定有不止一个的非序列的I/O端口范围的选择。

如果设备使用一个不基于零的内存窗口，DDInstall.LogConfigOverride N节必须包括一个 PcCardConfig 表项。如果一个重载部分有一个 MfCardConfig 表项和 PcCardConfig 表项，PCMCIA总线驱动程序在 PcCardConfig 表项里忽略了 ConfigIndex 值，并只是使用 MemoryCardBaseN 信息。看《Windows2000驱动程序开发参考》卷一的5.1.3节获得有关 PcCardConfig 表项的进一步的信息。
1.1.3 支持有不完全配置寄存器的PC Card

如果一个多功能16位PC Card设备没有每个功能的配置寄存器，这样的设备的厂商能够使用系统提供的多功能总线驱动程序（mf.sys），但是必须提供一个定制INF文件和支持单个的功能。

这样的设备的厂商在Windows2000上能够使用下面的系统提供的组件：

1. 多功能设备的一个功能驱动程序。（系统提供的）

设备的一个定制INF必须指定 mf.sys 作为设备的功能驱动程序。系统提供的 mf.sys 驱动程序然后将枚举设备的功能。

参见5.4节获得使用系统提供的 mf.sys 驱动程序的更多信息。

这样的设备的厂商在Windows2000上必须提供下面的设备：

1. 多功能设备的一个定制的INF文件。（厂商提供的）

厂商必须提供一个多功能INF文件，该文件指定 mf.sys 作为多功能总线驱动程序，指定类“多功能”（和在 devguid.h 里定义的相关联的GUID），并提供丢失的配置寄存器地址。在本部分的后面看进一步的信息。

2. 设备的每个功能一个PnP功能驱动程序。（厂商提供的）

由于多功能总线驱动程序处理多功能语义，当功能被打包为单个的设备的话，功能驱动程序能够与被使用的驱动程序相同。

3. 设备的每个功能一个INF文件。（厂商提供的）

当功能被打包为单个的设备的话，INF文件与被使用的文件能够是相同的。INF文件不需要任何特别的多功能语义。

厂商为这样的一个设备提供的定制INF必须指定：

n mf.sys 作为设备的服务。

参看5.4节获得更多的信息。

n 多功能设备的资源需求。

在 DDInstall.LogConfigOverride节里指定资源需求。

n 每个设备功能的硬件ID。

在一个 DDInstall.HW节里指定硬件ID。

n 每个设备功能的资源图，标明了每个子功能对父资源的需求

在一个 DDInstall.HW部分里指定资源图，参看5.5节获得有关生成资源图的更多信息。

如果由于重载configs存在于INF里，INF必须重述设备指定的所有资源需求。PnP管理器没有使用设备的任何设备资源需求。

对这样的一个设备，配置选项寄存器能够使用一个 PcCardConfig 表项而被编程，相似于编程一个单功能设备。PcCardConfig 表项包含应用于整个设备的信息，PcCardConfig 表项在《Windows2000驱动程序开发参考》卷一的第3部分的 INF LogConfig Directive 文档里。

当为一个多功能设备指定一个 PcCardConfig 表项时，ConfigIndex 的配置与定义的一个单功能设备的配置相同。单功能PC Card的配置寄存器包含在设备的属性里定义的一套资源的一个索引。这个命令也能够用于一定的多功能设备，这些多功能设备使用配置选项寄存器的基于索引的格式。

例5.1给出了一个INF文件来安装一个多功能设备，该设备使用了 mf.sys 作为它的总线驱动程序，并且有一个不完整的配置寄存器。

例5.1 一个具有不完整的配置寄存器的一个多功能PcCard设备的INF样本

; MFSupra.inf

; 这个文件安装了Supra Dual 56K调制解调器

; Copyright 1999 Microsoft Corporation

[version]

Signature = “$Windows NT$”

Provider = %MSFT%

Class = Multifunction ;系统定义的类

ClassGUID = {4d36e971-e325-11ce-bfc1-08002be10318}

[ControlFlags]

ExcludeFromSelect=*SUP2440 ;不要在将要手动安装的设备列表里包含PnP设备

[Manufacturer]

%M_Supra% = Supra

[Supra]

%Supra1% = Sup2231GoCard.mf, *SUP2440

[Sup2231GoCard.mf.NT]

Include = mf.inf ;说明这个设备需要mf.sys

Needs = MFINSTALL.mf

[Sup2231GoCard.mf.NT.HW]

AddReg=Sup2231.mf.RegHW

[Sup2231.mf.RegHW]

HKR, Child0000, HardwareID, , MF\Shotgun_DEV0 ;modem1

HKR, Child0000, ResourceMap, 1, 00, 02

HKR, Child0001, HardwareID, , MF\Shotgun_DEV1 ;modem2

HKR, Child0001, ResourceMap, 1, 01, 02

[Sup2231GoCard.mf.NT.Services]

Include = mf.inf

Needs = MFINSTALL.mf. Services

[Sup2231GoCard.mf.NT.LogConfigOverride]

LogConfig = Sup223x.mf.Override0, Sup223x.mf.Override1, \

Sup223x.mf.Override2, Sup223x.mf.Override3

[Sup223x.mf.Override0]

ConfigPriority = NORMAL

IOConfig =2F8-2FF ; Com2

IOConfig =20@100-FFFF%FFE0 ; NIC I/O

IRQConfig =3,4,5,7,9,10,11,12,15 ; IRQ

MemConfig =1000@0-FFFFFFFF%FFFFF000 ; Memory Descriptor

PcCardConfig =59 (w) ; ConfigIndex

[Sup223x.mf.Override1]

ConfigPriority = NORMAL

IOConfig =3E8-3EF ; Com3

IOConfig =20@100-FFFF%FFE0 ; NIC I/O

IRQConfig =3,4,5,7,9,10,11,12,15 ; IRQ

MemConfig =1000@0-FFFFFFFF%FFFFF000 ; Memory Descriptor

PcCardConfig =69 (w) ; ConfigIndex

[Sup223x.mf.Override2]

ConfigPriority = NORMAL

IOConfig =2E8-2EF ; Com4

IOConfig =20@100-FFFF%FFE0 ; NIC I/O

IRQConfig =3,4,5,7,9,10,11,12,15 ; IRQ

MemConfig =1000@0-FFFFFFFF%FFFFF000 ; Memory Descriptor

PcCardConfig =79 (w) ; ConfigIndex

[Sup223x.mf.Override3]

ConfigPriority = NORMAL

IOConfig =3F8-3FF ; Com1

IOConfig =20@100-FFFF%FFE0 ; NIC I/O

IRQConfig =3,4,5,7,9,10,11,12,15 ; IRQ

MemConfig =1000@0-FFFFFFFF%FFFFF000 ; Memory Descriptor

PcCardConfig =49 (w) ; ConfigIndex

[strings]

MSFT = “Microsoft”

M_Supra= “Supra”

Supra1 = “Supra Dual 56K modem”

象上面给出的一个INF拷贝子功能的ID和资源信息到注册表中。当 mf.sys 驱动程序枚举设备的子功能数目时，它从注册表里提取信息。

1.2 支持多功能PCI设备

如果一个多功能PCI设备完全遵照PCI多功能标准，PCI总线驱动程序枚举单个的功能。PCI总线驱动程序管理这个事实，即有不止一个功能驻留在单个的设备位置里。对系统的其余部分，单个的功能象独立的设备一样操作。

一个多功能PCI设备的厂商在Windows2000上必须做下面的事情：

1. 保证设备遵照多功能规范。

2. 提供设备的每个功能一个PnP功能驱动程序。

由于多功能总线驱动程序处理多功能语义，当功能被打包为单个的设备的话，功能驱动程序能够与被使用的驱动程序相同。

3. 提供设备每个功能的一个INF文件。

当功能被打包为单个的设备的话，INF文件能够与被使用的文件是相同的。INF文件不需要任何特别的多功能语义。

例如，图5.1给出了可能为具有ISDN和调制解调器功能的多功能PCI设备而建立的设备堆栈的一个例子，如图5.1所示的，不是枚举了一个多功能设备，PCI驱动程序枚举了两个子设备。PnP管理器象对待一个典型的设备一样对待每个子设备，定位INF文件，装载适当的驱动程序，调用它们的AddDevice例程等等，直到一个设备堆栈为每个设备建立为止。PCI驱动程序为子设备仲裁资源，并管理设备的任何其他的多功能方面。

图5.1 一个其父设备枚举每个功能的多功能设备的设备堆栈实例

多功能卡的厂商为ISDN和调制解调器设备提供功能驱动程序和INF，就如同它们是分离的设备。

图例集中于功能驱动程序和总线驱动程序，说明每个功能和它们相关联的FDO和PDO，为简单起见，忽略任何过滤器驱动程序（和过滤程序DO）。

1.3 支持其他总线上的多功能设备

对一个PnP ISA、USB或1394总线上的一个多功能设备，如果设备遵照总线标准的话，父总线驱动程序枚举单个的功能。

对这样的一个设备，父总线驱动程序管理这个事实，即有不止一个设备驻留在单个的总线里的。对剩下系统，单个的功能象独立的设备一样操作。

这样类型的多功能设备厂商必须做下面的事情：

1. 保证设备遵照总线的规范，这里，设备将留驻于这个总线上。

2. 提供设备的每个功能的一个PnP功能驱动程序。

由于多功能总线驱动程序处理多功能语义，当功能被打包为单个的设备的话，功能驱动程序能够与被使用的驱动程序相同。

3. 提供设备的每个功能的一个INF文件。

当功能被打包为单个的设备的话，INF文件能够与被使用的文件是相同的。INF文件不需要任何特别的多功能语义。

1.4 使用系统提供的mf.sys

如果一个设备的基础总线支持一个多功能总线标准，如PC Card，这样的设备的厂商在Windows2000上能够使用系统提供的多功能总线驱动程序（mf.sys）来支持设备。

mf.sys 总线驱动程序处理设备功能的PnP枚举，并在两个功能之间仲裁资源，如I/O端口和IRQs。mf.sys 驱动程序通过电源管理父多功能设备来为子功能处理电源管理。

为使用 mf.sys，一个多功能设备必须满足下面的需求：

n 设备的基础总线必须有一个多功能总线标准。

n 子功能的DEVICE_CAPABILITIES必须是相同的且必须匹配这些父设备。当查询一个子设备功能（IRP_MN_QUERY_CAPABILITIES）的设备能力时，mf.sys 驱动程序报告父设备的设备能力。

n 多功能设备驻留的总线的驱动程序，如 pcmcia.sys，必须处理任何IRP_MN_READ_CONFIG和IRP_MN_WRITE_CONFIG请求。mf.sys 驱动程序只是传递这些IRP给父总线驱动程序。

n 功能必须是独立的：它们不能够有启动顺序依赖；一个功能的资源需求不能够按照另外一个功能资源被表达（例如，功能1使用I/O端口X和功能2使用端口X+200）；并且每个功能必须能够作为一个独立的设备操作，即使它们作为另一个功能而被相同的驱动程序所服务。

为使用 mf.sys，一个厂商为多功能设备提供一个INF，这个多功能设备指定 mf.sys 作为设备的驱动程序的。如果一个设备完全地和正确地遵照它的基础总线的多功能标准，这样的设备的厂商能够使用系统提供的 mf.sys。如果一个设备没有完全地遵照这个标准，厂商必须提供一个定制INF。

在任何一种情况下，厂商也为设备上的单个功能提供驱动程序和INF文件。

下面的一个定制多功能INF骨架说明了必要的语义，来指定 mf.sys 作为一个多功能设备的驱动程序：

[Version]

Signature = “$Windows NT$”

;…

Class = Multifunction ;系统定义的MF设备类

ClassGUID = {4d36e971-e325-11ce-bfc1-08002be10318} ;MF的GUID

;…

;…

[ControlFlags]

ExcludeFromSelect = * ;不要让设备列表里的PnP设备被手动安装

[Manufacturer]

;…

;…

[ModelSection] ;models section

;…

;…

[DDInstall] ;install section

Include = mf.inf ;说明这个设备需要mf.sys

Needs = MFINSTALL.mf

;…

[DDInstall.HW]

AddReg= DDInstall.RegHW

[DDInstall.RegHW]

;在这里放置具有子功能硬件ID的表项

;…

;在这里放置重载节

;…

[Strings]

;…

考虑一个综合LAN/调制解调器PC Card设备，没有任何特定的多功能支持，这样一个设备可能被PCMCIA总线驱动程序作为一个单个的调制解调器设备来报告。有另外的一个多功能INF和 mf.sys 总线驱动程序的支持，设备的两种功能被枚举。图5.2给出了设备堆栈的例子，该堆栈可能为这样一个具有必要的多功能支持的combo PC Card而建立。

如图5.2所示的，驱动程序为多功能设备驻留的总线枚举一个设备。在多功能INF文件里的硬件ID引起PnP管理器装载 mf.sys 总线驱动程序作为设备的功能驱动程序。mf.sys 总线驱动程序枚举两个子设备，一个LAN设备和一个调制解调器。

图5.2被 mf.sys 枚举的一个多功能设备的设备堆栈实例

PnP管理器象对待一个典型的设备一样对待每个子设备，定位INF文件，装载适当的驱动程序，调用它们的AddDevice例程等等，直到一个设备堆栈为每个设备而建立为止。mf.sys 总线驱动程序为子设备仲裁资源，并管理设备的任何其他的多功能方面。多功能卡的设备的厂商为多个功能（LAN和调制解调器）提供功能驱动程序和INFs，就如同它们是分开的设备一样。

图例集中于功能驱动程序和父总线驱动程序和它们相关联的FDO和PDO，为简单起见，忽略任何过滤器驱动程序（和过滤程序DO）。

1.5 为一个多功能设备生成资源图

一个资源图在一个INF add-registry-section里标识了被一个子设备功能所使用的一个多功能资源设备。如果一个多功能设备需要资源图，设备的INF典型地包括一个每个设备功能的资源图。

本部分描述了怎样构建资源图。参看前节来决定哪一个多功能设备需要资源图。

为了在一个子功能资源图里引用一个父资源，当父资源出现在 log-config-section(s)里时，它被隐含地标数为00-N。

例如，用下面的 DDInstall.LogConfigOverride节考虑一个多功能PC Card：

[DDInstall.Override0] ;com2

IOConfig=2f8-2ff ;资源 00

IOConfig=20@100-FFFF%FFE0 ;资源 01

IRQConfig=3,4,5,7,9,10,11 ;资源 02

MemConfig=4000@0-FFFFFFFF%FFFFC000 ;资源 03

PcCardConfig=41:100000 (w) ;资源 04

在该例中的设备有五个资源，当构建一个资源图时，它们被标数为00-04。如果有多于一个 DDInstall.LogConfigOverride节，资源在每个部分里必须以相同的顺序列表。

如果一个子功能（Child0000）需要上面的第一个和第三个资源列表，这个子功能的资源图将是：00,02。如果另一个子功能（Child0001）需要所有的五个资源，那么它的资源图将是：00,01,02,03,04。例子中，资源00（IoConfig=2f8-2ff）和02（IRQConfig=3,4,5, 7, 9,10,11）被共享。这些资源图将在INF里按如下指定：

[DDInstall.RegHW]

;每个“子”功能列表硬件和资源图

HKR,Child0000,HardwareID,,child0000-hardware-ID

HKR,Child0000,ResourceMap,1,00,02 ;子0000图

HKR,Child0001,HardwareID,,child0001-hardware-ID

HKR,Child0001,ResourceMap,1,00,01,02,03,04 ;子0001图

紧随ResourceMap参数的“1”指定注册表表项是一个REG_BINARY，紧随“1”的数字是资源图值。

如果在INF里没有 DDInstall.LogConfigOverride节，父资源以资源需求被基础总线驱动程序构建的顺序来标数。对PC Card来说，总线驱动程序以这样的顺序报告资源：IRQ，I/O端口，内存地址。对多个I/O和内存需求来说，它们以与卡上的元组同样的顺序被标数。


在驱动程序内支持电源管理

本书的第1部分介绍了即插即用(PnP)驱动程序的一般要求。这一章介绍为支持电源管理对驱动程序的要求。它包括下列各节：

1.1 内核模式的电源管理组件

1.1.1 ACPI BIOS

1.1.2 ACPI 驱动程序

1.l.3 电源管理器

1.1.4 电源管理中驱动程序的作用

1.2 驱动程序的电源管理职能

1.2.1 报告设备的电源性能

1.2.1.1 DeviceD1和DeviceD2

1.2.l.2 WakeFromD0，WakeFromD1，WakeFromD2，和WakeFromD3

1.2.1.3 DeviceState

1.2.1.4 SystemWake

1.2.l.5 DeviceWake

1.2.1.6 D1Latency，D2Latency，和D3Latency

1.2.2 为电源管理设置设备对象标记

1.2.3 处理电源IRP

1.2.4 设备的上电

1.2.5 设备的掉电

1.2.6 激活设备唤醒能力

1.3 处理电源IRP的规则

1.3.1 使用PoCallDriver

1.3.2 传递电源IRP

1.3.3 设备休眠时排队I/O请求

1.3.4 处理未被支持的或无法识别的电源IRP

1.1 内核模式下的电源管理组件

下面的内容在电源管理中对三种内核模式组件的作用提供了简要的介绍。

1.1.1 ACPI BIOS

以MicrosoftÒ WindowsÒ 2000和Windows 98支持的集成电源管理特性仅在拥有高级配置和电源接口(ACPI)BIOS的系统上可用。 ACPI BIOS在硬件层上提供了一些特性使软件电源管理成为可能。 在启动时，操作系统装载器检查这种BIOS是否被装载。 如果这种BIOS已经装载，ACPI就能被激活于操作系统中。如果没有装载，ACPI就被禁止，同时由不健壮的高级电源管理(APM)模型代替使用。

在缺省情况下，Windows 2000假定任何ACPI BIOS的给定日期为1999年1月1日，或者稍晚,即Windows 2000准备完毕时。 但是，如果已告知这种BIOS有ACPI问题，装载器禁止ACPI并由APM代替使用。

设备管理器显示一个计算机是否支持ACPI。

1.1.2 ACPI 驱动程序

微软提供ACPI驱动程序作为操作系统的一部分。 在拥有ACPI BIOS的系统上，HAL使ACPI驱动程序以设备树为基础在系统启动时装载，其中HAL作为操作系统和BIOS之间的接口。 ACPI 驱动程序对其他驱动程序是透明的。

ACPI 驱动程序的职责包括支持PnP和电源管理。例如，一个系统上的ACPI任务可以包括为COM 端口再编写资源或者为系统唤醒重新激活USB控制器。

对于每一个在ACPI名称空间(硬件供应商建立的ACPI BIOS的一部分)中所描述的设备，ACPI驱动程序建立一种过滤设备对象(过滤D0)或者一种物理设备对象(PDO)。 如果设备集成在系统板卡上，驱动程序建立过滤设备对象，代表ACPI总线过滤，并且立即将它附加到总线驱动程序(PDO)之上的设备栈中。 对于其他在ACPI中所描述的名称空间中而不在系统板上的设备，ACPI建立PDO。 ACPI通过这些设备对象向设备栈提供电源管理和PnP特性。

ACPI建立了设备对象的设备，依赖于机器的BIOS和主插件板（母板）的配置，在不同系统中有所不同。注意，一个ACPI总线过滤程序只为在ACPI名称空间中描述并集成在系统板中的设备装载。并非所有的母板设备都有ACPI总线过滤程序。

所有ACPI的功能对高层驱动程序都是透明的。在任何给定的设备栈中，驱动程序都不能假定存在（或不存在）ACPI过滤器。

1.1.3 电源管理器

电源管理器负责为系统管理电源的使用。在第一部分中已提到，它管理系统范围的电源策略，并且通过系统跟踪电源IRP的路径。

电源管理器通过把IRP_MJ_POWER请求发送到驱动程序中来请求电源操作。一个请求可以指定一种新的电源状态或者能查询在电源状态方面的变化是否是可行的。

当要求休眠，冬眠，或者关机时，电源管理器把IRP_MJ_POWER请求发送到每一个要求适当电源行动的设备树中的叶节点。 电源管理器在确定该系统是否应该休眠，冬眠，或者关机时要考虑下列几点：

。系统活动电平

。系统电池电平

。应用程序请求关机，冬眠，还是休眠

。用户行动，诸如按下电源按钮

。控制面板设置

电源管理器也为驱动程序提供了接口，这主要包括PoXxx例程，电源管理IRP，和所需的驱动程序入口点。 驱动程序使用这个接口管理这些设备的电源，并且为系统电源管理去响应电源管理器的请求。

1.1.4 电源管理中驱动程序的作用

驱动程序在两个方面支持电源管理：

1.驱动程序响应由电源管理器所发出的系统范围的电源请求。

2.驱动程序为其独立设备管理电源。

所有驱动程序都必须有DispatchPower例程来处理IRP_MJ_POWER请求，就象在第1部分中所描述的。在其DispatchPower例程中，驱动程序检查每一电源IRP，或者是处理它，或者把它传递到相邻较低层驱动程序中。

对于参与电源管理的设备，此设备在设备栈中的所有驱动程序必须作出响应或者适当地传递电源IRP。单个驱动程序的失败会导致电源管理在整个系统中被禁止。

每一设备有一个驱动程序为其设备管理电源策略。这个驱动程序能把电源IRP发送到它自己的设备栈中，以在其设备上执行电源操作。电源策略管理器负责发出符合系统电源IRP的设备电源IRP。此外，驱动程序还可以执行一定的电源任务，诸如，在启动或关闭时管理设备的电源，而不必接收电源IRP，要考虑这些隐含的电源请求。

1.2 驱动程序的电源管理职能

支持电源管理的驱动程序，其职能如下：

。保证电源管理性能能在PnP列举过程中有详细的报告

。为电源管理在设备对象中设置标记

。由电源管理器或者驱动程序处理电源IRP的发送

。当系统启动或者停止空闲状态时立即给设备通电

。当系统关闭或空闲时，断电或让设备休眠

。如果设备支持唤醒能力，激活设备的唤醒能力

并非设备栈中的每一个驱动程序都执行这些任务。一般来说，总线驱动程序报告能力，设置标记，操纵物理设备，设备电源策略管理器(通常是功能驱动程序)发出请求使设备处于休眠状况并且激活唤醒。

只有极少例外情况，驱动程序响应电源IRP对设备进行加电，断电以及激活唤醒能力，即主代码为IRP_MJ_POWER的IRP。电源管理器可以发送电源IRP，在某些情况下驱动程序也可以发送电源IRP。

1.2.1 报告设备的电源性能

在列举过程中，驱动程序报告设备的信息以响应PnP IRP_MN_QUERY_CAPABILITIES请求。 与这些信息一起，驱动程序在DEVICE_CAPABILITIES 结构中，报告设备的电源管理能力。总线驱动程序一般都要填写这个结构。

高层驱动程序应该为查询能力IRP设置IoCompletion 例程，这样这些驱动程序就能做一个这种结构的本地拷贝，并且保证它包含适当的值。一般来说，高层驱动程序不能改变这些值。 但是，如果必须改变的话，驱动程序能更进一步地限制设备的能力，但不能增加能力。换句话说，驱动程序能限制规则而不能放松规则。

在IRP完成且所有驱动程序的完成例程都运行之后，此结构被放入高速缓存，而且驱动程序不能改变其内容。

DEVICE_CAPABILITIES 的以下字段属于电源管理：

。DeviceD1和DeviceD2

。WakeFromD0，WakeFromD1，WakeFromD2，和WakeFromD3

。DeviceState

。SystemWake

。DeviceWake

。D1Latency，D2Latency，和D3Latency

1.2.1.1 DeviceD1和DeviceD2

DeviceD1和DeviceD2字段表明设备硬件是否支持这些设备状态。 它们都只占用一位，这些位置位或清零表示设备支持这种状态还是不支持这种状态。 操作系统假定所有设备支持D0和D3状态。

1.2.1.2 WakeFromD0，WakeFromD1，WakeFromD2，和WakeFromD3

每一个这样的字段表明当设备在指定状态下时，能否被外部信号唤醒。

对于支持所有四种设备状态(D0，D1，D2，D3)但是只能从状态D0和D1中唤醒的设备，设置WakeFromD0和WakeFromD1位，同时对WakeFromD2和WakeFromD3位清零。

1.2.1.3 DeviceState

DeviceState字段是一个由DEVICE_POWER_STATE值构成的数组，按SYSTEM_POWER_STATE值进行索引，其范围为从PowerSystemWorking到PowerSystemShutdown。此数组中的每一个元素包含为索引中的系统状态设备支持的最大(最大功率)的设备状态，如果不支持系统状态，则置为PowerDeviceUnspecified。

例如，在仅仅支持S0，S4，和S5的系统上，支持D0和D3状态的设备的DeviceState数组包含下列情况：

DeviceState 元素 值

————————————————————————————

DeviceState[PowerSystemWorking] PowerDeviceD0

DeviceState[PowerSystemSleeping1] PowerDeviceUnspecified

DeviceState[PowerSystemSleeping2] PowerDeviceUnspecified

DeviceState[PowerSystemSleeping3] PowerDeviceUnspecified

DeviceState[PowerSystemHibernate] PowerDeviceD3

DeviceState[PowerSystemShutdown] PowerDeviceD3

在支持所有系统电源状态的系统上，对于当系统变为休眠状态时必须处于D2状态,当系统在冬眠状态时处于D3状态的设备DeviceState数组的设置如下:

DeviceState 元素 值

_________________________________________________________

DeviceState[PowerSystemWorking] PowerDeviceD0

DeviceState[PowerSystemSleeping1] PowerDeviceD2

DeviceState[PowerSystemSleeping2] PowerDeviceD2

DeviceState[PowerSystemSleeping3] PowerDeviceD2

DeviceState[PowerSystemHibernate] PowerDeviceD3

DeviceState[PowerSystemShutdown] PowerDeviceD3

注意，DeviceState数组的项目表示为相应系统状态设备支持的最大功率状态。 在前面的例子中，对任何系统电源状态，设备都可以处于D3状态，对系统状态PowerSystemWorking 到PowerSystemSleeping3，设备可处于D2状态，对系统状态PowerSystemWorking，设备可处于D1状态。

总线驱动程序或者 ACPI过滤程序基于双亲设备节点的能力，设置这些值。

一般来说，高层驱动程序不应该改变这些值。 然而，在一些很少出现的情况下，这种变化也是可能的，设备能指定一个比总线驱动程序或者比ACPI过滤程序的最初返回值低（较小功率）的状态。例如，在上面的表中，假定DeviceState[PowerSystemSleeping1]为 PowerDeviceD2。驱动程序能把此值改变为PowerDeviceD3，但不能改变为PowerDeviceD1或者PowerDeviceD0。

1.2.1.4 SystemWake

SystemWake字段包含设备能唤醒系统时的最低(最小功率)系统电源状态，或者当设备不能唤醒系统时此字段值为PowerSystemUndefined。

在列举中总线驱动程序设置了此值。 一个高层驱动程序能把此值改变为较高功率状态，但是不能把它改变为较低功率状态。 例如，如果总线驱动程序把SystemWake设置为S3，而设备栈上层的驱动程序仅仅能支持从S2唤醒，则高层驱动程序能改变此值为S2。 如果驱动程序改变了SystemWake，那么它也不得不改变DeviceWake，就像下面所解释的。

驱动程序很少需要将值的改变通知设备栈。 因为这些改变的值更加限制了设备能力，较低层的驱动程序不会收到他们不能处理的请求。 在前面的例子中，高层驱动程序从低功率状态（比S2低）唤醒系统的请求失败，因此较低层驱动程序不会收到这样的请求。但是一个驱动程序必须能意识到任何改变，这样它就能在IRP_MN_START_DEVICE处理中发送PnP IRP_MN_QUERY_ CAPABILITIES到它自己的设备栈中。

如果SystemWake和DeviceWake两个字段都非零(即不是PowerSystemUnspecified)，则设备和其驱动程序支持此系统上的唤醒。

在非ACPI硬件上，这个字段总包含零(PowerSystemUnspecified)。

1.2.1.5 DeviceWake

DeviceWake字段包含设备能响应唤醒事件的最低(最小功率)设备电源状态，或者如果设备不能被外部信号唤醒，则置为PowerDeviceUnspecified。

总线驱动程序设置此值。 高层驱动程序能改变此值为一个较大功率状态。例如，如果总线驱动程序把DeviceWake设置为D3，而设备栈上层的驱动程序仅仅能支持从D2唤醒，则高层驱动程序能把此值改变为D2。

注意，如果一个驱动程序改变DeviceWake，则它也不得不改变SystemWake，以避免DeviceState数组中的系统到设备映像发生冲突。 例如，假定总线驱动程序设置为下列情况：

。DeviceState[PowerSystemSleeping1]=PowerDeviceD1

。DeviceState[PowerSystemSleeping2]=PowerDeviceD3

。DeviceWake=PowerDeviceD3

。SystemWake=PowerSystemSleeping2

如果一个高层驱动程序确定其设备不能从D3状态唤醒系统，而只能从D2或者更高状态，那么它能改变DeviceWake为D2。 然而，这种变化引起从S2到D3的映像成为不可能的。DeviceState数组为给定系统状态列出了设备支持的最高功率状态。如果这个例子中的系统状态是PowerSystemSleeping2，则设备状态就不能是PowerDeviceD2。为了解决这个问题，驱动程序也必须把SystemWake改变为PowerSystemS1eeping1。这对WakeFromDx和DeviceDx的设置也是成立的。驱动程序必须保证它对SystemWake或者DeviceWake的所有改变都与WakeFromDx和DeviceDx值不冲突。WakeFromDx和DeviceDx的值反映驱动程序不能变化的硬件特性。

如果SystemWake和DeviceWake两个字段都是非零(即不是 PowerSystemUnspecified)，那么设备和其驱动程序都支持在这个系统上唤醒。

在非 ACPI硬件上，DeviceWake字段包含零(PowerSystemUnspecified)。

1.2.1.6 D1Latency，D2Latency，和D3Latency

D1Latency，D2Latency，和D3Latency字段包含从D0状态到每个休眠状态要求的近似时间(以100微秒为单位)。为不支持的任何设备状态驱动程序将等待时间置为零。

1.2.2 对电源管理设置设备对象标记

在AddDevice例程中，每一驱动程序建立一个设备对象(过滤DO，FDO或者PDO)，并且根据需要设置D0_Xxxx标记。 下列的设备对象标记属于电源管理：

。D0_POWER_INRUSH

。D0_POWER_PAGABLE

设备对象标记一般是在总线驱动程序为设备创建PDO时由总线驱动程序设置的。 然而，一些功能驱动程序可能需要改变这些标记的值，把它们作为这些驱动程序AddDevice例程的一部分。 总之，标记的值应该在整个设备栈的所有设备对象中是一致的。

在Windows 2000中，分页路径上的设备驱动程序不能设置DO_POWER_PAGABLE 标记。不设置标记的驱动程序必须在IRQL DISPATCH_LEVEL 下被调用。

总之，驱动程序不应改变总线驱动程序的标记值，如果较低层驱动程序清除了这个标记，驱动程序就决不能设置此标记。当处理包括PnP分页请求的转变时(一般来说，响应带有IRP_MN_DEVICE_USAGE_NOTIFlCATION 请求的IRP_MJ_PNP)，存储驱动程序必须认真安排其标记的设置和清除的顺序。

可分页的Windos 200O驱动程序必须设置DO_POWER_PAGABLE标记。 电源管理器在IRQL PASSIVE_LEVEL 下调用这些驱动程序。 所有WDM和Windows 98驱动程序必须设置 DO_POWER_PAGABLE。此外，Windows 98驱动程序不能在IRQL DISPATCH_LEVEL 下完成电源IRP。

启动时要求inrash启动功率的设备驱动程序必须在设备对象中设置DO_POWER_INRUSH 标记，而且要在此之后清除DO_DEVICE_INITIALIZING 标记。 在设备栈中只有一个设备（一般是总线驱动程序(PDO)）需要设置D0_POWER_INRUSH。此标记通知电源管理器必须同时给这些设备上电，

的设备按顺序依次上电。 电源管理器保证在任何给定时间内系统中只有一个IRP是活动的。

电源管理器能在IRQL DISPATCH_LEVEL 下发送启动功率IRP。 当处理一个IRP时，驱动程序不能访问任何可分页代码或者数据。

驱动程序不能同时设置 DO_POWER_PAGABLE 和 DO_POWER_INRUSH。

1.2.3 处理电源IRP

驱动程序在DispatchPower 例程中处理电源IRP。 所有电源管理请求都有主IRP代码IRP_MJ_POWER 和下列之一的次代码：

。IRP_MN_QUERY_POWER----通过查询确定电源状态的改变是否可行。

。IRP_MN_SET_POWER----请求从一种电源状态变化到另一种电源状态。

。IRP_MN_WAIT_WAKE----请求激活设备对自己或者对系统的唤醒。

。IRP_MN_POWER_SEQUENCE----请求特定设备的优化电源恢复信息。

支持IRP_MN_SET_POWER 和IRP_MN_QUERY_POWER 是需要的。所有驱动程序都必须准备处理这些IRP。

设备栈中所有能被外部信号唤醒的设备都支持IRP_MN_WAIT_WAKE。驱动程序发送此IRP以唤醒设备。

支持IRP_MN_POWER_SEQUENCE是可选的。 这个IRP为需要长时间恢复电源的设备提供优化。

电源IRP能指定系统电源操作或者设备电源操作。系统电源IRP和设备电源IRP采取略有不同的路径通过设备栈，如下列各段所述。

1.2.3.1 系统电源IRP

系统电源IRP指定主IRP代码IRP_MJ_POWER，指定下面列出的次电源IRP代码之一，以及IRP栈的Power.Type成员值SystemPowerState。只有电源管理器能发送这样一个IRP；驱动程序不能发送系统电源IRP。

由于下列原因之一，电源管理器发送系统电源IRP：

。改变系统电源状态以响应一个空闲超时、一个系统活动变化、一个用户请求、或者电池电压过低(IRP_MN_SET_POWER)

。查询设备以确定系统是否能进入休眠(IRP_MN_QUERY_POWER)

。在查询之后重新确定当前系统电源状态(IRP_MN_SET_POWER)

电源管理器代表系统发送IRP_MN_QUERY_POWER 和 IRP_MN_SET_POWER请求。驱动程序可以使IRP_MN_QUERY_POWER请求失败，但是不能使IRP_MN_SET_POWER请求失败。

例如，为了改变系统电源状态，在设备树的每一设备节点中，电源管理器把系统电源IRP发送到栈中的顶端驱动程序。 图1.1显示在一个设备栈内的驱动程序如何处理系统电源IRP。

图1.1表明：

1.电源管理器调用I/O管理器把系统电源IRP发送到设备树中的每一叶节点。

2. 如果可能，驱动程序处理IRP，必要时设置IoCompletion例程，并向栈底转发IRP (使用PoCallDriver)。如果驱动程序必须使 IRP失败，它会立即这样做并且完成IRP。 驱动程序能使IRP_MN_QUERY_POWER IRP失败，但是不能使IRP_MN_SET_POWER IRP失败。

3.当拥有设备电源策略的驱动程序接收到IRP时，驱动程序必须在发送IRP之前为系统设置IoCompletion例程。

4.最后，总线驱动程序完成系统IRP。I/O管理器调用IoCompletion例程。

5.在IoCompletion例程中，设备电源策略所有者调用PoRequest PowerIrp发送设备电源IRP，指定驱动程序设置一个在设备电源IRP完成后调用的回调例程。

如果有必要的话，驱动程序在 DEVICE_CAPABILITIES结构的可缓冲拷贝中参考DeviceState字段(参见“报告设备的电源性能”)，确定哪个设备电源状态符合在IRP中的系统电源状态。

6.在设备IRP完成且任何设备IRP完成例程都已运行时，调用电源策略所有者的回调例程。 在回调例程中，驱动程序将返回状态复制到系统IRP中。它先调用PoStartNextPowerIrp，然后调用IoCompleteRequest，完成系统IRP。

详见第3章。

图1.1 系统电源IRP的路径

由于一些设备在加电时需要有启动功率，系统启动功率IRP在整个系统中同步且串行地被处理。在同一时间内仅有一个这样的IRP是活跃的。详见l.3.1节。

1.2.3.2 独立设备的电源IRP

设备电源IRP指定主IRP代码IRP_MJ_POWER、下面列出的次电源IRP代码之一、以及Power.Type成员的值DevicePowerState。

。IRP_MN_QUERY_POWER

。IRP_MN_SET_POWER

。IRP_MN_WAIT_WAKE

。IRP_MN_POWER_SEQUENCE

所有设备栈中的驱动程序都接收这样的IRP；一般来说,只有设备电源策略管理器能发送这些IRP。 然而，当代表设备执行空闲检测时，电源管理器也能发送设备电源IRP，就像在“使用电源管理器程序进行空闲检测”中所解释的一样。

驱动程序发送设备电源IRP有以下原因：

。为了查询或者改变设备电源状态以响应系统电源IRP

。为了设备进入休眠状态中以节能

。为了将已进入休眠状态的设备返回到工作状态

。为了响应外部信号以唤醒设备

。为了在给设备上电时得到的电源顺序值

正如图1.2所示，设备电源IRP按以下步骤发送、转发和完成：

1.设备电源策略所有者调用PoRequestPowerIrp分配设备电源IRP，在参数中指定IRP完成时要调用的回调例程。

2.电源管理器分配设备电源IRP，并且为目标PDO把它发送到设备栈的顶端驱动程序中。

3.驱动程序执行任何需要的动作，如果需要,设置IoCompletion例程，调用PoStartNextPowerIrp，通知系统已准备好处理另一个电源IRP，并且调用PoCallDriver把IRP传递到相邻较低层驱动程序中。每一个栈中的驱动程序都这样做直到IRP到达总线驱动程序中。如果驱动程序必须使IRP失败，那么它将立即这样做并调用IoCompleteRequest。

4. 保持设备PDO的总线驱动程序，执行请求的动作，然后调用IoCompleteRequest完成IRP。

5.I/O管理器调用IoCompletion例程,此例程是驱动程序在IRP向栈底传送时设置的。在所有IoCompletion例程被调用后,调用回调例程。

关于设备电源IRP更多的信息，参见第3章和第4章。关于上电顺序IRP的细节情况，参见卷1的“Windows2000 驱动程序开发指南”中的IRP_MN_POWER_SEQUENCE。

图1.2 设备电源IRP的路径

1.2.4 设备的上电

当总线驱动程序为其子设备之一处理PnP IRP_MN_START_DEVICE请求时，它应该给设备加电，并调用PoSetPowerState以向电源管理器报告设备状态。设备加电是设备启动时的必须部分。设备电源策略所有者不为PowerDeviceD0发送IRP_MN_SET_POWER请求，因此驱动程序不必期待在启动时收到这些IRP。

当为了节能将设备关闭时，它的驱动程序应在I/O请求到达时给设备上电。在这种情况下，设备电源策略所有者必须发送IRP_MN_SET_POWER把设备返回到工作的状态。当IRP完成之后，驱动程序停止I/O排队，开始处理队列中的请求。

1.2.5 设备的掉电

除非设备能够被唤醒，否则当系统关闭时，其驱动程序会关闭其电源。设备必须是掉电删除或者意外删除。

当设备被删除时，PnP管理器发送IRP_MN_REMOVE_DEVICE请求到设备栈。在响应IRP时，设备驱动程序应该保证设备掉电。设备掉电是删除处理中必须的部分；设备电源策略所有者不要求为PowerDeviceD3发送IRP_MN_SET_POWER。

当驱动程序处理IRP_MN_REMOVE_DEVICE请求时，他们等候未决I/O的完成，执行任何必要的删除处理，调用PoSetPowerState通知电源管理器设备在状态D3中，并且删除它们为这种设备建立的设备对象。一般来说，总线驱动程序关闭设备的电源。

如果设备从Windows 2000系统中被意外删除,PnP管理模块发送IRP_MN_SURPRISE_REMOVAL请求。作为对这个IRP的响应，驱动程序应该关闭设备的电源，并且执行附加的删除处理，就象在“处理IRP_MN_SURPRISE_REMOVAL请求”中所描述的。

系统关闭时，电源管理器为系统电源状态(S4或者S5)发送IRP_MN_SET_POWER。 当设备电源策略所有者收到这个IRP时，它应该为PowerDeviceD3发送IRP_MN_SET_POWER，这样低层驱动程序就能完成它们的工作并关闭设备的电源。

驱动程序能选择性地执行对其设备的空闲检测，或者请求电源管理器执行空闲检测，以便在不使用时能关闭设备的电源。详见第2章中的“检测空闲的设备”。

1.2.6 激活设备唤醒能力

如果设备支持唤醒，其电源策略所有者必须能够激活或禁止设备的唤醒能力。驱动程序通过发送IRP_MJ_POWER及次功能代码IRP_MN_WAIT_WAKE激活唤醒能力，并且通过取消以前发送的IRP_MN_WAIT_WAKE禁止唤醒能力。设备在某一时间内只能有一个IRP_MN_WAIT_WAKE是未决的。

为了确定其设备是否支持唤醒、从哪个设备状态可接收唤醒信号、在哪个系统状态可唤醒系统,驱动程序参考DEVICE_CAPABILITIES结构中的SystemWake，DeviceWake，和WakeFromDx字段。

详见第4章，关于在驱动程序中激活、禁止和响应唤醒信号。

1.3 处理电源IRP的规则

支持电源管理的驱动程序必须在下列情况中符合一定的规则：

。使用PoCallDriver，而不是IoCallDriver，传递电源IRP

。把电源IRP传递到相邻较低层驱动程序中

。当设备正在休眠时，排队I/O请求

。处理没有支持或者不可识别的电源IRP

下面各节讲述驱动程序应该如何执行这些任务。

1.3.1 使用PoCallDriver

驱动程序必须使用PoCallDriver把电源IRP传递到相邻较低层驱动程序上，而不使用IoCallDrivcr。PoRequestPowerIrp和PoStartNextPowerIrp与PoCallDriver保证电源管理器在整个系统中适当地同步电源IRP。

系统限制活动的电源IRP的数量如下：

。在任何给定的时间内,对每个物理设备对象都不能有多于一个以上的系统电源IRP(IRP_MN_SET_POWER，IRP_MN_QUERY_POWER)活动。

。在任何给定的时间内,对每个物理设备对象都不能有多于一个以上的设备设置电源IRP(IRP_MN_SET_POWER)活动。

。在任何给定的时间内,系统中都不能有一个以上的请求启动功率电源IRP活动。

为了保证两种需启动功率的设备不试图同时上电，电源管理器在整个系统中追踪活动的需启动功率电源IRP,并且允许一次只有一个是活动的。当活动的启动功率IRP已经完成时，其他的启动功率IRP才能开始。

由于这些对启动功率IRP的限制，一个设备电源IRP可能会有其他设备的启动功率IRP完成时被阻塞。驱动程序编写者在调试时应该及时了解到这种行为。

1.3.2 传递电源IRP

必须把电源IRP通过设备栈的所有路径向下传递到PDO，以保证电源转换是被“干净”地管理。当IRP在栈中向下传递时，驱动程序处理减少设备能量的IRP。当IRP从栈中向上返回时，驱动程序在IoCompletion例程中处理给设备增加能量的IRP。

图l.3显示向下传递电源IRP的有关步骤。

图1.3 向下传递一个电源IRP

由图中所示，驱动程序必须做到下列各内容：

1.如果驱动程序没有设置IoCompletion例程，则调用PoStartNextPowerIrp。

2.为相邻较低层驱动程序设置IRP栈位置。没有设置IoCompletion例程的驱动程序应该使用IoSkipCurrentIrpStackLocation。

设置IoCompletion例程的驱动程序应该使用IoCopyCurrentIrpStackLocationToNext。拷贝当前栈的位置，以保证当IoCompletion例程运行时，IRP栈指针设置到正确的位置。

3.必要时，调用IoSetCompletionRoutine来设置IoCompletion例程。在IoCompletion例程中，驱动程序能调用PoStartNextPowerIrp以表明它已准备好处理下一个电源IRP。

4.调用PoCallDriver ，把IRP传递到栈中的相邻较低层驱动程序中。

驱动程序必须使用PoCallDriver而非IoCallDriver(就其他IRP而论)，以保证系统适当地同步电源IRP。详见1.3.1节。

当前IRP栈位置指向当前驱动程序时，驱动程序必须调用PoStartNextPowerIrp。 这个例程能从IoCompletion例程或从DispatchPower例程中调用。 然而，PoStartNextPowerIrp必须在IoCompleteRequest，IoSkipCurrentIrpStackLocation和PoCallDriver之前调用。若按其他顺序调用，程序可能会造成系统死锁。

即使某一驱动程序使IRP失败，这个驱动程序也必须调用PoStartNextPowerIrp通知电源管理器，它已准备好处理另一个电源IRP。

记住,IoCompletion例程能在IRQL DISPATCH_LEVEL中调用。因此，在带有IRP的低层驱动程序完成之后，如果在IRQL PASSIVE_LEVEL下驱动程序请求附加处理，驱动程序的完成例程应该对工作项目排队，然后返回STATUS_MORE_PROCESSING_REQUIRED。线程必须完成此IRP。

处理电源IRP时不能阻塞

驱动程序在处理电源IRP时决不能造成长时间的延迟。

当向下传递电源IRP时，驱动程序应该在调用PoStartNextPowerIrp和PoCallDriver之后，尽可能快地从DispatchPower例程中返回。驱动程序在返回之前不能等待一个内核事件或用其他原因延迟。如果驱动程序不能在一个短的时间内处理电源IRP，它将返回STATUS_PENDING并且对所有的IRP进行排队，直到电源IRP完成(注意,这种行为不同于PnP IRP和DispatchPnP例程，这两个例程允许阻塞)。

如果驱动程序必须等待设备栈下层另一个驱动程序的电源行动，则此驱动程序应该从其DispatchPower例程中返回STATUS_PENDING，并且为电源IRP设置IoCompletion例程。在IoCompletion例程中，驱动程序能执行所需的任何任务，然后调用PoStartNextPowerIrp和IoCompleteRequest。例如，当保持系统电源IRP以便设置设备电源状态适合于被请求的系统状态时,设备电源策略所有者常常必须发送一个设备电源IRP。

在这种情况下，电源策略所有者应该在系统电源IRP中设置IoCompletion例程，把系统电源IRP传递到相邻较低层驱动程序中，并且从其DispatchPower例程中返回STATUS_PENDING。在IoCompletion例程中，它调用PoRequestPowerIrp去发送设备电源IRP，把指针传递到请求中的回调例程。IoCompletion例程应该返回STATUS_MORE_PROCESSING_REQUIRED。最后，驱动程序从回调例程中向下传递系统IRP。驱动程序不能在DispatchPower例程中的内核事件上等待，对当前正在处理的IRP也不能用IoCompletion例程通知信号；否则，可能发生系统的死锁。详见第3章中关于“在设备电源策略所有者中处理一个系统设置电源IRP”。

类似的情况，当系统将要休眠时，电源策略所有者在发送设备IRP使其设备掉电之前可以根据需要完成一些未决的I/O。驱动程序应该对工作项目进行排队，然后从DispatchPower例程中返回STATUS_PENDING，而不是在I/O完成时通知一个事件并在DispatchPower例程中等待。在工人线程中，驱动程序等候I/O完成，然后发送设备电源IRP。详见在线DDK中的IoAllocateWorkItem (这个例程不适用于Windows98)。

1.3.3 设备休眠时排队I/O请求

当设备处于休眠时，在其设备栈中的驱动程序应该对设备的I/O请求排队。IoAllocateWorkItem，IoQueueWorkItem，和IoFree WorkItem支持例程提供一种对延迟处理排队IRP的方法。这一例子参见第2部分第3章“当设备暂停时保持内入的IRP”中的为PnP驱动程序的排队机制。

只有当设备在Working(DO)状态下时，驱动程序能访问其设备。当设备在休眠状态时，驱动程序不能触及任何设备寄存器，设备必须首先被返回到工作状态。

1.3.4 处理未被支持的或者无法识别的电源IRP

如果驱动程序不支持特定的电源IRP，那么它必须在设备栈中将IRP传递到相邻较低层驱动程序中。栈中更低的驱动程序可以准备处理IRP，但必须有机会这样做。

为了传递未被支持的或者无法识别的电源IRP，在“传递电源IRP”所描述顺序的中，驱动程序调用 PoStartNextPowerIrp，IoSkipCurrentIrpStackLocation，和PoCallDriver。驱动程序在向下传递IRP之前，不应该在IRP中改变任何事物。


独立设备的电源管理

作为对系统电源状态变化的响应，驱动程序管理电源，检测和关闭空闲设备，并在需要时给设备上电。本章介绍与设备电源管理有关的以下内容：

2.1 设备电源状态

2.1.1 设备工作状态DO

2.l.2 设备休眠状态D1，D2，和D3

2.l.3 设备电源状态所需的支持

2.2 管理设备电源策略

2.3 为设备电源状态处理IRP_MN_SET_POWER

2.3.1 处理设备掉电IRP

2.3.2 处理设备上电IRP

2.3.3 设备电源IRP的IoCompletion例程

2.4 为设备电源状态处理IRP_MN_QUERY_POWER

2.5为设备电源状态发送IRP_MN_QUERY_POWER或者发送IRP_MN_SET_POWER

2.6 检测空闲的设备

2.6.1 使用电源管理器例程进行空闲检测

2.6.2 执行设备特定的空闲检测

关于处理系统电源状态转变的内容，参见第3章“处理系统电源状态请求”，关于IRP_MN_WAIT_WAKE 请求的内容，参见第4章“支持具有唤醒能力的设备”。

1.1 设备电源状态

设备电源状态描述特定设备的电源状态。设备电源状态用下列标准术语定义为：

。能源消耗： 多少能源供设备使用？

。设备环境： 设备在此状态中需保持多少操作环境？

。设备驱动程序行为：设备驱动程序必须为设备做些什么才能使设备恢复成为完全操作状态？

。恢复时间： 设备恢复成为完全操作状态需要多长时间？

。唤醒能力：设备能从这种状态请求唤醒吗？通常，如果设备能从一种给定的电源状态(例如 D2) 请求唤醒，那么它也能从任何大功率的状态(D1)请求唤醒。

电源状态的确切定义是设备相关的。并非所有设备都能定义所有状态；许多设备仅仅定义DO和D3状态。要了解特定设备的设备状态的定义，以及每种状态的操作需求是什么，参见有关的“设备类型电源管理的参考规范”(可在微软网站www.microsoft.com上找到)。

设备的电源状态不必与系统的电源状态匹配。例如，即使系统是在（SO）工作状态下，某些设备也可以在（D3）关闭状态中。通常，因为许多设备是从系统中得到能量的，所以设备的电源状态没有系统的高。然而，能唤醒系统的设备是例外情况；一般来说，当系统是在休眠状态下时，这些设备就处于一种低潜伏休眠状态(D1或者D2)。

注意，某些设备在一种单一的设备电源状态之内时，有若干不同的低能模式。如果其驱动程序能自动地把设备从一方式转换到另一种方式，而不改变设备电源状态，那么这个设备就能使用这些模式。但是，通常如果在这些方式中没有用户可觉查到的区别，设备应该只使用最低能模式。如果一个低能模式，诸如低速率，影响性能或者除设备驱动程序外对软件是不透明的，硬件不应该自动地使用它。详见“设备类型电源管理的参考规范”。

驱动程序或者电源管理器可以请求设备电源状态的转变，而且所有驱动程序都必须准备处理请求转变的IRP。详见“为设备电源状态发送IRP_MN_QUERY_POWER或IRP_MN_SET_POWER，为设备电源状态处理IRP_MN_QUERY_POWER和IRP_MN_SET_POWER”。

和系统一样，设备也能从工作状态(DO)转变到任何休眠状态(D1-D3)，也能从任何休眠状态转变到工作状态。

1.1.1 设备工作状态DO

在D0状态下，设备是完全打开和操作的。

能源消耗

设备有最大的连续能源消耗

设备环境

保持所有的环境

设备驱动程序行为

正常操作

恢复时间

不可用

唤醒能力

不可用

1.1.2 设备休眠状态D1，D2，和D3

设备状态D1，D2，和D3是设备休眠状态。

D1和D2是休眠状态。许多设备的类别都不定义这些状态。所有设备必须定义状态D3。

设备状态D1

设备状态D1是最高功率的休眠状态。它有下列的特性

能源消耗

其消耗处在小于DO状态而大于或等于D2状态。

设备环境

一般来说，设备环境是由硬件保存，且不需要由驱动程序恢复。

设备驱动程序行为

驱动程序必须能保存、恢复或者再初始化硬件丢失的环境。但是，通常是在设备进入这种状态时就会丢失少量的环境。

恢复时间

一般来说，从D1到D0恢复设备所需要的时间应该小于从D2到D0恢复设备所需要的时间。

唤醒能力

状态D1的设备能够请求唤醒。

为了最大限度地减少用户觉察到的延迟，设备从D1恢复到D0应有可能的最小延迟。在状态转变中，最大限度地减少延迟比减少能源消耗更重要。

设备状态D2

状态D2是具有下列特性的一种中间休眠状态：

能源消耗

消耗小于或者等于在D1状态下时的消耗。

设备环境

一般来说，大多数的设备环境被硬件丢失。

设备驱动程序行为

设备驱动程序必须能保存、恢复或者再初始化被硬件丢失的环境。当它进入D2时，通常设备丢失大多数环境。

恢复时间

从D2到D0恢复设备至少和从D1到D0恢复设备的时间一样长。

唤醒能力

状态D2中的设备能够请求唤醒。

在状态D2中，能源消耗下降到可恢复的最低水平, 驱动程序可在合理的时间内恢复设备，即大多数用户不会发现此设备有明显的延迟。状态D1优先考虑用户对延迟感知,状态D2优先考虑节能。因此，通常从状态D2到状态D0的恢复时间超过从状态D1到状态D0的恢复时间。例如，在状态D2中，减少总线上的能量可以使设备关闭一些它的功能。这样，需要更多时间以重新启动设备，并且恢复设备。

许多类型的设备不定义这种状态。

设备状态D3

设备状态D3是最节能的休眠状态.所有设备都必须支持这种状态。它有下列的特征：

能源消耗

设备中和/或者系统中完全断电。

设备环境

设备驱动程序仅仅负责恢复设备环境。驱动程序必须保存、恢复设备环境或者必须在转变成不同的电源状态时重新初始化设备。

设备驱动程序行为

设备驱动程序仅仅负责恢复设备环境，通常从最近的工作配置。

恢复时间

总的恢复时间是其他所有设备状态中最高的。

唤醒能力

在状态D3中的设备不能唤醒一个休眠的系统。

在状态D3中，设备中完全断电或者仅有最小限度的节拍电流可供使用。驱动程序和硬件必须为断电作准备。当电源恢复之后，操作系统提供必要的硬件复位信号。为了使设备返回到工作状态，设备驱动程序必须能恢复和重新初始化设备而不依赖于所运行的BIOS代码。

电源管理器可以为进入D3的父设备中消除系统电源。

所有类型的设备都定义这种状态。

1.1.3 设备电源状态所需的支持

要了解正在工作的设备定义了哪些设备状态，以及每个状态的操作需求是什么，参见“设备类别电源管理的参考规范”。这些规范参见微软网站。

对没有电源管理规范的早期设备和其他设备应该遵循“缺省类别设备电源管理规范”。实质上，缺省规范有以下要求：

。支持D0和D3状态

。当设备被加电时，驱动程序存储、恢复或者重新初始化设备环境

。驱动程序管理设备电源策略

类别驱动程序和端口驱动程序一般支持电源管理。如果你正在写链接到这样一种驱动程序上的微驱动程序，那么检查可类别或者端口驱动程序说明，以查明在微驱动程序中要求的电源管理支持程度。使用下列一般的方法：

。网络适配器驱动程序必须符合NDIS 5.0规范，以及电源管理接口，参见“设备类型电源管理的参考规范”和在线DDK中的“网络驱动程序设计指南”。这些驱动程序不使用卷1“Windows 2000驱动程序开发参考”所描述的接口。

。流式驱动程序在流式类别驱动程序中使用电源管理接口来处理设备状态D0和D3。为了处理设备状态D1和D2，这些驱动程序必须使用在这里以及卷1“Windows 2000驱动程序开发参考”所描述的电源管理接口。

。SCSI端口驱动程序为微端口管理大多数的PnP和电源管理需求。SCSI微端口驱动程序必须支持MicrosoftÒWindowsÒ2000 PnP和带有相关例程（如HwScsiAdapterControl）的电源管理接口。详见在线DDK中“内核模式驱动程序设计指南”。

。视频端口驱动程序为微端口管理大多数的PnP和电源管理需求。视频微端口驱动程序必须支持微端口特定例程，参见在线DDK中“图表驱动程序设计指南和参考”。视频微端口驱动程序并不使用卷1中的“Windows 2000驱动程序开发参考”所描述的接口。

。所有其他的WDM驱动程序(USB、IEEE 1394等等)和Windows 2000 PnP驱动程序使用类别相关的电源管理接口。参见在线DDK中“核心模式驱动程序设计指南”)或卷1“Windows 2000驱动程序开发参考”所描述的接口，

1.2 管理设备电源策略

正象电源管理器为系统保持和管理电源策略一样，在设备栈中的每一设备都有一个驱动程序为设备保持和管理电源策略。这个驱动程序就是设备的“设备电源策略所有者”。

设备电源策略所有者是拥有有关设备使用和电源状态的大多数信息的驱动程序。实际上，它不必设置给设备加电或断电的设备寄存器，但是必须确定设备何时正被使用、何时处于空闲以及何时能改变电源状态。

一般来说，设备的功能驱动程序是其电源策略所有者，尽管对某些设备其他驱动程序或者系统组件可能会担当上述这个角色。与电源管理有关的驱动程序类型的信息，参见第1部分第1章“驱动程序的类型”。

一些驱动程序为设备(创建FDO)充当功能驱动程序，为列举的子设备充当总线驱动程序(创建PDO)。在电源和PnP IRP的Dispatch例程中，这种驱动程序必须能区别发送到FDO的IRP的处理和发送到PDO的IRP的处理。

例如，对SCSI适配器，驱动程序可以为适配器本身充当功能驱动程序(创建FDO)，为附属于适配器的磁盘充当总线驱动程序(创建PDO)。此驱动程序作为SCSI适配器的功能驱动程序/策略所有者，它为SCSI适配器接收系统IRP和请求设备IRP。作为磁盘的总线驱动程序，它处理和完成建立磁盘PDO的设备IRP。虽然驱动程序拥有设备(FDO)的电源策略，但并不意味着它也拥有子设备(PDO)的电源策略。

设备电源策略所有者负责下列情况：

。当它处理PnP管理的IRP_MN_START_DEVICE请求时，调用PoSetPowerState设置设备的初始电源状态为D0。

需要时，设备应该加电；例如，设备必须加电去处理一个I/O请求。设备电源策略所有者负责确定何时需要设备，设备是否已经加电，以及设置正确的设备电源状态。通常，PnP启动设备IRP经完成时，设备应该是加电的。

一般来说，大多数设备在没有使用时应该是断电的，甚至系统在工作状态下。

。通过调用PoRequestPowerIrp发送设备电源请求，以响应系统电源请求。

例如，当策略所有者接收一个系统设置电源IRP时，它就发送一个设备设置电源IRP。当系统进入任何休眠状态时，大多数设备进入D3。在DEVICE_CAPABILITY结构中的DeviceState数组列出设备能为每个系统状态保持的最大功率状态(参见第1章中“报告设备电源能力”)。

。检测设备何时是空闲的并将其置于休眠状态，以节能。

电源管理器或是设备策略所有者都能检测空闲设备，并且发送设备电源IRP以改变其状态。详见“检测空闲的设备”。

。需要时，把其设备返回到工作状态。

对一个休眠设备，当一个I/O请求到达时，设备的驱动程序应该把它返回到工作状态。

。需要时，为设备激活或禁止唤醒能力。

设备电源策略所有者发送和取消等待/唤醒IRP，参见第4章“支持具有唤醒能力的设备”。

1.3 为设备电源状态处理IRP_MN_SET_POWER

设备设置电源IRP为单一设备请求改变状态，并且被发送到此设备的设备栈中的所有驱动程序上。这样的IRP指定I/O栈位置的Power.Type成员为DevicePowerState。

当掉电IRP到达栈中时，驱动程序就处理这些IRP。当上电IRP到达栈中时，驱动程序设置IoCompletion例程，然后当IRP在栈中向上回传时，在IoCompletion例程中处理这些IRP。在典型的设备栈中，驱动程序处理设备设置电源IRP如下：

。大多数过滤驱动程序仅仅把IRP传递到相邻较低层驱动程序中(参见第1章中的“传递电源IRP”)，它传送PoCallDriver的状态作为其DispatchPower例程的返回状态。然而，一些过滤驱动程序，可能还首先需要执行一些设备的相关任务，诸如对内入IRP的排队或者保存设备状态。

。功能驱动程序执行设备的相关任务(诸如完成未决的I/O请求，内入I/O请求的排队，保存设备环境，或者改变设备能量)，如有必要，设置一个IoCompletion例程，并且把设备电源IRP传递到相邻较低层驱动程序中。它传送PoCallDriver的状态作为其DispatchPower例程的返回状态。

。如果总线驱动程序有能力改变设备电源状态，则调用PoSetPowerState，通知电源管理器新的设备状态，并且调用PoStartNextPowerIrp开始下一电源IRP。然后，完成IRP，指定IO_NO_INCREMENT。如果驱动程序不能立即完成IRP，则调用IoMarkIrpPending，从其DispatchPower例程中返回STATUS_PENDING，然后稍迟完成IRP。

驱动程序不应该失败IRP_MN_SET_POWER请求。

即使目标设备已处于请求的电源状态下，每一功能驱动程序或者过滤驱动程序都必须把IRP传递到相邻较低层驱动程序中。每一个设置电源IRP必须在设备栈中向下传递到能完成IRP的总线驱动程序中。

每当设备改变状态时，其驱动程序都必须调用PoSetPowerState以通知电源管理器此变化。同有关上电、掉电的其他驱动程序任务一样,在设备上电之后(如果新的状态是D0)或者在设备掉电之前(如果新的状态是其他任何状态)调用PoSetPowerState。

每一驱动程序应该追踪其设备的电源状态。电源管理器不向驱动程序提供此信息。

当为设备电源状态处理IRP_MN_SET_POWER请求时，驱动程序应该尽可能迅速地从DispatchPower例程中返回。

驱动程序不能在其DispatchPower例程中，等待由处理同一IRP的代码设置信号量的内核事件。因为电源IRP在整个系统中是同步的，可能会发生死锁。

驱动程序在处理电源IRP必须采取的确切步骤，依赖于设备是在上电还是掉电，如在下列各节中所描述的：

。2.3.1 处理设备掉电IRP

。2.3.2 处理设备上电IRP。

1.3.1 处理设备掉电IRP

设备掉电IRP指定次功能代码IRP_MN_SET_POWER和设备状态(PowerDeviceDO，PowerDeviceD1，PowerDeviceD2，或PowerDeviceD3),这些状态必须是小功率的或者是等于当前设备电源状态的。当IRP在栈中向下传递时，驱动程序必须处理掉电IRP。高层驱动程序必须在低层驱动程序之前处理IRP。没有设备相关任务执行的驱动程序,应该立即把IRP传递到相邻较低层驱动程序中。

图2.1显示了处理这样的IRP的有关的步骤：

如果IRP指定PowerDeviceD3，通常功能驱动程序将执行下列的任务：

。调用IoAcquireRemoveLock，传递当前IRP，以确保当处理电源IRP时，驱动程序不接收PnP IRP_MN_REMOVE_DEVICE请求。

如果IoAcquireRemoveLock返回一个失败状态，则驱动程序不应该继续处理IRP。相反，它应该调用PoStartNextPowerIrp，然后完成IRP(IoCompleteRequest)，返回失败状态。

。执行任何设备必须在设备断电之前完成的特定任务,，例如关闭设备，完成或者更新任何未决的I/O，禁止中断，排队内入的IRP，以及保存设备环境(可用保存的环境恢复或重新初始化设备)。

当时处理IRP时，驱动程序不应该造成长时间的延迟(例如，对于这种类型的设备用户可以检测到不合理的延迟)。

在设备返回到工作状态之前，驱动程序应该对任何内入的I/O请求进行排队。

图2.1 处理设备掉电请求

。有可能检查Parameters.Power.ShutdownType值。如果系统设置电源IRP是活动的，则ShutdownType提供关于系统IRP的信息。关于这个值,详见第3章中“系统电源行动”。

在冬眠路径上的设备驱动程序必须检查此值。如果ShutdownType是PowerActionHibernate,驱动程序应该保存任何需要的环境以恢复设备，而不应该使设备掉电。

。改变设备的物理电源状态，如果驱动程序有能力这样做，并且改变是适当的。

。调用PoSetPowerState，向电源管理器通知新的设备电源状态。

。调用PoStartNextPowerIrp，表明它已准备好处理下一个电源IRP。

。为相邻较低层驱动程序建立栈位置。

当设备在任何休眠状态下时，驱动程序不能与其设备通信。设备进入休眠状态时，IoCompletion例程是很少使用的。如果驱动程序没有在这个IRP中设置IoCompletion例程，那么它能用IoSkipCurrentIrpStackLocation为相邻较低层驱动程序设置栈位置。否则，驱动程序必须使用IoCopyCurrentIrpStackLocationtoNext。

。调用PoCallDriver，把IRP传递到相邻较低层驱动程序中。IRP必须向下传递到完成IRP的总线驱动程序中。

。调用IoReleaseRemoveLock，释放以前拥有的锁。

。由PoCallDriver传送返回的状态作为从其DispatchPower例程的返回状态。

驱动程序必须保存所有设备环境信息，并且在传发IRP之前设置新的电源状态。环境信息至少应该包含请求新的电源状态。它也应该包括在上电时驱动程序需要的有关任何附加信息。在完成IRP之后，且设备已经掉电的情况下，驱动程序不能再访问设备，同时设备环境不再有用了。

每一驱动程序都必须把IRP传递到相邻较低层驱动程序中。当IRP到达总线驱动程序时，总线驱动程序关闭设备的电源(如果它有这个能力的话)，调用PoSetPowerState通知电源管理器，并完成IRP。

然而，如果总线驱动程序服务于冬眠设备，则它应该检查IRP中的ShutdovnType值是否是PowerSystemHibernate。如果是，总线驱动程序将调用PoSetPowerState以便报告PowerDeviceD3，但是不应该使设备掉电。在冬眠文件以及系统的其余部分保存之后，设备将掉电。

最终，其子设备是要掉电的，总线驱动程序也可以选择使其总线掉电。这样的行为称为设备依赖。

如果IRP指定任何其他状态(D0，D1或者D2)，那么需要的驱动程序行动是设备依赖的。一般来说，支持这些状态的设备当I/O请求到达时，能迅速返回到工作状态。这种设备的驱动程序必须完成任何未决的I/O请求，对所有新的请求进行排队，在转发IRP到相邻较低层驱动程序之前，要保存所有必要的环境。当IRP到达总线驱动程序时，它将硬件设置为请求状态。当它休眠时，驱动程序不能访问设备。

在某些环境下，当设备已处于DO状态时，功能驱动程序或者过滤驱动程序可以收到指定PowerDeviceDO状态的设备电源IRP。驱动程序与处理其他设置电源IRP一样的方法处理此IRP； 完成未决的I/O请求，对内入的I/O请求进行排队，设置一个IoCompletion例程，并且把IRP传递到相邻较低层驱动程序中。但驱动程序不能改变设备的硬件设置。当总线驱动程序收到IRP时，它仅仅完成此IRP。当IRP完成之后，功能驱动程序和过滤驱动程序能处理任何排队请求。当一个高层驱动程序试图I/O时，对I/O请求排队, 直到IRP完成,以消除低层驱动程序试图改变设备寄存器的可能性。

1.3.2 处理设备上电IRP

设备上电IRP指定IRP_MN_SET_POWER和比当前设备状态更多能量的设备状态。一般来说，一个上电IRP指定设备工作状态PowerDeviceDO。

首先由下层的总线驱动程序为设备处理上电请求，然后通过各层驱动程序在栈中向上回传。

图2.2显示处理上电IRP的有关步骤。

图2.2 处理一个设备上电请求

当为了上电处理一个IRP_MN_SET_POWER请求时，功能驱动程序或者过滤驱动程序必须做到:

。调用IoAcquireRemoveLock，确保驱动程序在处理上电IRP时不会收到IRP_MN_REMOVE_DEVICE请求。

如果IoAcquireRemoveLock返回失败状态，驱动程序不应该继续处理IRP。相反，它应该调用PoStartNextPowerIrp，接着完成IRP(IoCompleteRequest)，然后返回失败状态。

。调用IoSetCompletionRoutine，建立一个上电IoCompletion例程。

当处理设备上电IRP时，在IRP已经完成且设备上电之后,驱动程序应该设置IoCompletion例程以便恢复环境，释放删除的锁，并且执行其他需要的任务。在IRP完成之前，驱动程序不应该恢复环境。详见“设备电源IRPIoCompletion例程”。

。调用IoCopyCurrentIrpStackLocationToNext，设置IRP栈位置。记住，如果一个驱动程序建立IoCompletion例程，则它不能调用IoSkipCurrentIrpStackLocation。

。调用PoStartNextPowerIrp，开始下一个电源IRP。

当IRP栈位置指向当前驱动程序时，驱动程序必须调用PoStartNextPowerIrp。因此，在调用PoCallDriver之前，驱动程序能从DispatchPower程序中或者从IoCompletion程序调用它。

。调用PoCallDriver，把IRP传递到相邻较低层驱动程序中。

IRP必须在设备栈中向下传递到总线驱动程序中，只有总线驱动程序可以完成它。

当总线驱动程序收到IRP时，应该首先检查以确认设备仍然存在并且在休眠时未被移去或替换。如果设备不存在，总线驱动程序将在父设备上调用IoInvalidateDeviceRelations，并通知PnP管理模块设备已经消失。在这种情况下，总线驱动程序可以使电源IRP失败。

如果设备仍然存在，总线驱动程序则执行请求的任务，把设备返回到操作状态中，然后调用PoSetPowerState通知电源管理器新状态，并且完成IRP(IoCompleteRequest)。如果设备正在休眠，驱动程序已排队I/O，或者设备请求启动功率，则总线驱动程序给设备供电。否则，一旦总线驱动程序必须与设备通信时，它立即就给设备供电。

1.3.3 设备电源IRPIoCompletion例程

在总线驱动程序完成IRP之后，I/O管理器调用由高层驱动程序注册的IoCompletion例程。

当设备进入D0状态时，不论是从休眠状态返回还是因系统启动而进入D0状态, 每个驱动程序通常都建立一个IoCompletion例程，用以完成返回工作状态所需执行的大多数任务。如图2.3中所显示。

图2.3 设备上电IoCompletion例程

这些任务包括：

。根据需要恢复设备状态或者再初始化设备。

。调用PoSetPowerState，向电源管理器通知设备处于D0电源状态。

。当设备处于非工作状态下时,准备处理由驱动程序排队的I/O。

。如果驱动程序最初没有发送当前的电源IRP,则调用PoStartNextPowerIrp去接收下一个电源IRP。

。调用IoCompleteRequest完成IRP。

。释放为设备环境分配的内存。

。当驱动程序收到IRP时，调用IoReleaseRemoveLock，释放驱动程序在其DispatchPower例程中获得的锁。

在总线驱动程序或者高层驱动程序与设备进行通信之前，总线驱动程序不会给设备上电。

当其设备进入休眠状态时，驱动程序能够建立IoCompletion例程，但这样的例程用处不大；当设备不在D0状态时驱动程序不能访问它。

1.4 为设备电源状态处理IRP_MN_QUERY_POWER

一个设备查询电源IRP用来查询单个设备状态的变化，并且把它发送到设备栈中的所有驱动程序上。这个IRP在I/O栈位置Power Type成员中指定DevicePowerState。

在查询电源IRP向栈底传送期间驱动程序处理它们。

如果下面的一切都成立，功能驱动程序或者过滤驱动程序就能使IRP_MN_QUERY_POWER请求失败：

。设备被激活了唤醒能力，同时请求的电源状态是在设备能唤醒系统的状态之下。例如，从D2而不是从D3唤醒系统的设备可以使D3的查询失败，但可以使D2的查询成功。

。进入请求的状态会迫使驱动程序放弃丢失数据的操作，诸如一个开放的调制解调器连接。驱动程序很少会因为这个原因使查询失败；在大多数情况下，都是应用程序处理的。

为了使一个IRP_MN_QUERY_POWER请求失败，驱动程序采取下列的措施：

1.调用PoStartNextPowerIrp，表明其准备好处理下一个电源IRP。

2.将Irp->IoStatus.Status置为一个失败状态，并且调用IoCompleteRequest，在调用中指定IO_NO_INCREMENT。驱动程序不把IRP在栈中再向下传递。

3.从其DispatchPower例程中返回一个错误状态。

如果驱动程序使查询IRP成功，为了防止其成功地完成后面一个查询电源状态的IRP_MN_SET_POWER请求，它不能开始任何操作或者采取任何行动。

成功完成IRP的驱动程序必须为查询的状态准备一个设置电源IRP，并且向下传递查询IRP，如下：

1.完成任何未完成的I/O操作。

2.排队内入的I/O请求。

3.避免开始任何可能干扰指定电源状态转变的新的活动。然而，驱动程序不会保存设备的环境，也不会为关闭采取其他步骤。

4.调用PoStartNextPowerIrp，表明准备处理下一个电源IRP。

5.调用IoSkipCurrentIrpStackLocation，为相邻较低层驱动程序设置IRP栈位置。

6.调用PoCallDriver，把查询IRP传递到相邻较低层驱动程序上。不要完成IRP。

7.传送由PoCallDriver返回的状态作为从其DispatchPower例程中的返回状态。驱动程序不能改变在Irp->IoStatus.Status中的值。

当查询IRP到达总线驱动程序时，总线驱动程序调用PoStartNextPowerIrp，如果它能变为指定的电源状态，则设置Irp->IoStatus.Status 为STATUS_SUCCESS。如果它不能变为指定的电源状态，则设置一个失败状态。然后总线驱动程序调用IoCompleteRequest，指定IO_NO_INCREMENT。

在一个典型的设备栈中的驱动程序处理设备查询电源IRP，其情形如下：

。大多数的过滤驱动程序仅仅把IRP传递到相邻较低层驱动程序中(参见第1章中“传递电源IRP”)，并且传送由PoCallDriver返回的状态作为从其DispatchPower例程中的返回状态。然而，某些过滤驱动程序，可能首先需要执行设备特定任务，诸如排队内入的IRP或者保存设备状态。

。功能驱动程序执行设备特定任务(诸如完成未决的I/O请求，排队内入的I/O请求，保存设备环境，或者改变设备状态)，必要时建立一个IoCompletion例程，并且把设备电源IRP传递到相邻较低层驱动程序。传送由PoCallDriver返回的状态作为从其DispatchPower例程中的返回状态。

。总线驱动程序调用PoStartNextPowerIrp开始下一个电源IRP。接着它完成IRP，指定IO_NO_INCREMENT。如果驱动程序不能立即完成IRP，则它调用IoMarkIrpPending，从其DispatchPower程序中返回STATUS_PENDING，然后稍迟再完成IRP。

即使目标设备已处于被查询的电源状态下，每一个功能驱动程序或者过滤驱动程序也必须排队I/O，并且把IRP向下传递到相邻较低层驱动程序中。IRP必须在设备栈中向下传递到完成它的总线驱动程序上。

当处理一个IRP_MN_QUERY_POWER请求时，驱动程序会尽可能迅速地从DispatchPower例程中返回。驱动程序不必在DispatchPower例程中等待由处理相同IRP的代码所发出的核心事件信号，因为电源IRP在整个系统中是同步的，可能会发生死锁。

1.5 为设备电源状态发送IRP_MN_QUERY_POWER或者发送IRP_MN_SET_POWER

设备电源策略所有者发送设备查询电源IRP(IRP_MN_QUERY_POWER)，以确定低层驱动程序是否能提供设备电源状态的变化和改变设备电源状态的设备设置电源IRP(IRP_MN_SET_POWER)。(这个驱动程序也能发送一个等待/唤醒IRP，使其设备唤醒以响应一个外部信号，详见第4章。)

当下面的任意一个条件成立时，驱动程序就会发送一个IRP_MN_QUERY_POWER请求：

。驱动程序收到一个系统查询电源IRP。

。驱动程序正在准备把一个空闲设备置于休眠状态，因此必须查询低层驱动程序，确定这样做是否可行。

当下面的任意一个条件成立时，驱动程序就会发送一个IRP_MN_SET_POWER请求：

。驱动程序已确定设备是空闲的、且能置为休眠状态。

。设备正在休眠，必须重新进入工作状态，处理等待的I/O。

。驱动程序收到一个系统设置电源IRP。

驱动程序不能分配自己的电源IRP；电源管理器为此目的提供PoRequestPowerIrp例程。PoRequestPowerIrp分配和发送IRP，并且与PoCallDriver一同，保证所有电源请求适当地同步(参见第1章“处理电源IRP的规则”)。PoRequestPowerIrp的调用者必须正在IRQL<=DISPATCH_LEVEL下运行。

下列是这个例程的原型：

NTSTATUS

PoRequestPowerIrp(

IN PDEVICE_OBJECT DeviceObject，

IN UCHAR MinorFunction，

IN POWER_STATE PowerState，

IN PREQUEST_POWER_COMPLETE CompletionFunction，

IN PV0ID Context，

QUT PIRP*Irp OPTIONAL

)；

为了发送IRP，驱动程序调用PoRequestPowerIrp，在DeviceObject中指定一个指向目标设备对象的指针,在MinorFunction中指定次IRP代码IRP_MN_SET_POWER或者在IRP_MN_QUERY_POWER，PowerState.Type 中指定值DevicePowerState，在PowerState.State中指定设备电源状态。在Windows 98中，DeviceObject必须指定下层设备的PDO；在Windows 2000中，这个值即能指向PDO也能指向同一设备栈中驱动程序的FDO。

当其他驱动程序都已完成IRP之后，如果此驱动程序必须执行附加的任务，那么它必须在CompletionFunction中传递一个指向电源完成函数的指针。当驱动程序把IRP向下传递到设备栈中时，I/O管理器在调用所有由驱动程序建立的IoCompletion程序之后调用CompletionFunction。

只要设备的电源策略所有者发送设备电源查询IRP，那么它接着就从调用PoRequestPowerIrp中指定回调例程(CompletionFunction)上发送一个设备设置电源IRP。如果查询成功，设置电源IRP将设备设置在查询的电源状态中。如果查询失败，设置电源IRP重新确认当前设备电源状态。重新确认当前状态是很重要的，因为驱动程序排队I/O以响应查询；策略所有者必须发送设置电源IRP，把其设备栈返回到工作状态。

记住，设备的策略所有者不仅发送设备电源IRP，而且当IRP向下传递到设备栈时也处理IRP。因此，这样的驱动程序常常建立一个IoCompletion例程，(和IoSetCompletionRoutine一起)作为其IRP处理代码的一部分，尤其是当设备正在上电时。在CompletionFunction之前，用由其他驱动程序建立的IoCompletion例程按顺序调用IoCompletion例程。详见2.3.3节“设备电源IRP IoCompletion例程”。

因为当调用CompletionFunction时，所有驱动程序都完成IRP，所以CompletionFunction不能用它创建的IRP 调用PoStarNextPowerIrp或者PoCallDriver（但是它调用不同于这些IRP的例程)。相反，这些例程执行由创建IRP的驱动程序请求的附加行为。

如果驱动程序发送响应系统IRP的设备IRP，CompletionFunction可以完成系统IRP(参见第3章中的“在设备电源策略所有者中处理一个系统设置电源IRP”)。

为了响应PoRequestPowerIrp的调用，电源管理器分配电源IRP并且把它发送到设备的设备栈顶端。电源管理器返回一个指向分配的IRP的指针。

如果没有错误发生，PoRequestPowerIrp返回STATUS_PENDING，意思是已经成功地发送IRP，并且是未决完成。如果电源管理器不能分配IRP，或者如果调用者指定了一个无效的电源次IRP代码,则此调用失败。

设备加电的请求，首先必须由位于下面的总线驱动程序为设备处理，然后由栈中的每一个高层驱动程序连续地处理。因此，当发送一个PowerDeviceDO请求时，驱动程序必须保证在IRP完成之后，其CompletionFunction执行请求的任务，同时设备已经上电。

当关闭设备电源(PowerDeviceD3)时，设备栈中的每一个驱动程序必须保存其所有必要的环境，并且在把IRP发送到相邻较低层驱动程序之前做必要的清理。环境信息的范围和清理范围依赖于驱动程序的类型。功能驱动程序必须保存硬件环境；过滤驱动程序可以按需要保存其自己的软件环境。在这种情形中的CompletionFunction设置能采取与完成的IRP有关的行动，但是驱动程序不能访问设备。

1.6 检测空闲的设备

每一种设备的电源策略所有者负责确定设备何时空闲、并且把它置为休眠状态以节能。策略所有者有两种方法检测空闲设备：

。用电源管理器例程进行空闲检测

。自己执行设备特定的空闲检测

1.6.1 用于空闲检测的电源管理器程序

电源管理器通过PoRegisterDeviceForIdle和PoSetDeviceBusy例程为空闲检测提供支持。

为了使设备能进行空闲检测，设备电源策略所有者调用PoRegisterDeviceForIdleDetection，并指定下列情况。

。系统为追求性能进行优化时用的空闲超时值

。系统为追求节能进行优化时用的空闲超时值

。空闲时设备应该转向的设备电源状态

PoRegisterDeviceForIdleDetection返回一个空闲计数器指针，以后驱动程序将用此指针禁止空闲检测。PoRegisterDeviceForIdleDetection的调用者必须在IRQL<DISPATCH_LEVEL上运行。

在设备已经启动、且已准备好处理设备电源IRP之后，驱动程序能在任何时间为空闲检测注册其设备。例如，对一个PnP启动设备IRP，驱动程序可以激活空闲检测作为其IoCompletion例程的一部分。

任何给定设备的超时值应该与设备的上电等待时间成正比，并基于观测到的设备行为。对于一定类型的设备，驱动程序能指定节能和性能超时值为-1，以对设备的类型使用标准电源策略超时值。详见设备特定文档。

当设备使用时，驱动程序必须调用PoSetDeviceBusy，传递由PoRegisterDeviceForIdleDetection返回的指针。PoSetDeviceBusy复位空闲计数，因此为设备重新启动空闲计数。驱动程序应该在每一个I/O操作中调用PoSetDeviceBusy。

为了确定设备是否空闲，电源管理器为当前系统电源策略(节能或者性能)把空闲计数器的值与驱动程序指定的空闲超时值进行比较。对系统电源策略常用的功能参见“平台SDK”。

当设备满足超时值时，电源管理器发送一个设备设置电源IRP，指定驱动程序在PoRegisterDeviceForIdleDetection调用中传送的设备电源状态。电源管理器在发送此设置电源IRP之前不发送查询IRP。在栈中的驱动程序处理此设置电源IRP。驱动程序必须以某种适当的方式完成它，而且不能失败。(参见“处理设备掉电IRP”)。

当驱动程序不再请求空闲检测或者不再准备处理设备掉电IRP时，它就会再一次调用PoRegisterDeviceForIdleDetection为超时值传递零值。为节能(电池)和性能(AC)电源策略，设置超时值为零，以禁止空闲检测。驱动程序能迅速地重新激活空闲检测； 它只需要用非零超时值调用PoRegisterDeviceForIdleDetection。一旦驱动程序注册了设备，它就能够通过改变超时值激活或禁止空闲检测，即使设备已经掉电或者再次被唤醒。

1.6.2 执行设备特定的空闲检测

驱动程序能够执行基于设备特定标准的自己的空闲检测，而不使用电源管理器的空闲检测例程。

这种驱动程序应该将其空闲设备放进最低可能的休眠状态中，此状态对当前系统状态是有效的。为此，驱动程序用次IRP代码IRP_MN_SET_POWER请求电源IRP(PoRequestPowerIrp)，把设备电源状态指定到设备应该转变的状态上。


处理系统电源状态请求

所有驱动程序都必须能够响应系统电源状态的请求，而不管系统是成功地处于休眠，冬眠，还是在唤醒状态。

如果任何驱动程序都不支持系统电源管理，独立设备可以休眠或者唤醒，但是电源管理器不能使整个系统进入休眠状态中。

本章描述驱动程序应该如何响应改变或者查询系统电源状态的IRP。 它包括下列主题：

3.1 系统电源状态

3.1.1 系统工作状态S0

3.1.2 系统休眠状态S1，S2，S3，S4

3.1.3 系统关机状态S5

3.1.4 系统电源行动

3.2 系统电源策略

3.3 防止系统电源状态变化

3.4 为系统电源状态处理IRP_MN_QUERY_POWER

3.4.1 使一个系统查询电源IRP失效

3.4.2 在设备电源策略所有者中处理系统查询电源IRP

3.4.3 在总线驱动程序中处理系统查询电源IRP

3.4.4 在过滤驱动程序中处理系统查询电源IRP

3.5 为系统电源状态处理IRP_MN_SET_POWER

3.5.1 在设备电源策略所有者中处理系统设置电源IRP

3.5.1.1 确定当前的设备电源状态

3.5.1.2 发送一个响应系统设置电源IRP的设备设置电源IRP

3.5.2 在总线驱动程序中处理系统设置电源IRP

3.5.3 在过滤驱动程序中处理系统设置电源IRP

1.1 系统电源状态

系统电源状态描述整体系统的能源消耗。操作系统支持六种系统电源状态，即从S0(充分加电和操作)到S5（断电）。每一状态具有下列特征：

。能源消耗：计算机使用多少功率电源？

。软件恢复：操作系统从什么点重新启动？

。硬件等待时间：计算机返回工作状态需要多少时间？

。系统环境：要保持多少系统环境？操作系统必须重新启动以返回工作状态吗？

状态S0是工作状态。状态S1 、S2 、S3，和S4是休眠状态，因为要降低能源消耗，所以在这些状态中，计算机显示关闭形式，可是它保持着足够的环境，这样不用重新启动操作系统就能返回到工作状态。状态S5是中断或者关机状态。

当系统从关机状态(S5)或者任何一个休眠状态(Sl-S4)转变到工作状态(S0)时，系统进入唤醒状态。当系统从工作状态转变到任何一个休眠状态或者关机状态时，系统进入休眠状态。图3.1显示系统电源状态的几种可能的转变。

图3.1 系统电源状态转变的可能

从图中可以看到，系统不能从一种休眠状态直接进入另一种休眠状态；它必须在进入另一种休眠状态之前先进入工作状态。例如，系统不能从状态S2转变到S4，也不能从状态S4转变到S2。它必须首先返回到状态S0，然后才能再进入下一个休眠状态。 因为处于一种中间休眠状态的系统已经失去某种操作环境，在它进行其他状态转变之前，它必须返回工作状态以恢复该环境。

1.1.1 系统工作状态S0

系统状态S0是系统的工作状态，具有下列特点：

能源消耗：

最大值。当然，单独设备的电源状态能够动态地改变在个别设备基础上的电源保护。未使用的设备可以根据需要掉电或者加电。

软件恢复：

不适应。

硬件等待时间：

没有。

系统环境：

保持所有环境。

1.1.2 系统休眠状态S1，S2，S3，S4

S1，S2，S3，和S4状态都是休眠状态。在这些状态中的系统不执行任何计算任务，并且显示关闭形式。但是它不同于关机状态(S5)中的系统，休眠系统保持存储状态，或者是在硬件上或者是在磁盘上。操作系统不需要重新启动就能使计算机返回到工作状态。当某些事件发生时，一些设备能从休眠状态唤醒系统，例如对调制解调器的调用。此外，在一些计算机上，外部指示器还告诉用户系统只是处于休眠状态。

从S1到S4，在顺序的每一种休眠状态中，大多数计算机关机。所有ACPI顺序计算机在S1中关闭处理器时钟，并且在S4丢失系统环境(除非在关机之前已写好“冬眠”文件)，就像本节下面几页中所列出的。休眠状态中间的一些细节情况主要取决于生产商如何设计机器。例如，在一些机器的某些芯片上，主板在状态S3上可能会掉电，而在其他一些类似的芯片上，直到状态S4还将保持电源。进一步说，一些设备或许只能从状态S1唤醒系统，而不能从较深层的休眠状态唤醒系统。

系统状态S1

系统状态S1是具有下列特点的一种休眠状态：

能源消耗：

小于在S0中的消耗，而大于在其他休眠状态的消耗。处理器时钟是关闭的，总线时钟是停止的。

软件恢复：

控制在其停止的地方重新启动。

硬件等待时间：

一般来说，不超过两秒时钟。

系统环境：

所有环境都由硬件保持和维护着。

系统状态S2：

除了CPU环境不同以外，系统状态S2类似于S1。由于处理器失去较低，所以系统高速缓存的内容被丢失。状态S2具有下列特点：

能源消耗：

小于在状态S1中的消耗，而大于在S3中的消耗。处理器时钟是关闭的，总线时钟是停止的。一些总线可能会掉电。

软件恢复：

在唤醒之后，从处理器的重置矢量开始控制。

硬件等待时间：

两秒或者更长时间，大于或者等于S1的等待时间。

系统环境：

CPU环境和系统高速缓存内容都已丢失。

系统状态S3：

系统状态S3是具有下列特点的一种休眠状态：

能源消耗：

小于状态S2中的消耗。处理器是关闭的，主板上的一些芯片也可能是关闭的。

软件恢复

在唤醒事件之后，控制从处理器的重置矢量开始。

硬件等待时间：

几乎与S2没有什么区别。

系统环境：

只有系统存储器被保留，而CPU环境、高速缓存内容、芯片上设置的环境都已丢失。

系统状态S4：

系统状态S4，即冬眠状态，是最低功率的休眠状态，并且有最长的唤醒等待时间。为了把能源消耗降低到最小，所有设备的硬件电源都是关闭。然而，操作系统环境被保持着，在进入S4状态之前系统就把它写在磁盘的那个冬眠文件上(内存映像)。当重新启动时，装入例程阅读此文件，并且跳到系统的上一个位置，即冬眠前的位置。

如果在状态S1，S2或者S3中的计算机失去了所有AC或者电池电源，那么它也就失去了系统环境，因此必须重新启动返回到S0。然而在状态S4中的计算机，甚至在它失去电池或者AC电源之后还能从其以前的位置重新启动。因为系统环境保存在冬眠文件中。在冬眠状态的计算机不使用电源(稀疏的电流是个例外情况)。

状态S4具有下列的特点：

能源消耗：

除了稀疏的电流会流到电源开关和类似的设备上之外，其余都是关闭的。

软件恢复：

系统从保存的冬眠文件中重新启动。 如果冬眠文件不能被装载，就要求重新启动。当系统在S4状态下时，重新配置的硬件可能会导致某些变化，阻止了冬眠文件的正确装载。

硬件等待时间：

等待时间长且未定义。只有物理互动才使系统返回到工作状态。这种互动可以包括：用户按下“ON”开关；如果有适当的硬件而且又能被唤醒，LAN上的调制解调器或活动的输入环等。如果硬件支持它，那么机器也能从恢复的定时器中唤醒。

系统环境：

硬件中没有保留任何内容。在掉电之前，系统在冬眠文件中写了一个存储映象。当操作系统被装载时，它阅读此文件并且跳到其以前的位置上。

1.1.3 系统关机状态S5

在S5状态或者称为关机状态中，机器没有存储状态，并且不执行任何计算任务。

状态S4和S5之间唯一的区别是：在状态S4中计算机能从冬眠文件中重新启动，而在状态S5中重新启动需要重新引导系统。

状态S5具有下列特点：

能源消耗：

除了流到电源按钮的稀疏电流之外，其余全是关闭的。

软件恢复：

在唤醒时要求引导。

硬件等待时间：

等待时间长且未定义。只有物理互动，诸如用户按下“ON”开关，使系统返回到工作状态。如果系统这样配置，BIOS也能从恢复的定时器上被唤醒。

系统环境：

没有保留任何内容。

1.1.4 系统电源动作

当电源管理器发送IRP设置或者查询系统电源状态时，它与附加的参数一同指定系统电源状态，给出有关电源状态变化的信息。这个参数，在Irp->Parameters.Power.ShutdownType中是一个POWER_ACTION类型的枚举变量。这个枚举变量描述系统电源状态的请求，如表3.1中所示：

表3.1系统电源动作

----------------------------------------------------------- POWER_ACTION 枚举变量 要求的系统电源状态

————————————————————————————

PowerActionNone S0或者无系统电源IRP活动

PowerActionSleep S1 、S2，或者S3

PowerActionHibernate S4

PowerActionShutdown(只有 S5

Microsoftz@Windows@2000)

PowerActionShutdownReset S5

PowerActionShutdownOff S5

---------------------------------------------------------

对于状态S5， 当驱动程序收到一个系统查询IRP或者设置电源IRP时，它能对更多关于请求关机的信息检查ShutdownType。当机器正在重置而不是不确定的关闭电源时，驱动程序能用这个信息来优化它的关机序列。当系统重置时，大多数设备的驱动程序保持着电源。然而，对某些设备，诸如执行DMA的视频流设备，当系统重置时，驱动程序可以选择其设备掉电，这样就可以停止任何正在进行的I/O操作。

当设备电源策略所有者把设备电源IRP发送到其设备栈中以响应系统电源IRP时，驱动程序能使用ShutdounType参数得到有关当前系统电源IRP的信息。在这种情况下，ShutdownType值表明当前请求的系统电源状态，或者在系统要求并不明显时它是PowerActionNone。然而，如果设备IRP请求状态D0，驱动程序应该不依赖于这个信息。

在Windows 98上，当IRP请求设备电源状态时，这个字段总包含PowerActionNone。

1.2 系统电源策略

在系统电源策略管理模块中，电源管理器追踪系统的活动，确定适当的系统电源状态，并且发送IRP_MJ_POWER请求，以查询或者改变系统电源状态。它也通过应用程序读写电源策略的设置提供接口(参见平台SDK)。

电源管理器支持两项不同的电源策略—— 一个是对AC（畴壁电流）另一个是对DC(电池或者UPS)，并且取决于当前电源在这两种电源策略之间的自动转换。一般来说，AC电源策略注重保护超过注重性能，而DC电源策略注重性能超过注重保护。为了查明系统何时从一种策略变成另一种策略，驱动程序能够注册带有系统\Callback\PowerState回调对象的通知。详见“Windows2000驱动程序开发参考”第2卷中的ExCreateCallback和“内核模式驱动程序设计指南”第1部分第3章中的Callback对象。

按照APCI说明的计算机能自动地从AC转换到电池电源，或者从一个电池转换到另一个电池，就象每一个这类电源断电一样。如果计算机硬件允许操作系统选择电源，那么电源管理器跟踪最近充电且功能依旧的电池，并且选择此电池作为计算机的电源。

一旦AC电源变得可供使用，计算机硬件便自动地开始给电池充电。如果硬件允许操作系统选择为哪一个电池充电，那么电源管理器就会选择放电最少的电池去再充电；这样任何时候系统至少具有一个充好电的电池的机会就增大了。

无论其他设置是什么，只要电池具有再充电能力或者提供的系统电源报告硬件条件“临界”，并且在两秒或者更长时间是处于放电状态中，那么电源管理器为临界电池执行DC电源策略。这种状态下的电源策略一般要求转变为冬眠状态或者关机状态。

1.3 防止系统电源状态变化

虽然驱动程序不能直接设置系统电源策略，可是电源管理器提供三个例程使驱动程序能防止系统的转变超越工作状态之外：PoSetSystemState，PoRegisterSystemState，和PoUnregisterSystemState。

通过调用PoRegisterSystemState或者PoSetSystemState，驱动程序能够向电源管理器通知用户已经到场或者驱动程序请求使用系统或者显示。

PoRegisterSystemState允许驱动程序注册一个连续地繁忙状态。由于驱动程序能稍迟改变其设置，它返回一个回柄。一旦状态注册生效，电源管理器就不能试图使系统进入休眠状态。驱动程序通过调用PoUnregisterSystemState撤消状态注册。

有了PoSetSystemState，驱动程序就能通知相同条件的电源管理器(用户在场，系统请求，显示请求)，但是这个设置不是连续的。它具有重新启动与指定条件有关的递减计数器的作用。

使用这些例程，驱动程序能阻止许多、但并非所有超越工作状态之外的转变。当电源损失是非常紧迫的、或者当用户明确请求关机时，电源管理器总是关闭系统。

1.4 为系统电源状态处理IRP_MN_QUERY_P0WER

电源管理器发送带有次IRP码IRP_MN_QUERY_POWER和Parameters.Power.Type中的SystemPowerState的电源IRP，以确定它是否能安全地变为一个指定系统电源状态(S1-S5)，并且允许驱动程序为这一变化做准备。

只要可能，电源管理器在发送一个较低(更小功率)状态请求IRP_MN_SET_POWER之前首先查询。然而，在失效的电池或者电源损失非常紧迫的情况下，电源管理器不先查询就直接发送设置电源IRP。在发送一个IRP将系统设置在工作状态之前，电源管理器决不发送查询请求。

对系统电源状态的IRP_MN_QUERY_POWER请求并不要求动作，除非驱动程序必须使IRP失效或者为其设备栈管理电源策略。大多数过滤驱动程序仅仅把IRP传递到相邻较低层驱动程序中，直到它到达完成它的总线驱动程序。驱动程序决不发送设备IRP_MN_SET_POWER请求以响应系统查询；它仅在收到系统设置电源请求之后，才请求这个IRP。

关于如何处理系统电源查询的更多信息参见下列内容：

。3.4.1 使系统查询电源IRP失效

。3.4.2 在设备电源策略所有者中处理系统查询电源IRP

。3.4.3 在总线驱动程序中处理系统查询电源IRP

。3.4.4 在过滤驱动程序中处理系统查询电源IRP

因为电源管理器把系统查询IRP发送到系统的每一个设备栈中，一种设备的驱动程序可能使查询失效，而其他设备的驱动程序则能成功地完成它。也可能电池用尽而查询激活,要求立即关机。因此，在查询IRP之后，每一个驱动程序必须准备接收下列任何电源IRP：

。查询状态的IRP_MN_SET_POWER

。不同电源状态的IRP_MN_SET_POWER

。当前电源状态的IRP_MN_SET_POWER

。任何状态的IRP_MN_QUERY_POWER

然而，一般来说，驱动程序收到一个跟随系统查询IRP之后的系统设置电源IRP。如果查询成功，则设置电源IRP指定查询状态。如果查询失效，则设置电源IRP重新确定当前电源状态。

1.4.1 使系统查询电源IRP失效

如果下列两者之一为真，功能驱动程序或者过滤驱动程序就使IRP_MN_QUERY_POWER请求失效：

。设备为唤醒被激活，并且请求系统电源状态的功率比SystemWake的值小，SystemWake的值指定了设备能唤醒系统的最小功率状态。例如，从S2而不是从S3中能唤醒系统的设备，对状态S3将是一个失效的查询，但是对状态S2可能是一个成功的查询。

。进入符合请求状态的设备状态会使驱动程序放弃丢失数据的操作，诸如打开调制解调器连接。由于这个原因，驱动程序很少丢失查询；在大多数情况下，应用例程处理这样的情况。

为了使IRP_MN_QUERY_POWER请求失效，驱动程序应该采取下列措施：

1.调用PoStartNextPowerIrp，表明其已准备处理下一个电源IRP。

2.设置Irp->IoStatus.Status为失效状态，并且调用IoCompleteRequest，指定IO_NO_INCREMENT。不把IRP传递到更下面的设备栈中。

3.调用IoReleaseRemoveLock，释放以前获取的锁。

4.从其DispatchPower例程中返回一个失效状态。

1.4.2 在设备电源策略所有者中处理系统查询电源IRP

当设备电源策略所有者为系统状态接收IRP_MN_QUERY_POWER时，它通过传递下来的查询请求，并且在IoCompletion例程中为设备电源状态发送一个IRP_MN_QUERY_POWER作为响应。当栈中所有的驱动程序都已完成设备查询时，设备电源策略所有者完成系统查询。拥有设备电源策略的驱动程序应该采取下列措施在其DispatchPower例程中响应一个系统查询请求：

1.调用IoAcquireRemoveLock，传递当前的IRP，保证在处理电源IRP时，驱动程序不接收PnP IRP_MN_REMOVE_DEVICE请求。如果IoAcquireRemoveLock返回失效状态，驱动程序不应该继续处理这个IRP。相反，它应该调用PoStartNextPowerIrp，然后，完成这个IRP(IoCompleteRequest)，并且返回失效状态。

2.保证驱动程序能支持查询的系统电源状态，如在“使一个系统查询电源IRP失效”中所描述的。如果不能保证，就用在那节中所描述的失效状态完成这个IRP。

然而，如果设备具有唤醒能力，但它不能从冬眠状态唤醒系统，则其驱动程序对状态S4不能使一个查询请求失效。在这种情况下，对于驱动程序(发送IRP_MN_WAIT_WAKE)电源策略所有者必须撤消这个等待唤醒的IRP，并且完成系统查询。详见第4章中“撤消一个等待/唤醒的IRP”。

3.如果驱动程序能支持查询系统电源状态，则在系统查询电源IRP中设置一个IoCompletion例程。

4. 通过调用IoCopyCurrentIrpStackLocationToNext，为相邻较低层驱动程序创建IRP栈位置。

5.调用PoCallDriver，把IRP传递到相邻较低层驱动程序中。

6.从其DispatchPower例程中，由IoCallDriver传递返回的状态。

IoCompletion例程应该做下列事情：

l。 检查Irp->IoStatus.Status，保证低层驱动程序成功地完成IRP。如果任何驱动程序已失效于IRP，调用IoCompleteRequest完成此IRP，并且返回失效状态。

2.如果低层驱动程序已经成功地完成这个IRP，调用PoRequestPowerIrp，为所查询的系统状态是有效的设备状态发送一个设备查询电源IRP。如果必要的话，根据在DEVICE_CAPABILITIES结构中的DVICE_STATE数组，确定查询的系统状态中哪一个设备状态是有效的。

3.在调用PoRequestPowerIrp中，指定一个回调例程(CompletionFunction)并且在环境区域内传递系统IRP。

4.从IoCompletion例程中返回STATUS_MORE_PROCESSING_REQUIRED，这样驱动程序在回调例程中能完成处理中的系统查询IRP。

在IRP已经完成之后，并且在IRP处理过程中所创建的所有IoCompletion例程都已运行的情况下，电源管理器通过I/O管理器调用电源策略管理模块的回调例程。此回调例程必须依次执行下列操作：

1. 调用 PoStartNextPowerIrp，开始下一个电源IRP。

2. 对设备查询电源IRP，用返回的状态完成系统查询电源IRP(IoCompleteRequest)。

3. 调用IoReleaseRemoveLock，释放以前所获得的锁。

4. 用完成的查询IRP返回状态。

设备电源策略所有者不仅发送设备查询，而且还必须在其进入设备栈时处理它。详见第2章中的“为设备电源状态处理IRP_MN_QUERY_POWER”。

1.4.3 在总线驱动程序中处理系统查询电源IRP

当系统查询IRP到达总线驱动程序中时，总线驱动程序保证它能支持符合查询系统状态的设备状态；同时，如果能够等待/唤醒，则查询系统状态将不阻止其设备唤醒系统。

总线驱动程序调用PoStartNextPowerIrp，如果它能变为指定的电源状态，则在STATUS_SUCCESS中设置Irp->IoStatus.Status；如果它不能变为指定的电源状态，则设置一个失效状态。然后总线驱动程序调用IoCompleteRequest，指定IO_NO_INCREMENT。

在总线驱动程序完成IRP之后，电源管理器调用IoCompletion例程，这个例程是由传递IRP到栈中的其他驱动程序所创建的。

1.4.4 在过滤驱动程序中处理系统查询电源IRP

对于所有过滤驱动程序和功能驱动程序，如果它们并不拥有其设备栈的电源策略，那么它们就应该把一个系统查询电源IRP传递到相邻较低层驱动程序中，其步骤如下：

1.调用IoAcquireRemoveLock，传递当前IRP，保证驱动程序当处理电源IRP时不能接收PnP IRP_MN_REMOVE_DEVICE请求。

如果IoAcquireRemoveLock返回失效状态，则驱动程序不应该继续处理此IRP。相反，驱动程序应该调用PoStartNextPowerIrp，然后完成此IRP，并且返回失效状态。

2.确定它是否应该使查询失效。其方法和完整的处理过程参见3.4.1“使系统查询电源IRP失效”。

3.调用PoStartNextPowerIrp。

4.设置此IRP栈位置(IoSkipCurrentIrpStackLocation或者IoCopyIrpStackLocationToNext)。虽然驱动程序能在此IRP中创建一个IoCompletion例程，但是这样做没有太大的必要。

5.把这个IRP传递到相邻较低层驱动程序中(PoCallDriver)。

6.调用IoReleaseRemoveLock。如果驱动程序为此IRP创建了一个IoCompletion例程，那么用IoCompletion例程代替这次的调用。

7. 传递由PoCallDriver返回的状态作为其DispatchPower例程的返回状态。

1.5 为系统电源状态处理IRP_MN_SET_POWER

电源管理器发送一个电源IRP，指定次代码IRP_MN_SET_POWER和一个系统电源状态,由于下列原因之一：

。改变系统电源状态

。在一个失效的IRP_MN_QUERY_POWER请求之后，重新指定当前的电源状态

通过I/O管理器，电源管理器在每一个PnP设备节点上把IRP发送到设备栈中的顶端驱动程序。这个IRP通知当前系统电源状态栈中的所有驱动程序。

为了保证正常启动，电源管理器对系统加电IRP排序，所以在子设备加电之前，父设备有机会加电。电源管理器在发送系统电源IRP之前不进行查询。

为了保证计算机能正常休眠或关机，电源管理器发送系统IRP，指定休眠、冬眠，或者关机都处于一种定义好的序列中，这样离根较远的设备要比离根近的设备先掉电。只要可能，电源管理器在发送这样一个IRP之前要进行查询。详见“为系统电源状态处理IRP_MN_QUERY_POWER”。

系统电源IRP并不直接请求改变电源状态——它只是发出一个通知。驱动程序不能直接响应系统电源IRP以改变其设备的电源状态，而只能响应设备电源IRP来改变其设备的电源状态(设备电源策略所有者发送设备电源IRP，参见“在设备电源策略所有者中处理系统设置电源IRP”)。

对请求的系统电源状态，即使设备已经在一个有效的设备电源状态中，每一个驱动程序也必须把系统设置电源IRP传递到相邻较低层驱动程序中，直到它到达总线驱动程序。只有总线驱动程序允许完成这个IRP。驱动程序根据它在设备栈中的作用，处理这个IRP，在下面几节中描述：

。3.5.1在设备电源策略所有者中处理系统设置电源IRP

。3.5.2在总线驱动程序中处理系统设置电源IRP

。3.5.3在过滤驱动程序中处理系统设置电源IRP

驱动程序不能使IRP_MN_SET_POWER请求失效。电源管理器忽略此IRP返回的任何失效状态。

1.5.1 在设备电源策略所有者中处理系统设置电源IRP

对每一个系统设置电源IRP中指定的系统电源状态，在每一个设备栈中的设备电源策略所有者负责将其设备栈放到一个适当的设备电源状态中。

当设备电源策略所有者为系统电源状态接收IRP_MN_SET_POWER，它通过传递下来的IRP响应，同时在IoCompletion例程中，为一个相应的设备电源状态发送IRP_MN_SET_POWER。当栈中所有的驱动程序都已完成这个设备IRP时，设备电源策略所有者才完成系统IRP。

详见下列两节：

。3.5.1.1确定当前的设备电源状态

。3.5.1.2发送一个响应系统设置电源IRP的设备设置电源IRP

1.5.1.1 确定当前的设备电源状态

电源策略所有者能在DEVICE_CAPABILITY结构中,根据Device_State数组对每一系统状态确定设备电源的有效范围。对每个系统状态，数组列出位于下层的设备能支持的最大功率。

当在这个范围中选择特定状态时，需考虑下列情况：

。当系统进入S0状态时，大多数设备进入S0状态。

。当系统进入任何休眠状态时，大多数设备进入D3状态。然而，能够被唤醒的设备如果支持这些状态，则可以请求代替进入D1或者D2。详见第1章中“报告设备电源能力”。

。申请特殊规则的设备将掌握冬眠文件。如果系统IRP请求PowerSystemHibernate，掌握冬眠文件的设备决不能掉电。这个设备的电源策略所有者应该请求设备状态D3，并且保存环境。但是设备的驱动程序决不能使设备掉电。

如果系统IRP请求PowerSystemHibernate，驱动程序将在Irp->Parameter.Power.ShutdownType中检查POWER_ACTION的值，以获得更多关于状态变化原因的信息。详见3.1.4节“系统电源行动”。

设备电源策略所有者必须为每一个系统设置电源IRP发送设备设置电源IRP，即使设备已经处于当前的设备状态中。如果设备预先已经暂停操作响应查询电源IRP，则设置电源IRP就会通知设备停止IRP的排队，并且为其当前的电源状态返回正常的操作。 当设备处于D3状态时，唯一的例外情况是驱动程序不必对D3发送一个附加的IRP_MN_SET_POWER请求。

1.5.1.2 发送一个响应系统设置电源IRP的设备设置电源IRP

设备电源策略所有者应该采取下列措施来响应一个系统设置电源IRP：

1.调用IoAcquireRemoveLock，传递当前的IRP，保证当处理电源IRP时驱动程序不接收PnP IRP_MN_REMOVE_DEVICE请求。

如果IoAcquireRemoveLock返回失效状态，驱动程序不应该继续处理IRP。相反，它应该调用PoStartNextPowerIrp，完成IRP(IoCompleteRequest)，并且返回失效状态。

2. 在系统设置电源IRP中创建IoCompletion例程。

3. 通过调用IoCopyCurrentIrpStackLocationToNext,为相邻较低层驱动程序创建IRP栈位置。

4.调用PoCallDriver，把IRP传递到相邻较低层驱动程序。

5.传递由PoCallDriver返回的状态作为其DispatchPower例程的返回值。

在IoCompletion例程中，驱动程序发送一个设备设置电源IRP的过程如下：

1.首先检查IRP得到请求的系统电源状态，然后为此系统电源状态选择一种适当的设备电源状态。详见3.5.1.1节“确定当前的设备电源状态”。

2.调用PoRequestPowerIrp，为以步骤1所确定的设备电源状态发送IRP_MN_SET_POWER。 即使设备已经处在那种状态中，电源策略所有者也必须发送设备SET_POWER请求。

3.在调用PoRequestPowerIrp指定回调例程(CompletionFunction)，并且在环境区域中传递系统IRP。

4.从IoCompletion例程中返回STATUS_MORE_PROCESSING_REQUIRED，这样驱动程序能在回调例程中完成处理系统设置电源IRP。

记住,设备电源策略所有者不仅能发送设备设置电源IRP，而且当此IRP穿过设备栈还必须能处理它。因而，设备电源策略所有者不仅可以包含一个与设备设置电源IRP有关的回调例程和一个系统设置电源IRP所创建的IoCompletion例程，而且还可以包含一个为设备设置电源IRP所创建的IoCompletion例程。详见第2章中“为设备电源状态处理IRP_MN_SET_POWER”。

当设备设置电源IRP传递到设备栈中时，所创建的IoCompletion例程已经运行，回调例程被调用。这时，栈中的所有驱动程序都已完成设备设置电源IRP，同时设备电源的转变也已完成。

回调例程必须处理下列情况：

1.调用PoStartNextPowerIrp，开始下一个电源IRP。

2. 对于设备设置电源IRP，用返回的状态完成系统设置电源IRP(IoCompleteRequest)。

3. 调用 IoReleaseRemoveLock，释放以前获取的锁。

4. 用完成的设置电源IRP返回此状态

1.5.2 在总线驱动程序中处理系统设置电源IRP

当总线驱动程序接收一个系统设置电源IRP时，它必须采取下列的措施：

1. 调用PoStartNextPowerIrp，开始下一个电源IRP。

2. 把Irp->IoStatus.Status设置为STATUS_SUCCESS。驱动程序不能使系统设置电源IRP失效。

3. 调用IoCompleteRequest，指定IO_NO_INCREMENT，完成IRP。

总线驱动程序不能改变设备电源的设置，直到它收到一个请求设备电源状态的电源IRP。

1.5.3 在过滤驱动程序中处理系统设置电源IRP

对于所有过滤驱动程序和功能驱动程序，如果它们并不拥有其设备栈的电源策略，那么它们就仅仅把系统设置电源IRP传递到相邻较低层驱动程序中，其步骤如下：

1. 调用IoAcquireRemoveLock，传递当前IRP，保证当处理电源IRP时驱动程序不能接收PnP IRP_MN_REMOV_DEVICE请求。

如果IoAcquireRemoveLock返回失效状态，则驱动程序不应该继续处理IRP。相反，驱动程序应该调用PoStartNextPowerIrp,然后完成IRP，并且返回失效状态。

2. 调用PoStartNextPowerIrp。

3. 设置此IRP栈位置(IoSkipCurrentIrpStackLocation或者IoCopyIrpStackLocationToNext)。虽然驱动程序能在此IRP中创建IoCompletion例程，但是这样做的必要性不大。

4. 把这个IRP传递到相邻较低层驱动程序中(PoCallDriver)。

5. 调用IoReleaseRemoveLock。如果驱动程序为此IRP创建了一个IoCompletion例程，那么用此IoCompletion例程代替这次的调用。

6. 传递由PoCallDriver返回的状态作为其DispatchPower例程的返回状态。


支持具有唤醒能力的设备

能响应外部唤醒信号设备的驱动程序必须能够处理IRP_MN_WAIT_WAKE请求(等待/唤醒IRP)。这种设备电源策略所有者必须能够发送一个IRP_MN_WAIT_WAKE请求。

一般来说，无论什么原因，设备的唤醒信号也是设备的一个正常的服务事件。例如，用户输入时，可以使键盘唤醒系统，键盘和其驱动程序就是一个正常的服务事件。

所有驱动程序的开发者都应该阅读本章的第一段，它概述了等待/唤醒操作。后面各段提供关于处理和发送等待/唤醒IRP。

本章包括下列的主题：

4.1 等待/唤醒操作综述

4.1.1确定设备是否能唤醒系统

4.1.2理解通过设备树的等待/唤醒IRP的路径

4.1.3完成等待/唤醒IRP综述

4.2 处理等待/唤醒IRP的步骤

4.2.1在功能驱动程序或者过滤驱动程序(FDO)中处理等待/唤醒IRP

4.2.2 在总线驱动程序(PDO)中处理等待/唤醒IRP

4.2.3 等待/唤醒IRP的IoCompletion例程

4.3 发送等待/唤醒IRP

4.3.1 确定何时发送等待/唤醒IRP

4.3.2 等待/唤醒IRP请求

4.3.3 等待/唤醒回调例程

4.3.4 撤消等待/唤醒IRP

4.4 等待/唤醒IRP的Cancel例程

1.1 等待/唤醒操作综述

实际上，Microsoft@Windows@2000和WDM唤醒机制的工作情况如图所示。

图4.1 IRP_MN_WAIT_WAKE处理综述

1.当系统和设备处于工作状态时，设备的电源策略所有者确定其设备应该何时被唤醒(“待命”)。电源策略所有者请求一个电源IRP(带有次代码IRP_MN_WAIT_WAKE的PoRequestPowerIrp)，这个电源IRP被发送到PDO用以通知其设备栈中所有驱动程序的。在这个请求中，策略所有者指定一个回调例程(并非与IoCompletion例程一样)。

2.电源管理器，通过I/O管理器，把IRP发送到设备栈的顶端。

3.驱动程序设置IoCompletion例程，并且向下传递IRP直到它到达总线驱动程序。

4.总线驱动程序能够唤醒物理设备，如果确定能够唤醒，标明IRP为未决状态。必要的话，总线驱动程序也为其父设备请求一个等待/唤醒IRP。

5.稍后，外部唤醒信号到达。

6.总线驱动程序完成IRP_MN_WAIT_WAKE。

7. 当驱动程序把IRP向下传递到栈中时，I/O管理器调用所创建的IoCompletion例程。

8.当I/O管理器请求此IRP时，它就调用由策略所有者创建的回调例程。

IRP_MN_WAIT_WAKE请求不能改变设备或者系统的电源状态。如果设备进入一种适当的休眠状态，一个外部信号将会使得设备(和可能的系统)唤醒。所以它只能唤醒设备，而且要稍迟一些。

当唤醒信号到达时，不论设备是唤醒系统还是唤醒自己,驱动程序的动作是相同的。如果设备有唤醒能力，并且系统正处在设备能唤醒它的休眠状态下时，设备将唤醒此系统。 如果设备有唤醒能力，并且系统正处在工作状态下，则只有设备将被唤醒。

因为计算机和设备在设计方面有所不同，特别是在电源启动方面，支持系统和设备电源状态——能够支持等待/唤醒的状态，在硬件配置方面都是不相同的。因此，拥有其设备电源策略的任何驱动程序和所有的总线驱动程序，必须注意正在运行中的设备的单独配置能力。详见“确定设备是否能唤醒系统”。

对于等待/唤醒操作上的进一步的细节，参见“理解通过设备树的等待/唤醒IRP的路径”。

1.1.1 确定设备是否能唤醒系统

一些设备，诸如键盘，调制解调器以及网卡等，在设备休眠状态下能响应外部信号。作为其电源管理技术的一部分，操作系统为这些设备唤醒一个休眠系统提供了一个方法，即能恢复系统以前的环境。软件唤醒机制依靠系统和设备硬件以及BIOS中的支持,允许系统从除S5(PowerSystemShutdown)之外的任何状态中被唤醒。处于状态S5的系统必须重新启动。

虽然操作系统设计成可以由任何中间的休眠状态被唤醒，但是其精确唤醒能力从计算机到计算机以及从设备到设备都是不同的。并非所有计算机都支持所有系统休眠状态；因此，对一些计算机而言，从某些状态唤醒的能力是毫无意义的。

同样地，大多数设备既不支持所有设备状态(D0到D3),也不支持从这些设备确实支持的所有设备状态而来的唤醒状态。

设备能进入的休眠状态与它支持唤醒的状态一同由总线驱动程序列举描述，并且被存储在DEVICE_CAPABILITY结构中。表4.1列出这种结构中与等待/唤醒支持有关的成员。

表4.1 DEVICE_CAPABILITY结构中唤醒信息

_________________________________________________________

成员 描述

_________________________________________________________

DeviceD1 如果设备支持状态PowerDeviceD1为真

DeviceD2 如果设备支持状态PowerDeviceD2为真

WakeFromDO 如果设备能从PowerDeviceD0中唤醒为真

WakeFromD1 如果设备能从PowerDeviceD1中唤醒为真

WakeFromD2 如果设备能从PowerDeviceD2中唤醒为真

WakeFromD3 如果设备能从PowerDeviceD3中唤醒

DeviceState[PowerSystemMaximum] 指定这种设备能为每一系统状态支持的最大功率的设备

状态，PowerSystemUnspecified 到 PowerSystemShutdown

SystemWake 指定系统能被唤醒的最低的电源状(S0到S4)。

DeviceWake 指定设备能唤醒的最低的电源状态(D0到D3)。

_________________________________________________________

DeviceWake项目列出设备能响应唤醒信号的最低电源设备状态。PowerDeviceUnspecified的值表示设备不能唤醒系统。SystemWake项列出系统能被唤醒的最低电源状态。由于这些值是基于父设备节点的能力，所以驱动程序不能改变它们。详见第1章中“报告设备的电源能力”。

总之，如果下列条件满足，设备就能唤醒系统：

。处于电源状态下的设备，其功率等于或大于 DeviceWake的值。

。处于电源状态下的系统，其功率等于或大于SystemWake的值

。

1.1.2 理解通过设备树的等待/唤醒IRP的路径

在一种独立设备栈内，电源策略所有者发送一个等待/唤醒IRP，同时所有驱动程序都处理这个等待/唤醒IRP，这个过程在“等待/唤醒操作综述”中概要介绍了，其详细内容分别在“发送等待/唤醒IRP”和“处理等待/唤醒IRP的步骤”。

在设备树(叶节点、父节点、祖父节点等等) 组成的分支之内，驱动程序必须相互合作，以保证等待/唤醒IRP到达一个能使所有必要的硬件都能够唤醒的驱动程序中。 在ACPI计算机上，ACPI负责系统特定的一般目的事件(GPE)寄存器，其寄存器与每一叶片设备来的唤醒信号有关。因而，驱动程序必须请求并且传递等待/唤醒IRP，直到到达一个ACPI过滤驱动程序(在开始插入在设备栈中)或者位于下面的ACPI驱动程序中。ACPI使寄存器能够控制这个未决的IRP，直到信号到达，然后再完成这个IRP。 因为ACPI能响应唤醒信号，所以它不能把IRP传递到一个较低层驱动程序中。

ACPI过滤驱动程序，和位于下面的ACPI驱动程序本身一样，对其他驱动程序是透明的。为了在硬件中提供最大限度的灵活性，任何设备栈中的ACPI过滤驱动程序的确切位置是设备或系统特有的。在设计驱动程序时，关于设备栈中的ACPI过滤例程的存在或者位置不能做任何假设。

如前所述子驱动程序为每个子设备创建一个PDO，为父设备创建一个FDO。这样，驱动程序为子设备和功能驱动程序以及策略所有者，为父设备充当总线驱动程序。因此，只要总线驱动程序为了子PDO接收一个等待/唤醒的IRP时，它就应该为其父PDO请求另一个等待/唤醒的IRP。

图4.2显示了这样一种状态发生的配置样例状况。

图4.2 USB的配置样例

在这个配置样例中，键盘和调制解调器都是USB集线器的子系统，而USB集线器又是USB主机控制器的子系统，PCI总线列举出USB主机控制器。在下一页的图4.3显示配置样例中的键盘设备栈。

根据图4.3显示，从底部向上读：

1.ACPI驱动程序为PCI创建PDO。

2.PCI驱动程序创建PCI FDO和USB主机控制器PDO，并且拥有PCI设备栈的策略。

3.USB主机控制器驱动程序(主机控制器/总线驱动程序对)创建USB主机控制器FDO和USB集线器PDO。它拥有USB主机控制器设备栈的策略。注意，ACPI驱动程序也在这个栈中创建一个过滤例程D0。

4. USB集线器驱动程序创建USB集线器FDO和键盘PDO。这个驱动程序拥有USB集线器设备栈的电源策略。

5. 键盘的功能驱动程序是USBHID类的驱动程序/微驱动程序对。这个驱动程序为键盘创建FDO并且拥有其电源策略。因为键盘没有子设备，所以驱动程序不创建PDO。

注意，每一设备栈可以包括不被显示的附加的和可选的过滤DOs。为了允许键盘输入唤醒系统，策略所有者为键盘的PDO请求IRP_MN_WAIT_WAKE。这个IRP发送出一连串的其他等待/唤醒IRP。如图4.4所示

图4.4 等待/唤醒IRP请求的USB配置样例

当总线驱动程序接收一个为它所创建的PDO的IRP_MN_WAIT_WAKE目标，那么总线驱动程序必须为它拥有电源策略的设备栈请求另一个IRP_MN_WAIT_WAKE，同时创建一个FDO。

由图4.4所示：

1.键盘驱动程序调用PoRequestPowerIrp，发送等待/唤醒IRP(IRP1)到它的PDO。

电源管理器分配此IRP，并且通过I/O管理器把IRP发送到键盘设备栈的顶端。 驱动程序创建IoCompletion例程，并且在栈中向下传递IRP直到它到达键盘PDO。USB集线器驱动程序，为键盘充当总线驱动程序，并控制未决的IRP1。

2.因为当唤醒信号到达时，USB集线器驱动程序不能唤醒系统，所以USB集线器驱动程序必须调用PoRequestPowerIrp，为USB集线器设备栈请求一个等待/唤醒的IRP(IRP2)。

电源管理器把这IRP发送到USB集线器设备栈的顶端。在此栈中的驱动程序创建IoCompletion例程，并且把IRP传递到USB主机控制器驱动程序中(为USB集线器充当总线驱动程序)。USB主机控制器驱动程序控制未决的IRP2，直到键盘发出一个唤醒事件信号。

3.同样地，USB主机控制器驱动程序不能唤醒系统，因此USB主机控制器驱动程序调用PoRequestPowerIrp，发送一个等待/唤醒IRP(IRP3)到USB主机控制器设备栈。

电源管理器把此IRP发送到USB主机控制器设备栈的顶端，在那里驱动程序创建IoCompletion例程，并且把IRP传递到PCI驱动程序中(为USB集线器充当总线驱动程序)。PCI驱动程序控制未决的IRP3，直到键盘发出一个唤醒事件信号。

4.PCI驱动程序不能唤醒系统，因此PCI驱动程序调用PoRequestPowerIrp，并且发送一个等待/唤醒IRP(IRP4)到PCI设备栈中。其父设备是根设备，ACPI是总线驱动程序。

电源管理器把IRP发送到PCI总线设备栈的顶端；其驱动程序创建完成例程并且把的IRP传递到ACPI驱动程序中。

5.ACPI驱动程序能唤醒系统，因此它不发送等待/唤醒IRP到其他任何PDO。ACPI驱动程序控制未决的IRP4，直到一个唤醒信号到达。

当键盘确定唤醒信号时，ACPI驱动程序立刻阻止它。但是ACPI不能限制键盘确定这个信号，仅能让信号穿过根设备。接着，ACPI驱动程序完成IRP4，同时I/O管理器调用IoCompletion例程，支持PCI设备栈。当IRP4已经完成并且所有IoCompletion例程都已运行时，这个PCI驱动程序的回调例程被调用。在其回调例程中，PCI驱动程序限制信号穿过USB主机控制器。接着，PCI驱动程序完成IRP3。相同的事件通过USB主机控制器栈和USB集线器栈继续发生，直到键盘驱动程序收到IRP1。这时，如有必要，键盘驱动程序可以为唤醒事件服务。

每次当驱动程序发送一个等待/唤醒IRP到父PDO时，它必须为其自己的IRP创建一个Cancel例程。如果触发它的IRP被撤消，则创建Cancel例程，可以使驱动程序有机会撤消新的IRP。在这个USB例子中，如果键盘驱动程序撤消其等待/唤醒IRP(这样就禁止键盘的唤醒能力)，USB集线器，USB主机控制器和PCI驱动程序必须撤消他们所发送的IRP，以此作为键盘IRP的结果。详见“等待/唤醒的IRP Cancel例程”。

虽然一个父驱动程序可以列举一个以上的能够进行等待/唤醒的子例程，但是对PDO仅有一个等待/唤醒IRP是未决的。在这种情况下，父驱动程序应该随时确保它保持未决的等待/唤醒IRP，以使它们任何设备都能够被唤醒。为此，每次驱动程序收到一个等待/唤醒IRP时，它就会增加一个内部计数器的计数。而每次驱动程序完成一个等待/唤醒IRP时，它就会减少计数器的计数；而且，如果计数器的值是零，则应该发送另一个等待/唤醒IRP到它的设备栈中。

例如，以前在图4.2中所显示的USB配置中，USB集线器列举了两种设备，键盘和调制解调器。当USB集线器驱动程序为键盘PDO接收一个等待/唤醒IRP时，它在为其PDO请求一个IRP之前，增加一个等待/唤醒IRP的计数。如果调制解调器的策略所有者稍迟能够唤醒调制解调器，那么当调制解调器的等待/唤醒IRP到达时，USB集线器设备栈中已经有未决的IRP2。因为它不能同时控制两个未决的等待/唤醒IRP，所以USB集线器驱动程序增加其等待/唤醒参考计数，并且使新的IRP失效。当唤醒信号从键盘或者调制解调器到达时，USB集线器驱动程序首先确定是哪一个设备发信号，然后完成IRP2，并且减少其参考数。因为两种设备都能够被唤醒(由于其参考数不是零)，它必须发送自己的另一个等待/唤醒IRP为唤醒“重新武装”它自己。(对USB主机控制器和PCI驱动程序同样适用。)

然而，一个驱动程序不能把自己的IRP发送到唤醒信号刚刚到达的相同设备上，使其再次等待/唤醒。只有设备电源策略管理模块能这样做。因为再次等待/唤醒不能进行。

1.1.3 完成等待/唤醒IRP综述

当一个唤醒信号到达时，一个等待/唤醒IRP就完成了。唤醒信号是设备特定的，但是通常又是设备的一个正常服务事件。例如，一个引入的圆环可能使一台休眠的调制解调器醒来。

图4.5显示完成等待/唤醒IRP的步骤。

当信号发生时，在总线检测设备已经唤醒的位置上控制重新进入总线驱动程序。总线驱动程序为请求事件服务，并且调用IoCompleteRequest，为它的PDO完成 IRP_MN_WAIT_WAKE IRP。

接着，在设备栈中I/O管理器调用相邻较高层驱动程序所创建的IoCompletion例程。在IoCompletion例程中，驱动程序在必要时为唤醒信号服务，并且调用IoCompleteRequest完成IRP。I/O管理器继续调用IoCompletion例程工作以支持设备栈，直到所有驱动程序都完成IRP。

图4.5完成一个等待/唤醒IRP的步骤

在它的IoCompletion例程中，列举多于一种子设备(创建多于一个PDO)的任何设备和从多于一种的这样设备已经接收到等待/唤醒请求的任何设备必须为在另一个子设备上的等待/唤醒发送自己的一个等待/唤醒IRP以“重新武装”自己。详见“理解通过设备树的等待/唤醒IRP的路径”。

在调用由驱动程序创建的IoCompletion例程之后，当它把IRP传递到栈中时，I/O管理器在它请求等待/唤醒IRP时，调用由电源策略所有者所创建的回调例程。在回调例程中，策略所有者应该把其设备返回到工作状态，并且为其子PDO完成一个未决的等待/唤醒的IRP。在子设备栈中完成其子IRP，使得I/O管理器调用由驱动程序所创建的IoCompletion例程，如此等等。最后，在设备节点上，原来的等待/唤醒IRP开始的策略所有者确定其设备被确定的唤醒信号，并且完成所有未决的等待/唤醒IRP。

1.2 处理等待/唤醒IRP的步骤

所有PnP驱动程序必须准备接收带有次IRP码IRP_MN_WAIT_WAKE的电源IRP。驱动程序如何处理等待/唤醒IRP，取决于其设备栈中的位置、它所控制的设备类型以及其设备支持唤醒的特定状态。

下列各段提供了处理这个IRP所基于的驱动程序的类型和支持等待/唤醒的水平。

1.2.1 在功能驱动程序或者过滤驱动程序 (FDO)中处理等待/唤醒IRP

当创建FDO或者过滤D0驱动程序为相关的PDO接收到一个IRP_MN_WAIT_WAKE的请求时，它仅仅把IRP传递到相邻较低层驱动程序中，或者在传递IRP之前采取一定的行动。

对于支持唤醒的设备

在上面接收一个等待/唤醒IRP时，功能驱动程序或者过滤驱动程序应该采取下列的措施：

1.调用IoAcquireRemoveLock，传递当前IRP，保证在处理等待/唤醒IRP时不接收PnP IRP_MN_RWMOVE_DEVICE请求。

如果IoAcquireRemoveLock返回一个失效状态，那么驱动程序不应该继续处理这个IRP。相反，它应该调用PoStartNextPowerIrp，完成这个IRP(IoCompleteRequest)，并且返回失效状态。

2. 检查Irp->Parameters.WaitWake.SyslemWake中的值，并且在DEVICE_CAPABILITY结构中比较当前设备电源状态与DeviceState[SystemWake]。

如果设备支持唤醒，但是等待/唤醒IRP即不是从指定的SystemWake状态中来，也不是从当前设备电源状态中来，那么驱动程序应该使这个IRP失效，如下：

。在Irp->IoStatus.Status中设置STATUS_INVALID_DEVICE_STATE。

。调用PoStartNextPowerIrp。

。完成IRP(IoCompleteRequest)，指定IO_NO_INCREMENT的优先级提升。

。在Irp->IoStatus.Status中，从DispatchPower例程返回状态设置。

3. 否则，为IRP创建IoCompletion例程，使用IoSetCompletionRoutine。

IoCompletion例程应该执行驱动程序请求的任务，把设备返回到工作状态。

如果要撤消IRP，则也要调用IoCompletion例程。

4. 在其IoCompletion例程中，保存驱动程序可能需要的任何信息。

5.调用PoStartNextPowerIrp，开始下一个电源IRP。

6.调用PoCallDriver，把等待/唤醒IRP传递到相邻较低层驱动程序。

7.调用IoReleaseRemoveLock，释放以前获取的锁。

8.从DispatchPower例程中返回STATUS_PENDING。当驱动程序控制IRP时，驱动程序不能在Irp->IoStatus.Status中改变值。

对于不支持唤醒的设备

对不支持唤醒的设备,如果功能驱动程序或者过滤驱动程序接收一个等待/唤醒的IRP时，则驱动程序应该使这个IRP失效，其过程如下：

1. 调用PoStartNextPowerIrp。

2. 完成IRP(IoCompleteRequest)，指定IO_NO_INCREMENT的优先级提升。

3. 在Irp->IoStatus.Ststus中，从DispatchPower例程返回状态设置。

1.2.2 在总线驱动程序(PDO)中处理等待/唤醒IRP

像其他电源IRP一样，每一个等待/唤醒IRP必须通过设备栈向下传递到总线驱动程序(PDO)中，最后PDO负责完成IRP。接收上面的IRP时，总线驱动程序能立即使IRP失效或者处理未决的IRP，稍后完成它。下面是总线驱动程序必须采取的步骤：

1. 检查Irp->Parameters.WaitWake.SystemWake中的值。如果设备支持唤醒，而IRP即不是来自指定的系统唤醒状态,也不是来自当前设备电源状态，则驱动程序应该使这个IRP失效，如下：

。在Irp->IoStatus.Status中设置STATUS_INVALID_DEVICE_STATE。

。调用PoStartNextPowerIrp。

。完成IRP(IoCompleteRequest)，指定IO_NO_INCREMENT的优先级提升。

。在Irp->IoStatus.Status中从DispatchPower例程返回状态设置。

2. 检查一个等待/唤醒IRP对PDO是否是未决的。如果是，给STATUS_DEVICE_BUSY设置Irp->IoStatus.status，增加驱动程序内部的等待/唤醒IRP的计数，并且按以前步骤所描述的方法完成IRP。

对一个PDO，只有一个等待/唤醒IRP可以是未决的。

3. 如果驱动程序支持指定系统状态的唤醒，并且没有等待/唤醒IRP已经成为未决的，调用IoMarkIrpPending以指明对I/O管理器，IRP将被完成或者稍晚被撤消。不创建IoCompletion例程。

4. 设置激活设备硬件唤醒能力。

总线驱动程序激活其硬件唤醒能力，这种机制是属于设备依赖的。对于一个PCI设备，pci.sys负责设置PME位，因为这个驱动程序拥有PME寄存器。对于其他设备，参阅设备类别特定文档。

5. 如果PDO是FDO的子设备，则为FD0请求一个等待/唤醒IRP，保证为当前IRP创建一个Cancel例程(它所控制的未决IRP)。不要试图传递或者再使用当前IRP。

6. 从DispatchPower例程中返回STATUS_PENDING。

7. 当一个唤醒信号到达时，调用PoStartNextPowerIrp。然后调用IoCompleteRequest完成未决的等待/唤醒IRP，把Irp->IoStatus.Status设置到STATUS_SUCCESS，并且指定IO_NO_INCREMENT的优先级提升。

对于不支持唤醒的设备

如果设备不支持唤醒，则总线驱动程序(PDO)将进行如下操作：

1. 开始下一个电源IRP(PoStartNextPowerIrp)。

2. 通过调用IoCompleteRequest完成等待/唤醒IRP，指定IO_NO_INCREMENT。

3. 从DispatchPower例程中返回，传递Irp->IoStatus.Status中的值作为其返回值。

1.2.3 等待/唤醒IRPIoCompletion例程

在设备栈中的相邻较低层驱动程序已经完成等待/唤醒IRP之后，I/O管理器调用驱动程序的等待/唤醒IoCompletino例程。处理等待/唤醒IRP的每个功能驱动程序和过滤(FDO)驱动程序应该为此IRP创建一个IoCompletion例程。

每个功能驱动程序或者过滤驱动程序在处理进入设备栈的等待/唤醒IRP时，创建一个IoCompletion例程。因此发送IRP的设备电源策略所有者除了回调例程之外还有一个IoCompletion例程。要记住，回调例程要在IoCompletion例程之后调用，而且这两个例程有不同的要求。详见“等待/唤醒回调例程”。

在一个等待/唤醒IoCompletion例程中所要求的动作依赖于设备和驱动程序的类型。下面驱动程序在其等待/唤醒IoCompletion例程中可能需要执行的一些动作：

1. 在设备扩展中重置相关域。例如，当一个等待/唤醒IRP是未决的时，大多数驱动程序设置一个标记并且保持指针指向设备扩展中的IRP。

2. 重置IoCancle例程，对于IRP，通过调用IoSetCancelPoutine，为例程指定一个NULL指针。

3. 调用PoStartNextPowerIrp，开始下一个电源IRP。

4. 调用IoCompleteRequest，指定IO_NO_INCREMENT，以完成IRP。

当每一个驱动程序连续的完成IRP时，I/O管理器把控制传递到相邻较高层驱动程序的IoCompletion例程中，支持设备栈。

当驱动程序传递等待/唤醒IRP到设备栈时，调用由驱动程序所创建的IoCompletion例程，在这之后，I/O管理器调用回调例程，并由发送IRP的驱动程序传递IRP到PoRequestPowerIrp。详见“等待/唤醒回调例程”。

1.3 发送等待/唤醒IRP

次电源IRP码IRP_MN_WAIT_WAKE为唤醒设备或者唤醒系统提供必要条件。能唤醒自己或者唤醒系统的设备驱动程序发送IRP_MN_WAIT_WAKE请求。系统只对总是唤醒系统的设备发送IRP_MN_WAIT_WAKE请求，诸如加电开关。

因为下面两个原因之一，驱动程序发送IRP_MN_WAIT_WAKE请求：

1.其设备必须能够从休眠状态返回到工作状态，以响应外部唤醒信号。

例如，为保存能量，在设置一个IRP进入电源中状态D1之前，调制解调器的驱动程序发送一个等待/唤醒IRP。这个等待/唤醒IRP使得调制解调器响应一个到达的调用。

2.其设备在响应一个唤醒信号时必须能够唤醒系统。

当系统将要休眠时，带有未决的IRP_MN_WAIT_WAKE的调制解调器可能仍然在状态D1中。在这种情况下，一个到达的调用将唤醒系统及调制解调器。

无论设备是准备唤醒自己还是唤醒系统，其驱动程序必须采取相同的动作。两者的主要区别在于设备和系统硬件如何响应初始唤醒信号。不论哪一种情况，驱动程序的动作是相同的。

1.3.1 确定何时发送等待/唤醒IRP

拥有设备电源策略的驱动程序发送等待/唤醒IRP代表其设备。当下列情况之一发生时，这个驱动程序必须发送一个等待/唤醒IRP：

。驱动程序正在使设备进入休眠，但是当响应一个外部唤醒信号时，设备必须能够被唤醒。

。系统将进入休眠，并且设备必须能够唤醒它。

。驱动程序接收一个带有次代码IRP_MN_WMI的IRP_MJ_SYSTEM_CONTPOL请求，而此次代码设置DEVICE_WAKE_ENABLE为TRUE。为获得更多关于WMI的信息，参见在线DDK中“内核模式驱动程序设计指南和参考”。

在这样的条件出现之前，电源策略所有者应该发送等待/唤醒IRP。当它的设备在状态D0的任何时间时，它都能发送这个IRP。但当它处理另一个设置电源IRP或者查询电源IRP时，它不能发送这样的IRP。通常，在驱动程序已经初始化且设备已开始之后，驱动程序在PNP管理模块的IRP_MN_START_DEVICE请求的期间，应该发送这个IRP。

1.3.2 等待/唤醒IRP请求

为了发送一个IRP_MN_WAIT_WAKE，驱动程序调用PoRequestPowerIrp，传递(在其他参数中间)一个指针到目标PDO中，传递一个系统电源状态和一个指针到回调例程中。

系统电源状态指定能唤醒系统IRP的最小功率。其值必须是等于或者大于在DEVICE_CAPABILITIES结构中SystemWake状态的功率。例如，如果驱动程序传递在IRP中的PowerSystemSleeping2，相关的IRP可以使系统从状态S0，S1和S2中唤醒。在这样一种情况下，系统必须支持S0和S2(在最高功率和最低功率范围中),但不需要支持S1。

请求一个等待/唤醒IRP的驱动程序应该指定一个回调例程，而此回调例程在所有其他驱动程序完成IRP之后被调用。在这个例程中，驱动程序能使其设备返回到工作状态。

在响应PoRequestPowerIrp时，电源管理器分配一个带有次代码IRP_MN_WAIT_WAKE的电源IRP，并且把它发送到目标PDO的设备栈的顶端。调用者返回一个指向分配的IRP的指针，如果不得不撤消该IRP，则可延迟使用。

如果没有错误发生，PoRequestPowerIrp返回STATUS_PENDING。这个状态意味IRP成功地被发送并且是未决的完成。

一个等待/唤醒IRP不能改变系统的电源状态或者设备的电源状态。它仅仅能激活一个设备的唤醒信号。此IRP仍然保持未决状态，直到一个外部信号使系统或者设备唤醒。

1.3.3 等待/唤醒的回调例程

当一个驱动程序请求等待/唤醒IRP时，它必须指定一个回调例程，当唤醒事件发生时它就能使设备返回到工作的状态(D0)。在唤醒事件发生之后，并且所有驱动程序已经完成IRP时，系统调用回调例程传递到PoRequestPowerIrp。

因为这个回调例程被设置为代表开始IRP的驱动程序，而不是对正在处理的IRP，所以它不能调用PoStartNextPowerIrp：只有当驱动程序把IRP传递到栈中时，IoCompletion例程设置才应该开始下一个电源IRP。记住，策略所有者不仅发送这个IRP而且还处理它。因此，当它把IRP传递到设备栈中时，它还可以创建一个IoCompletion例程；除此之外，当它请求等待/唤醒IRP时，又创建了一个回调例程。

回调例程具有下列职责：

1.如果驱动程序控制多于一个的设备，确定哪一个设备发出唤醒信号。

2.引起唤醒信号事件的服务。

3.设置发出唤醒信号的驱动程序，在D0状态通过调用PoRequestPowerIrp发送一个PowerDeviceDO请求。驱动程序也必须调用PoSetPowerState以通知新设备状态的电源管理器。详见“为设备电源状态发送IRP_MN_QUETY_POWER或者IRP_MN_SET_POWER”。

4.如果驱动程序为IRP创建一个Cancel例程，调用IoSetCancelRoutine将Cancel例程重置为NULL。

5.如果驱动程序为多于一种的设备拥有电源策略，则减少它的等待/唤醒参考数。 如果该数非零，表明以前另一个设备曾经发送了一个等待/唤醒IRP，那么为它的PDO请求另一个等待/唤醒IRP(PoRequestPowerIrp)。

例如，PCI设备可以使调制解调器与网络接口卡(NIC)两个设备都有等待/唤醒的能力。如果NIC唤醒系统(这样完成IRP)，那么PCI FDO必须发送另一个等待/唤醒IRP到它自己中，以便调制解调器仍然能够被唤醒。

因为请求等待/唤醒IRP的驱动程序为它的设备栈控制电源策略，所以当IRP完成时，它负责将其设备放进工作状态。虽然较低层驱动程序实际上已经把电源用到设备上，但是策略所有者必须为设备状态D0调用PoRequestPowerIrp，以发送一个IRP_MN_SET_POWER IRP。只有设备栈中所有驱动程序都已处理这个加电IRP，设备才返回到工作状态。

1.3.4 撤消等待/唤醒IRP

只有发送等待/唤醒IRP的驱动程序能撤消那个IRP。

在下面的环境下，驱动程序可以根据需要撤消一个未决的等待/唤醒的IRP：

。驱动程序为设备接收一个PnP IRP_MN_STOP_DEVICE，IRP_MN_QUERY_REMOVE_DEVICE，IRP_MN_REMOVE_DEVICE，或者IRP_MN_SURPRISE_REMOVAL请求。如果驱动程序计算它自己未决的等待/唤醒IRP作为未完成I/O，那么当它为设备接收一个IRP_MN_QUERY_REMOVE_DEVICE请求时，它就应该撤消此IRP。在设备被重新启动之后， 驱动程序应该再发送等待/唤醒IRP(PoRequestPowerIrp)。

。系统将进入休眠，而设备不能唤醒系统。例如，USB集线器驱动程序在设备启动时，可以发送一个IRP_MN_WAIT_WAKE请求，以防它稍晚将其输入设备中的一个设备放进休眠状态。当系统在工作状态下时，从设备而来的唤醒信号把设备返回到工作状态(但是对系统电源状态没有作用)。如果设备将不允许唤醒系统，那么当系统准备关机时，USB集线器驱动程序撤消这IRP。

。系统正在进入一个设备不能唤醒它的休眠状态，即它正在进入一个比在其DEVICE_CAPABILITIES结构所指定的SystemWake值的功率更小的状态。

。设备正在进入一个它不能响应唤醒信号的电源状态，即它正在进入一个比其DEVICE_CAPABILITIES结构所指定的DeviceWake值的功率更小的状态。

。驱动程序接收一个带有次代码IRP_MN_WMI的IRP_MJ_SYSTEM_CONTROL请求，此次代码将DEVICE_WAKE_ENABLE设置为FALSE。详见在线DDK中“内核模式驱动程序设计指南和参考”。

为了撤消等待/唤醒IRP，发送IRP的驱动程序调用IoCancelIrp，把指针传递到以前驱动程序调用PoRequestPowerIrp时返回的IRP中。驱动程序不可以撤消一个它没有发送的等待/唤醒IRP。

1.4 等待/唤醒IRP的Cancel例程

许多功能驱动程序和总线驱动程序应该为未决的等待/唤醒IRP创建Cancel例程；下面是必须创建Cancel例程的驱动程序类型：

。改变设备设置为激活或者禁止唤醒能力的驱动程序

。发送IRP_MN_WAIT_WAKE请求到父设备驱动程序的驱动程序。

Cancel例程允许驱动程序为其设备禁止唤醒能力，并且清除有关未决的等待/唤醒IRP的任何数据。为父设备请求等待/唤醒IRP的驱动程序也能撤消那些IRP。

在其等待/唤醒Cancel例程中，驱动程序采取下列措施：

1.调用IoSetCancelRoutine，重置IRP的Cancel例程为NULL。

2.调用IoReleaseCancelSpinLock，传递在IRP中指定的CancelIRQL，以释放IRP的撤消自旋锁。

3. 在设备扩展中重置相关域。例如，当一个等待/唤醒IRP是未决的时，大多数驱动程序设置标记，并且在设备扩展中保持IRP的指针。注意，驱动程序可能在收到一个等待/唤醒IRP的同时，也正在撤消另一个这样的IRP。驱动程序必须检查在自旋锁(或者其相似物)的保护下，它是否已经有一个IRP。如果已经有，驱动程序必须仔细地同步它的处理，以保证它撤消当前的IRP。为得到更多关于自旋锁在Cancel例程中的使用的内容，参见第12章“内核模式驱动程序设计指南”中的“Cancel例程”。

4.改变任何需要的设备设置。例如，调制解调器驱动程序可以丧失设备的唤醒设置能力。

5.把Irp->IoStatus.Status设置到STATUS_CANCELED。

6.调用PoStartNextPowerIrp，开始下一个电源IRP。

7.调用IoCompleteRequest，以完成等待/唤醒IRP，指定IO_NO_INCREMENT。

8.如果驱动程序以前为父设备请求相关的IRP_MN_WAIT_WAKE，那么驱动程序就应该从其Cancel例程中撤消那个IRP。驱动程序必须在它撤消父设备的IRP之前，释放这个撤消自旋锁。

例如，为设备充当总线驱动程序并且为父设备拥有电源策略驱动程序的驱动程序，应该撤消相关的等待/唤醒IRP，而此IRP是稍早发送到其父设备的。调用的IoCancelIrp请求父设备的Cancel例程，进入设备栈，如此等等。详见第12章“内核模式驱动程序设计指南”中的“Cancel例程”。


设备安装总览

在安装了微软Windows 2000的计算机上，Setup和其他系统提供的组件以及厂商提供的组件一起工作来安装设备。当计算机启动或者启动后的任何时候用户增加了一个即插即用（PnP）设备时，Setup安装设备（或者手工安装一个非即插即用设备）。

为了支持PnP，Setup基于机器中的设备来进行安装过程而不是按照驱动程序的顺序来安装。例如，不是装载一批驱动程序，然后让这些驱动程序检测它们所支持的设备。而是由Setup确定机器中提供了什么设备，再装载和调用相应的驱动程序。驱动程序，比如ACPI驱动程序和其他PnP总线驱动程序帮助Setup确定机器中有什么样的设备。

这一章包含了以下的信息：

·1.1 设备安装组件

·1.2 PnP设备安装示例

·1.3 Setup如何为设备选择驱动程序?

·1.4 系统设置阶段

1.1 设备安装组件

图1.1显示了在Windows 2000机器上安装设备所涉及的软件组件。

图1.1中的阴影矩形代表由IHV和OEM提供的组件，其他设备安装组件由微软提供。

图1.1 设备安装设置组件

以下描述了设备安装中各种组件所扮演的角色：

内核模式的PnP管理器

内核模式的PnP管理器通知用户模式的PnP管理器在机器中有一个新设备存在并需要被安装。内核模式的PnP管理器也调用设备驱动程序的DriverEntry和AddDevice例程并发送IRP_MN_START_DEVICE请求以启动设备。

PnP管理器有两部分：一部分运行在用户模式，一部分运行在内核模式。内核模式的PnP管理器向用户模式的PnP管理器报告PnP事件，用户模式的PnP管理器向内核模式的PnP管理器发送控制请求。

驱动程序

PnP驱动程序执行由PnP管理器指示的设备安装操作。例如，当PnP管理器为BusRelations发出IRP_MN_QUERY_DEVICE_RELATIONS请求时，PnP总线驱动程序返回它的子设备的列表。当被PnP管理器调用时，PnP驱动程序执行它们的DriverEntry和AddDevice例程。PnP驱动程序也处理由PnP管理器发送的其他请求，比如IRP_MN_QUERY_CAPABILITIES，IRP_MN_START_DEVICE和IRP_MN_REMOVE_DEVICE。

PnP驱动程序还能检测非即插即用设备，并使用IoReportDetectedDevice向PnP管理器报告。

用户模式的PnP管理器

用户模式的PnP管理器从内核模式的PnP管理器接受设备安装请求，调用其他的用户模式Setup组件以开始设备安装任务，并向内核模式的PnP管理器发出控制请求（比如“启动设备”）。

用户模式的PnP管理器和内核模式的PnP管理器一起维护设备树（device tree）。

用户模式的PnP管理器尽力在一个可信任的进程上下文中安装一个设备，而不要求用户对对话框做出响应（“服务器端”的安装）。这样的自动安装提供了更好的用户接口。如果PnP管理器不能完成可信任的安装，比如因为安装程序提供了一个定制的“完成”页面，则PnP管理器放弃可信任的安装。在这种情况下，当用户以系统管理员权限登录，PnP管理器启动New Device DLL中的发现新硬件向导重新开始设备安装（“客户端”安装）。

Setup API

Setup API包括SetupXxx函数和SetupDiXxx函数。这些函数执行许多设备安装任务，如搜索INF文件，为设备构造潜在的驱动程序列表，拷贝驱动程序文件，向注册表中写信息，注册设备的协同安装程序等等。大多数Setup组件调用这些函数以完成工作。

SetupDiXxx函数有时也被称为设备安装程序。但是这个术语是含混的，因为不只一个组件涉及到设备的安装。

更多的信息参见《Windows 2000驱动程序开发参考》第1卷中的第2章和3章。

CfgMgr API

配置管理API提供了基本的安装和配置操作，它们在Setup API中没有被提供。配置管理函数执行底层任务，比如取得设备节点的状态以及管理资源的描述符。这些函数主要被Setup API调用，但也能被其他Setup组件调用。

协同安装程序和类安装程序

类安装程序在一个特定的设备设置类别中执行适用于设备的安装操作。例如，端口类安装程序负责分配一个COM端口名给端口设置类别中的一个设备。如果在特别的设置类别中的设备不要求任何特殊的安装操作，对这个设置类就不需要类安装程序。

微软为大多数系统定义的设备设置类别提供了类安装程序。更多的信息，参见第5章---编写类安装程序。IHV和OEM可以提供类安装程序，但是它们通常提供协同安装程序。

协同安装程序执行对一个特别的设备或者一个设置类别设备的安装操作。更多的信息参见第4章---编写协同安装程序。

INF文件和目录文件

INF文件和目录文件提供了关于要安装的设备和驱动程序的信息。更多的信息参见第3章---建立INF文件。

设备管理器

设备管理器允许用户察看和管理机器中的设备。例如，用户可以察看设备状态并设置设备属性。如果用户请求更新驱动程序，设备管理器调用New Device DLL中的更新驱动程序向导。

更多的信息参见设备管理器的联机帮助。

增加/删除硬件向导

增加/删除硬件向导允许用户增加、删除、拔掉和修理设备。在systray中的Unplug或Eject硬件小程序是这个向导的“拔掉”任务的快捷方式。

New Device DLL

New Device DLL包含发现新硬件向导、更新驱动程序向导和UpdateDriverForPlugAndPlayDevices函数。

用户模式的PnP管理器在有适当权限的用户上下文中调用发现新硬件向导，以开始客户端的新设备的安装。当用户选择设备的驱动程序属性页上的“更新驱动程序...”按钮时，设备管理器调用更新驱动程序向导。发现新硬件向导、更新驱动程序向导调用Setup API和配置管理器API完成它们的任务。

定制Setup应用程序

IHV和OEM可以提供定制的设置应用程序。更多的信息参见第6章---编写定制的设备安装应用程序。

1.2 PnP设备安装示例

为了说明在设备安装涉及的组件间的交互，这一节分步骤的描述了热插拔PnP设备的安装（使用随书的CD中的支持文件）。

图1.2显示了参加PnP设备安装的软件组件。有阴影的组件是设备厂商提供的。

图1.2 PnP设备安装示例

以下的注释对应于图1.2中标出的带圈的数字：

1．用户将设备插入机器。

如果设备和总线支持热插拔通知，用户可以在机器运行时插入新设备。

2．设备被枚举。

总线驱动程序在总线支持下，接受新设备的热插拔通知。总线驱动程序通过调用IoInvalidateDeviceRelations通知内核模式的PnP管理器，总线上的设备已经改变。这种情况下，改变是总线上的新设备。

内核模式的PnP管理器通过为BusRelations发送IRP_MJ_PNP/IRP_MN_QUERY_DEVICE_RELATIONS向总线驱动程序来查询总线上存在的设备。总线驱动程序以当前总线上的设备列表响应此IRP。

内核模式的PnP管理器比较新列表和以前的列表，以确定总线上有一个新的设备。

内核模式的PnP管理器向总线驱动程序发出IRP以收集关于新设备的信息，比如设备的硬件ID、兼容ID和设备性能。这些IRP包括IRP_MN_QUERY_ID和IRP_MN_QUERY_CAPABILITIES。参见第2部分的第1章---动态添加新的PnP设备以获得更多有关内核模式PnP管理器的枚举活动的信息。

3．内核模式PnP管理器通知用户模式PnP管理器，有一个设备要被安装。

用户模式PnP管理器尽力执行可信任的安装（服务器端安装），但是在这个例子中不能进行，因为它需要显示一个用户界面，询问用户设备安装磁盘的信息。

4．用户模式PnP管理器使用rundll32.exe建立一个新进程，并启动newdev.dll安装新设备。

5．New Device DLL调用Setup API和CfgMgr API函数以执行它的安装任务。

New Device DLL调用SetupDiBuildDriverInfoList，为设备建立可能的驱动程序列表。在本例子中，机器中的INF没有和新设备相匹配的，因此返回的列表是空的。New Device DLL显示发现新设备向导。用户提供驱动程序的位置（软盘、CD、Windows Update）。在本例子中，驱动文件在CD上，所以用户装入CD并点击向导中“下一步”按钮。New Device DLL再次调用SetupDiBuildDriverInfoList，从CD接收到包含驱动程序的列表。

参见下一节获得更多有关驱动程序选择的信息。

6．类安装程序和协同安装程序通过处理DIF请求，可以参加到安装过程中。

例如，New Device DLL调用SetupDiCallClassInstaller以发送DIF_SELECTBESTCOMPATDRV安装请求。SetupDiCallClassInstaller对任何为此设备注册的类安装程序和协同安装程序发送DIF请求。

New Device DLL发送一系列的DIF编码，包括DIF_SELECTBESTCOMPATDRV，DIF_ALLOW_INSTALL，DIF_INSTALLDEVICEFILES，DIF_REGISTER_COINSTALLERS，DIF_INSTALLINTERFACES和DIF_INSTALLDEVICE。类安装程序和协同安装程序可以参加每一个操作。

Setup使用被选择驱动程序的INF文件的Version section中的Class和ClassGUID条目以确定设备的设置类别。设置类别确定了设备的类安装程序和协同安装程序，设备专用的协同安装程序在INF DDInstall.Coinstaller section中列出。更多的关于安装程序的信息，参见第4章---编写协同安装程序和第5章---编写类安装程序。

7．Setup将控制转移给内核模式以装载驱动程序并启动设备。

一旦Setup为设备选择了最佳的驱动程序，拷贝了适合的驱动程序文件，注册了任何设备专用协同安装程序，注册了任何设备接口等等，它将控制转移给内核模式以装载驱动程序并启动设备。适当的CfgMgr函数对用户模式的PnP管理器发送请求，用户模式的PnP管理器再将它传给内核模式的PnP管理器。

8．PnP管理器为设备装入适宜的功能驱动程序和任何可选的过滤器驱动程序。

PnP管理器为任何已请求但还没有被装入的驱动程序调用DriverEntry。PnP管理器然后为每个驱动程序调用AddDevice，开始于下层的过滤程序，然后是功能驱动程序，最后是顶层的过滤程序。PnP管理器分配资源给设备，如果需要，它发送IRP_MN_START_DEVICE给设备的驱动程序。

9．安装程序可以提供向导页以改变设备设置。

Setup在它显示标准完成页之前发送DIF_NEWDEVICEWIZARD_FINISHINSTALL请求。如果必要的话，对安装程序这是一个机会以增加定制页以改变设备的设置。

1.3 Setup如何为设备选择驱动程序?

Windows 2000 Setup为每个设备选择驱动程序，除非设备正在被以原始方式使用。这一节描述了Setup如何做出这个选择。Setup对设备的概念实际上是“驱动程序节点”，它包括了对一个设备的所有支持，如任何服务、设备特定的协同安装程序、注册表项等等。设备的服务可以包括一套PnP驱动程序（一个功能驱动程序和任何顶层、下层过滤器驱动程序）。

一些设备要求厂商提供为那个设备专门设计的驱动程序。另一些设备能够被厂商提供的、设计用来支持一个设备家族的驱动程序所驱动。还有一些设备能被系统提供的、能支持给定设备设置类别的所有设备的驱动程序所驱动。为了为设备选择驱动程序，Setup优先选择和设备特别匹配的驱动程序。如果不能找到这样的驱动程序，它从后继的更一般的驱动程序中选择。

为了确定驱动程序匹配，Setup比较设备的硬件ID、兼容性ID和由设备的父总线驱动程序报告的机器中INF文件所列出的硬件ID、兼容性ID。（如果Setup在任何已安装的INF文件中不能找到匹配项，它就会显示发现新硬件向导并询问用户设备的驱动程序。）

Setup为设备建立可能的驱动程序列表---即那些在Models项包含的ID和设备的ID之一相匹配的驱动程序。Setup为每个可能的驱动程序分配优先等级。等级指示了驱动程序对设备的匹配程度。等级号越低，就越匹配。0等级是最佳匹配，0xFFFF等级是最差匹配。Setup指定的等级如下：

驱动程序等级 描述

0-FFF 在INF的Models项的硬件ID和设备的硬件ID匹配。等级N

匹配意味着设备的N+1硬件ID和INF硬件ID匹配。

1000-1FFF 在INF的Models项的兼容性ID和设备的硬件ID之一匹配。

2000-2FFF 在INF的Models项的硬件ID和设备的兼容性ID之一匹配。

3000-3FFF 在INF的Models项的兼容性ID和设备的兼容性ID之一匹配。

FFFF 最差的匹配。这是个特殊的值，被如协同安装程序的组件使用。

等级在0-FFF范围内的驱动程序被称为硬件ID匹配，因为它们是硬件ID和硬件ID的匹配。这是最好的匹配。所有其他等级被称为兼容性ID匹配，因为它们在匹配中至少涉及一项兼容性ID匹配。当Setup在一个管理员用户进程的上下文中安装设备（客户端安装），如果驱动程序等级仅仅是兼容性ID匹配，则Setup显示发现新硬件向导以询问用户是否他/她有更好的驱动程序。

例如，考虑一个有两个硬件ID和两个兼容性ID的设备。ID的语法在术语集中定义；对于这个讨论，用以下的缩写来代表ID：

HwID_1，HwID_2，CID_1，CID_2

在硬件ID列表的第一个硬件ID对设备是最精确的标识。在本例子中就是HwID_1。

在本例子中，有一个INF文件，它的Models节有类似下面的表项：

DeviceDesc1 = InstallSection1，INF_HWID_1，INF_CID_1

INF_XXX_N是对在INF Models节列出的硬件和兼容性ID的缩写。

下表列出了当比较示例设备的ID和INF文件的ID时，Setup可以设置的可能的匹配等级：

设备的ID INF文件Models节的ID

INF_HWID_1 INF_CID_1

HwID_1 0级 1000-1FFF级

HwID_2 1级 1000-1FFF级

CID_1 2000-2FFF级 3000-3FFF级

CID_2 2000-2FFF级 3000-3FFF级

在这个例子中，Setup开始取出设备的第一个硬件ID，HwID_1，并把它和机器中的INF中的ID比较。如果Setup匹配了HwID_1和INF文件Models节列出的第一个硬件ID，它增加此驱动程序节点到它的潜在驱动程序列表中，并将此匹配等级置为0级。如果Setup匹配了任何其他INF文件Models节的ID和HwID_1，它给这些匹配分配等级并将它们增加到它的潜在驱动程序列表中。

接着，Setup取出设备附加的硬件ID，并和INF中的ID比较。如果它找到任何匹配，它给这些匹配分配等级并将它们增加到它的设备的潜在驱动程序列表中。然后它为设备的兼容性ID进行同样的过程（如果有兼容性ID）。

在Setup构造了它的完整的潜在驱动程序列表后，它为设备选择最佳的驱动程序（最小等级的驱动程序）。如果不只一个驱动程序有相同的最小等级，Setup选择最近日期的驱动程序。驱动程序的日期由驱动程序INF文件的DriverVer项说明。如果驱动程序包没有数字签名，Setup忽略任何DriverVer项并使用默认的日期00/00/0000。如果两个驱动程序有相同的等级和相同的日期，Setup选择任何一个。

通过处理DIF_SELECTBESTCOMPATDRV请求（或者对于手工安装的设备是DIF_SELECTDEVICE请求），类安装程序和协同安装程序能参与驱动程序的选择。在Setup构造了它的潜在驱动程序列表后但是在它选择最佳的驱动程序前，Setup发送这个DIF请求。当处理这些DIF编码时，类安装程序和协同安装程序对潜在的驱动程序设置DNF_BAD_DRIVER标记，如果安装程序能够判断驱动程序不能支持设备。如果潜在的驱动程序节点DNF_BAD_DRIVER标记被设置，Setup做选择时会忽略此驱动程序。

当安装程序处理DIF_SELECTBESTCOMPATDRV请求（或者DIF_SELECTDEVICE）时，安装程序通过改变潜在驱动程序的等级也可以参与驱动程序的选择。但是，安装程序改变驱动程序的等级的唯一方式是设置等级为0xFFFF。这个等级值指示驱动程序可以为设备使用，但是仅作为最后的选择。安装程序不应该把设备等级设为中间值，因为它们会干扰适当的驱动程序选择，也因为等级范围容易改变。

不是更改驱动程序的选择过程，类安装程序和协同安装程序可以重载Setup的条目选择过程并直接为设备选择驱动程序。但是，这并不被推荐，因为Setup的选择算法在将来版本中可能会被加强，它自己选择驱动程序也会引起安装程序阻止Setup的选择操作的进行。参见《Windows 2000驱动程序开发参考》卷1中的DIF_SELECTBESTCOMPATDRV和DIF_SELECTDEVICE获得更多信息。

为了说明驱动程序选择过程，考虑一个示例的视频设备，它匹配INF文件的三个驱动程序节点。设备的父总线驱动程序（pci.sys）报告以下的设备ID：

设备的硬件ID：

PCI\VEN_FFFF&DEV_493D&SUBSYS_001C105D&REV_00

PCI\VEN_FFFF&DEV_493D&SUBSYS_001C105D

PCI\VEN_FFFF&DEV_493D&CC_030000

PCI\VEN_FFFF&DEV_493D&CC_0300

设备的兼容性ID：

PCI\VEN_FFFF&DEV_493D&REV_00

PCI\VEN_FFFF&DEV_493D

PCI\VEN_FFFF&CC_030000

PCI\VEN_FFFF&CC_0300

PCI\VEN_FFFF

PCI\CC_030000

PCI\CC_0300

在这个例子中，Setup在INF文件中找到三个驱动程序节点可以匹配设备的ID。第一个匹配的驱动程序的节点如下：

%Sample% = Sample.DDInstall，PCI\VEN_FFFF&DEV_493D&CC_0300

设备的第四个硬件ID和上面的INF项的硬件ID匹配，所以这是个等级为3的驱动程序匹配。Setup增加这个驱动程序到它的潜在驱动程序列表中。这个驱动程序支持这种类型的任何视频设备（有任何子系统厂商ID和任何硬件版本号的视频设备）。

Setup为设备匹配的第二个INF ID在它的Models表项中有以下的驱动程序节点：

%Sample2% = Sample2.DDInstall，PCI\VEN_FFFF&DEV_493D& SUBSYS_001C105D

设备的第二个硬件ID和上面的INF项的硬件ID匹配，所以这是个等级为1的驱动程序匹配。Setup增加这个驱动程序到它的潜在驱动程序列表中。这个驱动程序支持有这个特定子系统厂商ID及任何硬件版本号的设备。

Setup为设备匹配的第三个INF ID在它的Models表项中有以下的驱动程序节点：

%Sample3% = vga，PCI\CC_0300

设备的最后一个兼容性ID和上面的INF项的硬件ID匹配，所以这个驱动程序匹配的等级在2000-2FFF范围内。Setup增加这个驱动程序到它的潜在驱动程序列表中。这个驱动程序是一个支持任何PCI视频设备的通用驱动程序（VGA驱动程序）。

对于这个例子，如果类安装程序和协同安装程序不更改Setup给驱动程序匹配指定的等级，Setup选择等级为1的驱动程序，%Sample2%。

1.4 系统设置阶段

对于大多数设备，你应该设计设备和支持文件以便Setup在系统启动的适当时间或者启动后机器正在运行的任何时候能安装设备。但是，某些设备必须在系统启动的特殊阶段被安装。为协助这些设备的厂商，这一节提供了一些系统设置阶段和设备安装相关的背景信息。

当Windows 2000启动时，Setup程序的初始阶段仅安装Windows 2000运行所需要的最小数目的设备，如键盘、鼠标、显示适配器、SCSI/Disk和Machine/HAL。这个阶段的Setup程序是文本模式的设置。对于在文本模式设置期间安装设备的用户，你的分发盘必须包括一个名为txtsetup.oem的文本文件。更多的信息，参见第9章---安装一个要求重新启动机器的设备。

在文本模式设置完成后，Setup程序启动Windows 2000并继续安装的GUI模式阶段。Setup在GUI模式设置阶段安装大多数设备。

在GUI模式设置阶段没有安装的设备包括那些需要用户交互的安装。例如，如果这是第一次设备被设置并且一个此设备的协同安装程序提供了定制一个完成页，Setup必须向用户显示此页。当用户以管理员权限登录时，Setup执行这种设备安装。

一旦机器启动并运行，用户可以安装新设备：

·为安装一个新的PnP设备，将设备插入机器中。

如果设备和总线支持热插拔通知，设备安装被自动初始化。

如果用户需要关掉机器，打开机箱以插入设备，设备将在系统启动时被识别和配置。

当系统正在运行时，如果设备能被插入，并且设备和总线不支持热插拔通知，使用增加/删除硬件向导以初始化设备配置。（选择“安装”）

·安装一个非PnP设备，使用增加/删除硬件向导。

如果Setup能在系统进程的可信任的上下文中安装设备，用户不需要管理员权限来安装设备。如果Setup需要提示用户信息，它要求在管理员用户的进程上下文中安装设备。

为了更新已安装设备的驱动程序，使用设备管理器或者适宜的控制面板小程序中的硬件页。（点击控制面板小程序中的硬件页的属性按钮以启动设备的设备管理器。）


为设备提供驱动程序

这一章讨论了驱动程序包的文件集并讨论了驱动程序作为一个整体的事宜。此文档的其他部分讨论了对驱动程序文件内容的要求和指南。

这一章包括以下信息：

·2.1 驱动程序文件

·2.2 注册表中的驱动程序信息

·2.3 指定驱动程序装载顺序

·2.4 安装过滤器驱动程序

·2.5 为设备安装Null驱动程序

1.1 驱动程序文件

支持一个特定的设备的软件依赖于设备的种类和设备被使用的方式。典型情况下，厂商在驱动程序包中提供下面的软件以支持设备：

1.一个设备安装信息文件（INF文件）。

一个INF文件包含系统Setup组件使用的信息以安装支持设备的文件。当Setup安装驱动程序时，它拷贝这些文件到%windir%\inf目录中。这个文件是必须的。

更多的信息，参见第3章，建立一个INF文件。

1． 设备的一个或多个驱动程序。

一个.sys文件时驱动程序的映像文件。当驱动程序被安装时，Setup拷贝此文件到%windir%\system32\drivers目录中。对大多数设备，驱动程序是必须的；某些设备不需要驱动程序，比如原始能力的设备。

更多的信息，参见这一章的剩余部分和其他章节。

2． 驱动程序包的数字签名（驱动程序目录文件）。

驱动程序目录文件包含数字签名。所有的驱动程序包都应被签名。

厂商通过提交它的驱动程序包给Windows 硬件质量实验室（WHQL）测试、签名，从而获得数字签名。WHQL将包返回，并附带一个目录文件（.cat 文件）。厂商必须为设备在INF文件中列出这个目录文件。更多的信息参见《Windows 2000驱动程序开发参考》卷1第3部分第1章的INF Version节。参考WHQL指南以获得如何提交驱动程序包以测试、签名的信息。

3． 一个或多个协同安装程序。

协同安装程序是一个Win32 DLL，它辅助设备在微软Windows 2000系统中的安装。例如，一个IHV也许会提供一个协同安装程序，从而将设备特定的信息写入注册表中，而这是不能被INF处理的。协同安装程序是可选的。更多的信息参见第4章，编写协同安装程序。

4． 其他文件。

一个驱动程序包可以包含其他文件，比如定制的设置应用程序，一个设备图标，一个驱动程序库文件（例如对视频驱动程序），等等。更多的信息参见第6章，编写定制的设备安装应用程序；第7章，提供设备属性页；第9章，安装一个要求重新启动机器的设备；以及图形驱动程序设计指南的第1部分，图形驱动程序。

1.2 注册表中的驱动程序信息

操作系统和驱动程序将关于驱动程序和设备的信息存储在注册表中。总的来说，驱动程序应使用注册表存储机器重启动后仍必须维护的数据。此外，驱动程序可以访问注册表以获得系统或者其他程序或者驱动程序存储在其中的信息。更多的关于注册表的信息，参见平台SDK文档。

以下的注册表中的树是驱动程序开发者特别感兴趣的（这里HKLM代表HKEY_LOCAL_MACHINE）：

·HKLM\SYSTEM\CurrentControlSet\Services

·HKLM\SYSTEM\CurrentControlSet\Control

·HKLM\SYSTEM\CurrentControlSet\Enum

·HKLM\SYSTEM\CurrentControlSet\HardwareProfiles

驱动程序和用户模式的设置组件必须使用系统例程如IoGetDeviceProperty和SetupDiGetDeviceRegistryProperty访问注册表的Plug and Play（PnP）主键。驱动程序不应直接访问这些主键。以下注册表信息的讨论仅仅是为调试一个设备的安装/配置问题。

在HKLM\SYSTEM\CurrentControlSet下的主键是保留驱动程序重要数据的安全地方，因为数据被存在系统区。系统采取特别的预防措施以保护系统区（例如做多个备份）。

HKLM\SYSTEM\CurrentControlSet\Services树

这个树存储机器每个服务的信息。每个驱动程序有一个形式为HKLM\SYSTEM\CurrentControlSet\Services\DriverName的主键。当PnP管理器调用驱动程序的DriverEntry例程时，它在RegistryPath参数中将此路径传给驱动程序。驱动程序可以将全局驱动程序定义的数据存储在它的Services树主键下。在此主键下存储的信息在它初始化时对驱动程序是可用的。

以下的主键和值表项是特别有意义的：

ImagePath

说明驱动程序映像文件全路径的值表项。Setup使用驱动程序INF文件的必须的ServiceBinary表项建立这个值。这个表项在驱动程序的INF AddService指令中引用的service-install-section中。这个路径的典型值是%windir%\system32\Drivers\DriverName.sys，此处DriverName是驱动程序的Services主键名称。

Parameters

用来存储驱动程序专用数据的主键。对一些类型的驱动程序，系统期望找到专用的值表项。你可以使用驱动程序的INF文件的AddReg表项来对这个子键添加值表项。

Performance

对可选的性能监视信息说明的主键。在此主键下的值说明了驱动程序的性能DLL的名称和在此DLL中某些导出的函数的名称。你可以使用驱动程序的INF文件的AddReg表项来对这个子键添加值表项。

HKLM\SYSTEM\CurrentControlSet\Control 树

注册表的这个树包含了控制系统启动和一些设备配置方面的信息。以下的子键特别令人感兴趣：

Class

包含关于计算机上的设备设置类别的信息。对每个类有一个子键，使用设置类别的GUID命名。每个子键包含一个设置类别的信息，如类安装程序（如果有的话），注册的类顶层过滤器驱动程序，注册的类底层过滤器驱动程序，等等。每个类别子键为每个安装在系统中的类设备实例包含软件主键（驱动程序的主键）。

CoDeviceInstallers

包含关于特定类别的、以注册的协同安装程序的信息。

DeviceClasses

包含关于机器中设备接口的信息。对每个设备接口类有一个子键，每个已注册到设备接口类的接口的实例，在那些子键下有一个表项。

HKLM\SYSTEM\CurrentControlSet\Enum 树

注册表的这个树包含了关于机器中设备的信息。PnP管理器为每个设备建立一个子键，名字是HKLM\SYSTEM\CurrentControlSet\Enum\enumerator\deviceID的形式。在这些主键下是一个子键，是机器中现有的每个设备实例。设备实例的子键具有如设备描述、硬件ID、兼容性ID、资源要求等等的信息。

Enum树为操作系统组件的使用而保留，并且它的布局容易改变。驱动程序和用户模式的Setup组件必须使用系统API，如IoGetDeviceProperty和SetupDiGetDeviceRegistryProperty，以从树中提取信息。驱动程序和Setup应用程序不应直接访问Enum树。当调试驱动程序时，你可以使用注册表编辑器察看Enum树。

HKLM\SYSTEM\CurrentControlSet\HardwareProfiles

注册表的这个树包含了计算机上关于硬件profiles的信息。

1.3 指定驱动程序装载顺序

对于大多数设备，Setup和PnP管理器装载驱动程序的顺序由机器中设备的物理层次所决定。Setup和PnP管理器从系统根设备的配置开始，然后是根设备的子设备（例如，PCI适配器），然后是这些子设备的子设备等等。如果驱动程序还没有为另一个设备装入，PnP管理器为每个设备装入驱动程序。

在INF文件中的设置可以影响驱动程序的装入顺序。这一节描述了厂商应当在在驱动程序的INF AddService指令中引用的service-install-section中说明的相关值。这一节讨论了StartType、LoadOrderGroup、Dependencies表项。

为说明StartType，驱动程序应该遵守这些规则：

·PnP驱动程序

PnP驱动程序应该有一个SERVICE_DEMAND_START（0x3）的开始类型，说明无论什么时候PnP管理器发现一个驱动程序可以服务的设备时，它都可以装入驱动程序。

·必须要起动机器的设备的驱动程序

如果设备要求重启动机器，设备的驱动程序应有一个SERVICE_BOOT_START（0x0）的开始类型。

·检测不是PnP可枚举的且不要求起动机器的设备的驱动程序

对于非即插即用设备，驱动程序通过调用IoReportDetectedDevice向PnP管理器报告此设备。这样的驱动程序有一个SERVICE_SYSTEM_START（0x01）的开始类型，这样Setup在系统初始化时将装入驱动程序。

仅仅报告非即插即用设备的驱动程序应该设置这种开始类型。如果驱动程序同时为PnP设备和非PnP设备服务，它应当设置这种开始类型。

·必须被服务控制管理器启动的非PnP的驱动程序

这样的驱动程序有一个SERVICE_AUTO_START（0x02）的开始类型。PnP驱动程序不应设置这种类型。

PnP驱动程序的开发者应使此驱动程序在Setup配置驱动程序所服务的设备的任何时候，都能被加载。相反，在PnP管理器确定驱动程序所服务的设备不存在时，驱动程序应能在任何时候被卸载。PnP驱动程序所依赖的唯一驱动程序装载顺序如下：

·子设备的驱动程序可以依赖于已被加载的父设备的驱动程序。

·在设备栈的驱动程序依赖于在它之下的已被加载的驱动程序。（例如，功能驱动程序要确定任何低层的过滤器驱动程序已经被加载。）注意在设备栈的驱动程序不能依赖于在任何较低层驱动程序之后被加载，因为当另一个此类设备被配置时，驱动程序也许已经被装载了。在过滤组之内的过滤器驱动程序不能预测它们的装载顺序。例如，如果一个设备有三个注册的顶层过滤器驱动程序，这三个驱动程序会在功能驱动程序之后被加载，但是在它们的过滤组之内，装载顺序是任意的。

如果一个驱动程序的装载顺序明显依赖于另一个驱动程序，这个依赖性通过父亲/孩子关系来实现。子设备的驱动程序被加载以前，它依赖于父设备的驱动程序已被加载。

为了增加设置正确的StartType值的重要性，下表描述了Setup和PnP管理器如何在INF文件中使用StartType表项：

1． 在系统启动时，操作系统装载程序在将控制转移给内核以前，装入SERVICE_BOOT_START类型的驱动程序。当内核得到控制时，这些驱动程序已在内存中。Boot-start类型的驱动程序使用INF LoadOrderGroup表项以安排装载顺序。（Boot-start类型的驱动程序在大多数设备被配置以前装入，这样它们的装入顺序不能被设备层次所决定。）操作系统忽略Boot-start类型的驱动程序INF的Dependencies表项。

2． PnP管理器调用SERVICE_BOOT_START类型的驱动程序的DriverEntry例程，这样驱动程序可以服务boot类型的设备。

如果boot类型的驱动程序的设备有子设备，这些设备就被枚举。如果子设备的驱动程序也是Boot-start类型的驱动程序，它们被配置和启动。如果一个设备的驱动程序不全是Boot-start类型的驱动程序，PnP管理器为设备建立设备节点但并不启动设备。

3． 在所有的boot类型的驱动程序被装入以及boot类型的设备被启动后，PnP管理器配置其余的PnP设备并装入它们的驱动程序。

PnP管理器遍历设备树并为还未启动的设备节点装入驱动程序（即任何在前面步骤还没有启动的设备节点）。当每个设备启动时，PnP管理器枚举设备的子设备（如果它有子设备）。

在PnP管理器配置这些设备时，它为设备装入驱动程序并启动设备，而不管驱动程序的StartType值。许多这些驱动程序是SERVICE_DEMAND_START类型的，但是它们可以有任何StartType值。

PnP管理器忽略在这一步骤装入驱动程序中由于INF Dependencies表项和LoadOrderGroup表项而建立的注册表表项。装载顺序基于物理设备层次。

在这一步骤的之后，所有的设备已经被配置，除了不是即插即用可枚举的设备和这些设备的子设备之外。（子设备也许是、也许不是即插即用可枚举的。）

4． PnP管理器装入StartType为SERVICE_SYSTEM_START的、还没有被装入的驱动程序。

这些驱动程序检测并报告它们的非PnP设备。PnP管理器处理由于这些驱动程序的INF LoadOrderGroup表项而导致的注册表表项。它忽略由于这些驱动程序的INF Dependencies表项而建立的注册表表项。

5． 服务控制管理器装入StartType为SERVICE_AUTO_START的、还没有被装入的驱动程序。

服务控制管理器处理关于服务的DependOnGroup和DependOnService的服务数据库信息。这个信息来自于INF AddService表项的Dependencies表项。注意Dependencies信息仅仅为非PnP驱动程序处理，因为任何必须的PnP驱动程序在系统启动的前面步骤中已经被装入。服务控制管理器忽略INF LoadOrderGroup信息。

参见平台SDK文档获得更多关于服务控制管理器的信息。

1.4 安装过滤器驱动程序

一个PnP过滤器驱动程序能支持一个特定的设备或者支持一个设置类的所有设备以及可以依附在设备的功能驱动程序之下（底层过滤程序）或者之上（顶层过滤程序）。参见第1部分的第1章驱动程序种类以获得更多关于PnP驱动程序层的信息。

为了注册一个设备专用的过滤器驱动程序，通过设备的INF文件的DDInstall.HW节的AddReg表项建立一个注册表表项。对于设备专用的顶层过滤，建立名为UpperFilters的表项。对于设备专用的底层过滤，建立名为LowerFilters的表项。例如，以下INF摘录了安装cdaudio作为cdrom驱动程序的顶层过滤：

：

；Installation section for cdaudio.Sets cdrom as the service

；and adds cdaudio as a PnP upper filter driver.

；

[cdaudio_install]

CopyFiles = cdaudio_copyfiles,cdrom_copyfiles

[cdaudio_install.HW]

AddReg = cdaudio_addreg

[cdaudio_install.Services]

AddService = cdrom,0x00000002,cdrom_ServiceInstallSection

AddService = cdaudio,,cdaudio_ServiceInstallSection

:

[cdaudio_addreg]

HKR,,"UpperFilters",0x00010000,"cdaudio";REG_MULTI_SZ value

:

[cdaudio_ServiceInstallSection]

DisplayName = %cdaudio_ServiceDesc%

ServiceType = 1 ; SERVICE_KERNEL_DRIVER

StartType = 3 ; SERVICE_DEMAND_START

ErrorControl = 1 ; SERVICE_ERROR_NORMAL

ServiceBinary = %12%\cdaudio.sys

:

为了安装一个设备设置类别的类范围的顶层/底层过滤程序，你可以提供一个安装必要服务的设置应用程序。设置应用程序然后为希望的设备设置类别注册作为顶层或底层过滤的服务。为拷贝服务的二进制文件，设置应用程序可以使用SetupInstallFiltersFromInfSection。为安装服务，设置应用程序可以使用SetupInstallServicesFromInfSection。为了为特别的设备设置类别注册作为顶层或底层过滤的服务，设置应用程序为每个感兴趣的设备设置类别调用SetupInstallFromInfSection，使用从SetupDiOpenClassRegKey提取的注册表主键句柄作为RelativeKeyRoot参数。例如，考虑以下的INF节：

：

[DestinationDirs]

upperfilter_copyfiles = 12

[upperfilter_inst]

CopyFiles = upperfilter_copyfiles

AddReg = upperfilter_addreg

[upperfilter_copyfiles]

upperfilt.sys

[upperfilter_addreg]

;append this service to existing REG_MULTI_SZ list,if any

HKR,,UpperFilters,0x00010008,upperfilt

[upperfilter_inst.Services]

AddService = upperfilt,,upperfilter_service

[upperfilter_service]

DisplayName = %upperfilter_ServiceDesc%

ServiceType = 1 ; SERVICE_KERNEL_DRIVER

StartType = 3 ; SERVICE_DEMAND_START

ErrorControl = 1 ; SERVICE_ERROR_NORMAL

ServiceBinary = %12%\upperfilt.sys

:

设置应用程序将为[upperfilter_inst]节调用SetupInstallFilesFromInfSection。接下来，它为[upperfilter_inst.Services]节调用SetupInstallServicesFromInfSection。最后，为[upperfilter_inst]节调用SetupInstallFromInfSection，为每个它想注册的顶层过滤服务的类主键分别调用一次。每个调用会为Flags参数说明SPINST_REGISTRY以指示仅仅注册表的修改需要被执行。

1.5 为设备安装Null驱动程序

如果设备在机器中没有使用并且设备不会被启动，你也许为设备安装一个null驱动程序。这样的设备典型情况下并不存在，但是如果存在，你可以安装一个null驱动程序，此设备的INF的表项如下：

：

[MyModels]

%MyDeviceDescription% = MyNullInstallSection,&BadDeviceHardwareID%

:

[MyNullInstallSection]

;the install section must be empty

[MyNullInstallSection.Services]

AddService = ,2 ; no value for the service name

:

在Models节的设备的硬件ID应该具体的标识此设备，使用子系统厂商ID和相关的其他信息。操作系统为设备建立设备节点，但是如果设备不是具有未加工能力（raw capable），操作系统不会启动设备。

如果设备有启动配置，那些资源会被保留。


建立INF文件

为了安装Microsoft Windows 2000驱动程序，必须有一个INF文件。一个INF文件是一个文本文件，包含了关于设备和要安装的文件的必要信息，如驱动程序映像、注册表信息、版本信息等等，这些都被Setup组件所使用。

Windows 2000 INF文件不包含安装脚本。安装程序是Win32安装应用程序的一部分，如新设备向导和增加/删除硬件向导，每个INF文件作为一个资源。

包括对设备的安装支持，INF文件为设备初始化一个新的设置类别，比如INF有一个ClassInstall32节。

这一章包含以下信息：

·3.1 INF文件总体指南

·3.2 为设备文件说明源和目标位置

·3.3 建立跨平台的和/或者双操作系统的INF文件

·3.4 建立国际化的INF文件

·3.5 在设备的INF文件中加强打开文件的安全性

·3.6 从应用程序访问INF文件

参见《Windows 2000驱动程序开发参考》第1卷中的第3部分的第1章---INF文件的节和指示以获得INF文件格式的完整描述。

1.1 INF文件总体指南

INF文件有一些公共部分并遵守一套单一的语法规则，但是由于Windows支持的各种设备，它们也是有区别的。编写INF文件时，你应该求教于以下的信息资源。

·这一章和INF文件节和指令参考资料。

·你的设备的类别的文档。

例如，如果你的设备是图形适配器，参见DDK文档的图形驱动程序一节。

·INF文件的DDK工具。

DDK提供了一些工具以协助编写INF文件。参见DDK的tools目录中的关于工具如GenINF和ChkINF的信息。

·INF示例文件和类似设备的INF文件。

DDK包括了它的示例驱动程序的INF文件。分析这些示例文件，看看是否有设备的INF文件类似于你的设备。

你可以使用任何编辑器建立或者修改INF文件。如果你的INF文件包含非ASCII字符，将文件存成Unicode文件。注意ChkINF不支持Unicode文件。

一个和Windows 2000操作系统一起发行的INF文件必须有xxxxxxxx.inf的文件名，这里xxxxxxxx不能超过8个字符。独立于操作系统发行的INF文件的名称不受8个字符的限制。

1.2 为设备文件说明源和目标位置

当Setup处理INF文件中的拷贝、更名、删除文件语句时，它为文件确定源和目标位置。为了决定这些位置，它估计驱动程序是和操作系统一起发行还是单独发行，并检查INF文件的各节和表项，包括SourceDisksNames，SourceDisksFiles，LayoutFile，Include，Needs，DestinationDirs。这一节描述了Setup如何确定源和目标位置以及提供指南以帮助你正确的说明这些位置。

独立于操作系统发行的INF的源介质

独立于操作系统发行的设备的INF文件使用SourceDisksNames和SourceDisksFiles节来说明文件的位置。如果这样的INF在DDInstall节包含Include和Needs表项，那些表项就说明了另外的可能的源位置。

如果INF有SourceDisksNames和SourceDisksFiles节，而没有Include表项，那么SourceDisksNames和SourceDisksFiles节必须列出所有的除了目录和INF文件以外的原介质和源文件。（Windows 98要求目录文件被列在SourceDisksFiles节；Windows 2000忽略这个表项。）目录文件必须和INF文件处在同一个位置。目录文件不能被压缩。

和操作系统一起发行的INF的源介质

如果支持设备的文件被包括在操作系统中，INF必须在Version节说明LayoutFile表项。这个表项说明文件在操作系统介质的何处。说明LayoutFile表项的INF不能包括SourceDisksNames和SourceDisksFiles节。

仅仅系统提供的INF可以直接引用layout.inf。和操作系统一起发行的OEM和IHV文件可以引用系统提供的INF（如mf.inf）中的公共install节。

源介质和包含Include和Needs表项的INF

如果INF有SourceDisksNames和SourceDisksFiles节，以及Include和Needs表项，Setup使用主INF文件加上任何“包括”的INF文件以确定源介质的位置。当说明源介质和源文件位置时，包含的文件要尽可能的精确是特别的重要。

考虑在图3.1所示的包含的INF的层次。

图3.1 包含的INF文件的层次示例

在图3.1，对于一个多功能设备的INF，MyMfDevice.inf，包含了系统提供的mf.inf文件。反过来，系统提供的mf.inf文件包含了系统提供的文件layout.inf.。当Setup搜索源介质以便从它拷贝在MyMfDevice.inf所引用的文件时，它在MyMfDevice.inf和任何引用被拷贝文件的包含的INF文件中寻找SourceDisksFiles节。Setup首先搜索MyMfDevice.inf，但是并不保证对包含的INF文件的搜索顺序。

修饰的SourceDisksFiles节比未修饰的节优先，即使修饰的节在一个包含的文件中。例如，图3.1所示的INF文件，当在一个x86机器上安装时，如果layout.inf.包含[SourceDisksFiles.x86]节并且MyMfDevice.inf仅仅包含了一个未修饰的[SourceDisksFiles]节，Setup首先从layout.inf.使用修饰的节。因此，包含其他INF的INF应包含使用平台扩展的某个节名。

典型情况下，厂商提供的INF应当说明它的驱动程序包的文件的位置，不应当使Setup为了文件位置而搜索包含的INF文件。换句话说，拷贝文件的厂商INF应说明SourceDisksNames和SourceDisksFiles节，那些节应当用平台扩展来修饰，也应包含被INF直接拷贝的所有文件的信息。另外，如果厂商文件名字尽可能的具体是有益的，除非安装程序正在替换和操作系统一起发行的厂商提供的文件。

为设备安装文件说明目标位置

INF文件用DestinationDirs节来说明设备文件的目标位置。这一节应当总是被说明的，并且和拷贝、更名、删除语句的节在同一个INF文件中。

DestinationDirs节应当包含DefaultDestDir表项。

如果一个INF有拷贝、更名、删除语句的节但是没有DestinationDirs节并且这个INF包含了其他INF，Setup搜索包含的INF以确定目标位置信息。但是，Setup搜索包含的INF的顺序是不可预测的。因此，Setup会冒一个风险，例如，拷贝文件到一个违背INF开发者意愿的位置。为避免这样的混淆，总是在包含拷贝、更名、删除语句的节中说明DestinationDirs节。DestinationDirs节应当至少包括DefaultDestDir表项，并能列出INF中的拷贝、更名、删除文件的节。

1.3 建立跨平台的和/或者双操作系统的INF文件

你可以使用系统定义的扩展以建立一个单一的跨平台的安装和/或者双操作系统安装的INF文件。扩展能使你建立修饰节名称和修饰指令。这个修饰说明了哪个DDInstall节和相关的指令对每个平台和/或者操作系统是重要的。你可以建立一个INF，对在x86平台的Windows 2000设备给与安装支持，对未来平台的Windows 2000给与安装支持，对所有平台的Windows 2000给与安装支持，对Windows 9x平台的支持，或者对所有以上平台的支持。

例如，如果你的设备支持Windows 2000和Windows 9x平台，你将建立一个有两个或多个“并行”DDInstall节的INF，它有下面的扩展以控制在每个平台和/或者每个操作系统安装什么：

·[install-section-name.NTx86]包含在所有的Windows 2000 x86平台上为安装设备或者一套设备兼容的模块而要求的Windows 2000专用指令。

·[install-section-name.NT]包含在所有的Windows 2000平台上为安装设备或者一套设备兼容的模块而要求的Windows 2000专用指令。

当前，.NT代表了由.ntx86定义的相同的一套平台。如果在将来Windows 2000支持非x86平台，.NT将代表包含.ntx86定义的平台和那些新平台。

·[install-section-name]包含Windows 9x专用指令。

未修饰的节在Windows 2000平台也适用。Setup在Windows 2000上使用未修饰的节如果它可以定位合适的修饰节。但是为了防止Windows 2000的节错误的被用在Windows 9x上，你应当对任何Windows 2000特定的DDInsatll节和相关的节以.ntx86或者.nt进行修饰。

Windows Setup如下处理DDInsatll节：

·在Windows 98，Setup处理未修饰的节并忽略以.nt*平台扩展修饰的任何节。

·在Windows 2000：

Setup检查特定平台扩展的install节，如.ntx86。如果有一个存在，Setup为当前平台处理合适的节。Setup在正被处理的INF和任何包含的INF文件（即在Include表项中包含的INF）中检查特定平台的install节。

如果没有特定平台扩展，Setup检查在INF或者任何包含的INF中检查DDInstall.nt节。如果有一个，Setup处理DDInstall.nt节。

如果没有DDInstall.nt节，Setup处理未修饰的解。

有平台扩展的DDInstall节也可以设置INF的附加的、每个设备节。特别的，对于设备和驱动程序安装，设置Windows 2000要求的DDInstall.Services以及可能DDInstall.HW，DDInstall.CoInstallers，DDInstall.LogConfigOverride，和/或者DDInstall.Interface节。在双操作系统和/或者跨平台设备/驱动程序INF文件中，在INF开发者定义的节的名称后立即说明适宜的扩展，例如，install-section-name.NTx86.HW。

如果在INF文件中没有一个或多个修饰的DDInstall.NT和/或者DDInstall.NTx86节，INF必须有并行的修饰和未修饰的附加的每个设备节。即，如果一个双操作系统INF有DDInstall.NT和DDInstall节以及DDInstall.HW节，它也必须有并行的DDInstall.NT.HW节（如果设备或者驱动程序要求一个Windows 2000 .HW节）。

INF文件节和指令文档说明了这些扩展，它作为正规语法语句的一部分，例如：

[DDInstall.HW]

[DDInstall.nt.HW]

[DDInstall.ntx86.HW]

但是，这样一个正规语法语句指示这些扩展是合法的，但是对基本节可能是可选的。它并不指示任何有DDInstall.nt.HW节的INF也必须有每一个其他特定平台的DDInstall.ntXXX.HW节。Windows 2000跨平台和/或者双操作系统INF文件的编写者可以使用任何这些系统定义的扩展的子集以说明特别的跨平台和/或者双操作系统设备/驱动程序安装所要求的最小数目的修饰节。

1.4 建立国际化的INF文件

将在国际市场使用的INF应当对所有用户可见的文本使用%strkey%令牌。字符串令牌被定义在[Strings]节，它通常在INF文件的末尾。

为了建立一个单一的国际化的INF文件，你应当包含一套本地专用的Strings.LanguageID节，在INF Strings节的参考页中有描述。

可选的，你也可以为每种本地化建立分离的INF。为减少重复和易于维护，除了Strings节，你建立一个有必须节和表项的主INF文件，。然后建立第二套文件，这里每个文件为支持的本地化仅包含Strings节。将主文件和每个字符串文件相连以产生本地专用的INF文件。

如果一个INF包含在ASCII范围之外的字符（在0-127之外），INF应当是Unicode 格式。为建立这样的文件，例如，从应用程序如记事本程序将INF存成Unicode 格式文件。如果INF不是Unicode格式，Setup使用当前的本地化翻译字符。如果INF是Unicode格式，Setup使用全字符集。

1.5 在设备的INF文件中加强打开文件的安全性

对Windows 2000，微软为某些设备类别加强了在类安装程序的INF中的打开文件的安全性，包括CDROM、DiskDrive、FDC、FloppyDisk、HDC和SCSIAdapter。如果你不能确定类安装程序是否为你的设备加强了文件打开时的安全性，你应在设备INF中加强安全性。

在设备的INF文件中，在[Xxx_AddReg]节增加以下的表项：

[install-section-name.HW] |

[install-section-name.nt.HW] |

[install-section-name.ntx86.HW]

...

AddReg = Xxx_AddReg

...

[Xxx_AddReg]

...

HKR,,DeviceCharacteristics,0x10001,characteristics

Characteristics值是赋给设备的特性数字值。这个值是定义在wdm.h或者ntddk.h中的一个或多个FILE_* 文件特性的逻辑或（OR）的结果。对于安全性重要的特性是FILE_DEVICE_SECURE_OPEN，它的数字值为0x100。设置这个特性使I/O管理器对所有设备的打开请求执行安全检查。FILE_DEVICE_SECURE_OPEN特性在wdm.h和ntddk.h中定义，并被Windows 2000平台支持，但是不被Win9x支持。

特性值为0使Setup忽略类范围内的设备特性（在相关联的类安装程序INF中说明）。

在设备安装期间，PnP管理器以如下方式为设备对象确定设备特性：

它提取设备类的特性设置（在类安装程序INF中说明）。

它提取设备的特性（在设备INF中说明）。特定设备的设置覆盖类的设置。

或者直接或者通过IoCreateDevice，它将特定INF的设置和那些由驱动程序在设备对象中的设置进行逻辑或。

它将FDO的设备特性设置和任何过滤DO逻辑或，并传播这些设置到FDO和过滤DO。如果设备在原始方式，那么就没有FDO，PnP管理器传播设备特性到PDO和任何过滤DO。

以上的步骤确保了，如果一个驱动程序为设备设置了FILE_DEVICE_SECURE_OPEN，那么这个特性被传播该所有的驱动程序所服务的设备的设备对象。

PnP管理器在它已经为设备的驱动程序调用AddDevice例程之后，以及在驱动程序已经建立了它们的设备对象并把它们连接到设备堆栈之后，但是在它发送IRP_MN_START_DEVICE请求之前，逻辑或并传播这些特性。

当PnP管理器传播FILE_DEVICE_SECURE_OPEN时，它也传播以下的设备特性：FILE_ROMOVABLE_MEDIA，FILE_READ_ONLY_DEVICE，FILE_FLOPPY_DISKETTE。

一个有ClassInstall32节的INF文件可以为设备的设置类设置FILE_DEVICE_SECURE_OPEN特性。参见第5章，在一个类安装程序的INF文件中加强打开文件的安全性。

1.6 从应用程序访问INF 文件

大多数INF文件由系统类安装程序和其他系统应用程序所使用。这一节包含了厂商提供的应用程序必须访问INF文件的信息。

这一节描述了可以用INF文件执行几个公共操作以及通用的Setup函数。对于如何使用这些函数的完整信息，参见平台SDK文档。
1.6.1 打开和关闭INF文件

在一个应用程序访问INF文件之前，它调用SetupOpenInfFile打开文件。这个函数返回一个INF文件的句柄。

如果你不知道需要打开的INF文件的名称，使用SetupGetInfFileList获得在目录中所有INF文件的列表。

一旦应用程序打开一个INF文件，它可以使用SetupOpenAppendInfFile将另外的INF文件插入到打开的INF文件之后。当后续的Setup函数引用打开的INF文件，他们也可以访问存储在任何插入的文件中的信息。

如果在调用SetupOpenAppendInfFile中没有INF文件被说明，这个函数在调用SetupOpenInfFile中打开INF文件的Version节的LayoutFile表项所说明的文件。

当在INF文件中的信息不再需要时，应用程序应当调用SetupCloseInfFile以释放在SetupOpenInfFile所分配的资源。
1.6.2 从INF文件提取信息

一旦你有了INF文件的句柄，你可以按不同的方式提取信息。诸如SetupGetInfInfomatiom，SetupQueryInfFileInfomatiom，SetupQueryInfVersionInfomatiom等函数从特定的INF文件中提取信息。

别的函数，例如SetupGetSourceInfo和SetupGetTargetPath，从源文件和目标目录得到信息。

另外的函数，例如SetupGetLineText和SetupGetStringField，能使你直接访问存在INF文件的一行或一个域的信息。这些函数被更高一层的Setup函数内部使用，但是如果你需要直接访问行或域级其他信息，也可以使用这些函数。


编写协同安装程序

协同安装程序是微软公司开发的win32的DLL，它是用来帮助在Windows 2000系统上进行设备安装。它被Setup API调用作为类安装程序的“助手”。例如，供应商可以提供协同安装程序将特定设备信息写入INF文件无法处理的注册表中。

本章内容：

l 4.1 协同安装程序总览

l 4.2 协同安装程序界面

l 4.3 协同安装程序操作

l 4.4 注册协同安装程序

1.1 协同安装程序总览

由Setup API调用的协同安装程序如图4-1所示。

图4-1 协同安装程序在设备安装中的分工

带有阴影的方框表示可由IHV和OEM提供的组件，其他组件则由OS提供。参见第1章“设备安装总览”可以了解更多的有关安装组件的信息。

协同安装程序可以是设备专用或类专用的。Setup API只在安装协同安装程序为其注册的设备时才调用一个设备专用的协同安装程序。操作系统(OS)及供应商可以为一个设备注册零个或多个设备专用的协同安装程序。在为协同安装程序注册安装设备设置类的任何设备时，Setup API调用类协同安装程序。操作系统及供应商可以为一个设备设置类注册一个或多个类协同安装程序。除此之外，类协同安装程序还可以为一个或多个设置类注册。

GUI模式设置、新设备DLL以及定制设置应用程序通过调用带有设备安装函数代码(DIF代码)的 SetupDiCallClassInstaller 来安装设备。对于每一个DIF请求，SetupDiCallClassInstaller 调用为设备设置类注册的任何类协同安装程序，调用为特定设备注册的任何设备协同安装程序，以及由系统提供用于设备设置类的类安装程序(如果有的话)。

定制设置应用程序须调用 SetupDiCallClassInstaller 而不是直接调用协同安装程序或类安装程序。这个函数可以保证所有注册的协同安装程序都能被正确调用。

类协同安装程序一般都在设备安装之前注册，而设备专用的协同安装程序则是作为设备安装的一部分被注册的。因此类协同安装程序总是在第一次构建时就被添加到协同安装程序列表之中，并在设备安装时被所有DIF请求调用。特定设备协同安装程序则是在为该设备完成DIF_REGISTER_COINSTALLERS请求之后被添加到重安装列表之中的(或是在调用了 SetupDiRegisterCoDeviceInstallers 之后)。特定设备协同安装程序并不参与以下DIF请求：DIF_ALLOWINSTALL、DIF_INSTALLDEVICEFILES及DIF_SELECTBESTCOMPATDRV。

如果协同安装程序需要响应以下任何一个DIF请求，它就必须是一个类协同安装程序(而不是设备专用协同安装程序)：

l DIF_FIRSTTIMESETUP，DIF_DETECT*

l DIF_NEWDEVICEWIZARD_PRESELECT

l DIF_NEWDEVICEWIZARD_SELECT

l DIF_NEWDEVICEWIZARD_PREANALYZE

l DIF_NEWDEVICEWIZARD_POSTANALYZE

设备协同安装程序并不适用于这样的上下文，这是因为并没有标识出某个特定的设备，或是因为在安装的这个初始阶段还没有注册过设备安装程序。

图4-2显示了在注册了任意一个特定设备的协同安装程序之后，SetupDiCallClassInstaller 调用协同安装程序及类安装程序的顺序。

图4-2 为DIF请求调用协同安装程序的处理及后处理示例

在图4-2所演示的示例中，为该设备的设置类注册了两个类协同安装程序以及一个特定设备协同安装程序。以下步骤对应于图4-2中的带圆圈的数字标号：

1. SetupDiCallClassInstaller 调用第一个类协同安装程序，同时指定一个表示安装请求正在处理中的DIF代码(在本例中是DIF_INSTALLDEVICE)。协同安装程序在安装请求中有参与的选择权。本例中，第一个注册的类协同安装程序返回NO_ERROR。

2. 接下来，SetupDiCallClassInstaller 调用任意额外注册的类协同安装程序。在本例中，第二个类安装程序返回了ERROR_DI_POSTPROCESSING_REQUIRED，它要求在后处理之后再调用协同安装程序。

3. SetupDiCallClassInstaller 调用任意注册的设备专用协同安装程序。

4. 在调用了所有的注册过的协同安装程序后，如果设备的设置类有一个系统提供的类安装程序，SetupDiCallClassInstaller 就调用它。在本例中，类安装程序返回ERROR_DI_DO_DEFAULT，这是类安装程序的一个典型返回值。

5. 如果有一个缺省的设备处理驱动程序，SetupDiCallClassInstaller 就为安装请求调用它。DIF_INSTALLDEVICE有一个缺省的设备处理驱动程序 SetupDiInstallDevice，它是Setup API的一部分。

6. SetupDiCallClassInstaller 调用任何要求后处理的协同安装程序。在本例中，第二个类协同安装程序要求了后处理。

除了协同安装程序在它的单个入口点被再一次调用外，协同安装程序的后处理与驱动程序的IoCompletion例程相似。当 SetupDiCallClassInstaller 为后处理调用协同安装程序时，它将 PostProcessing 设为TRUE，并将 InstallResult 设为 Context 参数中的恰当值。在本例中，Context.InstallResult是NO_ERROR，这是因为成功地执行了缺省的设备处理驱动程序。

在后处理中，SetupDicallClassInstaller 反向调用了协同安装程序。如果图4-2中的所有协同安装程序都已返回了ERROR_DI_POSTPROCESSING_REQUIRED，那么 SetupDiCallClassInstaller 就会为后处理先调用Device_Coinstaller_l，之后再是Class_Coinstaller_2，和Class_Coinstaller_1。类安装程序并不要求后处理，只有协同安装程序才要求。

即使先前的协同安装程序在安装请求中失败，要求后处理的协同安装程序也会被调用。

1.2 安装程序界面

协同安装程序具有以下原型：

typedef

DWORD

( CALLBACK* COINSTALLER_PROC) (

IN DI_FUNCTION InstallFunction,

IN HDEVINFO DeviceInfoSet,

IN PSP_DEVINFO_DADA DeviceInfoData OPTIONAL,

IN OUT PCOINSTALLER_CONTEXT_DATA Context

);

InstallFunction 指定了正被处理的设备安装请求，在其中协同安装程序具有参与的选择权。例如，DIF_INSTALLDEVICE。参见《Windows 2000 Driver Development Reference》一书的第一卷第3部分的第5章“安装功能代码”有关“在DIF代码上的文档处理”内容。

DeviceInfoSet 提供了一个设备信息集的标识值。

DeviceInfoData 有选择性地标识作为设备安装请求的目标设备。如果这个参数是非NULL的，它就在设备信息集中标识一个元素。当 SetupDiCallClassInstaller 调用一个特定设备协同安装程序时，DeviceInfoData 为非NULL。特定类协同安装程序可以同一个具有NULL DeviceInfoData的DIF请求（如DIF_DETECT或DIF_FIRSTTIMESETUP）一起被调用。

Context指向该安装请求的特定协同安装程序上下文结构。这个上下文信息的格式如下：

Typedef struct

_COINSTALLER_CONTEXT_DATA {

BOOLEAN PostProcessing;

DWORD InstallResult;

PVOID PrivateData;

} COINSTALLER_CONTEXT_DATA, *PCOINSTALLER_CONTEXT_DATA;

当在恰当的类安装程序（如有的话）处理了 InstallFunction 之后再调用协同安装程序时，PostProcessing 为TRUE。PostProcessing 对协同安装程序是只读的。

如果 PostProcessing 为FALSE，则 InstallResult 不相关。如果 PostProcessing 为TRUE，InstallResult 就是安装请求的当前状态。该值为NO_ERROR或是一个由为该安装请求调用的先前部分返回的错误状态。协同安装程序可以为它的函数返回通过返回该值传送状态，或者可以返回其他状态。InstallResult对协同安装程序是只读的。

PrivateData 指向一个被分配的协同安装程序缓冲。如果协同安装程序设置该指针并要求后处理，那么当 SetupDiCallClassInstaller 为后处理调用协同安装程序时，将该指针传给这个协同安装程序。

设备协同安装程序返回以下一个值：

l NO_ERROR

协同安装程序对特定的 InstallFunction 执行恰当的动作，或协同安装程序决定它无需为该请求执行任何操作。

l ERROR_DI_POSTPROCESSING_REQUIRED

协同安装程序对特定的 InstallFunction 执行任何恰当的操作，同时在类安装已处理了该请求之后并要求被再次调用。

l A Win32 error

协同安装程序遇到一个错误。

协同安装程序不会设置ERROR_DI_DO_DEFAULT的返回状态。这个状态只能由类安装程序使用。如果一个协同安装程序返回了这样的状态，那么 SetupDiCallClassInstaller 将不能正确地处理DIF_Xxx请求。协同安装程序也可能在后处理传送中传输ERROR_DE_DO_DEFAULT的一个返回状态，但是它永远不会设置这个值。

1.3 协同安装程序操作

协同安装程序是用户模式的Win32 DLL，它为注册编写额外的配置信息或执行要求动态信息的其他安装任务，而该动态信息无法通过编写INF来得到。

协同安装程序可以完成以下一些或所有的任务：

l 打开InstallFunction来处理仅一个或少量的DIF_Xxx请求。

l 根据它是否被后处理调用来执行不同的操作（也就是 Contex->PostProcessing 是否为TRUE?）

l 当为后处理调用时，检查 Context->InstallResult。如果它不是NO_ERROR，就进行任何必需的清除并返回 InstallResult。

协同安装程序必须不能给最终用户显示任何的UI。协同安装程序应该为设备提供恰当的缺省值。如果它没有缺省值并要求用户输入，那么其他的设备支持组件就应提示用户稍后的所需输入。例如，若调制解调器没有正确的拨号属性设置，则需在用户使用调制解调器而不是在设备设置时提示他们。
1.3.1 处理DIF代码

每个DIF代码的参考页都继续了以下一些或全部的部分：

何时发送

描述Setup应用程序发送该DIF请求的典型时间及原因。

由谁处理

指出允许哪些安装程序处理该请求。该安装程序包括了类安装程序、类协同安装程序（设置-类范围的协同安装程序），以及设备协同安装程序（特定设备协同安装程序）。

输入

SetupDiCallClassInstaller 通过在它的主入口点调用安装程序给一个安装程序发送一个DIF请求。除了DIF代码之外，这个功能提供与某请求相关的额外信息。参见每个DIF代码的参考页可得到与每个请求一起提供的信息细节。以下列表包括了额外输入的一般描述，还列出了用于处理参数的 SetupDiX xx函数：

l DeviceInfoSet

为设备信息集提供一个标识值。

该标识值是不透明的。利用这个标识值，例如，在调用中将设备信息集标识到 SetupDiX xx函数。

DeviceInfoSet 可能具有相联的设备设置类。如果是这样的，则调用 SetupDiGetDeviceInfoListClass 以得到类GUID。

l DeviceInfoData

有选择性地给一个SP_DEVINFO_DATA结构提供一个指针，该结构在设备信息集中标识了一个设备。

l Device Installation Parameters

这些非直接的参数为SP_DEVINSTALL_PARAMS结构中的设备安装提供了信息。如果DeviceInfoData是非NULL，就有与DeviceInfoData相关的设备安装参数。如果DeviceInfoData为NULL，则设备安装参数就与DeviceInfoSet相关的设备安装参数。

调用 SetupDiGetDeviceInstallParams 以得到设备安装参数。

l Class Installation Parameters

将可选的非直接参数指定给某个DIF请求。它们尤其是“DIF请求参数”。例如，一个DIF_REMOVE安装请求的类安装参数被包含在一个SP_REMOVEDEVICE_PARAMS结构中。

每个SP_XXX_PARAMS结构开始于一个固定大小的SP_CLASSINSTALL-HEADER。

调用 SetupDiGetClassInstallParams 以得到类安装参数。

如果DIF请求具有类安装参数，就有与 DeviceInfoSet 相关的参数集，及与DeviceInfoData相关的另一个参数集（如果DIF请求指定了DeviceInfoData）。SetupDiGetClassInstallParams返回了可得到的最特定参数。

l 上下文 （Context）

协同安装程序具有一个可选的上下文参数。

l 输出 （Output）

描述这个DIF代码所需的输出。

如果安装程序修改了设备安装参数，那么在返回之前安装程序必须调用 SetupDiSetDeviceInstallParams 来应用改变。类似地，如果安装程序修改DIF代码的类安装参数，安装程序必须调用 SetupDiSetClassInstallParams。

l 返回值 (Return Value)

指定DIF代码的恰当返回值。参见图4-3中有关返回值的更多信息。

l 缺省处理程序 (Default Handler)

指定 SetupDi 函数，它执行DIF代码的系统定义操作。并非所有的DIF代码都具有缺省处理程序。除非协同安装程序或类安装程序采取步骤阻碍调用缺省处理程序，SetupDiCallClassInstaller 才会在它调用类安装程序之后再调用DIF代码的缺省处理程序（但却是在它调用任何为后处理注册的协同安装程序之前）。

操作 (Operation)

描述安装程序可能用来处理DIF请求的典型任务。

其他 (See Also)

相关信息源的列表。

图4-3中是 SetupDiCallClassInstaller 中处理DIF代码的事件序列。

操作系统执行每个DIF代码的一些操作。由供应商提供的协同安装程序及类安装程序可以参与安装行为。请注意即使DIF代码失败了，SetupDiCallClassInstaller 也调用了为后处理注册的协同安装程序。

1.4 注册协同安装程序

协同安装程序可以为某个设置类的单个或全部设备注册。当特定设备中的一个已被安装时，这些设备的协同安装程序就通过INF文件动态注册。类协同安装程序被手工注册或由定制的设置应用程序及一个INF注册。

如要了解更多的信息，可参见《Registering a Device-Specific Coinstaller》及《Registering a Class Coinstaller》。

如要更新协同安装程序，DLL的每个新版本都需有一个新的文件名，这是因为当用户在设备属性页上点击Update Driver按纽时，尤其要用到DLL。

图4-3 在SetupDiCallClassInstaller中的DIF代码处理流程图
1.4.1 注册设备专用的协同安装程序

为了注册设备专用的协同安装程序，将以下部分添加到设备的INF文件中：

； ：

； ：

[DestinationDirs]

XxxCopyFilesSection = 11 \\DIRID_SYSTEM

\\ Xxx = driver or dev . prefix

； ：

； ：

[XxxInstall . OS-platporm.CoInstallers] \\ OS-platform is optional

CopyFiles = XxxCopyFilesSection

AddReg = Xxx.OS-platform. CoInstallers_AddReg

[XxxCopyFilesSection]

XxxCoInstall.dll

{Xxx. OS-platform.CoInstallers_AddReg}

HKR,,CoInstallers32.0x00010000.”XxxCoInstall.dll. \

XxxCoInstallEntryPoint”

DestinationDirs 部分中的项说明XxxCopyFiles部分中列出的文件将被复制到系统目录下。Xxx前缀标识出驱动程序、设备或设备组（如cdrom_CopyFilesSection）。Xxx前缀应是唯一的。

协同安装程序安装节的名称可以用可选的操作系统/架构扩展（如cdrom_install.NTx86.CoInstallers）来修饰。

Xxx_AddReg部分中的项在设备驱动程序密钥中建立一个 CoInstallers32 值项。该项包含了协同安装程序DLL，而且可选地还可有一个特定入口点。如果忽略这个输入点，则缺省为CoDeviceInstall。十六进制标志参数（0x00010000）指明这是REG_MULTI_SZ值项。

为了给设备注册多于一个的特定设备协同安装程序，复制每个协同安装程序的文件并在注册项中包含至少一个信息串。例如，为了注册两个协同安装程序，建立如下部分：

； ：

； ：

[DestinationDirs]

XxxCopyFilesSection = 11 \\DIRID_SYSTEM

\\ Xxx = driver or dev . prefix

； ：

； ：

[XxxInstall . OS-platporm.CoInstallers] \\ OS-platform is optional

CopyFiles = XxxCopyFilesSection

AddReg = Xxx.OS-platform. CoInstallers_AddReg

[XxxCopyFilesSection]

XxxCoInstall.dll \\ copy 1st coinst. file

YyyCoinstall.dll \\ copy 2nd coinst. file

[Xxx. OS-platform.CoInstallers_AddReg]

HKR..CoInstallers32.0x00010000. \

“XxxCoInstall.dll. XxxCoInstallEntryPoint”. \

“YyyCoInstall.dll. YyyCoInstallEntryPoint”

\\ add both to registry

当执行设备专用的协同安装程序INF部分时，该协同安装程序是在安装一个设备的过程中被注册的。接着Setup API在安装过程中的每个随后步骤上调用协同安装程序。如果为一个设备注册多个协同安装程序，那么Setup API按其在注册中所列顺序调用它们。
1.4.2 注册类协同安装程序

如要为某个设置类的每个设备都注册一个协同安装程序，可按以下所列建立一个注册表项

HKLM\System\CurrentControlSet\Control\CoDeviceInstallers subkey:

{setup-class-GUID}: REG_MULTI_SZ : “XyzCoInstall.dll. XyzCoInstallEntryPoint\0\0”

该系统建立了 CoDeviceInstallers 密钥。Setup-class-GUID为设备设置类指定GUID。如果协同安装程序提供设备的多个类，它就建立每个设置类的单个值项。

我们不能覆盖先前写给setup-class-GUID密钥的其他协同安装程序。读取这个密钥，将自己的协同安装程序信息串附加到REG_MULTI_SZ列表中，并将该密钥写回到注册表中。

如果忽略CoInstallEntryPoint，则缺省为CoDeviceInstall。

协同安装程序DLL必须也被复制到系统目录下。

一旦复制了文件且做出了注册表项，类协同安装程序就可被用来调用相关设备和服务。

不用手工建立注册项来注册一个类协同安装程序，就可以利用INF文件注册它，如以下所示。

[version]

signature = “$Windows NT$”

[DestinationDirs]

DefaultDestDir = 11 / / DIRID_SYSTEM

[DefaultInstall]

CopyFiles = @classXcoinst.dll

AddReg = CoInstaller_AddReg

[CoInstaller_AddReg]

HKLM.System\CurrentControlSet\Control\CoDeviceInstallers, \

{setup-class-GUID}, 0x00010008, “classXcoinst.dll,classXCoInstaller’

; above line uses the line continuation character (\)

这个例子INF将文件classXcoinst.dll复制到系统目录下并在 CoDeviceInstallers 密钥下建立了一个setup-class-GUID类的项。Xxx-AddReg部分的项指示两个标志：”00010000”标志表示这个项是REG_MULTI_SZ，而”00000008”标志表示新值将被附加到任何已有的值上（如果新值并未存在于信息串中）。

这样一个注册表类协同安装程序的INF可由右点击安装或通过 SetupInstallFromInfSection 应用程序激活。


编写类安装程序

类安装程序DLL执行在某个设备设置类中应用设备的操作。例如，端口类安装程序负责将COM端口名赋给端口设置类中的一个设备。如果某个设置类中的设备不要求任何特别的安装操作，那么对于该设置类也不要求类安装程序。

微软提供系统定义的 device setup classes（它要求类安装程序）类安装程序。IHV或OEM可以提供一个协同安装程序来执行任何所需的设置操作（参见第4章“编写协同安装程序”）。

如果为自己设备定义了一个新的设备设置类，那么可能会需要写出一个类安装程序。但是，很少需要定义一个新的设备设置类，这是因为几乎所有的设备都可以与系统定义的 device setup classes 中的一个相关。例如，数字相机的制造者可能会认为它需要一个新的设置类，但是相机却是属于图象类的。

本章包含以下内容：

l 5.1 类安装程序界面

l 5.2 设备安装函数小结

l 5.3 注册类安装程序

l 5.4 在类安装程序的INF文件中加强文件打开的安全性

大家还可参见“编写协同安装程序”一章以了解处理DIF代码的一般信息。

1.1 类安装程序界面

每个类安装程序都输出一个类安装程序函数。该函数的缺省名为 ClassInstall。不论何时当Setup API需要类安装程序来执行它的设置类设备的特定安装任务，它都会调用这个函数。

安装函数的原型如下：

typedef DWORD (CALLBACK* CLASS_INSTALL_PROC) (

IN DI_FUNCTION InstallFunction,

IN HDEVINFO DeviceInfoSet,

IN PSP_DEVINFO_DATA DeviceInfoData /* optional */

类安装函数接受两个或三个自变量：InstallFunction，它标识出要执行的安装请求；另一个是设备信息集的标识值；还有一个可选自变量，即指向SP_DEVINFO_DATA结构的指针，该结构包含了有关设备安装的信息。

类协同安装程序返回以下的一个值：

l ERROR_DI_DO_DEFAULT

典型地，类安装程序返回ERROR_DI_DO_DEFAULT，它构建出设置来执行DIF请求的缺省动作。

这个返回状态并不真是一个错误，而就是一个有效的返回值。ERROR_DI_DO_DEFAULT表明类安装程序有可能执行了一些过滤或增大安装的动作，但是类安装程序的动作并不能取代缺省动作。

例如，类安装程序可能设置一些标志对应于DIF_INSTALLDEVICE请求并返回ERROR_DI_DO_DEFAULT。对应于这个返回值，设置执行了DIF_INSTALLDEVICE的缺省动作(调用 SetupDiInstallDevice)之后再调用要求后处理的任何协同安装程序。

如果已知了所给的DIF要求并没有缺省动作，那么就应该仍是返回ERROR_DI_DO_DEFAULT。DIF请求可能会在今后发行的操作系统中具有缺省动作。

l A Win32 error

类安装程序可以返回一个恰当的Win32错误。

l NO_ERROR

当类安装程序决定它已完整地完成了设备安装任务，它就仅返回NO_ERROR，该任务包括了调用任何缺省处理程序或代替缺省操作。

在诸如下面的情况下，类安装程序可能会调用缺省处理程序。如果缺省处理程序已运行了，安装程序就需要执行某个动作。如果类安装程序调用缺省处理程序，则安装程序必须不返回ERROR_DI_DO_DEFAULT；它必须返回NO_ERROR或Win32错误。

1.2 设备安装函数小结

安装程序可以使用由Setup API提供的 device installation functions 来执行安装操作。设备安装函数允许安装程序搜索INF文件兼容的驱动程序，通过选择对话框向用户显示驱动程序选择，并执行真正的驱动程序安装。

大多数的设备安装函数依赖于SP_DEVINFO_DATA结构中的信息来执行安装任务。每个设备都具有一个与之相关的SP_DEVINFO_DATA结构。我们可以重新得到设备信息集的一个句柄(HDEVINFO)，该信息集通过 SetupDiGetClassDevs 函数将所有被安装的设备包含在一个特定的类中。我们可以利用 SetupDiCreateDeviceInfo 函数将新的设备添加到设备信息集中。还可以通过 SetupDiDestroyDeviceInfoList 函数将所有的SP_DEVINFO_DATA结构都释放到设备信息集中。这个函数还释放任意的兼容设备及可能已被添加到结构中的类设备列表。

通过使用 SetupDiBuildDriverInfoList 函数，可以生成一个列表，安装程序或用户可从该列表中选择驱动程序或设备来安装。SetupDiBuildDriverInfoList 建立一个兼容驱动程序列表或特定类的所有设备列表。

一旦有了这样的一个兼容驱动程序的列表之后，就可提示用户利用 SetupDiSelectDevice 函数从该列表中进行选择。这个函数显示一个包括了设备信息集中每个设备信息的对话框。我们可以通过使用 SetupDiInstallDevice 函数安装选定的驱动程序。这个函数使用驱动程序的INF文件信息来创建任何所需的新注册表项，设置设备硬件的配置，并将驱动程序文件复制到恰当的目录下。

安装程序可能需要检查，并在注册密钥下设置将要安装设备的值。我们可以利用 SetupDiCreateDevRegKey 或 SetupDiOpenDevRegKey 函数打开设备的硬件或驱动程序密钥。

可以通过 SetupDiInstallClass 函数安装设备的新类。这个函数从一个包含了 ClassInstall32 部分的INF文件安装设备的新类。

可以通过 SetupDiRemoveDevice 函数从系统中删除设备。这个函数删除设备注册项，如果可能还停止该设备的运行。如果不能动态停止设备，那么函数就设置提示最终导致用户关闭系统的标志。

1.3 注册类安装程序

通过添加 Installer32 和 Icon 项到类注册中可以注册类安装程序。微软的Windows 2000在 HKEY_LOCAL_MACHINE 下维持一个有关每个类信息的 System\CurrentControlSet\Control\Class 分支。(Windows9x在 HKEY_LOCAL_MACHINE 下维持一个有关每个类信息的 System\CurrentControlSet\Services\Class 分支)。该密钥的值包含了类的本地描述。我们可以在一个由 INF ClassInstall32 部分引用的 AddReg 部分中做出恰当的类安装注册项。

Installer32 和 Icon 项具有以下形式：

Installer32=安装程序-DLL-名[,安装程序入口点]

Icon=序号

系统使用图标表示用户的安装程序并调用它来在类中安装设备。如果 Icon 的值为负，它的绝对值代表类注册中的一个预定义的图标。如果 Icon 值为正，它表示将被抽取出的可执行的类安装程序中的图标。值1被保留。参见《Windows 2000 Driver Development Reference》卷一中的 SetupDiDrawMiniIcon 来查看预定义的小图标。

1.4 在类安装程序的INF文件中加强文件打开的安全性

对于Windows 2000，可以通过向类安装程序INF添加新的项来加强设备的一个类文件打开安全性。新项指定类中设备新的FILE_DEVICE_SECURE_OPEN特性。

新项出现在由 ClassInstall32 部分引用的 AddReg 部分中并有以下的句法：

[ClassInstall32] | [ClassInstall32.ntx86]

…

AddReg = Xxx_AddReg

…

[Xxx_AddReg]

…

HKR,, DeviceCharacteristics,0x10001 , characteristics ; new

characteristics 值是赋给设备特性的数字值。该值是一个或多个FILE_*文件特性被定义到wdm.h或ntddk.h中的Oring的结果。与安全性相关的重要新特性是FILE_DEVICE_SECURE_OPEN，其数字值是0x100。该特性的设置将I/O Manager直接用于执行设置类的设备的所有打开请求的安全性检查。FILE_DEVICE_SECURE_OPEN特性被定义在 ntddk.h 和 wdm.h 中，且仅被Windows 2000平台支持。

DeviceCharacteristics 是HKR AddReg项的特殊关键字。设置将特定的设备特性储存到注册表中的个人站点，它是从由Setup建立并由其他HKR AddReg项指定的用户定义的注册项中分离出来的。

如果合适的话，通过指定它的设备INF文件中的”HKR,,DeviceCharacteristics,…”项，类中的单个设备可以覆盖由类安装程序设置的特性。参见Plug and Play、电源管理及设置引用中的INF AddReg Directive部分以了解更多的内容。


编写定制设备安装应用程序

定制设备安装应用程序应调用 UpdatdDriverForPlugAndPlayDevices 函数来安装新设备而不是自己发送所有必要的DIF代码。若已给出了一个INF文件和硬件ID，这个函数就在机器上扫描设备，并尝试着将 FullInfPath 上指定的设备安装到与所给的匹配的任何设备上。如果设置应用程序调用 UpdeateDriverForPlugAndPlayDevices 而不是发送DIF代码，则该应用程序则更有可能是向上兼容的。例如，如果今后发行的操作系统对设备安装序列作了改变，那么微软也会修改 UpdateDriverForPlugAndPlayDevices 来发送恰当的DIF代码。

若由于某种原因，定制设备安装应用程序不能调用 UpdateDriverForPlugAndPlayDevices，这样的应用程序就应通过调用 SetupDiCallClassInstaller 来发送DIF代码。这个函数保证了调用任何的协同安装程序、类安装程序及 SetupDiX xx缺省设备驱动程序且顺序是是恰当的。

1.1 安装与驱动程序共用的软件实用程序

你的定制设备安装应用程序应该处理以下两种情况：

l 用户插接进硬件中并插入到分配媒介

l 用户插入到分配媒介以安装设备支持并插接进硬件

如要解决第一种情况，就应提供一个调用 UpdateDriverForPlugAndPlayDevices 的自动运行的设备程序，并运行你的设备实用程序或应用程序的设置程序。

如要解决第二种情况，则按以下步骤设置程序：

1. 在目标系统中，给驱动程序文件建立一个目录。如果设置程序安装了一个应用程序，驱动程序文件就应被储存在应用程序目录下的子目录中。

2. 由分配媒介向步骤1中的目录复制驱动程序包中的所有的文件。驱动程序包包括了驱动程序、INF文件、目录文件等。

3. 调用步骤1创建的目录中的INF文件的 SetupCopyOEMInf。指定OEMSourceMediaType的SPOST_PATH参数，并将OEMSourceMediaLocation参数指定为NULL。SetupCopyOEMInf 复制驱动程序包的INF文件到目标系统的%windir%\Inf目录下，并指示Setup API将INF文件的源位置储存到它的预处理INF文件列表中。SetupCopyOEMInf 还处理目录文件，所以PnP Manager将会在它下一次识别INF文件列出的设备时安装驱动程序。

当用户插接到设备中时，PnP Manager识别该设备，找出由 SetupCopyOEMInf 复制的INF文件，并安装步骤2所复制的驱动程序。

1.2 定制安装应用程序指导

定制安装应用程序必须进行以下操作：

l 支持应用程序卸载。作为卸载的一部分，应用程序的设置程序应查看是否有相关的设备仍出现在系统中，如果有，则向用户发出警告。

l 列出Add/Remove Programs中所有已安装的应用程序，这样用户如果需要就可以卸载了。

l 按照微软Windows 2000应用程序的指导。如需了解更多的信息，可参看MSDNTM站点(http://msdn.microsoft.com)。

l 将软件应用程序提交给恰当的软件Windows Logo程序。参见MSDN站点以了解更多信息。

定制安装应用程序必须不进行以下操作：

l 指导用户复制或覆盖任何文件，尤其是.inf和.sys文件。

l 即使是硬件已被删除，在卸载操作中从系统中删除已安装的驱动程序。

l 使用RunOnce Registry表项来生成设置程序，该程序要求强制的重启动。

l 强制任何不必要的系统重启。UpdateDriverForPlugAndPlayDevices 函数通过它的bRebootRequired参数表明重启的需要。重启在安装PnP设备或软件应用程序时一般是不需要的。

l 使用设备或类协同安装程序来生成应用程序设置程序，这是因为在设备安装时不能保证系统状态对于软件应用程序安装是安全的。尤其是如果设备安装及设置程序是在服务器一方运行，系统将会被挂起。

l 由于类似的原因，使用类安装程序来引起应用程序的设置程序。

l 使用Startup Group来生成设置程序。

l 使用win.ini项生成设置程序。

l 除了在以下情况下，强制用户安装任何应用程序：

l 如果Windows不能给内置的应用程序提供使用设备标准功能的所需功能，例如，如果Windows不为一个实用程序提供设置可配置的键盘密钥。

l 如果应用程序被特别要求使用这个特定设备-----例如，如果要求一个实用程序为调制解调器设置国家代码。


提供设备属性页

如果设备或类具有任何用户可以设置的单个属性，那么协同安装程序或类安装程序应该提供一个定制设备属性页。这样的属性可能会包括CD-ROM驱动器的缺省回放卷或调制解调器的麦克风卷。当用户在设备的Properties上点击时，Device Manager都会显示属性页。在Microsoft Windows NT的早先版本中，用户通过Control Panel applets设置这样的信息。相反地，Windows 2000驱动程序应提供属性页。

尽管你也可以写出提供定制属性页的类安装程序，一般多选的是提供协同安装程序的这个机能，以及其他设备或设备-类-特定特征。

Platform SDK文件提供属性页的复合文档以及操作它们的Win32函数。本章还给Platform SDK文件补充了以下与设备安装特定相关的信息：

l 7.1 设备属性页所需的支持

l 7.2 处理DIF_ADDPROPERTYPAGE_ADVANCED请求

l 7.3 属性页的回调函数

l 7.4 处理属性页的Windows信息

1.1 设备属性页所需的支持

定制的属性页提供者必须做到以下几点：

l 处理DIF_ADDPROPERTYPAGE_ADVANCED请求

l 如果必须给属性页分配及释放额外的存储量，就要提供处理PSPCB_CREATE和PSPCB_RELEASE信息的回调。

l 提供处理每个定制属性页的Windows信息的对话框过程

图7-1是定制属性页的一个样本。

图7-1 定制属性页的一个示例

在图中，一个安装程序提供名称为Properties的属性页。缺省地，系统如图提供General和Driver书签。在合适时，系统也提供Resources和Power书签。

设置组件可以为它的设备或类定义超过一个的属性页。每个书签都应有一个该页目的的简洁描述。系统并不检查来确保名称是唯一的。

1.2 处理DIF_ADDPROPERTYPAGE_ADVANCED

提供一个或多个定制属性页的安装程序必须处理DIF_ADDPROPERTYPAGE_ADVANCED设备安装函数代码。当用户在Device Manager或在Control Panel应用程序中的设备Properties上点击时，Device Manager通过Setupapi.dll发出这个请求。

为了响应这个请求，安装程序提供其每个定制属性页的信息、创建页并将创建的页添加到设备动态属性页列表中。

安装程序应采取以下步骤：

1. 调用 SetupDiGetClassInstallParams 以得到设备的当前类安装参数。例如：

SP_ADDPROPERTYPAGE_DATAAddPropertyPageData;

:

ZeroMemory (&AddPropertyPageData, sizeof( SP_ADDPROPERTYPAGE_DATA) );

AddPropertyPageData. ClassInstallHeader.cbSize =

sizeof(SP_CLASSINSTALL_HEADER);

if (SetupDiGetClassInstallParams(DeviceInfoSet, DeviceInfoData,

(PSP_CLASSINSTALL_HEADER)&AddPropertyPageData,

sizeof(SP-ADDPROPERTYPAGE_DATA), NULL ) ) {

示例零初始化结构，其中的类安装头参数将被返回，并设定 SetupDiGetClassInstallParams 要求 cbSize 字段中的类安装头的大小。类安装头是每个类安装参数结构的第一个成分。

当一个DIF_ADDPROPERTYPAGE_ADVANCED请求是激活的，这个函数返回一个在 ClassInstallParams 上的SP_ADDPROPERTYPAGE_DATA结构。因此，对 SetupDi 函数的调用可以将返回的ClassInstallParams放到这个类型的结构中。

2. 保证尚未遇到设备动态页的最大数字，且它还带有如下的语句：

if (AddPropertyPageData.NumDynamicPages <

MAX_INSTALLWIZARD_DYNAPAGES)

如果测试失败，就不要初始化或建立页了。类似地会返回NO_ERROR。

3. 为存放任何特定设备数据分配内存，该数据将在后面的对话框过程中用到，且初始化这个区域也会使用到该数据。这个数据必须包括与请求一起传送的DeviceInfoSet和DeviceInfoData。

例如，原型页供应者可以定义并使用以下的结构：

typedef struct TESTPROP_PAGE_DATA {

HDEVINFO DeviceInfoSet;

PSP_DEVINFO_DATA DeviceInfoData;

} TEST_PROP_PAGE_DATA, *PTEST_PROP_PAGE_DATA;

PTEST_PROP_PAGE_DAT pMyPropPageData;

pMyPropPageData = HeapAlloc (GetProcessHeap( ), 0,

sizeof(TESTPROP_PAGE_DATA) );

4. 利用定制属性页初始化PROPSHEETPAGE结构：

l 在 dwFlags 中，设置PSP_USECALLBACK标志以及定制页所要求的任意其他标志。PSP_USECALLBACK标志表明回调函数已被提供。

l 在 pfnCallback 中，设置指向属性页的回调函数的指针。

l 在 pfnDlgProc 中，设置指向属性页的对话框过程的指针。

l 在 IParam 中，传送一个分配给内存区域并在步骤3中初始化的指针。

l 将其他成分设为与定制的属性页相符。参见Platform SDK文档以了解有关PROPSHEETPAGE结构的更多内容。

5. 调用 CreatePropertySheetPage 以建立新页。

6. 将新页添加到类安装参数的 DynamicPages 元素中的动态属性页列表中并添加 NumDynamicPages 成分。

7. 对于每个额外的定制属性页，重复步骤2到步骤6。

8. 调用 SetupDiSetClassInstallParams 以设置新的类安装参数，该参数包括了更新的属性页结构。

9. 返回NO_ERROR。

设置增加新建立的属性页到设备属性表，且Device Manager使Win32 API调用来建立该表。当显示属性页时，系统调用在PROPSHEETPAGE结构中指定的对话框过程。

1.3 属性页回调函数

当安装程序设置它的设备或设备类的属性页时，它提供了一个回调函数的指针。当建立属性页时，回调函数被调用一次，当属性页被破坏时则再被调用一次。

回调是 PropSheetPageProc 函数，它的描述在Platform SDK文档中，它须能处理PSPCB_CREATE和PSPCB_RELEASE动作。

在建立属性页时，回调是同PSPCB_CREATE信息一起被调用的。与这个信息对应，回调可以分配与页相关的数据内存。函数应返回TRUE来继续建立页，或由于某个原因，该页不能被建立时，则返回FALSE。

当用户点击页上的对话框中的OK或Cancel时，或是点击Drivers书签上的卸载时，设备的属性页会被破坏。

当属性页正被破坏时，回调会与一个PSPCB_RELEASE信息一起被调用。该函数应释放建立属性页时被分配的所有数据。典型地，这涉及到PROPSHEETPAGE结构中 IParam 上的数据。当页正被破坏时，返回值被忽略。

1.4 处理属性页的Windows信息

当提供属性页的安装程序处理一个DIF_ADDPROPERTYPAGE_ADVANCED请求时，它设置了属性页的对话框过程的地址。当对话框过程得到一个WM_INITDIALOG信息时，它必须初始化属性页，而且当它得到一个WM_MOTIFY信息时，它还须准备处理设备属性的变化。该过程还可以处理它可能要求的任何其他这样的信息，这同Platform SDK文件中描述的相同。

与WM_INITDIALOG信息对应，对话框过程初始化属性页中的信息。这样的信息可能包括了表示设备的一个图标、设备的友好名称，以及它的PnP设备描述。

SetupDiLoadClassIcon 装载特定设备类的图标并返回被装载的大图标的句柄，该大图标可被用在 SendDlgItemMessage 的随后调用之中。例如：

if (SetupDiLoadClassIcon(

&pTestPropPageData->DeviceInfoData->ClassGuid, &ClassIcon,

NULL0 ) {

OldIcon = (HICON)SendDlgItemMessage(hDlg, IDC_TEST_ICON,

STM_SETICON, (WPARAM)ClassIcon, 0 );

if (OldIcon) {

DestroyIcon(OldIcon);

}

}

在ClassIcon中被返回的句柄可以被放置到由 SendDlgItemMessage 函数要求的WPARAM中。在示例中，IDC_TEST_ICON表示了接收STM_SETICON信息的对话框中的控制。IDC_TEST_ICON的值必须被定义在安装程序中。参见《Windows 2000 Driver Development Reference》一书第一卷第3部分的第3章“设备安装函数”了解操作图标和位图的其他 SetupDi 函数，并参见Platform SDK文档了解有关 SendDlgItemMessage 和 DestroyIcon 的有关信息，此外还可以利用对话框中的图标。

除了图标表示设备之外，典型设备属性页还可以包括设备的描述或“友好的名称”并显示设备属性的当前设置。PnP Manager将每个设备的PnP属性存在注册表中。属性页供应者可以调用 SetupDiGetDeviceRegistryProperty 来得到任意此类属性的值。如果也将指定设备或指定类的配置信息也作为安装过程的一部分储存在注册表中，属性页供应者就可利用其他的 SetupDi 函数来抽取该信息显示了。参见《Windows 2000 Driver Development Reference》一书第一卷第3部分的第3章“设备安装函数”的详细内容。

当页中出现了某种类型的变化，属性表给对话框过程发送WM_NOTIFY信息。对话框过程应准备从信息参数中抽取通知代码并作出恰当的反应。

PSN_APPLY通知是安装程序带来的一大好处。当用户点击OK或Apply时，属性表发送该信息。相应地，安装程序应证实该变化并在设备安装参数中设置DI_FLAGSEX_PROPCHANGE_PENDING标志，如下所示：

1 如果未做好它，取得设备的设备安装参数(SP_DEVINSTALL_PARAMS结构)指针。通过调用 SetupDiGetDeviceInstallParams 及传送保存的 DeviceInfoSet 和 DeviceInfoData(已在lParam的区域上传送)可得到该结构。

2 保证用户的更改是有效的。

3 在返回的SP_DEVINSTALL_PARAMS结构的 FlagsEx 字段中设置DI_FLAGSEX_PROPCHANGE_PENDING标志。但是如果安装程序能够绝对保证更改不要求设备的驱动程序停止及再重新启动，也就无需设置该标志。

4 与改变了的SP_DEVINSTALL_PARAMS结构一起调用 SetupDiSetDeviceInstallParams 来设置新参数。

在安装程序设置了新参数之后，Setup或Device Manager发送一个DIF_PROPERTYCHANGE请求。

当用户点击Cancel时，属性表发送一个PSN_RESET通知信息。与该信息相对应，对话框过程应删除由用户做出的任何更改。

参见Platform SDK文档中有关其他通知(可能会遇到这样的过程以及它如何处理它们)的更多内容。


设备安装疑难解答

为了证实设备是正确安装的，则应运行测试程序来操作设备并利用Device Manager观察设备的系统信息。

为了启动Device Manager，在My Computer图标上右击，从菜单上选择Manage，再在随后显示列出的System Tools中选择Device Manager。Device Manager显示每个设备的信息，这其中包括了设备类型、设备状态、制造商、特定设备属性及设备驱动程序信息。从Device Manager在线帮助可以了解更多的内容。

当测试新PnP设备安装时，使Device Manager列表隐藏在设备之后是有用的。如要了解更多的信息，可参看Device Manager中的Displaying Hidden Devices。

如果你的设备是一个引导程序设备，则设备安装有可能会出现一个阻止机器引导的问题。在这种情况下，就需要利用内核调试程序对设备安装进行故障检修。如想了解更多的内容，可以参看与Microsoft调试程序一起提供的在线文件，但它与这个DDK不在一起。

如果你的设备不要求来引导机器，就特别要注意设备安装的一个问题：设备并没有正常工作而且设备被Device Manager中的一个黄色的警告点（黄色的感叹号）标记出来。典型的安装问题包括：

l “没有安装该设备的驱动程序。（代码28）”或“设备未配置好。”

这些Device Manager错误表明Setup没有找到与设备相匹配的INF，或是虽然找到了一个INF，但却无法成功安装设备。此时可以考虑在你的INF文件上运行ChkINF并查看Setup API日志。(参见利用 SetupAPI登录。)

l “这个设备没有正确配置。（代码00）”

这个Device Manager错误尤指用户删除了Found New Hardware向导。

l “本设备由于Windows未能装载设备所需的驱动程序而不能正常工作。(代码31)”

这个Device Manager错误表示Setup找到了一个与设备匹配的INF也很可能找到了驱动程序，但或是由于你的INF没有列出所有所需复制的文件，因此设备在 DriverEntry 中失败。或是由于驱动程序在其AddDevice例程中失败。观察驱动程序项及装载到设备中的驱动程序的事件日志及Setup API日志。可能还需要与内核调试程序相连以调试失败的驱动程序。

l “这个设备无法找到足够多可用的自由资源。(代码12)。如果要使用该设备，则需使本系统中另一个设备不工作。”

这个Device Manager错误表示Setup找到了与设备匹配的INF并装载了恰当的驱动程序，但是它无法给设备分配所需的资源。

用户可以单击设备Device Manager属性表General标签上的Troubleshooter按钮。这个故障检修应帮助用户给设备找出资源并停止无用设备的工作，这样新的设备才能运行。

l “这个设备不能启动。(代码10)”

这个Device Manager错误表示Setup找到一个与设备匹配的INF，并已装载了恰当的驱动程序，分配了资源(如果有的话)，并发出IRP_MN_START_DEVICE请求。但是，设备的一个驱动程序无法执行启动请求。观察相关项的事件日志。可能还需要与内核调试程序相连来调试失败的驱动程序。在调试程序中，设置IRP_MN_START_DEVICE调度例程中的断点。

l “设备无法使用。(代码22)”

有这个错误的设备在Device manager中标为红色的”X”，而不是黄色的感叹号。这个错误表示用户无法使用设备或Setup无法安装设备。

协同安装程序(或类安装程序)可以提供帮助用户对设备中的故障进行检修的故障检修程序。可参见《Windows 2000 Driver Development Reference》一书第一卷第3部分的第5章中的DIF_TROUBLESHOOTER了解更多知识。

1.1 使用SetupAPI记录日志

Windows 2000 Setup and Device Installer Services，即SetupAPI，包括了控制Setup及设备安装的Windows函数。当Setup运行时，Setup函数(SetupXxx函数)及device安装函数(SetupDiXxx函数)建立了一个提供对设备安装问题进行故障检修有用信息的日志文件。

在Windows 2000中，SetupAPI登录到文件%systemroot%\setupapi.log。日志文件是一个纯文本文件。如要重新设置日志，对该文件重命名或删除即可。

本节包括以下内容：

l 8.1.1 设置SetupAPI记录日志级别

l 8.1.2 解释SetupAPI日志文件示例
1.1.1 设置SetupAPI记录日志级别

如要改变写到SetupAPI日志中的信息级，在以下注册密钥中建立(或修改)值即可：

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersions\Setup

通过在这个密钥下设置值，可以选择被记录的错误级别，还可修改日志的冗余性，或关掉日志。还可以将信息记录到调试程序及日志文件。

该密钥下的SetuAPI记录日志的重要值是 LogLevel 和 AppLogLevels。SetupAPI对这两个值有如下的解释：

l LogLevel = LoggingFlags

这个DWORD值设置了为Setup和设备安装而被记录的错误级别。如果该值未被指定或为零，SetupAPI使用以下缺省行为：

l 对于Setup，记录所有的错误及一些警告。

l 对于设备安装，记录所有错误及所有警告。

LoggingFlags DWORD由3个部分组成：0xGGGGDDSS、下八位——由掩模0x000000FF表示，设置Setup的记录级别、下一高八位——由掩模0x0000FF00表示，设置设备安装的记录级别。最高位是一般标志。

Setup标志如下解释：

Setup标志 | 意义
0x00000000 | 使用缺省设置(当前为0x20)
0x00000001 | 关掉(没有Setup记录)
0x00000020 | 登录错误及警告
0x000000FF | 所有项上带有时间信息的冗余登录。该设置在设备安装测试时有用

设备安装标志解释如下：

设备标志 | 意义
0x00000000 | 使用缺省设置(当前为0x3000)
0x00000100 | 关掉(没有设备安装记录)
0x00003000 | 记录错误、警告及信息
0x0000FF00 | 所有项上带有时间信息的冗余记录。该设置在设备安装测试时有用

SetupAPI如下解释一般标志：

一般标志 | 意义
0x10000000 | 转送所有的上下文信息(尤其是未用的)
0x80000000 | 将输出发送到调试程序及日志文件中

例如，SetupAPI如下解释LoggingFlags例值：

l 0x00000000意味着缺省登录

l 0x0000FFFF意味着冗余记录

l 0x8000FF00意味着将冗余设备安装信息记录到日志文件及调试程序中

l AppLogLevels\<appname>=LoggingFlags

如果存在，这个DWORD值能够记录到每个应用程序基础上。这个 LoggingFlags 值与前面的 LogLevel 值相同。例如，AppLogLevels\rundll32.exe=0xFFFF能够对Rundll32冗余记录。

为了在Windows 2000清除安装时修改缺省的SetupAPI记录级，就在文本模式Setup和GUI模式Setup之间进行编辑。以下步骤描述了该过程。这些步骤假设正在安装D:\Winnt并在其他部分上有一个Windows 2000的工作构件。如下改变SetupAPI记录级：

1. 开始安装正在测试的清除构件。

2. 在文本模式Setup之后(即在GUI模式Setup之前)的第一次引导程序中停止设置过程。

3. 通过从启动菜单中选择启动进入工作构件来进行该操作。

4. 找到D:\Winnt\System32\config中的注册处(文件)。在该情况下，需要修改Software.sav中的注册处。

5. 运行Regedt32，选择“Local Machine上的HKEY_LOCAL_MACHINE”窗口，并选择HKEY_LOCAL_MACHINE键。再在Registry菜单上单击并选择Load Hive。

6. 浏览文件并选择D:\Winnt\System32\config\software.sav。当提示要输入密钥名时，输入”_sw.sav.”。

7. 打开HKEY_LOCAL_MACHINE下的_sw.sav密钥并找到如下密钥：HKEY_LOCAL_MACHINE\_sw.sav\Microsoft\Windows\CurrentVersion\Setup
打开Security下拉菜单，选择Permissions，并对所登录进的账号赋于全部的控制权（例如Administrator）。

8. 利用Edit->AddValue在这个密钥下添加必要的登录值。例如，添加“LogLevel=0xFFFF”以进行完全冗余登录。

9. 选择HKEY_LOCAL_MACHINE\_sw.sav，并从菜单中选择Registry->Unload Hive (_sw.sav应消失)。

10. 将D:\Winnt\System32\config\software.sav复制到D:\Winnt\System32\config\soft
ware中。

11. 重启并接着再进行Setup。

12. 为了证实该变化，在GUI模式Setup中按下SHIFT+F10，运行Regedt32，并检查登录级。
1.1.2 解释SetupAPI日志文件示例

以下的日志样本解释了包含在SetupAPI日志文件中的信息。

一般地，安装的所有部分都会一起显示在日志文件中。日志中的安装部分开始格式的一个项[year/month/day time process-id.instance description]，其中instance是保证两个部分被同时安装的一个数，这是因为相同的过程是唯一的。

在以下的日志文件样本中，缩进排列的部分是注释。日志样本是由冗余输出的 LogLevel=0xFFFF生成的：

[1999/10/15 15:14:25 376.1560 Driver Install]

SetupAPI lists the hardware compatible Ids of the device being installed.The first ID listed is the most exact match.Lines without a time stamp provide context for time-stamped entries.

Searching for hardware ID(s):acpi\pnp0200,*pnp0200

Enumerating files E:\WINNT\inf\*.inf

@15:14:25.637: Opened PNF of “E:\WINNT\inf\1394.inf”.

PNFs are preprocessed INF files.

:

Searching other INFs…

:

@15:14:26.688: Opened PNF of “E:\WINNT\inf\machine.inf”.

Setup found a match here,so it logs the INF in which it found the match,what PNP id it matched on,descriptive information.,and the install section.

@15:14:26.698: Found *PNP0200 in E:\WINNT\inf\machine.inf; Device: Direct memory

access controller ; Driver: Direct memory access controller;Provider: Microsoft; Mfg: (Standard system devices); Section :NO_DRV_X

@15:14:26.698: Decorated section name: NO_DRV_X

:

Setup searches other INFs

:

@15:14:31.575: Opened PNF of “E:\WINNT\inf\wordpad.inf”.

Setup has finished searching INFs.Next, it loads class coinstaller and class installers that are registered for this device’s setup class.

Loading co-installer modules for Direct memory access controller.

@15:14:31.615: Verifying file: E:\WINNT\System32\SysSetup.dll using key: SysSetup.Dll

in catalog: -.

@15:14:31.695: Obtained module “ E:\WINNT\System32\SysSetup.dll”procedure

“CriticalDeviceCoInstaller” for use.

:

Setup performs various device installation oprations before it sends the DIF_INSTALLDEVICEFILES request.

:

@15:14:32.026: Device install function : DIF_INSTALLDEVICEFILES .

Setup logs each device installation function code .

CoInstallers are numbered in the order that they are loaded.

@15:14:32.026: Executing co-installer 1 of 1.

@15:14:32.036: Completed co-installer 1 of 1.

In this example ,there is no class installer for this device’s setup class.You can tell ths because there is no entry for “Executing class-installer.”

@15:14:32.046: Completed class-installer.

Now Setup calls the default handler for this DIF request . If “Executing default installer” is listed before the class installer completes, that means the class installer called the default handler function directly.

@15:14:32.046: Executing default installer.

@15:14:32.056: Install Device : Begin.

Setup lists the devnode here.

@15:14:32.066: Doing copy-only install of ACPI\PNP0200\4&12BA42B2&0.

Setup lists the INF and decorated INF section being used.

@15:14:32.136: Installing section NO_DRV_X from e:\winnt\inf\machine.inf.

@15:14:32.146: Install Device: Queueing files from INF(s).

@15:14:32.156: Install Device: Queueing co-installer files from INF(s).

Processing coinstaller registeration section NO_DRV_X.CoInstallers.

@15:14:32.236: Verifying file: E:\WINNT\Certcals.inf using key: certclas.inf in catalog:-.

@15:14:32.346: Co-Installer Registered.

@15:14:32.356: Installing section NO_DRV_X.Interface from e:\winnt\inf\machine.inf.

@15:14:32.366: Interface installed.

@15:14:32.376: Device install finished successfully(ACPI\PNP0200\4&12BA42B2&0)

@15:14:32.386: Install Device : end.

@15:14:32.396: Completed default installer.

The install pass for copying files has finished successfully. Now carry on with the rest of the install operation for this device.

@15:14:32.457: Verifying file: e:\winnt\inf\machine.inf using key: machine.inf in catalog:-.

@15:14:32.497: Verifying file: e:\winnt\inf\layout.inf using key: layout.inf in catalog:-.

@15:14:32.557: Pruning Files: Verifying catalogs/infs.

@15:14:32.567: Pruning Files: Verifying catalogs/infs completed.

:

Setup performs various other device installation functions preceeding DIF_INSTALLDEVICE.

:

@15:14:33.498: Device install function: DIF_INSTALLDEVICE.

@15:14:33.498: Executing co-installer 1 of 1.

@15:14:33.518: Completed co-installer 1 of 1.

@15:14:33.528: Completed class-installer.

@15:14:33.538: Executing default installer.

@15:14:33.538: Install Device : Begin.

@15:14:33.548: Doing full install of ACPI\PNP0200\4&12BA42B2&0.

@15:14:33.628: Install Device: Changing registery settings specified by INF(s).

@15:14:33.638: Install Device: Writing driver specific registery settings.

@15:14:33.668: Install Device: Installing required windows services.

@15:14:33.678: Install Device: Writing drive descriptive registery settings.

@15:14:33.708: Install Device: Restarting Device.

@15:14:33.768: Install Device: Restarting Device completed.

@15:14:33.788: Device install finished successfully(ACPI\PNP0200\4&12BA42B2&0)

@15:14:33.798: Install Device : end.

@15:14:33.798: Completed default installer.

@15:14:33.808: Executing co-installer ( Post Processing).

@15:14:33.838: Completed co-installer 1 (Post Processing).

:

Setup performs various device installation functions after DIF_INSTALLDEVICE.

:

如要了解DIF代码的更多信息，参见《Windows 2000 Driver Development Reference》一书第1卷第5章“设备安装函数代码”。

1.2 显示Device Manager中的隐藏设备

Device Manager列出了安装在机器上的设备。缺省地，某些设备并未出现在列表中。这些“隐藏”的设备包括了devnode状态位DN_NO_SHOW_IN_DM设置的设备及作为设置类一部分的设备，该设置类在注册表中标为 NoDisplayClass (例如，打印机和非PnP驱动程序)。

如要在Device manager显示中包括了隐藏设备，单击View并选择Show Hidden Devices。

并没有缺省地显示在Device Manager中的设备的其他目录包括了从机器中被物理删除的设备，但是机器的注册表项并未被删除。这些设备可被认为是“不存在的”设备。用户无需看到这些设备，这是因为不存在的设备不需要被注意到且也不会带来什么问题。如果一个用户需要查看并未显示的设备，那么对于驱动程序的设计可能会有所影响。但在测试中，却有可能需要查看这些设备。为了看到未显示的设备，输入以下的MS-DOS命令：“set DEVMGR_SHOW_NONPRESENT_DEVICES=1.”(还可以利用系统属性表中的Advanced标签将该值设置到永久环境值中。)在看到这个值后，运行Device Manager并选择Show Hidden Devices。


安装一个要求重新启动机器的设备

在Windows 2000安装的文本模式设置阶段中，Windows 2000 Setup程序为要求重新启动机器的设备安装了驱动程序。这些驱动程序大多数都包含在操作系统中。用户可以在文本模式设置时通过按恰当的键及插入由第三方供应商提供的软盘来安装另一个驱动程序。第三方供应商提供放在软盘上的 txtsetup.oem 文件以保证用户能在文本模式设置时安装这样的驱动程序。

为了安装一个要求重新启动机器的设备，用户就要安装设备硬件、打开机器，并(如果未被包括在操作系统中)安装设备的驱动程序。当机器正在引导时，用户在设置装载阶段开始时按Fn键来安装另一个设备的驱动程序。在这个阶段中，系统显示与“设置正在检查计算机的硬件配置。”类似的信息。如是海量储存器，用户按下F6键。如是HAL，用户则按F5键。

当机器无法引导时就会出现某个错误信息，它指明找不到引导驱动程序。例如，如果机器无法引导并显示“无法引导设备”的信息，这有可能是引导程序磁盘是要求驱动程序的其他海量储存器设备并未被包含在操作系统中，如果机器无法引导且显示信息“设置无法决定你的机器类型”，这有可能是因为要求新的HAL驱动程序。在大多数机器上并不会出现这后一种错误；它多会出现在高端服务器上。

注意这个过程是用来安装一个并未被操作系统包含在“在盒子中”的驱动程序。不要使用这个过程来代替或更新操作系统中的驱动程序。如要替代或更新已包含的驱动程序，可等到系统引导并利用Device Manager在设备上执行“更新驱动程序”操作。

如果供应商为在引导中所需的设备分配了一个驱动程序，并要求用户在文本模式设置时安装它，那么除了其他的驱动程序文件之外，该供应商所分配的磁盘必须包括一个名为 txtsetup.oem 的文件。txtsetup.oem 文件是一个包括了以下信息的文本文件：

l 由 txtsetup.oem 文件支持的硬件组件列表

l 由每个组件的分配盘复制的文件列表

l 注册密钥列表及用以建立每个组件的值列表

带有单个 txtsetup.oem 文件的单个分配盘可以支持多个硬件设备。这样的文件可以包括带有指定对每个硬件类型缺省选择的一个 Defaults 部分。该文件也可以包括每个硬件类型及与 Files.Hwcomponent.ID 及 Config.DriverKey 部分恰当相关的 HwComponent 项。

以下示例显示了PnP SCSI控制程序的 txtsetup.oem 文件样本：

[Disks]

d1 = “OEM SCSI Driver disk”,\disk1.tag,\

[Defaults]

SCSI = oemscsi

[SCSI]

oemscsi = “OEM Fast SCSI Controller”

[Files.SCSI.oemscsi]

driver = d1,oemfs2.sys,OEMSCSI

inf = d1,oemsetup.inf

dll = d1,oemdrv.dll

catalog = d1,oemdrv.cat

[Config.OEMSCSI]

value = “”,tag,REG_DWORD,5

value = parameters\PnpInterface,5,REG_DWORD,1

参见《Windows 2000 驱动程序开发参考》一书第一卷第9章第3节中有关 txtsetup.oem 文件格式的详细内容。


Windows 2000和WDM驱动程序

本章叙述了什么是Windows 2000内核模式驱动程序，介绍了一些重要的驱动程序名词，并总结了内核模式驱动程序的设计目标，解释为了满足这些设计目标，Windows 2000（和WDM）的内核模式驱动程序如何不同于其他操作系统的驱动程序。

本章内容组织如下：

1.1 Windows 2000组件一览

1.2 Windows 2000中的驱动程序种类

1.3 Windows 2000和WDM驱动程序设计目标

1.3.1移植性

1.3.2可配置性

1.3.3永远抢占优先和永远中断

1.3.4多处理器安全

1.3.5基于对象

1.3.6带有可复用IRP的包驱动I/O

1.3.7支持异步I/O

关于Windows NT/Windows 2000操作系统环境的更多细节，可以参看David Solomon的《Inside Windows NT》（微软出版社，1998）。

1.1 Windows 2000组件一览

图1.1显示了Microsoft Windows 2000操作系统环境的主要组件。

图1.1 Windows 2000组件一览

在Windows 2000操作系统环境中，一部分组件运行在用户模式下，其他的则运行在内核模式下。文件系统、中间层和最低层驱动程序被显示在图的左下方，其中包含了内核模式I/O管理器。

如图1.1所示，Windows 2000操作系统包括了许多内核模式组件，它们被精心地定义为功能相互独立的组件。对内核模式驱动程序设计者来说，最感兴趣的就是内核I/O管理、即插即用管理（Plug and Play Manager）、电源管理（Power Manager）、硬件抽象层（Hardware Abstraction Layer，HAL）、配置管理（Configuration Manager）、存储管理（Memory Manager）、运行支持（Executive Support）和过程结构（Process Structure）组件。对另一些设计者来说，感兴趣的其他组件可能包括对象管理（Object Manager）和安全引用监控器（Security Reference Monitor）。

即插即用（PnP）管理和电源管理是Windows 2000中的新组件。他们仅仅支持Windows 2000驱动程序和WDM驱动程序。有关Windows 2000和WDM驱动程序使用这些Microsoft操作系统新特性的更详细资料，请参见《Windows 2000驱动程序开发参考》的第1卷《即插即用、电源管理和安装设计指南》。

1.2 Windows 2000中的驱动程序种类

在Windows 2000操作系统中，有两个基本的驱动程序种类：

§ 用户模式驱动程序（例如Win32 VDD，它是为MS-DOS应用程序开发的专用设备）或另一个被保护的子系统的驱动程序

用户模式驱动程序是与子系统细节相关的，我们不在这里讨论。更多VDD的细节，参考在线DDK中的“Virtual Device Drivers（虚拟设备驱动程序）”文档。

§ 针对逻辑、虚拟或物理设备的内核模式驱动程序

这些驱动程序作为Windows NT执行体部分来运行：Windows NT是基础的、基于微内核的操作系统，它支持一个或更多的保护子系统。

一些Windows 2000内核驱动程序也是WDM驱动程序，它们符合Windows驱动程序模型（WDM）。所有的WDM驱动程序是PnP驱动程序，并支持电源管理。WDM驱动程序对Microsoft Windows 98和Windows 2000操作系统来说，是源代码兼容的（不是二进制兼容的）。

在这里，术语“Windows 2000驱动程序”通常指代任何运行于Windows 2000操作系统之上的内核模式驱动程序（是WDM或不是）。必要的时候，本书会特别指出哪些是WDM驱动程序，哪些只是Windows 2000驱动程序。名词“早期驱动程序”指的是那些为Windows NT以前版本写的驱动程序，它们是不支持PnP的。

像操作系统自己，内核模式驱动程序是依据仔细定义的需求功能集，作为单独的、模块化的组件来实现的。

所有的Windows 2000内核模式驱动程序，包括WDM驱动程序，都包含一组系统定义的标准驱动程序例程和一些依赖于个别设备需求的内部例程。

图1.2 内核模式驱动程序类型

如图1.2所示，有三种基本类型的内核模式驱动程序。每一种都有稍微不同的结构和完全不同的功能：

1. 最高层驱动程序，例如系统支持的FAT、NTFS和CDFS文件系统驱动程序（FSD）。最高层驱动程序通常依赖于较低层的驱动程序支持。

虽然特定的文件系统驱动程序可能、或不可能从一个或多个中间层驱动程序获得支持，但是每个文件系统驱动程序最终依赖一个或多个下层外围设备（可能是PnP硬件总线）驱动程序的支持。

2. 中间层驱动程序，例如虚拟磁盘、镜像、或指定设备类型的类驱动程序。中间层驱动程序也需要较低层驱动程序的支持。

PnP功能驱动程序是中间层驱动程序，它们在一个I/O总线上控制特定的外围设备，这个I/O总线是由PnP硬件总线驱动程序来控制的。同样还有PnP过滤器驱动程序（PnP filter driver），它们在驱动程序栈（driver stack）内将它们自己插入到PnP功能驱动程序中去，以支持任何特定的外围设备。PnP功能和过滤器驱动程序的子集也是WDM功能和过滤器驱动程序。

任何系统支持的向外输出一个系统定义的WDM类/微类接口的类驱动程序，实际上是一个带有一个或多个链接的WDM微类驱动程序（有时称为WDM微驱动程序）的中间层驱动程序。每一对相互链接的WDM类/微类，提供与WDM功能驱动程序或WDM“软件”总线驱动程序相同的功能。

PnP软件总线驱动程序表示一组子设备，较高层的类、功能和/或过滤器驱动程序能连接它们自己到这些子设备上，PnP软件总线驱动程序也是中间层驱动程序。例如，控制多功能适配器的驱动程序是一个PnP软件总线驱动程序，这样的多功能适配器是用于不同的设备的一个便携集合。根据便携设备的特性和它的驱动程序使用的接口，这样的驱动程序也可能是WDM软件总线驱动程序。它似乎使得较高层驱动程序成为一个PnP/WDM总线驱动程序。对于操作系统来说，驱动程序是以PnP/WDM驱动程序的面目出现的，它服务于它自己的适配器，将它自己插入底层PnP硬件总线驱动程序之上的驱动程序堆栈中。

3. 最低层驱动程序，例如PnP硬件总线驱动程序，它控制一个I/O总线，这个总线连了一些外围设备在上面。最低层驱动程序不依赖于较低层驱动程序，而是控制物理外围设备，例如总线。

早期的NT设备驱动程序直接地控制物理外围设备，它们是最低层的驱动程序，同样的驱动程序例如SCSI HBA（Host Bus Adapter，宿主总线适配器）驱动程序。

PnP硬件总线驱动程序是系统提供的，并且通常动态控制可配置的I/O总线。这个驱动程序通过与即插即用管理协作，为所有连到它所控制的I/O总线上的子设备，配置和重新配置系统硬件资源（例如设备存储的映射和IRQ）。对其I/O总线来说，PnP硬件总线驱动程序包含了一部分Windows NT早期版本中由HAL组件提供的功能。

Windows 2000网络驱动程序也能被划分为上述的基本类型之一。例如，NT服务器或转向器是一个特定的文件系统驱动程序；传输堆栈中的任何驱动程序都是中间层驱动程序；物理网卡（有时称为网络接口控制器或NIC）驱动程序则是最低层设备驱动程序。当然，Windows 2000为网络设备提供了明确的接口和支持，例如NDIS（Network Device Interface Specification，网络设备接口规范）库在网络传输堆栈的下界提供了NIC驱动程序和中间层驱动程序。

本书为文件系统和网络驱动程序设计者提供了一些有用的综述和背景信息，对于内核模式设备和中间层驱动程序，以及包括WDM驱动程序的设计者来说，本书主要是一个设计指南。网络中间层和设备驱动程序的设计者也应该参考在线DDK中的“Network Driver Design Guide”和“Reference”。文件系统驱动程序设计者和最高层网络驱动程序设计者应当参考IFS工具包。

1.3 Windows 2000和WDM驱动程序的设计目标

内核模式驱动程序与很多Windows 2000的设计目标相符合，特别是系统I/O管理器部分。这些设计目标包括：

§ 平台之间的移植性

§ 硬件和软件的可配置性

§ 永远抢占优先和永远中断

§ 多处理器平台上的多处理器安全

§ 基于对象

§ 带可重用I/O请求包（IRP）的包驱动I/O

§ 异步I/O支持

后面的小节中将向所有的Windows 2000内核模式和WDM驱动程序设计者介绍这些系统设计目标。
1.3.1 移植性

Windows 2000输出两个系统提供的运行时库，它们包含了以Rtl为前缀的函数。内核模式驱动程序可以使用相同的内核模式RtlXxx例程作为执行组件，但是不能调用用户模式RtlXxx例程。

每个NT执行组件输出一组内核模式支持的例程，驱动程序和所有其他的内核模式组件可以调用这些例程。如果支持例程的底层实现随着时间的推移发生了变化，它的调用者依然可以保持不变，因为定义组件的接口没有发生变化。

大多数Windows 2000组件完全是用C写成的，仅仅少部分的HAL和内核是用汇编语言写成的。所以操作系统对于硬件平台来说是容易移植的。内核模式驱动程序应该也使用C来写，从而使得它能被系统兼容的C编译器重新编译、链接，并且运行在不同的Windows 2000平台上。如果是一个WDM驱动程序，不用重新书写驱动程序部分的代码，也不用替换驱动程序中的模块，就可以在Windows 98平台上运行。

如果相同的特性不能保证被其他系统兼容的编译器所支持，驱动程序则不应该依赖于任何特定系统兼容的C编译器或C支持库的特性。通常，驱动程序代码应该符合ANSI C标准，要避免使用任何这个标准中描述为“实现定义的”的部分。

特别地，编写可移植的Windows 2000驱动程序，最好避免以下的情况：

§ 对数据类型的依赖，因为它可能随着平台的不同而在大小和表现形式上有所不同

§ 调用任何保持状态的标准C运行时库函数

§ 调用任何标准C运行时库函数，而操作系统已经提供了一个可以替换被调用函数的支持例程

DDK提供了一组包含文件，这些文件中定义了系统特定的数据类型和常数，驱动程序（以及所有其他内核模式组件）可以使用它们来保证平台之间的可移植性。大多数驱动程序包含了主DDK内核模式的包含文件之一，这些文件是：ntddk.h和wdm.h。这些主包含文件不仅收录了系统提供的头文件，它们定义了基本的内核模式类型；而且，还收录了针对不同处理器构架特定的头文件的合适选择，从而驱动程序能被使用相应的编译器指令编译。

对于WDM驱动程序，DDK提供了一组包含文件，它们输出Windows 2000内核模式支持例程、宏、常数以及类型的一个子集。WDM驱动程序使用主DDK包含文件wdm.h而不是ntddk.h。包含wdm.h以及只使用它所输出的支持例程、宏、常数以及类型，使得WDM驱动程序可以象在Windows 2000上一样在Windows 98上编译运行。

如果驱动程序需要与平台有关的定义，最好是将这些定义用#ifdef语句单独分割开，这样就能使驱动程序针对合适的硬件平台被编译和连接。当然，使用DDK主头文件提供的支持例程、宏、常数和类型，你几乎可以避免实现任何与平台有关的条件编译代码。

某些驱动程序包含系统提供的其他头文件来确保Windows平台上的可移植性，特别是，SCSI、NDIS、以及视频微端口驱动程序。关于这些类型的驱动程序的头文件的详细信息分别参看第3部分，“存储驱动程序”；以及在线DDK中的“Graphics Driver Design Guide（图形驱动程序设计指南）”和“Reference（参考）”， “Network Driver Guide （网络驱动程序设计指南）”和“Reference（参考）”。
1.3.2 可配置性

因为Windows NT/Windows 2000是一个可移植的操作系统，所以，设备及其驱动程序必须是硬件可配置和软件可配置的。

例如，在一个热插拔的RAID（廉价磁盘冗余阵列）配置中，用户可以在运行的时候替换磁盘。这样的设备被称为是硬件可配置的。它的驱动程序不能假定给定的磁盘设备会在某个特定的位置固定。因此，为了保持可移植性，它的驱动程序一定不能包含硬编码的与机器有关的值。

这类磁盘设备的驱动程序可以在任何给定的机器上对一个或多个文件系统提供硬件级支持，当然这取决于用户如何对这些磁盘分区。另外，中间层驱动程序能为高层文件系统驱动程序提供灾难恢复（镜像分区、带集、和/或卷集）支持，这样的机器通常具有充足的存储能力。换句话说，同样的物理磁盘驱动程序能为一些机器提供对多种高层驱动程序的支持，而对其他的机器则不能。这样的磁盘驱动程序是软件可配置的，它们都是中间层和文件系统驱动程序，并且它们最终依赖于底层设备和/或即插即用（PnP）硬件总线驱动程序。

Windows NT/2000 HAL组件被实现成一个动态连接库，它负责所有的硬件层、平台特定支持，包括内核模式驱动程序在内，系统中的其他任何组件都需要它的支持。HAL输出的例程提供了平台硬件与包括所有驱动程序在内的系统软件组件之间的一个接口，这些例程隐藏了平台特定的硬件细节，例如高速缓存、I/O总线、中断等等。

系统支持的PnP硬件总线驱动程序，在PnP管理器的合作下对HAL组件提供了类似的功能。这就是说，PnP管理器与每一个PnP硬件总线驱动程序合作，提供特定类型I/O总线的平台硬件和系统软件之间的接口。

PnP管理器创建一个设备树，它的节点代表系统中所有的设备，包括总线和连在总线上的设备。对于每一个设备，PnP管理器维护两个列表：一个是设备可以使用的硬件资源表，另一个是设备实际占用的硬件资源表。设备驱动程序辅助PnP管理器创建这些列表，这些列表被保存在注册表中。一旦在系统中增添或删除设备，PnP管理器有必要重新分配资源，并更新列表。
1.3.3 永远抢占优先和永远中断

内核组件确定什么时候依照下列优先级调度标准之一来运行一段特定的代码：

§ 针对线程的内核定义的运行时优先级

系统中每一个线程都被分配了一个优先级属性。通常，系统中的大多数线程拥有可变的优先级属性：它们是总是抢占优先的，并且当前在同一优先级上的线程被轮询调度。系统中的一些线程拥有实时优先属性：这些对时间要求苛刻的线程可以被拥有更高实时优先级的线程抢占，直到它们释放控制。

无论拥有什么优先级属性，系统中的任何线程在硬件和某种软件中断发生时被抢占。

§ 内核定义的中断请求级（IRQL），在给定平台上被赋予特定的中断向量

内核也区分硬件和软件中断的优先次序，使得一些内核模式代码可以在更高的IRQL上运行。从而使得这些代码，包括大多数驱动程序，比系统中所有的线程拥有更高的优先级。

一段内核模式代码在特定的IRQL上执行，这个IRQL定义了它的硬件优先级。内核模式代码是总是可中断的：一个带有更高IRQL值的中断可能在任何时候发生，从而导致拥有更高IRQL的另一段内核模式代码被处理器立即执行。换句话说，当一段代码在给定的IRQL上运行时，内核屏蔽微处理器上所有小于等于当前IRQL值的中断向量。

通常，线程运行在PASSIVW_LEVEL IRQL上：没有中断向量被屏蔽。软件中断被赋予相对较低的IRQL值（APC_LEVEL、DISPATCH_LEVEL、或针对内核调试WAKE_LEVEL）。设备中断拥有较高的IRQL值，内核则为系统关键的中断保留最高的IRQL值，例如系统时钟或总线错误中断。

一些系统支持例程运行在IRQL_PASSIVE_LEVEL，原因有两个方面：一是因为一些内核模式组件创建它们自己的线程，另一是因为一些支持例程被作为可分页代码实现，并且/或者访问可分页数据。

同样的，某种标准驱动程序例程通常运行于IRQL_PASSIVE_LEVEL。然而，几个标准的驱动程序例程要么运行于IRQL DISPATCH_LEVEL，要么运行于设备IRQL（也称为DIRQL），后一种是针对最低层的驱动程序来说的。参看《内核模式驱动程序设计指南》的第16章中的“管理硬件优先级”，可以得到关于IRQL的细节。

通常，如果线程正在请求驱动程序的当前I/O操作，则在这些线程的环境中，仅仅最高层的驱动程序被调用。如果线程已经请求了它的当前I/O操作，中间层或最低层驱动程序从不假定它正在这些线程的环境中执行。

由于性能的原因（避免环境交换），几乎没有驱动程序创建它们自己的线程。因此，当一个标准的驱动程序例程被调用做某些工作时，驱动程序例程通常恰恰在当前线程环境中执行。也就是说，它们在专用线程环境中执行。

任何内核模式例程都运行在比PASSIVE_LEVEL高的IRQL上，这些例程拥有比系统中所有线程都高的优先级。驱动程序中的任何例程都是可中断的：运行在特定IRQL上的任何内核模式例程一直保留处理器的控制权，除非例程运行时有更高的内核指定的IRQL发生。

甚至一个最底层的驱动程序中断服务例程（ISR）也能被其他运行在较高IRQL上的例程中断（例如，被另一个驱动程序的ISR中断）。不像一些旧的PC操作系统中的驱动程序，Windows NT/Windows 2000驱动程序的ISR几乎不做任何驱动程序的I/O处理，因为驱动程序的ISR一直到它返回，也不必保留它正在其上运行的CPU的控制。

相反，最低层的驱动程序必须在较低的IRQL上完成它的I/O操作的大部分，而不是它的ISR的DIRQL。为了获得好的系统整体性能，所有的运行在高级IRQL上的内核模式例程必须迅速释放CPU的控制权。这样的例程不仅仅做它们在高级IRQL上必须作的，通常将一个延迟过程调用（DPC）排队，以完成任何能在较低IRQL（DISPATCH_LEVEL）上完成的操作。

操作系统的可中断、可抢占优先的设计目标是最大化平均性能。任何驱动程序的ISR能被运行在较高IRQL上的例程中断，并且任何线程都能被具有较高优先级的线程中断。虽然一些线程拥有实时优先级属性，它们仍然能被具有更高优先级的线程抢占。然而Windows NT/Windows 2000构架并没有提供一个真正的实时系统。

如果想得到内核模式驱动程序中系统定义的标准例程的介绍，可以参看第2章，“分层的I/O、IRP和I/O对象”。参看第4章，“基本驱动程序结构”可以得到这些例程的概述。
1.3.4 多处理器安全

在任何Windows NT/Windows 2000多处理器平台中，需要满足下列条件：

§ 所有的CPU是相同的，并且它们要么都有协处理器，要么都没有。

§ 所有的CPU共享内存，并且一致地访问内存。

§ 在对称平台中，每一个CPU能访问内存、处理中断、访问I/O控制寄存器。（相对来说，在不对称多处理器机器中，一个CPU可能为一组从属CPU处理所有中断。）

Windows NT/Windows 2000被设计成一律运行在单一或对称多处理器平台上，内核模式的Windows 2000和WDM驱动程序应该被同样的设计。

为了能在对称多处理器（SMP）平台上安全运行，任何操作系统都必须解决这样一个问题：即，怎样保证正在某个处理器上运行的代码，不会同时访问和修改运行在另一个处理器上的代码正在访问和修改的数据。例如，正在一个处理器上处理设备中断的最低层驱动程序的ISR排斥其他对临界的、驱动程序定义的数据（或设备寄存器）的访问，以防它的设备同时在另一个处理器上中断。

此外，在单一处理器的机器中被序列化的驱动程序I/O操作在SMP机器中能被同时执行。这就是说，当一个处理输入I/O操作的驱动程序例程在一个处理器上被执行的时候，另一个与设备通讯的例程能同时在另一个处理器上被执行。不论是运行在单处理器机器上，还是SMP机器上，都要求包括WDM驱动程序在内的所有内核模式驱动程序能同步访问系统定义的数据或系统提供给驱动程序例程共享的资源，并且在任何时候要同步对物理设备的访问。

NT内核组件输出了一个机制，被称为自旋锁（spin lock），对称多处理器平台中一个或多个正在运行的例程同时访问时，它被用来保护共享数据（或设备寄存器）。内核对自旋锁的应用使用了两个策略：

§ 在任何时刻，一个且仅一个例程能拥有一个特定的自旋锁；仅仅自旋锁的拥有者才能访问它所保护的数据。另一个例程必须取得自旋锁后，才能访问相同的数据，并且，除非自旋锁的当前拥有者主动释放了它，否则其他任何例程都不能得到它。

§ 象硬件和软件中断向量一样，内核为系统中的每一个自旋锁制定一个相关的IRQL值。内核模式例程只有运行在自旋锁指定的IRQL上时，才能到特定的自旋锁。

这些策略用来保护某个驱动程序例程被更高优先级的驱动程序例程所抢占，前者通常是在较低的IRQL上运行，但是现在却拥有某个自旋锁，后者则希望获取同样的自旋锁，这通常会造成死锁。

分配给某个自旋锁的IRQL通常是能获得这个自旋锁的最高层IRQL例程的IRQL。一个最低层驱动程序的ISR频繁地与驱动程序的DPC例程共享一个状态区，后者靠调用驱动程序提供的临界区例程来访问共享区域。在这个例子中，保护共享区域的自旋锁拥有与DIRQL相同的IRQL，设备中断通常发生在DIRQL。当临界区例程拥有自旋锁并且在DIRQL上访问共享区域，ISR不能在单处理器机器中运行，这是因为设备中断被屏蔽，正如前面提到的那样。在一个对程多处理器机器中，当临界区例程拥有自旋锁并且在DIRQL访问共享数据时，ISR还不能获得自旋锁以保护共享数据。

通过等待内核的调度者对象之一，一组内核模式线程能同步访问共享数据或资源。这些调度对象有：事件、互斥体、信号量、定时器、或另一个线程。然而，大多数驱动程序不创建它们自己的线程，因为当它们避免线程环境交换的同时，获得了更好的性能。对时间要求比较苛刻的内核模式支持例程和驱动程序无论何时运行在IRQL DISPATCH_LEVEL或DIRQL，都必须使用内核的自旋锁，从而来同步对共享数据和资源的访问。

想要得到更详细的信息，可以参看第16章“使用自旋锁和管理硬件优先级”，第3章“内核调度对象”。
1.3.5 基于对象

Windows NT/Windows 2000是基于对象的系统。在执行体中，各种各样的组件定义一个或多个对象类型。每个组件输出仅内核模式具有的支持例程，当这些例程被调用时，它们操纵它的对象类型的实例。没有组件被允许直接访问另一个组件的对象类型的任何实例。每个组件要想使用其他的组件对象，必须调用输出的支持例程。

对这些约定的严格遵守使得Window NT/Windows 2000具有移植性和伸缩性。例如，操作系统将来的某个版本能包含一个完全或部分被重写的内核组件，这个组件定义了相同的对象类型，但是可能使用了完全不同的内部结构，却可以输出一组具有与现存组件所输出的例程相同名字和参数的支持例程。这个假定的被重写的内核版本将对现存系统中任何其他的执行组件的移植性没有任何影响。换句话说，操作系统组件没有使用后门通讯，并且驱动程序也必须避免这种使用，以保持系统的移植性和可配置性。

与操作系统相似，驱动程序及其设备也是基于对象的。包括用户模式代码在内，对于系统中所有其他的组件，对某个设备的连接被描述成I/O管理器的文件对象之一的打开操作。在I/O系统中，每一个驱动程序的逻辑、虚拟、和/或物理设备被描述为设备对象。在I/O管理器中，每一个驱动程序的加载映像被描述成一个驱动程序对象。I/O管理器为文件对象、设备对象和驱动程序对象定义对象类型。

像任何其他的可执行组件，驱动程序通过调用内核模式支持例程来使用对象，这些支持例程是由I/O管理器和其他系统组件输出的。内核模式支持例程通常拥有明确的名称，指明了每次操纵的特定对象和在这个对象上执行的操作。这些支持例程名称的形式如下：

PrefixOperationObject

这里

Prefix

指明了输出支持例程的内核模式组件，并且还通常指明了定义对象类型的组件。大多数前缀有两个字母。

Operation

描述对这个对象作什么。

Object

指明对象类型。

例如，IoCreateDevice，在设备初始化过程中每个内核模式驱动程序都要调用这个例程一次或多次，从名字可以看出，这个例程创建一个设备对象，以描述一个物理、逻辑、或虚拟设备，作为I/O请求的目标。

为了方便起见，一个系统组件能输出调用其他组件的支持例程。特别是，I/O管理器输出特定例程，以简化驱动程序开发。例如，IoConnectInterrupt是一个最低层的驱动程序调用，被用来注册它们的中断服务例程（ISR），它调用针对中断的内核的支持例程。

在线DDK中的术语表包括了一组系统对象的定义，它们对驱动程序开发者非常有用。关于对最低层和中间层驱动程序特别有用的支持例程的详细信息，参见《Windows 2000驱动程序开发参考》的卷1和卷2，以及在线DDK。
1.3.6 带有可复用IRP的包驱动I/O

I/O管理器的主要工作是接收I/O请求（通常来自用户模式应用程序）、创建IRP以描述它们、发送IRP到合适的驱动程序、跟踪它们直到它们被完成，还有返回状态到每个I/O操作的初始请求者。I/O管理器、即插即用管理器和电源管理器使用IRP与包括WDM驱动程序在内的内核模式驱动程序通信，并且允许驱动程序之间相互通讯。

注意，一些IRP可以被发送到多个驱动程序。例如，一个在某个磁碟上打开文件的请求可能首先到达文件系统驱动程序，通过中间层镜像驱动程序，最终到达磁盘驱动程序，可能是一个PnP硬件总线驱动程序。

因此，每个IRP有一个固定的部分和一个或多个驱动程序特定的I/O栈位置（stack location）：

§ 在固定部分（或头）中，I/O管理器维护与原始请求有关的信息，例如调用者的线程ID和参数，还有在其上被打开文件的设备对象的地址，等等。固定部分也包含一个I/O状态块，驱动程序在其中设置请求的I/O操作的状态信息。

§ 在最高层驱动程序的I/O栈位置中，I/O管理器、即插即用管理器、或电源管理器设置驱动程序特定的参数，例如由相应驱动程序请求的特定操作（描述成一个函数代码）和它使用的环境，以确定的它将要做些什么。如果需要的话，按照顺序，每一个较高层的驱动程序创建相邻的较低层驱动程序的I/O栈位置。

因为给定的IRP通过每个驱动程序的标准例程集被处理，每一个标准例程能在IRP中访问那个驱动程序的I/O栈位置，从而在驱动程序操作的每个阶段重用IRP。另外，更高层的驱动程序能创建（或重用）IRP以发送请求到低层的驱动程序。

参见第2章，“分层的I/O、IRP、和I/O对象”可以得到IRP通过分层的驱动程序的处理过程的介绍。如果希望得到关于IRP设备类型特定的信息，参看《Windows 2000驱动程序开发参考》的卷2；对于PnP和电源管理IRP，参看《即插即用、电源管理和安装设计指南》和《Windows 2000驱动程序开发参考》的卷1。
1.3.7 支持异步I/O

I/O管理器提供异步I/O支持，从而使得I/O请求的发出者（通常是用户模式应用程序，但有些时候是另一个驱动程序）可以继续执行，而不用等待它的I/O请求完成。提供异步I/O支持提高了系统整体的吞吐量，同样也提高了任何作出I/O请求的代码的性能。

结果之一是，内核模式驱动程序不必按照I/O请求被送往I/O管理器的顺序来处理它们。I/O管理器或更高层驱动程序能依据I/O请求被接受的顺序重新编程它们，或者能将一个大的数据传输请求拆分成几个较小的传输请求。此外，驱动程序能重叠I/O请求处理，特别是在对称多处理器平台中，就像1.3.4，“多处理器安全”一节中提到的那样。

还有，对内核模式驱动程序的单独I/O请求的处理不必要序列化。这就是说，在驱动程序开始处理下一个进来的I/O请求之前，它不必处理完每个IRP，就像在单处理器机器中，驱动程序在被写成支持同步I/O的单任务操作系统所作的那样。

IRP被传递给驱动程序的标准例程，通过完成任何例程指定的操作以最终满足当前请求，驱动程序响应当前的IRP。当然，最低层的驱动程序被要求帮助I/O管理器和任何高层的驱动程序处理同样的IRP，并通过设置IRP中的I/O状态块来跟踪每个请求的状态。

驱动程序也能在其设备对象的特定部分维护当前I/O操作的信息，这个部分被称为设备扩展。

若想得到IRP处理和设备对象如何表示驱动程序的物理和逻辑设备的概述，可以参看第2章“分层的I/O、IRP和I/O对象”。关于设备对象和设备扩展更详细的信息，参见第3章，“针对驱动程序系统定义的对象和支持”。


分层的I/O、IRP和I/O对象

本章介绍Microsoft Windows 2000的I/O模型，内容包括：内核模式驱动程序如何适合系统，驱动程序如何处理IRP，系统定义的标准驱动程序例程，通用设备配置和相应的分层驱动程序，以及描述设备和驱动程序的对象。

本章的组织如下：

2.1 Windows 2000 I/O模型概述

2.2 终端用户I/O请求和Windows 2000文件对象

2.2.1 用户 I/O请求的注意事项

2.3 IRP和驱动程序特定I/O栈位置

2.3.1 处理IRP的注意事项

2.4 驱动程序对象和标准驱动程序例程

2.4.1 对象不透明性

2.4.2 标准驱动程序对象入口点

2.4.3 其他的标准驱动程序例程

2.4.4 标准驱动程序例程的注意事项

2.5 设备配置和分层驱动程序

2.5.1 简单设备和驱动程序配置

2.5.2 添加驱动程序的注意事项

2.6 设备对象和分层的驱动程序

2.6.1 针对简单配置的设备对象

2.6.2 设备对象的注意事项

注意除了第一节，每节都包含了一个“注意事项”列表。这些小节总结对驱动程序的基本要求和限制，就像本节从整体上介绍一样。

1.1 Windows 2000 I/O模型概述

每一个操作系统都有一个隐含的或明确的I/O模型，以处理与外围设备之间的数据流。Microsoft Windows 2000 I/O模型的最大不同之处是它支持异步I/O，就如第1章中描述的那样。另外，I/O模型有下列特性：

§ I/O管理器提供一致的接口给所有的内核模式驱动程序，包括最低层、中间层和文件驱动程序。所有的对驱动程序的I/O请求被作为I/O请求包（IRP）发送。

§ I/O操作是分层的。I/O管理器输出I/O系统服务，用户模式保护子系统代表它们的应用程序和/或终端用户，调用它们完成I/O操作。I/O管理器解释这些调用，创建一个或多个IRP，并且通过可能的分层驱动程序发送它们到物理设备。

§ I/O管理器定义一组驱动程序支持的标准例程，一些是必需的，一些是可选的。所有的驱动程序遵从一个相对一致的实现模型，允许外围设备之间的差别，并且总线、功能、过滤器和文件系统驱动程序被要求的不同功能。

§ 就如操作系统本身，驱动程序是基于对象的。驱动程序、它们的设备和系统硬件被表示为对象。I/O管理器和其他操作系统组件输出内核模式支持例程，驱动程序通过操纵合适的对象调用这些例程来完成工作。

除了使用IRP传送传统的I/O请求之外，I/O管理器与PnP和电源管理器一起工作发送包含PnP和电源管理请求的IRP。如果希望了解驱动程序是如何操纵这些IRP的，可参看《安装、即插即用和电源管理设计指南》。

1.2 终端用户I/O请求和Windows 2000文件对象

内核模式驱动程序被Microsoft Windows 2000保护子系统向终端用户隐藏，这个子系统实现一个已经很友善的编程接口，例如Windows或POSIX。设备对用户模式代码是可见的，它包括保护子系统，仅仅作为命名的文件对象被I/O管理器控制。

图2.1说明了终端用户和I/O管理器之间的这种关系。

图2.1 代表文件、卷和设备的文件对象

Windows 2000保护子系统，例如Win32子系统，通过I/O系统服务传送I/O请求到合适的内核模式驱动程序。图2.1中显示的子系统依赖于显示、视频适配器、键盘和鼠标设备驱动程序的支持。

保护子系统隔离它的终端用户和应用程序，使得它们不必知道关于内核模式组件（包括驱动程序）的任何细节。同样，I/O管理器隔离保护子系统，使得它们不必知道关于机器特定的设备配置或驱动程序实现的任何细节。

I/O管理器的分层方法也使得大多数驱动程序不必知道任何下列的细节：

§ 是否一个I/O请求在任何特定的保护子系统中被生成，如Win32或POSIX

§ 是否一个给定的保护子系统拥有特定种类的用户模式驱动程序

§ 什么是保护子系统的I/O模型和什么是对驱动程序的接口

I/O管理器提供给驱动程序一个单独的I/O模型、一组驱动程序能用来完成I/O操作的内核模式支持例程，和I/O请求发出者与响应这个请求的驱动程序之间的一个一致接口。

如图2.1所示，子系统和它的本地应用程序仅仅通过I/O管理器提供的文件对象句柄，能访问驱动程序设备或海量存储设备上的文件。为了打开这样一个文件对象或获得一个句柄以实现与设备或数据文件的I/O，文件子系统使用一个请求调用I/O系统服务以打开一个命名的文件。这个命名的文件可以拥有一个子系统指定的别名（符号连接），以将文件对象联系到内核模式名字。

I/O管理器，输出这些系统服务，负责寻找或创建文件对象，这个文件对象用来描述设备或数据文件；I/O管理器还要寻找合适的驱动程序。图2.2描述了当子系统代表应用程序打开描述数据文件的文件对象时发生的事情。

1. 子系统调用一个I/O系统服务以打开一个命名文件。

2. I/O管理器调用对象管理器以寻找命名的文件，并帮助它决定到文件对象的符号连接。它也调用安全引用监视器以检查子系统是否拥有正确的权限打开那个文件对象。

3. 如果卷还没有被安装（mount），I/O管理器暂停打开请求，并且调用一个或多个Windows 2000文件系统，直到它们中间之一认可这个文件对象存储在文件系统使用的海量存储设备之一上面。当文件系统已经安装了卷，I/O管理器继续处理这个请求。

4. I/O管理器为这个打开请求分配内存并初始化一个IRP。对于驱动程序来说，打开等同于一个“创建”请求。

5. I/O管理器调用文件系统驱动程序，传递它给IRP。文件系统驱动程序访问它的在IRP中的I/O栈位置，以确定什么操作必须被完成、检查参数、确定是否请求的文件在缓存中，并且如果没有的话，在IRP中设置相邻的较低层的驱动程序的I/O栈位置。

6. 驱动程序处理IRP，并完成请求的I/O操作，调用I/O管理器和其他系统组件（图2.2种没有显示）提供的内核模式支持例程。

7. 驱动程序将IRP连同I/O状态块集返回到I/O管理器，以指明请求的操作是成功被执行了，还是失败了。

8. I/O管理器从IRP获得I/O状态，所以它能通过保护子系统返回状态信息到原始调用者。

9. I/O管理器释放完成的IRP。

10. 如果打开操作成功，I/O管理器返回文件对象的句柄到子系统。如果出现错误，它返回合适的状态到子系统。

子系统成功打开一个描述数据文件、设备或卷的文件对象后，它使用返回的句柄为设备I/O操作（通常是读、写、或设备I/O控制请求）在后续的请求中指明文件对象。为了创建这样的请求，子系统调用I/O系统设备。I/O管理器将这些请求作为IRP，发送它们到合适的驱动程序。
1.2.1 用户I/O请求的注意事项

当设计一个内核模式的驱动程序时，切记以下几点：

§ 驱动程序可以被分层，并且多个驱动程序能处理单独的I/O请求（IRP）。

§ 驱动程序不能做任何有关其他驱动程序将会在设备栈位置中的假设。因此，每个驱动程序应该被准备用于接收来自任何其他驱动程序的请求，并能处理所有潜在的错误。

§ 驱动程序在IRP的I/O状态块中获得请求的I/O操作成功或失败的消息。I/O管理器将请求的I/O操作成功或失败的消息发送给用户模式的请求者。

§ 驱动程序不必，也不应该被设计成能提供应用程序特定的支持。保护子系统或它的子系统特定的用户模式驱动程序提供这种支持。对这条规则有一个例外：依赖于一个应用专用设备的MS-DOS应用程序能请求Windows 2000驱动程序以控制这个设备，还能请求一个紧密结合的Wind32用户模式的虚拟设备驱动程序（VDD）。有关VDD的详细信息参看在线DDK中的“Virtual Device Drivers”（虚拟设备驱动程序）文档。

1.3 IRP和驱动程序指定的I/O栈位置

图2.2 带有两个I/O栈位置的IRP，但实际上一个IRP可以拥有任意多个I/O栈位置，这依赖于有多少分层的驱动程序处理给定的请求。

图2.3 在分层的驱动程序中处理IRP

图2.3更详细的说明了图2.2中的驱动程序是怎样使用I/O支持例程（IoXxx）为一个读/写请求处理IRP。

1. I/O管理器用一个IRP调用文件系统驱动程序（FSD），这个IRP是为子系统读/写请求分配的。FSD访问IRP中它的I/O栈位置，以决定它完成什么操作。

2. FSD能将原始请求分成更小的请求（可能是针对几个设备驱动程序），这项工作是通过调用一个I/O支持例程（IoAllocateIrp）一次或多次以分配额外的IRP。这些额外的IRP连同为更低层驱动程序创建的I/O栈位置（被填充为全零）被返回给FSD。由它随意决定，FSD能在原始IRP中安装相邻的较低层驱动程序的I/O栈位置，并传递它给更低层的驱动程序，通过这种方法FSD能重用原来的IRP，而不是象图2.3种所示的那样分配额外的IRP。

3. 对于每个驱动程序分配的IRP，图2.3中的FSD调用一个I/O支持例程以注册一个FSD支持的完成例程。在这个完成例程中，FSD能确定低层驱动程序是否满足请求，并能在低层驱动程序完成任务后释放每个驱动程序分配的IRP。不论每个驱动程序分配的IRP是被成功完成，还是返回一个错误状态，或者是被放弃，I/O管理器都将调用FSD支持的完成例程。更高层驱动程序负责释放IRP，这个IRP是它根据自己的需求为较低层驱动程序分配并安装的。I/O管理器在所有驱动程序完成任务后释放它分配的IRP。

下一步，FSD调用一个I/O支持例程（IoGetIrpStackLocation）访问相邻的较低层的驱动程序的I/O栈位置，从而为临近低层驱动程序设置请求。（图2.3种，相邻的较低层驱动程序碰巧是最低层的驱动程序。）FSD然后调用I/O支持例程（IoCallDriver）将那个IRP传递给相邻的较低层驱动程序。

4. 当最低层驱动程序被使用IRP调用时，它检查它的I/O栈位置以确定它将在目标设备上完成什么操作（通过IRP_MJ_XXX函数代码指定）。目标设备在它的被指定的I/O栈位置中被设备对象表示，并且连带IRP被传送给驱动程序。最低层驱动程序假定I/O管理器已经发送IRP到一个入口点，这个入口点是驱动程序为IRP_MJ_XXX（这里是IRP_MJ_READ或IRP_MJ_WRITE）操作定义的，并且假定高层驱动程序已经为请求检查过其他参数的有效性。

如果没有较高层驱动程序，最低层驱动程序将检查IRP_MJ_XXX的输入参数是否是有效的。如果它们是有效的，驱动程序通常调用I/O支持例程以通知I/O管理器设备操作正在IRP上等待决定，并且要么将IRP放入队列，要么传递它给另一个驱动程序提供的例程，这些例程可以访问目标设备（这里是物理设备或逻辑设备：磁盘或磁盘上的一个分区）。

5. I/O管理器确定目标设备的驱动程序是否正在处理另一个IRP，如果是这样的话，将IRP放入队列，并返回。否则，I/O管理器发送IRP到驱动程序提供的例程，这个例程在它的设备上开始I/O操作。（在这个阶段，图2.3中的驱动程序和I/O管理器返回控制。）

6. 当设备中断时，驱动程序的中断服务例程（ISR）所作的仅仅是阻止设备中断，并保存必要的操作环境。然后，ISR用IRP调用I/O支持例程（IoRequestDpc），等待一个驱动程序提供的DPC（延迟过程调用）例程以在比ISR更低的硬件优先级上完成请求的操作。

7. 当驱动程序的DPC获得控制，它使用环境（在ISR的调用中被传递给IoRequestDpc）以完成I/O操作。DPC调用支持例程将下一个IRP（如果有）取出队列，然后传递这个IRP到驱动程序提供的例程上，这个例程在设备上开始I/O操作（见第5步）。DPC然后在IRP的I/O状态块中设置刚刚完成的操作的状态，并使用IoCompleteRequest将它返回给I/O管理器。

8. I/O管理器将IRP中最低层的驱动程序的I/O栈位置赋零，并用FSD分配的IRP调用文件系统的已注册的完成例程（见第3步）。这个完成例程检查I/O状态块，以确定是否重试请求或更新任何保存原始请求的内部状态，以及是否释放它的驱动程序分配的IRP。文件系统能收集所有驱动程序分配的发送给低层驱动程序的IRP的状态信息，所以它能设置I/O状态，并能完成原始IRP。当文件系统完成原始IRP，I/O管理器返回Windows NT状态给I/O操作的原始请求者（子系统的本地函数）。

图2.3也显示了原始IRP中的两个I/O栈位置，因为它显示了两个驱动程序，一个文件系统驱动程序和一个海量存储设备驱动程序。I/O管理器给分层的驱动程序链中的每个驱动程序一个它自己的I/O栈位置，这个栈位置是在它安装的每个IRP中的。图2.3中，驱动程序分配的IRP没有FSD创建的栈位置。任何分配IRP给低层驱动程序的高层驱动程序通过设置相邻的较低层驱动程序的设备对象的StackSize值，来决定新IRP拥有多少I/O栈位置。

图2.4更详细描述了IRP环境。

图2.4 IRP中I/O栈位置的环境

如图2.4所示，IRP中每个驱动程序指定的I/O栈位置包含了下列通用的信息：

§ 主功能代码（IRP_MJ_XXX），指定驱动程序应该完成的基本操作

§ 对于一些被FSD操纵的主功能代码、更高层SCSI驱动程序、和所有的PnP驱动程序，一个次功能代码（IRP_MN_XXX），指明驱动程序应该完成的基本操作的子项。

§ 一组操作特定的参数，例如驱动程序用来传递数据的缓冲区的大小和起始位置。

§ 指向驱动程序创建的设备对象的指针，为请求操作描述目标设备（物理的、逻辑的或虚拟的）

§ 指向文件对象的指针，描述打开的文件、设备、目录或卷

文件系统驱动程序通过IRP中它的I/O栈位置访问文件对象。其他的驱动程序通常忽略这个文件对象。

特定驱动程序操作的IRP主要和次功能代码集可以是设备类型特定的。当然，更低层驱动程序（包括PnP函数和过滤器驱动程序）和中间层驱动程序通常操作下列基本请求集：

§ IRP_MJ_CREATE——打开目标设备对象，指明它对I/O操作是现存的和可用的；

§ IRP_MJ_READ——从设备传输数据；

§ IRP_MJ_WRITE——传输数据到设备；

§ IRP_MJ_DEVICE_CONTROL——设置（或重设）设备，通过系统定义的、设备类型特定的I/O控制代码（IOCTL）；

§ IRP_MJ_CLOSE——关闭目标设备对象

§ IRP_MJ_PNP——在设备上执行即插即用操作，IRP_MJ_PNP请求由PnP管理器通过I/O管理器发送；

§ IRP_MJ_POWER——在设备上执行电源操作，IRP_MJ_POWER请求被电源管理器通过I/O管理器发送。

特定种类设备的驱动程序被请求操作的主IRP函数代码和设备I/O控制代码的详细信息，可以参见《Windows 2000驱动程序开发参考》的卷1和卷2。

通常，I/O管理器至少使用两个I/O栈位置发送IRP给海量存储设备驱动程序，因为文件系统在海量存储设备的其他驱动程序上被分层。I/O使用单个栈位置发送IRP给那些其上没有其他分层驱动程序的驱动程序。

当然，I/O管理器为增添新驱动程序到系统内任何现存驱动程序链中提供支持。例如，一个在给定磁盘分区上完成数据备份的中间层镜像驱动程序可以被插入到一对驱动程序之间，例如图2.3中所示的文件系统驱动程序和最低层驱动程序。当这个新驱动程序将它自己连到（attach）设备栈，I/O管理器调整所有IRP中的I/O栈空间数目，它发送这些IRP给文件系统、镜像和最低层驱动程序。图2.3中文件系统分配的每个IRP也包含另一个针对新镜像驱动程序的I/O栈空间。

注意，在任何特定驱动程序对IRP中I/O栈空间的访问方面，这个对增添新设备到现存链的支持包含某种约束：

§ 分层的驱动程序链内的更高层驱动程序能安全的访问任何IRP中它自己的和相邻较低层驱动程序的I/O栈位置。这样的驱动程序必须在IRP中为相邻的较低层驱动程序创建I/O栈位置。然而，当设计这样一个较高层驱动程序时，你不能预知什么时候新驱动程序将被添加到现存的链中，并且恰好在你的驱动程序之下。

因此，你应该假定任何后来添加的驱动程序将处理IRP主功能代码（IRP_MJ_XXX），就像替换的相邻的较低层驱动程序作的那样。

§ 分层的驱动程序链内的最低层驱动程序能在任何IRP中安全地访问仅仅它自己的I/O栈位置。当设计这样的驱动程序时，你不能预见什么时候（或者是否）新驱动程序将被添加到现存链中你的驱动程序上面。

在设计最低层驱动程序期间，要假定驱动程序能使用传递到它自己的I/O栈位置的信息继续处理IRP，而不管给定IRP的开始源，以及很多驱动程序如何位于它之上。

与图2.3中所示的文件系统驱动程序类似，被添加到现存驱动程序链的任何新驱动程序能作下列事情：

§ 设置它自己的完成例程到IRP中。IoCompletion例程检查I/O状态块，以决定较低的驱动程序是否成功完成IRP、取消IRP、和/或带有一个错误完成它。在完成IRP之前，完成例程也能更新任何IRP特定的驱动程已经保存的状态，释放任何操作特定的驱动程序已经分配的资源，等等。另外，完成例程能推迟IRP完成（通过通知I/O管理器在IRP上更多处理被请求），并且在允许IRP完成之前，能发送另一个请求到相邻的较低层驱动程序。

§ 在它分配和发送请求到相邻的较低层驱动程序的IRP中，设置相邻的较低层驱动程序的I/O栈位置。

§ 通过在每个IRP中设置相邻的较低层驱动程序的I/O栈位置和调用IoCallDriver来传递任何到达的请求到较低层驱动程序。（注意，对于带有主要功能代码IRP_MJ_POWER的IRP，驱动程序必须使用PoCallDriver。）

希望得到中间层和最低层驱动程序调用的支持例程的特定信息，以及这些驱动程序必须处理的设备类型特定请求的信息，参见《Windows 2000驱动程序开发参考》卷2。也可参考《Windows 2000驱动程序开发参考》的卷1得到关于PnP和电源支持例程的信息。

如图2.3所示，Windows 2000文件系统是一个两部分驱动程序：

1. 文件系统驱动程序（FSD），它在用户模式线程的环境中执行，这个线程调用I/O系统服务

I/O管理器把相应的IRP发送到FSD。 如果FSD为IRP创建完成例程，则这个完成例程不必在原来用户模式线程的环境中被调用。

2. 一组文件系统线程，和一个可能的FSP（文件系统处理）

FSD能创建一组驱动程序专用系统线程，但是为了不阻碍作I/O请求的用户模式线程，大多数FSD使用系统工作者线程。任何FSD可以创建自己的过程地址空间，它的驱动程序专用线程在其中执行，但是系统提供的FSD避免这种做法，以节省系统存储空间。

Windows 2000文件系统一般使用系统工作者线程创建并且管理IRP的内部工作队列，它们发送IRP到一个或更多低层驱动程序，有可能对于不同的设备。

如图2.3中所示的最低层驱动程序通过一组离散的驱动程序提供的例程去处理每个IRP时，与文件系统不同，它不使用系统线程。最低层驱动程序不需要它自己的线程环境，除非为I/O安装它自己的设备是一个非常延时的过程，以至对系统性能有显著的影响。

几乎没有最低层或者中间层的驱动程序需要创建它们自己的驱动程序专用的或者设备专用的系统线程，并且这确实要付出性能上的代价，这通常是由向它们的线程作环境交换引起的。

大多数内核模式驱动程序，像图2.3的物理设备驱动程序一样，在一个专用线程环境中执行：当它们被调用处理IRP时，任何线程的环境恰巧是当前的。因而，驱动程序通常维护它们的I/O操作和服务的设备的状态信息，这些信息被存放在它们的设备对象中驱动程序定义的部分，被称为设备扩展。

每一驱动程序创建的设备对象代表一种物理、逻辑、或者虚拟设备，一个特定的驱动程序为它处理I/O请求。有关不同种类的设备驱动程序如何使用设备对象代表它们各自的设备的指导方针，参见本章后面的“设备对象和分层的驱动程序”。有关创建和安装设备对象的详尽信息，参见第3章。也可参见《即插即用、电源管理和安装设计指南》，提供了一个PnP驱动程序创建的设备对象的类型的讨论。

也如图2.3所示，大多数驱动程序通过驱动程序提供的一组系统定义的标准例程，来处理各阶段中的每一IRP，但是链中的不同层的驱动程序必然有不同的标准例程。例如，只有最低层驱动程序处理来自物理设备的中断，因此只有最低层驱动程序拥有ISR和DPC，它完成中断驱动的I/O操作。另一方面，因为当这样的驱动程序收到来自它的设备的中断，它知道I/O是完成的，它不需要一个完成例程。只有较高层的驱动程序拥有一个或更多个完成例程，像图2.3中所示的FSD一样。参见2.4节可以得到对驱动程序必须或者可以拥有的系统定义的例程的简要介绍。第4章提供了这些驱动程序例程的一个概述，后续的章节提供了例程特定的请求。
1.3.1 IRP处理的注意事项

设计内核模式驱动程序时，注意下列的事项：

§ 一个新的驱动程序必须与它所替换的任何系统提供的驱动程序一样，能处理相同的一组IRP_MJ_XXX。如果其驱动程序不为那个IRP_MJ_XXX定义入口点，I/O管理器将针对一个给定的I/O请求返回STATUS_INVALID_DEVICE_REQUEST到目标设备。设备驱动程序也必须与任何系统提供的驱动程序一样，能为IRP_MJ_DEVICE_CONTROL请求处理同样的I/O控制代码。 换句话说，通过实现比现存驱动程序为相同类型设备提供的功能少的功能，新的设备驱动程序一定不能“中断应用”。

§ 被插入现存驱动程序链中的新的中间层驱动程序应该象它所替换的驱动程序一样，能识别同样的一组IRP_MJ_XXX。新的驱动程序能简单地传递那些请求的IRP，而不为较低层驱动程序处理它们。然而，一个新的中间层驱动程序忽略为IRP_MJ_XXX请求（新替换的更低层的驱动程序处理这个请求）定义入口点，但不必为它上层和下层的驱动程序“打破这个链”。

§ 最低层驱动程序仅仅能访问被发送的任何IRP中它自己的I/O栈位置。一个较高层驱动程序仅仅访问被发送的任何IRP中它自己的和较低层驱动程序的I/O栈位置。

§ 每个驱动程序仅仅在IRP的I/O状态块中向较高层的驱动程序（并且最终通过I/O管理器向用户模式的应用程序）交换信息，因为当链中的每个驱动程序完成IRP时，I/O管理器将相应的I/O栈位置赋零。从一种Windows NT/Windows 2000到更高的平台或版本，试图与一个特殊的较高（或者低）的驱动程序实现后门通信的任何新驱动程序将在可移植性和互操作性方面与其他驱动程序做出妥协。

§ 一对驱动程序能为IRP_MJ_INTERNAL_DEVICEC_CONTROL请求定义一组设备特定的（也称为私有的）I/O控制编码，这个请求由比这对驱动程序高的驱动程序发送到较低的驱动程序。 然而，如果希望它们从一种Windows NT和Windows 2000到下一个平台或版本，保持与其他驱动程序的可移植性和互操作性，这样的一对驱动程序必须符合前面的所有方针。如果你用一个私有的接口设计一对驱动程序，要注意仔细定义I/O控制代码集。使它们尽可能通用，并且按照前述的方针设计你的一对驱动程序，从而使得当它们从一种Windows NT和Windows 2000平台或者版本向另一个迁移时，你（或者其他的人）能容易地重用、替换、或者移植两个新驱动程序或其中之一。

1.4 驱动程序对象和标准驱动程序例程

图2.5说明驱动程序对象，描述一个驱动程序，以及最低层和较高层驱动程序可以，或者必须拥有的一组系统定义的（或者标准的）例程。

图2.5 驱动程序对象

每个名称旁边带有星号的标准例程在被调用时，被传递一个IRP。在入口处，每个被传递IRP的标准例程也被传递一个I/O请求的目标设备对象的指针。

I/O管理器定义驱动程序对象类型，并且使用驱动程序对象来注册和跟踪驱动程序的加载的映像的信息。注意，驱动程序对象中Dispatch入口点（从DDDispatchXxx到DDDispatchYyy） 对应于被传递到IRP的I/O栈位置中的主要功能代码（IRP_MJ_XXX）。

如前面的图2.3所示，I/O管理器首先把每个IRP发送到驱动程序提供的Dispatch例程。最低层驱动程序的Dispatch例序通常调用I/O支持例程（IoStartPacket），从而排队（或者传递）每个IRP，这个IRP拥有驱动程序的StartIo例程的有效参数。StartIo例程在一个特定的设备上开始请求的I/O操作。较高层的驱动程序通常没有StartIo例程，但是它们可以有。

当驱动程序被装载时，其DriverEntry例程连带一个指向这个驱动程序对象的指针被调用。 DriverEntry例程在输入驱动程序对象中设置一个或多个Dispatch入口点，这样I/O管理器就能把IRP发送到适当的驱动程序提供的Dispatch例程。DriverEntry例程也在驱动程序对象中设置驱动程序的StartIo和Unload入口点，并且在DriverExtension中设置AddDevice例程。

DriverEntry或者可选的Reinitialize例程也能使用驱动程序对象（没有在图2.5中显示）中的域在配置管理器的数据库中获得信息和/或设置信息。更多信息，可参见《即插即用、电源管理和安装设计指南》的第4部分，第2章中的“注册表中的驱动程序信息”。
1.4.1 对象的不透明性

像所有系统定义对象一样，驱动程序对象是不透明的：只有定义的系统组件（这里是I/O管理器）“知道”对象类型的内部结构，并能直接访问对象包含的所有数据。定义的系统组件通常输出支持例程，驱动程序和其他内核模式组件能调用它们以操纵那些组件的对象。例如，Windows NT 内核输出支持例程，在最低层驱动程序注册它的中断服务程序（ISR）时，I/O管理器调用它们以初始化并且连接中断对象，如图2.5中所示的DDInterruptService。

为了保持驱动程序的可移植性，考虑下面这个有关系统定义的对象的实现方针：

§ 驱动程序必须使用系统提供的支持例程来操纵系统定义对象。定义的系统组件能随时改变其对象类型的内部结构。

然而，I/O管理器不输出支持例程来操纵驱动程序对象。驱动程序对象被I/O管理器仅仅用来跟踪当前加载的驱动程序。一些在驱动程序对象之内的域是不透明的：只有I/O管理器“知道”并且只有I/O管理器使用。其他是部分地不透明的；例如，你必须知道某个域名称，以定义AddDevice、Dispatch、StartIo，和Unload入口点。然而，你既不应该试图使用驱动程序对象内未发表的域，也不应该做有关本文中命名的任何驱动程序域的位置的假定。否则，你不能保证从一种Windows NT和Windows 2000个平台到另一个平台上的移植性。
1.4.2 标准驱动程序对象入口点

内核模式驱动程序必须在它的驱动程序对象中定义下列的入口点：

§ 至少一个Dispatch入口点，以得到请求PnP、电源、和I/O操作的IRP

§ 其AddDevice例程的入口点，在DriverObject->DriverExtension->AddDevice

§ 其StartIo例程的入口点，如果它管理它自己的IRP队列

§ 如果驱动程序能动态地被装载和/或者替换，还需要一个Unload入口点，从而可以释放任何系统资源，诸如驱动程序已分配的系统对象或者内存

当系统运行时不能被替换的驱动程序，诸如键盘驱动程序，不必提供Unload例程。

这些请求不适用于一些微端口驱动程序，相应的类别或者端口驱动程序在驱动程序对象中定义入口点。细节参见设备类型特定的文档。

支持PnP的任何驱动程序必须拥有AddDevice例程。AddDevice例程创建一个或多个设备对象，它们代表物理、逻辑、或者虚拟的设备，驱动程序则为它们完成I/O请求。如图2.5所示，I/O管理器在相应的驱动程序对象中维护驱动程序创建的设备对象的信息。

如果最低层驱动程序被设计成创建并且管理它自己的IRP队列，它不必定义StartIo入口点。然而，大多数这样驱动程序在它们的驱动程序对象中定义StartIo入口点，并且依靠I/O管理器为发往它们的StartIo例程的IRP排队。事实上，几乎没有最低层的系统提供的驱动程序被设计成没有StartIo例程，即使它们为IRP创建并且保持它们自己的辅助队列。

要不是为了更好的性能，较高层的的驱动程序（包括FSD和PnP功能，以及过滤器驱动程序）都可能有StartIo例程，但实际上很少这样做。相反，大多数Windows 2000文件系统驱动程序创建并且保持内部的IRP队列。其他较高层的驱动程序要么拥有内部的IRP队列，要么从它们的Dispatch例程简单的传递IRP到低层的驱动程序，这些都是在每个IRP中为更低层驱动程序安装I/O栈位置，并且可能为给定的IRP安装较高层驱动程序的IoCompletion例程之后发生的。

当驱动程序的DriverEntry例程被调用，它直接在驱动程序对象中设置Dispatch、StartIo（如果可能）、和Unload（如果可能）入口点，如下所示：

DriverObject->MajorFunction[IRP_NJ_xxx]=DDDispatchXxx;

: :

DriverObject->MajorFunction[IRP_NJ_yyy]=DDDispatchYyy;

: :

DriverObject->DriverStartIo=DDStartIo;

DriverObject->DriverUnload=DDUnload;

: :

它也在它的驱动程序对象内的DriverExtension中，设置它的AddDevice例程的入口点，如下：

DriverObject->DriverExtension->AddDevice=DDAddDevice；

驱动程序能定义若干Dispatch入口点，但是它只能在其驱动程序对象中定义一个StartIo入口点、一个AddDevice入口点，和一个Unload入口点。
1.4.3 其他标准驱动程序例程

如图2.5所示，与它们在各自驱动程序对象中为其设置入口点的那些例程一起，内核模式驱动程序还拥有其他的标准例程。大多数标准驱动程序例程及其使用的一些配置相关的对象是由I/O管理器定义的。ISR、SynchCritSection例程、以及图2.5中所示的名字包含“custom”的那些例程，是由Windows NT 内核定义的。

大多数驱动程序使用每个设备对象的设备扩展，它们创建这些设备对象以保持有关它们的I/O操作的设备特定状态，并且存储一些指针，这些指针指向那些为了其他标准例程它们必须分配的任何系统资源。例如，在图2.5中所显示的DDCustomTimerDpc例程请求驱动程序提供对内核定义的定时器和DPC对象的存储。

如2.2节，“终端用户I/O请求和Windows 2000文件对象”中提到那样，针对在图2.5中左边所示的最低层驱动程序的一组标准的驱动程序例程，必然不同于针对较高层的驱动程序的。在图2.5中所示的一些例程是设备相关或者配置相关的请求。其他的是可选的：你能选择实现这样一个例程，这取决于驱动程序的设备的性质或者配置、驱动程序的设计和驱动程序在分层驱动程序的链中所处的位置。

除了Dispatch、AddDevice、StartIo、和Unload例程之外，系统定义的标准驱动程序例程还包括：

InterruptService（ISR）

能产生中断的物理设备的驱动程序必须拥有ISR。ISR必须根据中断停止设备。然后，它所应该做的仅仅是保存状态，并且排队一个DPC，以在较低硬件优先级上完成中断驱动的I/O操作，而不是在ISR执行的优先级上完成。

DpcForIsr

拥有ISR的驱动程序也应该拥有DPC，要么是一个DpcForIsr，要么是一个或者多个CustomDpc例程，以完成中断驱动的I/O操作。

SynchCritSection

任何带有与它的ISR共享数据或设备寄存器的例程的驱动程序必须拥有一个或多个SynchCritSection程序，以便以多处理器安全方式访问共享的数据或者寄存器。

Cancel

驱动程序中的IRP可能为一个不确定的间隔保持排队状态（所以用户可以取消先前提交的I/O请求），这样的驱动程序必须有一个或多个Cancel例程让用户取消I/O请求。应该拥有Cancel例程的驱动程序的例子是键盘、鼠标、并口、串口、和声音设备驱动程序（或位于它们之上的驱动程序），以及文件系统驱动程序。

ControllerControl

必须通过一个物理控制器使到一个简单设备的操作同步的最低层设备驱动程序，诸如“AT”盘控制器，可以拥有ControllerControl例程。WDM驱动程序不能有ControllerControl例程。

IoCompletion

在IRP特定基础上监控较低层驱动程序如何完成特定请求的较高层驱动程序可能拥有一个或多个IoCompletion例程。分配IRP把请求发送到较低层驱动程序的较高层驱动程序必须有IoCompletion程序。关于较高层驱动程序如何使用IoCompletion例程的描述，参见“IRP和驱动程序特定的I/O栈位置”。

IoTimer

为了确定驱动程序设备操作是否有超时，或更新某种驱动程序定义的变量（诸如计数器），或者出于某种原因需要被周期性地调用的驱动程序，可能拥有IoTimer例程。IoTimer例程实际上是一个与设备对象有关的DPC例程，I/O管理器每秒调用一次。对驱动程序创建的每个设备对象，它可能有一个IoTimer例程。

CustomTimerDpc

需要周期性地以精细的间隔（不是每秒一次）或者可变的间隔被调用的驱动程序可以拥有一个CustomTimerDpc例程，而非IoTimer例程。除了它的IoTimer例程之外，驱动程序也能有一个或多个CustomTimerDpc例程。

CustomDpc

需要在较低的硬件优先级（IRQL）上延迟完成中断驱动的I/O操作的驱动程序可能有CustomDpc例程。几乎没有最低层驱动程序拥有CustomDpc而让它们的ISR排队，除非它们的设备要求多于一个DpcForIsr例程来完成一个不同组的中断驱动的I/O操作。

注意，图2.5中所示的CustomTimerDpc和IoTimer例程实际上是系统定义的CustomDpc例程在系统时钟中断发生之后执行。

Reinitialize

需要各阶段中初始化自己的任何驱动程序可能有Reinitialize例程。Reinitialize例程在DriverEntry例程已返回控制并且其他驱动程序已初始化了它们自己之后被调用。

AdapterControl

使用系统DMA的任何设备的驱动程序可能有AdapterControl例程，这个例程通过系统DMA控制器在它的设备和系统物理内存之间完成传递操作。非SCSI总线控制器的设备的驱动程序通常必须拥有AdapterContro1例程。作为选择，执行基于包的DMA（要么系统，要么总线控制器）的驱动程序可能拥有AdapterListControl例程，使用系统提供分散/收集支持。细节参见第3章。

出于惯例，除DriverEntry之外的所有的标准例程的名称中被加入一个标识性的、驱动程序或设备特定的前缀。作为例子，本文使用“DD”，如图2.5所示。遵循这个惯例，可以使调试和维护驱动程序变得更容易。
1.4.4 标准驱动程序例程的注意事项

当设计内核模式驱动程序时，注意下列事项：

§ 所有的驱动程序必须有至少一个Dispatch例程，并且必须在其驱动程序对象中为驱动程序处理的每个IRP_MJ_XXX定义发送入口点。驱动程序能拥有与它所能处理的IRP_MJ_XXX功能同样数量的Dispatch例程。

§ 如果在系统运行时驱动程序能被替换，驱动程序必须有Unload例程，并且在其驱动程序对象中定义一个Unload入口点。Unload例程负责释放任何系统资源，诸如Windows 2000对象或者驱动程序分配的内存，这些资源通常在驱动程序本身被从系统卸载之前使用。

§ 所有的PnP驱动程序必须有AddDevice例程，并且在驱动程序对象的驱动程序扩展中定义其入口点。AddDevice例程负责为驱动程序控制的每一个PnP设备创建和初始化设备对象。

§ 驱动程序可以有StartIo例程，并且能在其驱动程序对象中定义一个Startlo入口点。没有StartIo例程的任何最低层驱动程序必须创建并且管理发送到其Dispatch例程的内部IRP队列，除非它能在它的Dispatch例程中完成它得到的所有IRP。较高层驱动程序可以有StartIo例程，但是很少这样做，这是因为较高层驱动程序通常把IRP从它们的Dispatch例程直接传递到低层驱动程序。

§ 微端口驱动程序对于前面的要求是个例外。可以参见本DDK中的设备类型特定的文档中关于微端口驱动程序的要求。

§ 驱动程序是否有任何其他种类的标准例程取决于其功能和那个驱动程序如何适应系统（例如，它是否与系统提供的驱动程序互操作）。细节参见这个DDK中设备类型特定的文档。

也参见下列的文档：

§ 第2.6节，“设备对象和分层的驱动程序”，阐述不同种类的设备驱动程序如何使用设备对象描述物理、逻辑、和虚拟的设备

§ 第3章阐述驱动程序能使用的系统定义的对象的详细信息

§ 第4章是标准驱动程序例程的综述，第5章到第15章阐述例程特定的需求

§ 第16章是关于驱动程序如何管理系统定义的硬件优先级，标准驱动程序例程在其上执行

§ 在线DDK中的术语表，是与编写驱动程序有关的术语和缩写词

§ 《即插即用、电源管理、和安装设计指南》和《Windows 2000驱动程序开发参考》卷1，阐述了PnP的需求

1.5 设备配置和分层驱动程序

对于大多数普通设备，Windows 2000 DDK提供一组全功能的系统驱动程序的样例集。当为类似的设备开发新驱动程序时，单独的样例驱动程序可以被用作模型。然而，系统的驱动程序有个附加的设计请求：要使开发新的设备驱动程序是容易的。因而，许多系统的驱动程序具有分层结构，这样使得特定的驱动程序能被重用，以支持为类似的设备开发的新驱动程序。

在大多数情况下，DDK提供的可重用的驱动程序是WDM驱动程序，它是支持PnP的，并且能为系统提供的、设备特定的最低层（PnP总线）驱动程序提供处理硬件独立性的操作。在某些情况下，诸如并行端口和SCSI端口驱动程序，这些可重用的驱动程序为高层的、设备类型特定的类驱动程序提供支持。注意，几乎所有系统的可重用驱动程序进行新中间层驱动程序的开发，这些中间层驱动程序将被加入到现存的驱动程序链中，正如“IRP和驱动程序特定的I/O栈位置”中已经描述的那样。

一个新（或者替换）的驱动程序在设备的驱动程序链中适合位置，部分取决于一个给定的Windows平台中设备的硬件配置，也部分取决于新驱动程序能从现存的系统驱动程序获得多少支持。
1.5.1 样例设备和驱动程序配置

本节介绍硬件和驱动程序配置之间的关系，用键盘和鼠标设备作为例子。配置因设备不同而不同。任何设备配置的完成信息，参看本DDK中的设备特定的文档。

图2.6描述了键盘和鼠标这两个设备的可能硬件配置：

§ 它们都直接的连到系统总线的某个位置

§ 它们都通过一个键盘和辅助设备控制器被连结

图2.7描述了为图2.6中设备上的I/O操作所提供的相应的分层驱动程序。

图2.6 键盘和鼠标硬件配置

图2.7 键盘和鼠标驱动程序层

注意，无论硬件配置如何，键盘和鼠标设备能使用系统的键盘类和鼠标类驱动程序，以处理硬件无关的操作。这些被称为类驱动程序是因为每个都为特定的一类设备提供系统要求的、但却与硬件无关的支持。

相应的端口驱动程序实现设备特定的支持，以在每个物理设备上完成要求的I/O操作。x86平台的系统（i8042）的键盘和辅助设备端口驱动程序，为鼠标和键盘管理设备特定的操作。在每个设备被分别连结的硬件配置中，如图2.6所示，每个系统类驱动程序能在分开的设备特定的端口驱动程序之上被分层，或者每个设备的单独驱动程序能作为一个分开的、单一的（最低层）驱动程序被实现。

新的中间层驱动程序，例如PnP过滤器驱动程序，能被添加到图2.7中的配置中。例如，被增加到上述的键盘类驱动程序的过滤器驱动程序，可以在键盘输入通过I/O服务到达要求它的子系统前，以平台特定的习惯过滤这个键盘输入。这样的过滤器驱动程序象键盘类驱动程序一样，必须识别相同的IRP和IOCTL。
1.5.2 添加驱动程序的注意事项

设计内核模式驱动程序时，要注意下列事项：

§ 系统提供的SCSI和视频端口驱动程序不能被替换。

§ 替换最低层驱动程序必须实现被替换的驱动程序的同样功能。例如，替换键盘或者鼠标端口驱动程序必须使用系统定义它自己和它所重用的系统提供的类驱动程序之间的接口，反之亦然。

§ 将要插入到任何系统提供的一对驱动程序之间的新中间层驱动程序，必须能与那些驱动程序或操作。换句话说，这样的新驱动程序必须遵照“处理IRP的注意事项”和“标准驱动程序的注意事项”这些节中提供的事项。

1.6 设备对象和分层驱动程序

除了某些微端口驱动程序，所有的内核模式驱动程序创建至少一种设备对象。（系统提供的端口或者类驱动程序按照它们自己的需求创建和管理所有必要的系统定义的对象。） 一些驱动程序必须创建多个设备对象：无论分层驱动程序链中特定驱动程序处于哪一层，单独的设备对象描述一个物理、逻辑、和/或虚拟的设备，驱动程序为这个设备处理I/O请求。

换句话说，图2.5中显示的驱动程序对象描述一个单独的驱动程序，这个驱动程序可以创建多个设备对象。I/O管理器维护被每个驱动程序创建的设备对象的列表，并且在驱动程序对象中存储指向这个列表的指针。

当每个标准驱动程序例程被调用的时候，被传递IRP的例程也被传递一个指向目标设备的指针。大多数驱动程序使用目标设备对象的设备扩展来维护必要的设备状态信息或者驱动程序决定的有关它们当前正在处理的I/O请求的环境数据。

因为大多数总线、功能、和过滤器驱动程序（最低层和中间层驱动程序）在一个专用的线程环境中执行，设备扩展是每个驱动程序维护设备状态和这个驱动程序需求的所有其他设备特定的数据的主要地方。例如，在图2.5中所示的任何实现CustomTimerDpc或者CustomDpc例程的驱动程序通常在设备扩展中为必需的内核定义的定时器和/或DPC对象提供存储空间。

每个拥有ISR的驱动程序必须为指向一组内核定义的中断对象的指针提供存储空间，并且大多数设备驱动程序将这个指针存储在设备扩展中。当每个驱动程序创建设备对象，它确定设备扩展的大小，并且每个驱动程序定义它自己的设备扩展的内容和结构。

下一节通过说明这些对象和代表性硬件配置之间的对应关系来介绍设备对象。第3章较详细地分别描述设备对象，连同适配器和控制器对象。它也覆盖了所有其他系统定义的驱动程序可能或必须使用的对象，例如中断对象，这个对象是为那些产生中断的物理设备的最低层驱动程序而创建的。更多关于特定驱动程序必须创建的设备对象的信息，参见DDK中的设备类型特定文档。
1.6.1 针对样本配置的设备对象

图2.8说明代表键盘和鼠标设备的设备对象，这些设备在图2.6中显示。在图2.7中所示的键盘和鼠标驱动程序通过调用I/O支持例程（IoCreateDevice）来创建这些设备对象。

图2.8 键盘和鼠标设备对象

对于键盘和鼠标设备来说，它们各自的端口和类驱动程序分别创建设备对象。端口驱动程序创建物理设备对象（PDO）以代表物理的端口。每个类驱动程序创建它自己的功能设备对象（FDO）以代表键盘或者鼠标设备，作为I/O请求的目标。

每个类驱动程序调用I/O支持例程得到指向相邻的较低层驱动程序的设备对象的指针，因此类驱动程序能将自己链在端口驱动程序之上，并且每个类驱动程序能为代表其物理设备的目标PDO，把I/O请求发送到端口驱动程序。

增加到配置的可选的过滤器驱动程序，将创建过滤器设备对象（filter DO）。像类驱动程序一样，一个可选的过滤器驱动程序能链自己到设备栈中它的相邻的较低层驱动程序，并且为目标PDO发送I/O请求到相邻的较低层驱动程序。

如图2.7中所示，每个端口驱动程序是总线（最低层的）驱动程序，所以产生中断的设备的所有端口驱动程序必须创建中断对象，并且注册ISR。

双重设备端口驱动程序，如图2.6中所示的键盘和辅助设备控制器的（i8042）驱动程序，如果每个设备使用不同的中断向量，必须创建设备特定的中断对象。写这样的驱动程序时，要么为每个设备实现单独的ISR，要么两种设备实现单一的ISR。
1.6.2 设备对象的注意事项

设计内核模式驱动程序，应当遵从下列的事项：

§ 除某些微端口驱动程序之外，所有的驱动程序必须创建命名的设备对象，以代表每个物理、逻辑、或者虚拟设备，这些设备可能是I/O请求的目标。

最低层驱动程序，例如PnP硬件总线驱动程序，为每个它控制的设备创建一个物理设备对象（PDO）。中间驱动程序，例如PnP功能驱动程序，创建一种功能设备对象（FDO）。

例如，替换的并行端口或者串行驱动程序应该为每个端口创建一个物理设备对象。相比之下，描述机器中的每个硬盘为大硬盘的新虚拟硬盘驱动程序应该创建一个单独功能的设备对象和一定数量的额外功能的设备对象，前者描述它的硬盘，后者为位于这个中间层驱动程序之上的文件系统描述磁盘分区。

驱动程序在其AddDevice例程中创建设备对象，这个例程在设备列举之后被PnP管理器调用。

§ 对于大多数最低层和中间层驱动程序，每个设备对象的设备扩展是每一驱动程序的主要的（并且只是经常地）全局数据存储区域。许多驱动程序在驱动程序定义的设备扩展（属于一个或多个驱动程序创建的设备对象）中维护设备状态和驱动程序需要的所有其他设备特定的数据和资源。

对于某些种类的数据，IRP中驱动程序特定的I/O栈位置被认为是操作特定的本地存储区域。


系统定义的对象和对驱动程序的支持

本章包含下列的信息：

3.1 系统组成部分和分层内核模式驱动程序

3.1.1 操纵系统定义的对象的支持例程

3.1.2 存储系统定义的对象

3.2 设备对象和设备扩展

3.2.1 定义设备扩展

3.2.2 创建设备对象和设备扩展

3.2.3 初始化驱动程序特定的设备对象和设备扩展

3.2.4 设置用户缓冲区的访问

3.2.4.1 使用缓冲I/O

3.2.4.2 使用直接I/O

3.2.4.3 使用非直接也非缓冲I/O

3.3 适配器对象和DMA

3.3.1 映射寄存器

3.3.2 获取适配器对象

3.3.3 拆分传输请求

3.3.4 使用系统DMA

3.3.4.1 基于包的系统DMA

3.3.4.2 公用缓冲区的系统DMA

3.3.5 使用总线控制器DMA

3.3.5.1 基于包的总线控制器DMA

3.3.5.2 公用缓冲区的总线控制器DMA

3.3.6 执行分散/收集DMA

3.4 控制器对象

3.4.1 使用控制器扩展创建控制器对象

3.4.2 为I/O操作分配控制器对象

3.5 中断对象

3.5.1 获得系统指定中断向量、DIRQL、和处理器掩码

3.5.2 注册ISR

3.6 DPC对象

3.6.1 注册和排队DpcForIsr例程

3.6.2 注册和排队CustomDpc例程

3.7 带有相关DPC的定时器对象

3.7.1 注册和激活IoTimer例程

3.7.2 注册和排队CustomTimerDpc例程

3.8 设备队列对象和互锁队列

3.8.1 设置设备队列对象和排队IRP

3.8.2 设置一个互锁队列和排队IRP

3.9 内核调度者对象

3.9.1 定时器对象

3.9.2 事件对象

3.9.3 信号量对象

3.9.4 互斥体对象

3.9.5 调度者对象上线程等待的Alert和APC的处理

3.10 回调对象

3.10.1 定义回调对象

3.10.2 使用驱动程序定义的回调对象

3.10.3 使用系统定义的回调对象

所有内核模式驱动程序使用某些系统定义的对象。例如，所有的驱动程序（除某些微端口驱动程序之外）必须创建并安装设备对象，以代表它为其处理IRP的每个逻辑、虚拟、和/或物理的设备。

仅仅某些种类的驱动程序被要求使用特殊的系统定义的对象。例如，一个给定的驱动程序是否使用适配器对象取决于其设备是否使用DMA。

这章描述内核模式驱动程序使用的系统定义的对象。除了表明的以外，这个信息适用于Windows 2000和WDM驱动程序。哪个对象适合于特定设备类型的细节，参见DDK中设备特定的文档。

1.1 系统组成部分和分层内核模式驱动程序

图3.l总结了分层的内核模式驱动程序链和系统组件之间的联系，这些系统组件定义了这些驱动程序能调用哪些支持例程。

图3.l 对驱动程序的可执行组件支持

也可参看《Windows 2000驱动程序开发参考》卷2，得到关于对最低层和中间层驱动程序有用的支持例程的细节。

分层的驱动程序链中的驱动程序的数目有些依赖于设备的性质。例如，SCSI海量存储设备的系统驱动程序形成至少三个驱动程序的链：SCSI类、对应于中间层类的端口驱动程序和图3.1中所示的最低层物理设备驱动程序，并且至少一个文件系统驱动程序在每个类驱动程序上被分层。特定的HBA（宿主总线适配器）的SCSI微端口驱动程序被认为是SCSI端口驱动程序的一部分。

此外，如果用户能够分区镜像，或者创建带或卷集，系统提供的灾难恢复的磁盘驱动程序，ftdisk，可以在SCSI磁盘类驱动程序和文件系统驱动程序之间被分层。ftdisk驱动程序是可选的，但是它符合图3.1中显示的逻辑/虚拟的中间层设备驱动程序。
1.1.1 操纵系统定义的对象的支持例程

最低层驱动程序有与较高层驱动程序不同的标准驱动程序例程，所以图3.1中显示了与硬件和能使用这些对象的最低层驱动程序接近的某些对象（中断、控制器、和适配器）。硬件抽象层（HAL）为I/O管理器、内核、电源管理器和许多驱动程序提供平台特定的支持。

所有驱动程序也能调用由I/O管理器提供的支持例程，以获取和提供有关它们的设备的信息，并在它们的命名的设备对象与子系统特定的名字（针对设备）之间创建符号联接，保存在注册表中。最低层驱动程序也能调用IoXxx和/或RtlXxx支持例程，以获得和设置注册表中的配置信息。更多的信息参见《既插即用、电源管理和安装设计指南》的第4部分，第2章“注册表中的驱动程序信息”。

与I/O管理器类似，安全参考监视器（Security Reference Monitor）、配置管理器（Configuration Manager）、对象管理器、过程结构、内存管理器、运行支持和内核组件，每个都定义一组不透明的对象类型和数据结构。驱动程序只能通过调用适当的内核模式支持例程来使用这些对象和数据结构。

如图3.1所示，最低层（设备）和中间层驱动程序的作者可以忽略本地过程调用（LPC）组件。只有某些最高层的驱动程序调用由安全组件提供的支持例程。在IRP被送到较低层的驱动程序之前，I/O管理器或者文件系统驱动程序执行任何必要的进程内通信和安全访问检查。I/O管理器和对象管理器也在把IRP发送到低层驱动程序之前解析符号联接。

如图3.1所示，I/O管理器为操纵由其他组件定义的某些对象和数据结构提供支持例程，包括符号链接对象、中断对象、DPC对象、定时器对象和内存描述符表（MDL）。这些IoXxx支持例程帮助实现某些标准驱动程序例程，并且映射缓冲区，这可以与驱动程序定制的被发往较低层驱动程序的IRP相关。大多数驱动程序仅仅要求IoXxx支持例程做下列事情中的一些：

§ 为设备注册标准ISR（IoConnectInterrupt），以及当设备被移走的时候注销ISR（IoDisconnectInterrupt）。

§ 注册标准DpcForIsr例程（IoInitializeDpcRequest），并且从它的ISR中向驱动程序的DpcForlsr例程请求一个调用（IoRequestDpc）。

§ 注册标准IoTimer例程（IoInitializeTimer），并且能够启用或禁止使每秒对IoTimer例程的调用（分别是IoStartTimer和IoStopTimer）。

§ 将一个已经被与某个输入IRP相关的MDL映射的过大的缓冲区拆分成较小的映射的缓冲区，从而使的驱动程序能在具有有限数据传输能力的设备上进行DMA操作。

除了文件系统驱动程序，当它们调用IoCreateDevice时，I/O管理器也为驱动程序创建的每个设备对象安装一个相关的设备队列。它也提供这样的支持例程，驱动程序调用这样的支持例程以拥有发往它们的StartIo例程的IRP。IoStartPacket和IoStartNextPacket例程根据驱动程序的需要调用内核的设备队列支持例程。

然而，驱动程序如果必要的话能调用许多I/O管理器调用的相同的内核支持例程。取决于驱动程序的设计和它的设备，驱动程序能创建：

§ 定时器对象和能明确地被排队作为CustomTimerDpc例程的相关DPC对象。例如，如果驱动程序的设备要求可变的超时间隔或者比每秒一次更精细的间隔，驱动程序可能拥有CustomTimerDpc，而非IoTimer例程。

§ 一个或多个DPC对象和来自ISR明确地排队驱动程序提供的CustomDpc例程。例如，一个串口驱动程序可以排队一个CustomDpc，它可以在它的ISR检测到设备错误的时候取消未决的I/O请求。

§ 设备专用线程和工作者线程在其上回调例程的定时器对象能等待驱动程序指定的时间间隔。创建它们自己设备专用系统线程的驱动程序，诸如系统软盘驱动程序，也能有它们的线程来等待内核定义事件、信号量，或者可能有互斥体，就向最高层驱动程序的工作线程能回调例程那样。驱动程序能通过调用PsCreateSystemThread来创建设备专用线程，并能使用PsTerminateSystemThread释放这样的线程。

§ 附加的设备队列对象。例如，SCSI端口驱动程序（参见在线DDK中“Storage Drivers and Device Objects（存储驱动程序和设备对象）”） 创建单一的设备对象代表HBA，而I/O管理器通过（或者从）适当的SCSI类驱动程序把IRP发送到与那个HBA设备对象有关的设备队列中。然而，SCSI端口驱动程序为HBA特定的SCSI总线上的每个逻辑单元创建一个额外的设备队列对象，这个SCSI总是先由SCSI类驱动程序声明的。SCSI端口驱动程序使用这些额外的设备队列来排序进入的IRP到逻辑单元特定的设备队列。

因为所有的DriverEntry和AddDevice例程在系统线程的环境中被调用，在较高层驱动程序已经链自己到相邻的较低层驱动程序上以后，当它初始化的时候，它也可以等待内核事件对象。例如，类驱动程序的AddDevice例程可以调用IoBuildSynchronousFsdRequest使用一个相关的事件创建IRP，并且使用IoCallDriver传递它的驱动程序分配的IRP到下层的端口驱动程序。当位于这样的类驱动程序之下的端口驱动程序收集（或设置）设备状态信息（类驱动程序需要这些信息以完成它的设备的初始化）的时候，这样的类驱动程序可以通过调用KeWaitForSingleObject来等待事件。

运行支持（Executive Support）组件，图3.l中的内核和内存管理器组件之间，提供要求初始化的自旋锁的特定支持例程，调用者必须为这些自选锁提供存储。包含词“互锁”（interlocked）的任何执行体支持例程要求调用者提供的自旋锁作为参数。执行体支持组件也为分配系统空间（存储池）内存的驱动程序和使用系统工作者线程的最高层驱动程序（例如FSD）提供例程。

驱动程序是否使用内存管理器的例程来操纵MDL部分取决于其设备的性质。当每个驱动程序安装它的设备对象的时候，它决定它是否使用MDL去访问用户缓冲区。
1.1.2 存储系统定义的对象

使用I/O管理器的中断、DPC和定时器支持例程的驱动程序能依赖于I/O管理器为任何必要和有关的内核定义的对象提供存储。然而，内核并不代表调用者为支持例程分配内存。因而，任何调用KeXxx例程的系统组件，包括任何驱动程序，必须为它使用的系统定义的对象提供存储。例如，任何拥有标准CustomTimerDpc例程的驱动程序为了调用操作它所需要的定时器和DPC对象的内核支持例程，必须为这些对象提供存储。

有中断服务程序（ISR）的驱动程序必须为由IoConnectInterrupt返回的中断对象指针提供存储。I/O管理器为驱动程序的中断对象（在SMP机器中，每个处理器一个）提供存储，并且初始化它们。

内核实际上使用相关的中断自旋锁定义中断对象类型，并且提供KeSynchronizeExecution，驱动程序使用由IoConnectInterrupt返回的中断对象指针来调用它，并使用驱动程序提供SynchCritSection例程来保证对另一个驱动程序例程和驱动程序的ISR之间的共享数据的多处理器安全访问。

驱动程序也能提供存储、初始化、并使用执行体自旋锁，调用KeAcquireSpinLock和KeReleaseSpinLock来管理对数据的多处理器安全访问，这些数据是在驱动程序例程中间共享的，但是不包括ISR。在IRQ DISPATCH_LEVEL上运行并共享状态的驱动程序例程能调用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel，它们比KeAcquireSpinLock和KeReleaseSpinLock运行的更快。当然，注意自旋锁不是对象，它是一个不透明的、内核定义的使用内存的同步机制。标准驱动程序例程被调用的IRQL的总述和用法参见第l6章的“使用自旋锁”。

驱动程序通常在它们创建的设备对象的设备扩展内为它们的系统定义的对象和自旋锁（如果有）分配存储空间。如果驱动程序使用控制器对象，并且有ControllerControl例程，它们在控制器扩展中分配存储空间，像系统“AT”盘驱动程序那样。

大多数中间层和最低层驱动程序仅仅使用设备或者控制器扩展来维护必要的设备状态，并为其他驱动程序确定的数据，诸如系统定义对象、自旋锁、互锁队列，以及其他驱动程序定义的数据提供存储空间。然而，如果没有这样的缓冲，驱动程序的设备不能充分地被服务，驱动程序能在设备开始时使用内存管理器的支持例程分配邻接的或者非高速缓存的内部缓冲区。更多信息参见第16章中“管理内存使用”。

1.2 设备对象和设备扩展

设备对象，像驱动程序对象一样，对驱动程序是部分地不透明的。因为它们的驱动程序必须通过由IoCreateDevice返回的DeviceObject指针访问这些领域，驱动程序作者必须知道某个域名称和与设备对象有关的系统定义的符号常量，因为它们的驱动程序必须通过DeviceObject指针访问这些域，这个DeviceObject指针是由IoCreateDevice返回的，并且被传递给大多数标准的驱动程序例程。

注意，任何通过一种返回的DeviceObject指针而可以访问的域的位置能从一种Windows平台或版本改变成为另一个。此外，在任何对象之内的“未发表过的”域实际上是难以访问的：即，驱动程序不应该访问它们。已经依赖于系统定义对象中任何域的位置，或访问对象中未公开的域的驱动程序，会削减它自己未来的可移植性和与其他驱动程序的互操作性。

图3.2说明一个设备对象，它在系统之内代表一种物理、逻辑、或者虚拟设备。驱动程序通过从它的AddDevice例程调用IoCreateDevice来创建一个或多个设备对象。

图3.2 设备对象

图3.2 显示了对最低层和中间层驱动程序的作者特别重要的域名称和常量。以下小节中将被更详细描述这些域。
1.2.1 定义设备扩展

对于大多数中间层和最低层驱动程序，设备扩展是与设备对象有关的最重要的数据结构。 其内部的结构是驱动程序定义的，并且它被典型地用于：

§ 保持设备状态信息。

§ 为任何内核定义的对象或者被驱动程序使用的其他系统资源提供存储，如自旋锁。

§ 保持驱动程序必须拥有的任何数据是常驻的，并且在系统空间内，以便完成它的I/O操作。

I/O管理器从常驻的系统空间内存中分配设备对象内存和扩展：即，IoCreateDevice从非页式存储池中为设备对象分配内存。

所有被赋予一个IRP的标准驱动程序例程也被赋予一个指向代表请求I/O操作的目标设备的设备对象的指针，如在第2章中所述的那样。因而，任何给定IRP的驱动程序例程能通过这个指向合适设备对象的指针访问相应的DeviceExtension。通常，对最低层驱动程序的ISR来说，DeviceObject指针也是一个输入参数。

图3.3针对最低层驱动程序设备对象的扩展显示了驱动程序定义的数据的一个代表集。较高层的驱动程序将不再为中断对象指针提供存储，这里的指针由IoConnectInterrupt返回，并到KcSynchronizeExecution和IoDisconnectInterrupt。然而，如果驱动程序有CustomTimerDpc例程，较高层驱动程序将为图3.3中显示的定时器和DPC对象提供存储。较高层驱动程序也可以为执行的自旋锁和互锁的工作队列提供存储。

除为中断对象指针提供存储之外，如果最低层设备驱动程序为两个或更多的设备以不同的向量处理中断，或者它拥有多个ISR，那么它必须为中断自旋锁提供存储。更多的关于注册ISR的信息，参见本章中随后的“中断对象”。

图3.3 最低层驱动程序的样例设备扩展

如图3.3所示，大多数驱动程序发现在它们的设备扩展中存储指向它们设备对象的指针是方便的。驱动程序也可以在扩展中为设备保持一份资源表的拷贝。

较高层驱动程序典型地将指向相邻的较低层驱动程序的设备对象指针存储到其设备扩展中。在较高层驱动程序已经在IRP中创建相邻的较低层驱动程序的I/O栈位置之后，它必须把指向较低层驱动程序的设备对象的指针传递到IoCallDriver，如第2章中所述。

也要注意到，任何为低层驱动程序分配IRP的较高层驱动程序必须指定新的IRP应该有多少栈位置。尤其是，如果较高层驱动程序调用IoMakeAssociatedIrp（仅Windows 2000有）、IoAllocateIrp、或者IoInitializeIrp，为了给这些支持例程提供正确的StackSize作为参数，它必须访问相邻的较低层驱动程序的目标设备对象以读取其栈大小的值。

当较高层驱动程序能通过由IoAttachDeviceToDeviceStack返回的指针从相邻的较低层驱动程序的设备对象中读取数据时，这样的驱动程序必须遵循这些实现方针：

§ 从不试图把数据写到较低层驱动程序的设备对象中。

前面方针的唯一例外情况是文件系统，它在较低层可拆除媒介驱动程序的设备对象的Flags中设置和清除DO_VERlFY_VOLUME。

§ 从不试图因下列原因访问较低层驱动程序的设备扩展：

§ 在两个驱动程序之间，没有同步访问单个设备扩展的安全方法。

§ 实现这样一种后门通信方案的一对驱动程序不能单个地被升级，不改变现存的驱动程序资源不能在它们之间插入中间驱动程序，并且从一个Windows 平台到下一个不能被容易地重编辑和移植。

为了保有从一种Windows平台或者版本到下一个版本与低层驱动程序的互操作性能，较高层驱动程序必须重用给它们的IRP，或者必须创建新的IRP，并且它们必须使用IoCallDriver把请求传送到较低层驱动程序。
1.2.2 创建设备对象和设备扩展

当每个驱动程序调用IoCreateDevicc创建设备对象时，驱动程序在DeviceExtensionSize参数中指定了设备扩展的大小。

大多数驱动程序仅仅从AddDevice例程调用IoCreateDevice。对于一些驱动程序，例如必须对驱动器布局IOCTL作出响应的磁盘驱动程序，这个调用也能由Dispatch例程来作。

除了某些为驱动程序（它与创建设备对象的类驱动程序成对）之外，所有的驱动程序必须从其AddDevice例程调用IoCreateDevicc一次或多次，以创建设备对象，这个设备对象代表它为其处理I/O请求的每一种物理、逻辑、或者虚拟设备。否则，对于驱动程序没有为其创建设备对象的任何目标设备，驱动程序将不能为这些设备获取IRP。

除设备扩展的大小之外，IoCreateDevice要求确定以下参数：

§ 系统定义常量，表明设备对象代表的DeviceType。对于系统定义的FILE_DEVICE_XXX常量列表，分别参见DDK或者WDM控制头文件，ntddk.h或者wdm.h。

§ 一个或多个作过“或”（OR）操作的、系统定义的常量，表明一定类型的设备（软盘、CD-ROM、和WORM可拆卸媒介设备）的DeviceCharacteristics。否则，被传递到IoCreateDevice的DeviceCharacteristics值必须是零。

§ 指明设备对象的Flags中的一位是否被置为DO_EXCLUSIVE的布尔值（Exclusive，专有权），表明驱动程序服务于一个专有设备，例如视频、串行、并行、或声音设备。WDM程序必须设置Exclusize为FALSE。

§ 指向DriverObject的指针，它被输入到驱动程序的AddDevice例程，将驱动程序对象与调用者为其处理IRP的物理、逻辑、或者虚拟设备相联系。

§ 一个可选的指向零结尾的Unicode串（DeviceName）的指针，这个Unicode串为设备命名。PnP驱动程序不应该提供设备名称；这样做绕过了PnP管理器的安全特性。PnP驱动程序能通过调用IoAttachDeviceToDeviceStack把自己链到一个没有名称的设备的驱动程序。

如果用户模式组件需要一个到设备的符号连接，注册设备接口（参见IoRegisterDeviccInterface）。如果内核模式组件需要传统的设备名称，驱动程序必须命名设备对象，但是命名是不被推荐的。

对于除文件系统驱动程序（FSD）之外的所有驱动程序，I/O管理器也为每个对IoCreateDevice的成功调用创建一个相关的设备队列对象，如图3.2。与设备对象相关的设备队列对象在驱动程序被装载之后，描述一个发往StartIo例程的IRP队列。管理它们自己内部IRP队列的驱动程序，例如系统软盘控制器驱动程序（参见第3部分），不使用与它们的设备对象有关的设备队列。

驱动程序也能创建附加的设备队列对象。更多的信息参见“设备队列对象和互锁队列”。

如果对IoCreateDevice的调用成功，I/O管理器为设备对象自己和与设备对象有关的所有其他数据结构提供存储，包括驱动程序设备扩展，它用零初始化。

如果调用者创建多个设备对象，I/O管理器通过在设备对象中维护NextDevice指针，连接其后其创建的设备对象到输入驱动程序对象。
1.2.3 初始化驱动程序特定的设备对象和设备扩展

IoCreateDevice返回后，给调用者一个指向DeviceObject的指针，DeviceObject中包含了指向DeviceExtension的指针，驱动程序必须为它们各自的物理，逻辑，和/或者虚拟设备在设备对象中创建一定的域。

IoCreateDcvice设置新创建的设备对象的StackSize域为一。最低层驱动程序可以忽略这个域。当一个较高层驱动程序调用IoAttachDeviceToDeviceStack把自己附加到相邻的较低层的驱动程序上时，程序自动地设置设备对象中的域StackSize为相邻的较低层驱动程序的那个值加一。然而，对于某些设备类型，较高层驱动程序可能需要设置域StackSize为较大的值，就像在设备特定文档中说明的那样。设置栈大小保证发送到较高层驱动程序的IRP将包含驱动程序特定的I/O栈位置，加上当前I/O栈位置的数得到链中所有较低层驱动程序。

IoCreateDevice设置新创建的设备对象的AlignmentRequiremcnt域为处理器的数据高速缓存线的大小减一，以保证用于直接I/O的缓冲区被正确地排列。IoCreateDevice返回之后，最低层物理设备驱动程序必须做如下步骤：

1. 从设备的排列请求中减一。

2. 把步骤1的结果与设备对象AlignmentRequirement的当前值相比较。

3. 如果设备排列请求较大，设置AlignmentRequirement为步骤1的结果。否则，将AlignmentRequirement值作为IoCreateDevice的设置值。

在任何较高层驱动程序通过调用IoGetDeviceObjectPointer把自己链到另一个驱动程序之后，较高层驱动程序必须设置它的新创建的设备对象的AlignmentRequirement域为相邻的较低层驱动程序的设备对象的那个值。作为一般规则，一个较高层驱动程序不应该改变这个值。如果较高层驱动程序调用IoAttachDevice或者IoAttachDeviceToDevice，那些例程自动的设置设备对象中的AlignmentRequirement域为较低层驱动程序的设备对象的那个值。

IoGetDeviceObjectPointer返回指向较低层驱动程序的设备对象和指向相关的文件对象的两个指针。只有FSD（或者可能是另一个最高层驱动程序）能使用返回的文件对象指针。调用IoGetDeviceObjectPointer的中间驱动程序保存这个文件对象指针，从而使得当驱动程序被卸载时，它能通过调用ObDereferenceObject被间接引用。

在FSD安装包含文件对象（它代表一较低层驱动程序的设备对象）的卷后，中间驱动程序不能通过调用IoAttachDevice或者IoAttachDeviceToDeviceStack把自己链到文件系统和较低层驱动程序之间。

中间层或者最低层驱动程序也在设备对象的Flags中设置一个位，方法是将它与DO_DIRECT_IO或者是与DO_BUFFERED_IO作“或”操作。如果驱动程序作者决定有关的附加工作将以较好的驱动程序性能为代价，对于缓冲或者直接I/O的，逻辑或者虚拟的设备的最高层驱动程序能避免设置Flags。中间驱动程序必须创建其设备对象的Flags域以匹配相邻的较低层驱动程序的设备对象。

设置设备对象的Flags域为DO_DIRECT_IO或DO_BUFFERED_IO，决定I/O管理器如何在所有的后来发送到驱动程序的数据传输请求中传递访问到用户缓冲区。

然后驱动程序能在设备对象中设置任何其他的依赖设备的值。例如，可拆卸媒介设备的非WDM驱动程序在I/O操作期间检测到（或怀疑）媒介中的变化，它必须使用DO_VERIFY_VOLUME与设备对象的Flags成员作“或”操作。（更多信息参见第16章中“处理可删除的媒介”。）要求突发（inrush）电源的设备的驱动程序必须用DO_POWER_INRUSH与Flags成员做“或”操作，并且不在系统页路径上的设备的驱动程序必须用DO_POWER_PAGABLE与Flags成员做或操作。PnP功能和过滤器驱动程序必须清除DO_DEVICE_INITIALIZING标记。

在驱动程序初始化设备对象之后，它也能初始化任何内核定义的对象和它在设备扩展中为其提供存储的其他系统定义的数据结构。驱动程序具体什么时候执行这些任务依赖于其设备、对象的类型、和/或数据的性质。简言之，能持续经过PnP开始和停止请求的任何对象或者数据结构能在AddDevice例程中被初始化。那些要求以PnP IRP_MN_START_DEVICE请求予以提供的资源信息的对象和数据结构，或者那些当设备被停止和/或重新启动时可能要求变化的对象和数据结构，应该在驱动程序处理IRP_MN_START_DEVlCE请求时被初始化。
1.2.4 设置用户缓冲区的访问

大多数最低层和所有中间层驱动程序在DeviceObject->Flags中设置一个位，如图3.2所示，方法是使用DO_BUFFERED_IO或者DO_DIRECT_IO与他们创建的每个设备对象中的Flags做或操作。对于数据传递，驱动程序必须使用下列的三种方法之一来访问用户缓冲区：

§ 服务于一种交互式（缓慢）的设备或一次通常传输比较少的数据的驱动程序应该创建它的设备对象，以请求缓冲I/O。为小而交互式的传递使用缓冲I/O会全面地改进物理内存的使用，因为内存管理器不必为每个传输锁定整个物理页，而直接I/O通常是这样做的。一般来说，视频、键盘、鼠标、串口，和并口的驱动程序请求缓冲I/O。

任何在这样一种设备驱动程序之上的中间层驱动程序必须创建它的DeviceObject->Flags，以匹配相邻的较低层驱动程序的那个值。

§ 服务于一次能传递大量数据的设备的驱动程序应该创建它的设备对象，以请求直接I/O。通过减少常规的中断和最小化缓冲I/O固有的内存分配和拷贝操作，对大批量数据传递使用直接I/O将提高驱动程序性能。一般地，海量存储设备驱动程序对传输请求要求直接I/O，包括使用DMA或者PIO的最低层驱动程序，还有任何链在它们之上的中间层驱动程序。

甚至请求直接I/O的驱动程序使用缓冲I/O满足一定的IRP。尤其是，驱动程序能对某种系统定义的、设备类型特定的I/O控制代码使用缓冲I/O，这些控制代码是对要求数据传输的RP_MJ_DEVICE_CONTROL请求的，而不论驱动程序是否已经用DO_DIRECT_IO与它的设备对象的Flags做或操作。设备控制请求的缓冲方法由设备控制代码自己确定。

任何在这样一种设备驱动程序之上的中间层驱动程序层必须创建它的DeviceObject->Flags，以匹配相邻的较低层驱动程序的那个值。

§ 在创建一个设备对象时，一直在原始的、用户模式的线程（它请求一个I/O操作）的环境中被调用的驱动程序既不要求直接I/O也不要求缓冲I/O。这样一种驱动程序必须是最高层的驱动程序。

下面的小节描述如何使用DO_BUFFERED_IO、DO_DIRECT_IO与设备对象的Flags做“或”操作，或者也不影响发往驱动程序的数据传输请求，包括数据在物理内存中如何被存储，以及驱动程序如何访问那个内存。
1.2.4.1 使用缓冲I/O

图3.4说明I/O管理器如何创建一个对驱动程序请求传输操作的IRP，这个驱动程序使用DO_BUFFERED_IO与它们的设备对象的Flags做“或”操作。

图3.4 对用户缓冲区的缓冲I/O

图3.4显示驱动程序已经用DO_BUFFERED_IO与设备对象的Flags做“或”操作时，驱动程序如何为读请求使用IRP中的SystemBuffer指针以传递数据。循环中的序号解释如下：

1. 用户空间的虚拟地址的某些范围代表当前线程的缓冲区，并且缓冲区的内容可以存储在基于页的物理地址（图3.4中黑暗部分）的范围之内的某一地方。

2. I/O管理器为当前线程的读请求服务，线程传递给它一个代表缓冲区的用户空间虚拟地址的范围。

3. I/O管理器检查用户提供的缓冲区的可访问性，并且调用ExAllocatePool以创建一个常驻的系统空间缓冲区（SystemBuffer），大小为等于用户提供缓冲区。

4. I/O管理器提供机会访问IRP中的新分配的SystemBuffer，这个IRP是它发送给驱动程序的。

如果图3.4显示一个写请求，I/O管理器将在它发送IRP到驱动程序之前，把从用户缓冲区拷贝数据到系统缓冲区。

5. 对于图3.4中所示的读请求，驱动程序将数据从属设备读到系统空间缓冲区。当读的请求被满足，驱动程序使用IRP调用IoCompleteRequest。

6. 当原来的线程再一次成为当前的线程，I/O管理器从系统缓冲区将读入数据拷贝到用户缓冲区。它也调用ExFreePool以释放系统缓冲区。

在I/O管理器已经为驱动程序创建系统空间缓冲区之后，请求的用户模式线程可能被交换出去，而其物理内存可能被另一个线程所重用，并且这个线程可能是属于另一个过程的。 然而，直到驱动程序使用IRP调用IoCompleteRequest，在IRP中所提供的系统空间虚拟地址范围会一直保持有效。

一次不传输大量数据的设备（例如交互式的设备）的驱动程序能使用缓冲I/O。

一次传输大量数据的驱动程序，尤其是做多页传输的驱动程序，不应该试图使用缓冲I/O。当系统运行时，非页式存储池可能变成碎片，这样I/O管理器不能分配大而连续的系统空间缓冲区给这样的驱动程序来发送IRP。

注意，对于某种IRP_MJ_XXX，所有驱动程序使用缓冲I/O。甚至，为直接I/O而创建它们自己的设备对象的驱动程序会为大多数请求使用缓冲I/O，这些请求不包括IRP_MJ_READ、IRP_MJ_WRITE，还可能不包括驱动程序定义的IRP_MJ_INTERNAL_DEVICE_CONTROL请求，这个请求要求大数据传输。 设备控制请求的缓冲方法由这个设备控制编码自己来确定。
1.2.4.2 使用直接I/O

图3.5说明I/O管理器如何创建一个向驱动程序请求传输操作的IRP，这个驱动程序使用DO_DIRECT_IO与它们的设备对象的Flags作“或”操作。

图3.5 使用DMA的设备的用户缓冲区上的直接I/O

图3.5说明驱动程序如何使用IRP的MdlAddress来为读请求传递数据。图中的驱动程序使用基于包的系统或者总线控制器DMA，并且已经使用DO_DIRECT_IO对设备对象的Flags作“或”操作。

1. 用户空间的虚拟地址的某些范围代表当前线程的缓冲区，并且缓冲区内容实际上可以被存储在一些物理上不连续的页上（图3.5中黑暗部分）。I/O管理器创建MDL描述这个缓冲区。MDL是一个不透明的由内存管理器定义的数据结构，这个数据结构映射一块特殊的虚拟地址范围到一个或多个基于页的物理地址范围。

2. I/O管理器为当前线程的读请求服务，线程为它传递代表缓冲区的用户空间虚拟地址的范围。

3. I/O管理器或者FSD检查用户提供的缓冲区的可访问性，并且调用使用前面创建的MDL调用MmProbeAndLockPages。MmProbeAndLockPages也填充MDL中相应的物理地址范围。

如图3.5所示，虚拟范围的MDL可能有几个相应的基于页的物理地址入口和虚拟范围，因为缓冲区可以在距MDL中描述的首页和末页的开始一些字节偏移的地方开始和结束。

4. I/O管理器在IRP中提供一个指向MDL（MdlAddress）的指针。直到驱动程序完成IRP后I/O管理器或者文件系统调用MmUnlockPages，在MDL中所描述的物理页仍然锁定并被分配到缓冲区。然而，这样的MDL中的虚拟地址能变得不可见（和无效），即使是在IRP被发送到设备驱动程序或者发送到可能在设备驱动程序之上的任何中间层驱动程序之前。

5. 如果驱动程序使用基于包的系统或者总线控制器DMA，它使用IRP的MdIAddress指针调用MmGetMdlVirtualAddress，以得到MDL基于页的入口的基本虚拟地址。

6. 当驱动程序的AdapterControl例程拥有到DMA通道的访问和/或映射驱动器，为了从设备中直接读取数据到物理内存，驱动程序使用由MmGetMdlVirtualAddress返回的基地址调用MapTransfer。

驱动程序应该总是检查缓冲区长度。注意，I/O管理器不能为零长度的缓冲创建MDL。

在DMA和PIO传输期间，驱动程序必须拥有措施保持高速缓存的一致性。使用DMA的驱动程序也必须使用适配器对象。

使用可编程I/O（PlO）而不是DMA的设备必须两次映射用户空间缓冲区到系统空间地址范围，如图3.6所示。

图3.6 使用PIO的设备的直接I/O

图3.6显示使用PIO的设备如何处理同样的任务。

1. 用户空间虚拟地址的某些范围代表当前线程的缓冲区，并且缓冲区的内容实际上可以被存储在一些物理上不连续的页上。如果缓冲区长度是零，I/O管理器创建MDL描述这个缓冲区。

2. I/O管理器为当前线程的读请求服务，线程为它传递代表缓冲区的用户空间虚拟地址的范围。

3. I/O管理器或者FSD检查用户提供的缓冲区的可访问性。如果I/O管理器已创建MDL，它使用MDL调用MmProbeAndLockPages，这个MDL为用户缓冲区指定了虚拟地址的范围。 MmProbeAndLockPages也填充MDL中相应的物理地址范围。

4. I/O管理器在请求传输操作的IRP中提供一个指向MDL（MdlAddress）的指针。直到驱动程序完成IRP后I/O管理器或者文件系统调用MmUnlockPages，在MDL中所描述的物理页仍然保持锁定并被分配到缓冲区。然而，这样的MDL中的虚拟地址能变得不可见（和无效），即使是在IRP被发送到设备驱动程序或者发送到可能在设备驱动程序之上的任何中间层驱动程序之前。

5. 如果驱动程序请求系统（虚拟的）地址，驱动程序使用IRP的MdlAddress指针调用MmGetSystemAddressForMdlSafe（Windows 2000）或MmGetSystemAddressForMdl（WDM），以两次映射MDL中的用户空间虚拟地址到系统空间地址范围。在图3.6中，AliasBuff代表描述双重映射的地址的MDL。

6. 驱动程序使用来自双重映射的MDL的系统空间虚拟的地址范围（AliasBuff），将数据读到内存中。

当驱动程序通过调用IoCompleteRequest完成IRP，如果驱动程序调用MmGetSystemAddressForMdlSafe（Windows 2000）或者MmGetSystemAddressForMdl（WDM）的话，I/O管理器或者文件系统释放MDL的双重映射的系统空间范围。

I/O管理器或者文件系统解锁在MDL中描述的页，并且根据驱动程序的需要处置MDL和IRP。要想得到较好的性能，驱动程序应该避免双重映射MDL物理地址到系统空间，如在步骤3中描述的，除非它们必须使用虚拟的地址。这样做没有必要使用系统页入口，并能降低驱动程序性能和伸缩性。此外，如果它运行出页表入口，系统可能崩溃，因为大多数较老的驱动程序不能处理这种情况。

仅仅在那个线程是当前的时候，当前用户线程的缓冲区和线程本身被保证常驻在物理内存中。对于在图3.6中所显示的线程，当另一个过程的线程被运行时，其用户缓冲区的内容可能被换到二级存储中。当另一个过程的线程被运行时，请求的线程的缓冲区的系统物理内存能被写，除非内存管理器已经锁定并且保存包含原来线程的缓冲区的相应物理页。

然而，当另一个线程当前正在运行的时候，其缓冲区的原始线程的虚拟地址并不保持是可见的，即使内存管理器保存缓冲区的物理页。因而，驱动程序不能使用由MmGetMdlVirtualAddress返回的一个虚拟地址来访问内存。为了使用基于包的系统或者总线控制器DMA传输数据，这个例程的调用者必须把其结果传递到MapTransfer（与IRP的MdlAddress指针一同）。
1.2.4.3 使用非直接也非缓冲的I/O

如果设备对象没有被创建使用直接和缓冲I/O，那么直接I/O管理器将传递原始用户空间虚拟地址，这个地址在被发送到驱动程序的IRP中。因为这样一种驱动程序必须在调用的线程的上下文中执行，以安全地访问其缓冲区，仅仅最高层驱动程序（例如FSD）能创建它们的设备对象，而不用使用DO_DIRECT_IO或者DO_BUFFERED_IO与每个设备对象的Flags作“或”操作。

中间或者最低层驱动程序不能总满足这个条件。例如，如果一个正在请求的线程等待一个I/O请求的完成，或者如果一个较高层驱动程序（特别是文件系统）位于中间层或者最低层驱动程序之上，那么较低层驱动程序的例程是不可能在请求的线程的上下文中被调用的。

当I/O管理器为缓冲区发送带有当前线程的用户空间虚拟地址的IRP时，没有使用DO_BUFFERED_IO或者DO_DIRECT_IO与设备对象的Flags作“或”操作的驱动程序必须作下列事情：

1. 检查用户缓冲区的地址范围的有效性，并检查是否适当的读或写被许可，这个可以用ProbeForRead和ProbeForWrite支持例程。驱动程序必须将它对缓冲区的地址范围的访问封入驱动程序提供的例外情况处理者中，从而使得在这个驱动程序访问内存时用户线程不能改变对缓冲区的访问权限。如果检测到发生了例外情况，驱动程序将返回错误。驱动程序必须调用作I/O请求的线程的上下文之内的例程；因此，只有较高层驱动程序能执行这种任务。

2. 以下列的方法之一管理缓冲区和内存操作：

§ 完成它自己的双缓冲区操作，就像I/O管理器为使用缓冲I/O的驱动程序做的那样。

§ 创建它自己MDL，并且通过调用内存管理器的支持例程锁定缓冲区，就像I/O管理器为使用直接I/O的驱动程序做的那样。

§ 在调用的线程的上下文中的用户缓冲区上直接执行所有必要的操作。驱动程序必须在驱动程序提供的例外处理器中封装它对缓冲区的访问，以防在驱动程序访问内存时，用户线程改变访问缓冲区的权限，或者改变缓冲区中的数据。

实际上，驱动程序必须在每IRP基础上选择是否做缓冲I/O、直接I/O、或者在调用线程的上下文中的I/O，并且它必须处理可能在用户模式线程上下文中发生的任何例外情况。驱动程序必须管理它自己的用户缓冲区访问、双缓冲区操作、和内存映射，当必要时，让I/O管理器代替为驱动程序处理这些操作。

1.3 适配器对象和DMA

如果驱动程序为直接I/O创建它的设备对象，并且它的设备使用DMA，则它必须使用系统创建的适配器对象，如在3.2.4.2中论述的那样。适配器对象代表DMA控制器通道或者端口，或者总线控制器设备。

两种最低层驱动程序必须使用适配器对象：

§ 使用系统DMA控制器（也称为从属设备）的设备的驱动程序。这样的设备被称为“使用系统DMA” 。

使用系统DMA 控制器的从属DMA设备的驱动程序必须有AdapterControl例程，并且调用系统提供的支持例程，这些支持例程为了完成DMA传输而操作适配器对象。细节参见“使用系统DMA”。

§ 是总线控制器适配器的设备的驱动程序。这样的设备为I/O总线的使用进行系统仲裁，然后使用总线控制器DMA。细节参见“使用总线控制器DMA”。

非SCSI的总线控制器DMA设备的驱动程序通常必须有AdapterControl例程，并且必须调用系统提供的支持例程，这些支持例程为了完成DMA传输而操纵适配器对象。

SCSI端口驱动程序为HBA特定的SCSI微端口驱动程序创建所有必要的适配器对象。当微端口驱动程序的HwScsiFindAdapter例程执行时，微端口驱动程序为SCSI端口驱动程序提供必要的数据，以创建相应的适配器对象。

简短地讲，控制总线控制器设备或连接到系统DMA控制器的从属设备的任何最低层驱动程序，必须明确地使用适配器对象（参见“获取适配器对象”）关联它的设备对象。驱动程序为指向适配器对象的指针提供存储，通常是在设备扩展中。此外，驱动程序拥有一个AdapterControl例程，这个例程调用系统提供的适配器对象支持例程以执行DMA传输。

作为设备启动的一部分，这些驱动程序调用I/O管理器，I/O管理器轮流调用平台特定的HAL来创建一组适配器对象。这样，在任何Windows 平台上，适配器对象组通常为下述东西包括一个适配器对象：

§ 每个系统DMA控制器通道或者端口，从属设备被附加到这个端口。

§ 机器中的每个总线控制器DMA设备。

除使用适配器对象之外，执行DMA的驱动程序使用三种不同的地址空间，如图3.7中所示。

图3.7 物理、逻辑、和虚拟的地址映射

在任何Windows 平台上，驱动程序拥有对处理器支持全部虚拟地址空间的访问。在32位处理器上，虚拟地址空间可以描述四个G（gigabyte）的空间。CPU用页表的方法将虚拟地址空间中的地址转换为系统物理地址空间中的地址。每个页表项（PTE）映射虚拟内存的一页到物理内存的一页，必要时作翻页操作。MDL（内存描述符列表）为与驱动程序DMA操作有关的缓冲区提供类似的映射。

设备根据它们的能力改变它们对系统所有虚拟地址空间的访问。设备在逻辑（设备）地址空间中使用地址。每个HAL使用映射寄存器把设备或者逻辑地址转换成为一个物理地址（物理RAM中的位置）。对于设备硬件，映射寄存器执行与MDL（和页表）为软件（驱动程序）执行的相同的功能：它们把地址转换成为物理的内存。

因为这些地址被分别编址，所以驱动程序不能在虚拟地址空间中使用指针在物理内存中的定位一个位置，反之亦然。驱动程序必须首先把虚拟的地址转换成为一个物理的地址。 同样地，驱动程序不能使用一个逻辑地址来访问物理内存； 它必须首先转换地址。
1.3.1 映射寄存器

HAL必须创建支持不同机器上的各种各样的DMA设备和I/O总线DMA的适配器对象。例如，大多数ISA DMA控制器、从属设备、和总线控制器设备没有足够地址线以访问32位处理器的全部四G系统物理地址空间。相比之下，EISA DMA设备一般有多于足够数量的地址线在32位处理器中访问全部的系统物理地址空间。因而，所有的HAL都提供映射，这些映射将DMA设备能访问的逻辑地址范围映射到每台单独机器的物理地址范围。

每个适配器对象与一个或多个映射寄存器有关，依赖于将要传递的数据的数量和可供使用的内存的数量。在DMA传输期间，HAL使用每个映射寄存器用设备可访问的逻辑页作为CPU中物理内存页的别名。实际上，映射寄存器为使用DMA的驱动程序提供发散/收集支持，而不管它们的设备是否有发散/收集能力。

图3.8说明这样一种物理-逻辑地址的映射，它是没有分散和收集能力的ISA DMA设备的驱动程序的。

图3.8 样本ISA DMA设备的地址映射

图3.8显示了下列类型的映射：

1. 每个映射寄存器为ISA DMA设备映射物理地址（实线所指的）的范围到低级逻辑地址（虚线）。

在这里，三个映射寄存器被用于将系统物理内存中的三个分页的范围别称为ISA DMA设备的低级逻辑地址。

2. 在DMA操作期间，ISA设备使用绘制映射的逻辑地址来访问系统内存。

对于一种可比较的EISA DMA设备，三个映射寄存器也将用于数据的三个页大小的范围。然而，映射的逻辑地址范围将没有必要与相应的物理地址范围相同，所以EISA设备也将使用逻辑地址来访问系统内存。

3. 每个MDL中的项映射虚拟地址空间中的位置到物理地址。

注意映射寄存器与MDL中虚拟-物理项之间的对应：

§ 对一个DMA传输操作，每个映射寄存器和MDL中的每个虚拟项映射至少一个完整的数据物理页。

§ 每个映射寄存器和MDL中的每个虚拟项可以映射小于一个完整的数据物理页。例如，MDL中的初始虚拟项可能从物理页边界映射一个偏移量，如图3.5所示。

§ 每个映射寄存器和MDL中的每个虚拟项映射最小是一个字节。

§ 在IRP中请求读或写操作，在Irp->MdlAddress的对驱动程序不透明的MDL中的每个虚拟项代表用户缓冲区的物理内存中的一个页边界。同样地，单独的DMA传输需要的每个额外映射寄存器代表设备可访问的逻辑地址范围中的页边界，这个逻辑地址是对系统物理地址的别称。

在所有的Windows 平台上，每个适配器对象拥有一个相关的集合，这个集合由一个或多个位于平台特定的（和对驱动程序不透明的）基地址上的映射寄存器组成。从驱动程序的观点来说，在图3.9中所显示的MapRegisterBase是一组映射寄存器的句柄，这些映射寄存器可以是芯片中的硬件寄存器，或者是在系统DMA控制器中，或者在总线控制器适配器中，或者甚至是系统内存中HAL创建的虚拟寄存器。

然而，可供适配器对象使用的映射寄存器的数目可以根据不同的设备和Windows平台而变化。例如，在某些平台上，HAL能创建更多的使用系统DMA的驱动程序可用的映射寄存器，因为不同Windows平台上的DMA控制器有不同的能力。
1.3.2 获取适配器对象

在设备启动时，驱动程序可以使用系统或者总线控制器DMA调用IoGetDmaAdapter以获得指向一个适配器对象的指针，并且确定可供每个传输操作使用的映射寄存器的最大数目。当驱动程序调用IoGetDmaAdapter时，I/O管理器依次调用HAL以获得必要的平台信息。

在驱动程序调用IoGetDmaAdapter时，它必须在系统定义的DEVICE_DESCRIPTION结构中提供一定的信息。在给DEVICE_DESCRIPTION结构赋值前，所有驱动程序必须使用RtlZeroMemory以用零来初始化这个结构。

需要的数据包括关于驱动程序的设备的特性信息，诸如如果设备有分散/收集能力，那么它是否是总线控制器，还有设备一次能传输的数据量是多少字节（MaximumLength）。

需要的设备描述数据也包括平台特定信息，诸如总线的平台特定的和系统指定的总线数量，这些总线由总线控制器的驱动程序来控制。驱动程序能通过调用IoGetDeviceProperty来获得这个信息。

DEVICE_DESCRIPTION结构包括一些域，这些域可以与一些DMA设备或驱动程序不相干。例如，WDM驱动程序中不使用BusNumber域。每个驱动程序应该为相关的结构成员提供值，并且应该为所有其他成员赋零。

从属设备的驱动程序不应该在ScatterGather领域中传递TRUE，除非当请求必须被拆分成两个或多个DMA操作时，这个设备能等待系统DMA控制器以重新被组织。

IoGetDmaAdapter返回一个指向适配器对象的指针和一个平台特定或设备特定的值，这个值指明对每个DMA传输操作，有多少映射寄存器是可供适配器对象使用的。

返回的适配器对象包含驱动程序可以访问的三个域：

§ 版本号（Version）

§ 大小（Size），

§ 指向DMA_OPERATION结构的指针（DmaOperations）

DMA_OPERATION结构组成一个指向函数的指针表，程序必须使用这些函数在其设备上执行DMA操作。函数只能通过这个数据结构中的指针被访问；驱动程序不能通过名称直接调用它们。（这些程序替换了在Windows NT的以前版本中支持的HalXxx。为了确保早期驱动程序的兼容性，wdm.h和ntddk.h头提供了使用过时名称的宏命令，但是新的驱动程序应该总是通过数据结构调用这些函数。）

映射寄存器的数目可以是因设备不同而不同，也可以是因平台不同而不同。通常，HAL根据下列标准分配一定数量的映射寄存器：

§ 如果可能，HAL返回一个值，这个值比传输MaximumLength字节所需要的映射寄存器的数目大一，MaximumLength在对IoGetDmaAdapter的驱动程序的调用中被指定。

§ 否则，HAL返回一个较小的值，这个值对特定的平台则是尽可能的大。

换句话说，HAL通常给每个驱动程序足够的映射寄存器，从而使得它的设备最大化DMA容许能力，但是HAL能在一些Windows 平台上返回较小的值。不能保证驱动程序将得到它所要求数量的映射寄存器，因此驱动程序应该总是检查返回值。

任何DMA设备驱动程序必须为IoGetDmaAdapter返回的适配器对象指针和NumberOfMapRegisters值提供存储。这个指针是系统提供的用于DMA的支持例程所要求的参数。因为许多这样的支持例程必须在IRQL DISPATCH_LEVEL上被调用，驱动程序分配的存储空间必须是常驻的。大多数DMA驱动程序在设备扩展中提供必要的存储。当然，如果驱动程序也使用控制器对象，存储可以在控制器扩展中，或者在驱动程序分配的非页式存储池中。更多信息参见第16章中的“分配系统空间内存和管理硬件优先级”。

当驱动程序已经完成了所有的DMA操作，它调用PutDmaAdapter释放适配器对象。
1.3.3 拆分传输请求

任何驱动程序可能需要拆分传输请求，并完成多个DMA操作，以满足给定的IRP，这依赖于下述条件：

§ IoGetDmaAdapter返回的NumberOfMapRegisters

§ 当前IRP中驱动程序的I/O栈位置的Length值（将要被传递的数据字节数）

§ 缓冲区的系统物理内存中页边界的数目，这个缓冲区是驱动程序用来传递数据的

§ 对驱动程序的DMA操作的任何设备特定约束

驱动程序能使用宏ADDRESS_AND_SIZE_TO_SPAN_PAGE来确定需要多少映射寄存器来传输所有在IRP中请求的数据，如下：

1. 调用MmGetMdlVirtualAddress为缓冲区得到起始虚拟地址，把指针传递到在Irp->MdlAddress的MDL。注意，驱动程序不可试图使用这个虚拟的地址访问内存。 被MmGetMdlVirtualAddress返回的值是进入MDL的一个索引，不必是一个有效的地址。

2. 传递IRP的驱动程序的I/O栈位置中返回的索引和Length值到ADDRESS_AND_SIZE_TO_SPAN_PAGE。

如果由ADDRESS_AND_SIZE_TO_SPAN_PAGE返回的值大于由IoGetDmaAdapter 返回的NumbertwRegisters，驱动程序不能为这种单一的DMA操作中的IRP传输所有被请求的数据。相反，它必须做下列事情：

1. 将缓冲区拆分成一定尺寸的片，以适合可用的映射寄存器的数目和任何设备特定DMA的约束。

2. 完成它能满足当前传输请求的同样多的DMA操作。

例如，假设ADDRESS_AND_SIZE_TO_SPAN_PAGE表明需要十二个映射寄存器来满足给定的传输请求，但是在这个平台上驱动程序只有五个可供使用的映射寄存器，并且没有设备特定的DMA约束。在这种情况下，驱动程序必须完成（l2/5+1=3）DMA传输操作，至少调用MapTransfer（通过DMA_OPERATION结构中返回的函数指针）三次，以传输被这个IRP请求的所有数据。

当没有足够的映射寄存器用单一的DMA传输操作满足给定的IRP时，系统DMA设备驱动程序使用各种各样的技术来拆分一个DMA传输。例如，存储类驱动程序被请求为位于下面的SCSI端口/微端口驱动程序拆分大的传输请求。系统存储类驱动程序为每片这样的IRP请求分配额外的IRP，并使用每个IRP注册它的IoCompletion例程，以跟踪整个传输请求的状态和释放驱动程序分配的IRP。然后，它把使用IoCallDriver发送IRP到端口驱动程序上。

然而，仅仅在类驱动程序能确定多少映射寄存器可供端口驱动程序使用的情况下，其他类/端口驱动程序才能使用这种技术。换句话说，端口驱动程序必须为成对的类驱动程序在注册表中存储这个配置信息，或者成对的驱动程序必须定义一个私有接口，使用内部设备I/O控制请求，从端口驱动程序到类驱动程序传递有关可供使用的映射寄存器的数目的配置信息。

其他类/端口驱动程序不必使用这种技术，而整体式DMA设备驱动程序必须为自己拆分大的传输请求。这样驱动程序通常拆分大的请求成若干块，并完成一系列DMA操作以满足当前IRP。

随后的小节（“使用系统DMA”和“使用总线控制器DMA”）描述DMA设备的整体式驱动程序如何使用支持例程以满足传输请求。这些节假定驱动程序拥有以下部分：

§ 一个标准的StartIo例程，而不是设置和管理IRP的一个内部队列。

§ 用于因为没有充足数量的可用映射寄存器而拆分传输请求的一个内部例程。

§ 没有设备特定的DMA约束。

换句话说，这些节为驱动程序的DMA操作描述了最简单的可能技术，但是单独的驱动程序不必使用完全相同的技术。对于DMA设备的任何驱动程序，那些驱动程序例程应该拆分较大的DMA传输请求取决于驱动程序模型（类/端口，整体式的，如此等等），依赖于设备的特性，以及依赖于驱动程序必须处理的任何设备特定的DMA约束。
1.3.4 使用系统DMA

从属DMA设备的驱动程序使用下列类型的系统提供的DMA支持中的一个：

§ 基于包的DMA，如果驱动程序不必使用系统DMA控制器的自动初始化模式的话。参见“基于包的系统DMA”。

§ 公用缓冲区DMA，如果驱动程序确实使用自动初始化模式的话。参见“公用缓冲区的系统DMA”。

此外，任何使用基于包的DMA的驱动程序能使用预期是流线型分散/收集DMA的支持例程，而不管其设备是否拥有内部的分散/收集支持。细节参见“执行分散/收集DMA”。
1.3.4.1 基于包的系统DMA

当它们处理一个请求DMA传输的IRP时候，使用基于包的DMA的任何从属设备调用下列的支持例程的普通序列：

1. KeFlushIoBuffers，仅仅在试图分配系统DMA控制器之前调用

更多关于在DMA期间使用KcFlushIoBuffers和FlushAdapterBuffers以保持高速缓存一致性的信息参见第16章。

2. AllocateAdapterChannel，当驱动程序准备好为DMA编程它的设备，并且需要系统DMA控制器的时候调用

反过来，AllocateAdapterChannel调用驱动程序的AdapterControl例程。

3. 得到进入MDL的索引的MmGetMdlVirtualAddress，它被要求作为对MapTransfer初始调用中的一个参数

4. MapTransfer，它为传输操作编程系统DMA控制器

驱动程序可能需要调用MapTransfer多次，以传输所有请求的数据，如“拆分传输请求”中所述的那样。

5. FlushAdapterBuffers，恰好在每个DMA传输操作之后被调用

如果驱动程序必须调用MapTransfer多次以传输所有请求的数据，它必须调用FlushAdapterBuffers与调用MapTransfer同样多的次数。

6. FreeAdapterChannel，要么在所有请求的数据被传输完毕就调用，要么在由于设备I/O错误造成驱动程序处理IRP失败时调用

被IoGetDmaAdapter返回的AdaPterObject指针被每个这样的例程（除了KeFlushIoBuffers和MmGetMdlVirtualAddress）要求作为一个参数，KeFlushIoBuffers和MmGetMdlVirtualAddress需要指向在Irp->MdlAddress传递的MDL的指针。

单独的驱动程序在不同的点上调用这个支持例程的序列，这依赖于每一驱动程序如何被实现服务其设备。例如，一种驱动程序的StartIo例程可以对AllocateAdapterChannel作调用，另一个驱动程序可以从一个例程（这个例程从驱动程序创建的互锁队列中移出IRP）中做这个调用，并且还可以在另一个驱动程序的奴隶DMA设备表明它准备好传输数据的时候作这个调用。

1.3.4.1.1 为基于包的DMA分配适配器通道

为了为基于包的系统DMA作准备，驱动程序在收到IRP_MJ_READ或者IRP_MJ_WRITE之后调用KeFlushIoBuffers和AllocateAdapterChannel。

在驱动程序之前把这些程序称为，对于IRP_MJ_READ和/或IRP_MJ_WRITE请求，或者对于请求DMA传输的任何其他请求，它的Dispatch例程应该已经检查IRP参数的有效性（如果必要的话）。Dispatch例程也可以为将来的处理已经将IRP排到另一个驱动程序例程。传输请求是请求设备I/O操作的当前IRP。

调用AllocateAdapterChannel的驱动程序例程在这个调用发生时必须在IRQL DISPATCH_LEVEL上执行。更多关于驱动程序标准例程在其上执行的IRQL的信息参见第5到15章，它们描述了每种标准例程。此外，在每个支持例程必须在其上被调用的IRQL，在《Windows 2000驱动程序开发参考》卷2它的描述中被特别指明。

与指向被IoGetDmaAdapter返回的适配器对象的指针一同，驱动程序在它调用AIIocateAdapterChannel的时候必须提供下列：

§ 指向目标设备对象的指针

§ 其AdapterControl例程的入口点

§ 指向AdapterControl例程将使用的任何驱动程序确定的上下文信息的指针

AllocateAdapterChannel排队驱动程序的AdapterControl例程，这个例程在系统DMA控制器被分配到这个驱动程序并且一组映射寄存器已经为驱动程序的DMA操作被分配的时候运行。

在入口处，AdapterControl例程被给DeviceObject和Context指针，它们在这个调用中被传递给AllocateAdapterChannel，与对分配的映射寄存器的操作（MapRegisterBase）一样。

如果驱动程序有StartIo例程，AdapterControl例程也被给一个指向DeviceObject->CurrentIrp的指针。如果驱动程序管理它自己的IRP（代替StartIo例程）队列，驱动程序将包括指向当前IRP的指针，把它作为在它调用AllocateAdapterChannel时传递的上下文的一部分。

AdapterControl例程典型地做下列事情：

1. 保存或初始化驱动程序维护的关于DMA操作的所有上下文。上下文可以包括输入MapRegisterBase柄，驱动程序必须传递它到MapTransfer和FlushAdapterBuffers，可能还包括请求的传输（从其IRP中的I/O栈位置）的Length。

2. 在MapTransfer之后调用MmGetMdlVirtuaIAddress。参见“为基于包的DMA安装系统DMA控制器”。

3. 创建从属设备以开始传输操作。

4. 返回值KeepObject。

所有的AdapterControl例程必须返回一个类型为IO_ALLOCATION_ACTION的系统定义的值。 对于使用系统DMA的驱动程序，AdapterControl例程必须返回值KeepObject。这允许驱动程序保持系统DMA控制器的“所有权”，并且分配映射寄存器，直到它已传输完所有请求的数据。

因为AdapterControl例程不能等待从属设备完成DMA操作，所以每个AdapterControl例程最少必须做如下事情：

1. 保存驱动程序设备扩展、控制器扩展、或者其他驱动程序可访问的常驻存储区域（驱动程序分配的非页式存储池）中的上下文信息，特别是MaPRegisterBase句柄。

2. 返回KeepObject。

当每个DMA传输操作完成时，另一个驱动程序例程（有可能是DpcForIsr）必须调用FlushAdapterBuffers。如果创建多次DMA控制器以满足当前IRP的传输请求是必要的，这个程序也必须再一次调用MapTransfer和FlushAdapterBuffers。

当驱动程序已经满足当前IRP请求，它必须调用FreeAdapterChannel。这个支持例程应该在为当前IRP最后一次调用FlushAdapterBuffers之后立即被调用，从而使得系统DMA控制器立即可供使用，以满足其他传输请求（任何驱动程序）。

具有发散/收集能力的从属设备的驱动程序也应该从其AdapterControl例程返回KeepObject。当驱动程序必须拆分给定的DMA请求时，设备必须能在系统DMA控制器被重新编程的期间（DMA操作之间）有等待能力。在一些Windows平台上，这样设备对每个DMA操作至多能传输一页的数据，因为HAL只能把一个单一的映射寄存器分配到那个设备的驱动程序。

1.3.4.1.2 为基于包的DMA设置系统DMA控制器

当AllocateAdapterChannel传输控制到驱动程序的AdapterControl例程，这个驱动程序“拥有”系统DMA控制器和一组映射寄存器。然后，驱动程序必须为传输操作创建DMA控制器，如图3.9所示。

如果驱动程序有StartIo例程，AllocateAdapterChannel把指向PIrp参数中的DeviceObject->CurrentIrP的指针传递到AdapterControl例程。然而，如果驱动程序管理它自己的IRP队列，驱动程序应该包括指向当前IRP的指针，将其作为它传递到AdapterControl的上下文中的一部分。

图3.9 编程系统DMA控制器

如图3.9所示，驱动程序的AdapterControl例程创建DMA传输，如下：

1. AdapterControl例程在开始传输处得到地址。对于被要求满足IRP的初始传输请求，AdapterControl例程调用MmGetMdlVirtualAddress，在Irp->MdlAddress传递指向MDL的指针，它为这个DMA传输描述缓冲区。

MmGetMdlVirtuaIAddress返回一个虚拟地址，驱动程序能使用这个虚拟地址作为传输应该在其处开始的系统物理地址的索引。

如果IRP请求多个传输操作，驱动程序计算一个更新的起始地址，如本节中后面所述的。

2. AdapterControl例程保存第一步中由MmGetMdlVirtualAddress返回的或计算的地址。这个地址是MapTransfer要求的一个参数（CurrentVa）。

3. AdapterControl例程调用MapTransfer以创建系统DMA控制器，提供下列的参数：

§ 被IoGetDmaAdapter返回的AdaPterObject指针

§ 在当前IRP的Irp->MdlAddress中的指向MDL的指针

§ 被AllocateAdapterChannel传递到驱动程序的AdapterControl例程的MapRegisterBase句柄

§ 被MmGetMdlVirtualAddress返回的值（CurrentVa），如果这是对IRP的MapTransfer第一次调用的话

否则，驱动程序提供更新的CurrentVa值，表明下一个传输操作中应该在缓冲区的什么地方开始。

§ 指向一个变量（Length）的指针，这个变量表明这次传输的字节数

如果驱动程序能使用一个单一的对MapTransfer的调用传输所有请求的数据，并且对其DMA操作没有设备特定的约束，Length能被设置成在IRP的驱动程序的I/O栈位置中的Length的值。至多，以字节为单位的长度可以是IoGetDma适配器返回的（PAGE_SIZE *）NumberOfMapRegisters。否则，驱动程序必须拆分请求，如“拆分传输请求”中所述，并且必须为当前IRP把后来调用MapTransfer中的Length的值予以更新。

§ 一个布尔的值（WriteToDevice），表明传输操作（TRUE表示从系统内存到设备的数据传输）的方向

MapTransfer返回一个逻辑地址。使用系统DMA的驱动程序必须忽略这个值。

4. AdapterControl例程为DMA操作创建设备。

5. AdapterControl例程返回KeepObject。

当设备表明其当前DMA操作已经完成，驱动程序应该调用FlushAdapterBuffers，通常从驱动程序的DpcForIsr例程调用。

完成DMA操作的DpcForIsr或者另一个驱动程序例程调用FlushAdapterBuffers，以保证任何在系统DMA控制器中高速缓存的数据被读到系统内存中或者写到设备。如果为当前IRP再编程系统DMA控制器以传输更多数据是必要的，相同的例程也必须再一次调用MapTransfer。同样地，它在每次传输操作后必须再一次调用FlushAdapterBuffers。

如果驱动程序必须为当前IRP多次调用MapTransfer，它在所有的呼叫中提供同样的AdaPterObject指针、Mdl指针、MopRegisterBase句柄、和传输方向。然而，在它作第二次和任何后来的对MapTransfer调用之前，驱动程序必须更新CurrentVa和Length参数。为这些参数中的每一个计算更新的值，使用下列的公式：

§ CurrentVa=CurrentVa+（在前面对MapTransfer的调用中被请求的Length）

§ Length=Minimum（剩余的将要传输的长度，（IoGetDmaAdapter返回的PAGE_SIZE * NumberOfMapRegisters））

每个驱动程序应该维护的有关其DMA传输的上下文信息依赖于其特殊设备的需要。典型地上下文可以在MDL（CurrentVa）中包括当前的虚拟地址、到当前为止被传输的字节数、传输剩余的字节数，还可能有指向当前IRP的指针，和驱动程序作者认为有用的任何其他信息。

当请求的传输完成，或者如果驱动程序必须为IRP返回一个错误状态，驱动程序必须迅速调用FreeAdapterChannel，从而释放系统DMA控制器以供其他驱动程序或本驱动程序使用。
1.3.4.2 公用缓冲区系统DMA

使用系统DMA控制器的自动初始化方式的驱动程序必须为一个缓冲区分配内存，DMA传输使用这个缓冲区完成操作。驱动程序调用AllocateCommonBuffer以得到这个缓冲区，典型地从处理IRP_MN_START_DEVICE 请求的DispatchPnP例程中获得。图3.l0显示了驱动程序如何分配缓冲区，并且映射它的虚拟地址范围到系统物理内存。

图3.l0 为系统DMA分配一个公用缓冲区

如图显示，驱动程序使用以下步骤为系统DMA分配缓冲区：

1. 驱动程序调用AllocateCommonBuffer，传递指向适配器对象（由IoGetDmaAdapter返回）的指针，连同它的缓冲区请求的长度。为了经济地使用内存，输入缓冲区长度值要么小于等于PAGE_SIZE，要么应该是PAGE_SIZE的倍数。

2. 如果AllocateCommonBuffer返回NULL指针，驱动程序应该释放任何它已经请求的系统资源，并且返回STATUS_INSUFFICIENT_RESOURCE以响应IRP_MN_START_DEVICE请求。

否则，AllocateCommonBuffer在系统虚拟地址空间中分配请求数量的内存，并返回指向那个缓冲区的两种不同类型的指针：

§ 缓冲区（图3.10的BufferLogicalAddress）的LogicalAddress，驱动程序必须为其提供存储，但是此后应该忽略它

§ 缓冲区（图3.l0的BufferVirtualAddress）的虚拟地址，这驱动程序也必须存储它，这样它能为DMA操作创建一个MDL，这个MDL描述它的缓冲区

驱动程序应该将这些指针存储在设备扩展中或者存储在其他驱动程序分配的常驻内存中。

3. 驱动程序调用IoAllocateMdl，从而为缓冲区分配MDL。驱动程序传递由AllocateCommonBuffer返回的其缓冲区的VirtualAddress和用来分配MDL的缓冲区的Length。

4. 驱动程序使用由IoAllocateMdl返回的指针调用MmBuildMdlForNonPagedPool，从而为它的常驻内存映射虚拟地址范围到系统物理内存

在分配一个公用缓冲区和映射它的虚拟地址范围之后，从属设备的驱动程序可以开始处理一个请求DMA传输的IRP。为了这样做，驱动程序调用下列支持例程：

1. RtlMoveMemory，它从锁定的用户缓冲区拷贝数据到驱动程序分配的公用缓冲区（为到设备的传输而分配的）

2. AllocateAdapterChannel，当驱动程序准备好为DMA编程它的设备，并且需要系统DMA控制器时被调用

3. MapTransfer，使用描述驱动程序分配的公用缓冲区为传输操作安装系统DMA控制器

注意，驱动程序仅仅调用MapTransfer一次，以创建系统DMA控制器来使用其公用缓冲区。 在传输期间，驱动程序能调用ReadDmaCounter以确定仍然有多少字节被传输，并且如果必要的话，调用RtlMoveMemory向或从用户缓冲区拷贝更多的数据。

4. FlushAdapterBuffers，当驱动程序已经完成它到/从从属设备的DMA传输时被调用

5. FreeAdapterChannel，一旦所有被请求的数据被传输，或者因为设备I/O错误驱动程序必须失败

被IoGetDmaAdapter返回的AdapterObject指针是除了RtlMoveMemory之外所有这些支持例程所需要的一个参数。

单独的驱动程序在不同的点调用这些一系列支持例程，这依赖于每个驱动程序如何被实现服务它的设备。例如，一种驱动程序的StartIo例程调用AllocateAdapterChannel，另一个驱动程序可以从一个例程中调用，这个例程从驱动程序创建的互锁队列中移走IRP，并且另一个驱动程序可以在其从属DMA设备表明它已经准备好传输数据的时候作这个调用。

1.3.4.2.1 为公用缓冲区系统DMA分配适配器通道

驱动程序在其针对IRP_MJ_READ和/或IRP_MJ_WRITE请求（或者请求DMA传输的任何其他请求）的Dispatch例程已经检查IRP的参数（如果必要的话）的有效性之后调用AllocateAdapterChannel，可能为将来的处理排队一个或多个IRP到另一个驱动程序，并且如果适当的话，可能连带被传输的数据加载它的普通缓冲区。

调用AllocateAdapterChannel的驱动程序例程在这个调用发生时，必须在IRQL DISPATCH_LEVEL上执行。更多关于驱动程序标准例程在其上执行的IRP的信息参见第5章到第15章，它们描述了所有这些例程。更多关于支持例程特定的IRQL请求的信息，参见《Windows 2000驱动程序开发参考》卷2中的支持例程的参考页。

AllocateAdapterChannel排队驱动程序的AdapterControl例程，这个例程在系统DMA控制器已经被分配到这个驱动程序和一组映射寄存器已经被分配驱动程序的DMA操作时运行。

在入口处，AdapterControl例程被给指针，这个指针指向设备对象和在AllocateAdapterChannel调用中被传递的上下文，被给的还有分配的映射寄存器的句柄。如果驱动程序有StartIo例程，AdapterControl例程也被给一个指向DeviceObject->CurrentIrp的指针。如果驱动程序管理它自己的IRP队列而不是拥有一个StartIo例程，驱动程序应该包括指向当前IRP的指针，并在调用AllocateAdapterChannel时作为它传递的上下文数据的一部分。

AdapterControl例程典型地做下列事情：

1. 保存或初始化任何驱动程序维护的关于DMA操作的上下文。这个上下文可以包括输入MapRegisterBase句柄，驱动程序必须传递这个句柄到MapTransfer和FlushAdapterBuffers，以及可能还包括请求的传输（从其IRP中的I/O栈位置）的长度。

2. 创建从属设备以开始传输操作

3. 返回值KeepObject

对于使用系统DMA控制器的自动初始化模式的驱动程序，AdapterControl例程必须返回值KeepObject。这允许驱动程序保持系统DMA控制器和分配的映射寄存器的“所有权”，直到它已传输完所有数据。

因为AdapterControl例程不能等候从属设备完成DMA操作，AdapterControl例程至少必须做下列事情：

1. 保存上下文信息，特别是MapRegisterBase句柄，到驱动程序设备扩展、控制器扩展、或者其他驱动程序可访问的常驻存储区域（驱动程序分配的非页式存储池）。

2. 返回KeepObject。

当DMA传输操作被完成，另一个驱动程序例程（有可能时DpcForIsr）必须调用FlushAdapterBuffers和FreeAdapterChannel。

1.3.4.2.2 为公用缓冲区DMA设置系统DMA控制器

当AllocateAdapterChannel传输控制到驱动程序的AdapterControl例程时，驱动程序“拥有”系统DMA控制器和一组映射寄存器。然后，在驱动程序为传输操作创建其设备之前，驱动程序必须调用MapTransfer以创建系统DMA控制器，来使用驱动程序分配的公用缓冲区。

驱动程序向MapTransfer提供下列的参数；

§ AdaPterObject指针，被IoGetDmaAdapter（参见3.3.2）返回

§ 指向MDL的指针，MDL描述驱动程序分配的公用缓冲区（参见3.3.4.2）

§ MapRegisterBase句柄，被AllocateAdapterChannel传递到驱动程序的AdaPteroontrol例程

§ 指向变量（Length）的指针，它指出在驱动程序分配的公用缓冲区的大小，以字节为单位

§ 布尔值，表明传输操作（TRUE表示从系统内存到设备的传输请求）的方向

MapTransfer返回一个逻辑地址，使用系统DMA的驱动程序必须忽略它。当MapTransfer返回控制，驱动程序应该为DMA操作创建其设备。驱动程序仅仅调用MapTransfer一次，但是继续在其公用缓冲区和锁定的用户缓冲区之间拷贝数据，直到完成请求的传输。

驱动程序能调用ReadDmaCounter以确定当前仍然有多少字节在公用缓冲区中被传输；驱动程序能继续用用户数据填充它的公用缓冲区，或者从其公用缓冲区拷贝数据到用户缓冲区，这依赖于DMA操作的方向。

当传输被完成，或者如果驱动程序必须为IRP返回错误状态，驱动程序调用FlushAdapterBuffers以保证任何被高速缓存在系统DMA控制器中的数据被读到系统内存中或被写到设备上。然后，驱动程序应该立即调用FreeAdapterChannel以释放系统DMA控制器，从而保证任何驱动程序（包括自己）的使用。
1.3.5 使用总线控制器DMA

总线控制器DMA设备的驱动程序能使用下列种类的系统提供的DMA支持：

§ 基于包的DMA，如果总线控制器适配器允许驱动程序确定什么时候做一个DMA传输操作，和/或什么时候为给定的IRP开始另一个传输操作。细节参见“基于包的总线控制器DMA”。

§ 公用缓冲区的DMA（也称为连续的DMA），如果总线控制器适配器不为驱动程序提供一个方法以容易确定什么时候一个传输操作将开始，或者什么时候传输完成的，或者如果一个单一的缓冲区区域被连续地或重复地用于DMA传输。细节参见“公用缓冲区总线控制器DMA”。

依赖于总线控制器适配器的性质，一些驱动程序只使用基于包的DMA，一些只使用公用缓冲区DMA，而一些则两者都使用。例如，使用邮箱策略（mailbox scheme）来通信状态信息和命令的总线控制器适配器的驱动程序与基于包的DMA一道，可以使用邮箱的一个公用缓冲区，这些邮箱被驱动程序和它的适配器共享。

设置一个公用缓冲区能束缚一些（或者所有，这依赖于请求的缓冲区的大小）与适配器对象有关的映射寄存器，这些对象代表总线控制器适配器，所以考虑下列设计方针：

§ 经济地使用公用缓冲区DMA。

经济地设置公用缓冲区区域，诸如以PAGE_SlZE块的方式或以单独分配的方式，为任何基于包的DMA操作留下更多的可供使用的映射寄存器。它也为其他目的留下更多自由的系统内存，然而这样做降低驱动程序和系统的整体性能。
1.3.5.1 基于包的总线控制器DMA

为了使用基于包的DMA，当总线控制器DMA设备的驱动程序处理一个请求DMA传输的IRP时，它们调用下列支持例程：

1. KeFlushIoBuffers，恰好在试图为传输请求分配映射寄存器之前调用

更多关于在DMA期间用KeFlushloBuffers和FlushAdapterBuffers保持高速缓存一致性的信息参见第16章。

2. AllocateAdapterChannel，当驱动程序为DMA准备好编程总线控制器适配器时调用

3. MmGetMdlVirtualAddress，调用它以得到进入MDL的索引，被要求作为MapTransfer的一个初始参数；MapTransfer，调用它使得返还到IRP的缓冲区的系统物理内存变得设备可访问

注意，任何驱动程序可能需要完成多于一个的传输操作，以满足当前IRP，如“拆分传输请求”中所述。没有发散/收集能力的设备的驱动程序能在每次传输操作时调用MapTransfer一次。有发散/收集能力的设备的驱动程序能调用多次MapTransfer以创建每个传输操作。作为选择，这些驱动程序能使用系统内部的发散/收集支持，这个在“执行发散/收集DMA”中描述。

4. FlushAdapterBuffers，在每个到/从目标设备的DMA传输操作末尾调用，用以确定是否所有请求的数据被完全传输

5. FreeMapRegisters，一旦或者当前IRP的所有DMA被完成就被调用，因为所有的请求数据已经被完全传输，或者因为设备或总线I/O错误导致驱动程序必须失败这个IRP

被IoGetDmaAdapter返回的AdapterObject指针是AllocateAdapterChannel、MapTransfer、FlushAdapterBuffers、和FreeMapRegisters所需的一个参数。注意，在Windows NT的以前的版本中，总线控制器设备可以把NULL的AdaPterObject指针传递到MapTransfer和FIushAdapterBuffers。在Windows 2000中，驱动程序不能再这样做。

KeFlushIoBuffers和MmGetMdlVirtualAddress在Irp->MdlAddress要求一个指向MDL的指针。

单独的驱动程序在不同的点调用这一系列的支持例程，这依赖于每一驱动程序如何被实现服务其设备。例如，一种驱动程序的StartIo例程可以调用AllocateAdapterChannel，而另一个驱动程序可以从一个例程做这个调用，这个例程从驱动程序创建的互锁队列或者设备队列中移去IRP。

代替本节中描述的如何使用例程，使用基于包的DMA的任何驱动程序能使用预计流水线发散/收集DMA的支持例程，而不管其设备是否拥有内部的发散/收集支持。细节参见“执行发散/收集DMA”。

1.3.5.1.1 分配总线控制器适配器对象

为准备基于包的总线控制器DMA，驱动程序在收到IRP_MJ_READ或者IRP_MJ_WRITE之后调用KeFlushIoBuffers和AllocateAdapterChannel。在驱动程序调用这些程序之前，它的Dispatch例程应该检查IRP参数（如果必要的话）的有效性。它也可为进一步处理将IRP排队到另一个驱动程序例程。传输请求是请求设备I/O操作的当前IRP。

调用AllocateAdapterChannel的驱动程序例程在调用发生时，必须在IRQL DISPATCH_LEVEL上执行。更多关于驱动程序标准例程在其上执行的IRQL的信息参见第5章到第15章，它们描述了标准例程。更多关于支持例程特定的IRQL请求的信息，参见《Windows 2000驱动程序开发参考》卷2中的支持例程的参考页。

与指向适配器对象（被IoGetDmaAdapter返回）的指针一同，当驱动程序调用AllocateAdapterChannel时，它必须提供下列：

§ 指向当前IRP的目标设备对象的指针

§ 其AdapterControl例程的入口点

§ 指向AdapterControl例程将使用的任何任何驱动程序确定的上下文信息的指针

AllocateAdapterChannel排队驱动程序的AdapterControl例程，这个例程在下述时候运行：适配器对象被释放以及一组映射寄存器为驱动程序的DMA操作（到或从目标设备）已经被分配。

在入口处，Adapteroontrol例程被给DeviceObject和Context指针，它们被传递到AllocateAdapterChannel调用中，还被给分配的映射寄存器的句柄（MapRegisterBase）。

如果驱动程序有StartIo例程，AdapterControl例程也被给指向DeviceObject->CurrentIrp的指针。如果驱动程序管理它自己IRP队列，而不是拥有一个StartIo例程，驱动程序将包括一个指向当前IRP的指针，将它作为它调用AllocateAdapterChannel时传递的上下文的一部分。

对于没有发散/收集能力的总线控制器DMA设备的驱动程序，AdapterControl例程通常做下列事情：

1. 保存或者初始化驱动程序维护的关于DMA操作的任何上下文。上下文可以包括输入MopRegisterBase句柄，驱动程序必须传递这个句柄到MapTransfer和FlushAdapterBuffers，还包括其IRP中的I/O栈位置请求的传输的字节的Length，如此等等

2. 在MapTransfer（在“设置描述传输操作”中描述）之后调用MmGetMdlVirtuaIAddress，以得到其设备能使用的逻辑地址，从而开始传输操作

3. 创建总线控制器适配器以开始传输操作

4. 返回值DeallocateObjectKeepRegisters

对于带有发散/收集能力的总线控制器设备的驱动程序，AdapterControl例程通常做下列事情：

1. 保存或者初始化驱动程序维护的关于DMA操作的任何上下文。上下文可以包括输入MopRegisterBase句柄，驱动程序必须传递这个句柄到MapTransfer和FlushAdapterBuffers，还包括其IRP中的I/O栈位置请求的传输的字节的Length，如此等等

2. 在MapTransfer（在“设置描述传输操作”中描述）之后调用MmGetMdlVirtuaIAddress，以得到其设备能使用的逻辑地址，从而开始传输操作

AdapterControl例程反复地调用MapTransfer，直到它已使用所有可供使用的映射寄存器为总线控制器适配器建造发散/收集表。

3. 创建总线控制器适配器以开始传输操作

4. 返回值DeallocateObjectKeepRegisters

注意，执行总线控制器DMA的驱动程序能使用GetScatterGatherList和PutScatterGatherList例程，而不管它们的设备是否支持发散/收集DMA。使用这些程序改变对驱动程序的AdapterControl例程的请求；细节参见“执行发散/收集DMA”。

AdapterControl例程必须返回系统定义的类型为IO_ALLOCATION_ACTION的值。对于使用总线控制器DMA的驱动程序，AdapterControl例程应该典型地返回值DeallocateObjectKeepRegisters，这允许驱动程序为目标设备对象保持分配的映射寄存器，直到它已为当前IRP传输所有请求的数据。在传输完成之后，DPC程序应该调用FreeMapRegisters以释放分配的映射寄存器。然而，以防设备不支持命令排队，在当前IRP的传输完成时，AdapterControl例程能返回KeepObject，此时DPC例程调用FreeAdapterChannel。

AdapterControl例程不能等候总线控制器适配器完成DMA操作。

无论总线控制器适配器是否支持发散/收集，AdapterControl例程至少必须做下列事情：

1. 保存必要的上下文信息——特别是MapRegisterBase句柄——在驱动程序设备扩展、控制器扩展、或者其他驱动程序能访问的常驻存储区域（非页式存储池，被驱动程序分配）内。当驱动程序调用MapTransfer和FlushAdapterBuffers，它必须传递这个句柄。

2. 返回DeallocateObjectKeepRegisters。

当每个DMA传输操作被完成时，另一个驱动程序例程（有可能是DpcForIsr）必须调用FlushAdapterBuffers。这种程序也必须创建任何必要的附加DMA操作，以满足当前IRP。

当驱动程序已满足当前IRP传输请求或者因为设备或者总线I/O错误必须失败IRP，它必须调用FreeMapRegistcrs。这个调用应该在为当前IRP的最后一个对FlushAdapterBuffers的调用之后立即发生，这样使得驱动程序能服务其他DMA请求，有可能是总线上的其他设备。

1.3.5.1.2 设置传输操作

当AllocateAdapterChannel传输控制到驱动程序的AdapterControl例程的时候，它已分配一组映射寄存器。然而，驱动程序必须将当前IRP的传输请求的系统物理内存映射到总线控制器适配器的逻辑地址范围，如下：

1. 使用在Irp->MdlAddress的MDL调用MmGetMdlVirtualAddress，从而为传输应该开始的系统物理地址得到索引。

返回值是MapTransfer要求的参数（CurrentVa）。

2. 调用MapTransfer以映射IRP缓冲区的系统物理地址范围到总线控制器适配器的逻辑地址。

驱动程序能为传输操作创建适配器。图3.11显示与设置传输有关的步骤。

图3.11 设置DMA的逻辑范围

如图3.11所示，驱动程序的AdapterControl例程创建总线控制器DMA操作，如下：

1. AdapterControl例程得到开始传输的地址。对于被要求满足IRP的初始传输，AdapterControl例程调用MmGetMdlVirtualAddress，传递指向在Irp->MdlAddress的MDL的指针，它描述这个DMA传输的缓冲区。

MmGetMdlVirtualAddress返回一个虚拟地址，驱动程序能使用它作为传输应该开始的系统物理地址的一个索引。

如果IRP请求多个传输操作，驱动程序计算一个更新的起始地址，这在本节后面描述。

2. AdapterContro1例程保存由MmGetMdlVirtualAddress返回的或在步骤1中计算的地址。 这个地址是MapTransfer需要的参数（CurrentVa）。

3. AdapterControl例程调用MapTransfer，它返回一个驱动程序编程总线控制器适配器以开始传输操作的逻辑地址。在对MapTransfer的调用中，驱动程序提供下列的参数：

§ AdapterObject指针，被IoGetDmaAdapter（参见3.3.2）返回

§ 指向在当前IRP的Irp->MdlAddress的MDL的指针

§ MapRegisterBase句柄，通过AllocateAdapterChannel（参见“分配总线控制器适配器对象”）传递到驱动程序的AdapterControl例程

§ 被MmGetMdlVirtualAddress返回的值，如果这是对当前IRP的MapTransfer的第一次调用

否则，驱动程序提供更新的CurrentVa值，表明下一个物理-逻辑映射被做。（如何计算更新的CurrentVa在本节中后面叙述。）

§ 指向变量（Length）的指针，它表明这个传输的字节数

如果驱动程序有足够映射寄存器，可以在单一的DMA操作中传输所有请求的数据，并且没有对其DMA操作的设备特定约束，Length能被设置成IRP的驱动程序I/O栈位置中Length的值。至多，输入长度可以是（PAGE_SIZE *）NumberOfMapRegisters（由IoGetDmaAdapter返回）。否则，驱动程序必须拆分请求，如3.3.3中所述，并且必须为当前IRP在后来的MapTransfer调用中更新Length值。

§ 布尔值（WriteToDevice），表明传输操作（TRUE表示从内存到设备传输数据）的方向

4. AdapterControl例程为DMA操作创建设备。

5. AdapterControl例程返回DeallocateObjectKeepRegisters。

如果驱动程序必须调用MapTransfer多次以满足当前IRP，它在所有的MapTransfer中提供相同的AdapterObject指针、Mdl指针、MapRegisterBase句柄、和传输方向。然而，驱动程序必须在对MapTransfer的第二次以及后续调用中提供更新的CurrentVa和Length值。使用下列的公式来计算这些值：

§ CurrentVa=CurrentVa+（在前面对MapTransfer中请求的Length）

§ Length=Minimum（剩余的将要传输的长度，（被IoGetDmaAdapter返回的PAGE_SIZE*NumberOfMapRegisters））

每个驱动程序应该维护的有关其DMA传输的上下文信息依赖于其特殊设备的需要。典型地上下文可以包括MDL（CurrentVa）中的当前虚拟地址、到目前为止传输的字节数、可能还有指向当前IRP的指针。

对于带有发散/收集能力的设备的驱动程序，Length参数对于MapTransfer既是输入参数又是输出参数。在从MapTransfer返回时，它表明系统已映射数据的字节数。长度的返回值，与返回的逻辑地址结合起来，表明逻辑地址的范围，这里的逻辑地址范围是总线控制器适配器能为这个DMA操作中的一块的传输所使用的。

由于Length被MapTransfer重写，符合下面这个实现方针：

如果设备支持发散/收集，从不把指向IRP的驱动程序I/O栈位置中的Length作为MapTransfer的参数。

这样做可能会毁坏当前IRP中的值，使得不可能确定驱动程序是否已传输所有请求的数据。

在每个DMA操作末尾，驱动程序必须用一个有效的AdapterObject指针和MapRegisterBase句柄，调用FlushAdapterBuffers以确信所有数据已经被传输（见第16章），并且释放当前DMA操作的物理-逻辑映射。如果驱动程序必须创建附加的DMA操作以满足当前IRP，在每一种传输操作完成之后，它必须调用FlushAdapterBuffers。

当所有请求的传输被完成，或驱动程序必须为IRP返回错误状态，驱动程序应该在其最后一次调用FlushAdapterBuffers之后立即调用FreeMapRegisters，从而最大化总线控制器适配器的吞吐量。在其调用FreeMapRegisters中，驱动程序必须传递它在前面对AllocateAdapterChannel的调用中传递的AdapterObject指针。
1.3.5.2 公用缓冲区的总线控制器DMA

为了为总线控制器DMA创建一个公用缓冲区，总线控制器DMA设备驱动程序必须使用由IoGetDmaAdapter返回的适配器对象指针调用AllocateCommonBuffer。典型地，驱动程序在其为PnP IRP_MN_START_DEVICE请求的Dispatch例程中调用它。当驱动程序仍然是加载期间，驱动程序仅仅在它将为其DMA操作重复地使用缓冲区的时候，应该分配一个公用缓冲区。图3.12说明了这样一个对AllocateCommomBuffer的调用。

图3.12 为总线控制器DMA分配一个公用缓冲区

缓冲区请求的大小（在图3.12中是LengthForBuffer）确定多少映射寄存器必须被使用从而为公用缓冲区提供虚拟-逻辑映射。映射寄存器数至多可以是（BYTES_TO_PAGE（LengthForBuffer））的值。这个值不能大于IoGetDmaAdapter返回的NumberofMapRegister。更多关于驱动程序能使用的系统提供的宏命令的信息，诸如BYTES_TO_PAGE，参见《Windows 2000个驱动程序开发参考》的卷2。

此外，调用者必须提供下列信息：

§ 一个布尔值，表明高速缓存是否应该可用

§ 指向驱动程序定义的变量的指针，它将包含AllocateCommonBuffer返回的缓冲区（图3.l2中的BufferLogicalAddress）的设备可访问的基础逻辑地址

如果调用成功，AllocateCommonBuffer为缓冲区（图3.l2的BufferVirtualAddress）返回一个驱动程序可访问的基础虚拟地址，驱动程序必须保存它在其设备扩展、控制器扩展、或者其他驱动程序可访问的的常驻存储区域（被驱动程序分配的非页式存储池）中。

如果AllocateCommonBuffer不能为缓冲区分配内存，它返回NULL。如果返回的基础虚拟地址是NULL，驱动程序必须专门使用系统的基于包的DMA支持，或者驱动程序必须使IRP_MN_START_DEVICE请求失败，返回STATUS_INSUFFICIENT_RESOURCE。

否则，驱动程序能使用分配的公用缓冲区作为驱动程序的或适配器的可访问的DMA传输存储区域。

当PnP管理器发送一个IRP以停止或者移去设备，驱动程序必须调用FreeCommonBuffer以释放它已分配的每个公用缓冲区。更多的关于处理PnP的信息停止和移去IRP的信息，参见《即插即用、电源管理、和安装设计指南》。
1.3.6 执行发散/收集DMA

执行系统或者总线控制器基于包的DMA的Windows 2000和WDM驱动程序能使用特别是为发散/收集DMA设计的支持例程。代替在“基于包的系统DMA”和“基于包的总线控制器”中调用一系列例程，驱动程序能使用GetScatterGatherList和PutScatterGatherList。

设备不需要为它的驱动程序拥有内部的发散/收集支持，就可以使用这些例程。

为发散/收集操作使用基于包的DMA的驱动程序调用下列支持例程：

1. KeFlushIoBuffers，在调用GetScatterGatherList之前调用

更多信息参见第l6章中的“为DMA和PIO保持高速缓存一致性”。

2. MmGetMdlVirtualAddress，调用它得到进入MDL的索引，在对GetScatterGatherList的调用中被要求作为参数

3. GetScatterGatherList，当驱动程序准备好为DMA编程它的设备，并且需要系统DMA控制器或者总线控制器适配器

GetScatterGatherList分配系统DMA控制器或者总线控制器适配器，确定多少映射寄存器被请求，并且分配它们，填写发散/收集列表，并且在DMA控制器或适配器和映射寄存器可用时调用驱动程序的AdapterListControl例程。

4. PutScatterGatherList，一旦所有请求的数据被传输或者由于设备I/O差错造成驱动程序使IRP失败时被调用

PutScatterGatherList刷新适配器缓冲区，释放映射寄存器，并且释放发散/收集表。 在它能在缓冲区中访问数据之前，驱动程序必须调用PutScatterGatherList。

被IoGetDmaAdapter返回的AdapterObject指针是除KeFIushIoBuffers和MmGetMdIVirtuaIAddress之外每个这样的例程所需要的参数，KeFIushIoBuffers和MmGetMdIVirtuaIAddress请求一个指向在Irp->MdlAddress的MDL的指针。

GetScatterGatherList组合了AllocateAdapterChannel和MapTransfer例程的特性。它需要下列参数：

§ 指向由IoGetDmaAdapter返回的DMA_ADAPTER结构的指针

§ 指向DMA操作的目标设备对象的指针

§ 指向在Irp->MdlAddress中描述缓冲区的MDL的指针

§ 指向被Mdl所描述的缓冲区中的当前虚拟地址的指针

§ 将被映射的字节数

§ 指向执行传输的AdapterListControl例程的指针

§ 指向被传递到AdapterListControl例程的驱动程序定义的上下文区域的指针

§ 布尔值；到设备的传输是TRUE；否则FALSE

在确定要求的映射寄存器数量、分配适配器通道和映射寄存器、填写发散/收集表并准备传输之后，GetScatterGatherList调用系统提供的AdaPterListControl例程。AdapterListControl例程在一个专用线程环境中在IRQL DISPATCH_LEVEL上被运行。

在调用GetScatterGatherList时，驱动程序提供的AdapterLstControl例程不同于传递到AllocateAdapterChannel的AdapterControl例程，主要有以下几个方面：

§ AdapterListControl例程没有返回值，而AdapterControl例程返回IO_ALLOCATION_ACTION。

§ 不是指向系统分配映射寄存器的MapRegisterBase的指针，AdapterListControl例程的第三个参数用SCATTER_GATHER_LIST结构替换，驱动程序通过这个结构能执行DMA。

§ AdapterListControl例程执行在适配器控制程序中要求的任务的子集。

AdapterListContol程序不调用AllocateAdapterChannel或者MapTransfer。其唯一的责任是保存输入发散/收集表指针，创建其设备，并且使用发散/收集表来执行DMA。

发散/收集表结构包括一个SCATTER_GATHER_ELEMENT的数组和数组中的单元数。每个数组的单元提供长度以及物理的连续区域的开始物理地址。驱动程序在数据传输中使用长度和地址。

驱动程序能使用GetScatterGatherList，而不管其设备是否支持发散/收集DMA。对于不支持发散/收集DMA的设备，发散/收集表将包含仅仅一个单元。

使用发散/收集程序比调用AllocateAdapterChannel（以前在3.3.4.1和3.3.5.1中描述） 能改进性能。不同于AllocateAdapterChannel，多次对GetScatterGathcrList的调用能在任何一次为设备对象被排队。在驱动程序的AdapterListControl例程已经完成执行之前，驱动程序能为另一个相同驱动程序对象上的DMA操作再一次调用GetScatterGatherList。

当从驱动程序提供AdapterListControl例程返回时，GetScatterGatherList保持映射寄存器，但是释放DMA适配器结构。

当驱动程序已满足当前IRP传输请求，或者因为设备或者总线I/O错误必须失败IRP，在它能在缓冲区中访问传输的数据之前，它必须调用PutScatterGatherList。PutScatterGatherList刷新适配器缓冲区，并且释放映射寄存器和发散/收集表。

1.4 控制器对象

如其名称暗示的那样，控制器对象通常代表一个附加了设备的物理设备控制器。物理控制器所协同的一组相似设备的最低层Windows 2000驱动程序可以创建控制器对象，并且使用它来使附加设备之间的I/O操作同步。驱动程序执行ControllerControl例程，并且调用I/O管理器的控制器对象支持例程。在WDM驱动程序中，对控制器对象的使用不被支持。

一般地，如果下列的标准成立，驱动程序使用控制器对象来使操作同步到附加设备：

§ 没有中断控制器不能完成长的操作，因此驱动程序不需要创建设备专用线程或者使用系统工作者线程。

§ 连接到控制器的设备是类似的。就是说，它们不是拥有完全不同物理属性或者操作功能的设备，例如能连接到键盘和辅助设备控制器的键盘和鼠标设备。

§ 驱动程序被设计成是整体式的：单层的涉及设备控制器和附加物理设备，而非被设计成使用在端口驱动程序上的一个或多个类驱动程序（对于附加的设备）的端口驱动程序（对于控制器）。

带有I/O通道和一组逻辑设备对象的驱动程序也可以使用控制器对象以在这样的设备的通道中间同步它们的I/O操作。

控制器对象没有名称，于是不是I/O请求的目标。对于来自一组设备对象的连续I/O，它是一种简单的同步机制。因为控制器对象没有名称，它对用户模式的保护子系统是不可见的，没有获得代表目标设备对象的文件对象的句柄，它不能做设备I/O请求。控制器对象对较高层驱动程序是也不可见的，这不能把它们自己设备对象附加到控制器对象。换句话说，既不是I/O管理器也不是一个较高层驱动程序能创建在被控制器对象代表的设备上请求I/O 的IRP。I/O请求总被发布到设备对象。仅仅驱动程序能使用控制器对象。

同步和重复I/O

拥有像“AT”盘控制器（参见第3部分）一样特性的物理设备的整体式驱动程序没有被要求使用控制器对象以使它们的设备I/O操作同步。例如，驱动程序作者可以试一试类似下列同步技术的方法来代替使用控制器对象：

创建命名的设备对象来代表是I/O请求目标的设备。在每个设备扩展或者单一设备扩展中维护有关哪个设备对象是当前I/O操作的目标的状态（或许是一组DeviceBusy标记）。为当前忙的设备对象完成I/O，并且为其他设备对象再排队进入的IRP，直到当前IRP被完成。

前面的同步化技术为所有驱动程序的目标设备对象序列化IRP处理。注意，它也强迫这样的驱动程序在其StartIo例程能开始处理下一个IRP之前完成当前IRP，进而降低驱动程序性能。

如果某种设备操作能被重复，使用控制器对象能增加驱动程序的I/O吞吐能力，因为这种同步化技术允许驱动程序在它创建物理设备之前确定它是否能重复操作。例如，盘控制器可以允许驱动程序重复在一个盘上搜索而在另一个盘上做读和写操作。

此外，使用控制器对象是在诸如“AT”盘控制器的单独的物理设备上同步化多个目标设备I/O操作的简单方法。使用控制器对象允许整体式驱动程序同步经过一组命名的设备对象的I/O操作，而没有必要在一个或多个设备扩展中维护每个设备和设备控制器的状态，并且没有必要重新排队IRP。

然而，被设计成重复I/O操作的一些设备，例如双工序列控制器或者总线控制器适配器，一般确实拥有为IRP创建内部队列的驱动程序。细节参见“设备队列对象和互锁队列”和第7章，“ StartIO和队列管理例程”。
1.4.1 创建带有控制器扩展的控制器对象

如果驱动程序使用控制器对象，在它已创建设备对象和其设备为I/O作好准备之后，它必须调用IoCreateController，典型地是在收到PnP IRP_MN_START_DEVICE请求之后。图3.13说明这个调用。

图3.13 控制器对象

像驱动程序创建的设备对象一样，所有的控制器对象拥有相关的控制器扩展。如图3.13所示，IoCreateController的调用者确定控制器扩展的大小。其结构和内容是由驱动程序定义的。

除了驱动程序维护的物理设备（或带有通道的设备）的设备特定的状态信息，图3.13还显示了控制器扩展的驱动程序定义的数据的一个代表集。

PtrToControllerObject返回IoCreateController必须被传递到驱动程序对IoAllocateController和IoFreeController调用中，这在“为I/O操作分配控制器对象”中描述。驱动程序必须将返回的控制器对象指针存储在其驱动程序创建设备对象的设备扩展或者在另一个驱动程序可访问的常驻存储区域（非页式存储池，由驱动程序分配）中。如果驱动程序被卸载，它也必须把控制器对象指针传递到IoDeleteController。

大多数创建控制器对象的驱动程序寻求方便的方法保存指向当前目标设备对象或控制器扩展中的设备扩展的指针。通常，这样的驱动程序将控制器对象指针存储在其每个设备扩展中，这样它能使用ControllerObject->ControllerExtension指针访问驱动程序保持的、所有目标设备对象的I/O操作的控制器特定状态。

如果被控制器对象代表的物理控制器产生中断，驱动程序也能使用控制器扩展作为对IoConnectInterrupt返回的PtrToInterruptObject存储位置。更多信息参见“中断对象”。

IoCreateController为控制器对象和扩展分配常驻存储区域，它用零初始化这个存储区域。如果它不能分配内存，IoCreateController返回NULL指针。如果这发生，驱动程序必须使设备启动失败，并将返回STATUS_INSUFFICIENT_RESOURCE。
1.4.2 I/O操作分配控制器对象

在使用控制器对象的驱动程序已开始其设备之后，它准备好处理被发送到其目标设备对象的IRP。只要当前IRP为I/O操作请求驱动程序编程控制器对象代表的物理设备，控制器对象驱动程序就调用IoAllocateController。图3.14说明这样一个调用。

如图3.14所示，驱动程序在其IoAllocateController时，必须提供多个由IoCreateController返回的ControllerObject指针。与这个指针一同，它必须把指针传递到代表当前I/O请求的目标设备对象，传递到驱动程序提供ControllerControl例程，和任何Context，它的ControllerControl例程将需要这个Context来为请求的I/O操作创建设备上下文。

如果控制器对象代表的设备已经忙于为目标设备对象做I/O ，IoAllocateController排队驱动程序提供ControllerControl例程。否则，ControllerControl例程立即被调用，这个调用使用图3.14中所示的输入参数。当驱动程序的ControllerControl例程运行时，指向IoAllocateController的输入Context指针被传递到这个例程。

图3.14 为I/O分配控制器对象

考虑下列的设计方针以确定这样的上下文区域的位置：

§ 驱动程序提供的上下文区域不应该在控制器扩展中，除非驱动程序在开始在另一个物理控制器之上的操作之前处理每个IRP到完成。否则，控制器扩展中的上下文区域可以被其他的驱动程序例程或在收到新的IRP时被重写。

§ 即使驱动程序为另一个设备对象重复设备I/O操作，目标设备对象的设备扩展中的上下文区域不能被重写。

§ 如果为一个特殊的设备对象另一个I/O请求被做，并且驱动程序拥有StartIo例程，其设备扩展中的上下文区域也不能被重写，因为在驱动程序调用IoStartPacket时，引入的IRP将被排队，并且同样的IRP将保留在设备队列中，直到驱动程序调用IoStartNextPacket，这个调用是在它为那个设备对象完成当前IRP之前做的。

如果驱动程序有StartIo例程，I/O管理器把指向DeviceObject->CurrentIrp的指针传递到ControllerControl例程。如果驱动程序管理它自己的IRP队列，而不是拥有StartIo例程，I/O管理器不能将指向当前IRP的指针给ControllerControl例程。当驱动程序调用IoAllocateController，它应该传递当前IRP以作为Context可访问数据的一部分。

调用IoAllocateController的驱动程序例程在这个调用发生时必须在IRQL DISPATCH_LEVEL上被执行。从它的StartIo例程做这个调用的驱动程序已经在DISPATCH_LEVEL上运行。更多关于驱动程序的标准例程在其上执行的IRQL的信息，参见第5章到第15章，或者第16章中关于管理IRQ的节。更多关于支持例程特定的IRQL请求的信息，参见《Windows 2000驱动程序开发参考》卷2。

ControllerControl例程为IRP的请求操作创建物理控制器。

如图3.14所示，ControllerControl例程返回类型为IO_ALLOCATION_ACTION的值，它是下列系统定义的值之一：

§ 如果ControllerControl例程能在物理控制器上开始另一个操作，它应该返回DeallocateObject，使得驱动程序能重复下一个请求I/O操作。

例如，如果ControllerControl例程能为一个在盘上的寻找操作编程磁盘控制器，完成那个IRP，并返回DeallocateObject，如果任何传输请求当前被排队到其他磁盘，ControllerControl例程能被再一次调用，从而为在其他盘上的传输操编程磁盘控制器。

§ 如果当前IRP通过其他驱动程序例程请求进一步处理，ContollerControl例程必须返回KeepObject。

例如，如果驱动程序为传输操作编程磁盘控制器，但是直到传输完成还不能完成IRP，ControllerControl例程必须返回KeepObject。

当ControllerControl例程返回KeepObject，通常在设备中断时驱动程序的ISR运行，并且其DpcForIsr例程为目标设备对象完成I/O操作和当前IRP。

无论什么时候ControllerControl例程返回KeepObject，完成IRP的例程必须调用IoFreeController。这样的驱动程序例程尽快地调用IoFreeController，这样使得下一个设备I/O操作能快速地被创建。

1.5 中断对象

每个产生中断的物理设备的驱动程序在驱动程序开始设备时必须注册它的ISR。内核定义中断对象类型以跟踪以下信息：每个中断向量、其系统指定的DIRQL、当中断发生时被调用的ISR，如此等等。

所有的驱动程序应该是对所有Windows NT/Windows 2000平台可移植的，包括配有若干不同类型 （ISA，EISA，PCI如此等等）的I/O总线的SMP机器。因而，当最低层驱动程序和系统本身被装载时，下面列出的是可配置的：

§ 处理器组，中断能在其上发生

对于任何Windows NT和Windows 2000平台，一组处理器确定多少中断对象必须被创建以注册设备驱动程序的ISR。在SMP机器中，每个设备驱动程序可以请求每处理器一个中断对象。 然而，系统能执行加载平衡，方法是通过限制任何特殊设备中断到可供使用的处理器子集。PnP管理器在列举期间和在设备启动时为这个信息请求总线驱动程序，传递信息到未经加工和转换的资源中。

§ 系统提供的中断向量，为机器中每个I/O总线上的每个设备提供的

对于Windows NT/Windows 2000多总线机器，每个I/O总线上的设备的总线相关的中断向量必须被映射到一组系统指定的中断矢量。PnP管理器在列举期间为这个信息请求总线驱动程序。它然后映射向量，并在设备启动时分别传递它的总线相关的和映射的向量（在未经加工的和转换的资源结构中）给每个驱动程序。

§ 被分配到每个系统中断向量的DIRQL

对于最低层驱动程序，被分配到其映射的系统向量的DIRQL值确定驱动程序的ISR在其上执行的硬件优先级。

平台特定的HAL为每个Windows NT和Windows 2000机器中的I/O总线、外围设备、和处理器提供硬件层支持给内核和I/O管理器，如图3.1所示。PnP管理器也与HAL通讯，以获得对使用中断对象的驱动程序提供的平台特定的支持，如下节所述。
1.5.1 获得系统指定的中断向量、DIRQL和处理器掩码

当PnP管理器把IRP_MN_START_DEVICE请求发送到驱动程序，它分别在Irp->Parameters.StartDevice.AllocatedResources和Irpe>Parameters.StartDevice.AllocatedResourcesTranslated传递未加工和转换的硬件资源。每个资源表包括一个全部资源描述符的列表，它反过来包含特定接口类型和总线数的局部资源描述符的列表。每个局部的资源描述符包含一个资源类型、提供给类型的共享规则、和针对那种类型的、标记和数据特定的资源。中断资源包含中断向量、DIRQL，和被要求创建中断对象和注册ISR的处理器掩码，还有它的共享规则和中断方式。

为了连接其中断，驱动程序使用在AllocatedResourcesTranslated.List.PartialResourceList.PartialDescriptors[]的资源。驱动程序必须扫描局部描述符的数组，以找到需求的资源，这个资源带有成员Type，它的值等于CmResourceTypeInterrupt。对于这个类型的资源，ShareDisposition成员规定资源是否或者如何能被分享，并且Flag成员指示中断方式。此外，每个这样的资源在u.Interrupt.Level包含系统DIRQL，在u.Interrupt.Vector包含系统中断向量，在u.Interrupt.Affinity包含处理器掩码，它指明设备能在其上中断的平台特定的一组处理器。
1.5.2 注册ISR

当驱动程序处理PnP IRP_MN_START_DEVICE请求，它必须调用IoConnectInterrupt注册其ISR。作为响应，I/O管理器调用内核以创建驱动程序的中断对象。

除非ISR为不同的设备处理多个中断向量，驱动程序使用下列的参数注册ISR：

§ 指向驱动程序的存储区域的指针，这个区域是由IoConnectInterrupt返回的中断对象指针分配的

§ NULL自旋锁指针，除非驱动程序本身已分配存储，并且已经使用KeInitializeSpinLock初始化InterruptSpinLock

§ 指向ServiceContext区域的指针和ISR入口点，当它被调用时将使用ServiceContext区域

§ （映射的）DeviceSystemVector，它连同IRP_MN_START_DEVICE请求被传送到转换的中断资源表 （u.Interrupt.Vector） 中

§ 被分配到这个机器的那个向量的SystemDirql，它也可以从转换的中断资源列表（u.Interrupt.Level）中被获得

§ 与给SynchronizeIrql的参数一样的IRQL值（u.Interrupt.Level）

§ ProcessorMask，指明一组在这台机器中设备能在其上中断的处理器，也可以从转换的中断资源表（u.Interrupt.Affinity）中获得

§ 中断模式是否是LevelSensitive（CM_RESOURCE_INTERRUPT_LEVEL_SENSITIVE）或者是Latched（CM_RESOURCE_INTERRUPT_LATCHE），这可以从转换的中断资源表（Flags）中获得

§ 设备是否能共享矢量，这从转换的中断资源表（ShareDisposition） 中获得

§ 当设备中断时，是否保存浮点寄存器

在对IoConnectInterrupt调用的响应中，I/O管理器做下列事情：

1. I/O管理器为与输入ProcessorMask指明的同样多的中断对象分配足够的常驻内存，这个数目可以与SMP机器中的处理器一样多，或比这个小，这依赖于ProcessorMask。I/O管理器也为中断自旋锁提供存储，并且如果驱动程序传递NULL指针到IoConnectInterrupt，初始化它（见步骤1）。

2. I/O管理器调用内部的内核例程，以初始化每个中断对象，并且把它连接到机器中设备能在其上中断的一个特殊的处理器。连接中断对象的它的调用实际在特定处理器的内核中断发送表（IDT）中设置给定的DeviceSystemVector。

3. 当所有必要的中断对象已经被初始化，并连接到处理器，IoConnectInterrupt返回指向中断对象组的指针。驱动程序例程必须传递在它们的调用中返回的PointerToInterruptObject到KeSynchronizeExecution。

何时设备中断在一台给定的处理器上发生，在那个处理器上驱动程序的ISR在DIRQL上被运行，并被传递一个指向ServiceContext的指针，当驱动程序调用IoConnectInterrupt时，这个ServiceContext被创建。

如果驱动程序必须处理来自多个设备的中断，每个带有不同的中断矢量，它能为所有它的设备拥有一个单独的多向量ISR，否则它能拥有多个ISR。这样的驱动程序必须注册ISR，如下：

1. 驱动程序必须使用指向InterruptSpinLock的驱动程序提供的存储区的指针调用KeInitializeSpinLock，这个InterruptSpinLock必须在常驻内存内（在驱动程序分配的设备扩展、控制器扩展、或者非页式存储池）。

2. ISR处理的每个向量，当除了下列的参数，驱动程序如前所述调用IoConnectInterrupt：

§ 驱动程序必须传递一个指向初始化过的InterruptSpinLock的它的存储的指针，而非一个NULL自旋锁指针。

§ 驱动程序必须指定一个SynchronizeIrql值，这个值是分配设备的最高的SystemDirql，这里的设备是指它的ISR为其处理中断的任何设备。

这样的驱动程序的ISR必须在最高的DIRQL上运行，这个DIRQL被分配到驱动程序处理的DeviceSystemVectors，因为驱动程序提供的访问设备寄存器（或数据）的SynchCritSection例程共享一个ISR。

因为ISR在一个相对较高的IRQL上运行，它必须尽可能迅速地返回控制。因而，驱动程序的ISR应该做它能做的小的I/O处理，通常是以下事情：

1. 确定它的设备是否实际上产生中断，并且如果没有的话，尽早地返回FALSE。

2. 如果它的设备产生中断，停止设备这样做，保存对中断引起的操作是必要的上下文，并且排队一个DPC以在较低的IRQL上完成中断驱动的I/O操作。更多信息参见“DPC对象”。

更多信息，参见第16章中“使用自旋锁”；第8章，“中断服务例程”；和第10章，“SynchCritSection例程”。

1.6 DPC对象

像前面几节中描述的中断对象类型一样，DPC对象类型被内核定义。DPC对象代表一个延迟过程调用：一个调用者提供的例程，它稍晚在较低的硬件优先级（IRQL）上被运行。

注册ISR的任何驱动程序必须使用一个或多个DPC对象来注册其DpcForIsr和/或CustomDpc例程。驱动程序提供的DpcForIsr或CustomDpc例程负责完成中断驱动的I/O操作，所以ISR能尽可能迅速地返回控制。

一些系统驱动程序只有一个单独的DpcForIsr例程；这些驱动程序使用一个单独的DPC对象。然而，驱动程序能拥有一个或多个CustomDpc例程，每个使用一个DPC对象，除了DpcForIsr例程，或者作为DpcForIsr例程的替代。

I/O管理器为注册DpcForIsr例程和从ISR排队这个例程提供支持，如下节所述。内核为注册CustomDpc和从ISR排队这个例程提供支持。细节参见“注册和排队CustomDpc例程”。
1.6.1 注册和排队DpcForIsr例程

在驱动程序已经创建设备对象之后，它通过调用IoInitializeDpcRequest为设备对象注册DpcForIsr例程。驱动程序能由其AddDevice例程或者由处理PnP IRP_MN_START_DEVICE请求的代码作这个调用。在设备被开始并且处理中断驱动的I/O请求之后，ISR仅仅在它返回控制前调用IoRequestDpc，从而将DpcForIsr例程排队供执行。图3.15说明了对这些例程的调用。

图3.15 为DpcForIsr例程使用DPC对象

如图3.15所示，调用IoInitializeDpcRequest将一个内核DPC对象与驱动程序提供的DpcForIsr例程和驱动程序创建的设备对象相结合。I/O管理器为DPC对象分配内存，并且根据驱动程序的需要调用KeInitializeDpc。

当ISR被调用以处理设备在DIRQL上的中断，为了获得更好的系统整体和驱动程序性能，它应该尽早地把控制返回到系统。通常，ISR仅仅阻止设备产生更多的中断，收集DpcForIsr例程需要的任何上下文信息（被用来完成引起中断的操作），调用IoRequestDpc，并且返回。

如图3.15所示，ISR传递下述三个指针到IoRequestDpc：一个指向设备对象的指针，这个设备对象代表操作被完成的目标设备；一个指向DeviceObject->CurrentIrp的指针；以及一个指向操作的驱动程序确定的上下文的指针。I/O管理器根据驱动程序的需要调用KeInsertQueueDpc，并且相应的DPC对象被排队，直到在处理器上IRQL落在DISPATCH_LEVEL之下。然后，内核将DPC对象取出队列，并且在那个处理器上驱动程序的DpcForIsr例程被在IRQL DISPATCH_LEVEL上运行。

在入口处，DpcForIsr例程被给一个指向DPC对象的指针，连同DeviceObject、当前的Irp、和Context指针，这个指针在ISR对IoRequestDpc的调用中被传递。上下文易可访问的区域必须在常驻内存中。除非驱动程序为目标设备重复I/O，上下文区域通常在DeviceObject->DeviceExtension中，但是如果驱动程序使用控制器对象，它能在控制器扩展中，或者在被驱动程序分配的非页式存储池中。DpcForIsr例程负责做任何必要的事情以完成在当前IRP中请求的I/O。

因为ISR和DpcForIsr例程在对称多处理器机器中能被同时运行，这些例程应该遵循这些方针：

§ 在ISR返回控制之前，它必须调用IoRequestDpc。否则，在ISR已为DpcForIsr例程完成设置上下文可访问的区域之前，DpcForIsr例程可以在另一个处理器上被运行。

§ 在DpcForIsr例程被运行期间，或者在此之前，如果设备中断，ISR可以被再一次调用。当驱动程序使用设备扩展来保持关于其设备I/O操作的上下文，DpcForIsr例程应该从不为输入DeviceObject（也不要为输入DeviceObject将IRP从队列中取出，如果驱动程序管理它自己的IRP排队）调用IoStartNextPackct，直到在它使用当前Irp调用IoCompleteRequest之前。否则，驱动程序的StartIo或者队列管理例程可以开始一个设备I/O操作，这个操作在DpcForIsr例程完成当前操作之前重写共享的上下文区域。

§ 与ISR共享上下文区域的DpcForIsr例程和任何其他驱动程序例程必须使用驱动程序提供SynchCritSection例程调用KeSynchronizeExecution，从而以多处理器安全方式访问共享的上下文区域。

甚至在单处理器机器中，在DpcForIsr例程被运行时，或者在此之前，如果设备发生中断，ISR可以再一次被调用。如果这发生，DpcForIsr例程仅仅被运行一次。换句话说，如果驱动程序为它的目标设备对象重复I/O操作，在ISR对IoRequestDpcde调用和DpcForIsr例程的实例之间没有一对一的对应关系。

更多信息参见第9章，“ DpcForIsr例程和CustomDpc例程”。
1.6.2 注册和排队CustomDpc例程

在创建设备对象之后，设备驱动程序能通过调用KeInitializeDpc来注册CustomDpc例程。 驱动程序应该从其AddDevice例程作这个调用。

仅仅在驱动程序的ISR返回控制之前，它能调用KeInsertQueueDpc以便为执行而排队CustomDpc例程。图3.16说明了调用这些例程。

图3.16 为CustomDpc例程使用DPC对象

如图3.16所示，有CustomDpc例程的驱动程序必须为DPC对象提供存储。因为驱动程序必须把指针从其ISR传递到DPC对象，存储必须在常驻的系统空间内存中。大多数带有CustomDpc例程的驱动程序在驱动程序创建的设备对象的设备扩展中为它们的DPC对象提供存储，但是存储空间能在控制器扩展中（如果驱动程序使用控制器对象），或者存储在被驱动程序分配的非分页存储池中。

当驱动程序调用KeInitializeDpc，它必须传递它的CustomDpc例程的入口点，还要传递指向DPC对象的驱动程序分配的存储空间和驱动程序定义的DeferredContext区域的指针，它被传递到CustomDpc例程。因为在DeferredContext的区域必须在提高的IRQL上是易访问的，这个区域也必须在常驻内存中。

不像DpcForIsr例程那样，CustomDpc例程没有被关联到一个设备对象。然而，CustomDpc例程的DeferredContext区域几乎总应该包括指向目标设备对象的指针和当前的IRP。像DpcForIsr例程一样，CustomDpc例程使用这个信息以在比ISR低的IRQL上完成一个中断驱动I/O操作。

如图3.16所示，ISR把指向DPC对象和两个附加参数（它们是由调用者确定的）的指针传递到KeInsertQueueDpc。如果机器中的所有处理器拥有正在大于或等于DISPATCH_LEVEL的IRQL上运行的代码，DPC对象被排队，直到在处理器上IRQL落到DISPATCH_LEVEL之下。然后，内核将DPC对象从队列中取出，并且在处理器上驱动程序的CustomDpc例程在IRQL_DISPATCH_LEVEL上被运行。

在任何时刻，任何一个DPC对象的实例只有一个能被排队。这就是说，如果在驱动程序的CustomDpc例程被运行之前ISR使用相同的DPC指针调用KeInsertQueueDpc多次，在IRQL跌落到处理器上的DISPATCH_LEVEL以下之后，CustomDpc例程运行。

CustomDpc例程负责做任何必要的事情，以完成中断引起的I/O操作。细节参见第9章，“DpcForIsr例程和CustomDpc例程”。

ISR和CustomDpc例程能同时在一台对称多处理器机器中运行。因此，当实现CustomDpc例程时，遵循的方针如上一节“注册和排队DpcForlsr程序” 中所述的。

1.7 带有相关的DPC的定时器对象

任何驱动程序能有一个或多个IoTimer或者CustomTimeDpc例程，它们被周期性地调用以满足驱动程序作者决定的某种目的。每个这种类型的标准驱动程序实际上是一个DPC，这个DPC在一些数量的时钟中断被系统处理后被排队执行。

内核定义定时器对象类型，而I/O管理器创建能关联到每个驱动程序创建的设备对象的定时器对象，从而为驱动程序的IoTimer例程提供支持。驱动程序能为每个设备对象拥有一个单独的IoTimer例程，所有其设备对象有一个单独的IoTimer例程，每个它的设备对象有一个单独的IoTimer例程，或者一定数量的IoTimer例程关联到驱动程序的设备对象的非交叉子集。

在驱动程序启用定时器后，IoTimer例程每秒被调用一次，直到驱动程序禁止定时器。 I/O管理器为注册带有设备对象的IoTimer例程和为启用和禁止定时器提供支持例程。

作为变通，驱动程序能拥有一个或多个CustomTimerDpc例程，这些例程能以精确的驱动程序指定的间隔被调用。内核为注册CustomTimerDpc例程和排队CustomTimerDpc提供支持例程，在一个指定的定时器间隔耗尽后，这个例程被调用。

更多的关于定时器例程的信息，参见下列几节：

§ 3.7.l 注册和启用IoTimer例程

§ 3.7.2 注册和排队CustomTimerDpc例程
1.7.1 注册和启用IoTimer例程

在任何驱动程序创建一个或多个设备对象之后，它能通过调用IoInitializeTimer注册IoTimer例程。在设备被开始之后，驱动程序能通过调用IoStartTimer启用定时器。图3.17说明这些调用。

如图3.17所示，驱动程序使用它的IoTimer例程的入口点和指向驱动程序创建的设备对象和TimerContext区域的指针调用IoInitializeTimer，驱动程序在TimerContext区域中维护它的IoTimer例程使用的任何上下文。I/O管理器把设备对象与内核定时器对象相联系，并设置定时器对象每秒一次超时。

在驱动程序调用IoStartTimer之后，它的IoTimer例程每秒被调用一次，直到驱动程序调用IoStopTimer。驱动程序可以使用IoStartTimer再次启用对其IoTimer例程的调用。

在入口处，IoTimer例程被传递一个指向DeviceObject的指针和一个指向TimerContext区域的指针，这个TimerContext区域在驱动程序调用IoInitiaIizeTimer时被创建。

因为IoTimer例程在IRQL DISPATCH_LEVEL被运行，其TimerContext区域必须在常驻的系统空间内存中。拥有IoTimer例程的大多数驱动程序使用相关的设备对象的DeviceObject->DeviceExtension作为上下文可访问区域，但是上下文可以在控制器扩展中（如果驱动程序使用控制器对象），或者在被驱动程序分配的非页式存储池中。

图3.17 为IoTimer例程使用定时器对象

遵循这些有关TimerContext区域的方针：

§ 如果IoTimer例程把其TimerContext区域与驱动程序的ISR共享，它必须使用SynchCrilSection例程调用KeSynchronizeExecution，从而以多处理器安全方式访问TimerContext区域程序。

更多关于KeSynchronizeExecution的信息参见第16章中的“使用自旋锁”。

§ 如果IoTimer例程不把其TimerContext区域与ISR相共享，但是确实把它与另一个驱动程序例程相共享，驱动程序必须用初始化的执行自旋锁来保护共用的上下文区域，从而以多处理器安全方式访问TimerContext。

更多信息，参见章14，“IoTimer和CustOmTimempc例程”。
1.7.2 注册和排队CustomTimerDpc例程

驱动程序能通过调用下列例程来注册CustomTimerDpc例程，通常从其AddDevice例程中调用：

1. 创建其CustomTimerDpc例程的KeInitializeDpc

2. 创建定时器对象的KeInitializeTimer或者KeInitializeTimerEx

初始化设备之后，驱动程序能调用KeSetTimer或者KeSetTimerEx以创建一个带有间隔和DPC对象的定时器对象，并且使得它的CustomTimerDpc在给定的间隔耗尽时被调用。图3.18说明了这些调用。

图3.18 为CustomTimerDpc例程使用定时器和DPC对象

如图3.18所示，如果驱动程序有CustomTimerDpc例程，它必须为DPC对象和定时器对象提供存储。有CustomTimerDpc例程的大多数驱动程序为这些对象提供存储在驱动程序创建的设备对象的设备扩展中，或者在驱动程序分配的其他常驻内存中。

驱动程序必须首先通过调用KeInitializeDpc来初始化DPC对象，然后通过调用KeInitializeTimer或者KeInitializeTimerEx来初始化定时器对象。更多关于调用KeInitializeDpc的信息，参见“注册和排队CustomDpc例程”。

如图3.18所示，当驱动程序调用KeInitializeTimer或者KeInitializeTimerEx，驱动程序必须传递指向其定时器对象的存储的指针。

在初始化设备之后，驱动程序能调用KeSetTimer以排队CustomTimerDpc，CustomTimerDpc在驱动程序确定间隔后被调用。在对KeSetTimer的调用中，驱动程序传递指向Dpc和Timer对象的指针，附带一个以100纳秒为单位的DueTime，如图3.18所示。DueTime的正值指定了一个CustomTimerDpc例程应该再其上被调用的绝对时刻（自从1601年1月1日以来）。 DueTime的负值表明相对的过期时间。绝对的过期时间跟踪系统时间的任何变化；相对的过期时间不受系统时间改变的影响。

为了重复地调用CustomTimerDpc例程，使用KeSetTimerEx来设置定时器，并在Period参数中规定一个再次发生的间隔。除了这个附加的参数，KeSetTimerEx就象KeSetTimer一样。

如图3.18所示，对KeSetTimer或者KeSetTimerEx的调用排队有一个指定间隔的定时器对象，如下：

1. 当DueTime期满，定时器对象被出列，并被设置Signaled状态。

2. 如果机器中的所有的处理器当前在大于或者等于DISPATCH_LEVEL的IRQL上运行代码，与定时器对象有关的DPC对象被放入DPC队列中。否则，CustomTimerDpc例程被调用。

3. 如果当DueTime间隔期满的时候DPC对象已经队列中，在机器中任何处理器上的IRQL落到DISPATCH_LEVEL之下后，CustomTimerDpc例程被调用。

CustomTimerDpc例程，像所有DPC一样，在IRQL DISPATCH_LEVEL上被运行。

内核定时器对象间隔大约是十个毫秒，所以驱动程序能使用CustomTimerDpc例程以获得比使用IoTimer（每秒运行一次）更小的时间间隔。

如同DPC对象，在任何给定的瞬间，一个给的定时器对象只有一个实例能被排队。再一次用同样的定时器对象指针调用KeSetTimer或者KeSetTimerEx可以取消排队的定时器对象，并且重新设置它。

更多信息参见“IoTimer和CustomTimerDpc例程”。

相同的方针适用于CustomTimerDpc例程的DeferredContext区域和IoTimer例程的TimerContext区域。细节参见“注册和启用IoTimer例程”。

1.8 设备队列对象和互锁队列

除了文件系统驱动程序，I/O管理器将设备队列对象与驱动程序创建的每个设备对象相联系，如图3.2中所示。

大多数设备驱动程序调用I/O管理器的支持例程，以使用相关的设备队列，在对目标设备对象的设备I/O请求到达的速度比驱动程序能完成处理它们的速度更快的时候，这个设备队列保存IRP。使用这种技术，IRP被排队到驱动程序提供的StartIo例程。

为了获得更好的性能，与IRP到达的速度一样快，大多数中间的驱动程序简单地把IRP传递传递到较低层驱动程序，所以中间层驱动程序几乎从不使用与它们各自设备对象有关的设备队列。

然而，你能通过明确地设置个一个或多个设备队列对象或者互锁队列来设计驱动程序管理IRP的内部队列。如果驱动程序控制作重复I/O操作的设备，这种方法是特别有用的。对于这样的设备，仅仅使用一个单独的队列，为相同的目标设备对象，来管理两个或者更多IRP的并行处理是困难的。

你也可以选择为一组不同种类的物理设备在设备控制器的驱动程序中创建辅助的IRP队列。

例如，SCSI端口驱动程序为内部队列使用设备队列对象。这个驱动程序拥有StartIo例程，并且创建设备队列对象作为辅助队列，另外还拥有设备队列，这个设备队列被关联到它创建的用来代表HBA的设备对象。SCSI端口驱动程序使用其辅助的设备队列来持有发往在HBA控制的SCSl总线上的特殊逻辑单元的IRP。

系统软盘控制器驱动程序是一个没有StartIo例程并且使用一个互锁队列的驱动程序的例子。这个驱动程序创建一个双重连接的互锁队列，驱动程序和其设备专用线程向这个队列插入IRP，以及从这个队列移去IRP。

内核定义设备队列对象类型。运行支持（Executive Support）组件为在互锁队列中插入和移去IRP提供列程。下面两节解释如何使用设备和互锁队列：

§ 3.8.1 设置设备队列对象和排队的IRP

§ 3.8.2 设置一个互锁队列和排队的IRP

更多关于I/O管理器的内部的对在与设备对象相关的设备队列中排队的IRP的支持，参见第14章中“最低层设备驱动程序的阶段IRP处理”。
1.8.1 设置设备队列对象和排队IRP

驱动程序通过在驱动程序或者设备初始化时调用KeInitializeDeviceQueue来创建设备队列对象。在开始其设备之后，驱动程序通过调用KeInsertDeviceQueue或者KeInsertByKeyDeviceQueue将IRP插入这个队列。图3.20说明这些调用。

图3.20 使用设备队列对象

如图3.20所示，驱动程序必须为设备队列对象提供存储，它必须是常驻的。创建设备队列对象的驱动程序通常在驱动程序创建的设备对象的设备扩展中提供必要的存储，但是如果驱动程序使用控制器对象，存储能在控制器扩展中，存储还可以在驱动程序分配的非分页存储池中。

如果驱动程序为设备扩展中的设备队列对象提供存储，在创建设备对象之后和开始设备之前，它调用KeInitializeDeviceQueue。换句话说，驱动程序能从其AddDevice例程初始化队列，或者在它处理PnP IRP_MN_START_DEVICE请求的时候初始化队列。在对KeInitializeDeviceQueue的调用中，驱动程序传递指向它为设备队列对象提供的存储的指针。

在开始其设备之后，驱动程序能通过调用KeInsertDeviceQueue将IRP插入其设备队列，它将IRP放置在队尾，或者调用KeInsertByKeyDeviceQueue，它根据驱动程序确定SortKey值把IRP放置到队列中，如图3.20所示。

每个这样的支持例程返回一个表明IRP是否被插入队列的布尔值。如果队列当前是空的（Not-Busy）,每个这样的调用也置设备队列对象的状态为忙。然而，如果队列是空（Not-Busy）的，KeInsert..DeviceQueue例程从不将IRP插入队列。它所做的是，设置设备队列对象的状态为忙并且返回FALSE。因为IRP尚未被排队，驱动程序必须为进一步处理将它传递到另一个驱动程序例程。

在设置辅助的设备队列时，遵从下面这个实现方针：

当对KeInsert的调用返回FALSE时，调用者必须传递它试图为进一步处理而试图排队的IRP到另一个驱动程序例程。

然而，对KeInsertDeviceQueue的调用改变设备队列对象的状态为忙，因此下一个到达的IRP被插入在队列中，除非驱动程序调用它。

当设备队列对象的状态被置为忙碌，驱动程序能为进一步处理将IRP出列，或者通过调用下列支持例程之一来重设状态为Not-Busy：

§ KeRemoveDeviceQueue，在队列头移去IRP

§ KeRemoveByKeyDeviceQueue，根据驱动程序确定的SortKey值移去IRP

§ KeRemoveEntryDeviceQueue，在队列中移走一个特定的IRP，或者确定特定的IRP是否在队列中

KeRemoveEntryDevice队列返回的布尔值表明IRP是否在设备队列中。

调用这些例程任何一个以从空的但是忙的设备队列中除去一个项，可以改变队列状态为不忙。

每个设备队列对象被一个内部的执行自旋锁（图3.20中没有显示）保护。结果是，驱动程序能从任何运行在小于等于IRQL DISPATCH_LEVEL的驱动程序例程中以多处理器安全方式插入IRP到队列，或移走它们。由于这个IRQL的限制，驱动程序不能从它的ISR或者SynchCritSection例程调用任何Ke..DeviceQueue例程，它的ISR或者SynchCritSection例程在DIRQL上运行。

更多信息参见第16章中“管理硬件优先级和使用自旋锁”。 对于特定支持例程的IRQL请求，在《Windows 2000驱动程序开发参考》卷2中参见那个例程的参考页。
1.8.2 设置一个互锁队列和排队IRP

带有设备专用线程的驱动程序或者使用执行工作者线程的驱动程序，例如大多数系统FSD，最有可能是这样一种驱动程序，这种驱动程序在一个互锁队列中管理他们自己运行时内部IRP排队。所有的PnP驱动程序，包括WDM驱动程序，也必须在作PnP和电源状态转变时内在地排队确定的IRP。

通常，这些驱动程序创建一个双重连接的互锁队列；所有的IRP包含一个类型为LIST_ENTRY的成员，驱动程序能使用它双重连接到当前正在保持的IRP。如果驱动程序创建一个单独连接的互锁队列，它不能为重试而重排队IRP。

驱动程序必须在设备初始化时创建其互锁队列。图3.21说明一个双重连接互锁队列、驱动程序必须调用以创建这样的队列的支持例程、和一组ExInterlockease例程，驱动程序能调用这组例程把IRP插入到队列或从队列中除去IRP。

图3.21 使用一个互锁队列

如图3.21所示，为了创建双重连接的互锁队列，驱动程序必须为队列本身和为下列事物提供存储：

§ ExecutiveSpinLock，驱动程序必须调用KeInitializeSpinLock以初始化它。通常，当驱动程序为它的设备对象在它的AddDevice中创建设备扩展时，它初始化这个自旋锁。

§ 队列的ListHead，驱动程序必须通过调用InitializeListHead来初始化它。

使用双重连接互锁队列的大多数驱动程序在驱动程序创建的设备扩展中提供必要的存储。 如果驱动程序使用控制器对象，队列和执行的自旋锁能在控制器扩展中，还可以在被驱动程序分配的非页式存储池。

在驱动程序接受I/O请求期间，如果ListHead是类型LIST_ENTRY的，驱动程序能通过调用下列支持例程的任何一个将IRP插入其队列中：

§ ExInterlockedInsertTailList，它在队列的尾部放置IRP

§ ExInterlockedInsertHeadList，它在队列的前面放置IRP。仅仅在驱动程序必须再试特殊的请求时，它们通常调用这个例程。

驱动程序必须传递指向IRP（ListEntry）的指针，如它以前初始化过的ListHead和ExecutiveSpinLock（锁）指针一样，驱动程序将它们传递到这些ExInterlockedInsert..List例程的一个。当驱动程序通过调用ExInterlockedRemoveHeadList以从队列中移出IRP的时候，仅仅指向ListHead和Lock的指针被请求。为了防止死锁，驱动程序不保有它传递给任何ExInterlockedXxx例程的ExecutiveSpinLock。

因为互锁队列被执行的自旋锁保护，驱动程序从运行在小于等于IRQL DISPATCH_LEVEL的驱动程序以多处理器安全方式将IRP插入到其双重连接队列中，以及移走它们。

带有类型为LIST_ENTRY的ListHead的队列，如图3.21所示，是一个双重连接的表。带有类型为SLIST_HEADER的ListHead的队列是一个顺序的、单独连接的表。驱动程序通过调用ExInitializeSListHead为序列的单独连接的互锁队列初始化ListHead。

从不重试I/O操作的驱动程序能使用ExInterlockedPushEntrySList和ExInterlockedPopEntrySList以管理它的在序列的单独连接的互锁队列内的IRP排队。使用这种类型的互锁队列的任何驱动程序也必须为类型为SLIST_HEADER的ListHead和为ExecutiveSpinLock提供常驻的存储空间，如图3.21所示。在驱动程序调用ExInitializeSListHead以插入初始的项到它的队列中之前，它必须初始化自旋锁并创建它的队列。

更多信息，参见第16章中“管理硬件优先级和使用自旋锁”。关于特定的支持例程的IRQL请求的信息，参见《Windows 2000驱动程序开发参考》卷2中那个例程的参考页。

1.9 内核调度者对象

如在“带有相关DPC的定时器对象”中所提到的那样，内核为定时器对象定义对象类型，这个定时器对象能被关联到驱动程序提供的CustomTimerDpc例程。定时器对象是一组被称为调度者对象的内核定义的对象类型中的一个。

内核也为其他调度者对象定义类型，诸如事件、信号量、和互斥体。内核定义的线程对象，它代表在系统内执行的线程，也是调度者对象。

当在IRQL PASSIVE_LEVEL运行时，调度者对象能在一个非专用线程环境内被用作同步化机制。

调度者对象状态和线程属性

每个内核定义的线程调度者对象类型都有一个状态，要么被设置为有信号（Signaled），要么被设置为无信号（Not-Signaled）。

一组线程能同步它们的操作，方法是使用调度者对象让一个或多个线程调用KeWaitForSingleObject、KeWaitForMutexObject，或者KeWaitForMultipleObjects，并且等待直到另一个例程或者线程把调度者对象设置为有信号状态。

当线程使用调度者对象调用KeWaitForSingleObject，或者使用互斥体调用KeWaitForMutexObject，线程被放到等待状态中，直到调度者对象被设置为有信号状态。 当线程使用一个或多个调度者对象调用KeWaitForMultipleObjects，线程被放到等待状态中直到一个或者所有调度者对象被设置为有信号状态。

无论何时调度者对象被设置为有信号状态，内核改变任何等待那个对象的线程的状态为就绪（ready）：根据它的当前运行时优先级和当前的处理器（拥有那个优先级的任何线程在其上执行）可用性，线程将被调度运行。同步化事件和同步化定时器是这项规则的例外情况；当同步化事件或者定时器被置为有信号，仅仅一个等待线程被置为就绪状态。

一些驱动程序创建它们自己的驱动程序或者设备专用的系统线程，并且把它们的线程的基础优先级设置为最低的实时优先级值。其他最高层驱动程序，特别是文件系统驱动程序，使用带有基本优先级的系统工作者线程，这些工作者线程通常被设置为最高的可变优先级值。内核调度带有最低实时优先级的线程，从而可以在每个带有可用优先级的线程，包括系统中几乎所有的用户模式线程，之前运行。

在所有当前处于就绪状态的线程之前，大多数标准的驱动程序例程在一个专用线程环境中被运行。

无论它们各自的运行时优先级是什么，线程都在IRQL PASSIVE_LEVEL上被运行。许多标准的驱动程序例程在提高的IRQL上被运行：就是说，在大于PASSIVE_LEVEL的硬件优先级上，例如IRQL DISPATCH_LEVEL或者DIRQL。

等待驱动程序中的调度者对象

对于大多数设备和中间层驱动程序，内核的调度者对象只有在下列环境被用于同步：

§ DriverEntry、AddDevice、Reinitialize和Unload，还有一定的Dispatch例程，在系统线程的上下文中被运行，所以一个驱动程序能在这些例程之内等待调度者对象。

§ 最高层的驱动程序的Dispatch例程也通常在一个非专用线程环境中被运行，通常是请求I/O操作的用户模式线程环境。

§ 开始固有的同步I/O操作的低层驱动程序的Dispatch例程，例如创建、刷新、停止、关闭，也能为请求的操作的完成等待调度者对象。

然而，低层驱动程序的Dispatch例程不能为固有异步I/O操作的完成等待一个内核定义的调度者对象。

对SCSI微端口驱动程序考虑下面的设计方针：

为了移植性要求，SCSI不应该为端口驱动程序使用内核调度者对象。SCSI微端口驱动程序只应该调用ScsiPortXxx例程。

对任何驱动程序考虑下列的设计方针：

驱动程序不能，并且不可试图为向或从页式设备传输操作的完成等待调度者对象。

换句话说，读和写请求的驱动程序Dispatch例程通常不能等待调度者对象。带有I/O控制代码（它的传输类型值是除了MEHTOD_BUFFERED之外的值）的任何设备I/O控制请求的Dispatch例程也不能等待用调度者对象。更多关于驱动程序必须支持的设备类型特定的请求和I/O控制代码，参见《Windows 2000驱动程序开发参考》卷2。

所有的其他标准驱动程序例程在一个专用线程环境中被运行：当无论是什么驱动程序例程被调用以处理IRP或者处理设备中断，这个例程的上下文恰恰就是当前的。此外，大多数标准的驱动程序例程在提高的IRQL上被运行，要么是在DISPATCH_LEVEL，要么是为设备驱动程序在DIRQL。

记住下列的事实：

§ 在提高的IRQL上为非零间隔等待内核调度者对象是一个致命的错误。

开始对设备异步I/O操作的驱动程序Dispatch例程和在提高的IRQL上运行的所有驱动程序例程不能使用调度者对象来同步它们的IRP处理。标准驱动程序例程在其上运行的IRQL的总结参见章l6中“管理硬件优先级”。

极少有设备和中间的驱动程序创建驱动程序创建的系统线程。极少有最高层的设备驱动程序使用系统工作者线程，FSD通常这样作。因此，大多数设备和中间层驱动程序仅仅在驱动程序初始化、卸载、或在同步I/O请求的Dispatch例程中能使用调度者对象。

如果必要的话，驱动程序能创建设备专用的线程，这个线程能等待一个调度者对象，那个驱动程序的其他例程（除ISR或者SynchCritSection程序之外）能设置这个调度者对象为有信号状态，或者重设为无信号状态。作为一般方针，如果期待的新设备驱动程序在I/O操作期间等待设备状态变化的时候经常需要等待大于50微秒的迟延，考虑用设备专用线程实现驱动程序。如果设备驱动程序也是最高层驱动程序，考虑使用系统工作者线程并实现一个或多个工作者线程回调例程。参见《Windows 2000驱动程序开发参考》卷2第8章中的“PsCreateSystemThread”，和第7章中的“使用驱动程序创建的线程管理互锁队列”。

下面几节描述定时器、事件、信号量，和有拥有驱动程序的互斥的调度者对象，这些驱动程序带有设备专用线程和/或者工作者线程的回调例程，或者必须在DriverEntry、Reinitialize、AddDevice、Unload、和同步I/O操作的Dispatch例程中等待一个或多个调度者对象。

任何在其DriverEntry、Reinitialize、Unload例程中等待调度者对象的驱动程序在系统线程上下文中运行。实际上，在其同步I/O操作的Dispatch例程中等待调度者对象的任何驱动程序从一个专用线程的总量中偷取一个周期。

为简单期间，下面几节说明对KeWaitForSingleObject的调用。可以视为对KeWaitForMultipleObjects调用。
1.9.1 定时器对象

任何驱动程序能在一个非专用线程环境之内使用定时器对象，以使在驱动程序的其他例程之内的操作超时，或者执行一个周期性的操作。驱动程序调用KeInitializeTimer或者KeInitializeTimerEx以创建定时器对象。

定时器能是通知定时器或者同步化定时器。当通知定时器被发信号，所有等待的线程满足它们的等待条件，而定时器的状态仍然是有信号，直到它被明确地复位。当同步化定时器期满，其状态被设置为有信号，直到一条单独的等待线程被释放，然后定时器被复位到无信号状态。KeInitializeTimer总是创建通知定时器（notification timer）。KeInitializeTimerEx接受一个Type参数，它可以是NotificationTimer或者SynchronizationTimer。通知和同步化定时器能可选地拥有一个相关的CustomTimerDpc例程。

定时器仅仅能期满一次，或者它能在一个给定间隔中被重复地设置为期满。KeSetTimer总设置一个将仅仅期满一次的定时器。KeSetTimerEx接受一个可选的Period参数，调用者能使用这个参数为定时器规定一个再次发生的间隔。

图3.22说明使用通知定时器为一个操作创建一个超时间隔，然后在其他驱动程序例程处理I/O请求时等待。

图3.22 等待定时器对象

如图3.22所示，驱动程序必须为定时器对象提供存储空间，这个定时器对象必须通过使用指向这个存储的指针来调用KcInitializeTimer，从而被初始化。驱动程序典型地从其AddDevice例程中作这个调用。

在一个特殊的线程的上下文之内，例如驱动程序创建的线程或请求同步I/O操作的线程，这个驱动程序可以等待它的定时器对象，如图3.22所示：

1. 线程用指向定时器对象的指针和给定的DumTime调用KeSetTimer，DueTime的单位是100 纳秒。DueTime的正值规定定时器对象应该从内核的定时器队列中被除去，并设置为有信号状态的绝对时间。DueTime的负值规定与当前系统时间有关的间隔。

注意，如果等待定时器对象而不是排队CustomTimerDpc例程，在线程调用KeSetTimer的时候，线程（或者驱动程序例程试车系统线程）传递DPC对象的NULL指针如以前在图3.19中所示。

2. 线程用指向定时器对象的指针调用KeWaltForSingIeObject，当定时器对象在内核的定时器队列中时，它把线程放到等待状态中。

3. 给定的DueTime期满。

4. 内核将定时器对象出列，设置它为有信号状态，并且将线程的状态从等待改变为就绪。

5. 一旦处理器是可供使用的，内核为执行发送线程：就是说，没有其他带有较高优先级的线程当前在就绪状态中，没有在提高的IRQL（大于PASSlVE_LEVEL）上被运行的内核模式例程。

通过使用带有相关的DPC对象的定时器对象，运行在提高的IRQL上的驱动程序例程能超时请求，以排队驱动程序提供的CustomTimerDpc例程。只有在一个非专用线程环境之内运行的驱动程序例程能等待定时器对象非零的间隔，如图3.22。

像所有其他线程一样，驱动程序创建的线程由内核线程对象描述，这个内核线程对象也是调度者对象。因而，驱动程序不必使其驱动程序创建的线程使用定时器对象，以为给定的间隔将自己自愿地放到等待状态中。相反，线程能使用调用者提供的间隔调用KeDelayExecutionThread。更多关于这种技术的信息，参见第16章中“轮询设备”。

DriverEntry、Reinitialize和Unload例程也在系统线程环境中运行，因此当它们初始化或卸载时，驱动程序能使用驱动程序初始化的定时器对象调用KeWaitForSingleObject或调用KeDelayExecutionThread。如果设备驱动程序在其初始化期间必须等候设备，它能为一个十分短的间隔（最好是小于50微秒）调用KeStallExecutionProcessor。

然而，较高层驱动程序通常在它们的DriverEntry和/或Reinitialize例程中使用另一个同步化机制，而不是使用定时器对象。较高层驱动程序应该总是被设计成位于特殊类型设备的任何低层驱动程序之上。因此，如果更高层驱动程序等待定时器对象或调用KeDelayExecutionThread，它倾向于变成缓慢加载的，因为这样的驱动程序必须等待足够的间隔，以便可能最慢的设备支持它。也要注意，对于这样一个等待，“安全”但是最小的间隔是很难确定的。

同样地，PnP驱动程序不应该等候其他动作发生，但是作为代替应该使用PnP管理器的通知机制。

如果在定时器期满之前系统时间变化，相对定时器不受影响，除非系统调整每一个绝对的定时器。在指定数量的时间单元流逝之后，不管绝对系统时间如何，相对定时器总是期满。 一个绝对的定时器在特定系统时刻期满，在系统时间方面这样的变化改变一个绝对的定时器的等待时期。
1.9.2 事件对象

如在“内核调度者对象”中所述的，当相邻较低层驱动程序处理等待的驱动程序创建的IRP时，驱动程序使用事件对象来等待。拥有驱动程序创建的线程的驱动程序或等待同步I/O请求完成的驱动程序Dispatch例程也能使用事件对象来同步它们的线程和/或其他驱动程序例程之间的操作。

使用事件对象的任何驱动程序必须在它等待、设置、清除、或者复位事件之前调用KeInitializeEvent或者IoCreateXxxEvent。图3.23说明带有线程的驱动程序如何能为同步化使用事件对象。

图3.23 等待事件对象

如图3.23所示，这样一种驱动程序必须为事件对象提供存储，它必须是常驻的。驱动程序能使用驱动程序创建的设备对象的设备扩展、控制器扩展（如果驱动程序使用控制器对象），或者被驱动程序分配的非页式存储池。

当驱动程序调用KeInitializeEvent，它必须传递指向驱动程序的常驻的事件对象存储空间的指针。此外，调用者必须指定事件对象的初始状态（有信号，或无信号），如图3.23所示。调用者也必须为其事件对象指定Type，它能可以是下列值之一：

§ SynchronizationEvent

当这样的事件被放置为有信号状态，正在等待这个事件的单独线程变得适合执行，并且事件的状态被自动复位为无信号。

这种类型的事件有时被自动清除事件（autoclearing event）调用，因为每次等待被满足后，其有信号状态自动地被复位。

§ NotificationEvent

当这样的事件被设置为有信号状态，等待这个事件的所有线程变得适合执行，并且事件保持有信号状态，直到明确将其复位为无信号：就是说，使用给定的Event指针调用KeClearEvent或者KeResetEvent。

很少设备或者中间层驱动程序有一个单独的驱动程序专用线程，更不用说可以通过等待保护共享资源的事件使它们的操作同步的一组线程了。

大多数使用事件对象来等候完成I/O操作的驱动程序在它们调用KeInitializeEvent时，设置输入Type为NotificationEvent。为IRP（驱动程序使用IoBuildSynchronousFsdRequest或者IoBuildDeviceIoControlRequest创建这个IRP）创建的事件对象几乎总是作为NotificationEvent被初始化，因为调用者将等待通知它的请求已经被一个或多个较低层驱动程序满足的事件。

在驱动程序已经初始化之后，其线程（如果有）和其他例程能在事件上使它们的操作同步。 例如，带有管理IRP排队的线程的驱动程序，诸如系统软盘控制器驱动程序，可以在事件上同步IRP处理，如图3.23所示：

1. 已经为在设备上处理而将一个IRP取出队列的线程，使用指向驱动程序提供的初始化的事件对象的存储空间的指针调用KeWaitForSingleObject。

2. 其他驱动程序例程完成必要的设备I/O操作以满足IRP，并且，当这些操作被完成时，驱动程序的DpcForIsr例程调用KeSetEvent，这个调用使用指向事件对象的指针、驱动程序确定的为线程而被提高的优先级（Increment，如图3.23中所示）、被设置为FALSE的布尔值Wait。调用KeSetEvent设置事件对象为有信号状态，进而等待的线程的状态为就绪。

3. 一旦处理器是可供使用的，内核为执行调度线程：就是说，没有带有较高优先级的其他线程当前在就绪状态，被运行在提高的IRQL上（大于PASSIVE_LEVEL）。

如果DpcForIsr已经没有使用IRP调用IoCompleteRequest，线程现在能完成IRP，并且能将另一个IRP出列，使其在设备上被处理。

使用被设置成TRUE的Wait参数调用KeSetEvent表明调用者的意图，在从KeSetEvent返回时立即调用KeWait..Object(s)支持例程。

给KeSetEvent设置Wait参数要考虑以下方针：

运行在IRQL PASSIVE_LEVEL的可分页的线程或可分页的驱动程序例程从不应该使用被设置为TRUE的Wait参数调用KeSetEvent。如果在调用KeSetEvent和KeWait..Object(s)之间调用者碰巧被页调出，这样的调用会引起致命的错误。。

在大于PASSIVE_LEVEL的IRQL上运行的任何标准驱动程序例程不能在任何调度者对象上等待非零间隔而不降低系统性能（参见3.9）。然而，当在小于或者等于DISPATCH_LEVEL的IRQL上运行时，这样的例程能调用KeSetEvent。

想获得标准的驱动程序例程在其上运行的IRQL的总结，参见第16章中“管理硬件优先级”。 对于支持例程特定的IRQL请求，参见《Windows 2000驱动程序开发参考》卷2。

KeResetEvent返回给定的事件以前的状态：当对KeResetEvent的调用发生时，它是否被设置为有信号。 KeClearEvent简单地把给定的事件的状态设置为无信号。

当调用前面的支持例程时，考虑下列的方针：

§ 要获得较好的性能，所有的驱动程序应该调用KeClearEvent，除非调用者需要由KeResetEvent返回的信息以确定下一步该怎么办。
1.9.3 信号量对象

任何驱动程序能使用信号量对象来使它的驱动程序创建的线程和其他驱动程序例程之间的操作同步。例如，当没有对驱动程序的显著I/O请求时，驱动程序专用线程可以把自己放到等待状态中，并且驱动程序的Dispatch例程刚好在它们排队IRP之后，可以设置信号量为有信号状态。

最高层的驱动程序的Dispatch例程，它在请求I/O操作的线程环境中被运行，可以使用信号量来保护在Dispatch例程中间共享的资源。同步I/O操作的低层驱动程序Dispatch例程也可以使用信号量来保护在Dispatch例程的子集中间或者与驱动程序创建的线程共享的资源。

在使用信号量对象的任何驱动程序等待或者释放信号量之前，它必须调用KeInitializeSemaphore。图3.24说明带有线程的驱动程序如何使用信号量对象。

图3.24 等待信号量对象

如图3.24所示，这样的驱动程序必须为信号量对象提供存储，这个存储应该是常驻的。如果驱动程序使用控制器对象，它能使用驱动程序创建的设备对象的设备扩展、控制器扩展（如果它使用控制器对象），或者被驱动程序分配的非页式存储池。

当驱动程序的AddDevice例程调用KeInitializeSemaphore，它必须传递指向为信号量对象提供的驱动程序的常驻存储空间的指针。此外，调用者必须为信号量对象指定一个Count，如图3.24所示，它确定它的初始状态 （非零表示有信号）。

调用者也必须为信号量规定Limit，它可以是下列值之一：

§ Limit=1

当这个信号量被设置为有信号状态，等待信号量的一个单独的线程变得适合于执行，并且能访问任何被信号量保护的资源。

因为线程要么拥有，要么没有对信号量保护的资源的排它访问，这种类型的信号量也被称为二进制信号量（binary semaphore）。

§ Limit>1

当这种信号量被设置为有信号状态，一些等待信号量对象的线程变得适合于执行，并且能访问任何有信号量保护的资源。

这种类型的信号量被称为计数信号量（counting semaphore），因为设置这个信号量为有信号状态的例程也指定有多少线程可以使它们的状态由等待改变为就绪。这样等待的线程数目可以是Limit，Limit在信号量被初始化或某个数目小于这个预设Limit的时候被设置。

极少数设备或者中间层驱动程序有一个单独的驱动程序创建的线程：甚至更少有一组可以等待信号量的线程。极少有系统提供的驱动程序使用信号量对象，并且，即使它们这样做，甚至更少使用二进制信号量。虽然二进制信号量似乎可以在功能上类似一个互斥体对象，但是二进制信号量不提供内部保护以避免死锁，互斥体对象为在SMP机器中运行的线程提供这种保护。

在带有一个初始化信号量的驱动程序被装载之后，它能在保护一种共享资源的信号量上使操作同步。例如，带有管理IRP排队的设备专用线程的驱动程序，诸如系统软盘控制器驱动程序，可以在信号量上同步IRP排队，如图3.24所示：

1. 线程用指向驱动程序提供的存储空间（为初始化的信号量对象提供这个存储空间）指针调用KeWaitForSingleObject，以把自己放到等待状态中。

2. IRP开始进入那个请求设备I/O操作的驱动程序。驱动程序的Dispatch例程在自旋锁控制下把每个这样的IRP插入一个互锁队列，并且使用下列参数调用KeReleaseSemaphore，包括一个指向信号量对象的指针、为线程提升的驱动程序确定的优先级 （Increment，如图3.24所示）、在每个IRP被排队时被添加到信号量的计数器的Adjustment、和一个被设置为FALSE的布尔值Wait。非零信号量计数器设置信号量对象为有信号状态，进而更改等待的线程状态为就绪。

3. 一旦处理器是可供使用的，内核为执行调度线程：就是说，没有带有较高的的优先级的其他线程当前在准备状态中，没有内核模式例程被在提高的IRQL（大于PASSIVE_LEVEL）上被运行。

线程在自旋锁控制下从互锁队列中除去IRP，传递它到其他驱动程序例程以供进一步处理，并且再一次调用KeWaitForSingleOhject。如果信号量依旧被设置为有信号状态（即它的Count保持非零，表明更多的IRP在驱动程序的互锁队列中），内核再一次将线程状态从等待改变为就绪。

通过这种方式使用计数的信号量，在线程无论什么时候被运行，这样的驱动程序线程都“知道”存在一个从互锁队列中被除去的IRP。

使用被设置为TRUE的Wait参数调用KeReleaseSemaphore表明调用者的意图，即在从KeReleaseSemaphore返回时立即调用KeWait..Object(s)支持例程。

为KeReleaseSemaphore设置Wait参数，考虑下列方针；

§ 在IRQL PASSIVE_LEVEL运行的可分页的线程或可分页的驱动程序例程从不应该使用被设置为TRUE的Wait参数调用KeReleaseSemaphore。如果调用者碰巧在对KeReleaseSemaphore和KeWait..Object(s)的调用之间被页调出，这样的调用将引起致命的错误。

§ 如果不降低系统性能，在大于PASSIVE_LEVEL的IRQL运行的任何标准驱动程序例程不能在任何调度者对象上等待非零间隔；细节参见“内核调度者对象”。然而，当这样的例程正运行在低于等于DISPATCH_LEVEL的IRQL上，它可以调用KeReleaseSemaphore。

对于标准驱动程序例程在其上运行的IRQL的总结，参见第16章中的“管理硬件优先级”。关于支持例程特定的IRQL请求，参见《Windows 2000驱动程序开发参考》卷2。
1.9.4 互斥体对象

如其名所暗示的那样，互斥体对象是一个被设计成相互地保证对在一组内核模式线程之间共享的单个资源排它访问的机制。只有最高层的驱动程序，诸如使用执行的工作者线程的FSD，很有可能使用一个互斥体对象。

有可能，带有驱动程序创建的线程和/或工作者线程的回调例程的最高层驱动程序可以使用互斥体对象。然而，带有可分页线程或者工作者线程的回调必须非常仔细地管理它的互斥体对象的获得、等待和释放。

如已经在前面的节中被论及的那样，互斥体对象拥有内部的特性，它提供系统（只有内核模式）线程相互排斥地、没有死锁地对SMP中共享资源的访问。所有的互斥体对象有两个基本的内部特性，它为在系统线程中间共享的资源提供这种保护：

§ 所有权

内核一次把给定的互斥体的所有权分配到一个单独的线程，这的结果可能如下：

§ 如果拥有线程的优先级还不在实时范围内，提高那个线程的实时优先级到最低的实时优先级

§ 防止拥有线程的处理离开平衡设置：即，处理将不被页调出到二级存储

§ 防止正常的内核模式异步过程调用（APC）被交付。线程将不被APC先占，除非内核发布一个APC_LEVEL软件中断以运行特殊的内核APC，诸如把结果返回到I/O操作的原来请求者的I/O管理器的IRP完成例程。

线程能获取它已经拥有（递归的所有权）的一个互斥体对象的所有权，但是一个递归获取的互斥体对象不能被设置为有信号状态，直到线程完全释放其所有权。在另一个线程能获得这个互斥体之前，这样的线程必须明确释放互斥体的次数要和它获得所有权的次数相等。

如果首先释放互斥体并且设置它为有信号状态，内核从不允许拥有互斥体的线程引发一个向用户模式的转变。如果拥有互斥体的任何FSD创建的或者驱动程序创建的线程试图在释放互斥体的拥有权之前向I/O管理器返回控制，内核停止系统。

§ 层

驱动程序线程能获取多个互斥体。在这种情况下，驱动程序应该被设计成每个互斥体对象的层确定这样一种线程是否能在任何特定时刻获取互斥体的所有权。特别地，驱动程序应该加强下列策略：

§ 线程应该按指定层的上升次序来获取互斥体。当它初始化互斥体的时候，驱动程序能分配一个非零的层。

然而，互斥体的所有者不可试图获取带有较低Level的另一个互斥体，因为这个次序互斥体可以已经被另一个线程拥有，而这个线程正在试图获取第一个线程的较高的Level的互斥体。如果驱动程序不加强这项策略，结果将造成死锁，因为没有线程可以获取其他线程当前拥有的互斥体的所有权。

使用互斥体对象的任何驱动程序在它等待或释放其互斥体之前，必须调用KeInitializeMutex一次。图3.25说明两个系统线程可以怎样使用一个互斥体对象。

图3.25 等待一个互斥体对象

如图3.25所示，这样的驱动程序必须为互斥体对象提供存储。因为驱动程序仅仅能在线程上下文之内等待并且释放互斥体，创建它自己系统线程的驱动程序能在页式存储池中为它的互斥体对象提供存储，这里的页式存储池是由驱动程序分配的。

如果这样的最高层的驱动程序在其Dispatch例程中使用系统工作者线程或者等待互斥体，它必须在下列地方提供常驻存储：在驱动程序创建的设备扩展中、在控制器扩展中（如果它使用控制器对象）、或者在被驱动程序分配的非页式存储池中。

当AddDevice例程调用KeInitializeMutex，它必须传递指向为互斥体对象提供的驱动程序的存储空间的指针，内核初始化这个互斥体对象为有信号状态。此外，如果调用者使用多个互斥体，调用者能为互斥体对象（如图3.25中所示）指定一个非零的Level，以控制获得次序。

在这样的最高层的驱动程序已经初始化之后，它能管理相互排斥地对如图3.25中所示的共享资源的访问。例如，驱动程序的固有同步操作的Dispatch例程和线程可以使用互斥体为IRP保护驱动程序创建的队列。

因为KeInitializeMutex总把一个互斥体对象的初始状态设置为有信号，如图3.25所示：

1. Dispatch例程使用互斥体指针对KeWaitForSingleObject的调用立即把当前线程放到就绪准备状态中，给线程互斥体的所有权，并且把互斥体的状态复位到无信号。一旦Dispatch例程恢复运行，它能把IRP安全地插入到互斥体保护的队列中。

2. 当第二个线程（另一个Dispatch例程，驱动程序提供工作者线程回调例程，或者驱动程序创建的线程）使用互斥体指针调用KeWaitForSingleObject，第二个线程被放到就绪状态中。

3. 当Dispatch例程结束如在步骤1中所说的排队IRP，它使用互斥体指针和一个Wait布尔值调用KeReleaseMutex，这个布尔值表明一旦KeReleaseMutex返回控制，它是否计划使用互斥体调用KeWaitForSingleObject（或者KeWaitForMutexObject）。

4. 假定Dispatch例程在步骤3（等待放置把为FALSE）中释放其互斥体所有权，互斥体被KeReleaseMutex设置为有信号状态。互斥体当前没有所有者，因此内核确定是否另一个线程正在等待那个互斥体。如果如此，内核使第二个线程（见步骤2）成为互斥体的所有者，把线程的优先级有可能提升到最低的实时优先级值，并且把其状态改变成就绪。

5. 一旦处理器可以使用，内核为执行发送第二个线程：即，没有其他带有较高优先级的线程当前是在就绪状态，并且没有内核模式的例程被运行在提高的IRQL（大于PASSIVE_LEVEL）上。第二个线程（排队IRP的Dispatch例程，或者驱动程序的工作者线程回调例程或者驱动程序创建的排队IRP的线程）能安全地访问互斥体保护的IRP队列，直到它调用KeReleaseMutex。

如果线程递归地获取一个互斥体对象的所有权，线程必须明确调用KeReleaseMutex与它等待互斥体一样多的次数，以设置互斥体对象为有信号状态。例如，如果线程调用KeWaitForSingleObject，然后使用同样的互斥体指针调用KeWaitForMutexObject，为了设置互斥体对象为有信号状态，当它获得互斥体时，它必须调用KeReleaseMutex两次。

使用被设置为TRUE的Wait参数调用KeReleaseMutex表明调用者的意向，即在从KeReleaseMutex返回时立即调用KeWait..Object(s)支持例程。

为KeReleaseMutex设置Wait参数，考虑下列的方针：

§ 在IRQL PASSIVE_LEVEL运行的可分页线程或者可分页驱动程序例程应该从不使用被设置为TRUE的Wait参数调用KeReleaseMutex。如果调用者碰巧在调用KeReleaseMutex和KeWait..Object（s）中间被页调出，这样的调用将引起致命的错误。

§ 如果没有停止系统，在大于PASSIVE_LEVEL的IRQL运行的任何标准驱动程序例程不能在任何调度者对象上等待非零间隔。（附加的方针参见“内核调度者对象”。）然而，这样的例程在它运行在小于等于DISPATCH_LEVEL的IRQL上的时候，如果它拥有互斥体，它可以调用KeReleaseMutex。

对于标准驱动程序例程在其上运行的IRQL的总结，参见第16章中的“管理硬件优先级”。关于支持例程特定的IRQL请求，参见《Windows 2000驱动程序开发参考》卷2。
1.9.5 线程在调度者对象上等待的报警和APC的处理

一个等待的线程对报警和APC的运送作出反应的方法依赖于一些因素。在某些情况下，一个报警将引起等待被放弃，而在其他情况下又不会这么样。同样地，仅仅在一定的环境下APC被传送等待线程。表3.1总结了线程在调度者对象上等待的报警和APC的处理。

四种内核支持例程在调度者对象上开始等待：KeWaitForSingleObject、KeWaitForMultipleObjects、KeWaitForMutexObject、和KeDelayExecutionThread。所有这些例程中的四个使用一对布尔值参数：Alertable和WaitMode。可能被分配到这两个参数的值的结果是复杂的。例如，把TRUE值分配到Alertable的参数，不保证等待的线程能被报警。本节余下的部分提供关于Alertable和WaitMode参数的设置如何影响等待线程的行为的总结。

表3.1 等待线程的报警和APC的处理

KeWaitForXxx例程的参数设置 | 特定的内核模式的APC |  | 正常的内核模式的APC |  | 用户模式的APC |  | 警报
 | 放弃等待？ | 交付和执行APC？ | 放弃等待？ | 交付和执行APC？ | 放弃等待？ | 交付和执行APC？ | 放弃等待？
Alertable=TRUE WaitMode=User | 否 | 如果(A)则是 | 否 | 如果(B)则是 | 是 | 时，在线程返回到用户模式以后 | 是
Alertable=TRUE WaitMode=Kernel | 否 | 如果(A)则是 | 否 | 如果(B)则是 | 否（因为WaitMode=Kernel） | 否 | 是
Alertable=FALSE WaitMode=User | 否 | 如果(A)则是 | 否 | 如果(B)则是 | 否（因为Alterable=FALSE） | 否（随着例外。EX.^C以终止） | 否
Alertable=FALSE WaitMode=Kernel | 否 | 如果(A)则是 | 否 | 如果(B)则是 | 否（因为Alterable=FALSE并且WaitMode=Kernel） | 否 | 否
(A) IRQL<APC_LEVEL (B) IRQL<APC_LEVEL，APC中线程没有就绪，线程不在临界区中

Alertable参数规定线程是否能被来自未说明的内核例程或由用户模式APC生成的警告报警，以及它的等待状态是否被放弃。如果警报的值是FALSE，那么无论WaitMode参数值是什么或报警的起源是什么，线程却不能被报警。这项规则的唯一例外情况是终止线程的情况。线程自动地被设成可报警的，例如，当被用户用Ctrl+C终止。

如果警报的值是TRUE，而下列的条件之一出现，线程将被报警：

1. 警报的起源是内部的、没有说明的内核模式例程使用报警线程。

2. 警报的起源是用户模式APC，并且WaitMode参数的值是UserMode。

在这两种情况的前一种中，线程的等待被使用完成状态STATUS_ALERTED满足；在第二种情况下，它被使用完成状态STATUS_USER_APC满足。

线程必须对用户模式APC被传送是可报警的。然而，即使线程不是被报警，内核模式APC能被传送和执行。一旦APC执行完成，线程的等待恢复。线程从不因内核模式APC的运送被报警，它的等待也不因此被放弃。

内核模式APC到一个等待线程的运送不依赖于线程是否能被报警。如果APC是一个特殊的内核模式APC，假若IRQL<APC_LEVEL，那么APC被传送。如果APC是一个正常的内核模式APC，假如下列的三个条件成立,那么APC被传送：（1）IRQL<APC_LEVEL；（2）没有内核模式APC是在处理中的；（3）线程不是在一个临界区中。

注意，不同于KeWaitXxx例程，程序KeDelayExecutionThread不获得Object参数。 KeDelayExecutionThread在与调用线程有关的调度者对象上开始等待。在调度者对象上等待的线程响应警报和APC传送的方式与在其他调度者对象上等待的线程相同。

1.10 回调对象

内核的回调机制为驱动程序提供一种一般方法使得当一定的条件被满足时，驱动程序请求和提供通知。

驱动程序能创建回调对象，而其他驱动程序能为与这个驱动程序定义的回调有关的条件请求通知。此外，系统定义两个回调对象供驱动程序使用。

所有的回调对象拥有名称和一组属性，它们在对象被创建时候被定义。系统定义的回调对象被命名为\Callback\SetSystemTime和\Callback\TowerState；驱动程序定义的回调不可复制这些名称。细节参见“系统定义的回调对象”。

为了从系统或者驱动程序定义的回调请求通知，驱动程序打开回调对象并且注册回调例程。当为回调定义的条件变成TRUE，其创建者触发通知。反过来，系统调用所有为回调注册的回调例程。
1.10.1 定义回调对象

驱动程序能创建回调对象，通过回调对象，其他驱动程序能请求由创建驱动程序定义的条件的通知。图3.26显示了与定义回调对象有关的步骤。

图3.26 定义回调对象

在创建对象之前，驱动程序调用InitializeObjectAttributes以设置其属性。回调对象必须有名称，它不能与系统定义的回调名称相同；它可以拥有其创建者认为适当的任何属性，典型地有OBJ_CASE_INSENSITIVE。其次，驱动程序调用ExCreateCallback，传递指向初始化的属性和接收句柄位置的指针到回调对象。它也传递两个布尔值，表明如果这样的一个命名的对象还不存在，系统是否应该创建回调对象，以及对象是否应该允许多个注册的回调例程。

驱动程序为它将要调用的那个注册的回调例程定义条件。条件的形式是两个变元，每个指向创建回调的驱动程序所定义的一个参数。你应该为驱动程序的客户说明这些条件，连同回调对象的名称和它在其上请求通知的IRQL。

当回调条件发生，驱动程序调用ExNotifyCallback，把其句柄传递到回调对象和两个变元。 系统按照回调例程被注册的次序调用所有为回调对象注册的回调例程，传递两个变元和指向在例程被注册时提供的上下文的指针。驱动程序必须在IRQL<=DISPATCH_LEVEL时调用ExNotifyCallback；系统在与它在其上作这个调用的相同的IRQL上调用回调例程。

所有带有回调对象的操作被完成后，创建回调的驱动程序应该调用ObDereferenceObject以减少它的引用计数，并且确保这个对象被删除。
1.10.2 使用驱动程序定义的回调对象

为了使用另一个驱动程序定义的回调对象，驱动程序打开这个对象，然后注册一个例程，这个例程在回调被触发时被调用，如图3.27所示。驱动程序请求通知必须知道回调对象的名称，并且必须理解传递到回调例程的变元的语义。

图3.27 为回调通知注册

在它能打开对象之前，驱动程序必须调用InitializeObjectAttributes以创建属性块，它指定了对象的名称。在它拥有指向属性块的指针之后，它调用ExCreateCallback，传递属性指针、在其中为回调接收句柄的位置、和值为FALSE的Create参数，这个参数表明它请求一个存在的回调对象。

驱动程序然后能使用返回的句柄调用ExRegisterCallback以注册其回调例程。

回调例程拥有以下原型：

typedef VOID (*PCALLBACK_FUNCTION) (

IN PVOID CallbackContext,

IN PVOID Argument1,

IN PVOID Argument2

);

每次回调例程被调用时，CallbackContext参数是被传递到回调例程的上下文。除非上下文仅仅是32位的数据，如果这个例程能被在DISPATCH_LEVEL调用，调用者应该从非页式存储池中分配它。两个变元被创建回调的组件定义。典型地，变元提供有关触发回调的条件的信息。

当回调的创造者触发通知，系统调用注册的例程，传递指向上下文的指针和两个变元。变元的值由创建回调的组件提供。回调例程在与创建驱动程序在其上触发的相同IRQL上被调用，通常是IRQL<=DISPATCH_LEVEL。

在其回调例程中，驱动程序能执行它为当前条件请求的任何任务。

当驱动程序不再请求通知，它应该调用ExUnregisterCallback以从注册的回调列表中移出它的例程，并且移走它的到回调对象的引用。
1.10.3 使用系统定义的回调对象

系统为驱动程序使用定义两个回调对象：

\Callback\SetSystemTime

\Callback\PowerState

使用系统时间（例如，文件系统驱动程序）的驱动程序可以为SetSystemTime回调注册。当系统时间变化，这个回调规定通知。

当下例情况之一发生，PowerState回调规定通知：

§ 系统从AC到DC电源的切换，或者反之亦然。

§ 由于用户或者应用请求而造成的系统电源策略变化。

§ 即将到来的到系统睡眠或者关闭状态的转换。驱动程序能请求通知，这样它能把代码锁到内存中期待着中断。回调例程将在电源管理器发送系统设置电源IRP之前被通知。

为了使用系统定义的回调，驱动程序使用回调的名称初始化属性块（InitializeObjectAttributes），然后打开回调对象（ExCreateCaIlback），这个对象正好就是给驱动程序定义的回调的。驱动程序不应该请求回调对象被创建。

使用被ExCreateCallback返回的句柄，驱动程序调用ExRegisterCallback以注册通知例程，传递一个指向专用环境的指针和指向它的例程的指针。驱动程序能在任何时间注册其回调例程。当指定的条件发生，系统在IRQL<=DISPATCH_LEVEL调用注册的回调例程。

当驱动程序不再请求通知，它应该调用ExUnregisterCallback以从注册的回调列表中删除它的回调例程，并且移走对回调对象的引用。


驱动程序基本结构

本章概述了Microsoft Windows 2000 和WDM驱动程序的标准例程，以使你可以明确以下内容：

§ 任何处理IRP的驱动程序所必需的标准例程

§ 一个驱动程序可能需要的其他标准例程，这取决于下层设备的属性或该驱动程序的必要功能

§ 通常是如何开始设计和开发一个新的驱动程序

一些驱动程序与系统提供的，定义了大量驱动程序必要功能的端口驱动程序或类驱动程序互相作用。例如，一个SCSI微端口驱动程序主要与SCSI端口驱动程序相互作用。对于这类驱动程序，详见必要和可选的驱动程序支持的类特定的文档。

本章包含以下信息：

4.1 标准驱动程序例程

4.2 最低层设备驱动程序的分段IRP处理

4.3 中间层设备驱动程序的分段IRP处理

4.4 设计和开发一个驱动程序

4.4.1 设备对象命名和设备接口注册

4.4.2驱动程序例程命名

4.4.3 开始设计

4.4.4 开始开发

关于每个标准驱动程序例程必要功能的具体内容，见第5章到第15章。关于任何本章中提及的系统提供支持的例程，见《Windows 2000 驱动程序开发指南》第2卷。

1.1 标准驱动程序例程

一个Microsoft Windows 2000 或WDM 驱动程序的基本结构包括一组必要的，系统定义的标准驱动程序例程，加上一些可选的标准例程与内部例程，这取决于驱动程序的类型和下层设备。公共标准例程组允许所有的内核模式驱动程序通过调用一个系统提供支持的例程来处理IRP。

图4.1显示一个驱动程序对象，表示一个驱动程序装入内存的情况，及内核模式驱动程序的标准例程组。

图4.1 驱动程序对象与标准驱动程序例程

如图4.1所示，所有的驱动程序，不管它们在附属驱动程序链中所处的层，都必须有一组基本标准例程以处理IRP。（那些当它们被调用时传送一个IRP的例程被用一个星号标出。）如图所示，这些例程的入口点在驱动程序对象中设定。一个驱动程序是否必须执行附加标准例程取决于该驱动程序是控制一个物理设备的驱动程序，还是在一个物理设备驱动程序之上的驱动程序，也取决于下层物理设备的属性。控制物理设备的最低层驱动程序比较高层驱动程序拥有更多要求的例程，较高层驱动程序一般传送IRP给较低层驱动程序处理。

表4.1列出了标准驱动程序例程，概述了它们的目的，以及每个例程在那些驱动程序中被需要。

例程 | 哪些驱动程序需要？ | 任务
DriveEntry | 所有驱动程序 | 初始化驱动程序并设置其他标准例程的入口点
AddDevice | 所有驱动程序 | 创建设备对象
Dispatch | 所有驱动程序必须至少有一个 | 用一个或多个主要功能编码处理IRP
StartIo or Queue Management | 对于在Dispatch例程中不能完成所有I/O请求的最低层设备驱动程序是必需的。对于更高层驱动程序是可选的 | 为驱动程序管理IRP队列
Reinitialize | 对所有驱动程序可选 | 在其他驱动程序装入之后完成附加的初始化任务
InterruptServiceRoutine（ISR） | 对于任何产生中断的最低层设备驱动程序是必需的 | 响应来自一个物理设备的中断
DpcForIsr or CustomDpc | 对于任何包含ISR的驱动程序是必需的 | 在ISR返回后处理中断
SynchCritSection | 对于任何与它的ISR或其他例程共享数据的驱动程序是必需的 | 同步访问共享数据
AdapterControl | 对于完成DMA的驱动程序是可选的 | 完成DMA转换
ControllerControl | 对于使用物理控制器的Windows 2000 驱动程序是可选的 | 管理物理控制器
Cancel | 对于任何在一段不确定的时间段中对IRP排队的驱动程序是必需的。通常每个栈中的最高层驱动程序至少有一个撤销例程。 | 在一个IRP被撤消后清除它
IoCompletion | 对任何分配与发送IRP的驱动程序必需。 | 释放驱动程序分配的IRP，并且执行任何其他任务
IoTimer | 对任何驱动程序可选 | 以一个定长的、一秒间隔周期执行某个任务。
CustomTimerDpc | 对任何驱动程序可选 | 以一个短于一秒或可变的时间间隔周期执行某个任务。
Unload | 任何在系统继续运行时能够被卸载的驱动程序 | 清除以使驱动程序能够被卸载。

当前IRP及目标驱动程序对象是许多标准例程的输入参数。所有驱动程序通过其标准例程组分段处理每个IRP。就象在“最低层设备驱动程序分段IRP处理”和“中间层设备驱动程序分段IRP处理”中所描述的。

1.2 最低层设备驱动程序的分段IRP处理

如图4.1所示，最低层物理设备驱动程序拥有较高层驱动程序不需要的某些标准例程。这组最低层驱动程序标准例程也根据下列标准不同而不同：

§ 每个驱动程序控制的设备的属性

§ 驱动程序是否为其设备对象设置直接或缓冲I/O，如同第三章所描述的。

§ 单个驱动程序的设计

为说明标准驱动程序例程的作用。图4.2显示了一个样例IRP的路径，它由一个最低层海量存储设备驱动程序处理。图中的驱动程序具有如下特点：

n 设备在每次I/O操作结束时产生中断。因此这个驱动程序有ISR和DpcForIsr例程。

n 该驱动程序拥有一个StartIo例程，而不是建立内部IRP队列并管理自己的队列。

n 这个驱动程序使用系统DMA，因此它为直接I/O设置其设备对象标志，如同第三章所描述的，并且它拥有一个AdapterControl例程。

图4.2 通过最低层驱动程序例程的IRP路径

如图4.2所示，对于给定的主功能码（IRP_MJ_XXX），一个IRP首先被发送给驱动程序的Dispatch例程，在这个例子中是DDDispatchReadWrite。因为在驱动程序初始化时，每个驱动程序在驱动程序对象中基于IRP_MJ_XXX设置其Dispatch例程，所以操作系统从来不会向一个驱动程序发送有未知功能码的IRP。

调用IoGetCurrentIrpStackLocation

处理多于一个IRP_MJ_XXX，用次级功能（IRP_MN_XXX）处理IRP_MJ_XXX，或者处理设备I/O控制请求（IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL）的Dispatch例程，及所有其他处理每个IRP的驱动程序例程必须调用IoGetCurrentIrpStackLocation以决定做什么及使用什么参数。

图4.2中的IRP请求一个数据传输操作（IRP_MJ_READ或者IRP_MJ_WRITE），并且这个驱动程序的I/O栈位置在IRP中最低，如图有不确定个较高层驱动程序的I/O栈位置在它上面。为简单起见，从DispatchReadWrite、StartIo、AdapterControl和DpcForIsr例程中调用IoGetCurrentIrpStackLocation没有在图4.2中显示。

调用IoMarkIrpPending和IoStackPacket

样例驱动程序不是在其Dispatch例程中完成IRP，而是在其StartIo例程中处理IRP。在它这样做之前，Dispatch例程调用IoMarkIrpPending以表明该IRP并没有完成。然后它调IoStartPacket将该IRP排队或传送该IRP给驱动程序的StartIo例程以便进一步处理。Dispatch例程也返回NTSTATUS值STATUS_PENDING。

图4.3 调用IoStartPacket

如果驱动程序正忙于在设备上处理其他IRP，IoStartPacket把该IRP插入到与设备对象相连的设备队列中。驱动程序可以选择提供一个Key值作为参数给IoStartPacket，以将驱动程序确定的命令作用于设备队列中的IRP。

如果驱动程序不忙，而且设备队列空，I/O管理器立即调用其StartIo例程，传送输入IRP。

当它因为以下两个原因调用IoStartPacket时，一个海量存储设备的最低层驱动程序不需要提供一个Cancel例程：

1. 一个在这类驱动程序上分层的文件系统处理文件I/O请求的撤消。

2. 海量存储设备驱动程序迅速处理IRP。

通常，在一系列分层的驱动程序中的最高层驱动程序处理IRP的撤消。

调用AllocateAdapterChannel和MapTransfer

假定图4.2中所示的StartIo例程发现传输请求可以由一个单独DMA操作来做，StartIo例程用驱动程序的AdapterControl例程入口点和IRP调用AllocateAdapterChannel。

当系统DMA控制器可用时，I/O管理器调用驱动程序的AdapterControl例程设置传输操作，如图4.2所示。AdapterControl例程调用MapTransfer设置系统DMA控制器。这时驱动程序为DMA操作对其设备编程并返回。（关于使用DMA和适配器对象的更详细的信息，见第3章。）

从驱动程序的ISR调用IoRequestDpc

当设备中断表明其传输操作完成时，驱动程序的ISR从产生中断处终止设备并调用IoRequestDpc，如图4.2所示。

这个调用对驱动程序的DpcForIsr例程排队，以在一个较低的硬件优先级（IRQL）上完成尽可能多的传输操作。

调用IoStartNextPacket和IoCompleteRequest

当DpcForIsr例程已经进行传输处理时，它迅速调用IoStartNextPacket以使驱动程序的StartIo例程被设备队列中下一个IRP调用，如果排队的话。DpcForIsr例程也设置刚完成的IRP的I/O状态块并用IRP调用IoCompleteRequest。

图4.4说明驱动程序对IoStartNextPacket和IoCompleteRequest的调用。

驱动程序应当调用IoStartNextPacket或IoStartNextPacketByKey，以尽可能快的开始下一个请求的I/O操作，这个调用最好在它们调用IoCompleteRequest之前，并且必须在它们返回控制之前做。

图4.4 调用IoStartNextPacket和IoCompleteRequest

如果IRP 为设备排队，IoStartNextPacket调用KeRemoveDeviceQueue将下一个IRP从队列中删除。这时I/O管理器调用驱动程序的StartIo例程，传送从队列中取出的IRP。如果当前设备队列中没有IRP，IoStartNextPacket只是返回到调用它的例程。

在一个IRP中设置I/O状态块

调用IoCompleteRequest之前，所有最低层驱动程序必须在IRP中设置I/O状态块（在图中，第二个共享区域表示状态块）。I/O状态块为任何较高层驱动程序提供信息，最终提供给该I/O操作的最初请求者。任何在图4.4中的驱动程序之上分层的较高层驱动程序都创建一个IoComplete例程以读取由该设备驱动程序设置的I/O状态块。当然，较高层驱动程序通常并不改变一个已由设备驱动程序完成的IRP的I/O状态块，除非这个较高层驱动程序正重试这个IRP，并且因此重新初始化I/O状态块。

所有完成一个IRP，而不是把它发送到相邻较低层驱动程序的较高层驱动程序也必须在调用IoCompleteRequest之前设置该IRP的I/O状态块。为了良好的I/O流量，一个较高层驱动程序应检查其每个IRP的I/O栈位置中的参数，并且，如果这个参数是无效的，应自己设置I/O状态块并完成该请求。无论何时，一个驱动程序应尽可能避免传送一个无效请求给驱动程序链中的较低层驱动程序。

I/O状态块被定义如下：

typedef struct _IO_STATUS_BLOCK {

NTSTATUS Status;

ULONG Information;

} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

假定图4.4中的传输操作成功，图4.2中所示的DpcForIsr例程设置该IRP的I/O状态块中的Status为STATUS_SUCCESS，Information为传输的字节数。

很多标准驱动程序例程也返回NTSTATUS类型的值。关于象STATUS_SUCCESS这样的NTSTATUS常量的更多信息，见第16章的“错误记录和NTSTATUS值”。

在调用IoCompleteRequest时提高优先级

如果一个最低层驱动程序可以在其Dispatch例程中完成一个IRP，它用一个值为IO_NO_INCREMENT的PriorityBoost调用CompleteRequest。不需要在运行时提高优先级，因为驱动程序可以假定初始的请求者并不等待它的I/O操作。

否则，最低层驱动程序提供一个系统定义的，设备类型特定的值提高请求者的运行时优先级，以减少请求者等待其设备I/O请求所花的时间。关于这个提高值，见wdm.h或ntddk.h。

当它们用一个IRP调用IoCompleteRequest时，较高层驱动程序使用相同的PrioritBoost作为其各自的下层设备驱动程序。

调用IoCompleteRequest的结果

当驱动程序用一个IRP调用IoCompleteReqest时，I/O管理器在调用相邻较高层驱动程序之前将该驱动程序的I/O栈位置清零，这个较高层驱动程序创建一个IoCompletion例程为该IRP调用。

一个较高层驱动程序的IoCompletion例程仅能检查该IRP的I/O状态块以决定如何处理所有较低层驱动程序的请求。

IoCompleteRequest的调用者切不可试图访问刚刚完成的IRP。这种企图是一种会导致系统崩溃的程序性错误。

1.3 中间层驱动程序的分段IRP处理

如图4.1所示，与最低层设备驱动程序相比，较高层驱动程序有一组不同的标准例程，并有一个对两种类型的驱动程序都是公共的重叠的标准例程子集。

这组中间层和最高层驱动程序的例程也依下列标准的不同而变化：

§ 下层物理设备的属性

§ 下层物理设备是否为直接或缓冲I/O设置设备对象

§ 单个较高层驱动程序的设计

图4.5举例说明一个IRP通过在一个前一部分所描述的最低层设备驱动程序上分层的中间层镜像驱动程序的路径。

图4.5 通过中间层驱动程序例程的IRP路径

图4.5所示的驱动程序有以下特点：

§ 驱动程序在至少一个物理设备和可能至少一个设备驱动程序上分层。

§ 驱动程序有时为较低层驱动程序分配附加IRP，这取决于输入IRP的请求操作。

§ 驱动程序有至少一个文件系统驱动程序在其上分层，并且该文件系统驱动程序可以在比这一驱动程序更高的其他中间层驱动程序上分层。

如图4.5所示，I/O管理器首先发送一个IRP_MJ_WRITE请求给中间层驱动程序的Dispatch例程。这个IRP请求一个数据传输操作（IRP_MJ_WRITE）。该中间层驱动程序的I/O栈位置在中间显示，有不确定个较高层或较低层驱动程序的I/O栈位置在它上面或下面。

分配IRP

这个驱动程序向另一个物理设备映射写请求，而向拥有镜像分区的设备的驱动程序交替发送读请求。对于写请求，它为原始设备与数据被镜像的一个二级设备创建IRP，假定在输入IRP中的参数是有效的。

图4.5恰好显示一个对IoAllocateIrp的调用，但是较高层驱动程序可以调用其他支持例程为它们发给较低层驱动程序的请求分配IRP。关于分配IRP的更多信息可参见第6章。

当Dispatch例程调用IoAllocateIrp时，它指定每个新IRP中需要的I/O栈位置数量。驱动程序必须为链中每个较低层驱动程序指定一个栈位置，从每个位于镜像驱动程序之下的设备对象获取适当的值。根据驱动程序开发者的意愿，当它调用IoAllocateIrp以获取一个自己的栈位置（它在每个IRP中分配的）时，它可以为这个值加一，如同图4.5中的驱动程序所做的那样。

这个中间层驱动程序的Dispatch例程用一个初始IRP调用IoGetCurrentIrpStackLocation（不显示）以决定它应当做什么，如果可能的话，还检查参数。

因为它给每个新创建的IRP分配它自己的栈位置，所以调用IoSetNextIrpStackLocation和IoGetCurrentIrpStackLocation为它自己创建它随后要在IoCompetion例程中使用的环境。

接着，它用一个新创建的IRP调用IoGetNextIrpStackLocation以便它可以在它分配的IRP中设置相邻较低层驱动程序的I/O栈位置。镜像驱动程序的Dispatch例程拷贝IRP功能码与参数（指向传输缓存的指针，表示将为IRP_MJ_WRITE传输的比特长度）到相邻较低层驱动程序的I/O栈位置。这些驱动程序，依次为它们下层的驱动程序设置I/O栈位置。

调用IoSetCompletionRoutine和IoCallDriver

图4.5中的Dispatch例程用它分配的任一IRP调用IoSetCompletionRoutine。

一个较高层驱动程序可以在较低层驱动程序用STATUS_SUCCESS、STATUS_CANCELLED或错误状态中的一个、一些或任意一个完成一个IRP时请求其被调用的IoCompletion例程。由于图4.5中的驱动程序必须释放它所分配的IRP，该驱动程序在较低层驱动程序完成它的IRP时设置IoCompletion例程以供调用，而不管它们的完成状态值是什么。

由于图4.5中的驱动程序并行映射，它传送通过两次调用IoCallDriver分配给相邻较低层驱动程序的两个IRP。每次每个目标设备对象表示一个镜像的分区。

在驱动程序的IoCompletion例程中处理IRP

当两组较低层驱动程序完成请求的操作时，I/O管理器调用中间层镜像驱动程序的IoCompletion例程。当较低层驱动程序已完成所有DupIrpN请求时，镜像驱动程序在其自身初始IRP的I/O栈位置中维护一个计数值以跟踪它。

假定I/O栈位置表明一组较低层驱动程序已成功完成图4.5中所示的DupIRP1，镜像驱动程序的IoCompletion例程减小其计数，但是不能完成初始IRP，除非它将值减到零。如果减后的值不是零，IoCompletion例程用第一次返回的驱动程序分配的IRP（图4.5中的DupIRP1）调用IoFreeIrp，并返回STATUS_MORE_PROCESSING_REQUIRED。

当镜像驱动程序的IoCompletion例程被图4.5中的DupIRP2再次调用时，IoCompletion例程减小初始IRP中的计数值，并确定两组较低层驱动程序已执行的请求操作。

假定DupIRP2中的I/O状态块也被设成STATUS_SUCCESS，IoCompletion例程从DupIRP2拷贝I/O状态块到初始IRP并释放DupIRP2。它用初始IRP调用IoCompleteRequest并返回STATUS_MORE_PROCESSING_REQUIRED。返回这个状态防止I/O管理器尝试任何DupIRP2上进一步的完成处理；由于IRP没有与线程相连，它的完成处理应由创建它的驱动程序终止。

如果任一组较低层驱动程序没有成功完成镜像驱动程序的IRP，镜像驱动程序的IoCompletion例程应记录错误并尝试适当的镜像数据恢复。更多信息，见第16章的“错误记录和NTSTATUS值”。

1.4 设计和开发一个驱动程序

以下部分提供了关于开始设计和开发过程的一般建议。
1.4.1 设备对象命名和设备接口注册

用户模式的可以直接I/O请求的物理、逻辑或虚拟设备驱动程序必须为其用户模式客户提供某种名字。当然，PnP驱动程序并不采用Windows NT 4.0及其更早版本中的驱动程序那样的方式为设备对象指定名字。相反，对于每个这类I/O请求可能发送的设备对象，PnP驱动程序注册并激活一个与GUID连接的设备接口。

下面是设备驱动程序注册一个接口必须遵循的步骤：

1. 当PnP总线驱动程序或其他最低层驱动程序调用IoCreateDevice创建PDO时，它应为设备名传送FILE_AUTOGENERATED_DEVICE_NAME和FILE_DEVICE_SECURE_OPEN标志，连同一个NULL串。作为响应，Windows为PDO选择一个唯一的设备名。

2. 当中间层或较高层驱动程序（PnP功能与选择驱动程序）调用IoCreateDevice时，它们也传送FILE_DEVICE_SECURE_OPEN标志。这些驱动程序禁止给它们的设备对象命名。

3. 创建设备对象并把它放入设备栈后，驱动程序调用IoRegisterDeviceInterface，传送一个标识被注册的接口的设备接口类GUID。显然，功能驱动程序从其AddDevice例程产生该调用。系统在设备特定的头文件中为公共设备接口定义GUIDs。

I/O管理器为设备接口创建一个注册键并返回一个符号链接到接口。

4. 如果驱动程序能成功启动设备，它通过调用IoSetDeviceInterfaceState激活接口，传送符号链接。这个调用可以象驱动程序处理PnP IRP_MN_START_DEVICE请求那样做。

当一个应用程序或终端用户调用一个Win32函数连接一台打印机的端口，如LPT1，Win32子系统将应用程序I/O请求解释为一个对I/O管理器系统服务的调用，传送表示该打印机端口的象征性连接。
1.4.2 为驱动程序例程选择名字

除了DriverEntry例程，本文中的标准驱动程序例程名被用来描述每种类型标准例程的功能，并且有时候提供带有特定标准例程的驱动程序必须使用的何种类型对象的提示。在驱动程序中，你可以为大多数标准驱动程序例程起你喜欢的任何名字。

当然，使用一套区别于其他驱动程序例程命名机制的驱动程序会更容易开发、调试和测试。如果采用区别于驱动程序的标准例程及其内部例程的名字，一个驱动程序也更容易调试与测试。

因此，大多数系统提供的驱动程序采用遵循以下方针的例程名：

§ 为一个所有例程预先计划一个驱动程序特定前缀，最好所有标准例程用一个前缀，而另一个是所有内部例程的前缀。

§ 为每个例程创建一个表明它的功能以及调试时驱动程序中一个给定的IRP被处理的地方的名字。

例如，系统键盘类驱动程序预先定义KeyboardClass为它的标准驱动程序例程，Kbd为它的内部例程。另一个系统驱动程序使用一个单独定义的前缀给所有例程，但是预先定义小写字母“p”（私有的）作为它们的内部例程的前缀。

系统键盘类驱动程序中的每个例程也有一个表明该例程的功能的名字。该驱动程序的Dispatch例程根据每个例程控制的IRP_MJ_XXX命名：KeyboardClassOpenClose、KeyboardClassRead、KeyboardClassDeviceControl等等。该驱动程序中的其他标准例程命名为类似KeyboardClassStartIo和KeyboardClassCancel，以支持驱动程序开发者跟踪通过该驱动程序的IRP的路径。

键盘类驱动程序的命名约定也支持系统测试工程师评价该驱动程序的性能，以便发现任一特定驱动程序例程是否是一个性能瓶颈，并且如果错误发生时发现“bug”的准确位置。
1.4.3 开始设计

下列建议是一组一般的、可以用来开始设计一个驱动程序的设计标准：

哪个I/O请求？

编写任何代码之前，看一下DDK中关于该设备类型的文件以决定那个IRP主功能码是你的驱动程序必须处理的：

§ 如果要设计一个最低层物理设备驱动程序，那么驱动程序必须处理与同类型外设的所有其他驱动程序相同的一组IRP_MJ_XXX和与设备I/O控制码。

§ 如果要设计一个中间层驱动程序，识别下层物理设备或驱动程序将在其上被分层的设备。一个较高层驱动程序必须拥有其下的驱动程序必须处理的大多数IRP_MJ_XXX的Dispatch入口点。一个较高层驱动程序必须设置相邻较低层驱动程序在IRP中的I/O栈位置，并且当每个这样的IRP自己的I/O栈位置中的参数有效时，用IoCallDriver传送它们给较低层驱动程序。因此，任何中间层驱动程序必须在它的驱动程序对象中为那些将要传送的IRP_MJ_XXX设置Dispatch入口点。

§ 所有驱动程序必须为IRP_MJ_PNP与IRP_MJ_POWER提供Dispatch入口点。这些调度例程必须准备用这两个主IRP码处理IRP或者把它们传送给较低层驱动程序。

多少个Dispatch例程？

当已经识别了驱动程序必须处理的IRP_MJ_XXX时，可以决定驱动程序可能拥有的Dispatch例程数量的上限。也可以开始考虑是否将特定IRP_MJ_XXX加进由特定Dispatch例程处理的邻近子集中。

例如，大多数最低层与中间层驱动程序需要为一个IRP_MJ_CREATE（对于这些类型的驱动程序来说，相当于一个“打开目标设备”请求）或IRP_MJ_CLOSE请求做很少，或什么也不做，或IoCompletion处理。除了带可分页映像分区的下层磁盘设备和驱动程序，包含IRP_MJ_CREATE的IRP通常仅仅通过I/O管理器的系统服务为较高层驱动程序和用户模式保护子系统建立目标设备对象。对于较低层驱动程序，包含IRP_MJ_CLOSE的IRP通常表明一个用户模式子系统（代表一个应用程序）已关闭了为它发送I/O请求的设备处理的文件对象。

对于创建/关闭请求，许多最低层设备及中间层设备简单设置IRP的I/O状态块中的Status成员为STATUS_SUCCESS和Information成员为零，接着用来自于它们的Dispatch例程的IRP调用IoCompleteRequest。因此，对于创建/关闭请求，许多最低层及中间层驱动程序有一个合成的Dispatch例程。

当然，一个最低层串行设备驱动程序通常为一个创建请求重新设置它的设备；它可以在处理创建请求时锁定一个可分页的映像段，并在处理关闭请求时解锁它的可分页的映像段。最低层磁盘设备驱动程序仅在一个较高层驱动程序调用IoAttachDeviceToDeviceStack、IoAttachDevice或IoGetDeviceObjectPointer时被创建请求调用；由于任何磁盘驱动程序都可以控制处理系统页面文件的设备，它们不用关闭请求调用。另一方面，文件系统驱动程序为创建/关闭请求进行相当多的处理。

如果要设计一个较高层驱动程序，考虑你的驱动程序必须处理的IRP主功能码组，并决定需要用来执行一个IoCompletion例程的请求组。

一般，一个较高层驱动程序不必对每种请求都有一个IoCompletion例程。它必须有一个IoCompletion例程来释放驱动程序分配的IRP，如果有的话。否则，仅仅那些要求在较高层驱动程序中做进一步处理的IRP_MJ_XXX需要IoCompletion例程，这取决于较低层驱动程序如何处理IRP_MJ_XXX请求。

多少个设备对象和存储在设备扩展中的是什么？

紧接着，考虑驱动程序必须创建的设备对象：

§ 最低层驱动程序必须为每个可能成为I/O请求目标的物理或逻辑设备创建设备对象。例如，一个磁盘驱动程序必须为每个物理磁盘创建设备对象并且为该磁盘上的分区创建附加设备对象。

§ 一个较高层驱动程序必须创建表示其自身的虚拟设备的设备对象，以使更高层驱动程序可以连接它们的设备对象到该驱动程序的设备对象。另外，一个较高层驱动程序通常创建一组与相邻较低层驱动程序创建的设备对象对应的虚拟或逻辑设备对象。

如同在“开始开发”中概述的，可以分阶段开发驱动程序，以使一个未开发的驱动程序不需要创建完全开发的驱动程序拥有的所有设备对象。

开发者还需要确定驱动程序的设备对象的设备扩展的内容与结构。例如，如果设备扩展包含与较低层设备驱动程序的ISR及它的其他例程共享的环境，考虑设置设备扩展以隔离所有该设备扩展部分所共享的环境。然后，驱动程序的非ISR例程可以访问该设备扩展中的其他区域，而不必用SynchCritSection例程调用KeSynchronizeExcution。

又例如，如果一个非ISR例程与另一个例程共享一块区域，驱动程序设备扩展将包含一个执行自旋锁。该自旋锁保护共享区域以防两个例程同时访问。更多信息，见第16章的“自旋锁的使用”

驱动程序线程环境中的StartIo例程或队列管理？

如果为一个物理设备编写驱动程序，确定由于设备状态改变，设备I/O设置是否要求等待超过50微秒：

n 假如这样，驱动程序应当拥有一个设备专用线程，或者在一个文件系统驱动程序中，可能有一个或多个工作者线程回调进程。这样的驱动程序必须管理其自己的内部IRP队列。

n 如果不是，驱动程序可以拥有一个StartIo例程，并可以在其Dispatch例程中调用IoStartPacket，在其DpcForIsr（或CustomDpc）例程中调用IoStartNextPacket或IoStartNextPacketByKey，如“最低层设备驱动程序的分段IRP处理”中所描述的。如果这样，该驱动程序依靠I/O管理器管理IRP队列。
1.4.4 开始开发

开发分阶段编码的驱动程序更容易些，所以为了建立一个基本PnP功能驱动程序的框架并使它很快运行，这是一本“秘诀”。注意，过滤器驱动程序与总线驱动程序有略微不同的要求。另外，本DDK中包含多种驱动程序样例代码，在开发过程中你可以发现它们很有用处。

装载一个基本功能驱动程序

1．编写一个为AddDevice、DispatchPnP、DispatchPower和DispatchCreate例程设置入口点的DriverEntry例程。

2．编写一个完成下面内容的AddDevice例程：

§ 调用IoCreateDevice创建一个独立设备对象（如何设置一个设备对象见第3章）。

§ 调用IoAttachDeviceToDeviceStack把它自己加入设备栈。

§ 调用IoRegisterDeviceInterface为它的设备暴露一个接口。暴露的接口为访问该设备的用户模式应用程序提供了途径。

§ 为其类型的设备执行任何其他必要动作。

3．为IRP_MJ_PNP请求编写一个基本DispatchPnP例程。该DispatchPnP例程必须准备处理具体的PnP IRP，例如IRP_MN_START_DEVICE请求。当驱动程序处理启动的IRP，它必须调用IoSetDeviceInterfaceState以激活它先前注册的接口。关于该发送例程至少要做的工作，见《即插即用、电源管理和安装设计指南》。

4．为IRP_MJ_POWER编写一个基本DispatchPower例程。关于该发送例程至少要做的工作的描述，见《即插即用、电源管理和安装设计指南》。

5．接下来，为IRP_MJ_CREATE请求编写一个基本DispatchCreate例程。关于DispatchCreate例程至少要做的工作的描述，见“开始设计”。对于任何请求，一个较高层驱动程序的AddDevice例程必须建立一个到相邻较低层驱动程序的链接，并且每个Dispatch例程必须在IRP中设置相邻较低层驱动程序的I/O栈位置。

6．编译并链接驱动程序。

7．编写一个基本INF文件，和其他必需的文件，安装与装载驱动程序。

8．使用控制面板上的“添加/删除硬件”来安装驱动程序。当进行到选择一个驱动程序时，点击“从磁盘安装”并提供编译及链接后的驱动程序路径。

9．用一个简单的测试程序检测该基本驱动程序。

测试程序应调用Win32函数CreateFile以确保驱动程序被装入，并且I/O管理器可以发送IRP给驱动程序的DispatchCreate例程。为确定在CreateFile调用中说明的正确名字，测试程序应调用SetupDi例程。

支持对更多目标设备对象的附加请求

1．修改DriverEntry例程为IRP_MJ_CLOSE请求设置一个Dispatch入口点。

注意，为IRP_MJ_CREATE请求工作的基本Dispatch例程通常可以处理对一个最低层或中间层驱动程序的IRP_MJ_CLOSE请求，因此是否需要编写一个单独的DispatchClose例程完全取决于开发者。

2．编译与链接修改后的驱动程序。

3．如前一部分所述，测试驱动程序以确定所有驱动程序的新设备对象可以用Win32 CreateFile与CloseHandle函数打开和关闭。

支持设备I/O请求

1．为要求比创建/关闭请求进行更多处理的IRP_MJ_XXX添加另一个Dispatch例程。

例如，编写一个从设备到内存读数据或截取一组较低层驱动程序读请求的DispatchRead例程，也就是说，处理IRP_MJ_READ请求。

2．修改DriverEntry例程以便在驱动程序对象中设置DispatchRead入口点。修改DispatchPnP例程中的IRP_MN_START_DEVICE处理以建立任何将要处理读请求的对象。编写必需处理读请求的任何附加驱动程序。

§ 一个最低层驱动程序至少需要StartIo、ISR、和DpcForIsr例程，还可能需要SynchCritSection例程，并且如果设备使用DMA，还需要基本AdapterControl例程。

DriverEntry例程必须在驱动程序对象中设置StartIo入口点，并设置驱动程序的其他新例程。当DispatchPnP例程处理IRP_MN_START_DEVICE请求时，应注册ISR（中断对象），为DMA设置一个适配器对象，并初始化物理设备。注意，ISR可以在从IoConnectInterrupt成功返回后立即被调用，所以连接中断应当是启动设备编码的最后步骤。否则，驱动程序需要在注册ISR之前使设备的中断失效（之后再启用设备的中断）。

§ 一个较高层驱动程序拥有一个基本IoCompletion例程，这个例程至少检测I/O状态块是否为STATUS_SUCCESS，并用IRP调用IoCompleteRequest。

DriverEntry例程必须在驱动程序对象中设置DispatchRead入口点。DispatchRead例程必须调用IoSetCompletionRoutine以在IRP中设置驱动程序的新IoCompletion例程。

3．必要的话，修改设备扩展的内部结构：

§ 对于一个最低层驱动程序，设备扩展应当存储中断对象指针，让ISR和StartIo或DpcForIsr例程共享环境区域，如果设备使用DMA，还要存储适配器对象信息。

§ 对于一个较高层驱动程序，设备扩展必须存储相邻较低层驱动程序的设备对象指针。否则，它的内容和结构由驱动程序定义。

4．编译并链接修改的驱动程序。

5．通过调用Win32 CreateFile、ReadFile和CloseHandle函数，并通过使用内核调试器跟踪经过新驱动程序标准例程的IRP的路径，来测试这组例程。

继续通过编写新的Dispatch例程为驱动程序添加功能，并且在较高层驱动程序中，可能要编写新的IoCompletion例程来处理这组要求的IRP_MJ_XXX。按照要求修改DriverEntry例程及设备扩展（或其他驱动程序分配的存储）。如同添加新Dispatch例程一样，增加驱动程序的已存在例程，例如添加错误记录。在每个开发阶段测试驱动程序，并使用调试器跟踪通过驱动程序的IRP的路径以发现所有“bug”。

关于记录I/O错误的更多信息，见第16章的“错误记录和NTSTATUS值”。关于调试驱动程序的更多信息，见在线DDK的“Debugging Drivers”。


DriverEntry 和 Reinitialize 例程

本章概述了内核模式驱动程序的标准DriverEntry例程的必要功能。也概述了一个可选的Reinitialize例程的必要功能。

本章包含以下信息：

5.1 DriverEntry例程需求：

5.1.1 DriverEntry例程语法与参数

5.1.2 DriverEntry返回值

5.2 DriveEntry例程功能：

5.2.1 DriverEntry例程必备功能

5.2.2 DriverEntry例程可选功能

5.3 Reinitialize例程需求与功能

1.1 DriverEntry例程需求

每个驱动程序必须有一个DriverEntry例程以用来初始化驱动程序范围的数据结构和资源。当I/O管理器装入驱动程序时，它调用DriverEntry例程。对于一个支持“即插即用”（PnP）的驱动程序，就象所有的驱动程序将要支持一样，DriverEntry例程负责驱动程序的初始化，而AddDevice例程负责设备初始化（也可能是处理PnP IRP_MN_START_DEVICE请求的Dispatch例程）。驱动程序初始化包括输出该驱动程序其他入口点，初始化该驱动程序使用的特定对象，并设置每个驱动程序系统资源。（早期的驱动程序有差别较大的要求，就象Windows NT 4.0及其更早版本的DDK中所描述的那样。）
1.1.1 DriverEntry例程语法与参数

I/O管理器定义DriveEntry例程原型如下：

NTSTATUS

(*PDRIVER_INITIALIZE) (

IN PDRIVER_OBJECT DriverObject,

IN PUNICODE_STRING RegistryPath

);

假如驱动程序为链接器命名，该例程可以取一个不同于DriverEntry的名字。链接器必须有一个初始化例程的名字以便链接驱动程序，把地址传输给操作系统装入。通过对一个驱动程序初始化例程的命名，DriverEntry自动地建立这种链接。

就象在声明中所显示的那样，每个DriverEntry例程用两个输入指针被调用。DriverObject输入指针允许DriverEntry例程为它在驱动程序对象中的Dispatch、AddDevice、StartIo和Unload例程设置适当入口点。

DriverObject输入指针也使驱动程序可以访问DriverObject->HardwareDatabase，它指向一个指定注册表\Registry\Machine\Hardware内容树路径的Unicode字符串。

这个输入到DriverEntry例程的RegistryPath指向一个指定一条到驱动程序注册码\Registr\Machine\System\CurrentControlSet\Services\DriverName的路径的Unicode字符串。驱动程序可以用这个注册码装入驱动程序指定的信息和参数；具体细节可参见 《即插即用、电源管理和安装设计指南》第4部分第2章“注册表中的驱动程序信息”。假如一个驱动程序在它的DrivereEntry例程完成后需要使用这条路径，I/O管理器就会在DriverEntry完成后释放RegistryPath缓冲，从而使该指针失效。
1.1.2 DriverEnty返回值

DriverEntry例程返回一个NTSTATUS值，它不是STATUS_SUCCESS就是一个具体的错误信息。

DriverEntry例程在它返回STATUS_SUCCESS之前将延迟任何对IoRegisterDriverReinitialization的调用。如果它不返回STATUS_SUCCESS，就不必进行这种调用。

如果DriverEntry例程返回的不是STATUS_SUCCESS，那么驱动程序就不再被继续加载。

DriverEntry例程在它返回控制之前必须释放所有它已经安装的系统对象、系统资源和注册表资源，否则初始化将失败。如果驱动程序支持这些请求，它应该将驱动程序对象的IRP_MJ_FLUSH_BUFFERS重置为驱动程序Dispatch入口点，并且/或者将IRP_MJ_SHUTDOWＮ设为NULL。

如果驱动程序初始化失败，DriverEntry例程还将在返回控制之前记录一个错误信息。参见第16章的“错误记录与NTSTATUS值”。

1.2 DriverEntry例程功能

DriverEntry例程在以下阶段中初始化：

1. 为需要与其设备通信的硬件配置信息（如果有）分配内存。

2. 在驱动程序对象中设置驱动程序的Dispatch、AddDevice、StartIo（如果有）和Unload（如果有）入口点。

3. 建立所有驱动程序对象或其他系统资源，例如自旋锁，使驱动程序可以用它们来处理I/O请求。

4. 释放任何不再需要的已分配内存，可能调用IoRegisterDriverReinitialization，并返回一个适当的NTSTATUS值。

每一步的细节可参见“DriverEntry例程必备功能”与“DriverEntry例程可选功能”。

每个DriverEntry例程都运行在位于IRQL PASSIVE_LEVEL的系统线程环境中。因此，只要驱动程序不控制持有系统页文件的设备，任何由ExAllocatePool在初始化过程中分配的专用内存都能从页式存储池中取得。在DriverEntry返回控制前，分配的内存必须由ExFreePool释放。当然，设置Reinitialize例程的驱动程序可以在它调用IoRegisterDriverReinitialization的时候传送一个指针到内存，从而使驱动程序的Reinitialize例程负责释放已分配的内存。
1.2.1 DriverEntry例程必备功能

一个DriverEntry例程的任务如下：

1． 为驱动程序的Dispatch、AddDevice、StartIo（如果有）和Unload（如果有）例程填入驱动程序对象入口点。

2． 注册驱动程序的其他入口点。

3． 创建或初始化各种驱动程序范围的对象，类型，或驱动程序使用的资源。注意大多数标准驱动程序例程基于设备使用对象，所以驱动程序应当在它们的AddDevice例程中或在接收到一个IRP_MNSTART_DEVICE请求后建立这种对象。

如果驱动程序有一个设备专用线程或等待任何内核定义的调度者对象，DriverEntry例程将初始化这个调度者对象的线程或者通过调用适当的KeInitialize Xxx支持的例程使驱动程序等待，这些例程有一个指向事件、信号量、互斥体和定时器对象（由驱动程序为它们提供存储空间）的指针，这取决于驱动程序如何使用对象。它将在它的AddDevice例程中或者在接收一个IRP_MN_START_DEVICE请求后执行这个任务。

因为它在系统线程环境中执行，DriverEntry例程本身可以在一个调度者对象上等待一个非零中断，这个调度者对象必须在等待开始前被初始化。例如，请求到一个通讯端口驱动程序的类驱动程序在初始化过程中通常等待一个事件对象，它们通过IRP连接起来。它们由IoBuildSynchronousFsdRequest分配并用IoCallDriver传送到物理设备的端口驱动程序。

关于等待调度者对象的更多信息，参见第3章。

4． 返回的NTSTATUS表明驱动程序是否成功装入并能接收和处理来自PnP管理器的配置，增加及启动其设备的请求。

如果驱动程序分配了不再需要的内存，它将在返回前释放这些内存。
1.2.2 DrierEntry可选功能

这取决于一系列分层的驱动程序中一个特定驱动程序的位置，取决于下层驱动程序的属性，以及取决于驱动程序的设计。DriverEntry例程也要负责以下任务：

§ 如果一个驱动程序需要在一个驱动程序级基础上存储数据，其DriverEntry例程可以调用IoAllocateDriverObjectExtension来创建和初始化一个驱动程序对象扩展。这个驱动程序对象扩展是一个驱动程序特定的数据结构，与设备特定的设备对象扩展类似。例如，一个驱动程序可以用它的驱动程序对象扩展来存储一条注册路径或其他的各种信息。

§ 如果一个最高层的驱动程序使用执行工作者线程，就象很多文件系统驱动程序所做的那样，它可以调用PsCreateSystemThread来创建该线程。另外，它必须有一个WORKER_THREAD_ROUTINE类型的回调例程，这个例程获得一个单独的PVOID输入变量。

§ 如果一个驱动程序有一个可选的Reinitialize例程，当且仅当其DriverEntry例程返回STATUS_SUCCESS，该DriverEntry例程调用IoRegisterDriverReinitialization。一旦Reinitialize例程运行的话，该驱动程序可以调用IoRegisterDriverReinitialization。通常，一个运行Reinitialize例程的驱动程序是一个较高层驱动程序。参见“Reinitialize例程需求和功能”。

§ 与一个端口驱动程序或类驱动程序协同工作的特定设备的微端口驱动程序或微类驱动程序有某些类特定的初始化需求，而与这儿讨论的这些不同。详件DDK中的设备类型特定的文挡。

为系统资源提供存储空间

每个驱动程序必须为一切内核定义的对象及其使用的执行自旋锁提供存储空间。它还必须为指向从HAL或I/O管理器获得的具体对象的指针提供存储空间，就象第3章中所解释的那样。

在Windows 2000和WDM驱动程序中，每个设备对象应在AddDevice例程或者在处理PnP IRP_MN_START_DEVICE请求的Dispatch例程中分配，而不是在DriverEntry例程中，就象在Windows NT 4.0及其更早的版本中那样。

当然，一个驱动程序可能需要为其他驱动程序范围的使用分配额外的系统存储空间。如果这样，DriverEntry例程可以调用一个（或多个）下列例程：

§ IoAllocateDriverObjectExtension，创建一个与驱动程序对象连接的上下文区间。

§ ExAllocatePool，用于页式或非页式系统内存空间。

§ MmAllocateNonCachedMemory或MmAllocateContiguousMemory，用于分配给高速缓存的非页式系统内存空间（用于I/O缓冲区）

关于内存使用的更多信息，参见第16章。关于DMA公用缓冲区使用的更多信息，参见第3章。

声明硬件资源

尽管早期的驱动程序从注册表声明资源，但PnP驱动程序既不在注册表中声明设备资源，也不直接对注册表写请求。相反，这些驱动程序报告请求以响应某些PnP IRP，这是PnP管理器的列举处理的一部分。一个PnP驱动程序在一个PnP IRP_MN_START_DEVICE请求中接收它已分配资源。

不直接与PnP管理器相互作用的驱动程序（如微端口驱动程序）和直接与PnP管理器相互作用的类或端口驱动程序可能有不同的报告需求。这些请求对设备类是特定的。

一个DriverEntry例程可以使用注册表获得一些它需要的信息以初始化驱动程序。这个例程也在注册表中为其他驱动程序或者受保护的子系统设置信息以便使用。这些信息的属性取决于设备的类型。

关于设备和类的特定细节，参见DDK中相关设备的文档。

关于注册表使用及驱动程序安装的更多信息，参见《即插即用、电源管理和安装设计指南》和《Windows 2000驱动程序开发指南》第1卷。

1.3 Reinitialize例程需求与功能

Reinitialize例程由I/O管理器定义如下：

VOID

( *PDRIVER_REINITIALIZE) (

IN PDRIVER_OBJECT DriverObject，

IN PVOID Context，

IN ULONG Count

)；

就象DriverEntry例程那样，一个Reinitialize例程在IRQL_PASSIVE_LEVEL级系统线程环境中运行。

如果一个驱动程序不能在其DriverEntry例程中完全初始化自己，它可以在DriverEntry例程返回控制一段时间之后提供一个Reinitialize例程被调用。显然，这个Reinitialize例程执行另一个驱动程序启动后必须要做的任务。

例如，系统键盘类驱动程序，kbdclass，既支持PnP也支持早期的键盘端口。如果一个系统包含一个或多个PnP管理器不能检测到的早期端口，键盘类驱动程序必须为每个端口创建一个设备对象并且对该端口在较低层驱动程序之上划分层次。因此，类驱动程序在其DriverEntry例程和AddDevice例程被调用，及其他驱动程序被装入后，将调用一个Reinitialize例程。这个Reinitialize例程检测端口，为它创建一个设备对象，并对这个设备在其他较低层驱动程序之上划分驱动程序层次。

如果一个驱动程序有一个Reinitialize例程，它的DriverEntry例程通过调用IoRegisterDriverReinitialization一次使Reinitialize例程延迟运行。DriverEntry例程负责在它调用IoRegisterDriverReinitialization之前设置一切要传送给Reinitialize例程的环境数据。

如果其Reinitialize例程使用注册表，环境中的数据应包括注册表路径指针，它被传送到DriverEntry例程，因为这个指针不是Reinitialize例程的一个输入指针，就象它的声明中所显示的。

DriverEntry例程可以调用IoRegisterDriverReinitialization。如果Reinitialize例程在其他驱动程序调用的Reinitialize例程返回控制后再次运行，Reinitialize例程调用IoRegisterDriverReinitialization的次数可以等于驱动程序的Reinitialize例程应当运行的次数。传送到Reinitialize例程的计数器指示了这个例程被调用的次数，包括现行的调用。

注意，一个驱动程序的Reinitialize例程不应被注册，除非其DriverEntry例程返回STATUS_SUCCESS，表明驱动程序已被成功装入。

通常，一个带有Reinitialize例程的驱动程序是一个控制PnP与早期设备的较高层驱动程序。除了为这些PnP管理器检测到的设备创建设备对象（为此PnP管理器调用驱动程序的AddDevice例程），驱动程序也必须为PnP管理器检测不到的早期设备创建设备对象。Reinitialize例程创建那些设备对象并且在下层设备的相邻较低层驱动程序之上对驱动程序分层。

如果一个驱动程序有Reinitialize例程，它的初始化与“DriverEntry例程功能”中描述的基本步骤相同，它也与DriverEntry例程有相同的基本需求。


Dispatch例程

本章概述了驱动程序标准Dispatch例程的必需功能和一些实现Dispatch例程的最一般方针。它包含以下信息：

6.1 Dispatch例程需求

6.1.1 Dispatch例程一般实现

6.1.1 Dispatch例程条件实现

6.2 Dispatch例程基本功能

6.2.1 何时检查I/O栈位置

6.2.2 如何在一个Dispatch例程中完成一个IRP

6.2.3 何时在Dispatch例程中完成一个IRP

6.2.4 如何从一个Dispatch例程向IRP传送有效参数

6.2.5 为较低层驱动程序分配IRP

6.2.6 处理异步传输请求

6.3 设计Dispatch例程

6.3.1 DispatchCreate与DispatchClose功能

6.3.1.1 基本DispatchCreateClose例程

6.3.1.2实现DispatchCreateClose的注意事项

6.3.2 DispatchCleanup功能

6.3.3 DispatchRead或DispatchWrite功能

6.3.3.1 使用缓冲I/O的DispatchReadWrite

6.3.3.2 使用直接I/O的DispatchReadWrite

6.3.3.3 较高层驱动程序中的DispatchReadWrite

6.3.3.4 实现DispatchReadWrite的注意事项

6.3.4 Dispatch(Internal)DeviceControl功能

6.3.4.1 最低层驱动程序中的DispatchDeviceControl

6.3.4.2 较高层驱动程序中的DispatchDeviceControl

6.3.4.3 类/端口驱动程序中的Dispatch(Internal)DeviceControl

6.3.4.4 实现Dispatch(Internal)DeviceControl的注意事项

1.1 Dispatch例程需求

Dispatch例程由I/O管理器定义如下：

NTSTATUS

(*PDRIVER_DISPATCH) (

IN PDEVICE_OBJECT DeviceObject，

IN PIRP Irp

)；

在由DriverEntry例程设置的Dispatch例程中处理任何IRP首先处理已给定的主功能码（IRP_MJ_XXX），这个编码在每个到达的IRP的驱动程序的I/O栈位置中设置。

大多数驱动程序的Dispatch例程在一个位于IRQL PASSIVE_LEVEL的专用线程环境中被调用，除了下列情况：

§ 任何最高层驱动程序的Dispatch例程在产生I/O请求的线程环境中被调用，它一般是一个用户模式应用线程。

换句话说，文件系统驱动程序及其他最高层驱动程序的Dispatch例程在一个位于IRQL PASSIVE_LEVEL的非专用线程环境中被调用。

§ 系统页式路径中最低层设备驱动程序及其之上的中间层驱动程序的DispatchRead、DispatchWrite和DispatchDeviceControl例程，可以在IRQL APC_LEVEL及一个专用线程环境中被调用。

DispatchRead、DispatchWrite和也在这样一个最低层设备或者中间层设备驱动程序中处理读/写请求的其他例程必须任何时候都常驻内存。这些驱动程序例程既不能分页，也不能成为驱动程序的页式映像分区的一部分；它们不能访问任何页式内存。此外，它们不应当依赖于任何模块化的调用（例如带有一个非零超时的KeWaitForSingleObject）。更多信息见在线DDK。

§ 在“冬眠”和/或页路径中的驱动程序的DispatchPower例程可在IRQL DISPATCH_LEVEL中被调用。这类驱动程序的DispatchPnP例程必须准备处理PnP IRP_MN_DEVICE_USAGE_NOTIFICATION请求。

§ 请求在启动时加电的DispatchPower例程可以在lRQL DISPATCH_LEVEL中被调用。

驱动程序能有与IRP_MJ_XXX函数一样多的Dispatch例程为驱动程序控制编码。
1.1.1 Dispatch例程一般实现

通常，大多数驱动程序必须处理一些或者所有以下请求：

§ LRP_MJ_PNP表明一个包括PnP设备识别的请求，硬件配置，或者资源分配的请求。这类请求显然从PnP管理器或者从一个与较高层驱动程序密切相关的设备发送到一个设备驱动程序。 关于这个例程的更多信息，参见《即插即用、电源管理和安装设计指南》 第l部分第2章的“DispatchPnP例程”。

§ IRP_MJ_POWER表明一个属于设备或者系统的电源状态请求。这类请求通过电源管理器或者一个密切连接的较高层驱动程序发送到设备驱动程序。关于这个例程的更多信息，参见《即插即用、电源管理和安装设计指南》第l部分第2章的“DispatchPower例程”。

§ IRP_MJ_CREATE表明用户模式保护子系统，可能代表应用或者特定子系统驱动程序，已请求一个与目标设备对象有关的文件对象的控制，或者一个较高层的驱动程序把其设备对象连接或者附加到目标设备对象。

§ IRP_MJ_CLEANUP表明，为代表目标设备对象的文件对象的处理被关闭，因此任何在清除IRP的驱动程序I/O栈位置中找到的，在给定文件对象目标设备的当前队列中的IRP， 应该被取消。

§ IRP_MJ_CLOSE表明代表目标设备对象的文件对象句柄或指向目标设备对象的指针已经被关闭，还表明没有未完成的、对文件对象指针的引用。

§ IRP_MJ_READ表明I/O请求从下层物理设备向系统传输数据。

§ IRP_MJ_WRITE表明I/O请求从系统向下层物理设备传输数据。

§ IRP_MJ_DEVlCE_CONTROL表明一个带有系统定义的，指定一个被发送给设备驱动程序的具体设备操作的特定设备类型I/O控制码的请求。

关于最低层设备驱动程序必须支持的，系统定义的（有时也叫公共的）I/O控制码的更多信息，见《Windows 2000 驱动程序开发指南》第2卷。注意，较高层驱动程序也必须支持IRP_MJ_DEVICE_CONTROL以传送这些IRP到它们之下的设备驱动程序。

§ IRP_MJ_INTERNAL_DEVICE_CONTROL表明一个传给设备驱动程序的请求，大多数情况下是来自一个紧密连接的较高层驱动程序，通常带有一个私有的、驱动程序特定的及特定设备类型的或特定设备的I/O控制码，该控制码请求一项特定设备类型的或特定设备的操作。

仅仅某些种类的驱动程序要去处理系统定义的内部设备I/O控制请求，包括SCSI驱动程序，键盘或鼠标设备驱动程序，及与系统支持的驱动程序进行互操作的并行驱动程序。

任何驱动程序都必须在驱动程序对象中设置的Dispatch例程入口点根据下层物理设备的类型和功能的不同而不同。关于驱动程序必须处理的IRP主功能码的特定设备类型信息，见DDK中特定设备类型的文档。关于PnP与电源IRP的更多信息，见《即插即用、电源管理和安装设计指南》。关于SCSI与视频微端口驱动程序的信息，见在线DDK。关于其他类型的SCSI驱动程序的更多信息，见在线DDK。
1.1.2 Dispatch例程条件实现

设备或中间层驱动程序可能处理的其他请求如下：

§ 用于某些最高层驱动程序IRP_MJ_QUERY_INFORMATION和IRP_MJ_SET_INFORMATION：

这类请求表明一个用户模式应用程序、组件或驱动程序请求关于文件对象（代表驱动程序的设备对象）长度的信息，为此，用户模式的请求方要进行一项处理，或者，用户模式的请求方试图设置该文件对象的文件结束标志。

并行类与串行设备驱动程序通过将FILE_STANDARD_INFORMATION或FILE_POSITION_INFORMATION长度或位置置为零来处理这些请求。其他最高层设备驱动程序应支持这些请求，特别是在一个用户模式应用程序或用户模式驱动程序可能调用运行时C函数以处理文件对象的情况下。文件系统驱动程序必须比这些最高层设备驱动程序更完全地支持这些请求。

§ IRP_MJ_FLUSH_BUFFERS用于在设备中高速缓存数据或在驱动程序分配的内存中内在地缓冲数据的驱动程序：

接受这一请求表明驱动程序应写它的缓冲数据或刷新设备高速缓存的数据，或者应丢弃从设备读到的缓冲区或高速缓存数据。

例如，系统键盘和鼠标类驱动程序，它们带有从设备输入数据的内置环形缓冲器，支持刷新请求。海量存储设备的驱动程序及在它们之上分层的驱动程序也支持该请求。

§ IRP_MJ_SHUTDOWN用于任何在系统本身关闭之前被调用的驱动程序：

如果下层设备不是海量存储设备，当它初始化时驱动程序可以在驱动程序对象中设置DispatchShutdown入口点。驱动程序的DriverEntry例程必须用指向设备对象的指针调用IoRegisterShutdownNotification，它的DispatchShutdown例程必需在电源管理器发送一个系统设置电源IRP以关闭系统之前进行任何驱动程序确定的清除（cleanup）。

当系统将要关闭时，海量存储设备的驱动程序及在它们之上分层的中间层驱动程序可以依靠最高层文件系统驱动程序发送其关机IRP 。简而言之，FSD负责在系统关机之前保证任何缓存数据写入外围设备，并调用下层驱动程序刷新来自于它们的设备高速缓存与缓冲区（如果有）中的数据。

缓存数据的海量存储设备的驱动程序必须在它初始化时在驱动程序对象中为它的DispatchShutdown与DispatchFlush例程设置入口点。如果这样一个驱动程序在内存中分配数据缓冲区，但是它的设备没有内部高速缓存，它也必须在初始化时在驱动程序对象中设置DispatchShutdown与DispatchFlush入口点。

任何在处理IRP_MJ_FLUSH与IRP_MJ_SHUTDOWN请求的驱动程序之上分层的中间层驱动程序也必须在它初始化时在驱动程序对象中设置DispatchShutdown与DispatchFlush入口点。

1.2 Dispatch例程基本功能

一个特定Dispatch例程的功能要求根据它处理的IRP_MJ_XXX，驱动程序列中该独立驱动程序的位置，及下层物理设备类型的不同而不同。

大多数Dispatch例程如下处理输入IRP：

1．在IRP中检测驱动程序的I/O栈位置以决定做什么，并且如果有参数的话，检测参数的有效性。

2．尽可能满足请求并完成IRP；否则，由较低层驱动程序或其他设备驱动程序例程传送它以做进一步处理。

驱动程序是否必须检查其I/O栈位置以确定做什么并检查参数取决于所给的IRP_MJ_XXX，就还取决于驱动程序是否为每个驱动程序处理的IRP_MJ_XXX建立了单独的Dispatch例程。

驱动程序是否必须传送进一步处理的IRP取决于参数（如果有）的有效性，还取决于IRP_MJ_XXX和驱动程序在一连串分层驱动程序中的所在层（如果有）。
1.2.1 何时检查I/O栈位置

如果满足下列任何条件，驱动程序的Dispatch例程必须检查其IRP本身的I/O栈位置以确定做什么：

§ Dispatch例程控制多个IRP_MJ_XXX。

§ Dispatch例程必须为确定的主功能码处理一组次功能码。带有次功能码的IRP包括所有IRP_MJ_PNP和IRP_MJ_POWER请求，和某些SCSI端口驱动程序及文件系统驱动程序必须处理的IRP。

§ 设备驱动程序或紧密连接的较高层驱动程序的Dispatch例程处理IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求，它有一组关联的特定设备类型，或可能是私有定义且特定设备的I/O控制码。

为确定要求的是什么操作及使用的是什么参数（如果特定IRP_MJ_XXX有），Dispatch例程调用IoGetCurrentIrpStackLocation以获得一个指向它自己在输入IRP中的I/O栈位置的指针。

较高层驱动程序的Dispatch例程总是调用IoGetCurrentIrpStackLocation，它还调用IoGetNextIrpStackLocation以获得一个指向它们为相邻较低层驱动程序建立的IRP中的相邻较低层驱动程序I/O栈位置的指针，就象第4章提及的。

设备驱动程序的Dispatch(Internal)DeviceControl例程，或者其可能紧密连接的类驱动程序，必须确定在驱动程序的Parameters.DeviceIoControl.IoControlCode I/O栈位置为每个请求设置的是哪个I/O控制码。这类驱动程序的Dispatch(Internal)DeviceControl例程的特征经常是有Switch语句，如同在“Dispatch(Internal)DeviceControl功能”中所述的。

在大多数情况下，一个较高级驱动程序的Dispatch(Internal)DeviceControl例程仅在IRP中建立了栈位置之后在其上传送一个IRP_MJ_DEVICE_CONTROL或者IRP_MJ_INTERNAL_DEVlCE_CONTROL请求到相邻较低层驱动程序。当然，SCSI类驱动程序必须检查确定的IOCTL_SCSI_XXX控制码，以便能在传送这些请求之前正确地设建立SCSI端口驱动程序的I/O栈位置。关于这些对SCSI类驱动程序的请求的更多信息，见在线DDK。
1.2.2 如何在一个Dispatch例程中完成一个IRP

如果一个输入IRP能被迅速完成，Dispatch例程要做以下工作：

1．通常将IRP的I/O状态块中的Status与Information成员设置为恰当的值：

§ Dispatch例程将Status设置为STATUS_SUCCESS或者一个适当错误STATUS_XXX，可以是调用一个支持例程所返回的值，或者对于某些的同步请求来说，是一个较低层驱动程序返回的值。

如果较低层驱动程序返回STATUS_PENDING，较高层驱动程序不应当用IRP调用IoCompleteRequest，除非它首先用该IRP调用了IoMarkIrpPending。当然，较高层驱动程序的Dispatch例程未必为返回STATUS_PENDING的较低层驱动程序完成任何IRP。

§ 如果满足了一个传输数据的请求，例如一个读或写请求，则将Information设置为成功传输的比特数。

§ 对其他以STATUS_SUCCESS完成的IRP的不同特定请求而将Information设置为不同的值。

§ 对以警告性STATUS_XXX完成的IRP的不同特定请求而将Information设置为不同的值。例如，对于象STATUS_BUFFER_OVERFLOW这样的警告，将Information设置为传输的字节数。

§ 通常，对以一个错误STATUS_XXX完成的请求，将Information置为零。

2．用IRP和PriorityBoost IO_NO_INCREMENT调用IoCompleteRequest。

3．返回已在I/O状态块中设置的适当的STATUS_XXX。注意，对IoCompleteRequest的调用使给定的IRP不能被调用者访问，因此来自一个Dispatch例程的返回值不能从一个已完成的IRP的I/O状态块中设置。

以下是用IRP调用IoCompleteRequest的实现方针：

§ 调用IoCompleteRequest之前，总是释放驱动程序持有的自旋锁。

完成一个IRP的时间是不确定的，尤其是在一系列分层的驱动程序中。另外，如果一个较高层驱动程序的IoCompletion例程向一个正持有自旋锁的较低层驱动程序发送一个IRP ，就会产生死锁。
1.2.3 何时在Dispatch例程中完成一个IRP

通常，驱动程序不在它们的Dispatch例程中完成IRP，除非给定请求的参数是无效的，或者在设备驱动程序中，特定IRP_MJ_XXX不要求设备I/O操作。

一个较高层驱动程序应根据以下方针在它的Dispatch例程中完成IRP或者将它传送给较低层驱动程序来处理：

§ 如果Dispatch例程确定它自己的I/O栈位置中有参数是无效的，它用一个恰当错误STATUS_XXX，例如STATUS_INVALID_PARAMETER，迅速完成该IRP。

§ 如果IRP包含功能码IRP_MJ_CLEANUP，DispatchCleanup例程必须为驱动程序清除IRP的I/O栈位置文件对象，完成当前在目标设备对象排队的每个IRP，并完成清除IRP.

一个清除请求表明应用程序被终止或者已关闭一个代表驱动程序设备对象的文件对象的文件处理。当DispatchCleanup例程返回时，通常驱动程序的DispatchClose例程被再次调用。

§ 否则，一个较高层驱动程序可以仅仅通过传送它到相邻的较低层驱动程序满足请求。

一系列分层的驱动程序中的所有驱动程序都可以检查它自己的，输入到驱动程序的Dispatch例程每个IRP的I/O栈位置中的参数的有效性。在这一系列中，每个驱动程序的Dispatch例程仅完成那些在它的I/O栈位置中可以确定参数有效性的IRP。在最高层驱动程序的Dispatch例程里完成带有无效参数的IRP，将改善任何系列的驱动程序与系统整体的I/O流量。

一个最低层驱动程序按照以下方针在它的Dispatch例程里完成IRP：

§ 如果Dispatch例程确定它自己的I/O栈位置里有参数是无效的，它应当用一个适当的错误STATUS_XXX立即完成该IRP。

通常，任何较高层驱动程序已经为一个请求的操作检查参数，但最低层设备驱动程序也可以执行它们自己的参数“健康检查”。当然，最低层设备驱动程序不必检查IRP_MJ_INTERNAL_DEVICE_CONTROL请求，因为一个紧密连接的较高层驱动程序负责设置这些请求的参数。

§ 如果IRP包含功能码IRP_MJ_CLEANUP，DispatchCleanup例程必须为给定的驱动程序清除IRP的I/O栈位置的文件对象，完成当前在目标设备对象排队的所有IRP，并完成清除IRP。

一个清除请求表明一个应用程序被终止或关闭了一个代表驱动程序设备对象的文件对象的文件处理。当Dispatch例程返回时，通常驱动程序的DispatchClose例程被再次调用。关于处理可取消的IRP的更多信息，也参见第12章。

§ 如果请求不要求设备I/O操作，Dispatch例程应满足请求并完成IRP。

例如，一个驱动程序可以在设备扩展中存储该设备的当前模式，尤其如果它在初始化后很少改变设备模式。它的DispatchDeviceControl例程可以满足一个通过返回存储信息来查询当前设备模式的请求。

否则，Dispatch 例程必须用IRP调用IoMarkIrpPending，将IRP排队到其他驱动程序例程以做进一步处理，并返回STATUS_PENDING。
1.2.4 如何从一个Dispatch例程传送带有效参数的IRP

当Dispatch例程检查了一个输入IRP中它自己的I/O栈位置并确定任何参数都是有效的，它必须为进一步通过较低层驱动程序或其他设备驱动程序例程处理传送IRP，如果它无法满足并完成Dispatch例程本身给定的请求。

一个较高层驱动程序应如下传送一个请求到相邻较低层驱动程序：

1．用输入IRP调用IoGetCurrentIrpStackLocation，如果Dispatch例程还没有这样做，这样就为IRP中它自己的I/O栈位置获得一个指针。

在大多数情况下，一个较高层驱动程序的Dispatch例程已经进行了该调用。然而，一个Dispatch例程可能只是处理特定的IRP_MJ_XXX，它或者带有驱动程序无法验证有效性的参数或者不带参数，这种Dispatch例程可能会调用IoGetCurrentIrpStackLocation。

2．如果驱动程序传送输入IRP到相邻较低层驱动程序，调用IoGetNextIrpStackLocation。如果它为一个或多个较低层驱动程序分配了附加IRP，Dispatch例程用每个它分配的IRP进行这两组调用：

§ 如果驱动程序没有在新IRP中分配栈位置，调用IoGetNextIrpStackLocation获得一个指向相邻较低层驱动程序I/O栈位置的指针

§ 调用IoSetNextIrpStackLocation（在它之后是IoGetCurrentIrpStackLocation）获得一个指向新IRP中它自己的栈位置的指针，在那里它可以设置任何IoCompletion例程需要的环境；然后调用IoGetNextIrpStackLocation获得一个指向相邻较低层驱动程序I/O栈位置的指针

3．设置相邻较低层驱动程序的I/O栈位置，这通常是通过拷贝它自己在初始IRP中的I/O栈位置到相邻较低层驱动程序的I/O栈位置来实现的。不过，Dispatch例程可以为某些请求修改相邻较低层驱动程序的I/O栈位置中的参数。

例如，当下层的设备已经知道传输能力限制，并重新使用IRP发送部分传输请求到下层的设备驱动程序时，一个较高层驱动程序可以为一个大的传输请求修改参数

4．用每个Dispatch例程分配的IRP调用IoSetCompletionRoutine，以便驱动程序的IoCompletion例程在较低层驱动程序完成它时释放每个这样的IRP。

Dispatch例程也可以用已设置相邻较低层驱动程序I/O栈位置的输入IRP调用IoSetCompletionRoutine。这时，它的IoCompletion例程可以检查较低层驱动程序如何完成请求；为部分IRP传输重新使用IRP；如果跟踪IRP，更新任何驱动程序维护的状态；可能的话重试一个返回错误的请求，等等。

关于IoCompletion例程的更多信息，见第13章.

5．用每个将由较低层驱动程序处理的IRP调用IoCallDriver。

6．返回适当的NTSTATUS值，例如：

§ 如果输入IRP是一个象IRP_MJ_READ或IRP_MJ_WRITE那样的异步请求，通常是STATUS_PENDING

§ 如果输入IRP是一个象IRP_MJ_CREATE或IRP_MJ_DEVICE_CONTROL那样的同步请求，则经常是对IoCallDriver的调用结果

一个最低层设备驱动程序将任何在它的Dispatch例程中无法完成的IRP传送到下列其他驱动程序例程：

1．用输入IRP调用IoMarkIrpPending。

2．调用IoStartPacket以传送或排队IRP到驱动程序的StartIo例程，除非驱动程序管理它自己的内部IRP队列。关于驱动程序管理队列的更多信息见第7章。

如果驱动程序不拥有StartIo例程但却处理可撤消的IRP，在将IRP排队以做进一步处理之前，它必须用Cancel例程与IRP的入口点调用IoSetCancelRoutine。关于Cancel例程的更多信息见第7章。

3．返回STATUS_PENDING。
1.2.5 为较低层驱动程序分配IRP

根据它在一系列分层的驱动程序中的位置，驱动程序可以通过调用某些系统支持例程为较低层驱动程序分配IRP。

最高层驱动程序可以调用IoMarkAssociatedIrp分配IRP并为一系列较低层驱动程序建立它们。只要驱动程序不用初始IRP或任何它分配的相连的IRP调用IoSetCompletionRoutine ，I/O管理器当所有它相连的IRP都被完成时就自动完成初始IRP。然而，最高层驱动程序决不能为任何请求缓冲I/O操作的IRP分配相连的IRP。

一个中间层驱动程序不能通过调用IoMakeAssociatedIrp为较低层驱动程序分配IRP。任何中间层驱动程序接收的IRP也是相连的IRP，并且驱动程序不能把这类IRP与另一个IRP相连。

如果中间层驱动程序为较低层驱动程序创建IRP，它应调用IoAllocateIrp、IoBuildDeviceIoControlRequest、IoBuildSynchronousFsdRequest或IoBuildAsynchronousFsdRequest。然而，IoBuildSynchronousFsdRequest仅可以在下列环境中被调用：

§ 由一个驱动程序创建的线程为读或写请求建立IRP，因为这样一个线程可以在非专有线程环境中等待一个调度对象，例如驱动程序初始化的Event传送到IoBuildSynchronousFsdRequest

§ 在系统线程环境中初始化或卸载时

§ 为自同步操作建立IRP，例如创建、刷新、关机、关闭和设备控制请求

然而，相比IoBuildSynchronousFsdRequest，驱动程序更喜欢调用IoBuildDeviceIoControlRequest来分配设备控制IRP。
1.2.6 处理异步传输请求

除了最高层驱动程序，所有驱动程序都异步处理IRP_MJ_READ与IRP_MJ_WRITE请求。即使最高层驱动程序也不能在它的Dispatch例程中为较低层驱动程序等待，以完成处理一个异步的读或写请求。它必须传送这样的请求给较低层驱动程序并从它的DispatchRead或DispatchWrite例程返回STATUS_PENDING。

同样，最低层设备驱动程序必须传送这类传输请求给其他处理设备I/O请求的驱动程序例程，并从它的DispatchRead及/或DispatchWrite例程，或者DispatchReadWrite例程返回STATUS_PENDING。

如6.2.4节所提到的那样，较高层驱动程序有时必须设置部分传输IRP，并把它们传送给较低层驱动程序。当部分传输请求已由较低层驱动程序完成时，较高层驱动程序仅仅完成初始读/写IRP。

例如，一个SCSI类驱动程序的DispatchReadWrite例程被用来把超出下层的HBA的传输能力的大传输请求分割成一组部分传输请求。类驱动程序必须在部分传输IRP中设置参数以便SCSI端口/微端口驱动程序可以在单个DMA操作中满足每个部分传输请求。

其他使用DMA或PIO的设备驱动程序也需要为它们自身分割大传输请求。

关于DMA与PIO使用的更多信息，见第3章的“设置用户缓冲区的访问”和“适配器对象和DMA”。关于SCSI类驱动程序要求的更多信息，也见在线DDK。

1.3 Dispatch例程设计

本节更全面地描述设备与中间层驱动程序的最一般实现，包括以下内容：

§ DispatchRead和DispatchClose

§ DispatchCleanup

§ DispatchRead和DispatchWrite

§ DispatchDeviceControl与DispatchInternalDeviceControl

关于DispatchPnP和DispatchPower例程的信息，见《即插即用、电源管理和安装设计指南》第1部分第2章 “DispatchPnP例程和DispatchPower例程”。
1.3.1 DispatchCreate和DispatchClose功能

创建请求可以在一个用户模式子系统试图获得代表设备（可能代表一个应用程序或子系统层的驱动程序）的文件对象的句柄时产生，或者一个较高层驱动程序对IoGetDeviceObjectPointer或IoAttachDevice调用时产生。

与它相逆的关闭请求在一个用户模式子系统关闭与驱动程序设备对象相连的文件对象句柄时产生。

这些请求自然是同步的。

IRP_MJ_CREATE与IRP_MJ_CLOSE请求的驱动程序Dispatch例程可能仅仅以STATUS_SUCCESS完成输入IRP，就象第4章提到的那样。

IRP_MJ_CREATE与IRP_MJ_CLOSE请求的另一驱动程序Dispatch例程做更多的工作，这取决于下层设备驱动程序或下层设备。

考虑以下情况：

§ 一收到创建请求，类驱动程序就初始化内部队列并发送IRP_MJ_INTERNAL_DEVICE_CONTROL请求给相关请求设备配置信息或到一个控制器端口的专有连接的端口设备驱动程序。

§ 一收到关闭请求，最高层驱动程序就需要延迟直到为所有当前排队到目标设备对象的未完成IRP完成设备I/O处理。

§ 一收到创建请求，一个不常用设备的驱动程序不能调用MmLockPageableCodeSection以常驻一些处理其他IRP_MJ_XXX请求的驱动程序例程。一收到相逆的关闭请求，当所有为这类驱动程序的设备对象处理的文件对象被关闭时，驱动程序调用MmUnlockPageableImageSection以通过将可分页映像分区所在页调出内存来保留内存。

虽然许多驱动程序对创建与关闭请求只有一个Dispatch例程，但可以分别实现单独的DispatchCreate与DispatchClose例程。一些驱动程序仅为了对称才处理IRP_MJ_CLOSE请求，因为在设备对象被一个保护的子系统或较高层驱动程序打开后，较低层驱动程序的设备对象直到系统关机时才关闭。

如果有一个较低层驱动程序控制的设备必须为系统继续运行而存在，驱动程序的DispatchClose例程通常不会被调用。

例如，键盘与鼠标驱动程序建立代表系统运行时必须被功能化的物理设备的设备对象，因此驱动程序可以拥有最低限度DispatchClose例程或者结合的DispatchCreateClose例程。

再例如，一些系统磁盘驱动程序没有DispatchClose例程，但是在系统关机之前，这些驱动程序通常有DispatchFlush与DispatchShutdown例程来完成任何未完成的文件I/O操作。
1.3.1.1 基本DispatchCreateClose例程

许多驱动程序收到创建请求时仅仅需要建立自身，并且仅仅需要知道收到关闭请求，特别是分层的驱动程序链中的较低层驱动程序。

例如，一个带有一个或多个连接紧密的调用IoGetDeviceObjectPointer的类驱动程序的设备控制器的端口驱动程序有最低限度的DispatchCreateClose例程。这类端口驱动程序的DispatchCreateClose例程只如下完成IRP：

: :

{

Irp->IoStatus.Status = STATUS_SUCCESS;

Irp->IoStatus.Information = 0;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return STATUS_SUCCESS;

}

最低限度DispatchCreateClose例程将I/O状态块的Information成员置为零，表明文件系统为创建请求打开；对于关闭请求的Information是没有意义的。它用STATUS_SUCCESS设置Status成员，并返回状态值。当类驱动程序调用IoGetDeviceObjectPointer时，这个状态表明它已准备好从使创建IRP 发送到端口驱动程序的DispatchCreateClose例程的较高层类驱动程序目标设备对象接收I/O请求。

最低限度DispatchCreateClose例程完成创建IRP，而没有提高IRP（IO_NO_INCREMENT）始发者的优先级，因为假定请求始发者在请求完成过程中等待一个不确定但非常小的间隔。

DispatchCreateClose例程做多少工作一方面取决于驱动程序设备或下层设备的属性，一方面取决于驱动程序的设计。一个驱动程序可以在单独的DispatchCreate和DispatchClose例程中处理这些请求，尤其是如果该驱动程序为每个请求执行完全不同的操作。

对于一个打开代表一个逻辑或物理设备的文件对象的创建请求，最高层驱动程序做以下工作：

1．调用IoGetCurrentIrpStackLocation以获取一个指向IRP中I/O栈位置的指针。

2．如果FileName中的Unicode串长度为零，检查I/O栈位置中的FileObject.FileName并用STATUS_SUCCESS完成IRP；否则，用STATUS_INVALID_PARAMETER完成IRP。

按照上述步骤可以确保不试图在设备上打开一个可能在以后产生问题的伪文件。例如，它能防止试图打开一个不存在的\\device\parallel0\temp.dat。
1.3.1.2 实现DispatchCreateClose的注意事项

当实现DispatchCreateClose例程时要考虑以下几点：

§ 至少，Dispatch例程必须做以下工作：

1．用适当的NTSTATUS，通常是STATUS_SUCCESS设置输入IRP的I/O状态块中的Status域。

2．将输入IRP的I/O状态块中的Information域置为零。

3．用IRP和值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest。

4．返回在IRP的I/O状态块的Status域中设置的NTSTATUS。

§ 在最高层或中间层驱动程序中，该例程可能需要为处理一个创建及/或关闭请求做额外工作，这取决于设备或下层设备的属性与驱动程序的设计。

§ 对于打开一个代表逻辑或物理设备的文件对象的创建请求，最高层驱动程序应检查I/O栈位置中的FileObject.FileName，并用STATUS_SUCCESS完成IRP，如果FileName中Unicode串长度为零。否则，它应用STATUS_INVALID_PARAMETER完成IRP。

§ 最低层驱动程序的DispatchCreateClose例程仅仅在相邻的较高层驱动程序调用IoAttachDeviceToDeviceStack，IoGetDeviceObjectPointer，或IoAttachDevice时被调用。一系列分层的驱动程序中的最低层驱动程序常常仅进行一个创建或关闭请求的最低要求的处理。
1.3.2 DispatchCleanup功能

任何驱动程序中保持一个不确定的间隔的IRP应有一个DispatchCleanup例程。如果在为代表下层物理或逻辑设备的文件对象的一个用户模式处理被关闭时，IRP可以被排队，为那些设备控制IRP界限的最高层驱动程序必须有一个DispatchCleanup例程。

一个驱动程序的DispatchCleanup例程被调用以撤消当前被排在文件对象的目标设备对象的所有IRP，这个文件对象在清除IRP（cleanup IRP）的驱动程序的I/O栈位置中被指定。

通常，一个DispatchCleanup例程必须做以下工作：

§ 在当前设备队列（或者驱动程序的内部IRP队列）中的每个IRP中使用IoSetCancelRoutine将驱动程序的Cancel入口点（如果有）重设为NULL，这个设备队列是目标设备对象和给定文件对象的队列。

§ 撤消所有当前为目标设备对象和清除IRP的驱动程序I/O栈位置中特定的文件对象排队的IRP。

§ 完成输入清除IRP并返回STATUS_SUCCESS。

DispatchCleanup例程必须在它完成每个要被撤消的IRP之前，在每个为给定文件对象排队到目标设备对象的IRP中设置I/O状态块如下：

Status 设置为STATUS_CANCELLED

Information 设置为零

DispatchCleanup例程在它用每个撤消的IRP调用IoCompleteRequest时指定IO_NO_INCREMENT。当它撤消了为给定文件对象排队的全部IRP之后，DispatchCleanup例程用STATUS_SUCCESS和一个IO_NO_INCREMENT的PriorityBoost完成请求本身。

如果一个驱动程序管理它自己的IRP内部队列，而不是使用它的设备对象连接的设备队列，驱动程序应当建立一个用执行自旋锁保护的互锁队列。否则，另一个驱动程序例程（运行在一个SMP机器的另一个处理器上）可能将一个应由DispatchCleanup例程撤消的IRP从队列中取出。为从队列中删除一个IRP并撤消它，DispatchCleanup例程调用一个带有驱动程序提供的执行自旋锁的ExInterlocked..List支持例程。

由DispatchCleanup例程和另一个驱动程序例程共享的目标设备对象的设备扩展中的任何区域也必须由自旋锁保护。

每个驱动程序必须使它对可撤消的IRP的访问与I/O管理器和它自己的Cancel例程（如果有）同步。

为在必须用一个或多个Cancel例程使之同步的DispatchCleanup例程内管理自旋锁的使用，应考虑以下方针：

§ 如果DispatchCleanup例程必须获得系统撤消自旋锁和一个或多个驱动程序自己的自旋锁，它应在取得它自己的自旋锁之前调用IoAcquireCancelSpinLock。

DispatchCleanup例程必须以相反顺序释放获得的嵌套自旋锁：换句话说，它必须在调用IoReleaseCancelSpinLock之前释放它自己的自旋锁。

§ 如果驱动程序在设备对象中使用I/O管理器提供的设备队列，这时它的DispatchCleanup例程在它调用IoSetCancelRoutine将IRP中的Cancel入口点重置为零之前必须调用IoAcquireCancelSpinLock。

如果驱动程序管理它自己的IRP队列，这时DispatchCleanup例程通常在调用IoSetCancelRoutine之前获得撤消自旋锁。更多的信息见第12章“Cancel例程”。

§ 在它用IRP调用IoCompleteRequest之前，DispatchCleanup例程必须释放所有正持有的自旋锁，包括它撤消的每个IRP。

关于Cancel例程的更多信息，见第12章。

当实现一个DispatchCleanup例程时记住以下要点：

§ 在它完成输入清除IRP之前，该例程必须遍历目标设备对象的IRP队列，并撤消所有对于给定文件对象未完成的请求。清除IRP在代表用户模式设备的文件对象的操作被关闭时发送。

§ 队列应由自旋锁保护以防止将被撤消的IRP被任何别的驱动程序例程从队列中取出。与设备对象连接的设备队列带有自旋锁。如果驱动程序建立它自己的IRP队列，应建立一个互锁队列。

§ 如果驱动程序有一个Cancel例程，该例程必须在它调用IoSetCancelRoutine将每个排队的IRP中的Cancel入口点重置为NULL之前用IoAcquireCancelSpinLock获得系统撤消自旋锁。除非驱动程序管理它自己的IRP队列并用系统提供的自旋锁使对它们的访问同步；驱动程序不需要在调用IoSetCancelRoutine之前获得撤消自旋锁。

§ 该例程必须在它完成每个这类IRP之前设置每个排队的IRP的I/O状态块，将Status设置为STATUS_CANCELLED，Information设置为零。

§ 在用一个IRP调用IoCompleteRequest之前，该例程必须释放所有它保持的自旋锁。

§ 当它完成每个撤消的IRP与清除IRP时，该例程应设置PriorityBoost为IO_NO_INCREMENT。
1.3.3 DispatchRead或DispatchWrite功能

一个设备（数据可以从它传输到系统）的所有驱动程序必须有一个DispatchRead例程，并且必须在初始化时设置它在驱动程序对象中的入口点。一个设备（数据可以从系统传输到它）的所有驱动程序必须有一个DispatchWrite例程，并且必须在初始化时设置它在驱动程序对象中的入口点。任何双向传输数据的最低层设备驱动程序可以有一个结合的DispatchReadWrite例程，如同在它之上的分层的较高层驱动程序那样。

任何在这样一个最低层设备驱动程序之上的分层的较高层驱动程序必须设置它的驱动程序对象中的Dispatch例程入口点，以响应由下层设备驱动程序处理的IRP_MJ_READ和/或IRP_MJ_WRITE请求。

如6.2.6节所提到的，较低层驱动程序异步地处理IRP_MJ_READ与IRP_MJ_WRITE请求。因此，DispatchRead和/或DispatchWrite例程必须为进一步处理传送这些请求，假如IRP的驱动程序的I/O栈位置中的请求有效参数。

驱动程序是否为缓冲或直接I/O建立设备对象影响它如何处理传输请求，如同已在第3章中提到的。特别地，使用直接I/O进行DMA操作的驱动程序需要将大的传输请求分割成一系列较小的传输操作，以便满足IRP_MJ_READ或IRP_MJ_WRITE请求。

以下小节讨论了一些使用缓冲I/O与直接I/O的最低层设备驱动程序，以及它们之上的较高层驱动程序中的DispatchReadWrite例程的设计与实现要点。6.3.3.4小节概述了实现DispatchRead、DispatchWrite或结合的DispatchReadWrite例程的注意事项。
1.3.3.1 使用缓冲I/O的DispatchReadWrite

任何为缓冲I/O建立设备对象的最低层设备驱动程序通过返回从设备传输到锁定的位于Irp->AssociatedIrp.SystemBuffer的系统空间缓冲区的数据来满足一个读请求。它通过从相同的缓冲区到设备传输数据来满足一个写请求。

因此，这类设备驱动程序的DispatchReadWrite例程通常一收到一个传输请求就做以下工作：

1．调用IoGetCurrentIrpStackLocation，并且，如果必要的话，确定传输请求的方向。

2．为该请求检查参数有效性。

§ 对于一个读请求，该例程通常检查驱动程序的IoStackLocation->Parameters.Read.Length值以确定缓冲区是否足够包含从设备传输来的离散数量的数据。

例如，系统键盘类驱动程序处理来自Win32用户输入线程的读请求。该驱动程序定义了一个结构，KEYBOARD_INPUT_DATA，存放来自设备的键击，并在任何给定时刻，保存一些内部环形缓冲区中的结构编号，以满足到来的读请求。

§ 对于一个写请求，这类驱动程序通常检查位于Parameters.Write.Length处的值，并且如果必要，检查Irp->AssociatedIrp.SystemBuffer处的数据的有效性；也就是说，是否设备只接收包含已定义的值域的成员的结构化数据包。

3．如果任一参数是无效的，DispatchReadWrite例程立即完成IRP，就如6.2.2节中描述的。否则，DispatchReadWrite由其他驱动程序例程为进一步处理传送IRP，如同6.2.4节描述的。

使用缓冲I/O的最低层设备驱动程序通常必须满足一个带有一些离散数量的数据的传输请求，请求的始发方可以使用或者设置这些数据。这类驱动程序往往为来自或要被发送到设备的数据定义结构，并往往如系统键盘类驱动程序那样内在地缓冲结构化的设备。

内在地缓冲数据的驱动程序支持IRP_MJ_FLUSH_BUFFERS请求，并且也支持IRP_MJ_SHUTDOWN请求（见6.1.2节）。

通常，一系列驱动程序中最高层驱动程序负责在传送一个读/写请求到较低层驱动程序之前检查输入IRP的参数。因此，许多较低层驱动程序可以假定一个读/写IRP中的I/O栈位置有有效参数。如果它的设备对那个设备驱动程序的数据传输有特殊限制，一系列驱动程序中最低层驱动程序需要检查I/O栈位置中参数的有效性。
1.3.3.2 使用直接I/O的DispatchReadWrite

任何为直接I/O建立设备对象的较低层设备驱动程序通过返回从其设备到系统物理内存所传输的数据，这个物理内存由位于Irp->MdlAddress的MDL所描述，满足一个读请求。它通过从系统物理内存到它的设备传输数据满足写请求。

如6.2.6节提及的，较低层驱动程序必须异步地处理读/写请求。所有较低层驱动程序的DispatchReadWrite例程必须传送带有有效参数的IRP_MJ_READ与IRP_MJ_WRITE IRP到其他驱动程序例程，就如6.2.4节所描述的。

对于所有发送给较低层驱动程序的读/写IRP，由位于Irp->MdlAddress处的MDL描述的页式物理内存已被检查过有正确的访问权以执行请求的传输，并且已被一系列驱动程序中的最高层驱动程序或I/O管理器锁定。任何为直接I/O建立设备对象的中间层或最低层驱动程序不应调用MmProbeAndLockPages，因为它已经被调用。如果设备使用PIO传输数据，一个Windows 2000最低层驱动程序调用MmGetSystemAddressForMdlSafe，而一个WDM最低层驱动程序调用MmGetSystemAddressForMdl以获得系统映射虚拟地址。

如果它无法确信最高层驱动程序仅传送带有效参数的IRP，任何中间层或最低层设备驱动程序的DispatchReadWrite例程应检查它的读/写IRP的I/O栈位置的参数。如果这类驱动程序的DispatchReadWrite例程在一个到来的传输IRP中找到一个参数错误，它将用一个适当错误STATUS_XXX值完成IRP，如6.2.2节所描述的。否则，这类中间层驱动程序的DispatchReadWrite例程必须传送进一步处理请求，根据6.3.3.3节中的方针。

任何设备驱动程序的DispatchReadWrite例程必须调用带传输请求的IoMarkIrpPending，传送IRP以供其他驱动程序例程做进一步处理，并返回STATUS_PENDING，就象6.2.4节描述的。注意，设备驱动程序的DispatchReadWrite例程可以通过用一个驱动程序确定的Key值调用IoStartPacket为控制在它的设备上排队的IRP的次序以得到更快的I/O吞吐。驱动程序内的另一个例程稍后从队列中取出IRP，决定请求的长度是否必须分割进行部分传输操作，并且为传输数据的设备编程。

通常，必须将大的传输请求分割以满足设备限度的设备驱动程序应延迟这些操作直到为一个给定的传输请求建立设备之前。这类设备驱动程序的DispatchReadWrite例程不应该为特定设备传输约束而检查输入的IRP的I/O栈位置，也不应该试图计算部分传输量，如果驱动程序能延迟这些检查直到其StartIo（或其他驱动程序例程）为传输操作对设备编程之前的话。
1.3.3.3 较高层驱动程序的DispatchReadWrite

较低层设备驱动程序的DispatchReadWrite例程队列为在设备上进一步处理传输带有效参数的IRP到其他驱动程序例程，并返回STATUS_PENDING。

除了文件系统，在这类设备驱动程序之上分层的较高层驱动程序通常不拥有任何IRP内部驱动程序队列。这类驱动程序的DispatchReadWrite例程可能在建立了它的IoCompletion例程之后，可以传送带有效参数的IRP到较低层驱动程序，如6.2.4节描述的。

然而，如果必要，在它发送一个带主功能玛IRP_MJ_READ或IRP_MJ_WRITE的IRP到SCSI端口/微端口驱动程序对之前，一个SCSI类驱动程序的DispatchReadWrite例程负责分割传输数据。关于对SCSI类驱动程序的要求与如何分割一个大的传输请求的更多信息，见在线DDK和第3章的适配器对象部分。

如果一个较高层驱动程序分配一个或多个IRP来请求部分传输，这些IRP是它在DispatchReadWrite例程中为相邻较低层驱动程序建立的，那么DispatchReadWrite例程必须用每个驱动程序分配的IRP调用IoSetCompletionRoutine。驱动程序必须注册它的IoCompletion例程以跟踪在每个部分传输中传输的数据量，以便IoCompletion例程可以释放所有驱动程序分配的IRP，并且，最终完成初始请求。

如果下层驱动程序控制一个可删除的媒体设备，任何由较高层驱动程序分配的IRP必须拥有一个线程环境。为建立线程环境，分配驱动程序必须用输入传输IRP中的相同值设置每个新分配的IRP中的Irp->Tail.Overlay.Thread。

如果下层设备驱动程序为部分传输返回一个带有错误的IRP，在释放所有较高层驱动程序分配的IRP和内存之后，IoCompletion例程可以重置部分传输请求或者用返回的错误设置I/O状态块完成初始IRP。

如果较高层驱动程序的DispatchReadWrite例程为部分传输操作分配内存，并且它的分配将被该驱动程序（或者下层设备驱动程序）的IoCompletion例程访问，DispatchReadWrite例程必须从非页式缓冲池分配内存
1.3.3.4 实现DispatchReadWrite的注意事项

当实现DispatchRead、DispatchWrite或DispatchReadWrite例程时要记住以下要点：

§ 一系列分层的驱动程序中的最高层驱动程序负责检查输入的读/写IRP的参数有效性，在设置一个IRP中相邻较低层驱动程序的I/O栈位置之前。

§ 中间层和最低层驱动程序通常可以依靠该系列中最高层驱动程序来传送带有效参数的传输请求。然而，任何驱动程序可以进行IRP中它的I/O栈位置的参数检查，并且每个设备驱动程序应当在任何可能违反设备赋予的约束的情况下检查参数。

§ 如果DispatchReadWrite例程完成一个带有错误的IRP，它应设置将I/O栈位置中的Status成员置为适当的NTSTATUS类型的值，将Information成员置为零，并用这个IRP与一个值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest。

§ 如果驱动程序使用缓冲I/O，它需要定义一个存放要传输的数据的结构，并需要在这些结构内部缓存一些数据。

§ 如果驱动程序使用直接I/O，它需要检查位于Irp->MdlAddress处的MDL是否描述一个包含对下层设备太过多数据（或太多的页中断）的缓冲区，以致无法在一个独立传输请求中的处理的。如果这样，驱动程序必须把初始传输请求分割为一系列较小的传输操作。

一个连接紧密的类驱动程序可能在它的DispatchReadWrite例程中为下层端口驱动程序分割这样的请求。SCSI类驱动程序（特别是对于海量存储设备）更需要这么做。关于SCSI驱动程序要求的更多信息，见在线DDK。

§ 较低层设备驱动程序的DispatchReadWrite例程应延迟分割一个大的传输请求为部分传输直到另一个驱动程序例程把为传输而设置系统的IRP从队列中取走。

§ 如果较低层设备驱动程序把一个读/写IRP排队以供自己的例程做进一步处理，它必须在把IRP加入队列之前调用IoMarkIrpPending。在这些情况下，DispatchReadWrite例程也必须用STATUS_PENDING返回控制。

§ 如果DispatchReadWrite例程传送一个IRP到较低层驱动程序，它必须为IRP中的相邻较低层驱动程序设置I/O栈位置。较高层驱动程序是否也要在用IoCallDriver传送之前在IRP中设置IoCompletion例程取决于驱动程序的设计与它之下的那些驱动程序。

§ 当然，较高层驱动程序必须在它调用IoCallDriver之前调用IoSetCompletionRoutine，如果它分配资源，例如IRP或内存。当较低层驱动程序已经完成请求，但IoCompletion例程调用带有初始IRP的IoCompleteRequest之前，IoCompletion例程必须释放所有驱动程序分配的资源。

§ 如果较高层驱动程序为可能包括一个下层可删除媒体设备驱动程序的较低层驱动程序分配IRP，分配驱动程序必须在它分配的每个IRP内建立线程环境。
1.3.4 Dispatch(Internal)DeviceControl功能

对于所有一般类型的外围设备，系统为IRP_MJ_DEVICE_CONTROL请求定义一组I/O控制码。每种类型的设备的新驱动程序必须支持这些请求。在大多数情况下，这些每种类型的设备的公共I/O控制码不输出到用户模式应用程序。

一些系统定义的I/O控制码被较高层的驱动程序使用，这些驱动程序使用IoBuildDeviceIoControlRequest为下层设备驱动程序IRP。通过调用Win32函数DeviceIoControl，其他的被Win32驱动程序或部分Win32组件用来与一个下层设备驱动程序通信。该Win32函数调用一项系统服务，然后I/O管理器用主功能码IRP_MJ_DEVICE_CONTROL和Parameters.DeviceIoControl.IoControlCode处的I/O栈位置中的给定的I/O控制码设置一个IRP。接着，I/O管理器用IRP调用较高层驱动程序的DispatchDeviceControl例程。

对于某些系统提供的、设计成为与新驱动程序互操作地、并支持新驱动程序的驱动程序，操作系统也为IRP_MJ_INTERNAL_DEVICE_CONTROL请求定义了一组I/O控制码。在大多数情况下，这些公共I/O控制码允许附加的较高层驱动程序与一个下层设备驱动程序互操作。

例如，IOCTL_SCSI_GET_INQUIRY_DATA与IOCTL_SCSI_GET_CAPABILITIES码支持SCSI类驱动程序的初始化和配置。SCSI类驱动程序发送这些设备控制请求给SCSI端口驱动程序以查找每种类驱动程序类型的SCSI外围设备（对于那些带有连接的外围设备的类驱动程序类型来说），并且对于那些带一个连接的外围类驱动程序类型的，收集有关下层SCSI微端口HBA的相关信息，例如最大传输能力和每个DMA传输的最大页中断数等。

再例如，系统并行端口驱动程序支持一组附加并行类驱动程序在IRP_MJ_INTERNAL_DEVICE_CONTROL请求中设置的公共I/O控制码，以与它们的设备通过一个并行口通信。系统并行端口驱动程序可被替换，一旦这类新并行端口驱动程序为由现存的和新的并行端口类驱动程序继续支持同一组内部设备控制请求。

几乎所有系统定义的I/O控制码都使用缓冲I/O，因为该类型的请求很少要求传输大量的数据。换句话说，除了与Win32多媒体驱动程序联系紧密的几种最高层设备驱动程序之外，即使为直接I/O建立它们设备对象的驱动程序也为设备控制请求被发送IRP，连带被传输到位于Irp->AssociatedIrp.SystemBuffer的缓冲区的数据。

另外，驱动程序可以定义一组其他驱动程序用来与它进行通信的私有I/O 控制码。新的公共I/O控制码只有与Microsoft公司合作才能加入到系统中，因为公共I/O控制码是嵌入到操作系统本身的。

关于这组不同操作系统必须支持的公共I/O控制码和已定义的私有I/O控制码的具体信息，见《Windows 2000 驱动程序设计指南》第2卷。
1.3.4.1 最低层驱动程序中的DispatchDeviceControl

最低层驱动程序的IRP_MJ_DEVICE_CONTROL请求需要驱动程序改变其设备的状态或者提供其设备状态的信息。由于大多数驱动程序都要处理大量的I/O控制码，它们的DispatchDeviceControl例程通常包含一个如下的Switch语句：

: :

switch(IRPp->Parameters.DeviceIoControl.IoControlCode) {

case IOCTL_DeviceType_XXX;

case IOCTL_DeviceType_XXX;

if (IRPp->Parameters.DeviceIoControl.InputBufferLength <

(sizeof(IOCTL_XXXYYY_STRUCTURE))) {

status = STATUS_BUFFER_TOO_SMALL;

break;

} else {

IoMarkIrpPending(Irp);

: : // 为进一步处理传送IRP

case …

: :

就如这个代码段所示，DispatchDeviceControl例程也检查参数，有时在每个驱动程序必须支持的I/O控制码上，有时在这些IOCTL_XXX组上。

对于设备驱动程序的DispatchDeviceControl例程考虑下列实现方针：

§ 对于任何IRP和驱动程序的Dispatch例程，DispatchDeviceControl必须检查参数有效性，并立即完成带参数错误的IRP，就如6.2.3节所描述的。

§ 首先加入DispatchDeviceControl例程能为其满足和完成IRP的I/O控制码，以提高性能，因为驱动程序可以更快的返回控制。

§ 为了更好的性能，所有最低层设备驱动程序应在其DispatchDeviceControl例程中满足任何它可以满足的设备控制请求，而不用让IRP到其他设备排队。

§ 稍后加入不经常指定要求的操作的I/O控制码，这也可以在处理IRP_MJ_DEVICE_CONTROL_REQUESTS时改进设备性能。

§ 当测试参数有效性时，对（特定）case语句中的I/O控制码分组从驱动程序的性能与规模及代码维护角度来看是经济的。从前面的代码段可以看出，使用公共结构的I/O控制码非常适合这类case组。

对于任何DispatchDeviceControl例程可以完成的IRP，它应该用值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest。DispatchDeviceControl例程必须用每个为进一步处理而在设备排队的请求调用IoMarkIrpPending，并且它必须返回STATUS_PENDING。
1.3.4.2 较高层驱动程序中的DispatchDeviceControl

通常，一个较高层驱动程序的DispatchDeviceControl例程只是简单地为相邻较低层驱动程序设置IRP中的I/O栈位置，并用IoCallDriver传送。DispatchDeviceControl例程很少检查输入IRP中的参数有效性，因为下层设备驱动程序被假定拥有关于在其自己的设备上如何处理每个设备类型特定的I/O控制请求的更好信息。

对于这个一般规则的例外是在一个类/端口驱动程序对的类驱动程序中的DispatchDeviceControl。关于处理成对的类/端口驱动程序的设备控制请求的更多信息，见6.3.4.3节。

任何与特定设备驱动程序连接并不紧密的新的较高层驱动程序应为相邻较低层驱动程序设置I/O栈位置，并为进一步处理传送IRP_MJ_DEVICE_CONTROL请求。

设备控制请求通常被同步地处理。也就是说，较高层驱动程序的DispatchDeviceControl例程经常如下返回控制到系统：

: :

return IoCallDriver(DeviceObject->NextDeviceObject, Irp);

当然，如果较低层驱动程序为这样一个请求返回STATUS_PENDING，较高层驱动程序不能使用前面的技术。在这些情况下，较高层驱动程序应设置IRP中的IoCompletion例程。当其IoCompletion例程被IRP调用时，它可以检查I/O状态块以决定IRP是否仍然挂起。如果这样，IoCompletion例程可能重试该请求，或者可能用IRP调用IoMarkIrpPending，在它调用IoCompleteRequest并返回STATUS_PENDING之前。较高层驱动程序决不能用STATUS_PENDING完成一个IRP，除非它首先用IRP调用IoMarkIrpPending。

如果下层设备驱动程序在完成请求之前必须处理大量由设备传送的数据，这时较高层驱动程序可以异步地处理这样一个设备控制请求。也就是说，较高层驱动程序可以设置IRP中它的IoCompletion例程，传送IRP到较低层驱动程序，并且从其自身的DispatchDeviceControl例程返回控制。

几乎所有系统定义的I/O控制码都要求下层设备驱动程序传送合适量的数据，通常远小于一个PAGE_SIZE量。作为一条一般规律，较高层驱动程序应异步地处理这些请求，就如前面代码段所示，因为较低层驱动程序迅速返回控制。也就是说，调用较高层驱动程序的IoCompletion例程的开销无法补偿驱动程序在如此短的间隔内所进行的附加IRP处理。

用IoBuildDeviceIoControlRequest为下层设备驱动程序分配IRP的较高层驱动程序可以同步地处理这些设备控制请求。较高层驱动程序等待一个被传送到IoBuildDeviceIoControlRequest的可选事件，并连接设备分配的IRP。在用那个事件指针调用IoBuildDeviceIoControlRequest之前，驱动程序必须为事件对象提供存储，并且必须用KeInitializeEvent初始化其事件。
1.3.4.3 类/端口驱动程序中的Dispatc(Internal)DeviceControl

类/端口对的较高层驱动程序有时在其DispatchDeviceControl例程完成IRP。例如，如果一个类驱动程序在初始化时收集并存储关于下层设备特性的信息，这可以在一个传统IRP_MJ_DEVICE_CONTROL请求中查找到，类驱动程序可以满足这样一个请求而不用传送它到下层设备驱动程序以存储处理时间。按照设计者的意愿，类驱动程序也可以为其成对端口驱动程序检查IRP参数以便它可以发送带有效参数的唯一请求。

连接紧密的类/端口驱动程序也定义一组驱动程序的和/或特定设备的I/O控制码，类驱动程序用来为IRP_MJ_INTERNAL_DEVICE_CONTROL请求设置IRP到端口驱动程序。

例如，系统键盘和鼠标类驱动程序发送系统定义的内部设备控制请求以激活/废止键盘/鼠标的，从每个类驱动程序的DispatchCreateClose例程到下层端口驱动程序的中断。这些系统类驱动程序处理IRP_MJ_DEVICE_CONTROL请求，通过为一个下层端口驱动程序设置IRP_MJ_INTERNAL_DEVICE_CONTROL请求。任何与这些系统类驱动程序互操作的新的键盘或鼠标端口驱动程序也必须支持这些公共内部设备控制请求。

关于这些公共内部设备控制请求和如何为新的类/端口驱动程序定义私有I/O控制码的更多信息，见《Windows 2000 驱动程序开发指南》第2卷。

对于一对连接紧密的端口/类驱动程序，类驱动程序可以处理某些设备控制请求过程，而不用传送它们到端口驱动程序。在一个新的类/端口驱动程序对内，类驱动程序的DispatchDeviceControl例程可以做以下工作：

§ 在IRP自身的I/O栈位置中检查参数有效性，如果它找到任何参数错误则设置I/O状态块，并且用IRP和值为IO_NO_INCREMENT的PriorityBoost调用IoCompleteRequest；否则，调用IoGetNextIrpStackLocation，拷贝其自身I/O栈位置到端口驱动程序的I/O栈位置，并用IoCallDriver传送IRP。

§ 或者，仅仅在IRP中设置端口驱动程序的I/O栈位置，而不检查参数并传送到端口驱动程序。

SCSI类驱动程序对于处理设备控制请求有特定要求。关于这些要求的更多信息，见在线DDK。
1.3.4.4 实现Dispatch(Internal)DeviceControl的注意事项

在实现一个新的DispatchDeviceControl或DispatchInternalDeviceControl例程时要记住以下要点：

§ 至少，较高层驱动程序必须拷贝一个IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求的参数从IRP中其自身I/O栈位置到相邻较低层驱动程序的I/O栈位置。这时，它必须用一个指向相邻较低层驱动程序的设备对象的指针和IRP调用IoCallDriver。

当较高层驱动程序为一个较低层驱动程序同步控制的请求返回控制时，它应当传播由IoCallDriver返回或者在返回的I/O状态块中设置的状态值。

§ 下层设备驱动程序必须处理设备控制请求，除非它有一个代表它完成这些请求的子集的、连接紧密的类驱动程序。设备驱动程序的DispatchDeviceControl例程通常通过替换每个IRP的I/O栈位置中的Parameter.DeviceIoControl.IoControlCode来开始处理这些请求。

§ 较低层设备驱动程序应检查请求传送的参数，并用一个适当的错误使IRP失效，如果任何参数无效的话。对这些请求的参数有效性的最一般检查有如下规范：

if (Irp->Parameters.DeviceIoControl.InputBufferLength <

(sizeof(IOCTL_SPECIFIC_STRUCTURE))) {

status = STATUS_XXX;

或者

if (Irp->Parameters.DeviceIoControl.OutputBufferLength <

(sizeof(IOCTL_SPECIFIC_STRUCTURE))) {

status = STATUS_XXX ;

状态值设置为STATUS_BUFFER_TOO_SMALL或STATUS_INVALID_PARAMETER。

§ 所有设备驱动程序的DispatchDeviceControl或DispatchInternalDeviceControl例程必须通过用适当NTSTATUS值设置I/O状态块，将其Information域置为零，并用一个IO_NO_INCREMENT的PriorityBoost完成IRP来处理对一个未被承认的I/O控制码的接收。

§ 一个设备驱动程序控制的部分I/O控制码必须为相同类型的设备包括任何特定设备类型的、系统定义的I/O控制码。关于系统对于每种类型的设备和每个相关（SDK）的、用ntdd前缀开始的头文件的信息，以及关于系统为这些I/O控制码定义的结构声明的要求的更多信息，参见在线DDK。

§ 一个连接紧密的类/端口驱动程序对的类驱动程序可以处理并完成一部分设备控制请求，而不用传送它们给下层端口驱动程序。当然，这样一个类驱动程序必须传送所有有效的、要求改变设备状态的和那些要求返回设备限制信息（例如当前波特率、流量或视频模式）的设备控制请求。


StartIo和队列管理例程

本章概述了驱动程序的标准StartIo例程的必要功能。也概述了Windows NT/Windows 2000为在驱动程序中建立内部队列提供的支持，并讨论了一些管理驱动程序创建的内部队列的问题。

本章包含以下信息：

7.1 排队IRP

7.2 StartIo例程需求

7.2.1 设备驱动程序中的StartIo例程

7.2.2 StartIo例程功能

7.2.2.1 使用缓冲I/O的设备驱动程序StartIo例程

7.2.2.2 使用直接I/O的设备驱动程序StartIo例程

7.2.2.3 在StartIo中处理设备控制请求

7.2.3 对设备编程以实现I/O操作

7.2.4 较高层驱动程序中的StartIo例程

7.2.5 实现StartIo的注意事项

7.3 使用互锁队列或设备队列的需求

7.3.1 驱动程序管理的队列的存储需求

7.3.2 建立驱动程序管理的队列

7.3.3 驱动程序管理的队列中排队的IRP

7.3.4 用驱动程序创建的线程管理互锁队列

7.3.5 管理补充设备队列

1.1 排队IRP

因为Windows NT/Windows 2000在多任务及多线程环境中支持异步I/O，I/O请求可以比驱动程序处理完成更快地到达，特别是在一个多处理器的机器中。因此，任何具体设备在忙与处理另一个IRP时，必须让超出限度的IRP在驱动程序上排队。

因此，最低层驱动程序必需以下之一：

§ StartIo例程，I/O管理器用一个IRP调用它来启动一个该驱动程序的设备上的I/O操作

§ 它自己的IRP排队机制，它用来控制比驱动程序的处理速度更快地到达的IRP

仅有一个最低层设备驱动程序能在其Dispatch例程中满足和完成所有可能的IRP，而不需要StartIo例程和/或驱动程序管理的IRP队列.

较高层驱动程序几乎从来没有StartIo例程。大多数中间层驱动程序也没有内部队列；一个中间层驱动程序通常从其Dispatch例程传送带有效参数的IRP，并且为任何其IoCompletion例程中的IRP做必要的处理后操作。

通常，以下内容描述了一些确定实现一个StartIo例程是否使用内部的、系统管理的IRP队列的设计考虑。

驱动程序中的StartIo例程

大多数设备驱动程序有StartIo例程，因为大多数PC外设同时仅能处理一个设备I/O操作。对于有一个StartIo例程的驱动程序，I/O管理器提供IoStartPacket和IoStartNextPacket来管理驱动程序中的IRP队列。

较高层驱动程序通常没有StartIo例程。关于在较高层驱动程序中折衷实现StartIo例程的更多信息，见“较高层驱动程序中的StartIo例程”。

驱动程序中的内部IRP队列

如果一个下层设备可以支持多个并发的I/O操作，最低层设备驱动程序必须建立内部请求队列，并管理其自身的IRP队列。例如，系统串行驱动程序为其设备上的读、写、清除和等待操作维护不同的队列，因为它支持全双工串行设备。

发送请求给一些下层设备驱动程序的较高层驱动程序也维护内部IRP队列。例如，文件系统驱动程序几乎总拥有内部IRP队列。

更多信息，见“使用互锁队列或设备队列的需求”。

驱动程序中带有StartIo例程的内部队列

管理其自身内部队列的驱动程序可以拥有StartIo例程，但是并不需要。大多数最低层设备驱动程序拥有StartIo例程或者管理它们的IRP队列。

SCSI类驱动程序是一个例外，它有一个StartIo例程并管理内部IRP队列。I/O管理器在与驱动程序创建的、代表一个SCSI HBA的设备对象连接的设备队列中排队IRP到端口驱动程序的StartIo例程。SCSI端口驱动程序也为每个机器中任意HBA驱动的SCSI总线上的目标设备（与一个SCSI逻辑单元相关的）建立各自的设备队列对象，并管理IRP队列。

无论何时SCSI总线上某个设备特别忙，SCSI端口驱动程序使用它的补充设备队列保有从在LU特定队列中SCSI类驱动程序发送来的IRP。实际上，该驱动程序的补充的、LU特定的设备队列允许SCSI端口驱动程序为通过HBA的不同SCSI设备序列化操作，而同时保持在那个HBA的SCSI总线上的设备尽可能的忙。

内部队列和驱动程序线程

带有设备专有线程的驱动程序和使用执行工作者线程的最高层驱动程序（包括大多数文件系统驱动程序）通常在它们设备对象的设备扩展中设置一个互锁的IRP队列。该队列由驱动程序线程或驱动程序提供的回调工作者线程和其他处理IRP的驱动程序例程共享。

驱动程序的Dispatch例程把IRP插入到互锁队列，并且一个驱动程序创建的线程或驱动程序的工作者线程通过调用ExInterlocked..List支持例程把它们移去。

例如，系统软盘控制器驱动程序使用这样一个互锁队列。其设备专有线程进行与其他设备驱动程序的StartIo例程所做的相同的IRP处理，以及一些与其他设备驱动程序的DpcForIsr例程所做的相同的IRP处理。

1.2 StarIo例程需求

StartIo例程由I/O管理器定义如下：

VOID

( *PDRIVER_STARTIO) (

IN PDRIVER_OBJECT DeviceObject,

IN PIRP Irp

);

StartIo例程运行在位于IRQLDISPATCH_LEVEL上的任意线程环境中。

运行在IRQLDISPATCH_LEVEL上限制了这组StartIo例程可以调用的支持例程。关于管理IRQL的更多信息见第16章中“硬件优先级管理”。关于任何特定支持例程可能在其上被调用的IRQL的具体信息，见《Windows 2000 驱动程序开发指南》第2卷。
1.2.1 设备驱动程序中的StartIo例程

正如它的名字所暗示的，最低层设备驱动程序中的StartIo例程负责启动物理设备上的I/O操作。

当一个最低层设备驱动程序的StartIo例程被调用时，它假定有输入设备对象代表的目标设备并不忙。刚刚调用IoStartPacket的驱动程序Dispatch例程和IRP并没有被插入到与目标设备相连的设备队列，并且驱动程序的DpcForIsr例程也正完成另一个请求，并刚刚调用过IoStartNextPacket。

在最高层设备驱动程序中的StartIo例程被调用之前，驱动程序的Dispatch例程必须查找并锁定用户缓冲区。如果必要，在对其StartIo例程排队的IRP中设置有效映射缓冲区地址。如果最高层设备驱动程序为直接I/O（或非直接也非缓冲I/O）建立其设备对象，驱动程序不能延迟锁定用户缓冲区到其StartIo例程；所有StartIo例程在一个位于IRQLDISPATCH_LEVEL的任意线程环境中被调用。

记住以下情况：

§ 任何由一个驱动程序的StartIo例程访问的缓冲内存必须被锁定或者从常驻系统空间内存中分配，并且必须是在一个任意线程环境中可访问的。

为缓冲I/O建立其设备对象的驱动程序可以依赖I/O管理器传送所有它发送给驱动程序的IRP中的有效缓冲区。为直接I/O建立设备对象的较低层驱动程序可依靠同系列中最高层驱动程序传送所有经由任意一个中间层驱动程序到下面的较低层设备驱动程序的IRP。

通常，任何较低层设备驱动程序的StartIo例程负责用输入IRP调用IoGetCurrentIrpStackLocation，并进行任何必需在其设备上启动I/O操作的特定请求处理。特定请求处理包括以下：

§ 建立或更新任何有关当前驱动程序保留的请求的状态信息。该状态信息可能存放在目标设备对象的设备扩展或者其他由驱动程序分配的非页式缓冲池中。

§ 例如，如果一个设备驱动程序维护一个关于当前传输操作的InterruptExpected布尔量，其StartIo例程可能设置这个变量为TRUE。如果该驱动程序为当前操作维护一个超时计数器，其StartIo例程可以设置这个值，或者StartIo例程可以对驱动程序的CustomerTimerDpc 例程排队。

如果状态信息或任何其他资源是与其他驱动程序例程共享的，该状态区域或资源必须由一个自旋锁保护。如果StartIo例程与其他“非ISR”例程共享状态或资源，它必须在访问之前获得保护状态或资源的自旋锁。如果StartIo例程与驱动程序的ISR共享状态，StartIo必须用一个驱动程序提供的、设置状态的SynchCritSection例程调用KeSynchronizeExecution。

更多细节，见第16章的“自旋锁的使用”。

§ 分配一个序列号给IRP以防驱动程序在处理IRP时必须注册一个设备I/O错误

更多信息，见第16章的“错误记录和NTSTATUS值”。

§ 如果必要，将驱动程序I/O栈位置中的参数转换为系统特定值

例如，一个磁盘驱动程序可能需要为一次传输操作计算开始扇区和到磁盘物理地址的偏移量，并且计算请求的传输长度是否将超出一个特定扇区范围或超出其物理设备的传输能力。

§ 如果驱动程序控制一个可删除媒体设备，在该设备为I/O编程之前检查媒体变化，如果媒体有变化的话，通知其上的文件系统

更多信息，见第16章“可删除媒体控制”。

§ 如果设备使用DMA，检查请求的长度（将被传输的字节数，可在IRP的驱动程序的I/O栈位置找到）是否应被分割成部分传输操作，就象第3章所说明的，假定连接紧密的较高层驱动程序并不为该设备驱动程序事先分割大的传输请求

这类设备驱动程序的StartIo例程也负责调用KeFlushIoBuffers，并且，如果该驱动程序使用基于包的DMA，则它负责用驱动程序的AdapterControl例程调用AllocateAdapterChannel。

更多细节，见第11章“AdapterControl和ControllerControl例程”，以及第16章的“为DMA和PIO维护高速缓存一致性”。

§ 如果设备使用PIO，对于一个系统空间地址，用Windows 2000上的MmGetSystemAddressForMdlSafe或者WDM驱动程序中的MmGetSystemAddressForMdl映射缓冲区虚拟基地址（在IRP 的Irp->MdlAddress处）。

对于读请求，设备驱动程序的StartIo例程负责在PIO操作开始之前调用KeFlushIoBuffers。更多信息，见第16章的“为DMA和PIO维护高速缓存一致性”。

§ 如果Windows 2000驱动程序使用一个控制器对象，用其ControllerControl例程调用IoAllocateController。

§ 如果驱动程序控制可撤消的IRP，检查输入IRP是否已经被撤消

§ 如果一个输入IRP可以在它完成前被撤消，StartIo例程必须用IRP和驱动程序的Cancel例程入口点调用IoSetCancelRoutine。StartIo例程必须为它对IoSetCancelRoutine的调用获得撤消自旋锁。
1.2.2 StartIo例程功能

到达一个设备驱动程序Dispatch例程的IRP不一定非要驱动程序在其设备上进行I/O处理。通常，每个驱动程序的Dispatch例程完成一部分要求的请求，并为完成它们传送一些给其他驱动程序例程。Dispatch例程也完成任何驱动程序I/O栈位置中有参数错误的IRP。

当然，所有带一个StartIo例程的设备驱动程序往往为IRP_MJ_READ与IRP_MJ_WRITE请求从其Dispatch例程调用IoStackPacket，并且通常对于部分I/O控制码，它支持IRP_MJ_DEVICE_CONTROL请求。

I/O管理器对驱动程序的Dispatch例程的调用是满足一个设备I/O请求的第一阶段。StartIo例程是第二阶段。当一个Dispatch例程用IRP调用IoStartPacket，I/O管理器为进一步处理直接传送IRP到StartIo例程，或者在创建包时插入包到与目标设备对象连接的设备队列。

作为一般规则，使用缓冲I/O的驱动程序有一个比使用直接I/O的驱动程序更简单的StartIo例程。使用缓冲I/O的驱动程序为每个传输请求传输少量的数据，而那些使用直接I/O的驱动程序（不论DMA或PIO）传输大量数据到/从锁定的缓冲区，这些缓冲区可以在系统内存中扩展物理页面边界。

在物理设备驱动程序之上分层的较高层驱动程序通常建立它们的设备对象与它们各自的设备驱动程序相匹配。当然，一个最高层驱动程序，特别是一个文件系统驱动程序，可以建立非直接也非缓冲I/O的设备对象。关于建立直接I/O和缓冲I/O的设备对象的更多信息，见第3章的“设备对象和设备扩展”。
1.2.2.1 使用缓冲I/O的设备驱动程序StartIo例程

假定I/O管理器的DispatchRead、DispatchWrite或DispatchDeviceControl例程已经确定一个请求的有效性，并调用了IoStackPacket，那么I/O管理器调用一个最低层设备驱动程序的StartIo例程立即处理包，如果设备不忙的话。如果这个包由IoStartPacket排队，最终从驱动程序的DpcForIsrh或CustomDpc例程对IoStackNextPacket的调用会引起StartIo例程被使用出列的IRP调用。

StartIo例程调用IoGetCurrentIrpStackLocation，并确定为满足请求必须执行哪些操作。它在对执行I/O请求的物理设备编程之前，用任何必要的方法预处理IRP。

如果对物理设备（或设备扩展）的访问必须由驱动程序的ISR同步，StartIo例程必须调用驱动程序的SynchCritSection例程以执行必要的设备编程。更多信息，见“I/O操作设备编程”。

一个使用缓冲I/O的物理设备驱动程序传输数据到（读）或从（写）一个由I/O管理器分配的、驱动程序在每个IRP的Irp->AssociatedIrp.SystemBuffer找到的系统空间缓冲区。
1.2.2.2 使用直接I/O的设备驱动程序StartIo例程

假定其DispatchRead、DispatchWrite或DispatchDeviceControl例程已经确定一个请求的有效性，并调用了IoStartPacket，一个设备驱动程序的StartIo例程被调用以处理下一个包。如果这个包由IoStartPacket排队，最终从驱动程序的DpcForIsrh或CustomDpc例程对IoStackNextPacket的调用会引起StartIo例程被使用出列的IRP调用。

StartIo例程调用IoGetCurrentIrpStackLocation，并确定哪个请求必须执行以满足请求。它用任何必要的方法预处理IRP，例如分割一个大的DMA传输请求为部分传输容量，并存储一个将要到达的，必须被分割的传输请求的长度的相关状态。这时它为执行I/O请求对物理设备编程。

如果对物理设备（或设备扩展）的访问必须由驱动程序的ISR同步，StartIo例程必须使用一个驱动程序提供的SynchCritSection例程来执行必要的编程。更多信息，见“I/O操作设备编程”。

任何使用直接I/O的驱动程序对一个锁定的、由驱动程序在IRP中Irp->MdlAddress处找到的内存描述符（MDL）描述的缓冲区读或写数据。这样一个驱动程序一般为设备控制请求使用缓冲I/O。更多信息，见“处理StartIo中的设备控制请求”。

作为由内存管理器定义，专用的，并且可变的类型，所有MDL对驱动程序都是不透明的。考虑以下实现方针：

§ 驱动程序不能直接使用包含在任何MDL中的地址映射，包括位于Irp->MdlAddress处的MDL。

相反，使用PIO的驱动程序重新映射用户缓冲区，通过用Irp->MdlAddress调用MmGetSystemAddressForMdlSafe（仅在Windows 2000驱动程序中）或MmGetSystemAddressForMdl（WDM驱动程序）。在它们的带缓冲区地址的传输操作重新映射到它们的驱动程序的逻辑范围时，使用DMA的驱动程序也传送Irp->MdlAddress到支持例程。

如果一个连接紧密的较高层驱动程序不为下层的设备驱动程序预先分割大的DMA传输请求，一个最低层设备驱动程序的StartIo例程必须分割每个大于其设备在一次单独传输操作中所能完成的容量的传输请求。使用系统DMA的驱动程序被要求分割对于系统DMA控制器或者其设备在一次单独的传输操作中的处理能力来说过大的传输请求。

如果该设备是一个从属 DMA设备，其驱动程序必须同步一个带有驱动程序分配的适配器对象，代表DMA通道的系统DMA控制器和一个驱动程序提供的AdapterControl例程之间的传输。一个总线控制DMA设备的驱动程序也必须使用一个驱动程序分配的适配器对象使它的传输同步，并且必须提供一个AdapterControl例程（如果它使用系统基于包的DMA支持）或者一个AdapterListControl例程（如果它使用系统的散布/收集支持）。

依据驱动程序的设计，可以使用一个控制器对象使一个物理设备上的传输与设备控制操作同步，并提供一个ControllerControl例程。

更多信息，见第3章的“适配器对象和DMA”和第11章的“AdapterControl和ControllerControl例程”。
1.2.2.3 在StartIo中处理设备控制请求

通常，仅有部分设备I/O控制请求被从DispatchDevice(Internal)Control例程传送，以供最低层设备驱动程序的StartIo例程做进一步处理。也就是说，任何最低层驱动程序的StartIo例程必须处理要求设备状态改变或返回当前设备状态易失信息的有效设备控制请求。

每个新的驱动程序必须与所有同类设备的其他驱动程序一样支持相同的公共I/O控制码组。系统为作为缓冲请求的IRP_MJ_DEVICE_CONTROL请求定义公共的、特定设备类型的I/O控制码。

因此，物理设备驱动程序做从/到一个系统空间缓冲区的数据传输，每个驱动程序在设备控制请求的IRP中的Irp->AssociatedIrp.SystemBuffer找到这个缓冲区。每个为直接I/O建立设备对象的驱动程序使用缓冲I/O满足带有公共I/O控制码的设备控制请求。

每个I/O控制码的定义决定请求的数据传输是否被缓冲。任何为驱动程序对之间的特定驱动程序的IRP_MJ_INTERNAL_DEVICE_CONTROL请求私有定义的I/O控制码却能定义一个带缓冲方法、直接方法或两者都不是的代码。作为一般规律，如果一个连接紧密的较高层驱动程序必须为该请求分配缓冲区，任何私有定义的I/O控制码应不能用这三种中的最后一种定义。

关于任何一般类型的PC设备必须支持的一个新驱动程序的I/O控制码和私有I/O控制码的定义的更多信息，见在线DDK。

1.2.3 I/O操作设备编程

通常，设备驱动程序中的StartIo例程必须通过用系统提供的SynchCritSection例程调用KeSynchronizeExecution同步访问任何内存或它与驱动程序的ISR共享的设备注册器。

换句话说，一个最低层设备驱动程序的StartIo例程通常导致SynchCritSection例程为位于DIRQL的I/O对物理设备编程。

在用对设备编程的SynchCritSection例程调用KeSynchronizeExecution之前，StartIo例程必须进行该请求必要的处理。预先处理包括计算一个初始化部分传输容量，并存储任何关于对其他设备例程初始请求的状态信息。

如果设备驱动程序使用DMA，其StartIo例程通常用一个系统提供的AdapterControl例程调用AllocateAdapterChannel。在这些情况下，StartIo例程把对物理设备编程的响应推迟到AdapterControl例程。紧接着，它调用KeSynchronizeExecution使一个驱动程序提供的SynchCritSection例程为一个DMA传输对设备编程。

关于驱动程序例程如何与一个ISR共享资源，或者多个非ISR例程之间如何以多处理器安全方式共享资源，见第16章的“使用自旋锁”。关于DMA使用的更多信息，见第3章的“适配器对象和DMA”和第11章的“AdapterControl和ControllerControl例程”。
1.2.4 较高层驱动程序中的StartIo例程

任何较高层驱动程序都有一个StartIo例程。当然，这样一个驱动程序往往不能与现存的较低层驱动程序共同操作，并且往往表现出很差的性能特性。

较高层驱动程序中的StartIo例程有下列作用：

§ 可以通过从驱动程序的Dispatch例程调用IoStartPacket和从其IoCompletion例程调用IoStartNextPacket对到达的IRP排队，从而使得通过StartIo例程一次处理一个IRP。

§ 在繁重的I/O命令期间，驱动程序的I/O吞吐量显著变小，因为其StartIo例程变成了一个瓶颈。

§ 驱动程序的StartIo例程用位于IRQLDISPATCH_LEVEL的每个IRP调用IoCallDriver，由此使得所有较低层驱动程序的Dispatch例程也在IRQLDISPATCH_LEVEL上运行。这限制了较低层驱动程序可以在Dispatch例程中调用的支持例程。因为大多数驱动程序开发者假定它们的驱动程序的Dispatch例程在IRQLDISPATCH_LEVEL上运行，较高层驱动程序往往不能与许多现存的较低层驱动程序共同操作。

§ StartIo例程减少系统整体吞吐量，因为它和所有较低层驱动程序的Dispatch例程在提高的IRQL上处运行。

关于标准驱动程序例程在其上运行的IRQL的更多信息，见第16章的“硬件优先级管理”。关于任何具体的标准驱动程序的特定信息，见第5章到第15章的相关章节。

没有一个系统提供的较高层驱动程序有StartIo例程，因为对于所有其上和其下的驱动程序以及系统整体，它延缓驱动程序本身的IRP处理。

大多数较高层驱动程序只是从它们的Dispatch例程发送IRP到较低层驱动程序，并在它们的IoCompletion例程中进行任何必要的清除处理。

当然，较高层驱动程序可以为请求特定操作的IRP建立内部队列，或者建立内部队列以控制一组不同种类的下层设备，象SCSI端口驱动程序，的IRP界限，如“IRP排队”中所示的。
1.2.5 实现StartIo的注意事项

当实现StartIo例程时记住以下要点：

§ StartIo例程必须使它对物理设备和对驱动程序在设备扩展中维护的共享状态信息或资源的访问与访问相同设备、内存位置或资源的这个驱动程序的其他例程同步。

如果StartIo例程与ISR共享设备或状态，它必须用系统提供的SynchCritSection例程调用KeySynchronizeExecution以对设备编程或访问共享状态。

如果StartIo例程与ISR之外的其他例程共享状态或资源，它必须用一个驱动程序初始化的、驱动程序提供存储的执行自旋锁保护共享的状态与资源。

§ 如果一个整体式的Windows 2000设备驱动程序建立一个控制器对象，其StartIo例程可以通过一个共享的物理设备使用该控制器对象与附属（类似）设备同步操作。

更多信息，见第3章的“控制器对象”和第11章“AdapterControl和ControllerControl例程”。

§ 如果一个密切相关的较高层驱动程序不为其下层设备驱动程序预先分割大的DMA传输请求，下层设备驱动程序的StartIo例程必须为部分传输请求分割大的传输请求，并且驱动程序必须执行一系列部分传输设备操作。每个部分传输必须使其规模适合硬件能力：要么是驱动程序的设备的能力，要么，对于一个从属DMA设备来说，是任意一个有严格限制的系统DMA控制器的能力。

关于系统或总线控制DMA的使用的更多信息，见第3章的“适配器对象和DMA”。

§ 使用DMA的驱动程序的StartIo例程必须同步使用适配器对象的传输。

§ StartIo例程在IRQL DISPATCH_LEVEL上运行 ，限制了一组调用的支持例程。

例如，一个StartIo例程既不能访问，也不能分配可分页内存，并且它也无法在一个适配器对象上等待。另一方面，一个StartIo例程可以用KeAcquireSpinLockAtDpcLevel和KeRealseSpinLockFromDpcLevel获得和释放一个驱动程序分配的执行选自旋锁，KeAcquireSpinLockAtDpcLevel和KeRealseSpinLockFromDpcLevel可以比KeAcquireSpinLock和KeRealseSpinLock运行的更快些。

更多信息，见第16章的“硬件优先级管理”和“自旋锁的使用”。

§ 如果驱动程序以可撤消状态持有IRP，其StartIo例程必须在它为其设备上的请求启动任何处理之前检查输入IRP是否已经被撤消。关于可撤消IRP的管理的更多信息，见第12章“Cancel例程”。

1.3 使用互锁队列或设备队列的需求

最低层驱动程序要么必须拥有一个StartIo例程，要么必须建立及管理其自身的内部IRP队列。较高层驱动程序可以拥有其自身的内部IRP队列，但不需要这么做。

根据要求的功能与下层设备的属性，，你可以选择实现一个StartIo例程还是建立一个或多个驱动程序管理的IRP补充队列。

系统为以下驱动程序可以使用的内部IRP队列提供支持：

§ 一个双重连接的互锁队列，驱动程序通过调用ExInterlockedInsertTailList或ExInterlockedINsertHeadList插入IRP到其中，并且一个设备专有线程、回调工作者线程或另一个驱动程序例程通过调用ExInterlockedRemoveHeadList从其中移去IRP

§ 一个与设备对象连接的补充设备队列，驱动程序通过调用KeInsertDeviceQueue或KeInsertByKeyDeviceQueue插入IRP到其中，并且它可以通过调用KeRemoveDeviceQueue、KeRemoveByKeyDeviceQueue或KeRemoveEntryDeviceQueue从其中移去IRP

如果驱动程序拥有一个StartIo例程，它可以通过调用IoStartNextPacket或IoStackNextPacketByKey从与其设备对象连接的设备队列中加入和删除IRP。

一个互锁队列更容易管理，尤其是对于带设备专有线程或回调工作者线程的驱动程序。另一方面，补充设备队列给驱动程序更多的关于给定IRP是否被放置在队列中以及排序队列中项的控制。

驱动程序不应使用一个单连接的IRP互锁队列。
1.3.1 驱动程序管理的队列的存储需求

任何使用一个互锁或补充设备队列的驱动程序必须为必需的系统定义的对象和资源提供存储空间，如下：

§ 对于一个双重连接互锁队列，既要为ExInterlocked..List例程的参数要求的一个执行自旋锁提供存储空间，也要为LIST_ENTRY类型的队列头提供存储空间。

§ 对于一个设备队列，为一个内核定义设备队列对象提供存储空间。

每个设备队列对象有一个与执行自旋锁和一个队列头的连接；当驱动程序为其设备队列对象提供存储空间时这些资源的存储是自动分配的。

驱动程序通常在其设备对象的设备扩展中提供该存储区间，但是它们也可以调用PoolType参数设置为NonPagedPool的ExAllocatePool分配存储区间。另一种方法是，使用一个控制器对象的Windows 2000驱动程序可以在一个控制器扩展中提供存储区间。

关于缓冲池内存分配的更多信息，见第16章的“内存使用管理”。
1.3.2 建立驱动程序管理的队列

当驱动程序处理PnP管理器的IRP_MN_START_DEVICE请求时，它在设备启动期间建立其队列。队列必须如下来建立：

n 对于使用一个双重连接互锁队列的驱动程序，它必须通过调用一个运行时库例程InitializeListHead初始化队列头。还必须通过调用KeInitializeSpinLock初始化执行自旋锁。

n 对于使用一个补充设备队列的驱动程序，它必须调用KeInitializeDeviceQueue以初始化设备队列对象。

如果一个新驱动程序为一个未确定（但在任何给定情况下都是有限的）数量的固定大小的，驱动程序结构化的入口建立内部队列，后备队列可以被用来分配和释放这类入口的内存。也可参见《Windows 2000驱动程序开发指南》第2卷中的Ex..LookasideList。

关于建立与使用设备队列的更多信息，见第3章的“设备队列对象和互锁队列”。
1.3.3 驱动程序管理的队列中排队的IRP

驱动程序可以通过用相关特定IRP参数调用ExInterlocked..List或Ke..DeviceQueue来支持例程对两种队列插入或删除IRP：

§ 对于双重互锁队列是&Irp->Tail.Overlay.ListEntr y

§ 对于补充设备队列是&Irp->Tail.Overlay.DeviceQueueEntry

驱动程序也可以为驱动程序定义的数据结构建立内部队列或缓冲区，如第6章的“DispatchRead或DispatchWrite功能”中提到的。
1.3.4 用驱动程序创建的线程管理互锁队列

如同系统软盘控制器驱动程序，它带有设备专有线程，而不是StartIo例程的驱动程序，通常在一个双重连接互锁队列内管理其自身的IRP队列。当设备上有任务运行时，驱动程序从其互锁队列中取出IRP。

通常，驱动程序必须管理与任何由线程和其他驱动程序例程共享的资源的线程的同步。驱动程序也必须有方法通知其驱动程序创建的线程IRP被排队。通常，线程等待一个调度者对象，在设备扩展中存储，直到驱动程序的Dispatch例程在插入一个IRP到互锁队列之后设置调度者对象为Signaled状态。

当驱动程序的Dispatch例程被调用时，每个例程检查输入IRP的I/O栈位置中的参数，如果它们有效的话，为进一步处理对请求排队。对于每个为驱动程序专有线程排队的IRP，Dispatch例程应建立其线程需要处理的任何环境，在它调用ExInterlockedInsert..List之前。每个IRP中的驱动程序I/O栈位置使驱动程序线程可以访问目标设备对象的设备扩展，在那里驱动程序可以与其线程共享环境信息，就如同线程从队列中移走每个IRP一样。

当驱动程序调用PsCreateSystemThread时，任何驱动程序创建的线程运行在IRQL PASSIVE_LEVEL上和一个前面已设置好的基本运行时优先级处。当从驱动程序内部队列中移出IRP时，线程对ExInterlockedRemoveHeadList的调用暂时提升了当前处理器上DISPATCH_LEVEL的IRQL。在从该调用返回时，初始IRQL被恢复成PASSIVE_LEVEL。

任何驱动程序线程（或驱动程序提供的工作者线程回调）必须管理它在其上运行的IRQL。例如，考虑以下内容：

§ 因为系统线程通常运行在IRQL PASSIVE_LEVEL，一个驱动程序线程可能等待内核定义的调度者对象。

例如，一个设备专有线程可以为其他驱动程序例程等待一个事件以满足并完成一定量的、该线程用IoBuildSynchronousFsdRequest设置的部分传输IRP。

§ 当然，这样一个设备专有线程必须在调用某些支持例程之前提高当前处理器上的IRQL。

例如，如果一个驱动程序使用DMA，其设备专有线程必须在调用KeRaiseIrql和KeLowerIrql之间嵌套调用AllocateAdapterChannel和FreeAdapterChannel，因为这些例程和其他的DMA操作具体支持例程必须在IRQL DISPATCH_LEVEL处调用。

记住StartIo例程在DISPATCH_LEVEL处运行，所以使用DMA的驱动程序不需要从它们的StartIo例程调用Ke..Irql。

§ 驱动程序创建的线程可以访问页式内存，因为它在位于IRQL PASSIVE_LEVEL的非任意线程环境（它自身的）运行，但是许多其他的标准驱动程序例程在提升的IRQL处运行。

因此，如果一个驱动程序创建的线程分配可由其他驱动程序例程访问的内存，它必须必须从非页式缓冲池分配内存。例如，如果一个设备专有线程分配任何可由驱动程序的ISR或SynchCritSection、 AdapterControl、 AdapterListControl、 ControllerControl、 DpcForIsr、 CustomDpc、 IoTimer,、CustomTimerDpc或在较高层驱动程序中的IoCompletion例程访问的缓冲区，该线程分配的内存不能是可分页的。

§ 如果驱动程序在设备扩展中维护共享的状态信息或资源，驱动程序例程（例如StartIo例程）必须将它对物理设备和共享数据的访问与访问相同设备、内存位置或资源的驱动程序的其他例程同步。

如果线程与ISR共享设备或状态，它必须用驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution从而对设备编程或访问共享数据。

如果线程与例程，而不是与ISR，共享状态或资源，驱动程序必须用一个驱动程序初始化的、由驱动程序提供存储空间的执行自旋锁保护共享的状态或资源。

关于使用一个慢速设备的驱动程序线程的折衷设计的更多信息，见第16章的“轮询设备”。也可参考第16章的“硬件优先级管理”。关于调用任何特定支持例程所必需的IRQL的详细信息，见在线DDK。
1.3.5 管理补充设备队列

I/O管理器（除FSD外）通常在驱动程序调用IoCreateDevice时创建一个相连的设备队列对象。它也提供IoStartPacket和IoStartNextPacket，驱动程序可以调用它们以使I/O管理器插入IRP到相连的设备队列或者调用它们的StartIo例程。

因此，驱动程序为IRP建立自己的设备队列对象是没有必要的，或者说是没有什么特别的用处。这主要是对驱动程序，例如SCSI端口驱动程序，它必须为通过一个独立控制器或总线适配器服务的不同种类的设备，以协调来自一些连接紧密的类驱动程序的将要到达的IRP。

换句话说，一个磁盘阵列控制器的驱动程序更可能使用一个驱动程序创建的控制器对象，而不是建立补充设备队列对象；然而一个附加总线适配器的驱动程序和一组类驱动程序中的驱动程序使用补充设备队列的可能性就要小一些。

对补充设备队列的支持

KeInsert..DeviceQueue例程返回一个Boolean值表明给定IRP是否被插入到设备队列。KeInsertDeviceQueue要么插入在设备队列尾部插入一个项并设置设备队列对象状态为忙，要么简单地将状态设置为忙并返回FALSE以表明IRP没有被排队。一个建立了补充设备队列的驱动程序可以通过调用KeInsertByKeyDeviceQueue将一个驱动程序确定的指令强加到IRP处理过程中，这个调用还要返回一个Boolean值表明给定的IRP是否被插入到设备队列中。

对于直接调用KeInsertDeviceQueue或KeInsertByKeyDeviceQueue的驱动程序，考虑以下实现方针：

§ 如果返回值为FALSE，调用者必须立即通过其他驱动程序例程为进一步处理传送IRP。

对KeInsertDeviceQueue或KeInsertByKeyDeviceQueue的调用设置设备队列对象为Busy，甚至队列为空以及输入IRP并不插入到队列中也是如此。逆调用KeRemove..DeviceQueue重置一个空设备队列对象的状态为Not-Busy。

驱动程序可以通过调用KeRemoveDeviceQueue从其补充设备队列中删除IRP（或者重置设备队列状态为Non-Busy）。驱动程序也可以通过调用KeRemoveEntryDeviceQueue删除一个特定入口或确定一个特定入口是否在当前队列。

使用带StartIo例程的补充设备队列

通过调用IoStartPacket和IoStartNextPacket，驱动程序的Dispatch和DpcForIsr（或CustomDpc）例程同步对其使用设备队列的StartIo例程的调用，这个设备队列是在驱动程序创建设备对象时由I/O管理器调用的。对于带有StartIo例程的端口驱动程序，IoStartPacket和IoStartNextPacket插入及删除端口驱动程序的共享设备控制器/适配器的设备队列中的IRP。如果端口驱动程序也建立补充设备队列以控制来自相关的较高层类驱动程序的请求，它必须对输入的IRP分类排进其补充设备队列，通常在其StartIo例程中。

在试图插入IRP到适当的队列中之前，端口驱动程序必须确定每个IRP属于哪个补充设备队列。一个指向目标设备对象的指针被一个IRP传送到驱动程序的Dispatch例程。驱动程序将存储该指针以在对到达IRP分类时使用。注意，传送给StartIo例程的设备对象指针是驱动程序自身的设备对象，代表设备控制器/适配器，所以它不能为该目的使用。

在IRP排队之后，驱动程序为其共享的控制器/适配器编程以执行该请求。因此，端口驱动程序可以基于“先来先服务”为所有设备处理到达请求，直到一个对KeInsertDeviceQueue的调用将一个IRP放入一个特定类驱动程序的设备队列中。

通过对所有将由StartIo处理的IRP均使用自己的设备队列，下层端口驱动程序通过共享设备（或总线）控制器/适配器串行操作所有附加设备。通过有时在一个单独设备队列中为每个支持的设备持有IRP，端口驱动程序在为所有通过其共享资源进行I/O的其他设备增加I/O流量时，禁止对于一个已忙着的设备处理IRP。

作为对调用来自端口驱动程序的Dispatch例程的IoStartPacket的响应，I/O管理器要么立即调用该驱动程序的StartIo例程，要么把IRP放入与端口驱动程序共享的控制器/适配器的设备对象相连的设备队列中。

端口驱动程序必须维护其自身的、关于每个不同类型的、它通过共享的设备控制器/适配器为之服务的设备的状态信息。

当用补充设备队列设计类/端口驱动程序时，记住以下几点：

§ 驱动程序不容易获得指向由其上的驱动程序创建的设备对象的指针，除了其设备栈中位于栈顶的设备对象。

通过设计，I/O管理器不再为获得这样一个指针提供支持例程。另外，位于驱动程序被装入处的指令不可能为较低层驱动程序获得较高层驱动程序的设备对象的指针，这些设备对象在任何较低层驱动程序加入到其设备时还没有创建。

虽然IoGetAttachedDeviceReference返回一个指向一个驱动程序栈中的最高层设备对象的指针，驱动程序应当仅仅使用该指针设计一个其栈的I/O请求的目的地址。驱动程序不应试图读或写该设备对象。

§ 驱动程序不能使用一个指向由任何其上的驱动程序创建的设备对象的指针，除了给它自己的设备栈的栈顶发请求。

在多处理器安全模式下，无法同步访问两个驱动程序之间的一个单独的设备对象（和其设备扩展）。驱动程序也不能对其他驱动程序当前正进行的I/O处理进行任何假定。

即使对于联系紧密的类/端口驱动程序，每个类驱动程序应仅仅使用指向端口驱动程序的设备对象的指针传送使用IoCallDriver的IRP。下层端口驱动程序必须维护其自身的、可能在该端口驱动程序的设备扩展中的、关于它为任何联系紧密的类驱动程序的设备处理的请求的状态。

通过驱动程序例程管理补充设备队列

任何为一组联系紧密的类驱动程序而在补充设备队列中排队的IRP的端口驱动程序也必须有效处理以下情况：

1．其Dispatch例程为一个特定设备在驱动程序创建的设备队列中为该设备插入IRP。

2．其他设备的IRP继续到达，排队等待驱动程序的带IoStartPacket的StartIo例程，并通过共享的设备控制器被处理。

3．设备控制器不会变空闲，但是每个驱动程序创建的设备队列中的IRP一有可能的话也必须排队等待驱动程序的Dispatch例程。

因此，无论何时端口驱动程序完成一个IRP，端口驱动程序的DpcForIsr必须尝试从一个特定设备的驱动程序内部设备队列传输一个IRP到共享适配器/控制器设备队列。如下：

1．DpcForIsr例程调用IoStartNextPacket以使StartIo例程开始处理共享设备控制器队列中下一个IRP。

2．DpcForIsr例程调用KeRemoveDeviceQueue以从其内部设备队列中移去下一个IRP，该设备作为将要完成一个IRP的代表。

3．如果KeRemoveDeviceQueue返回一个非NULL指针，DpcForIsr例程用刚从队列中取出的IRP调用IoStartPacket以使它排队等待共享的设备控制器/适配器。否则，对KeRemoveDeviceQueue的调用简单地重置设备队列对象状态为Not-Busy，并且DpcForIsr省略对IoStartPacket的调用。

4．这时，DpcForIsr用输入IRP调用IoCompleteRequest，端口驱动程序要么通过设置I/O状态块为一个错误，要么通过满足I/O请求刚好为这些IRP完成I/O处理。

注意，预先排序意味着DpcForIsr例程还必须确定哪个设备完成当前（输入）IRP以便有效管理内部IRP队列。

如果在其补充设备队列中取出IRP之前，端口驱动程序试图等待，直到其共享的控制器/适配器空闲，在它立即服务其他每个设备（这些设备当前的I/O任务实际上比较轻）期间，驱动程序可能 “饿死”有繁重I/O任务的设备。


中断服务例程

本章概述了一个驱动程序的标准中断服务例程的必要功能和建立一个ISR的需求。

本章包含以下信息：

8.1 ISR需求

8.1.1 ISR性能

8.1.2 附加的需求的驱动程序例程

8.1.3 建立一个ISR

8.2 ISR基本功能

8.3 ISR重叠I/O操作功能

1.1 ISR需求

一个产生中断的物理设备的所有驱动程序必须有一个ISR。中断服务例程由内核定义如下：

BOOLEAN

( *PKSERVICE_ROUTINE ) (

IN PKINTERRUPT Interrupt,

IN PVOID ServiceContext

);

ISR运行在DIRQL上，特别是在驱动程序用IoConnectInterrupt注册其ISR时说明的SynchronizeIrql层上。当驱动程序的ISR运行时，所有带一个中等或较低IRQL值的中断被当前处理器所屏蔽。

当然，另一个带有一个较高的系统分配的DIRQL的设备可以中断，或者一个高IRQL系统中断可以在任何时间发生在Windows NT/Windows 2000机器上。

记住以下情况：

§ 一个驱动程序的ISR是可中断的。

因为ISR运行在一个相对高的、系统分配的DIRQL上，因此在当前处理器上用一个中等或较低的IRQL屏蔽中断，ISR应尽可能快的返回控制。

在DIRQL运行一个ISR限制了该ISR可以调用的支持例程。关于IRQL管理的更多信息，见第16章。关于任何特定支持例程都能被调用的IRQL的说明信息，见在线DDK。
1.1.1 ISR性能

Windows NT/Windows 2000在驱动程序ISR方面完全不同于其他一些操作系统。在Windows NT/Windows 2000系统上，如果其ISR能尽可能快的返回控制，而不是试图保持对CPU的控制并在其ISR中做尽可能多的I/O处理，尤其是在SMP机器上，那么驱动程序会有更好的表现。

反之，ISR应从中断处停止设备，并保存一切必要的关于导致中断的操作的状态信息或该操作的环境。ISR应在常驻内存中保存这些信息或环境，这类内存通常位于设备扩展中。这时，它应对驱动程序的DpcForIsr例程或一个CustomDpc排队以完成这个位于一个较低的IRQL（通常是IRQL DISPATCH_LEVEL）上的操作。

ISR返回一个Boolean，表明驱动程序的设备是否产生中断。对于共享一个中断向量或DIRQL的设备的驱动程序，每个ISR一旦确定其设备不是中断源，就应返回FALSE。
1.1.2 附加的需求的驱动程序例程

所有拥有一个ISR的驱动程序也必须拥有DpcForIsr或CustomDpc例程。驱动程序也可以有附加的CustomDpc例程，以用来完成特定的中断驱动的I/O操作。

如果任何驱动程序例程与驱动程序的ISR分享数据、设备寄存器或环境信息，该驱动程序还必须有一个或多个SynchCritSection例程。

ISR运行在比DpcForIsr或CustomDpc例程更高的IRQL上。因此，在一台Windows NT/Windows 2000单处理器的机器上，ISR必须在DpcForIsr或CustomDpc例程执行之前返回。当然，在一台SMP机器上，ISR和DpcForIsr（或CustomDpc）可以并行运行。
1.1.3 建立一个ISR

驱动程序通过在设备启动时调用IoConnectInterrupt注册其ISR。驱动程序在处理一个PnP IRP_MN_START_DEVICE请求时作为最终步骤应连接中断。

每个拥有一个ISR的驱动程序必须为至少一个中断对象指针提供常驻内存。通常，该指针被存放在代表产生中断的物理设备的设备对象的设备扩展中。如果驱动程序创建一个控制器对象，中断对象指针可以存放在控制器扩展中，或者它可以存放在由驱动程序分配的，非页式缓冲池中。

如果下列两者之一为真，驱动程序必须为中断自旋锁提供存储空间以连接其所有设备的所有中断对象：

§ 驱动程序有一个单独ISR为两个或多个设备处理不同向量的中断。

§ 驱动程序的ISR处理一个在多个向量上中断的设备

驱动程序在注册其ISR之前必须通过调用KeInitializeSpinLock初始化中断自旋锁。驱动程序也必须为它处理的、与中断对象指针一样多的IRQ提供存储区间。

1.2 ISR基本功能

在入口处，ISR被赋予一个指向驱动程序的中断对象的指针和一个指向驱动程序在调用IoConnectIntertupt时建立的任意区域的ServiceContext指针。大多数驱动程序设置ServiceContext指针以代表产生中断的物理设备的设备对象或者该设备对象的设备扩展。在设备扩展中，驱动程序可以为驱动程序的DpcForIsr例程设置状态信息，DpcForIsr例程通常进行几乎所有的I/O处理以满足每个导致设备中断的请求。

在没有重叠设备I/O操作的驱动程序中，ISR应做以下工作：

1．确定中断是否为假。如果是的话，立即返回FALSE以使中断设备的ISR迅速被调用。否则，继续中断处理。

2．从中断处停止设备。

3．收集所有DpcForIsr（或CustomDpc）例程需要用来完成为当前操作的I/O处理的环境信息。

4．存放该环境信息于DpcForIsr或CustomDpc例程可访问的区域，通常在处理当前导致中断的I/O请求的目标设备对象的设备扩展中。

5．如果驱动程序有一个DpcForIsr例程，用指向当前IRP、目标设备对象和存储的环境信息的指针调用IoRequestDpc。IoRequestDpc对DpcForIsr例程排队以便IRQL一低于处理器上的DISPATCH_LEVEL就运行。

如果驱动程序有一个CustomDpc例程，用一个指向DPC对象（与CustomDpc例程连接）的指针和指向任何保存的环境（CustomDpc例程将需要它来完成操作）的指针调用KeInsertQueueDpc。通常，ISR也传送指向当前IRP的指针与目标设备对象。一旦IRQL低于处理器上的DISPATCH_LEVEL ，CustomDpc例程便运行。

6．返回TRUE以表明其设备产生中断。

通常，一个ISR不做实际的I/O处理以满足一个IRP。相反，它从中断处停止设备，建立必要的状态信息，然后将驱动程序的DpcForIsr 或 CustomDpc排队以便进行任何满足当前导致设备中断请求的必要I/O处理。

考虑以下实现方针：

§ 为了获得可能最短的间隔，一个ISR必须运行于DIRQL。

根据上述策略可以为机器中的所有设备增加I/O流量，因为运行于DIRQL屏蔽了所有系统已经分配了一个较低或中等IRQL值的中断。

驱动程序的中断对象的SynchronizeIrql（在驱动程序调用IoConnectInterrupt时被指定）确定驱动程序的ISR与SynchCritSection例程在其上运行的DIRQL。

当一个驱动程序的StartIo例程用驱动程序的SynchCritSection例程调用KeSynchronizeExecution时，该调用者也传送指向与ISR相连的中断对象的指针。因此，来自设备的中断被屏蔽在处理器运行的SynchCritSection例程上。与此同时，KeSynchronizeExecution持有与中断对象相连的中断自旋锁，以使ISR不能从另一个处理器访问设备寄存器或者设备扩展中共享的状态，直到驱动程序的SynchCritSection例程返回控制。

关于使用一个中断自旋锁的KeSynchronizeExecution的调用者的更多信息，见第16章的“使用自旋锁”。

1.3 ISR重叠I/O操作功能

当Windows NT/Windows 2000 SMP机器内的ISR和DpcForIsr（或CustomDpc）可以并行运行时，只有代表DpcForIsr或CustomDpc例程的DPC对象的一个实例可以为在任何给定的时刻执行而被排队。

如果相同DPC对象在DpcForIsr（或CustomDpc）例程运行之前通过一个ISR不止一次地排队，相关DpcForIsr或CustomDpc例程仅被调用一次。如果DPC对象在DpcForIsr（或CustomDpc）运行时排队，该例程的两个实例可以在Windows NT/Windows 2000 SMP机器内并行运行。

因此，任何在其设备上重叠I/O操作的驱动程序必须拥有DpcForIsr和/或CustomDpc例程，在这些例程被调用时它们可以完成多个IRP。对于驱动程序的ISR的基本要求与没有重叠I/O操作的设备驱动程序一样。见“ISR基本功能”。

当然，如果一个驱动程序重叠I/O操作，其ISR必须为DpcForIsr或CustomDpc例程设置附加状态。附加状态包括一个关于DPC例程需要完成而没有完成的请求的数量以及相关的环境信息。此外，如果ISR在DPC运行之前被调用以处理另一个中断，ISR必须小心不要覆盖为没有完成的请求存储的环境信息。

因为驱动程序的DPC例程与 ISR共享该状态，其DPC例程必须用一个系统提供的SynchCritSection例程调用KeSynchronizeExecution以访问代表每个DPC例程的共享状态。

关于这些例程的更多信息，见第9章“DpcForIsr和CustomDpc例程”。关于ISR和为ISR排队的DPC互动的更多信息，见第16章的“使用自旋锁”。


DpcForIsr例程和CustomDpc例程

本章概述了一个驱动程序的标准DpcForIsr例程与CustomDpc例程的必要功能。它包含以下信息：

9.1 DpcForIsr和CustomDpc例程需求

9.1.1 DpcForIsr例程与CustomDpc例程

9.1.2 DpcForIsr例程基本需求

9.1.3 CustomDpc例程基本需求

9.1.4 其他需要的驱动程序例程

9.1.5 建立DpcForIsr和CustomDpc例程

9.2 DpcForIsr和CustomDpc功能

9.2.1 使用缓冲I/O和满足设备控制IRP

9.2.2 使用直接I/O

9.2.3 实现一个DpcForIsr或CustomDpc例程的注意事项

1.1 DpcForIsr和CustomDpc例程需求

任何拥有一个ISR的驱动程序也必须有一个DpcForIsr或CustomDpc例程以完成其对中断驱动的I/O操作的处理。

一个DpcForIsr或CustomDpc例程运行在位于IRQL DISPATCH_LEVEL的任意线程环境中。

运行于IRQL DISPATCH_LEVEL限制了一个DpcForIsr或CustomDpc例程可以调用的支持例程。更多信息见第16章的“硬件优先级管理”。

关于任何特定支持例程可以被调用的IRQL的说明信息，见在线DDK。
1.1.1 DpcForIsr例程与CustomDpc例程

根据驱动程序的设计，它可以有一个单独DpcForIsr以完成所有中断驱动的I/O操作，有一个DpcForIsr和一组特定操作CustomDpc例程以从其ISR排队，或者有一组一个或多个CustomDpc例程。

大多数最低层设备驱动程序有一个单独DpcForIsr例程以便为每个IRP（它要求多个操作）在其各自设备上完成I/O处理。使用一个单独DpcForIsr以在一台一次做一个操作的设备上完成预先请求的，中断驱动的I/O操作相对容易一些。这样的驱动程序的ISR仅需要为每个中断驱动的I/O操作调用IoRequestDpc。

使用一个单独DpcForIsr以在一台可以进行并行操作的设备上完成重叠的、中断驱动的I/O操作，这可以通过精心设计实现，但相对要困难一些。另外，或替代排列一个DpcForIsr，一个ISR可以通过调用KeInsertQueueDpc排队一组特定操作地、驱动程序提供的CustomDpc例程。

例如，考虑一些包含在编写一系列驱动程序中的设计挑战。作为一个完全双向的设备的驱动程序，一系列驱动程序不能依赖一个在IRP排队等待一个StartIo例程的指令和一个多任务多处理器系统内的设备中断序列之间一对一的通信。此外，串行驱动程序必须处理超时请求和异步用户产生的请求以撤消先前的请求操作，清除缓冲区数据，等等。

因此，一个串行驱动程序可以保留为用户模式应用程序可以请求的读、写、清除和等待操作内部队列。它也可以保留相关数据或使用一些其他跟踪机制，例如一组为IRP在其内部队列中使用的标志。其ISR将用任何驱动程序分配和初始化的DPC对象调用KeInsertQueueDpc，它们每个与驱动程序提供的CustomDpc例程相关联。

一个驱动程序是否有一个单独DpcForIsr例程和/或一组取决于其下层设备属性的CustomDpc例程，以及它必须支持的一组I/O请求。
1.1.2 DpcForIsr例程基本需求

DpcForIsr例程由I/O管理器定义如下：

VOID

( * PIO_DPC_ROUTINE ) (

IN PKDPC Dpc,

IN PDEVICE_OBJECT DeviceObject,

IN PIRP Irp,

IN PVOID Context

);

在入口处，DpcForIsr被赋予指向当前IRP、目标设备对象和环境信息的指针，该指针在ISR对IoRequestDpc的调用中被传送。

通常，DpcForIsr例程负责以下工作：

§ 为目标设备对象完成由IRP请求的I/O处理，使用由ISR设置的环境

§ 确保下一个IRP尽可能快地被处理，通常通过调用IoStartNextPacket或IoStartNextPacketByKey以使驱动程序的StartIo例程可以在目标设备上启动下一个请求的I/O操作

如果一个驱动程序管理其自身的队列，其DpcForIsr例程必须有办法通知驱动程序将下一个IRP从队列中移出并为下一个请求开始设备I/O处理。

§ 设置输入IRP中的I/O状态块，并用这个刚完成的请求调用IoCompleteRequest

关于DpcForIsr例程功能的更详细的信息，见“DpcForIsr和CustomDpc例程”。
1.1.3 CustomDpc例程基本需求

CustomDpc例程由Kernel定义如下：

VOID

( *PKDEFERRED_ROUTINE ) (

IN PKDPC Dpc,

IN PVOID DeferredContext,

IN PVOID SystemArgument1,

IN PVOID SystemArgument2

);

在入口处，任何传送给CustomDpc例程的环境信息都是驱动程序确定的。当ISR调用KeInsertQueueDpc时，它可以传送指向任何它在SystemArgument1或SystemArgument2设置的相关环境的指针。驱动程序必须在它初始化其DPC对象时为其CustomDpc例程定义一个DeferredContext指针，以使其ISR也能为位于DeferredContext的CustomDpc例程设置环境。

一个ISR经常为每个中断驱动的I/O操作传送指向目标设备对象的设备扩展的指针和一个指向当前IRP的指针给其CustomDpc例程。每个CustomDpc例程负责完成一个与中断驱动的I/O操作相连的任务。

如果一个驱动程序没有DpcForIsr例程，其CustomDpc例程负责完成与DpcForIsr例程处理的相同的基本操作。在这种情况下，驱动程序的CustomDpc例程通常负责下列任务。

§ 为设备上产生中断的目标设备对象完成由当前IRP请求的I/O处理，使用由ISR设置的环境

§ 确保下一个IRP尽可能快地被处理，可能通过调用IoStartNextPacket或IoStartNextPacketByKey以使驱动程序的StartIo例程可以启动目标设备上的下一个请求的I/O操作。

如果一个驱动程序管理其自身的队列，CustomDpc例程必须有办法通知驱动程序将下一个IRP从队列中移出并为下一个请求开始设备I/O处理。

§ 设置当前IRP中的I/O状态块，并用这个刚完成的请求调用IoCompleteRequest。

更多信息见“DpcForIsr和CustomDpc例程”。
1.1.4 其他需要的驱动程序例程

一个最低层设备驱动程序的ISR为其DpcForIsr或CustomDpc例程存储环境信息。有时，DpcForIsr或CustomDpc例程负责重试操作，建立另一个DMA传输操作以满足一个给定的请求，或者否则的话，象ISR所做的那样访问相同的设备寄存器或环境域。

当驱动程序例程与ISR共享环境信息或设备寄存器时，该例程必须用一个驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution以便安全地访问共享的环境。

一个驱动程序必须有至少一个SynchCritSection例程，除非它在为下一个IRP启动设备I/O操作之前完成每个请求，从而串行化其IRP处理。这时，对于ISR和其他例程，例如其DpcForIsr或CustomDpc例程，几乎不可能并行地访问共享状态，如果驱动程序串行化IRP处理，用这种方式处理IRP以防止驱动程序为其设备获得可能是最好的I/O流量。

注意，串行化其I/O处理的驱动程序可以运行在Windows NT/Windows 2000 SMP机器上，而不比运行在单处理器的机器上快。
1.1.5 建立DpcForIsr和CustomDpc例程

在其AddDevice例程中，驱动程序必须注册其DpcForIsr并初始化每个CustomDpc例程，如下：

§ 用DpcForIsr的入口点和一个指向代表产生中断的物理设备的驱动程序创建的设备对象的指针调用IoInitializeDpcRequest。

§ 用一个指向驱动程序DPC对象的存储空间的指针、CustomDpc例程的入口点和一个指向环境信息的DeferredContext指针调用KeInitializeDpc，它在运行时被传送到CustomDpc例程。

如果驱动程序有一个CustomDpc例程，它必须为一个内核定义的DPC对象提供常驻内存。通常，驱动程序在代表产生中断的物理设备的设备对象的设备扩展中存储DPC对象。驱动程序的ISR往往访问设备扩展以存储Custom例程环境，并且ISR必须在它给CustomDpc例程排队时传送DPC对象指针给KeInsertQueueDpc。

当然，驱动程序可以在驱动程序分配的非页式缓冲池，或者在控制器扩展中（如果驱动程序设置控制器对象的话）为其DPC对象和中断驱动I/O操作的环境提供存储区间。

注意，CustomDpc例程与CostomTimerDpc例程在如何排队上是不同的，前者用KeInsertQueueDpc而后者用KeSetTimer或KeSetTimerEx。也就是说，对于一个CustomTimerDpc例程，驱动程序还必须为一个内核定义的定时器对象分配内存并初始化它，并且它不能用KeSetTimer或KeSetTimerEx传送SystemArgument1和SystemArgument2指针到其CustomTimerDpc例程。

更多信息，见第3章的“DPC对象”和第14章“IoTimer和CustomTimerDpc例程”。

1.2 DpcForIsr和CustomDpc功能

除了在“DpcForIsr例程基本需求”和“CustomDpc例程基本需求”概述的任务，所有DpcForIsr或CustomDpc例程要负责使用ISR提供的环境以完成一个或多个中断驱动的I/O操作。

换句话说，除了确保下一个设备I/O操作迅速被启动和完成当前IRP，由任何DpcForIsr或CustomDpc例程完成的工作取决于驱动程序的设计与其设备的属性。

例如，DpcForIsr或CustomDpc例程也可以做以下工作以便完成一个中断驱动的I/O操作：

§ 重试一个超时或失败的操作。

§ 调用IoAllocateErrorLogEntry，建立错误记录包以报告设备I/O错误，并调用IoWriteErrorLogEntry。

关于处理I/O错误的更多信息，见第16章的“错误记录和NTSTATUS值”。

§ 如果驱动程序使用缓冲I/O或当前IRP请求一个设备控制操作，在完成当前IRP之前传输从设备中读的数据到位于Irp->AssociatedIrp.SystemBuffer的系统缓冲区。

§ 如果驱动程序使用直接I/O，存储关于一个刚刚完成的部分传输操作的状态，计算下一个部分传输容量，并且使用一个系统提供的SynchCritSection例程以为下一个传输操作对设备编程。

如果其设备有传输能力限制，即使使用缓冲I/O的驱动程序也不得不分割一个传输请求。

§ 如果驱动程序使用基于包的DMA，在每个设备传输操作之后调用FlushAdapterBuffers，并在一系列部分传输被完成及完全传输请求被满足时调用FreeAdapterChannel或FreeMapRegisters。

如果请求的传输通过一个单独DMA操作仅仅被部分满足，DpcForIsr通常负责建立一个或多个DMA操作直到当前IRP的请求长度（字节数）被完全传输。

关于DMA使用的更多信息，见第3章的“适配器对象和DMA”；第11章“AdapterControl和ControllerControl例程”；以及第16章的“维护高速缓存一致性”。

§ 如果驱动程序使用PIO且当前IRP请求读，则在每个设备传输操作之后调用KeFlushIoBuffers。

如果一个请求的传输仅通过一个单独PIO操作被部分满足，DpcForIsr（或CustomDpc）通常负责建立一个或多个传输操作直到当前IRP的请求长度（字节数）已经被完全传输。

关于PIO使用的更多信息，见直接I/O的使用，也见第16章的“为DMA和PIO维护高速缓存一致性”。

§ 如果驱动程序有一个ControllerControl例程，在一个请求的操作完成时调用IoFreeController。

更多信息，见第3章的“控制器对象”。

注意，一个DpcForIsr（或CustomDpc）例程通常进行大部分驱动程序的设备I/O处理以满足IRP。该例程也负责在带驱动程序Dispatch例程的设备上对IRP排队。

记住下面的一般设计准则：

§ 任何DpcForIsr或CustomDpc例程一旦能安全地调用IoStartNextPacket，就应调用它。也就是说，驱动程序的StartIo例程或任何其他由StartIo例程引发的例程不可能导致资源冲突或竞态条件。

如果驱动程序管理其自身的IRP队列，其DpcForIsr或CustomDpc例程应注意一旦下一个IRP安全从队列中取出就通知驱动程序，并设置下一个请求的设备。

一个DpcForIsr或CustomDpc例程必须在DpcForIsr或CustomDpc返回控制之前调用IoStartNextPacket，或者否则的话，在下一个请求的设备I/O处理可以启动时通知适当的驱动程序例程。根据驱动程序及其设备，它可以在DpcForIsr或CustomDpc例程用IoCompleteRequest完成当前IRP之前进行，或者它可以在该例程完成当前IRP并返回控制前立即进行。
1.2.1 使用缓冲I/O和满足设备控制IRP

如第7章已提到的，使用缓冲I/O的驱动程序的StartIo例程通常比那些使用直接I/O的驱动程序的StartIo例程要简单，因为前者每次中断时传输相对较少的数据进出一个由I/O管理器分配的系统空间缓冲区。大多数使用缓冲I/O的设备驱动程序的DpcForIsr例程也比那些使用直接I/O的驱动程序的DpcForIsr例程要简单。

即使在I/O控制码用缓冲方法定义时，为直接I/O建立其设备对象的驱动程序为IRP_MJ_DEVICE_CONTROL请求使用缓冲I/O。

使用缓冲I/O的驱动程序传输数据进出由I/O管理器分配的，位于Irp->AssociatedIrp.SystemBuffer的系统空间缓冲区。

任何驱动程序的DpcForIsr或CustomDpc例程通常仅为一组驱动程序必须支持的公共I/O控制码完成I/O处理。特别地，DPC例程为具有以下特点的设备控制请求完成操作：

§ 改变物理设备的状态的请求

§ 需要返回物理设备的本身易失信息的请求

关于系统定义的，驱动程序必须支持的特定设备类型I/O控制码的更多信息，见在线DDK。
1.2.2 直接I/O使用

使用直接I/O传输数据的驱动程序将不得不把一个大的传输请求分割成几个与其设备，或者对于一个从属DMA设备，是系统DMA控制器的能力相符的传输操作。因此，这样一个驱动程序的DpcForIsr例程可能为多个传输操作对设备重新编程以满足给定的读/写请求并完成该IRP。

因为DpcForIsr或CustomDpc例程运行在IRQL DISPATCH_LEVEL处。它必须用一个SynchCritSection例程调用KeSynchronizeExecution以更新任何与驱动程序的ISR共享的状态，例如设备寄存器。

使用直接I/O的驱动程序可以被请求以传输大量数据。IRP中由MDL描述的，传送给这类驱动程序的DispatchRead或DispatchWrite例程的锁定缓冲区可以大于系统定义的页大小。另外，位于Irp->MdlAddress的MDL可以描述一个由系统内存中几个不连续的物理页面支持的缓冲区。

此外，这类驱动程序传送请求数据所通过的设备有它自身的传输限制。例如，由于磁盘控制器的限制，系统“AT”磁盘驱动程序必须分割所有大于256扇区的传输请求。

当这类设备驱动程序的StartIo、AdapterControl或ControllerControl例程被一个传输超出物理设备所能处理数量的数据的请求调用时，它可以建立其设备只传输该设备允许量的数据。在驱动程序的ISR处理一个最大设备传输中断之后，DpcForIsr（或CustomDpc）必须再次设置设备，以传输满足该IRP所需的剩余数据。
1.2.3 实现一个DpcForIsr或CustomDpc例程的注意事项

在实现一个DpcForIsr或CustomDpc例程时记住以下要点：

§ 一个DpcForIsr或CustomDpc例程必须同步它对物理设备和任何驱动程序在设备扩展中维护的，与驱动程序的其他访问相同设备或内存位置的例程共享的状态信息或资源的访问。

如果DpcForIsr或CustomDpc与ISR共享设备或状态，它必须用一个驱动程序提供的SynchCritSection例程调用SynchronizeExection以对设备编程或访问共享的状态。

如果DpcForIsr或CustomDpc与ISR之外的其他例程共享状态或资源，例如一个互锁队列或一个定时器对象，它必须用一个驱动程序初始化的、由驱动程序提供常驻内存的执行自旋锁来保护共享的状态或资源。

§ DpcForIsr或CustomDpc例程运行于IRQL DISPATCH_LEVEL，这限制它们所能调用的支持例程。

例如， DpcForIsr或CustomDpc例程既不能访问，也不能分配可分页内存，并且它不能等待一个调度者对象。另一方面，DpcForIsr或CustomDpc例程可以用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel获得并释放驱动程序执行自旋锁，KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel可以比KeAcquireSpinLock与KeReleaseSpinLock更快运行。

§ DpcForIsr或CustomDpc例程负责启动设备上的下一个I/O操作。

对于使用直接I/O的最低层物理设备驱动程序，其任务包括在驱动程序调用IoStartNextPacket之前使用一个SynchCritSection例程对传输大量数据以满足当前IRP的设备编程。

§ 如果一个最低层物理设备驱动程序在其DriverEntry例程中建立了一个控制器对象以使通过控制器对附加设备的I/O操作同步，其DpcForIsr或CustomDpc例程负责在它完成当前IRP之前用IoFreeControl释放控制器对象，并返回控制。

§ 如果驱动程序使用DMA并且其AdapterControl例程返回KeepObject或DeallocateObjectKeepRegisters（因此为附加传输操作而保留系统DMA控制器通道或总线控制器适配器），驱动程序的DpcForIsr或CustomDpc例程负责在它完成当前IRP之前用FreeAdapterChannel或FreeMapRegisters释放适配器对象或映射寄存器，并返回控制。

§ DpcForIsr或CustomDpc例程通常负责记录一切在给定请求处理期间发生的设备错误，如果必要（也可能）的话重试当前请求，并设置I/O状态块以及用当前IRP调用IoCompleteRequest。

§ 在其设备上重叠操作的驱动程序的DpcForIsr或CustomDpc不能依靠输入到StartIo例程的请求和对IoRequestDpc或KeInsertQueueDpc的ISR调用之间的一对一通信。换句话说，为完成仅仅那个IRP，这类驱动程序的DpcForIsr或CustomDpc不必使用指向IRP和ISR提供的环境的指针，也不必使用目标设备对象中的CurrentIrp指针。

在任何给定的情况下，相同DPC对象不能两次排队。如果这类驱动程序的ISR在相关DpcForIsr或CustomDpc执行之前不止一次调用IoRequestDpc或KeInsertQueueDpc，一旦一个处理器上的IRQL降到低于DISPATCH_LEVEL，仅有一个该DPC例程的实例运行。另一方面，如果ISR在相关DpcForIsr或CustomDpc运行于另一个处理器上时调用IoRequestDpc或KeInsertQueueDpc，该DPC例程的两个实例可以并行地运行。

因此，任何在其设备上重叠中断驱动的I/O操作的驱动程序必须有以下：

§ 可以完成一些驱动程序维护其个数的，调用时未完成的请求的DpcForIsr或CustomDpc

§ 从不覆盖为一个中断驱动的I/O操作存储的环境，直到DpcForIsr或CustomDpc已使用该环境信息并为存储的环境完成IRP为止的ISR

§ 连接代表DpcForIsr或CustomDpc例程的环境区的SynchCritSection例程。

更多信息，见第10章“SynchCritSection例程”和第11章“AdapterControl和ControllerControl例程”。关于本节中提到的任何特定支持例程的细节，见在线DDK。


SynchCritSection例程

本章概述了设备驱动程序的SynchCritSection例程的必需功能。本章包含以下信息：

10.1 SynchCritSection例程需求

10.1.1 使用SynchCritSection例程

10.1.2 运行SynchCritSection例程

10.1.3 SynchCritSection例程性能需求

10.2 SynchCritSection例程功能

10.2.1 为I/O操作对设备编程

10.2.2 维护有关中断驱动I/O操作的状态

1.1 SynchCritSection例程需求

任何带有与其ISR共享状态、资源或设备端口/寄存器的例程的设备驱动程序必须拥有SynchCritSection例程。大多数设备驱动程序拥有至少两个SynchCritSection例程，并且许多驱动程序拥有更多的该例程。

SynchCritSection例程由内核定义如下：

BOOLEAN

( *PKSYNCHRONIZE_ROUTINE ) (

IN PVOID SynchronizeContext

);

SynchronizeContext参数提供一个指向由驱动程序建立的环境域的指针。

象ISR一样，SynchCritSection例程运行在驱动程序用IoConnectInterrupt注册其ISR时指定的SynchronizeIrql层上。

运行于DIRQL限制了SynchCritSection例程可以调用的支持例程。

关于管理驱动程序中的IRQL的更多信息，见第16章的“硬件优先级管理”。关于任何特定支持例程可以在其上被调用的IRQL的说明信息，见在线DDK。
1.1.1 使用SynchCritSection例程

就象其名字所暗示的，SynchCritSection例程是用来访问一个共享区域的代码中的临界区，它对该区域的访问必须与来自一个ISR的可能访问同步。

对于设备驱动程序，StartIo与DpcForIsr或CustomDpc例程经常必须访问一些相同内存或设备端口/寄存器，就象驱动程序的ISR。Dispatch、AdapterControl、ControllerControl或驱动程序提供的定时器例程也可以访问驱动程序维护的状态和/或与其ISR共享的设备端口/寄存器，这取决于驱动程序的设备或其设计。

如果任何非ISR例程试图直接访问这样一个共享区域，设备可以在其例程对设备编程或更新状态信息时中断。换句话说，无论一个设备中断发生在何时，ISR可以在Dispatch、StartIo、DpcForIsr、CustomDpc、AdapterControl、ControllerControl、IoTimer或CustomTimerDpc例程中改变状态或设备寄存器。
1.1.2 运行SynchCritSection例程

一个设备驱动程序中的所有非ISR例程必须同步访问与其ISR共享的区域，通过用一个指向与ISR相连的中断对象的指针和一个驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution。

这一调用通过与驱动程序的中断对象相连的自旋锁保护共享的数据或设备端口/寄存器。控制自旋锁屏蔽了SynchCritSection例程处理器上较低或中等IRQL的设备中断，并且防止共享区域被来自Windows NT/Windows 2000 SMP机器中的ISR同时访问。
1.1.3 SynchCritSection例程性能需求

象ISR一样，SynchCritSection例程必须尽可能快地执行，仅进行必要的设置寄存器或更新环境数据，在返回控制到调用KeSynChronizeExecution的例程之前。

记住以下设计与实现方针：

§ 因为它运行于DIRQL，任何SynchCritSection例程必须尽可能快地返回控制。

因为KeSynchronizeExection在其SynCritSection例程运行时持有一个设备驱动程序的中断自旋锁，所以驱动程序的ISR不能进行任何工作，直到SynchCritSection例程返回控制。

每个设备驱动程序应使位于DIRQL的每个设备上处理的IRP的运行时间最小。

在从KeSynchronizeExecution返回后，调用者的IRQL被恢复。

1.2 SynchCritSection例程功能

驱动程序为以下两个基本目的使用SynchCritSection例程：

§ 为I/O操作对一个与ISR共享的物理设备编程

§ 访问与来自其他驱动程序例程的ISR共享的状态信息
1.2.1 为I/O操作对设备编程

对于任何输入IRP，在它们为I/O操作对其设备编程之前，设备驱动程序在其Dispatch例程（或可能是驱动程序线程）中的IRQL PASSIVE_LEVEL上或者IRQL DISPATCH_LEVEL上进行尽可能多的I/O处理，如同其StartIo例程那样。

另外，设备驱动程序的StartIo例程必须延迟其用SynchCritSection例程对KeSynchronizeExecution的调用，对以下标准驱动程序例程的设备编程：

§ 如果一个Windows 2000驱动程序建立一个控制器对象来代表一个带附加设备的设备控制器，其StartIo例程不能对设备控制器编程，该设备控制器可能已被用于一个在另一台附加设备上的I/O操作，直到物理控制器不忙。

1．驱动程序的StartIo例程必须用其ControllerControl例程调用IoAllocateController以同步物理控制器的操作。

2．只有这时它的ControllerControl例程可以使用SynchCritSection例程调用KeSynchronizeExecution，这里SynchCritSection例程为请求的从/到目标设备的操作重新对物理设备编程。

如果一个通过驱动程序的控制器传输操作不能满足一个特定IRP，驱动程序的DpcForIsr例程也必须用相同的SynchCritSection例程调用KeSynchronizeExecution以便为每个满足当前IRP的必要附加传输对物理控制器编程。

§ 如果一个驱动程序为系统或基于包的总线控制DMA建立了适配器对象，其StartIo例程（或者可能是驱动程序创建的线程或ControllerControl例程）不能对一个从属设备编程，直到可能忙于为另一从属设备传输数据的系统DMA控制器空闲，或者直到系统已经为驱动程序的总线控制器适配器分配DMA映射寄存器。

1．驱动程序的StartIo例程必须用其AdapterControl例程调用AllocateAdapterChannel以便同步通过系统DMA控制器或驱动程序的总线控制DMA适配器的操作。

2．这时仅其AdapterControl例程可以用一个为DMA传输对驱动程序的设备编程的SynchCritSection例程调用KeSynchronizeExecution。

如果一个驱动程序的设备/适配器上的DMA传输操作不能满足一个特定IRP，驱动程序DpcForIsr例程也必须用相同SynchCritSection例程调用KeSynchronizeExecution以便为每个满足当前IRP必要的附加DMA传输对设备/适配器编程。

关于分级DMA操作的更多信息，见第3章“适配器对象和DMA”。

为设计、实现和调用为I/O操作对设备编程的SynchCritSection例程，考虑以下一般方针：

§ 在它调用KeSynchronizeExecution以拥有一个驱动程序提供的、为I/O操作对设备编程的SynchCritSection例程之前，设备驱动程序为一个IRP进行所有预处理，为其他也处理设备I/O请求的驱动程序例程设置任何必要状态，并获得所有必要资源，例如对设备控制器、系统DMA通道或者DMA映射寄存器组的访问。

§ 一个为I/O操作对设备编程的SynchCritSection例程必须尽可能快的返回控制。为此，SynchCritSection例程应仅进行必要的设备I/O设置。

§ 如果必要，设备驱动程序可以有一组SynchCritSection例程对设备编程。例如，建立读请求明显不同于某些设备控制请求的设备的驱动程序，它可能有不同的SynchCritSection例程为每种类型的请求对其设备编程。

§ 驱动程序不应当拥有一个单独的、大的、通用目的的SynchCritSection例程，这样的SynchCritSection例程会用一个Switch语句或许多嵌套的if..then..else语句决定它将执行什么来为I/O操作对设备编程，和/或更新什么状态信息。另一方面，驱动程序也应避免拥有一组大量SynchCritSection例程，每个仅负责对一个单独的设备寄存器编程。

所有SynchCritSection例程必须尽可能快地返回控制，因为运行任何SynchCritSection例程可防止驱动程序的ISR获得任何并行运行的工作。
1.2.2 维护有关中断驱动I/O操作的状态

其DpcForIsr、CustomDpc、ControllerControl、AdatperControl、IoTimer或CustomTimerDpc例程与ISR共享设备扩展中状态的驱动程序也使用各种技术使其SynchCritSection例程尽可能快地运行。

例如，设备驱动程序可以使用一些以下方案中描述的技术以使其SynchCritSection例程的执行间隔最小。考虑一个在其设备扩展中维护一个定时器计数器的设备驱动程序以检查设备I/O操作是否超时以及在其设备上是否可以进行重叠I/O操作。几种驱动程序例程可能访问定时器计数器，使用SynchCritSection例程，如下：

§ 其StartIo例程为当前I/O请求用一个设备决定的超时值初始化定时器计数器。驱动程序给该设备超时值增加一秒以防其IoTimer例程恰好返回控制。

§ 当为请求的操作被调用以处理一个来自设备的中断时，其ISR必须使该定时器计数器减一。

§ 其IoTimer例程每秒被调用一次以读取定时器计数器的值，并决定ISR是否已经将它重置为-1。如果不是，IoTimer例程通过用SynchCritSection例程调用KeSynchronizeExecution减小计数器。

如果计数器变成零，表明设备上的请求超时，SynchCritSection_1例程调用SynchCritSection_2例程编程重置设备。如果计数器减一，IoTimer例程仅仅返回。

§ 如果驱动程序的DpcForIsr必须对设备重新编程以开始另一个部分传输操作，它必须重新初始化定时器计数器，就象StartIo所做的那样。

DpcForIsr也必须用SynchCritSection_2例程调用KeSynchronizeExecution，或者可能是SynchCritSection_3例程，为另一个传输操作对设备编程。

在这种方案中，驱动程序有多于一个SynchCritSection例程，都有各自特定的任务，一个维护其定时器计数器，另一个对设备编程。因此，每个SynchCritSection例程可以尽可能快地返回控制，因为每个只完成一个单独特定的任务，而不做其他。

也要注意，这类驱动程序有一个单独的SynchCritSection_1例程与驱动程序的ISR一起维护定时器计数器的状态。因此，几个SynchCritSection例程与ISR之间不会争用访问定时器计数器。

为设计和实现维护状态的SynchCritSection例程，考虑以下一般方针：

§ 如果一个设备驱动程序维护关于设备（或控制器）扩展中的中断驱动I/O操作的状态，放置所有状态信息于设备扩展的一个子域中。这使得其他互相共享资源的非ISR例程可以访问“它们的”设备扩展区域，而不可能与ISR和SynchCritSection例程争用。

例如，一个驱动程序管理的互锁队列不能被一个ISR或SynchCritSection例程访问，因为它们两者都不能调用一个来自DIRQL的ExInterlocked..List例程，在其他驱动程序例程同步访问来自IRQL DISPATCH_LEVEL的队列。这类队列头与其保护自旋锁对其他驱动程序例程是可访问的，如果其ISR和SynchCritSection例程总是发现这些状态的话，它们在设备扩展（不包括任何被其他驱动程序例程访问的资源）中一个定义的子域内编辑这些状态。

另一方面，如果ISR或SynchCritSection例程访问分散于设备扩展的状态变量，则争用驱动程序互锁队列或其他共享资源的访问冲突更为常见。

§ 给每个保存状态信息的SynchCritSection例程各自不同的状态信息。也就是说，避免编写维护重复状态信息的SynchCritSection例程。

这可以防止试图同时访问相同状态的SynchCritSection例程（与ISR）之间的冲突，以及可能的竞态条件。

这也确保每个SynchCritSection例程尽可能快地返回控制，因为一个SynchCritSection例程从不用等待另一个更新相同状态信息的SynchCritSection例程以便返回控制。

§ 避免编写一个单独的、庞大的、一般目的的、为确定做什么而做了比实际工作更多的条件测试的SynchCritSection例程。当然，另一方面，也要避免有很多从不执行一个条件语句的SynchCritSection例程，因为每个只更改一字节的状态信息。

所有SynchCritSection例程必须尽可能快地返回控制，因为运行任何SynchCritSection例程都可防止驱动程序的ISR获得正并行运行的工作。


AdapterControl和ControllerControl例程

本章包含以下信息：

11.1 驱动程序控制例程

11.1.1 驱动程序控制例程语法和参数

11.1.2设计有DriverControl例程驱动程序

11.1.3 从DriverControl例程返回

11.1.4 存储需求

11.1.5 建立AdapterControl和ControllerControl例程

11.2 AdapterControl例程需求

11.3 ControllerControl例程需求

本章概述了驱动程序的标准AdapterControl和ControllerControl例程的必要功能。AdapterControl例程由WDM和Microsoft Windows 2000驱动程序使用。ControllerControl例程仅由Windows 2000驱动程序使用而不被WDM所支持。

关于由执行发散/收集DMA的驱动程序使用的AdapterListControl例程的更多信息，见第3章的“执行发散/收集DMA”和《Windows 2000驱动程序开发指南》第2卷第1部分第3章的GetScatterGatherList。

关于支持系统与总线控制DMA设备驱动程序以及适配器对象的更多信息，见第3章的“适配器对象和DMA”。关于控制器对象的更多信息，也见第3章的“控制器对象”。

1.1 驱动程序控制例程

AdapterControl和ControllerControl例程运行在一个位于IRQL DISPATCH_LEVEL上的专有线程环境中。

运行于IRQL DISPATCH_LEVEL限制了AdapterControl或ControllerControl例程可以调用的支持例程。

更多信息，见第16章的“硬件优先级管理”。关于任何特定支持例程可以被调用的IRQL处的详细信息，见在线DDK中该例程相关文档。
1.1.1 驱动程序控制例程语法和参数

AdapterControl和ControllerControl例程由I/O管理器定义如下：

IO_ALLOCATION_ACTION

( *PDRIVER_CONTROL ) (

IN PDEVICE_OBJECT DeviceObject,

IN PIRP Irp,

IN PVOID MapRegisterBase,

IN PVOID Context

);

每个驱动程序控制例程用四个参数调用：

§ 一个指向当前I/O请求的目标的设备对象（DeviceObject）的指针

§ 一个指向当前IRP的指针，如果驱动程序不管理其自身的IRP队列的话

§ 一个指向MapRegisterBase的指针，仅在AdapterControl例程中使用（ControllerControl例程应将其认为是一个系统保留值。）

§ 一个指向驱动程序定义的环境域的指针
1.1.2 从DriverControl例程返回

AdapterControl或ControllerControl例程必须返回一个IO_ALLOCATION_ACTION类型的值，定义如下：

Typedef enum _IO_ALLOCATION_ACTION {

KeepObject,

DeallocateObject,

DeallocateObjectKeepRegisters

} IO_ALLOCATION_ACTION, *PIO_ALLOCATION_ACTION;

DeallocateObjectKeepRegisters值仅对使用基于包的总线控制器DMA的驱动程序有意义。

如果ControllerControl例程完成一个IRP或者如果它可以建立一个操作，例如一次磁盘搜索，对于一个可能被对另一个设备对象的操作重叠的目标设备对象，它应返回DeallocateObject。否则，它应返回KeepObject。
1.1.3 设计有DriverControl例程驱动程序

大多数DMA设备的驱动程序必须有一个AdapterControl例程：

§ 一个连接系统DMA控制器的DMA设备的所有驱动程序必须有一个AdapterControl例程。

§ 一个总线控制DMA设备的任何驱动程序必须有一个AdapterControl例程，除非该驱动程序设计者决定独占DMA支持的系统公用缓冲区。

根据驱动程序编写者的意愿，一个Windows 2000驱动程序可以有一个ControllerControl例程以便通过一个单独物理控制器或适配器同步其对相似设备的操作。

I/O管理器为AdapterControl与ControllerControl例程定义了一个单独类型（DRIVER_CONTROL），因为当驱动程序需要同步访问一个可能已忙的物理设备时，每个例程被间接地调用，通常是由StartIo例程调用：

§ 当驱动程序调用AllocateAdapterChannel时，如果系统DMA控制器或总线控制器适配器对于DMA操作可用，其AdapterControl例程立即运行。否则，AdapterControl例程排队直到DMA控制器或总线控制器适配器空闲。

§ 当驱动程序调用IoAllocateController时，如果由控制器对象代表的硬件对I/O操作可用，其ControllerControl例程立即运行。否则，ControllerControl例程排队直到控制器空闲。

对于ControllerControl例程，在DRIVER_CONTROL声明中所示的输入PVOID MapRegisterBase是一个系统保留值。
1.1.4 存储需求

如果有AdapterControl例程，驱动程序必须为以下内容提供常驻内存：

§ 将要在DMA操作中使用的环境信息

§ 一个由IoGetDmaAdapter返回的AdapterControl指针

§ 一个ULONG类型变量，保存系统确定的，任何给定的DMA传输请求可行的最大值NumberOfMapRegisters

驱动程序可以在设备扩展、控制器扩展或由驱动程序分配的非页式缓冲池中提供必要存储空间。

如果有ControllerControl例程，Windows 2000驱动程序必须为以下内容提供常驻内存：

§ 一个由IoCreateController返回的ControllerObject指针

驱动程序可以在设备扩展或由驱动程序分配的非页式缓冲池中提供必要存储空间。通常，使用控制器对象的驱动程序在每个代表一个由控制器对象代表的硬件所控制的物理或逻辑设备的设备对象的设备扩展中存储ControllerObject指针。

一个不能指定名字的控制器对象不能成为一个I/O请求的目标。它代表的硬件通常控制一组同类设备，这些设备是I/O请求的实际目标。
1.1.5 建立AdapterControl和ControllerControl例程

设备启动期间，最低层驱动程序必须为它们的AdapterControl或ControllerControl例程建立相连的内核定义对象。

对于AdapterControl例程，一个PnP IRP_MN_START_DEVICE请求的驱动程序Dispatch例程必须做以下工作：

1． 通过填入一个DEVICE_DESCRIPTION结构和调用IoGetDmaAdapter为设备的DMA能力建立适配器对象。

2． 存储由IoGetDmaAdapter返回的AdapterObject指针和NumberOfMapRegisters。

由IoGetDmaAdapter返回的平台特定的最大NumberOfMapRegisters或者驱动程序的设 备的传输能力决定驱动程序是否必须分割一个所给的大的传输请求，并在其设备上执行多个DMA操作以满足IRP。

返回的AdapterObject指针、驱动程序的AdapterControl例程的入口点、代表当前IRP的目标设备的DeviceObject指针、一个指向一个已为AdapterControl例程设置的区域的环境指针和一个NumberOfMapRegisters值（它可以小于较小传输请求的最大可能量）必须在对AllocateAdapterChannel的调用中传送。通常，一个驱动程序的StartIo（或可能是ControllerControl）例程在它调用AllocateAdapterChannel之前建立位于Context的区域。

对于一个Windows 2000驱动程序的ControllerControl例程，一个PnP IRP_MN_START_DEVICE请求的Dispatch例程必须做以下工作：

1．调用IoCreateController建立控制器对象，为系统由非页式缓冲池分配并初始化为零的控制器扩展指定驱动程序确定的Size。

2．存储由IoCreateController返回的ControllerObject指针，通常在每个表示一个由控制器对象表示的硬件所控制的物理或逻辑设备的设备对象的设备扩展中。

3．设置并初始化驱动程序确定的ControllerObject->ControllerExtension的内容。

返回的ControllerObject指针，驱动程序的ControllerControl例程的入口点，表示当前IRP的目标设备的DeviceObject指针，以及一个指向已为ControllerControl例程建立的区域的Context指针必须在驱动程序对IoAllocateController的调用中传送。通常，一个驱动程序的StartIo例程在它调用IoAllocateController之前建立位于Context的区域。

1.2 AdapterControl例程需求

至少，一个AdapterControl例程必须做以下工作：

1. 存储输入MapRegisterBase值连同一切驱动程序需要用来为当前IRP执行一个或多个DMA传输操作的其他环境信息。当每个DMA传输完成时，驱动程序必须传送MapRegisterBase值给FlushAdapterBuffers

2. 返回适当的IO_ALLOCATION_ACTION值：

§ 如果设备是一个从属设备，因此驱动程序使用系统DMA，则为KeepObject。

§ 如果设备是一个总线控制器，因此驱动程序使用基于包的总线控制器DMA，则为DeallocateObjectKeepRegisters。

根据驱动程序的设计，其AdapterControl例程在返回控制之前也可以做以下工作：

1．为设备上的传输确定开始位置。

2．计算可能的传输容量，已知传输开始位置对其设备的一切限制。

通常，调用AllocateAdapterChannel的例程负责确定是否必须分割成部分传输，它的起因是每个DMA传输操作可行的NumberOfMapRegisters上的平台特定的限制，就象在前面章节提到的以及在“分割传输请求”中详述的。

3．在设备（或控制器）扩展中设置一切驱动程序维护关于每个传输请求的状态。

例如，一个AdapterControl例程可能用一个驱动程序超时DMA传输 的CustomDpc例程入口点调用KeSetTimer。关于CustomTimerDpc例程的更多信息，见第14章。

4．用位于Irp->MdlAddress处传送的MDL指针调用MmGetMdlVirtualAddress以获得一个传输开始的索引，适合于传送给MapTransfer。

5．调用MapTransfer以设置系统DMA控制器或或者获得一个总线控制器设备的物理对逻辑地址映射。

6．通过用SynchCritSection例程调用KeSynchronizeExecution为一个传输操作对驱动程序的设备编程。

如果一个传输请求需要驱动程序执行一系列部分传输操作以满足当前IRP，驱动程序的DpcForIsr例程通常负责为后继传输操作重新对设备编程。对于每个到达的传输IRP，一个AdapterControl例程仅被调用一次。

完成当前IRP的驱动程序例程（通常为DpcForIsr）也通过分别调用FreeAdapterChannel或FreeMapRegisters负责释放系统DMA控制器或总线控制器适配器。当其最后一次部分传输操作被完成时，该驱动程序例程一旦可能就应进行适当的调用，以便从属DMA设备的驱动程序可以分配系统DMA控制器或者一个总线控制器驱动程序可以开始迅速处理下一个传输IRP。

1.3 ControllerControl例程需求

如同它的名字所暗示的，一个ControllerControl例程与一个内核定义的控制器对象相连。当ControllerControl例程执行时，由控制器对象代表的硬件被释放，并且控制器扩展通常不能由另一个驱动程序例程访问，除非控制器扩展包含与驱动程序的ISR共享的内容。

通常，一个ControllerControl例程至少做以下工作：

1．更新或初始化驱动程序在目标设备对象的设备扩展与控制器扩展中维护的任何内容

如果驱动程序使用DMA，其ControllerControl例程通常负责确定一个给定的传输请求由于系统或设备赋予的每个DMA传输规模限制是否必须被分割成部分传输。在这些情况下，如果驱动程序有一个AdapterControl例程，ControllerControl例程也负责调用AllocateAdapterChannel。

如果驱动程序使用PIO且其硬件需要的话，其ControllerControl例程也负责分割大的传输请求为部分传输容量，并且用位于Irp->MdlAddress的MDL调用MmGetSystemAddressForMdlSafe。

2．为请求的I/O操作对其硬件编程

如果设备或控制器扩展可以从ISR访问，ControllerControl例程必须用一个SynchCritSection例程调用KeSynchronizeExecution以便对硬件编程或建立与ISR共享的环境。

如果驱动程序有Cancel例程，其ControllerControl例程也必须检查Irp->Cancel域以确定当前IRP是否应被撤消，并进行以下两者之一：

如果Irp->Cancel被设为TRUE，ControllerControl例程必须做以下工作：

1．设置IRP的I/O状态块中的Status为STATUS_CANCELLED，Information为零。

2．调用IoFreeController释放控制器对象，以便下一个设备操作可以迅速启动。

3．调用IoStartNextPacket或将下一个IRP从队列中取出，如果驱动程序管理其自身的队列。

4．用IoCompleteRequest完成撤消的IRP，并返回控制。

如果Irp->Cancel没有设成TRUE，ControllerControl例程则必须做以下工作：

1．调用IoSetCancelRoutine以重置IRP的Cancel例程入口点为NULL。如果驱动程序使用设备对象中I/O管理器提供的设备队列，为该调用获得撤消自旋锁。

2．用一个驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution以便为请求的I/O操作对硬件编程。

关于处理可撤消IRP的更多信息，见第12章“Cancel例程”。

对于除物理控制器/适配器附属的不同设备上的重叠操作之外的大多数中断驱动I/O操作，ControllerControl例程应返回KeepObject，因为DpcForIsr或CustomDpc例程完成操作与IRP。

一旦满足当前请求的I/O操作被进行，将完成IRP的例程应调用IoFreeController和IoStartNextPacket，以使下一个请求尽可能快地被处理。

如果ControllerControl例程自己完成一个IRP或者它设置一个对目标设备对象（磁盘）的操作，例如一个磁盘搜索，这个操作可能被一个对另一个设备对象的操作所覆盖，ControllerControl例程应返回DeallocateObject。


Cancel例程

这一章总结了驱动程序标准Cancel例程所需的功能，提出了管理其他驱动程序例程中可撤消IRP的策略。

本章包括下列内容：

12.1 Cancel例程需求

12.2 处理可撤消IRP

12.2.1 对传递IRP的驱动程序例程中可撤消IRP的处理

12.2.2 对处理请求的驱动程序例程中可撤消IRP的处理

12.2.3 使用系统撤消自旋锁

12.2.4 使用驱动程序提供的自旋锁和Cancel例程

12.3 Cancel例程功能

12.3.1 带有StartIo例程的驱动程序中的Cancel例程

12.3.2 没有StartIo例程的驱动程序中的Cancel例程

12.4 处理可撤消IRP时的注意事项

1.1 Cancel例程需求

I/O管理器 对Cancel例程的定义如下：

VOID

(*PDRIVER_CANCEL)(

IN PDEVICE_OBJECT DeviceObject

IN PIRP Irp

);

Cancel例程的目的是尽快完成IRP，以确保驱动程序的数据结构是一致的。

Cancel例程在将一个IRP的I/O状态块中的Status设为STATUS_CANCELLED、 将Information设为0后，为它的目标设备对象完成此IRP。

Cancel例程总是用代表它的系统撤消自旋锁调用。在Cancel例程调用IoReleaseCancelSpinLock之前，它都运行在值为DISPATCH_LEVEL的IRQL上，而且它可运行在专用线程环境中。

运行在IRQL为DISPATCH_LEVEL和专用线程环境中限定了Cancel例程可以调用的支持例程的集合。

如果想对管理NT驱动程序中的IRQL有更多的了解，请看本书第16章中管理硬件优先级部分。如果想要了解任何特定支持例程都可被在其上调用的IRQL的具体内容，请看在线DDK。

驱动程序中的Cancel例程

其中IRP可以在不确定的时间内被保持为未决状态的驱动程序，必须有一个或多个Cancel例程。键盘驱动程序就是这样的一个例子：驱动程序可能不确定地等待用户按下一个键。相反，如果驱动程序能确定5分钟内它不能排队比能完成的更多的IRP，它可能就不需要Cancel例程。

例如，假设一个用户模式的线程发出了一个I/O请求，这个请求被最高层设备驱动程序的Dispatch例程排队，并且在这个IRP排队期间，请求线程被终止了。终止线程被排队的IRP应该被撤消，因此驱动程序必须在每个排队的IRP中建立一个驱动程序提供的Cancel例程。

创建相关IRP的驱动程序必须在主IRP（master IRP）被撤消时，撤消其他相关的IRP。因为相关IRP与请求线程无关，所以主IRP的Cancel例程应该在主IRP被撤消时，撤消所有与其相关的IRP。

一个驱动程序所具有的Cancel例程的个数取决于这个驱动程序的设计。一般来说，IRP可以在不确定间隔内保持未决状态，在这种情况发生在驱动程序的I/O处理期间时，这个驱动程序应该为此期间的每个阶段设置一个Cancel例程，也可以称这些IRP被保持于可撤消状态。

在设计驱动程序时应该考虑以下几点：

§ 在一系列分层的驱动程序中，如果最高层驱动程序要对IRP排队或使IRP保持于可撤消状态，那么它至少应该有一个Cancel例程。需要的话，它还可以有更多的Cancel例程。

§ 对于低层驱动程序，如果其中的IRP要在较长时间内被保持于可撤消状态，那么它也应当有一个或多个Cancel例程。

§ 如果一个驱动程序要管理它自己的IRP内部队列，那么它的每个队列都应当有一个单独的Cancel例程。

某些交互设备的最高层驱动程序（例如键盘、鼠标、声卡、并行类和串行驱动程序）必须有Cancel例程。某些低层驱动程序也应该有Cancel例程，例如一个长时间使IRP排队等待一些更高层类驱动程序的并行驱动程序。

处于海量存储设备驱动程序之上的中间层驱动程序和海量存储设备驱动程序一般没有Cancel例程。也就是说，由文件系统驱动程序来处理文件I/O请求的撤消，而输入到低层海量存储设备驱动程序的IRP由于完成处理太快，以致于不能被撤消。

没有Cancel例程的高层驱动程序中的可撤消IRP

高层驱动程序无法知道现有的低层驱动程序是否或怎样处理可撤消IRP。高层驱动程序一旦用IRP调用了IoCallDriver，它就不再拥有这个IRP，而且它既不能确定也不能控制低层驱动程序对此IRP的处理。

然而，任何高层驱动程序在调用IoCallDriver之前，都可以通过调用IoSetCompletionRoutine在IRP中设置一个IoCompletion例程。这个高层驱动程序在给低层驱动程序传递IRP之前，可以通过调用IoSetCompletionRoutine并把参数InvokeOnCancel设为TRUE，从而确定是否低层驱动程序中每个未决的IRP都被撤消了。这样做可以确保驱动程序的IoCompletion例程将被调用，而不管IRP是被撤消了还是被完成了。

高层驱动程序可以用分配给它的任何未决的IRP来调用IoCancelIrp。然而，这样调用不能保证分配给驱动程序的IRP在完成时它的I/O状态块被设为STATUS_CANCELLED，因为另一个线程可能已经完成了这个IRP。为了检查IRP是否被撤消了，高层驱动程序必须在传递此IRP到下一个低层驱动程序之前，用值为TRUE 的InvokeOnCancel参数调用IoSetCompletionRoutine。参见第13章IoCompletion例程以了解更多关于完成例程的内容。

高层驱动程序决不能用没有分配的IRP来调用IoCancelIrp。

1.2 处理可撤消IRP

从驱动程序角度看，一个IRP可在任何时刻被撤消。IRP撤消是异步发生的，因此，驱动程序必须能处理大量可能的竞态条件（race condition），其中包括IRP是否在下列时刻被撤消：

§ 在驱动程序例程被调用之后，而在对IRP排队之前

§ 在驱动程序例程被调用之后，而在试图处理IRP之前。例如，一个IRP可能是在驱动程序的StartIo例程被调用之后，并且在StartIo例程将此IRP移出设备队列之前撤消的。

§ 在驱动程序例程将此IRP从队列中取出之后，而在开始请求的I/O之前。

注意：在驱动程序将一个IRP排队并释放了任何保护队列的自旋锁之后，另一个线程可以访问并且改变这个IRP。当原来的线程继续时（甚至只是到下一行代码），这个IRP可能已经被撤消了或者被修改了。

下列IRP成员具有关于撤消的关键信息：

§ Irp->Cancel指明一个IRP是否正被撤消或是否应被撤消。

§ Irp->CancelRoutine指明一个IRP是否是可撤消的。如果这个成员是一个指向Cancel例程的指针，那么这个IRP就是可撤消的；如果这个成员是NULL，那么那么这个IRP就是不可撤消的。如果这个成员是NULL，但Irp->Cancel为“1”，这就表明Cancel例程正在运行，而且这个IRP正被撤消。

如果驱动程序要处理可撤消IRP，那么它就要为其中被保持于可撤消状态的每个IRP设立适当的Cancel例程。

如果想知道下列情况下驱动程序例程的详细策略，请看：

§ 12.2.1对传递IRP的驱动程序例程中可撤消IRP的处理

§ 12.2.2对处理请求的驱动程序例程中可撤消IRP的处理

§ 12.2.3使用系统撤消自旋锁

§ 12.2.4使用驱动程序提供的自旋锁和Cancel例程
1.2.1 对传递IRP的驱动程序例程中可撤消IRP的处理

如果设备驱动程序有StartIo例程，那么它的Dispatch例程必须在调用IoStartPacket时提供可选的CancelFunction参数。

如果设备驱动程序没有StartIo例程，那么它的Dispatch例程在将一个IRP排队以供其他驱动程序例程做进一步处理之前，必须做下列事情：

1．调用IoAcquireCancelSpinLock。

2．用输入IRP和驱动程序提供的Cancel例程的入口点调用IoSetCancelRoutine。

3．调用IoReleaseCancelSpinLock。

类似的，对于传递IRP以供该驱动程序中其他例程处理的驱动程序例程，如果这个IRP在被另一个驱动程序例程处理之前可能被保持于可撤消状态，那么也需要为它建立Cancel例程。

管理自己的IRP队列，而不用I/O管理器提供的设备队列的驱动程序，在调用IoSetCancelRoutine时，不需要获得撤消自旋锁。然而，这些驱动程序应该检查IoSetCancelRoutine返回的Cancel例程指针，以确定是否撤消例程已经开始。
1.2.2 对处理请求的驱动程序例程中可撤消IRP的处理

驱动程序的StartIo例程（如果有的话）和其他任何将被保持于可撤消状态的IRP移出队列或用这样的IRP调用的例程必须做下列事情：

调用IoAcquireCancelSpinLock。

检查Irp是否等于DeviceObject->CurrentIrp。如果不是的话，调用IoReleaseCancelSpinLock并返回控制。

如果这两个不等，说明CurrentIrp可能已经在IoStartPacket释放撤消自旋锁之后，并且在此例程获得它之前被撤消了

用一个值为NULL的CancelRoutine指针调用IoSetCancelRoutine，以将IRP从可撤消状态中解除。

检查Irp->Cancel域以确定是要撤消这个IRP，还是要开始处理请求。

如果Irp->Cancel为TRUE，做下列事情：

调用IoReleaseCancelSpinLock。

将Irp->IoStatus.Status设为STATUS_CANCELLED。

将Irp->IoStatus. Information设为0。

在StartIo例程中调用IoStartNextPacket，从而开始下一个包。

用值为IO_NO_INCREMENT的优先级提高值调用IoCompleteRequest以完成这个IRP。

如果Irp->Cancel为FALSE，调用IoReleaseCancelSpinLock，然后为此IRP做请求的处理，或将此IRP传递给低一层驱动程序。

其他从队列中移出IRP或用被保持于可撤消状态的IRP调用的例程也应该做类似的事情。这种例程必须检查IRP是否可被撤消，或者必须在它开始为IRP做I/O处理之前，将此IRP从可撤消状态中解除。

管理自己的IRP队列、而不用I/O管理器提供的设备队列的驱动程序，在调用IoSetCancelRoutine时，不需要获得撤消自旋锁。然而，这些驱动程序应该检查IoSetCancelRoutine返回的Cancel例程指针，以确定是否撤消例程已经开始。

在处理可撤消IRP的驱动程序里，在底层设备已为请求的I/O操作编程之前处理IRP的驱动程序例程应该检查所有输入IRP是否处于可撤消状态。特别对于既有StartIo例程又有ControllerControl例程的最高层设备驱动程序，在这两个例程中都应该处理输入的IRP。
1.2.3 使用系统撤消自旋锁

改变可撤消IRP状态的驱动程序例程（包括所有可能以STATUS_CANCELLED完成IRP的例程）必须按照这一节中的策略来获得和释放系统撤消自旋锁。

在用I/O管理器提供的设备队列的驱动程序中，除了Cancel例程以外，所有改变可撤消IRP状态的驱动程序例程必须先调用IoAcquireCancelSpinLock以获得系统撤消自旋锁。

获得系统撤消自旋锁保证了只有调用者可以改变这个IRP的可撤消状态。当调用者持有自旋锁时，I/O管理器不能用此IRP调用这个驱动程序的Cancel例程。类似的，另一个驱动程序例程（如DispatchCleanup例程）不能同时试图改变这个IRP的可撤消状态。

在管理自己的IRP队列并用驱动程序提供的自旋锁来同步队列访问的驱动程序中，驱动程序例程不需要在调用IoSetCancelRoutine之前获得撤消自旋锁。然而，这些驱动程序应该检查IoSetCancelRoutine返回的Cancel例程指针，以确定是否撤消例程已经开始。请参见下一节《用驱动程序提供的自旋锁和Cancel例程》。
1.2.4 使用驱动程序提供的自旋锁和Cancel例程

管理自己的IRP队列的驱动程序可以用驱动程序提供的自旋锁（而不是系统撤消自旋锁）来同步对队列的访问。为了改善性能，除非不得已，否则最好避免使用撤消自旋锁。

因为系统只有一个撤消自旋锁，所以驱动程序有时必须等待这个自旋锁变得可用。用驱动程序提供的自旋锁消除了这种可能的延迟，它使得撤消自旋锁对I/O管理器和其他驱动程序始终可用。尽管系统在调用驱动程序的Cancel例程时仍然获得撤消自旋锁，但是驱动程序可以用它自己的自旋锁去保护它的IRP队列。

即使驱动程序没有将未决的IRP排队，而是以其他某种方式保持着所有权，它仍然必须为此IRP建立一个Cancel例程，并且必须用自旋锁保护IRP指针。例如，假设驱动程序将一个IRP标记为“未决”，然后将此IRP指针作为环境传递给IoTimer例程。这个驱动程序必须建立一个能撤消定时器的Cancel例程，并且当访问此IRP时，必须在Cancel例程和定时器回调中使用同一个自旋锁。

对自己的IRP排队并用自己的自旋锁的驱动程序必须做下列事情：

§ 创建保护队列的自旋锁。

§ 只有当持有此自旋锁时，才能建立并清除Cancel例程。

§ 如果驱动程序正在将一个IRP从队列中移出时，Cancel例程已开始运行，那么允许Cancel例程完成此IRP。

§ 获得保护Cancel例程中队列的锁。

为了创建自旋锁，驱动程序要调用KeInitializeSpinLock。在下面的例子中，驱动程序将自旋锁和它创建的队列保存在DEVICE_CONTEXT结构中：

typedef struct{

LIST_ENTRY irpQueue;

KSPIN_LOCK irpQueueSpinLock;

}DEVICE_CONTEXT;

VOID InitDebiceContext(DEVICE_CONTEXT *deviceContext)

{

InitializeListHead(&deviceContext-> irpQueue);

KeInitializeSpinLock(&deviceContext-> irpQueueSpinLock);

}

为了将IRP排队，驱动程序要先获得自旋锁，调用InsertTailList，然后将这个IRP标记为“未决”，如下面例子所示：

NTSTATUS QueueIrp(DEVICE_CONTEXT *deviceContext,PIRP Irp)

{

PDRIVER_CANCEL oldCancelRoutine;

KIRQL oldIrql;

NTSTATUS status=STATUS_PENDING;

KeAcquireSpinLock(&deviceContext-> irpQueueSpinLock,&oldIrql);

//Queue the IRP and call IoMarkIrpPending

//to indicate that the IRP may complete on a different thread.

//N.B. It’s okay to call these inside the spin lock because they’re macros,not

//functions.

IoMarkIrpPending(Irp);

InsertTailList(&deviceContext-> irpQueue,&Irp->Tail.Overlay.ListEntry);

//Must set a Cancel routine before checking the Cancel flag.

oldCancelRoutine=IoSetCancelRoutine(Irp,IrpCancelRoutine);

ASSERT(!oldCancelRoutine);

if(Irp->Cancel){

//The IRP was canceled.Check whether our cancel routine was called.

oldCancelRoutine=IoSetCancelRoutine(Irp,NULL);

if(oldCancelRoutine){

//The cancel routine was NOT called.

//So dequeue the IRP now and complete it after releasing the spinlock.

RemoveEntryList(&Irp->Tail.Overlay.ListEntry);

status= Irp->IoAtatus.Status=STATUS_CANCELLED;

}//endif

else{

// The cancel routine WAS called.

//As soon as we drop our spin lock it dequeues and completes the IRP.

//So leave the IRP in the queue and otherwise don’t touch it.

//Return pending since we’re not completing the IRP here.

}//endif

}//endif

KeReleaseSpinLock(&deviceContext-> irpQueue,oldIrql);

//Normally you shouldn’t call IoMarkIrpPending

//and return a status other than STATUS_PENDING.

//But you can break this rule if you complete the IRP.

if (status!= STATUS_PENDING){

IoCompleteRequest(Irp,IO_NO_INCREMENT);

}

return status;

}

这个例子表明，当驱动程序建立并清空Cancel例程时，它持有着自旋锁。例子中的排队例程包括了两个对IoSetCancelRoutine的调用。

第一个调用为IRP建立Cancel例程。然而，由于当排队例程运行时，这个IRP可能已经被撤消了，所以驱动程序必须检查此IRP的Cancel成员。

§ 如果Cancel已被置为1，就说明撤消已被请求，因此驱动程序必须再次调用IoSetCancelRoutine以确定先前设置的Cancel例程是否被调用了。

§ 如果IRP已被撤消，但Cancel例程却还没有被调用，那么当前例程将此IRP从队列中移出，将其状态设为STATUS_CANCELLED，然后完成它。

§ 如果IRP已被撤消而且Cancel例程已被调用，那么当前返回将这个IRP标记为“未决”，然后返回STATUS_PENDING。Cancel例程会完成此IRP。

下面的例子表明了怎样将一个IRP从先前创建的队列中移出：

PIRP DequeueIrp(DEVICE_CONTEXT *deviceContext)

{

KIRQL oldIrql;

PIRP nextIrp=NULL;

KeAcquireSpinLock(&deviceContext->irpQueueSpinLock,&oldIrql);

while(!nextIrp&&!IsListEmpty(&deviceContext->irpQueue)){

PDRIVER_CANCEL oldCancelRoutine;

PLIST_ENTRY listEntry=RemoveHeadList(&deviceContext->irpQueue);

//Get the next IRP off the queue.

nextIrp=CONTAINING_RECORD(listEntry,IRP,Tail.Oberlay.ListEntry);

//Clear the IRP’s cancel routine

oldCancelRoutine=IoSetCancelRoutine(nextIrp,NULL);

//IoCancelIrp() could have just been called on this IRP.

//What we’re interested in is not whether IoCancelIrp() was called

//(nextIrp->Cancel flag set),

//but whether IoCancelIrp() called (or is about to call) cancel routine.

//To check that,check the result of test-and-set macro IoSetCancelRoutine.

if(oldCancelRoutine){

//Cancel routine not called for this IRP.Return this IRP.

ASSERT(oldCancelRoutine= =IrpCancelRoutine);

}

else{

//This IRP was just canceled and our cancel routine was (or will be)

//called.

//The cancel routine will complete this IRP as soon as we drop the

//spin lock,we don’t do anything with the IRP.

//Also,the cancel routine will try to dequeue the IRP,

//so make the IRP’s listEntry point to itself.

ASSERT(nextIrp->Cancel);

InitializeListHead(&nextIrp->Tail.Overlay.ListEntry);

nextIrp=NULL;

}

}

KeReleaseSpinLock(&deviceContext->irpQueueSpinLock,oldIrql);

return nextIrp;

}

在这个例子中，驱动程序在访问队列之前先获得相关的自旋锁。在持有此自旋锁时，驱动程序检查队列是否为空。如果是，就把下一个IRP从队列中取出。然后调用IoSetCancelRoutine，从而为此IRP重置Cancel例程。因为当驱动程序将IRP从队列中移出并重置Cancel例程时，这个IRP可能已经被撤消了，所以驱动程序必须检查IoSetCancelRoutine返回的值。如果IoSetCancelRoutine返回NULL，表明Cancel例程已经或很快就会被调用，那么上面例子中的例程就让Cancel例程完成这个IRP，然后它释放保护队列的锁并返回。

注意在上面例程中InitializeListHead的使用。驱动程序可能还会将这个IRP再次排队，因此可以用Cancel例程将它移出，但更简单的做法是调用InitializeListHead。InitializeListHead重新初始化这个IRP的ListEntry域，使它指向此IRP本身。用自引用指针非常重要，因为在Cancel例程获得自旋锁之前，这个表的结构可能会改变。如果表结构改变了，可能使ListEntry的初始值变为无效，那么Cancel例程就可能在它将此IRP从队列中移出时破坏这个表。但是如果ListEntry指向这个IRP本身，那么Cancel例程就会永远使用正确的IRP。

接下来，Cancel例程只是做下列事情：

VOID IrpCancelRoutine(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)

{

DEVICE_CONTEXT *deviceContext=DeviceObject->DeviceExtension;

KIRQL oldIrql;

//Release the global cancel spin lock.

//Do this while not holding any other spin locks so that we exit at the right IRQL.

IoReleaseCancelSpinLock(Irp->CancelIrql);

//

//Dequeue and complete the IRP.

//The enqueue and dequeue functions synchronize properly so that if this

//cancel routine is called,the dequeue is safe and only the cancel routine will

//complete the IRP.Hold the spin lock for the IRP queue while we do this.

//

KeAcquireSpinLock(&deviceContext->irpQueueSpinLock,&oldIrql);

RemoveEntryList(&Irp->Tail.Overlay.ListEntry);

KeReleaseSpinLock(&deviceContext->irpQueueSpinLock,oldIrql);

//Complete the IRP.This is a call outside the driver,so all spin locks

//must be released by this point.

Irp->IoStatus.Status=STATUS_CANCELLED;

IoCompleteRequest(Irp,IO_NO_INCREMENT);

return;

}

I/O管理器总是在调用Cancel例程之前获得全局撤消自旋锁，因此Cancel例程的第一个任务就是释放这个自旋锁。然后它获得保护此驱动程序IRP队列的自旋锁；将当前IRP从队列中移出；释放它的自旋锁；以STATUS_CANCELLED完成此IRP；如果没有优先级提高，就返回。

1.3 Cancel例程功能

I/O管理器用将被撤消的输入IRP和代表请求目标的DeviceObject指针调用驱动程序提供的Cancel例程。

这个IRP可以是在用户关闭当前Win32应用程序时由驱动程序的DispatchReadWrite例程已经排队的IRP，也可以是高层驱动程序显式撤消的IRP。这取决于底层设备的性质。

当Cancel例程被调用时，如果驱动程序有StartIo例程，那么输入IRP可能已经是目标设备对象中的CurrentIrp，或可能已经在与目标设备对象相关的设备队列中了。如果驱动程序没有StartIo例程，那么当Cancel例程被调用时，这个IRP可能在驱动程序管理的IRP内部队列中。无论哪种情况，I/O管理器都要将输入IRP的Cancel入口点重置为NULL，然后用这个IRP调用Cancel例程。

对于有相关IRP的主IRP，Cancel例程负责调用IoCancelIrp以撤消那些相关IRP。

所有Cancel例程都必须遵守下列策略：

§ 因为Cancel例程总是在持有系统撤消自旋锁时被调用，所以它决不能在调用IoReleaseCancelSpinLock之前调用IoAcquireCancelSpinLock。

§ 当Cancel例程返回控制时，不能正持有着系统撤消自旋锁。也就是说，每个Cancel例程在返回控制之前，至少调用一次IoReleaseCancelSpinLock。

§ 如果调用了IoAcquireCancelSpinLock，Cancel例程必须尽快做相反调用IoReleaseCancelSpinLock。

§ 当持有自旋锁时，决不要用IRP调用IoCompleteRequest。在持有自旋锁时试图完成一个IRP会导致死锁。
1.3.1 带有StartIo例程的驱动程序中的Cancel例程

只有IRP在相关设备队列对象中排队时，I/O管理器才维护此设备对象中的CurrentIrp域。也就是说，只有驱动程序有StartIo例程时，这个域才是有效的。

在有StartIo例程的驱动程序中，一个典型的Cancel例程必须做下列事情：

1. 检查输入IRP的指针是否与目标设备对象的CurrentIrp地址匹配。

如果这些指针是相等的，Cancel例程调用IoReleaseCancelSpinLock，传递Irp->CancelIrpl，然后返回控制。

2. 如果撤消的IRP不是当前IRP，通过用IRP的Tail.Overlay.DeviceQueueEntry指针调用KeRemoveEntryDeviceQueue，来检查是否输入撤消IRP在与目标设备对象相关的设备队列中。

§ 如果IRP在设备队列中，调用KeRemoveEntryDeviceQueue将它从队列中移出。Cancel例程调用IoReleaseCancelSpinLock；将IRP的I/O状态块的Status设为STATUS_CANCELLED，Information设为0；用撤消的IRP调用IoCompleteRequest；然后返回控制。

§ 如果IRP不在设备队列中，Cancel例程调用IoReleaseCancelSpinLock，然后返回控制。

驱动程序的Cancel例程应该调用KeRemoveEntryDeviceQueue，以检查IRP是否在设备队列中。这个支持例程要么将给定IRP从设备队列中移走，要么只是返回FALSE以表明给定入口没有排队。Cancel例程无法确定输入IRP在设备队列中的具体位置，因此它不能调用KeRemoveEntryDeviceQueue或KeRemoveByKeyDeviceQueue来比较返回IRP的指针和输入IRP的指针。

有Cancel例程的驱动程序也能够处理IRP_MJ_CLEANUP请求，与此相关的详细内容请参见第6章中的DispatchCleanup功能。
1.3.2 没有StartIo例程的驱动程序中的Cancel例程

只有IRP在相关设备队列对象中排队时，I/O管理器才维护此设备对象中的CurrentIrp域。

没有StartIo例程的驱动程序管理自己的IRP内部队列。在这样的驱动程序中，Cancel例程可以用一个既不是输入目标设备对象CurrentIrp，又不是驱动程序内部队列中的IRP的输入IRP来调用。驱动程序必须维护自己的、关于哪个IRP当前正被处理的状态。此外，它还应该为其每个队列分别建立一个Cancel例程。驱动程序的内部队列应该是互锁的队列，因为它的内部队列必须被执行自旋锁保护。

当驱动程序的Cancel例程被调用时，它通常做下列事情：

1. 调用IoReleaseCancelSpinLock，传递Irp->CancelIrpl。

2. 获得保护其互锁队列的自旋锁，然后遍历队列，以寻找Irp->Cancel为TRUE的IRP。

§ 如果在互锁队列中找到了这样的IRP，那么就将它移出队列；释放保护此队列的自旋锁；将此IRP的I/O状态块的Status设为STATUS_CANCELLED，将Information设为0；对撤消的IRP调用IoCompleteRequest；然后返回控制。

§ 如果没有找到这样的IRP，Cancel例程就释放其持有的所有自旋锁，然后返回控制。

有Cancel例程的驱动程序也能处理IRP_MJ_CLEANUP请求，与此相关的详细内容请参见第6章中的DispatchCleanup功能。

1.4 处理可撤消IRP时的注意事项

这一节讨论实现Cancel例程及处理可撤消IRP的一些策略。

适用于所有Cancel例程的一般策略

I/O管理器在调用驱动程序Cancel例程时一直持有自旋锁。因此，每个Cancel例程都必须：

§ 在返回控制之前，调用IoReleaseCancelSpinLock。

§ 不要调用IoAcquireCancelSpinLock，除非先调用了IoReleaseCancelSpinLock。

§ 对每个IoAcquireCancelSpinLock，都做逆调用IoReleaseCancelSpinLock。

Cancel例程每次调用IoReleaseCancelSpinLock时，都必须传递由最近一次调用IoAcquireCancelSpinLock返回的IRQL。在释放I/O管理器获得的自旋锁时，Cancel例程必须传递Irp->CancelIrpl。

驱动程序决不能在持有自旋锁时调用外部例程，因为这样会导致死锁。

使用I/O管理器定义的队列

除非驱动程序管理自己的IRP内部队列，否则它的Cancel例程可以用下列输入IRP之一调用：

§ 输入目标设备对象中的CurrentIrp

§ 与目标设备对象相关的入口

除非驱动程序自己管理自己的IRP内部队列，否则驱动程序的Cancel例程应该用输入IRP调用KeRemoveEntryDeviceQueue，以检测出它是否是与目标设备对象相关的设备队列中的入口。驱动程序的Cancel例程不能调用KeRemoveDeviceQueue或KeRemoveByKeyDeviceQueue，因为它无法确定给定IRP在设备队列中的确切位置。

输入IRP的当前状态

如果Cancel例程是用驱动程序已经开始为它做I/O处理并且请求将很快完成的IRP调用的，那么这个Cancel例程应该释放系统撤消自旋锁，并返回控制。

如果输入IRP的当前状态是“未决”（Pending），Cancel例程必须做下列事情：

1. 将这个输入IRP的I/O状态块中的Status设为STATUS_CANCELLED，Information设为0。

2. 释放它持有的所有自旋锁，包括系统撤消自旋锁。

3. 用给定的IRP调用IoCompleteRequest。

将IRP保持在可撤消状态

要把一个IRP保持在可撤消状态的驱动程序例程必须调用IoMarkIrpPending，而且还必须调用IoSetCancelRoutine以在IRP中设置Cancel例程的入口点。只有这样做了之后，此驱动程序例程才可以调用IoStartPacket、IoAllocateController等其他支持例程，或ExInterlockedInsert.List例程。

接着处理可撤消IRP的驱动程序例程必须检查：在IRP开始被处理以满足请求前，是否它已被撤消了。这个例程必须调用IoSetCancelRoutine，从而在IRP 中将其Cancel例程入口点重设为NULL。只有这样做了之后，这个例程才能开始为输入IRP进行I/O处理。

如果驱动程序要传递IRP以供其他驱动程序例程作进一步处理，那么它可能也需要将IRP中的Cancel例程入口点重设。这些IRP可能会被保持在可撤消状态。

将IRP保持在可撤消状态的高层驱动程序，必须在它把此IRP用IoCallDriver传递给低一层驱动程序之前，将其Cancel例程入口点重设为NULL。

撤消IRP

高层驱动程序可以用它分配的IRP调用IoCancelIrp，然后传递它以供低层驱动程序做进一步处理。然而，这样的驱动程序无法保证给定IRP能够被低层驱动程序以STATUS_CANCELLED状态完成。

同步

驱动程序可以（或必须，这取决于它的设计）维护其设备扩展（device extension）中的其他状态信息，以跟踪IRP的可撤消状态。如果这种状态是与运行在IRQL<=DISPATCH_LEVEL的驱动程序例程共享的，那么共享数据应该用驱动程序分配并初始化的自旋锁保护起来。

驱动程序应该管理获得的自旋锁，并谨慎地释放系统撤消自旋锁和它自己的自旋锁。它应该在尽可能短的时间间隔内持有系统撤消自旋锁。在访问可撤消IRP之前，这种驱动程序应该先检查IoSetCancelRoutine返回的值，以确定Cancel例程是否已经在运行（或正要开始运行）。如果是，它应当命令Cancel例程完成这个IRP。

如果设备驱动程序维护各种驱动程序例程与其ISR共享的可撤消IRP的状态信息，其他的驱动程序必须同步对共享状态的访问。只有驱动程序提供的SynchCritSection例程可以访问这种在多处理器安全方式下与ISR共享的状态信息。

请参见第16章“使用自旋锁”、第8章“中断服务例程”和第10章“SynchCritSection例程”。


IoCompletion例程

这一章总结了驱动程序的标准IoCompletion例程所需要的功能，并提出了建立被IoCompletion例程处理的IRP的一些策略。

本章包括下列内容：

13.1 使用IoCompletion例程

13.1.1 建立IoCompletion例程

13.1.2 调用IoCompletion例程

13.1.3 为低层驱动程序分配IRP

13.2 IoCompletion例程需要的功能

1.1 使用IoCompletion例程

I/O管理器对IoCompletion例程的定义如下：

NTSTATUS

{*PIO_COMPLETION_ROUTINE}(

IN PDEVICE_OBJEXT DeviceObject,

IN PIRP Irp,

IN PVOID Context

);

高层驱动程序完成各种操作的IRP需要多少个IoCompletion例程，就可以建立多少个IoCompletion例程。

IoCompletion例程是运行在等于或低于DISPATCH_LEVEL的IRQL上和专用线程环境中的。驱动程序无法预先知道IoCompletion例程将在哪一级IRQL上被调用。

下面是IoCompletion例程设计时的策略：

§ 假设每个IoCompletion例程总是运行在DISPATCH_LEVEL上。

运行在IRQL DISPATCH_LEVEL限定了IoCompletion例程可以调用的支持例程的集合。

如果想对如何管理驱动程序中的IRQL有更多的了解，请参见第16章中的“管理硬件优先级”。如果想具体知道每个支持例程能在哪个IRQL上被调用，请参见在线DDK。
1.1.1 建立IoCompletion例程

高层驱动程序的Dispatch例程一般要负责执行下列任务：

检查它们各自的输入IRP的I/O栈中参数的有效性，对带有无效参数的请求予以拒绝。

用有效参数发送IRP。对那些Dispatch例程无法确定是否有效的，用IoCallDriver传送给低层驱动程序。

如果想更深入地了解高层驱动程序的Dispatch例程，请参见第6章“Dispatch例程”。

Dispatch例程可以用驱动程序提供的IoCompletion例程入口点调用IoSetCompletionRoutine，也可以用调用IoCallDriver时要传递给低层驱动程序的任何IRP来调用。

当调用IoSetCompletionRoutine时，Dispatch例程可以指定下列任一个或多个NTSTATUS值是否应该使IoCompletion例程运行：

§ STATUS_SUCCESS：表示所有低层驱动程序都成功地完成了请求的操作。

§ STATUS_CANCELLED：表示低层驱动程序中的IRP被撤消了。

§ STATUS_XXX：表示低层驱动程序发生了错误，IRP出现了故障。

IoCompletion例程的用途是监视低层驱动程序用IRP做了些什么，并且对高层驱动程序传递给低层驱动程序处理的IRP做必要的清理工作。

如果想更多地了解NTSTATUS和I/O错误，请参见第16章中的“错误记录和NTSTATUS值”。

如果想知道本章中提到的每个支持例程的具体信息，请参见在线DDK。
1.1.2 调用IoCompletion例程

高层驱动程序的IoCompletion例程最常见的用途如下：

§ 清除（dispose）驱动程序用IoAllocateIrp或IoBuildAsynchronousFsdRequest分配的IRP。

使用这些支持例程之一分配到IRP的高层驱动程序必须在这个IRP中建立它的IoCompletion例程。这个IoCompletion例程必须调用IoFreeIrp清除这些分配给驱动程序的IRP。

§ 重用输入IRP以请求低层驱动程序完成某些操作（如部分传输），直到初始请求能被满足，并且被IoCompletion例程完成为止。

§ 重试一个低层驱动程序以错误状态完成的请求。

比起中间层驱动程序（除了与端口驱动程序紧密相关的类驱动程序以外），最高层驱动程序（如文件系统）更可能有试图重试请求的IoCompletion例程。然而，任何中间层驱动程序都可以从它的IoCompletion例程中重试请求。

最高层或中间层驱动程序的DispatchReadWrite例程通常确定了指定IRP是否需要驱动程序建立IoCompletion例程。驱动程序的DispatchDeviceControl或其他Dispatch例程也能够为任何传递给低层驱动程序的给定IRP建立IoCompletion例程。

为使驱动程序分配并重用IRP，Dispatch例程必须用下列布尔类型的参数调用IoSetCompletionRoutine：

§ 将InvokeOnSuccess设为TRUE。

§ 将InvokeOnError设为TRUE。

§ 如果任何低层驱动程序都可能处理可撤消IRP，则将InvokeOnCancel设为TRUE。

通常，无论IRP的返回状态是否是STATUS_CANCELLED，InvokeOnCancel都被设为TRUE。这样可以保证IoCompletion例程能释放掉每个分配给驱动程序的IRP，或者能能检查IRP的每个重用的完成状态。

当在高层驱动程序的Dispatch例程中调用IoSetCompletionRoutine例程时，考虑下列实现策略：

§ 任何用IoAllocateIrp或IoBuildAsynchronousFsdRequest为低层驱动程序分配IRP的Dispatch例程，都必须在每个分配给驱动程序的IRP中建立IoCompletion例程。

§ Dispatch例程必须建立初始IRP和分配给IoCompletion例程使用的IRP的状态信息。至少，IoCompletion例程需要知道访问初始IRP的路径，以及有多少个附加IRP被分配了。

§ Dispatch例程应该为它分配的IRP，用值为TRUE 的InvokeOnXxx参数调用IoSetCompletionRoutine。

§ 如果Dispatch例程在IRP中分配了一个它自己的I/O栈位置，那么它必须在调用IoGetCurrentIrpStackLocation之前调用IoSetNextIrpStackLocation，从而在它自己的I/O栈位置中为IoCompletion例程建立环境。（参见第6章中“如何从Dispatch例程中用有效参数传递IRP”）

§ Dispatch例程必须用初始IRP调用IoMarkIrpPending，而不是用分配给驱动程序的IRP，因为IoCompletion例程会释放它们。

§ 如果Dispatch例程正在为部分传输分配IRP，而且下层设备驱动程序可能要控制一个可删除存储介质设备，那么Dispatch例程必须根据初始IRP中Tail.Overlay.Thread的值，在它新分配的IRP中建立线程环境。

底层可删除存储介质设备的驱动程序可能会用一个分配给驱动程序的IRP调用ToSetHardErrorOrVerifyDevice，这个例程引用了指向Irp->Tail.Overlay.Thread的指针。如果底层设备驱动程序调用了这个支持例程，文件系统驱动程序可以向适当的用户线程发送一个弹出菜单，提示用户撤消、重试或完成设备驱动程序无法成功执行的操作。

更多信息参见第16章中的“处理可删除介质”。

§ Dispatch例程在把所有分配给驱动程序的IRP传给低层驱动程序后，必须返回STATUS_PENDING。

§ 驱动程序的IoCompletion例程必须在它用初始IRP调用IoCompleteRequest之前，用IoFreeIrp释放所有分配给驱动程序的IRP。当它完成此初始IRP时，IoCompletion例程必须在它返回控制之前释放所有分配给驱动程序的IRP。

§ 如果要执行一系列操作或者如果IoCompletion例程可以重试请求，那么Dispatch例程必须用将被重用的IRP调用IoSetCompletionRoutine。

§ Dispatch例程必须建立初始IRP的状态信息，以供IoCompletion例程使用。

例如，DispatchReadWrite例程在为低一层驱动程序在输入IRP中建立部分传输之前，必须为IoCompletion例程保存此输入IRP的相关传输参数。如果DispatchReadWrite例程修改了IoCompletion例程用来确定初始请求何时被满足的参数时，保存这些参数就显得格外重要。

如果IoCompletion例程可以重试请求，Dispatch例程必须建立一个驱动程序决定的上限，用来限制在它以错误状态完成初始IRP之前，它的IoCompletion例程可以执行多少次重试。

§ 对于将被重用的IRP，Dispatch例程应当以值为TRUE的InvokeOnXxx参数调用IoSetCompletionRoutine。

§ 对于异步请求，中间层驱动程序的Dispatch例程必须用初始IRP调用IoMarkIrpPending。

§ 对于异步请求，中间层驱动程序的Dispatch例程必须在它把IRP传送给下层驱动程序之后，返回STATUS_PENDING。

驱动程序的IoCompletion例程必须维护IRP的每个重用的状态，直到初始请求被满足（或被拒绝）为止。然后它用这个IRP调用IoCompleteRequest。IoCompletion例程可能还要负责重新初始化公共状态区，或通知另一个驱动程序例程将下一个待处理的IRP移出队列。

重试IRP的IoCompletion例程必须保存重试次数，这样如果请求的操作不能被成功完成时，它可以用这个参数决定何时拒绝一个IRP。

§ 为传递给低层驱动程序的一类请求分配每IRP（per-IRP）资源的Dispatch例程，必须用这种类型的任一IRP调用IoSetCompletionRoutine。

例如，如果Dispatch例程用IoAllocateMdl分配了一个MDL，并为一个它分配的部分传输IRP调用了IoBuildPartialMdl，那么IoCompletion例程必须用IoFreeMdl释放这个MDL。如果它分配了用来维护初始IRP状态的资源，它必须释放这些资源。最好在它用初始IRP调用IoCompleteRequest之前，最迟在它返回控制之前。

通常，在释放或完成一个IRP之前，IoCompletion例程应当释放由Dispatch例程分配给它的所有每IRP资源。此外，在IoCompletion例程从完成初始请求返回控制之前，驱动程序必须保存将被释放的资源的状态。

§ Dispatch例程可以用它传递给低层驱动程序的其他任何一个IRP调用IoSetCompletionRoutine。
1.1.3 为低层驱动程序分配IRP

最高层或中间层驱动程序的DispatchRead和/或DispatchWrite例程常常会在IRP中设立IoCompletion例程，因为传输请求必须被低层驱动程序异步处理。为了给一个异步请求分配IRP（此IRP将被低层驱动程序在专用线程环境中处理），DispatchReadWrite例程可以调用下列支持例程之一：

§ IoAllocateIrp：调用者可以用它在IRP中为自己分配一个I/O栈位置，并在此IRP中将所有I/O栈位置初始化为0。

Dispatch例程必须在新分配的IRP中设置低一层驱动程序的I/O栈位置，通常是从初始IRP中它自己的栈位置拷贝（可能会有些修改）过来的。如果高层驱动程序在一个新分配的IRP中分配了一个它自己的I/O栈位置，Dispatch例程可以在那里建立每请求（per-request）环境信息以供IoCompletion例程使用。

§ IoBuildAsynchronousFsdRequest：根据调用者指定的参数，为调用者建立低一层驱动程序的I/O栈位置。

高层驱动程序可以调用这个例程为IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_FLUSH_BUFFERS和IRP_MJ_SHUTDOWN请求分配IRP。

当IoCompletion例程被这样的IRP调用时，它能够检查I/O状态块。如果需要（或可能）的话，它在此IRP中再次建立低一层驱动程序的I/O栈位置，然后重试请求或重用它。然而，IoCompletion例程没有在IRP中为它自己保存本地环境，因此驱动程序必须在存储器的其他地方保存初始请求的环境。

§ IoMakeAssociatedIrp：最高层驱动程序可以用它在IRP中为自己分配一个I/O栈位置，但调用者很少在一个相关IRP中设立IoCompletion例程。

中间层驱动程序不能调用IoMakeAssociatedIrp来为低层驱动程序创建IRP。

调用IoMakeAssociatedIrp来为低层驱动程序创建IRP的最高层驱动程序，可以在传送出它的相关IRP并用主（初始）IRP调用了IoMarkIrpPending之后，将控制返回给I/O管理器。当所有相关IRP被低层驱动程序完成以后，最高层驱动程序可以靠I/O管理器来完成主IRP。

如果最高层驱动程序为它创建的相关IRP调用了IoSetCompletionRoutine，而且驱动程序从其IoCompletion例程返回STATUS_MORE_PROCESSING_REQUIRED，那么I/O管理器不完成主IRP。在这种情况下，驱动程序的IoCompletion例程必须用IoCompleteRequest显式地完成这个主IRP。

高层驱动程序的DispatchDeviceControl例程可以用IoBuildDeviceIoControlRequest分配IRP。理论上，它可以为这样分配的IRP建立IoCompletion例程，但实际上很少有人这么做。如果驱动程序为一个事件对象分配了常驻内存并将此对象初始化了，那么当其DispatchDeviceControl例程传递分配给驱动程序的IRP以满足异步设备控制请求时，它会等待一个事件。请参见高层驱动程序的DispatchDeviceControl这部分内容。

通常，在这种情况下，高层驱动程序不用IoBuildSynchronousFsdRequest在一个已被分配的IRP中设立它的IoCompletion例程。如果想知道更多关于调用这一例程的限制，请参见第6章中“为低层驱动程序分配IRP”这部分内容。

1.2 IoCompletion例程需要的功能

在入口点上，IoCompletion例程用DeviceObject、Irp和Context指针调用。调用IoSetCompletionRoutine的Dispatch例程能把Context指针传递给一个驱动程序决定的环境，这个环境是它在处理给定IRP时为IoCompletion例程建立的。注意：环境区不能是可分页的（pageable），因为IoCompletion例程可以在DISPATCH_LEVEL上调用。

IoCompletion例程负责做驱动程序请求的任何附加的IRP处理，以及满足此请求所需的任何清理操作，这取决于Dispatch例程是如何建立请求的。

实现IoCompletion例程时，应考虑以下几点：

§ 如果输入IRP是由Dispatch例程用IoAllocateIrp或IoBuildAsynchronousFsdRequest分配的，IoCompletion例程必须调用IoFreeIrp释放这个IRP，最好在它完成初始IRP之前调用。

§ IoCompletion例程必须释放Dispatch例程为分配给驱动程序的IRP分配的每IRP资源，最好在它释放相应的IRP之前。

例如，如果Dispatch例程为IoCompletion例程处理的部分传输IRP分配了一个MDL，IoCompletion例程必须在它从完成初始请求返回控制之前，调用IoFreeMdl释放这个MDL。

§ 如果IoCompletion例程不能以STATUS_SUCCESS完成初始IRP，它必须将初始IRP中的I/O状态块的值设为分配给驱动程序的IRP中返回的值，这个IRP使得IoCompletion例程拒绝了初始请求。

§ 如果IoCompletion例程将以STATUS_PENDING完成初始IRP，它必须在调用IoCompleteRequest之前用此初始IRP调用IoMarkIrpRending。

§ 如果IoCompletion例程必须以STATUS_Xxx错误拒绝初始IRP，那么它能够记录错误。然而，由于记录任何设备I/O错误是下层设备驱动程序的任务，所以IoCompletion例程一般不记录错误。

更多信息参见第16章中的“错误记录和NTSTATUS值”。

§ 当IoCompletion例程已经处理并释放了分配给驱动程序的IRP之后，它必须以STATUS_MORE_PROCESSING_REQUIRED返回控制。

§ 从IoCompletion例程返回STATUS_MORE_PROCESSING_REQUIRED防止了I/O管理器对分配给驱动程序并被释放的IRP做完成处理。第二次调用IoCompleteRequest使得I/O管理器继续调用IRP的完成例程。

§ 如果IoCompletion例程重用了一个输入IRP以给下层驱动程序发送一个或多个请求，它应该：在再次建立低一层驱动程序的I/O栈位置之前，更新IoCompletion例程维护的此IRP的每个重用（或重试）的所有环境；用它自己的入口点调用IoSetCompletionRoutine；用此IRP调用IoCallDriver。

§ IoCompletion例程不应该在IRP的每个重用（或重试）中调用IoMarkIrpRending。

Dispatch例程已经将初始IRP标记为“未决”。它将保持为“未决”，直到各层驱动程序都用IoCompleteRequest完成了初始IRP为止。

§ 在重试一个请求之前，IoCompletion例程应该将I/O状态块中的Status重设为STATUS_SUCCESS ，将Information重设为0。可能的话，需要在保存了返回的错误信息之后这样做。

通常，每次重试时，IoCompletion例程将一个由Dispatch例程建立的重试计数器递减。一般，当限定次数的重试都失败了时，IoCompletion例程必须调用IoCompleteRequest撤消这个IRP。

§ 在IoCompletion例程用正被重用或重试的IRP调用了IoSetCompletionRoutine和IoCallDriver后，它必须返回STATUS_MORE_PROCESSING_REQUIRED。

以STATUS_MORE_PROCESSING_REQUIRED从IoCompletion例程返回防止了I/O管理器对重用或重试IRP做完成处理。

§ 如果IoCompletion例程不能以STATUS_SUCCESS完成初始IRP，它必须将I/O状态块恢复到导致IoCompletion例程拒绝此IRP的重用或重试操作时低层驱动程序返回的状态。

§ 如果IoCompletion例程将以STATUS_PENDING完成初始IRP，它必须在调用IoCompleteRequest之前用此初始IRP调用IoMarkIrpRending。

§ 如果IoCompletion例程必须以STATUS_Xxx错误拒绝初始IRP，那么它能够记录错误。然而，由于记录任何设备I/O错误是下层设备驱动程序的任务，所以IoCompletion例程一般不记录错误。

详细内容请参见第16章中的错误记录和NTSTATUS值部分。

§ 所有在IRP中设立了IoCompletion例程然后将此IRP传递给低层驱动程序的驱动程序，都应该检查IoCompletion例程中的IRP->PendingReturned标志。如果标志为1，IoCompletion例程必须用这个IRP调用IoMarkIrpRending。然而，注意：将IRP下传然后等待事件发生的驱动程序不应该将此IRP标记为“未决”。相反，它的IoCompletion例程应该向这个事件发信号，并返回STATUS_MORE_PROCESSING_REQUIRED。

§ IoCompletion例程必须释放由Dispatch例程分配给它用来处理初始 IRP的所有资源，最好在IoCompletion例程用此初始IRP调用IoCompleteRequest之前，最晚在IoCompletion例程从完成此初始IRP返回控制之前。

如果还有更高层驱动程序在这个初始IRP中设立了它的IoCompletion例程，直到所有低层驱动程序已经用此初始IRP调用了IoCompleteRequest之后，此驱动程序的IoCompletion例程才会被调用。


IoTimer和CustomTimerDpc例程

这一章总结了驱动程序标准IoTimer和CustomTimerDpc例程的需求，提出了使用这些例程的策略。本章包括以下内容：

14.1 定时器例程需求

14.2 IoTimer例程功能

14.2.1 建立IoTimer例程

14.2.2 为IoTimer例程建立环境

14.2.3 使用IoTimer例程

14.3 CustomTimerDpc例程功能

14.3.1 建立CustomTimerDpc例程

14.3.2 为CustomTimerDpc例程建立环境

14.3.3 使用CustomTimerDpc例程

如果想知道关于定时器对象的更多内容，请参见第3章中《与DPC相关的定时器对象》部分。

1.1 定时器例程需求

驱动程序通常为了下列目的使用定时器例程：

§ 监视I/O操作是否超时

§ 重启动超时的设备操作

§ 当某项操作超时或发生了设备错误时，记录错误

除此之外，实际上驱动程序可以用定时器例程实现能满足它或它的设备需要的任何目的。

系统定义了两种类型的定时器例程：IoTimer例程和CustomTimerDpc例程。驱动程序可以有与其设备对象一样多的IoTimer例程，可以有任意多个CustomTimerDpc例程。

一般，驱动程序用IoTimer例程为需要固定的1秒时间间隔的操作定时，用CustomTimerDpc例程为需要可变时间间隔或时间间隔小于1秒的操作定时。

1.2 IoTimer例程功能

I/O管理器对IoTimer例程的定义如下：

VOID

(*PIO_TIMER_ROUTINE)(

IN PDEVICE_OBJECT DeviceObject,

IN PVOID Context

);

DeviceObject参数表示与IoTimer例程相关的设备对象。Context参数指向驱动程序定义的环境信息。请参见“为IoTimer例程建立环境”。

每个IoTimer例程都与一个驱动程序创建的设备对象相关。驱动程序的所有设备对象可以共用一个IoTimer例程，也可以每个设备对象有一个独立的IoTimer例程，还可以设备对象的每个不相交子集有一个相关的IoTimer例程。

IoTimer例程运行在DISPATCH_LEVEL及专用线程环境中。

运行在DISPATCH_LEVEL限定了IoTimer例程可以调用的支持例程的集合。请参见第16章中的“管理硬件优先级”。
1.2.1 建立IoTimer例程

驱动程序通常在它的AddDevice例程中建立每个IoTimer例程，如下所示：

1. 调用IoCreateDevice建立IoTimer例程将被相关的设备对象。

2. 调用IoInitializeTimer，将IoTimer例程入口点——一个Context指针传递给所有IoTimer例程将会用到的驱动程序决定的环境区，并传递指定调用定时器例程的目标设备的DeviceObject指针。

调用IoInitializeTimer为指定设备对象注册了驱动程序的IoTimer例程。当驱动程序为指定设备对象调用IoDeleteDevice时， I/O管理器将定时器例程注销并释放与它相关的环境。

在建立了IoTimer例程之后，驱动程序通过调用IoStartTimer使这个例程可用，给相关的设备对象传递一个指针。在IoStartTimer返回后，I/O管理器约每秒调用一次IoTimer例程。然后驱动程序可以通过用同一个DeviceObject指针调用IoStopTimer使此设备对象的定时器停止工作。驱动程序可以再次调用IoStartTimer来重新使这个定时器可用，因此IoTimer例程再次以1秒的时间间隔为指定设备对象被自动调用。

通常，驱动程序用当前IRP的I/O栈位置中的DeviceObject指针来调用IoStopTimer。

不要在驱动程序的IoTimer例程内部调用IoStopTimer。

请参见“与DPC相关的定时器对象”和IoInitializeTimer。
1.2.2 为IoTimer例程建立环境

传递给IoInitializeTimer的Context指针指向一个环境区，其他驱动程序例程及IoTimer例程自己可以用这个环境区来保存被定时操作的状态。I/O管理器每次调用IoTimer例程时，都会传递这个Context指针。

环境区必须在驱动程序分配的常驻内存中。虽然这个环境区可以位于驱动程序分配的非页式存储池中，但通常它都在相关设备对象的设备扩展中。如果驱动程序使用了一个控制器对象，那么环境区就可以在此控制器扩展中。环境区的内容是由驱动程序决定的。

如果IoTimer例程与驱动程序的ISR共享其环境区，为了能安全访问共享的环境，IoTimer例程必须用驱动程序提供的SynchCritSection例程调用KeSynchronizeExecution。

如果IoTimer例程只是与非ISR驱动程序例程共享其环境区，由Context指定的共享区必须用执行自旋锁保护起来。请参见第16章中的“使用自旋锁”部分。
1.2.3 使用IoTimer例程

当相关设备对象的定时器可用时，IoTimer例程约每秒被调用一次。然而，I/O管理器是用Kernel定义的定时器对象调用驱动程序的IoTimer例程的。所以每次IoTimer例程调用的间隔最终取决于系统时钟的精度，因此IoTimer例程不会总是精确地以1秒的时间间隔被调用。

IoTimer例程最常见的应用是在事件到时停止当前IRP的设备I/O操作。在设备驱动程序中将IoTimer例程作为一个运行定时器使用时，需要考虑下列情况：

1. 当启动设备时，驱动程序将此设备扩展中的定时计数器初始化为-1，表明当前没有设备I/O操作。然后在返回STATUS_SUCCESS之前调用IoStartTimer。

每次IoTimer例程被调用时，它都要检查定时计数器是否是-1。如果是，就返回控制。

2. 驱动程序的StartIo例程将设备扩展中的定时计数器初始化为一个上限。为了防止IoTimer例程刚刚被运行，所以要给此上限加上附加的秒数。然后用SynchCritSection_1例程调用KeSynchronizeExecution，来对物理设备编程以实现当前IRP所请求的操作。

3. 在将驱动程序的DpcForIsr例程或CustomDpc例程排队前，驱动程序的ISR将定时计数器重设为-1。

4. 每次IoTimer例程被调用时，它都要检查定时计数器是否已被ISR重设为-1。如果是，就返回控制；否则，IoTimer例程用SynchCritSection_2例程调用KeSynchronizeExecution，SynchCritSection_2例程能在驱动程序决定的秒数范围内调节定时计数器。

5. 只要当前请求还没有超时，SynchCritSection_2例程就会向IoTimer例程返回TRUE。如果定时计数器变为0，SynchCritSection_2例程将定时计数器重设为一个驱动程序决定的重设超时值，在它的环境区中为它自己（和DpcForIsr）设一个重设期望标志（reset-expected flag），试着重设设备，然后返回TRUE。

如果SynchCritSection_2例程的对设备的重设操作仍然超时，它会在返回FALSE时被再次调用。如果重设成功，DpcForIsr例程根据重设期望标志确定设备已被重设并重试请求，重复第2步描述的StartIo例程的动作。

6. 如果SynchCritSection_2例程返回FALSE，由于重设的努力已经失败，IoTimer例程认为物理设备处于一种不可知的状态。在这种情况下，IoTimer例程将CustomDpc例程排队，然后返回。这个CustomDpc例程记录一个设备I/O错误，调用IoStartNextPacket，拒绝当前IRP，然后返回。

如果设备驱动程序的ISR将共享的定时计数器重设为-1（像第3步中描述的那样），那么驱动程序的DpcForIsr例程将会完成当前IRP的中断驱动的I/O处理。重设的定时计数器表示这个设备的I/O操作还没有超时，因此IoTimer例程不需要改变定时计数器。

在大多数情况下，前面的SynchCritSection_2例程只是减小定时计数器。只有在当前I/O操作超时时（即定时计数器变为0时），SynchCritSection_2例程才会试着重设设备。而且只有当重设设备失败后，SynchCritSection_2例程才会向IoTimer例程返回FALSE。

因此，前面的IoTimer例程和它的支持例程——SynchCritSection_2例程在正常情况下执行的时间非常短。用这种方式使用IoTimer例程，设备驱动程序能够保证每个有效的设备I/O请求都可以被重试（如果需要的话），它还能够保证：只有当不可修复的硬件错误使IRP无法被满足时，DpcForIsr例程才会拒绝这个IRP。此外，驱动程序执行这项功能在时间上的开销是非常小的。

然而，前面所说的简单方案要求设备每次只做一项操作，还要求驱动程序在正常情况下不做重叠的（overlap）I/O操作。执行重叠设备I/O操作的驱动程序，或使用IoTimer例程使得驱动程序分配的被发送到多个低层驱动程序链的IRP超时的高层驱动程序，需要管理更复杂的超时情况。

1.3 CustomTimerDpc例程功能

Kernel对CustomTimerDpc例程的定义如下：

VOID

(*PKEDFERRED_ROUTINE)(

IN PKDPC Dpc,

IN PVOID DeferredContext,

IN PVOID SystemArgument1, //reserved for system use

IN PVOID SystemArgument2 //reserved for system use

);

Dpc参数指明了与定时器相关的DPC对象，DeferredContext指向驱动程序提供的环境区，在CustomTimerDpc例程中SystemArgument1和SystemArgument2没有使用。详细内容请参见下一节。

CustomTimerDpc例程运行在DISPATCH_LEVEL及专用线程环境中。

运行在DISPATCH_LEVEL限定了CustomTimerDpc例程可以调用的支持例程的集合。请参见第16章中的管理硬件优先级部分。

内核定义的定时器对象的时间间隔的精度大约是10毫秒，它与系统时钟精度有关。因此，CustomTimerDpc例程可以以驱动程序决定的可变间隔调用。它的调用间隔比IoTimer例程小，后者每秒被调用一次。

1.3.1 设置CustomTimerDpc例程

每个CustomTimerDoc例程都被关联到一个内核定义的定时器对象和一个DPC对象。驱动程序调用KeInitializeDpc以创建这个DPC对象，以及调用KeInitializeTimer或KeInitializeTimerEx来创建这个定时器对象并将它关联到那个DPC对象，如第3章中的“注册和排队CustomTimerDpc例程”所叙述的那样。

作为最低限度，任何拥有一个或多个CustomTimerDpc例程的驱动程序必须为至少一个定时器对象（KTIMER类型的）提供常驻存储空间，并且为一定数量（这取决于这个驱动程序拥有多少DPC例程）的DPC对象（KDPC类型的）提供常驻存储空间。

拥有CustomTimerDpc例程的驱动程序应该为驱动程序拥有的每个驱动程序的定时器对象和DPC对象提供常驻存储空间，几乎没有例外情况。

为设置定时器对象，驱动程序必须在它的AddDevice例程中作下列事情：

1. 使用CustomTimerDop例程和指向驱动程序定义的环境（Context）区域的DeferredContext指针调用KeInitializeDpc以设置DPC对象。

2. 调用KeInitializeTimer或KeInitializeTimerEx来设置定时器对象被关联到这个CustomTimerDpc例程

设置CustomTimerDpc例程与设置CustomDpc例程非常相似，只是多了个额外的步骤来初始化定时器对象。（事实上，他们的原型是相同的。）

当然，与CustomDpc例程不同，CustomTimerDpc例程不能使用在其原型中声明的两个SystemArgument指针。在驱动程序的ISR能传递这两个指针（当驱动程序调用KeInsertQueueDpc以排队CustomDpc例程时，使用这两个指针）期间，当驱动程序调用KeSetTimer或KeSetTimerEx以激活定时器时，驱动程序不能传递这些额外的上下文指针到它的CustomTimerDpc例程。

更多信息参见第9章，“DpcForIsr例程和CustomDpc例程”，以及第3章中的“与Dpc相关的定时器对象”。关于本章内提到的任何支持例程的详细信息参见在线DDK。

1.3.2 为CustomTimerDpc例程设置环境

传递给KeInitializeDpc的DeferredContext指向其他驱动程序例程和CustomTimerDpc例程自己能在这里维护状态信息的环境区域。内核在每个对DPC例程的调用中传递DeferredContext指针。

与IoTimer例程不同，CustomTimerDpc与驱动程序创建的设备对象间没有特别的关联。然而，驱动程序可以通过在调用KeInitializeDpc时，将一个指针作为DeferredContext传递给设备对象，从而使CustomTimerDpc例程与一个驱动程序创建的设备对象相关联。

环境区必须在驱动程序分配的常驻内存中。通常，这个环境区在设备扩展中，但是它也可以在驱动程序分配的非页式存储池中。如果驱动程序使用了控制器对象，它也可以在控制器扩展中。环境区的内容是由驱动程序决定的。

如果CustomTimerDpc例程与驱动程序的ISR共享环境信息，为了安全地访问共享环境，CustomTimerDpc例程必须用SynchCritSection例程调用KeSynchronizeExecution。

如果CustomTimerDpc例程与其他非ISR驱动程序例程共享环境信息，DeferredContext处的区必须用一个执行自旋锁来保护。详见第16章的“使用自旋锁”。

1.3.3 使用CustomTimerDpc例程

在初始化了AddDevice例程中的定时器和DPC对象后，驱动程序必须显式地将CustomTimerDpc例程与一个定时器对象相关联。为了这样做，它调用KeSetTimer或KeSetTimerEx，传递指向定时器和DPC对象的指针和表示定时器到期的DueTime参数。KeSetTimer使得一旦定时器到期就调用CustomTimerDpc例程。KeSetTimerEx设置一个循环（recurring）定时器，它除了上述参数外，还要接受一个Period参数。这个参数定义了CustomTimerDpc例程应该被调用的时间间隔。

到期时间是以100纳秒为单位被指定的。正值表示从1601年1月1日起以100纳秒的间隔为单位的绝对时间。负值表示相对于当前系统时间的间隔。（调用KeQuerySystemTime以获得当前系统时间。）

当DueTime到了时，内核将CustomTimerDpc例程排队。因为这个例程可能要在队列中待不确定的时间长度，所以在CustomTimerDpc例程开始运行前的总时间可能会比驱动程序在调用KeSetTimer或KeSetTimerEx时指定的DueTime时间间隔长些。

如果在定时器到期前，系统时间有改变，那么相对定时器将不受影响，但系统会调节绝对定时器。相对定时器总是在指定个时间单位过去后到期，不管绝对系统时间如何改变。绝对定时器在明确的系统时间到期，因此对系统时间的改变会改变绝对定时器的等待。

为了使先前设立的定时器对象不可用，驱动程序调用KeCancelTimer。如果驱动程序例程用Dpc指针为它的CustomTimerDpc例程调用KeSetTimer或KeSetTimerEx，接下来用同一Timer指针调用KeCancelTimer会产生下列结果之一：

§ 如果传递给KeSetTimer或KeSetTimerEx的最近指定的时间间隔还没有到，那么定时器对象和代表CustomTimerDpc例程的DPC对象将被从队列中移出并被设为不可用。也就是说，KeCancelTimer调用撤消了对驱动程序CustomTimerDpc例程的调用。

§ 如果传递给KeSetTimer或KeSetTimerEx的最近指定的时间间隔在KeCancelTimer调用时正要到期，CustomTimerDpc例程可能会也可能不会被调用，这取决于在KeCancelTimer执行之前系统时钟中断是否使定时器对象从定时器队列中移出。

在前面指定的时间间隔到时前，用相同的Timer指针和Dpc指针再次调用KeSetTimer或KeSetTimerEx将会产生下列结果：

§ 对KeSetTimer或KeSetTimerEx的新调用为已排队的定时器对象隐式地撤消了先前指定的DueTime。

§ 在定时器对象中设置了新的DueTime。

§ 对于KeSetTimer，CustomTimerDpc例程在最近指定的DueTime到时后只被调用一次。对于一个用KeSetTimerEx设置的周期定时器，这个定时器的一个实例被删除。（因此CustomTimerDpc例程的一个执行也被删除了）

将同一个定时器对象用于不同目的会引起竞态条件或严重的驱动程序错误。例如，同一个Timer指针既被驱动程序用来建立对CustomTimerDpc例程的调用，又被用来建立在驱动程序专用线程中的等待。无论何时驱动程序专用线程用此公共Timer指针调用KeSetTimer（或KeSetTimerEx或KeCancelTimer），如果定时器对象已经在排队等待CustomTimerDpc调用，那么这个线程可能会随机地撤消掉对CustomTimerDpc例程的调用。

对于有CustomTimerDpc例程且在非专用线程环境中等待定时器对象的驱动程序：

§ 决不要在非专用线程环境中使用线程环境敏感的定时器对象，反之亦然。

§ 每个CustomTimerDpc应该有自己的相关定时器对象。在非专用线程环境中调用的每个驱动程序线程和/或驱动程序例程的集合应该有自己的“可等待”定时器对象集合。

如果使用了CustomTimerDpc例程，要慎重选择在驱动程序调用KeSetTimer或KeSetTimerEx时传递的时间间隔。此外，要考虑使用来自其他驱动程序例程的同一定时器对象调用KeCancelTimer的所有可能结果，特别是在Windows NT/Windows 2000 SMP平台上。

记住下列关于CustomTimerDpc例程和CustomDpc例程的事实：

§ 在任何时刻，只有代表一个特定的DPC例程的DPC对象的一个实例可以排队等待执行。

如果在第一个调用者指定的时间间隔到时之前，第二个驱动程序例程调用KeSetTimer或KeSetTimerEx运行同一个CustomTimerDpc例程，那么这个CustomTimerDpc例程只有在第二个调用者指定的时间间隔到时之后才会运行。在这种情况下，CustomTimerDpc例程不会做第一个例程调用KeSetTimer或KeSetTimerEx时要做的任何工作。

对于有CustomTimerDpc例程而且使用周期定时器的驱动程序：

§ 驱动程序不能从DPC例程中重新分配一个周期定时器。驱动程序只能从DPC例程中重新分配非周期定时器。

下面是既有CustomDpc例程又有CustomTimerDpc例程的驱动程序的设计策略：

§ 为了防止出现竞态条件，决不要把同一个Dpc指针传递给KeSetTimer或KeSetTimerEx和KeInsertQueueDpc。

也就是说，如果StartIo例程调用KeSetTimer或KeSetTimerEx以运行CustomTimerDpc例程，而且ISR用同一个Dpc指针从另一个处理器同时调用KeInsertQueueDpc，那么这个DPC例程将会在处理器上的IRQL降至DISPATCH_LEVEL以下或定时器间隔到时时被运行。具体是二者中的哪一个，得看哪个先到。无论哪个先到，只是由DPC例程完成为StartIo例程或ISR所做的必要工作。

此外，被两个标准驱动程序例程以很小功能差别使用的DPC的性能特性比用独立的CustomDpc和CustomTimerDpc例程时的差。DPC不得不决定究竟执行哪些操作，这取决于它被StartIo例程或ISR排队的原因。在DPC中检测这些条件需要额外的CPU循环。


Unload例程

这一章总结了驱动程序标准Unload例程的需求。本章包括以下内容：

15.1 Unload例程需求

15.2 Unload例程环境

15.3 Unload例程功能

15.3.1 PnP驱动程序的Unload功能

15.3.2 非PnP驱动程序的Unload功能

15.3.2.1 释放分配给驱动程序的资源

15.3.2.2 释放设备和控制器对象

如果想知道本章提到的支持例程的具体内容，请参见在线DDK。

1.1 Unload例程需求

任何在系统运行时可以被替换或卸载并重载的驱动程序都必须有Unload例程。

I/O管理器对Unload例程的定义如下：

VOID

(*PDRIVER_UNLOAD)(

IN PDRIVER_OBJECT DriverObject

);

DriverObject参数是传递给驱动程序的DriverEntry例程的指针。

视频、键盘或鼠标设备和能保存系统当前页文件的海量存储设备的驱动程序没有Unload例程，因为这些类型的设备必须在当前系统运行时始终可用。位于这些设备驱动程序之上的高层驱动程序也没有Unload例程。

Unload例程负责在驱动程序自身被卸载之前释放驱动程序使用的所有系统对象和资源。具体内容请参见Unload例程功能一节。

像DriverEntry例程一样，Unload例程运行在PASSIVE_LEVEL上并在系统线程的环境中被调用。

1.2 Unload例程环境

I/O管理器和PnP管理器一起工作来卸载驱动程序。要么是I/O管理器，要么是PnP管理器调用驱动程序的Unload例程。

当驱动程序被替换，或驱动程序服务的所有设备均被移除，或驱动程序初始化失败时，操作系统会卸载这个驱动程序。在卸载程序段开始时（有可能在调用驱动程序的Unload例程之前），I/O管理器或PnP管理器将此驱动程序对象和它的设备对象标记为“卸载未完”（Unload Pending）。

在驱动程序被标记为“卸载未完”后，没有其他驱动程序可以附属于这个驱动程序，也不能对此驱动程序的设备对象作任何其他引用。驱动程序可以完成未决的IRP，但I/O管理器不会向这个驱动程序发送任何新的IRP。当所有对此驱动程序及其设备对象的引用都被释放后，I/O管理器调用驱动程序的Unload例程。

因此，I/O管理器只有在下列条件均满足时，才会调用驱动程序的Unload例程：

§ 驱动程序创建的设备对象的引用均没有保留。也就是说，与底层设备相关的文件不能被打开，而且驱动程序的任何设备对象的IRP都不能是未决的。

§ 没有其他驱动程序附属于这个驱动程序。

§ 驱动程序将所有它以前注册的PnP通知（notification）撤消注册。（参见《Windows 2000驱动程序开发指南》第1卷中的IoRegisterPlugPlayNotification和IoUnregisterPlugPlayNotification）

当驱动程序从它的DriverEntry例程返回一个错误状态时，I/O管理器也会调用Unload例程。

如果驱动程序在处理完一个IRP_MN_REMOVE_DEVICE请求后，已没有其他设备对象，那么PnP管理器会调用PnP驱动程序的Unload例程。PnP驱动程序释放设备指定的资源；从设备栈中移除；删除其函数和过滤器设备对象以响应这个请求。在设备移除后，PnP管理器调用驱动程序的Unload例程，并且不再向此驱动程序发送IRP。

在系统关闭时，PnP管理器和I/O管理器均不会调用Unload例程。需要执行特殊关闭处理的驱动程序应该注册一个DispatchShutdown例程。（参见《Windows 2000驱动程序开发指南》第1卷中的IoRegisterShutdownNotification）

1.3 Unload例程功能

在Unload例程中，驱动程序必须释放所有对象和所有分配给驱动程序的资源。总之，它必须撤消相应的DriverEntry例程和Reinitialize例程在初始化驱动程序及其设备和资源时所做的所有工作。

就像PnP驱动程序的DriverEntry例程通常很简单一样，它们的Unload例程也很简单，如PnP驱动程序的Unload功能所描述。

文件系统和不处理PnP管理器的IRP_MN_REMOVE_DEVICE请求的早期驱动程序一般必须在Unload例程中释放设备指定的对象和资源。具体内容请参见非PnP驱动程序的Unload功能部分。
1.3.1 PnP驱动程序的Unload功能

PnP驱动程序释放设备指定的资源和设备对象以响应PnP设备移除IRP。PnP管理器用IoReportDetectedDevice发送代表设备的IRP，这些设备包括PnP管理器列举的所有PnP设备和驱动程序报告的根列举（root-enumerated）的早期设备。

因此，PnP驱动程序的Unload例程通常是很简单的，一般只包括一个Return语句。然而，如果驱动程序在它的DriverEntry例程中分配了驱动程序范围的资源，它必须先在其Unload例程中收回这些资源（除非它已经这么做过了）。一般，卸载PnP驱动程序的处理是同步操作。

I/O管理器用IoAllocateDriverObjectExtension释放驱动程序对象和所有驱动程序分配的驱动程序对象扩展。
1.3.2 非PnP驱动程序的Unload功能

高层文件系统和不处理PnP设备移除请求的早期驱动程序必须在它们的Unload例程中释放资源、删除设备对象并从设备栈中移除。

如果它还没有这么做，早期设备驱动程序在其Unload例程中应该做的第一件事是使设备中断无效。否则，在Unload例程正释放设备扩展（ISR需要用它来处理中断）中的资源期间，它的ISR可能被调用以处理设备中断。即使它的ISR在这种情况下能成功返回，此ISR排队的DpcForIsr或CustomDpc例程，可能还有运行在IRQL>=DISPATCH_LEVEL的其他驱动程序例程，会在Unload例程重新获得控制之前执行，这样就增加了Unload例程删除一个其他驱动程序例程还在引用的资源的可能性。请参见第16章中的“管理硬件优先级”。

在关闭了中断后，文件系统和早期驱动程序必须释放资源和对象。具体内容请参见以下两节：

§ 15.3.2.1 释放分配给驱动程序的资源

§ 15.3.2.2 释放设备和控制器对象

1.3.2.1 释放分配给驱动程序的资源

每个驱动程序怎样使用注册表、怎样在其设备扩展或控制器扩展或分配给驱动程序的非页式存储池中建立系统对象和资源都是不同的。然而，所有Unload例程都必须分阶段释放驱动程序使用的资源。

实现Unload例程时应考虑下列策略：

§ 所有驱动程序的Unload例程都必须保证：在它释放资源之前，没有其他驱动程序例程正在使用或可能很快会使用这个资源。

一般，Unload例程用下列步骤释放所有分配给驱动程序的资源：

1. 如果驱动程序还没有禁止物理设备的所有中断，那么关闭它们。一旦中断被关闭了，就调用IoDisconnectInterrupt。

2. 确保没有其他驱动程序例程能引用Unload例程将要释放的资源。

例如，如果驱动程序的IoTimer例程正为某个设备对象使用，那么Unload例程必须调用IoStopTimer。它必须保证在释放其调用对象的存储区之前，没有线程正在等待驱动程序的任何调度对象，而且它的定时器对象没有排队等待其CustomTimerDpc例程的调用。如果它有ISR可能已经排队的CustomTimerDpc例程，它必须调用KeRemoveQueueDpc，其余同理。

如果驱动程序调用了IoQueueWorkItem，它必须保证工作项（work item）已经完成。IoQueueWorkItem会取得一个对相关设备对象的引用。如果这种引用保留的话，驱动程序不能被卸载。

如果驱动程序调用了PsCreateSystemThread，Unload例程还必须使这个驱动程序创建的线程被运行，这样这个线程自身就可以在驱动程序被卸载之前调用PsTerminateSystemThread。驱动程序不能通过用PsCreateSystemThread返回的ThreadHandle调用ZwClose来释放驱动程序创建的系统线程。

3. 释放所有分配给驱动程序的设备指定的资源。这样做可能需要调用下列系统支持例程：

§ IoDeleteSymbolicLink：如果DriverEntry例程或Reinitialize例程调用了IoCreateSymbolicLink或IoCreateUnprotectedSymbolicLink；IoDeassignArcName：如果驱动程序调用了IoAssignArcName

§ ExFreePool：如果DriverEntry例程或任何其他驱动程序例程调用了ExAllocatePool，而且驱动程序还没有释放分配的内存

§ MmUnmapIoSpace：如果DriverEntry例程或Reinitialize例程调用了MmMapIoSpace

§ MmFreeNonCachedMemory：如果DriverEntry例程或Reinitialize例程调用了MmAllocateNonCachedMemory

§ MmFreeContiguousMemory：如果DriverEntry例程或Reinitialize例程调用了MmAllocateContiguousMemory

§ FreeCommonBuffer：如果DriverEntry例程或Reinitialize例程调用了AllocateCommonBuffer

§ IoAssignResourecs或IoReportResourceUsage：如果DriverEntry例程或Reinitialize例程调用了这些支持例程之一，或调用了HalAssignSlotResources以在配置注册表中为它自己和/或它的物理设备分别声明硬件资源

4. 释放DriverEntry例程或Reinitialize例程在设备对象的设备扩展或控制器对象（如果创建了的话）的控制器扩展中建立的系统对象和资源。特别地，驱动程序必须在试图删除此设备对象（IoDeleteDevice）或控制器对象（IoDeleteController）前做下列事情：

§ 调用IoDisconnectInterrupt以释放存储在相应设备或控制器扩展中的中断对象指针。

§ 如果调用了IoGetDeviceObjectPointer，并将指向低一层驱动程序的文件对象的指针存储在了设备或控制器扩展中，用这个指针调用ObDereferenceObject。

§ 如果调用了IoAttachDevice或IoAttachDeviceToDeviceStack，并将指向下层驱动程序的设备对象的指针存储在了设备或控制器扩展中，用这个指针调用IoDetachDevice。

5. 如果有的话，释放DriverEntry例程或Reinitialize例程为驱动程序的物理设备声明的硬件资源，它们位于注册表的\Registry\Machine\Hardware\ResourceMap下。

6. 删除DriverEntry例程或Reinitialize例程存储在注册表的\Registry…\DeviceMap下的所有设备名。

在驱动程序释放了设备、系统和硬件资源之后，它可以删除它的设备和控制器对象，这在下一节中详细说明。
1.3.2.2 释放设备和控制器对象

在驱动程序删除设备或控制器对象之前，它必须先释放对存储在相应设备或控制器扩展中的外部资源的引用，如指向其他驱动程序对象和/或中断对象的指针。然后它可以为每个驱动程序创建的设备对象调用IoDeleteDevice。同理，前面调用了IoCreateController的Windows 2000驱动程序必须调用IoDeleteController。

驱动程序在设备扩展中保存的Kernel定义的对象，在Unload例程用相应设备对象调用IoDeleteDevice时自动被释放。一般，对于DriverEntry例程或Reinitialize例程通过调用KeInitializeXxx建立的对象，如果驱动程序在其设备扩展中为它提供了存储区，那么它可以通过调用IoDeleteDevice自动释放。例如，如果驱动程序有CustomTimerDpc例程，而且在它的设备扩展中为必要的DPC和定时器对象提供了存储区，那么调用IoDeleteDevice可以释放掉这些系统资源。

类似地，驱动程序在控制器扩展中保存的Kernel定义的对象，在Unload例程用相应控制器对象调用IoDeleteController时自动被释放。

如果DriverEntry例程或Reinitialize例程调用了IoGetConfigurationInformation以增加对特定类型设备的记数值，Unload例程也必须调用IoGetConfigurationInformation，并在它删除相应设备对象时，在I/O管理器的全局配置信息结构中减小对此设备的记数值。

在返回控制前，Unload例程还要负责释放分配给其他驱动程序而且还没有被其他驱动程序释放的所有资源。


常见的驱动程序设计问题

这一章介绍了所有驱动程序开发者都会感兴趣的一些内容，主要包括以下几部分：

§ 总结了标准驱动程序例程运行的缺省硬件优先级（IRQL）以及在适当的IRQL上调用支持例程的一些策略

§ 关于使用自旋锁的一般策略，这些自旋锁用来同步对驱动程序例程共享的数据或资源的访问

§ 关于用内核栈和后备列表分配系统空间内存的一般策略。

§ 驱动程序应该怎样处理I/O错误，以及NTSTATUS值是怎样定义的

§ 怎样使所有或部分驱动程序映像可分页

§ 怎样注册设备接口以使其他内核模式和用户模式的代码可以访问设备

§ 怎样避免会影响驱动程序可靠性的的常见问题

这一章还讨论了设备类型决定或设计决定的设计问题，包括下列内容：

§ 对最低层设备驱动程序，是驱动程序轮询设备，还是建立一个等待Kernel定义的调度者对象的线程，即是用时间还是用信号量

§ 对于DMA或PIO驱动程序，怎样在传输操作期间维护缓存的一致性和数据的完整性

§ 对于可删除存储介质设备（removable-media）的驱动程序，怎样处理用户引起的错误（如提供了错误的存储介质或移除了在其上有文件打开的存储介质）

这一章的目录如下：

16.1 管理硬件优先级

16.2 使用自旋锁

16.2.1 为自旋锁和被保护数据提供存储空间

16.2.2 初始化自旋锁

16.2.3 调用使用了自旋锁的支持例程

16.2.4 快速释放自旋锁

16.2.5 使用自旋锁时防止错误或死锁的出现

16.3 轮询设备

16.4 管理内存的使用

16.4.1 使用系统内存

16.4.1.1 访问用户空间内存的驱动程序

16.4.1.2 为部分传输请求建立MDL

16.4.1.3 分配系统空间内存

16.4.1.4 将总线相关（Bus-Relative）的内存空间地址重新映射为虚地址

16.4.2 使用内核栈

16.4.3 使用后备列表（lookaside list）

16.5 对DMA和PIO维护缓存的一致性

16.5.1 在DMA操作期间刷新缓存数据

16.5.2 在PIO操作期间刷新缓存数据

16.6 错误记录和NTSTATUS值

16.6.1 调用IoAllocateErrorLogEntry

16.6.2 填充错误记录包

16.6.3 设置错误记录包中的NTSTATUS值

16.6.4 调用IoWriteErrorLogEntry

16.6.5 定义新的IO_ERR_XXX

16.6.6 定义私有NTSTATUS常量

16.7 处理可删除存储介质

16.7.1 响应来自文件系统的验证（Check-Verify）请求

16.7.2 通知文件系统可能的存储介质改变

16.7.3 检查设备对象中的标志

16.7.4 在中间层驱动程序中建立IRP

16.8 使设备对应用程序和驱动程序可用

16.8.1 注册设备接口

16.8.2 使设备接口可用和不可用

16.8.3 使用设备接口

16.9 可分页代码和数据

16.9.1 使驱动程序代码可分页

16.9.2 锁住可分页代码或数据

16.9.3对整个驱动程序分页

16.10 常见的驱动程序可靠性问题

16.10.1 缓冲I/O中的错误

16.10.2 引用用户空间地址时的错误

16.10.3 直接I/O中的错误

16.10.4 调用者输入和设备状态的错误

16.10.5 Dispatch例程中的错误

16.10.6 多处理器环境中的错误

16.10.7 处理IRP时的错误

1.1 管理硬件优先级

特定设备或中间层驱动程序例程运行的IRQL决定了它能调用哪些内核模式的支持例程。例如，有些支持例程要求调用者运行在为DISPATCH_LEVEL的IRQL上。其他例程在调用者运行在提高的（raised）IRQL（即高于PASSIVE_LEVEL的IRQL）时不能被安全地调用。

表16.1列出了最常见的标准驱动程序例程被调用的缺省IRQL以及Kernel定义的IRQL值（由低到高）。

表16.1 驱动程序例程的缺省IRQL

IRQL（由低到高） | 屏蔽掉的中断 | 运行在此IRQL的支持例程
PASSIVE_LEVEL | 无 | Dispatch、DriverEntry、AddDevice、Reinitialize、Unload例程、驱动程序创建的线程、工作者线程（work-thread）回调、文件系统驱动程序
DISPATCH_LEVEL | DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了。设备、时钟和电源错误中断仍可发生 | StartIo、AdapterControl、AdapterListControl、ControllerControl、IoTimer、Cancel（持有撤消自旋锁时）、DpcForIsr、CustomTimerDpc、CustomDpc例程
DIRQL | 驱动程序中断对象中所有IRQL<=DIRQL的中断。时钟和电源错误中断仍可发生 | ISR、SyncCritSection例程

当运行在下列三种IRQL之一时，由最低层驱动程序处理IRP：

§ PASSIVE_LEVEL：没有处理器中断被屏蔽掉，在驱动程序的Dispatch例程中。

DriverEntry、AddDevice、Reinitialize和Unload例程也运行在PASSIVE_LEVEL，此外还有驱动程序创建的系统线程

§ DISPATCH_LEVEL：处理器的DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了，在StartIo例程中。

AdapterControl、AdapterListControl、ControllerControl、IoTimer、Cancel（持有撤消自旋锁时）、DpcForIsr、CustomTimerDpc和CustomDpc例程也都运行在DISPATCH_LEVEL。

§ Device IRQL（DIRQL）：处理器上所有低于或等于驱动程序中断对象的SynchronizeIrql的中断都被屏蔽掉了，在ISR和SyncCritSection例程中。

当运行在下列两种IRQL时，由更高层驱动程序处理IRP：

§ PASSIVE_LEVEL：没有处理器中断被屏蔽掉，在驱动程序的Dispatch例程中。

DriverEntry、AddDevice、Reinitialize和Unload例程也运行在PASSIVE_LEVEL，此外还有驱动程序创建的系统线程、工作者线程回调或文件系统驱动程序。

§ DISPATCH_LEVEL：处理器的DISPATCH_LEVEL和APC_LEVEL中断被屏蔽掉了，在驱动程序的IoCompletion例程中。

IoTimer、Cancel和CustomTimerDpc例程也都运行在DISPATCH_LEVEL。

有时，海量存储设备的中间层和最低层驱动程序在等于APC_LEVEL的IRQL上被调用。特别是，这种情况会在文件系统驱动程序向低层驱动程序发送IRP_MJ_READ请求导致页错误时发生。

大多数标准驱动程序例程运行在仅能使它们调用适当的支持例程的IRQL上。例如，当设备驱动程序运行在等于DISPATCH_LEVEL的IRQL上时，它必须调用AllocateAdapter或IoAllocateController。由于多数设备驱动程序从StartIo例程中调用这些例程，因此它们通常运行在DISPATCH_LEVEL。

应注意的是，对于没有StartIo例程的设备驱动程序，因为它建立并管理自己的IRP队列，所以当它应该调用AllocateAdapter（或IoAllocateController）时，不一定非要运行在等于DISPATCH_LEVEL的IRQL上。这样的驱动程序必须在调用KeRaiseIrql和调用KeLowerIrql之间调用AllocateAdapter，于是当它调用AllocateAdapter时，就能运行在要求的IRQL上，而且当调用例程重新获得控制时，能够恢复初始IRQL。

为了能在适当的IRQL调用支持例程并能在驱动程序中成功地管理硬件优先级，应当注意下列情况：

§ 用低于当前IRQL的输入NewIrql值调用KeRaiseIrql会导致一个致命错误。调用KeLowerIrql以期望恢复初始IRQL（也就是，在调用KeRaiseIrql之后）也会导致一个致命错误。

§ 当运行在提高的IRQL上时，用Kernel定义的调度者对象调用KeWaitForSingleObject或KeWaitForMultipleObjects以在非零时间段中等待会导致一个致命错误。只有运行在非任意线程和PASSIVE_LEVEL的驱动程序例程（如驱动程序创建的线程、DriverEntry例程和Reinitialize例程、或像大多数设备I/O控制请求那样的同步I/O操作的Dispatch例程）能在非零时间段中安全地等待时间、信号量、互斥体或定时器。

§ 即使运行在PASSIVE_LEVEL上，可分页代码也决不能在输入Wait参数为TRUE的情况下，用它调用KeSetEvent、KeReleaseSemaphore或KeReleaseMutex。这样的调用会导致一个致命的页错误。

§ 运行在高于APC_LEVEL的IRQL上的例程既不能从页式存储池中分配内存，也不能安全地访问页式存储池中的内存。如果这样的例程引起了一个页错误，这个错误将是致命的。

§ 当驱动程序调用KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel时，它必须运行在DISPATCH_LEVEL上。

当驱动程序调用KeAcquireSpinLock时，它可以运行在低于DISPATCH_LEVEL的IRQL上，但是它必须通过调用KeRelaeseSpinLock释放这个自旋锁。也就是说，通过调用KeRelaeseSpinLockFromDpcLevel释放由调用KeAcquireSpinLock获得的自旋锁是编程错误。

当驱动程序运行在高于DISPATCH_LEVEL的IRQL上时，它绝对不能调用KeAcquireSpinLockAtDpcLevel、KeRelaeseSpinLockFromDpcLevel、KeAcquireSpinLock或KeRelaeseSpinLock。

§ 如果调用者还没有运行在这些提高后的IRQL上，调用使用了自旋锁的支持例程（如ExInterlockedXxx）会将当前处理器上的IRQL提高到DISPATCH_LEVEL或DIRQL。

§ 运行在提高IRQL上的驱动程序代码应该尽快执行。为了获得好的整体性能，例程运行的IRQL越高，就越应该将例程执行速度调得尽可能快。例如，调用KeRaiseIrql的驱动程序应当尽快地做逆调用KeLowerIrql。

请使用在线DDK参见“使用自旋锁”部分和例程相应的参考部分。

1.2 使用自旋锁

自旋锁是由内核定义的内核模式仅有（kernel-mode-only）的一种同步机制，它以一种不透明类型KSPIN_LOCK向外界输出。当在Windows NT/Windows 2000 SMP机器上同时执行并运行在提高IRQL上的例程同时访问共享数据或资源时，自旋锁用来保护这些共享数据或资源。

包括驱动程序在内的许多组件（component）都使用了自旋锁。任何类型的驱动程序可能都要使用一个或多个执行自旋锁。例如，大多数文件系统在FSD的设备扩展中使用一个互锁的工作队列，来保存由文件系统的工作者线程回调例程和FSD处理的IRP。互锁工作队列用执行自旋锁来保护，这个锁可以解决FSD中一个试图将IRP插入队列，而同时有其他线程要将IRP移出队列时所引起的问题。又如，系统软盘控制器驱动程序用两个执行自旋锁。一个保护与驱动程序设备专用线程共享的互锁工作队列，另一个用来保护三个驱动程序例程共享的定时器对象。

每个有ISR的驱动程序都使用一个中断自旋锁来保护被其ISR和其SynchCritSection例程（通常在驱动程序的StartIo和DpcForIsr例程中调用它）共享的数据或硬件。中断自旋锁与驱动程序调用IoConnectInterrupt时创建的中断对象集相关，在《注册ISR》部分对此有详尽的阐明。

在驱动程序中使用自旋锁时，应遵守下列规则：

§ 在常驻系统空间内存（非页式存储池，如图16.3所示）中，为自旋锁保护的所有数据或资源和相应的自旋锁提供存储空间。驱动程序必须为它使用的所有执行自旋锁提供存储空间。然而，设备驱动程序不需要为中断自旋锁提供存储空间，除非它有多重矢量（multivector）ISR或者有一个以上的ISR，在注册ISR部分对此有详尽的阐明。

§ 在使用驱动程序提供存储空间的每个自旋锁，以同步对被保护的共享数据或资源的访问之前，先要调用KeInitializeSpinLock来初始化这些自旋锁。

§ 在适当的IRQL上调用每个使用了自旋锁的支持例程。一般，对于执行自旋锁，IRQL<=DISPATCH_LEVEL；对于与驱动程序中断对象相关的中断自旋锁，IRQL<=DIRQL。

§ 实现例程时，应使其在持有自旋锁时尽快地执行。所有例程持有自旋锁的时间都不应超过25毫秒。

§ 实现例程时注意，当它持有自旋锁时一定要避免做下列事情：

§ 引起硬件异常或软件异常

§ 试图访问可分页内存

§ 做可能引起死锁或自旋锁持有时间超过25毫秒的递归调用

§ 试图获得另一个自旋锁（这样做可能会导致死锁）

§ 调用一个违反了上述任一条规则的外部例程

参见下列部分以更深入地了解这些规则：

§ 16.2.1 为自旋锁和被保护数据提供存储空间

§ 16.2.2初始化自旋锁

§ 16.2.3调用使用了自旋锁的支持例程

§ 16.2.4快速释放自旋锁

§ 16.2.5使用自旋锁时防止错误或死锁的出现
1.2.1 为自旋锁和被保护数据提供存储空间

作为设备启动工作的一部分，驱动程序必须在下列各处之一为所有自旋锁保护的数据或资源以及相应的自旋锁分配常驻存储空间：

§ 驱动程序通过调用IoCreateDevice建立的设备对象的设备扩展

§ 驱动程序通过调用IoCreateController建立的控制器对象的控制器扩展

§ 驱动程序通过调用ExAllocatePool获得的非页式系统空间内存

当持有自旋锁时，如果试图访问可分页数据而这一页不在内存中，就会导致一个致命的页错误。引用无效自旋锁（原来被保存在可分页内存中，而现在它所在的页已被调出内存（paged-out））也会导致一个致命的页错误。

驱动程序必须为下列各种可能用到的执行自旋锁提供存储空间：

§ 调用了KeAcquireSpinLock和KeRelaeseSpinLock 或者调用了KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel的非ISR驱动程序用来同步对驱动程序定义数据的访问的所有自旋锁

§ 通过调用资源确定的ExInterlockedXxx例程集来同步对驱动程序分配资源的访问的所有自旋锁

驱动程序可以从其ISR或SynchCritSection例程调用ExInterlocked..List例程，然而当它运行在高于DISPATCH_LEVEL的IRQL上时，它不能调用KeAcquireSpinLock和KeRelaeseSpinLock 或者KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel。因此，所有在调用Ke..SoinLock和ExInterlockedXxx时重用了自旋锁的驱动程序，都必须在运行的IRQL低于DISPATCH_LEVEL时做每个调用。

驱动程序可以将相同的自旋锁传递给ExInterlockedInsertHeadList，就像传递给另一个ExInterlocked Xxx例程一样，这样做的前提是两个例程在相同的IRQL上使用此自旋锁。如果想深入了解自旋锁的使用对性能有何影响，请参见快速释放自旋锁一节。

除了为其执行自旋锁提供存储空间以外，如果设备驱动程序有多重矢量ISR或一个以上的ISR，那么它还必须为与其中断对象相关的另一个自旋锁提供存储空间。
1.2.2 初始化自旋锁

在调用需要访问调用者提供的执行自旋锁的支持例程之前，驱动程序必须调用KeInitializeSpinLock来初始化相应的执行自旋锁。需要初始化执行自旋锁的支持例程如下：

§ KeAcquireSpinLock和随后的KeRelaeseSpinLock

§ KeAcquireSpinLockAtDpcLevel和随后的KeRelaeseSpinLockFromDpcLevel

§ ExInterlocked Xxx例程

在调用IoConnectInterrupt和KeSynchronizeExecution之前，最低层驱动程序必须调用KeInitializeSpinLock以初始化由它提供存储空间的中断自旋锁。
1.2.3 调用使用了自旋锁的支持例程

调用KeAcquireSpinLock可以将当前处理器上的IRQL设为DISPATCH_LEVEL，直到用对应的KeRelaeseSpinLock调用将此IRQL恢复到改变前的值为止。因此，当驱动程序调用KeAcquireSpinLock时，它必须在低于DISPATCH_LEVEL的IRQL上执行。

KeAcquireSpinLockAtDpcLevel和KeRelaeseSpinLockFromDpcLevel的调用者运行得更快一些，因为它们已经运行在DISPATCH_LEVEL上，所以这些支持例程不需要将当前处理器上的IRQL重新设置。因此，在大多数Windows NT/Windows 2000平台上，当运行在低于DISPATCH_LEVEL的IRQL上时，调用KeAcquireSpinLockAtDpcLevel是个致命的错误。通过调用KeRelaeseSpinLockFromDpcLevel来释放用KeAcquireSpinLock获得的自旋锁也是一个致命的错误，因为没有恢复调用者的初始IRQL。

持有执行自旋锁的例程（如ExInterlocked Xxx）通常运行在DISPATCH_LEVEL上，直到它们释放了这个自旋锁并向调用者返回控制为止。然而，只要传给ExInterlockedXxx集的自旋锁是被驱动程序的ISR和SynchCritSection例程排他地使用，这个ISR这个SynchCritSection例程（运行在DIRQL）就可以调用其中的某个ExInterlockedXxx例程（如ExInterlocked..List例程）。

持有中断自旋锁的例程运行在相关中断对象集的DIRQL上。因此，驱动程序绝对不能从它的ISR或SynchCritSection例程中调用KeAcquireSpinLock和KeRelaeseSpinLock例程，也不能调用其他任何使用了执行自旋锁的例程。这种调用会导致系统死锁，需要用户重新启动他的计算机。还应注意，如果驱动程序的ISR或SynchCritSection例程调用了ExInterlocked..List例程，那么此驱动程序就不能在它调用Ke..SpinLock或Ke..SpinLock..DpcLevel时重用它传给ExInterlocked..List例程的自旋锁。

如果驱动程序有多重向量ISR或多个ISR，当运行IRQL高于相关中断对象指定的SynchronizeIrql值时，它可以调用KeSynchronizeExecution。

请参见“管理硬件优先级”。如果想对管理支持例程确定的IRQL需求有更多的了解，请参见在线DDK。
1.2.4 快速释放自旋锁

将驱动程序持有自旋锁的时间最小化可以很明显地改善驱动程序和系统整体的性能。例如，图16.1表示了中断自旋锁怎样保护SMP机器上必须被ISR和StartIo及DpcForIsr例程共享的设备确定的数据。

图16.1 使用中断自旋锁

1. 驱动程序的ISR运行在一个处理器的DIRQL上，而它的StartIo例程运行在第二个处理器的DISPATCH_LEVEL上。内核中断处理者在驱动程序的设备扩展中持有此驱动程序ISR的InterruptSpinLock，它用来访问设备确定的被保护数据，如设备寄存器（SynchronizeContext）的状态或指针。已准备好访问SynchronizeContext的StartIo例程调用KeSynchronizeExecution，传递指向相关中断对象的指针、共享SynchronizeContext和驱动程序的SynchCritSection例程（图16.1中的AccessDevice）。

KeSynchronizeExecution在第二个处理器上一直循环以防止AccessDevice访问SynchronizeContext，直到ISR返回（从而释放了驱动程序的InterruptSpinLock）时为止。然而，KeSynchronizeExecution还提高了第二个处理器上的IRQL，使它等于中断对象的SynchronizeIrql的值，从而防止了在这个处理器上发生其他设备中断，因此ISR一返回，AccessDevice就可以运行在DIRQL上。不过，其他设备的更高级的DIRQL中断、时钟中断和电源错误中断仍可以在两个处理器中的任一个上发生。

2. 当ISR将驱动程序的DpcForIsr排队并返回时，第二个处理器上的AccessDevice运行在等于相关中断对象SynchronizeIrql值的IRQL上，而且访问了SynchronizeContext。同时，另一个处理器上的DpcForIsr运行在DISPATCH_LEVEL。DpcForIsr也已准备好访问SynchronizeContext，因此它调用KeSynchronizeExecution，调用参数与步骤1中StartIo例程的参数相同。

当KeSynchronizeExecution获得自旋锁并代表StartIo例程运行AccessDevice时，驱动程序提供的同步例程AccessDevice可以排他地访问SynchronizeContext。因为AccessDevice运行在SynchronizeIrql值指定的IRQL上，所以驱动程序的ISR直到自旋锁被释放时，才能获得此自旋锁并访问相同的存储区，否则，即使AccessDevice正在运行时另一个处理器上发生了设备中断也不行。

3. AccessDevice返回时释放自旋锁。StartIo例程继续在第二个处理器的DISPATCH_LEVEL上运行。现在KeSynchronizeExecution在第三个处理器上运行AccessDevice，因此它可以代表DpcForIsr访问SynchronizeContext。然而，如果设备中断在第2步中DpcForIsr调用KeSynchronizeExecution之前就发生了，那么此ISR可能会在KeSynchronizeExecution获得自旋锁并在第三个处理器上运行AccessDevice之前在另一个处理器上运行。

如图16.1所示，当一个处理器上运行的例程持有自旋锁时，其他每个试图获得此自旋锁的例程都无法成功。每个试图获得已占用自旋锁的例程都在其当前处理器上循环，直到持锁者释放了这个自旋锁为止。一个自旋锁被释放后，有且只有一个例程能够获得它，没有获得此自旋锁的其他各例程将继续循环。

任何自旋锁的持锁者都运行在提高IRQL上，对于执行自旋锁，在DISPATCH_LEVEL；对于中断自旋锁，在DIRQL。KeAcquireSpinLock的调用者运行在DISPATCH_LEVEL上，直到它们调用KeRelaeseSpinLock为止。KeSynchronizeExecution的调用者自动将当前处理器上的IRQL提高为中断对象的SynchronizeIrql值，直到调用者提供的SynchCritSection例程退出且KeSynchronizeExecution返回控制为止。请参见调用使用自旋锁的支持例程。

记住下列使用自旋锁的规则：

§ 在被自旋锁持有者占用或其他例程占用的处理器集合上，运行在低级IRQL上试图获得相同自旋锁的代码将无法实现其目的。

因此，最小化驱动程序持锁时间可以极大地改善驱动程序的性能和系统的整体性能。

如图16.1所示，在多处理器机上，Knernel中断处理者按“先到先服务”的原则执行那些在相同IRQL上运行的例程。Knernel还要做下列事情：

§ 当驱动程序例程调用KeSynchronizeExecution时，Knernel使驱动程序的SynchCritSection例程运行在调用KeSynchronizeExecution的处理器上。（见步骤1和3）

§ 当驱动程序的ISR将其DpcForIsr排队时，Knernel使DPC运行在IRQL低于DISPATCH_LEVEL的第一个可用的处理器上。它不一定是IoRequestDpc调用发生的那个处理器。（见步骤2）

在单处理器机上，驱动程序中断驱动的I/O操作可能需要串行化。但是在SMP机上，同样的操作完全可以真正异步实现。如图16.1所示，在驱动程序的DpcForIsr开始处理那些ISR已经为其处理设备在CPU1上中断的IRP之前，此驱动程序的ISR可以运行在SMP机中的CPU4上。

也就是说，在DpcForIsr例程或CustomDpc例程运行之前，中断自旋锁不能阻止：ISR在运行于一个处理器上时保存的操作指定数据，在另一个处理器上发生设备中断时被此ISR写覆盖。

尽管驱动程序可以试着将所有中断驱动的I/O操作串行化以保存ISR收集的数据，但是这个驱动程序在SMP机器上的运行不会比在单处理器机上快多少。在保持Windows NT/Windows 2000单处理器和多处理器平台之间可移植性的前提下，为了获得尽可能好的性能，驱动程序应该用其他技术保存那些由ISR获得的以供DpcForIsr随后处理的操作指定数据。

例如，ISR可以在它传给DpcForIsr的IRP中保存操作指定的数据。对这种方法的一种改进是：将DpcForIsr实现为可以查询ISR增加的计数值（ISR-augmented count），用ISR提供的数据来处理计数值代表的IRP个数，然后在返回前将计数值重置为0。当然，必须用驱动程序的中断自旋锁来保护这个计数值，因为驱动程序的ISR和SynchCritSection例程会动态改变它的值。
1.2.5 使用自旋锁时防止错误或死锁的出现

驱动程序持有自旋锁时，只要它引起了硬件或软件异常，系统性能就会下降。这也就是说，驱动程序的ISR和驱动程序在调用KeSynchronizeExecution时提供的任何SynchCritSection例程，都不能引起页错误或算法异常这样的错误或陷阱，也不能引起软件异常。调用KeAcquireSpinLock的例程在释放了它的执行自旋锁而且不再运行在DISPATCH_LEVEL上之前，也不能引起硬件或软件异常。

可分页数据和支持例程

持有自旋锁时，驱动程序决不能调用任何访问可分页数据的例程。记住：驱动程序可以访问某些访问可分页数据的支持例程，当且仅当此调用发生时驱动程序运行在低于DISPATCH_LEVEL的IRQL上。对IRQL的这个限定使得驱动程序在持有自旋锁时不可能调用这些支持例程。如果想对某个具体的支持例程的IRQL需求有更多了解，请在在线DDK上参见此例程的相应参考部分。

递归

试图递归地获得自旋锁必然会引起死锁：递归例程的持有实例在第二个实例循环，以试图获得相同自旋锁时，不会释放此自旋锁。

在递归例程中使用自旋锁应遵守下列策略：

递归例程决不能在持有自旋锁时调用它自己，也决不能在递归调用时试图获得相同的自旋锁。

当递归例程持有自旋锁时，如果递归可能导致死锁或可能使调用者的持锁时间超过25毫秒，那么另一个驱动程序例程决不能调用这个递归例程。

如果想对递归驱动程序例程有更多的了解，请参见“使用内核栈”。

获得嵌套自旋锁

当持有自旋锁时，试图获得第二个自旋锁也会导致死锁或很差的驱动程序性能。

在实现持有自旋锁的驱动程序时，应该遵守下列策略：

§ 驱动程序决不能调用使用自旋锁的支持例程，除非能保证不会发生死锁。

§ 即使不会死锁，驱动程序也不应该调用使用自旋锁的支持例程，除非替代它的程序技术无法提供同等的驱动程序性能和功能。

§ 如果驱动程序做了嵌套调用以获得自旋锁，它必须以相反的顺序释放那些自旋锁。也就是说，如果驱动程序在获得自旋锁B之前获得了自旋锁A，那么它必须先释放B，后释放A。

一般情况下，应避免使用嵌套自旋锁来保护重叠的共享数据和资源的子集或离散集（discrete set）。应当考虑：如果驱动程序使用两个执行自旋锁来保护离散资源（比如，可能由不同驱动程序例程来单独或共同设置的一对定时器对象），那么可能会发生什么情况。在SMP机上，当两个各持有一个自旋锁的例程中的一个试图获得对方的自旋锁时，驱动程序会间歇地发生死锁。

即使能够设计出不会死锁的驱动程序来使用嵌套自旋锁，它也很难成功地实现。在Windows NT/Windows 2000 SMP机器上，很难充分地调试并检测嵌套自旋锁。此外，使用嵌套自旋锁会极大地降低驱动程序和系统的性能。

1.3 轮询设备

除非不得已，否则设备驱动程序应该尽量避免轮询（pulling）其设备，而且设备驱动程序不该用整时间片轮询。轮询设备是一项开销很大的操作，它使操作系统在做轮询的驱动程序内受计算限制（compute-bound）。要做很多轮询的设备驱动程序与其他设备上的I/O操作相冲突，从而使系统变得很慢，甚至对用户不做响应。

现在开发的设备和运行Windows NT/Windows 2000的处理器一样，它们技术先进，很少需要驱动程序轮询它的设备以确保设备已准备好启动I/O操作或操作完成。

不过，有些仍在使用的设备是以前设计的，它们和数据总线窄、时钟速率慢的老式处理器一起协同工作。老式处理器上的操作系统执行同步I/O，而且是单用户单任务的。这样的设备可能需要轮询或用其他方式等待设备更新它的寄存器，特别是对Windows NT/Windows 2000来说，因为它们是被设计为在具有宽数据总线和快速时钟速率的新型处理器上做异步I/O的。

虽然通过编写一个增加计数器的简单循环来解决慢速设备的问题似乎可行（这样可以在设备更新寄存器时，“浪费”少量的时间），但这样的驱动程序往往不能在Windows NT/Windows 2000平台之间移植。需要为每个Windows NT/Windows 2000平台分别配置循环计数器的最大值。而且，如果驱动程序是用优化非常好的编译器编译的，编译器可能会移除驱动程序的记数变量和增加计数器的那段循环。

如果驱动程序必须在设备硬件更新状态时停下等待，应遵照下列实现策略：

§ 驱动程序可以在读设备寄存器之前调用KeStallExecutionProcessor。驱动程序应该最小化它的等待时间间隔，而且等待时间间隔一般应该不超过50毫秒。

KeStallExecutionProcessor时间间隔的单位为1毫秒。

如果设备更新状态的时间经常超过50毫秒，可以考虑在驱动程序中建立一个设备专用线程。
1.3.1 驱动程序线程

慢速设备或很少使用设备（如软盘控制器）的驱动程序可以通过创建一个设备专用的系统线程来解决很多等待问题。类似的，大多数文件系统驱动程序使用系统工作者线程，并提供工作者线程回调例程。线程可以调用KeDelayExecutionThread等待完整时间片长度或更长时间的间隔。

KeDelayExecutionThread等待时间间隔的单位大约是10毫秒。因为KeDelayExecutionThread是定时器驱动的例程，其等待间隔的单位会比10毫秒稍快或稍慢些，这取决于操作系统平台。然而，对此例程的调用是可移植的，因为指定的时间增量是常量。

如果设备驱动程序有自己的线程环境或运行于系统线程环境中，设备专用线程或最高层驱动程序的工作者线程回调例程，可以在驱动程序设备扩展的共享通信区中，同步Kernel定义的调度者对象（如事件或信号量）上的同步操作。当其设备没有使用时，设备专用线程可以在共享调度者对象上等待，例如通过用信号量调用KeWaitForSingleObject来等待。在调用这种设备驱动程序来执行I/O操作并将信号量设为Signaled状态之前，它的等待线程不占用CPU时间。

驱动程序可以通过调用KeSetBasePriorityThread来设置它用PsCreateSystemThread创建的驱动程序专用或设备专用线程的基优先级（base priority）。驱动程序应该将优先级指定为能避免在SMP机上运行时优先级倒置（runtime priority inversion）的值。将驱动程序创建的线程的基优先级设得过高，会延迟提交I/O请求给驱动程序的低优先级线程的执行。

1.4 管理内存的使用

许多驱动程序只是将分配给其设备对象的设备扩展的内存用作其全局存储区；只是将其在IRP中的I/O栈用作操作指定的本地存储区。然而，驱动程序可以按需要分配额外的系统空间内存，而且可以用内核栈在调用内部驱动程序例程时传递少量的数据。
1.4.1 使用系统内存

图16.2表示Windows NT/Windows 2000虚内存空间及它们与系统物理内存的关系。

图16.2 虚内存空间和物理内存

如图16.2所示，虚内存实际对应的是分页的物理内存，虚地址范围实际对应的是CPU中不邻接的页。用户空间虚内存和从页式存储池中分配的系统空间内存总是可分页的。也就是说，任何非当前处理及其数据都可以分页到辅助存储区中去，通常是磁盘上。

图16.2中的高位空间（hyperspace）是系统空间地址的专用区，内存管理器用它将当前处理的虚地址空间映射为CPU中的一系列物理页。注意：任何非当前处理的虚地址都是不可见的，因此它的内存空间是不可访问的。
1.4.1.1 访问用户空间内存的驱动程序

驱动程序不能分配用户空间的虚内存，因为它们运行在内核模式。此外，驱动程序不能通过用户模式的虚地址访问内存，除非它正运行在引起驱动程序当前I/O操作的用户模式线程环境中而且它正在使用此线程的虚地址。

只有最高层驱动程序（如FSD）可以保证它们的Dispatch例程会在这样的用户模式线程环境中被调用。最高层驱动程序可以在为低层驱动程序建立IRP之前调用MmProbeAdnLockPages以锁住（lock down）用户缓冲。

最低层驱动程序和为缓冲或直接I/O建立设备对象的中间层驱动程序，可以依赖I/O管理器或最高层驱动程序，来在IRP中传递对被锁用户缓冲或系统空间缓冲的合法访问。
1.4.1.2 为部分传输请求建立MDL

如果传输请求太大以致于下层设备驱动程序无法处理，那么高层驱动程序可以调用IoBuildPartialMdl，为下层设备驱动程序建立部分传输IRP队列。

如果最高层驱动程序不能在一台内存有限的计算机上用MmProbeAndLockPages锁住整个用户缓冲，初始请求也必须被分割成部分传输。对这种大传输请求，最高层驱动程序不能做下列事情：

1. 调用IoBuildSynchronousFsdRequest来分配部分传输IRP并锁住用户缓冲的一部分。通常加锁区的大小要么是PAGESIZE的倍数，要么是下层设备的传输容量。

2. 如果低层驱动程序返回STATUS_PENDING，就用部分传输IRP调用IoCallDriver，并调用KeWaitForSingleObject，以等待驱动程序建立与其部分传输IRP相关的事件对象。

3. 当它重新获得控制时，重复步骤1和2，直到所有数据都被传输为止，然后完成初始IRP。

必须处理很大传输请求的最高层设备驱动程序可以使用前述技术，简单地用它分配的部分传输IRP调用它自己。除此之外，还有另一种方案，其中最高层设备驱动程序要做下列事情：

1. 调用IoAllocateMdl来分配描述用户缓冲的一部分MDL。

2. 调用MmProbeAndLockPages来锁住这部分用户缓冲。

3. 给这部分用户缓冲传输数据。

4. 调用MmUnlockPages，做下列事情之一：

§ 如果驱动程序在步骤1中分配的MDL非常大，足够下次传输，调用MmPrepareMdlForReuse并重复步骤2到4。

§ 否则，调用IoFreeMdl并重复步骤1到4。

5. 所有数据都被传输后，调用MmUnlockPages和IoFreeMdl。
1.4.1.3 分配系统空间内存

图16.2所示的系统空间虚内存由有限个页式存储池和更少的非页式存储池组成。

非页式存储池总是常驻的。因此，运行在任何级别的IRQL时，它都可以被安全地访问。

对于驱动程序，页式存储池只有在下列条件下，才能被分配和访问：

§ 使用对应的页式存储池虚地址的例程必须运行在低于APC_LEVEL的IRQL上。如果运行在高于APC_LEVEL的IRQL上时发生了页错误，那么它将是一个致命的错误。参见管理硬件优先级部分以了解更多关于IRQL的内容。

除了驱动程序或设备初始化，或者卸载（有时可能发生）以外，最低层和中间层驱动程序很少从页式存储池中分配内存，因为这些类型的驱动程序通常运行在高于APC_LEVEL的IRQL上。这样的驱动程序分配的任何可分页存储区只能被驱动程序创建的线程或DriverEntry、AddDevice、Reinitialize（如果有的话）和Unload（如果有的话）例程安全访问，这些线程或例程可以用页式存储池分配方式来存放只在驱动程序或设备初始化、或者卸载时需要的数据、对象和资源。

因为有些标准驱动程序例程运行在高于APC_LEVEL的IRQL上，所以从页式存储池中分配的内存对大多数中间层或设备驱动程序例程是不可访问的。例如，高层驱动程序的IoCompletion例程在专用线程环境中和（通常）DISPATCH_LEVEL上执行。这样的驱动程序决不应为将被IoCompletion例程访问的数据分配可分页存储区。请参见“管理硬件优先级”。

分配驱动程序缓冲空间

为了分配I/O缓冲空间，驱动程序可以调用MmAllocateNonCachedMemory、MmAllocateContiguousMemory、AllocateCommonBuffer（如果驱动程序的设备使用总线控制器DMA或系统DMA控制器的自动初始化模式）或ExAllocatePool。

系统运行时，非页式存储池往往会变成很多内存碎片，因此驱动程序的DriverEntry例程应该调用这些例程以建立驱动程序需要的长期I/O缓冲。这些例程（可能除了ExAllocatePool）均在处理器指定的边界（由处理器的数据缓存范围（data-cache-line）的大小决定）内分配内存以避免发生缓存及一致性问题。

驱动程序应尽可能节省地分配它们的内部I/O缓冲（如果有的话），因为非页式存储池是很有限的系统资源。一般，驱动程序应该避免重复调用这些支持例程来请求小于PAGE_SIZE的分配。

为了节省地分配I/O缓冲内存，记住下列事实：

§ 每次调用MmAllocateNonCachedMemory至少占用非页式系统空间内存中的一整页，无论请求分配多大的存储区。对于小于一页的请求，页中余下的字节都被浪费掉了：调用MmAllocateNonCachedMemory的驱动程序不可访问它，它也不能被其他内核模式的程序使用。

§ 如果指定的字节个数少于或等于一页，调用MmAllocateContiguousMemory分配至多一页的存储区。对于大于一页的请求，最后分配的页中剩余的字节被浪费：调用MmAllocateContiguousMemory的驱动程序不可访问它，它也不能被其他内核模式的程序使用。

§ 调用AllocateCommonBuffer至少使用一个适配器对象映射寄存器，它至少映射1字节，最多映射一页。如果想对映射寄存器和使用公用缓冲有更多的了解，参见第3章中的“适配器对象和DMA部分”。

用ExAllocatePool 或ExAllocatePoolWithTag分配内存

驱动程序也可以调用ExAllocatePool 或ExAllocatePoolWithTag，将参数PoolType指定为下列系统定义的值之一：

§ NonPagedPoolCacheAligned：驱动程序使用永久的I/O缓冲。如SCSI类驱动程序为请求检测（request-sense）数据开辟的缓冲

驱动程序应当调用MmAllocateNonCachedMemory或MmAllocateContiguousMemory分配永久I/O缓冲。

§ NonPagedPoolCacheAlignedMustS：临时但非常重要的I/O缓冲。如存放物理设备初始化数据的缓冲，这些数据在系统启动时要用。

§ NonPagedPool：没有存储在设备扩展或控制器扩展中的对象或资源，驱动程序可能会在运行IRQL高于APC_LEVEL时访问它们。

当PoolType取了这个值时，如果指定的NumberOfBytes小于或等于PAGE_SIZE，ExAllocatePool 或ExAllocatePoolWithTag就按需分配内存。否则，最后分配的页中剩余的字节被浪费：调用者不可访问它，它也不能被其他内核模式的程序使用。

例如，在x86机上，一个5K的分配请求会获得两个4K的页。第2页中余下的3K不能被调用者或其他调用者使用。为了避免浪费非页式存储池，驱动程序应该有效地分配多页。比如在这种情况下，驱动程序可以做两次分配，一次大小等于PAGE_SIZE，另一次等于1K，加起来共分配了5K。

§ NonPagedPoolMustSucceed：临时但非常重要的存储区，驱动程序会尽快释放它。如驱动程序用来修复错误的内存，否则错误会使系统瘫痪。

§ PagedPoolCacheAligned：文件系统的I/O缓冲。驱动程序将它锁住，然后下层海量存储设备驱动程序在请求DMA传输的IRP中传递它。

§ PagedPool：如果缓冲将在调用者返回之前释放，DriverEntry或Reinitialize例程可用此值开辟一个临时缓冲，用来保存初始化时必需的对象、数据或资源。此值也可用来开辟只能被一个或多个驱动程序创建的线程访问的存储区。

如果缓冲将在Unload例程返回控制之前被释放，那么驱动程序的Unload例程也可以从页式存储池中分配内存。

因为必须成功（must-succeed）存储池是非常有限的系统资源，驱动程序应该通过调用ExFreePool尽快释放分配的空间。大多数驱动程序不应该用值为NonPagedPoolMustSucceed或NonPagedPoolCacheAlignedMustS的PoolType参数调用ExAllocatePool 或ExAllocatePoolWithTag，除非是如果驱动程序的分配请求不成功，系统就不继续运行。如果PoolType参数指定为这些值，ExAllocatePool会在系统无法分配请求的内存时，使系统终止运行。

对其他PoolType参数值，如果不能分配请求的NumberOfBytes字节的内存，ExAllocatePool 或ExAllocatePoolWithTag返回NULL指针。驱动程序应该检查返回的指针。如果它的值为NULL，DriverEntry例程（或其他任何返回NTSTATUS的驱动程序例程）应该返回STATUS_INSUFFICIENT_RESOURCES或处理错误（可能的话）。参见“错误记录和NTSTATUS值”。

对于CacheAligned类的PoolType参数值，ExAllocatePool 或ExAllocatePoolWithTag在处理器指定的边界（由处理器的数据缓存范围的大小决定）内分配内存以避免发生缓存及一致性问题。
1.4.1.4 将总线相关的内存空间地址重新映射为虚地址

有些处理器有独立的内存和I/O地址空间，而有些没有。由于硬件平台上的这些差异，Windows 2000和WDM驱动程序用来访问常驻I/O或常驻内存的设备资源的机制因平台而异。

驱动程序请求设备I/O和内存资源来响应PnP管理器的IRP_MN_QUERY_RESOURCE_REQUIREMENTS的IRP。根据硬件结构的不同，HAL可以在I/O空间或内存空间分配I/O资源，也可以在I/O空间或内存空间分配内存资源。

如果HAL用总线相关内存空间来访问设备资源（如设备寄存器），驱动程序必须将I/O空间映射到虚内存，这样它就可以访问这些资源。驱动程序可以通过检查PnP管理器在设备启动时传给驱动程序的被映射资源，来确定资源是常驻I/O的，还是常驻内存的。如果HAL用I/O空间，不需要做映射。

具体地说，当驱动程序接收到一个IRP_MN_START_DEVICE请求时，它应该检查IrpSp->Parameters.StartDevice.AllocatedResources和 IrpSp->Parameters.StartDevice.AllocatedResourcesTranslated结构，它们分别描述了初始和映射后的PnP管理器分配给设备的资源。驱动程序应该在设备扩展中保存每个资源列表的拷贝，以供调试时辅助使用。

资源列表是成对的CM_RESOURCE_LIST结构，其中初始列表的每个元素都对应着转换后列表的相同元素。例如，如果AllocatedResources.List[0] 描述初始I/O端口范围，那么AllocatedResourcesTranslated.List[0]就描述了转换后的相同范围。每个被转换资源都包括物理地址和资源类型。

如果驱动程序被分配了一个转换的内存资源（CmResourceTypeMemory），它必须调用MmMapIoSpace将物理地址映射为可用来访问设备寄存器的虚地址。对以平台无关方式操作的驱动程序，如果需要的话，它应该检查每个返回的、转换后的资源并将其映射。

以下是每个驱动程序在响应IRP_MN_START_DEVICE以确保能访问所有设备资源时，都应该采取的步骤：

1. 在设备扩展中复制IrpSp->Parameters.StartDevice.AllocatedResources。

2. 在设备扩展中复制IrpSp->Parameters.StartDevice.AllocatedResourcesTranslated。

3. 在循环里，检查AllocatedResourcesTranslated中的每个描述元素。如果描述资源类型是CmResourceTypeMemory，调用MmMapIoSpace，传递物理地址和转换后资源的长度。

当驱动程序收到来自PnP管理器的IRP_MN_STOP_DEVICE或IRP_MN_REMOVE_DEVICE请求时，它必须在类似循环中通过调用MmUnmapIoSpace释放映射。如果驱动程序必须拒绝IRP_MN_START_DEVICE请求，它也应该调用MmUnmapIoSpace。

初始资源类型表明驱动程序应当调用哪个HAL访问例程（READ_REGISTER_Xxx、WRITE_REGISTER_Xxx 、READ_PORT_Xxx、 WRITE_PORT_Xxx）。大多数驱动程序不需要检查初始资源列表以确定用这些例程中的哪一个，因为驱动程序本身已请求了这个资源，或者驱动程序开发者在已知设备硬件性质时已经确定了所需的类型。

对于I/O空间中的资源（CmResourceTypePort、CmResourceTypeInterrupt、CmResourceTypeDma），驱动程序应该用返回的物理地址的低32位访问设备资源（例如，通过HAL的READ_REGISTER_Xxx、WRITE_REGISTER_Xxx 、READ_PORT_Xxx、 WRITE_PORT_Xxx读写例程）。
1.4.2 使用内核栈

当驱动程序可以向其内部例程传递数据时，Windows 2000内核模式栈的大小约为两页。因此，驱动程序不能在内核栈上传送大量的数据。

为了避免用尽内核模式栈的空间，遵守以下设计规则：

避免从一个内部驱动程序例程中深度嵌套调用另一个，如果它们每个都要在内核栈上传送数据的话。

如果驱动程序设计中用到了递归例程，注意限制递归调用发生的次数。

也就是说，驱动程序的调用树结构应该比较平坦。由于非页式存储池也是有限的系统资源，因此驱动程序最好分配系统空间缓冲，而不是用尽内核栈空间。

Windows 2000内核模式栈是在缓存中，因此驱动程序不能用DMA在栈上传送数据。

为了避免DMA数据分配和/或数据完整性问题，遵守以下设计规则：

决不要试图用DMA在内核栈上传送数据。

DMA设备的驱动程序可以通过调用ExAllocatePool 或ExAllocatePoolWithTag获得一个NonPagedPoolCacheAligned类型的缓冲，从而缓冲要被传输的数据（如果有的话）。有些驱动程序可以通过使用公用缓冲DMA来做到这些。参见第3章中的“公用缓冲系统DMA或公共总线控制器DMA”。
1.4.3 使用后备列表

必须动态分配固定大小的缓冲以执行要求的I/O操作的Windows 2000和WDM驱动程序，可以使用Ex..LookasideList支持例程。在这样的驱动程序初始化了其后备列表后，OS会在驱动程序的后备列表中占有某些动态分配的、指定大小的缓冲，高效地为此驱动程序保留了一系列可重用的、固定大小的缓冲。驱动程序在其后备列表中的固定大小缓冲的格式和内容是由驱动程序决定的。

例如，必须为下层SCSI端口/微端口（miniport）驱动程序建立SCSI请求块（SRB）的存储类驱动程序使用了后备列表。这样的类驱动程序从它的后备列表中按需为SRB分配缓冲，并且只要SRB在完成的IRP中返回类驱动程序，就释放每个SRB缓冲到后备列表中。由于驱动程序上的I/O请求时多时少，存储类驱动程序无法预先确定某时刻它需要使用多少个SRB，因此在这样的驱动程序中后备列表是管理固定大小SRB的缓冲的分配与释放的一种便利且经济的方式。

OS维护所有当前正在使用的页式和非页式后备列表的状态，动态跟踪所有表中对分配和释放表项的请求，以及新表项的可用系统存储池。当分配请求很多时，OS增加它在每个后备列表中持有的表项个数。当请求又减少了，OS就将增加的后备表项释放回系统存储池。

在使用Ex..LookasideList例程的驱动程序中，遵守以下设计规则：

§ 如果驱动程序本身或它传送后备列表表项的下层驱动程序可能以高于DISPATCH_LEVEL的IRQL或在专用线程环境中访问这些表项，用ExInitializeNPagedLookasideList建立一个非页式后备列表。

§ 只有对驱动程序后备列表表项的访问不可能导致致命页错误时，才建立有页式表项的后备列表。

§ 在非页式系统空间中为后备列表头提供常驻存储区，即使驱动程序用ExInitializePagedLookasideList建立了页式后备列表。

§ 为了得到更好的性能，当调用ExInitialize(N)PagedLookasideList时为Allocate和Free传递NULL指针，除非这些可选的、驱动程序提供的例程除了为后备列表表项分配、释放内存之外还做其他事情（如维护驱动程序对动态分配缓冲的使用情况的状态信息）。

§ 如果驱动程序提供Allocate例程，当此例程调用ExAllocatePoolWithTag时，在例程中使用给定的输入参数（PoolType、Tag和Size）。

§ 对每个ExInitialize(N)PagedLookasideList调用，一旦先前分配的表项不再使用时，应尽快做逆调用ExFreeTo(N)PagedLookasideList。

对于页式后备列表，表项是从页式存储池中分配的，但是这样一个列表的头必须在常驻内存中。

Allocate和Free例程分别与调用ExAllocatePoolWithTag和ExFreePool的效果相同，提供它们会浪费CPU循环。ExAllocate(N)PagedLookasideList 和ExFreeTo(N)PagedLookasideList在驱动程序向ExInitialize(N)PagedLookasideList传递值为NULL的Allocate和Free指针时，会自动调用ExAllocatePoolWithTag和ExFreePool。

驱动程序提供的Allocate例程决不能从页式存储池中为将要记录在非页式后备列表中的表项分配内存，反之也一样。它还必须分配固定大小的表项，因为驱动程序对ExAllocate(N)PagedLookasideList后来的调用将返回当前记录在后备列表中的第一个表项，除非列表为空。也就是说，调用ExAllocate(N)PagedLookasideList只有在给定的当前后备列表为空的情况下，才会调用驱动程序提供的Allocate例程。因此，每次调用ExAllocate(N)PagedLookasideList，只有在后备列表中的所有表项都为一个固定的大小时，返回的表项才恰好是驱动程序需要的大小。驱动程序提供的Allocate例程也不应该改变驱动程序开始传给ExInitialize(N)PagedLookasideList的Tag值，因为对存储池标记的改变会使调试和跟踪驱动程序的内存使用情况变得非常困难。

调用ExFreeTo(N)PagedLookasideList将会返回先前分配的、将保存在后备列表中的表项，除非列表表项数已经达到系统决定的最大值。为了得到更好的性能，驱动程序应该尽快为每次ExAllocate(N)PagedLookasideList调用做其逆调用ExFreeTo(N)PagedLookasideList。当驱动程序迅速将表项释放回其后备列表后，此驱动程序对ExAllocate(N)PagedLookasideList的下次调用，就几乎不可能导致为另一个表项显式分配附加内存所引起的性能恶化了。
1.4.4 只读内存保护

Microsoft的Windows 2000增强了对标记为可写的页的只读访问。

只读内存在用户模式中总是被保护着。但是在Windows NT 4.0和早期版本中，它在内核模式下没有被保护。

如果Windows 2000内核模式驱动程序或应用程序试图写只读内存段，系统就发布错误检测（bug check）0xBE。（如果想了解对错误检测代码的描述，请参见使用Microsoft调试器文档）

截获（intercepting）系统调用

有些驱动程序通过重写驱动程序代码和插入跳转指令或其他修改来截获系统调用。这种技术会导致发布一个错误检测。

全局字符串

如果一个字符串将会被修改，那么决不能将它说明为指向常量值的指针：

CHAR *myString=”This string cannot be modified.”;

在这种情况下，连接器可能会将此字符串放在只读内存段中，因此试图修改它会导致错误检测。

相反，这个字符串应该被显式地说明为L值（L-value）字符的队列：

CHAR myString[]=”This string can be modified.”;

这样就能保证此字符串被放在可写内存中。

1.5 为DMA和PIO维护缓存的一致性

在Windows NT/Windows 2000计算机上，当驱动程序在系统内存和它的设备之间传送数据时，数据可以被缓存一个或多个处理器缓存中和/或系统DMA控制器的缓存中。使用DMA或PIO来为读/写IRP或任何需要DMA或PIO数据传送操作的设备I/O控制请求服务的驱动程序，应该保证传送操作期间可能缓存数据的完整性。有关这些内容将在以下几个小节中阐明。
1.5.1 在DMA操作期间刷新缓存数据

在有些平台上，处理器和系统DMA控制器（或总线控制器DMA适配器）表现出缓存一致性异常。

为了在DMA操作期间保持数据完整性，最低层驱动程序必须遵照下列规则：

1. 在传送操作之前调用KeFlushIoBuffers，以保持可能被缓存在处理器中的数据和内存中数据之间的一致性。

如果驱动程序用值为TRUE的参数CacheEnabled调用AllocateCommonBuffer，驱动程序必须在向/从其缓冲进行传送操作之前调用KeFlushIoBuffers。

2. 在每次设备传送操作完成时，调用FlushAdapterBuffers以保证系统DMA控制器缓冲中的所有剩余字节都已被写入内存或从属设备。

或在给定IRP的每次设备传送操作完成时，调用FlushAdapterBuffers以保证所有数据都已被读入系统内存或写入总线控制器DMA设备。

图16.3表明了，如果主处理器和DMA控制器不能自动维护缓存一致性，那么在使用DMA读或写之前刷新处理器缓存有多么重要。

图16.3 使用DMA的读写操作

异步DMA读或写操作访问内存中的数据，而不是处理器缓存中的数据。除非缓存已经在读操作之前通过调用KeFlushIoBuffers进行了刷新，否则如果处理器缓存稍后才刷新的话，DMA操作传送给系统内存的数据可能会被旧数据覆盖。除非缓存已经在写操作之前通过调用KeFlushIoBuffers进行了刷新，否则缓存中的数据可能比内存中的拷贝还要新。

如果处理器和DMA控制器可以自动保持缓存的一致性，就不需要使用KeFlushIoBuffers，因此在这种平台上调用此支持例程几乎没有任何开销（overhead）。

图16.3还表明，适配器对象代表的DMA控制器可以有内部缓冲。这样的DMA控制器可以以固定大小传送缓存数据，通常是一次8个或更多个字节。此外，这些DMA控制器可以在传送操作之前一直等待，直到它们的内部缓冲满了为止。

对于以可变大小或非系统DMA控制器缓存大小的整数倍的固定大小来使用从属DMA读数据的最低层驱动程序，除非这个驱动程序在每次设备传送完成后都调用FlushAdapterBuffers，否则它不能确定驱动程序请求的每个字节实际将在什么时候被传送。

总线控制器DMA设备的驱动程序也应该在IRP的每次设备传送完成后调用FlushAdapterBuffers，这样可以保证所有数据都已传送到了系统内存或传送出了设备。

FlushAdapterBuffers返回一个布尔量，指出请求的刷新操作是否成功。驱动程序可以用这个值在完成DMA读或写操作的IRP时，决定怎样设置I/O状态块。
1.5.2 在PIO操作期间刷新缓存数据

在有些平台上，处理器的指令和数据缓存在PIO读操作期间表现出缓存一致性异常。

为了在它们的读操作期间保持数据完整性，使用PIO的驱动程序必须遵守下列规则：

§ 在每次读操作完成后调用KeFlushIoBuffers。

§ 例如，从设备到系统内存做PIO传送的驱动程序应该在每次设备传送操作完成后调用KeFlushIoBuffers。又比如，将一类设备寄存器读入系统内存的驱动程序应该在读完每个类后调用KeFlushIoBuffers。否则在有些平台上，，这样的驱动程序可能会试图访问仍在处理器数据缓存中的数据，而不是系统内存中的。

如果处理器和DMA控制器可以自动保持缓存的一致性，就不需要使用KeFlushIoBuffers，因此在这种平台上调用此支持例程几乎没有任何开销。

1.6 错误记录和NTSTATUS值

Windows NT/Windows 2000的设计目标之一是在运行时错误方面比其他PC操作系统更强壮、更友好。也就是说，系统被设计做下列事情：

§ 当发生错误时，能够继续运行，而不让一个组件（或线程）破坏其他组件的代码或数据。

§ 无论何时发生错误，都能够继续运行，而不会发送大量含义模糊的信息来终止用户。

得承认有些I/O错误是用户引起的。例如，请求从可删除存储介质上的文件中读数据，可是用户提供了错误的磁盘、磁带或CD-ROM，这样就产生了一个用户引起的错误。如处理可删除存储介质部分所讨论的，这种错误很容易纠正，只要提示用户提供正确的介质就可以了。

其他I/O错误不能简单地通过终止用户操作来纠正。对于这种I/O错误，Windows NT/Windows 2000继续运行，并不强制用户意识到这些他们不可能立即解决的错误。相反，它提供了系统错误记录线程，在文件中将I/O错误信息作为表项格式化并保存。

Win32事件查看器可以读并显示这个错误记录文件。Windows NT/Windows 2000用户、系统管理员或技术支持人员可以用它来监视给定计算机上的硬件状态；如果需要的话，更换故障硬件；调整设备配置以获得更好的性能；如果发生硬件问题，调试这些问题。
1.6.1 调用IoAllocateErrorLogEntry

当驱动程序在处理IRP期间发现了一个I/O错误时，它应该如下调用IoAllocateErrorLogEntry：

size=sizeof(IO_ERROR_LOG_PACKET)+(n*sizeof(ULONG)) +sizeof(InsertionStrings);

//where n depends on how much

//DumpData the driver will supply

errorLogEntry=(PIO_ERROR_LOG_PACKET)IoAllocateErrorLogEntry(

deviceExtension->DeviceObject,

//target device for current operation

size);

错误记录包的大小是有限制的。系统定义的限制适用于所有转储数据（dump data）和驱动程序提供给包的插入字符串。驱动程序可以用指定的EntrySize值（通常是ERROR_LOG_MAXIMUM_SIZE）调用IoAllocateErrorLogEntry。

IoAllocateErrorLogEntry返回一个指向错误记录包的指针。如果返回的指针为NULL，驱动程序就不需要记录错误。它应该只是继续运行，并保证如果同样的错误再次发生的话，能在那时记录下来。
1.6.2 填充错误记录包

错误记录包定义如下：

typedef struct _IO_ERROR_LOG_PACKET{

UCHAR MajorFunctionCode;

UCHAR RetryCount;

USHORT DumpDataSize;

USHORT NumberOfAtrings;

USHORT StringOffset;

USHORT EventCategory;

NTSTATUS ErrorCode;

ULONG UniqueErrorValue;

NTSTATUS FianlStatus;

ULONG SequenceNumber;

ULONG IoControlCode;

LARGE_INTEGER DeviceOffset;

ULONG DumpData[1];

} IO_ERROR_LOG_PACKET,* PIO_ERROR_LOG_PACKET

驱动程序应当用下列数据填充错误记录包：

MajorFunctionCode

指出当前IRP的驱动程序I/O栈中的IRP_MJ_XXX。

RetryCount

指出驱动程序重试操作和遇到此错误的次数。

RetryCount是个基于0的值。也就是说，驱动程序应该在当前IRP的第一次遇到错误时，将它设为0。

DumpDataSize

指出驱动程序将在包中设置的所有DumpData需要的字节数。

指定的值应该是sizeof（ULONG）的整数倍。

NumberOfStrings

支持驱动程序将提供给这个包的插入字符串的个数。对于不需要插入字符串的错误，驱动程序将此值设为0。

错误记录线程可以用这些驱动程序提供的、以0结尾的Unicode字符串填充写入Win32事件日志的信息，这些信息可以用Win32事件查看器查看。I/O管理器假定：初始插入字符串（如果有的话）要么是驱动程序的名字，要么是发生错误的设备的名字。

驱动程序提供的插入字符串应该是与语言无关的。记录错误并使用插入字符串的驱动程序应该使用从注册表中读出的字符串，或者使用语言无关或在任何语言中都相同的名字（如文件名）。

在大多数情况下，设备和中间层驱动程序可以仅仅记录I/O错误，而不需要为高层事件记录组件提供插入字符串，它们也可以不建立驱动程序指定的事件记录组件。在系统提供的驱动程序中，当前只有网络设备驱动程序在错误记录包里提供插入字符串。

StringOffset

就在DumpData之后，指出驱动程序提供的与插入字符串数据开始处的偏移量。

如果驱动程序提供了这个数据，每个字符串必须是以0结尾的Unicode字符串。

EventCategory

对将其自身作为事件记录组件保存在注册表中的驱动程序，这是一个驱动程序定义的值，它在驱动程序的分类信息文件（message file for categories）中指定。

ErrorCode

指出错误类型。

这是一个系统定义或驱动程序定义的常量，参见定义新的IO_ERR_XXX部分。

UniqueErrorValue

指出错误是在驱动程序中的什么地方检测到的。

FinalStatus

当IRP被完成时，指出此IRP的I/O状态块中设置的值；或指出驱动程序调用的支持例程返回的STATUS_XXX。

SequenceNumber

指出驱动程序分配给当前IRP的队列号，它在给定请求的生命期内应该是个常量。

IoControlCode

如果MajorFunctionCode是IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL，就指出当前IRP的驱动程序I/O栈中的I/O控制代码。否则，这个值应该是0。

如果想对I/O控制代码和设备I/O控制请求有更多的了解，请参见《Windows 2000驱动程序开发指南》第2卷第13章“IRP函数代码和IOCTL”。

DeviceOffset

指出设备中错误发生的偏移量。

DumpData

可以用来存放驱动程序指定的数据，如寄存器值或识别错误原因时要用到的其他有用信息。

任何驱动程序提供的插入字符串都必须紧跟在转储数据后面，从StringOffset处开始。
1.6.3 设置错误记录包中的NTSTATUS值

错误记录包中的ErrorCode和FinalStatus成员都是NTSTATUS类型的。图16.4表明了NTSTATUS值的格式。

图16.4 NTSTATUS格式

系统提供了一系列公共的IO_ERR_XXX常量来设置错误记录包中的ErrorCode。例如，驱动程序可以使用下列系统定义的常量：

IO_ERR_RETRY_SUCCEEDED

IO_ERR_INSUFFICIENT_RESOURCES

IO_ERR_CONFIGURATION_ERROR

IO_ERR_INCORRECT_IRQL

IO_ERR_INBALID_IOBASE

IO_ERR_DRIVER_ERROR

IO_ERR_PARITY

: :

IO_ERR_OVERRUN_ERROR

IO_ERR_TIMEOUT

IO_ERR_CONTROLLER_ERROR

IO_ERR_INTERNAL_ERROR

系统也提供了一系列公共的STATUS _XXX值，驱动程序可以用它们来设置错误记录包中的FinalStatus，它们可以从能返回NTSTATUS值的标准驱动程序例程返回。例如，驱动程序可以从其标准驱动程序例程中返回下列系统定义的常量，可以在IRP的I/O状态块中用下列值来进行设置，可以将错误记录包中的FinalStatus设为下列值：

STATUS_SUCCESS

STATUS_DEVICE_CONFIGURATION_ERROR

STATUS_DRIVER_INTERNAL_ERROR

STATUS_INVALID_DEVICE_STATE

STATUS_IO_DEVICE_ERROR

: :

STATUS_DEVICE_BUSY

STATUS_DEVICE_DOSE_NOT_EXIST

STATUS_ADAPTER_HARDWARE_ERROR

公共IO_ERR_XXX和STATUS _XXX常量是系统资源。每个STATUS _XXX都由系统映射到相应的Win32常量。因为它们是内嵌在Windows NT/Windows 2000中的，所以只有在与Microsoft公司有合作的情况下，公共IO_ERR_XXX和STATUS _XXX常量才能被添加到系统。
1.6.4 调用IoWriteErrorLogEntry

当驱动程序已经向IoAllocateErrorLogEntry返回的包中填充了数据以后，它必须调用IoWriteErrorLogEntry，这样错误记录线程就可以在错误记录文件中写它的表项了。

只要可能，驱动程序都应该记录错误；如果需要的话，拒绝IRP；在遇到异常或未料到的I/O错误时，继续运行。

正在销售的驱动程序决不应该调用KeBugCheckEx（或KeBugCheck）使系统终止。然而，可以用KeBugCheckEx来调试正在开发的驱动程序。
1.6.5 定义新的IO_ERR_XXX

用户可以在驱动程序的资源文件xxxlog.mc中创建驱动程序指定的、NTSTATUS类型的私有IO_ERR_XXX代码。

如图16.4所示，对于这样的错误，NTSTATUS类型的Facility域必须被设成FACILITY_IO_ERROR_CODE。必须为每个新的IO_ERR_XXX提供Sev值和唯一的Code值。驱动程序也必须将其新IO_ERR_XXX中的C位设置成‘1’。

图16.4中的Sev域指出严重程度代码，它必须是下列系统定义的值之一：

STATUS_SEVERITY_SUCCESS

指出错误记录包中的FinalStatus被设为STATUS_SUCCESS，而且ErrorCode被设为IO_ERR_RETRY_SUCCEEDED这样的值。

虽然STATUS_PENDING也属于STATUS_SEVERITY_SUCCESS类的值，驱动程序却不能在未决的IRP中记录错误。

STATUS_SEVERITY_INFORMATIONAL

表示提示消息的NTSTATUS值，如STATUS_SERIAL_MORE_WRITES。

STATUS_SEVERITY_WARNING

表示警告的NTSTATUS值，如STATUS_DEVICE_PAPER_EMPTY。

STATUS_SEVERITY_ERROR

表示错误的NTSTATUS值，如在错误记录包中，将FinalStatus值设为STATUS_INSUFFICIENT_RESOURCES，或将ErrorCode值设为IO_ERR_CONFIGURATION_ERROR。

绝大多数公共IO_ERR_XXX常量属于STATUS_SEVERITY_ERROR类。

为了使一系列驱动程序定义的错误对系统管理者可见，或为了通过Win32事件查看器终止用户，驱动程序必须在注册表中将它本身作为错误记录组件来建立。
1.6.6 定义私有NTSTATUS常量

一对新的驱动程序（如类/端口驱动程序或视频显示/微端口驱动程序）可以定义驱动程序指定的STATUS _XXX值，来交换有关私有定义的IRP_MJ_INTERNAL_DEVICE_CONTROL请求的信息，这个请求是从低层向高层驱动程序发的。

如果现存的高层驱动程序的IoCompletion例程可能为某个IRP调用，那么当类驱动程序完成此IRP时，它必须将所有私有STATUS _XXX值映射为系统定义的NTSTATUS值。

对于成对的显示和视频微端口驱动程序，视频端口驱动程序负责公共STATUS _XXX值和Win32定义的常量（由视频微端口驱动程序返回）之间的映射。如果想对视频微端口驱动程序有更多的了解，请参见《图形驱动程序设计指南》。

为一系列IRP_MJ_INTERNAL_DEVICE_CONTROL请求定义私有STATUS _XXX值的驱动程序必须做下列事情：

§ 如图16.4所示，将Facility域设成适当的驱动程序定义的常量，这个值用来指出设备的类型。

§ 设置用户代码标志（图16.4中用C标出）。

§ 用适当的值设置Sev域。参见“定义新的IO_ERR_XXX部分”。

1.7 处理可删除存储介质

文件系统和可删除存储介质设备驱动程序必须：保证当文件在可删除存储介质设备上打开时，正确的介质已被安装；并且保证在访问介质的操作期间，正确的介质始终被安装着。位于文件系统和可删除存储介质设备驱动程序之间的中间层驱动程序也必须负责保证这一点。

因此，可删除存储介质设备的驱动程序应该能做到下列中的至少一件事情：

响应来自文件系统的验证请求（check-verify request）

通知文件系统可能的存储介质改变

检查DeviceObject->Flags

建立中间层驱动程序的IRP
1.7.1 响应来自文件系统的验证请求

文件系统可以随时向设备驱动程序的Dispatch入口点发送IRP，通过将I/O栈中的Parameters.DeviceIoControl.IoControlCode设为以下值，来提出IRP_MJ_ DEVICE_CONTROL请求：

IOCTL_XXX_CHECK_VERIFY

其中XXX是设备类型，如DISK、TAPE或CDROM。

DISK类型包括不可分区（软盘）和可分区可删除存储介质设备。

如果下层设备驱动程序确定了存储介质没有改变，驱动程序应该完成IRP，用下列值返回IoStatus块：

Status：设为STATUS_SUCCESS

Information：设为0

此外，如果设备类型是DISK或CDROM且调用者指定了输出缓冲，驱动程序就返回缓冲中Irp->AssociatedSystemBuffer保存的存储介质改变记数，并且将IoStatus. Information设为sizeof（ULONG）。通过返回这个记数值，驱动程序使得调用者可以如实地确定存储介质是否已经改变。

如果下层设备驱动程序确定了存储介质已经改变，它会根据存储介质是否安装而执行不同的举措。如果安装了存储介质（VPB中的VPB_MOUNTED标志被置为1），驱动程序应该做下列事情：

1. 将DeviceObject中的Flags与DO_VERIFY_VOLUME进行“或”运算，得到新的Flags值。

2. 将IRP中的IoStatus块设为下列值：

§ 将Status设为STATUS_VERIFY_REQUIRED

§ 将Information设为0

3. 用输入IRP调用IoCompleteRequest。

如果没有安装存储介质，驱动程序决不能将DO_VERIFY_VOLUME置为1。驱动程序应该将IoStatus. Status设为STATUS_IO_DEVICE_ERROR，将IoStatus. Information设为0，然后用输入IRP调用IoCompleteRequest。
1.7.2 通知文件系统可能的存储介质改变

可删除存储介质设备驱动程序必须保证：驱动程序处理请求向/从存储介质发送的IRP或驱动程序处理会影响到存储介质的设备I/O控制操作时，DeviceObject（每个被发送IRP的驱动程序例程的输入）代表的设备的存储介质没有改变。如果物理设备总是向驱动程序通告状态变化，那么对改变介质最好的检查时间就是在从无介质存在状态向介质存在状态转化之后。

如果在驱动程序开始I/O操作之前或操作期间，物理设备指出存储介质的状态可能已经改变了，驱动程序就必须做下列事情：

1. 通过检查VPB中的VPB_MOUNTED标志确保安装了存储介质。（如果没有安装存储介质，驱动程序决不能将DO_VERIFY_VOLUME置为1。驱动程序应该将IoStatus. Status设为STATUS_IO_DEVICE_ERROR，将IoStatus. Information设为0，然后用此IRP调用IoCompleteRequest。）

2. 将DeviceObject中的Flags与DO_VERIFY_VOLUME进行“或”运算，得到新的Flags值。

3. 将IRP中的IoStatus块设为下列值：

§ 将Status设为STATUS_VERIFY_REQUIRED

§ 将Information设为0

4. 在完成IoStatus块中的Status域值不是STATUS_SUCCESS的那些IRP之前，驱动程序必须调用IoIsErrorUserInduced，当Status域为下列值时，它返回TRUE：

STATUS_BERIFY_REQUIRED

STATUS_NO_MEDIA_IN_DEVICE

STATUS_WRONG_VOLUME

STATUS_UNRECOGNIAED_MEDIA

STATUS_MEDIA_WRITE_PROTECTED

STATUS_IO_TIMEOUT

STATUS_DEVICE_NOT_READY

如果IoIsErrorUserInduced返回TRUE，驱动程序必须调用IoSetHardErrorOrVerifyDevice，这样FSD就可以向用户发送提供了正确存储介质的弹出菜单；或重试初始请求；或撤消请求的操作。
1.7.3 检查设备对象中的标志

对每个请求向/从可删除存储介质进行I/O操作的IRP，可删除存储介质设备驱动程序必须确定其DeviceObject->Flags 是否被设为DO_VERIFY_VOLUME。如果是，驱动程序必须做下列事情：

§ 对于IRP_MJ_READ、IRP_MJ_WRITE和某些IRP_MJ_DEVICE_CONTROL请求，检查I/O栈位置Flags的值是否为SL_OVERRIDE_VERIFY_VOLUME。如果是，继续请求的操作。

当IFS安装或重新安装可删除存储介质时，返回下层存储介质逻辑结构信息的设备控制请求将I/O栈位置Flags的值设为SL_OVERRIDE_ VERIFY_VOLUME。

§ 否则，驱动程序必须在DeviceObject->Flags 为DO_VERIFY_VOLUME时，拒绝为相应驱动器、设备或分区执行I/O操作。如前面几个小节所述，可删除存储介质设备的驱动程序必须拒绝发送给相应设备的IRP，对每个IRP重复步骤2和3，直到FSD清除了DeviceObject->Flags中的DO_VERIFY_VOLUME值为止。

如果可删除存储介质设备驱动程序在设置了DO_VERIFY_VOLUME且没有设置SL_OVERRIDE_VERIFY_VOLUME时，没有拒绝IRP，那么文件系统既不能保持缓存文件数据的完整性，又不能提示用户重新安装这个已经打开了文件的存储介质。
1.7.4 在中间层驱动程序中建立IRP

位于文件系统和可删除存储介质设备驱动程序之间的中间层驱动程序必须在IRP中建立低一层驱动程序的I/O栈位置。当中间层驱动程序为下层驱动程序建立I/O栈位置时，它必须从输入的IRP_MJ_READ、IRP_MJ_WRITE和IRP_MJ_DEVICE_CONTROL请求中，将自己的I/O栈位置Flags复制到低一层驱动程序的I/O栈位置。

如果中间层驱动程序为下层可删除存储介质驱动程序分配了新的IRP，它必须按以下所述建立这些IRP：

§ 对于传输请求，它必须根据初始IRP中Tail.Overlay.Thread的值，在每个分配给驱动程序的IRP中建立线程环境。

§ 对于IRP_MJ_READ、IRP_MJ_WRITE和IRP_MJ_DEVICE_CONTROL请求，它必须从初始IRP中将I/O栈位置Flags复制到每个分配给驱动程序的IRP。

否则，文件系统既不能保持缓存文件数据的完整性，又不能提示用户重新安装这个已经打开了文件的存储介质。

1.8 使设备对应用程序和驱动程序可用

对于任何用户模式代码可以直接递交I/O请求的设备，无论它是物理的、逻辑的或虚拟的，其驱动程序都必须为它的用户模式客户提供某种名字。用这个名字，用户模式应用程序（或其他系统组件）就可以识别出请求I/O的那个设备。

在以前的操作系统版本中，驱动程序命名它们的设备对象，并在注册表中为这些名字和用户可见的Win32逻辑名建立符号连接。

然而，Windows 2000和WDM驱动程序并不命名设备对象，而是为每个设备对象注册并使一个用户模式I/O请求可被发送到的设备接口可用。设备接口是向其他系统组件（包括其他驱动程序和用户模式应用程序）输出设备和驱动程序功能的一种方式。

设备对象被划分成类，每一类都与一个GUID相关。系统在设备指定的头文件中为公共设备接口类定义GUID。当驱动程序注册了一个设备接口，I/O管理器将其设备接口类GUID与一个符号连接名相关。连接名保存在注册表中，它在系统启动期间一直存在。使用此设备接口的应用程序可以查找它的符号连接名，并将它保存用作I/O请求的目标。
1.8.1 注册设备接口

Windows 98和Windows 2000提供了两种注册设备接口的方法：

§ 对于内核模式组件，像大多数驱动程序一样，使用I/O管理器例程。这一节描述了如何使用这些例程。

§ 对于用户模式代码，使用SetupDi Xxx函数。如果想对这些函数有更多的了解，请在在线DDK中参见设备接口函数部分。

Windows 2000和WDM驱动程序并不命名其设备对象。相反，当驱动程序调用IoCreateDevice以创建一个设备对象时，它应该将设备名指定为NULL字符串。总线驱动程序应该将FILE_AUTOGENERATED_DEVICE_NAME标志设为1。所有PnP函数、过滤器和总线驱动程序都应该将FILE_DEVICE_SECURE_OPEN标志设为1。相应地，系统为此PDO选择一个唯一的设备名。

在创建了设备对象并将它连接到设备栈之后，一个驱动程序调用IoRegisterDeviceInterface。I/O管理器对这个例程的定义如下：

NTSTATUS

IoRegisterDeviceInterface(

IN PDEVICE_OBJECT PhysicalDebiceObject,

IN CONST GUID *InterfaceClassGuid,

IN PUNICODE_STRING ReferenceString, OPTIONAL

OUT PUNICODE_STRING SymbolicLinkName

);

通常，函数驱动程序从它的AddDevice例程中做此调用，但有时过滤器驱动程序注册这个接口。

调用者用PhysicalDeviceObject传递指向设备PDO的指针。InterfaceClassGuid用来标识正被注册的接口。大多数函数和所有过滤器驱动程序应该在ReferenceString中传递NULL字符串，这个参数为总线驱动程序提供了给按要求即时创建的软件设备定义接口的方法。

注册过的接口在操作系统启动期间一直存在。如果指定的接口已经被注册过了，I/O管理器就在SymbolicLinkName中传送它的名字，并返回提示成功状态STATUS_OBJECT_NAME_EXISTS。

如果接口还没有被注册过，I/O管理器就为这个设备接口创建一个注册键，并在分配给调用者的Unicode字符串结构中返回隐式的SymbolicLinkName。驱动程序使此设备接口可用或不可用时传递此连接名。它也用这个名字访问注册键，在这个注册键中它可以存放为设备接口指定的信息。（参见IoOpenDeviceInterfaceRegistryKey）应用程序用这个连接名打开设备。

在兼容设备接口类下，驱动程序可以按需要任意多次地调用IoRegisterDeviceInterface来注册附加的设备接口。

其他系统组件在驱动程序使设备接口可用之前，不能使用它。参见“使设备接口可用和不可用”。
1.8.2 使设备接口可用和不可用

在成功地启动了设备后，注册了接口的驱动程序调用IoSetDeviceInterfaceState使此接口可用。I/O管理器对这个例程的定义如下：

NTSTATUS

IoSetDebiceINterfaceState(

IN PUNICODE_STRING SymbolicLinkName,

IN BOOLEAN Enable

);

驱动程序传递由IoRegisterDeviceInterface返回的SymbolicLinkName和值为TRUE的Enable参数来使此接口可用。

如果驱动程序能成功地启动其设备，它应该在处理PnP管理器的IRP_MN_START_DEVICE请求时调用这个例程。

在IRP_MN_START_DEVICE请求完成后，PnP管理器向所有请求了它们的内核模式和用户模式组件发设备接口返回通知。参见在线DDK上的“Registering for Device Interface Change Notification（设备接口注册改变通知）”。

为了使设备接口不可用，驱动程序调用IoSetDeviceInterfaceState，传递由IoRegisterDeviceInterface返回的SymbolicLinkName和值为FALSE的Enable参数。

当驱动程序为设备处理IRP_MN_SURPRISE_REMOVAL或IRP_MN_REMOVE_DEVICE时，它应该使设备的接口不可用。当设备被停止（IRP_MN_STOP_DEVICE）或处于休眠状态时，驱动程序不因该使接口不可用，而应该让所有设备接口可用并对I/O排队。
1.8.3 使用设备接口

设备接口可以被内核模式组件和用户模式应用程序使用。用户模式的代码必须用SetupDi Xxx函数找出注册了的、可用的设备接口。参见在线DDK中的“Device Interface Functions（设备接口函数）”。

在内核模式组件可以使用指定的设备或文件对象之前，它必须做下列事情：

1. 确定所需的设备接口是否是注册过并可用的。

驱动程序在注册表中注册时，可以配置成能够通知PnP管理器何时设备接口可用或不可用。为了注册，组件调用IoRegisterPlugPlayNotification。无论何时为指定设备类将设备接口设置成可用或不可用，IoRegisterPlugPlayNotification例程都会调用驱动程序提供的回调函数。参见《即插即用、电源管理和安装设计指南》第4章中的“使用PnP设备接口改变通知”。

驱动程序或其他内核模式组件也可以调用IoGetDeviceInterface来获得指定设备接口类的所有注册过并可用的设备接口的列表。返回的列表中有指向标识设备接口的Unicode字符串的指针。

2. 获得代表所需设备接口类的符号连接的Unicode字符串。

IoGetDeviceInterface返回指向SymbolicLinkList参数中字符串的指针。如果驱动程序注册为可通知PnP管理器，上述例程的回调（callback）可以从DEVICE_INTERFACE_CHANGE_NOTIFICATION结构中取回这个字符串。

3. 用这个Unicode字符串得到指向相应设备或文件对象的指针。

为了访问指定的设备对象，驱动程序必须调用IoGetDeviceObjectPointer，ObjectName参数中放的是所需接口的Unicode字符串。为了访问文件对象，驱动程序必须用ObjectName参数传递这个Unicode字符串，调用InitializeObjectAttributes，然后调用ZwCreateFile传递成功初始化了的属性结构。

1.9 可分页代码和数据

用户可以使某些驱动程序的全部或部分可分页。对驱动程序代码分页减小了驱动程序加载映像的大小，从而为其他应用节省了系统空间。这对突发使用（sporadically-used）设备（如调制解调器和CD-ROM）的驱动程序或部分很少被调用的驱动程序非常有效。

在频繁使用的驱动程序中，决定性能好坏的代码段不应该是可分页的。按需要随时对代码分页会对驱动程序和系统的性能产生负面影响。

为了确定驱动程序的某部分是否可以设成可分页的，记住下列事实：

§ 如果下列情况之一为真，驱动程序代码就可以是可分页的：

§ 它访问页式存储池。

§ 它调用另一个可分页例程。

§ 它在用户线程环境中引用用户缓冲。

§ 下列驱动程序代码必须是常驻的，不能是可分页的：

§ 运行于或高于DISPATCH_LEVEL的IRQL上的代码

§ 获得自旋锁的代码

§ 调用Knernel的对象支持例程的代码。如Wait参数被设为TRUE的KeReleaseMutex或KeReleaseSemaphore例程。如果Knernel在Wait值为TRUE的情况下被调用，调用返回时IRQL为DISPATCH_LEVEL，而且发送者数据库被锁。

一般，如果所有可分页代码（或数据）的总量至少为4K，那么使此段可分页是可行的。应该尽可能地将纯可分页代码（或数据）从必须可分页或按要求随时可被锁的代码中分离出来，放到单独的一段中。将纯可分页代码和按要求随时可被锁的代码混在一起会使一些不需要被锁的系统空间被锁住。

然而，如果驱动程序的可分页代码（或数据）少于4K，可以将这些代码和按要求随时可被锁的代码混在一起，放在同一段中，从而节省系统空间。

如果想对此要更深入的了解，请参见下列：

§ 16.9.1 使驱动程序代码可分页

§ 16.9.2 锁住可分页代码或数据

§ 16.9.3 对整个驱动程序分页
1.9.1 使驱动程序代码可分页

为了使驱动程序例程可分页，必须保证它运行在低于DISPATCH_LEVEL的IRQL上，而且还要保证它不获得任何自旋锁。

为了检测出运行在高于DISPATCH_LEVEL的IRQL上的代码，可以使用PAGED_CODE()宏。在调试模式下，如果代码运行在高于DISPATCH_LEVEL的IRQL上，这个宏就生成信息。将此宏放在例程的第一句处，就将整个例程标记为分页式代码，如下面例子所示：

NTSTATUS

MyDriverXxx(

IN OUT PVOID ParseContext OPTIONAL,

OUT PHANDLE Handle

)

{

NTSTATUS Status;

PAGED_CODE();

.

.

.

}

为了保证能正确执行，在已编好的驱动程序中选中“Force IRQL Checking”选项运行“Driver Verifier”。这个选项使系统可以在每次驱动程序将IRQL提高到DISPATCH_LEVEL或更高时，自动将可分页代码调出内存。使用“Driver Verifier”可以在这个区内快速查找驱动程序错误。否则，这些错误一般只能通过用户来查找，而它们通常很难重现。

使用了自旋锁的例程不能被分页。然而，在有些情况下，可以将不在可分页段中的单独例程中的那些需要自旋锁的操作分离出来。

例如，对于以下代码段：

//PAGED_CODE();

KeInitializeEvent(&event,NotificationEvent,FALSE);

irp=IoBuildDeviceIoControlRequest(IRP_MJ_DEVICE_CONTROL,

DeviceObject,

(PVOID)NULL,

0,

(PVOID)NULL,

0,

FALSE,

&event,

&ioStatus);

if(irp){

irpSp=IoGetNextIrpStackLocation(irp);

irpSp->MajorFunction=IRP_MJ_FILE_SYSTEM_CONTROL;

irpSp->MinorFunction=IRP_MN_LOAD_FILE_SYSTEM;

status=IoCallDriver(DeviceObject,irp);

if(status= =STATUS_PENDING){

(VOID)KeWaitForSingleObject(&event,

Executive,

KernelMode,

FALSE,

(PLARGE_INTEGER)NULL);

}

}

SPINLOCKUSE !

ExAcquireSpinLock(&IopDatabaseLock,&irql);

//Code inside spin lock

DeviceObject->ReferenceCount--;

if(!DeviceObject->ReferenceCount&& !DeviceObject->AttachedDevice){

//Unload the driver

.

.

.

}else{

ExReleaseSpinLock(&IopDatabaseLock,irql);

}

可以通过将引用自旋锁的几行代码移到一个单独的例程中，使前面的例程可分页（大约节省160个字节）。

此外，记住：如果驱动程序调用了Wait参数值为TRUE的KeXxx支持例程（如KeReleaseMutex或KeReleaseSemaphore），那么它的代码决不能标记为可分页的。这种调用返回时，IRQL为DISPATCH_LEVEL，而且发送者数据库被锁。
1.9.2 锁住可分页代码或数据

非页式代码是内存常驻的，它运行在等于或高于DISPATCH_LEVEL的IRQL上，而且永远不会引起页错误。页式代码运行在低于DISPATCH_LEVEL的IRQL上，当对它的引用不会导致系统瘫痪或负面影响驱动程序操作时，它可以被页调入（page in）。

某些驱动程序（如串行和并行驱动程序）不需要常驻内存，除非它们管理的设备是打开的。只有有一个活动的连接或端口，管理此端口的驱动程序代码的某部分就必须常驻来为设备服务。然而，当端口或连接没有被使用时，驱动程序代码就不需要了。与此相反，存放系统代码、应用程序代码或系统分页文件的磁盘的驱动程序必须总是常驻内存的，因为这个驱动程序要经常在其设备和系统之间传输数据。

突发使用设备（如调制解调器）的驱动程序可以在设备处于非活动状态时释放系统空间。如果在单独的段中有必须常驻以服务活动设备的代码，这一段可以设计成可分页的。当驱动程序的设备被打开时，操作系统就将可分页段放入内存。

系统CD音频驱动程序代码也有这个特征。驱动程序代码根据CD设备制造商来分类到个可分页段中。某些品牌可能永远不会出现在给定系统上。此外，即使系统上有CD-ROM，它可能很少被访问。因此按CD类型将代码分类到个可分页段中可以保证某个机器上没有的设备的代码永远不会被加载，而当设备被访问时，系统为适当的CD设备加载代码。然后如下面所述的，驱动程序调用MmLockPagableCodeSection，在其设备正被使用时，将它的代码锁进内存。

为了将可分页代码分离到一个已命名的程序段中，用下列编译器指令对它进行标记。所有与Windows NT/Windows 2000兼容的编译器都支持这条指令：

#pragma alloc_text(PAGEXxxx,RoutineName)

这个程序段的名字必须以PAGE开头，而且必须以1到4个唯一标识驱动程序可分页段的字符完成。段名是区分大小写的，也就是说，PAGE必须大写。RoutineName标识了将包括在可分页段中的入口点。

例如，下列代码将RdrCreateConnection表示为PAGEELK段内部的入口点：

#ifdef ALLOC_PRAGMA

#pragma alloc_text(PAGELK,RdrCreateConnection)

#endif

为了使可分页驱动程序代码常驻并被锁住，驱动程序调用MmLockPagableCodeSection，传递在可分页代码段中的一个地址（通常是驱动程序例程的入口点）。MmLockPagableCodeSection在整个代码段中加了锁，包括在此调用中引用的例程。也就是说，它使每个与同一个PAGEXxxx标识符相关的例程都常驻并被锁。

只有在被传递的符号地址没有在可分页段中时，对MmLockPagableCodeSection的调用才会失败。

MmLockPagableCodeSection返回一个句柄，这个句柄将在对程序段解锁时（MmUnlockPagableImageSection），或驱动程序必须从其代码的其他地方锁程序段时用到。

驱动程序也可以将很少使用的数据视为可分页的，这样就可以将它的页调出内存，直到它支持的设备活动为止。例如，系统混频器（mixer）驱动程序使用可分页数据。混频器设备没有与它相关的异步I/O，因此这个驱动程序可以使其数据可分页。

为了创建可分页数据段，在数据模块开始处使用下列编译器指令：

#pragma data_seg(“PAGE”)

在模块末尾，用下列指令：

#pragma data_seg()

关键词PAGE是区分大小写的，因此它必须大写。

为了使数据段常驻并被锁，驱动程序调用MmLockPagableDataSection，传递出现在可分页数据段中的数据项。MmLockPagableDataSection返回一个将在后续加锁或解锁请求中用到的句柄。

为了恢复被锁段的可分页状态，调用MmUnlockPagableImageSection，传递由MmLockPagableCodeSection或MmLockPagableDataSection返回的句柄值。驱动程序的Unload例程必须调用MmUnlockPagableImageSection释放它为可锁代码和数据段获得的每个句柄。

对段加锁是一项开销很大的操作，因为内存管理器必须在将页锁进内存之前检索它的加载模块列表。如果驱动程序在其代码的许多地方都要加锁，它应该在第一次调用MmLockPagableXxxxSection之后，使用更有效的MmLockPagableSectionByHandle。

传递给MmLockPagableSectionByHandle的句柄是由先前调用MmLockPagableCodeSection或MmLockPagableDataSection返回的句柄。

内存管理器为每个段句柄维护一个记数值。每次驱动程序为这个段调用MmLockPagableXxx，内存管理器就将此记数值加1。调用MmUnlockPagableImageSection使此记数值减1。当段句柄的记数值非零时，这个段在内存中保持被锁状态。

段句柄是有效的，只要其驱动程序已被加载。因此，驱动程序应该只调用MmLockPagableXxxxSection一次。如果驱动程序需要其他的加锁调用，它应该使用MmLockPagableSectionByHandle。

如果驱动程序为已经加锁的段调用MmLockPagableXxxxSection例程，内存管理器只是将此段的引用记数值加1。如果当调用加锁例程时，此段对应的页已被调出，内存管理器将此页调入，并将它的引用记数值设为1。

用这种技术最小化了驱动程序对系统资源的影响。当驱动程序运行时，它可以将需要常驻的代码和数据锁进内存。当对设备没有未完成的I/O请求时，（也就是说，当设备被关闭或如果设备从未被打开时），驱动程序可以对同一段代码或数据开锁，使它的页可以被调出。

然而，在驱动程序连接了中断后，在中断处理期间可被调用的驱动程序代码必须是常驻内存的。有些设备驱动程序可以按需要随时使其可分页或锁进内存，而这样的驱动程序的代码和数据的某些核心部分必须在系统空间中永久常驻。

对代码或数据段加锁时，应考虑下列实现策略：

§ Mm(Un)LockXxx的基本用途是：使一般情况下被视为非页式的代码或数据成为可分页的，并作为非页式代码或数据被引入（bring in）。像串行或并行驱动程序这样的驱动程序是很好的例子：如果这种驱动程序管理的设备没有打开的句柄，部分代码是不需要的，可以将其页调出。换向器（redirector）和服务器也是可以使用这种技术的好例子。当没有活动连接时，这些组件的页都可以调出。

§ 整个可分页段被锁进内存。

§ 每个驱动程序中，一段给代码、一段给数据是很有效的。一般，使用许多命名的可分页段效率比较低。

§ 保存纯可分页段并将它分页，但是要与按要求随时可被锁的代码分开。

§ 记住：MmLockPagableCodeSection 和MmLockPagableDataSection不应该频繁地被调用。当内存管理器加载段时，这些例程会使I/O负载过重。如果驱动程序必须在其代码的几处对段加锁，它应该使用MmLockPagableSectionByHandle。

使用MmLockPagableCodeSection和MmUnlockPagableImageSection减少了每个驱动程序加载映像的大小，但同时影响了驱动程序和系统性能。一般，对系统性能有决定性影响的驱动程序（如键盘和鼠标驱动程序）和经常使用设备的驱动程序（如磁盘驱动程序）不应该有可分页段。不值得以性能损失为代价换来驱动程序映像大小暂时的减少。
1.9.3 对整个驱动程序分页

使用MmLockPagableXxx支持例程并指定分页和可丢弃段的驱动程序，是由非页式段、页式段和驱动程序初始化后就丢弃的INIT段组成的。

在设备驱动程序为它管理的设备连接了中断之后，驱动程序的中断处理路径必须常驻在系统空间。中断处理代码必须是页不能被调出的驱动程序段的一部分，以防中断发生。

两个附加的内存管理器例程MmPageEntireDriver和MmResetDriverPaging可用来设置组成驱动程序映像的所有段的可分页或不可分页属性。这些例程允许驱动程序在其设备没有被使用和不能产生中断时整个被调出。

完全可分页系统驱动程序的例子有win32.sys驱动程序、串行驱动程序、信件槽（mailslot）驱动程序、蜂鸣器（beep）驱动程序和零位驱动程序（null driver）。

典型的串行驱动程序是被间歇使用的。串行驱动程序可以整个被调出，直到它管理的端口打开为止。端口只要一打开，串行驱动程序中必须常驻内存的那部分就必须被放入非页式系统空间。驱动程序的其他部分可以仍是可分页的。

能整个被调出的驱动程序应该在中断被连接之前的驱动程序初始化期间调用MmPageEntireDriver。PnP驱动程序不应该使用这个例程。

当被调出的驱动程序所管理的设备收到一个打开请求时，驱动程序页被调入。然后，驱动程序必须在连接中断之前调用MmResetDriverPaging。调用MmResetDriverPaging使内存管理器根据编译链接阶段获得的属性来处理驱动程序段。非页式段（如文本段）将被调入非页式系统内存，可分页段将在它们被引用时调入。

这样的驱动程序必须保存其设备打开句柄的引用记数值。每当有设备打开请求时，驱动程序增加这个记数值；每当有关闭请求时，驱动程序减少这个记数值。当此记数值为0时，驱动程序应当断开中断，然后调用MmPageEntireDriver。如果驱动程序管理的设备超过一个，在所有设备的记数值都为0以后，驱动程序才能调用MmPageEntireDriver。

驱动程序要做在改变引用记数值时同步所需的所有工作，并且在驱动程序可分页状态改变时，它要防止引用记数值也改变。也就是说，在SMP机上，驱动程序必须保证：当一个处理器上的打开调用导致中断被连接且引用记数值被增加时，另一个处理器上不能正在运行MmPageEntireDriver。

1.10 常见的驱动程序可靠性问题

以内核模式执行的驱动程序占用了大量代码基地址（base）。因此，要改善系统可靠性就必须考虑到这个庞大的代码基地址。

可用来保证驱动程序可靠性的最重要的工具是Driver Verifier。它可以检查许多常见的驱动程序问题，它们中的一部分将在这一节中讨论。

这一节为下列常见的驱动程序问题提出了预防策略：

§ 缓冲I/O

§ 用户空间地址

§ 直接I/O

§ 输入数据和设备状态

§ Dispatch例程

§ 多处理器环境

§ 处理IRP

对上述问题的讨论分布在以下各节，其中有很多节在讲述时附带了程序片段。这些小程序段说明了典型的错误以及怎样更正它们。（为了简明，已经对这些代码做了适当的修改）
1.10.1 缓冲I/O中的错误

没有检查缓冲大小可能是最常见的驱动程序问题。这个问题可以在许多环境中发生，但是在下述情况下尤为麻烦。

例如，假设下列代码出现在被Dispatch例程调用的一个例程中，并假设驱动程序没有验证IRP中传递的缓冲大小的有效性：

switch(ControlCode)

case IOCTL_NEW_ADDRESS:{

tNEW_ADDRESS *pNewAddress=

pIrp->AssociatedIrp.SystemBuffer;

pDeviceContext->Addr=ntohl(pNewAddress->Address);

这个例子没有在分配语句（粗体）之前检查缓冲大小。结果，若输入缓冲大小不足以放置tNEW_ADDRESS结构，则第二行中的pNewAddress->Address引用就是错的。

下列代码检查了缓冲大小，避免了可能出现的问题：

case IOCTL_NEW_ADDRESS:{

tNEW_ADDRESS *pNewAddress=

pIrp->AssociatedIrp.SystemBuffer;

if(pIrpSp->Parameters.DeviceIoControl.InputBufferLength>=

sizeof(tNEW_ADDRESS)){

pDeviceContext->Addr=ntohl(pNewAddress->Address);

处理其他缓冲I/O的代码（如使用可变大小缓冲的WMI请求）可能有类似的错误。

为缓冲IOCTL和FSCTL输出请求检查缓冲大小

输出缓冲问题与输入缓冲问题相似。它们很容易破坏存储池，而且用户模式的调用者不易察觉它们的发生。

在下面的例子中，驱动程序没有检查SystemBuffer的大小：

case IOCTL_GET_INFO:{

Info=Irp->AssociatedIrp.SystemBuffer;

Info->NumIF= NumIF;

Irp->IoStatus.Information=

NumIF*sizeof(GET_INFO_ITEM)+ sizeof(ULONG);

Irp->IoStatus.Status=ntStatus;

}

假定系统缓冲的NumIF域表示输入项的个数，这个例子可以将IoStatus.Information设为大于输出缓冲的值，因此向用户模式代码返回了过多信息。如果应用程序编得不好，用过小的输出缓冲调用，前面的代码段会因在系统缓冲外写而破坏存储池。

记住：I/O管理器假定Information域中的值是有效的。当调用者为输出缓冲传递了一个无效的内核模式地址和零字节大小，如果驱动程序没有检查输出缓冲的大小而过后发现错误，就会产生严重问题。

以缓冲I/O路径向调用者返回未初始化数据

驱动程序应该在将输出缓冲数据返回给调用者之前，把所有输出缓冲数据都初始化为0。没有初始化缓冲会导致垃圾数据，其个数为未初始化字节数。

在下面的例子中，驱动程序返回无用的垃圾数据：

case IOCTL_GET_NAME:{

outputBufferLength=

ioStack->Parameters.DeviceControl.OutputBufferLength;

outputBufferLength=(PGET_NAME)Irp->AssociatedIrp.SystemBuffer;

if(outputBufferLength>=sizeof(GET_NAME)){

length= outputBufferLength-sizeof(GET_NAME);

ntStatus=IoGetDeviceProperty(

DeviceExtension->PhysicalDeviceObject,

DevicePropertyDriverKeyName,

length,

outputBuffer->DriverKeyName,

&length);

outputBuffer->ActualLength=

length+sizeof(GET_NAME);

Irp->IoStatus.Information=outputBufferLength;

}else{

ntStatus=STATUS_BUFFER_TOO_SMALL;

}

将IoStatus.Information的值设为输出缓冲大小使得整个输出缓冲都被返回给调用者。I/O管理器没有初始化超过输入缓冲大小的数据（由于缓冲要求，输入和输出缓冲是重叠的）。因为系统支持例程IoGetDeviceProperty没有写整个缓冲，这个IOCTL向调用者返回了未初始化的数据。

有些驱动程序用Information域返回提供I/O请求更多细节的代码。在这样做之前，这样的驱动程序应该检查IRP标志以确保这个IRP_INPUT_OPERATION没有被置1。当此标志位没有被置1时，IOCTL或FSCTL没有输出缓冲，因此Information域不需要提供缓冲大小。在这种情况下，驱动程序可以安全地使用Information域返回它自己的代码。

验证可变长度缓冲有效性的错误（会引起整数下溢或上溢）

驱动程序经常采用有固定大小头部和可变长度数据的输入缓冲，如下面例子所示：

typedef struct_WAIT_FOR_BUFFER{

LARGE_INTEGER Timeout;

ULONG NameLength;

BOOLEAN TimeoutSpecified;

WCHAR Name[1];

} WAIT_FOR_BUFFER,*PWAIT_FOR_BUFFER;

if(InputBufferLength<sizeof(WAIT_FOR_BUFFER)){

IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER);

Return(STATUS_INVALID_PARAMETER);

}

WaitBuffer=Irp->AssociatedIrp.SystemBuffer;

if(FIELD_OFFSET(WAIT_FOR_BUFFER,Name[0])+

WaitBuffer->NameLength>InputBufferLength){

IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER);

Return(STATUS_INVALID_PARAMETER);

}

如果WaitBuffer->NameLength是一个很大的ULONG值，把它加到偏移量（offset）上会引起整数上溢。相反，驱动程序应该从InputBufferLength中减去偏移量，将结果与WaitBuffer->NameLength相比较，如下面例子所示：

if(InputBufferLength<sizeof(WAIT_FOR_BUFFER)){

IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER);

Return(STATUS_INVALID_PARAMETER);

}

WaitBuffer=Irp->AssociatedIrp.SystemBuffer;

if((InputBufferLength

FIELD_OFFSET(WAIT_FOR_BUFFER,Name[0])>

WaitBuffer->NameLength){

IoCompleteRequest(Irp,STATUS_INVALID_PARAMETER);

Return(STATUS_INVALID_PARAMETER);

}

前面的减运算不会下溢，因为第一个if语句保证了InputBufferLength大于或等于WAIT_FOR_BUFFER的大小。

下面例子是一个更复杂的下溢问题：

case IOCTL_SET_VALUE:

dwSize=sizeof(SET_VALUE);

if(inputBufferLength<dwSize){

ntStatus=STATUS_BUFFER_TOO_SMALL;

break;

}

dwSize=FIELD_OFFSET(SET_VALUE,pInfo[0])+

pSetValue->NumEntries*sizeof(SET_VALUE_INFO);

if(inputBufferLength<dwSize){

ntStatus=STATUS_BUFFER_TOO_SMALL;

break;

}

在这个例子中，乘运算时会发生整数上溢。如果SET_VALUE_INFO结构的大小是2的倍数，像0x80000000这样的NumEntries值在做乘运算位左移时就会上溢。然而，缓冲大小仍然可以通过有效性验证，因为上溢使dwSize显得非常小。为了避免这个问题，像前一个例子那样减去长度，用sizeof（SET_VALUE_INFO）去除，然后将结果与NumEntries相比较。
1.10.2 引用用户空间地址时的错误

驱动程序应该在使用任何用户空间中的地址之前，验证它的有效性。I/O管理器既不验证这种地址的有效性，也不验证缓冲中传递给驱动程序的指针的有效性。

没有验证在Type3(METHOD_NEITHER)IOCTL和FSCTL中被传递的地址的有效性

I/O管理器不为METHOD_NEITHER的IOCTL和FSCTL做任何有效性验证。为了保证用户空间地址是有效的，驱动程序必须使用ProbeForRead和ProbeForWrite例程，将所有缓冲引用嵌入try/except块中。

在下面的例子中，驱动程序假定Type3InputBuffer中传递的值是一个有效的地址：

case IOCTL_GET_HANDLER:{

PULONG EntryPoint;

EntryPoint=

IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;

* EntryPoint=(ULONG)DriverEntryPoint;

下面的代码避免了这个问题：

case IOCTL_GET_HANDLER:{

PULONG_PTR EntryPoint;

EntryPoint=

IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;

try{

if(Irp->RequestorMode!=KernelMode){

ProbeForWrite(EntryPoint,

sizeof(ULONG_PTR),

TYPE_ALIGNMENT(ULONG_PTR));

}

*EntryPoint=(ULONG_PTR)DriverEntryPoint;

}except(EXCEPTION_EXECUTE_HANDLER){

应注意：正确的代码将DriverEntryPoint强制转换为ULONG_PTR类型。这个转换在64位Windows环境下可以做更进一步的应用。

没有验证嵌在缓冲I/O请求中的指针的有效性

通常驱动程序会在缓冲请求中嵌入指针，如下面例子所示：

struct ret_buf{

void *arg; //Pointer embedded in request

int rval;

};

pBuf=Irp->AssociatedIrp.SystemBuffer;

…

arg=pBuf->arg; //Fetch the embedded pointer

…

//If the pointer is invalid,

//this statement can corrupt the system.

RtlMoveMemoru(arg,&info,sizeof(info));

在这个例子中，驱动程序应该使用放在try/except块中的ProbeXxx例程来验证嵌入指针的有效性，像在前面验证METHOD_NEITHER IOCTL的有效性一样。虽然嵌入指针使得驱动程序可以返回更多的信息，但是驱动程序可以通过使用有关的偏移量或可变长度缓冲来更有效地达到这个目的。
1.10.3 直接I/O中的错误

最常见的直接I/O问题是不能正确地处理零长度缓冲。因为I/O管理器不为零长度传输创建MDL，所以零长度缓冲使得Irp->MdlAddress的值为NULL。

为了映射地址空间，Windows 2000驱动程序应该使用MmGetSystemAddressForMdlSafe，它在映射失败时返回NULL。如果驱动程序传递了一个值为NULL的MdlAddress，它也会返回NULL。在Windows 98上，MmGetSystemAddressForMdlSafe在错误时返回NULL。驱动程序应该在每次试图使用返回的地址之前，先检查返回的是不是NULL。

直接I/O包括用户地址空间与系统地址缓冲之间的双向映射，这样两个不同的虚地址就有了同一个物理地址。双向映射会引起下列结果，有时它们会引起问题：

§ 用户地址虚页的偏移量变成了系统页的偏移量。

超出系统缓冲边界的访问可能长时间没有被通知，这个时间取决于映射的页粒度。如果分配给调用者的缓冲距离页边界较远，那么写在缓冲边界外的数据仍然会出现在缓冲中，调用者不会意识到已经发生了错误。如果缓冲边界恰好与页边界重合，在边界之外的系统虚地址可能会指向其他内容或无效。这样的问题很难发现。

§ 如果调用进程有修改的另一个线程，在用户内存映射改变时，系统缓冲的内容也会改变。

在这种情况下，使用系统缓冲来保存过期（scratch）数据会带来问题。两个从相同内存单元取数的操作可能会得到不同的数值。

下列代码段在一次直接I/O请求中接收到一个字符串，然后试图把这个字符串转换成大写的：

PWCHAR PortName=NULL;

PortName=(PWCHAR)MmGetSystemAddressFromMdl(irp->MdlAddress);

//

//Null-terminate the PortName so that RtlInitUnicodeString will not

//be invalid.

//

PortName[Size/sizepf(WCHAR)-1]=UNICODE_NULL;

RtlInitUnicodeString(&AdapterName,PortName);

因为缓冲可能没有所要求的正确格式，所以代码试图将Unicode的NULL强制作为最后一个缓冲字符。然而，如果下层物理内存同时被映射为用户模式和内核模式地址，一旦写操作完成，这个进程的另一个线程就会写覆盖掉缓冲。

相反地，如果没有NULL，那么对RtlInitUnicodeString的调用就会超出缓冲的范围。并且如果它处于系统映射之外的话，可能引起错误检测。

如果驱动程序创建并映射它自己的MDL，那么它应该保证它只是用自己已经验证过的方法来访问这个MDL。也就是说，当驱动程序调用MmProbeAndLockPages时，它指定一种访问方法（IoReadAccess、IoWriteAccess或IoModifyAccess）。如果驱动程序指定了IoReadAccess，它就决不能再试图通过MmGetSystemAddressForMdl或MmGetSystemAddressForMdlSafe向系统缓冲写。
1.10.4 调用者输入和设备状态的错误

驱动程序应该验证所有来自调用者的输入的有效性。因为驱动程序不能保证调用者传递了正确的数据类型或个数，也不能保证调用者只是在设备或驱动程序处于正确状态时才进行调用。

没有验证设备状态的有效性

下面例子的驱动程序在调试版（checked build）中使用了ASSERT宏检查设备是否处于正确状态，而在发行版（free build）中没有检查设备的状态：

case IOCTL_WAIT_FOR_EVENT:

ASSERT((!Extension->WaitEventIrp));

Extension->WaitEventIrp=Irp;

IoMarkIrpPending(Irp);

status=STATUS_PENDING;

在调试版中，如果驱动程序已经使IRP保持在未决状态，系统就会发通知（assert）。然而，在发行版中，驱动程序没有检查这种错误。对相同IOCTL的两个调用会使系统失去对IRP的跟踪。

在多处理器系统中，这个代码段可能会带来其他问题。假定这个例程在入口点有这个IRP的所有权（操纵的权利）。当例程把Irp指针保存在全局结构Extension->WaitEventIrp中时，另一个线程可以从这个全局结构中得到此IRP地址，然后对这个IRP进行操作。为了防止这种问题的发生，驱动程序应该在保存IRP之前将此IRP标记为未决，并且应该在互锁的队列中包括对IoMarkIrpPending的调用和分配。驱动程序可能还需要为此IRP建立Cancel例程，请参见第12章“Cancel例程”。

收到不明设备对象的意外I/O请求

很多驱动程序通过调用IoCreateDevice创建了一种以上的设备对象。有些驱动程序甚至在驱动程序创建FDO之前，在它们的DriverEntry例程中创建了控制设备对象，使得应用程序可以与驱动程序通信。例如，文件系统驱动程序在用IoRegisterFileSystem将自己注册为文件系统时，创建设备对象来处理文件系统通知。

驱动程序应该能够在它自己创建的任何设备上处理Create请求。在成功地完成了Create请求之后，它应该准备在创建的文件对象上接收任何用户可访问I/O请求。因此，创建了一个以上设备对象的驱动程序必须检查每个I/O请求指定的是哪个设备对象。

例如，驱动程序可能期望I/O请求为给定设备指定FDO，而实际上请求指定了它的控制设备对象。如果驱动程序没有像在其他设备对象中那样，在此控制设备对象的设备扩展中初始化相同的域，当尝试使用来自此控制设备对象的设备扩展信息时，驱动程序可能会崩溃。

没有验证句柄的有效性

有些驱动程序必须操纵调用者传给它们的对象，或必须在同一时间处理两个文件对象。例如，调制解调器驱动程序可能收到一个事件对象的句柄，或网络驱动程序可能收到两个不同文件对象的句柄。驱动程序必须验证这些句柄的有效性。因为它们是由调用者传来的，而不是通过I/O管理器，I/O管理器不能做任何有效性验证。

例如，在下面的代码段中，驱动程序被传递了句柄AscInfo->AddressHandle，却没有在调用ObReferenceObjectByHandle前验证它：

//

//This handle is embedded in a buffered request.

//

status=ObReferenceObjectByHandle(

AscInfo->AddressHandle,

0,

NULL,

KernelMode,

&fileObject,

NULL);

if(NT_SUCCESS(status)){

if((fileObject->DeviceObject= = DeviceObject)&&

(fileObject->FsContext2= =TRANSPORT_SOCK)){

虽然对的ObReferenceObjectByHandle调用成功了，代码却不能保证返回的指针引用了一个文件对象，它相信调用者传递了正确的信息。

即使调用ObReferenceObjectByHandle的所有参数都是正确的，而且调用成功了，如果文件对象不是驱动程序想要的，那么驱动程序仍然会得到无法预料的结果。在下面的代码段中，驱动程序假定成功调用返回了它想要的指向文件对象的指针：

status=ObReferenceObjectByHandle(

AscInfo->Handle,

0L,

DesiredAccess,

*IoFileObjectType,

Irp->RequestorMode,

(PVOID *)&AcpEndpointFileObject,

NULL);

if(!NT_SUCCESS(status)){

goto complete;

}

AcpEndpoint= AcpEndpointFileObject->FsContext;

if(AcpEndpoint->Type!=BlockTypeEndpoint)

虽然ObReferenceObjectByHandle返回了指向文件对象的指针，驱动程序还是不能保证指针引用的是它想要的文件对象。在这种情况下，驱动程序应该在访问驱动程序指定的AcpEndpointFileObject->FsContext数据之前，验证这个指针的有效性。

为了预防这样的问题，驱动程序应该检查以得到有效的数据，如下所示：

§ 检查对象类型以保证它是驱动程序想要的。

§ 保证请求的访问适合于这种对象类型和所需的任务。例如，如果驱动程序执行快速文件复制，就要保证句柄可以做读访问。

§ 一定要指定正确的访问模式（UserMode或KernelMode），要保证这种访问模式与请求的访问兼容。

§ 如果驱动程序需要它自己创建的文件对象的句柄，按照设备对象或驱动程序验证此句柄的有效性。然而，注意不要破坏了想不明设备发送I/O请求的过滤器。

§ 如果驱动程序支持多种类型的文件对象（如控制通道；地址对象；TDI驱动程序或文件系统的Volume、Directory、File对象的连接），要保证能区分它们。
1.10.5 Dispatch例程中的错误

有些驱动程序没有区分DispatchCleanup例程和DispatchClose例程中要求的任务。当文件对象的最后一个句柄被关闭时，I/O管理器调用DispatchCleanup例程。而当最后一个被从这个文件对象中释放时，调用DispatchClose例程。驱动程序不应该试图在它的DispatchCleanup例程中释放资源，因为它们正被连接在一个文件对象上，可能被其他Dispatch例程使用。

调用发送例程时，I/O管理器为正常I/O调用保持一个对文件对象的引用。结果，驱动程序可以在其DispatchCleanup例程被调用之后，而DispatchClose例程被调用之前，收到对文件对象的I/O请求。例如，在来自另一个线程的I/O管理器请求正被处理时，用户模式调用者可能关闭这个文件句柄。如果驱动程序在I/O管理器调用其DispatchClose例程之前，已经删除或释放了必需的资源，就会发生无效的指针引用或其他问题。

合并公共IOCTL和私有IOCTL路径

一般，驱动程序不应该有私有（内部）和公共IOCTL的合并执行路径。驱动程序无法仅凭IOCTL代码确定IOCTL来源于内核模式还是用户模式。因此以同一路径处理二者会使驱动程序的安全得不到保障。如果某个私有IOCTL是授权的，那么知道了这个IOCTL代码的未授权用户就可能能够访问它。因此，如果驱动程序创建了私有IOCTL，那么要保证将这些IOCTL与它必须支持的公共IOCTL分开处理。
1.10.6 多处理器环境中的错误

在Windows NT/Windows 2000系统中，驱动程序是多线程的，它们可以同时从不同的线程收到多个I/O请求。在设计驱动程序时，必须保证它能在SMP系统上运行，而且要采取适当的措施以确保数据完整性。

具体地说就是，无论何时驱动程序改变了全局或文件对象数据，它必须用锁或互锁队列防止竞态条件的产生。

引用全局或文件对象指定的数据时产生了竞态条件

在下面的代码段中，当驱动程序访问Data.LpcInfo中的全局数据时，会产生竞态条件：

PLPC_INFO pLpcInfo=&Data.LpcInfo; //Pointer to global data

…

…

//This saved pointer may be overwritten by another thread,

pLpcInfo->LpcPort.Buffer=ExAllocatePool(

PagedPool,

arg->PortName.Length);

作为IOCTL调用结果的多个进入此代码段的线程会使内存泄漏，因为指针被写覆盖了。为了避免这个问题，驱动程序应该在改变全局数据时，使用ExInterlockedXxx例程或某类锁。驱动程序的需求决定了锁的类型。细节请参见“使用自旋锁”、“调度者对象”和ExAcquireResourceLite。

下面的例子试图重新分配一个文件指定的缓冲（Endpoint->LocalAddress）来保存终点（endpoint）地址：

Endpoint=FileObject->FsContext;

if(Endpoint->LocalAddress!=NULL&&

Endpoint->LocalAddressLength<

ListenEndpoint->LocalAddressLength){

FREE_POOL(Endpoint->LocalAddress,

LOCAL_ADDRESS_POOL_TAG

);

Endpoint->LocalAddress=NULL;

}

if(Endpoint->LocalAddress= =NULL){

Endpoint->LocalAddress=ALLOCATE_POOL(NonFagedPool,

ListenEndpoint->LocalAddressLength,

LOCAL_ADDRESS_POOL_TAG);

}

在这个例子中，竞态条件会在访问文件对象时发生。因为驱动程序没有持有任何锁，所以对同一文件对象的两个请求可以进入这个函数。结果可能是内存被释放、多个试图释放相同内存或内存泄漏。为了避免这些错误，两个if语句应该被放在一个自旋锁中。
1.10.7 处理IRP时的错误

下列是在驱动程序处理IRP时常会犯的错误：

丢失或完成两次的IRP

这些问题和忘记调用I/O管理器例程（如IoStartNextPacket）通常发生在错误处理路径上。快速检查驱动程序路径可以发现这样的问题。

错误地复制栈位置

将IRP下传到栈中时，使用标准函数IoSkipCurrentIrpStackLocation和IoCopyCurrentIrpStackLocationToNext，而不要编写驱动程序决定的代码来复制栈位置。使用这些标准例程能保证驱动程序没有复制位于其上的驱动程序的Cancel例程。

完成驱动程序没有处理的IRP时返回成功

驱动程序决不能为它没有处理的IRP返回STATUS_SUCCESS。例如，有些驱动程序尽管没有实现所要求的功能，仍以成功状态不正确地完成了查询IRP。这样做很容易使系统崩溃或瘫痪，特别是在像文件名查找这样的操作期间，如果I/O管理器或其他组件试图使用Dispatch例程没有初始化的数据的话。除非某个IRP在文档中有另外说明，否则驱动程序应该为它没有处理的所有IRP都返回STATUS_NOT_SUPPORTED。

使用撤消自旋锁时引起死锁

I/O管理器无论何时调用驱动程序的Cancel例程都持有撤消自旋锁。如果驱动程序的Cancel例程中获得了第二个自旋锁，就会发生死锁。当驱动程序内的另一个线程以相反顺序获得这个锁时，死锁也会发生。


图形系统概述

Microsoft Windows NTÒ/Windows 2000提供了一个健壮的图形体系结构，使第三方图形硬件公司能很容易地集成其视频显示器和打印设备。本书为编写有效的图形驱动程序提供了设计指南。可分为以下几个部分：

§ Part 1：图形驱动程序

Part 1描述了图形驱动程序接口（GDI）和设备驱动程序接口（DDI），讨论了显示器和打印机驱动程序公共的设计和实现细节。

§ Part 2：显示器和视频微端口驱动程序

Part 2描述了Windows NT/Windows 2000的视频显示器环境，为显示器和视频微端口驱动程序编写者提供了设计和实现细节。

§ Part 3：打印机驱动程序和假脱机打印部件

Part 3描述了构成Windows NT/Windows 2000打印环境的驱动程序和假脱机打印部件，解释了如何提供驱动程序和假脱机打印部件的定制，以提供对新的打印机硬件和网络配置的支持。

§ Part 4：静态映像驱动程序

Part 4可以在在线DDK上找到，描述了用Microsoft静态映像体系结构（Microsoft STI）定义的COM接口，这对提供平板扫描仪和数字静态映像相机这些静态映像硬件的供应商是有用的。

图形驱动程序的术语表，在在线DDK上Design Guide的结尾可以找到，对图形子系统和驱动程序设计定义了详细的术语和缩略语。

图形驱动程序函数参考可以在在线DDK的Graphics Driver Reference中找到。

1.1 文档约定

本书使用的字体约定和所有其他的Microsoft WindowsÒ 2000 DDK书中的约定是一样的。这些约定在驱动程序编写者指南中描述。

许多DDK视频和打印机代码例子中使用了匈牙利命名规则。匈牙利命名规则在平台SDK文档中描述。


对图形驱动程序的GDI支持

本章描述了Microsoft Windows NTÒ/Windows 2000图形设备接口（GDI），详细说明了GDI提供的对图形驱动程序的支持。

本书中术语“GDI”指的是核心模式GDI（也称作图形引擎）；对MicrosoftÒ Win32Ò DDI的引用是显式的。核心模式GDI也称作图形引擎。

在线Windows 2000 DDK Graphics Driver Reference中记录了GDI函数和结构参考。大多数GDI函数声明和结构定义在winddi.h中。对于显示器驱动程序，DirectDraw堆管理器函数在dmemmgr.h中声明。这两个文件都和Windows 2000 DDK一起发布。

2.1 从驱动程序的观点看GDI

GDI是Windows NTÒ/WindowsÒ 2000图形驱动程序和应用之间的中介支持。应用程序调用Win32Ò GDI函数进行图形输出请求，这个请求通过核心模式GDI发送。然后核心模式GDI把这些请求发送到相应的图形驱动程序，如显示器驱动程序或打印机驱动程序。核心模式GDI是一个不能被替代的系统提供的模块。

GDI通过一系列设备驱动程序接口（DDI）函数和图形驱动程序通信。这些函数用其前缀Drv标识。信息通过这些入口点的输入/输出参数在GDI和驱动程序之间传递。驱动程序必须支持某些Drv Xxx函数用于GDI调用。在返回GDI之前，驱动程序通过执行在其相关硬件上相应的操作来支持GDI请求。

GDI本身包括许多图形输出能力，去除驱动程序中支持这些能力的需求就能减小驱动程序的大小。GDI也输出驱动程序能够调用的服务函数，进一步减小了驱动程序必须提供支持的图形输出能力。GDI服务函数用其Eng前缀标识，而提供访问GDI维护的结构的函数用Xxx OBJ_Xxx的形式命名。

图2.1显示了这个通信流。

图2.1 图形驱动程序和GDI的相互作用

2.1.1 作为应用图形语言的GDI

Win32 GDI和图形引擎都是完全与设备无关的。因此，应用不需要直接访问硬件。基于一个应用图形请求，GDI与设备无关的驱动程序一起工作，为一组图形设备提供高品质的图形输出。打印和显示设备使用相同的GDI代码路径。

2.1.2 作为绘制引擎的GDI

对于绘图操作，驱动程序首先必须对每个已经有效的PDEV结构启用一个表面。PDEV是一个物理设备的逻辑表示。如果硬件能够用GDI标准格式的位图建立，GDI就能用来进行一些或所有的位图表面的绘制。GDI也能处理高级的过渡调色技术。

对于启用PDEV和表面的信息，参考图形驱动程序参考中的DrvEnablePDEV和DrvEnableSurface函数。

2.1.2.1 GDI管理的位图

GDI全部用设备无关位图（DIB）格式管理位图，包括每个像素1位、4位、8位、16位、24位和32位。在这些位图上，GDI能进行所有的绘制直线、填充、文本输出以及位块传输（bitblt）操作。这使得驱动程序用GDI进行所有的图形绘制，或者使用实现函数都是可能的，因为硬件提供了特别支持。

如果设备在DIB格式中有帧缓冲区，GDI能够直接把一些或所有的图形输出到帧缓冲区，因而减小了驱动程序的大小。如果设备使用了非标准格式的帧缓冲区，驱动程序就必须实现所有要求的绘图函数。GDI还能模拟大多数绘图函数，尽管提高了性能上的代价：像素在被GDI操作之前必须拷贝到一个标准格式的位图中，并且在绘图完成后必须拷贝回原来的格式。

2.1.2.2 GDI管理的直线和曲线

GDI提供了改进的直线和曲线的定义。在DEVICE坐标中直线端点的坐标不要求是整数，就像Windows 3.x一样。这允许驱动程序不进行大致的取舍就传送图形对象。在GDI中基本的曲线是贝塞尔曲线（立方体锯齿）而不是一个椭圆。所有的GDI内部操作是用贝塞尔曲线处理的，它们被大多数高端的设备支持。对那些不处理贝塞尔曲线的设备，GDI在调用驱动程序绘制它们之前把曲线分割成直线段。

GDI能够下载用路径形式填充的区域，还有矩形的形式。驱动程序能够把路径分解成梯形或区间进行填充。

2.1.2.3 GDI管理的属性：画刷

GDI也管理所有的属性。GDI把属性作为画刷传递给驱动程序：驱动程序通过把画刷转换成有用的内部格式来实现它们。GDI为驱动程序维护这些转换信息。GDI还维护画刷所有的状态：包括范围、相关性、当前位置和线型。驱动程序能够缓存信息但不用来维护任何状态。除了初始化和画刷实现，GDI仅调用驱动程序在设备上绘图。在调用驱动程序之前，GDI关心转换、区域锁定以及指针与非操作。

当驱动程序要求使用还未实现的画刷，就需要回调GDI。GDI为画刷分配存储器并调用驱动程序来实现它，如果需要，可以进行抖动处理。

2.1.2.4 GDI过渡调色功能

GDI的过渡调色功能产生了高品质的抖动或颜色过渡调色映像，提供给没有内置这些功能的打印设备和显示设备。颜色过渡调色能提供：

§ 在给定的设备上提供最高品质的可再现的彩色和灰度级别

§ 在一组有限的强度级别上提高可视分辨率

§ 在不同的输出设备之间改进彩色相关性

传统的模拟过渡调色是一个使用过渡调色屏幕的单元过程。这个屏幕由相等大小的单元组成，中心到中心是固定的单元间隔。固定单元间隔调节墨水的浓度，而点的尺寸可以改变，用来产生连续色调的印记。

在计算机上大多数打印或屏幕阴影也使用固定单元的像素尺寸。为模拟点大小的变化，用一束像素的组合来模拟过渡调色屏幕。在Windows NTÒ/WindowsÒ 2000中，GDI包括提供一个好的第一近似的过渡调色缺省参数。额外的设备指定信息能加进系统以改进输出。

2.2 GDI/驱动程序的分工

为了理解图形驱动程序设计，弄懂GDI和驱动程序的角色以及它们如何协调是重要的。具有增强能力的GDI能够处理很多图形驱动程序预先要求的操作。GDI对于管理一些对图形操作很关键的数据结构，例如表面，也是有效的，尽管每个图形驱动程序必须访问它们。

2.2.1 GDI和驱动程序的通信

驱动程序对于GDI仅输出一个函数：DrvEnableDriver。所有其他的驱动程序支持的函数，包括DrvDisableDriver函数，是GDI通过一个指针数组来输出的。GDI调用DrvEnableDriver初始化驱动程序并返回驱动程序支持的DDI函数列表。因为有一些驱动程序必须支持的函数，所以GDI处理操作不包括在驱动程序的DrvEnableDriver例程的函数表中。当驱动程序被卸载时，GDI调用DrvDisableDriver。DDI函数在第3章“支持DDI”中深入讨论。

GDI使大量的服务对于驱动程序是可用的。这些服务分为两类：用户对象和服务例程。

2.2.1.1 GDI用户对象

GDI保护重要的内部数据结构，但通过把它们作为用户对象传递使驱动程序能访问这些结构的公共域。用户对象是中间数据结构，在GDI数据结构和需要访问这些结构内的信息的驱动程序之间提供了接口。驱动程序能把指向用户对象的指针传递回GDI，用以询问消息信息或请求各种服务。带有公共域的用户对象有下列优点：

§ 排除了直接访问内部GDI数据结构相关的问题。

§ 为驱动程序提供了一个空间保存GDI数据。例如，PATHOBJ结构能保存计算一个复杂对象（如路径）要求的所有额外数据。

下列用户对象是可用的：

对象 | 描述
BRUSHOBJ | 为图形函数定义了画刷对象，输出直线、文本或填充。 驱动程序能调用BRUSHOBJ服务来实现画刷或找到GDI预先缓存的实现方法。
CLIPOBJ | 为绘图或填充提供能访问裁剪区的驱动程序。这个区域能用一系列矩形计算。
FLOATOBJ | 允许图形驱动程序模拟浮点操作。浮点操作不适用所有其他的核心模式驱动程序。
FONTOBJ | 使驱动程序访问字体的一个特别的实例（或实现）的信息。
PALOBJ | 包含RGB调色板颜色的结构；通过PALOBJ_cGetColors和DrvSetPalette函数可以访问。
PATHOBJ | 定义路径指定要绘制什么（直线或贝塞尔曲线）。PATHOBJ结构传递给驱动程序用以描述要绘制或填充的一系列直线和贝塞尔曲线。
STROBJ | 为驱动程序计算轮廓处理和位置的列表，描述如何绘制文本字符串。
SURFOBJ | 识别一个表面，它可以是GDI位图、设备相关位图或设备管理的表面。更多的信息参见表面类型。
XFORMOBJ | 描述一个任意的线性二维变换，如几何宽度直线（geometric wide line）。
XLATEOBJ | 定义从源表面格式到目的表面格式转换像素所需要的变换。

2.2.1.2 GDI服务例程

GDI输出许多服务例程，其名字的形式是EngXxx。驱动程序动态地连接到win32k.sys来直接访问这些例程。GDI服务例程包括表面管理、绘图模拟以及路径、调色板、字体和文本服务。这些服务在GDI支持的服务中详细讨论。

2.2.2 PDEV协商

任何图形驱动程序的首要任务之一是在驱动程序初始化期间使PDEV有效。PDEV是物理设备的逻辑表现。这个表现由驱动程序定义，一般是私有的数据结构。启用PDEV的更多的信息参考DrvEnablePDEV。

通过DrvEnablePDEV函数，驱动程序必须给GDI提供信息，描述请求的设备及其能力。驱动程序给GDI的一条重要信息是DEVINFO结构的flGraphicsCaps和flGraphicsCaps2成员中的一组图形能力标志（GCAPS_Xxx和GCAPS2_Xxx标志）。

能力标志允许GDI确定那些操作是PDEV支持的。例如，GDI测试能力标志，指示在GDI尝试用这些基本的类型调用DrvStrokePath函数来绘制路径之前，PDEV是否能处理贝塞尔曲线和几何宽度直线。如果能力标志指示PDEV不能处理这些基本类型，GDI断开直线或曲线，使GDI能简化对驱动程序的调用。

从驱动程序一侧来看，无论何时驱动程序从GDI获得一个高级的路径相关的调用，如果路径或裁剪区对设备进行的处理过于复杂，它可返回 FALSE。

当驱动程序处理一条装饰线（cosmetic line）时它不能从DrvStrokePath返回FALSE，因为它必须为装饰线处理任意复杂的裁剪区或造型。然而，如果路径是贝塞尔曲线或几何直线，DrvStrokePath能够返回FALSE。当这种情况出现时，GDI把调用分割成简单的调用。就像能力标志位没有置1一样。例如，如果当DrvStrokePath发送一个几何直线时返回FALSE，GDI简化直线并调用DrvFillPath函数。

如果DrvStrokePath被报告一个错误，它必须返回DDI_ERROR。

在GDI和驱动程序之间的这种协商，对依赖于PDEV的函数，允许GDI和驱动程序产生高质量的输出而无须过多的通信。

2.2.3 表面协商

绘图和文本输出要求一个绘制的表面。这个表面由DrvEnableSurface函数创建并称作主表面（primary surface）。它也被称作屏幕上的表面（on-screen surface），因为它出现在视频显示器上。每个PDEV只能启用一个主表面，尽管一个驱动程序能够支持几个PDEV。支持DrvCreateDeviceBitmap函数的驱动程序能够创建和使用其他的表面。这些位图表面称作次要表面（secondary surface）或屏幕外的表面（off-screen surface）。对任一种类型的表面，驱动程序负责确定它支持的绘图操作的类型。

2.2.3.1 表面坐标

设备表面是228*228像素数组的子集。这些像素是由一对28位的带符号数寻址的。设备表面左上角的像素坐标是（0，0）。设备表面位于这个坐标空间的右下象限，两个坐标都是非负的。

2.2.3.2 DC原点

应用程序要求在227*227像素的数组范围内保存其图形。设备空间在DDI级有其他的尺寸，因为窗口管理器可能用一个带符号的27位的坐标即DC原点偏移应用程序的坐标。DC原点对驱动程序是不可见的，驱动程序在偏移执行之后才识别图形坐标。

2.2.3.3 FIX坐标

DDI使用分数坐标，能够在设备表面上1/16像素的范围内表示一个位置。（在矢量设备上，分数坐标比设备分辨率精确16倍。）分数坐标用32位数字表示，是带符号的28.4位FIX表示法。在这种表示法中，高28位表示坐标的整数部分，最低的4位表示分数部分。例如，0x0000003C等于+3.7500，0xFFFFFFE8等于-1.5000。

FIX坐标表示直线和贝塞尔曲线的控制点。对某一对象，如矩形裁剪区，GDI用带符号的32位整数表示坐标。因为坐标是28位数，整数坐标最高的5位或者全清0或者全置1。

2.2.3.4 表面类型

表面类型在如何处理它们的上下文中可以看到。这些类型如下：

§ 引擎管理的表面

§ 设备管理的表面（标准格式位图）

§ 设备管理的表面（非标准表面）

引擎管理的表面

引擎管理的表面有以下特征：

§ 由GDI创建和管理。

§ 用一种标准的DIB格式作为DIB创建：从上至下，原点位于左上角，或者从底向上，原点位于左下角。

§ 类型为STYPE_BITMAP。

§ 表面没有相应的设备句柄。

标准格式位图是单平面、压缩像素（每个像素的数据用连续的方式存储）格式的位图。位图的每条扫描线在4字节的边界排列。

在EngCreateBitmap函数中创建的位图是DIB格式。为了使引擎能管理位图，必须是DIB格式。

设备管理的表面（标准格式位图）

设备管理的表面有以下特征：

§ 通过对设备驱动程序的DrvCreateDeviceBitmap函数的调用创建。

§ 有一个表面的相关设备句柄（DHSURF；参见在线DDK Graphics Driver Reference中的SURFOBJ。）

§ 可以是透明的或不透明的。

不透明的设备管理表面是一种GDI既没有任何有关位图格式的信息，也没有位图中位的参考信息的表面。因为这些原因，驱动程序最少必须支持DrvBitBlt、DrvTextOut和DrvStockePath函数。这样的表面的类型是STYPE_DEVICE。

透明的设备管理表面是一种GDI含有有关位图格式的信息并知道位图中位的位置的表面。因为这个原因，驱动程序不需要实现任何绘图操作，并使它们都服从GDI。这样的表面的类型是STYPE_BITMAP。

驱动程序为了转换不透明的位图到透明的位图，它必须调用EngModifySurface函数。通过这一调用，驱动程序通知GDI位图格式和在存储器中位图的位置。

设备管理的DIB表面允许驱动程序回调GDI使GDI在表面绘图。管理其自身表面的驱动程序，也能通过在其表面周围封装DIB（用EngCreateBitmap创建）而引用到GDI的回调，使用DIB除外。

设备管理的表面（非标准格式位图）

通过调用EngCreateDeviceSurface函数使GDI创建表面并返回一个句柄，驱动程序可以启用一个设备管理的非DIB表面。GDI依赖驱动程序访问和控制绘制到何处，并从设备管理的表面读出。

设备相关位图（DDB），有时称作设备格式位图，是另一种类型的非DIB、设备管理的表面。DDB支持某些驱动程序，如VGA驱动程序，实现快速的位图到屏幕的块传送。DDB也允许驱动程序在屏幕外的显示存储器绘制非DIB位图。如果请求了DDB，驱动程序能支持DrvCreateDeviceBitmap函数并调用EngCreateDeviceBitmap函数使引擎返回一个到位图的句柄。

2.2.3.5 GDI彩色空间转换

GDI使用三个RGB颜色空间描述它的位图。在每个颜色空间中，三个位域，或颜色通道，在给定的颜色中分别用来指定红、绿、蓝使用的位的数量。为了能匹配GDI的位图能力，显示器驱动程序必须能从一个RGB颜色空间转换到另一个。

GDI能识别下列的颜色空间。

§ 5，5，5RGB：红、绿、蓝都是5位颜色通道。

§ 5，6，5RGB：红色是5位颜色通道，绿色是6位颜色通道，蓝色是5位颜色通道。

§ 8，8，8RGB：红、绿、蓝都是8位颜色通道。

通常，当从一个多位的颜色通道向少位的颜色通道转换时，GDI丢弃掉低位。当从一个少位的颜色通道向多位的通道转换时，较小通道的所有位全部拷贝到较大的通道。为了填充较大通道的剩余位，较小通道的某些位将再次拷贝到较大的通道。下表概述了GDI从一个RGB颜色空间转换到另一个所使用的规则。在这个表中，转换过程中值发生改变的颜色通道用黑体表示。

GDI颜色空间转换规则

From | To | 规则 | 例子
5,5,5 | 5,6,5 | 源的绿色通道的最高有效位（MSB）加到目标的绿色通道的低位最后。 | （0x15，0x19，0x1D）变成（0x15，0x33，0x1D）。 注意只有绿色通道改变。 源的5位通道的值是二进制11001，转换成6位值，110011。
5,5,5 | 8,8,8 | 对每个通道，源通道的3个MSB加到目标通道的最低位的最后。 | （0x15，0x19，0x1D）变成（0xAD，0xCE，0xEF）。 在红色通道中，10101变成10101101。类似的变化也出现在绿色和蓝色通道。
5,6,5 | 5,5,5 | 丢弃源绿色通道的最低有效位（LSB）。 | （0x15，0x33，0x1D）变成（0x15，0x19， 0x1D）。 注意只有绿色通道改变。 丢弃110011的最低位，得到11001。
5,6,5 | 8,8,8 | 对源的5位通道（红色和蓝色），从源通道拷贝3个MSB加到目标通道的最低位的最后。对6位的绿色通道，从源通道拷贝2个MSB加到目标通道的最低位的最后。 | （0x15，0x33，0x1D）变成（0xAD，0xCE，0xEF）。 在红色通道中，10101变成10101101。在绿色通道中，110011变成11001111。蓝色通道的变化和红色通道类似。
8,8,8 | 5,5,5 | 丢弃源通道的3个最低有效位（LSB）。 | （0xAB，0xCD，0xEF）变成（0x15，0x19，0x1D）。 在红色通道中，10101101变成10101。类似的变化也出现在其他两个通道。
8,8,8 | 5,6,5 | 丢弃红色和蓝色通道的3个最低有效位（LSB）。丢弃绿色通道的2个最低有效位（LSB）。 | （0xAB，0xCD，0xEF）变成（0x15，0x33，0x1D）。 在绿色通道中，11001101变成110011。红色和蓝色通道的变化与前面列出的变换相同。

2.2.3.6 Hooking和Punting

术语Hooking和Punting指的是驱动程序决定是否提供标准的位图绘画操作，或依赖GDI提供这些操作。如果驱动程序实现了引擎管理的表面，GDI能处理所有的绘图操作。然而，如果硬件能加速这些操作，驱动程序能提供一个或更多的绘图函数。通过实现，或Hooking DrvXxx函数来做到这一点。

也许只想实现绘图操作的一个子集，实现一个特殊的DDI接口点。对它不支持的任何操作，驱动程序能调用相应的GDI函数来实现。这称作Punting到GDI。有一些情形是操作必须在驱动程序内实现。例如，如果驱动程序实现了一个设备管理的表面，某些绘图函数必须在显示器驱动程序内完成。

Hooking

缺省情况下，当绘图表面是引擎管理的表面时，GDI处理绘画（绘图）操作。驱动程序利用硬件对一给定的表面，为一些或所有的绘图函数提供了加速，或者使用了特别的块传送硬件，能够hook这些函数。对于hook调用，驱动程序把hook指定为EngAssociateSurface和EngModifySurface函数flHook参数的标记。

如果驱动程序指定了一个函数的hook标记，它必须提供在其支持DDI入口点的列表中的函数。驱动程序能够优化具有硬件支持的操作。这样的驱动程序在一个hook调用中可能只能处理某一种情况。例如，如果在一个hook调用中请求了复杂的图形，它可能还要更有效地直接回调GDI，允许GDI处理操作。

这里还有另一个例子，驱动程序选择是否处理hook调用。考虑应该支持硬件的驱动程序，有能力用某些ROP处理位块传输调用。即使驱动程序能够独立地实现许多操作，其他方面仅是一个缓冲区。这样的驱动程序将为帧缓冲区给位图表面返回一个句柄，就像为其PDEV表面一样，但它将为自己hook DrvBitBlt调用。当GDI调用DrvBitBlt时，驱动程序能够检查ROP来看它是否是由硬件支持的一个函数。如果不是，驱动程序能通过对EngBitBlt的一个调用把操作传递GDI。

支持设备管理表面的驱动程序必须向外hook一些绘图函数，名字是DrvCopyBits、DrvTextOut和DrvStrokePath。虽然GDI模拟能够处理其他绘图函数，因为性能上的原因推荐这种类型的驱动程序hook其他函数，就像DrvBitBlt和DrvRealizeBrush函数，因为模拟需要从或向表面绘图。

Punting

Punting回调到GDI的意思是提交一个调用到相应的GDI模拟。通常，对每个DrvXxx调用都有一个相应的GDI EngXxx模拟调用，带有相同的参数。在驱动程序制作透明的位图时，所有的参数都没有改变地传递到GDI模拟。对每个调用驱动程序punt回GDI，驱动程序的大小缩减了（因为功能上的代码忽略了）。然而，因为引擎拥有调用，驱动程序在执行速度上没有控制。对一些复杂的情况，在驱动程序中提供支持也许没有实际的优势。

可以hook的GDI图形输出函数

驱动程序能够hook的图形输出函数和相应的GDI模拟在下表中列出。

驱动程序图形输出函数 | 相应的GDI模拟
DrvBitBlt | EngBitBlt
DrvPlgBlt | EngPlgBlt
DrvStretchBlt | EngStretchBlt
DrvStretchBltROP | EngStretchBltROP
DrvTextOut | EngTextOut
DrvStrokePath | EngStrokePath
DrvFillPath | EngFillPath
DrvStrokeAndFillPath | EngStrokeAndFillPath
DrvLineTo | EngLineTo
DrvCopyBits | EngCopyBits
DrvAlphaBlend | EngAlphaBlend
DrvGradientFill | EngGradientFill
DrvTransparentBlt | EngTransparentBlt

2.3 GDI支持的服务

GDI输出许多服务例程，能简化驱动程序的设计。驱动程序能够直接调用这些例程。通常图形引擎服务例程的名字以Eng开头。与特殊的对象相关的服务例程总是用对象的名字开头；例如，CLIPOBJ_cEnumStart是一个CLIPOBJ服务。

注意 第一个参数是用户对象的指针的任务例程，是用户对象的方法和使用通常的C++惯例调用。因此，用C++写的驱动程序能作为方法访问服务例程。

这些服务例程有以下几类：

§ 表面管理

§ 调色板服务

§ 路径服务

§ 绘图服务

§ 字体和文本服务

第3章“支持DDI”描述了DDI的入口点，并解释许多这些服务例程能用来帮助驱动程序实现入口点。每个服务函数的详细描述参见在线DDK。

2.3.1 GDI对表面的支持

对每个PDEV，驱动程序必须支持DrvEnableSurface函数。DrvEnableSurface建立要绘制的表面并将其和PDEV相关。驱动程序也必须支持DrvDisableSurface函数停止创建的表面。因为GDI创建和维护表面，驱动程序依赖几个GDI服务函数（在下页的表中列出）实现启用和禁止表面。

函数名 | 用途
EngAssociateSurface | 用PDEV相关一个表面并定义驱动程序编写者要hook的那个表面的绘图操作。它使用PDEV缺省的调色板和方式步骤。驱动程序在DrvEnableSurface的执行过程中必须对主表面进行调用。当驱动程序在锁定表面进行写之前启用第二个表面时，也必须进行这个调用。
EngCheckAbort | (仅针对打印机)使打印机驱动程序能够测定打印机的工作是否已经结束。
EngCreateBitmap | 创建一个标准格式DIB。在这种类型表面上GDI能够执行所有绘图操作。
EngCreateDeviceBitmap | 创建一个DDB，驱动程序在上面绘图是可靠的（虽然也可创建一个DIB，在这种情况下驱动程序能够回调以使GDI在上面绘图。
EngCreateDeviceSurface | 创建一个设备管理表面。驱动程序为这个表面管理某些绘图操作是可靠的。此函数又返回一个驱动程序管理句柄。
EngDeleteSurface | 删除一个表面（DIB，DDB，或设备管理表面）
EngEraseSurface | 在表面上用给定的颜色填充指定的矩形，有效地清除表面。调用这个函数仅可清除一个GDI位图表面。
EngLockSurface | 通过创建表面的一个用户对象（SURFOBJ）使驱动程序访问已创建的表面。（源表面是不锁定的。）
EngMarkBandingSurface | (仅针对打印机)标记表面为相关的表面。
EngUnlockSurface | 当驱动程序已完成一项绘图操作时释放一个表面（禁止第二个表面时调用此函数）

这些函数更详细的内容，请参考在线DDK。

2.3.2 GDI支持的调色板

GDI可以做大多数有关调色板管理的工作。当GDI调用DrvEnablePDEV函数，驱动程序把缺省的调色板作为DEVINFO结构的一部分返回到GDI。驱动程序必须用EngCreatePalette函数创建这个调色板。

调色板有效地把32位颜色索引映射到24位RGB颜色值，这是GDI使用调色板的方法。驱动程序指定其调色板，这样GDI能确定不同的颜色索引如何在设备上出现。

驱动程序只要使用GDI提供的XLATEOBJ，就不需要处理大多数调色板操作和计算。

如果设备支持可改变的调色板，必须实现DrvSetPalette函数。当应用为设备改变调色板并把作为结果的新调色板传递给驱动程序时，GDI调用DrvSetPalette。驱动程序要设置内部的硬件调色板，尽可能地匹配新调色板。

可以用下表列出的两种格式为GDI定义调色板。

调色板格式 | 描述
Indexed | 一个颜色索引是RGB值的数组的一个索引。这一数组可以是小的，例如，包括16个颜色索引；或者是大的，例如，包括4096个颜色索引，甚至更多。
Bit Fields | 颜色索引里的位域依据每一种颜色里R、G和B的数量指定颜色。例如，5位能够用来为每一种颜色提供0和31之间的一个值。当转换为RGB时，每个部分的5位的值将放大到包括0~255的范围。（通常RGB代表自己是由位域定义的）

相反，GDI一般使用调色板映射。也就是说，应用指定一个RGB颜色进行绘图，并且GDI必须指定设备显示的颜色索引的地址。就像下表指出的，GDI提供两种主要的调色板服务函数用来创建和删除调色板，以及一些与PALOBJ和XLATEOBJ有关的服务函数用于在两个调色板之间转换颜色索引。

函数 | 描述
EngCreatePalette | 创建一个调色板。驱动程序通过给DEVINFO结构里的调色板返回一个句柄使设备和调色板相联系。
EngDeletePalette | 删除给定的调色板。
PALOBJ_cGetColors | 允许驱动程序从从一个索引的调色板下载RBG颜色。它由显示驱动程序在DrvSetPalette函数中调用。
XLATEOBJ_iXlate | 从一个源颜色索引转换到目的颜色索引
XLATEOBJ_piVector | 从一个索引的源调色板中检索转换向量。驱动程序能使用这个向量完成其自身从源索引到目的索引的转换。
XLATEOBJ_cGetPalette | 在一个索引的源调色板中检索24位RGB颜色或颜色的位域格式驱动程序能使用这个函数从调色板获得信息执行颜色混合。

这些函数更详细的内容，请参考在线DDK。

2.3.3 路径的GDI服务

为帮助矢量设备填充复杂的区域，驱动程序能调用创建、修改和计算路径的引擎函数；这些函数在下表列出。驱动程序通过PATHOBJ结构访问路径。

GDI路径服务函数 | 描述
EngCreatePath | 为驱动程序的临时使用分配一个路径。驱动程序在从当前的绘图调用返回GDI之前要删除这个路径。
EngDeletePath | 删除通过EngCreatePath函数创建的路径。
PATHOBJ_vGetBounds | 返回路径的一个混合矩形。
PATHOBJ_vEnumStart | 通知PATHOBJ驱动程序将调用PATHOBJ_bEnum用指定的路径计算曲线。这个函数假设计算重新开始时必须要调用。
PATHOBJ_bEnum | 从路径检索下一个PATHDATA记录。每个记录描述了一个子路径的全部或部分。
PATHOBJ_vEnumStartClipLines | 允许驱动程序请求相对一个PATHOBJ要裁剪的直线。当裁剪区比一个矩形更复杂时是有用的。
PATHOBJ_bEnumClipLines | 从路径计算已裁剪的直线段
PATHOBJ_bMoveTo | 在一个已定义的PATHOBJ路径里改变现有的位置
PATHOBJ_bPloyLineTo | 在一个已定义的PATHOBJ路径里绘制直线
PATHOBJ_bPloyBezierTo | 在一个已定义的PATHOBJ路径里绘制贝塞尔曲线
PATHOBJ_bCloseFigure | 通过正绘制的一条直线回至起始点关闭路径

这些函数更详细的内容，请参考在线DDK。

2.3.4 GDI绘图服务及其他

为了支持CLIPOBJ，BRUSHOBJ，和XFORMOBJ结构，GDI提供几种绘图服务（在下页的表中列出）此外GDI模拟在前面hooking对punting中描述。

GDI绘图服务函数 | 描述
EngCreateClip | 为驱动程序的临时使用分配一个CLIPOBJ。当不再需要它时驱动程序调用EngDeleteClip函数删除它。
EngDeleteClip | 用 EngCreateClip函数删除已分配的CLIPOBJ
EngBitBlt | DrvBitBlt函数的GDI模拟
EngCopyBits | DrvCopyBits函数的GDI模拟
EngStretchBlt | DrvStretchBlt函数的GDI模拟
EngFillPath | DrvFillPath函数的GDI模拟
EngStrokeAndFillPath | DrvStrokeAndFillPath函数的GDI模拟
EngStrokePath | DrvStrokePath函数的GDI模拟
EngLineTo | DrvLineTot函数的GDI模拟
BRUSHOBJ_pvAllocRbrush | 为实现驱动程序的画刷分配存储器。
BRUSHOBJ_pvGetRbrush | 为实现驱动程序的画刷返回一个指针；如果还没有实现画刷就实现画刷。
CLIPOBJ_cEnumStart | 在已裁剪的全部或部分区域里为矩形的计算设定参数。（没有调用这个函数时能够计算一次这个区域，但是随后的计算需要使用这个函数。）
CLIPOBJ_bEnum | 从裁剪区域恢复大量矩形。
CLIPOBJ_ppoGetPath | 作为路径用来恢复复杂的区域。

这些函数更详细的内容，请参考在线DDK。

2.3.5 GDI字体和文本服务

GDI提供对字体管理和文本输出的支持。FONTOBJ结构和相关函数允许驱动程序访问一个字体的特定实例。为了支持文本输出，驱动程序访问STROBJ结构和相关函数。下表列出了FONTOBJ和STROBJ相关的函数。

函数 | 描述
EngT ExtOut | GDI模拟DrvTextOut函数
FONTOBJ_cGetAllGlyphHandles | 允许驱动程序恢复GDI字体的每一个字形句柄。驱动程序使用这个服务下载全部字体。
FONTOBJ_vGetInfo | 返回描述已分配字体的信息。
FONTOBJ_cGetGlyphs | 为字体使用者把字形句柄翻译成指针给已分配的字形数据。到下一次调用FONTOBJ_cGetGlyphs之前这些指针均有效。
FONTOBJ_pQueryGlyphAttrs | 返回有关字体字形的信息。
FONTOBJ_pxoGetXform | 为相关的字体恢复从抽象到设备的转换。这个转换要求驱动程序实现一个驱动程序提供的字体。
FONTOBJ_pifi | 取得一个到描述相关字体的IFIMETRICS结构的指针。
FONTOBJ_pvTrueTypeFontFile | 取得一个到与TrueType字体相关的ROM映射TrueType文件的指针。
STROBJ_vEnumStart | 为指定的STROBJ结构重新计算GLYPHPOS数组。驱动程序要在随后的计算之前调用这个函数。
STROBJ_bEnum | 在指定的STROBJ中计算字体轮廓一致性和位置。
STROBJ_dwGetCodePage | 返回与指定的STROBJ相关的代码页。

这些函数更详细的内容，请参考在线DDK。

2.3.6 GDI支持的服务及其他

GDI给驱动程序编写者提供了多种一般的支持服务，包括装载、读取、操作数据文件的能力，以及分配内核模式和用户模式的存储器。

2.4 浮点数支持

内核模式图形驱动程序在调用GDI提供的EngSaveFloatingPointState和EngRestoreFloatingPointState 例程之间必须进行所有的浮点操作。

如果硬件有一个浮点处理器，驱动程序能够直接做浮点操作。另外，驱动程序能使用GDI的FLOATOBJ服务来计算浮点操作。不考虑处理器类型，当声明浮点值时，驱动程序要使用FLOAT数据类型。

2.5 数据类型

下表中定义的数据类型在设备驱动程序接口里出现。这些数据类型中的几个在前面GDI用户对象里描述过。指针型数据类型用星号（*）标记。

DDI数据类型 | 变量前缀名 | 定义
BOOL | b | 可以是TRUE或FALSE的32位值
BYTE | j | 8位无符号整数
BRUSHOBJ* | pbo | 指向画刷对象的指针
CLIPLINE | cl | 裁剪线对象
CLIPOBJ | pco | 裁剪对象的指针
DHPDEV | dhpdev | 由设备驱动程序定义的32位句柄。它识别一个物理设备。
DHSURF | dhsurf | 由设备驱动程序定义的32位句柄。它识别一个设备管理表面。
FIX | fix | 定点数
FLOATL | e | 浮点数
FLOAT-LONG | el | 32位溢出值，依据上下文解释成LONG或FLOAT
FLONG | fl | 32位标志的设定
FONTOBJ* | pfo | 字体对象的指针
FSHORT | fs | 16位标志的设定
HBM | hbm | 由GDI定义能识别位图的32位句柄
HPAL | hpal | 由GDI定义能识别调色板的32位句柄
HSURF | hsurf | 由GDI定义能识别表面的32位句柄
LONG | l | 32位有符号整数
MIX | mix | 32位数量,它的低16位定义前景和背景的混合模式
PALOBJ* | ppalo | 调色板对象指针
PATHOBJ* | ppo | 路径对象指针
POINTE | pte | 由{FLOAT x,y;}组成的小数点结构
POINTFX | ptfx | 由{FIX x,y;}组成的小数点结构
POINTL | ptl | 由{LONG x,y;}组成的小数点结构
PWSZ | pwsz | 以0结束的Unicode字符串的指针
PVOID | pv | 没有定义数据类型的VOID指针
RECTL | rcl | 由{LONG xLeft,yTop,xRight,yBottom;}组成的矩形结构
RECTFX | rcfx | 由{FIX xLeft,yTop,xRight,yBottom;}组成的矩形结构
ROP4 | rop4 | 指定如何混合源、目标、图形以及屏蔽像素的32位值
SHORT | s | 16位有符号整数
SIZEL | sizl | 由{ULONG Cx,Cy;}组成的二维结构
STROBJ* | pstro | 文本字符串对象指针
SURFOBJ* | pso | 表面对象指针
ULONG | ul | 32位无符号整数
USHORT | us | 16位无符号整数
XFORMOBJ* | pxo | 坐标转换对象指针
XLATEOBJ* | pxlo | 颜色转换对象指针

下表中列出的参数前缀用于修改它们使用的变量名前缀。

前缀 | 参数用途
i | 已计算的索引
c | 计数
p | 指针


支持 DDI

为响应通过图形设备接口（GDI）传送的设备无关应用调用，图形驱动程序必须确认其图形设备产生请求的输出。图形驱动程序通过实现必须的图形驱动程序接口（DDI）控制图形输出。

DDI函数用DrvXxx的形式命名。GDI调用这些DrvXxx函数传递数据到驱动程序。当应用进行GDI请求并且DDI确定驱动程序支持相应的函数，GDI调用这个函数。驱动程序对提供函数是可靠的并且在函数的实现中返回到GDI。

这一章描述显示器和打印机驱动程序编写者必须明白的DDI函数。DDI函数声明、结构定义和常量在winddi.h中可以找到。这个头文件和MicrosoftÒ WindowsÒ 2000 DDK一起发布 。每个DDI入口点函数、GDI服务函数以及DDI结构和类型的详细信息参见在线DDK的Graphics Driver Reference。

3.1 图形驱动程序函数支持

这一节描述驱动程序入口点函数，把它们归类为必需的、有条件必需的和可选的，后一节提供实现信息，对一般的图形驱动程序入口点从功能类别上组织。这个指南的第二和第三部分分别给出了显示器驱动程序和打印机驱动程序详细的设计信息。

当设备驱动程序返回一个错误，一般调用GDI的EngSetLastError函数报告扩展的错误代码。应用程序可以取得这个错误代码。

3.1.1 必需的图形驱动程序函数

所有的图形驱动程序必须支持GDI调用的入口点，启用或禁止驱动程序、PDEV结构和表面与每个PDEV相关。下表以一般调用使用的顺序列出了需要的函数。

入口点 | 描述
DrvEnableDriver | 作为初始化驱动程序入口点，这个函数把驱动程序版本号以及所支持的任意函数的入口点提供给GDI。
DrvGetModes | 由指定的视频硬件设备列出所支持的模式。（只有显示器驱动程序需要这个函数）
DrvEnablePDEV | 启用PDEV。
DrvCompletePDEV | 通知驱动程序设备安装已完成。
DrvEnableSurface | 为指定的硬件设备创建一个表面。
DrvDisableSurface | 通知驱动程序不再需要为当前设备已创建的表面。
DrvDisablePDEV | 当不再需要硬件时，释放已创建但还没有删除、由设备和一些表面使用的存储器和资源。
DrvDisableDriver | 为驱动程序释放所有已分配的资源并把它的初始化状态返回硬件。
DrvAssertMode | 为指定的硬件设备重新设置视频模式。（只有显示器驱动程序需要这个函数）

3.1.2 有条件必需图形驱动程序函数

除了经常请求的函数，还有其他一些函数可能请求，这依赖于驱动程序如何实现。条件请求函数在下表中列出。如果驱动程序管理其自身的主表面（使用EngCreateDeviceSurface函数取得表面的句柄）或者其自身的屏幕外位图，它也必须支持几个绘图函数。驱动程序向标准格式的DIB写时通常允许GDI管理大多数或全部的这些操作。支持可设置调色板的显示器必须也支持DrvSetPallete函数。

对于打印机驱动程序定义或绘制字体比显示器更加普通。显示器驱动程序不要求处理字体。如果硬件有驻留的字体，驱动程序必须对GDI支持有关这个字体的信息。这个信息包括规格、从Unicode到个体字体轮廓识别、个体字体轮廓属性和字距调整表的映射。

入口点 | 请求 | 描述
DrvCopyBits | 设备管理表面 | 源到目标的拷贝
DrvDescribePixelFormat | 在单一表面上具有不同像素格式、支持windows的显示器 | 描述PDEV的像素格式
DrvGetTrueTypeFile | TrueType字体驱动程序 | 使GDI访问映射存储器TrueType字体文件
DrvLoadFontFile | 字体驱动程序 | 为实现字体指定使用的文件
DrvQueryFont | 打印机驱动程序 | 为已给字体检索GDI结构
DrvQueryFontCaps | 字体驱动程序 | 询问驱动程序字体驱动程序的容量
DrvQueryFontData | 打印机驱动程序 | 检索有关已实现字体的信息
DrvQueryFontFile | 字体驱动程序 | 询问驱动程序字体文件信息
DrvQueryFontTree | 打印机驱动程序 | 查询子目录结构定义的字体映射的三种类型之一
DrvQueryTrueTypeOutline | TrueType字体驱动程序 | 返回TrueType字形句柄给GDI
DrvQueryTrueTypeTable | TrueType字体驱动程序 | 使GDI访问TrueType字体文件
DrvResetPDEV | 在文档里允许模式改变的设备 | 从旧PDEV到新PDEV传送驱动程序状态
DrvSetPalette | 支持可设置调色板的显示器 | 为指定设备实现调色板
DrvSetPixelFormat | 在单一表面上具有不同像素格式、支持windows的显示器 | 设定窗口的像素格式
DrvStrokePath | 设备管理表面 | 在显示器上实现路径
DrvSwapBuffers | 具有成对缓冲器、支持像素格式的驱动程序 | 显示表面隐藏的缓冲器的目录
DrvTextOut | 设备管理表面或定义字体的驱动程序 | 在指定位置实现一组字符映像（图示符）
DrvUnloadFontFile | 字体驱动程序 | 通知驱动程序不需要字体文件

3.1.3 可选的图形驱动程序函数

为了减小驱动程序的大小，驱动程序编写者通常只加进硬件支持最好的几个可选的函数。例如，支持图象颜色管理（ICM）的硬件驱动程序能实现DrvIcmXxx函数。下表列出了图形驱动程序能选择实现的函数。

显示器和打印机驱动程序函数

入口点 | 描述
DrvAlphaBlend | 提供具有a混合能力的位块传送。
DrvBitBlt | 执行一般的从和向表面的位块传送
DrvCreateDeviceBitmap | 用驱动程序定义的格式创建和管理位图。
DrvDeleteDeviceBitmap | 删除设备管理位图
DrvDitherColor | 请求设备创建一个相对设备调色板抖动的画刷。
DrvFillPath | 为设备管理的表面绘制一个闭合的路径。
DrvGradientFill | 屏蔽已指定的原语
DrvIcmCheckBitmapBits | 检查在指定位图里的像素是否位于指定转换的设备范围内
DrvIcmCreateColorTransform | 创建ICM颜色的变换
DrvIcmDeleteColorTransform | 删除已指定的ICM颜色的变换
DrvIcmSetDeviceGammaRamp | 设置指定的显示器设备的硬件g斜面
DrvLineTo | 绘制单一的实心装饰线。
DrvPlgBlt | 在设备管理和GDI管理表面的结合之间提供旋转的位块传送能力。
DrvRealizeBrush | 为已定义的表面实现指定的画刷
DrvStretchBlt | 在设备管理和GDI管理表面之间允许加长的位块传送。
DrvStretchBltROP | 使用ROP实现加长的位块传送。
DrvStrokeAndFillPath | 同时填充和识别路径。
DrvSynchronize | 在GDI和显示器驱动程序支持的协处理器设备之间协调绘图操作；这仅针对引擎管理表面。
DrvSynchronizeSurface | 在GDI和显示器驱动程序支持的协处理器设备之间协调绘图操作；这仅针对引擎管理表面。如果驱动程序提供DrvSynchronize和DrvSynchronizeSurface,GDI将仅能调用DrvSynchronizeSurface.
DrvTransparentBlt | 提供透明的位块传送能力。

仅由显示器驱动程序使用的函数

入口点 | 描述
DrvMovePointer | 把指针移动至新位置并刷新它。
DrvSaveScreenBits | 存储或恢复屏幕上指定的矩形。（仅针对显示器驱动程序）
DrvSetPointerShape | 如果驱动程序已完成绘图，从屏幕上删除指针，然后设置一个新的指针形式。

主要由打印机驱动程序使用的函数

入口点 | 描述
DrvDestroyFont | 通知驱动程序不再需要认识字体;驱动程序释放已分配的数据结构。
DrvDrawEscape | 完成绘图类型退出的函数。
DrvEscape | 在独立设备DDI里从无效设备查询信息。
DrvFree | 释放同指示数据结构相关联的字体存储器。

仅由打印机驱动程序使用的函数

入口点 | 描述
DrvEndDoc | 发送文档结束的信息。
DrvFontManagement | 允许访问不能直接通过GDI可用的打印机功能。
DrvGetGlyphMode | 返回作为特殊字体存储的字体类型信息。
DrvNextBand | 了解表面刚刚绘图的区域的内容。
DrvQueryPerBandInfo | 为指定相关的打印机表面返回相关信息。
DrvSendPage | 发送从表面至打印机的写后读位。
DrvStartBanding | 为驱动程序准备分区。
DrvStartDoc | 发送文档开始控制的信息。
DrvStartPage | 发送页首控制的信息。

字体驱动程序函数

入口点 | 描述
DrvQueryAdvanceWidths | 为指定的一组字形提供增加宽度的字符。

3.2 支持初始化和终止函数

图形驱动程序能支持多个设备和每个设备的多个并发使用。因此，初始化和终止发生在3个不同的层次，每个层次都有其自己的定时。初始化以下列顺序发生：

1. 驱动程序初始化

2. PDEV初始化

3. 表面初始化

终止以相反的顺序发生。

3.2.1 驱动程序初始化和清除

当设备驱动程序要实现几个或多个函数，它只向GDI输出DrvEnableDriver。驱动程序通过一个函数表输出其他的支持的函数。GDI向设备驱动程序调用的第一个函数是DrvEnableDriver。在这个函数中，驱动程序在传递进的DRVENABLEDATA结构内填充，这样GDI能确定支持其他的哪个DrvXxx函数以及它们的位置在哪里。在DRVENABLEDATA结构中驱动程序提供下列信息：

§ GDI的DDI版本号。这个常量在winddi.h中定义为DDI_DRIVER_VERSION。

§ 一个指向DRVFN结构的数组的指针，这个结构列出了支持的函数和它们的索引。

§ 在数组中DRVFN结构的成员。

为了使GDI调用一个函数和驱动程序的启用和禁止函数不同，驱动程序必须使函数名和位置对GDI可用。

虽然DrvEnableDriver也能完成一次初始化，如信号的分配，驱动程序在DrvEnableDriver期间不能实际启用硬件。硬件初始化要在驱动程序的DrvEnablePDEV函数中发生。同样地，驱动程序在DrvEnableSurface函数中使表面有效。

GDI调用DrvDisableDriver函数通知驱动程序它将要卸载。为响应这个调用，驱动程序要释放在这一点它分配的所有资源和存储器。

如果硬件需要重新设置，GDI调用驱动程序的DrvAssertMode函数。

3.2.2 PDEV初始化和清除

每个核心模式图形驱动程序表示一个GDI管理的逻辑设备。反过来，一个驱动程序能管理一个或多个PDEV结构。PDEV是物理设备的逻辑表现。它由硬件的类型、逻辑地址和能支持的表面来描述：

§ 硬件的类型─驱动程序支持由硬件的类型描述的PDEV的一个例子是支持假设的LaserWhiz、LaserWhizⅡ和LaserWhiz超级打印机。由GDI传递的设备名指定在驱动程序支持的全部设备集中哪一个设备被请求。

§ 逻辑地址—例如，一个驱动程序能支持连在LPT1、COM2和名为\\SERVER1\PSLASER的服务器上的多个打印机。另外，一个能同时支持超过一个VGA显示器的显示器驱动程序可能通过端口号进行区分，如0x3CE，0x2CE，等等。打印机和其他硬拷贝输出设备的逻辑地址由GDI决定；EngWritePrinter函数向相应的目标直接输出。显示器既可以隐含地确定其逻辑地址，也可以从DEVMODEW的私有部分取到地址。

DEVMODEW结构给驱动程序提供了请求的环境设置，如设备名和指定打印机或显示器驱动程序的其他信息。

§ 表面—每个PDEV请求一个唯一的表面。例如，如果一个打印机驱动程序同时有两个打印作业在工作，每个作业请求不同的页面格式，如风景和肖像格式，每个打印作业请求不同的PDEV。类似地，显示器驱动程序可能在同一显示器上支持两个桌面，每个桌面请求不同的PDEV和表面。对每个表面请求，有一个对DrvEnablePDEV函数的调用为表面创建一个不同的PDEV。

为响应对DrvEnablePDEV的调用，驱动程序通过几个结构返回有关硬件设备能力的信息。

在GDI调用DrvEnablePDEV之前，GDIINFO结构填充着0。驱动程序在GDIINFO中填充下列信息与GDI通信：

§ 驱动程序版本号

§ 基本的设备技术（光栅和矢量）

§ 可打印页的大小和分辨率

§ 彩色调色板和灰度比例信息

§ 字体和文本性能

§ 过渡调色支持

§ 样式步号

驱动程序只填充它支持的域而忽略其余部分。

驱动程序用标志在GDIINFO结构中填充，描述这个PDEV的图形性能。在几乎所有的情况下，GDIINFO的信息告诉GDI驱动程序能提供的图形支持的级别。例如，如果需要绘制一个三角记号，GDIINFO中的信息告诉GDI是否驱动程序能处理贝塞尔曲线或者是否GDI必须发送多条直线线段。驱动程序要填充它支持的所有域，而不涉及其他的域。

驱动程序必须提供的另一条重要的信息是一个指针（phsurfPatterns），指向用表面的句柄填充的缓冲区，表示标准的填充模式。除了标准的填充模式以外，phsurfPatterns能包括空（null），这将导致GDI根据设备分辨率和像素大小自动创建模式表面。当GDI被调用，用标准模式实现一个画刷，它调用DrvRealizeBrush函数实现为请求的模式定义的画刷。

GDI为支持设备的核心驱动程序传递给DrvEnablePDEV一个句柄，hDriver。对于一个打印机驱动程序，hDriver提供句柄给打印机并在假脱机的调用中使用，如EngWritePrinter。

无论何时GDI调用DrvEnablePDEV，驱动程序必须分配存储器，用以支持创建的PDEV，即使调用DrvEnablePDEV用来为不同的模式创建其他的PDEV。（一个驱动程序可能有几个激活的PDEV，虽然在一个时刻只有一个能被启用。）然而，直到GDI调用DrvEnableSurface，不支持一个实际的表面。

如果设备表面请求位图的分配，直到表面启用为止分配是不必要的（通常在DrvEnableSurface函数中）。虽然在实际写到设备之前应用常请求设备信息，等待分配一个大的位图能保存有价值的资源，并在系统初始化时改进驱动程序的性能。

PDEV的初始化完成时，GDI调用DrvCompletePDEV函数通知驱动程序物理设备的安装已经完成。这个函数也给驱动程序提供到PDEV的GDI的逻辑句柄，驱动程序在调用GDI函数中使用。

对驱动程序的DrvDisablePDEV函数的调用指示给定的物理设备不再需要。在这个函数中，驱动程序要释放物理设备使用的所有存储器和资源。

3.2.3 启用和禁止表面

在初始化阶段的最后，GDI调用DrvEnableSurface使驱动程序为一个存在的PDEV启用一个表面。DrvEnableSurface必须通过调用相应的GDI服务指定表面的类型来创建它。就像第2章GDI对表面的支持中描述的，以及依据设备和环境，驱动程序能从DrvEnableSurface中调用合适的GDI服务创建表面：

对设备管理的表面，驱动程序调用EngCreateDeviceSurface函数得到表面的句柄。

为创建一个GDI能完全管理的标准格式（DIB）位图，包括所有绘图操作的性能，驱动程序要调用EnfCreateBitmap函数。驱动程序能钩住它能优化的所有的绘图操作。驱动程序也能使GDI为每个像素分配空间或为自身提供空间，虽然后一个选项通常只由打印机或帧缓冲区使用。

DrvEnableSurface返回一个有效的表面句柄作为返回值。

随着表面的创建，驱动程序必须通过调用GDI的EngAssociateSurface服务使表面与一个PDEV相关。这个调用也告诉GDI驱动程序为表面挂上了哪个绘图函数。

GDI调用DrvDisableSurface函数通知驱动程序不再请求由DrvEnableSurface为PDEV创建的当前的表面。驱动程序必须释放在执行DrvEnableSurface期间分配的所有的存储器和资源。如果PDEV有一个启用的表面，DrvDisableSurface总是在DrvDisablePDEV之前调用。

表面一旦创建，当它不再使用时必须删除。不能正确匹配表面创建的删除将导致无用对象的累积并使系统性能下降。

3.3 创建设备相关位图

当应用请求位图的创建，驱动程序通过支持DrvCreateDeviceBitmap函数能创建和管理DDB。当这样一个驱动程序创建了位图，它能用任何格式存储位图。驱动程序检查传递的参数，并用至少是请求的每个像素的位数提供位图。

注意 图形驱动程序通过在屏幕外的存储器支持位图和使用硬件绘制位图能提高性能。这方面的一个例子是Permedia显示器驱动程序的例子。

在DrvCreateDeviceBitmap中，驱动程序调用GDI的EngCreateDeviceBitmap服务使GDI为设备位图创建一个句柄。

如果驱动程序支持DrvCreateDeviceBitmap，它创建一个DDB，定义其格式，并返回一个它的句柄。驱动程序控制位图用什么格式在哪里存储。驱动程序要支持最接近匹配其设备表面的颜色格式。

位图的内容在创建之后不能定义。如果驱动程序返回NULL，它不能创建和管理位图；而由GDI替代执行这些任务。

如果驱动程序创建了位图，也必须能通过执行DrvDeleteDeviceBitmap函数删除它。

3.4 支持图形输出

驱动程序处理的特殊图形操作依赖于绘图表面和硬件的能力。如果表面是一个标准格式的DIB，GDI处理所有驱动程序不支持的绘图操作。驱动程序能hook任意的绘图函数并实现它们，得到硬件支持的优势。

对于设备管理的表面，驱动程序最少必须支持图形输出函数DrvCopyBits、DrvTextOut和DrvStrokePath。它也能可选支持任何其他的图形输出函数，例如支持DrvBitBlt，能提高性能。有些函数要求确定的能力级别，而其他的允许设备通过在DEVINFO结构中设置相应的GCAPS标志指示其性能。

对驱动程序的所有的绘图调用都是单线程的，而不关心表面的类型。

下面的部分描述驱动程序如何实现下列操作：

§ 绘制直线和曲线

§ 绘制和填充路径

§ 拷贝位图

§ 过渡调色技术

§ 图象颜色管理

3.4.1 绘制直线和曲线

包括在图象输出中的直线和曲线类型是几何直线、装饰直线和贝塞尔曲线。

对于直线和曲线输出，驱动程序能支持DrvStrokePath、DrvFillPath和DrvStrokeAndFillPath函数。如果表面是设备管理的，驱动程序必须支持DrvStrokePath绘制直线；驱动程序不要求支持曲线。

当GDI用任意的属性集绘制直线或曲线，GDI调用DrvStrokePath。至少，DrvStrokePath函数必须支持用实色画刷和任意的裁剪绘制实线和样式装饰线。为实现这一点，GDI的PATHOBJ_Xxx和CLIPOBJ_Xxx服务函数通过预先计算的裁剪把这些直线分割成一个像素宽的线段的集合。DrvStrokePath提供一个指针，plineattrs，指向定义不同直线属性的LINEATTRS结构。

当路径或裁剪对于驱动程序在设备上的过程过于复杂，驱动程序能通过调用EngStrokePath函数把回调函数punt到GDI。对这种情况，GDI能把对DrvStrokePath的调用分割成用预先计算的裁剪得到的一个像素宽的线段的集合。

通过调用GDI的CLIPOBJ_Xxx服务，驱动程序能使GDI计算在路径中的所有的直线，并执行所有的直线裁剪计算。另外，驱动程序可以使用PATHOBJ_Xxx、CLIPOBJ_Xxx或XFORMOBJ_Xxx服务简化图形操作。例如，驱动程序能使用CLIPOBJ_cEnumStart和CLIPOBJ_cEnum计算在一个裁剪区域内的矩形，发送这个区域到打印机，以及裁剪它。驱动程序也能使用PATHOBJ_cEnumStart和PATHOBJ_cEnum计算路径中的直线或曲线。然后它能把路径发送到设备并绘制它。

3.4.1.1 装饰线

装饰线通常是一个像素宽并使用实色画刷绘制。它按照网格交叉点量化（GIQ）菱形约定绘制，这种方法确定装饰线上哪个像素要被绘制。

图3.1显示了一条直线置于一个矩形网格上，像素定位在网格交叉点上。为确定哪个像素将被点亮，想象一个菱形在直线中间并沿直线滑动。菱形的宽度和高度恰好等于两个相邻像素中心的距离。当菱形沿着直线移动，中心完全被菱形覆盖的像素被选中。如果直线穿过的一个点在两个相邻像素的中间，被点亮的像素取决直线的斜度和相邻像素的走向：水平的或是垂直的。

下表概括了这些情况。

直线的斜率（绝对值） | 相邻像素的走向 | 结果
斜率<1或斜率>1 | 水平的 | 点亮菱形左顶点的像素
斜率<1或斜率>1 | 垂直的 | 点亮菱形上顶点的像素
斜率=1 | 水平的 | 点亮菱形上顶点的像素
斜率=1 | 垂直的 | 点亮菱形右顶点的像素

菱形协议使斜率在-1和1之间的直线上每一列有一个像素点亮，使斜率的绝对值比1大的直线上每一行里有一个像素点亮。用这种方式，装饰线能没有间隙地绘制。

装饰线的首尾像素也由菱形协议确定。装饰线是首像素包含而末像素不包含的；即如果直线起始于菱形内的一个像素，这个像素变亮。类似地，如果直线结束于菱形内的一个像素，这个像素不变亮，如图3.1显示。

图3.1 装饰线的菱形协议

为了着色装饰线，DrvStrokePath函数遵循GIQ菱形协议。DrvLineTo函数是一个可选入口点，驱动程序能够为应用调用Microsoft® Win32 LineTo函数提供优化。DrvLineTo比DrvStrokePath简单，因为它仅支持整数端点和实心装饰线。

对于支持R2_NOT混合模式的光栅设备，二进制光栅操作改变目标颜色为相反色，驱动程序必须使用精确的着色。对于要求GDI和驱动程序两者绘图的设备，着色也必须是精确的。这包括GDI在一些位图上绘图的设备以及驱动程序在其他表面上绘图的设备（除非像素太小不能产生可视的差异）。这也包括请求GDI 处理复杂裁剪的设备。

3.4.1.2 几何宽度直线（Geometric Wide Line）

几何直线的形状是由宽度、连接类型、画刷端点类型以及在XFORMOBJ结构里当前区域至设备转换来确定的。能够使用实心的或非实心的画刷绘制直线。

更高级的硬件设备驱动程序可以支持DrvStrokePath函数中的几何宽度直线。在调用DrvEnablePDEV返回的DEVINFO结构里测试GCAPS_GEOMETRICWIDE性能标志位，GDI确定驱动程序是否能够绘制包括几何直线的路径。如果驱动程序没有这个能力，或者因为路径或裁剪对设备过于复杂，而使函数处理此操作失败，GDI自动转换调用较简单的DrvFillPath函数。

几何宽度直线对显示器驱动程序图形函数有特殊的含义。包含设备坐标的路径使用当前转换的逆转换变成世界坐标。具有指定宽度的几何结构获得加宽的路径，考虑连接和端点。这个路径又转换成设备坐标并用指定的画刷填充。

几何宽度直线的线型是由浮点数数组确定的。数组是定长的，但好象能无限重复使用。在世界坐标里，第一个数组条目指定第一个短线的长度；下一个条目指定第一个间隙的长度。在这以后，短线和间隙的长度交替。例如，线型数组{3.0,1.0,1.0,1.0}可绘制长短线交替的直线。

当驱动程序沿着加宽前的路径移动，可以考虑线型，“擦除”路径对应于间隙的部分。这将路径分割成许多子路径。然后分割开的路径被加宽，就像它没有线型，照常应用端点盖住和连接。线型数组可以是不定长的。例如，线型数组{1，0}使驱动程序用交替的短线绘制一条直线。线型状态（定义当前的距离到线型数组）在路径中第一个子路径的开头提供。在每个后来的子路径的开始考虑重置为0.0，这在任何Win32 MoveToEx操作后出现。

3.4.1.3 花式装饰直线（Styled Cosmetic Line）

DrvStrokePath函数必须支持用实色画刷绘制的带任意裁剪的装饰线。驱动程序能够进行一个到GDI的PATHOBJ_vEnumStartClipLines的调用来预先计算裁剪。

装饰线的线型类似于几何宽度直线，因为它由一个重复的数组指定。对于一条造型的装饰线，数组条目是LONG类型的值，包含类型步的长度。类型步和像素的关系由DrvEnablePDEV函数返回的GDIINFO结构的xStyleStep、yStyleStep和DenStyleStep字段定义。

当驱动程序通过复杂的裁剪调用PATHOBJ_vEnumStartClipLines处理装饰线，GDI修改CLIPLINE结构的iStyleState成员来表示线型状态。线型状态是回到线段的第一个像素的偏移，即如果直线没有被裁剪，将绘制第一个像素。线型状态由压缩到一个ULONG值中的两个16位值组成。如果HIGH和LOW是线型状态的高序（high-order）和低序（low-order）的16位值，线型状态的分数版，称作线型位置，可计算为：

style position = HIGH + LOW/denStyleStep

例如，如果在iStyleState中的值是1和2，denStyleStep是3，style position是5/3。为精确确定在线型数组中线型从哪里开始绘制，计算乘积：

style position * denStyleStep

在这个例子中，denStyleStep的值是3，绘图位置计算不包括线型数组的前5个像素（5/3*3）。即这条裁剪线从线型数组中的第6个像素开始绘制。

有y线型的装饰线和x线型的装饰线。如果一条线在x方向扩展dx个设备单元，在y方向扩展dy个设备单元，当下列条件为真直线是y型的：

(dy * yStyleStep) ≥ (dx * xStyleStep)

在这种情况下，对每个像素在y方向的前进，线型位置的前进由yStyleStep/ denStyleStep计算。

反过来，直线是x线型的，当下式为真时，对每个像素在x方向的前进，线型位置的前进由xStyleStep/ denStyleStep计算：

(dx * yStyleStep) > (dy * xStyleStep)

当线型位置前进到一个新的整数时，在线型数组中线型步前进一个单元。

图3.2显示不同斜率的几条装饰线。像素网格显示不是方的；它就像在EGA显示器中显示的那样，在x方向的4个像素与y方向的3个像素表示相同的距离。在这个图示中，线型数组（由LINEATTRS结构的pstyle成员定义）是{1，1}，它把直线分割成相等大小的点和间隙。驱动程序的xStyleStep的值是3，yStyleStep的值是4，denStyleStep是12。

为进一步说明，假设一个点阵打印机有144dpi的水平分辨率和72 dpi的垂直分辨率。另外，假设最小点的长度是1/24英寸。为支持这个打印机，要选择能补偿打印机的方式比的最小的xStyleStep和yStyleStep值，如xStyleStep为1，yStyleStep为2（144/72），denStyleStep为6（144/24）。

图3.2 花式装饰线的例子

如果LINEATTRS结构里标志的LA_ALTERNATE位置1，装饰线使用了指定的线型。在这种情况下，其他每个像素都选中，而不管方向或方式比。线型状态返回，好像线型数组是{1，1}，xStyleStep、yStyleStep和denStyleStep都是一个。换句话说，如果IstyleState是0，第一个像素点亮；IstyleState是1，第一个像素不亮。

如果LINEATTRS结构里标志的LA_STAARTGAP位置1，在线型数组里元素的检测是相反的。第一个数组条目指定第一个间隙的长度，第二个条目指定第一个短线的长度，如此下去。

3.4.1.4 贝塞尔曲线

一些高级硬件设备能够绘制包含贝塞尔曲线的路径，是一般作用的曲线图元。如果这样，在DrvStrokePath函数中驱动程序包含对这些曲线的支持。

当GDI必须在设备管理的表面绘制贝塞尔曲线路径，它测试GCAPS_BEZIERS标志（在DEVINFO结构中）确定是否调用DrvStrokePath。如果调用，这个函数执行执行请求操作或决定不处理它，就像在几何宽度直线中做的那样。在后一种情况中，GDI把请求分割成简单的操作，例如，把曲线转换成近似的直线。

3.4.2 绘制和填充路径

图形驱动程序把路径考虑成通过一个路径对象（PATHOBJ结构）定义的直线和/或曲线序列。为处理闭合路径的填充，驱动程序支持DrvFillPath。

GDI能在设备管理的表面上调用DrvFillPath填充一个路径。GDI比较用DEVINFO结构中的GCAPS_BEZIERS、GCAPS_ALTERNATEFILL和GCAPS_WINDINGFILL标志填充的请求，决定是否调用驱动程序。如果GDI调用驱动程序，驱动程序或者执行操作或者返回，通知GDI路径或裁剪请求对由设备处理过于复杂。对后一种情况，GDI把请求分割成几个简单的操作。

驱动程序也能支持可选的DrvStrokeAndFillPath函数完成路径填充的请求。这个函数在同一时间内填充和绘制路径。许多GDI图元请求这个函数。如果绘制使用了宽线，填充区域必须减小，以补偿路径边界宽度的增加。

当驱动程序从DrvFillPath或DrvStrokeAndFillPath函数返回FALSE，GDI把填充路径请求转换为一系列简单的操作并再次调用驱动程序函数。如果设备在第二次调用DrvFillPath时再返回FALSE，GDI把路径转换成裁剪对象并调用EngFillPath。对再次调用DrvStrokeAndFillPath返回的FALSE，GDI把调用转换成对DrvStrokePath和DrvFillPath的独立的调用。

3.4.2.1填充路径模式

路径定义的两种填充模式是交替（alternate）和旋转（winding）。两种填充模式使用一个奇偶规则决定如何填充闭合路径。

FP_ALTERNATEMODE如下应用奇偶均等规则：从闭合路径中的任意一个起始点向闭合路径外的某点画一条直线。如果直线穿过奇数个路径段，起始点在闭合区域内，是填充区的一部分。穿过偶数个路径段则意味着起始点不在填充区内。

FP_WINDINGMODE不仅考虑矢量穿过路径段的次数，还考虑每个路径段的方向。路径看作从起点向终点绘制，每个路径段的方向由指定点的顺序包含：线段的第一个顶点是“起始”点，第二个顶点是“终”点。现在用交替模式绘制同一个任意线。从0开始，为每个直线穿过“向前”方向的线段加1，对每个“相反的”方向的线段减1。（向前和相反取决于线段产生的点和任意的直线。）如果计数的结果是非0，起始点在填充区内；计数值是0则意味着点在填充区以外。

图3.3显示了如何把两种规则应用到更复杂的自交叉路径的情况。在交替填充模式中，点A在填充区中因为RAY1经过线段奇数次，而点B和C不在，因为RAY2和RAY3经过线段偶数次。在旋转填充模式中，点A和C在填充区中，因为RAY1和RAY3穿过的全部正向（正的）和反向（负的）线段的计数值加起来都不为0，而B在填充区外，因为RAY2穿过的正向（正的）和反向（负的）线段的计数值加起来都为0。

图3.3 交替和旋转模式填充

3.4.2.2 填充区（闭合路径）

像在直线绘制中那样，填充的像素考虑成整数坐标。区域中每条扫描线由路径线段确定左右边界。落在左右边界之间的像素认为是在填充区域内。恰好在左边界上的像素也在区域内，而右边界上的像素则在区域之外。如果顶边界恰好是水平线，边界上的像素都在区域内，而底边界上的像都在区域之外。

图3.4显示了相对于区域的左右边界如何确定包括在填充区域内的像素。从数学的角度来看，这个区域在左边界和上边界是“闭合的”，而在右边界和底边界是“开放的”。

图3.4 确定像素包括在填充区内

上面有关填充区x轴的描述约定也应用于y轴，用顶边界代替左边界，用底边界代替右边界。

3.4.3 拷贝位图

驱动程序实现的位块传输（BitBlt）函数必须从一个表面向另一个拷贝位块。这些函数包括：

§ DrvBitBlt

§ DrvStretchBlt

§ DrvCopyBits

还有一个显示器驱动程序指定的BitBlt函数，名字为DrvSaveScreenBits。

如果绘制的表面是在设备管理的表面或位图上，驱动程序必须支持最小级别的位块传输函数。如果表面是GDI管理的标准格式位图，GDI只处理没有被驱动程序挂起的操作。

DrvBitBlt函数提供了一般的位块传输能力。如果驱动程序支持设备管理的表面或位图，它必须实现DrvCopyBits函数。至少，当调用DrvCopyBits时，驱动程序必须进行以下操作：

§ 和位图之间进行块传输，用驱动程序和设备表面推荐的格式。

§ 用SRCCOPY（0xCCCC）ROP执行传输。

§ 允许任意的裁剪。

驱动程序能使用GDI CLIPOBJ计算服务，减少对一系列裁剪矩形的裁剪。GDI向下传递一个转换向量，XLATEOBJ结构，帮助源和目标表面的颜色索引转换。

如果设备的表面由标准格式的DIB组成，驱动程序可以仅支持简单传输。如果调用用一个复杂的ROP进来，驱动程序通过调用EngCopyBits函数把块传输请求直接传回到GDI。这允许GDI把调用分割成驱动程序能够执行的简单函数。

如果使用了源，DrvBitBlt映像一个几何源矩形到几何目标矩形，如果没有源，DrvBitBlt忽略了pptsrc参数。目标矩形是要修改的表面，它由两个整数端点左上角和右下角定义，矩形是右下排斥的；矩形的右边界和下边界不是块传输的一部分。DrvBitBlt不能由一个空的目标矩形调用。矩形的两个点总是有顺序的，即右下点的两个坐标都大于对角左上角点的坐标。

DrvBitBlt处理不同的ROP并能依赖设备进行一些优化。在一些情况下，如果ROP是实色，宁可进行填充而不执行BitBlt。对不支持所有ROP的设备，真正的所见即所得是不可能的。例如，PostScript驱动程序不支持ROP。

可选地，DrvBitBlt处理的位块传输能被屏蔽，包括颜色索引转换。转换矢量帮助调色板颜色索引转换。传输可能会被显示器驱动程序任意地裁剪，使用一系列的裁剪矩形。请求区域和信息由GDI提供。

实现DrvBitBlt描述了编写光栅显示器驱动程序的一个重要的部分，它没有标准格式的帧缓冲区。随WindowsÒ 2000 DDK供应的Microsoft VGA驱动程序提供了例子代码，完全支持平面设备的基本函数。为其他设备实现DrvBitBlt的复杂性更小。

驱动程序能可选地提供DrvStretchBlt函数，即使驱动程序支持设备管理的表面。这个函数提供在设备管理的表面和GDI管理的表面之间延伸块传输能力。DrvStretchBlt仅支持某些类型的延伸，如通过整数乘延伸。

DrvStretchBlt也允许驱动程序在GDI位图上写，特别当驱动程序能进行中间色调处理时。这个函数也允许同一个中间色调算法应用于GDI位图和设备表面。

DrvStretchBlt精确映像一个几何源矩形到一个几何目标矩形。源矩形角的坐标由给定的整数坐标替换为（-0.5，-0.5）。函数参数中指定的点位于相应的像素中心的整数坐标。由两个这样的点定义的矩形认为是几何的，两个顶点是坐标给定的点，但每个坐标都用0.5代替。（GDI POINTL结构使用一个速记符指定这些分数坐标顶点。）注意如何这样的矩形的边不会有像素交叉，但提供一系列像素。矩形内的像素是通常的底边和右边排除的矩形的像素。

源矩形的点是有序的。不能给定一个空的源矩形到DrvStretchBlt。与DrvBitBlt不同，DrvStretchBlt能用一个简单的裁剪矩形调用，防止在裁剪输出中的舍入错。

目标矩形由两个整数点定义。这些点不是有序的，这意味着第二个点的坐标不必须比第一个大。这些点描述的源矩形不包括底边和右边。因为矩形不是有序的，DrvStretchBlt必须不时执行两个x坐标和/或两个y坐标的倒置。（驱动程序必须尝试读出不在源表面上的像素。）DrvStretchBlt不能用一个空的目标矩形调用。

对于颜色转换，DrvStretchBlt提供一个指针，pxlo，指向XLATEOBJ结构，用于在源和目标表面之间转换。XLATEOBJ结构可用来查询，找到任何源索引的目标索引。对高品质的延伸块传送，DrvStretchBlt在某些情况下要求加入颜色。DrvStretchBlt也使用COLORADJUSTMENT结构定义颜色调整值，在位被延伸之前用于源位图。

DrvStretchBlt使用iMode参数定义源像素如何组成输出。特别地，iMode提供HALFTONE选项，允许驱动程序在输出表面使用像素组到最接近的颜色或灰度级。在下一次用HALFTONE的iMode调用DrvStretchBlt后，COLORADJUSTMENT结构的改变传递到驱动程序。另外，如果驱动程序为GDI位图请求GDI处理中间色调，驱动程序可以挂起DrvStretchBlt，iMode设置参数到HALFTONE，并在EngStretchBlt返回。

如果DrvStretchBlt挂起一个对EngStretchBlt函数的调用，并请求了它不支持的某些操作，它把请求返回到GDI，由相应的函数进行处理。

DrvCopyBits函数由GDI从其模拟操作调用，在设备管理的光栅表面和GDI标准格式位图之间转换。DrvCopyBits为SRCCOPY（0xCCCC）ROP位块传输提供了一个快速路径。这个函数为具有设备管理的位图或光栅表面的图形驱动程序请求，必须转换驱动程序表面到（或从）任何标准格式位图。DrvCopyBits不会用一个空的目标举行调用，目标举行的两个端点是有序的。这个调用和DrvBitBlt有相同的请求。

DrvCopyBits也用RLE位图（参见平台DDK文档）和DDB调用。位图作为应用程序调用几个Win32 GDI例程的结果提供给这个函数。可选的DDB仅由少数几个指定的驱动程序支持。

3.4.4 过渡调色技术

传统的模拟中间色调使用相等大小的单元组成的中间色调屏幕，固定单元的距离是从中心到中心。固定单元的距离调节墨水的浓度，当每个单元中点的大小变化时产生连续色调的印象。

在计算机中，大多数打印或屏幕阴影也使用固定单元像素大小。为模拟点大小的变化，一组像素的组合模拟中间色调屏幕。GDI包括过渡调色技术的默认参数，提供一个好的首次近似。附加的设备指定的信息能加入到系统中改进输出。

驱动程序发送GDI设备相关的规范，GDI需要通过DrvEnablePDEV函数返回的GDIINFO结构进行过渡调色处理。驱动程序用GDIINFO的ulHTPatternSize成员指定模型的大小，它定义过渡调色技术推荐的输出格式。对指定的设备，过渡调色技术与调色模型大小有关。GDI提供许多预定义的模型尺寸，从2´2到16´16。

对每个标准的模型尺寸，还有一个修改版。它通过在标准模型名上加上后缀“_M”来区分。例如，标准的6乘6的模型定义的名字是HT_PATSIZE_6´6，修改的6乘6的模型定义的名字是HT_PATSIZE_6´6_M。修改版给定了更多的颜色分辨率，但产生一个副作用，即水平或垂直噪声。另外，因为每个模型的尺寸是设备分辨率相关的，相应的模型尺寸依赖于指定的设备。

在模型尺寸（空间分辨率）和颜色分辨率之间的折中是由模型尺寸决定。一个大的过渡调色模型产生更好的颜色分辨率，而小的模型得到更好的空间分辨率。确定最好的模型尺寸是进行频繁地尝试和出错。更多的信息参考在线DDK中 Graphic Driver Reference的GDIINFO。

GDIINFO结构中影响过渡调色技术的另一个成员是flHTFlags，它包含描述过渡调色需要的设备分辨率。

GDI处理应用的颜色调整请求，并通过DDI把信息传递到驱动程序函数。如果应用选择过渡调色，而且表面是标准格式的DIB，在位图发送到设备之后，GDI使用其过渡调色能力处理位图。在PostScript驱动程序中，EngStretchBlt函数能使用DrvCopyBits或DrvBitBlt（在SRCCOPY模式中）函数发送位图到打印机。

例如，让GDI代替PostScript打印机执行过渡调色处理，提供了更快的输出，更好的所见即所得品质。如果打印机内嵌的过渡调色能力更好，PostScript驱动程序的接口允许用户调整过渡色调并提供一个检查框关闭GDI的过渡调色。

DrvDitherColor函数能返回DCR_HALFTONE值，请求GDI使用存在的设备（过渡调色）调色板近似一个颜色。仅当设备包含一个设备（过渡调色）调色板，DCR_HALFTONE能够用于显示器驱动程序，如VGA-16适配器卡，因为它有一个标准的固定调色板。单色驱动程序，包括大多数光栅打印机，能使用DrvDitherColor中iMode的参数获得一个好的灰度效果。

注意：Windows 2000在24位（或更高）的设备上不支持过渡调色技术。

3.4.5 图像颜色管理

在计算机监视器上显示的图像在彩色打印机上打印时经常出现不同。为解决这个问题，Windows 2000把图像颜色管理（ICM）并入到执行图像的颜色修正，这样在不同的输出设备上颜色的出现是一致的。

有关图像颜色管理和输出设备的特别类的更多的信息，参见：第二部分第2章，显示器的颜色管理；第三部分第12章，打印机的颜色管理；或者在线DDK上Color Management for Still Image Devices。

有关ICM的一般讨论，参见平台DDK文档。

3.5 支持DDI颜色和调色板函数

DDI颜色和调色板函数包括调色板管理和画刷实现函数。

3.5.1 管理调色板

就像第2章对图形驱动程序的GDI支持描述的，GDI处理了许多调色板管理工作。当GDI调用DrvEnablePDEV函数时，驱动程序在DEVINFO结构中必须提供其确省的调色板到GDI。这时，驱动程序必须调用GDI的服务函数EngCreatePalette创建确省的 调色板。

支持建立调色板的驱动程序也必须支持DrvSetPalette函数。这个函数是显示器驱动程序专有的。

3.5.2 实现画刷

输出直线、文本或者进行填充的图形函数至少接受一个画刷作为参数。画刷定义在指定的表面用来绘制图形对象的模式。每个接受画刷的输出函数请求一个画刷原点。画刷原点提供了像素在设备表面的坐标，与画刷模型左上角的像素排列在一起。画刷模型重复（平铺）覆盖整个设备表面。

驱动程序支持下列函数定义画刷：

DrvRealizeBrush

DrvDitherColor

画刷总是使用混合模式，定义模型如何与设备表面上已经存在的数据混合。MIX数据类型包括压缩到一个ULONG值中的两个ROP2值。前景ROP在低字节。下一个字节包含背景ROP。更多的信息参见平台DDK文档。

GDI保存所有的应用请求使用过的逻辑画刷。在请求驱动程序绘制之前，GDI首先发出请求调用驱动程序的DrvRealizeBrush函数。这允许驱动程序为其自身的绘图代码计算所请求模型的最优表示。

调用DrvRealizeBrush来实现由psoPattern（画刷的模型）和psoTarget（实现画刷的表面）定义的画刷。实现的画刷包含驱动程序为了用模型填充一个区域而需要的信息和加速器。这个信息只由驱动程序定义和使用。画刷的驱动程序实现写进一个缓冲区，驱动程序通过从DrvRealizeBrush中调用GDI服务函数BRUSHOBJ_pvAllocRBrush分配这个缓冲区。GDI缓存所有已实现的画刷；这样，它们很少需要重新计算。

在DrvRealizeBrush中，BRUSHOBJ使用对象表示画刷。画刷实现的表面可以是设备的物理表面、DDB或标准格式的位图。对于一个光栅设备，表面描述画刷模型表示一个位图；对于矢量设备，它总是由DrvEnablePDEV函数返回的模型表面之一。画刷使用的透明掩码是每像素一位的位图，与模型的长度相同。掩码位为0表示像素是画刷的背景像素。DrvRealizeBrush使用一个XLATEOBJ结构把画刷模型中的颜色转换成设备颜色索引。

当BRUSHOBJ结构的iSolidColor成员的值是0xFFFFFFFF并且pvRBrush成员是NULL时，驱动程序要调用GDI的BRUSHOBJ_pvGetRBrush函数。BRUSHOBJ_pvGetRBrush取出一个驱动程序的指定画刷实现的指针。当驱动程序调用这个函数时，如果画刷还没有实现，GDI自动调用DrvRealizeBrush进行这个画刷的驱动程序实现。

抖动

如果需要，当GDI试图使用硬件不能精确表示的实色创建画刷时，它会请求驱动程序的帮助。GDI调用驱动程序的DrvDitherColor函数，请求驱动程序相对于设备调色板的保留部分抖动画刷。

抖动使用几个颜色的模型近似选择的颜色，其结构是一个设备颜色索引数组。使用这些颜色为其模型创建的画刷通常对于给定的颜色是很好的近似。DrvDitherColor也能表现设备不能精确指定的颜色。为实现这一点，DrvDitherColor请求一个几种颜色的模型，并创建一个画刷近似给定的实色。

函数DrvDitherColor是可选的，并且只在DEVINFO结构的flGraphicsCaps成员中GCAPS_COLOR_DITHER或GCAPS_MONO_DITHER能力标志置1时才调用。DrvDitherColor可返回的值在下表中列出。

值 | 含义
DCR_DRIVER | 指示抖动值已经由驱动程序计算。在这个情况下，设备颜色索引数组的cxDither乘cyDither的句柄传递回去。
DCR_HALFTONE | 指示GDI使用存在的设备（过渡色调）调色板近似一个颜色。例如，GDI可以使用打印机的典型调色板，只有3种或4种颜色。DCR_HALFTONE能被显示器驱动程序使用，仅当设备包含一个设备（过渡色调）调色板，如VGA-16适配器卡，它有一个标准的固定调色板。
DCR_SOLID | 指示GDI要用存在的设备调色板把请求的颜色映像到最接近的颜色值（多对一）。

为使GDI获得好的灰度模型，单色显示器也支持DrvDitherColor。

3.6 支持DDI字体和文本函数

对许多设备，GDI能处理所有的字体函数。然而，某些驱动程序只能在设备表面上画其自己的字体，或者其设备的字体。其他驱动程序是字体驱动程序，能提供字形位图和/或轮廓和字形规格给GDI。对这些情况，驱动程序必须支持有些有用的字体函数。

文本输出是更一般的函数。如果表面是标准格式位图，GDI能处理所有的文本输出，除非驱动程序挂起调用以提高性能。对设备管理的表面，驱动程序必须支持文本输出。

3.6.1 管理和优化字体

生产者（producer）是能生成字体的驱动程序。在输出时它产生字形信息作为输出，包括字形规格、位图和轮廓。消费者（consumer）是使用字体的驱动程序。为产生文本输出，它接受字形信息作为输入，必须绘制其自身的字体或在设备管理表面上硬件的字体。驱动程序可以既是生产者又是消费者。例如，打印机驱动程序在调用DrvQueryFontData服务提供字形规格时是生产者，处理DrvTextOut调用时是消费者。

驱动程序只有在它是字体生产者或字体消费者时才请求处理字体。如果硬件有驻留的字体，驱动程序必须给GDI提供有关这个字体的信息，包括在IFIMETRICS结构中的字体规格，从Unicode到个体字形标识的映像，独立字形属性以及字距调整表。还有一些驱动程序必须支持的函数。一些函数被字体驱动程序以及使用驱动程序指定或设备指定的字体的驱动程序两者请求。其他的只被字体驱动程序请求。

字体函数的支持依赖于驱动程序的能力。通常的类型有：

§ 规格函数

§ 字形函数

§ TrueType函数

3.6.1.1字体规格函数

当驱动程序必须支持字体时，它必须通过IFIMETRICS结构给GDI提供字体信息。对每个字体有独立的FIMETRICS结构。大多数字段用术语FWORD表达，每个字段在设计空间中是带符号的16位数。如果字体是光栅字体，设计空间和设备空间是相同的，字体单元相当于像素间的距离。

基本上，IFIMETRICS结构是文本规格结构的DDI版本。所有的距离依赖于字体设计者的抽象坐标系统。抽象空间坐标系统是右手笛卡儿坐标系统，y坐标向上增长，x坐标向右增长。

IFIMETRICS结构设计为变长的。与字体联合的字符串长度没有限制。一般的习惯是紧跟着IFIMETRICS结构的最后一个字段立即存储字符串。

任何提供字体的驱动程序必须支持DrvQueryFont函数。驱动程序也包括函数DrvQueryFontData，取出有关实现字体的信息。在对DrvQueryFontData的调用中，GDI提供一个指针指向字形或字距调整句柄数组。驱动程序在GDI GLYPGDATA结构中返回相关字形的信息。如果DrvQueryFontData给定了字距调整句柄，它在Win32 POINTL表中返回有关字距调整对的信息。下表列出了字体规格函数。

函数 | 描述
DrvDestroyFont | 通知驱动程序不再需要字体实现，驱动程序可以释放任何分配的数据结构。GDI调用这个函数一次为字体生产者，一次为字体消费者。这个函数是可选的，只在驱动程序必须释放所分配的资源时才支持。
DrvFree | 通知驱动程序不再需要指定的数据结构。这个函数是可选的，只在驱动程序的存储器管理请求这个信息时才被实现。
DrvQueryFont | 为字体返回指向IFIMETRICS结构的指针。这个函数被所有处理字体的驱动程序请求。
DrvQueryFontData | 返回有关实现的字体的信息。这个函数被所有处理字体的驱动程序请求（为选择的iMode值）。
DrvQueryFontTree | 返回指向结构的指针，定义从Unicode到字形句柄的映像或字距调整对到字距调整句柄的映像。这个函数被所有处理字体的驱动程序请求。

DrvQueryFontTree函数允许GDI获得指向由下列之一定义的树结构的指针：

§ 从Unicode到字形句柄的映像，包括字形变量（GDI FD_GLYPHSET结构）

§ 字距调整对到字距调整句柄的映像（FD_KERNINGPAIRS结构）

DrvQueryFontTree请求尽力产生需要的结构，如果可能驱动程序要预先计算这些文件。结构可以存储在资源或文件中。如果结构存储在文件中，装载或读取它们的理想模式是调用EngMapFontFile函数，把文件映像到存储器。因为这个文件没有加进交换文件，如果需要存储器可置为可用的，这比打开和读取文件更有效。

特别地，驱动程序在pid参数中返回一个标识。当FD_GLYPHSET结构或FD_KERNINGPAIRS结构的数组不再需要时，GDI把它传递到DrvFree函数，带一个返回指针。依靠在驱动程序中存储器如何管理，pid能确定这个结构，确定结构如何分配，或什么都不做。

DrvFree和DrvDestroyFont都是可选函数。GDI调用DrvFree通知驱动程序指定的数据结构不再需要。驱动程序不需要实现它，除非它为结构分配了存储器并且当相应的数据结构被释放时需要通知。例如，如果数据与FONTOBJ结构关联，删除要被推迟直到调用DrvDestroyFont，因此不必要实现DrvFree。

DrvDestroyFont通知驱动程序字体实现不再需要，驱动程序可以释放它分配的任何数据结构。GDI调用这个函数一次为字体生产者，一次为字体消费者。只有当字体实例被摧毁时，驱动程序必须释放分配的资源，才需要实现它。

3.6.1.2 字体驱动程序函数

下表列出了字体驱动程序要支持的其他几个函数。

函数 | 描述
DrvLoadFontFile | 指定用于创建字体实现的文件；驱动程序必须准备这些要使用的文件。字体驱动程序请求这个函数。
DrvQueryAdvanceWidths | 请求驱动程序为指定的字形集发送GDI符号增长宽度。
DrvQueryFontCaps | 向指定的缓冲区拷贝定义字体驱动程序能力的位的数组。
DrvQueryFontFile | 依靠查询模式，返回字体文件或描述串中字体表面的数量。字体驱动程序请求这个函数。
DrvQueryGlyphAttrs | 请求关于字体字形的信息。
DrvUnloadFontFile | 通知驱动程序不再需要字体文件，驱动程序可以进行必要的清除操作。字体驱动程序请求这个函数。

GDI用一个特别的用来创建字体实现的文件调用DrvLoadFontFile函数。这个函数只由字体驱动程序请求。当调用DrvLoadFontFile函数时，驱动程序执行准备要使用的文件的转换。

DrvLoadFontFile返回一个唯一的标识，允许GDI使用一个GDI维护的字体使用表请求正确的字体。一旦装入了字体，GDI不再调用装入相同的字体。

当指定的字体文件不再需要时GDI调用DrvUnloadFontFile。DrvUnloadFontFile函数只由字体驱动程序请求。DrvUnloadFontFile引起删除所有的临时文件，分配的所有的系统资源被释放。

GDI调用DrvQueryFontFile函数返回驱动程序装入的字体文件的信息。DrvQueryFontFile只由字体驱动程序请求。要返回的信息的类型由iMode指定。如果iMode是QFF_DESCRIPTION，函数返回一个MicrosoftÒ Windows NTÒ/WindowsÒ 2000用来描述字体文件的字符串。如果iMode是QFF_NUMFACES，函数返回字体文件中表面的数量。表面由一个范围是1到表面数量的索引标识。

3.6.1.3 TrueType字体驱动程序函数

TrueType字体驱动程序必须支持下表列出的函数。

函数 | 描述
DrvGetTrueTypeFile | 给定GDI有效访问存储器映像的TrueType字体文件。
DrvQueryTrueTypeOutline | 用本地的TrueType格式返回字形句柄。
DrvQueryTrueTypeTable | 给定GDI用TrueType字体文件格式访问指定的文件。

所有这些函数给GDI提供有关TrueType字体文件的信息。DrvQueryTrueTypeTable给定GDI用TrueType字体文件格式访问指定的表。DrvQueryTrueTypeOutline必须用本地的TrueType格式给GDI发送字形轮廓。DrvGetTrueTypeFile给GDI返回TrueType驱动程序的私有入口点，允许GDI有效访问存储器映像的TrueType字体文件。

3.6.2 绘制文本

如果驱动程序在EngAssociateSurface函数中挂起调用，文本输出函数只为设备管理的表面（设备位图或表面）或GDI管理的表面调用。主要为文本的图形输出函数是DrvTextOut和DrvGetGlyphMode。

GDI调用DrvTextOut为文本输出在指定的位置绘制一系列字形的像素。DrvTextOut的许多功能用DEVINFO结构的GXAPS位定义，它由DrvEnablePDEV函数返回。

DrvTextOut的输入参数定义两个像素集，foreground和opaque。

驱动程序绘制表面提供下列结果：

1．首先使用不透明的画刷绘制不透明的像素。

2．然后用前景画刷绘制前景像素。

每个绘图操作在裁减区内进行。裁减区外的像素不受影响。

驱动程序必须绘制表面，要首先使用不透明画刷在表面上计算和绘制不透明像素。然后用前景画刷计算和绘制前景像素。每个操作都由裁减进行限制。

前景和不透明像素通过画在表面上的颜色构造一个掩码。字体的字形本身没有颜色。前景像素集定义为字形的像素和某些额外的矩形的像素，用来模拟删除线和下划线。不透明像素由不透明矩形定义。

Drv TextOut使用一个指针pfo选择指定的字体，查询当前的FONTOBJ结构。这个过程包括下载一个软字体或字体替换，或者任何其他的对设备需要的字体优化。

如果驱动程序有可升级的字体，它要为当前的FONTOBJ结构调用FONTOBJ_pxoGetXform函数，为相关的字体返回抽象到设备的转换。它为驱动程序支持的字体请求的。抽象空间是设备字体的设计空间。例如，PostScript字体用1000乘1000单位字符单元定义。在IFIMETRICS结构里返回的大多数规格转换到抽象空间，这说明为什么抽象到设备的转换是必要的。

图形引擎通过调用DrvGetGlyphMode函数查询驱动程序，寻找在内部如何缓存字体信息。独立的字形可以缓存为位图、轮廓或其他（设备字体合适的选择）。


显示器介绍

运行在Windows NTâ/Windowsâ 2000上的图像适配器要求一对显示器驱动程序和视频微端口驱动程序。这一章介绍这些驱动程序并提供有关这两方面的信息。

Microsoftâ Windowsâ 2000 DDK提供了支持标准图形卡的显示器和视频微端口驱动程序的例子。

1.1 显示器体系结构

图1.1显示了Windows 2000上显示器要求的部件。

图1.1 Windows 2000显示器子系统

图1.1中带阴影的元素是Windows 2000提供的服务。未带阴影的元素指示请求了第三方显示器驱动程序和视频微端口驱动程序，使图形适配器在Windows 2000系统中显示。

对每种可以和Windows NTâ/Windowsâ 2000一起使用的图形卡，必须具有显示器驱动程序和相应的视频微端口驱动程序。微端口驱动程序是特别为一种图形适配器（或适配器家族）编写的。显示器驱动程序可以为共享一个公共的绘图接口的任意多个适配器编写；例如，VGA显示器驱动程序可以与VGA或ET4000微端口驱动程序一起使用。这是因为显示器驱动程序进行绘制，而微端口驱动程序执行诸如模式设置的操作并给驱动程序提供有关硬件的信息。也可能有多个显示器驱动程序和一个特别的微端口驱动程序一起工作；例如，16和256色的SVGA显示器驱动程序可以使用同一个微端口驱动程序。

下面几节描述显示器和视频微端口驱动程序的主要职责。职责的分解是不严格的；主要在模块性和性能之间平衡。例如，VGA驱动程序的硬件指针代码驻留在微端口驱动程序中。这促进了模块化，同一个显示器驱动程序能够处理具有硬件指针的Vedio Seven VRAM以及没有硬件指针的ET4000。

1.1.1 显示器驱动程序职责

显示器驱动程序是一个主要职责为绘制的DLL。当一个应用通过设备无关图形请求调用Win32函数时，图形设备接口（GDI）解释这些指令并调用显示器驱动程序。显示器驱动程序再把这些请求转换成命令，视频硬件在屏幕上绘制图形。

显示器驱动程序能够直接访问硬件，因为图形硬件的能力很广泛，而且在任何系统中显示是对时间最苛求的部分。GDI的这种可访问性和广泛的能力在实现一个显示器驱动程序时提供了相当的灵活性。

缺省情况下，GDI在标准格式位图上处理绘图操作，如在包含帧缓冲区的硬件上。显示器驱动程序能hook和实现任何硬件提供特别支持的绘图函数。对时间要求较低的操作和更复杂的操作，图形适配器不支持，驱动程序能punt函数到GDI并允许GDI工作。细节参见第一部分第2章，Hooking和Punting。

对时间要求特别严格的操作，显示器驱动程序能直接访问视频硬件寄存器。例如，x86系统的VGA显示器驱动程序对一些绘图和文本操作使用优化的汇编代码实现对硬件寄存器的直接访问。

显示器驱动程序的细节在第2章显示器驱动程序中讨论。

1.1.2 视频微端口驱动程序职责

视频微端口驱动程序通常处理必须和其他NT核心部件交互的操作。例如，硬件初始化这样的操作和由Windows NT I/O子系统操作的存储器映像请求。视频微端口驱动程序职责包括资源管理，如硬件的配置，以及物理设备存储器映像。视频微端口驱动程序必须指定视频硬件。

显示器驱动程序使用视频微端口驱动程序进行操作不需要频繁请求，例如，管理资源，执行物理设备存储器映像，确保寄存器的输出出现在最近的地方，或者响应中断。

视频微端口驱动程序还能处理：

§ 与图形卡的模式设置交互作用。

§ 多种硬件类型，显示器驱动程序中最小地依赖硬件类型。

§ 把视频寄存器映像到显示器驱动程序的地址空间。I/O端口直接寻址。

视频微端口驱动程序的细节在视频微端口驱动程序中讨论。

1.2 图形适配器的类型

图形适配器一般有4种类型：

§ 帧缓冲区

§ 加速器

§ 协处理器

§ VGA

1.2.1 帧缓冲区

帧缓冲区是图形适配器上的专用存储器。简单的图形适配器有帧缓冲区而没有加速器。如果硬件是线性寻址的并且支持标准格式位图（DIB格式），GDI能处理绘图操作并直接写入帧缓冲区。这能减小显示器驱动程序的大小，因为驱动程序能让GDI进行全部或大多数的绘图。大多数帧缓冲区是线性寻址的，或者说是“平面的”。

在例子中显示器驱动程序是framebuf目录中的帧缓冲区驱动程序。因为这个驱动程序支持线性寻址帧缓冲区硬件，它允许GDI进行大多数工作，并且很小也相对简单。

1.2.2 加速器

加速器包括显示卡上带的电路，对于指定的绘图操作，如直线的绘制和填充，或位块传输，能提高图形操作的速度。硬件必须提供对一个指针的支持，驱动程序使用它通常比由GDI提供支持的指针速度要块得多。

与Windows 2000 DDK一起发布 的3Dlabs Permedia2例子驱动程序是一个复杂和相当难以处理的加速器。显示器和微端口驱动程序能在displays\3dlabs和miniport\3dlabs源程序例子文件夹中找到。3Dlabs Permedia2驱动程序完全用C语言编码，因此在整个系统中可移植。

1.2.3 协处理器

包括协处理器的图形适配器提供了加速图形显示函数的另一种方法。协处理器本质上是带有附加的可编程命令逻辑的加速器。因为协处理器是可编程的，它们支持多进程，允许协处理器处理一些费时的操作，而其他操作在别处进行。

1.2.4 VGA

基于x86的计算机的标准图形卡是VGA。这个硬件可以在文本和图形模式下运行。在VGA兼容的硬件上建立了一系列加速器。

1.3 一般的设计和实现策略

为了设计有效的Windows 2000显示器驱动程序和视频微端口驱动程序，考虑以下的策略：

l 修改一个已存在的为类似的图形适配器类型设计的DDK例子，减少驱动程序的设计时间。

l 为保证最大的可移植性，尽可能多地使用C语言编写驱动程序，仅当编写硬件没有很好支持的对时间要求严格的的程序才使用汇编语言。虽然汇编语言代码有优化的潜力，时间和可移植性在价值上超过了其代价。

l 操作中使用视频微端口驱动程序管理资源、执行物理设备存储器映像、确保寄存器以最近似的值输出，或响应中断。微端口驱动程序主要用于处理硬件家族中的变化，以及使显示器驱动程序对硬件类型的依赖最小。

还必须考虑以下事实：

l 显示器驱动程序和视频微端口驱动程序在相同的特权核心模式地址空间中操作，就像是Windows NT Executive的一部分。任何一个驱动程序中的错误都将引起系统剩余部分的故障。

l 显示器驱动程序和视频微端口驱动程序在任何时刻都是抢先的。

l 显示器驱动程序的代码和数据段都是完全分页的。

l 输出函数在入口处必须执行标准的Windows NT/ Windows 2000 prolog，退出时执行epilog。更多的信息参见平台SDK文档。

1.4 访问图形适配器

为确保显示器的性能，显示器驱动程序能通过下列方法访问图形卡：

通过发送IOCTL到图形适配器的视频微端口驱动程序直接访问。参见通过IOCTL和视频微端口驱动程序通信。

通过读写视频存储器（帧缓冲区）或硬件寄存器直接访问。参见帧缓冲区和硬件寄存器。

1.4.1 通过IOCTL和视频微端口驱动程序通信

图1.2显示了显示器驱动程序如何使用IOCTL和视频微端口驱动程序通信。

图1.2 显示器驱动程序/视频微端口驱动程序的通信

显示器驱动程序使用一个IOCTL调用EngDeviceIoControl向视频微端口驱动程序发送一个同步请求。对于输入和输出GDI使用单个的缓冲区向I/O子系统传递请求。I/O子系统把这个请求发送到视频端口，用视频微端口驱动程序处理请求。

一些IOCTL请求要求微端口驱动程序访问视频寄存器，其他的则从微端口驱动程序的数据结构中存储或取出信息。通常，没有请求要求视频微端口驱动程序执行实际的绘图操作。

一般地，除非模块性要求，否则显示器驱动程序处理绘图和其他的时间要求严格的操作。发送一个IOCTL到微端口驱动程序来执行时间要求严格的函数会使系统的性能下降。

系统定义的视频IOCTL的描述参见在线DDK中Video I/O Control Codes。通过加入一个私有的IOCTL，格式必须是在线DDK中Kernel Mode Driver Reference中描述的，可以在显示器驱动程序和视频微端口驱动程序之间扩展接口。如果需要写一个新的IOCTL，必须首先和Microsoft 技术支持联系。

1.4.2 访问帧缓冲区和硬件寄存器

减小显示器驱动程序的大小可以有几种方法。例如，可以仅实现显示器驱动程序执行比GDI快的函数，而指定GDI执行所有的其他函数。GDI经常执行大量实际的绘图到线型的帧缓冲区以减小驱动程序的大小。GDI不能直接访问分组的存储器；因此，当帧缓冲区不是线性寻址时，显示器驱动程序必须将帧缓冲区分割成一系列的组，并为GDI提供一种手段执行相应的组上的绘图操作。详细情况参见第2章支持分组的帧缓冲区。

显示器驱动程序直接访问I/O和存储器映像的视频寄存器。这个访问允许显示器驱动程序实现高性能。例如，驱动程序可能需要访问视频硬件寄存器，以高吞吐率发送绘制直线的命令。

类似地，对于图形卡，如S3，在图形引擎代码中许多最内层的循环要求读写几个视频控制器端口（例如，图形模式下的文本输出，位块传输和直线绘制）。与其请求显示器驱动程序为每个请求发送一个IOCTL到微端口驱动程序，不如显示器驱动程序允许直接访问视频硬件。

1.5 创建图形INF文件

Windows NT/ Windows 2000显示器和视频微端口驱动程序必须使用INF文件安装。Windows 2000 DDK提供了一个称作geninf.exe的工具，为显示器和视频微端口驱动程序产生一个INF文件。当geninf.exe运行时，它显示一系列对话框给出提示信息，如公司名，显示器驱动程序和视频微端口驱动程序的名字等。geninf.exe从这些信息中产生一个INF文件。

注意 由geninf.exe产生的文件可能不是一个完全有效的INF文件。geninf.exe产生一个INF文件，很可能需要客户为INF文件中描述的每个设备加入注册设置。

如果微端口驱动程序对VideoPortGetAccessRanges或VideoPortVeerifyAccessRanges的调用总是失败，可以手工编辑INF文件，包含在INF GeneralConfigData段中描述的段和适当的条目。

在运行geninf.exe时，当提示选择设备类时，选择显示器。在驱动程序初始化时，标记为Class=Display的INF文件被系统提供的显示器类安装程序解释。这确保所有与视频驱动程序相关的注册入口都能正确初始化。

在系统中Display类的INF文件只能初始化下列文件：

§ 单个微端口驱动程序

§ 一个或多个显示器驱动程序

§ 控制面板扩展DLL

Display类的INF文件不能安装其他类型的驱动程序或应用文件。

geninf.exe 的限制

不能使用geninf.exe产生：

支持超过一个体系结构的INF文件；例如，可以创建x86体系结构或ia64体系结构的INF文件，但不能同时创建两个。

支持Windows 9x.或Windows NT 4.0的INF文件。

镜像驱动程序INF文件。作为模板使用mirror例子驱动程序提供的INF文件。细节参见第2章中镜像驱动程序INF文件。

监视器INF文件。作为模板使用名为monsamp.inf的INF文件。细节参见监视器INF文件。

这些例子INF文件与Windows 2000 DDK一起发布 。

当更新例子INF时，详细的参见第四部分、第3章即插即用、电源管理和设置设计指南中的创建一个INF文件，以及卷1 的第三部分Windows 2000 驱动程序开发指南的第1章，INF文件段和指令。

1.5.1 INF GeneralConfigData段

如果微端口驱动程序对VideoPortGetAccessRanges或VideoPortVeerifyAccessRanges的调用总是失败，可以编辑geninf.exe产生的INF文件，修改GeneralConfigData段中相应的条目。

[GeneralConfigData]

[MaximunDeviceMemoryConfiguration = n]

[MaximunNumberOfDevices = n]

[KeepExistingDriverEnabled = n]

下面是GeneralConfigData的条目和值：

MaximunDeviceMemoryConfiguration =n

指定了微端口驱动程序的设备尝试映像的物理地址空间的最大兆字节数。为使这个接口有效，需要重新启动。可以通过在DeviceManager中检查设备的状态来决定是否需要重新启动。

MaximunNumberOfDevices =n

指定了微端口驱动程序一次能够支持的适配器的最大数量。只在指定了MaximunDeviceMemoryConfiguration关键字并且在多监视器系统中微端口驱动程序可以是第2个适配器时这个条目才指定。为使这个接口有效，需要重新启动。可以通过在DeviceManager中检查设备的状态来决定是否需要重新启动。

KeepExistingDriverEnabled =n

指定了是否当前的驱动程序保持有效。为保持存在的驱动程序，设置这个注册键为1（TRUE）。为卸载当前的驱动程序，设置这个键为0（FALSE）。当INF文件编写者想使当前的驱动程序保持有效，这个键主要在镜像驱动程序INF文件中使用。

1.5.2 显示器INF文件段

下一段仅用于图形适配器安装。关于INF文件更一般的信息，参见卷1 的第三部分Windows 2000 驱动程序开发指南第1章，INF文件段和指令。

DDInstall.SoftwareSettings段

[DDInstall.SoftwareSettings]段包含AddReg和/或DelReg指令。每个指令指向一个分离的INF编写者定义的段，包含要加入或删除的注册条目。

在下例中，Addreg 指令指向命名为ACME-1234_SoftwareDeviceSettings的INF编写者定义的加入注册段。DelReg指令指向命名为ACME-1234_DeleteSWSettings的另一个INF编写者定义的删除注册段。

在加入注册段里，加入了InstalledDisplayDrivers和CapabilityOverride两个键，并给出了显示的值。删除注册段显示了从注册里删除的两个键：GraphicsClocking和MemClocking。

 [ACME-1234.SoftwareSettings]

AddReg=ACME-1234-SoftwareDeviceSettings

DelReg= ACME-1234-DeleteSWSettings

[ACME-1234-SoftwareDeviceSettings]

HKR.. InstalledDisplayDrivers, %REG_MULTI_SZ%, Acmel

HKR.. CapabilityOverride, %REG_DWORD%, 0

[ACME-1234-DeleteSWSettings]

HKR.. "GraphicsClocking"

HKR.. "MemClocking"

1.5.3 监视器INF文件段

在Windows NT/Windows 2000中监视器必须使用INF文件安装。Windows 2000 DDK提供监视器INF文件示例，monsamp.inf，为监视器用模板产生INF文件。用“创建图形INF文件”里描述的geninf.exe 工具不能产生监视器INF。

这个主题的其余部分在monsamp.inf的一些段中注释掉了监视器INF编写者感兴趣的细节。关于INF文件更一般的信息，参见卷1 的第三部分Windows 2000 驱动程序开发指南第1章，INF文件段和指令。

SourceDisksFiles段

监视器安装过程中必须在[SourceDisksFiles]段里放拷贝的文件。下例可识别分配磁盘1的.icm文件。

[SourceDisksFiles]

profile1.icm=1

更一般的信息参见卷1 的第三部分Windows 2000 驱动程序开发指南第1章，INF SourceDisksFiles段。关于颜色管理和profile的更多信息参见在线DDK里的Monitor Profiles。

Model段

对于给定制造商支持的每一个模型的信息放在模型段。下例可识别由ACME制造的两个模型：

[ACME]

%ACME-1234%=ACME-1234.Install , Monitor\MON12AB

%ACME-5678%=ACME-5678.Install , Monitor\MON34CD

每个模型用一行来描述。每行包括三个元素：

§ 模型名。例如，%ACME-1234%是表示实际模型名的标记（在Strings段中出现）。

§ 链接到随后的DDInstall段。例如，ACME-1234.Install是到随后的[ACME-1234.Install]段的一个链接。

§ 硬件识别。例如，当表达式Monitor\MON12AB在设备的EDID中出现时，就组合了设备类（监视器）和设备标识（MON12AB）。

更一般的信息参见卷1 的第三部分Windows 2000 驱动程序开发指南第1章，INF Model段。

DDInstall段

当DDInstall段安装指定的设备时，给驱动程序提供关于所执行操作的信息。在这段中，每行给随后在INF文件里出现的不同INF编写者定义的段提供一个或多个链接。下例为ACME-1234模型显示DDInstall段：

[ACME-1234.Install]

DelReg=DEL_CURRENT_REG

AddReg=ACME-1234.AddReg, 1280, DPMS

CopyFiles=ACME-1234.CopyFiles

§ DelReg指令¾提供到DEL_CURRENT_REG段的一个链接，详述被删除的注册键。

§ AddReg指令¾提供到三个段的链接，三个段是ACME-1234.AddReg、1280和DPMS，他们详述述加入的注册键。

§ CopyFiles指令¾提供到ACME-1234.CopyFiles段的链接，指定从分配磁盘中拷贝的文件。

更一般的信息参见卷1 的第三部分Windows 2000 驱动程序开发指南第1章，INF DDInstall段。

INF编写者定义段

INF编写者定义段可以有任意的名字，只要它在INF文件里是唯一的。在其他段里的指令指向这些段。以下是来自monsamp.inf的INF编写者定义段的一部分:

§ DEL_CURRENT_REG 段¾标识将被删除值的四个注册键: MODES、MaxResolution、DPMS和ICMProfile。在随后的段中用新值相应地更新这些键。

[DEL_CURRENT_REG]

HKR, MODES

HKR, , MaxResolution

HKR, , DPMS

HKR, , ICMProfile

§ 1280 段¾更新MaxResolution注册键值到显示的字符串值。

[1280]

HKR, , MaxResolution, , “1280, 1024”

§ DPMS 段¾更新DPMS 注册键值为1(TRUE)。对于不支持电源管理的监视器,下行代替代替DPMS 键值为0(FALSE) 。

[DPMS]

HKR, , DPMS, , 1

§ AddReg 段¾下列的第一行设定“MODES\1280,1024”子键为显示的字符串值。这一行也为这个子键识别Mode1这个值名。注意,这个值可建立新的Mode1时钟值而不考虑在EDID里列出的那些值。在第二行中, PreferredMode注册键被设定为在附随的字符串中显示的值。此字符串中的值是用来设定像素中水平和垂直的两个图形分辨率,以及屏幕的刷新速率，用赫兹（Hz）表示，这是较喜欢的屏幕方式。第三行设定ICMProfile键为“profile1.icm”字符串值。

[ACME-1234.AddReg]

HKR, “MODES\1280, 1024”, Mode1, , “27.0-106.0, 55.0-160.0, +,+ ”

HKR, , preferredMode, , “1024, 768, 70”

HKR, , ICMProfile, 0, “profile1.icm”

ICMProfile键也能够用内部的外形号码设置。如下例所示：

HKR, , ICMProfile，1，9

此例中，数字1指出这个键是原始数据(FLG_ADDREG_BINVALUETYPE类型)，而9是mnP22G21.icm的外形号码。Windows 2000发布 下表所列的外形。有关更多的信息参见第二章监视器外形。

外形编号 | 外形名
1 | mnB22G15.icm
2 | mnB22G18.icm
3 | mnB22G21.icm
4 | mnEBUG15.icm
5 | mnEBUG18.icm
6 | mnEBUG21.icm
7 | mnP22G15.icm
8 | mnP22G18.icm
9 | mnP22G21.icm
10 | Diamond Compatible 9300K G2.2.icm
11 | Hitachi Compatible 9300K G2.2.icm
12 | NEC Compatible 9300K G2.2.icm
13 | Trinitron Compatible 9300K G2.2.icm

§ CopyFiles 段¾指出从分配磁盘，或磁盘到目标目录要拷贝的适当ICM文件。

[ACME-1234.CopyFiles]

PROFILE1.ICM

1.6 显示器和视频微端口驱动程序的兼容性测试要求

这段为满足Windows Hardware Quality Lab (WHQL) 测试标准的 Windows NT®/ Windows® 2000 图形驱动程序和关联部分列出一些要求。满足或超过这些测试标准的驱动程序授权显示Microsoft® Windows® 徽标。有关更多的信息，到WWW.microsoft.com/hwdev站点查找。

1.6.1 INF和安装要求

Windows NT/ Windows 2000显示器和视频微端口驱动程序必须用INF文件安装。为确保和视频驱动程序相关联的所有注册条目完全地初始化，INF必须由系统支持的显示器类安装程序来解释并标记为Class=Display。

Windows 2000 DDK提供命名为geninf.exe的工具，它容易地为显示器和视频微端口驱动程序产生INF文件。细节参见创建图形INF文件。

1.6.2 视频微端口驱动程序请求

以下是视频微端口驱动程序的一些请求。

§ Windows NT/ Windows 2000视频微端口驱动程序必须是一个Single.sys文件。

微端口驱动程序由单一的二进制文件组成。微端口驱动程序的主要用途是检测，初始化，及安装一个或多个同类型的图形适配器。

§ 微端口驱动程序仅能够做由videoprt.sys导出的调用。

微端口驱动程序仅能够调用那些由系统支持的视频端口驱动程序导出的函数。用下列语句可测定微端口驱动程序正在调用那个函数：

link -dump -imports my_driver.sys

微端口驱动程序不能在机器中用无正式文件的OS函数调用来装入或安装另外的

驱动程序。

§ 微端口驱动程序仅在接收终端用户请求时才能够启动全方位显示。

必须禁止由默认值产生的全方位显示。仅当通过控制面板请求全方位显示时，微端口驱动程序才启动它。OEMs通过把默认值当作preinstall的一部分才能够启动全方位显示

1.6.3 显示器驱动程序的要求

Windows NT®/ Windows® 2000显示器驱动程序必须满足在PC 99 设计指南中指定的要求。如果驱动程序为了WHQL测试即将被提交，它也必须满足WHQL要求。

1.6.4 控制面板的要求

以下是对显示器控制面板扩展的要求：

§ 具有属性的页面必须保持不变。不能屏蔽，修改，删除，或替换Microsoft提供的任意属性页面（包括设置页面）。

§ 借助于高级属性才能增加自定义属性页面。显示在最前面的特性是普通的包含在每一个Windows系统里的存取特性。因为Windows 2000 支持多重显示器，自定义属性页面不能追加到在显示器控制面板中最前面的属性页面的设置中。

§ 控制面板的扩展必须能够操作现有的Windows控制面板的元素。

§ 必须用除文本名以外的图标作为自定义属性页面的标志。为了避免同将来的操作系统或壳体释放相冲突，必须包括第三方的标志，除页面标志以外，图标（用公司的徽标）或着文本均可作为供应商的名字。例如，“Acme Video Controls”是容许的；“Video Controls”是不容许的。

§ 如果必须的硬件/驱动程序结合不是当前的，控制面板的扩展就不必初始化。如果用自定义控制面板扩展封装，显示器硬件不是当前的，扩展就不必装载。同样地，如果自定义属性页面具有取决于所有权驱动程序扩展的特性（例如，在每个其他的显示器驱动程序中当前不允许扩展），当没有安装必须的驱动程序时，这些特性自己必须禁止使用或者所有权页面必须没有装载。

§ 控制面板扩展必须考虑后台模式，监视器不能显示在监视器选项卡里的检查框。如果选择了检查框，控制面板扩展不必显示任何不通过EnumDisplaySettings计算的模式（在平台SDK文档中描述）。

§ 控制面板状态必须存储在注册表中。不允许.ini文件。由控制面板扩展维护的任何状态必须存储在注册表的SOFTWARE键中，它在INF中通过HKR存取。


显示器驱动程序

Microsoft® Windows NT®/Windows® 2000显示器驱动程序编写者关心两个核心的软件接口：

§ DDI接口—显示器驱动程序实现的函数设置。GDI能够调用DDI接口处理图形命令。

§ GDI接口—系统支持的帮助例程，显示器驱动程序调用它来简化驱动程序的实现。

这一章描述与Windows NT/Windows 2000显示器驱动程序以及一些实现信息相关的关键的概念。参见第一部分，第二章，图形驱动程序的GD I支持，以及第一部分，第三章，支持DDI，得到打印机驱动程序和显示器驱动程序两者公用的图形驱动程序设计细节，如驱动程序初始化、终止和图形输出。

也能够实现下列的DDI：

§ DirectDraw DDI—允许供应商为DirectDraw提供硬件加速的图形接口。细节参见第三章，DirectDraw DDI。

§ Direct3D DDI—允许供应商为Direct3D 提供硬件加速的3-D图形接口。细节参见Direct3D DDI。

§ 小客户端驱动程序(MCD)—为OpenGL允许低速的内务操作硬件加速的图形接口。细节参见在线DDK Graphics Driver Design Guide中Mini Client Driver。

DDI入口点和结构，以及GDI服务函数和对象的完整描述，参见在线DDK Graphics Driver Reference。

2.1 显示器驱动程序DDI 函数

Microsoft® Windows NT®/Windows® 2000 显示器驱动程序必须实现几个DDI函数。虽然利用现有GDI能力的驱动程序编写的驱动程序是比较短和简单的，必须确保驱动程序也能够比GDI更有效地实现操作。

每个显示器驱动程序最少必须满足：

§ 启用和禁用图形硬件

§ 提供具有关于硬件能力信息的GDI

§ 启用绘图表面

§ 如果是设备管理表面，执行图形操作(如最小的位图传输和绘制文本)

对于任何指定的表面，驱动程序调用是连续的。

2.1.1 请求的显示器驱动程序函数

下表列出所有的显示器驱动程序必须实现的函数。在DrvEnableDriver之后，其余的函数是按字母顺序列出的。

函数 | 描述
DrvEnableDriver | 作为初始化驱动程序入口点，给GDI提供驱动程序版本号和可选函数支持的入口点
DrvAssertMode | 为指定的视频硬件设备重新设置视频模式
DrvCompletePDEV | 通知驱动程序设备安装完成的信息
DrvDisableDriver | 释放驱动程序所有已分配的资源，并使设备返回到最初装入的状态
DrvDisablePDEV | 当不再需要硬件时，释放设备使用的和表面创建但还没有删除的存储器和资源
DrvDisableSurface | 通知驱动程序不再需要为当前设备创建的表面
DrvEnablePDEV | 使PDEV有效
DrvEnableSurface | 为指定的硬件设备创建表面
DrvGetModes | 列出指定的视频硬件设备支持的模式

2.1.2有条件请求的显示器驱动程序函数

依赖于如何实现驱动程序以及基本适配器的特性，可以请求其他的DDI函数。例如，如果驱动程序自己管理表面(使用EngCreateDeviceSurface得到表面的句柄)，它也必须最少支持以下的绘图函数：

§ DrvBitBlt

§ DrvStrokePath

§ DrvTextOut

编写标准格式DIBs的驱动程序通常允许GDI管理许多或所有这些操作。支持可设置调色板的显示器必须支持DrvSetPalette函数。对所有图形驱动程序的有条件请求函数在第三章，第一部分有条件请求图形驱动程序函数表中列出。

2.1.3任意显示器驱动程序函数

为了减小驱动程序大小，显示器驱动程序编写者通常只增加在视频硬件中较好支持的那些可选函数。显示器驱动程序能够实现下列可选的函数：

函数 | 描述
DrvAlphaBlend | 提供具有a混合的位块传送能力
DrvCreateDeviceBitmap | 创建和管理驱动程序定义格式的位图
DrvDeleteDeviceBitmap | 删除设备管理位图
DrvDestroyFont | 通知驱动程序不再需要字体实现；驱动程序能够释放分配的数据结构
DrvDitherColor | 请求设备创建一个相对于设备调色板抖动的画刷
DrvDrawEscape | 实现绘画类型转义函数
DrvEscape | 在设备无关DDI里从无效设备中查询信息
DrvFillPath | 为设备管理表面绘制闭合的路径
DrvFree | 释放与指定的数据结构相关的存储器
DrvGradientFill | 为指定的图元增加阴影
DrvMovePointer | 移动指针到新位置并刷新它
DrvNotify | 允许GDI通知显示器驱动程序某些信息
DrvRealizeBrush | 为已定义的表面实现指定的画刷
DrvSaveScreenBits | 保存或恢复屏幕中指定的矩形
DrvSetPointerShape | 如果驱动程序已画过指针，可从屏幕上删除它，然后可设定新的指针形状
DrvStretchBlt | 在设备管理和GDI管理的表面中允许扩展的块传送
DrvStrokeAndFillPath | 同时填充和绘制路径
DrvSynchronize | 在GDI和显示器驱动程序支持的协处理器设备之间协调绘图操作；仅对引擎管理的表面
DrvSynchronizeSurface | 允许由设备的协处理器和GDI协调完成绘图操作
DrvtransparentBlt | 提供透明的位块传送能力

显示器驱动程序也能够可选地实现Microsoft® DirectDraw®，Direct3D®和/或

MCD接口。细节参见以下章节：

§ DirectDraw DDI

§ Direct3D DDI

§ Mini 客户端驱动程序

2.2 显示器驱动程序初始化

显示器驱动程序初始化和图形驱动程序初始化相似，如第一部分第三章，支持初始化和结束中所描述的。这一节提供了显示器驱动程序的初始化细节。

在Windows NT Executive和Win32子系统装入和初始化以后，进行视频微端口和显示器驱动程序的初始化。系统装入在注册表中启用的视频微端口驱动程序，然后确定使用那对视频微端口驱动程序和显示器驱动程序。在处理过程中，根据Window Manager提供的信息，GDI打开所有必需的的显示器驱动程序。

在基本的显示器驱动程序初始化过程中创建桌面，如图2.1所示。

1. 当调用GDI时为视频硬件创建第一个设备环境（DC），GDI调用显示器驱动程序函数DrvEnableDriver。返回时，DrvEnableDriver给GDI提供 DRVENABLEDATA结构，保存驱动程序的DDI版本号，以及所有由驱动程序实现的可调用的DDI函数入口点（DrvEnableDriver除外）。

2. 然后GDI调用驱动程序的DrvEnablePDEV函数，请求驱动程序的物理设备特性的描述。在调用过程中，GDI在DEVMODEW结构中传送，并识别GDI想要设定的模式。如果GDI请求的是显示器或基本的微端口驱动程序不支持的模式，显示器驱动程序必须使这次调用失效。

图2.1 显示器驱动程序初始化

3. 显示器驱动程序代表由GDI控制的逻辑设备。如图 2.2所示，单个逻辑设备能够管理几个物理设备，每一个特征由硬件类型、逻辑地址和支持的表面来表现。显示器驱动程序分配存储器支持它创建的设备。虽然指定的物理设备一次仅能使一个PDEV有效，但是相同的物理设备可以访问显示器驱动程序来管理多个PDEV。在独立的GDI调用DrvEnablePDEV中创建一个PDEV，并且每次调用创建另外一个使用不同表面的PDEV。

因为驱动程序必须支持多个PDEV，所以它不能使用公共变量。

图2.2 对物理设备的逻辑

4. 当物理设备的安装完成时，GDI调用DrvCompletePDEV。当为设备请求GDI函数时，函数给驱动程序提供一个GDI产生的物理设备句柄。

5. 在初始化的最后阶段，通过GDI调用DrvEnableSurface为视频硬件创建一个表面，它使图形输出到硬件。依靠设备和环境，显示器驱动程序用以下两种方式之一使一个表面有效：

§ 驱动程序通过调用GDI函数EngCreateDeviceSurface获取表面的句柄来管理其自身的表面。设备管理的表面方法为不支持标准格式位图的硬件请求，对支持标准格式位图的硬件则是可选的。

§ 如果硬件设备具有作为标准格式位图管理的表面，那么GDI能够象引擎管理表面那样完全地管理表面。驱动程序能够调用EngModifySurface把设备管理的原始位图转换成引擎管理的原始位图。驱动程序还能够hook任何绘图操作。

任何现有的GDI位图句柄是一个有效的表面句柄。驱动程序能够调用EngModifySurface把设备管理的原始位图转换成引擎管理位图。如果是引擎管理表面，GDI能够处理任一或所有绘图操作。如果是设备管理表面，驱动程序至少必须处理DrvTextOut、DrvStrokePath和DrvBitBlt。

GDI在调用DrvEnableSurface之后自动地使DirectDraw有效。在初始化DirectDraw以后，驱动程序能够用DirectDraw堆管理器执行屏幕外存储器的管理。细节参见DirectDraw 和GDI。

为了接收通知事件,，尤其是DN_DRAWING_BEGIN 事件，显示器驱动程序必须执行DrvNotify。在GDI开始绘图之前，GDI立即发送这个事件，因此它能够用来测定什么时候初始化了高速缓冲存储器。

启动过程的细节参见即插即用, 电源管理, 和 Setup设计指南。

2.3 桌面管理

显示器驱动程序必须实现DrvAssertMode和DrvGetModes来管理桌面。

如果显示器驱动程序是调色板管理的，它也将接收对DrvSetPalette的调用，把其调色板重新设置为正确的状态。

为了处理动态模式的变化，GDI的机制在 Windows 2000中已经有了较大的改变。在初始化期间分配给驱动程序的GDI HDEV不同于在完成模式改变之后分配的HDEV。因为以下原因，显示器驱动程序一般不受这个改变影响：

§ 驱动程序总是在DrvCompletePDEV的实现中分配ppdev->hdevEng= hdev。

§ 驱动程序总是在任一请求HDEV的回调过程中引用ppdev->hdevEng。

2.3.1 切换桌面：对DrvAssertMode的响应

当在显示器上的桌面之间切换时，窗口管理者确保正确地重画桌面、使鼠标指针有效并在正确的位置显示。当桌面切换时显示器驱动程序仅接收对DrvAssertMode 的调用。

调用这个函数时，驱动程序确保指示的PDEV或者在创建PDEV时指定的模式里，或者在文本模式里。然后窗口管理者选择正确的指针形式并移动它到当前位置。是GDI而不是驱动程序负责维护鼠标指针的状态。

GDI调用DrvAssertMode设定指定硬件设备的模式。当创建显示器驱动程序定义的PDEV结构时，这个函数选择指定的模式或者硬件的缺省模式。驱动程序保存PDEV当前模式的记录。

当用户在x86应用中从窗口应用切换成全屏应用时，或者当用户切换桌面时(在所有平台上)，GDI也调用DrvAssertMode，允许把参数设定成FALSE。在对视频微端口驱动程序的EngDeviceIoControl调用中，通过发送IOCTL_VIDEO_RESET_DEVICE，显示器驱动程序必须把视频硬件恢复为缺省模式。

2.3.2返回显示器模式：DrvGetModes

显示器驱动程序也必须支持DrvGetModes。这个函数给GDI一个指向DEVMODEW结构数组的指针。此结构为它支持的各种模式定义显示器的属性，包括维数(对像素和毫米两者)、平面数、每个平面的位数、颜色信息等等。

2.3.3 支持多PDEV

这一节显示在当前的PDEV仍然在装载时，应用程序如何能够创建一个新的PDEV。因为应用能够用新桌面来创建一个新的PDEV，那么控制面板的Display applet程序要求显示器驱动程序支持另外的PDEV的启用。特别地，通过Display applet程序，最终用户通过运行一个测试程序，能够改变诸如大小、颜色数和屏幕的刷新速率这样的元素。Display applet程序动态地创建新桌面来测试显示器模式的改变。

当用户通过Display applet程序请求模式改变时，GDI执行下面的步骤。这些步骤假设当前驱动程序实例不拥有活动的Direct3D、WNDOBJ或DRIVEROBJ对象。

1. 暂时使当前的PDEV无效。

§ 调用DrvAssertMode(旧PDEV实例)。如果假设用微端口驱动程序控制，使用FALSE进行调用，或者如果使用PDEV。则用TRUE进行调用。

2. 装入一个新驱动程序(如果通过新PDEV实例请求)。

§ 调用DrvEnableDriver(新驱动程序实例)。

3. 创建一个新PDEV。

§ 调用DrvEnablePDEV(新PDEV实例)。

§ 调用DrvCompletePDEV(新PDEV实例)。

§ 调用DrvEnableSurface(新PDEV实例)。

4. 取出DirectDraw信息(如果DirectDraw通过驱动程序hook)。仅当第一次调用成功才进行第二次对DrvGetDirectDrawInfo的调用。

§ 调用DrvGetDirectDrawInfo (新PDEV实例)。

§ 调用DrvGetDirectDrawInfo (新PDEV实例)。

5. 使DirectDraw有效(如果通过驱动程序hook并且前一个对DrvGetDirectDrawInfo的调用成功)。

§ 调用DrvEnableDirectDraw(新PDEV实例)。

6. 拷贝旧PDEV状态到新的PDEV实例(如果使用相同驱动程序和DirectDraw的两个实例是通过驱动程序hook)。

§ 调用DrvResetPDEV。

7. 通知每一个与新HDEV相关的驱动程序实例。对DrvCompletePDEV的第一次调用通知新驱动程序实例；第二次调用通知旧驱动程序实例。

§ 调用DrvCompletePDEV(新PDEV实例)。

§ 调用DrvCompletePDEV(旧PDEV实例)。

驱动程序在任何请求HDEV的对GDI回调的中使用新HDEV值。

8. 使DirectDraw无效(如果通过驱动程序hook并且DirectDraw是活动的)。

§ 调用DrvDisableDirectDraw(旧PDEV实例)。

9. 使表面无效。

§ 调用DrvDisableSurface(旧PDEV实例)。

10. 使PDEV无效。

§ 调用DrvDisablePDEV(旧PDEV实例)。

在这个例子中，当用户点击Apply时，GDI暂时使当前的PDEV无效，然后在对话框中创建符合显示器模式选择的第二个PDEV。在测试模式下，用户在显示器屏幕上观察位图以后，撤消第二个PDEV，并且Display applet 程序为桌面恢复原来的PDEV。注意不能恢复原来的显示器的设定，如果设置与硬件和驱动程序是不兼容的，那么系统就不可用。

如果驱动程序的当前实例拥有Direct3D、WNDOBJ或者DRIVEROBJ 对象，前面模式的驱动程序的视图按如下改变顺序变化。（注意在Windows 2000中，驱动程序一初始化DirectDraw就总是有效的。）

§ 拥有的驱动程序实例的析构要推迟。特别地，在模式改变时第7步、第8步和第9步不产生对DrvCompletePDEV的第二次调用。由于在第1步中调用DrvAssertMode(FALSE)，因此禁止旧驱动程序实例，并且保留它，直到系统还原至原来的模式或者所有对象引用实例均被撤消为止。

§ 在撤消引用对象之前如果系统还原到原来的模式，再现原来的驱动程序实例。即不产生第2至第5步并且通过调用DrvAssertMode(TRUE) 重新启用原来的驱动程序实例。(参见第一步。)

§ 在撤消所有的引用对象之前如果系统不还原到原来的模式，当删除撤消最后一个引用对象时撤消驱动程序实例。即当撤消最后一个引用对象时在第7步、第8步、和第9步中产生对DrvCompletePDEV的第二次调用(例如，当终止所有自己的进程时)。

这隐含着在任何时候，调用Direct3D或OpenGL驱动程序能撤消非活动的驱动程序实例。如果驱动程序是在全屏DOS模式中，或者另外的驱动程序(例如VGA驱动程序)完全地拥有硬件，那么即使另外的驱动程序实例目前有效也能够调用这些驱动程序。因此驱动程序的DrvDisableDirectDraw、DrvDisableSurface、和DrvDisablePDEV例程(参见第8至第10步)不能假定设备处于图形模式中，以及它们拥有独占访问。一般地，在DrvDisableXxx例程中驱动程序不必操作视频硬件，除非驱动程序知道它们的实例当前是活动的(从最后一次调用DrvAssertMode存储状态)。

注意 PDEV是驱动程序专用的并且包括表示相关物理设备的所有信息和数据。为了创建多PDEV，图形驱动程序必须满足以下两个要求：

1. 驱动程序不必使用全局变量来代替间接引用PDEV结构的成员。当创建新PDEV或恢复旧PDEV时，如果使用了全局变量，它们可以包含或指向随机数据。在PDEV中必须保存所有状态信息。总是把PDEV传送给任一图形操作，并且为此用来得到或设定全局数据。

2. 在图形驱动程序中必须执行DrvDisableSurface 、DrvDisablePDEV和DrvDisableDriver例程，以便使应用程序能够创建和撤消另外的PDEV，并且在一些情况下装载多个驱动程序。

注意 如果驱动程序的版本号是1.0，GDI不调用驱动程序来创建第二个PDEV。在DRVENABLEDATA中返回驱动程序的版本号。

注意 有时候用不同于当前装载的驱动程序来显示Display applet的测试位图。例如，如果系统用VGA驱动程序在16色模式中运行，并且用VGA64K显示器驱动程序来测试64K色模式，动态地装载VGA64K驱动程序并且当测试完成时卸载。

2.4 指针控制

每一个应用程序必须能够控制一个在已开窗口的显示器周围移动的指针，响应指示设备，如鼠标。显示器驱动程序、GDI或视频微端口驱动程序能够绘制指针。

GDI能够直接为使用线性可变址缓冲区的显示器处理所有的指针拖动。对于没有线性帧缓冲区的设备，GDI用DrvCopyBits完成指针拖动。然而在显示器驱动程序中，由硬件支持的和执行的指针代码速度更快。

显示器驱动程序有时可以选择将绘制那一种类型的指针，以及允许GDI处理那一种类型的指针。例如，设备可以支持硬件中的单色指针，但是无法进行彩色指针的调用，而允许GDI去处理彩色指针的调用。

显示器驱动程序能够控制处理器不独占的指针，并且这个指针不会被中断移动，如垂直同步中断。在这些特别的情况中，微端口驱动程序必须绘制和控制指针，因为需要某个核心模式回调（只在微端口驱动程序中可用）。这会从反面影响性能，因为它要求IOCTL为每个指针操作和微端口驱动程序通信。

为编写显示器驱动程序和微端口驱动程序对，在两个驱动程序之间传递指针信息必须包含IOCTL，并允许微端口驱动程序在必要时假定绘制任意或所有的指针。permedia和framebuf例子驱动程序使用整个方法处理指针。视频指针IOCTL定义参见在线DDK的Video I/O Control Codes。

2.4.1 指针绘制

GDI支持彩色指针和单色指针。单色指针的形状用单个位图定义。位图的宽度与显示器上指针的宽度相同，但是位图的垂直范围是显示器上的两倍，允许它包含两个掩码。

对指针函数的调用由GDI串行化。这意味着驱动程序中两个不同的线程不能同时执行指针函数。有两种可能的指针函数：DrvSetPointerShape和DrvMovePointer。

2.4.1.1 绘制单色指针

单色位图由两部分组成：第一个定义指针的AND掩码，第二个定义XOR掩码。合到一起，这些掩码为指针图像的每个像素提供两位信息。下表描述了在AND和XOR掩码中显示指定值的结果。

AND掩码值 | XOR掩码值 | 显示结果
0 | 0 | 像素是黑的
0 | 1 | 像素是白的
1 | 0 | 像素不变（透明）
1 | 1 | 像素颜色翻转

这个位图定义和使用提供了一个黑白图像，同时提供了组成指针的像素的透明和翻转的支持。

2.4.1.2 绘制彩色指针

彩色指针的定义方法和单色指针相同，也提供了对透明和翻转的支持。

§ 如果彩色位图是黑色（索引0）并且AND掩码值是1，结果是透明。

§ 如果彩色位图是白色并且AND掩码值是1，结果是翻转。

§ 如果设备不能显示彩色，指针可被画成黑色或白色。

这些约定允许应用为彩色和单色显示器使用一个指针定义。

2.4.2 控制指针：DrvSetPointerShape

如果显示器驱动程序控制指针，驱动程序必须支持DrvSetPointerShape允许改变指针的形状。对DrvSetPointerShape的一个调用产生下列结果：

1． 函数删除任何已经存在的指针，驱动程序在显示器上已经绘制了这些指针。

2． 设置新的请求的形状，除非它不能处理这个形状。

3． 新指针在调用的参数指示的位置显示。

驱动程序能调用DrvSetPointerShape使GDI管理一个软件光标。

2.4.3 移动指针：DrvMovePointer

如果DrvSetPointerShape包含在驱动程序中，也必须支持DrvMovePointer。DrvMovePointer把驱动程序管理的指针移动到一个新的位置。因为GDI串行调用指针函数，当任何线程在显示器驱动程序中绘制时并不调用DrvMovePointer，除非在DEVINFO结构中设置了GCAPS_ASYNCMOVE标志。

驱动程序要调用MovePointer使GDI在设备上移动引擎管理的指针。通过调用EngSetPointerShape驱动程序请求GDI管理光标。

2.5 管理显示器调色板

如果视频硬件支持被设置的彩色，它维护一个称作调色板的颜色查找表。GDI得到每个RGB值并转换成设备颜色索引，使其能够被显示。GDI使用了预先计算和缓存的表进行转换。这些表对驱动程序可以作为用户对象XLATEOBJ访问。因此，每个GDI图像函数取到源彩色，使用XLATEOBJ结构把它们移动到目标设备，进行颜色转换。有关调色板以及GDI如何处理它们的更多的信息，参见第一部分，第2章，GDI对调色板的支持。

如果视频硬件支持被设置的调色板，当GDI结束了把颜色映像到应用请求的设备调色板，GDI调用在显示器驱动程序中的DrvSetPalette函数。GDI传递新的调色板到显示器驱动程序，驱动程序查询PALOBJ，为视频硬件设置其内部硬件调色板以匹配调色板变化。这称作调色板实现。

DrvSetPalette函数为驱动程序提供了对PDEV的处理，请求驱动程序实现设备的调色板。驱动程序设置硬件调色板，尽可能地匹配给定调色板中的入口。

如果设备支持一个能够设置的调色板，就请求这个入口点，否则就不再提供。在DrvEnablePDEV返回的DEVINFO结构的flGraphicsCaps字段中设置GCAPS_PALMANAGED位，显示器驱动程序指定设备有一个可设置的调色板。

服务例程PALOBJ_cGetColors对显示器驱动程序是可用的。这个函数从索引调色板中下载RGB颜色，在DrvSetPalette的实现中调用。

2.6 显示器驱动程序中的位图

某些设备，如16色VGA显示器，执行位块传输比非标准位图更快。为支持这一点，驱动程序可以挂起DrvCreateDeviceBitmap，允许驱动程序创建驱动程序完全管理的位图。当驱动程序创建了这样一个位图，可以用任何格式存储这个位图。它检查要传递的参数并提供至少和请求的每像素的位数一样多的位图。在创建后不定义位图的内容。如果应用请求设备管理的位图，在DrvCreateDeviceBitmap 返回控制后，GDI为绘图函数调用驱动程序。如果驱动程序返回FALSE，驱动程序管理的位图就没有创建，GDI可以在一个引擎管理的表面处理绘图操作。

DrvSaveScreenBits函数也和显示器驱动程序中的位块传输有关。一些显示器驱动程序从屏幕外的设备存储器移动数据比从DIB重画或拷贝的区域更快。这些驱动程序挂起DrvSaveScreenBits，当出现菜单或对话框时，它使调用驱动程序保存或恢复指定的显示图像的矩形更快。

注意 对位块传输调用，GDI（不是驱动程序）处理指针删除和裁剪区的锁定。

在屏幕外存储器实现设备位图的驱动程序能显著地改变系统的性能。屏幕外设备位图通过以下方法改进系统的性能：

使用加速器硬件代替GDI进行绘图。

改进位图到屏幕位块传输的速度。

减少主存储器的请求。（存储在屏幕外存储器的位图不占用主存储器的空间。）

使用硬件方法执行支持OpenGL的操作，如屏蔽位块传输和双缓冲。

驱动程序可以通过DrvCreateDeviceBitmap在屏幕外的存储器中实现设备位图。例子permedia驱动程序屏幕外的存储器中实现设备位图并提供一个完整的例子。

2.7 异步绘图

异步处理一个或多个DDI绘图操作以及通过使用EngModifySurface提供GDI访问其位图的显示器驱动程序必须实现一个同步例程。在批量进行DDI绘图操作时为了避免绘图出错，驱动程序也必须提供同步例程。

这样的驱动程序有两个选项实现DrvSynchronizeSurface或DrvSynchronize作为同步例程。GDI调用这些例程之一，仅当驱动程序将它们在EngAssociateSurface中挂起。这两个同步例程都挂起时，驱动程序在GDI中只调用DrvSynchronizeSurface。

DrvSynchronizeSurface给驱动程序提供额外的信息，不管是否有同步事件以及为什么会发生。这使驱动程序减少了同步产生的性能延迟。例如，跟踪哪个设备位图在在加速器队列中的驱动程序可能要从DrvSynchronizeSurface中立即返回，如果指定的表面当前不在队列中。

除了提供同步例程之外，驱动程序通过在DEVINFO结构的flGraphicsCaps2字段中设置下列标志也能激活一个基于时间的或可编程刷新机制。

§ GCAPS2_SYNCTIMER，它引起周期性地调用驱动程序的同步例程。批量DDI调用驱动程序必须指定这个标志。通过这些操作，驱动程序避免了一些问题，如软件光标移动或猝发执行的绘图产生的延迟。

当同步例程因周期性事件调用时GDI传递DSS_TIMER_EVENT标志到DrvSynchronizeSurface。

§ GCAPS2_SYNCFLUSH，当调用Microsoft® Win32® GdiFlush API时，引起调用驱动程序的同步例程。执行异步绘图的驱动程序必须指定这个标志并提供一个同步例程。

当同步例程因基于刷新的事件调用时，GDI传递DSS_FLUSH_EVENT标志到DrvSynchronizeSurface。有关GdiFlush更多的信息参见平台SDK文档。

批量DirectDraw绘图调用的限制

当目标表面是可视屏幕时，驱动程序必须不进行批量DirectDraw调用。这种情形在Windows DirectX®应用中出现，完整的帧通过DdBlt更新到屏幕，因此被立即显示。这个限制也作用于DirectDraw®视频端口表面，进行异步地翻转。

2.8 显示器驱动程序中的透明性

如果显示器硬件支持透明性，显示器驱动程序要实现DrvTransparentBlt。

为了减少从视频存储器读的代价，当源和目标表面都在视频存储器中时，必须实现这个函数。驱动程序让GDI处理从系统存储器到视频存储器的透明位块传输，同时让GDI处理扩展的位块传输。

例子代码参考permedia例子驱动程序。

2.9 显示器驱动程序的特别效果

Windows® 2000 支持下列特殊效果：

如果显示器硬件支持a混合，显示器驱动程序能实现DrvAlphaBlend。

如果显示器硬件支持梯度填充，显示器驱动程序能实现DrvGradientFill。

a混合

Windows 2000命令解释程序使用广泛的a混合执行一些操作如blend-in和blend-out动画以及 a光标。因为a混合要求从源和目标表面读取，当源或目标是视频存储器时直传到GDI很慢。因此，在驱动程序中硬件加速器产生可视的光滑动画并改进整体系统的性能。

驱动程序为非延伸的位块传输实现DrvAlphaBlend，使用一个常量a从兼容的位图，以及使用每像素的a值从32 bpp BGRA系统存储器表面。使用三角形纹理填充能够实现DrvAlphaBlend，提供无缝的可视性。

注意DrvAlphaBlend产生的最坏情况的错误是每个颜色通道超过1。

梯度填充

Windows 2000命令解释程序在所有的标题条上使用梯度填充。

注意 DrvGradientFill产生的最坏情况的错误是，累积超过3个颜色通道，不超过8。

2.10 显示器的颜色管理

GDI支持图像颜色管理（ICM）2.0版。显示器驱动程序能不要实现任何特殊代码使用ICM。

如果显示器硬件支持g斜面，显示器驱动程序要实现DrvIcmSetDeviceGammaRamp。彩色校准应用要求颜色精确使用这个性能。DirectDraw也使用这个函数允许DirectX应用（如在RGB模式中执行调色板动画的游戏）控制g斜面。例子代码参考permedia例子驱动程序。

2.10.1 监视器Profile

监视器profile是使用彩色管理的设备profile。这个profile包含一些信息，关于如何把监视器颜色空间和颜色范围里的颜色转换到与设备无关的颜色空间的颜色。任何用户模式的应用，如设置程序或具有图形能力的字处理程序，能使用监视器profile，提供ICM已经启用及应用已知profile的格式。

虽然使用第三方工具可以创建客户监视器profile，也可以使用与Windows 2000一起封装的监视器profile之一，在下表中描述。

Profile | 监视器特征
mnB22G15.icm | B22 phosphor，gamma 1.5
mnB22G18.icm | B22 phosphor，gamma 1.8
mnB22G21.icm | B22 phosphor，gamma 2.1
mnEBUG15.icm | EBU phosphor，gamma 1.5
mnEBUG18.icm | EBU phosphor，gamma 1.8
mnEBUG21.icm | EBU phosphor，gamma 2.1
mnP22G15.icm | P22 phosphor，gamma 1.5
mnP22G18.icm | P22 phosphor，gamma 1.8
mnP22G21.icm | P22 phosphor，gamma 2.1
Diamond Compatible 9300k G2.2.icm | 9300° Kelvin white point，gamma 2.2
Hitachi Compatible 9300k G2.2.icm | 9300° Kelvin white point，gamma 2.2
NEC Compatible 9300k G2.2.icm | 9300° Kelvin white point，gamma 2.2
Trinitron Compatible 9300k G2.2.icm | 9300° Kelvin white point，gamma 2.2

安装一个监视器profile

用户可以用3种不同的方法安装监视器profile：

l 在Windows资源管理器中选择一个profile，在名字上单击鼠标右键，再点击Install Profile。

l 参考监视器INF文件中的profile。

l 在应用中用显式的代码指定profile的路径和文件名。

因为监视器profile默认的目录受变化的影响，所以不推荐使用显式的代码指定profile的路径和文件名。

使用一个监视器profile

与打印机profile不同，监视器profile不支持输出设备和应用程序之间很小的通信。例如，如果一个用户在视频缓冲区中改变g斜面，监视器profile不会注意这样一个小的变化发生。在这种情况下，通过ICM两个颜色修正在显视之前应用于图像，如下列步骤所示。

1．应用打开并操作图像。

2．应用通过调用Win32 GDI ICM函数，如SetICMMode，使ICM有效。（更多的信息参见平台SDK。）

3．应用发送图像到Win32 GDI。

4．如果ICM启用，Win32 GDI使用监视器profile转换图像中的颜色。

5．Win32 GDI发送图像到核心模式GDI。

6．核心模式GDI为显示器驱动程序格式化图像，基于这样一些DC的设备profile，如位深度、分辨率和过渡调色等。

7．显示器驱动程序（或视频硬件）对图像进行g修正。

2.11 DirectDraw和GDI

当显示器驱动程序初始化时，GDI自动启用DirectDraw。为在DirectDraw和驱动程序的DDI部分之间提供更好的相互作用，驱动程序也要支持DirectDraw DDI实现或调用下列函数：

DrvDeciveSurface

GDI驱动程序表面包裹DirectDraw驱动程序表面的驱动程序实现的函数，允许任何GDI绘制到DirectDraw视频存储器，或被硬件加速的AGP表面（而不是通过DIB引擎用软件绘制）。一般地，如果驱动程序已经支持屏幕外的设备位图，这个函数只在很少几个额外的线的代码中请求。

DrvDeciveSurface改进了也使用GDI的DirectDraw应用的性能，当软件光标在DirectDraw或Direct3D应用中使用时也消除了光标闪烁。

例子驱动程序的实现参见permedia驱动程序源代码。

HeapVidMemAllocAligned和VidMemFree

驱动程序调用的函数使用DirectDraw堆管理器进行所有的屏幕外存储器管理。DrvCreateDeviceBitmap调用HeapVidMemAllocAligned请求DirectDraw为GDI位图分配空间；DrvDeleteDeviceBitmap调用VidMemFree释放分配。

DirectDraw对于屏幕外的存储器分配比驱动程序的DDI部分更加优先。驱动程序挂起DirectDraw DdFreeDriverMemory回调，它允许驱动程序从屏幕外存储器移开GDI表面，得到为高优先级的DirectDraw表面分配的空间。

HeapVidMemAllocAligned和VidMemFree都在dmemmrg.h中声明，它发布在Windows 2000 DDK中。在包含这个头文件之前，驱动程序必须定义_NTDDKCOMP_。

例子驱动程序的实现参见permedia驱动程序源代码。

2.12 跟踪窗口变化

窗口的变化，包括在多监视器系统中，能由设备驱动程序通过WNDOBJ跟踪。WNDOBJ是一个驱动程序级的窗口对象，包含窗口的位置、大小和可视的客户区域等信息。通过对应于应用程序窗口创建一个WNDOBJ，驱动程序能跟踪窗口的位置、大小和可视的客户区域的变化。

应用程序使用Win32 API访问设备驱动程序实现的WNDOBJ_SETUP函数。通过Win32 ExtEscape函数获得访问。GDI用DrvEscape把这个出口调用传递到设备驱动程序，DrvEscape由设备驱动程序用WNDOBJ_SETUP的iEsc值实现。

应用调用ExtEscape(hdc, WNDOBJ_SETUP, …)并通过驱动程序的输入缓冲区传递句柄到应用创建的窗口（由CreateWindow或一些等效的Win32函数）。如果驱动程序保持跟踪窗口，它在ExtEscape调用的环境中调用EngCreateWnd，为给定的窗口创建一个WNDOBJ结构。从这一点，窗口的任何改变都传递到驱动程序。

驱动程序用类似于下面的方式处理ExtEscape调用：

ULONG DrvEscape(

SURFOBJ *pso,

ULONG  iEsc,

ULONG  cjIn,

PVOID  pvIn,

ULONG  cjOut,

PVOID  pvOut,

{

WNDOBJ *pwo;

WNDDATA *pwd;

if (iEsc == WNDOBJ_SETUP)

{

pwo = EngCreateWnd(pso,*((HWND *)pvIn),&DrvVideo,

WO_RGN_CLIENT,0);

//Allocate space for caching client rects. Remember the

//pointer in the pvConsumer field.

pwd = EndAllocMem(0,sizeof(WNDDATA),DRIVER_TAG);

WNDOBJ_vSetConsumer(pwo,pwd);

//Update the rectangle list for this wndobj.

vUpdateRects(pwo);

return(1);

}

)

创建一个窗口对象包括锁定特定的窗口资源，因此，只在WNDOBJ_SETUP的环境脱离DrvEscape或DrvSetPixelFormat，才调用EngCreateWnd。

EngCreateWnd函数支持多驱动程序的窗口跟踪。通过EngCreateWnd，每个驱动程序标识自己的回调例程，GDI用来调用改变相应的窗口。例如，这个特性允许一个活的视频驱动程序跟踪活的视频窗口的变化，而OpenGL驱动程序跟踪OpenGL窗口的变化。

如果一个新的WNDOBJ 在DrvSetPixelFormat或ExtEscape中调用，GDI用最新的窗口状态回调驱动程序。当WNDOBJ引用的窗口被撤消时，GDI也回调驱动程序。

作为一个加速器，驱动程序可以访问WNDOBJ的公共域，定义如下：

typedef struct {

CLIPOBJ       coCLIENT;

PVOID     pvConsumer;

RECTL     rclClient;

} WNDOBJ;

这里coClient是CLIPOBJ，描述窗口的区域；pvConsumer是一个驱动程序定义的值，标识这个特别的WNDOBJ；rclClient是屏幕坐标中窗口客户区域无右下边的矩形。

注意 这些公共字段要确保保持不变，只在EngCreateWnd中提供给GDI的驱动程序回调例程的环境中，或者是给定WNDOBJ的DDI函数。

跟踪窗口改变包括给WNDOBJ提供支持的3个回调函数的使用。可视区域通过WNDOBJ_cEnumStart和WNDOBJ_bEnum回调函数计算。驱动程序通过调用WNDOBJ_vSetConsumer回调函数将其自己的数据与WNDOBJ相关。

2.13 支持DitherOnRealize标志

在GDI/DDI的早期版本中，GDI对显示器驱动程序函数的两个调用被请求用来抖动指定的颜色，并实现颜色的画刷。例如，当应用请求用抖动的颜色填充矩形时，GDI一般调用DrvBitBlt，传递要使用的矩形的范围和画刷对象。显示器驱动程序检查画刷，发现它还没有被实现，并用BRUSHOBJ_pvGetRbrush回调GDI进行GDI的画刷实现。因为是显示器驱动程序而不是GDI执行画刷抖动，GDI传递在DrvDitherColor回调函数中应用最初为抖动提供的RGB给显示器驱动程序。

DrvDitherColor返回一个指向彩色索引数组的指针，描述给GDI提供的抖动信息。GDI通过调用DrvRealizeBrush把这个抖动信息立即传递给显示器驱动程序。随着BRUSHOBJ的实现，控制返回到GDI并因此返回到最初的DrvBitBlt函数。

为使用以上技术实现抖动，在调用DrvRealizeBrush之后，GDI必须立即调用DrvDitherColor，两个独立的函数调用。在DEVINFO结构里设置GCAPS_DITHERONREALIZE标志，修改DrvRealizeBrush来有效地组合这些函数，排除了独立调用DrvDitherColor以及保存某些存储器分配的需求。在这个模式下，如果显示器驱动程序设置GCAPS_DITHERONREALIZE，GDI使用将要抖动的RGB和在iHatch中设置的RB_DITHERCOLOR标志调用DrvRealizeBrush。RB_DITHERCOLOR标志在iHatch的高字节中设置，而要抖动的RGB颜色包含在3个低字节中。在这个情况下排除了调用DrvDitherColor，因为两次调用的函数放进了一个中。

例子代码参见permedia例子驱动程序。

2.14 支持分组的帧缓冲区

当前大多数加速器有帧缓冲区，能够线性映射到CPU地址空间。这些设备的显示器驱动程序不必要支持分组的帧缓冲区。

GDI不能直接访问与分组的帧缓冲区相关的分组存储器。因此，具有这样的帧缓冲区的设备的 显示器驱动程序必须把帧缓冲区分成一系列连续的组，并提供一种方法使GDI在相应的帧缓冲区中执行其绘图操作。即GDI在移动到后一个组之前，向帧缓冲区的一个组写入数据，如果必要，通过一个称作“组回调”的机制完成绘图操作。

与Windows 2000 DDK一起发布的permedia例子驱动程序提供了例子代码，实现分组的帧缓冲区支持。

图2.3显示了例子加速器的帧缓冲区，一个1024´768的VGA显示器缓冲区，分成几个组。这个图仅仅是示意图。显示器驱动程序特地不使用物理地址A000；它使用一个逻辑地址通过微端口驱动程序传递。

图2.3 映射视频缓冲区到分组帧缓冲区的例子

在这个例子中，视频存储器的内容通过一系列绘图操作写到加速器，帧缓冲区的地址连续的组中。至于GDI所关心的，出现的每个绘图操作是到标准的帧缓冲区，而不是到加速器的帧缓冲区的不同的组。加速器的设备驱动程序在分组的基础上，处理引起GDI向加速器的帧缓冲区绘图的分组操作。

当加速器使用一个分组的帧缓冲区时，帧缓冲区是设备管理的表面，因此显示器驱动程序挂起绘图函数调用。当显示器驱动程序挂起一个调用，如绘制路径、填充路径或位块传输，它确定在帧缓冲区中哪一个组受被调用的绘图函数的影响。

如果驱动程序选择用GDI执行绘图函数，它调用相应的EngXxx函数。然而，在进行调用之前，显示器驱动程序必须修改它在挂起调用时接收的裁剪和表面对象，并在GDI的回调中传递这些修改的对象。修改它们可以防止GDI在组外绘图。也就是如果调用GDI绘制一部分在下一个组中的路径，以及如果裁剪和表面对象没有被修改，GDI写到当前组的范围以外。如果GDI试图在组的范围以外绘图，存取失败的结果将很难跟踪。

图2.4中分组帧缓冲区的例子显示在显示器跨越分组帧缓冲区的两个组，BANK_1和BANK_2，如何绘制一个椭圆对象。

图2.4 帧缓冲区中绘制跨越多个组的对象

为绘制这个对象，GDI必须首先绘制椭圆的顶部（在BANK_1中）到标准的帧缓冲区，然后绘制椭圆的下半部分到同一个标准缓冲区。显示器驱动程序必须通过GDI映射这两个连续的写操作到分组的帧缓冲区BANK_1和BANK_2进行显示，并防止GDI写到每组的限制以外。

当执行分组帧缓冲区时，显示器驱动程序通过检查调用的参数或回调GDI，能确定对象的边界（目标矩形的大小）。根据对象的边界，驱动程序能确定对象跨越多少组。对边界矩形接触到的每个组，显示器驱动程序回调相应的GDI绘图函数，改变每个调用的值。

驱动程序改变由GDI最初传递的CLIPOBJ成员，对应于组边界的改变。顶部和底部的扫描值重新定义，GDI不试图在组的限制以外绘制。组管理器从GDI获得原始的CLIPOBJ数据，并保留这些值为以后恢复用。然后改变边界，提供新的rclBounds.top和rclBounds.bottom值，描述要绘制的组的范围。分组期间，GDI必须裁剪到一个尺寸，防止绘制整个路径和超出当前组的限制。

如果GDI传递的原始的CLIPOBJ定义为NULL或DC_TRIVIAL，显示器驱动程序传递通过EngCreateClip创建的替代的CLIPOBJ。这个替代的CLIPOBJ被改变，用来定义一个裁剪窗口，这样GDI对一个组的范围进行裁剪。如果CLIPOBJ是复杂的，如在椭圆上的三角形对象，如图2.4所示，显示器驱动程序用rclBounds.top和rclBounds.bottom的值改变这个复杂的CLIPOBJ，在两个裁剪对象间产生一个附加的效果。作为结果，GDI防止写到组的边界以外。驱动程序必须恢复前面从GDI获得的CLIPOBJ数据的原始边界。

除改变边界值之外，显示器驱动程序在裁剪对象中设置OC_BANK_CLIP标志，通知GDI这是一个分组的回调。

GDI也必须参考标准帧缓冲区的起始点进行绘制。当调用绘图是，GDI简单地得到一个指向SURFOBJ的指针，包括pvScan0、lDelta和iBitmapFormat成员。GDI使用这些值计算在表面的什么位置绘制，算法如下：

start_draw_point = pvScan0 + (y*lDalta) + (x*PixelSize(iBitmapFormat))

这里x和y是绘图开始的坐标，start_draw_point是第一个像素绘制的地址。GDI在每个绘图调用时执行这个计算，并总是以pvScan0为SURFOBJ的参考，它是标准帧缓冲区的起始逻辑地址。

例如，如果GDI需要绘制每像素8位的64K帧缓冲区的全部内容，pvScan0的起始逻辑地址为0x100000，将在0x10FFFF（0x100000+（63*1024）+（1023））结束绘图操作，这里y是63，lDalta是1024，x是1023（最后一根扫描线的最后像素的位置）。

下一次显示器驱动程序调用GDI绘制的对象的部分落在分组的帧缓冲区的下一个组，GDI解释y的值为64。pvScan0的值为0x100000，y的值为64，GDI尝试开始在0x110000处写入数据。然而，0x110000超出了64K帧缓冲区0x10FFFF的范围，在这个操作中GDI不能进行写数据。

因此，当显示器驱动程序请求GDI写入出现在帧缓冲区的第二个及随后的组中时，驱动程序必须减pvScan0的值，使GDI计算起始点仍然参考0x100000的例子地址。继续这个例子，当在帧缓冲区的第二个组中绘图时，这个方法减去pvScan0的值到0x090000。作为对pvScan0改变的一个结果，GDI仍用地址0x100000参考绘图。即0x090000+（64*1024）+0等于0x100000，这里GDI必须开始以数据映射到帧缓冲区第二个组的顺序绘图。

2.15 卸载视频硬件

当一个表面不再被请求，GDI调用DrvDisableSurface通知显示器驱动程序，通过DrvEnableSurface为当前硬件设备创建的表面可以被禁止。驱动程序必须释放表面使用的任何资源。

表面被禁止后，GDI调用DrvDisablePDEV通知驱动程序不再需要意见设备。然后驱动程序释放在处理DrvEnablePDEV过程中分配的任何存储器和资源。

最后，GDI通过调用DrvDisableDriver禁止显示器驱动程序。驱动程序必须释放在DrvEnableDriver期间分配的任何资源，并恢复视频硬件的缺省状态。当驱动程序从DrvDisableDriver函数返回后，GDI释放它为驱动程序分配的存储器，并从存储器中移走代码和数据。

图2.5显示了GDI禁止视频硬件的调用序列。

图2.5 禁止视频硬件

2.16 显示器驱动程序中的多监视器支持

Windows 2000提供了多监视器支持，显示器驱动程序编写者为提供这个支持不必实现任何特别的代码。

显示器驱动程序的实现必须不使用任何全局变量。对一特定的显示器驱动程序所有的状态必须在PDEV中。GDI为每个通过视频微端口驱动程序创建的硬件设备的扩展调用DrvEnablePDEV。

在多监视器系统中为跟踪窗口变化，驱动程序可以请求GDI用桌面坐标创建WNDOBJ对象。驱动程序通过用WO_RGN_DESKTOP_COORD标记调用EngCreateWnd来进行这个工作。更多的信息参见跟踪窗口变化。

在多监视器系统中，GDI在DEVMODEW结构的dmPosition成员中存储设备的桌面位置。

2.17 镜像驱动程序

镜像驱动程序是一个虚拟设备的显示器驱动程序，反映一个或多个额外的显示器驱动程序的绘图操作。它的实现和属性与其他任意的显示器驱动程序很相似；然而，与其配对的视频微端口驱动程序与一般的视频微端口驱动程序相比则很小。关于镜像系统中视频微端口驱动程序的更多的信息参见第6章微端口中的镜像驱动程序支持。Windows 2000 DDK包含了一个例子镜像驱动程序，在下面3个目录中包括了组件源文件。

目录

包含的源文件

Video\displays\mirror\dll

镜像驱动程序

Video\miniport\mirror

微端口驱动程序

Video\displays\mirror\app

用户模式服务。也包含mirror.inf。

GDI支持虚拟桌面并提供在镜像设备上复制虚拟桌面的一部分的能力。GDI把虚拟桌面作为物理显示器驱动程序层上面的图形层来实现。所有的绘图操作从这个虚拟桌面开始空间；在存在于虚拟桌面的相应的物理显示器设备上，GDI裁剪并绘制它们。

在虚拟桌面上镜像设备能指定任意的裁剪区域，包括跨越多个物理显示器设备的区域。然后GDI发送驱动程序的裁剪区域交叉的镜像设备所有的绘图操作。镜像设备能设置一个裁剪区，与特定的物理设备精确匹配；因此，它能有效的镜像那个设备。

注意 在Windows 2000中，镜像驱动程序的裁剪区必须包括主显示器设备。

mirror 驱动程序例子代码说明了如何实现镜像驱动程序。帮助理解这个例子的更多的信息如下：

l 使用例子INF文件，mirror.inf，作为一个模板。细节参见镜像驱动程序INF文件。

l 参见mirror.exe应用程序，它展示镜像驱动程序如何连接到虚拟桌面。细节参见镜像驱动程序安装。

l 参考平台SDK文档中有关使用Win32 EnumDisplayDevices函数的信息。使用这个函数确定\\.\Display#名与镜像显示器设备相连。这个数字用来改变镜像设备的设置。对于多个实例，#是每个实例的不同数值，必须反复通过可用的显示器设备确定这个数值。

要连接镜像设备和全局桌面：

1．加入REG_DWORD注册表条目Attach.ToDesktop到驱动程序的服务主键。

2．设置键的条目为1。

要禁止镜像驱动程序，设置键的条目为0。

像前面所提及的，驱动程序在设备层上面驻留的绘图层安装和操作。因为镜像驱动程序的坐标空间是桌面坐标空间，它能跨越多个设备。如果镜像驱动程序要镜像主显示器，显示坐标要符合主显示器的桌面坐标。

l 镜像驱动程序安装之后，它被交叉驱动程序的显示区域的所有绘图操作调用。在多监视器系统中，如果镜像驱动程序仅覆盖主显示器设备，这不包括所有的操作。

l 推荐使用用户模式服务维护镜像驱动程序的设置。这个应用能确保在启动时驱动程序正确装入，并且通过WM_DISPLAYCHANGE消息得到显示器改变的通知，它能响应相应的改变。

l GDI调用镜像驱动程序进行任何交叉驱动程序的边界矩形的二维DDI操作。注意，如果表面是设备格式位图，即如果SURFOBJ具有STYPE_DECBITMAP的iType，GDI不执行矩形边界检查。

l 通常，镜像驱动程序必须不使用全局变量实现。对一特定的显示器驱动程序所有的状态必须在PDEV中。GDI为每个通过视频微端口驱动程序创建的硬件设备的扩展调用DrvEnablePDEV。

l 镜像驱动程序不支持DirectDraw。

l 在DEVINFO结构的flGraphicsCaps成员中，镜像驱动程序必须设置GCAPS_LAYERED标志为TRUE。

l 通过实现DrvRealizeBrush，镜像驱动程序可选择支持画刷实现。

GDI允许同一个驱动程序运行在单监视器系统或多监视器系统中。在多监视器系统中的驱动程序只需要知道它在全局桌面中的位置。当Win32 ChangeDisplaySettings调用发生，如用户通过控制面板的Display applet动态改变监视器在桌面上的位置，GDI给驱动程序提供这个位置。当这样的改变发生，GDI相应地更新DEVMODEW结构的dmPosition成员。通过实现DrvNotify，驱动程序能接收到这个改变的通知。更多的信息参见镜像驱动程序安装。

注意 镜像驱动程序不要求进行像素精度的绘制，在客户端这样精度的绘制可能很困难。例如，对于适配器/监视器接收镜像图像，不要求使用和适配器/监视器被镜像的相同的精度绘制网格交叉量化（GIQ）直线和多边形填充。

2.17.1 镜像驱动程序INF文件

可使用Mirror.inf例子镜像驱动程序INF文件作为模板，构造镜像驱动程序INF文件。

注意 不能使用与Windows 2000 DDK一起发布的geninf.exe工具创建镜像驱动程序INF文件。

更多的信息，参见第四部分，第9章，安装机器启动时请求的设备，即插即用、电源管理和Setup设计指导，以及Windows 2000驱动程序开发参考卷1的第三部分，第1章，INF文件段和指令。

2.17.2 镜像驱动程序安装

系统初始化镜像驱动程序来响应对Win32 ChangeDisplaySettings或ChangeDisplaySettingsEx的调用。需要实现一个用户模式的服务进行上面两个调用中的一个来实现镜像驱动程序并维护其设置。这个应用用于：

§ 确保镜像驱动程序在启动时正确装载。应用要指定CDS_UPDATEREGISTRY标志保存对注册表的设置，这样在以后的启动中，驱动程序可以使用下面将描述的同一个DEVMODEW信息自动地启动。

§ 通过WM_DISPLAYCHANGE消息得到显示器改变的通知，响应桌面相应的改变。

例子Mirror.exe，可以从Video\displays\mirror\app目录的源代码文件中生成，实现了一个操作的子集，由用户模式的服务提供装入一个镜像驱动程序。

在镜像驱动程序安装之前，用户模式的应用将填充DEVMODEW结构指定下列的显示器属性：

§ 位置（dmPosition）

§ 大小（dmPelsWidth和dmPelsHeight）

§ 镜像显示器的格式（dmBitsPerPel）

通过为这些结构中每个被改变的成员包含一个标志，用户模式应用也必须设置适当的dmFields。镜像显示器的位置坐标必须用桌面坐标指定；它们可以跨越多个设备。对直接镜像主显示器，镜像驱动程序要指定其位置和主显示器的桌面坐标相一致。

在DEVMODEW结构成员被设置以后，在对Win32 ChangeDisplaySettingsEx函数调用中传递这个结构可以改变镜像显示器的设置。

镜像驱动程序安装之后，GDI调用它进行交叉驱动程序的显示区域的所有绘图操作。在多监视器系统中，如果镜像驱动程序仅覆盖主显示器设备，GDI可能不发送所有的绘图操作到镜像驱动程序。

关于ChangeDisplaySettings和ChangeDisplaySettingsEx函数以及显示器改变桌面通知的更多的信息，参见平台SDK文档。

2.18 Newdisp：动态重新装载显示器驱动程序

除了提供调试器外，Windows 2000提供下列工具测试和调试显示器驱动程序：

newdisp，无须重启系统就可重新装载驱动程序。细节参见NewDisp:动态重新装载显示器驱动程序。

控制面板中Display applet的滑动控制，动态地改变驱动程序中GDI调用的加速器。细节参见允许的驱动程序加速器的动态改变。

2.18.1显示器驱动程序测试工具

Windows 2000 DDK提供了工具允许无须重启就重新动态装载显示器驱动程序。这个工具，称作newdisp.exe，在开发中更新显示器驱动程序代码时，减少必须重启的次数，加速了显示器驱动程序的测试。

要运行newdisp.exe：

1． 关闭所有的Direct3D®和OpenGL应用。

2． 把更新过的显示器驱动程序的DLL拷贝到\system32目录。

3． 运行newdisp（无须任何参数）。

newdisp 的每次调用都重新装载显示器驱动程序。假定在调用时没有驱动程序引用存在，newdisp通过以下操作完成动态重新装载：

§ 用640´480´16色调用ChangeDisplaySettings，这将引起系统装入并运行16色VGA显示器驱动程序DLL，同时，旧的显示器驱动程序DLL从存储器中卸载。

§ 立即执行另一个ChangeDisplaySettings回调到原始模式，这引起新的显示器驱动程序DLL从\system32目录装入，而卸载16色VGA显示器驱动程序DLL。

如果驱动程序有活动的Direct3D、WNDOBJ或DRIVEROBJ对象，就有对驱动程序实例的引用存在。当newdisp运行时对驱动程序实例的引用存在，旧的显示器驱动程序DLL就不会被卸载，并且，相应地，新的显示器驱动程序DLL也不能装入。

newdisp 依赖已加入到Windiws 2000的动态驱动程序装入函数重新装入驱动程序而无须重启；因此，它不能在Windiws NT® 4.0或更早的系统中运行。如果VGA驱动程序不能装入到图形设备，或者本地显示器驱动程序支持的640´480´16色模式代替VGA驱动程序处理的模式，它也不能工作。

注意newdisp不立即引起视频微端口驱动程序的重新装入。如果微端口驱动程序改变，系统必须重新启动来安装和测试它。

2.18.2允许的驱动器加速的动态改变

通过使用包括在控制面板里的显示小应用程序里的游标，驱动器的加速级能够被改变成用户接口。取决于这个游标的设置值，GDI允许下面的驱动器加速级：

值 描述

0 允许所有的显示驱动器加速。

1 DrvSetPointerShape 和DrvCreateDeviceBitmap是不可用的。

2 除过1，不允许更多复杂的显示驱动器加速，包括DrvStretchBlt. DrvFillPath. DrvGradientFill. DrvLineTo. DrvAlphaBlend.和DrvTransparentBlt.

3 除过2，不允许所有的DirectDraw和Direct3D加速。

4 除过3，除过固体颜色填充、DrvCopyBits. DrvTextOut和DrvStrokePath. DrvEscape是不能够外，不允许几乎所有的显示驱动器加速。

5 不允许硬件加速，仅仅在从一个系统内存表面到屏幕做位块传输时调用驱动器。

一个显示驱动器能够通过下面决定当前的加速级：

n 通过执行DrvNotify从GDI接收加速级改变的通知

n 调用EngQueryDeviceAttribute来查询当前的加速级


DirectDraw DDI

微软的DirectDraw DDI是一个图形接口，该接口允许生产厂商为DirectDraw提供硬件加速。它可能是一个显示驱动程序的集成部分或者一个独立的动态链接库（DLL），此动态链接库通过被驱动程序开发者定义的一个私有接口与显示驱动程序通信。在微软的Windows2000上，一个驱动程序必须是一个32位的实现。

本章描述了DirectDraw体系结构和接口，并为DirectDraw驱动程序编写者提供了实现方针，这些方针特别针对Windows NT和Windows2000。然而，在可能的地方，对Windows95和Windows98实现的差异被指出。读者应该熟悉DirectDraw API并对Windows2000显示驱动程序模型有一个牢固的掌握。为了获取有关DirectDraw的更多信息，请参看SDK平台。

3.1 DirectDraw头文件、例子代码和参考

Windows2000 DirectDraw驱动程序编写者使用下面的头文件：

n Ddrawint.h，它包括DirectDraw驱动程序的基本类型、常量和结构

n Ddraw.h，它包括被应用程序和驱动程序使用的基本类型、常量和结构

n Dvp.h，当驱动程序支持DirectDraw视频端口扩展（VPE）时其被使用

n Dxmini.h，当驱动程序包含为内核模式视频传输的微端口支持时其被使用

n Ddkmapi.h，当驱动程序需要为DxApi功能获取内核模式DirectDraw支持时其被使用

n Dmemmgr.h，当驱动程序需要执行它自己的内存管理而不依靠DirectDraw运行时时其被使用

n Ddkernel.h，当驱动程序包含内核模式支持时其被使用

n Dx95type.h，它允许你容易地将现存的Windows95和Windows98驱动程序移植到Windows2000。这个头文件映射两个平台上不同的名字。

除过ddraw.h外，所有的头文件与Windows2000 DDK一起发售，头文件ddraw.h与SDK平台一起发售。

Windows2000 DDK也在s3virge视频显示目录里为DirectDraw驱动程序提供例子代码。

DirectDraw驱动程序函数、回调和结构的参考能够在DirectDraw驱动程序函数和DirectDraw驱动程序结构里找到。

DirectDraw接口的SDK相关的方面包含在SDK平台里。

你可以通过e-mail发送问题和评论至directx@microsoft.com。

3.2 DirectDraw体系结构总览

DirectDraw包括下面的组件：

n 用户模式DirectDraw（ddraw.h），一个系统提供的被DirectDraw应用程序装载和调用的DLL。这个组件提供硬件仿真，管理各种DirectDraw对象，并提供显示内存和显示硬件管理服务。

n 内核模式DirectDraw，一个win32k.sys的集成，它是由系统提供的被一个内核模式显示驱动程序装载的图形引擎。DirectDraw的这个组件为驱动程序执行参数的确认，这样使得实现更多的健壮的驱动程序变得更为容易。自从显示驱动程序成为Windows2000操作系统的可信组件以后，这是一个关键的设计目标。内核模式DirectDraw也处理图形设备接口（GDI）和所有的隔行扫描处理状态的同步问题。

n 显示驱动程序的DirectDraw部分，与其余的显示驱动程序一道，它由图形硬件生产厂商来实现。这个组件在本文档里被称为DirectDraw驱动程序，显示驱动程序的其他部分处理GDI和其他的非DirectDraw相关的调用。

本文档一般称系统提供的这两个组件为DirectDraw。

图3.1给出了DirectDraw驱动程序体系结构的一个模块图表。

如图3.1所示的，一个应用程序通过GDI（用户和内核模式部分）和显示驱动程序访问显卡。显示驱动程序总是支持GDI调用和通常的DirectDraw和Direct3D调用。当设备独立的GDI位图（DIB）引擎部分不被显示驱动程序支持的时候，它从功能上仿真。

当DirectDraw被调用时，它通过DirectDraw驱动程序直接访问图形卡。不象GDI，DirectDraw被通知一个DirectDraw可能的驱动程序的能力，DirectDraw为被支持的硬件功能调用DirectDraw驱动程序或为那些必须在软件里仿真的功能调用硬件仿真层（HEL）。不是如GDI那样为它不支持的请求调用驱动程序，DirectDraw为硬件支持的请求调用驱动程序以及为非硬件支持的请求调用DirectDraw HEL。在另一方面，GDI调用被送到驱动程序，如果调用不被支持的话，那么驱动程序必须回调到DIB引擎里。

注意：如果DirectDraw驱动程序使一个操作失败，DirectDraw不把操作传递给DirectDraw HEL；相反，它将DirectDraw驱动程序的错误代码传回给应用程序。

在初始化的时间和模式改变过程当中,显示驱动程序返回能力（caps）位给DirectDraw。这能够使DirectDraw获取关于可用的驱动程序函数、它们的地址和显卡及驱动程序（如延伸、透明的blit、显示字距和另外的高级特性）的能力的信息。一旦DirectDraw有这些信息，它能够使用DirectDraw驱动程序来直接地访问显卡，而不必进行GDI调用或使用显示驱动程序的特定的GDI部分。

3.2.1 关于DirectDraw

DirectDraw是DirectX的显示组件，该DirectX允许硬件设计者直接操纵显示内存、硬件位块转移、硬件重叠和交换表面。DirectDraw为游戏和Windows子系统软件提供一个设备独立的方法，例如3-D图形包和数字视频编码译码器，来获取特定显示设备的特性。

DirectDraw在一个直接的32位路径里提供设备独立的存取给特定设备显示功能。它在一个直接访问显示卡的驱动程序里存取其重要的功能，而不用Windows GDI或DIB引擎的介入。

通过利用这个直接路径，游戏和另外的显示加强应用程序运行更快且避免撕裂，一个撕裂是由一幅被同时显示和写入的图象引起的屏闪烁，直接存取允许游戏性能只被显卡性能限定。DirectDraw也使用页交换来提供平滑的动画。

快速运动和许多游戏的不断变化屏幕和多媒体应用程序给显示处理施加了重荷并有加剧撕裂的趋势。尽管GDI在画电子数据表和图形、着色TrueType字体等等非常快，这并不意味着它是一个实时图形API。DirectDraw通过在一个32位的驱动程序里处理设备独立的硬件加速器功能增强了GDI。

3.2.2 DirectDraw驱动程序

DirectDraw通过DirectDraw驱动程序，一个通常由显示硬件制造商提供的特定设备接口提供设备独立性。DirectDraw暴露方法给应用程序并使用显示驱动程序的DirectDraw部分直接与硬件工作，应用程序从来没有直接调用显示驱动程序。

在Windows2000上，DirectDraw驱动程序总是32位代码的实现，DirectDraw驱动程序能够是显示驱动程序或者一个独立的DLL部分，该DLL通过被驱动程序编写者定义的一个私有的接口与显示驱动程序通信。在Windows2000上，DirectDraw驱动程序被假定为显示驱动程序的一个部分。

注意：（对Windows95/Windows98而言）当DirectDraw驱动程序单独的DLL，它被ddhelp.exe装载，这发生在DirectDraw被调用时。

DirectDraw仍旧为BIOS调用（其一定是16位的）和模式变化（其不是时间关键的）使用Windows95/Windows98显示驱动程序的非DirectDraw特定的部分。DirectDraw在模式变化过程来更新它自己并为GDI不支持的模式提供支持，例如，GDI在低于640X480的模式下不能够很好的操作。

DirectDraw驱动程序仅仅包括设备独立的代码并没有执行仿真。如果一个功能没有被硬件执行，DirectDraw驱动程序不会将它作为一个硬件能力报告。同样，由于DirectDraw运行时在驱动程序被调用之前验证参数的有效性，DirectDraw驱动程序不验证参数的有效性。

3.2.2.1 DirectDraw驱动程序的第一步

开始实现DirectDraw功能的一个好的方法是修改一个存在的驱动程序，如果没有可用驱动程序，以DDK的DirectDraw部分里的例子代码为开始，并且使驱动程序初始化、锁定和交换工作，这将使驱动程序尽可能快的运行。从这个基本功能，你能够添加更强有力的功能，这些功能将继续改进显示性能。

DirectX软件开发包（SDK），这在SDK平台上能够找到，包括有用的help和readme文件、例子代码和工具如DirectDraw测试，它允许你在开发过程当中监视和测试驱动程序能力。一个与内核调试器连接的远程终端和调试printf状态是验证驱动程序执行的每个动作的不可或缺的工具。

微软已经尽力使开发一个DirectDraw驱动的驱动程序尽可能的容易，然而，为了使终端用户的一切变得简单，复杂被隐藏在防护罩下面。由于预期即将出现的所有无数的硬件变革和竞争标准是不可能的，一定量的复杂程度留给了驱动程序编写者，他们必须改变技术以适应特定的硬件。

DirectDraw需要的最小的驱动程序功能是锁定、打开锁定和交换一个表面。假定硬件支持相关的操作，驱动程序支持也应该增加为blits（包括透明的blits，它对加速游戏的速度是重要的）、延伸和重叠（它对视频回放是关键的）。

3.2.3 硬件仿真层次

DirectDraw驱动程序并不仿真，然而，任何没有被驱动程序处理的DirectDraw功能由DirectDraw HEL处理，HEL（被微软作为DirectDraw的一个部分写入）在软件里仿真驱动程序，并且做DirectDraw需要但DirectDraw驱动程序不能处理的任何事情。这个仿真在用户模式里被完成。

例如，如果DirectDraw驱动程序有为blits设置但不是为透明blits设置的能力位（caps），DirectDraw驱动程序为blits所调用且HEL为透明blits所调用。在透明的blit状况下，HEL被传递一个显示内存指针，并且从后备的表面与颜色键比较每个字节。如果字节不匹配颜色键，HEL使用CPU拷贝它到目标表面。仿真发生在为别的不受支持的操作或者显示卡内存不足。

注意到DirectDraw没有传递失败的DirectDraw驱动程序操作给HEL是重要的，相反，它返回DirectDraw驱动程序的错误代码给应用程序。DirectDraw决定哪一个组件将调用取决于能力位。如果HEL或DirectDraw驱动程序使一个特别的操作失败，一个错误代码被返回到应用程序。

注意：（对Windows95/Windows98而言）在一个小数目的低端卡上，该卡没有DirectDraw-capable 驱动程序，DirectDraw能够使用通用的帧缓冲器（Framebuf）显示驱动程序存取卡，这个驱动程序仅仅可以锁定、打开锁定和交换。如果没有DirectDraw驱动程序且Framebuf没有在卡上工作，DirectDraw为所有的事情使用HEL，这仍快于运行所有的通过GDI和DIB引擎的操作。HEL在驱动程序里也能够使用显示控制接口（DCI）支持在缺少DirectDraw支持的情况下来存取主要的表面。

3.3 DirectDraw驱动程序基础

这部分包括关于为DirectDraw显示驱动程序设计的一般信息，按以下几个类别组织：

n 3.3.1 DirectDraw表面

n 3.3.2 显示内存

n 3.3.3 内存配置例子

n 3.3.4 交换

n 3.3.5 使用图阵

n 3.3.6 假定状态信息的驱动程序

n 3.3.7 打开显卡并在Windows95/98上改变模式

3.3.1 DirectDraw表面

DirectDraw表面是DirectX图形里的基本图象单元，它或者是特定的宽度、高度和象素格式的长方形集合，或者是一个包含微软的Direct3D的命令或至高点的缓冲器。表面有相关于它们的位来表示它们的行为和用法，这些位叫作表面能力位（surface capability bits）（或者简称caps bits），表面能力位表示打算的用法如为着色（DDSCAPS_TEXTURE caps bit）保留纹理象素，为3D着色（DDSCAPS-3DDEVICE）而成为目标和许多其他的方面。为获得表面能力位的更多信息，参考DDSCAPS下的部分内容。

主表面是当前正被显卡扫描输出到监视器的表面，更多有关主表面的陈述将贯穿此文档。

3.3.1.1复杂的表面和附件

表面也能够是复杂的，这意味着它们是相关联的表面的一个更大集合的一部分。复杂表面的例子包括前缓冲和相联系的后缓冲、一个mipmap的不同级别和一个立方图的不同面。

DirectDraw运行时使用一个被称为表面附件（surface attachment）概念来管理不同的简单表面连接成的复杂表面。当应用程序调用IdirectDraw::CreateSurface来构造一个交换链（可能具有Z-buffer的前缓冲和后缓冲）时，表面能够被隐式附着；或者当应用程序通过调用IdirectDrawSurface::AddAttachedSurface用一个着色目标关联一个Z-buffer时， 它们能够被显式附着。

3.3.1.2创建和注销DirectDraw表面

DirectDraw表面在一个四状态的过程里被创建：

1. DdCanCreateSurface。运行时调用驱动程序的DdCanCreateSurface来看是否驱动程序将允许这种类型、大小和格式的表面创建，驱动程序能够返回一个将被传送到应用程序的失败代码。

2. DdCreateSurface。驱动程序创建正被讨论的表面，为表面的内容潜在地分配内存。复杂的表面用对DdCreateSurface的调用能够被立即创建。这样驱动程序也许被要求在一次调用创建许多表面。

3. Memory Allocation。DirectDraw运行时为没被分配的任何表面分配内存来响应DdCreateSurface调用。这个过程在下面的部分里包含了更多的细节。

4. D3dCreateSurfaceEx。这个调用对DirectX7.0 DDI是新的，这个功能用正被讨论的表面联系一个句柄来为以后在Direct3D D3dDrawPrimitives2符号流里所使用。驱动程序也创建在这时被DirectDraw维护的自己的表面结构拷贝。参见Direct3D DDK文档获得有关D3dCreateSurfaceEx的更多信息。

表面被对驱动程序的DdDestroySurface入口点的单个调用所注销。

只要表面创建时的模式保持，表面就会保持。就驱动程序而言，当有一个模式改变，所有驱动程序控制下的表面就被注销。另外的事件也能够引起所有的表面在这种方法下被注销。决定引起一个DdDestroySurface调用的原因对驱动程序而言是不必要的。

3.3.1.3丢失表面和原子恢复

运行时表面对象生命周期比驱动程序的表面对象的生命周期长。在极少的状况下，大部分显著的模式发生变化，表面丢失。这意味着当DdDestroySurface被调用时驱动程序的表面对象被注销了，但运行时表面对象仅仅处于未决的状态，称作“丢失”。稍后，运行时对象能够被恢复，它对应于驱动程序级上的一个DdCreateSurface调用。

正常地，驱动程序不知道这个中间的丢失状态，然而，在某些状况下，对这个过程的理解将有助于驱动程序编写者。

在释放DirectX7.0 DDI之前，表面总是一次一个的被恢复，即使如果它们被最初在一组里作为一个复杂创建的部分而创建。至于DirectX7.0 DDI，驱动程序编写者能够在一个原子调用里选择处理复杂的表面恢复。在表面恢复时期，DirectDraw运行时检查驱动程序的D3dCreateSurfaceEx入口点，如果这个入口点被定义，DirectDraw运行时在一个对DdCreateSurface的调用里恢复所有的复杂表面。驱动程序将最有可能不在原始创建和由恢复表面所产生的创建之间进行区别。

3.3.1.4丢失驱动程序管理的纹理

由于驱动程序管理的表面也消耗视频内存，这些内存资源是稀有的资源，它们也需要能力被放置在未决的状态（丢失状态）。由于驱动程序管理驱动程序管理的表面的视频内存的分配，当这样的表面需要丢失时，一个方法是必要的来通知驱动程序，这个通知已经通过一个扩展的DdDestroySurface调用被添加到DirectX7.0 DDI。

当一个驱动程序管理的表面（用DDSCAPS2_TEXUREMANAGE标志标志）丢失时，驱动程序在一个用DDRAWISURF_INVALID指定的表面结构的dwFlags成员里接收到一个特别的DdDestroySurface调用。驱动程序应该释放相关于管理的表面的视频内存但不应该释放其余的私有的表面信息，包括后备表面拷贝，由于它们并没有真正从驱动程序的观点丢失，将没有新的DdCreateSurface调用来恢复丢失的驱动程序管理表面。对大多数部分，这个特殊的DdDestroySurface调用被特殊地使用来通知驱动程序它将收回它的视频内存拷贝。

3.3.2 显示内存

使用可与前台的一个缓冲器交换的后台的一个缓冲器是利用DirectDraw的最好的办法，页面交换对平滑、游戏里的自由撕裂动画和视频回放是重要的。总的来说，给DirectDraw分配尽可能多的显示内存增强了显示性能并允许游戏和其他的DirectDraw应用程序运行更快且图象质量更好。

注意：（对Windows95/Windows98而言）当DirectDraw被初始化时，显示驱动程序的非DirectDraw特定的部分必须被通知哪一个内存正被DirectDraw所使用，这样，当DirectDraw被使用时，它能够避免写到那个内存上。这对DirectDraw的兼容性是必须的，并且在显示驱动程序的非DirectDraw特定的部分里完成。

当为DirectDraw的使用分配内存时，虚拟的显示驱动程序（VDD）内存必须被保存以交付给MS-DOS。对Windows画笔、字体、刷子等等的高速缓存也必须被保存。通过检查显示驱动程序代码的非DirectDraw特定部分来看哪处的VDD内存被分配，你可以决定什么内存可以使用。最好给Windows95/Windows98一点额外的内存，比方说32KB，确信它被分配到一个4字节的边界上，那个结果的地址被指向延伸区域的显示指针所使用（例子代码里的VIDEOMEMORY结构的fpStart成员）。

通常地，显卡将行距设置成显示的宽度以便没有内存在前缓冲的右边（概念上）浪费，这在概念底部留出一个延伸区以为其他的表面所使用。在这种状况下，由于一个指针能够引用可能存取的显示内存的整个区域，所以内存存取是直接的。

如果行距大于主表面的宽度，前缓冲之概念的右端内存被浪费掉了。它不得不作为一个单个的矩形堆被收回，而不管是否卡上的内存是线性的或矩形的。（甚至如果内存是线性的，一些显示驱动程序固定行距来加速位块传输。不是重写驱动程序，内存能够被简单的作为一个单个的堆而收回。）

3.3.2.1内存堆分配

为分配一个表面，DirectDraw彻底扫描显示内存堆以便它们能够被驱动程序指定。堆在一个VIDEOMEMORY结构里的数组被指定，DirectDraw 以数组里的VIDEOMEMORY结构的次序访问堆。VIDEOMEMORY结构设置堆特定的度量，如启动和终止内存地址和标志，这些内存地址和标志描述了堆存取和什么类型的用法为放置在这个堆上的表面所限制。DirectDraw通过子分配和重新分配内存管理堆，即是说，在每个堆的权限下通过创建和注销表面。物理的限制决定怎样设置这些属性。

当企图分配内存来响应一个面创建或恢复时，DirectDraw的堆管理器通过VIDEOMEMORY结构做两个传送。VIDEOMEMORY结构的ddsCaps成员通知DirectDraw在第一个传递上堆里的什么内存不能被使用，例如，如果堆对一个后台的缓冲器是足够大的，你能通过设置DDSCAPS_OFFSCREENPLAIN标志从第一个传递上排除子画面被分配。按这样的方法，当后台的缓冲器为页面交换保留时，另外的堆将用脱屏的普通表面填充。

你能够设置VIDEOMEMORY结构ddsCapsAlt成员来允许子画面在第二次传递上，所谈及的堆可以允许子画面，但是如果当子画面不能够在任何另外的堆里创建时。不要在ddsCapsAlt里指定DDSCAPS_OFFSCREENPLAIN标志，这允许堆为它们的最高和最好利用而被优先地使用，没有取消可选择的使用。

显存堆能够是线性或者矩形，这取决于位块转移或者一个存在的显示驱动程序的需求。VIDEOMEMORY结构的dwFlags成员被使用来指定内存分配类型。线性堆描述内存的区域，在这个区域里每个表面的行距可以是不同的；矩形的堆描述内存的区域，这里每个表面的行距是固定的。如果必要，这些堆可以在相同的显卡内混合和匹配。（看内存配置例子获得更多信息。）

象素深度变化取决显示模式。行距（有时被称为跨距或位移）是被添加的字节的数目，该字节从显存的一个列到其下面行上同样的列而得到（如图3.2所示），用8位象素，这里每个象素有一个字节跨越整个水平内存长度。

图3.2 行距

如图3.2所示，行距是用字节表示的显存的整个水平延伸。在线性内存里，它与前缓冲的宽度、系数分配限制是相同的。在矩形内存里，它是前面缓冲器的宽度加上概念上相对于它右边的剩余内存。在上面的任何一种状况下，行距是被添加的字节的数目，它被添加到一个扫描行的开始地址来到达它下面的下一个扫描行的起始地址。

3.3.3 内存配置例子

下面的部分包括内存配置的三种类型：线性、矩形和混合显存分配，每一个包括例子代码，你可以修改这些例子代码来适合你的卡的物理特性并添加到HAL来分配显存堆。

3.3.3.1线性内存分配

显存被认为是线性的，不论何时行距能够被改变来匹配表面宽度、系数分配限制（如图3.3所示）。例如，如果位块转移器仅能够找到8位的跨距以及一个31象素子画面被使用，显示的每一行需要被调节1以在一个8位的边界上分配下面的行。

图3.3 线性内存堆分配

单字分配限制能够应用于内存配置三种类型的任何一种，由于行被顺序地存储，线性内存比矩形内存普遍而更有效地使用于应用程序。任何位置能够通过沿着这条线性范围向前和向后移动而被容易地存取。

行距被象素深度、表面宽度和单字分配限制决定，如果显示是640 的8位象素隔行扫描，行距是640。如果象素是16位且显示屏是640X800，行距是1280。同样的，640宽度，32位象素屏在线性显存里有2560的行距。

图3.3给出了一个具有一个主表面和一个延伸的线性显存堆分配的一个例子。指向主表面的开始部分的指针是fpPrimary，主表面的的尺寸和多种高速缓存被添加到这里来给指针延伸区的的开始部分的指针，此开始部分被VIDEOMEMORY结构的FPstart成员所表示。被VIDEOMEMORY结构的fpEND成员所表示的终端点用通过加上存留内存的大小再减去1来计算。

VIDEOMEMORY结构保留管理显存堆的信息，这个例子在VIDEOMEMORY结构的数组里仅有一个元素，因为只有一个堆。VIDMEM_ISLINEAR是在VIDEOMEMORY结构里的dwFlags成员中的一个标志，它指示这是线性内存。

下面的伪代码给出了一个VIDEOMEMORY结构怎样为线性内存而被设置：

/*

* video memory pool usage

*/

static VIDEOMEMORY vidMem [ ]= {

{ VIDMEM_ISLINER, 0X00000000, 0X00000000,{0},{0}},

};

下面的伪代码给出了线性内存堆怎样被设置：

/*

* video memory pool information

*/

/* Caculate the number of video memory heaps */

ddHALInfo.vmiData.dwNumHeaps = sizeof(vidMem) / sizeof(vidMem[0]) ;

/* Set up pointer to first avaliable video memory after primary surface */

ddHALInfo.vmiData.pvmList = vidMem ;

/*

* remender of screen memory

*/

VideoHeapBase = ddHALInfo.vmiData.fpPrimary + dwPrimarySurfaceSize + dwCacheSize ;

VideoHeadEnd = VideoHeapBase + dwDD0ffScreenSize –1;

VidMem[0].fpStart = VideoHeapBase;

VidMem[0].fpEnd = VideoHeapEnd;

第一个可用的延伸区的开始通过加上GDI主表面的开始和主表面的大小以及Windows刷子、画笔和VDD高速缓存而计算，结果被用来在VIDEOMEMORY结构的第一个元素里设置起始点为延伸区的开始。

通过从延伸区的开始加上延伸区的尺寸再减去一就可以找到延伸区的末尾。结果被使用来设置在VIDEOMEMORY结构的第一个（仅仅在这种状况下）元素的终端点为延伸区的终端。如果多于一个堆，终端点被设置成这个堆的终端且下一个堆从这一个离开的地方开始。

3.3.3.2矩形内存分配

不论何时在一个给定的堆里的所有表面的行距被固定为一个特定的尺寸时，显存被认为是矩形的。

用矩形显存，分布是两维的，且有限定的宽度和高度，这个宽度并非一直与屏的宽度相同。由于显存必须考虑不同的显示分辨率并设计考虑，实际的水平宽度比当前正在显示器上显示的内容应该跨越一个更大的区域。

例如，即使一个屏幕应该被显示640的象素跨越，如果矩形显存是1280字节的跨越，行距是1280（而非640），行距跨越一个具有16位象素内存的1280象素水平延伸的行距是2560。（记住，行距基于将添加的字节的数目，该数目从一个扫描行的开始到下一个而得到。）为32位象素的行距是四倍于8位象素的行距，这样如果显示是1280的32位象素跨越，行距是5120。

矩形内存的主要的问题是它很少模仿辅助的数据大小，而应用程序想在其中存储数据。这样，它可能浪费显存，因此你应该在尽可能的避免它。DirectDraw允许这个空间基于先来先服务的原则而被存取。这些区域的使用有时被它们的笨拙的形状和尺寸所限制，经常地，矩形内存的残留的区域仅当空间可以存储小画面时才是有用的。

一个矩形堆能够与可用的内存的一个连续区域一样的大，但是由于它的尺寸以X、Y坐标来度量，它不能够是L形状的。如果矩形堆不足够高和足够宽来保留一个主平面，它不能够是一个后缓冲。如果主表面的行距不等于主表面的显示宽度，一个显示的概念右边内存的矩形块被残留下来了（堆1在图3.4里），这个块与行距减去显示的宽度一样。如果显示驱动程序假定一个固定的行距，右端的残留内存也能够出现在线性卡里。矩形的或线性的内存也可以在主表面下留存下来（但是不在这个例子中）。

图3.4 矩形内存分配

在图3.4里，矩形堆的起始点（被VIDEOMEMORY结构的FPstart成员表示）通过加上主表面的宽度和主表面的起始地址来计算。宽度和高度也被计算来给矩形堆维数，如果内存留存在Windows高速缓存下的话，一个堆也能够在此创建。

下面的伪代码给出了一个VIDEOMEMORY结构怎样为矩形内存而被设置：

/*

* video memory pool usage

*/

static VIDEOMEMORY vidMem [ ] = {

{ VIDMEM_ISRECTANGULAR, 0X00000000, 0X00000000,{0},{0}},

};

矩形内存的代码和线性内存的代码仅有的区别是VIDMEM_ISRECTAGULAR标志，该标志表示这是一个矩形内存。

下面的伪代码给出了矩形内存堆怎样被设置：

/*

* video memory pool information

*/

/* Set up pointer to first avaliable video memory after primary surface */

ddHALInfo.vmiData.pvmList = vidMem ;

/* this is set to zero because there may only be one heap depending on the pitch */

ddHALInfo.vmiData.dwNumHeaps = 0 ;

/*

* compute the Pitch here ...

*/

VidMem[0].fpStart = ddHALInfo.vmiData.fpPrimary + dwPrimarySurfaceWidth ;

VidMem[0].dwWidth = dwPitch – dwPrimarySurfaceWidth ;

VidMem[0].dwHeight = dwPrimarySurfaceHeight ;

VidMem[0].ddsCaps.dwCaps = 0 ; // surface has no use restrictions

内存堆起始点被设置成主平面的起始地址加上主平面的宽度。宽度由行距减去主表面的宽度而决定，高度设置成主表面的高度。主表面能力被设置成零，表明没有强加的表面使用限定（这样，表面能够被子画面或者任何别的类型的表面使用）。

3.3.3.3混合内存分配

只要硬件允许，线性的和矩形的内存堆能够在任何方式下被混合和匹配。例如，如果一个前缓冲有一个固定的行距，DirectDraw-capable驱动程序能够分配一个矩形堆在它的右边，如图3.5所示，如果内存也被残留在主表面的下面，这个区域能够被做成一个可为后缓冲使用的线性堆（也就是说，如果它足够大）。

图3.5 混合内存分配

图3.5给出了在主表面（堆1）之下的内存的一个线性条和内存的一个矩形条，该矩形条被DirectDraw右面的主表面（堆2）收回。

下面的伪代码给出了一个VIDEOMEMORY结构怎样为线性和矩形内存的混合而被设置：

/*

* video memory pool usage

*/

static VIDEOMEMORY vidMem [ ] = {

{ VIDMEM_ISRECTANGULAR, 0X00000000, 0X00000000,{0},{0}},

{ VIDMEM_ISLINER, 0X00000000, 0X00000000,{0},{0}},

};

在这个例子中，显存的两个区域可以分配。主表面的右边区域必须是矩形，并且由VIDMEM_ISRECTANGULAR标志指示。主表面下的区域是线性的，并且由VIDMEM_ISLINEAR标志指示。

以下的伪代码显示了混合的矩形和线性内存堆如何被设置：

/*

* video memory pool information

*/

/* Set up pointer to first avaliable video memory after primary surface */

ddHALInfo.vmiData.pvmList = vidMem ;

/* how many heaps are there */

ddHALInfo.vimData.dwNumHeaps = 2 ;

/* The linear piece: */

/*

* remender of screen memory

*/

VideoHeapBase = ddHALInfo.vmiData.fpPrimary + dwPrimarySurfaceSize + dwCacheSize ;

VideoHeadEnd = VideoHeapBase + dwDDoffScreenSize –1;

VidMem[0].fpStart = VideoHeapBase;

VidMem[0].fpEnd = VideoHeapEnd;

/* The rectangular piece */

/* Set up the pointer to the next avaliable video memory */

ddHALInfo.vmiData.pvmList = vidMem [1];

/*

* compute the Pitch here ...

*/

VidMem[0].fpStart = ddHALInfo.vmiData.fpPrimary + dwPrimarySurfaceWidth ;

VidMem[0].dwWidth = dwPitch – dwPrimarySurfaceWidth ;

VidMem[0].dwHeight = dwPrimarySurfaceHeight ;

VidMem[0].ddsCaps.dwCaps = 0 ; // surface has no use restrictions

n 一个线性内存堆通过在主表面之下确定延伸区的起始和末端点而设置，它被VIDEOMEMORY结构的Fpstart和fpEnd成员所表示（vidMem[0]）。

n 矩形条通过使用主表面之下的延伸区的起始点而设置，它被矩形VIDEOMEMORY结构的Fpstart成员所表示（vidMem[1]）；且这个区域的高度和宽度分别由dwWidth和dwHeight表示。行距（dwPitch成员）必须在矩形条被设置之前枚举，除过行距是取代第一个元素的VIDEOMEMORY结构的第二个元素，这与前面的矩形的例子是相同的。

每一个堆需要一个新的VIDEOMEMORY结构。

在一些情况下，交换寄存器仅能够处理256KB的边界。在这些实例里，一个小堆能够用尽高速缓存底端和后缓冲的起始之间的空间，且允许后缓冲开始于一个256KB的边界。本例没有给出，但是你能通过添加另外的元素到VIDEOMEMORY结构和设置起始点刚好超出高速缓存、末端点刚好在256KB边界之前而执行它。这样的一个堆应该用DDSCAPS_BACKBUFFER标志以便当堆管理器找寻一个后缓冲时它能够被跳过，这个后缓冲堆（排列的一个）也应该用DDSCAPS_OFFSCREENPLAIN标志从使用它来保留画面和纹理直到没有另外的内存在另外的堆里为脱屏平表面可用。

3.3.3.4在Windows95/98上寻址的分组转换内存

Windows2000驱动程序无须支持分组转换的内存，这个部分仅仅应用于Windows95/Windows98驱动程序。

如果线性的内存寻址被支持，就使用它。如果硬件或驱动程序仅能够使用分组转换，你必须使用虚拟的平面边框缓冲器VflatD设备。

VflatD为一个显示驱动程序帮助管理边框缓冲器，为了克服寻址多于显存的64 KB的问题，VflatD为使用分组转换技术的显卡虚拟缓冲器，它通过映射显存到内存地址的一个更小的块来完成此事。其后，无论何时一个缺页故障在绘图过程中发生，VflatD调用分组转换程序，指令它映射一个显示数据的块到物理的边框缓冲器。

如果分组转换被使用，DirectDraw从显示驱动程序得到这个信息并设置DDCAPS_BANKSWITCHED标志，这表明显存通过一个内存窗口存在于硬件，内存用VflatD被处理而出现平面。

用分组转换内存，仅仅内存的一个小窗口（通常64 KB）在任何给定的时间对应用程序是可见的，当应用程序到达窗口的末端时，一个缺页故障发生。VflatD捕捉缺页故障并移动内存指针，这样一个新的窗口对应用程序可见，如图3.6所示。

图3.6 分组转换内存

如图3.6所说明的，分组转换内存一次暴露一个可见内存64 KB的窗口，确信堆起始地址至少是双单字排列的，由于不这样做的话，将引起系统崩溃和执行变慢。如果一个分组转换出现在非双单字排列的内存的字节片内，VflatD就挂起了系统，这是由于被执行的处理器指令不能够打碎成更小的片的缘故。例如，指令可能在一个四字节的内存增长里被读。一次仅仅有内存的一页在窗口里，这样一个跨越一页边界的指令的四字节字节片将引起缺省的处理者没完没了的在两个内存窗口之间来回进行分组转换。也是这个道理，当数据被正常地排列时，标准拷贝操作达到三倍之快。

写排列好地数据意味着目标地址是四字节的排列（PTR==PTR & 0xFFFFFFFC）。

如下面的伪代码所示的，单字排列限制能够用一个简单的调整被处理：

/*

*required alignments of the scan lines for each kind of memory

*/

ddHALInfo.vimData.dwOffscreenAlign = AlignTbl[ wBpp >> 2 ];

ddHALInfo.vimData.dwOverlayAlign = AlignTbl[ wBpp >> 2 ];

ddHALInfo.vimData.dwTextureAlign = AlignTbl[ wBpp >> 2 ];

ddHALInfo.vimData.dwAlphaAlign = AlignTbl[ wBpp >> 2 ];

ddHALInfo.vimData.dwZBufferAlign = AlignTbl[ wBpp >> 2 ];

注意：前述的例子仅仅给出了行距排列；它们没有控制表面内存的起始排列。

3.3.3.4.1使用VflatD

VflatD 是一个页转换的动态库，当一个分组边界被跨越时，该动态库改变内部的指针，除非在你的图形卡上的内存是分组转换的，VflatD是不必要的。当一个分组边界被跨越时，一个缺页故障触发了一个缺省处理者调用VflatD来做一个分组转换，这将暴露一个新的内存页。即使一个新的分组已经被转移，内存作为线性地址的空间出现在应用程序里。内存从一个被VflatD内存管理器追踪的新起始位置简单地继续着。正常情况下，这发生在后台，而没有错误。

为使用VflatD，驱动程序必须：

1. 调用VflatD_Query，它核实VflatD的正确版本已被装载。

2. 调用VflatD_Create_Virtual_Frame_Buffer，它注册驱动程序，返回一个选择器识别合法的边框缓冲器，并指定一个分组转换程序给VflatD。

3. 调用VflatD_Begin_Linear_Access，它提供一个线性地址的边框缓冲器的存取，这个功能应该当用DDCreateDriverObject初始化DirectDraw时被使用。

4. 调用VflatD_End_Linear_Access，当用HALDestroyDriver推出退出DirectDraw时使用这个功能。

分组转换程序必须使用32位的代码编写，且长度一定不能超过100个字节，当程序被调用时，EAX寄存器标识将要交换到物理帧中的新分组。除过EAX和EDX以外，程序必须保存所有的寄存器并且不返回值。（也就是说，EAX和EDX必须在出口和入口是相同的。）

VflatD_Begin_Linear_Access 启动了线性存取给边框缓冲器，边框缓冲器通过它的线性地址能够被存取。（就缺省而言，边框缓冲器必须用缺省的选择器被存取。）边框缓冲器留驻在线性存取模式里直到VflatD_End_Linear_Access被调用，该调用能够被嵌套。

如果成功的话，VflatD在EAX里返回存取记数并清除进位标志。存取记数是随每次调用VflatD_Begin_Linear_Access时增加的，而随每次调用VflatD_End_Linear_Access时是减小的。当存取记数为零时，线性存取模式被关闭。

为关于VflatD和VflatD功能的更多信息，看Windows95/Windows98 DDK的参考部分。

3.3.4 交换

主表面是正被读的内存区来绘制当前正被显示的屏，如果一个主表面有一个或更多附着的后备缓冲器，它是一个可交换的表面。

交换结构被用于DirectDraw里的页交换，就概念上的来说，它们能够被认为是由表面构成的链接的列表，前缓冲是“可见的”，后缓冲和所有附着的可交换的表面必须与前缓冲是同等的大小和象素深度。大多数现代的图形卡为高分辨率的模式里可交换的前面的和后缓冲有足够的内存。

所有的表面类型在DirectDraw里被交换；页交换是普遍的特殊情况。例如，交换没有限定在卡上的主表面，该卡支持重叠或者在3-D可能的显卡上有纹理内存，在这些状况下，重叠和纹理用相同的驱动程序入口点能够与主表面有相同的方法交换。

没有被用来交换的主表面可以有任何维数且可以存储不可交换的对象如图象数据，图象数据也能够被存在系统内存里，但是在此种状况下，由于硬件位块转移不能够正常的到达系统内存来转移图象数据，DirectDraw应该使用硬件仿真层（HEL）。一些卡允许硬件位块转移以便有系统内存的直接存取（DMA），这样DirectDraw为DMA执行一个检查。

图3.7说明了在两个可交换的表面之间的关系。

图3.7交换

如果一个前缓冲有一个或更多的后缓冲附着，如图3.7所示，它是一个可交换的表面。后缓冲和所有的附着的可交换的表面必须与前缓冲有同样的大小和象素深度，一个后缓冲表面使用交换变成了主表面。一个交换仅改变了一个指针以便它指向一个不同的可交换的表面，因此显示了新的表面，前缓冲（其不再是主表面）将变成可存取的且可以让新的数据被写到其上。

正确地被执行，交换解决了大多数屏闪烁问题，着色一个没被显示的表面的能力允许游戏和视频回放中平滑、无撕裂动画。

3.3.4.1撕裂

象在交换部分里讨论的一样，一个交换本质上改变了一个内存指针，以便它指向显存的一个新的区域。（参见Permedia2例子代码。）被转移走的表面必须在应用程序能够锁定、位块转移或改变内存之前完成显示，否则将导致一个撕裂（如图3.8所示）。如果在交换过程当中正被交换的表面正在进行数据写入，一个撕裂也许可能发生，撕裂是普遍的且可能在一张图象正被同时绘制和显示的任何时候发生，更快的帧速率没有解决这个问题。由于主表面、重叠和纹理是所有的DirectDraw表面，它们都能够阻止撕裂的以相同方法被交换。

图3.8撕裂

当一个页交换或位块转移发生在错误的时间时，一个撕裂发生。如果一个页交换发生在当监视器扫描行处于显示一个表面的中央时，如图3.8中的虚线指出的一样，一个撕裂发生了。撕裂能够通过仅在整个表面被显示之后定时交换而被避免（如图3.8中下面的例子）。当位块转移到一个正在被显示过程的表面时一个撕裂也能够发生。

3.3.4.2三倍的缓冲

增加能够拥有一个主表面的缓冲器的数量增强了显示性能，有至少三个可交换的表面是更好的。（一些游戏使用五个或更多。）当仅仅两个表面和一个页交换发生时，显示被延迟直到监视器的垂直回扫完成为止。延迟是必要的以确保后缓冲完成被显示之前没有被写入。用三倍的缓冲，第三个表面一直是可写的；由于它是一个后缓冲且可以立即绘制（如图3.9所示）。在一个没有使用子画面内存的游戏里，使用了三倍的缓冲的3-D着色比两倍缓冲快20-30%。

图3.9三倍的缓冲

除过现在三个缓冲器被使用外，图3.9中的交换结构与图3.8中的交换结构相同。由于一个缓冲器几乎一直是可写的（由于它没有包含在一个交换里），在允许后缓冲被重新写入之前，驱动程序不必等待显示扫描完成。

下面是在三倍缓冲的系统里的交换和位块转移一个简短说明，使用了图3.9中的标签。例子开始于正被显示的表面象素内存11，这是一个被前缓冲（用Windows2000 DDK提供的例子代码里的fpVidMem）所指向的主表面。在某些点，在象素内存22上位块转移到表面是期望的。由于表面的fpVidMem点开始于11（而非22）且交换状态为false（没有交换发生在被请求的表面），位块转移能够进行。驱动程序锁定表面，写入到其上，然后解锁，为了显示表面，一个交换必须发生。DirectDraw前缓冲对象现在能够改变fpVidMem（显示内存指针）来使表面在22主表面。由于没有交换待定，显示指针被交换（看图3.9的下半部分），且交换状态被设置为TRUE。前缓冲现在指向表面象素内存22，后缓冲指向表面象素内存33，且第三个缓冲器对象指向表面象素内存11（老的主表面）。不象两倍缓冲，此时DirectDraw可以自由的写入到后缓冲，换句话说，由于没有交换待定，DirectDraw能够写入到表面象素内存33。这个循环的交换过程继续为使用DirectDraw的应用程序无尽的提供平滑的动画、更快的播放和视频回放。

3.3.4.3定时一个交换

如果一个交换寄存器是可用的且知道扫描行，定时一个交换是容易的。简单地检查交换寄存器来看是否最后一个交换已经发生，然后在做交换之前确信扫描行不在垂直的空白区。然而，如果扫描行和交换寄存器不可用，你将必须使用下面的一个或更多算法。

n 如果硬件没有一个位来检查扫描行是否已经通过刷新循环，你应该使用多于一个方法来定时交换，可替换的方法基于消逝的时间。如果自上一次交换，有适当的时间绘图整个表面，交换是可行的。所需的时间（除非监视器更新速率已知）决定于驱动程序初始化时和通过查询直到显示处于垂直的同步，然后查询直到它不处于垂直的同步之模式改变上。这反复做20次，且线执行优先权设置为最大，结果被20所除来给出显示的持续时间，这是允许存取后台的缓冲器之前最大的时间等待。（如果你已经知道了显示的持续时间，你不需要枚举它。）在大多数状况下，一个更短的时期在做一个交换之前被需求，但是显示的持续时间被用为一个退却是由于它是完全可靠的。

n 如果仅仅扫描行的行数目可行，最快的方法是记录时间和扫描行，当扫描行少于它从前的数目时，它已经结束了一个更新的循环。例如，如果第一次扫描行在300行被查询，下一次它在50行被查询时，整个表面至少已经被显示过一次。然而，在所有的显卡上扫描行并非完全是可靠的，在一些状况下，通过给出两个不同的数字的一半，一个寄存器同时能够被读出和写入，在这些情况下，寄存器需要被读两次来核实它没有报告一个错误的数字。在一些状况下，当扫描行经过水平同步时，它可能是不准确的。当扫描行处于水平空白时，表明扫描行是否在当前正被显示的标志常常会返回一个为false的否定结果。

n 如果扫描行的行数目不可得到，你也许能够校验扫描行已经从显示经过垂直空白并返回显示。通过跟踪何时扫描行传递进和传递出显示，你能够确定何时一个整个的刷新循环已经被完成。检查是否扫描行在显示或在垂直空白里的能力应该在所有的显卡上可用，但是扫描行也许不是这样。（在任何状况下，显示的持续时间仍旧是退却。）

一定的调整也许对最后的闪烁位远离显示是需要的，如确信在地址被读之后没有计算发生（也就是说，由于地址在双单字边界上，向右转换两个距离来得到真正的地址，等等）。为避免撕裂，确信扫描行不在垂直的空白区且刚好在垂直的同步。在一个驱动程序状况下，这个问题每7分钟引起一个撕裂，所以要小心。你可以通过确信扫描行在写到交换寄存器之前处于显示状态从而容易地补救这些问题。

尽管显卡基于最初的IBM视频图形卡（VGA）规格，超级VGA有相当地变化。（看程序员的EGA、VGA和超级VGA卡指南，Richard F.Ferraro，Addison-Wesley，1990）一些卡提供非直接的方法决定是否扫描行处于垂直的空白区或者是否它甚至在显示当中。当竞争以满足高级显示能力增长的需求时这些不足将变得更为重要，同时，你必须创造性的补偿这些缺陷。

一旦可以交换，对欲显示的下一个表面的参考被装载到交换寄存器里。什么也不会发生直到扫描行得到过去的垂直的同步且放回到显示区为止，然后，交换寄存器被读一次。如果别的东西在那个时间之前被存储在交换寄存器里或者如果在它正被读的同时表面正被写入，将会发生一个撕裂。一旦寄存器被读，当表面正仍旧被绘图时，它不应该被锁定或者写入。

检查是否寄存器已经被读在大多数较新的显示卡里只是检查一个标志的事。在更旧的卡里，一个硬件中断常常为这个目的使用。当某个点跨越在交换寄存器的那个读时，任何钩住那个中断的函数都将被调用。问题是在一些较新的硬件里这个中断可能为另外的目的所使用。

3.3.4.4交换间隔

开始于DirectX6.0，DirectDraw允许一个应用程序决定何时一个交换命令被执行。这些取决于硬件能力，你能够将对这个新特性的支持添加到现存的驱动程序中。

下面的术语被用于描述相关于应用程序的Flip调用的定时：

Posted

应用程序调用Flip和DirectDraw调用驱动程序的DdFlip入口点的时间。

Retired

硬件开始从新表面显示的时间段。

在DirectDraw的以前版本里，交换紧随当它们被登记时在或者接近垂直同步上一直是退役的。用DirectX6.0和后面的版本，应用程序能够指定交换立即是退役的---也就是说，正好当被登记的或者在它被登记之后的某套垂直同步的数目。DirectX6.0指定了两个新的能力位（在DDNTCORECAPS结构里的DDCAPS2_FLIPNOVSYNC和DDCAPS2_FLIPINTERVAL）和四个新的标志（在DD_FLIPDATA结构里的DDFLIP_NOVSYNC、DDFLIP_INTERVAL2、DDFLIP_INTERVAL3和DDFLIP_INTERVAL4）来使这些特征可能。

如果驱动程序设置DDCAPS2_FLIPNOVSYNC，它将在DD_FLIPDATA结构的成员里接收到DDFLIP_NOVSYNC标志，DDCAPS2_FLIPNOVSYNC标志表明只要交换一被登记，它应该是退役的。然而在这种状况下，你的硬件必须能够转换缓冲，在至少每个扫描行的基础上。如果直到下一个垂直的同步显示实际上没有退役交换，对驱动程序来说为DDCAPS2_FLIPNOVSYNC接受支持是不合法的，即使如果驱动程序立即返回。

在DDFLIP_INTERVAL2、DDFLIP_INTERVAL3和DDFLIP_INTERVAL4终端的数目表示了硬件在退役一个被登记的交换之前应该等待多少个垂直的同步，举例来说，DDFLIP_INTERVAL2意味着硬件应该记数两个垂直的同步，然后在或者接近第二个垂直的同步上退出交换。

如果你的驱动程序暴露DDCAPS2_FLIPINTERVAL，DirectDraw放置垂直同步的数目，通过垂直同步可延迟一个交换而进入DD_FLIPDATA结构的dwFlags成员的大多数重要的字节。由于DDFLIP_INTERVAL2、DDFLIP_INTERVAL3和DDFLIP_INTERVAL4标志被定义使这个为真，驱动程序不应该作为位标志对待这三个标志。同样，如果驱动暴露DDCAPS2_FLIPINTERVAL，DirectDraw确保dwFlags成员的大多数重要的字节被按照当DDFLIP_INTERVAL2、DDFLIP_INTERVAL3和DDFLIP_INTERVAL4标志没有被设置时而设置。在被登记的交换之后，自从一个交换调用的缺省行为将在或者接近第一个垂直的同步上退出交换时，DDFLIP_NOVSYNC引起一个将被放置在大多数重要的字节里零数字，大多数重要的字节的缺省值变为1。

自从DirectX1.0以来，当一个交换待定时（也就是说，当交换已经被登记但还没有退役时），驱动程序已经被要求返回DDERR_WASSTILLDRAWING，这个需求已经为交换间隔而被扩展了。既然当DDFLIP_NOVSYNC交换被登记时它们退役了，驱动程序将不会作为这样的一个交换（既然它将不会是待定的）的结果返回DDERR_WASSTILLDRAWING。相反，使用DDFLIP_INTERVAL2、DDFLIP_INTERVAL3和DDFLIP_INTERVAL4标志中的一个意味着驱动程序必须为一个长的时间段返回DDERR_WASSTILLDRAWING，这是由于在登记和退役的这段时间里一个交换被扩展了。

DirectDraw没有排除这些带有重叠表面的标志的使用，但是甚至如果驱动程序确实设置了DDCAPS2_FLIPINTERVAL或DDCAPS2_FLIPNOVSYNC能力位，驱动程序不需要考虑它们。如果驱动程序有这种能力的话，它们可能为重叠选择考虑这些标志，但是应用程序不可能利用这些特征。

注意：DDFLIP_INTERVAL2、DDFLIP_INTERVAL3和DDFLIP_INTERVAL4标志被用来利用硬件能力。驱动程序不应该通过在驱动程序里循环直到交换能够因请求而退役而尝试仿真这些标志。因为当调用一个驱动程序时，重要的操作系统互斥量是保留的，这样的一个实现可能严重地损害系统性能。

3.3.4.5重叠支持

DirectDraw也支持重叠，一个重叠表面是一个可被显示在主表面的顶端而没有改变其下表面的物理位的表面。用一个重叠，寄存器被设置以在一个包含重叠表面的主表面上定义一个矩形。数字到模拟的转换器（DAC）改变了矩形的位置。扫描行在主表面内存里读数据直到它到达为重叠留下的矩形为止。它从重叠表面读出直到在重叠里的那行被完成，然后，它继续最初的主表面图象，从主表面到重叠及其后面的这个转换发生在扫描行的每一次传递，并且继续直到重叠被完全地显示为止。

重叠表面能够有与主表面不同的象素深度，例如，当每个象素（bpp）8位时，主表面可能看起来很好，一个视频限幅可能需要16来可接受地显示，象素深度在主表面和重叠之间无缝地转换。为了获得有关具有DirectDraw的重叠，看DirectX的视频端口扩展。

重叠交换与主表面相同，DirectDraw表面对象交换指针，这样当扫描行到达包围重叠的矩形时，新的重叠表面被读出。在“定时交换”里描述的同样的交换算法以阻止撕裂。

3.3.4.6纹理支持

在3-D空间里交换一个纹理与其他任何表面是相同的，一个纹理恰好是一个平面的图象，该平面的图象有位设置来指定它能够被转移（纹理映射的）到一个3-D的表面上，一个纹理能够被映射到一个3-D的表面上且动作能够被通过页交换纹理光滑地着色。交换等待着色器来完成读出（象等待扫描行一样），如果交换驱动程序支持纹理，它必须能够恰当地记忆和处理它们。看第四章的Direct3D纹理管理获得纹理的更多细节。

3.3.5 使用图阵

如果一个位块转移发生在一个表面内，且资源和目标区交迭，必须决定适当的方向来避免在部分资源被拷贝之前而被重叠，这恰好能够用位于表面相对角的两个潜在的起始点来完成。所有的位块转移引擎的需求是每个图象的位置和维数。

你应该做每件可能的事情来加速实际的位块转移，例如，复制代码的各个部分来避免IF语句则可能使驱动程序运行更快。也许这个技术的最佳实现是将代码放置在一个宏代码里且在不同的位置使用代码而不是做函数调用，这是最优化速度的绝对合适的位置。参见在线DDK的DdBlit。

3.3.5.1透明的位块转移

在一个透明的位块转移里，一个颜色键正常地指定了将不被移去的颜色。资源颜色键与使用在动画里的蓝屏是相似的，颜色与每个象素做比较，如果它们匹配的话，象素不会被拷贝，如果它们不匹配的话，象素则被拷贝。DirectDraw也支持范围内的颜色键。

在一些情况下，一个透明的位块转移可能仅仅被硬件部分地支持，这可能仍旧比在软件里执行它要更快。在这些情况下你应该设置DDCAPS_COLORKEYHWASSIST标志。

一个部分地支持透明的位块转移的例子是一个要求位屏蔽而不是颜色键的显卡。在此情况下，不是与颜色键比较每个象素来决定是否拷贝它，而是一个单色屏蔽被构造。也就是说，所有的象素与颜色键比较且整个表面转换成一个位屏蔽（通常每个字节一位，取决于颜色深度）。当使用DirectDraw时，在表面未锁定时完成这个过程。

一旦alpha屏蔽被构造，它与源表面比较。没有设置在alpha屏蔽上的每件事物被拷贝到目标表面这实现了与源颜色键相同的作用，但是需要一个屏蔽被首先构造而不是在同时比较和拷贝。屏蔽必须被构造颜色键设置的任何次数，由于在那时一个颜色键重载可被指定，无论何时一个位块转移发生，它也必须被检查。当应用程序的功能被调用时，检查颜色键重载（传递给位块转移的唯一的颜色键）与设置在表面上的颜色键相同。如果它们是相同的，将不是一个真正的重载且屏蔽无须构造，如果它们是不同的，屏蔽确实需要构造。（驱动程序的功能一直认为颜色键为一个重载。）

3.3.5.2限幅列表

限幅列表从来没有传递给Windows NT/Windows2000上的驱动程序，DD_BLTDATA的IsClipped成员一直为FALSE，且限幅列表一直为NULL。

注意：对Windows 95/Windows98来说，为限幅列表里的每个矩形，DirectDraw支持传递一个整个的限幅列表给驱动程序（如果驱动程序支持这个），而不是多次调用驱动程序。如果驱动程序能够获得一个矩形列表并在一个FOR循环里全部位块转移它们，驱动程序应该在DDNTCORECAPS结构里设置DDCAPS_CANCLIPSTRETCHED和DDCAPS_CANCLIP标志。

在DdBlt里，如果一个限幅器被附着到目标表面上，且限幅器表明目标矩形是阻塞的，DirectDraw打破位块转移到一个简单的阻塞矩形列表里。如果驱动程序设置DDCAPS_CANCLIPSTRETCHED和DDCAPS_CANCLIP标志，调用驱动程序一次，仅仅期望它运行通过在DD_BLTDATA结构里描述的矩形列表。如果驱动程序没有设置这些标志，DirectDraw为每个未被阻塞的矩形调用驱动程序一次，并设置DD_BLTDATA以便它每次仅包含一个矩形。

3.3.5.3颜色填充和图案填充

颜色填充，如被一个矩形描述的一样，用一个特定的颜色填充屏幕的一个区域。在大多数卡上，显存的地址、矩形的维数和颜色都是需要的。一些卡需要起始和终止X、Y坐标，知道Windows会自动地落到这些坐标的最后一行。例如，当它们从0到640计数时，Windows落到640行。

一些卡使用一个图案填充，它们能够完成一个颜色填充同样的事情。象素（图案）的8X8区域弥补了希望的颜色，且那个图案被用来填充指定的区域，图案设置等同希望的颜色且与一个颜色填充是相同的。一个图案填充获得四个分别的颜色，这些颜色可以被混合，这样缩减了必要的指令的数目。

3.3.5.4 DirectDraw和颜色空间转换

DirectDraw允许表面被创建和以YUV格式存储，四个字符代码（FourCC）表示了什么样的颜色空间转换正被使用，然后，在重叠过程中，图象转换为16位RGB。与每个象素（bpp）16位有相同的密度，YUV 4:2:2是管用的，但是颜色可信度更好。图象能够如YUV一样写入且如RGB一样进入显示内存，但是当它在显存之外被读出时，正常地转换被执行以便压缩可被维护，这样节省了显存并加速了回放。对Windows2000来说，一些YUV格式（UYVY和YUY2，都归到4:2:2类）被仿真，但是仅仅是当作为纹理使用时。注意到未被支持的YUV格式表面在系统内存里是不能够创建的。

三个普通的YUV颜色空间（它有许多类型）是：

n 4:2:2（标准的电视出于这个类型）

n 4:1:1（更多压缩的）

n 4:4:4（类似于RGB）

今天多种这些颜色空间和其他的YUV格式在用，总结这些超出了本文档的范围，但是FOURCC信息的一个极好的资源可在www.webartz.com/fourcc/上找到。

3.3.6 假定状态信息的驱动程序

当大多数显示驱动程序做一个操作时，它设置位块转移的状态，然而，一些显示驱动程序期望位块转移处于一个为人所知的状态。例如，一些显示驱动程序假定起点被设置来做一个标准的位块转移而不是一个透明的位块转移，诸如此类。在这些情况下，在DirectDraw使用过状态之后，状态必须被重设。

举个例子，为源和目标表面或者用一个固定的起点或者用一个单个的起点，两个表面之间的位块转移能够被实现。如果显示驱动程序期望起点保持不变且DirectDraw改变它来存取一个次要的表面，当操作完成之后，老的指针必须被保留和存储。

如果当操作正被这样执行的时候这迫使DirectDraw等待，以便它能够恢复寄存器到它们从前的状态时，性能将受损。这是由于DirectDraw的速度是异步的。

在这些状况下你必须小心来最小化显示状态的改变，在这个场景下移动起点也浪费堆栈上的空间，否则的话这个空间将为传递参数而用。

3.3.7 打开显卡并在Windows95/ Windows 98上改变模式

下面的功能被用来打开显卡并在Windows95/ Windows 98上改变它的模式：

n Enable

n ReEnable

3.3.7.1 Enable

UINT FAR PASCAL _loadds Enable (

LPVOID lpDevice ,

UINT style ,

LPSTR lpDeviceType ,

LPSTR lpOutPut ,

LPVOID lpStuff )

Enable 被调用来打开显卡，或者通过ReEnable它可被调用来改变显示模式。Enable是显示驱动程序的16位部分里的一个功能。（关于Enable的更多信息，看Windows95/ Windows 98 DDK文档的图形设备接口参考部分。）

3.3.7.2 ReEnable

UINT FAR PASCAL _loadds ReEnable (

DIBENGINE FAR * lpPDevice,

GDIINFO FAR * lpGDIInfo )

{

DPF(“ReEnable”);

If ( Enable(lpPDevice,EnableDevice,NULL,NULL,NULL))

{

Enable( lpGDIInfo,InquireInfo,NULL,NULL,NULL);

Return TRUE;

}

return FALSE;

}

每次当显卡模式改变时ReEnable被调用，依次的，ReEnable用参数调用Enable来重新初始化驱动程序对象。取决于当它被调用时传递给它的参数，Enable可以或者打开显卡或者改变显示模式。这样，当Enable通过ReEnable（这是ReEnable所做的所有的事情）被调用时，模式被改变但是没有另外的对象结构产生。在Enable里DDCreateDriverObject被调用，但是bReset参数被设置为1，这正好重设了模式。

如果成功，ReEnable返回TRUE，否则返回FALSE。

3.4 DirectDraw驱动程序初始化

在Windows 95/98和Windows NT上，仅仅当一个应用程序请求时，才检索驱动程序信息。换句话说，为响应一个DirectDraw应用程序的请求而创建一个DirectDraw对象实例，图形引擎调用驱动程序函数来初始化一个DirectDraw驱动程序。

从Windows 2000开始，这个顺序在引导时及每个模式改变之后被执行，这有一个副作用，在Windows 95/Windows 98上，驱动程序典型地有两个操作模式：GDI模式和DirectDraw模式。如果DirectDraw运行，它不会允许GDI高速缓存位图等等，相反，它给DirectDraw（和当在GDI模式里的viceversa）所有的内存，这个行为会损害窗口化的应用程序（如使用DirectX的Web页）。这样，在Windows 2000上，GDI和DirectDraw对相互协作内存如何被使用是必须的。打包在Windows 2000 DDK里的Permedia2和S3 Virge例子驱动程序都是怎样实现这个的极好的例子。

驱动程序初始化顺序通过调用下面的函数而得到：

n DrvGetDirectDrawInfo，检索关于硬件能力的信息，调用这个功能两次：

n 第一次调用决定现存堆的大小和驱动程序支持的FOURCCs的数目。GDI为pvmList和pdwFourCC参数传递NULL，驱动程序应该初始化并仅仅返回pdwNumHeaps和pdwNumFourCC参数。

n 在GDI分配显存和FOURCC内存之后，第二次调用被进行，显存和FOURCC内存基于在pdwNumHeaps和pdwNumFourCC参数里从第一次调用所返回的值。在第二次调用里，驱动程序应该初始化并返回pdwNumHeaps、pvmList、pdwNumFourCC和pdwFourCC参数。

GDI分配和零初始化pHalInfo指向的DD_HALINFO结构，DrvGetDirectDrawInfo功能应该用驱动程序特定的信息在DD_HALINFO结构的相关成员里填充：

n 驱动程序应该初始化VIDEOMEMORYINFO结构的适当成员来描述显存的总体格式。看显存部分。

n 驱动程序应该初始化DDNTCORECAPS结构的适当成员来描述驱动程序的DirectDraw内核能力。

n 如果驱动程序支持DirectX特征的任何一种，该特征通过发送一个GUID给驱动程序的DdGetDriverInfo回调而被查询，驱动程序必须初始化GetDriverInfo成员来指向驱动程序的DdGetDriverInfo回调并在dwFlags里设置DDHALINFO_GETDRIVERINFOSET位。

n 驱动程序必须设置dwSize成DD_HALINFO结构的字节大小。

n 运行时用DrvEnableDirectDraw来使DirectDraw硬件可用，并决定一些驱动程序的回调支持。GDI分配和零初始化DD_CALLBACKS、DD_SURFACECALLBAKS和DD_PALETTECALLBACKS参数结构，驱动程序应该为这些它所执行的回调中的每一个做下面的事情：

n 设置适当结构的对应成员来指向回调。

n 在适当结构的dwFlags成员里设置对应的DDHAL_XXX_XXX位。

一个驱动程序的DrvEnableDirectDraw实现也能够致力于硬件资源，如仅仅为DirectDraw使用的显存。

如果它不是NULL，GetDriverInfo回调被驱动程序的DrvGetDirectDrawInfo返回到DD_HALINFO结构里。GDI分配和零初始化DD_GETDRIVERINFODATA并为描述在DdGetDriverInfo参考部分里的每个GUID调用DdGetDriverInfo，所有的GUID在ddrawint.h里定义。

锁定表面内存（为整个表面或者一个表面的部分）确保一个应用程序和硬件不能够在同时获得表面内存的存取，这阻止了当一个应用程序写到内存表面时的错误发生。此外，一个应用程序不能够分页交换直到表面内存解锁。

3.5 DirectX视频端口扩展

具有硬件视频端口的设备的驱动程序开发者应该可以执行DirectX的VPE。硬件视频端口致力于视频设备之间的连接，通常在一个硬件动画专家组（MPEG）设备或者国家电视标准委员会（NTSC）的译码器和视频卡。这个固定的连接用视频数据传送水平同步（H-sync）和垂直同步（V-sync）信息，硬件视频端口和重叠可以使用这个同步信息自动地在多个缓冲器之间交换，当重叠显示另外一个时写入到一个表面上。这允许自由撕裂视频而没有加重应用程序的负担。

DirectDraw VPE允许客户---典型地是微软的DirectShow---协商在MPEG或NTSC译码器和硬件视频端口之间连接。VPE也允许客户在视频流里控制效果，包括裁剪，定标等等。一个VPE实现应该仅仅做客户请求的事情：例如，它应该仅当客户请求裁剪时裁剪。

VPE对象没有控制视频译码器是由于它提供它自己的服务，DirectDraw也没有控制视频源：它超出了VPE的范围。宁可，一个DirectDraw VPE对象代表了硬件视频端口自己，它监视进入的信号并传递图象数据到帧缓冲器，使用通过它的接口方法设置的参数来修改图象、执行交换或者执行另外的服务。

VPE不与Windows NT和Windows 2000视频端口系统模块。

3.5.1 视频端口扩展背景

VPE技术是一个DirectDraw扩展，它支持与一个视频译码器直接的硬件连接并在图形帧缓冲器里自动地交换。

当视频数据放到帧缓冲器时，一个视频重叠可用来显示它。在图形系统里的视频重叠告诉DAC在它不能够普通显示的区域里显示数据。重叠的使用是有效的，由于不需要位块转移来使数据可见，由于视频数据可在一个比可见的帧缓冲器更高的颜色深度里被浏览。

在一个VGA图形控制器上的硬件视频端口为了使数据能够到达帧缓冲器而提供一个快速的机制。硬件端口是一个致力于设备之间的连接，特别在一个MPEG设备或者一个NTSC译码器和显卡之间。这个致力于的连接用视频数据输送H-sync和V-sync信息。硬件视频端口和重叠能够使用这个同步信息自动地在多个缓冲器之间交换，当重叠显示另外一个的时候写入到一个表面上，这允许自由撕裂视频而没有加重应用程序的负担。

硬件视频端口是一个视频流输送选项，它可以代替PCI或者AGP总线使用。对期望与其他应用程序协作运行的应用程序，且这些其他的应用程序缺少PCI波宽，使用硬件视频端口确保低等待时间视频在译码器和VGA图形控制器之间的传输，是有利的。这个路径不象PCI总线那样灵活是由于它连接译码器到一个特别的图形成套芯片，但是它提供一个机会当希望如此时绕过PCI总线。

在内核模式视频传输部分里描述的内核模式视频传输能力，也提供VPE支持确保增强的视频回放质量和增强的视频捕获支持。驱动程序必须支持内核模式视频传输以便支持Windows NT/Windows 2000的VPE。

注意：（对Windows 95/Windows 98而言）内核模式视频传输功能在Windows 98上是可选择的，但是Windows 95不支持，这是因为它需要操作系统支持微软的Win32驱动程序模式。

VPE仅仅支持基于硬件的数据连接，内核模式视频传输支持数据转移的直接存取。

VPE不是WDM技术。在Windows 98和Windows 2000上，WDM使用于其他的外设但不使用于显示驱动程序。

由于VPE支持是DirectX 5.0及以后版本的部分，且它在两个操作系统里都是被支持的，应用程序能够利用这些能力确保这个分辨率将在任何图形卡上起作用。该图形卡支持Windows 2000或Windows 98的VPE。

3.5.1.1视频的显示问题

在下面的两个场景里，隔行扫描的视频必须为Windows 2000或Windows 98的回放而整合：

n 隔行扫描的内容被显示在一个隔行扫描的电视和一个具有隔行扫描的监视器（NTSC /PAL）的枚举机上。

n 现存的隔行扫描的内容被显示在一个隔行扫描的电视上，它也需要用最可能好的质量在一个顺序的扫描（proscan）计算机监视器上被显示。

3.5.1.1.1 NTSC和隔行扫描的数据

NTSC标准每秒提供一系列59.94的隔行扫描的域，每个以1/59.94秒隔离。偶数行域的扫描行在奇数行域的扫描行之间。然而，由于电视监视器的磷光体持续，两个域从来不会同时显示在电视屏上。观众一直看或者一个偶数域或者一个奇数域，但从来不能两个都看。

在NTSC里的一个帧是两个完全不相关的顺序域的一个任意的分组，也就是说，帧里的第一个域只是相关于相同帧的第二个域，而不是从前帧里的第二个域。逐行倒相制式（PAL）格式和顺序传送彩色与记忆制式（SECAM）标准以每秒50域相同的工作。

如果视频包含高动画内容，奇数域里的数据可能与偶数域里的数据不同，由于不会同时看到这两种域，且眼睛做了这个整合数据的好工作，所以这不会在电视监视器上引起问题。然而，在一个枚举机上，这个隔行扫描的数据被交织进一个单个的缓冲器里，然后使用顺序的扫描被显示。这意味着同时这两个域都是可见的，并有高的动画人工痕迹的可能。

放置视频到一个数字视频硬盘（DVD）之上的过程和然后重新播放它是复杂的。典型地，盘上的源素材给电视创建，这样每个帧有两个隔行扫描的域。然而，每秒（fps）24帧的胶片镜头必须转化成每秒59.94域以兼容一个电视显示。

3.5.1.1.2 NTSC /PAL转换

简单地稍快一点播放胶片，以25 fps的速度播放，可以完成从NTSC 到PAL的转换，两个顺序的域从相同的帧创建，且以1/50秒apart显示。通过用一个调用了3:2的拉片距的过程重复每第五个视频域，可以实现从PAL到NTSC的转换。使用第一个胶片帧创建两个视频域，然后使用第二个胶片帧创建三个视频域，这个过程被重复以便奇数和偶数域以Ao Ae Ao Be Bo Ce Co Ce次序送出。

这样，从一个胶片创建的一个隔行扫描的视频包含着域对，但是不象在一个标准的电视信号，此处每个域是1/60或1/50秒apart，这些域的许多对包含从相同的胶片帧而来的数据，同时显示这些域对不会引起任何人工痕迹，且提供比电视监视器所提供的更好的结果。然而，不从相同的帧而来的任何域对将是1/24秒apart且可能潜在地引起更大的人工痕迹。

当译码隔行扫描的视频时，设置一个标志表示怎样解码流。在一个理想的世界里，由于为DVD或DSS的一个MPEG-2或NTSC译码器的输出在理论上是每秒50或60隔行扫描的域，译码和选择地显示被译码的图片将有足够的信息。然而，更旧的胶片和一些更新的胶片常常在标志节奏里有一个中断，特别在胶片卷完全转变点时。这需要一个方法标识这样一个顺序的内容，以便选择适当的方法在一个帧到帧的基础上显示信息。

3.5.1.1.3 MPEG和顺序的内容

MPEG-2语法提供必要的信息来标识顺序的内容3:2的拉片距，这个信息在下面的一位标志里存在每个帧的头文件里：

n PROGRESSIVE_FRAME：当为TRUE时，这表示两个域（帧的）实际上来自于相同的时间instant（顺序的胶片）。当为FALSE时，这表示域可能是帧时间apart的一半（隔行扫描的视频）。

n TOP_FIELD_FIRST：表示哪一个域及时的首先到来。

n REPEAT_FIRST_FIELD：表示是否一个域为3:2的拉片距而重复。

用DirectX 5.0或以后版本下的VPE和DirectDraw，视频一直能够以最可能好的质量显示，如果这些标志或出自于这些标志的一个符号能够在每个域的基础上传送到系统。隔行扫描的视频也能够为DirectShow所支持，用媒体例子里的新标志表示是否没有压缩的媒体例子或者是一个完全的帧，或者是一个域，并加上任何别的信息。如下面的部分所描述的，通过使用或者帧基础的媒体例子，或域基础的媒体例子，DirectShow可在每个帧的基础之上被指令在显示模式之间转换。

3.5.1.1.4 顺序扫描监视器和隔行扫描的数据

在一个proscan监视器上，行作为帧显示---也就是说，行1先被显示，然后是行2，行3 等等。典型的电视显示一个2:1光栅---也就是说，它在奇数行显示一个帧的第一个域，然后它在偶数行显示帧的第二个域。任何将要显示在电视上的信息必须在显示在一个proscan监视器之前被处理。

3.5.1.2 用VPE：Bob和Weave显示隔行扫描的视频

许多昂贵和不完善的方法为解除隔行扫描而存在。当为大尺寸背投影显示解除隔行扫描时，专业的电视制作人使用设备如行加倍装置，且当创建焦距和低运动序列时，它们使用具有运动适应过滤器的印象系统。

对在一个顺序的枚举机监视器上显示隔行扫描有两种简单的方法是可行的：Bob和Weave。枚举机和电视工业为这些方法使用不同的术语；为了简单，这个文档使用了Bob和Weave。

3.5.1.2.1 Bob方法

显示数据的bob方法使用一个重叠单独地（类似于电视）显示每个域，结果图象是正常的一半高度，这样它必须使用一个内插的重叠延伸在垂直方向放大为200%。然而，如果这是bob算法所做的全部，结果图象将上下起伏，因为奇数域与偶数域有一行冲突。只要垂直延伸使用一个内插器被执行，添加一行到奇数域的重叠起始地址可解决这个问题。

Bob方法每秒产生了60（NTSC）顺序的帧，并且保留了所有从隔行扫描源来的临时信息。如果用一个视频相机创建一个视频，且图象是动画，bob是一个顺序的监视器最好的低开销显示过程。这个方法对所有的隔行扫描的视频数据源都起作用，但是weave方法创建一个勾边的图象。

缺省时，DirectShow使用bob方法更正隔行扫描显示。

3.5.1.2.2 Weave方法

weave方法使用硬件视频端口显示数据以交织隔行扫描的域到一个重叠的表面里，然后同时显示两个域。如果这是weave算法所做的全部，动画的人工痕迹会出现，weave算法也依赖MPEG驱动程序记忆3:2图案，然后使用内核模式视频传输的功能解开它。内核模式视频传输可能引起硬件视频端口丢弃重复的域，引起所有的域对偶来自于相同的帧。结果是全帧的视频，它以每秒24帧显示，如同它使用胶片原始采集一样。

每个源胶片帧以两个或三个域的NTSC信号表示，这可以认为是弥补了一个帧的两个一个域。四个胶片帧的每个序列转换成五个电视帧，胶片帧A、B、C和D变成了五个具有域特征AA、BB、BC、CD和DD的电视帧。当REPEAT_FIELD标志在一个MPEG流里编码这个特征时，MPEG数据有效载荷仅包含四个帧，但是所有五个电视帧的域序列被保留下来了。

weave方法每秒产生24个顺序的帧并从一个隔行扫描的原理保留全部的垂直分辨率。如果一个视频从使用一个3:2拉片距的胶片创建，或者它不包含动画，weave是顺序的监视器可付的起的最好的显示过程。

3.5.1.2.3 模式指示器和变形格式

好的驱动程序设计调用bob、weave和边缘适应的过滤的一个综合。动画探测电路能够动态地控制每个使用在一个象素到象素的基础之上的技术所使用的程度。不幸的是，将完成这个的显示逻辑是难于实现的，除非额外的数据可用。随着大型枚举机等级显示能力的提高，操作系统需要这个额外的数据来确保基本的图形电路能够信赖的传输一个可选择的图象。

在DVD世界里，许多格式可能被综合在一张盘上，例如，24fps胶片可能被编辑成525/60视频且然后编辑成30fps胶片，这样的编辑发生的每个时候，它潜在的改变了怎样最好的显示数据。当前的DVD标签没有保证在显示系统里处理这个混合格式的好的执行。

另外一个可能的场景是一个胶片的显示，此处由于一个错误，REPEAT_FIELD标志是不规则性的或缺少的。当译码器尝试解释一个不规则性的3:2图式时，它尝试重新获得同步，但这可能是杂乱的。对一个紧随REPEAT_FIELD标志的显示而言，字面上说在镜头的中间，从weave模式改变到bob模式或者反之是完全可能的，对一个观众来说这将非常糟。

显示一个偶然有3:2图式不规则性的胶片的方法是从weave模式到bob模式转换并返回围绕不规则性的镜头截除。在其他情况下，特别的标识一个新图案第一个域对和维持weave模式可能是最好的。

你可以想象许多内容场景，一些自动显示方案比其他的工作的更好，这取决于所包含的特定的内容。并非所有的内容提供者都需要保证回放平台的最大范围的可选择结果，但是DirectShow给了内容作者判断显示模式对它们的标题的影响的一个方法并传送它们的选择权。

3.5.1.3 在DirectDraw API和驱动程序里的VPE功能

在DirectX 5.0和后来的版本里的VPE是低级的DirectDraw API扩展，VPE允许客户---通常DirectShow---协商在MPEG或NTSC译码器和硬件视频端口之间连接，VPE也允许客户在视频流里控制效果，包括裁剪，定标等等。

VPE不是为应用程序广泛使用的一个高级API，应用程序应该使用DirectShow，它提供对VPE自由支持。图3.10给出了VPE和内核模式结构的一个简单的浏览；欲知更多的信息。请看内核模式视频传输部分。

图3.10 VPE和内核模式结构

图3.10 给出了VPE与DirectDraw结构里的其他组件的关系。DirectShow使用VPE协商连接，它提供这样的信息，即关于数据和V-sync和H-sync信息是怎样被传送的，这个信息可以是一个APIC连接（ITU 656）、具有额外插针的外部数据行或者专有的数据流，如被Brooktree和Philips实现的那些。

在关于连接的协商里，VGA硬件表明什么样的连接能够被支持，MPEG或NTSC译码器表明了它的参考。DirectShow协商在这两个之间的最好连接。连接被描述为一个GUID，它具有标志来描述其他的参数，如双倍定时、视频激活等等。

3.5.2 DirectX VPE初始化

为了使VPE功能可能，驱动程序必须做下面的事情：

n 当调用DrvGetDirectDrawInfo时，初始化下面的DDNTCORECAPS结构成员，该结构嵌套在pHalInfo参数指向的DD_HALINFO结构里：

n 当调用DrvGetDirectDrawInfo时，实现一个函数并设置DD_HALINFO结构的GetDriverInfo成员来指向这个函数。驱动程序的DdGetDriverInfo函数必须分析GUID_VideoPortCallbacks和GUID_VideoPortCaps GUIDs。

n 当用GUID_VideoPortCallbacks GUID调用DrvGetDirectDrawInfo时，填充一个具有适当的驱动程序回调和标志设置的DD_VIDEOPORTCALLBACKS结构。然后驱动程序必须拷贝这个被初始化过的结构到DirectDraw-allocated缓冲器里，DD_GETDRIVERINFODATA结构的lpvData成员指向了该缓冲器，并在dwActualSize里返回写入到缓冲器里的字节的数目。

n 当用GUID_VideoPortCaps GUID调用DrvGetDirectDrawInfo时，填充一个具有每个硬件视频端口能力的DDVIDEOPORTCPS结构的数组。每个硬件视频端口在数组里有一个入口点，硬件视频端口首先被指定为0，接下来指定为1，等等。如果设备仅支持一个硬件视频端口，将只有一个DDVIDEOPORTCPS结构在数组里。然后驱动程序必须拷贝这个数据到DirectDraw-allocated缓冲器里，DD_GETDRIVERINFODATA结构的lpvData成员指向了该缓冲器，并在dwActualSize里返回写入到缓冲器里的字节的数目。

3.5.3内核模式视频传输支持的DirectDraw接口

内核模式视频传输必须明了为它使用的每个表面和VPE对象的表面信息，这个信息必须当DdUpdateOverlay或DdVideoPortUpdate每次为表面或硬件视频端口而被调用时得以更新。在DirectDraw发送这个信息到内核模式视频传输之前，它调用两个驱动程序功能的一个：DdSyncSurfaceData或DdSyncVideoPortData。这些功能允许驱动程序出于它自己的目的填充或修改信息的一些结构并使用DD_SYNCSURFACEDATA的四个dwDriverReserved成员或DD_SYNCVIDEOPORTDATA结构的三个dwDriverReserved成员。这些驱动程序功能对内核模式视频传输正确地工作是必要的。

驱动程序怎样使用这些dwDriverReserved成员的一个好的例子是设置一个标志，该标志表明了如果硬件支持超过一个重叠时，哪一个物理重叠表面正在使用。

3.6 彩色控制初始化

DdControlColor 被添加到DirectX 5.0来控制亮度/一个重叠的亮度控制和/或主表面，为了使颜色控制可能，DirectDraw HAL必须在初始化时间里做下面的工作：

n 如果重叠和/或主表面包含颜色控制，在DDVIDEOPORTCPS结构的dwCaps2域里设置DDCAPS2_COLORCONTROLOVERLAY和/或DDCAPS2_COLORCONTROLPRIMARY标志，该结构嵌套在DD_HALINFO结构里。

n 驱动程序必须在DD_HALINFO结构里指定一个功能，DirectDraw能够调用这个结构得到附加的信息，这在DdGetDriverInfo里有描述。

n DdGetDriverInfo 回调必须与指定的DD_ColorControlCallbacks GUID一起被调用，驱动程序必须填充一个具有适当的驱动程序回调和标志设置的DD_VIDEOPORTCALLBACKS结构，然后拷贝这个结构到输入结构的lpvData成员里。

3.7 AGP支持

DirectDraw对待高级图形端口（AGP）内存为一个子类的显存，这个内存类型参照非本地显存，从DirectDraw和DirectDraw驱动程序的角度来看，术语AGP内存和非本地显存是同步的。

AGP内存被认为是一个显存的纯子类。也就是说，如果一个驱动程序表明它支持AGP内存，那么在大多数情况下，它为本地和非本地显存应具有同样的功能，尽管性能差异被允许。它必须有同样的功能能力。例外是如果设置DDCAPS2_NONLOCALVIDMEMCAPS标志，在这种情况下，非本地显存的位块转移能力能够与本地显存不同。

例如，如果一个驱动程序声明它能够从显存组织纹理，它必须能够从本地和非本地显存组织纹理，位块转移被相似的对待。一个输出了源颜色键位块转移能力的驱动程序必须能够到和从非本地显存做一个源颜色键位块转移。这个规则的一个例外是，有可能排除某些表面类型在非本地显存中被分配，例如，使用堆阻止重叠表面在AGP内存里被分配是可能的。

由于AGP内存被看待为显存的一个子类，DirectDraw没有为AGP内存单独设置显示驱动程序的入口点。AGP表面和本地显存表面都使用了存在的显示驱动程序调用，一个AGP兼容的驱动程序必须检查进入的表面来看它们是否处于非本地或本地显存，然后采取适当的步骤。从系统到AGP（和反之）的位块转移如平常一样检查DirectDraw仿真层，除非一个驱动程序支持系统到显存的位块转移（在这种状况下，它也必须支持系统到AGP的传输）。

由于Direct3D纹理管理器在AGP内存里保留了它的后备表面，驱动程序应该尽可能多的设置DDCAPS2_TEXMANINONLOCALVIDMEM标志。

本章的其余部分使用了DirectDraw非本地显存特征，讨论修改存在的驱动程序以支持AGP内存所必须的步骤。

3.7.1为非本地显存标志支持

一个驱动程序必须通过在DDNTCORECAPS结构的dwCaps2成员里指定能力位DDCAPS2_NONLOCALVIDMEM来通知DirectDraw（和DirectDraw应用程序），说驱动程序是AGP兼容的，DDNTCORECAPS结构是传递到DirectDraw的DD_HALINFO结构的部分。

如果DirectDraw正在一个不支持AGP服务的操作系统上运行，它关闭DDCAPS2_NONLOCALVIDMEM能力位和所有相关的非本地堆。

3.7.2指定非本地显存堆

一个DirectDraw驱动程序控制多少AGP内存是可用的和在传回到DirectDraw的 DD_HALINFO结构里返回堆到哪一个表面。通过在描述了堆的VIDEOMEMORY数据结构的dwFlags成员里指定VIDMEM_ISNONLOCAL标志，驱动程序标识了非本地堆。此外，通过指定除过VIDMEM_ISNONLOCAL之外的VIDMEM_ISWC标志，一个驱动程序能够选择使得内存综合写入一个非本地堆。（然而，VIDMEM_ISWC标志当前没有在DirectDraw里执行。）

一个AGP兼容的DirectDraw驱动程序负责描述DirectDraw的大小（线性的或矩形的）、属性（写结合）和堆不应该和不能够使用的表面类型。然而，驱动程序不负责实际的堆的保存地址空间或提交内存给它。DirectDraw代表驱动程序处理这个事情，并隐藏了从驱动程序管理AGP内存的细节。

对一个非本地显存堆而言，驱动程序指定的起始地址没有意义，当DirectDraw请求创建一个对时，操作系统决定一个非本地堆的起始地址。这样，驱动程序能够为起始地址返回任何值。对一个矩形堆来说，DirectDraw忽略了这个起始地址，DirectDraw需要的所有用来决定内存需求的事情是指定的宽度和高度。对一个线性堆来说，起始地址有意义，但是仅仅用来枚举堆的大小。

DirectDraw通过枚举（fpEnd-fpStart）+1决定了线性堆的大小。（注意到被指定的末端是堆里的最后一个字节，而不是在堆的末端之后的第一个字节。）象这样一样，只要当DirectDraw从末端地址减去那个地址并加上1，就能够指定任何起始的地址，结果是堆的最大尺寸。

尽管仅当需要时（即是，当分配表面时）物理内存被提交给AGP堆，不指定每个大的非本地堆是重要的。甚至在提交物理内存之前，这样的堆就消耗了共享的地址空间和其他重要资源。

注意到DirectDraw和Windows操作系统给内存量强加了对AGP内存的政策限制，此AGP内存能够在任何给定的时间里被提交。这阻止了剩余系统的资源“饥饿”。这样，对一个非本地显存表面的一个请求来说，即使非本地堆没有完全被提交，失败是很有可能的。

当DirectDraw已经决定了正确的堆地址（线性的和物理的），就将它们存储在它的堆描述器里。它也提供一个机制在初始化时通知驱动程序这些地址，如何完成这个是特定平台相关的：

n 在Windows NT/Windows 2000上，用一个DdGetDriverInfo调用GUID_UpdateNonLocalHeap可以完成，当这个GUID被传递给DDGetDriverInfo时，UpdateNonLocalHeap数据在数据结构里被传输。

n 在Windows 95/Windows 98上，用DD_MISCELLANEOUSCALLBACKS可以完成此事，用DDGetDriverInfo调用从驱动程序提取MISCELLANEOUSCALLBACKS结构。在初始化时，DirectDraw运行时检查一个FOR循环，调用被UpdateNonLocalHeap成员指定的回调。然后，DirectDraw为每个堆请求驱动程序提供的功能，并为功能传递堆数目和堆的线性高端的起始物理地址。

3.7.3实际堆基本地址的通知

一个驱动程序在DirectDraw初始化时（例如，在模式改变的过程当中）可能需要知道堆的线性物理基地址，而不是等待一个表面创建请求和在全局DirectDraw表面对象里考虑堆。为了支持这个，DirectDraw用一个GUID调用驱动程序提供的DdGetDriverInfo回调功能，GUID标识了驱动程序将要返回的信息。如果驱动程序认出了GUID且有信息返回，它拷贝这个信息到提供的数据结构里并传回给DirectDraw。

驱动程序使用两个GUIDs来收集和提供关于DirectDraw堆的更多信息。

n GUID_GetHeapAlignment

n GUID_UpdateNonLocalHeap

GUID_GetHeapAlignment发送信号到驱动程序来收集传递给它的任何DirectDraw堆的堆分配信息，使用DD_GETHEAPALIGNMENTDATA结构传递堆信息给驱动程序，GUID_GetHeapAlignment被定义为：

DEFINE_GUID ( GUID_GetHeapAlignment,

0x42e02f16, 0x7b41, 0x11d2, 0x8b, 0xff, 0x0, 0xa0, 0xc9, 0x83, 0xea, 0xf6);

GUID_UpdateNonLocalHeap给驱动程序发出信号，以便用DirectDraw提供的具有非本地堆结构的堆信息来更新它的内部状态。这个信息被包含在DD_UPDATENONLOCALHEAPDATA结构里，GUID_UpdateNonLocalHeap被定义为：

DEFINE_GUID ( GUID_UpdateNonLocalHeap,

0x42e02f16, 0x7b41, 0x11d2, 0x8b, 0xff, 0x0, 0xa0, 0xc9, 0x83, 0xea, 0xf6);

如果驱动程序必须通过它自己为AGP表面分配内存，但是已经暴露堆给DirectDraw，为了这个目的，HeapVidMemAllocAligned被暴露作为一个Eng功能。HeapVidMemAllocAligned仅仅处理堆地址，这样它返回一个偏移，驱动程序必须做它所需要的任何内存映射工作，来转变从HeapVidMemAllocAligned返回的信息为一个虚拟的地址。

3.7.4处理非本地显存表面的回调

按照驱动程序回调，以完全相同的方法对待非本地显存表面与本地显存表面。例如，当试图创建非本地（如同本地）显存表面时，一个驱动程序的DdCanCreateSurface回调被调用；当在本地和非本地显存表面之间进行位块转移时，DdBlt被调用了；当表面显存正被丢弃时，DdDestroySurface被调用了。

既然本地和非本地显存表面使用相同的驱动程序功能，驱动程序必须明确地检查进入的表面的类型。可通过检查传递到驱动程序的本地表面对象DD_SURFACE_LOCAL的ddsCaps.dwCaps成员来标识内存类型，而不是能力位DDCAPS_LOCALVIDMEM和DDCAPS_NONLOCALVIDMEM。

应用程序和AGP使用了两个不同的地址存取一个DirectDraw表面的位，应用程序使用了一个虚拟地址，该虚拟地址通过操作系统的页表被转变成物理地址空间的一个部分，这个物理地址被GART硬件映射为连续的。硬件存取这个物理线性地址（重新被GART映射为不连续的真的内存页），DD_SURFACE_GLOBAL结构的fpVidMem成员保留这个对应用程序（和一些潜在的驱动程序操作）有用的虚拟线性地址，通过计算可以发现设备一方的物理地址。

FpStartOffset = pSurface -> fpHeapOffset – pSurface->lpVidMemHeap->fpStart;

这个偏移然后被添加到设备的GART物理基地址中（包含在VMEMHEAP结构的liPhyAGPBase成员）。

在其他所有的方面，非本地显存表面表现得与本地显存表面完全相似。当一个应用程序试图存取非本地显存表面的表面数据时，驱动程序接收到锁定请求。如在非本地显存与本地显存之间的位块转移操作能够是异步的，就如同它们能够在本地显存表面之间一样。当包括在这些表面的操作仍旧待处理时，锁定非本地显存表面的努力将由于在通常方法下具有DDERR_WASSTILLDRAWING错误代码的驱动程序而失败。

此外，尽管DirectDraw代表驱动程序管理分配和释放非本地显存表面的任务，驱动程序仍旧被通知创建和注销非本地显存表面里的表面。当一个非本地显存表面被注销时，驱动程序不应该返回直到表面不再使用。

非本地显存恰好以与本地显存同样的方法里丢失---也就是说，当一个模式转换发生时，或当唯一的模式改变发生时，所有的本地和非本地显存表面丢失了，且DdDestroySurface驱动程序回调为每个表面而被请求。然而，没有保证实际的保留地址范围和调拨的内存将被保存。可能选择丢弃所有调拨的内存和保留的地址范围，或者它可能选择解除调拨内存但是保存地址范围，它也可能保存这两个且简单地将表面标志为丢失状态，一个驱动程序不应该基于这三个情景的任何一个来做出假设。

3.7.5在非本地显存里重新排序纹理

在特殊的情况下，驱动程序编写者可能想要在AGP内存里重新排序纹理以便允许更有效的纹理管理。DDCAPS2_SYSTONONLOCAL_AS_SYSTOLOCAL标志发出了信号表明驱动程序能够支持从后备表面到仍然使用了caps的非本地视频内存的位块转移，该caps为后备表面内存而指定到本地视频内存的位块转移。

仅仅如果DDCAPS2_NONLOCALVIDMEMCAPS标志被设置， DDCAPS2_SYSTONONLOCAL_AS_SYSTOLOCAL标志才是有效的。如果设置了DDCAPS2_SYSTONONLOCAL_AS_SYSTOLOCAL，驱动程序必须设置DDCAPS_CANBLTSYSMEM标志，且所有相关的后备表面位块转移caps必须正确。DDCAPS2_SYSTONONLOCAL_AS_SYSTOLOCAL表示了后备表面到视频内存DDCAPS位块转移caps也应用于后备表面到非本地视频内存位块转移。例如，保证DDCAPS（DDNTCORECAPS）的dwSVBCaps、dwSVBCKeyCaps、dwSVBFXCaps和dwSVBRops成员将被正确地填充。任何从后备表面到匹配于这些caps位的非本地内存位块转移被传递到了驱动程序。

注意：这个特征的目的是使驱动程序自己能够做有效的纹理里重新排序，这并不意味着硬件能够写入AGP内存，直接地写入AGP内存的硬件在当前并不被支持。

3.7.6处理DMA-style AGP

一个AGP兼容的显卡能够用两个方法中的一个使用AGP内存：使用执行模式或直接内存存取（DMA）模式。

n 在执行模式里，如果在非局部视频内存里有一个纹理，显卡直接地存取AGP内存，也就是说，如果一个显卡从AGP内存被组织纹理，它直接从一个后备表面读纹理象素。

n 在一个DMA模式里，表面的内容必须在执行一个纹理操作之前被明确地移到显卡上的本地显存。

注意到模式提到了显卡的一个客户如何看待传输是重要的，例如，当进行纹理时，一个显卡可能自动地从一个后备表面移动纹理象素到一个小的本地显存高速缓冲。这也许象DMA模式，但是由于客户应用程序没有关于这个传输发生的信息，事实上，是显卡暴露了一个执行模式。仅仅当客户应用程序不得不采取明确的步骤移动一个后备表面的内容到本地显存时，显卡才被认为将暴露DMA模式。

前面处理AGP内存的部分描述了一个驱动程序如何能够和暴露AGP用法的执行模式，这一部分描述了一个驱动程序必须采取的步骤来给应用程序暴露DMA模式AGP的使用。注意到你必须决定当写驱动程序时是否暴露执行模式或DMA模式，驱动程序应该暴露一个模式或者另一个，但不能够是全部。

在从一个驱动程序露DMA模式之前，你应该考虑隐含DMA模式给应用程序作者。如果一个驱动程序暴露执行模式AGP支持，DirectDraw保证在AGP（非本地显存）和本地显存里的表面在功能上是一样的。这样，显卡能够从非本地或本地显存组织纹理，而没有应用程序的额外步骤。当设置一个着色状态时，一个应用程序能够直接地指定处理给一个纹理表面，而不管是否表面在非本地或本地显存里。

然而，如果一个驱动程序暴露DMA模式，在非本地显存里的表面可能有与本地显存里的表面不同的能力。这样，在尝试从一个非本地显存里的表面组织纹理之前，应用程序必须检查是否硬件能够从非本地显存里组织纹理。通过检查驱动程序暴露的能力可以完成这个工作，对位块转移来说这也是相同的。

一个应用程序通过用DDCAPS2_NONLOCALVIDMEM指定DDCAPS_VIDEOMEMORY明确地请求AGP内存，如果一个应用程序没有指定一个内存类型或仅指定了DDCAPS_VIDEOMEMORY，将不会考虑非本地显存。同样，如果一个调用没有指定本地或非本地显存的话，表面是一个纹理，设备设置了D3DDEVCAPS_TEXTURENONLOCALVIDEONEMORY标志，且能够在AGP内存里分配表面。

这意味着如果一个驱动程序暴露DMA模式，不会从AGP内存分配表面。这相对于暴露了执行模式的驱动程序，在它里面甚至如果应用程序没有明确地请求它，AGP内存仍然被分配，因而暴露了执行模式的驱动程序对应用程序的使用来说更为简单。此外，一个执行模式驱动程序允许一个合法的应用程序获得AGP收益，然而一个DMA模式驱动程序仅仅加速为AGP而明确地写入的新应用程序。当决定是否要暴露执行模式或DMA模式时，你应该考虑到这些。

3.7.6.1为DMA模式的非本地显存标志支持

除过指定DDCAPS2_NONLOCALVIDMEM标志来报告AGP支持外，一个DMA模式驱动程序也必须输出能力标志DDCAPS2_NONLOCALVIDMEMCAPS，这个标志表示非本地（AGP）内存跟本地显存相比有不同的能力。

3.7.6.2为DMA模式的非本地显存报告DirectDraw能力

对非本地内存和本地显存来说，一个DMA模式驱动程序的能力不同。例如，一个显卡可能延伸位块转移本地显存表面，但是不能够延伸非本地显存表面。如果驱动程序指定DDCAPS2_NONLOCALVIDMEMCAPS标志，非本地显存表面的DirectDraw能力通过DdGetDriverInfo驱动程序入口点探测驱动程序。标识这个探测的GUID是GUID_NonLocalVidMemCaps。

注意到DirectDraw的这个版本是重要的，一个驱动程序能够仅为非本地显存到本地显存的位块转移指定能力。从本地显存到非本地显存及从非本地显存到本地显存的转移一直被DirectDraw HEL所访真，这个限制可能会在以后的版本里放松。

3.7.6.3为DMA模式的非本地显存报告Direct3D能力

一个DMA模式驱动程序也必须为非本地显存表面输出Direct3D能力，这比报告DirectDraw能力要简单的多，唯一被影响的能力是D3DDEVCAPS_TEXTURENONLOCALVIDEOMEMORY。如果输出DMA模式的一个显卡能直接地从非本地显存组织纹理，它应该在它的Direct3D设备描述里设置这个能力。如果它不能够，且应用程序必须在执行纹理之前明确地装载或位块转移非本地显存表面到一个本地显存表面，则它不能够设置这个能力。为了完整起见，一个执行模式驱动程序应该一直设置这个能力位。

3.8 内核模式视频传输

内核模式视频传输指一个DirectDraw组件，如DirectShow，用户可以使用这个组件来增强视频功能，这个功能的主要任务是当V-sync发生时调用微端口来告诉它执行硬件视频端口和重叠交换。只要硬件视频端口支持V-sync中断请求（IRQ），这个能力能够支持达到10个缓冲器而没有遇到硬件限制。当客户指定自动交换且硬件不能够自动交换时，DirectX5.0及以后的版本提供的DirectDraw版本自动地使用这个能力。

内核模式视频传输也确保增强的捕获支持。在Windows 98和Windows 2000上，基于WDM的视频捕获驱动程序在内核模式里运行，且直接存取帧缓冲器，捕获驱动程序能够“手工地”交换重叠。Windows 2000微端口视频传输驱动程序能够从硬件视频端口或显示提供V-sync通知；它也能够得到极性域，当捕获垂直清空间隔（VBI）数据时可使用该极性域。

尽管内核模式驱动程序的主要目的是增强硬件视频端口自动交换的能力，它也支持视频总线主设备，当处于内核模式时此主设备能够写入数据。由于一个模式的改变或者由于全屏命令提示实例启动，总线主设备能够在失去表面之前被通知。又由于新的驱动程序支持允许在改变发生之前调用一个总线主设备，因而总线主设备能够关闭而不会引起问题。

3.8.1 VPE和内核模式视频传输的体系结构

本部分为DirectX 5.0及以后的版本里的VPE和内核模式视频提供了Windows 2000体系结构的一些细节。内核模式视频传输的体系结构在Windows 98和Windows 2000上是相同的；它基于微软作为设备独立的代码所添加的新功能。内核模式视频传输包括一个DxApi功能，此功能作为DirectDraw的部分，驱动程序在微端口里提供的DxApi回调，和作为DirectDraw部分而提供的COM接口方法。

在Windows 98里，DxApi回调被添加到小DVD，其包含特定图形设备的代码。

在Windows 2000里，如图3.11所示，DxApi回调是Windows 2000微端口的部分。为获得关于DxApi回调的更多信息，请看Windows 2000 DxApi微端口功能。

图3.11 内核模式视频传输的体系结构

图3.11给出了与其他内核模式和用户模式组件相关的内核模式视频传输的体系结构。（虚线意味着内核转变。）在这个体系结构里，DirectShow（或另外的用户模式客户）调用IDirectDrawKernel和IDirectDrawSurfaceKernel DirectDraw COM接口来处理DirectDraw对象和表面对象。

注意：体系结构也为MPEG设备和VGA设备之间的数据流支持使用PCI总线。

在Windows 2000里，客户然后传递这些处理给微端口驱动程序，在内核模式视频传输的调用里指定这些处理。图3.12给出了如何传递这些处理一个简单的版本。

图3.12 在Windows 2000视频传输里传递句柄

图3.12 给出了如何在用户和内核模式视频传输里传递这些句柄。

3.8.2 使用内核模式视频传输

视频微端口驱动程序通过与dxapi.lib链接存取内核模式视频传输功能，这个dxapi.lib文件允许它以后调用dxapi.sys，仅仅当装载了DirectDraw时，这个功能才是可用的。

一个硬件译码器使用与内核模式DirectDraw一起提供的DxApi功能来存取DxApi微端口回调功能。DxApi函数是一个单个的入口点，这个入口点接受一个功能识别器、一个输入缓冲器及其大小和一个输出缓冲器及其大小。这个功能的行为以及输入、输出缓冲器的大小和格式均依赖于指定的功能识别器，DxApi功能和它的功能识别器在ddkmapi.h文件里定义。

DirectShow或者另一个客户存取驱动程序通过DirectDraw提供的DxApi微端口回调功能，DxApi微端口功能在dxmini.h文件里定义。

为了使用内核模式视频传输接口，微端口驱动程序必须首先接收用户模式句柄的每个DirectDraw对象、表面和VPE对象。对捕获和MPEG模式来说，这些句柄是通过其现存的API向下传递，如果一个非流类驱动程序需要这个功能，一个用户模式组件能够通过使用IDirectDrawKernel和IDirectDrawSurfaceKernel COM接口重新获得这个句柄，并向下传递给驱动程序，COM接口及其方法在ddkernel.h文件里做了标识。

3.8.2.1获得用户模式句柄

下面的程序给出了如何获得用户模式句柄。

为获得DirectDraw处理

1. 在DirectDraw接口上调用QueryInterface（lpDD，&IID_IDirectDrawKernel，&pNewInterface）。

2. 在新的接口上调用IDirectDrawKernel::GetKernelHandle方法。

IDirectDrawKernel::GetKernelHandle 方法返回DirectDraw驱动程序的一个内核模式句柄。为释放这个句柄，使用IDirectDrawKernel::ReleaseKernelHandle方法。

为获得DirectDrawSurface句柄

1. 在DirectDrawSurface接口上调用QueryInterface（lpSurface，&IID_IDirectDrawSurfaceKernel，&pDDSK）。

2. 在新的接口上调用IDirectDrawSurfaceKernel::GetKernelHandle方法。

IDirectDrawSurfaceKernel::GetKernelHandle 方法返回DirectDrawSurface驱动程序的一个内核模式处理。为释放这个句柄，使用IDirectDrawSurfaceKernel::ReleaseKernelHandle方法。

3.8.3 Windows 2000的DxApi微端口功能

由于DxApi微端口支持需要改变操作系统，所以它仅在Windows 2000上可行。

DxApi微端口支持对下面的应用程序是有用的：

n 使用了设备的IRQ的自动交换。这个设备不支持硬件自动交换，或由于有某些限制而不可靠。当无法使用硬件自动交换时，DirectDraw就总会转变成软件自动交换。

n 使用了一个IRQ来支持MPEG驱动程序的域跳读，该驱动程序能够松开最初从胶片采样的MPEG数据的3:2拉片距。

n 设备的总线主控。这些设备需要连续地总线主控数据而不必为每个帧调用DdLock/DdUnlock，由于支持这些设备的驱动程序是WDM驱动程序，这在微端口里特别有用。

n 视频和VBI捕获。在微端口驱动程序里，很容易捕获基于一个硬件视频端口IRQ或图形IRQ的视频。

对这个API的支持已经添加在Windows 2000的微端口驱动程序里。

3.8.3.1 DxApi微端口初始化

为了使用DxApi微端口驱动程序的功能，DirectDraw驱动程序必须在初始化时间里执行下面的任务：

1. 它必须指定DD_HALINFO结构里的DdGetDriverInfo功能，这样DirectDraw能够调用来得到额外的信息。

2. 它必须用指定的GUID_KernelCallbacks GUID调用DdGetDriverInfo回调。驱动程序必须用适当的回调和标志集填充一个DD_KERNELCALLBACKS结构，然后将这个结构拷贝到DD_GETDRIVERINFODATA结构的lpvData成员。

3. 它必须用指定的GUID_KernelCaps GUID调用DdGetDriverInfo回调。驱动程序填充DDKERNELCAPS结构，然后将这个结构拷贝到DD_GETDRIVERINFODATA结构的lpvData成员。

4. 内核用MajorFunction=IRP_MJ_PNP、MinorFunction=IRP_MN_QUERY_INTERFACE和InterfaceType=GUID_DxApi调用微端口驱动程序的IOCTL处理程序，这使得微端口驱动程序填充DXAPI_INTERFACE结构，且指针指向DirectDraw能够调用的微端口驱动程序的功能。

微端口驱动程序能够在DXAPI_INTERFACE结构的Context成员里指定一个值，该结构在每次调用其中一个功能时传递给微端口驱动程序。

3.8.3.2视频VBI捕获

DirectX 5.2引进了视频捕获的两个DirectDraw驱动程序功能：DxTransfer和DxGetTransferStatus。

DxTransfer 功能给视频和VBI捕获带来了便利。由于这个功能在IRQ时间调用，驱动程序必须调用DxTransfer并尽可能快的返回，如果显示硬件在DxTransfer被调用的时候没有准备执行一个总线主设备，驱动程序应该保留总线主设备数目的一个内部队列。（保存在队列里的总线主设备的实际数目由驱动程序开发者决定。）这使得当硬件准备好时它可以执行总线主设备，换句话说，驱动程序不应该查询和等待总线主设备。

当DirectDraw调用DxTransfer功能时，它在DDTRANSFERININFO结构的DxTransfer成员里返回一个转移的ID，然后当调用DxGetTransferStatus功能时，DirectDraw驱动程序能够使用这个ID。

当总线主设备完成时，驱动程序必须创建一个IRQ，然后驱动程序必须调用在DxEnableIRQ里指定的IRQCallback功能并指定DDIRQ_BUSMASTER标志之后，DirectDraw调用DxGetTransferStatus功能来决定完成哪一个总线主设备。驱动程序必须返回DirectDraw先前传递给驱动程序的转移的ID（dwTransferID），用这样的方法，驱动程序在队列里有五个总线主设备，DirectDraw能够决定哪一个是最近完成的。

3.9 扩展的表面定位

DirectDraw在每个堆的基础上支持表面分配需求，在DirectX 5.0中有对这个支持的介绍。驱动程序能够为矩形堆指定X和Y分配，并为线性堆指定行距和启动冲突分配，这些分配根据表面类型的不同而有变化。

一些显示硬件不能够在一个原子的操作里设置它的显示启动偏移，在刚开始显示时，当驱动程序未能彻底地设置这个值时，这样的一个硬件可能闭锁一个新的显示启动偏移。DirectDraw现在允许驱动程序为可见的后备缓冲器指定分配需求，一些硬件可能能够为潜在的后备缓冲器表达分配需求，该后备缓冲器迫使显示启动偏移为一个需要仅一个寄存器写入的值。这个技术能够帮助避免偶而的闪烁，当主平面在一个高频上交换时，这个闪烁就有可能是可见的。

3.9.1旧式定位方法回顾

在DirectX 5.0以前的DirectDraw版本允许驱动程序为线性堆表达行距分配需求。为了便于讨论，这些分配需求的DirectDraw的使用分为以下三步：

1. 创建表面并填充一个分配的IPitch成员，基于驱动程序的全局分配需求（如在VIDEOMEMORY结构返回的一样）和表面的ddsCaps成员，行距增加到成为适当的分配需求的一个倍数。

2. 如果定义了，就调用驱动程序的DdCreateSurface回调，驱动程序能够修改IPitch值，但是Windows 2000忽略了这个改变。

3. 如果驱动程序的调用没被处理或者它请求分配，从驱动程序的一个堆中给表面分配显存。被分配的表面的宽度适用于在第一步里所决定的行距，除非在第二步里驱动程序对行距做了修改。

如果驱动程序实现了DdCreateSurface回调，就能确保任何后继的表面具有其设置不分配值的lPitch成员。为了向后的兼容性，新的DirectDraw版本里仍旧保留了这个行为。除非驱动程序已经暴露了一个GetHeapAlignment入口点，第三步严格维持了同样的行为。如果，且仅仅如果定义了这个入口点，从前枚举的lPitch分配被丢弃了，且所有的表面分配遵从使用GUID_GetHeapAlignment而被报告的需求。当驱动程序在老的DirectDraw运行时运行时，驱动程序能够如它们一样保留它们的VIDEOMEMORYINFO结构分配需求，并期望同样的分配行为，这个分配行为在新的（DirectX 5.0及以后的版本）DirectDraw运行时已经被代替。暴露GetHeapAlignment关闭了所有堆的这个合法的分配程序，并非仅仅是GUID_GetHeapAlignment报告分配需求的那些堆。

3.9.2使用扩展的表面定位

为了使用扩展的表面分配功能，DirectDraw驱动程序必须在初始化时执行下面的任务：

n 它必须在DD_HALINFO结构里指定一个DdGetDriverInfo功能，这样DirectDraw就能够调用这个结构得到额外的信息。

n 它必须用指定的GUID_GetHeapAlignment GUID调用DdGetDriverInfo回调。驱动程序必须填充一个DD_GETHEAPALIGNMENTDATA结构，然后将这个结构拷贝到DD_GETDRIVERINFODATA结构的lpvData成员。

驱动程序应该为这个堆里需要分配的任何类型的表面，在HEAPALIGNMENT结构里驱动程序应利用DDSCAPS_xxxx标志的逻辑OR，来填充被指向的DDSCAPS结构。如果在DDSCAPS里设置了一个位，DirectDraw遵从在一个适当的SURFACEALIGNMENT结构成员里表达的分配限定。DDSCAPS_FLIP位和Flip Target成员适用于那些主交换链里的后备缓冲器的表面---也就是说，适用于一个潜在的主（可见的）表面。下面的列表给出了当前允许的表面能力设置，分配可被指定于这些能力：

n DDSCAPS_OFFSCREENPLAIN

n DDSCAPS_EXECUTEBUFFER

n DDSCAPS_OVERLAY

n DDSCAPS_TEXTURE

n DDSCAPS_ZBUFFER

n DDSCAPS_ALPHA

n DDSCAPS_FLIP

注意：DirectDraw比较一个新表面的能力与HEAPALIGNMENT结构里的入口点以便在这个结构里对其进行指定。例如，由于这是第一次指定了分配的可应用的能力位，所以按照HEAPALIGNMENT结构的Texture成员来分配具有DDSCAPS_MIPMAP|DDSCAPS_TEXTURE|DDSCAPS_FLIP设置的表面。（也就是说，在HEAPALIGNMENT结构里，Texture在Flip Target之前出现。）在这个例子里没有考虑Flip Target成员。既然没用DDSCAPS_FLIP和指定了分配的位来标志一个主交换链里的后备缓冲器，那么就可以按照Flip Target成员来指定这样的表面。可潜在地变成一个主交换链里（这些与主表面具有相同的象素格式和大小）成员的表面也按照Flip Target成员被分配。

3.10 扩展的表面能力

从DirectX 6.0开始，DirectDraw包括除在以前的版本里能看到的能力之外的表面能力，这些扩展的能力需要额外的几个新结构，特别是DDSCAPS2和DD_SURFACE_MORE结构。DDSCAPS2结构包括最初在DDSCAPS结构里发现的dwCaps成员，但是，它也包括三个新成员：dwCaps2、dwCaps3和dwCaps4。只有dwCaps2是用在DirectX 6.0的DirectDraw中，DDSCAPS2结构的最后三个成员也是相同的安排在DDSCAPSEX结构里。

3.10.1新的表面能力

当应用程序在DDSCAPS2结构的dwCaps2成员里设置适当的标志时，在DirectX 6.0里及更新版本添加给DirectDraw的新的表面能力对驱动程序来说是可见的。

应用程序仅可以在与DDSCAPS2_OVERLAY标志相连接时设置DDSCAPS2_HARDWAREDEINTERLACE标志，如果一个驱动程序在CreateSurface时间里看到这个标志设置，这意味着DirectDraw期望驱动程序需要做任何必要的事情来确保硬件视频端口帧速率与设备帧速率匹配。

DDSCAPS2_HINTDYNAMIC、DDSCAPS2_HINTSTATIC和DDSCAPS2_OPAQUE标志是应用程序在CreateSurface时间设置的暗示，它通知驱动程序应用程序打算利用表面做什么。DDSCAPS2_HINTDYNAMIC标志意味着应用程序将定期更新表面，DDSCAPS2_HINTSTATIC标志意味着将很少更新表面但是仍旧需要存取。这意味着驱动程序必须能够实现表面上的锁定，这包括隐藏的解压和压缩步骤。DDSCAPS2_OPAQUE标志意味着应用程序将从来不会为那个表面生命时间的剩余部分锁定、位块转移或者更新表面，驱动程序可以自由解压或者重新排序表面而不必解压它。

注意：驱动程序不需要设置使其可用的这些标志。当调用DdCreateSurface时，DirectDraw仅仅给驱动程序传递这些位。

你也许想使用DirectDraw的扩展堆限定特征（在“扩展的堆限定”里描述）来自动地在最优堆里放置DDSCAPS2_OPAQUE纹理，这完全取决于你。

在微软的Direct3D DDK文档里，有DDSCAPS2_HINTDYNAMIC、DDSCAPS2_HINTSTATIC和DDSCAPS2_OPAQUE标志的详细的描述。

DDSCAPS2_TEXTUREMANAGE标志与驱动程序不相关，这个标志通知Direct3D运行时，在适当时，它负责从显存的一个后备表面移动表面以能够加速3-D组织纹理。

3.10.2暴露扩展的表面能力

DDNTCORECAPS结构包括一个DDSCAPS域，驱动程序将填充这个域来表明它们支持什么类型的表面。在向应用程序报告这些caps时会返回一个稍微不同的结构，即DDCAPS。这个DDCAPS结构从驱动程序的DDNTCORECAPS和另外的结构中构造，使用DdGetDriverInfo接口查询这些结构。对DirectX 6.0来说，一个应用程序可见的DDCAPS包括一个DDSCAPS2成员，这个DDSCAPS2成员从DDNTCORECAPS结构的DDSCAPS成员和DD_MORESURFACECAPS结构的ddsCapsMore成员构成。

在驱动程序初始化时间里使用DdGetDriverInfo调用从驱动程序里查询DD_MORESURFACECAPS结构，如在ddrawint.h文件里定义的，适当的GUID应是GUID_DDMoreSurfaceCaps。

响应GUID_DDMoreSurfaceCaps查询完全是可选择的，这表示允许驱动程序做两个不同的事情：

n 暴露新的表面能力，驱动程序能够在显存里产生这种能力。

n 新的表面能力给DirectDraw传达这些作为堆的新限制的表面新能力。

第一项已在前面提到过而且每个人对此都有自己的解释。第二项更复杂，在读下一章之前，读者应该熟知VIDEOMEMORY结构的ddsCaps和ddsCapsAlt成员的重要性，在内存堆分配里有对它们的描述。

3.10.3扩展的堆限制

DD_MORESURFACES结构的大小可变，它总是有ddsCapsMore成员，但是它能够有零个或者多个ddsExtendedHeapRestrictions入口点。如果驱动程序响应GUID_DDMoreSurfaceCaps请求，它应该返回一个DD_MORESURFACECAPS结构，该结构包括如它在DD_HALINFO结构里返回的显示内存堆一样多的ddsExtendedHeapRestrictions入口点。（DirectDraw保证在驱动程序报告DD_HALINFO之后执行GUID_DDMoreSurfaceCaps请求。）

驱动程序也应该在DD_MORESURFACECAPS结构里填充一个适当的dwSize值，dwSize值按这种方法计算：

DDMORESURFACECAPS.dwSize = (DWORD) (sizeof(DDMORESURFACECAPS)

+ (((signed int) DDHALINFO.vimData.dwNumHeaps)-1)

* sizeof(DDSCAPSEX)*2);

注意到-l说明了将声明DD_MORESURFACECAPS结构有一个tagExtendedHeapRestrictions成员。

ddsCapsEx 和ddsCapsExAlt成员与IDEOMEMORY结构数组dsCaps和ddsCapsAlt成员严格相似，该结构在OMEMORYINFO结构的pvmList成员里返回。IDEOMEMORYINFO结构作为DD_HALINFO结构的一个成员而被包含。任何设置在ddsCapsEx里的位意味着具有这种位设置的表面不能够放置在那个堆里，任何设置在ddsCapsExAlt成员里的位意味着表面不能够在那个堆里放置。当分配表面时，DirectDraw首先经过所有的堆，且如果它发现对一个堆来说，在VIDEOMEMORY结构里的ddsCaps成员的能力位不能匹配那个表面的DDSCAPS位，则它在那个堆里分配表面。如果这个传递没有发现这样的堆，DirectDraw就做同样的传递，但要检查ddsCapsEx域。如果这个传递未能发现任何堆，则不能够在任何堆里创建表面。

3.10.4扩展的表面描述结构

扩展的DirectDraw表面描述结构的DDSURFACEDESC2与DDSURFACEDESC结构相同，作为除了在结构的末端的指向DDSCAPS结构的指针已经用指向DDSCAPS2结构的一个指针代替。

DdCreateSurface 和DdCanCreateSurface驱动程序调用的数据块每个包括指向DDSURFACEDESC结构的指针。至于DirectX 6.0数据块，这些指针可能实际上指向DDSURFACEDESC2结构，即使这些指针被定义为LPDDSURFACEDESC。如果驱动程序选择，它能够检查DDSURFACEDESC指针的dwSize成员，然后决定指针是否实际上指向一个DDSURFACEDESC2结构。如果你的驱动程序必须在比DirectX 6.0更早的版本上运行，它就必须做这个检查。

如果返回的大小是sizeof（DDSURFACEDESC2），驱动程序然后能够检查DDSCAPS2结构的dwCaps2、dwCaps3和dwCaps4成员。

3.11 压缩的纹理表面

一个表面可以包括将被用作组织纹理3-D对象的一个位图，为了减少纹理消耗的内存量，DirectDraw支持纹理表面的压缩。

注意：压缩过的纹理表面来得到新回调的支持，DirectDraw通过现存的驱动程序回调向驱动程序传递压缩的纹理表面信息。

表3.1给出了驱动程序支持的压缩纹理的五种类型。

表3.1压缩的纹理类型

FourCC Description AlphaPremultiplied？

DXT1 Opaque/one-bit alpha n/a

DXT2 Explicit alpha Yes

DXT3 Explicit alpha No

DXT4 Interpolated alpha Yes

DXT5 Interpolated alpha No

如果要了解更多压缩纹理格式的信息，参见SDK平台文档的DirectDraw部分“压缩的纹理格式”。

3.11.1枚举DXT格式

在DirectX里，驱动程序有两种方法枚举象素格式。第一种方法枚举使用于纹理的格式，该方法由D3DNTHAL_GLOBALDRIVERDATA结构的lpTextureFormats成员实现。第二种方法枚举DDSCAPS_OVERLAY表面或DDSCAPS_OFFSCREENPLAIN表面用到的格式。第二种方法使用包括在DD_HALINFO结构和lpdwFourCC数组里的DDNTCORECAPS结构的dwNumFourCCCodes成员，该lpdwFourCC数组也包括在DD_HALINFO结构里。

由于DXT格式是主要作为纹理使用的，所以驱动程序仅仅通过第一种方法枚举DXT格式，无须给lpdwFourCC数组添加DXT格式。

3.11.2创建压缩的纹理表面

无论何时DirectDraw请求驱动程序创建表面，驱动程序必须决定是否它被请求创建一个压缩的纹理表面。为了做出决定，它必须检查DirectDraw以前在DDSURFACEDESC2结构里已经为正在创建的表面所设置的信息。驱动程序必须包括下面的校验步骤（对任何表面来说）：

n 检查DD_CANCREATESURFACEDATA结构的bIsDifferentPixelFormat成员，从而决定创建的表面的象素格式是否要与主表面一样。

n 如果不是，在DDCAPS结构的dwFlags成员里检查DDSCAPS_TEXTURE标志。

n 在DDPIXELFORMAT结构的dwFlags成员里检查正在创建的表面的DDPF_FOURCC标志。

n 在DDPIXELFORMAT结构的dwFourCC成员里检查正在创建的表面的一个DXT代码。

n 检查高度和宽度（DDSURFACEDESC2的dwWidth和dwHeight），DirectDraw将设置这些值为若干个四个象素。

3.11.3使用压缩的纹理表面

仅仅当设置了DDCAPS2_COPYFOURCC标志时，DirectDraw才调用驱动程序在两个具有相同类型的表面之间进行位块转移。如果没有设置这个标志，DirectDraw HEL将执行位块转移。由于这是一种将纹理从后备表面下载到显存上的机制，所以这对后备表面到显示拷贝位块转移是最重要的。其结果是，暴露DXT纹理表面可有效地要求驱动程序支持DDCAPS2_COPYFOURCC标志。

DDCAPS2_COPYFOURCC标志有一些另外的含义，驱动程序必须能够用至少以下的这些属性在FourCC格式之间执行位块转移：

n 源和目标格式是相同的FourCC。

n 源和目标表面不是同样的表面。

n 源和目标表面大小一样。

n 源和目标矩形都是整个的表面（也就是说，没有延伸也没有子矩形）。

n 两个表面都在显存里。

n 驱动程序必须能够为每个在显存里支持的FourCC格式执行这个位块转移。

注意：DirectShow使用DDCAPS2_COPYFOURCC标志来加速一些视频功能，所以这个标志必须暗示能够拷贝所有的FOURCC格式。

如果位块转移操作要求压缩一个DXT格式，DirectDraw HEL就总能执行这个位块转移，这意味着DirectDraw从来没有请求驱动程序为以下执行位块转移：

n 目标表面有一个DXT格式。

n 源和目标表面的格式不相同。

DirectDraw的DDCAPS_CANBLTSYSTEM能力位语义暗示显示驱动程序为所有的后备表面到显存的位块转移调用。因此，可能从DXT表面到非DXT表面为所有的后备表面到显存的位块转移调用驱动程序，在这种状况下的唯一要求是如果驱动程序不能够执行解压，就返回DDHAL_DRIVER_NOTHANDLED。这将引起DirectDraw传播一个DDERR_UNSUPPORTED错误代码给应用程序，在驱动程序里为后备表面到显存的位块转移实现解压是可接受的，但不是必须的。

DirectDraw显示内存分配例程对象素格式不考虑。举例来说，当它的参数输入时，HeapVidMemAllocAligned需要一些字节。同样的，DDHAL_PLEASEALLOC_BLOCKSIZE表示DD_SURFACE_GLOBAL结构的dwBlockSizeX和dwBlockSizeY成员分别是一些字节和行数。因此，如果驱动程序使用这些机制中的任何一种来通过DirectDraw分配器分配显存，驱动程序必须能够以一个DXT表面自己的字节枚举内存消耗。

当应用程序在一个压缩的表面上调用它的lock或GetSurfaceDesc方法时，驱动程序必须在DDSURFACEDESC2结构的dwFlags成员里设置DDSD_LINEARSIZE标志。此外，驱动程序必须设置被分配的字节数以便在相同结构的dwLinearSize成员里包括压缩的表面数据。（dwLinearSize成员存留在具有lPitch成员的一个联盟里，这样这些成员互相是唯一所选的，如DDSD_LINEARSIZE和DDSD_PITCH标志一样。）

你的硬件或驱动程序能够在你选择的（典型地是一个额外的硬件效率布局的重新排序）任何格式里转换和存储压缩的纹理。然而，硬件或驱动程序必须能够转换压缩的纹理返回为它最初的DXT代码格式。（无论何时DirectDraw需要它----也就是说，无论何时当应用程序调用lock方法。）

3.12 运动补偿

如果两个连续的视频数据帧大致显示了相同的图片，仅仅是做了水平或垂直上的转换，那么只需通过转换第一个帧，然后描述除到达第二个帧时的转变之外的其他改变，就可以取得一个巨大的压缩增益。这就是动画补偿所做的。

动画补偿是MPEG解码处理的最后阶段，许多显示设备为硬件里的这个执行一些支持级。除了动画补偿外，一些设备执行相反的离散余弦传输（IDCT）和另外的硬件功能，软件MPEG译码器能够使用这些硬件功能来加速译码处理。动画补偿设备驱动程序接口足够灵活以处理这些设备。

软件MPEG译码器的输入数据被很好地定义了，也就是说，如果译码器为MPEG-2而设计，将以MPEG-2格式输入。译码器的输出也被很好的定义---许多格式的未被压缩的帧。然而，却没有对在软件译码器和显示设备之间的临时格式进行良好的定义；许多设备需要它们自己的属性数据格式。因此，动画补偿设备驱动程序接口是灵活可变的，且临时格式被描述为GUID。显示驱动程序报告描述它支持能力的GUID，软件译码器选择最好地匹配了它的需求的GUID。

为了使用动画补偿功能，驱动程序必须做下面的工作：

n 执行DdGetDriverInfo功能并设置DD_HALINFO结构的DdGetDriverInfo成员以便当DrvGetDriverInfo调用时指向这个功能，驱动程序的DdGetDriverInfo功能必须分析GUID_MotionCompCallbacks GUID。

n 当用GUID_MotionCompCallbacks GUID调用DdGetDriverInfo时，用适当的驱动程序回调和标志设置填充DD_MOTIONCOMPCALLBACKS结构。驱动程序然后拷贝这个被初始化过的结构到DirectDraw-allocated缓冲器，这里DD_GETDRIVERINFODATA结构的lpvData成员指向这个缓冲器，并返回在dwActualSize里写入到缓冲器里的字节数目。

3.13 返回DirectDraw和Direct3D的值

负值表示了错误，且错误不能够被结合。在下一页有一些值，这些值由在Windows 2000 DDK里描述的DirectDraw和Direct3D功能返回。为了获得每个功能能够返回的错误代码的一个列表，参见参考部分里的功能描述。如要了解一个完全的错误代码列表和返回的数值，请参考DirectDraw头文件ddraw.h。

DirectDraw驱动程序必须返回两个返回码中的一个：DDHAL_DRIVER_HANDLED或者DDHAL_DRIVER_NOTHANDLED。如果驱动程序返回DDHAL_DRIVER_HANDLED，它也必须返回DD_OK，或者在ddraw.h中列出的错误码之一。

DD_OK

请求完全成功。

DDHAL_DRIVER_HANDLED

驱动程序已经执行了操作并为此操作返回一个合法的返回码。返回码在传给驱动程序回调的结构的成员ddrval中。如果是DD_OK，DirectDraw或Direct3D继续执行。否则，DirectDraw或Direct3D返回由驱动程序提供的错误码，并放弃执行。

DDHAL_DRIVER_NOTHANDLED

驱动程序对请求的操作不处理。如果驱动程序被要求实现一个特别的回调，DirectDraw或者Direct3D报告一个错误条件。否则，DirectDraw或者Direct3D通过执行设备独立的实现来处理此操作，就好像驱动程序的回调没有定义。DirectDraw或者Direct3D忽略任何在回调的结构参数的ddrval成员中返回的值。

DDHAL_DRIVER_NOCKEYHW

显示驱动程序不能处理调用，因为它用尽了颜色键硬件资源。

DDERR_UNSUPPORTED

操作不被支持。

DXERR_GENERIC

有一个未定义的错误条件。

DXERR_OUTOFCAPS

请求的操作需要的硬件已经被分配。

DXERR_UNSUPPORTED

操作不被支持。


Direct3D DDI

微软Direct3D®设备驱动程序接口(DDI)是一个图形接口，它允许厂商为Direct3D提供硬件加速。这个接口很灵活，厂商可以基于硬件能力来提供Direct3D加速。驱动程序的编写者把实现Direct3D设备驱动程序接口作为显示驱动程序的完整部分。

这一章描述了Direct3D的体系结构和接口，并对Direct3D驱动程序的编写者提供了实现上的指导。它假设编写者熟悉Direct3D和微软DirectDraw APIs，对于Windows NT/Windows 2000显示驱动程序模型,包括DirectDraw设备驱动程序接口编程有牢固的掌握。

所有的为Windows 2000编写的Direct3D驱动程序必须符合微软Direct X 7.0 Direct3D驱动程序模型。Direct X 6.0 驱动程序模型在Windows 2000正式版中不再被支持。（为了适应升级的需要，Windows 2000 Beta3仍然支持DirectX 6.0）

4.1 Direct3D DDI 头文件、示例代码和参考

Windows NT®/Windows 2000®的驱动程序编写者开发微软Direct3D驱动程序应该引用以下头文件：

d3dnthal.h

包含了由驱动程序实现的回调原型和为驱动程序层次结构定义。枚举类型D3DNTHAL_DP2OPERATION就定义在这个文件中。这个头文件被包含在winddi.h中，winddi.h包括了所有Windows NT/Windows 2000的显示驱动程序。

d3dtypes.h

包含了应用程序和驱动程序都要用到的Direct3D类型定义。除了D3DNTHAL_DP2OPERATION，所有的Direct3D枚举类型都定义在此头文件中。

d3dcaps.h

包含了描述Direct3D驱动程序各方面的结构和定义。

dx95type.h

允许编写者写可在Windows NT/Windows 2000和Windows 95/98间移植的代码。

ddrawint.h

这个头文件用来开发显示驱动程序的DirectDraw部分。它包含在winddi.h中。

所有这些头文件都和Windows 2000 DDK一起发布。Windows 2000 DDK也为Direct3D驱动程序提供了示例代码，它位于permedia2 视频显示器目录中。

Direct3D DDI函数、回调和结构的参考可在Windows 2000 DDK Graphics Driver Reference找到。

Direct3D接口和SDK相关的方面的参考在平台SDK文档中。

对于缺乏图形编程经验的程序员，可以参考Foley、van Dam、Feiner和Hughes所著的《计算机图形：原理和实践》。

4.2跨平台Direct3D驱动程序开发

因为命名的差异和一些成员类型的改变，编译时，Windows NT/Windows 2000和Windows 95/98 Direct3D DDI类型并不直接兼容。但是逻辑上所有的成员用于同一目的。

随同Windows 2000 DDK提供了dx5type.h，它用于辅助编写能在Windows NT/Windows 2000和Windows 95/98之间移植的代码。它包含一整套类型和宏定义，用以映射在Windows NT/Windows 2000和Windows 95/98见的命名差异，从而使一些公共代码对两者都能使用。

4.3 Direct3D驱动程序的回调函数

以下几节列出了包含Windows NT/Windows 2000 Direct3D DDI的回调函数。有一些回调必须的，另外一些是可选的。是否包括它们取决于硬件的性能。回调的名字是驱动程序的编写者决定的伪名字。

Direct3D DDI回调的原型如下：

typedef DWORD （APIENTRY *LPD3DNTHAL_MYFUNCTIONCB）（LPD3DNTHAL_MYFUNCTIONDATA）;

这里，

l LPD3DNTHAL_MYFUNCTIONCB指向一个驱动程序实现的回调，称为MyFunction。所有回调的名字都是假设的，由驱动程序的编写者来决定。

l LPD3DNTHAL_MYFUNCTIONDATA是一个指向D3DNTHAL_MYFUNCTIONDATA结构的指针,它被传送给回调函数。回调参数结构的特点如下：

l 每一个结构的第一个成员dwContext是设备环境句柄，它描述了回调操作时的三维环境。唯一的例外是D3DNTHAL_CONTEXTCREATEDATA结构。

l 每一个结构的最后一个成员是ddrval。这个成员用来将回调的返回值传给Direct3D，从而可以返回给调用的应用程序。

决定如何初始化Direct3D回调参见Direct3D驱动程序初始化。

4.3.1请求的Direct3D驱动程序回调函数

下表列出了必须在Direct3D 驱动程序中实现的Direct3D回调函数。

请求的回调 | 描述
D3dContextCreate | 环境的创建和注销
D3dContextDestroy |
D3dCreateSurfaceEx | 在纹理句柄和表面之间生成一个联系
D3dDestroyDDLocal | 注销所有以前由D3dCreateSurfaceEx生成的Direct3D表面，它属于同一个给定的本地DirectDraw对象
D3dDrawPrimitives2 | 着色原语并返回对Direct3D的更新状态
D3dGetDriverState | 返回DirectDraw和Direct3D运行时的状态信息
D3dValidateTextureStageState | 执行纹理阶段状态刷新，所有支持纹理的驱动程序都要请求

为了支持Direct3D，驱动程序编写者最少必须要支持DirectDraw。这个驱动程序也必须实现某些DirectDraw DDI函数和回调。那些有关对Direct3D的支持列在下表中。

DDI函数 | 描述
DrvGetDirectDrawInfo | 这个函数提取了图形硬件的性能。它位于初始化函数中指示它支持Direct3D
DdGetDriverInfo | 运行时用GUID查询这个回调以获取驱动程序的额外信息。有几个GUID特别属于驱动程序的Direct3D支持。

DirectDraw函数和回调的实现细节在DirectDraw DDI文档中讨论。

4.3.2操作码处理

为了支持DirectX 7.0 Direct3D驱动程序模型，必须使驱动程序对D3dDrawPrimitives2中的许多新的操作码做出反应。这些操作码中，一些代替旧的请求的回调和有条件请求的回调，而另外一些提供了新的功能。最重要的新操作码总结在下表中，从被代替的回调开始。

操作码 | 条件/描述
D3DDP2OP_SETRENDERTARGET | 总是必要的。在当前环境中映射一个新的着色目标表面和深度缓冲区。替换了D3dSetRenderTarget。
D3DDP2OP_CLEAR | 总是必要的。用来清除环境的着色目标。替换了D3dClear2。也用来清除硬件模板缓冲区以及深度缓冲区，它通常不能被深度填充位块完全清除。
D3DDP2OP_SETPALETTE | 用来在调色板句柄和表面句柄间映射一个联系，并描述调色板的特性。仅在支持调色版纹理的驱动程序中需要，否则应置为NO_OP。
D3DDP2OP_UPDATEPALETTE | 用来对支持调色版纹理的驱动程序更改调色版纹理。否则应置为NO_OP。
D3DDP2OP_TEXBLT | 描述了从源纹理到目标纹理的一个BLT操作。

其他有用的操作码在参考部分的D3DNTHAL_DP2OPERATION描述中总结。

4.3.3对Direct3D驱动程序回调的返回码

对所有驱动程序回调的返回值必须是以下值之一：

D3DHAL_CONTEXT_BAD

说明传送的环境不合法。

D3DHAL_OUTOFCONTEXTS

说明在这个处理中在没有更多的环境。

DDHAL_DRIVER_HANDLED

驱动程序已经执行了操作并为那个操作返回一个合法的返回码,此操作是将结构的成员ddrval传送给驱动程序的回调。如果返回码是DD_OK，Direct3D继续处理此函数。否则，Direct3D返回由驱动程序提供的错误码并放弃此函数的处理。

DDHAL_DRIVER_NOTHANDLED

驱动程序对于请求的操作不做处理。如果驱动程序被要求实现一个特别的回调，Direct3D报告一个错误条件。否则，Direct3D处理这个操作就好像驱动程序回调没有被定义一样，它执行Direct3D的设备独立的实现。通常Direct3D忽略任何由回调的参数结构成员ddrval返回的值。

4.4 Direct3D驱动程序初始化

当驱动程序的函数DrvGetDriectDrawInfo被DirectDraw运行时所调用以便初始化DirectDraw支持，驱动程序必须做以下的事以指示它的Direct3D能力：

在结构DD_HALINFO的成员ddCaps.dwCaps设置DDCAPS_3D标志以指示此驱动程序的硬件有三维加速功能。

在结构DD_HALINFO的成员ddCaps.ddsCaps设置以下任何DDSCAPS_Xxx标志以描述此驱动程序的视频存储器的三维能力：

DDSCAPS_3DDEVICE

指示驱动程序的表面能被用来作为三维着色的目标。

DDSCAPS_TEXTURE

指示驱动程序的表面能被用来做三维纹理映射。

DDSCAPS_ZBUFFER

指示驱动程序的表面能被用来做Z缓冲区。

设置结构DD_HALINFO的成员GetDriverInfo以指向驱动程序的回调函数DdGetDriverInfo。驱动程序必须设置DDHALINFO_GETDRIVERINFOSET标志,此标志在结构DD_HALINFO的成员dwFlags中。以指示它已实现了DdGetDriverInfo回调功能。

申请和初始化结构D3DNTHAL_CALLBACKS的成员并且将此结构返回到结构DD_HALINFO的成员lpD3DNTHALCallbacks中。

申请和初始化结构D3DNTHAL_GLOBALDRIVERDATA的成员并且将此结构返回到结构DD_HALINFO的成员lpD3DGlobalDriverData中。

为了指示能和DriectX 7.0协调工作，驱动程序应当作以下事情：

在结构D3DNTHALDEVICEDESC_V1的devCaps域包含D3DDEVCAPS_DRAWPRIMITIVES2EX标志，这在Direct3D驱动程序初始化时被报告。

通过设置结构DD_MISCELLANEOUS2CALLBACKS的GetDriverState，CreateSurfaceEx和DestroyDDLocal域对回调DdGetDriverInfo中的GUID_Miscellaneous2Callbacks响应。这些都是指向Driect3D驱动程序适当回调的指针,并且要将域dwFlags的DDHAL_MISC2CB32_CREATESURFACEEX，DDHAL_MISC2CB32_GETDRIVERSTATE和DDHAL_MISC2CB32_DESTROYDDLOCAL位分别置位。

在DrvGetDirectDrawInfo返回后，GDI为不同的GUID调用驱动程序的回调DdGetDriverInfo以完成驱动程序的初始化。DdGetDriverInfo必须对以下的GUID响应以支持Direct3D：

GUID_D3Dcallbacks3

驱动程序应申请和初始化结构D3DNTHAL_CALLBACKS3的成员，并将此结构返回到结构DD_GETDRIVERINFODATA的成员lpvData中。

GUID_Miscellaneous2Callbacks

驱动程序应申请和初始化结构DD_MISCELLANEOUS2CALLBACKS的成员, 并将此结构返回到lpvData中。

GUID_D3DextendedCaps

驱动程序应申请和初始化结构D3DNTHAL_D3DEXTENDEDCAPS中适当的成员, 并将此结构返回到lpvData中。

GUID_ZpixelFormats

驱动程序应为每个它所支持的Z缓冲区格式申请和初始化结构DDPIXELFORMAT中适当的成员，并将此结构返回到lpvData中。如果此驱动程序在它实现D3drawPrimitives2时支持D3DDP2OP_CLEAR操作码,它就必须响应这个GUID。

GUID_D3DparseUnknownCommandCallback

驱动程序应存储指向Direct3D运行时的回调函数D3DparseUnknownCommand的指针。这个指针被传递给驱动程序的lpvData。驱动程序的回调D3drawPrimitives2将调用D3DparseUnknownCommand以解析驱动程序不能识别的命令。

参考第三章的DirectDraw驱动程序初始化以更详细的了解如何初始化DirectDraw驱动程序。

4.5 Direct3D环境管理

一个环境为应用建立的Direct3D硬件抽象层（HAL）设备封装了所有的状态信息；也就是此环境描述了驱动程序应当如何“画”。状态包括表面着色、深层表面、阴影信息、纹理信息等信息。

Direct3D驱动程序负责建立和管理它自己的着色环境。

4.5.1 建立环境

驱动程序必须建立和初始化设备相关的环境以封装所有它需要执行着色的信息。状态并不在多个环境中所共享，因此驱动程序必须为每个它所生成的环境维护完整的状态信息。

为建立环境，驱动程序应当：

申请设备相关的环境并以零初始化它。

参见DDK在线文档的D3DcontextCreate获得更详细的信息。当应用建立Direct3D HAL设备时D3DcontextCreate被调用。

驱动程序必须能够在已经建立的环境中引用所有的由D3dCreateSurfaceEx建立的纹理句柄，这样当调用D3dContextDestroy时，它能在这个环境中清除所有设备相关的纹理数据。

4.5.2 删除环境

当应用程序请求注销Direct3D HAL设备时，Direct3D调用D3dContextDestroy。驱动程序应当释放它对此环境申请的所有资源。

4.5.3 在环境中维护状态

当驱动程序的回调函数D3DrawPrimitives2被调用时，驱动程序更新它所联系的环境的内部状态。此函数同时必须返回更新过的环境的公共状态给Direct3D。参见DDK在线文档的D3DrawPrimitives2获得更详细的处理和更新状态的信息。

4.5.4 Direct3D表面处理总览

DirectX 7.0 DDI提出了一个模型，Direct3D运行时组件在把命令传递给驱动程序前解析尽可能少的命令流。命令流应是格式化的，这样理论上在将来它能够被硬件直接理解。

对这些目标的一个重要改变是所有表面相关的数据从Direct3D/DiretcDraw拥有的中间结构移到了由驱动程序拥有并更新和格式化的结构中。

处理内嵌的命令流涉及到表面。在这些高频率的操作中，驱动程序能很容易的从句柄察看它自己的表面请求，而不必借助于潜在的、代价高的锁住表面的操作如EngLockDirectDrawSurface。

分派这些句柄的机制是一个新的驱动程序入口点称为D3dCreateSurfaceEx。在现存的DdCanCreateSurface和DdCreateSurface调用以及视频存储器的地址和其句柄被赋给一个表面之后，这个入口点被直接调用。在调用DdCreateSurfaceEx时，驱动程序可以从DiretcDraw的运行时的表面结构中拷贝所有的相关信息倒它自己的表面结构中。表面数据如尺寸、各适和fpVidMem经常是被拷贝的对象。

句柄对于每个设备和每个进程都是独一无二的，这是由运行时所保证的。但对于每个环境句柄并不保证是独一无二的；这一点将在下一节做更详细的讨论。

对于DestroySurfaceEx调用没有响应，所以驱动程序一方的表面结构在DdDestroySurface运行时被注销。

4.5.5 用D3dCreateSurfaceEx建立驱动程序方的表面结构

DiretcDraw运行时在它调用DdCreateSurface入口点后会调用驱动程序的D3dCreateSurfaceEx入口点并会为此表面申请内存。它仅为那些标记为DDSCAPS_TEXTURE，DDSCAPS_EXECUTEBUFFER，DDSCAPS_3DDEVICE或者DDSCAPS_ZBUFFER的表面调用D3dCreateSurfaceEx。

在调用D3dCreateSurfaceEx前，运行时会为表面分配一个整型值作为句柄。这个值存储在结构DDRAWI_DDSURFACE_MORE的成员dwSurfaceHandle中。

这些整型值从一开始，并尽量小（0对于表面句柄是不合法的）。目的是使驱动程序能保持指向它自己表面的一个指针数组。当驱动程序收到超过数组大小的句柄时（即D3dCreateSurfaceEx被调用）它能重申请数组的大小并继续运行。Direct3D在D3dCreateSurfaceEx没有生成句柄以前并不将句柄值传递给驱动程序。但是，驱动程序应当足够健壮以处理超范围的值或引用已经释放的句柄（即对某个句柄调用DdDestroySurface）。注意因为0是非法值，在句柄表中的0项能被用作别的目的。Permedia2例子驱动程序使用0项存储当前数组的长度。

4.5.6 为什么驱动程序方的结构不能是一个指向表面的指针？

你也许试图在你的驱动程序方的表面结构中使用一个指向DiretcDraw表面结构的指针。这将会在Windows 2000中失败，因为DiretcDraw的内核数据结构的访问通过将这些结构和用户模式及驱动程序隔离开的管理配置来作为中介。这就是为什么EngLockDirectDrawSurface存在的原因。它提供了一个指向结构的指针，这个指针在很短的时间内有效，直到EngUnlockDirectDrawSurface被调用。在这个锁/解锁操作以外，此结构并不保证处在相同的位置。此外，这个锁/解锁操作对于性能并不一定是构成阻碍。如果驱动程序保留它自己的表面结构的拷贝，锁就不需要。不常用的调用如D3dCreateSurfaceEx使驱动程序方的表面结构的数据被更新。结果使经常用的调用如D3DrawPrimitives2需要更少的代码来执行。

4.5.7 D3dCreateSurfaceEx和后援表面

D3dCreateSurfaceEx也为后援表面被调用。这允许驱动程序为表面申请驱动程序方的结构，从而响应D3DDP2OP_TEXBLT令牌。

驱动程序不会由于它不支持像素格式，当为后援表面格式调用D3dCreateSurfaceEx时失败，这是很重要的。这样的表面也许被建立来和软件光栅使用。驱动程序应当简单的忽略它不支持的后援表面。（它能建立驱动程序方的结构，但相应的句柄却不会传送给驱动程序）驱动程序必须要小心，因为对这些表面调用D3dCreateSurfaceEx失败会引起失效码传播到应用程序。因此驱动程序能潜在的影响一个应用程序。通过运行DirectX 7.0 SDK中的程序ddtest.exe，能测试驱动程序对此种情况的响应。运行此程序，建立一个驱动程序不支持但是DirectDraw仿真层却支持的后援表面纹理格式。

4.5.8 使用D3dCreateSurfaceEx要考虑的其他问题

某些情况能引起为一个已经注销的表面调用D3dCreateSurfaceEx。驱动程序通过忽略任何为fpVidMem是0的视频存储器表面的D3dCreateSurfaceEx调用来处理这种情况。

4.5.8.1 D3dCreateSurfaceEx和复杂表面

正如在DirectDraw文档中所解释的那样，复杂表面的建立引起一个表面数组被传给DdCreateSurface。但是，即使在很复杂的情况下，仅有一个指向根表面的指针被传给D3dCreateSurfaceEx。驱动程序必须从根表面遍历链表，建立所有表面的驱动程序一方的拷贝。如果驱动程序试图处理如立方图、mipmap等的建立，这会是个技巧型的操作。

请温习第三章的DirectDraw驱动程序基础并参见D3dCreateSurfaceEx参考中的示例代码。

4.5.8.2 D3dCreateSurfaceEx和MipMap

在一个mipmap的每一级都联系有不同的句柄值，但这些句柄不一定要连续。Direct3D被设计为仅将顶级表面的句柄作为参数传送给SetTexture函数，然后当前级的细节被另外的着色状态所说明。最自然的方法是构造驱动程序方的结构以代替整个mipmap。

4.5.8.3 D3dCreateSurfaceEx句柄和Flip

DirectDraw的表面结构被设计成代表概念表面，并不必须说明视频存储器的位置。这个抽象的主要用法在于一个主要的flipping链，应用程序使用一个恒定的表面对象代表后台缓冲区，即使由于DdFlip的操作结果使此缓冲区在视频存储器中移动。

DdFlip方法接受一个表面环并且绕着环旋转视频存储器指针。在有两个表面对象的情况下，处理被简化为交换它们的视频存储器的指针。此外，DirectDraw旋转每个表面相联系的D3dCreateSurfaceEx句柄和驱动程序拥有的dwReserved域的内容。这样的行为对DirectX 7.0驱动程序带来一些有趣的后果并有效的排除了将指向DirectDraw的表面结构的指针嵌入到驱动程序自己的表面结构中。

考虑两个表面对象A和B，句柄分别为Ha和Hb，fpVidMem是Fa、Fb。假设应用程序正使用表面结构A来引用一个flipping链的的后台缓冲区，在DdFlip操作时，句柄和fpVidMem被交换，这样A有Hb、Fb，B有Ha、Fa。应用程序现在想对后台缓冲区绘画，应用认为在Fb处表面A代表了视频存储器。一个绘图命令被送给驱动程序，然后它查询和表面相联系的句柄（现在是Hb而不是Ha）。

如果驱动程序仅仅存储指向DirectDraw的表面结构的指针，将会怎样？驱动程序将查到Hb，然后沿着指向表面B的指针，它现在有一个fpVidMem为Fa的值。绘画将从Fa处的视频存储器开始。、这并不是应用所期望的。另外一方面，如果驱动程序存储表面数据在它自己的结构中而不是指针指向DirectDraw的表面结构，Hb会解析出Fb，绘画将画在正确的表面上。后者正是当前DDI实现的方式。

4.5.8.4 D3dCreateSurfaceEx句柄和DirectDraw DDI

句柄没有完全隔离DirectX 7.0和DirectDraw管理的DDRAWI_DDSURFACE结构。在DirectDraw DDI（DdBlt、DdFlip等）中，驱动程序仍然传递表面结构的指针并仍然需要使用这些结构。

4.5.8.5 当恢复复杂的Flipping链时要考虑的问题

当一个复杂的主表面建立时，它不一定有一个依附的Z缓冲区。当表面被恢复，应用程序可能增加一个相依附的Z缓冲区。当在D3dCreateSurfaceEx中遍历表面链表时，应注意这些问题。Permedia2例子驱动程序提供了一种典型的技术，就是当为一个表面调用D3dCreateSurfaceEx时，标记此表面的dwReserved域。驱动程序标记此表面仅仅在fpVidMem不为0时。这是因为由于应用程序恢复了一个主表面，此表面有一个后台缓冲区和一个依附的Z缓冲区，但Z缓冲区还没有被恢复，从而fpVidMem也许是0。在以后，应用程序将恢复Z缓冲区，然后驱动程序才能标记它。注意如果应用程序在恢复主链前恢复Z缓冲区，驱动程序也许接收已经标记的Z缓冲区，并当调用D3dCreateSurfaceEx时将其依附于后台缓冲区。

4.5.8.6 表面句柄、丢失表面和环境

丢失表面的概念在DirectDraw SDK文档中介绍。丢失表面在DirectX 7.0 DDI模型中有一些涵义。

运行时的表面对象的生命期比驱动程序的表面对象要长。在几种情况下，最明显的模式改变导致表面丢失。这意味着驱动程序的表面对象在调用DdDestroySurface时被注销，但是运行时的表面对象仅仅被置于一个挂起状态，称为“丢失”。以后，运行时对象能被恢复，这对应着驱动程序级的DdCreateSurface调用。

一些表面必须被引用到一个环境中（如着色目标、Z缓冲区、纹理），典型情况下是通过存储驱动程序环境中那些表面的句柄。这些表面也许已经丢失，就驱动程序而言即为注销。环境也许有已经过时的句柄指向注销掉的表面。运行时保证在这种状态下没有着色命令被传给环境，但是仍有一个问题，即在着色能继续前，怎样联系环境和恢复的表面。运行时保证丢失表面的句柄不会改变。这反过来保证了如果环境保持表面的句柄（着色目标、Z缓冲区、纹理），它就不需要担心被这些句柄引用的表面对象已经被注销。这些表面将重新建立（恢复）成同一个句柄值，从而在这个环境中着色能继续。

一些驱动程序的编写者也许想优化环境的状态，通过存储表面数据在环境中而不是对每个D3dDrawPrimitives2调用做重复引用句柄的工作。由于同执行一个D3dDrawPrimitives2批处理的代价相比，重复引用的代价是很小的，这种优化并不被鼓励。但是如果真的想这样做，你必须清楚当表面恢复后，它也许在显存中的位置已经移动了。这意味着尽管句柄相同，但是并不能保证恢复的表面的fpVidMem指针和它建立时的一样。在这种方式下被优化的环境有过时的显存指针，没有已经移动的表面的信息。

处理这种情况是相当有技巧性的，但有几种方法是可能的。一种方法是驱动程序标记任何联系到环境的表面（着色目标、Z缓冲区、纹理），然后在D3dCreateSurfaceEx中，它能搜索任何引用此表面的环境并更新环境。

并不推荐表面保持环境的指针，因为表面也许和多个环境相联系。

4.6 Direct3D纹理管理

尽管纹理支持是可选的，大多数驱动程序能支持它。支持纹理映射的驱动程序必须对所有Direct3D DDI中纹理相关的操作码做出响应。

驱动程序也必须用回调D3dValidateTextureStageState验证纹理阶段状态。

4.6.1 多纹理

Direct3D驱动程序能支持同时使用多个纹理。这种多纹理支持在驱动程序使用纹理阶段状态类型（D3DTEXTURESTAGESTATETYPE）时可用。这些允许纹理的多个属性被定义并和定点扩展结合，它们说明了独立的一套纹理坐标数据。

对Direct3D驱动程序增加多纹理支持需要设置正确的性能位（Caps），实现纹理混合并实现D3dValidateTextureStageState。

为了和Direct X 6.0和后续版本兼容，驱动程序必须适当的解析八个纹理坐标系列，即使此设备仅仅能反复使用在dwFVFCaps中定义的坐标的数目。驱动程序使用D3DTSS_TEXCOORDINDEX(s)来得到正确的纹理坐标。

Flexible vertex formats（FVFs）允许多纹理，因为它允许在一个定点结构中传送多个纹理坐标。多纹理然后能被混合在一起。

纹理处理不再由Direct3D驱动程序产生，相反它们被Direct3D运行时产生。纹理高速缓冲区管理完全由Direct3D运行时完成，所以对于驱动程序，纹理似乎总是从应用程序自身而来。所有的纹理状态在D3dDrawPrimitives2命令流中被送给驱动程序。

除了多纹理，混合和纹理过滤的方法业已被精华，使一个更清楚和良好定义的混合机制成为可能。你必须理解这些混合和纹理过滤怎样工作。由于这个原因，大力推荐你温习DirectX SDK文档而不只是这里讲的内容。

纹理阶段

纹理阶段指示在纹理管道中纹理的位置。最高的、非NULL的纹理的位置最靠近帧缓冲区。每个阶段是一个纹理混合单位，它执行结合纹理和多边形的操作，如图4.1所示。

图4.1 单纹理阶段

图4.1说明了单纹理阶段。当前的纹理进入此阶段并和另一个纹理混合，产生的结果传送到纹理管道中的下一个阶段。（如果是最后的阶段，则是帧缓冲区）

有八个纹理阶段，标记为0到7，0离帧缓冲区最远，并对应着色状态纹理句柄D3DRENDERSTATE_TEXTUREHANDLE。驱动程序必须处理八个纹理坐标，即使硬件并不支持那么多。

在多纹理着色中，小标号的纹理阶段离帧缓冲区远。挑出和过滤最小的纹理阶段以得到一个texel。一个混合操作发生在纹理管道中直到帧缓冲区。

例如，如果两个纹理Texture0和Texture1混合到一起，结果texel进入光栅化的管道，就好像一个单纹理将使用以前的纹理。对于三个纹理，Texture0和Texture1先混合，然后结果texel按照一些（可编程）权值与Texture2混合。这意味着Texture0不能直接影响Texture2；它仅能通过代理这样做，即和Texture1混合。第二个例子在图4.2中说明。

图4.2中显示了一个三阶段纹理管道。每个纹理阶段只引入一个纹理到管道中。像素管道是分离的，并在多纹理操作以后到来。这可能包括雾化应用和帧缓冲区alpha混合。

图4.2 三阶段纹理管道

命令流

在驱动程序层，指令来自于对D3dDrawPrimitives2的调用。输入结构D3DNTHAL_DRAWPRIMITIVES2DATA包含一个指针指向命令缓冲区。这是一个D3DNTHAL_DP2COMMAND结构序列，每个结构包含bCommand成员，它说明了在缓冲区中后续数据的类型。这个说明来自于D3DNTHAL_DP2OPERATION枚举类型的形式,如D3DDP2OP_INDEXEDDTRIANGLESTRIP，或者设置纹理状态的情况D3DDP2OP_TEXTURESTAGESTATE。

例如，如果操作码是D3DDP2OP_TEXTURESTAGESTATE，wVertexCount的值是7，那么在下一个D3DNTHAL_DP2COMMAND指令到达前，将会跟随7个D3DDP2OP_TEXTURESTAGESTATE结构。为将这个例子举得完整，D3DDP2OP_TEXTURESTAGESTATE结构包含一个dwStage成员，它说明哪个纹理混合管道的阶段需要让一个纹理状态改变。D3DDP2OP_TEXTURESTAGESTATE的成员TSState说明哪个D3DDP2OP_TEXTURESTAGESTATE的枚举类型状态要被设置，成员dwValue包含了要设置的状态的值。

这个过程对所有的着色状态或者任何别的指令类型都是相同的。如果D3DNTHAL_DP2COMMAND结构的成员bCommand是D3DDP2OP_RENDERSTATE，那么接着的结构是D3DNTHAL_DP2RENDERSTATE，那个结构中的信息用来设置相应的着色状态。

每个着色状态值没有用布尔值着色状态来控制坐标，而是一系列的由D3DWRAP_U和D3DWRAP_V组成的标志。这个变化是为了和更高维度的纹理相兼容。

其他属于多纹理实现的有用的信息能在DirectX SDK文档中的覆盖混合平衡、颜色操作和alpha操作部分找到。更多的兼容DirectX 6.0和后续版本的纹理阶段状态类型，参见D3DTEXTUREOP，D3DTEXTUREMAGFILTER，D3DTEXTUREMINFILTER和D3DTEXTUREMIPFILTER枚举类型。

这一节剩下部分被分成以下主题：

·纹理定位和过滤操作

·纹理阶段操作

·纹理阶段参数

·多纹理验证

纹理定位和过滤操作

在Direct3D中，纹理定位、过滤和混合操作由一个称为纹理阶段的独立的逻辑单元来执行。在这里描述纹理定位和过滤操作，是因为它们组成了一个逻辑上成组独立的混合操作。有关纹理操作更进一步的信息参见D3DTEXTUREOP枚举类型。

尽管定位和采样操作在DirectX 6.0及后续版本中与混合操作连在一起，在以后的DirectX版本中他们可能独立于混合操作。以下的纹理阶段状态被用来为纹理管道中的每个阶段设置纹理定位和过滤操作：

D3DTSS_MAGFILTER

定义了当纹理被放大时（即当一个texel被展开到多个着色表面像素上），对纹理采样的过滤类型。对纹理放大能使用的过滤器是D3DTEXTUREMAGFILTER。

D3DTSS_MINFILTER

定义了当纹理被缩小时（即当一个texel被映射到少于一个屏幕像素上），对纹理采样的过滤类型。对纹理缩小能使用的过滤器是D3DTEXTUREMINFILTER。

D3DTSS_MIPFILTER

定义了在mipmap层间对纹理采样的过滤类型。能使用的过滤器是D3DTEXTUREMIPFILTER。

D3DTSS_MIPLEVEL

当硬件不能设置mip级时，允许应用程序设置。当硬件决定mip级时，它被重载。

D3DTSS_MIPMAPLODBIAS

这个D3DVALUE说明了mipmap级的详细斜线。这个斜线影响miomap级的计算，允许所期望的或多或少的模糊纹理。单位在mip级中。

D3DTSS_MAXMIPLEVEL

这个DWORD类型说明了最大可使用的mipmap级。它指示这个纹理不应当对比这个值大的miomap级进行采样。因此最大维度是2^MAXMAPLEVEL，0说明没有限制。

D3DTSS_ANISOTROPY

这个DWORD类型说明各向异性过滤的比例限制。它说明在纹理采集中各向异性过滤最大方向的比例。

D3DTSS_TEXCOORDINDEX

这个DWORD类型说明纹理坐标集的索引。它说明定位单元应该采集的起始坐标。这些坐标按数字顺序列在新来的FVF定点数据中，0是标准的DirectX坐标集，1是第二个纹理坐标集，等等。这就允许纹理共享纹理坐标集。

注意 为了和Direct3D兼容，驱动程序必须适当解析八个纹理坐标集，即使硬件设备仅仅能使用定义在dwFVFCaps中的坐标。驱动程序必须使用D3DTSS_TEXCOORDINDEX(s)获取相应的坐标。

纹理阶段操作

应用程序通过调用IDirect3Ddevice3::SetTextureStageState方法为纹理阶段进行混合操作。多纹理混合操作通过纹理混合单元阶段集来执行。这些单元中的每一个能被单独编程，以执行各种纹理混合操作，这些操作由参数选择。

在每个混合阶段Direct3D不提供说明多个纹理的机制。管道中的纹理阶段之间会发生饱和，但是在每个阶段它应尽可能迟地发生。

合法的操作的集合在D3DTEXTUREOP中枚举。这些操作要求与PC98兼容性标准相符合：

·D3DTOP_DISABLE

·D3DTOP_SELECTARG1,D3DTOP_SELECTARG2

·D3DTOP_MODULATE

·D3DTOP_ADD

·D3DTOP_BLENDTEXTUREALPHA

在阶段一中，D3DTOP的默认值是MODULATE，在其他阶段则是DISABLE。MODULATE用在阶段一是为了后向兼容性，但默认情况下纹理应当是失效的。

纹理阶段参数

每个多纹理混合操作组合两个输入。通过调用IDirect3Ddevice3::SetTextureStageState方法以及说明以下D3DTEXTURESTAGESTATETYPE枚举类型来选择：

D3DTSS_COLORARG1

对颜色操作控制第一个输入。

D3DTSS_COLORARG2

对颜色操作控制第二个输入。

D3DTSS_ALPHAARG1

对alpha操作控制第一个输入。

D3DTSS_ALPHAARG2

对alpha操作控制第二个输入。

参数标志

在每个纹理阶段，任何前面的四个参数使用以下纹理参数标志来设置：

D3DTA_CURRENT

纹理参数是以前混合阶段的结果。在第一个纹理阶段（阶段0），这个参数等价于D3DTA_DIFFUSE。如果前面的混合阶段使用一个bump-map纹理（D3DTOP_BUMPENVMAP操作），系统从bump-map纹理以前的阶段选择纹理。（如果s代表当前的纹理阶段，s-1包含一个bump-map纹理，此参数变为s-2纹理阶段的输出结果）

D3DTA_DIFFUSE

迭代的颜色数据从Gouraud插值算法中获得。在第一阶段它经常被用作ARG2，因为在那个阶段尚无D3DTA_CURRENT纹理。

D3DTA_TFACTOR

在Direct3D中使用D3DRENDERSTATE_TEXTUREFACTOR设置的值。

注意：一些实现可能不能同时使用D3DTA_TFACTOR和D3DTA_DIFFUSE。

D3DTA_TEXTURE

纹理阶段使用IDirect3Ddevice3::SetTexture（n，lpTex3）方法绑定纹理，这里n是阶段号。SetTexture定义了当D3DTA_TEXTURE是参数之一时，对这个阶段的纹理采用哪个纹理对象。D3DTA_TEXTURE只能由D3DTSS_ALPHAARG1和D3DTSS_COLORARG1提供，不能是D3DTSS_ALPHAARG2和D3DTSS_COLORARG2。

修饰标志

这两个值应当使用位或操作和前面的标志之一结合使用。

D3DTA_COMPLEMENT

指示纹理参数应被进行反转操作。

D3DTA_ALPHAREPLICATE

指示纹理参数在被使用到此操作以前，应将它的alpha通道复制到所有的颜色通道。如果是仅有一个部件的纹理，它自动被复制到所有颜色通道。此标志对于ALPHA_ARGs不需要说明，但是使用它也不会产生错误。

默认值

如果应用没有完全设置状态参数，就使用以下的默认值。这些默认值已经被定义，以使多纹理操作更加方便，健壮的代码总是完全说明了所期望的状态。

如果已经设置相应的纹理，D3DTSS_COLORARG1和D3DTSS_ALPHAARG1对于D3DTA_TEXTURE都是默认值。如果没有设置纹理，二者的默认值都是D3DTA_DIFFUSE。

D3DTSS_COLORARG2和D3DTSS_ALPHAARG2对于D3DTA_CURRENT都是默认值。注意在第一阶段，D3DTA_CURRENT默认值是D3DTA_DIFFUSE（除了在D3DTA_CURRENT描述中的例外情况）。

ARG2的默认是D3DTA_DIFFUSE，但是如果操作的默认值是D3DTOP_SELECTARG1，它则被忽略。

如果在FVF顶点中没有说明特别的颜色，D3DTA_DIFFUSE的默认值是0xFFFFFFF。

如果在第一阶段，D3DTA_CURRENT默认值是D3DTA_DIFFUSE，除了当前面的混合阶段是BUMPENV*MAP颜色操作，在这种情况下，有以下结果：

·如果前面的阶段是BUMPENV*MAP，此参数是倒数第二阶段的结果。

·在第二纹理阶段（阶段1），此参数默认是D3DTA_DIFFUSE。

对于任何阶段的D3DTSS_COLORARG1和D3DTSS_ALPHAARG1状态，D3DTA_TEXTURE是一个值，或者如果这个阶段没有纹理绑定，它的默认值是0x0。

多纹理验证

当前的硬件不需要实现Direct3D所支持的所有特性。应用决定了是否要通过首先设置期望的混合模式来执行一个特别的混合操作以及随后调用IDirect3DDevice3::ValidateDevice方法。驱动程序必须在初始化的时候准确报告它的性能，并且支持D3DvalidateTextureStageState,以允许验证它的性能。验证覆盖了TBLEND级指定的操作。

IDirect3DDevice3::ValidateDevice返回码

WRONGTEXTUREFORMAT

硬件不能支持当前选择的纹理格式状态。

UNSUPPORTEDCOLOROPERATION

不支持指定的颜色操作。

UNSUPPORTEDCOLORARG

不支持指定的颜色参数。

UNSUPPORTEDALPHAOPERATION

不支持指定的alpha操作。

UNSUPPORTEDALPHAARG

不支持指定的alpha参数。

TOOMANYOPERATIONS

硬件不能处理所说明的操作的个数。

CONFLICTINGTEXTUREFILTER

硬件不能同时进行三线过滤和多纹理。

UNSUPPORTEDFACTORVALUE

硬件不支持比1.0大的D3DTA_TFACTOR。

4.6.2 调色板纹理

Direct3D也允许调色板和纹理一起使用。调色板可以依附于纹理，就象它能依附于任何别的DirectDraw表面一样。为了支持调色板纹理，驱动程序必须在D3dDrawPrimitives2的实现中对操作码D3DDP2OP_SETPALETTE和D3DDP2OP_UPDATEPALETTE做出响应。这些操作码在命令流中分别在D3DNTHAL_DP2SETPALETTE和D3DNTHAL_DP2UPDATEPALETTE结构之后。D3DDP2OP_SETPALETTE在被使用的调色板句柄和表面句柄（已经由D3DcreateSurfaceEx建立）之间建立一个联系。然后，D3DDP2OP_UPDATEPALETTE能发送多次，以设置为纹理使用的调色板的值。

4.6.3 调色板Blitting

在DirectX7.0生效的一个重要的Direct3D DDI改变是纹理被blitted——不是直接调用DdBlt，而是通过在D3dDrawPrimitives2命令流中嵌入一个新的记号。这个新的记号D3DDP2OP_TEXBLT向驱动程序说明纹理必须从后援表面传输到本地或者非本地的视频存储器中。

同样，驱动程序不必通过旧的D3dTextureCreate/D3dTextureDestroy回调来负责为纹理建立内部句柄，而是由运行时为每个在Direct3D环境中建立的DirectDraw表面分配一个句柄号。驱动程序通过D3dCreateSurfaceEx回调得知此句柄号。

D3dCreateSurfaceEx在每个HAL DDCreateSurface调用完成后被调用。D3dCreateSurfaceEx在每个内部HEL CreateSurface调用完成后也被调用。HEL调用通常发生在后援DirectDraw表面被建立时。这些调用也发生在Direct3D环境用D3dContextCreate建立前或建立后。

当应用运行时，调用D3dDestroyDDLocal清除和注销任何为这些表面建立的驱动程序数据。在Direct3D环境被建立前也发出此调用以保证没有“脏”的句柄和任何没有被清除的环境相联系。这只是一个预防性措施，如果环境在使用后被适当清除，它不应注销任何数据。

4.6.3 驱动程序管理的纹理

驱动程序可以管理标记为可管理的纹理。这些DirectDraw表面使用lpSurfMore->ddCapsEx引用的结构中的域dwCaps2中的标志DDSCAPS2_TEXTUREMANAGE来标记为可管理的纹理。

驱动程序通过设置DD_HALINFO结构dwCaps2域中的DDCAPS2_CANMANAGETEXTURE来表示支持驱动程序纹理管理。作为对驱动程序DirectDraw部件初始化的响应（DrvGetDirectDrawInfo），DD_HALINFO被返回。

驱动程序然后以一种“懒惰”方式在视频存储器或者非本地存储器建立必须的表面。也就是驱动程序把它们留在后援表面直到需要的时候（即光栅化一个使用纹理的原语前）。

表面应当按照它们的优先权顺序注销。驱动程序应当对D3dDrawPrimitives2命令流中的D3DDP2OP_SETPRIORITY做出响应，以对给定的表面设置优先权。第二个措施是驱动程序使用最近最少使用算法（LRU）。无论何时，在一个特定的情况下，两个或者多个纹理的优先权是一样的时候采用这种策略。逻辑上，任何正在使用的表面不应被注销。

驱动程序必须注意，管理纹理的时候仍不能忽略DdBlt和DdLock。这是因为后援表面的任何改变必须在纹理被再次使用以前，传播到表面在视频存储器中的拷贝中。驱动程序应决定是更新表面的一部分还是表面的全部。

驱动程序被允许执行纹理管理是为了优化纹理上的变形以及能自己决定何时、何地将纹理传送到内存中。

4.7 绘画原语和状态改变

所有的Direct3D®原语和状态在命令和顶点缓冲区中传递到D3dDrawPrimitives2回调。驱动程序负责解析这些缓冲区并处理所有的绘画和状态改变的请求。

以下几小节讨论了命令和顶点缓冲区的布局，并描述了驱动程序应如何处理它们。

4.7.1 命令和顶点缓冲区

D3dDrawPrimitives2 DDI使用两种类型的缓冲区：命令缓冲区和顶点缓冲区。（参见主输入结构中D3DNTHAL_DRAWPRIMITIVES2DATA的描述）命令缓冲区包含了指令，紧随其后的是结构数据，它类似于执行缓冲区中的结构。它们包含索引或者未索引的原语以及内联的顶点数据。命令缓冲区或者是API级的执行缓冲区，或者是Direct3D的内部命令缓冲区。

对于内部命令缓冲区，驱动程序申请内存，也许作多个缓冲区。内部命令缓冲区是只写的。指令格式参见D3DNTHAL_DP2COMMAND。

如果D3DNTHALDP2_USERMEMVERTICES标志被设置，顶点缓冲区被用户内存指针说明。否则，顶点缓冲区是一个DirectDraw表面，它可以是API级的执行缓冲区、内部隐含的顶点缓冲区或者API级的顶点缓冲区。

顶点缓冲区API能建立、注销、加锁和解锁顶点缓冲区，也能使用ProcessVertices处理从源缓冲区到目标缓冲区的顶点。DrawPrimitivsVB和DrawIndexedPrimitiveVB是主要的API级的调用。顶点缓冲区也能优化，但是优化的顶点缓冲区不能加锁。

4.7.1.1命令和顶点缓冲区的申请

在Direct3D使用三种类型的缓冲区：

·隐含的顶点缓冲区（IVB）仅用于内部使用；应用并不知道IVBs。一个IVB总是在环境建立后才建立。Direct3D存储顶点数据在IVBs。

·显式的顶点缓冲区（EVB）在应用请求时建立。Direct3D然后在EVB存储顶点数据。

·命令缓冲区仅用于内部使用；应用并不知道命令缓冲区。Direct3D在命令缓冲区中存储命令数据。

IVB是特殊的顶点缓冲区，被Direct3D用于内部批处理。它们在设备初始化时建立，可以是多缓冲区。它们可读可写，因此不应将它们放在视频存储器中。通过辨别DDSCAPS2_VERTEXBUFFER和DDSCAPS2_COMMANDBUFFER标志的不存在来区分这种类型的缓冲区。

EVB由应用建立和控制。它们不能是多缓冲区，不能置于本地或者非本地视频存储器中，除非设置了DDSCAPS_WRITEONLY。这种类型的缓冲区由DDSCAPS_VERTEXBUFFER标记。

命令缓冲区被Direct3D用于批处理命令。它们可以是多缓冲区，能用于所有的API，除了TLVERTEX或者未处理的执行缓冲区API调用。这种类型的缓冲区由DDSCAPS2_COMMANDBUFFER标记。命令缓冲区总是只写的，它们从不包含不合法的指令。

默认情况下，Direct3D运行时申请所有的这些缓冲区。IVB和命令缓冲区通过与它们相联系的表面被访问。所有缓冲区传递给驱动程序的D3dDrawPrimitives2回调。

驱动程序申请顶点和命令缓冲区

Direct3D驱动程序能执行顶点和命令缓冲区的申请。这是通过划分DD_D3DBUFCALLBACKS结构，并将DD_HALINFO结构的lpD3DbufCallbacks成员指向它。在DD_D3DBUFCALLBACKS中的回调报告包括：CanCreateD3Dbuffer，CreateD3Dbuffer，DestroyD3Dbuffer，LockD3Dbuffer和UnlockD3Dbuffer。这些函数与Dd*Surface回调函数（如DdCanCreateSurface）一样以同样的方式调用，并且DDSCAPS_EXECUTEBUFFER标志置1。缓冲区建立标志是DDSCAPS_WRITEONLY，DDSCAPS_VERTEXBUFFER和DDSCAPS2_COMMANDBUFFER。

驱动程序通过检查传给回调函数CanCreateD3Dbuffer和CreateD3Dbuffer的DD_SURFACE_LOCAL结构中的ddsCaps成员，来决定被请求的缓冲区的类型。DdsCaps可以是以下标志：

·DDSCAPS_VERTEXBUFFER指示驱动程序应申请EVB。

·DDSCAPS_COMMANDBUFFER指示驱动程序应申请命令缓冲区。

·以上两者都没有设置说明驱动程序应申请IVB。

IVB不应当被置于视频存储器中，因为它们是可读/可写的。只有设置了DDSCAPS_WRITEONLY标志的EVB能安全地置于视频存储器中。

驱动程序能使用存在的执行缓冲区回调申请顶点缓冲区和命令缓冲区。驱动程序应当暴露vidmem堆，即使它们自己做堆管理，这样通过暴露vidmem堆报告合适的大小。

驱动程序能内部申请N个顶点和命令缓冲区，并循环利用这些驱动程序。当硬件从别的队列缓冲区进行异步着色，Direct3D填充给定的一对缓冲区。这对使用DMA是非常有用的。

一个多纹理集的缓冲区能在各种不同的内存类型中。这种方式工作的原因是调用驱动程序建立第一个缓冲区。它能立即建立缓冲区集并返回第一个缓冲区。对于每个D3dDrawPrimitives2调用，如果D3DNTHALDP2_SWAPVERTEXBUFFER或者D3DNTHALDP2_SWAPCOMMANDBUFFER标志被置1，驱动程序应返回一个新的缓冲区。如果返回的缓冲区在视频存储器中，相应的D3DNTHALDP2_VIDMEMVERTEXBUF或者D3DNTHALDP2_VIDMEMCOMMANDBUF标志应置1。

偶然情况下，Direct3D将为下一个缓冲区请求最小的尺寸。如果太大的话，驱动程序应该在后援表面申请缓冲区。如果太小，驱动程序应给一个较大的缓冲区。驱动程序应该跟踪有多少缓冲区以及它们是什么内存类型，并在退出时清除所有的缓冲区。

4.7.2 Direct3D命令缓冲区

图4.3示例命令缓冲区

图4.3显示了一个逻辑命令缓冲区例子的一部分。驱动程序的D3dDrawPrimitives2回调函数接收一个指针，指向D3DNTHAL_DRAWPRLMITIVES2DATA结构lpDDCommands成员中的一个命令缓冲区。命令缓冲区总是被顺序处理。

正如图4.3所示，命令缓冲区包含数目不详的D3DNTHAL_DP2OPCOMMAND结构，每个结构的bCommand成员说明了以下命令之一：

l D3DDP2OP_REBDERSTATE指示在命令缓冲区后面有wStateCount个D3DNTHAL_DP2RENDERSTATE结构。驱动程序应当从这些结构解析状态，并相应地更新它自己私有的驱动程序状态。驱动程序也应当更新lpdwRState指向的状态数组中相应的状态。如果驱动程序不支持在命令流中的状态请求，它就应该用它支持的值重载请求的值。

l D3DDP2OP_TEXTURESTAGESTATE指示在命令缓冲区后面有wStateCount个D3DNTHAL_DP2TEXTURESTAGESTATE结构。驱动程序应当从这些结构解析状态，并相应地更新与指定的纹理阶段相关的驱动程序的纹理状态。驱动程序不必向Direct3D运行时报告纹理阶段状态。

不论驱动程序实际支持几个坐标集，它都要求适当的解析八个纹理坐标集。

l D3DDP2OP_VIEWPORTINFO指示一个结构D3DNTHAL_DP2VIEWPORTINFO跟在命令缓冲区后面。驱动程序应解析此结构，并更新存储驱动程序内部着色环境中的w-buffer信息。

l 任何剩下的D3DDP2OP_Xxx命令指示有足够的数据跟在命令缓冲区后面，以着色wPrimitiveCount次原语。取决于原语命令，驱动程序应当从命令缓冲区中解析D3DNTHAL_DP2Xxx结构，以及来自于顶点缓冲区、命令缓冲区或者两者的与顶点相关的数据。驱动程序应该尝试处理所有合法的D3DDP2OP_Xxx命令；即它不能选择忽略某些已经定义的原语类型。参见D3DNTHAL_DP2Xxx结构的详细参考。

取决于当前命令，下面的额外信息存储在命令缓冲区中：

l 所有的D3DDP2OP_INDEXEDXxx原语命令的索引信息。

l D3DDP2OP_TRIANGLEFAN_IMM和D3DDP2OP_LINELIST_IMM原语命令的顶点数据。

l 额外的操作也定义为D3DNTHAL_DP2OPERATION中的D3DDP2OP_Xxx命令。

命令缓冲区偶然也包括只能被Direct3D理解的命令。如果驱动程序的D3dDrawPrimitives2的回调函数不能识别此命令，驱动程序应当调用Direct3D的回调函数D3dParseUnknownCommand，以试图解析此命令。当D3dParseUnknownCommand成功返回时，驱动程序继续解析和处理命令缓冲区。如果D3dParseUnknownCommand返回D3DERR_COMMAND_UNPARSED表示解析不成功，D3dDrawPrimitives2应在设置以下D3DNTHAL_DRAWPRIMITIVES2DATA结构中的成员后返回：

l 在dwErrorOffset中，将第一个不能处理的D3DNTHAL_DP2COMMAND命令的偏移写入lpDDCommands的缓冲区中。

l 在ddrval中设置D3DERR_COMMAND_UNPARSED。

参见Direct3D驱动程序初始化，以更详细地了解如何初始化D3dParseUnknownCommand。

为了简化D3dDrawPrimitives2的实现，可以从permedia2示范代码中拷贝解析代码，只写和具体驱动程序相关的着色及状态更新代码。

并不是总是通知Direct3D当前的着色状态。例如，执行缓冲区在到达驱动程序以前，并不被运行时所检查。驱动程序使用D3DNTHAL_DRAWPRIMITIVES2DATA的lpdwRState域跟踪着色状态数组。lpdwRState是一个指向内部着色状态数组的指针，从而使驱动程序能及时更新状态的变化。

4.7.3 Direct3D顶点缓冲区

顶点缓冲区包含了在D3dDrawPrimitives2调用中与命令缓冲区的原语相关的顶点数据。顶点以FVF的形式来表示，这里每个顶点可以和以下数据相关：

·位置（x，y，z，可选的w）（必须的）

·扩散的颜色（可选）

·反射颜色（可选）

·纹理坐标（可选）。Direct3D能发送八套坐标值。驱动程序必须提供FVF支持。

实际的顶点和它们被处理的顺序取决于D3DD2OP_*原语命令从命令缓冲区解析的顺序。参见D3DNTHAL_DP2*结构的详细参考。

4.7.4加速状态管理

加速状态管理是为了在一个单独的调用中，使API和DDI在大的状态改变时通信的机制。这种安排允许应用定义一整套状态设置调用，它作为一个状态块，被定义为一个整数。发送这个整数，就可以在一个调用中执行所有的状态改变。

通过减少必须的SetRenderstate函数调用的数目，减少了API负载；驱动程序通过将定义的状态块的阶段变化“预编译”成它们自己的硬件格式，而不是执行每个状态的变化改善了效率。

大多数应用仅在少数状态中着色，所以有微小的状态转换不是很重要。重要的是当驱动程序在公共着色脚本中切换时能定义可以被交换的状态块。这是状态块管理的核心。

状态设置符号被用来设置驱动程序中的状态。一个句柄引用了一个状态的集合。D3DNTHAL_DP2STATESET结构用来通知驱动程序要执行的状态集操作。

如果D3DNTHAL_DP2STATESET的dwOperation成员是D3DNTHAL_DP2STATESETBEGIN，驱动程序开始为dwParam中的句柄纪录状态。当驱动程序收到D3DNTHAL_STATESETEND时，就停止纪录状态。

如果dwOperation成员是D3DNTHAL_DP2STATESETDELETE，dwParam中的句柄引用的状态集应被删除。

如果dwOperation成员是D3DNTHAL_DP2STATESETEXECUTE，dwParam中的句柄引用的状态块应被应用于设备。

如果dwOperation成员是D3DNTHAL_DP2STATESETCAPTURE，当前驱动程序的状态以一种指定的方式被捕获，给在状态块中定义的当前状态一个“快照”。即只有状态块中的状态才被捕获。这样，状态快就像一个掩码，仅纪录定义在它里面的状态。例如，如果D3DRENDERSTATE_ZENABLE在状态块中，D3DRENDERSTATE_ZENABLE的当前状态被捕获并放置在状态块中。如果在状态块没有D3DRENDERSTATE_ZENABLE，此状态就不被捕获。

状态分组用来定义一般的、能为不同的着色脚本稍作修改的状态块。这些预定义的分组（在D3DSTATEBLOCKTYPE中枚举）定义了能被顺序修改的通用状态块，这些状态块的状态会因为着色脚本的不同而发生变化。例如驱动程序也许建立100个通用的预定义状态块，然后修改每一个状态以适应不同的着色脚本。状态块类型被传送给D3DHAL_STATESET结构的sbType成员中。

sbType成员仅仅对D3DHAL_STATESETBEGIN和D3DHAL_STATESETEND有效，并使用以下的D3DSTATEBLOCKTYPE枚举说明预定义的状态块：NULL代表无状态，D3DSBT_ALL代表所有的状态，D3DSBT_PIXELSTATE代表像素状态，D3DSBT_VERTEXSTATE代表顶点状态。

驱动程序应忽略sbType，除非它实现了扩展的着色状态。如果驱动程序实现了扩展的着色状态（着色状态超过了Direct3D运行时所提供的），它就能使用sbType决定使用的预定义的状态块的类型。从它获取的这个信息，它能决定如何添加状态块以支持它的扩展。

4.8 柔性顶点格式（FVF）

驱动程序的D3dDrawPrimitives2回调函数接收柔性顶点格式（FVF）的顶点数据。因为顶点格式是柔性的，对于数据没有定义全面的数据结构。驱动程序必须实现完全的FVF功能。

注意DirectX7.0对FVF的更新包括1D、3D、4D以及通常的2D纹理。参见permdia2示范驱动程序和Platform SDK文档，以得到这方面更多的信息。

4.8.1决定顶点缓冲区数据格式

为了识别顶点缓冲区的数据格式，驱动程序应判断以下的信息：

·纹理维度（1D，2D，3D或4D）。

·FVF数据中的组成部分。

·部件的顺序。

纹理维度

驱动程序应从D3DTEXTURETRANSFORMFLAGS纹理坐标计数标志（D3DTTFF_COUNT*）判断顶点的维度。计数标志的数字说明提供了多少纹理坐标。注意正如以下所解释的，它并不一定等于纹理的维度。

对于非映射纹理

·D3DTTFF_COUNT1指示光栅期望1D纹理坐标。

·D3DTTFF_COUNT2指示光栅期望2D纹理坐标。

·D3DTTFF_COUNT3指示光栅期望3D纹理坐标。

·D3DTTFF_COUNT4指示光栅期望4D纹理坐标。

如果使用映射纹理，D3DTTFF_PROJECTED标志被置位，以指示纹理坐标按照纹理坐标集的最后一个元素分割。这样，对于2D映射纹理，计数值将是3，因为前两个元素被第三个分割，导致2D纹理查询是两个浮点数，也就是D3DTTFF_COUNT2和D3DTTFF_COUNT3|D3DTTFF_PROJECTED都引用一个2D纹理。

FVF顶点数据组成部分

驱动程序通过分析D3DNTHAL_DRAWPRIMITIVES2DATA结构的dwVertexType的成员说明的标志，判断提供了什么组成部分。具体来讲，下表指示了在dwVertexType中可能设置的位以及所表示的部件：

值 | 含义
D3DFVF_XYZRHW | 每个顶点有x，y，z，w
D3DFVF_DIFFUSE | 每个顶点有扩散色
D3DFVF_SPECULAR | 每个顶点有反射色
D3DFVF_TEX0 | 顶点数据没有提供纹理坐标
D3DFVF_TEX1 | 每个顶点有一个纹理坐标集
D3DFVF_TEX2 | 每个顶点有两个纹理坐标集
D3DFVF_TEX3 | 每个顶点有三个纹理坐标集
D3DFVF_TEX4 | 每个顶点有四个纹理坐标集
D3DFVF_TEX5 | 每个顶点有五个纹理坐标集
D3DFVF_TEX6 | 每个顶点有六个纹理坐标集
D3DFVF_TEX7 | 每个顶点有七个纹理坐标集
D3DFVF_TEX8 | 每个顶点有八个纹理坐标集

D3DFVF_TEXn标志只能有一个被设置。

FVF顶点组成部分顺序

包含顶数据的组成部分被严格排序。具体讲，Direct3D仅仅给驱动程序提供了图4.4所示的顶点数据顺序。注意此图表示的是2D纹理，尽管1D、2D、3D和4D纹理对于DirectX7.0和后续版本也是有效的。

图4.4 柔性顶点格式

如图4.4所示，Direct3D总是发送x，y，z，w值；其余的数据只有当应用需要时才发送。具体讲，顶点数据有以下组成部分：

1．位置（x，y，z，w）（必须有）

第一个顶点成分是四个D3DVALUE，它说明了顶点的位置。Direct3D总是在dwVertexType中设置D3DFVF_XYZRHW位。

2．扩散色（可选）

如果被提供，此组成部分是一个D3DCOLOR值，它说明这个顶点的扩散色。Direct3D在dwVertexType中设置D3DFVF_DIFFUSE位。

3．反射色（可选）

如果被提供，此组成部分是一个D3DCOLOR值，它说明这个顶点的反射色。Direct3D在dwVertexType中设置D3DFVF_SPECULAR位。

4．纹理数据（可选）

这部分按照纹理的维度是可变的。对于纹理的每个维度，一个D3DVALUE值说明了每个部分的u、v、w或q（参见FVF纹理维度的解释）。例如，如果使用2D非映射纹理，需要每个纹理的两个D3DVALUE值描述每个纹理以至全部八个纹理的顶点的u、v值。u、v值对代表的是n，这里n对应的是dwVertexType的D3DFVF_TEXn标志。例如，如果设置dwVertexType的D3DFVF_TEX3标志，每个顶点提供三个u、v值对。

FVF数据总是被紧密包装；也就是说，在顶点缓冲区中未作说明的组成部分不会浪费内存。例如当dwVertexType是D3DFVF_XYZRHW| D3DFVF_TEX2，纹理维度是2D，在缓冲区中的每个顶点由八个紧包装的D3DVALUE组成。这些为两纹理（tu，tv，tu，tv）说明了位置（x，y，z，w）和纹理坐标，如图4.5所示。

注意图4.5假设仅有两个纹理坐标。为驱动程序提供的顶点数据总要被转换。驱动程序从不接收常规数据。在FVF纹理坐标集的所有数据是单精度IEEE浮点数。

图4.5 顶点缓冲区

4.9 Direct3D驱动程序的高级主题

这一节讲述Direct3D的特别方面的主题和优化问题。这些特性并不集中于实现Direct3D驱动程序的核心功能；应当在核心驱动程序功能完成之后，再实现这些特性。

这一节描述的主题有：

·4.9.1 优化的纹理

·4.9.2 模版位面

·4.9.3 Guard Band Clipping

·4.9.4 范围调整

·4.9.5 W缓冲区

·4.9.6 Bump Mapping

·4.9.7 硬件转化和照明

4.9.1 优化的纹理

为提高性能，一些硬件厂商改变了它们的纹理格式，不同于标准的DirectDraw表面格式。一种方式是把像素叠起来，这样它们以2D引用位置被安排在内存中。例如，不是以下面的线性关系安排像素：

0 1 2 3 …width

width+1 width+2 width+3 width+4 …

它们被安排在4×8的块中：

0 1 2 3

4 5 6 7

这样，一个单独连续的32位内存引用包含了4×8的像素块。这种布置对于由光栅生成的内存引用模式，与标准的DirectDraw布置相比，是更好的。这种安排被称为“swizzling”或者“弥补”纹理。这些操作执行起来相对较快，也不影响申请的内存表面的大小。但是，这个操作仍然引入了显式的应用必须控制的等待时间。

另外一种方式是将纹理压缩到视频存储器中，把它在加速器的本地高速缓存中解压缩。这减少了为全速运行光栅化所需要的视频存储器带宽。在DirectX5.0中，驱动程序也可以这样做，但是它是个较慢的操作，由于导致纹理变小，影响了申请应用的视频存储器。

为了适应硬件厂商，DirectX6.0和后续版本能“优化”纹理，这给了驱动程序一个机会，当使用纹理时，将表面转化成私有格式。一旦优化，表面不能加锁，比原来要大或小一些。

市场上许多三维加速芯片有自己的格式，不能用标准的DirectDraw机制描述。这个特性的目的是想用这样的表面类型包容所有的硬件。

目前，应用有一些需要经常更新的纹理，也有些纹理在应用存活期间保持不变。拥有后一种纹理的应用能从改善了的填充率中获益。驱动程序如何接收这样的优化的细节参见优化纹理API。

4.9.1.1优化纹理API

三个新的caps指示了能用于DirectDraw表面的优化级。在DirectX6.0和后续版本中，仅有纹理能用caps位标记。尽管和纹理的语义不同，优化也许在将来能扩展，以至覆盖所有类型的表面。

为使这个问题更清楚，给DirectDrawSurface4::Create（）提供了三个标志。当这三个标志没有说明任一个，是否优化就完全由驱动程序来决定。这些标志如下：

DDSCAPS2_HINTDYNAMIC

对驱动程序指示此表面将会经常加锁（例如每帧一次）使用，如视频流或程序上的纹理。这个标志对所有的驱动程序枚举纹理表面格式都起作用。驱动程序应避免对这些纹理的任何转变，特别是如果它需要一些负载。

DDSCAPS2_HINTSTATIC

指示驱动程序此表面在Load（）和Blt（）中会被重排序/重叠/优化。此操作不改变纹理的大小。因为应用也许仍然锁住了这些位，它相对较快，也是对称的（这样做的时候尽管使性能受影响）。驱动程序不允许在这些表面上加锁失败，因此不能使用压缩技术。Mipmap表面在这种情况下能被交叉存取。

这个标志并不是要在任何环境下都要优化，特别是那些对性能无益的情况下。特别的，一些格式对优化会失败，但并不报错。

DDSCAPS2_OPAQUE

指示驱动程序此表面不会被应用再次存取。这个标志像DDSCAPS2_HINTSTATIC，但是允许使用硬件相关的压缩方式进行实际压缩。这个操作相对较慢，但是允许使用简单、对称的压缩算法（如YUV4：2：0或者颜色单元压缩），提供了2到6倍的压缩率。不应再次使用非对称方法如VQ，因为它会导致不可接受的基准。

Mipmap纹理能被驱动程序任意交错存取。这种技术仅在着色循环的内部或外部也许是必须的，如当纹理被从磁盘装入时。在这样的纹理装入后，如果压缩被采用，堆大小的报告反映了内存的消耗。记住在纹理上有额外的头负载，所以压缩许多小的纹理不会节省很多内存。

总的来说，此标志并不意味着对纹理压缩率或者压缩质量有任何保证。

用此标志建立的表面在下列情况下会失效：

Lock（）调用

GetDC（）调用

子矩形blit到这种表面

从这种表面来的所有blit

将数据放入这种表面的唯一方式是用Load（）或者完全表面blit调用。

4.9.2模版位面

模版位面能控制在每像素基础上的绘画。它们典型用在多通过（multipass）算法中以达到特殊的效果，如贴纸、轮廓、阴影和实心几何着色。

一些硬件设计用来加速Direct3D的模版位面的实现。由模版位面带来的特效对于娱乐应用特别有用。

假设模版位面嵌入在z缓冲区中。

在DirectX5.0，应用程序使用在D3DDEVICEDESC的dwDeviceZBufferBitDepth成员中设置的DDBD_*标志找到可用的z缓冲区位深度。为了支持带模版的z缓冲区和使用现存的DDBD_*标志不能表示z缓冲区位深度，在DirectX6.0和后续版本有一个新的API入口点IDirect3D3：：EnumZBufferFormats，它返回DDPIXELFORMAT结构数组，描述可能的z缓冲区/模版像素格式。DDPIXELFORMAT结构增加了三个新的z缓冲区相关的域：dwStenciBitDepth，dwZBitMask和dwStenciBitMask。一个新的标志DDPF_STENCILBUFFER,指示在z缓冲区中是否有模版位。dwZBufferBitDepth（以前就存在）给出了z缓冲区位的总数，包括模版位。DDPIXELFORMAT结构的新成员如下：

dwStenciBitDepth

说明模版位的位数。

DwZBitMask

说明z值占的是哪些位。如果非0，这个标志意味着z缓冲区是一个标准的、无符号整数z缓冲区格式。

DwStenciBitMask

说明模版值占的是哪些位。

DirectX6.0和后续版本的驱动程序应当为它们所支持的z缓冲区格式在dwDeviceZBufferBitDepth中设置合适的DDBD_*标志。如果不支持模版位面，DDBD_*标志能代表所有可用的z缓冲区格式，设置这些标志就足够了（因为通过EnumZBufferFormats它们将会被翻译成DDPIXELFORMAT）。否则，通过返回一个缓冲区（此缓冲区的第一个DWORD指示有效的z缓冲区DDPIXELFORMAT 的位数，紧随的是DDPIXELFORMAT结构本身），Direct3D驱动程序必须对DdGetDriverInfo查询做出响应（它使用GUID_ZpixelFormats GUID）。

DDPIXELFORMAT的更多信息参见DirectDraw DDK参考文档。

新的和模版位面相关的着色状态如下：

名称 | 类型 | 描述
D3DRENDERSTATE_STENCILFUNC | D3DCMPFUNC | 比较函数。如果以下的表达为真，则测试通过： (ref&mask)操作符(stencil&mask)。这里refs是引用值，stencil是stencil缓冲区的值，mask是D3DRENDERSTATE_STENCILMASK。
D3DRENDERSTATE_STENCILREF | DWORD | 在stencil测试使用的引用值
D3DRENDERSTATE_STENCILMASK | DWORD | 在stencil测试使用的掩码值
D3DRENDERSTATE_STENCILWRITEMASK | DWORD | 写掩码，对任何写入stencil缓冲区的值都适用
D3DRENDERSTATE_STENCILFAIL D3DRENDERSTATE_STENCILZFAIL D3DRENDERSTATE_STENCILPASS | D3DSTENCILFUNC | 参见下一段的解释

分别定义了三个新的着色状态— D3DRENDERSTATE_STENCILFAIL，D3DRENDERSTATE_STENCILZFAIL，D3DRENDERSTATE_STENCILPASS，通知硬件当模版测试失败时、或当模版测试通过但是z测试失败时和两者都通过时这三种情况下，它该如何去做。这些着色状态接收D3DCMPFUNC枚举类型的数据作为参数。

4.9.2.1模版操作码

D3DSTENCILOP枚举类型包含被用来说明所期望执行的模版操作。

4.9.3 Guard Band Clipping

当DdGetDriverInfo中填入GUID_D3DextendedCaps GUID时,说明驱动程序支持Guard Band Clipping。一个guard band是一个比视口（甚至着色的目标）还要大的矩形，里面的顶点能被驱动程序自动裁剪。Direct3D裁减码针对的是矩形而不是视口。因此，由于裁剪而要求产生新的顶点的需求就减少了，由于驱动程序说明的是大的guard band矩形。

一个合理的真实世界的例子是：只要屏幕的X和Y坐标落在（-2048）到2047的范围内，硬件就能正确着色。

Guard band clipping对于抗干扰的硬件也是有益的，因为过滤区域能扩展至着色表面的范围之外，并且如果原语在此范围内被几何裁剪，过滤错误就会引进。

为了做裁减，视口信息被传送给驱动程序。这说明了应用要求被裁减的几何图形的实际的视口。如果不想实现guard band clipping，可以忽略此信息。建议驱动程序不应通过剪取或掩码操作来使用这个数据实现裁减，因为这些可能比让Direct3D做裁减更慢。

4.9.4范围调整

一些硬件使用抗干扰内核，就能影响由屏幕空间顶点所定义的扩展矩形之外的像素。为了“脏”矩形的处理而使用D3DCLIPSTATUS结构中的扩展矩形的应用能感受到人工着色，因为扩展矩形不能覆盖由硬件所修改的象素。

Direct3D使硬件驱动程序要求扩展矩形向外调整在D3DNTHAL_D3DEXTENDEDCAPS结构的dvExtentsAdjust域说明的像素数(当驱动程序响应在DdGetDriverInfo的GUID_D3DextendedCaps GUID时被填入)时提出了这个问题。扩展矩形被裁剪到着色目标表面的范围。默认是0。

4.9.5 W缓冲区

通常，为了比较深度以及在z缓冲区中存储，使用透视矫正z。因为这种方式下为了维护平面多边形，必须产生光栅迭代。一些实现通过用相对于眼睛的w或z表达的深度信息填充z缓冲区，可以执行隐藏表面的消除。这就是在本文中被称为w的缓冲区。通过线性插入在经典变换中说明的顶点1/w项以及顶点结构（TLVERTEX），计算它的每像素的倒数，然后为了深度比较及有条件地将它存储到深度缓冲区中，然后使用此w值。

典型的，硬件存储一个浮点值到缓冲区中。以下精度格式是常用的：

16位 12.4

24位 IEEE单精度浮点，没有低位尾数

32位 标准IEEE单精度浮点

常规的z缓冲区是为使用CAD或写作工具的技术市场开发的，它的视面积/空间是已知的和有限范围的。被存储的深度值的范围因此也是有限的，它允许远、近（远近平面的距离）的比例是2到10。

为这种应用设计的典型硬件重复透视矫正z，并将它直接存储在z缓冲区中。由于涉及到数学，这种透视矫正z在z缓冲区的范围内没有均匀分布。使用远近比例为100将导致深度缓冲区的90%被花费在前10%的场景深度。对于工具这也许是足够的，但是典型的有外部场景的娱乐或者可视化仿真应用要求远近比例为1000到10000。在1000时，范围的98%花费在深度的前2%上。这能引起远处对象的隐藏表面的虚假化，特别是当使用16位深度缓冲区时。

相反，当w（或相对于眼睛z）被使用，在世界空间的近和远裁剪平面之间申请的缓冲区位更均匀。最关键的好处是远近比例不再是个问题，它允许应用支持最大几英里的范围，但是在视点几英寸里仍可得到合理的准确的深度缓冲区。

4.9.5.1 w缓冲区API

ZBUFFERENABLE着色状态支持以下的三种枚举类型设置：

D3DZBUFFERTYPE

D3DZB_FALSE

禁止所有的深度缓冲区。

D3DZB_TRUE

允许使用透视矫正z的z缓冲区。

D3DZB_USEW

禁止z缓冲区，但是允许w缓冲区，它是相对于眼睛的z。

因为存储w的确切格式变化很大，它应被认为是透明的。

当使用w缓冲区，表面申请和深度填充操作同样的工作。所有的z缓冲区比较模式在任何一种情况下同样的工作。

4.9.5.2 w缓冲区DDI

通过设置D3DPRIMCAPS结构dwRasterCaps域的D3DPRASTERCAPS_WBUFFER位,驱动程序支持w缓冲区。D3DRENDERSTATE_ZENABLE着色状态被传给驱动程序以允许或者禁止w缓冲区或z缓冲区。

D3DNTHAL_DP2VIEWPORTINFO结构支持对应于世界空间的前面和后面裁剪平面的域。这个信息能也被用来调整雾化表。

4.9.6 Bump Mapping

bump mapping 是一种很好的技术，它能使表面看起来起皱或者起涟漪，但不需要从几何上使表面改变。

在通常意义上，bump mapping涉及到按照由二维“bump map”给定的信息而生硬的干扰表面的标准。这欺骗了本地反射模型（这种模型中强度是表面标准的一个主要功能），导致在光滑的表面产生了局部变化。当产生影子的时候这种技术很突出，因为那种情况下，在边缘的干扰不再可见。也就是，影子跟随模型的变化，因此没有被干扰。这是一项重要的技术，因为看起来对表面增加了纹理而不是调整表面的颜色。

因为Direct3D的bump mapping通过在着色阶段给表面增加纹理而不干扰几何图形的方式来实现的，它绕过了严重的模型问题。如果对象是多边形，要足够好的技巧来接受来自纹理映射的干扰。如果纹理是一个选项，这就是一个严重的缺点。

bump mapping干扰环境映射，它允许说明任意数目、形状、颜色的灯光。DirectDraw为bump说明了一种新的像素格式：DDPF_DU和DDPF_UV。通过D3DTEXTURESTAGESTATETYPE，它集成到多纹理混合中。环境映射由应用来进行。

在这种情况下，bump mapping被更准确地解释成由于为扩散和反射环境地图的光栅化对每个像素纹理坐标的干扰。这种能力被集成到Direct3D多纹理支持中。bump mapping被在本地表面坐标系统（纹理u，v）中执行。

这种技术允许场景的照明环境在一个图像环境地图中表现（为了扩散或者反射的效果）。它允许任意数目、形状、颜色或强度分布的灯光在这样的地图中表现。能事先安排这些地图，这是为了静止的情况，或者为改变光源而使用blit方法在飞行中容易更新。

从Phong shading导出的传统bump mapping设计受限于恒定颜色和固定下降曲线的球体光源。这些不需要每像素环境映射的额外的纹理发布能力，并且对扩散照明效果工作地更好，但是它们不能产生光现实要求的可视的、结构化的反射强光。在将来，这种技术的使用会由于环境映射计算直接集成到Direct3D几何管道中而更方便。

bump mapping通常在照片着色中提供。它的使用显著地增加表面细节效果不必将表面镶嵌进很多小三角中。当和反射效果使用时，bump map能模仿反射粗糙的表面，如湿的石头。当由3D加速器支持，这种效果能在实时执行，允许动态光源的改变。Direct3D对bump mapping的支持减少了余下的几个用照片质量表示表面和环境的障碍。

当一个Texture2对象从一个bump map格式的DirectDraw表面建立时，此纹理被认为是一个bump map。使用D3DRENDERSTATE_TEXTUREn状态的任何一个都能使此Texture2对象绑定到Direct3D设备中。通过设置D3DTOP_BUMPENVMAP，一个可编程的纹理混合阶段能设置执行bump map操作。为这个阶段定义的纹理应是从一个bump map格式的DirectDraw表面建立。它使用FVF纹理坐标，此坐标由这个Texture2阶段的纹理坐标的标识说明。它也会注意纹理状态的控制过滤、包裹等。

在此纹理中的bump的值干扰紧随纹理所使用的纹理坐标，在扩散或反射环境地图中应考虑到这一点。

仿真

这种方法能在任何支持8位调色板纹理方式的硬件中被仿真。限制是所使用的环境地图（紧随bump纹理地图的纹理）必须有确切的16X16 texel的分辨率，并且没有过滤。这由D3DTEXOPCAPS_MAXBUMPENVMAP16X16标志指示。别的部分对bump map能干扰的环境地图的大小没有限制。

当额外的信息如常规矢量数据在DDI中提供，在将来的DirectX版本中也许支持bump map的环境映射的更一般的方法。

在多纹理混合操作D3DTOP_BUMPENVMAP和D3DTOP_BUMPENVMAPPREMODULATE中允许Bump mapping。后一个操作是bump mapping和gloss mapping的结合，从而允许镜面反射被编码在同一个表面，就如同bump map数据一样。

在每个阶段都提供四个额外的纹理状态：

D3DTS_BUMPENVMAT00

D3DTS_BUMPENVMAT01

D3DTS_BUMPENVMAT10

D3DTS_BUMPENVMAT01

4.9.7硬件变换和照明

允许几何操作的硬件加速，如照明和变换，同时修改了DirectX7.0的DrawPrimitives2 DDI。在API一级，分别列举了在硬件中支持顶点操作的设备和那些仅仅作光栅化的设备。

现存的caps结构已经被扩展到指示硬件加速变换设备也许能提供的特性上。例如，支持的光源数目被设置在D3DLIGHTINGCAPS结构的dwNumLights成员中，在D3DNTHALDeviceDesc_V1结构中报告。

其他标志有：

·D3DDEVCAPS_DRAWPRIMITIVES2EX，指示驱动程序是DirectX7.0兼容的，它支持扩展DrawPrimitives2。

·D3DDEVCAPS_SEPARATEXTUREMEMORIES，指示设备从独立的内存池中进行纹理。

·D3DDEVCAPS_HWTRANSFORMANDLIGHT，指示设备支持硬件变换和照明。

·D3DDEVCAPS_CANBLTSYSTONONLOCAL，指示设备支持从后援表面到非本地视频存储器的纹理Blt。

·D3DDEVCAPS_HWRASTERIZATION，指示设备是否对光栅化有硬件支持。

·D3DTRANSFORMCAPS_CLIP，指示当进行变换时硬件能裁剪。

因为硬件几何加速的特性集也许不同（如支持的光源的数目），caps结构指示设备执行哪个几何操作的子集。支持的光源的数目为0是有效的，指示硬件仅仅作变换。

软件实现几何管道使用的所有的关键状态和数据结构在DDI级可以得到。一些显示卡仅仅实现了硬件照明，而由主机处理器作变换和裁剪。

4.9.7.1顶点混合

在DirectX7.0支持顶点混合操作。顶点混合的工作方式是：模型空间的对象乘以世界空间的4x4的矩阵，结果将模型的原点置于一个相对于世界空间的原点的特别的世界空间。矩阵的一部分定方位，另一部分定位置。直到三个世界空间矩阵能被申请，通过混合顶点和对象上不同的权，这就允许对象被“弯曲”。一个顶点混合的例子可在Windows 2000中找到。

其次，视矩阵被申请，它有效地压缩相对于一个特别视点的空间，就像一个照相机将现实世界压缩成一个两维图像。

4.9.7.1.1多矩阵顶点混合

多矩阵顶点混合是用光滑的混合表层着色对象的技术。尽管在当前的软件中，这种技术用的并不多，但是大多数涉及动画角色的游戏使用某种光滑表层。

多矩阵顶点混合更新每个顶点的位置，而不需对每个顶点说明一个单独的4x4变换，从而允许执行光滑表层混合。它适合有连续光滑表面的常规情况，并仅仅要求一个额外的值---表层权值Beta---每个顶点最小的额外带宽要求。

指明的顶点位置数据被所有的世界空间矩阵变换，结果再与相应的顶点权值混合。

对任何顶点的标准都执行相同的步骤。这就产生一个单顶点（带着位置和标准），然后它被注入管道的剩下部分以便进行常规照明和裁剪。更多的细节参见多矩阵顶点混合实现算法。

有多种几何混合技术。这里描述的技术是几何混合的一种形式。它混合穿过独立的关节模型接合部位的顶点，用减少的多边形数目提供逐步增加的现实表现。当每帧的结合部位改变时，几何管道更新附近顶点的位置，使在结合部位的段之间进行光滑的混合。这个能力在除了旋转以外的结合类型（如转换、伸缩、修剪或者任何由4x4变换矩阵能表示的结合类型的结合）的段之间也可以实施。

一般原理是通过混合结合部位的顶点以扩展典型的被分割的角色。分段模型被认为从段或者坐标系统逐步建立，每个由一个变换矩阵定义。传统上，每个顶点只属于一个单独的段。此范例已被扩展以允许顶点可以部分属于别的段。在两个或多个段的结合部位附近的每个顶点有一个表层权值Beta---也就是属于别的段的比例。

当用在经典的层次结构模型中，“根”段没有混合。因为没有别的变换来混合，所以它总是固定的形式。表层权值Beta被定义为使用的父坐标系统的比例。如果段完全固定（DirectX7.0以前都是这种情况），则所有的对象的表层权值Beta为0.0。

对那些严格依附父段的部分Beta是1.0。它随着父段对混合的贡献的降低而降低。一些点必须是1.0，这是为了维护几个段之间的几何连续性。

角色经常包含大多数场景中的多边形，是一个关键的性能问题。对帧率等级行为有严重影响。对交互式应用，水平帧率比高平均帧率更重要。

顶点混合必须在变换后执行。如果混合没有被集成进着色管道中，变换必须执行两次：为表层执行一次，为着色执行一次。

集成顶点混合到几何管道中允许顶点数据的一次变换，在有限带宽的情况下这就使它和未混合的顶点一样快。

4.9.7.1.1.1 多矩阵顶点混合实现算法

此处实现的算法假设仅使用两个矩阵。

当矩阵改变，更新下面的四个矩阵：

CTM 当前变换矩阵=WORLD*VIEW-1*PROJ

CTM2 第二CTM（父坐标）WORLD1*VIEW-1*PROJ

ITCTM 如果照明，需要反转的CTM

ITCTM2 如果照明，需要反转的CTM2

注意在一些情况下首先使用顶点的权来混合矩阵也许更有效率，然后仅做一次矩阵与顶点的乘积。

FVF编码的变化

对多矩阵顶点混合API的关键性影响是对FVF的位置部分增加了顶点混合权参数。这些参数存储为32位IEEE单精度浮点数。通过在FVF编码增加的四个新的位模式来指示它们在输入的顶点数据中提供：D3DVFV_XYZB2，D3DVFV_XYZB3，D3DVFV_XYZB4，D3DVFV_XYZB5。

注意这些编码标识占据的额外的DWORD空间也许已经被别的应用所申请，如粒子半径或雾化参数，这取决于哪一个特性被激活。

注意 如果说明的混合权数字小于当前正在被混合的矩阵数目，那么赋给最后的矩阵的权被定义为（1.0-Bt），此处Bt是那个顶点的别的权的的总和。

D3DTRANSFORMSTATE的变化

多矩阵混合也要求三个额外的世界空间变换矩阵的说明。

除了原来的矩阵---D3DTRANSFORMSTATE_WORLD（技术上称为WORLD0），D3DTRANSFORMSTATE_VIEW，D3DTRANSFORMSTATE_PROJECTION---现在有：D3DTRANSFORMSTATE_WORLD1（要混合的第二个矩阵），D3DTRANSFORMSTATE_WORLD2（要混合的第三个矩阵），D3DTRANSFORMSTATE_WORLD3（要混合的第四个矩阵）。

注意这些并不是在原来的D3DTRANSFORMSTATE_WORLD0之后的连续的枚举。

没有被这个调用定义的但是被允许混合的矩阵被认为是和默认的矩阵相同的矩阵。

D3DRENDERSTATETYPE的变化

定义一个新的着色状态以激活和控制多矩阵顶点混合操作：D3DRENDERSTATE_VERTEXBLEND。它接受以下的D3DVERTEXBLENDFLAGS参数：

·D3DVBLEND_DISABLE（仅使用TRANSFORMSTATE_WORLD0）

·D3DVBLEND_1WEIGHT（在两个矩阵间混合）

·D3DVBLEND_2WEIGHTS（在两个矩阵间混合）

·D3DVBLEND_3WEIGHTS（在三个矩阵间混合）

·D3DVBLEND_4WEIGHTS（在四个矩阵间混合）

注意当使用D3DVBLEND_1WEIGHT时，两个矩阵仍被混合，第二个矩阵的权被计算为（1.0-第一个的权）。

即使额外的混合世界空间矩阵也许已经用SetTransform定义，任何权值超过在此着色状态所说明的权的大小的矩阵，其权被设置为0。

4.9.7.2用户裁剪位面

用户定义的裁剪位面在DirectX7.0中使用。这些工作就象其他裁剪位面一样，但是由应用来设置。驱动程序通过在D3dDrawPrimitives2响应D3DDP2OP_SETCLIPPLANE操作码，处理这些位面。

4.9.7.3纹理坐标变换

纹理坐标变换在DirectX7.0中使用。

纹理坐标变换是顶点级别的转换操作。这些操作能被任何变换和允许照明的HAL驱动程序以及任何HAL设备类型执行。

通过定义和每个纹理阶段相联系的4x4矩阵，纹理变换被激活。所有的软件实现几何管道用到的关键状态和数据结构在DDI级可以得到。

使用纹理变换，纹理坐标可相对于它们被绘制的起始点被移动。纹理变换描述了纹理坐标如何在纹理图中移动。每次进行一个纹理变换时，矩阵变换纹理的坐标。标准的世界空间矩阵在这些变换中使用。

在API一级，Idirect3Ddevice3：：SetTransform（D3DTS_TEXTUREi,mat）入口点定义了和当前第i个纹理状态绑定的纹理相联系的纹理变换矩阵。这个矩阵应当在着色时适用于那个纹理的纹理坐标。注意同样的顶点级纹理坐标集和一个分开的纹理变换矩阵能用在不同的阶段，这也许适用于不同的纹理。

纹理变换操作被一个由SetTextureStageState API调用（使用D3DTSS_TEXTURETRANSFORMFLAGS标志）设置的激活标志控制。

D3DTEXTURETRANSFORMFLAGS枚举用来控制纹理坐标集的维度,它由纹理坐标变换操作产生；也用来控制任何透视部分是否应当适用于它。

D3DTTFF_DISABLE指示纹理坐标被直接传送给硬件。

D3DTTFF_COUNT* 标志的数字说明会提供多少纹理坐标。注意如果使用映射纹理，它并不必须等于它们自己的纹理维度。

如果使用映射纹理，D3DTTFF_PROJECTED标志被设置以指示纹理坐标被纹理坐标集的最后一个元素（第COUNT个）划分。这样，对一个2D映射纹理，计数（COUNT）将是3，因为前两个元素被第三个划分，导致2D纹理查询是两个浮点数。也就是D3DTTFF_COUNT2和D3DTTFF_COUNT3| D3DTTFF_PROJECTED都引用一个2D纹理。

对于非映射纹理：

·D3DTTFF_COUNT1指示光栅期望1D纹理坐标。

·D3DTTFF_COUNT2指示光栅期望2D纹理坐标。

·D3DTTFF_COUNT3指示光栅期望3D纹理坐标。

·D3DTTFF_COUNT4指示光栅期望4D纹理坐标。

第一个字节是在一个特别的阶段期望使用的纹理坐标的计数值的编码。若将它设为0，即使用SetTransform调用定义了一个纹理变换，也将不会有纹理变换被应用。这是从纹理坐标变换阶段输出的数字。

传给纹理变换的纹理坐标个数由Direct3D API FVF码定义。

D3DTSS_TEXTURETRANSFORMFLAGS的默认是D3DTFF_DISABLED且D3DTTFF_PROJECTED标志没有设置。纹理变换矩阵默认是4x4的一致矩阵。

在非变换和照明HAL设备（即那些要求在主机处理器上进行变换操作）上，输出的顶点以及合适的DDI级的FVF编码被提供给驱动程序，这也许不同于在API级所说明的。不支持硬件加速变换和照明的设备也许仍使用映射纹理，所以驱动程序必须仍要对D3DTSS_TEXTURETRANSFORMFLAGS做出响应。

4.9.7.4立方环境图支持

在Direct3D中多纹理支持允许为照明和反射使用环境图。但是，单图360度解决方案如环形或球形图并不足够健壮，使它能够被广泛用在实时环境中。对于实时产生和引入360度环境的最适合的办法是一个由六个纹理（面）组成的立方图。每个面通过将90度视野的照相机置于合适的方向而产生。每个顶点矢量（常态、反射、折射）提供给光栅化硬件，然后硬件将它们穿过多边形并计算每个立方图面的增加的矢量的交集。如果应用或API产生立方环境图，驱动程序不需要变换矩阵或坐标空间（每个顶点矢量在其中被定义）的信息。这是因为矢量仅被用来引入环境图，它们逻辑上在同一坐标空间。

引入环形图涉及矢量规范化；引入球形图要求使用三角函数。所有类型的单图环境都是非线性的：环形图的外围附近是极度变形和各向异性的，然而球形图在它的轴附近有很大的变形。这就使每次视点改变（中心视区变得扭曲）时必须重建环境图。

立方环境图，通过放置一个真的或者仿真的90度视野的照相机在六个不同的方向而形成，就避免了上述的那些缺点。它们能更快地产生，不需要频繁更新，变形较小，能通过使用类似于为透视矫正纹理映射使用的等价物而引入。

总之，立方图对于为复杂的照明和反射提供实时环境映射是好的选择。使用最广的方法在D.Voorhies和J.Foran所著的Siggraph 94中有详细论述。

使用D3dDrawPrimitives2着色状态机制使立方图传给驱动程序。FVF纹理坐标和它的FVF编码01也被传送。

立方图定义在世界空间坐标；即：它的世界空间变换矩阵是相同的。如果纹理变换被用在相应的纹理坐标索引上，立方图可在不同的空间中出现。这些纹理坐标索引正对者第四面---+z面。Y轴默认向上。u、v格的原点在每个面的左上角，通过从立方的中心放置照相机而不需要任何变换，从而允许面的建立。

DirectDrawCreate接受一个指示立方图要建立的标志。一些面在API级不需分配，尽管驱动程序也许宣称它需要。表面描述符包含一个位域，六位指示出应用期望使用的面。当用GetAttachedSurface枚举面时，面为NULL的被略去。每个面的维度从它的表面描述符中可以得知，面的位域指示它是哪个面。

从DirectDrawCreate返回的指针实际上是指向立方图第一个非空的面的指针。面标识符可以用表面的位编码获得。这就是在CreateTexture使用的指针，以得到一个纹理对象，此对象被传给SetTexture使此图可以在多纹理管道中可用。

如果任何表面想要着色，立方图必须用D3DPTEXTURECAPS_CUBEMAP标志集建立。

任何没有被此调用建立的面都假设为使用在表面描述符的EmptyFaceColor域说明的颜色填充。

注意 （当前限制）所有的立方面必须有相同的大小、面积、2的幂。它们能被mipmapped。

立方图纹理不支持颜色按键。和别的纹理一样，alpha通道和alpha调色板都被支持。

4.9.7.4.1环境映射和标准扩散照明的集成

标准的扩散照明计算能在几何管道中执行。但是，为了获得最好的真实感，反射部分可以被立方环境映射产生。当两者都在操作中时，应用说明的FVF应包含一个为照明计算的矢量和一个独立的3D纹理坐标集（使用在矢量进入立方图时）。

4.9.7.5顶点和像素雾化

雾化有三个主要的类型：线性，指数和指数的平方。有两种主要的实现方法：顶点雾化（也称为重复或局部雾化）和像素雾化（也称为表或全局雾化）。

雾化混合因子f用在所有的计算中。它代表了雾的颜色和对象颜色的比例。最终的颜色由对象颜色和雾化混合因子f的乘积加上雾化颜色和（1-f）的乘积所决定，即：Color=f*objColor+（1.0-f）*fogColor。因此，雾化混合因子为0时，颜色是全雾色；为1时，则是对象色。典型的，f随着距离而减小。图4.6是线性雾化的图。

图4.6 线性雾化

如图4.6所示，线性雾化密度随距离的增加而呈线性增加。这不同于指数雾化，它的雾化密度是指数增加。线性雾化可以如下设置：FOGTABLESTART设置为Zfront且f=1.0；FOGTABLESTART设置为Zback且f=0.0。FOGTABLEDENSITY被忽略。

4.9.7.5.1顶点雾化

顶点雾化使用D3DRENDERSTATE_FOGENABLE着色状态激活。顶点雾化对于大的多边形并不理想，因为顶点离得太远。顶点雾化能用来透视校正。它用于两种方式。第一种使用Direct3D照明码，用D3DVERTEX结构定义雾化混合因子。另一种使用D3DLVERTEX或者D3DTLVERTEX结构。对于建立定制的雾化效果如层次的、基于范围的、体积的等是有用的。

当使用D3DVERTEX结构建立雾化，驱动程序应当设置D3DPRIMCAPS.dwRasterCaps中的D3DPRASTERCAPS_FOGVERTEX标志。用D3Ddevice：：SetRenderState将FOGENABLE设为TRUE，FOGCOLOR设为24位RGB颜色。D3Ddevice：：SetRenderState被用来设置FOGMODE为LINEAR，EXP，EXP2。对线性雾化，FOGSTART和FOGEND被设置，对指数或指数平方雾化，FOGDENSITY被用D3DLIGHTSTATETYPE设置。

当使用D3DTLVERTEX结构建立雾化，D3DDevice：：SetRenderState用来设置D3DRENDERSTATE_FOGENABLE为TURE，设置D3DRENDERSTATE_FOGCOLOR和D3DRENDERSTATE_FOGTABLEMODE为D3DFOG_NONE。（后者在D3DTLVERTEX结构本身中设置。）雾化混合因子f在每个顶点都定义。这是反射RGBA的alpha成分。

这种类型的雾化允许应用使用层次的大气模型，如图4.7所示。

图4.7显示了层次大气模型中纬度和雾的密度的关系。

雾化混合因子在照明阶段计算，并放在顶点的反射色的alpha成分中。然后按照当前由D3DRENDERSTATE_SHADEMODE设置的阴影模式添加进去。

对于顶点V1，V2和V3的雾化混合因子，计算如下：

基于当前阴影模式，f1，f2，f3被内插入三角。如果：

src_color是源、被内插的、纹理颜色。

fog_color是当前雾化颜色（被着色状态D3DRENDERSTATE_FOGCOLOR设置）。

f是源、内插的、雾化混合因子。

c，新的颜色计算如下：

c=（1-f）*fog_color+f*src_color

这意味着如果雾化混合因子f=0，新颜色c将全是雾化颜色。如果f=1，将没有雾。

在一个顶点中的雾化因子是照相机位置到顶点的距离的函数。通过设置照相机空间的Z值，此距离可以被拉近。对于每个顶点的雾化，使用Mworld*Mview变换顶点分别计算

（在照相机空间），然后计算到顶点的距离。

在RGB模式，雾化因子f的范围从0到255，并被写到反射输出色的alpha成分中。

在Ramp模式，扩散和反射成分与雾化因子f相乘并被限制在0.0到1.0的范围中。

4.9.7.5.2像素雾化

像素雾化类似于顶点雾化，但是雾化混合因子在光栅化时而不是在照明时计算。像素雾画比顶点雾化更准确。在LVERTEX或者D3DTLVERTEX中说明的雾化混合因子被忽略。像素雾化受限于三种雾化类型（线性，指数和指数的平方）。它工作的方式是硬件在插入在每个像素的深度值中做一个表查询。如果在两者间它们被线性的插入，那么无需256个表项。未来版本的DirectX也许会提供非线性、z分布的补充。ZFront/Zback从0到1。通过设置PRIMCAPS.dwPRasterCaps成D3DPRASTERCAPS_FOGTABLE来使用像素雾化。D3DDevice：：SetRenderState用来设置D3DRENDERSTATE_FOGENABLE为TURE，D3DRENDERSTATE_FOGCOLOR为24位RGB，D3DRENDERSTATE_FOGTABLEMODE为D3DFOG_LINEAR、D3DFOG_EXP或D3DFOG_EXP2。此处，雾化混合因子按照三个着色状态计算如下：

fStart由着色状态D3DRENDERSTATE_FOGTABLESTART决定,范围为[0,1]。

fEnd由着色状态D3DRENDERSTATE_FOGTABLEEND决定,范围为[0,1]。

fDensity由着色状态D3DRENDERSTATE_FOGTABLEDENSITY决定,范围为[0,1]。

雾化混合因子f的计算基于z和上面描述的三个着色状态。实际的计算取决于着色状态D3DRENDERSTATE_FOGTABLEMODE。仅仅D3DFOGMOD_LINEAR使用雾化开始和结束值。

·D3DFOGMODE_NONE

没有像素雾化被应用。

·D3DFOGMODE_LINEAR

线性雾化的增长。

·D3DFOGMODE_EXP

指数雾化的增长。

·D3DFOGMODE_EXP2

指数平方雾化的增长。

典型的指数和指数平方雾化太过于昂贵而不能直接去做。相反，查询表使用当前雾化密度，在范围[0.0,1.0]的许多z值是事先计算好的，然后最近的表项能为当前z值使用或者一个在两个z值之间的线性混合被用来得到合适的雾化因子。

最终的雾化颜色以和顶点雾化相同的方式计算如下：

src_color是源、被内插的、纹理颜色。

fog_color是当前雾化颜色（被着色状态D3DRENDERSTATE_FOGCOLOR设置）。

f是雾化混合因子。

c，新的颜色计算如下：

c=（1-f）*fog_color+f*src_color

这意味着如果雾化混合因子f=0，新颜色c将全是雾化颜色。如果f=1，将没有雾。

4.9.7.5.3基于范围的雾化

雾化也可以是基于范围的。基于范围的雾化是有用的，因为在通常的基于z的雾化，一个对象能在视的一侧出现，但是当观察者绕着它旋转，因为它的z值的变化，此对象就消失在雾中。这在图4.8中说明。如果雾化是基于范围而不是深度，当观察者绕着旋转时它不会改变。对于飞行仿真、坦克游戏和别的应用（在此应用中，不希望当观察者绕着旋转时，在一定距离中对象会消失或者重现）这是必须的。为设置雾化为基于范围的，D3DPRASTERCAPS_FOGRANGE和D3DRENDERSTATE_RANGEFOGENABLE应被设置。

图4.8中说明了对基于范围的雾化的需要。注意如果在一个指定的范围内，仅有雾可见，观察者就能旋转而对象不会在雾中进出，就象通常的基于深度的雾化。即：能被看见的对象仍然可见而不管是否旋转。

4.10 Direct7.0发行事项

以下的章节描述了在DirectX7.0中的关键性升级。

4.10.1 FVF 更新

原来定义在DirectX6.0中的FVF编码现在支持在DirectX7.0中的对纹理坐标集的说明（在以下定义）。

除了DirectX6.0支持的常规2D纹理，DirectX7.0支持1D、3D、4D纹理。此外，纹理能被映射。当调用D3dDrawPrimitives2时，能检查D3DNYHAL_DRAWPRIMITIVES2DATA的dwVertexType以决定每个纹理坐标集的维度。

例如，如果一个顶点有五个纹理坐标集，每个纹理可以为1D、2D、3D或者4D，也可以是映射纹理。每个纹理阶段是独立的，所以每个坐标集的维度可以不同。包含在dwVertexType中的FVF编码的上16位被检查以决定每个纹理坐标集的维度。

纹理坐标计数是一个4位的域，范围从0 到8。这给出了在字的上16位给定的纹理坐标集的个数。FVF编码的上16位的每两位被分配给8个纹理坐标集之一。纹理坐标位的含意如下：

·00b 0 u，v 2维纹理坐标对

·01b 1 u，v，q 3维纹理坐标集

·10b 2 u，v，w，q 4维纹理坐标集

·11b 3 u 1维纹理坐标

三维纹理坐标集可为三种不同目的的任何一种使用：映射纹理（D3DTTFF_PROJECTED指定），体积纹理，或者立方图矢量纹理（由一套类似于D3DRENDERSTATE_WRAP0-7模式的着色状态决定。D3DRENDERSTATE_WRAP0-7已在纹理坐标集的基础里说明。）。

为了1D到4D纹理坐标和D3DRENDERSTATE_WRAP着色状态使用的标志分别如下：D3DWRAPCOORD_0（和D3DWRAP_U相同，说明了在u坐标的包装），D3DWRAPCOORD_1（和D3DWRAP_V相同，说明了在v坐标的包装），D3DWRAPCOORD_2（说明了在w坐标的包装），D3DWRAPCOORD_3（说明了在q坐标的包装）。

当使用投影纹理时，从相应的纹理坐标域而不是位置域得到RHW值。然而，位置域的RHW仍然为w缓冲区和模糊计算使用，因此不管哪一个使用都要提供。

4.10.2光栅更新

引用光栅被抽取到一个独立的DLL，使额外的WHOL测试异步地加入普通的DirectX发布循环（一般每季度一次）。它的更新用于支持任何加进API的光栅级操作，API或者在内核中，或者作为要求担保实现一致性的扩展。

光栅产品不能更新用于支持这些技术，因为在软件中运行时，环境在顶点级映射可能比在像素级快。

对于等同的情况，这个光栅在性能的术语中可能已经升级。


小型客户驱动程序

小型客户驱动程序(MCD：Mini Client Driver)是一个图形接口，其主要的目标是通过简化OpenGL、减少硬件的开销来加速OpenGL。MCD可用于设计所有的硬件加速器特别是高端的3-D硬件加速器。这样，它可以允许不同产品提供商在光栅化级别全面加速OpenGL。

小型客户驱动程序的名称来源如下：

■“小”指的是这样一个事实，即MCD驱动程序模型只使用了OpenGL的API部分子集用于加速。

■“客户驱动程序”则是指一部分OpenGL驱动程序是一个用户模式的动态链接库这样一个事实。确切地说，就是MCD32.DLL这一在用户模式下运行的动态链接库。

本章描述了MCD的体系结构及接口，并对MCD驱动程序的编写人员提供了一个设计及实现的纲要。

由于MCD接口设计用于加速OpenGL，因而所有的MCD实现都必须通过OpenGL相应的测试。全部OpenGL一致性的套件都是随Microsoft Windows 2000 DDK中一起发布的。

5.1 MCD头文件，示例代码及参考

开发MCD驱动程序所需的头文件是mcdrv.h，该头文件包含在Windows 2000 DDK中。实现MCD模型的示例代码应该考虑作为MCD的最根本的定义。Windows 2000 DDK在mga视频显示目录中提供了一个实现MCD驱动程序的示例代码。MCD函数的功能及结构引用可通过DDK在线式的图形驱动程序参考中查找到。

Windows 2000 DDK之外的MCD材料，如MCD的绘制及光栅化，可参考以下资料：

■The OpenGL graphics System:A Specification by Kurt Akeley and Mark Segal(Silicon Graphics,Inc.)

■OpenGL Programming Guide by Neider,Davis,and Woo(Addison-Wesley,1993)

■OpenGL Reference Manual by OpenGL ARB(Addison-Wesley,1992)

Windows 2000 DDK中不提供OpenGL专用的设计及实现的细节，因为这些内容都包含在以上枚举的这些材料之中。

Win32相关的MCD接口方面的基本内容，如OpenGL，则可参考平台的SDK文档。

对一些经验欠缺的图形编程人员来说，Computer Graphics:Principles and practice by Foley,van Dam,Feiner,and Hughes(Addison-Wesley,1990)是一本非常有用的一般性图形参考书。

5.2 MCD体系结构

图5-1显示了Windows NT或Windows 2000中图形子系统的小客户驱动程序的体系结构。

插入图5-1 MCD体系结构图。

MCD由下面的部分组成：

■MCD32，由系统提供的，客户端的DLL，由OpenGL装载及调用。它是一个较薄的层次，主要执行数据建立并调用Win32®的ExtEscape函数，并从OpenGL用户模式发送命令到MCD的内核部分。该模块在本文档中也称为客户端MCD。

■MCDSRV32，由系统提供，是一个由内核模式的显示驱动程序装载的服务器端DLL。MCDSRV32管理着OpenGL与显示驱动程序之间的传输层。也执行大对象管理及MCD驱动程序的参数校验。这些任务需要驱动程序开发者的大量开发工作，即一般常说的设备独立性以及和硬件加速无关的工作。因此，MCD的这种体系结构使得3-D驱动程序的编写人员设计实现健壮的驱动程序更加容易——其中一个关键的设计目标就是因为显示驱动程序是Windows NT/Windows 2000操作系统信任的组件。该模块在本文档中也指的是MCD Library或服务器端MCD。

■MCD，是指显示驱动程序的MCD部分。整个的显示驱动程序包括MCD部分在内，是由不同的图形硬件厂家实现的。该模块在本文档中指的就是MCD驱动程序。

MCD接口提供了显示驱动程序存取OpenGL光栅化的状态以及被转换的顶点数据。为保证最高性能，在前端(transform/lighting)及MCD之间没有数据的翻译。就是说，MCD能处理的顶点数据量也就是OpenGL能处理的顶点数据量。相应的，MCD接口就需要显示驱动程序能处理浮点数据及操作，但该驱动程序不必使用GDI提供的特殊的FLOATOBJ_Xxx例程。

在处理绘制命令的任一点上，驱动程序都可以向OpenGL返回失败值，这就导致OpenGL必须使用它自已的软件仿真来结束未完成的绘制操作，MCD体系结构允许它利用帧缓冲并直接存取DirectDraw/DCI来进行。这样，便将软件因为返回原点造成的性能损失减少到最小程度，并且免去编写一些硬件不支持操作的软件仿真。另外，能为那些性能可以被改善的情况选择实现软件仿真。

MCD最显著的特征是它不具备位能力，这样便大大简化了驱动程序的实现，因为MCD驱动程序只需要实现用来加速硬件速度的函数。

MCD模块提供了一个语言/编译独立的、可以容易地升级驱动程序以及系统所需服务的实现环境。

5.3 MCD接口

本部分枚举了构成MCD接口的函数，其中一些函数是必须的，有一些是可选的，可选函数是否包含于MCD驱动程序决定于其被优化的功能。

一个MCD驱动程序的DLL不需要导出任何MCD函数。

以下列出支持MCD实现的显示驱动程序中所必须的、某些特定条件下必须的以及可选的MCD函数。

5.3.1必需的MCD函数

下表列出了MCD实现中必需的函数，其函数按字母顺序排列。

必需的函数 描述

MCDrvAllocBuffers | 为专用的表面分配缓冲
MCDrvBindContext | 将专用的设备环境内容与MCD表面绑定
MCDrvClear | 清空所需的缓冲区
MCDrvCreateContext | 产生一个新的绘制设备环境
MCDrvDeletecontext | 删除一个绘制设备环境
MCDrvDescribePixelFormat | 枚举MCD实现中所支持的像素格式
MCDrvDraw | 通过MCD实现绘制简单的OpenGL图像
MCDrvGetEntryPoints | 提供一个MCD驱动程序的入口点
MCDrvInfo | 向MCD调用者提供MCD驱动程序的基本信息
MCDrvSpan | 在软件绘制仿真中，传输像素数据
MCDrvState | 修改一个绘制设备环境的相关状态
MCDrvTrackWindows | 通知表面的驱动程序以及窗口地区的变化
MCDrvViewport | 提供具有运行简单剪辑信息的驱动程序

5.3.2特定条件下必需的MCD函数

下表列出了MCD实现中一些特定条件必需的函数，并描述了每一个函数所需的条件。

一定条件下必需的函数 描述

MCDrvGetHdev | 对一些专用的接口提供HDEV查询，在 WindowsNT/Windows 2000 MCD驱动程序中是必需的
MCDrvSwap | 在前台缓冲显示后台缓冲区中的内容，在支持双缓冲的像素格式中是必需的
MCDrvSync | 同步硬件绘制，对那些并不需要等待到硬件完成的从绘制调用返回的驱动程序是必需的

5.3.3可选的MCD函数

下表列出了MCD实现中可选的函数。

可选函数 描述

MCDrvCopyPixels | 拷贝一像素的矩形块到当前光栅位置
MCDrvCreateMem | 分配驱动程序内存
MCDrvCreateTexture | 在驱动程序内存中创建纹理
MCDrvDeleteMem | 释放由MCDrvCreateMem所分配的内存
MCDrvDeletetexture | 删除由专用的纹理使用的驱动程序分配的资源
MCDrvDescribleLayerPlane | 枚举专用的像素格式的分层平面
MCDrvDrawPixels | 将一个像素块写入某一特定缓冲区
MCDrvGetbuffers | 返回设备的前面、后面及更深的缓冲区的信息
MCDrvPixelMap | 建立一个由MCDrvDrawPixels，MCDrvReadPixels及MCDrvcopyPixels所用的翻译表
MCDrvReadPixels | 从特定缓冲区中读矩形的像素块
MCDrvSetLayerPalette | 对所有的特定分层平面设置调色板
MCDrvTextureStatus | 检索特定纹理的信息
MCDrvUpdateSubTexture | 修改已经建立的纹理的矩形区域
MCDrvUpdateTexturePalette | 修改纹理调色板
MCDrvUpdateTexturePriority | 指派或重新指派高效驱动程序管理纹理的高速缓存等特定纹理的优先级
MCDrvUpdateTextureState | 将纹理的边界/范围修改成为纹理的对象

5.4 MCD初始化

OpenGL通过装载客户端模块或者调用MCD初始化函数来开始初始化过程。MCD模块用Win32的ExtEscape函数从OpenGL向显示驱动程序发送命令。由于显示驱动程序直接通过DrvEscape函数处理Escape序列，该驱动程序必须能区别这些MCD Escape命令的含义并将它们传送到服务器端的MCD。那些需要MCD处理的Escape命令通过MCDFUNCS给出的Escape值区别开来。而一个MCD驱动程序检查一个Escape必须用它的Escape句柄进行检查。

如果一个escape是MCDFUNCS类型的escape，该显示驱动程序必须检查它是否之前已装载服务器端的MCD。驱动程序必须装载此模块以初始化OpenGL与驱动程序中MCD部分之间的通信层。装载是通过调用具有MCDENGDLLNAME这一常量的EngLoadImage函数来实现的，用已定义的常量可以保证与以后版本的MCD驱动程序的代码兼容性以及跨Windows NT/Windows 2000/Windows 95平台的兼容性。

在服务器端的MCD装载之后，显示驱动程序将通过执行以下步骤初始化其中的MCD部分：

1．获得服务器端MCD初始化例程的地址。驱动程序要将MCDENGINITFUNCNAME的常量值传递给EngFindImageProcAddress函数以确定初始化函数的地址。

2．调用上一步骤所确定的初始化例程，并将一个指针传递给MCD驱动程序的MCDrvGetEntryPoints例程。本步骤中，服务器端MCD调用MCDrvGetEntryPoints取回显示驱动程序给所有其他MCD函数提供的指针。

3．获得服务器端MCD的过滤函数地址。驱动程序将MCDENGESCFILTERNAME常量传递给EngFindImageProcAdress函数以确定过滤函数的地址，驱动程序将所有Escape命令的值等于MCDFUNCS的Escape全部过滤掉。

支持MCD的驱动程序中的DrvEscape例程示例代码可以在mga\escape.c中找到。

在服务器端MCD初始化以及MCD驱动程序的函数表获得之后，OpenGL调用驱动程序的MCDrvInfo函数以获得该驱动程序的基本信息。MCDrvInfo报告一些支持信息，诸如驱动程序支持的MCD版本、驱动程序版本的识别符、一次绘制的大小以及它的DMA能力等等。

5.4.1 Hdev句柄

每一个Windows NT/Windows 2000显示驱动程序都需要一个自已设备对象的句柄，这个句柄就是我们所知道的Hdev。Hdev是由GDI在驱动程序初始化时创建的并且被驱动程序保存以备调用GDI引擎服务时使用。由于服务器端的MCD也进行GDI引擎服务的调用以执行驱动程序对象管理以及内存分配等函数功能，因此，它同样需要设备的句柄。相应的，Windows NT/Windows 2000显示驱动程序必须在MCD初始化时调用MCDrvGetHdev函数之后返回Hdev这一句柄。

5.4.2像素格式

在初始化过程中——也就是在任何的设备环境还没有产生或者绘制还未完成之前，驱动程序被请求决定它可以支持的像素格式。像素格式是MCD驱动程序中唯一可以反映设备硬件能力的部分。

驱动程序的MCDrvDescribePixelFormat函数可以根据调用者专用的iPixelFormat索引返回相应的描述。该信息在pMCDPixelFmt指针的MCDPIXLFORMAT结构中返回，描述了特定格式设备的硬件能力。当驱动程序完成了该像素格式结构的填充，它可以返回它可以支持的总的像素格式的数量，如果失败则返回0值，如遇到不合法的像素格式索引或未完成的MCDPIXELFORMAT结构。如果pMCDPixelFmt指针返回空值(NULL)，驱动程序应当返回它可以支持像素格式的总数值，在这种情况下，返回0值并不意味着失败。像素格式标识符应当是一个基于索引的值。

为了支持OpenGL提供的软件仿真，驱动程序的像素格式应当支持下面的能力：

■总的色彩位数(即MCDPIXELFORMAT的成员值cColorBits)必须是6，16，24或32位。

■总是深度位数(即MCDPIXELFORMAT的成员值cDepthBufferBits)必须是16或32位。

当然，驱动程序也可以支持除上面之外的其他物理缓冲区配置，但是没有上面这些配置，它就不能凭空支持软件仿真。

MCDrvDescribePixelFormat同样标识一个驱动程序格式是否支持分层平面。该函数应该在MCDPIXELFORMAT结构中写出cOverlayPlanes和cUnderlayPlanes的数字以表示该驱动程序可以支持的最大分层平面数和最低数。如果一个驱动程序不支持分层平面则应将这两个值均设为零。支持分层平面的驱动程序，则可用MCDrvDescribeLayerPlane函数返回每一层的详细信息，详细内容可参考分层平面部分的内容。

5.5绘制设备环境

绘制设备环境定义了一个绘制应当怎样发生。一个MCD驱动程序负责生产、管理它自已的绘制设备环境。驱动程序开发者，则必须实现以下列出的设备环境相关的函数。

函数 描述

MCDrvCreateContext | 产生一个新的绘制设备环境
MCDrvBindContext | 将专用的绘制设备环境与某MCD接口绑定
MCDrvDeleteContext | 删除一个绘制设备环境
MCDrvState | 修改一个绘制设备环境的相关状态
MCDrvViewPort | 提供给驱动程序要执行原始裁剪所需的信息

下面的部分，我们将讨论设备环境相关的问题。

5.5.1设备环境的创建

在驱动程序执行任何的绘制之前设备环境就应该创建，因为当前的OpenGL的状态还没有明确地传递到驱动程序绘制的例程。在设备环境建立时将发生以下一些情况：

1． OpenGL按照默认的色彩规模，视口调节，方向及深度规模等分配并初始化MCDRCINFO结构。

2． 服务器端MCD分配MCDRC结构并初始化iPixelFormat、iLayerPlane以及createFlags成员值。

3． 服务器端MCD调用驱动程序的MCDrvCreaterContext函数，并传递MCDRCINFO和MCDRC结构。

驱动程序的MCDrvCreateContext函数主要执行以下步骤：

■检验驱动程序支持的像素格式以及在MCDRC结构中指定的层次平面。

■通过调用EngAlocMem函数分配设备专用的绘制设备环境并初始化绘制用的信息内容。MCDRC结构是驱动程序独立的绘制设备环境，它允许将所有的设备环境信息封装于同一个地方。这样，驱动程序就可以对被创建的驱动程序独立的绘制设备环境存储一个指针，该指针是以MCDRC结构的成员pvUser存储的。

■如果说明的表面没有被初始化，则需调用EngAllocMem函数给该表面的特定设备数据分配空间并按需要初始化，对窗口式表面，驱动程序可以在MCDWINDOW结构的pvUser成员中存储一个指针，MCDWINDOWS由服务器端的MCD来分配并维持；它的指针由MCDSURFACE结构中pWnd成员传递给驱动程序，该指针实质就是pMCDSurface指针。当没有别的设备环境和表面绑定时，此表面未被初始化。驱动程序可以检验缓存在MCDWINDOW结构pvUser变量中的信息以决定是否一个窗口式的表面是绑定到设备环境。

1.0版的MCD不支持非窗口式的表面。

驱动程序可以选择性地将绘制设备环境绑定到表面。特别是，绘制设备环境像素格式、创建标识以及如果支持的话，分层平面都将受到表面的限制，随后驱动程序的MCDrvBindContent被调用，绘制设备环境与表面的绑定也推迟到这个时候进行。设备环境产生的表面通常是限定于该绘制设备环境的表面。

■如果确定的表面被初始化，则检验该表面设备环境像素格式。一个表面的像素格式一旦受第一次设备环境限制，则设定便不能被改变。相应地，如果驱动程序需要的像素格式与已经存在的表面的像素格式不一致，其设备环境产生也将失败。

■将MCDRCINFO结构中的值随意地设为任意值以修改OpenGL计算方式比修改硬件达到此目的更具优势。如果驱动程序需求OpenGL给的不同的值时，一定要多加注意，因为在许多情况下，OpenGL对驱动程序不支持的绘制函数根本不能执行软件仿真。

一个独立的绘制设备环境不足以执行绘制。驱动程序必须还具有绘制的缓冲资源如后续或更深级的缓冲区。但是，驱动程序的MCDrvCreateContext函数并不分配这些资源，因为这由另外一些函数来处理。可以参考在绘制缓存区的帧缓冲管理部分获得更多的信息。

5.5.2设备环境绑定

调用驱动程序的MCDrvBindContext函数可以将已经存在的绘制设备环境与MCD表面绑定，这里，允许具有相同像素格式的表面共享同一绘制设备环境。

MCDrvBindContext函数实现的细节与MCDrvCreateContext实现类似，除非设备环境已经存在。如果没有以前的绑定存在，驱动程序将给新的特定设备表面数据分配地址并设置相应的像素格式，否则，驱动程序应当保证该专用的绘制设备环境的像素格式与表面匹配。如果像素格式不匹配，MCDrvBindContext函数调用失败。可以参考设备环境创建部分获得更详细的内容。

5.5.3设备环境删除

驱动程序的MCDrvDeleteContext函数用于释放任何由驱动程序分配的绘制设备环境数据。驱动程序分配的表面资源，如窗口数据以及绘制缓冲区等则不能在这时时释放，这些资源要等到窗口注销之后释放。

5.5.4状态

客户端MCD将状态信息存储于包含一个或多个MCDSTATE结构的缓冲区中。对于驱动的MCDrvState函数，这个缓冲状态总是在绘制完成以前刷新。

图5-2显示了一个包含两个MCDSTATE结构的状态缓冲区的例子。MCDrvState收到由pStart指针所指的存储于一个连续存块中的所有的状态命令及关联数据。

插入图5-2 MCDSTATE缓冲区

包含MCDSTATE命令缓冲区可能包含任何如下类型的状态：

状态 意义

MCD_PIXEL_STATE | 处理包含像素状态的状态
MCD_RENDER_STATE | 处理包含的光栅化级别的OpenGL的状态
MCD_SCISSOR_RECT_STATE | 处理包含删除的状态
MCD_TEXENV_STATE | 处理包含纹理环境状态的状态

MCD驱动程序应该处理所有的状态缓冲区，关联由绘制设备环境所支持的状态。驱动程序必须按它们发生在缓冲区中的顺序处理MCDSTATE命令。在处理任何的状态之前，驱动程序首先应当校验专用的绘制设备环境。

驱动程序可以跳过那些它不加速的状态并且依然返回真值(TRUE)，如果驱动程序返回假值(FALSE)，将导致所有的绘制失败直至收到一套完整的状态信息之后。否则，驱动程序可能以错误的或者不完全的状态信息运行。

MCDSTATE缓冲由客户端分配，但是驱动程序可以通过从MCDrvInfo的MCDDRIVERINFO结构的drvBatchMemsizeMax成员返回一个较大值，从而影响这种分配。

5.5.5视口

客户端的MCD调用MCDrvViewport提供驱动程序在3-D剪辑所需的缩放比例以及偏移值。驱动程序应当将这一信息存储在它的设备环境信息中，以备用户在以后的剪辑中使用。

5.6内存管理

默认情况下，服务器端的MCD分配数据传输所需的内存块并通过MCDMEM提供驱动程序存取内存。驱动程序的MCDrvCreateMem函数可以通过分配特殊区域支持DMA或者其他设备专用的能力而覆盖默认的分配值。一个驱动程序可以在设备有特殊的能力或需求时实现MCDrvCreateMem功能，例如，设备能够直接处理传递给MCDrvDraw的绘制缓冲区时。

驱动程序可以通过MCDrvDeleteMem释放任何特殊类型的内存。

5.6.1 DMA支持

不需要重新格式化数据缓冲区，只要通过设定MCDDRIVERINFO结构中成员drvMemFlags成员的MCDRV_MEM_DMA标识，驱动程序就可以利用DMA直接处理批次绘制。OpenGL分配好几个数据缓冲区并且允许设备在另一个缓冲区正在被OpenGL填充时处理一个缓冲区。服务器端的MCD库导出MCDEngSetMemStatus以协调驱动程序访问这些数据缓冲区。

5.7帧缓冲区管理

绘制设备环境产生之后，一个表面的像素格式所需要的绘制缓冲区必须在绘制完成之前分配，资源分配则是由MCD驱动程序来处理。

以下部分叙述了绘制缓冲是怎样产生、存取及删除，并且讨论了窗口跟踪的问题。特别是关于以下函数实现的基本指导。

函数 描述

MCDrvAllocBuffers | 给专用的表面分配所需要的缓冲
MCDrvGetBuffers | 返回设备前面、后面以及更深层缓冲的信息
MCDrvSwap | 在前面的缓冲区上显示后面缓冲区的内容
MCDrvTrackWindows | 通知驱动程序表面及窗口区域的改变

5.7.1分配绘制缓冲区

如果需要后面或更深层的缓冲区，驱动程序的MCDrvAllocBuffers函数将会决定是否专用的表面已经与这些缓冲区关联。如果需要的缓冲区不存在，该驱动程序将基于设备的像素格式及它们与表面的关联进行分配。缓冲的分配要靠显示驱动程序的中央视频内存堆管理程序来完成。

如果驱动程序成功分配完缓冲区，则它返回真值，如果该缓冲区已经存在，则它依然返回真值。因为，在缓冲区已经存在的情况下，MCDrvAllocBuffers函数仍然调用。否则MCDrvAllocBuffers将会调用失败。

缓冲区可以按每个窗口尺寸或全屏尺寸分配，全屏分配需要驱动程序剪贴所有的绘制缓冲区到这个窗口。

5.7.2查询绘制缓冲区

OpenGL调用MCDrvGetBuffers以获得对软件仿真描述分配绘制缓冲区的存取能力信息。如果该绘制缓冲区不存在，则返回FALSE。否则，驱动程序将根据pMCDBuffers指针所指的MCDBUFFERS结构返回前面、后面及更深层缓冲区的信息。

5.7.3交换绘制缓冲区

支持双缓冲区像素格式的MCD驱动程序一定有一个MCDrvSwap函数，对专用的表面来说，这一函数将后面缓冲区的内容在前面级冲区内显示。考虑到以下的因素，必须实现一种合理的交换机制：

如果绑定于特定表面的绘制设备环境用MCDCONTEXT_SWAPSYNC的标识集产生，则驱动程序应当尽可能同步交换缓冲区的请求与垂直返回显示，这样可以最小化图像撕裂现象，或者可以在一定程度上减少这种现象。

如果MCDrvSwap调用失败，将不提供软件仿真。

5.7.4 MCD中的窗口跟踪

显示驱动程序通过WNDOBJ结构来跟踪一个窗口的变化。在第二章的跟踪窗口变化部分中描述了WNDOBJ结构是如何产生的并且怎样随着对窗口的处理而变化，这里讲解一下特定于MCD的详细内容。

当设备环境被创建或被限定时，服务器端的MCD建立窗口跟踪。它调用GDI的EngCreateWnd函数以建立WNDOBJ并且注册WNDOBJCHANGEPROC回调结构。它注册的回调结构，反过来调用MCDrvTrackWindow例程，同时使得MCD驱动程序可以跟踪窗口变化。

驱动程序允许服务器端MCD处理大多数的窗口跟踪事件，驱动程序必须处理WOC_DELETE来释放所有被删除窗口的表面所占有的资源。

绘制受到剪贴矩形及其他剪辑的影响。驱动程序可以下面三种方式之一执行剪辑。

■利用OpenGL提供的MCDWINDOW结构中的成员pClip和pClipUnscissored传递给MCDrvTrackWindows的信息，即剪贴或剪辑的信息。这种情况下，驱动程序可以忽略由MCDSTATE结构中的MCD_SCISSOR_RECT_STATE类型确定的矩形剪贴。

■可以利用MCDWINDOW结构中的pClipUnscissored成员以及由MCDSTATE结构中的MCD_SCISSOR_RECT_STATE类型确定的矩形剪贴。

■可以构建或者维持它自已的剪辑清单并使用由MCDSTATE结构中的MCD_SCISSOR_RECT_STATE类型确定的矩形剪贴。

驱动程序主要靠跟踪WOC_RGN_CLIENGT来跟踪缓冲区大小的变化。如果设备精度已经改变，则驱动程序必须重新分配所有的表面绘制资源；如果设备精度没有改变，则驱动程序只有在它们与窗口尺寸相依赖时才重新分配缓冲区资源。

MCDrvTrackWindows的pMCDWnd参数一直标志表面信息为空值NULL，即，pMCDWnd对WOC_RGN_SURFACE及WOC_RGN_SURFACE_DELTA一直是NULL值。

5.8绘制操作

一个MCD驱动程序必须实现所有的MCD接口的绘制函数，这些函数在下表中列出：

绘制函数 意义

MCDrvClear | 清除特定缓冲区
MCDrvDraw | 绘制OpenGL原语
MCDrvSpan | 绘制软件仿真时像素数据传输的传输范围

在返回绘制调用之前，一个驱动程序不必等到硬件结束绘制就必须对OpenGL的同步机制实现MCDrvSync函数。

5.8.1绘制OpenGL的原语

所有的OpenGL原语是按批次传递给MCDrvDraw的。批次传送时包含一个或多个MCDCOMMAND结构，每个结构包含OpenGL原语类型、顶点指针、到下一原语的指针等信息。任意的OpenGL原语都可以结合在一起出现于命令缓冲区中，尽管驱动程序永远都不会被请求进行直线的循环绘制(GL_LINE_LOOP)，那是因为OpenGL将它们翻译成为线条(GL_LINE_STRIP)。

图5-3 说明了部分示例命令缓冲区可以传送到MCDrvDraw。

pStart所指示的指针包含两个MCDCOMMAND结构用于驱动程序处理。第一个MCDCOMMAND结构包含按次序排列的MCDVERTEX结构；第二个包含一个块，这个块列出了所有发现的MCDVERTEX结构按指针索引的列表。所有的命令和它们相关的顶点数据都存储于同一个缓冲区中。

驱动程序在MCDrvDraw函数中主要执行以下步骤：

■在处理任何原语之前，检验专用的绘制设备环境及窗口，检查窗口是否没有完成剪辑。

■按顺序处理每一个MCDCOMMAND结构直到任一个或全部命令绘冲区被处理，或者驱动程序控制软件仿真处理余下的缓冲区。如果命令缓冲区中的原语被专用于软件描述，则驱动程序不能处理它们。

驱动程序在处理每个MCDCOMMAND结构时必须考虑以下问题：

■flags成员意指是否挑选出来的原语是一个单色，或者它的线条需要重新设置。

■clipCodes成员提供了在原语中所有顶点的剪辑代码集合。驱动程序可以用这一信息来测试整个原语的接收或拒绝信息。

■如果pIndices成员值为NULL，则原语在MCDVERTEX结构中按次序批次处理。pStartVertex指针指示MCDVERTEX结构的开始，则驱动程序应当按顺序单步执行MCDVERTEX结构。如果pIndices值非空，原语将按根据MCDVERTEX结构中UCHAR的最大索引顺序批次处理。在pStartVertex[pIncices[0]]指示的MCDVERTEX结构，驱动程序能够按顺序处理这些索引，并根据索引指示存取MCDVERTEX结构。

■如果驱动程序支持纹理，它应该提供纹理的签别符textureKey，如果不支持纹理，则绘制时需根据原语。

在原语绘制时，状态变化是禁止的，驱动程序应使用绘制设备环境所提供的相关状态信息。

OpenGL对所有绘制都提供软件仿真，以下情况除外：

■由于OpenGL不支持的像素格式导致软件仿真不能完成。

■色彩值及MCDRCINFO结构中顶点可变值已经被驱动程序修改时软件仿真不能完成。

MCDrvInfo在初始化过程中调用时，MCD驱动程序可以覆盖OpenGL的批次处理的默认值，即MCDDRIVERINFO结构中的drvBatchMemSizeMax成员值。

MCD驱动程序不需要处理OpenGL的结构如顶点数组、显示列表及选择等等。

5.8.1.1每个最大值的考虑

驱动程序在处理每个MCDVERTEX结构时应当考虑如下几点：

■所有顶点指针都必须是验证为有效的，当被说明时，所有顶点的索引也必须是验证有效的。

■MCDrvDraw函数必须注意防止未屏蔽浮点数异常问题，所有坐标值及色彩值都说明为浮点值。

■MCDrvDraw应当提供专用的色彩范围、视口调节、方向、深度范围值等以适应MCDVETEX的成员。这些值是由OpenGL在设备环境创建时建立的，MCDrvCreateContext函数具有是否将它们覆盖到MCDRCINFO结构的可选功能，但是驱动程序却不是依赖OpenGL来为绘制执行软件仿真。

默认情况下，OpenGL提供了一个在windowCoord函数中指定窗口座标的优选项，从而提供从浮点数向固定点数(float-to-fixed-point)的完全转换。这一优选项在MCDRVINFO结构中的viewportXAdjust及viewportYAdjust成员说明。

5.8.2绘制变化范围

图5-4显示了MCDrvSpan是怎样取变化范围信息及内存块的。

插入图5-4

pMCDSpan参数指向一个MCDSPAN结构，该结构的pPixels成员指向第一个像素的区间。pMCDMem参数指向一个MCDMEM结构，它的pMemBase成员指向内存块的开始位置。要注意像素数据并不需要在该内存块的开始位置，由服务器端的MCD校验第一个像素是否在即定的内存区域内，驱动程序并负责检查最后的像素是否也是在区间范围之内。

在实现MCDrvSpan函数时，应当考虑以下问题：

■校验绘制设备环境及表面。

■校验传输的像数数据不是负数。

■根据读到的(或者被写的)第一像素到最后像素计算在适当的缓冲区的位置，并根据需要剪辑相关区间。

在仿真过程中或出现下边的任一种情况，将调用MCDrvSpan：

■不能获得对软件OpenGL的帧缓冲区的存取控制。

■对软件OpenGL的帧缓冲区的存取控制可以获得，但是帧缓冲区的格式与仿真不兼容。

MCDrvSpan函数每次调用时收到一个区间。对所有调用的完成，MCDrvSpan只提供部分的接口。

5.8.3清除缓冲区

MCDrvClear可以在绘制设备环境中用当前的值清除由buffers参数指定的缓冲区。在指定的缓冲区可以是色彩、深度、图案等的缓冲区的结合。

5.8.4同步化

驱动程序不必等待硬件完成绘制操作，并必须在返回绘制调用之前对OpenGL以同步机制实现MCDrvsync函数。驱动程序可以利用覆盖CPU及硬件应用等来实现此例程调用的最大性能。

MCDrvSync应当保证所有的硬件绘制在返回给调用者之前完成并实现刷新。

驱动程序如果回复软件仿真，则它将负责同步。但是，OpenGL在执行任何的像素功能前先调用MCDrvSync函数。

5.9纹理映像

MCD驱动程序开发者可以任选以下的纹理映像函数来实现。

纹理映像函数 意义

MCDrvCreateTexture | 创建驱动程序边(driver-side)的纹理
MCDrvDeleteTexture | 删除任何由特定纹理使用的驱动程序分配的资源
MCDrvTextureStatus | 检索特定纹理的信息
MCDrvUpdateTexturePalette | 修改已经创建的纹理的矩形区域
MCDrvUpdateTexturePalette | 修改特定纹理的调色板
MCDrvUpdateTexturePriority | 对特定纹理分配或重新分配优先值
MCDrvUpdateTextureState | 修改限定于纹理对象的纹理

在纹理映像中有两种相关的状态，即：纹理环境状态及纹理状态，这些将在下面部分讨论。

5.9.1纹理环境状态

MCD驱动程序可以利用纹理环境状态来决定纹理在绘制过程中是怎样应用的。在同一个时间OpenGL中可以存在好几个纹理，但是只有当前的纹理可以在绘制时被原语应用，正好是纹理环境状态应用的这一纹理。

MCDrvState由MCDTEXENVSTATE结构中收到纹理环境状态，当输入状态缓冲中包含一个MCDSTATE结构而它的state成员正好用于设置MCD_TEXENV_STATE。可以参考5．5．4状态部分学习驱动程序如何收到并处理状态的详细信息。

5.9.2纹理状态

纹理状态受限于专用的纹理，而不管它是否是当前纹理。图5-5显示了定义纹理状态的MCD结构的帧框架。

插入图5-5纹理状态结构

MCDTEXTURE是包含所有纹理信息的最上层的结构，所有的MCD纹理信息用MCDTEXTURE结构来存取每一个纹理的数据。驱动程序可以改变以下成员：

■驱动程序的MCDrvCreatTexture函数的成功调用必须产生并且返回一个单一的textureKey成员的成功标识符，无论它是否是装载的纹理，这个标识符随后便在绘制时传递到MCDCOMMAND结构的textureKey成员中。

■一个驱动程序可以根据需求使用userFlags和pvUser成员并可以随时修改这些成员。

pSurface和createFlags当前保留为系统使用。OpenGL提供并管理所有的纹理状态。也就是，驱动程序不能通过pMCDTextureData结构改变任一纹理的状态。由于MCDTEXTUREDATA结构在客户端空间来存放，驱动程序必须通过EngProbeForRead函数利用try……except在存取它的数据之前对其进行校验。校验必须包含任意的可通过MCDTEXTUREDATA结构指针存取的纹理数据。

MCDTEXTUREDATA结构包含以下信息：

■在TextureDimension中表示的纹理的维数。

■纹理调色板数据主要用于解释在paletteSize、paletterBaseFormat以及paletteRequestedFormat中的纹理色彩。纹理调色板中的一系列条目都可以通过驱动程序的MCDrvUpdateTexturePaletter函数来修改，也可以用于调色板动画中。

■所有的每个纹理状态，由textureObjState在MCDTEXTUREOBJSTATE结构说明。这包括纹理的包裹模式、边界颜色和过滤信息。MCDrvUpdateTextureState在和纹理关联的状态改变时调用。驱动程序相应地更新任何驱动程序的纹理状态。

■每个纹理对象的状态都由textureObjState函数中的MCDTEXTUREOBJSTATE给定的，它包括纹理对象的标识符、优先权。客户端的MCD调用MCDrvUpdateTexturePriority给驱动程序一个优先权的提示。驱动程序可以在对纹理进行高速缓存时利用这一信息。MCDrvTextureStatus则查询是否驱动程序在设备内存中对其进行了高速缓存。

■机器指令处理映像的信息则在level中保存。Level指向了一个MCDPIPMAPLEVEL结构的数目。每个结构提供一个指向文本数据以及其他可获得的机器指令处理器层的信息的指针。最大的纹理总是可以通过MCDMIPMAPLEVEL的第一个结构(level[0])获得。接下来的每个纹理的维数则一半是水平排列的另一半是垂直排列。如果其中的一个维数是1，即该维数一直处于1直到最高层的机器指令处理器映射达到1*1时。而每个纹理的大小则常常是2的幂。

当MCDTEXTURESTATE的成员minFilter或magFilter分别不等于GL_LINEAR或者GL_NEAREST时，机器指令处理器映像是允许的。如果机器指令处理器映像允许，并且所有的机器指令处理器映像层次都不在level指针所指的块中，则纹理的绘制必将失败。

5．10像素操作

MCD驱动程序开发者可以选择性地实现下列像素函数：

像素函数 意义

MCDrvCopyPixels | 拷贝一个像素块到当前的矩形块
MCDrvDwawPixels | 将一个像素块写到一个特定缓冲区中
MCDrvPixelMap | 建立起翻译表或映像，它由MCDrvDrawPixels、MCDrvReadPixels和MCDrvCopyPixels使用
MCDrvReadPixels | 从一个专用的缓冲区中读矩形的像素块

驱动程序实现的像素函数常常比软件仿真实现更好的加速功能。如果驱动程序的像素函数调用失败，OpenGL一般通过软件仿真来实现这些操作。

5.10.1像素状态

驱动程序在执行像素操作时应当使用当前的像素状态。驱动程序不能以任何方式改变像素状态。

当输入状态缓冲区中包含一个MCDSTATE结构而它的state成员设置为MCD_PIXEL_STATE时，MCDrvState函数可获得在MCDPIXELSTATE结构中的像素状态。参考5.5.4状态部分，了解驱动程序如何接收并处理状态的详细信息。

图5-6显示了MCD结构定义像素状态的框架

插入图5-6 像素状态结构

MCDPIXELSTATE是一个包含所有像素状态的顶层结构，它包括以下信息：

■像素的传输状态。在MCDPIXELTRANSFER结构的pixelTransferModes指定这一状态。该状态确定了驱动程序的MCDrvCopyPixels，MCDrvDrawPixels以及MCDrvReadPixels函数应当怎样将像素块从帧缓冲的一部分传输到另外一部分，从帧缓冲区传输到客户端内存，或各自从客户端内存传输到帧缓冲区。

■像素的打包状态。在MCDPIXELPACK结构的pixelPackModes指定这一状态。这一状态确定了驱动程序的MCDrvReadPixels函数应当怎样对从帧缓冲区读到的数据进行格式化并在被返回的像素阵列中存储。

■像素的解包状态。在MCDPIXELUNPACK结构的pixelUnpackModes来指定，这一状态确定了驱动程序的MCDrvDrawPixels函数应当怎样从收到的像素阵列中读取数据。如果MCDrvDrawPixels以packed值等于TRUE来调用，则驱动程序应当按以下表格的值覆盖MCDPIXELUNPACK中的指定值。

MCDPIXELUNPACK成员 覆盖值

swapEndian | FALSE
lsbFirst | FALSE
lineLength | Width
skipLines | 0
skipPixels | 0
alignment | 1

■ReadBuffer中的读缓冲区。该缓冲区被驱动程序的MCDrvCopyPixels和MCDrvReadPixels函数用作源缓冲区。

■当前光栅位置，即rasterPos。它指定了驱动程序的MCDrvCopyPixels或MCDrvDrawPixels函数绘制缓冲区时指定像素块的左下角。它在窗口的坐标系中指定。

在用户模式的内存中，传递给MCDrvDrawPixels的指针参数pPixels指向一个像素块。驱动程序在读取任何数据之前，必须通过调用包含try……except从句的EngProbeForRead函数来检验其读取位置的有效性。类似的，驱动程序也必须在写任何数据之前，通过调用包含try……except从句的EngProbeForReadAndWrite。像素数据也是在窗口的坐标系中指定的。

5.11分层平面

驱动程序开发者可以选择性地实现以下分层平面函数：

重叠平面函数 意义

MCDrvDescribeLayerPlane | 枚举MCD实现中支持的特殊像素格式的所有分层平面
MCDrvSetLayerpaletter | 对专用的分层平面设置物理调色板的值

在MCD驱动程序中，所有的分层平面支持有选择的实现。软件仿真不支持分层平面，因此在驱动程序中实现分层平面支持的程序开发者必须处理驱动程序中所有的操作。

本部分接下来的内容指定驱动程序怎样描述它的分层平面并设定分层平面调色板。

5.11.1描述分层平面

任何的设备环境产生或绘制完成之前，驱动程序的MCDrvDescribePixelFormat函数调用以确定它支持的像素格式。像素格式是MCD驱动程序唯一暴露设备硬件能力的部分。驱动程序在MCDPIXELFORMAT结构中以如下的内容指出它所支持的像素格式：

■cOverlayPlanes成员指定了由驱动程序像素格式提供的重叠的平面的数量。

■cUnderlayPlanes成员指定了由驱动程序像素格式提供的其下面的平面数量。

■dwTransparentColor成员指定了与一像素格式相关的这一分层平面指定的透明式的色彩索引。

■dwFlags成员中指定的PFD_SWAP_LAYER_BUFFERS标识指示分层平面的交换缓冲区的特征，当设备分层平面之间可以独立交换而不是作为一个整体交换时，驱动程序应当设置该值。

如果驱动程序支持分层平面，MCDrvDescribeLayerPlane将通过填写MCDLAYERPLANE结构中的专用的分层平面来指定其特征。

MCDrvCreateContext函数处理设备环境产生并使其可以绘制分层平面。可以参考设备环境的产生部分以获得详细资料。

5.11.2交换分层平面

驱动程序的MCDrvDescribePixelFormat函数指示了通过设置PFD_SWAP_LAYER_BUFFERS，即MCDPIXELFORMAT中的dwFlags位，来实现对独立交换分层平面的支持。若驱动程序的MCDrvSwap函数随后调用，则它应当考虑以下内容：

■如果没有设置PFD_SWAP_LAYER_BUFFERS，则驱动程序应当同时交换所有的层，包括主层，而不管传递给MCDrvSwap的flags参数的值。

■如果设置了PFD_SWAP_LAYER_BUFFERS，则 flags指定了那些平面之间应当交换。驱动程序可以忽略不支持的平面的flags。如果flags为零，驱动程序则应同时交换所有的层，包括主层，而不管PFD_WAP_LAYER_FUBBERS是否设置。也就是说，驱动程序应当将flags等于零当作flags的所有位都被设定来看待。

5.11.3管理分层调色板

OpenGL负责管理分层模板。驱动程序的MCDrvSetLayerPalette函数只有在需要实现调色板时才调用。MCDrvSetLayerPalette常常在一个具有分层平面的窗口接收到前台的程序初始化时的通知消息时才调用。

MCDrvSetLayerPaletter应当一对一映射专用的分层调色板到物理的调色板，也就是，对专用的调色板数据不需要翻译。驱动程序应当支持RGB和色彩索引调色板。

pPaletterEntries指针所指向的调色板缓冲区保证是校验有效的，因此，MCDrvSetLayerPaletter不需要再对之进行校验。


视频微端口驱动程序

Windows NT/Windows 2000视频微端口驱动程序(Video Miniport Driver)是特定适配器的、内核模式的驱动程序。每一种可在Windows NT/Windows 2000中应用的图形卡都必须有视频微端口驱动程序和显示驱动程序。

视频小驱动程序将它自身链接到视频端口驱动程序上，视频端口驱动程序是一个系统提供的内核模式的动态链接库(DLL)。视频端口驱动程序的VideoPortXxx函数调用微端口驱动程序来与系统及它的硬件进行通信。

Windows 2000微端口驱动程序必须以即插即用(PnP：Plug and Play)的驱动程序来实现。Windows 2000视频端口驱动程序同时继承性地支持Windows NT 4.0驱动程序。

本章提供了视频微端口驱动程序实现及编写的详细资料，描述了微端口驱动程序与显示驱动程序以及视频端口驱动程序的交互操作。对所有视频端口和微端口驱动程序的函数参考可以在图形驱动程序参考的在线DDK中查到。

显示驱动程序将主要在第一章显示介绍及第二章显示驱动程序中讨论。

6.1视频微端口头文件、示例代码及参考

Windows NT/Windows 2000视频微端口驱动程序包括下列头文件：

文件 内容

dderror.h | 由微端口驱动程序返回给视频端口驱动程序的win32状态常量，也可以返回到与微端口驱动程序对应的内核模式显示驱动程序
devioctl.h | 用于定义I/O控制代码的宏及常量
miniport.h | 视频微端口和SCSI微端口驱动程序所需的基本类型、常量和结构
ntddvdeo.h | 系统定义的I/O控制(IOCTLs)以及相应的结构等，可以按视频请求包VRPs传送到视频微端口驱动程序。
tvout.h | 用于实现TV连接以及拷贝保护支持的VIDEOPARAMETERS结构以及此结构中使用的常量
video.h | VideoPortXxx和SvgaHwIoPortXxx视频端口函数声明。视频专用的结构如VIDEO_REQUEST_PACKET以及HwVidXxx视频微端口函数原型
videoagp.h | AGP专用的结构，AgpXxx微端口驱动程序函数原型。以及支持视频微端口驱动程序的实现图形端口(AGP：Accelerated Graphics Port)加速所需的VideoPortXxx函数声明

这些头文件与Windows 2000 DDK一起发布。需要包含在这些头文件中的函数、结构、系统定义的I/O控制代码、常量的更详细信息，可参考在线DDK中图形驱动程序参考部分的内容。

Windows 2000 DDK同样提供好几个视频微端口驱动程序工作的实例代码，其内容放在video\miniport目录下。3dlabs微端口驱动程序实例实现了广泛的功能；s3virge微端口驱动程序实例则实现了VPE；mirror实例则包含该驱动程序专用的微端口驱动程序实现。

6.2在图形体系结构中的视频微端口驱动程序

图 6-1显示了Windows NT/Windows 2000图形子系统中的视频微端口驱动程序。

插入图6-1 Windows NT/Windows 2000图形体系结构

每一个视频微端口驱动程序提供硬件级的显示驱动程序支持，显示驱动程序调用图形引擎函数EngDeviceIocontrol请求下面的视频微端口驱动程序支持，并依次调用一个I/O系统服务以通过视频端口驱动程序向微端口驱动程序发送请求。

在大多数情况下，显示驱动程序完成对用户可见的时间关键的操作，而其下层的微端口驱动程序则提供对非经常性请求操作的支持，或者提供对不能被中断或设备环境转换到另外一个进程所剥夺的真正的时间关键性操作的支持。

显示驱动程序不能支持设备中断，只有微端口驱动程序可以建立起设备内存并将它映射到显示驱动程序的虚拟地址空间。

视频端口驱动程序是系统提供的用于支持视频微端口的驱动程序模块，并充当显示驱动程序与视频微端口驱动程序之间的中介。

要获得更多的Windows NT/Windows 2000的显示驱动程序的信息，参考第一章显示介绍及第二章显示驱动程序中的内容。

6.2.1视频微端口驱动程序特定平台的详细内容

在基于x86的Windows NT/Windows 2000平台，有两种视频微端口驱动程序：非VGA兼容微端口驱动程序和VGA兼容微端口驱动程序。

6.2.1.1非VGA兼容、基于x86的微端口驱动程序

许多微端口驱动程序是非VGA兼容的并因此非常容易实现。非VGA兼容的视频微端口驱动程序依赖于系统提供的VGA微端口驱动程序(vga.sys)或另外一个当前装载的VGA兼容的SVGA微端口驱动程序。这些微端口驱动程序的设置是在registry注册文件中将vgacompatible设置为零(FALSE)并且具有以下特征：

■它对全屏的、基于X86机器的MS-DOS应用程序不支持。但是，它是随着系统提供VGA(或者可能是VGA兼容的SVGA)微端口驱动程序的装载来提供对全屏的MS-DOS应用程序的支持。

■在大多数情况下，它是对没有VGA兼容模式适配器而编写的或者是为独立工作于VGA的加速器而编写的。

6.2.1.2VGA兼容的基于x86的微端口驱动程序

VGA兼容的微端口驱动程序主要包括以下特点：

■它基于系统提供的VGA微端口驱动程序，可以修改一定的代码来支持特定适配器的特性。系统提供的VGA显示驱动程序利用VGA兼容的微端口驱动程序支持，这样VGA兼容的适配器的微端口驱动程序开发者就不需要再编写新的显示驱动程序。

■它提供全屏MS-DOS应用程序的支持可以直接对适配器寄存器进行I/O操作。它同时作为一个视频检验器以防止这些应用程序发布任何使系统中止的指令序列。

许多SVGA适配器的微端口驱动程序也归入此类。但是对非SGVA适配器的任何新的微端口驱动程序可以根据驱动程序设计者的判断来提供VGA兼容支持。

VGA兼容的微端口驱动程序可以由在注册表中的vgacompatible设置成为1(TRUE)来配置。

6.3视频微端口驱动程序接口

接下来的部分枚举了构成视频微端口驱动程序接口的函数，一些函数是必需的，其他的函数有的是有条件的，有的是可选择的。而它们在微端口驱动程序中的包含则取决于适配器的特性和驱动程序的开发者。

除了DriverEntry函数，本文档建议对每一个必需的标准的微端口驱动程序都用假名。换句话说，即给出的每一个标准的微端口驱动程序除DriverEntry之外都可以由微端口驱动程序的开发者任意选择。

6.3.1必需的微端口驱动程序函数

以下系统定义的函数必须在每一个视频微端口驱动程序中实现。除了DriverEntry函数，所有其他的微端口驱动程序都是按字母顺序排列的。

必需的函数 描述

DriverEntry | 初始化视频微端口驱动程序
HwVidFindAdapter | 获取设备可枚举的总线的范围，如果可能，确定设备的类型。
HwVidGetPowerState | 查询设备是否可以支持需要的电源状态
HwVidGetVideoChildDescriptor | 枚举附加于特定设备的子设备
HwVidInitialize | 执行以前的对于相应的显示驱动程序的适配器的初始化。该函数调用是作为打开适配器的请求的响应。
HwVidSetPowerState | 设置特定设备的电源状态
HwVidStatrtIO | 开始处理进入的VRP

6.3.2一定条件下必需的视频微端口驱动程序函数

下面的系统定义的函数是在一定条件必需的视频微端口驱动程序函数。依赖于给定的适配器的特征及驱动程序的设备。它们同样按字母顺序排列：

一定条件下必须的函数 描述

HwVidInterrupt | 处理适配器产生的中断，如果微端口驱动程序的适配器产生中断则该函数是必需的。
HwVidResetHw | 重新设置适配器到字符模式，这样HAL可以在系统被控制宕机或崩溃时显示系统的关闭信息，或者在软启动时显示初始化信息。如果适配器的寄存器不能自动设定为INT 10复位，该函数是必需的。
HwVidSynchronizeExecutionCallback | 同步存取由HwVidInterrupt中断所共享的数据。允许关键部分的代码以更高的软件或硬件优先级别运行(IRQL,具有设备或系统中断屏蔽)。如果与HwVidInterrupt小驱动程序函数共享，则它是必需的。

6.3.3可选的视频微端口驱动程序

下面的系统定义的函数可以在视频微端口驱动程序中选择性地实现，函数以字母顺序排列。

可选函数 描述

HwVidLegacyResources | 返回一个特殊设备的继承资源
HwVidQueryDeviceCallback 和/或 HwVidQueryNamedValueCallback | 处理存储在寄存器中的配置信息。该函数在小驱动程序的HwvidFindAdpter函数利用I/O总线类型专用的配置信息或驱动程序提供的默认值时是可选的。
HwVidQueryInterface | 返回一个微端口驱动程序实现的函数接口，该函数接口可以由子设备来调用
HwVidTimer | 由视频微端口驱动程序大约隔一秒钟周期性调用一次
SvgaHwIoPortXxx 函数 | 由基于x86的全屏的MS-DOS应用程序校验存取I/O端口的有效性，这些函数是对VGA兼容的SVGA微端口驱动程序。可以参考VGA兼容的微端口的SvgaHwIoPortXxx获取更多的信息。

6.4视频微端口驱动程序初始化

视频微端口驱动程序初始化发生在NT内核、HAL及核心驱动程序如PCI总线驱动等装载及初始化之后。基本的系统初始化发生顺序如下：

■NT内核及HAL加载并初始化。

■内核驱动程序如PCI总线驱动程序加载并初始化。

■PCI总线驱动程序从每个子PCI的配置空间获得PCI资源信息及设备ID、供应商ID，并将这些信息报告给系统。

■如果PnP管理识别出设备或供应商的ID，I/O管理器将从已知的位置加载相应的视频微端口驱动程序和视频端口驱动程序。如果PnP管理器不能识别出这些ID，则它给用户提示加载微端口驱动程序的位置。

■I/O管理器用两个系统提供的指针调用微端口驱动程序的DriverEntry例程。DriverEntry用驱动程序特定及适配器专用的值分配并初始化一个VIDEO_HWINITIALIZATION_DATA结构，包括指向微端口驱动程序其他条目的指针。DriverEntry也必须声明其他继承资源，这些资源没有被列入设备PCI配置空间但是由设备进行解码。可参考声明遗资源部分获得更详细信息。

■微端口驱动程序的DriverEntry函数调用VideoPortInitalize函数。VideoPortInitialize执行微端口驱动程序初始化方面的工作，这是对所有的微端口驱动程序者非常一般性的工作。例如，对非即插即用的驱动程序，VideoPortInitialize验证微端口驱动程序初始化的VIDEO_HW_INITIALIZATION的结构，初始化一些系统产生的设备对象的公有成员，并对这些设备对象及扩展对象分配内存，收集并存储相关的扩展设备的信息。可以参见视频微端口驱动程序扩展部分获得更详细的信息。对即插即用的驱动程序来说，设备的对象相关动作则发生在以后的时间里。

■如果VideoPortInitialize返回，DriverEntry将其返回值再传播给VideoPortInitialize调用者。微端口驱动程序开发者应当对VideoPortInitialize的返回值不作任何的假设。

在这一点上来说，系统已经装载并且初始化了视频微端口驱动程序。下一步就是PnP管理器启动设备，可参考启动视频微端口驱动程序部分了解详细内容。

6.4.1启动视频微端口驱动程序

PnP管理器向视频端口驱动程序发送IRP消息请求启动图形适配器，视频端口驱动程序分派HwVidFindAdapter例程对IRP响应，HwVidFindAdapter的详细任务的讨论将在本部分的后面内容中叙述到。

■6.4.1.1建立视频适配器存取区间

■6.4.1.2设置注册表中的硬件信息

■6.4.1.3改变适配器中的状态

6.4.1.1建立视频适配器存取区间

一个VIDEO_ACCESS_RANGE类型的元素数组描述一个视频适配器解码内存和/或I/O端口的一个或多个区间，在这个数组中的每个存取区间元素都包含总线相关的物理地址值。

微端口驱动程序例程HwVidFindAdapter例程必须声明所有的PCI内存及端口或者适配器可以响应的所有区间的端口。依赖于适配器及在VIDEO_PORT_CONFIG_INFO中的AdapterInterfaceType值，HwVidFindAdapter可以调用下面VideoPortXxx函数以得到必要的总线相关的配置数据：

■VideoPortGetAccessRanges

■VideoPortGetBufData

■VideoPortGetDeviceData

■VideoPortGetRegistryParameters

■VideoPortVerifyAccessRanges

如果HwVidFindAdapter不能通过调用VideoPortGetBusData或VideoPortGetAccessRanges函数，或者利用VideoPortGetDeviceData或VideoPortGetRegistryParameters函数从寄存器中得到总线相关的可存取区间的相关信息，微端口驱动程序应当有一套总线相关的存取区间的默认值。

小驱动程序试图与适配器通信之前，HwVidFindAdapter函数应当映射每一个声明的总线相关的物理地址区到用VideoPortGetDeviceBase函数得到的内核模式的地址空间的区间。HAL能够将总线相关的存取区间值重新映射到系统地址的逻辑地址区间，尤其是在多条总线的机器中。

利用映射的逻辑区间地址，驱动程序可以调用VideoPortReadXxx和VideoPortWriteXxx函数以读或写到一个适配器，这些内核模式的地址也可以被传递到VideoPortCompareMemory，VedeoPortMoveMemory，VideoPortZeroDeviceMemory和/或VideoPortZeroMemory。对一个被映射的I/O空间中的区间来说，微端口驱动程序调用VideoPortReadPortXxx和VideoPortWriterPortXxx函数。对一个在内存中映射的区间来说，微端口驱动程序调用VideoPortReadRegisterXxx及VideoPortWriterRegisterXxx函数。

HwVidFindAdapter函数应当总是在调用VideoPortGetDeviceBase函数之前成功调用VideoPortVerityAccessRanges或者VideoPortGetAccessRanges。

■任何对VideoPortVerifyAccessRanges或VideoPortGetAccessRanges的成功调用将建立起一个微端口驱动程序的声明，即声明特定总线的视频内存和寄存器地址或适配器在注册表中的I/O端口。非常值得注意的是接下来对VideoPortVerityAccessRanges或VideoPortGetAccessRanges的任何顺序的调用都会导致驱动程序以前声明的资源被删除或者传送给最近调用函数的值所代替。因此，如果驱动程序单独调用函数声明区间，它必须传递到所有区间的数组，包括那些已经声明的区间。

■HwVidFindAdapter可以从适配器要求一小部分的存取区间，利用这一小部分区间来决定该适配器是否是微端口驱动程序支持的，并且利用另外一个函数调用VideoPortGetAccessRanges或者VideoPortVerityAccessRanges要求一个全部的对适配器支持的存取区间。同样的，对某专用的适配器的这些VideoPort…AccessRanges例程的成功调用，都会导致调用者以前在注册表中声明的信息覆盖。

■声明其他类型的硬件资源，如中断向量，一个微端口驱动程序应当在VIDEO_PORT_CONFIG_INFO中设置合适的值并调用VideoPortVerityAccessRanges或它应当调用VideoPortGetAccessRanges函数。

■对VideoPortGetAccessRanges或VideoPortVerifyAccessRanges的成功调用保证了微端口驱动程序不去使用已经由另外一个驱动程序使用的寄存器或设备内存地址。

■在寄存器中声明适配器的总线相关硬件资源，防止后来加载的驱动程序企图利用其他适配器的同一存取区间(即其他硬件地址)。它同样防止按顺序装载的驱动程序更改视频适配器的初始状态。

硬件解码的微端口驱动程序继承资源时必须通过它的DriverEntry例程来声明，或者，如果实现，则必须通过HwVidLegacyResources例程，继承资源是那些没有列入设备的PCI配置空间但由设备解码的资源。可以参考声明继承资源部分获得详细内容。

在微端口驱动程序装载之后，它的HwVidInitialize函数便已经运行，微端口驱动程序的HwVidStartIO函数调用，以映射那些微端口驱动程序对显示驱动程序可见的视频内存存取区间。

6.4.1.2在注册表中设置硬件信息

HwVidFindAdapter可以调用VideoPortGetRegistryParameters及VideoPortSetRegistryParameters函数来获得并设置注册表中的配置信息。例如，HwVidFindAdapter可以调用VideoPortsSetRegistryParameters来设置下一次启动时非易失性的注册表配置信息。也可以调用VideoPortGetRegistryParameters来获得由一个安装程序写入到注册表中特定适配器的、总线相关的配置参数。

推荐使用通过微端口驱动程序设置注册表中的一定的硬件信息，来向用户显示有用的信息或在调试时显示信息。微端口驱动程序可以设置芯片类型、DAC类型、适配器内存大小、鉴别适配器的字符串等，这些信息都是通过控制面板(Control Panel)的显示小应用程序来显示的。

驱动程序通过调用VideoPortSetRegistryParameters函数来设置这些信息，而且通常是驱动程序在它的HwVidFindAdapter例程中调用。

下表描述了驱动程序可以注册并提供的VideoPortSetRegistryParameters中ValueName和ValueData参数的详细信息：

条目信息 ValueName ValueData

Chip type | HardwareInformation.ChipType | 包含芯片名称的以NULL结尾的字符串
DAC type | HardwareInformation.DacType | 包含DAC名称或ID的以NULL结尾的字符串
Memory Size | HardwareInformation.MemorySize | 包含ULONG，以MB为单位，适配器上视频内存的总量
Adapter ID | HardwareInformaiton.AdapterString | 包含适配器名称的以NULL结尾的字符串
BIOS | HardwareInformation.BiosString | 包含BIOS信息的以NULL结尾的字符串

6.4.1.3改变适配器的状态

微端口驱动程序在调用HwVidInitialize例程之前不能永久性改变适配器的状态。微端口驱动程序例程在HwVidInitalize函数之前调用，如HwVidFindAdapter函数，它不能改变任何不需要的视频适配器的状态并且不能永久性改变任何视频适配器的状态。

当HwVidFindAdapter运行时，HAL已经控制了视频适配器，因此它可以在系统启动的早期阶段将信息写到屏幕上。如果HwVidFindAdapter试图签别适配器是否影响适配器状态，这一例程应当立即恢复原先的状态信息。因此一经HwVidFindAdapter返回，HAL就可以继续显示启动的消息。

例如，HwVidFindAdapter应当推迟决定一个适配器的DAC类型到HwVidInitalize函数，由于做这一决定并不会影响微端口驱动程序是否已经被装载，但是永久性地改变了适配器的状态。

6.4.2声明继承资源

视频微端口驱动程序必须在驱动程序初始化过程中在VIDEO_HW_INITIALIZATION_DATA结构中声明并报告所有的继承资源。继承资源是那些没有列在PCI配置空间但由设备解码的资源。Windows NT/Windows 2000遇到那些没有在本部分以提纲形式列出的未报告的继承资源时，将禁止电源管理及停靠功能。

微端口驱动程序必须在报告继承资源时做如下事情：

■如果对设备列出的继承资源在编译时可知，则将填充VIDEO_HW_INITIALIZATION_DATA结构中的两上字段，它是在DriverEntry例程中创建和初始化的。

结构成员 定义

HwLegacyResourceList | 指向一个VIDEO_ACCESS_RANGE结构，每一个结构描述了视频驱动程序未列入PCI配置空间的设备I/O端口或者内存区间。
HwLegacyResourceCount | HwLegacyResourceList指向的数组所包含的元素个数

■如果列出的设备的继承资源在编译时是未知的，则实现一个HwVidLegacyResources函数，初始化VIDEO_HW_INITIALIZATIONH_DATA结构的GetLegacyResources成员并指向该函数。例如，一个支持具有不同继承资源设置的两个设备微端口驱动程序可以实现HwVidLegacyResources以在运行时报告继承资源。小视频端口驱动程序实现HwVidLegacyResources函数时，视频端口驱动程序将忽略VIDEO_HW_INITIALIZATION_DATA结构的HwLegacyResourceList及HwLegacyResourceCount成员。

■根据微端口驱动程序的定义，对每一个VIDEO_ACCES_RANGE结构填充其RangePassive字段值。设置RangePassive为VIDEO_RANGE_PASSIVE_DECODE意味着该区由硬件进行解码，但是显示及视频微端口驱动程序将永不会接触到它。设置RangePassive为VIDEO_RANGE_10_BIT_DECODE意指设备对该区的端口地址进行10位解码。

同样，驱动程序应当包括一种由硬件解码的但没有被PCI声明的资源。驱动程序中声明的较小的继承资源的代码也许看起来如下边代码所示：

// RangeStart RangeLength

// | | RangeInIoSpace

// | | | RangeVisible

// +……+……+ | | | RangeShareable

// low high | | | | RangePassive

// v v v v v v v

VIDEO_ACCESS_RANGE AccessRanges[]={

//[0] (0x3b0-0x3bb)

{0x000003b0,0x00000000,0x000000c,1,1,1,0},

//[1](0x3c0-0x3df)

{0x000003c0,0x0000000,0x00000010,1,1,1,0},

//[2](0xa0000-0xaffff)

{0x000A0000,0x00000000,0x00010000,1,0,0,0},

};

//Within the DriverEntry routine:

VIDEO_HW_INITIALIZATION_DATA hwInitData;

hwInitData.HwLegacyResourceList=AccessRanges;

hwInitData.HwLegacyResourceCount=3;

微端口驱动程序可以按顺序调用VideoPortVerityAccessRanges重声明继承资源。但是，视频端口驱动程序简单地忽略任何以前声明的继承资源。如果微端口驱动程序企图在VideoPortVerifyAccessRanges声明一个继承存取区间，但是并没有在DriverEntry的HwlegacyResourceList中声明过或通过hwVidLegacyResources的legacyResourceList参数返回时，将不支持电源管理及停靠管理。

6.4.3初始化视频微端口与显示驱动程序的通讯

对每一个由PnP管理器发现的适配器，都可以由微端口视频驱动程序进行成功的配置，微端口驱动程序的HwVidInitialize函数在相应的显示驱动程序加载时调用。HwVidInitialize可以初始化软件的状态信息，但是它不应当配置成适配器上的可见状态信息。对HwVidInitialize函数返回，适配器应当设置成与微端口驱动程序的HwVidResetHw例程返回值相同的状态值。更多的HwVidResetHw信息，参考重新设置视频微端口驱动程序部分。

如果需要，微端口驱动程序的HwVidInitialize函数可以对适配器执行一次由HwVidFindAdapter发起的初始化操作。例如，一个微端口驱动程序可以推迟对适配器加载微代码并利用hwVidInitalize函数调用VideoportGetRegistryParameters。

当HwVidInitialize函数返回控制，图形引擎则对微端口驱动程序适配置器进行处理。相应的显示驱动程序可以调用引擎的EngDeviceIoControl函数以请求存取映射的视频内存或请求任何其他操作。视频端口驱动程序以VRP将这样的请求发送到微端口驱动程序的HwVidStartIO函数。参考视频请求处理部分获得更多信息。

通常，显示驱动程序控制显示给终端用户所见的内容，除了在偶然情况下即基于X86的Windows NT/Windows 2000机器上运行一个全屏的MS-DOS应用程序。关于支持这一特征的VGA兼容模式的微端口驱动程序，可以参考VAG兼容的微端口的SvgaHwIoPortXxx部分获得详细内容。

HwVidInitialize函数调用VideoPortGetRegistryParameters或者VideoPortSetRegistryParameters以获得或设置在注册表中的配置信息。例如，HwVidInitaialize可以调用VideoPortSetRegistryParameters来在注册表中设置下一次启动时的非易失性信息。它也可以调用VideoportGetRegistryParameters来获得特定适配器、总线相关的配置参数并由安装程序写入注册表中。

6.5视频微端口设备扩展

设备扩展是每个微端口驱动程序特定适配器状态信息唯一的全局存储地。

每个微端口驱动程序定义大小、内部结构及设备扩展的内容。视频端口驱动程序将一个指针作为一个系统定义的除DriverEntry之外的微端口驱动程序函数HwVidSynchronizeExecutionCallback及SvgaHwIoPortXxx函数的输入参数传送到每一个设备扩展，同时，许多VideoPortXxx函数需要将这一指针作为参数。

微端口驱动程序必须用设备扩展来维持单一适配器的状态信息。每一个系统检测到的适配器都在设备扩展里有一份单独的状态信息。微端口驱动程序不需要全局变量来存储任何适配器的状态。这对提供无缝的多显示器支持也是格外严格的。

6.6视频微端口驱动的注册回调信号

微端口驱动程序的HwVidFindAdapter函数可以请求检索注册表中的配置信息，并通过用驱动程序提供的回调函数调用下面一种的视频端口驱动程序函数。

■VideoPortGetDeviceData函数。该函数的 CallbackRoutine参数设置的指针指向一个驱动程序提供的HwVidQueryDeviceCallBak。

■VideoPortGetRegistryParameters函数。该函数的CallbackRoutine参数设置的指针指向一个驱动程序提供的HwVidQueryNamedValueCallback。

VideoPortGetDeviceData和VideoPortGetRegistryParameters从注册表的不同关键字中检检信息。相应地，每一个都声明微端口驱动程序来提供一个系统定义的具有不同格式参数的HwVid…Callbak函数。

任一HwVid…Callbak函数都不能检索显示驱动程序注册表中默认的配置信息，因为，它们常常是在显示驱动程序被系统加载之前调用的。

微端口驱动程序也可以调用VideoPortsSetRegistryParameters来存储信息，如在注册表中…\CurrentControlSet\Services\DriverName\DeviceNumber关键字下的芯片及DAC类型信息。更多的注册表中关于硬件的信息，可以参考设置注册表中硬件信息部分的内容。以后再从注册表中检索信息，则微端口驱动程序可调用VideoPortSetRegistryParameters。

需要注意的是，注册表信息可以通过调用VideoPortGetDeviceData来得到，但这是易失性的：它每一次都在系统装载时重新生成。同时也要注意，对视频微端口驱动程序，视频端口驱动程序由VideoPortGetRegistryParameters函数调用得到这些信息及配置信息。微端口驱动程序不能调用VideoPortSetRegistryParameters来向它相应的显示驱动程序传递配置信息。

6.7处理视频请求

那些产生于显示驱动程序调用EngDeviceIoControl的所有的I/O请求都被视频端口驱动程序从IRP映射到VRP。视频端口驱动程序然后用一个它自已建立起来的到每一个VIDEO_REQUEST_PACKET结构的指针。所有的VRP发送到HwVidStatIO，其IoControlCode成员被设置为IOCTL_VIDEO_XXX。

视频端口驱动程序同样同步管理所有对视频微端口驱动程序的请求，它向每一个微端口驱动程序HwVidStartIO例程一次只发送并处理一个VRP。HwVidStartIO拥有每一个输入VRP，直到微端口驱动程序完成请求的操作并且返回控制。当微端口驱动程序完成当前的VRP，视频端口驱动程序特有I/O管理器发送的任何突出的IRP，以响应由相应显示驱动程序对EngDeviceIoControl的后续调用。

对于收到一个视频请求，HwVidStartIO必须检查VRP，处理对适配器的视频请求，设置适当的状态以及其他在VRP中的信息，并返回TRUE值。

6.7.1系统定义的IOCTL_VIDEO_XXX请求

典型地，许多视频微端口驱动程序支持下列请求：

■IOCTL_VIDEO_QUERY_NUM_AVAIL_MODES

■IOCTL_VIDEO_QUERY_AVAIL_MODES

■IOCTL_VIDEO_QUERY_CURRENT_MODE

■IOCTL_VIDEO_SET_CURRENT_MODE

■IOCTL_VIDEO_RESET_DEVICE

■IOCTL_VIDEO_MAP_VIDEO_MEMORY

■IOCTL_VIDEO_UNMAP_VIDEO_MEMORY

■IOCTL_VIDEO_SHARE_VIDEO_MEMORY

■IOCTL_VIDEO_UNSHARE_VIDEO_MEMORY

■IOCTL_VIDEO_QUERY_PUBLIC_ACCESS_RANGES

■IOCTL_VIDEO_FREE_PUBLIC_ACCESS_RANGES

■IOCTL_VIDEO_GET_POWER_MANAGEMENT

■IOCTL_VIDEO_SET_POWER_MANAGEMENT

■IOCTL_VIDEO_GET_CHILD_STATE

■IOCTL_VIDEO_SET_CHILD_STATE_CONFIGURATION

■IOCTL_VIDEO_VALIDATE_CHILD_STATE_CONFIGURATION

依赖于适配器的特性，视频微端口驱动程序可以支持下列附加的请求：

■IOCTL_VIDEO_QUERY_COLOR_CAPABILITIES

■IOCTL_VIDEO_SET_COLOR_REGISTERS(如果设备有模析则必需)

■IOCTL_VIDEO_DISABLE_POINTER

■IOCTL_VIDEO_ENABLE_POINTER

■IOCTL_VIDEO_VIDEIO_QUERY_POINTER_CAPABILITIES

■IOCTL_VIDEO_QUERY_POINTER_ATTR

■IOCTL_VIDEO_SET_POINTER_POSITION

■IOCTL_VIDEO_QUERY_POINTER_POSITION

■IOCTL_VIDEO_SET_POINTER_POSITION

■IOCTL_VIDEO_HANDLE_VIDEOARAMETERS

VGA兼容的SVGA微端口驱动程序需要支持下列附加的请求：

■IOCTL_VIDEO_SAVE_HARDWARE_STATE

■IOCTL_VIDEO_RESTORE_HARDWARE_STATE

■IOCTL_VIDEO_DISABLE_CURSOR

■IOCTL_VIDEO_ENABLE_CURSOR

■IOCTL_VIDEO_QUERY_CORSOR_ATTR

■IOCTL_VIDEO_SET_CURSOR_ATTR

■IOCTL_VIDEO_QUERY_CURSOR_ATTR

■IOCTL_VIDEO_SET_CURSOR_POSITION

■IOCTL_VIDEO_GET_BANK_SELECT_CODE

■IOCTL_VIDEO_SET_PALETTER_REGISTERS

■IOCTL_VIDEO_LOAD_AND_SET_FONT

每一个IOCTL的详细信息可以在在线DDK的视频I/O控制代码部分查找到。微端口驱动程序开发者不应当使用不在文档中的、由系统定义的IOCTL。

6.7.2自定义的显示微端口IOCTL_VIDEO_XXX请求

一个微端口驱动程序可以为自已相应的显示驱动程序定义一个或多个自定义I/O控制代码。

但是，只有专用的显示微端口驱动程序对可以使用自定义的I/O控制代码。也就是，被设计运行于一个已经存在的显示驱动程序下的微端口驱动程序不应当定义私有的I/O控制代码，因为已经存在的显示驱动程序如果不被重写或者如果可能不破坏它已使用的微端口驱动程序就不能构建新的I/O控制请求。已经存在的在许多不同模式的适配器上的一类显示驱动程序，如SVGA适配器，同样不能依赖于私自定义的I/O控制代码来在每一个下层的微端口驱动程序中起到相同的作用。

6.7.3处理不支持的IOCTL_VIDEO_XXX请求

每一个HwVidStartIO函数也必须处理对不支持的IOCTL_VIDEO_XXX函数的接收，如下：

1．设置输入的VRP的Status字段值为ERROR_INVALID_FUNCTION。

2．设置输入的VRP的Information字段为零。

3．返回TRUE值意味着请求被处理。

参考VIDEO_REQUEST_PACKET和STATUS_BLOCK结构获得更详细信息。

6.8在视频微端口中支持PnP及电源管理

所有的Windows 2000微端口驱动程序必支持PnP和电源管理，包括枚举子设备如DDC监示器、I2C(I2C)设备以及二级适配器的能力。

视频端口驱动程序为微端口驱动程序管理许多PnP需求，包括创建功能设备对象(FDO：Functional Device Object)及代表微端口驱动程序接收和发送PnP专用的IRP。

微端口驱动程序必须实现以下的函数以支持PnP和电源管理：

■HwVidSetPowerState

■HwVidGetPowerState

■HwVidGetVideoChildDescriptor

一个继承的微端口驱动程序的图形适配器不能在系统运行时被删除掉，也不能在加入到一个正在运行的系统时自动检测到。

可参考图形适配器子设备部分获得关于检测及与适配器子设备通信的内容。对PnP驱动程序的一般性知识，参考即插即用、电源管理及建立设计指南部分。

6.9在视频微端口驱动程序中使用AGP

视频微端口驱动程序提供AGP支持，这样便允许微端口驱动程序设备直接存取系统的(物理)内存。

为使用AGP，微端口驱动程序必须首先检索视频端口的驱动程序实现的AGP来支持调用VideoPortGetAgpServices例程。视频端口对AGP返回一个指针并支持在微端口驱动程序中分配的将VIDEO_PORT_AGP_SERVICES结构传送给VideoPortGetAgpServices的例程。

一个微端口驱动程序可以基于系统的物理内存总量来设计它的AGP内存使用，这一信息保存在VIDEO_PORT_CONFIG_INFO结构的SystemMemorySize成员中。

6.9.1通过AGP存取系统内存

微端口驱动程序应当调用以下驱动程序提供的视频端口函数来使其设备可以存取主内存：

1．调用AgpReserverPhysical来在系统总线上保留一段物理内存区间，AGP控制器可以对其响应。

2．调用AgpCommitPhysical来锁定物理内存并将它映射到AgpReservePhysical返回的AGP解码的一部分物理地址上，微端口驱动程序可以多次调用AgpCommitPhysical来执行多次小的提交而不是执行一次大的提交。

这样，对一个可以看见并使用这一提交的AGP控制的内存应用程序来说，视频微端口驱动程序必须调用如下函数：

1．调用AgpReserverVirtual来在应用程序的地址空间预留一段虚拟地址空间。

2．调用AgpCommitVirtual来锁定虚拟内存并将虚拟内存映射到由AgpReservePhysical返回的AGP解码的物理地址区间。

应用程序可以使用这些提交的虚拟地址空间，如同这些虚拟地址空间是连续的一样。

6.9.2从AGP使用中释放内存

微端口驱动程序负责释放所有它保留的以及它提交的虚拟内存和物理内存，如同在通过AGP存取系统内存部分中简述的一样，它是通过调用下列的视频端口驱动程序的AGP服务例程来释放内存的。

■AgpFreeVirtual去除以前的AgpCommitVirtual函数调用所锁住的虚拟内存映射。

■AgpReleaseVirtual释放由以前的AgpReserveVirtual函数所保留的虚拟地址区间。

■AgpFreePhysical去消由以前的AgpCommitPhysical函数所锁住的物理内存的映射。

■AgpReleasePhyscal释放由以前的AgpReservePhysical函数所保留的物理地址区间。

6.10图形适配器的子设备

以下两部分讨论具有一个或多个子设备的图形适配器对微端口驱动程序的影响问题。

6.10.1侦测子设备

必须在微端口驱动程序中实现HwVidGetVideoChildDescripter来使PnP管理器可以侦测到图形适配器的子设备。

默认情况下，HwVidGetVideoChildDescriptor函数在父设备未启动之前不能调用。也就是，HwVidGetVideoChildDescriptor在HwVidFindAdapter结束之后才可以调用。为了覆盖这一默认值，以允许枚举在任何时间发生的的子设备，可以设置VIDEO_HW_INITIALIZATION_DATA结构的AllowEarlyEnumeration成员为TRUE即可。

一个设备在新硬件连接到系统或已经存在的硬件脱离开系统时都会产生中断，为处理这些中断，微端口驱动程序必须做以下事情：

■实现一个调用VideoPortEnumerateChildren函数的DPC(PMINIPORT_DPC_ROUTINE)。

■当一个设备上的中断产生时，实现一个调用VideoPortqueue到DPC队列的中断句柄(HwVidInterrupt)。

对每一个父设备的子设备，如果调用了HwVidGetVideoChildDescriptor函数，则也需调用VideoPortEnumerateChild函数对适配器的子设备进行重行枚举，PnP管理器根据需要修改父设备与子设备之间的关系。

6.10.2与子设备驱动程序通信

视频微端口驱动程序与子设备的驱动程序可以人为地定义一个允许子设备驱动程序与它的硬件通过父微端口驱动程序通信的接口。子设备驱动程序通过向父微端口驱动程序的视频端口驱动程序发送一个IRP_MN_QUERY_INTERFACE请求获得这一接口，接收到这样的请求后，视频端口驱动程序调用微端口驱动程序的HwVidQueryInterface函数，如果它已定义，并且微端口驱动程序返回一个接口的接针，则子设备的驱动程序就可以在任何时间通过HwVidQueryInterface函数将它调入到微端口驱动程序中。

如果微端口驱动程序没有实现HwVidQueryInterface或者调用失败，则视频端口驱动程序将请求传给微端口驱动程序设备的父，如果子驱动程序向该微端口驱动程序的另一个子设备发送了IRP_MN_QUERY_INTERFACE且另一子设备没有实现HwVidQueryInterface或者调用失败，则视频端口驱动程序返回一个错误。

由于子设备驱动程序可以无需知道任何视频端口驱动程序的相关知识而调入微端口驱动程序，微端口驱动程序就必须在所有HwVidQueryInterface发起的函数中同步存取它自身。它可以通过分别调用VideoPortAcquireDeviceLock和VideoPortReleaseDeviceLock来获得或释放视频端口驱动程序维持的设备锁。

子设备枚举是通过HwVidGetVideoChildDescriptor函数完成的。

6.11视频微端口中的中断

适配器的微端口驱动程序产生中断必须实现一个HwVidInterrupt例程，微端口驱动程序的DriverEntry例程应当初始化VIDEO_HW_INITALIZATION_DATA的成员HwInterrupt以指向中断句柄。

如果适配器产生中断，则视频端口驱动程序为微端口驱动程序建立起中断对象，由于它是由视频端口驱动程序产生并管理的，不需要了解进一步的关于中断对象的信息。如果感兴趣，可以参考内核模式的驱动程序指南获得详细信息。

如果微端口驱动程序的HwVidFindAdapter函数发现视频适配器实际上没有产生中断或者它不能决定对该适配器有效的中断向量或中断层次，则HwVidFindAdapter应当将VIDEO_PROT_CONFIG_INFO结构中的InterruptLevel和InterruptVector设置为零。

当HwVidFindAdapter返回控制，视频端口驱动程序检查在VIDEO_PORT_CONFIG_INFO中的中断配置成员，都为零，它不将中断连接到一个微端口驱动程序。如果微端口驱动程序允许函数入口点，则它的驱动程序是由DriverEntry函数建立来的。HwVidfindAdapter中断成员明确地设置为零将禁止HevidInterrupt的入口点，

需要注意的是，HwVidInterrupt可以存取微端口驱动程序的设备扩展，即使它没有分页。依赖于微端口驱动程序的设计，也许对其他的驱动程序函数安全地使用HwVidInterrupt，从共享设备或设备扩展一些特殊地区是不可能的。。

例如，假定微端口驱动程序的HwVidStartIO函数在适配器中断时用于存取设备扩展，HwVidInterrupt运行在另外一个处理器上，并且hwVidInterrupt也可以存取设备扩展，如果这种情况发生，则HwVidStartIO应当由驱动程序提供的HwVidSynchronizeExecutionCallback的函数去调用VideoPortSynchronizeExecution。

6.11.1何时实现HwVidSynchronizeExecutionCallbak例程

适配器的产生中断的微端口驱动程序很少用HwVidSychronizeExecutionCallback函数去调用VideoPortsynchronizeExecution，实际上，微端口驱动程序都有一个HwVidInterrupt函数而没有必要有HwVidSynchronizeExecutionCallback函数。因为视频端口驱动程序在处理完以前的请求之前不向微端口驱动程序的hwVidStartIO函数发送请求(参考处理视频请求部分)，微端口驱动程序很少调用VideoPortSynchronizeExecution。

对微端口驱动程序HwVidSynchronizeExecutionCallback函数有两种可能的使用：

■用微端口驱动程序的驱动程序函数的设备扩展而不是HwVidInterrupt函数存取适配器寄存器。当HwVidsynchronizeExecutionCallback函数给定控制，从适配器来的中断也被屏蔽，这样微端口驱动程序的HwVidInterrupt函数将不能改变HwVidSynchronizeExecutionCallback函数运行在SMP机器上时设备扩展的状态。

■如果适配器需要，向适配器寄存器或者端口写命令是非常快的。当HwVidSynchronizeExecutionCallback给定控制，几乎所有的系统中断被屏蔽掉，这样HwVidSynchronizeExecutionCallback函数将不能被一个设备中断(或时钟中断)剥夺。并且，hwVidsynchronizeExecutionCallback函数必须尽可能快地返回控制。

对上面第一种类型的HwVidsynchronizeExecutionCallback函数，微端口驱动程序按设置给VpMediumPriority的Priority值来调用VideoPortsynchronizeExecution。对第二种类型的HwVidsynchronizeExecutionCallback函数，如果驱动程序没有HwVidInterrupt函数，则微端口驱动程序同样按设置于VpMediumPriority的Priority值来调用。否则，这样的微端口驱动程序将按设置于VpHighPriority的Priority来调用。

一般情况下，微端口驱动程序不能用第二种类型的HwVidSynchronizeEcecutionCallback函数调用VideoPortsynchronizeExecution，除非驱动程序设计者有其他的选择——即，适配器被用系统的中断屏蔽来编程。否则，微端口驱动程序应按设置于VpLowPriority的priority值来调用VideoPortSynchronizeExecution函数。

HwVidSynchronizeExecutionCallback函数，像hwVidInterrupt函数不能被分页也不能不用系统重启就调用某些VideoPortXxx函数。对VideoPortXxx函数总体来说，HwVidSynchronizeExecutionCallback函数可安全地调用它们，可参考在线DDK。

6.12视频微端口的定时器

任何微端口驱动程序都可以根据驱动程序开发者的意愿而有一个HwVidTimer函数。HwVidtimer函数允许微端口驱动程序中止操作或者通过VideoPortStallExecution来监控大致的时间间隔中的状态变化。HwVidTimer同时也防止其他系统的操作发生，如VideoPortStallExecution所做的那样。

例如，适配器的微端口驱动程序仿效VGA的功能可以有一个HwVidTimer函数来定期监视它的适配器的VGA寄存器的状态，这样驱动程序就可以仿效VGA格式的图形。

在调用VideoPortStartTimer之后，端口驱动程序又每秒调用一次HwVidTimer函数，直到微端口驱动程序调用VideoPortStopTimer之后。微端口驱动程序可以允许或禁止对HwVidTimer函数重复调用。

需注意的是，HwVidTimer函数不能用VideoPortStopTimer禁止对它自身的调用，另外一个微端口驱动程序函数必须控制通过VideoPortStartTimer和VideoPortStopTimer允许或禁止对HwVidTimer函数的调用。

6.13在视频微端口中对适配器重置

每一个微端口驱动程序必须有HwVidResetHw函数。

HwVidResetHw由HAL在机器将要崩溃或用户想软启动机器时调用。HwVidResetHw重置适配器到专用的字符模式，这样HAL就可以在关闭系统时显示关闭信息而在软启动机器时显示初始化信息。

HwVidResetHw不能调用BIOS，不能调用任何的可分页代码，也不能进行分页。如果可能，它只调用VideoPortReadXxx和VideoPortWriteXxx函数，但也可以调用下面函数的任何一个。

■VideoPortStallExecution

■VideoPortZeroDeviceMemory

■VideoPortZeroMemory

6.14微端口驱动中的镜像驱动程序支持

Windows 2000提供了视频微端口驱动程序对镜像驱动程序的支持，这样，微端口驱动程序不需要任何特殊的代码来支持。可以参考第二章镜像驱动程序部分的内容来获得关于在镜像系统中的显示驱动程序的详细信息。

微端口驱动程序中的镜像驱动程序的需求是最小的，唯一必须实现的函数是DriverEntry，它是由微端口驱动程序导出的，也可以由以下函数导出：

■HwVidFindAdapter

■HwVidInitialize

■HwVidStartIo

既然没有物理的显示设备与一个镜像的表面相关联，这三个函数可以空执行并且总是返回成功。

6.15视频微端口的TV连接器及拷贝保护支持

所有的视频微端口驱动程序应当用IOCTL_VIDEO_HANDLE_VIDEOPARAMETERS I/O控制代码处理VRPs，这个IOCTL被发送到微端口驱动程序查询硬件能力、TV连接器当前设置及拷贝保护或者设置拷贝保护的硬件功能。微端口驱动程序根据检查VIDEOPARAMETERS结构的dwCommand字段值决定要执行的动作，它被传送到VRP的InputBuffer中。如果一个微端口驱动程序不处理这一VRP，则系统根本不允许DVD回放。

如果设备没有TV连接器，微端口驱动程序将总是在VRP的StatusBlock结构的Status字段值返回NO_ERROR值，这样的微端口驱动程序不能以任何方式修改VIDEOPARAMETERS结构值。

接下来的两部分将提供微端口驱动程序设备的TV连接器实现的详细内容。

6.15.1查询TV连接器及硬件的拷贝保护

微端口驱动程序在VIDEOPARAMETERS结构的dwCommand字段值为VP_Commnad_GET时及设备具有TV连接器时做如下工作：

■验证Guid(必需的)

■如果硬件可以更改模式或标准，则在dwFlags中都设置VP_FLAGS_TV_MODE和VP_FLAGS_TV_STANDARD。微端口驱动程序也应当对其他所支持的能力设置合适的标志位。例如，如果设备支持闪烁过滤，则它应当设置VP_FALGS_TV_FLAGS

■在dwMode中设置合适的位则意指目前设备的显示器模式(可选的)。如果TV连接器正处于激活状态即向TV显示图像，则微端口驱动程序设置VP_MODE_TV_PLAYBACK，如果设备只是向CRT播放，则应当设置为VP_MODE_WIN_GRAPHIC，或者两个位同时都设置则表示设备同时向TV及CRT播放。

■用设备支持的回放模式填充dwAvailableModes，所有的驱动程序必须设置成VP_MODE_WIN_GRAPHICS。如果设备能够打开全部扫描而关闭闪烁过滤，则微端口驱动程序还应当设置VP_MODE_TV_PLAYBACK。

■如果设备正通过TV连接器在显示，则应以目前世界上电视显示的标准频率填充dwTVStandard(可选)。如果TV输出被禁止，微端口驱动程序将仅设置VP_TV_STANDARD_WIN_VGA位。

■用TV连接器支持的TV标准填充dwAvailableTVStandard。

■设置dwMaxUnscaledX和dwMaxUnscaledY，即TV显示器能支持的最大x值及y值，不是硬件尺度必需的。

如果，对TV连接器来说，设备还提供了硬件拷贝保护，则微端口驱动程序将修改下列函数的VIDEOPARAMETERS结构的字段值。

■在dwFlags中设置VP_FLAGS_COPYPROTECT(如同VP_FLAGS_TM_MODE和VP_FLAGS_TV_STANDARD)。

■设置dwCPType为拷贝保护状态。

■设置dwCPStandard为拷贝保护工作状态时的TV标准。

6.15.2设置TV连接器及拷贝保护硬件

对由微端口驱动程序设置的VIDEOPARAMETERS的dwFlags成员的VP_COMMAND_GET任一位来说，微端口驱动程序可以执行对VP_COMMAND_SET的一个设定。调用者负责调用微端口驱动程序并设置微端口驱动程序支持VP_COMMAND_GET的未设定功能。微端口驱动程序应当为VP_COMMAND_GET作出响应，而这是通过设置硬件的VIDEOPARAMETERS字段的dwFlags中的相应的字段值来实现的。例如：

■如果微端口驱动程序在VP_COMMAND_GET上设置了VP_FLAGS_TV_MODE位，当VP_FLAGS_TV_MODE在VP_COMMAND_SET上设置时，它应当改变VP_MODE_WIN_GRAPHICS状态到VP_MODE_TV_PLAYBACK状态。

■如果微端口驱动程序在VP_COMMAND_GET上设置VP_FLAGS_TV_STANDARD位，当VP_FLAGS_TV_STANDARD设置在VP_COMMAND_SET上时，它应当改变TV标准到dwTVStandard指示的标准。

■如果微端口驱动程序在VP_COMMAND_GET上设置VP_FLAGS_CONTRAST位，当VP_FLAGS_CONTRAST设置于VP_COMMAND_SET上时，它应当设置对比值为dwContrast中的值。

如果相应的位在dwFlags中没有设置，则VIDEOPARAMETERS字段包含未定义的数据

6.15.2.1设置拷贝保护硬件(Copy Protection Hardware)

那些在VIDEOPARAMETERS结构的dwFlags成员在VP_COMMAND_GET上返回VP_FALGS_PROTECTED的微端口驱动程序，应当按以下方式对VP_COMMAND_SET命令进行响应，并依赖VIDEOPARAMETERS结构的dwCPCommand成员：

■如果dwCPCommand是VP_CP_CMD_ACTIVATE，微端口驱动程序应当打开拷贝保护，产生并返回一个在dwCPKey中的唯一性保护键。

■如果dwCPCommand是VP_CP_CMD_DEACTIVATE并且在dwCPKey中的拷贝保护键是有效的，则微端口驱动程序应当关闭拷贝保护。

■如果dwCPCommand是VP_CP_CMD_CHANG并且在dwCPKey中的拷贝保护键也合法，微端口驱动程序将根据bCP_APSTriggerBits中的触发数据改变保护状态。

对没有拷贝保护硬件的设备的微端口驱动程序，只需简单地在VRP的StatusBlock结构的Status字段返回NO_ERROR。

6.15.2.2多会话拷贝保护

具有拷贝保护的设备的微端口驱动程序可选择性地支持多个同步拷贝保护会话，如果这样做的话，微端口驱动程序应当如下这样做：

■对每一个拷贝保护的激活，都返回一个在dwCPKey中的唯一的拷贝保护关键字。

■在所有的会话都临时性关闭(通过VP_CP_CMD_CHANGE)或解除(VP_CP_CMD_DEACTIVATE)之前都可以保持拷贝允许。例如，微端口驱动程序可以在每次拷贝保护激活及解除或关闭时，增加或减少引用的总数，当总数为零时，所有的拷贝保护才被禁止。

6.16VGA兼容的微端口SvgaHwIoPortXxx

在基于x86的机器中，VGA兼容的微端口驱动程序替换了系统提供的VGA微端口驱动程序，因而，VGA兼容的微端口驱动程序可以有一套SvgaHwIoPortXxx函数来支持全屏的MS-DOS应用程序，就如同系统提供的VGA微端口驱动程序所做的一样。

VGA兼容的新的SVGA微端口驱动程序的设计者应当使之适应一种根据适配器特性的由系统提供的SVGA微端口驱动程序的SvgaHwIoPortXxx函数。如同在基于x86的VGA兼容微端口部分中提到的，对其他基于x86类型适配器的微端口驱动程序的机器可以有一套SvgaHwioPortXxx例程并且可以根据设计者自身判断是否提供相同的支持或者在VGA微端口驱动程序加载时能不能被同时装载。

6.16.1基于x86机器的窗口式VDM

每一个MS-DOS应用程序都作为一个Windows VDM来运行，它同样在Win32保护子系统中作为一个控制台管理程序运行。

在Windows NT/Windows 2000平台上，一个称作V86仿真器的内核模式的部件捕获MS-DOS应用程序的指令，只要这一应用程序运行于一个窗口，任何对视频适配器的访问都被捕获并反射回系统提供VDD，VDD主要用于仿真适配器对应用程序的行为。换句话说，显示驱动程序在VDM运行于一个窗口时保留有对视频适配器的控制。

6.16.2基于x86机器的全屏VDM

由于性能的原因，当用户切换一个基于X86机器的MS-DOS应用程序到全屏模式时，显示驱动程序放弃对适配器控制。系统的VGA或VGA兼容的微端口驱动程序则从V86仿真器中分出所有的I/O指令，如应用程序相关的IN，REP INSB/INSW/INSD，OUT以及REPOUTSB/OUTSW/OUTSD指令到视频I/O端口。这些被分出的I/O操作转交给VAG兼容的微端口驱动程序的SvgaHwIoPortSxx函数。

然而，为了更高的性能，微端口驱动程序可以调用VideoPortSetTrappedEmulatorPorts来允许一些I/O端口可以直接被应用程序访问，微端口驱动程序继续用它的SvgaHwIoPortXxx分出到其他的I/O端口，以验证对这些端口的应用程序相关的指令流。

为了防止全屏应用程序发布一系列指令而使系统挂起，SvgaHwIoPortXxx函数监视到一个驱动程序确定的适配器寄存器应用程序的指令流。微端口驱动程序必须允许只对这些I/O端口的完全安全的直接存取。例如，对顺序生成器端口及杂项输出寄存器端口应当始终被V86仿真器分出并并捕获到微端口驱动程序提供的SvgaHwIoPortXxx函数进行验证。

应用程序是否直接存取I/O端口取决于VGA兼容的微端口驱动程序通过调用VideoPortSetTrappedEmulatorPorts设置的IOPM(I/O permission map：即x86上的I/O允许映射)。但要注意，微端口驱动程序可以通过调用这一函数，使其具有一定的存取区间来调节IOPM，它描述了I/O端口，释放给应用程序直接存取或者由SvgaHwIoPortXxx重新捕获。当前的IOPM决定了哪能一端口能被应用程序直接存取以及哪一个保留有V86仿真器的钩子或以SvgaHwIoProtXxx函数捕获进行验证。

默认情况下，所有的I/O端口建立起这一个微端口驱动程序仿真器的存取数组，并被捕获到相应的SvgaHwIoPortXxx函数。但是，VGA兼容的微端口驱动程序常常在收到一个IOCTL_VIDEO_ENABLE_VDM，即请求重新为VDM设置IOPM以允许直接存取一些I/O端口时调用VideoPortSetTrappedEmulatorPorts。大多数情况下，这一驱动程序允许直接存取除VGA顺序生成器寄存器、杂项输出寄存器及任何SVGA特定适配器寄存器等驱动程序开发者认为应当一直由SvgaHwIoPortXxx函数验证有效性之外所有的视频适配器寄存器。

关于VideoportSetTrappedEmulatorPorts，可参考在线DDK中的图形驱动程序参考部分。

6.16.3VGA兼容的微端口的HwFindAdapter

VGA兼容的微端口驱动程序的HwVidFindAdapter函数(或寄存器HwVid…Callback)必须在VIDEO_PORT_CONFIG_INFO中建立起如下缓冲区：

■NumEmulatorAccessEntries，意指在EmulatorAccessEntries数组中的条目数。

■EmulatorAccessEntries，指向一个包含给定数量的EMULATOR_ACCESS_ENTRY类型元素的静态数组，每一个都描述了一个从V86仿真器分出的I/O端口。默认的，其值都传递给SvgaHwIoPortXxx函数。每一个条目都包括一个起始的I/O地址、一个区间长度、被捕获的存取的大小(UCHAR,USHORT或ULONG)，微端口驱动程序是否支持通过I/O端口输入或输出字符串数据，微端口驱动程序提供的SvgaHwIoPortXxx函数以进行实际验证，可能的话还会传输数据。每一个SvgaHwIoPortXxx函数处理读(IN或REP INSB/INSW/INSD)和/或写(OUT或REP OUTSB/OUTSW/OUTSD)传输的UCHAR-，USHORT-，或ULONG-尺寸的数据。

■EmulatorAccessEntriesContext，一个存储的指针，如微端口驱动程序的设备扩展中的一个区域，在这个区域中，微端口驱动程序的SvgaHwIoPortXxx函数可以批次处理一个验证需要的应用程序相关的指令序列。

■VdmPhysicalVideoMemoryAddress和VdmPhysicalVideoMemoryLeagth描述了一个必须映射到VDM地址空间以支持MS-DOS全屏应用程序BIOS INT10调用的视频内存的区间。当一个应用程序改变视频模式到微端口驱动程序适配器可以支持的一种模式时，微端口驱动程序可以调用VideoPortInt 10函数。

■HardwareStateSize，它描述了对适配器IOCTL_VIDEO_SAVE_HARDWARESTATE响应的存储硬件状态信息所需的最小字节数。当用户切换到运行在一个窗口的MS-DOS全屏模式的应用程序时，微端口驱动程序必须在显器驱动程序重新获取对视频适配器的控制之前存储适配器的状态。需要注意的是VGA兼容的微端口驱动程序同样必须支持可逆的IOCTL_VIDEO_RESTORE_HARDWARE_STATE请求，因为用户也可能从窗口化的应用程序转回到全屏模式。

VGA兼容的微端口驱动程序仿真器存取专用的条目的子集，即适配器存取区间数组中指定的子集。仿真器存取条目可以是，而且经常是由HwVidFindAdapter函数建立起来的被映射的存取区间数组的所有I/O端口，存取的区间传送给VideoPortSetTrappedEmulatorPorts调用，它确定了当前的IOPM并决定由全屏MS-DOS应用程序可直接存取的I/O端口，指定了微端口驱动程序仿真器可存取条目的子集。

6.16.4在SvgaHwIoPortXxx验证指令

如同在VGA兼容的微端口HwFindAdapter中提到的，IOPM设置了可直接存取的I/O端口包括除顺序生成器寄存器和杂项输出寄存器之外的所有的SVGA寄存器，这些I/O端口继续VGA兼容的视频微端口驱动程序用SvgaHwIoPortXxx函数监视着。顺序生成器寄存器控制内部VGA兼容显示适配器上的芯片时钟。如果全屏MS-DOS应用程序在同步重新设置时触及其他适配器寄存器，则机器可以挂起。同样地，如果杂项输出寄存器设置选择一个不存在的时钟，机器同样将挂起。

VGA兼容的微端口驱动程序必须保证全屏的MS-DOS应用程序不发布引起机器挂起的指令。每一个这样的微端口驱动程序必须提供SvgaHwIoPortXxx函数来监视应用程序的发给I/O端口的为适配器顺序生成器寄存器及杂项输出寄存器发出的指令。每一个具有新特性的适配器的新VGA兼容的微端口驱动程序必须监视并继续验证任何可能由应用发送到任何I/O端口的能引起机器挂起的任何指令序列。

无论何时应用程序企图存取顺序生成器的时钟寄存器，SvgaHwIoportXxx函数必须改变IOPM的顺序以捕获所的有在同步重设置时进入的指令。一旦一个应用程序发送了一个影响顺序生成器或者企图向杂项输出寄存器写入的指令，则SvgaHwIoPortXxx函数将通过调用VideoPortSetTrappedEmulatorPorts来调节IOPM以禁止它直接存取所有的适配器寄存器。

微端口驱动程序提供的SvgaHwIoPortXxx函数应当在EmulatorAccessEntriesContext区域中缓冲接下来的IN(或INSB/INSW/INSD)和/或OUT(或OUTSB/OUTSW/OUTSD)指令，这一区域是直到同步重置完成之后或者应用程序重新存储了杂项输出寄存器或重置它为安全时钟后建立起的VIDEO_PORT_CONFIG_INFO(参考VGA兼容的微端口的HwFindAdapter驱动程序)。

微端口驱动程序这时负责检查缓冲的指令不能挂起机器。如果缓冲的指令不能挂起机器，微端口驱动程序将处理缓冲指令，通常情况都是通过用驱动程序提供的HwVidSynchronizeExecutionCallback调用VideoPortSynchronizeExecution函数，否则的话，微端口驱动程序将摒弃缓冲区指令。

6.16.5VGA兼容的HwVidStartIO

当用户从一个全屏的MS-DOS应用程序切换到一个窗口下运行时，VGA兼容的微端口驱动程序的HwVidStartIO函数发送了一个带有I/O控制代码IOCTL_VIDEO_SAVE_HARDWARE_STATE的VRP。微端口驱动程序必须存储适配器的状态以备用户再切换回到应用程序的全屏模式。

值得注意的是，微端口驱动程序的SvgaHwIoPortXxx函数必须具有一个缓冲的应用程序的IN和/或OUT序列，如同在SvgaHwIoPortXxx验证指令部分中所描述的那样。当它的HwVidStartIO例程调用以存储适配器的状态，在这种情况下，微端口驱动程序应当存储当前的状态，包括缓冲区中的指令，这样SvgaHwIoProtXxx函数可以重新准确地验证用户什么地方又将应用程序切换到全屏模式等一类的操作。

当微端口驱动程序完成一个存储操作，端口驱动程序自动禁止当前对VDM和微端口驱动程序的SvgaHwIoPortXxx函数的IOPM。如果应用程序又切换到全屏模式，则视频微端口驱动程序重新自动存储IOPM。它也在响应IOCTL_VIDEO_RESTORE_HARDWARE_STATE请求并调用微端口驱动程序的HwVidStartIO例程之后重新调用微端口驱动程序的SvgaHwIoPortXxx函数。

6.17将Windows NT 4.0的微端口驱动程序转换到Windows 2000

好的Windows NT 4.0微端口驱动程序可以很容易地成为Windows 2000的微端口驱动程序，下面是一些在Windows 2000微端口驱动程序中由PnP支持提供的需要修改的内容：

■实现在视频微端口驱动程序中支持PnP和电源管理部分中列出的新函数。但要确保初始化VIDEO_HW_INITALIZAITON_DATA的新成员要指向这些新函数。

■在DriveEntry函数中修改对VideoPortInitialize函数的调用。在Windows 2000中，第四个参数(Hwcontext)必须为NULL值。

■修改HwVidFindAdapter函数，对可枚举的总线上的设备来说，必须如下改变HwVidFindAdapter：

第一、删去设备侦测到的大多数代码(由于在Windows 2000上对HwVidFindAdapter的调用意味着PnP管理器已经探测到设备)。

第二、调用VideoPortGetAccessRanges来获得设备响应的总线相关的物理地址，这些地址是由PnP管理器分配的。

第三、如果驱动程序支持一个以上的设备类型，则确定设备的类型。

第四、忽略Again参数。(由于系统对每个设备只调用一次HwVidFindAdapter)。

对ISA等不可枚举的总线上的设备来说，PnP依旧企图启动设备，尽管HwVidFindAdapter负责决定设备是否存在。

■修改驱动程序INF文件的.Mfg部分以包含设备及生产厂的ID。这一工作是必需的，这样PnP管理器可以与它的INF文件协调设备。Windows NT 4.0的例子及Windows 2000的修改后的.Mfg部分如下：

[ABC.Mfg] ;Windows NT V4.0 INF

%ABC% ABC Graphics Accelerator A=abc

%ABC% ABC Graphics Accelerator B=abc

[ABC.Mfg] ;Windows 2000 INF

%ABC% ABC Graphics Accelerator A=abc,PCI\VEN_ABCD&DEV_0123

%ABC% ABC Graphics Accelerator B=abc,PCI\VEN_ABCD&DEV_4567

可以用与Windows 2000 DDK一起发布的geninf.exe工具来产生.INF。但要记住，geninf.exe不能为Windows NT 4.0产生INF，如果用geninf.exe生产的INF想支持Windows NT 4.0而不是Windows 2000，则必须修改由geninf.exe产生的INF文件。更多的内容可参考第一章创建图形的INF文件。

Windows 2000的视频端口作为Windows NT 4.0的继承来支持微端口驱动程序。当系统正在运行时，微端口驱动程序继承的图形适配器不能删除掉。同样，继承的微端口加入到一个正在运行的系统时也不能被自动地侦测到。


打印介绍

微软的Windows 2000打印体系结构由打印假脱机和一套打印驱动程序组成。通过调用设备独立的Win32打印和GDI函数，应用程序可以创建打印作业并将它们发送到任何类型的设备，包括激光打印机、矢量绘图仪、光栅打印机及传真机等。打印驱动程序包括一个允许用户控制打印机选项的接口组件。

应用程序对Win32 GDI函数的调用传送到GDI图形引擎，GDI图形引擎可以将绘制指令假脱到一个EMF文件中，或者与打印驱动程序连接，向假脱机绘制一个可打印的图像。假脱机组件解释EMF文件，它们可以在数据流中插入页面布局信息以及作业控制指令。假脱机然后将数据流发送给具有目标打印机I/O端口的串行、并行或网络端口的驱动程序。

假脱机和驱动程序组件设计为可替换的，这样硬件的厂商可以很容易地提供对新硬件的支持，关于打印假脱机机驱动程序组件的更多信息，可参考以下章节：

■第2章打印假脱机体系结构

■第3章打印驱动程序体系结构

支持一个新的打印机常常需要创建一些新的数据文件并与微软提供的打印驱动程序一起使用。关于微软的打印驱动程序的更多信息，参考以下章节：

■第4章微软统一打印驱动程序

■第5章微软PostScript打印驱动程序

■第6章微软绘图仪打印驱动程序

可以定制微软统一打印驱动程序及微软PostScript打印驱动程序的行为。更多的信息参考第7章定制微软打印驱动程序部分。也可以定制Windows 2000打印假脱机，可以参考第8章定制打印假脱机组件。

其后的其他章节包含以下一些内容：

■第9章Internet打印

■第10章安装配置打印驱动程序

■第11章CPSUI(一种创建基于属性表单用户接口的工具)

■第12章打印机的色彩管理


打印假脱机体系结构

微软Windows 2000打印假脱机由一套微软提供的和一些可选的厂商提供的组件组成，其职责包括：

■决定一个打印作业应当本地处理还是通过网络进行。

■与打印机驱动程序相连接，接收由GDI创建的数据流，并向特定类型的打印机上输出。

■如果假脱打印允许，则将数据假脱打印到文件。

■在逻辑打印机队列中选择第一个可用的物理打印机。

■将假脱机格式(如EMF)的数据流转换成可以发送到打印机硬件的格式(如PCL)。

■向打印机硬件发送数据流。

■为假脱机组件及打印机表格维护一个基于注册表的数据库。

2.1打印机组件

本部分提供下列主题

■2.1.1假脱机组件介绍

■2.1.2打印提供者

2.1.1假脱机组件介绍

微软Windows 2000打印假脱机最基本的组件如图2-1所示：

插入图2-1Windows 2000打印假脱机组件

应用程序

一个打印应用程序，主要通过调用GDI函数创建打印作业。

GDI

图形设备接口(包括用户模式和核心模式的组件)

Winspool.drv

这是一个到假脱机的客户端接口。它导出的函数构成了假脱机的Win32 API。并对存取服务器提供RPC栈。(GDI是最基本的客户端，但是应用程序也可以调用一些Win32函数)。

Spoolsv.exe

这是一个假脱机的API服务器。它作为Windows 2000的一个服务来实现的，并且随着操作系统的启动而启动。这一模块对服务器端的假脱机的API导出一个RPC接口。客户端的spoolsv.exe包括winspool.drv(本地)以及Win32spl.dll(远程)。该模块实现一些API函数，但是许多的函数调用通过发送的手段(spoolss.dll)通知到打印提供者。

Spoolss.dll

这是一个假脱机的发送器。它决定哪一个打印提供者去调用，它基于打印机的名称或每一个函数调用提供的句柄，并将函数调用传递到正确的提供者。

打印提供者

打印提供者支持专用的打印设备。

如果打印机硬件在应用程序运行的本地系统，则客户端及服务器端是同一系统(尽管这在上图中并不是很明显)。

所有的假脱机组件者在用户模式下执行。

2.1.2打印提供者

本部分提供以下主题：

■2.1.2.1打印提供者介绍

■2.1.2.2打印提供者示例

■2.1.2.3打印提供者能力

■2.1.2.4由打印提供者定义的功能

■2.1.2.5本地打印提供者

2.1.2.1打印提供者介绍

打印提供者负责将打印作业直接送到本地或远程的打印设备。它们同样负责打印队列管理的操作，如开始、停止以及枚举一个服务器端的打印队列等。打印提供者定义了一个高层的、机器独立的、操作系统独立的打印服务器的视图。

所有的打印机提供者提供实现一个普通的打印提供都能力集。这些能力由一套API函数定义的，并被假脱机发送器(spoolss.dll)调用。

许多由打印提供者定义的函数需要一个打印句柄作为输入，假脱机的客户端在winspool.drv中通过调用OpenPrinter获取一个打印机的句柄(在平台的SDK中描述)，该客户端调用API服务器(spoolsv.exe)。假脱机发送器(spoolss.dll)调用每一个打印提供者的OpenPrinter函数直至其中一个函数提供一个打印机的句柄并且返回一个值，即指明打印提供者识别出专用的打印机名称。发送器此时则返回它的句柄到API服务器，发送器的句柄包括打印机的句柄以及打印提供者的句柄。该句柄返回到应用程序，这样应用程序的后续的调用就可以直接送到正确的提供者和打印机。

微软在Windows 2000中提供以下打印提供者：

localspl.dll

本地打印提供者。从本地服务器进行管理，处理所有的到打印机的打印作业。

win32spl.dll

Windows网络打印提供者。处理所有的到远程的Win32(Windows NT/Windows 2000/Windows for Workgroup)服务器的打印作业，当作业到达远程服务器，它又传递给服务器的本地打印提供者。

nwprovau.dll

Novell Netware的打印提供者，直接处理打印作业到Novell Netware的打印服务器。

inetpp.dll

HTTP打印提供者。处理发送到一个URL的打印作业 (参考第9章，Internet打印)。

不同的厂商可以创建附加的网络打印提供者，更多的信息，可以参考第8章编写网络打印提供者部分的内容。

以下图示了包括这些打印提供者的可能的流程：

插入图2-2 打印提供者流程

当看上图时，应当考虑以下几点问题：

■如果打印机由客户系统管理，打印作业被本地打印提供者(localspl.dll)处理。由localspl.dll管理的打印机则不必在物理上在本地与客户相连，它们可以直接连接到网络卡上。

■如果打印机设置在Windows NT/Windows 2000服务器上，网络提供者(win32spl.dll)利用RPC重定向从客户端发送器发送到服务器spllsv.exe处理的调用。因为打印机对服务器是本地的，因此服务器本地打印提供者将处理打印作业。

■如果打印机设置在其他一些类型服务器上，它则可以被本地打印提供者或支持该服务器类型、使用服务器支持的数据格式及网络协议的网络打印提供者存取。

■对本地打印提供者存取远程打印机，它必须包含一个端口监视器，它使用远程打印机或服务器能识别的网络协议。

2.1.2.2打印提供者举例

在DDK中包含了一个打印提供者的实例。该例子不是一个完全功能的打印提供者。它是一个实现了打印提供者定义的函数基本轮廓的模板。实例代码放在DDK树形实例代码的pp子目录下。

2.1.2.3打印提供者能力

通过支持预定义的API函数集，Windows 2000打印提供者可以支持以下能力：

■打印队列管理：增加、删除、打开、关闭、枚举及设置打印队列的参数，也能提供打印队列状态变化的通知。

■打印机驱动程序管理：增加、删除、枚举及指定一个打印机驱动程序的目录。

■打印作业创建：开始和结束一个文档，开始和结束一个文档分页，将作业的数据流写入一个端口，并读取打印机的状态信息。

■打印作业安排：安排、枚举、并设置打印作业的参数。

■表格管理：增加、删除、枚举并设置打印表格的参数。

■打印处理器管理：增加、删除、枚举、指定目录及打印处理器支持的数据类型。

■打印监视管理：增加、删除以及枚举打印监视器。

■端口管理：增加、删除、配置、枚举并设置打印机端口的参数。

■注册表管理：创建、删除并枚举与打印提供者相关的注册表键及值。

■其他能力：显示消息框、关闭打印提供者、读一个内存映像的假脱文件、提供与端口监视器用户接口UI DLL与端口监视器服务器端DLL的通信通道。

这些能力都按照打印提供者定义的一套函数来实现的。

2.1.2.4打印提供者定义的函数

本部分主题列出了所有打印提供者可以提供的函数，这里面的许多函数在平台的SDK文档中都有介绍。如果函数在DDK中有介绍，则根据函数的名称提供一个到相关参考页的链接。

所有的打印提供者必须对所有列出的函数提供指针。但是，许多厂商提供的打印提供者是“部分的提供者(Partial Providers)”，其实质并没有提供支持打印提供者的很多的由函数定义的操作。因此，许多的函数指针可以是空值NULL。对更多的关于部分打印提供者的信息，参考第8章编写网络打印提供者。

在下面列出的函数表格中，必须支持的函数都标记有“必需的”字样。

所有的打印提供者必须导出初始化的函数InitializePrintProvidor。指向所有其他函数的指针都必须在PRINTPROVIDOR结构中提供。(注意这两个名称用错误码拼写，但是与在头文件winsplp.h中的名称是一致的)。

函数分成以下几组：

■初始化函数

■打印队列管理函数

■打印机驱动程序管理函数

■打印作业创建函数

■打印作业调度函数

■表格管理函数

■打印处理器管理函数

■打印监视器管理函数

■端口管理函数

■注册表管理函数

■其他函数

初始化函数

函数 描述

InitializePrintProvidor(必需的)

初始化打印提供者并对提供的函数返回一个指针

打印队列管理函数

函数 描述

AddPrinter | 向打印提供者管理的列表中增加一个打印队列并且为该打印队列协调相应的打印处理器
AddPrinterConnection | 创建一个到特定打印队列的连接
ClosePrinter(必需的) | 禁止调用者存取专用的打印队列
DeletePrinter | 从打印提供者管理的列表中删除一个打印队列
DeletePrinterconnection | 删除到某特定打印队列的连接
EnumPrinters(必需的) | 枚举由打印提供者管理的当前的打印队列
FindClosePrinterChangeNotifcication | 禁止打印机改变由FindFirstPrinterchangeNotification函数允许的通知信息。
FindFirstPrinterChange Notification | 对一个等待的调用者的对象返回一个句柄，调用者可用它来等待专用的打印机消息
GetPrinter(必需的) | 返回特定打印队列的当前参数值
OpenPrinter(必需的) | 允许调用者存取一个专用的打印队列
RefreshPrinterchangeNotification | 如客户端以PRINTER_NOTIFIY_OPTIONS_REFRESH标志调用FindNextPrinterChangeNotification则由发送器调用(可参考平台的SDK文档)
ResetPinter | 修改一个打印队列的数据类型或DEVMODE结构
SetPrinter(必需的) | 对一个专用的打印队列设置参数
WaitForPrinterChange | 已废弃

打印驱动程序管理函数

函数 描述

AddPrinterDriver | 给专用的服务器增加打印机驱动程序文件
AddPrinterDriverEx | 功能与AddPrinterDriver相同，参数不同
DeletePrinterDriver | 删除专用的服务器上的打印机驱动程序
EnumPrinterDrivers | 返回一个通过调用AddPrinterDriver或AddPrinterDriverEx增加到一个服务器上的打印机驱动程序的列表
GetPrinterDriver | 返回一个打印驱动程序的信息，其信息可以被通知到AddPrinterDriver函数(返回的信息主要是从.inf文件获得的)
GetPrinterDriverEx | 同GetPrinterDriver功能相同，参数不同
GetPrinterDriverDirectory | 返回服务器的打印驱动程序的目录名称

打印作业创建函数

函数 描述

AborPrinter(必需的) | 试图删除从专用的打印队列中删除当前作业
AddJob(必需的) | 返回一个作业的标识符及假脱机文件路径，调用者可以用CreateFile和WriteFile将数据发送到假脱文件
EndDocPrinter(必需的) | 执行打印作业结束的操作
EndPagePrinter | 执行分页结束的操作
ReadPrinter | 获得一个双向打印机的状态信息
ScheduleJob(必需的) | 通知打印提供者一个专用的作业可以安排调度，该作业由之前的AddJob函数返回的标识符来鉴别
StartDocPrinter(必需的) | 准备使打印提供者开始假脱一个打印作业
StartPagePrinter | 准备使打印提供者接收一个打印作业页面
WritePrinter(必需的) | 收到打印作业的部分数据流

注意：从AddJob……ScheduleJob的顺序是从StartDocPrinter……EndDocPrinter顺序的变化。

打印作业调度安排

函数 描述

EnumJobs(必需的) | 返回一个调度好的打印作业的列表
GetJob(必需的) | 返回作业参数
SetJob(必需的) | 撤消、暂停、恢复或重新开始一个打印作业或者设置作业参数

表格管理函数

函数 描述

AddForm | 将一个专用的表格增加到一个指定打印机的可用列表中
DeleteForm | 从一个指定的打印机可用的列表中删除一个特定表格
EnumForms | 从一个指定的打印机可用的列表中返回一个表格的列表
GetForm | 返回一个指定表格的特征
SetForm | 修改一个指定表格的特征

打印处理器管理函数

函数 描述

AddPrintProcessor | 在一个专用的服务器上安装打印处理器并将它增加到打印提供者可以调用到的列表中
DeletePrintProcessor | 从打印提供者可以调用的列表中删除一个特定处理器
EnumPrintProcessorDataTypes | 返回一个打印处理器支持并且可以打印提供者调用的数据类型的列表
EnumPrintProcessors | 返回打印提供者可以调用的打印处理器的列表
GetPrintProcessorDirectory | 返回打印处理器文件必须存储的目录路径
AddMonitor | 增加一个打印监视器到打印提供者可以调用的列表中

打印监视管理函数

函数 描述

DeleteMonitor | 从打印提供者可以调用的列表中删除一个打印监视器
EnumMonitors | 返回一个打印提供者可以调用的打印监视器列表

端口管理函数

函数 描述

AddPort | 给可用的列表中增加端口，通常是通过调用专用的AddPortUI这一端口监视函数
AddPortEx | 与AddPort相同，参数不同
ConfigurePort(必需的) | 配置一个打印机的端口，通常是通过调用ConfigPortUI这一端口监视函数
DeletePort(必需的) | 从可用的列表中删除打印机端口，通常通过调用DeletePortUI这一端口视示器函数
EnumPorts(必需的) | 返回一个可用的打印机端口列表
SetPort | 给一个专用的打印机端口设置参数

注册表管理函数

函数 描述

DeletePrinterData | 删除当前分配给一个特定值名称的值，即特定打印机的PrinterDriverData关键字下的值
DeletePrinterdataEx | 与DeletePrinterData相同，参数不同
DeleteprinterKey | 删除专用的键及子键，如果他们当前存储于特定打印机的注册表中PrinterDriverData关键字下
EnumPrinterData | 返回每一个值的名称以及当前分配的、存储于特定打印机的注册表中的值
EnumPrinterDataEx | 与EnumPrinterData相同，参数不同
EnumPrinterKey | 返回包含于当前注册表中的专用的键字名称下的子键的列表
GetPrinterData | 返回当前分配给特定值名称的值，它存储于特定打印机的注册表中PrinterDriverData关键字下

其他函数

函数 描述

GetPrinterDataEx | 与GetPrinterData相同，具有附加的参数
SetPrinterData | 存储特定打印机的PrinterDriverData关键字下的专用的值名称以及在注册表中的值
SetPrinterDataEx | 与SetPrinterData相同，具有附加的参数
XcvData | 提供在端口监视器UI DLL和端口监视器的服务器端DLL之间的通信通路

2.1.2.5本地打印提供者

Windows 2000的本地打印提供者为所有可以通过本地打印提供者的端口监视器存取的打印机提供作业控制以及打印机管理能力。(客户端的管理者在使用增加打印机向导Add Printer Wizard时通过选择“我的电脑”选项建立对这些打印机的存取)。这样的打印机包括那些连接到本地系统串口及并口的打印机，他们也能包括连接到其他I/O，如连接在远程非Windows NT/Windows 2000服务器的SCSI端口打印机。

本地打印提供者实现整套由打印提供者定义的函数，它也支持以下能力：

■打印作业假脱机，可以不假脱打印作业而直接将其打印到可存取的打印队列中

■支持具有到本地打印机接口DLL调用的Windows 2000打印驱动程序

■支持不同厂商提供的打印处理器(参考第8章的写打印处理器部分)

■支持不同厂商提供的打印监示器(参考第8章的写打印监视器部分)

下面的图示，提供了应用程序创建打印作业时(某种程度上是简化了的)，在本打印机提供者组件之间的流控视意图。

如图所示，一个应用程序通过调用GDI创建了一个打印作业，不管打印作业初始时的输出格式是否是EMF，本地打印提供者作业创建的API则创建了一个假脱文件，以后，当作业安排调度，假脱机文件被读并且如果该格式是EMF，则EMF打印处理器将此作业送回到GDI并在打印机图形的DLL(Pinter Graphics DLL)帮助下转换成为RAW格式，转换后的数据流可以通过本地打印提供者被送回到打印机(不需要再进行假脱)。

插入图2-3 本地打印提供者流控

厂商可以创建与本地打印提供者相连的部分打印提供者以支持自定制的网络配置。


打印机驱动程序体系结构

打印作业是由应用程序通过微软的Win32 GDI函数调用来创建的，这些函数调用可以被假脱为EMF数据类型记录以备EMF打印处理器以后回放，或者它们可以对每个文档页面立即绘制一个可打印的图像。是否立即执行或在EMF记录的回放时期进行图像的绘制，是由GDI绘制引擎来控制的，在绘制的操作中，GDI绘制引擎将调用合适的Windows 2000打印驱动程序来支持。

Windows 2000打印机驱动程序具有如下责任：

■通过提供GDI不能支持的特定打印机的绘制能力来帮助GDI绘制打印作业。

■发送绘制的图像数据流到打印假脱机。

■提供一个与打印机及打印文档相关的可修改配置参数的用户接口，如选择的输入输出的托架、打印的份数、图像的精度及方向等等。

Windows 2000打印驱动程序由一套打印驱动程序组件组成，这些组件将驱动程序的绘制以及用户接口操作分到独立的DLL中。

本章的目的是解释怎样为Windows 2000编写驱动程序，但非常重要的一点是，必须记住下面三个与操作系统一起发布的打印机驱动程序：

■微软统一打印机驱动程序

■微软Postscript打印机驱动程序

■微软绘图仪打印机驱动程序

这三个驱动程序支持当今市场上许多打印设备。只需要在打印机设备与微软提供的合适的打印驱动程序不兼容时才写打印机驱动程序，许多新的打印机可以简单到只需增加一个打印机的数据文件到这些驱动程序中就可以获得支持。那些包含由私有的命令序列控制的硬件绘制加速器的设备可能才需要重新写驱动程序。

3.1打印机驱动程序组件

所有的Windows 2000打印机驱动程序由以下组件构成：

■一个打印机图形DLL，它帮助GDI绘制打印作业并发送绘制的数据流到打印假脱机。

■一个打印机接口的DLL，它为驱动程序参数配置提供一个用户接口和一个假脱机可以调用以来通知驱动程序系统打印相关事件的接口。

另外，微软提供的打印机驱动程序利用打印机的数据文件。

3.1.1打印机图形DLL

本部分提供以下主题：

■3.1.1.1打印机图形DLL介绍

■3.1.1.2由打印机图形DLL定义的函数

■3.1.1.3打印机图形DLL实例

■3.1.1.4绘制一个打印作业

■3.1.1.5支持设备字体

■3.1.1.6返回专用的打印机信息

■3.1.1.7选择用户模式或内核模式

■3.1.1.8构建一个打印机图形DLL

3.1.1.1打印机图形DLL介绍

打印机图形DLL实现了在第一部分的第3章支持DDI中描述的Drv为前辍的DDI函数。这些DLL具有以下列两项责任：

■帮助GDI绘制打印作业：一个打印机图形DLL可以支持DDI绘制函数来处理那些必须以特定设备的方式执行以及那些不能由GDI的绘制引擎完成的绘制操作。

■发送绘制数据流到假脱机：打印机图形DLL通常以RAW数据类型(包括命令序列)产生一个输出流，假脱机可以通过打印监视器将它发送到打印机硬件。

一个打印机图形DLL必需提供的、特定打印机类型的绘制帮助的总数量，依赖于硬件的绘制能力并包括以下情形：

■GDI绘制引擎用一个GDI管理的表面执行所有的绘制，图形的DLL不提供任何的DDI绘制函数。

■图形DLL提供一些DDI绘制函数用一个GDI管理的表面与GDI绘制引擎一起工作。由图形DLL提供绘制DDI函数可以选择性地回调GDI绘制引擎的GDI支持的服务。

■图形DLL通过提供DDI绘制函数和一个设备管理的表面来执行所有的绘制。

例如，当微软统一打印机驱动程序使用一个设备管理的表面时，微软统一打印机驱动程序(Unidrv：Microsoft Universal Printer Driver)使用一个GDI管理的表面并提供一些DDI绘制函数。

更多的关于在图形驱动程序中提供绘制帮助的信息，参考第一部分的第2章表面类型和第一部分的第3章支持DDI部分的内容。

打印机图形DLL在EMF假脱一个打印作业时不能调用，当EMF打印处理器回放EMF记录时适当的图形DLL调用。

但要注意，打印机图形DLL不能发送绘制的打印作业到硬件，被绘制的数据流返回到假脱机，假脱机发送该数据流到一个端口监视器，该端口监视器是一个用户模式的接口到一个内核模式的本地并行端口或网络端口的微端口驱动程序

对Windows 2000来说，可以设计一个打印机的图形DLL以在内核模式执行，或者可以设计它在用户模式执行，更多的信息可以参考选择用户模式或内核模式部分的内容。

下面的两个图表示当一个应用程序用GDI创建一个打印作业时发生的数据流，第一个图表是一个用户模式的打印机图形DLL。

插入图3-1 打印作业数据流，使用一个用户模式的打印机图形DLL

第二个图表是一个内核模式的打印机图形DLL。

插入图3-2 打印作业数据流，使用一个内核模式的打印机图形DLL

注意，在图表中，如果从GDI输出的格式是EMF，打印机图形DLL在EMF打印处理器回放EMF作业记录前不接收作业，也要注意的是EMF打印处理器改变输出格式到非EMF格式。

上面两个图表示了一个完整的本地环境，如果打印机连接到一个服务器，EMF记录则通常客户的GDI绘制引擎(GRE)拷贝来产生，然后假脱到一个本地文件后送到服务器，服务器的假脱机拷贝读该文件并将这些记录送到服务器的EMF打印处理器，并且服务器端的GRE拷贝调用服务器打印机图形DLL。

3.1.1.2由打印机图形DLL定义的函数

如果所有的图形驱动程序，打印机图形DLL负责定义以下的DDI函数，由初始化驱动程序DrvEnableDriver函数的入口点开始，其余的函数以字母顺序列出。

函数名称 描述

DrvEnableDriver | 允许驱动程序初始化它自身并返回支持DDI函数的指针
DrvCompletePDEV | 对一个设备实例提供具有GDI句柄的驱动程序
DrvDisableDriver | (可选)允许驱动程序在卸载之前执行清除工作
DrvDisablePDEV | 允许驱动程序删除设备特定实例的信息
DrvDisableSurface | 允许驱动程序删除绘制表面
DrvEnablePDEV | 允许驱动程序提供具有物理设备特征的GDI并初始化设备特定实例的信息
DrvEnableSurface | 允许驱动程序创建绘制表面
DrvQueryDeviceSupport | (可选)返回被请求的特定设备的信息
DrvQuery | (可选)返回被请求的特定驱动程序的信息

打印机图形DLL也对定义以下特定打印的DDI函数负责，它们在绘制打印作业过程中的某一特定点时被调用。

函数 何时调用

DrvEndDoc | 当GDI已经结束发送一个文档到驱动程序以备绘制时
DrvNextBand | (可选)当GDI已经结束绘制一个物理页面的一个区带(band)，这样驱动程序可以将该区发送到打印机
DrvQueryPerBandInfo | (可选)在GDI开始为一个物理页面绘制区带之前，这样驱动程序可以提供具有特定区带信息的GDI。
DrvSendPage | 当GDI已经结束绘制物理页面，这样驱动程序可以将此页面发送到打印机。
DrvStartBanding | (可选)当GDI准备绘制时开始发送一个物理页面的区带信息到驱动程序
DrvStartDoc | 当GDI准备绘制时开始发送一个文档到驱动程序
DrvStartPage | 当GDI准备绘制时开始发送一个文档页到驱动程序

通常，一个打印机图形DLL也定义任何必需的附加DDI函数以完成打印作业绘制。被定义函数的数量及类型依赖于：

■是否驱动程序支持绘制表面时使用被管理的GDI或被管理的设备(或者两者都支持)，更多的信息，可以参考第一部分的第2章表面类型。

■一个绘制操作可以被GDI函数处理而不是由驱动程序自身执行的程度。更多的信息，参考第一部分第3章支持DDI部分的内容。

所有的由打印机图形DLL定义的函数都是被GDI内核模式图形绘制引擎(GRE)来调用的。

DrvEnableDriver和DrvQueryDriverInfo函数由图形DLL导出，所有其他支持DDI函数的地址放置于一个由DrvEnableDriver函数返回的表中。

3.1.1.3打印机图形DLL实例

plotter.dll的源代码是打印机图形DLL为微软绘图仪驱动程序提供的，与些DDK一起提供，其代码放置于包含DDK实例目录的子目录中。

3.1.1.4绘制一个打印作业

打印作业或者是在它们创建时绘制，或者是被假脱成为EMF记录绘制。在后一种情况下，绘制发生在当EMF打印处理器(Winprint.dll)回放EMF记录时。绘制则由一系列对GDI的Win32绘制函数的调用构成，并以CreateDC为调用的开始。

当一个应用程序调用GDI的CreateDC函数来创建打印机的设备设备环境，GDI检查看是否有合适的打印机图形DLL被装载，如果没有，GDI装载DLL并调用它的DrvEnableDriver函数，该函数不会被再次调用，除非驱动程序被重新装载。

接下来，GDI调用打印机图形DLL的DrvEnablePDEV函数，这样驱动程序可以创建一个设备实例并返回设备特征。GDI用返回的信息以创建设备实例的内部描述。图形DLL必须用这一句柄作为一个Eng为前缀，由GDI绘制引擎(参考第一部分第2章GDI支持服务)提供的回调的输入。

调用图形DLL的DrvEnableSurface函数之后，驱动程序可以通过调用EngCreateBitmap为设备实例创建一个绘制表面，或者，如果绘制表面是设备管理的，则调用EngCreateDeviceSurface。如果EngCreateBitmap不能提供一个足够大的位图来包含整个的物理页面，并且如果驱动程序支持页面分区带的功能，EngMarkBandingSurface可以被调用以通知进行GDI分区带的工作。最后，EngAssociateSurface必须被调用以允许GDI将创建的表面与特定设备实例协调，并允许GDI知道哪一个驱动程序提供的DDI绘制函数(如果任意)应当在绘制这一特殊表面时调用。

从这一点来看，一个绘制表面已经创建并且绘制可以开始，对每一个绘制的文档来说，GDI在打印机图形DLL中调用以下的函数：

l DrvStartDoc

For each physical page{

l DrvStartPage

DrStartBanding

For each banding surface{

DrvQueryPerbandInfo

Rendering operations

DrvNextBand

}

DrvSendPage

}

l DrvendDoc

除DrvqueryPerBandInfo函数之外，这些函数都是为了允许打印机图形DLL向打印机硬件发送控制序列(通过调用EngWritePrinter)，并执行初始化或完成处理一个文档、页面或区带等任何内部操作。

打印机图形DLL负责在适当的时间发送绘制图像(即，绘制表面的内容)到打印机(通过调用EngWritePrinter),如下：

■对GDI管理或设备管理的位图表面：绘制表面是GDI提供的或驱动程序提供的位图，打印机图形DLL可以分出一些起绘制函数(参考第一部分第2章的表面协商部分)，如果使用了页面区带，DrvNextBand函数应当发送绘制表面内容。如果不使用区带，DrvSendPage函数应当发送绘制表面内容。

■对设备管理的向量表面：绘制表面是在设备里，打印机图形DLL分出的所有绘制函数(参考第一部分第2章的表面协商部分)，并且这些函数在绘制操作过程中发送图像数据到打印机，而页面分带区不能被使用。

如果预料任意由打印机DLL提供的DDI函数可以潜在执行5秒以上，就应当包括至少每5秒一次调用EngCheckAbort至少每5秒一次的代码并看是否打印作业被中断。

在GDI调用DrvendDoc以指明一个文档已经绘制完成后，它调用DrvDisableSurface。如果DrvEnableSurface调用EngCreateBitmap，则DrvdisableSurface必须调用EngDeletesurface。

GDI在一个应用程序调用DrvDeleteDC时调用一个打印机图形DLL的DrvDisablePDEV函数。

如果一个应用程序在打印一个文档时调用GDI的ResetDC函数，GDI创建一个新的设备设备环境并为该新设备设备环境调用打印机图形DLL的DrvEnablePDEV函数。然后，GDI调用DrvResetPDEV函数，这样图形DLL可以用从旧的设备环境获得的信息修改新的设备环境。接着，DrvDisableSurface及DrvDisablePDEV为旧的设备环境而调用，对新的设备环境，接下来被调用的函数就是DrvEnableSurface。最后，GDI调用DrvStartDoc在一个新页上进行绘制。

GDI调用DrvDisableDriver优先于卸载打印机图形DLL。

如果打印机硬件支持GDI绘制函数不支持的绘制操作，则打印机图形DLL可以提供一个DrvDrawEscape函数。

如果需要支持通过GDI函数不能实现的绘制或非绘制的操作，则一个打印机图形DLL需要提供一个DrvEscape函数。例如，微软的Postscript打印机驱动程序用escape来支持Postscript入口点，或者，一个应用程序也许需要获得一个传真机的电话号码，DrvEscape函数也用于指出由DrvDrawEscape函数支持的操作。

3.1.1.5支持设备字体

如果一个打印机提供设备字体，打印机图形DLL必须定义一个DrvTextOut函数以产生文本输出命令，图形DLL必须也定义以下函数：

DrvQueryAdvanceWidths

DrvQueryFont

DrvQueryFontData

DrvQueryFontTree

更多的关于支持设备字体的信息，参考第一部分第3章中的支持DDI字体及文本函数部分。

3.1.1.6返回专用的打印机信息

GDI有时请求打印机图形DLL返回在打印作业之间的打印机专用的信息，通过调用这些DrvQuery前缀的DDI函数如DrvqueryAdvanceWidths(如果被图形DLL定义)。

什么时候会发生这些的一个例子是在字处理应用程序中维持了一个所见即所得(WYSIWYG)屏幕显示的可打印页面。为了准确地显示文本中的线的间断，字处理器必须对字符宽度及其他选定的打印机实现的字体的尺度进行基准线的合适性计算。

3.1.1.7选择用户模式或内核模式

在4.0版以前的Windows NT中，GDI及所有的图形驱动程序都是在用户模式下执行，在4.0版中，这些组件被移到内核模式，这一改变提高了显示设备的性能，但对打印设备的性能获得并不显著，不足以抵消继承用户模式失去的优点。

因此，对Windows 2000，可以设计一个打印机图形DLL以在内核模式下执行，或者可以设计在用户模式下执行。用户模式下的打印机图形DLL执行提供以下超过内核模式下执行的优点：

■无限制的栈空间

■存取Win32 API

■更少的导致系统崩溃的潜在因素

■用户模式的纠错器，更容易纠错

■更好的浮点计算能力，尽管DDI浮点函数不是必需的

■调用任何定制的，厂商提供的用户模式的但不是Windows 2000打印机驱动程序体系结构中描述的部分的DLL的能力。

在用户模式下使用图形DDI

一个用户模式打印机图形DLL并没有限定于去调用GDI支持的服务(第一部分第2章)和其他以Eng为前缀的图形DDI回调函数。但是，有一些必须遵从的规则：

■如同内核模式图形DLL，用户模式图形DLL必须调用图形DDI的版本函数以创建或修改绘制表面，这些回调函数是GDI支持的服务，并且不允许调用这些绘制函数在Win32中的等效的函数。

对用户模式的DLL，调用这些绘制的回调函数被用户模式的GDI客户端截听，之后，客户端会通知GDI的内核模式图形绘制引擎(GRE)。

■下面的以Eng为前缀的DDI函数不能被用户模式的DLL调用。

EngCreatePath

EngCreateClip

EngDeleteClip

EngGetTypeFontList

EngMapModule

EngDebugBreak

所有其他以Eng为前缀的函数对内核模式的打印机图形DLL都是可用的，在用户模式下也是也用的。对那些与Win32完全等效的函数，如EngGetPrinter或EngGetPrinterDriver，则Win32等效的函数被调用。没有完全与Win32等效的函数，则在GDI客户端来实现。

■用户模式的打印机图形DLL可以继续用DDI函数以实现对浮点的支持。

将一个存在的打印机图形DLL转换到用户模式

如果以前开发过运行于内核模式的打印机图形DLL，对Windows 2000来说，会有以下选择：

1．什么也不做。已经存在的内核模式的打印机图形DLL将会继续在内核模式下工作，微软也许会在以后的操作系统发行版本中删除对内核模式打印机图形DLL的支持。

2．转换该DLL到用户模式下执行。为了执行，只需增加一个DrvQueryDriverInfo函数到DLL，然后遵从构建打印机图形DLL的规则。

在创建一个新的在用户模式下的打印机图形DLL

当开发一个新的执行在用户模式下的打印机图形DLL时，可以继续使用所有的被内核模式DLL使用的图形DDI函数。但是，也有下边这些选择：

■对以Eng为前缀有完全等效Win32的函数，可以调用Win32函数，这些函数由以下一些函数构成：

EngAllocMem EngGetForm EngMulDiv

EngAllocUserMem EngGetLastError EngSetLastError

EngEnumForms EngGetPrinter EngSetPrinterData

EngFreeMem EngGetPrinterData EngUnloadImage

EngFreeUserMem EngGetPrinterDriver EngWritePrinter

EngFindImageProAddress EngLoadImage

■对以Eng为前缀的那些与Win 32函数符合一些简单功能的函数，可以调用Win32函数，这些函数由以下一些函数组成：

EngAcquirSemaphore EngGetCurrentCodePage

EngMultiByteToWideChar EngCreateSemaphore

EngGetDriverName EngQueryLocalTime

EngDeleteSemaphore EngGetPrinterDataFileName

EngReleaseSemaphore EngFindResource

EngLoadModule EngUnicodeToMultiByteN

EngFreeModule EngMultiByteToUnicodeN

EngWideCharToMultiByte

■对那些创建或修改一个绘制服务的函数，新的驱动程序必须继续调用GDI支持服务而不是它们等效的Win32函数。

■不使用DDI函数来支持浮点，可以使用FLOAT数据类型。

3.1.1.8构建一个打印机图形DLL

当构建一个打印机图形DLL时，必须清楚打算在用户模式下执行和打算在内核模式下执行的DLL之间的以下差别：

构建打印机图形DLL的规则

用户模式图形DLL 内核模式图形DLL

在source文件中，设置TARGETTYPE=DYNLINK | 在source文件中，设置TARGETTYPE=GDI_DRIVER
处理器宏USERMODE_DRVER必须在winddi.h包含之前在source文件中被定义 | 处理器宏USERMODE_DRIVER不能被定义
对象模块必须被与umpdddi.lib和gdi32.lib导入库链接在一起 | 对象模块必须与win32k.lib导入库链接
DrvQueryDriverInfo函数必须返回DRVQUERY_USERMODE | DrvQueryDriverInfo函数必须给DRVQUERY_USERMODE返回FALSE(或二者选择其一，该函数可以被省略)

3.1.2打印机接口DLL

本部分提供以下主题：

■3.1.2.1打印机接口DLL介绍

■3.1.2.2由打印机接口DLL定义的函数

■3.1.2.3打印机接口DLL实例

■3.1.2.4为打印机创建原型表单页面

3.1.2.1打印机接口DLL介绍

打印机通常给用户提供大量的可修改配置的选择，这些选择项可以在每一个被付印的文档中被改变。如页面、托架、字体选择、图像精度、尺寸、色彩等等都必须可以通过用户接口被存取，也可以被应用程序调用。

一个打印机驱动程序的打印机接口DLL，它是执行在用户模式下，负责导出一个用户的接口到打印机的配置选项。提供这一接口包括为打印机创建原型表单页面。应用程序(如打印折叠器)通过调用由打印假脱机导出的Win32函数显示接口，并且，假脱机返过来，又调用由打印机接口DLL定义的函数。

为配置选项提供一个用户接口不是打印机接口DLL的唯一责任，DLL同时也导出假脱机可以调用的函数以通知驱动程序打印相关的系统事件，如驱动程序安装、升级或者添加打印机及打印机连接等。

3.1.2.2由打印机接口DLL定义的函数

打印机接口DLL导出函数列于下面表格中：

函数 目的

DllEntryPoint | 初始化DLL的入口点，通常调用DLLMain(在平台的SDK文档中有描述)
DrvConvertDevMode | 转化专用的DEVMODE结构到另外一个版本
DrvDeviceCapabilites | 返回请求的关于一个打印机能力的信息
DrvDevicePropertySheets | 调用CPSUI以产生描述打印机属性的表单页面属性
DrvDocumentEvent | (可选)允许打印机接口DLL去处理与打印文档相关的一定的事件
DrvDriverEvent | (可选)允许打印机接口DLL对假脱机来的已经发生的关于特定打印机事件的消息通知作响应
DrvDocumentPropertySheets | 调用CPSUI以产生描述打印文档属性的表单页面属性
DrvPrinterEvent | 允许打印机接口DLL对假脱机来的已发生的关于特定打印机的事件的消息通知作响应
DrvQueryColorProfile | (可选)允许打印机接口DLL指定一个ICC的profile以在色彩管理中使用
DrvQueryJobAttributes | (可选)允许打印机接口DLL指定诸如打印多个文档页面在一个物理页上(“N-up”打印)、打页某一页面的多个拷贝以及校对页面等
DrvQueryPrintEx | 决定一个打印作业是否能用打印机当前配置进行打印
DrvUpgradePrinter | (可选)当一个新版本的驱动程序增加到系统时，修改打印机的注册表设置

3.1.2.3打印机接口DLL实例

plotui.dll的源代码，即微软绘图仪驱动程序的打印机接口DLL与此DDK一起提供，其代码放置于包含DDK示例代码的目录树的子目录中。

3.1.2.4为打印机创建原型表单页面

打印机接口DLL，与CPSUI连在一起，负责创建Windows 2000用户从事的与打印机及打印文档相关的视图及修改配置参数表单页面的属性。每一个打印机接口的DLL必须提供一个DrvDevicePropertySheets函数以创建特定打印机页面和一个DrvDocumentPropertySheets函数以创建特定文档的页表。

为了理解这些函数是怎样被设计，阅读描述CPSUI、显示表单页面属性包括应用程序之间的交互、打印假脱机、打印机接口DLL以及CPSUI执行流等许多在打印机驱动程序中使用CPSUI一章中描述的内容是非常重要的。

3.1.3打印机数据文件

微软提供的打印机驱动程序利用数据文件来描述各个打印设备，更多的信息，可以参考以下几章内容：

■微软统一打印机驱动程序：描述了.gpd文件

■微软Postscript打印机驱动程序：描述了.ppd文件

■微软绘图仪打印机驱动程序：描述了.pcd文件


微软统一打印机驱动程序

本章叙述了微软统一打印机驱动程序(Unidrv)，解释了怎样增加小驱动程序来允许Unidrv来支持新的打印机，主要包括了下面的主题：

■4.1统一打印机驱动程序介绍

■4.2GPD文件介绍

■4.3打印机属性

■4.4打印机命令

■4.5打印机特性

■4.6打印机选项

■4.7打印机字体描述

■4.8条件语句

■4.9压缩光栅数据

■4.10过滤光栅数据

■4.11处理色彩格式

■4.12用Unidrv进行中间色调整

■4.13控制图像质量

■4.14处理可安装的特性及选项

■4.15指定特性和选项显示的顺序

■4.16描述打印机内存配置

■4.17指定纸张大小

■4.18引用场所

■4.19安装一个Unidrv小驱动程序

4.1统一打印机驱动程序介绍

统一打印机驱动程序(Unidrv)是微软公司对非Postscritp打印机的标准打印机驱动程序，对Unidrv的介绍包括下列主题：

■4.1.1Unidrv能力

■4.1.2Unidrv组件

■4.1.3Unidrv小驱动程序

■4.1.4Unidrv用户接口

■4.1.5Unidrv绘制器

■4.1.6GPD文件实例

■4.1.7微软小驱动程序开发工具

4.1.1Unidrv能力

Unidrv提供以下能力：

■用描述每一个打印机的特征的特定打印机Unidrv小驱动程序，支持所有的非Postscript打印机。

■一个Unidrv的用户接口，基于Treeview控件和属性表单，它对所有的打印机者是一致的，而且对每一个打印机的单一选项也是可以修改的。

■一个单一的Unidrv绘制器，即与GDI图形引擎一起，转换微软Win32 GDI调用从应用程序到打印机的可以发送给假脱机的命令。

为提供对以前的打印机驱动程序用GPC文件编写的小驱动程序的支持，RasDD，即微软小驱动程序开发工具(Microsoft Minidriver Development Tool)提供从RasDD GPC文件到Unidrv GPD文件的自动转换。

4.1.2Unidrv组件

Unidrv组件由DLL加上文本及二进制数据文件构成，如下图所示：

插入Unidrv组件图？？？

在图中的组件包括：

应用程序

一个用户应用程序，如字处理程序，它提供给用户打印的能力。

gdi32.dll

用户模式的DLL，导出Win32 GDI函数

内核模式图形引擎

实现GDI函数功能的NT的可执行代码

小驱动程序文本文件

基于文本的Unidrv小驱动程序，它利用GPD文件的条目描述打印机

二进制数据文件

Unidrv在解析了包含于小驱动程序文本文件中的信息后创建的临时文件(具有.bud扩展名)

Unidrvui.dll

Unidrv用户接口的DLL，对所有Unidrv支持的打印机提供一个通用的UI代码。

用户接口插件

可选的、特定打印机的用户接口插件

compstui.dll

CPSUI给打印机的用户接口

Unidrv.dll

Unidrv绘制器，它绘制图像及发送图像数据流到打印假脱机

绘制插件

可选的、特定打印机的绘制插件

4.1.3Unidrv小驱动程序

Unidrv小驱动程序是包含打印机说明的文本文件。每一个小驱动程序描述了一种生产厂家的打印机类型。这一基于文本的说明即称为GPD(Generic Printer Desciption)，并且每一个文件都称为GPD文件，每一个小驱动程序由一个或多个GPD文件构成。

利用GPD文件来描述一个打印机，Unidrv支持以下能力：

■在许多打印机上发现的一般的、标准的打印机特性。

■打印机提供的唯一的、定制的打印机特性。

■可安装的打印机选项，只要在选项被安装的情况下可以选择。

■选项约束，允许指定不兼容的选项。

■条件语句，允许指定一些依赖于其他特征的打印机的特征。

■打印机命令的规范说明，它可以包括当前一大堆选择的标准变量的值，也可以对这些变量执行算术操作。

■一个定制的帮助文件，它不同于Unidrv提供的标准的帮助文件，而是用于描述定制的特性。

关于创建GPD文件的信息，参考GPD文件介绍部分。

一个Unidrv小驱动程序可以由一个或多个GPD文件构成，更多的信息，可以参考在一个小驱动程序中使用多个GPD文件部分的内容。

当一个打印机被安装，Unidrv的GPD解析阅读所有的GPD文件，GPD文件中包含的信息被用于创建打印机的临时的二进制文件。所有的Unidrv用户接口和Unidrv绘制器的参考都在这个二进制文件中。

通常，小驱动程序必须提供资源，如字体、位图以及本地化文本字符串等。这些资源被置于一个资源DLL中。更多的信息，参考在小驱动程序中使用资源DLL部分的内容。

4.1.4Unidrv用户接口

Unidrv用户接口用CPSUI以创建以下的属性表单页面：

■设备设置(Device Settings)页面：用于打印机属性表单，它在一个用户选择了来自打印文件夹或打印机窗口的“属性”菜单项的时候被显示。该页面列出了特定打印机的配置信息。

■文档属性表单的布局(Layout)、纸张/质量(Paper/Quality)以及高级(Advanced)属性页面：它在一个用户选择了打印机折叠器或打印机窗口的文档默认值(Document Defaults)菜单项时被显示，或者当一个应用程序调用了PrinterProperties或者DocumentProperties函数被显示(在SDK平台文档中有描述)。该页面列出了特定文档的配置信息。

这些属性表单页包含有由一个打印机的Unidrv小驱动程序指定的打印机的特性及打印机的选项，它们也允许用户修改选项值。

Unidrv用户接口作为一个用户模式的打印机接口DLL来实现，代码都含在DLL中，并与CPSUI连接来指定了属性表单的内容。DLL基于在小驱动程序中的信息，实施哪一个打印机选项可以被组合的限制。它也保证用户不选择没有在打印机上安装的选项。

4.1.5Unidrv绘制器

Unidrv绘制器作为一个打印机图形DLL来实现，并导出由微软设备驱动程序接口(Microsoft Device Driver Interface DDI)为图形驱动程序定义的函数。当一个应用程序调用图形设备接口(Graphics Device Interface GDI)函数以发送图像到一个打印机设备，内核模式的图形引擎调用绘制器的DDI函数，这些DDI函数帮助GDI绘制一个打印作业的页图像。

绘制器也负责与打印机的命令序列一起发送已绘制的图像数据到打印假脱机，打印假脱机然后再直接将图像和命令送到打印机硬件。打印机绘制器发送的命令是在Unidrv小驱动程序被指定的。

可以通过提供一个绘制的插件来修改Unidrv的绘制操作，该绘制插件在第7章定制微软打印机驱动程序部分有详细描述。

4.1.6GPD文件实例

DDK包括大量的小驱动程序实例，对一些类型的打印机来说，这些实例包括GPD以及字体和资源文件。实例文件被放置于包含DDK实例的树形目录的mini子目录中。

4.1.7微软小驱动程序开发工具

微软小驱动程序开发工具(Microsoft MDT)，即包含在该DDK中的minidev.exe，是设计用于Unidrv的小驱动程序开发的软件开发工具。

微软的MDT提供下列特征：

■一般打印机描述(GPD：Generic Printer Description)编辑器

一个用于创建构成Unidrv小驱动程序的GPD文件的、可以进行语法检查的文本编辑器。

■Unidrv字体规格(UMF:Unidrv Font Metrics)编辑器

基于属性表单的产生Unidrv字体规格文件的编辑器

■符号翻译表(GTT:Glyph Translation Table)编辑器

基于属性表单的产生符号翻译表文件的编辑器

■小驱动程序工作间

对Unidrv小驱动程序的基于Windows的编辑工作间，提供对小驱动程序的GPD文件、资源文件及GTT文件的存取。

■小驱动程序转换向导

自动转换二进制小驱动程序为基于文本的Windows 2000小驱动程序，用于以前的Windows及Windows NT版本。当使用该转换向导，它将根据转换下表列出的文件而产生一个Windows 2000的小驱动程序。

输入文件 文件内容 输出文件

Windows GPC文件 | 打印机能力描述 | Windows 2000GPD文件
Windows PFM文件 | 字体描述 | Windows 2000UFM文件
Windows CTT文件 | 符号翻译描述 | Windows 2000GTT文件

另外，转换向导将用正确的文件名及资源标识符产生资源文件。

■INF向导

对被转换的小驱动程序自动产生一个INF文件(参考安装一个Unidrv小驱动程序部分)

关于怎样使用微软的MDT的教学指导，可以运行该开发工具并阅读其帮助文件。

4.2GPD文件介绍

GPD文件用于创建Unidrv小驱动程序，一个Unidrv小驱动程序由一个基于文本的一般打印机描述(GPD)构成，它可以包含一个或多个GPD文件。

GPD文件用GPD语言描述一个打印机，GPD文件包含用GPD语言提供下列类型信息的GPD文件条目：

■描述打印机特征的打印机属性

■控制打印机操作的打印机命令

■可以被Unidrv控制的描述打印机能力的特性

■表示可以分配于打印机特性的打印机选项

■打印机字体描述，说明了和硬件驻留与Cartridge字体相联系的特性。

■描述打印机特性及打印机配置的相互依赖关系的条件语句

GPD语言同样定义了GPD文件的条目，这些条目用于控制以下操作：

■压缩光栅数据

■处理色彩格式

■用Unidrv过渡调色

■处理可安装的特性及选项

■描述打印机内存配置

这一介绍性部分也包括了对在小驱动程序中使用多个GPD文件和在小驱动程序中使用资源DLL的基本单位(Master Unit)的讨论。

4.2.1GPD文件条目

GPD文件由条目构成，并使用描述每一个光栅打印机的特定设备的特征的GPD语言，这一部分包括以下的主题，这们描述了GPD文件的条目：

■GPD文件条目格式

■GPD值类型

■行连续(Line Continuation)

4.2.1.1GPD文件条目格式

所有的GPD文件和条目符合下面的格式：

*条目名：条目值{GPD文件条目(GPD_FileEntry)，GPD文件条目，……}

条目名总是一个预定义的GPD解析器可识别出的关键字，其前置星号。

条目值必须是GPD值类型的一种。

每一个GPD_FileEntry是另外一个GPD文件的条目，符合上面显示的格式。每一个条目的子条目必须对指定的包含该它自身的条目是有效。

一些条目名称不接受包含花括号或封闭的子条目。

每一个GPD条目由行尾(end-of-line)或右括号(})终止。

一个简单的GPD文件条目，它不接受子条目，是一个下面这样属性的条目：

*MaxCopies:99

这一条目指定了打印机可以处理的最大的复制数量是99。

下面，是一个更复杂的例子，描述了一个可以两种方向(portrait或landscape，即横向或竖向)打印的打印机。例子中了指定了驱动程序必须发送以选择每个方向的命令。

*Feature:Orientation

{

*Name:”Orientation”

*DefaultOption:Portrait

*Option:Portrait

{

*Name:”Portrait”

*Command:CmdSelect

{

*Order:DOC_SETUP.7

*Cmd:”<1B>&100”

}

}

*Option:Landscape_CC90

{

*Name:”Landscape”

*Command:CmdSelect

{

*Order:DOC_SETUP.7

*Cmd:”<1B>&110”

}

}

}

4.2.1.2GPD值类型

所有的GPD文件条目包含一个值的规范说明，GPD语言定义了下列值类型：

■Symbolic names(符号名称)

■Text strings(文本字符串)

■Constants(常量)

■Numeric values(数字值)

■Boolean values(布尔值)

■Lists(列表)

■Pairs(对值)

■Rectangles(矩形)

Symbolic names

符号名称是一个用作标识符的字母串，它具有如下特征：

■第一位字符必须是大写或小写的字母

■在第一位字符之后的名称部分可以由大写或小写的字母、数字或下划线组成。

■名称是大小写敏感的。

■名称中的字母数不受限制。

GPD语言预定义了大量的符号名称，预定义的名称为打印机属性、命令名称、标准特性、标准选项以及标准变量等而存在。

可以为定制的特性、定制的选项及宏等定义符号名称。

Text Strings

文本字符串是文字字母的字符串，以引号作为分隔符，应用于Unidrv小驱动程序的字符串可以被置于以下两种位置的任一个：

■可以被放置在资源文件中。需要本地化的字符串，如用户接口文本，就应放置当在资源文件中，在小驱动程序中使用资源DLL部分中已经描述过。

■可以包含于一个GPD文件中。字符串代表了构成打印机命令的escape序列，这些通常都包含于GPD文件中，但这些字符串不需要被本地化。

字符串必须服从以下规则：

■字符串必须以引号分隔开(“……”)。

■十六进制字节值可以被置于一个尖括号中，括起来的十六进制数就是一个字符串，如,<03><1B>。在一套尖括号中，每两个数字被解释为一个十六进制的字节值，因此，<03><1B>,<03 1B>和<031B>都是等同的。

■百分号(%)被用作escape字符，为了在字符串中包括引号或左尖括号(“<”)，先于它们之前使用一个百分号。为了指定一个以百分号结束的字符串，必须指定百分号的十六进制值，如“<25>”。另外，为在一个文本字符中包括一个百分号并代表一个打印机命令，必须用另外一个百分号来处理它，为指定一个以百分号结束的打印机命令，必须指定两个十六进制的%值，如“命令字符串<25><25>”

一个字符串的实例是这样一个命令，它为佳能BJC-600打印机选择信纸大小的纸张，这一命令的字节序列是1B 28 67 03 00 6E 01 72,可以被指定如下：

“<1B>(g<03 00>n<01>r”

每一个包含于字符串中的ASCII字符都被转换成为一个字节的十六进制等值。

在GPD文件中包含的字符串必须服从下列附加规则：

■为扩展超过一个单行上的字符串，在处理第一行之后每行之前加上一个连续字母(+)，并且在每一行上以引号分隔文本。

■一个字符串值可以由多个文本字符串构成，例如，下列的两上GPD条目是等同的：

*Name: “abc”“def”* % Comment

+ “gh”“ijk”

*Name: “abcdefghijk”

对属于被定义于资源文件中的附加的字符串规则，可以参考平台的SDK文档中的STRINGTABLE语句声明的描述。

更多的关于指定打印机命令escape序列的信息，参考命令字符串格式部分的内容。

Constants

常量是由GPD语言预定义的名称值，常量名称通常由大写字母构成。如GPD常量可以是PAGE、SERIAL、TTY,它们是可以赋给*PrinterType属性的值。

Numeric Values

数字型值，所有的指定的条目值或GPD文件中的参数值的Numeric Values数字值都必须是整数。十进制小数是不允许的，除非在文本字符串中。

数字值被假定为正数，除非在其前有一个减号(“-”)。

数字值被假定为十进制数，除非在其前有0x的情况时，它们是非负的十六进制值。

如果可适用于一个特殊的GPD文件的设备环境，则星号(“*”)可以被用于指明或者是无穷大值，或者是忽略值，

Boolean Values

布尔值被定义为等于预先定义的常量TRUE或FALSE。根据常规，所有打印机属性的字符名称都接受以问号标识结束的布尔值，如下例所示：

*RotaterasterData?:FALSE

Lists

列表值。用于分配属性的一系列值，利用LIST关键字，其格式为：

LIST(值1，值2，值3，……，值n)

这里，值1，值2，值3，……，值n代表了一套一个或多个值，所有的类型都是为属性指定的。例如，打印机的色彩平面数据的顺序可以按如下指定的顺序来发送：

*ColorPlaneOrder:LIST(YELLOW，MAGENTA，CYAN，BLACK)

Pairs

分配一对值给属性，可利用PAIR关键字，其格式如下：

PAIR(值1,值2)

当值1和值2是数字型值时，例如，光标的起始点位置可以被指定为基本单位，如：*CursorOrigin:PAIR(120,180)

Rectangles

矩形值描述在GPD文件中的一个矩形，利用RECT关键字，其格式为：

RECT(left,top,right,bottom)，其中left,top,right,bottom都代表基本单位格式的数字值。

4.2.1.3行连续

GPD文件条目如果太长而不能在一行容纳下，则可以放在接下面的行里。为了连续一个条目，在第一行之后的每一行都必须以加号(“+”)打头，加号必须是每一行的第一个字母，不用预置空格，如下边例子所示：

#DeviceFonts:

+ LIST(

+ =RC_FONT_courier_10pt_regular

+ =RC_FONT_CGTimes_regular

+ =RC_FONT_Univers_regular

+ =RC_FONT_Univers_condensed_regular

+ =RC_FONT_Antique_Olive_regular

+ =RC_FONT_Albertus_Medium

+ =RC_FONT_Albertus_Extra_Bold

+ =RC_FONT_Courier_regular

+ =RC_FONT_Letter_Gothic_regular

+ =RC_FONT_Letter_Gthic_regular

+ =RC_FONT_Wingdings

在下面行的开始，不需要使用行连接字母：

■以星号开始的行。

■以左括号开始的行。

4.2.1.4注解及忽略块

GPD文件可以包含注释，注释的格式如下：

*%注解字符串

注解字符串可以是任何以行结束符终止的字符串。在一个多行注解的每一行注解里都必须以*%字开始，*%必须放置于空格或分行符之前。

下面是一个有效的注解的例子：

*% This section of the GPD file

*% Contains macro definitions

*Macros:HP4L

{

*% These macros define command prefixes for the paper size feature

LetterCmdPrefix:”<1B>&12a8c1E<1B>*p0x0Y” *% Prefix for letter option.

A4CmdPrefix:” <1B>&126a8c1E<1B>*p0x0Y” *% Prefix for A4 option

Env10CmdPrefix:” <1B>&181a8c1E<1B>*p0x0Y” *% Prefix for Env10 option.

}

为请求GPD解析器忽略一组GPD条目，可以创建一个包含被忽略条目的被忽略块，忽略块的格式如下：

*IgnoreBlock{IgnoredEntries}

被忽略的条目是一套GPD文件的条目，包含有相同数目左括号和右括号。

在下边的例子中，GPD解析器将忽略GPD条目描述Landscape_CC90的选项。

*Feature:Orientation

{

*Name:”Orientation”

*DefaultOption:Portrait

*Option:Portrait

{

*Name:”Portrait”

*Command:CmdSelect

{

*Order:DOC_SETUP.7

*Cmd:”<1B>&100”

}

}

*IgnoreBlock

{

*Option:Landscape_CC90

{

*Name:”Landscape”

*Command:CmdSelect

{

*Order:DOC_SETUP.7

*Cmd:”<1B>&110”

}

}

}

}

4.2.1.5宏

GPD语言支持两种类型的宏：

■块宏(Block Macros)，它允许指定一套GPD文件条目，并可以插入到GPD文件的任何条目位置。

■值宏(Value Macros)，它允许指定一套值，它可以被分别插入到GPD文件的任何位置。

块宏

块宏用于划定一套GPD的将重复插入到GPD文件中的一套GPD文件条目。可以包括任何条目类型到块宏定义中，如特性、选项语句、属性说明及对值宏的引用或者其他块宏。

下面的规则将应用于块宏中：

■在一个GPD文件中的块宏定义必须位置于任何对它的引用之前。

■在根层次(即不在括号里)的块宏定义可以通过GPD文件的定义来获得，并在GPD定义之后。否则，块宏的范围将是设置于左括号与右括号之间的定义部分。

■块宏的定义可以包含另外块宏或值宏的定义。

■块宏的定义可以引用其他前边定义的块宏及值宏，但是它不能引用它自身。

■块宏不能接受参数。

■如果括号包含于一个宏体中，它们必须成对的(即，必须有相同数量的左、右括号)。

■如果用同一个名称创建两个块宏，则直到解析器遇到第二个定义之前第一个定义一直是有效的，这时第二个将替代第一个。如果第二个定义的范围结束，则第一个块宏又处于有效的任命状态。

块宏格式

在GPD文件中，用如下格式定义块宏：

*BlockMacr：BlockMacroName{BlockMacroBody}

BlockMacroName是一个唯一的名称，而BlockMacroBody则是一套一个或多个GPD文件的条目。如果BlockMacroBody包含括号，则相同数量的左括号及右括应当被包含其中。

例如，可以定义一个名为EnvelopeDefaults的块宏，并定义如下：

*BlockMacro:EnvelopeDefaults

{

*PrintableArea:PAIR(4646,6738)

*PrintableOrigin:PAIR(150,150)

*RatateSize:TRUE

}

引用块宏

引用块宏，可以用以下的格式：

*InsertBlock:=BlockMacroName

BlockMacroName是一个唯一名称，之前指定在*BlockMacro条目中定义该宏。

例如，为了在一个选项说明中引用EnvelopeDefaults宏，可以使用以下的条目：

*Option:Env9

{

*InsertBlock:=EnvelopeDefaults

}

值宏

值宏用于指定可重复地分别插入到GPD文件中的一套一个或多个值。值可以是任何GPD值类型。

以下的规则适应于所有的值宏：

■一个定义于GPD文件中的值宏必须放置在任何对它引用之前。

■在根层次(即，不在括号内)的值宏定义可以通过GPD文件定义并获得，并且要在定义之后。否则，值宏的范围将是包含在左右括号之间的定义部分。

■值宏必须解析一种GPD值类型。

■如果所有的值都是文本字符串，值宏的定义就可以引用其他前边定义的值宏，但是一个值宏不能引用它自身。

■值宏不接受参数。

■如果创建了具有相同名称的两上值宏，则在GPD解析器遇见第二个定义之前，第一个值宏一直是有效的，并且第二个替换了第一个值宏。如果第二个值宏定义的区间结束，则第一个又重回到任命状态。

值宏的格式

为了在GPD文件中定义一个或多个值宏，可以使用以下格式：
*Macros:ValueMacrogroupName{ValueMacroBody}

这里ValueMacroGroupName是一个唯一名称，ValueMacroBody是一套与值相关的单一的值名称，如下：

ValueMacroName:MacroValue

这里，ValueMacroName是一个唯一的宏名称，MacroValue代表了一个GPD值类型。(只要被解析的字符串代表了一种GPD值类型，MacroValue就可以包括对以前定义的值宏的引用。)

如下例，可以为一套常用的命令前缀定义值宏：

*Macros:HP4L

{

LetterCmdPrefix:”<1B>&12a8c1E<1B>*p0x0y”

A4CmdPrefix:” <1B>&126a8c1E<1B>*p0x0y”

Env10CmdPrefix:” <1B>&181a8c1E<1B>*p0x0y”

}

但要注意，ValueMacroGroupName(例中为HP4L)是可选的，并被当作注解来看待。

引用值宏

引用值宏，使用下边这种格式：

=ValueMacroName

这里ValueMacroName是一个唯一名称，之前在定义宏的*Macros条目中被说明。

例如，为了在一个命令规范说明中引用一个HP4L宏，可以使用以下条目：

*Command:CmdSelect

{

Cmd:=LetterCmdPrefix” <1B>*c0t5760x7680y”

}

唯一一个可以通过联接宏引用与非宏值来分配值的时间，是当所有的宏定义及其他值表示文本或命令子串时，如在例中所示的。在其他情况下，宏引用必须表示整个被分配的值。

4.2.1.6预处理器定向

GPD文件可以包含预处理器定向，它可以被用于控制对GPD文件中不同部分的条件解析，下表描述了可用于GPD文件中的预处理器定向。

预处理器定向 定义

*Define:symbolName | 定义一个符号
*Undefine:symbolName | 取消以前定义的一个符号
Ifdef:symbolName | 意指一个GPD文件条目块的开始，如果专用的符号被定义，则在该定向与下一定向*Ifdef,*Elseifdef,*Else,或*Endif定向之间的GPD文件条目，将会由GPD处理器进行处理
Elseifdef:SymbolName | 如果指定的符号被定义，并且该符号指定的由之前的*Ifdef或*Elseifdef定向没有被定义，在这一定向及下一*Ifdef,*Elseifdef,*Else,或*Endif定向间的GPD文件条目将会由GPD解析器来处理
*Else | 如果由以前的*Ifdef或者*Elseifdef定向指定的符号没有被定义，在这一定向与下边的*Ifdef或*Endif定向间的GPD文件条目将会由GPD解析器来处理。
*Endif | 意指GPD文件条目块的结束
*Include:”FileName” | 指定一个附加GPD文件的名称，参考在小驱动程中使用多个GPD文件部分的内容
*SetPPPrefix:PrefixString | 改变为预处理器定向准备的前置字符串，参考改变预处理器前缀部分的内容

条件预处理器定向可以被嵌套，在每一个嵌套层，使用条件预处理器定向的顺序如下：

*Ifdef:symobl1

GPD file setcion

*Elseifdef:Symbol2

GPD file seciton

*elseifdef:Symbol3

GPD file section

……

*Else:

GPD file section

*Endif:

对每一个应用的*Ifdef定向，*Endif是必需的，*Elseifdef及*Else定向是可选的。每一个GPD的文件部分(file seciton)可以包含GPD文件条目以及可选择的，一个条件预处理器定向的嵌套序列。

用*Define定义的所有的符号明确地保留其定义，直到用*Undefine取消其定义之后。

*Include定义允许指定一个附加的GPD文件的名称，更多的信息，可以参考在小驱动程序中使用多个GPD文件部分的内容。

注意*IgnorBlock的GPD条目不影响预处理器定向，因为预处理器在GPD解析之前执行。

改变预处理器定向前缀

*SetPPPrefix定向允许改变用于预处理器定向的前缀。也就是，可以用这一定向来将星号所代替的预处理器定向替换为另外一个字母或字符串。

例如，如果GPD文件包含下列定向：

*SetPPPrefix:#SpecialPrefix#

当预处理器停止查找以*号开始的预处理器定向并开始寻找以#SpecialPrefix#开始的定向时。接着的序列将临时改变预处理器前缀为#SpecialPrefix#，并将它再恢复为*。

*SetPPPrefix:#SpecialPrefix#

#SpecialPrefix#Ifdef:WINNT_50

#SpecialPrefix#Include:”ExtraGPD.gpd”

#SpecialPrefix#Endif:

#SpecialPrefix#SetPPPrefix:*

这一特性最初的目的是允许为以后版本的操作系统写的GPD文件可以与Windows 2000兼容。例如，支持未来操作系统版本的GPD文件可以包括GPD与以星号打头的预处理器定向冲突的但是windows 2000支持的文件条目。通过改变前缀，一个为未来操作系统版本写的GPD文件可以在Windows 2000中使用，如下面的实例：

#Ifdef:WINNT_70

*SetPPPrefix:#SpecialPrefix#

*%Do special,OS-Specific Proessing of

*%GPD file entries that might confilict with

*%asterisk-prefixed preprocessor directives

#Endif

但是要注意，这一技术只改变预处理器查找的前缀，由解析器识别的关键字必须总是前置星号*。

预定义的预处理器符号

微软定义了以下预处理器符号

符号 何处被定义 定义

WINNT_50 | Windows 2000的GPD预处理器 | 环境是Windows 2000
WINNT_40 | Windows NT 4.0及2000的预处理器 | 环境是Windows NT 4.0
PARSER_VER_1.0 | Windows NT 4.0及2000的预处理器 | GPD解析器(Version 1)

WINNT_40和WINNT_50符号对创建与Windows NT 4.0及Windows 2000都兼容的GPD文件时是非常有用的。如果，例如Windows 2000支持一个打印机的能力但是Windows NT 4.0不支持，这样一能力就可以指定于一个GPD文件中并由*Ifdef:WINNT_50和*Endif定向限定。

4.2.1.7标准变量

GPD语言定义了一套标准变量，它们可以在命令字符串中被引用，利用命令字符串格式，Unidrv驱动程序对这些变量赋值。从GPD文件的观点来说，这些变量是只读的。

所有的标准变量以一个DWORD整型类型存储。

下面的打印机命令条目指定了当一个光栅数据块准备好时发送给一个HP LaserJet 4P打印机的命令字符串：

Command:CmdSendBlockData:”<1B>*b”%d{NumOfDataBytes}”W”

下表包含所有的标准变量，以字母顺序排列。

标准变量

名称 值 注解

BlueValue | 当前色彩的蓝色组件 | 在CmdDefinePaleterEntry命令字符串中使用时是有效的(也可参考GreenValue，RedValue)
CurrentFontID | 当前下载的软字体的标识代码 | 如果当前打印作业包含下载的软字体就是有效的
CurrentPaletterIndex | 目前色彩模板的索引 | 在CmdSelectPaletteEntry命令字符串中使用是有效的(也可参考GreenValue，RedValue
CusorOriginX | 基本单位中坐标源点的X坐标 | 无论何时打印机作业正在处理时都有效
CusorOriginY | 基本单位中从标源点的Y坐标 | 无论何时打印机作业正在处理时都有效
DestX | 基本单位中坐标目标点的X坐标 | 在CmdXMoveAbsolute字符串命令中使用时是有效的
DestXRel | 档准单位中相对于当前坐标的目标点的X坐标 | 在CmdXMoveRelLeft及CmdXMoveRelRight命令字符串中使用时是有效的
DestY | 基本单位中坐标目标点的Y坐标 | 在CmdYMoveAbsolute命令字符串中使用时是有效的
DestYRel | 档准单位中相对于当前坐标的目标点的Y坐标 | 在CmdYMoveRelUp和CmdYMoveRelDown命令字符串中使用时都时有效的
FontBold | 如果当前字体是bold则设为1,否则都设为0 | 一种字体被指定后是有效的
FontHeight | 在基本单位中，当前字体的高度 | 一种字体被指定后是有效的
FontItalic | 如果当前字体是italic则设为1,否则都设为0 | 一种字体被指定后是有效的
FontStrikeThru | 如果当前字体允许中划线则设为1，否则都设0 | 一种字体被指定后是有效的
FontUnderLine | 如果当前字体有下划线则设为1，否则都设为0 | 一种字体被指定后是有效的
FontWidth | 在基本单位中，当前字体的宽度 | 一种字体被指定后是有效的
GraphicsXRes | 当前图形的水平精度(DPI) | 无论何时打印机作业正在处理时都有效
GraphicsYRes | 当前图形的垂直精度(DPI) | 无论何时打印机作业正在处理时都有效
GrayPercentage | 进行灰色填充时的级别(百分比) | 使用CmdRectGrayFill命令字符串时是有效
GreenValue | 当前色彩的绿色组件 | 在使用CmdDefinePaletterEntry命令字符串时是有效的(也可参考BlueValue和RedValue)
LinefeedSpacing | 垂直空间的总量，基本单位中代表一个回行 | 在使用CmdSetLineSpacing命令字符串时是有效的
NextFontID | 下一个被下载的软字体的标识码 | 使用CmdSetFontID命令字符串时是有效的
NextGlyph | 要下载的下一符号的两字节代码 | 在使用CmdSetCharCode命令字符串时是有效的
NumOfCopies | 用户请求的打印份数 | 无论何时打印机作业正在处理时都有效
NumOfDataByes | 准备传送的光栅数据的字节数 | 在使用CmdSendXXXData命令字符串时是有效的，如果数据被压缩，该值是被压缩后的值
PaletterIndexToProgram | 程序的下一个入口点的调色板索引 | 在CmdDefinePaletterEntry命令字符串中使用时是有效的(也可参考RedValue,GreeValue,BlueValue, CurrentPaletterIndex)
PatternBrushID | 一个下载的调色板画刷的标识码 | 与CmdDownloadPattern和CmdSelectPattern命令字符串一起使用时是有效的
PatternBrushsize | 当前调色板画刷的字节大小 | 与CmdDownloadPattern命令字符串一起使用时是有效的
PatternBrushType | 当前调色板画刷的类型，其值可为： 2：Shading pattern 3:Cross-hatch pattern 4:User-defined pattern | 与CmdDownloadPattern与CmdSelectPattern命令字符串一起使用时是有效的
PhysPaperLenth | y轴基本单位中，当前打印用纸的竖向模式的长度 | 无论何时打印机作业正在处理时都有效
PhysPaperWidth | 基本单位中,当前打印用纸竖向模式的宽度 | 无论何时打印机作业正在处理时都有效
RasterDataHeightInPixels | 当前数据代表的图像的像素高度 | 在任何CmdSendXXXData命令字符串及在CmdSetSrcBmpHeight命令字符串中时都是有效的，压缩不修改该值
RasterDataWidthInBytes | 一个扫描行中包含的字节数量 | 在任何CmdSendXXXData命令字符串及在CmdSetSrcBmpWidth命令字符串中都是有效的，压缩不修改该值
RectXSize | 矩形宽度，以x轴基本单位 | 在CmdSetRectWidth命令字符串中使用时是有效的
RectYSize | 矩形高度，以y轴基本单位 | 在CmdSetRectHeight命令字符串中使用时是有效的
RedValue | 当前色彩的红色组件 | 在CmdDefinePaletterEntry命令字符串中使用时是有效的(也可参考GreenValue,BlueValue)
TextXRes | 文本的当前水平精度(DPI) | 无论何时打印机作业正在处理时都有效
TextYRes | 文本的当前垂直精度(DPI) | 无论何时打印机作业正在处理时都有效

4.2.2基本单位

许多打印机支持具有可变化的水平及垂直精度的命令，例如，一个特殊打印机的Immediate Line Feed命令提供一个1/288th每英寸的精度，而同样的打印机也可以支持垂直图像的1/96th每英寸精度。类似的，这一打印机可以提供水平的1/80th,1/160th和1/320th 每英寸的精度。

Unidrv提供一个单一的坐标系统来处理这些可变的精度，这一坐标系统中的单位就称为基本单位(master units)。一个打印机的单位可以表示为(x,y)这样一对值，这时x是一个水平平面的基本单位，而y是垂直平面的基本单位。

为了确定一个平面的基本单位，必须计算实际精度的分母的最小公倍数，利用示例的打印机，可以按如下这样去做：

■计算80,160,320的最小公倍数，结果是320。这样，水平的基本单位就是1/320th每英寸。

■计算288和96的最小公倍数，结果是576。这样，垂直的基本单位就是1/576th每英寸。

为了指定一个打印机的基本单位，需要使用*MasterUnits属性。该属性格式如下：

*MasterUnits:PAIR(X_Denominator,Y_Denominator)

这里，X_Denominator是水平精度分母的最小公倍数，Y-Denominator是垂直精度分母的最小公倍数，下面的GPD条目指定了例子中的基本单位。

*MasterUnits:PAIR(320,576)

一般地，在GPD文件条目中使用的位置及大小值必须以基本单位指定，例如，为指定最大的可定制页面的尺寸为9*12英寸，下边的条目应当被用到，这里9*320=2880,12*576=6912:

*MaxSize:PAIR(2880,6912)

在计算基本单位时，只使用Unidrv支持的设备的精度。例如，如果打印机支持水平精度1/80th,1/96th,1/160th,1/320th每英寸，但是不打算在GPD文件中指定1/96th每英寸的精度，则在最小公倍数LCM计算中，不要包含该精度即可。

如果打印机在移动光标位置时支持光标命令(cursor commands)，则该值可以在*XmoveUnit及*YmoveUnit两个光标属性中指定并被包含于基本单位计算中。假设，如一个GPD文件包含下面的条目：

*XMoveUnit:60

*YmoveUnit:60

在计算这一打印机的基本单位时，1/60th每英寸则必须被包含于水平及垂直的基本单位计算中。

4.2.3在小驱动程序中使用多个GPD文件

Unidrv小驱动程序可以由多于一个的GPD文件组成，这就允许置换对一些打印机的一处或多处对GPD文件来说通用的特征，并将这些通用的GPD文件包含于打印机的一个特殊GPD文件中。

为包含附加的GPD文件，就必须用*Include定向，它在预处理定向中描述。可以使用多个*Include定向，如下面的例子所示：

*Include:”Common1.gpd”

*Include:”common2.gpd”

*Include:”common3.gpd”

*Include定向的文件名参数不能是一个宏引用，也不能包含一个路径说明。

每一个被包含的文件都必须以GPD文件条目结束，并且该文件必须包含相等数目的左括号和右括号，如果一个GPD文件条目重复，则最近被解析的条目将代替前一个条目，而没有重复的条目将被增加到Unidrv的数据库中。

4.2.4在小驱动程序中使用资源DLL

通常，打印驱动程序需要这样一些外部存储的资源，如字体、图标及其他位图、可本地化的用户接口文本字符串等。这些项目的描述被放置于资源的DLL中，在平台的SDK文档有描述。

在Unidrv小驱动程序中使用资源DLL，必须以下面的任一种方式对资源进行鉴别：

■如果使用多于一个的资源DLL，可以用RESDLL特性来鉴别它们，使用RESDLL特性使用的实例如下：

*Feature:RESDLL

{

*Option:FirstRes

{*Name:”MyFirstRes.dll”}

*Option:SecondRes

{*Name:”MySecondRes.dll”}

*Option:ThirdRes

{*Name:”MyThirdRes.dll”}

}

如果要引用包含于这些资源DLL中的一种资源，使用下面的格式：

RESDLL.ResourceOptionName.ResourceID

下面是一个例子，它的OptionName是从上边的特性条目例子中来的：

*rcNameID:RESDLL.SecondRes.288

这一例子，引用了包含于MySecondRes.dll上的一个资源标识符为288的一个字符串资源。

■如果只使用一个资源DLL，可以通过给*ResourceDLL特性赋值来使用它。

为了引用包含于这一DLL中的资源，可以简单地指定适当的资源标识符，如下例子所示：

*rcNameID:288

所有的与小驱动程序使用的资源DLL必须在一个打印机的INF文件中被指定，参考安装一个Unidrv小驱动程序部分的内容。

在一个GPD文件中，在给任何以*rc开始(如*rcIconID,*rcPromptMsgID和*rcCartridgeNameID)的条目赋值时都必须使用资源标识符。

另外，如果打印机包含硬件驻留的字体，必须以.ufm或.ifi的表格文件形式给这些字体提供打印机字体描述，并且必须在一个资源DLL中标识这些文件，并分别使用RC_UFM和RC_FONT资源类型。

微软提供了一个资源DLL，即unires.dll，它为标准特性及标准选项包含字符串资源。微软提供的GPD文件，stdnames.gpd，给每一个资源标识符都分配了一个宏符号名称，这就允许通过它们的宏名称引用这些资源，如例所示：*rcNameID:=LETTERSMALL_DISPLAY。

4.3打印机属性

打印机属性是GPD文件条目(GPD file entries)代表的打印机的特征。要设置一个特性值，可以在GPD文件中包含特性名称及想要设定的值。

以下主题描述了怎样在GPD文件中指定打印机的特性：

■4.3.1打印机特性格式

■4.3.2特性类型

■4.3.3一般特性

4.3.1打印机属性格式

为在GPD文件条目中指定一个打印机的属性，使用下面的格式：

*AttributeName:AttributeValue

这里AttributeName是一个预先定义的属于属性类型(Attribute Types)中的一个名称，AttributeValue是GPD值类型(Value types)的一种。

例如，*ModelName属性用于指定一个描述打印机硬件的文本字符串，为了给该属性赋值，可以将下面一行放入GPD文件中：

*ModelName:”Canon Buble-Jet BJC-600”

所有的属性名称都是由Unidrv的GPD解析器来预先定义并识别的。

4.3.2属性类型

一共定义了好几种类型属性，包括：

■一般属性：与打印机设备相关

■命令属性：与打印机命令相关

■特性属性：与专用的打印机特性相关

■选项属性：与属于特定特性的可选项相关

4.3.3一般属性

一般属性代表了由GPD语言定义的属性类型中的一种。一般属性与特殊的特性属性或可选属性都不相关。一般属性可以分为以下几种：

■根层次属性

■一般打印属性

■文本打印属性

■光栅打印属性

■向量打印属性

经常情况下，将GPD文件中的一般属性放置于根层次(即，不置于括号内)，根层次属性必须总是置于根层次。

偶然情况下，一般属性(除过根层属性)的值依敕于配置的参数。这时，属性条目可以被一个*Option语句替换，或者用*Case语句替换(置于根层次或包含于一个*Option语句)。如果属性不是根层次的(因为它包含于一个*Option语句或者它在于个非根层次的*Case语句)，这一属性名称必须以EXTERN_GLOBAL符号为前缀，如下：

EXTERN_GLOBAL：*AttributeName:AttributeValue

更多的关于指定相依关系的配置信息，可参考条件语句部分。

4.3.3.1根层次属性

根层次属性是描述特定驱动程序的一些特处，如资源文件名称、帮助文件或另外的包含于GPD文件，连同驱动程序的基本单位、版本号、字符代码页的一般特性。

附加的根层次特性指定这些特定设备的特征，如打印机的名称、类型、最大可复印能力及字体的Cartridge槽数。

这些属性被称为根层次属性，由于它们必须总是置于一个GPD文件的根层次(即不含于括号内)。

下表列出根目录的属性：

属性名称 属性参数 注解

*Codepage | 数值型Windows的代码页标识符，在平台SDK文档中有描述 | 可选。如果没有被指定就使用Unicode码，这代码页用于所有的被显示的字符串
*FontCartSlots | 表示打印机能提供的字体Cartridge槽的数值型数值 | 可选，如果没有被指定，默认值为0
*GPDFileName | 加引号的文本字符串，表示了GPD文件名(没有路径) | 可选
*GPDFileVersion | 加引号的文本字符串，表示当前GPD文件的版本。推荐的格式是MajorVersion.MinorVersion，即主版本号.小版本号，如“1.0” | 可选。如果没有被指定，这一字符串显示于Unidrv的About对话框中。
*GPDSpecVersion | 加引号的文本字符串，表示当前GPD说明的版本，所需的格式是MajorVersion.MinorVersion，即主版本号.小版本号，如“1.0” | 必需的。且必须是GPD文件中的第一个条目，在任何注解之前。对Windows 2000，这一值必须的“1.0”
*HelpFile | 加引号的字符串，包含一个定制的帮助文件的名称，具有.hlp的扩展名 | 可选。如果包含，它可以在帮助文件中增加主题或覆盖已有的主题。帮助文件的索引由*HelpIndex的特征及选项来指定
*Include | 加引号的字符串，包含附加GPD文件的名称 | 已废弃。这一条目已经被重定义为处理器重定向
*InstalledOptionName | 被显示的加引号的字符串，用于指明一个可以安装的特征或已经安装的选项。通常，这一字符串是“Installed”，但是也可以指定为任何合适的字符串 | 对任何特征或选项来说，如果*Installable?是TRUE时，且*rcInstalled OptionNameID没有被指定时，是必需的(参考特性属性部分)
*MasterUnits | 一个对值(PAIR)，表示打印机的基本单位 | 必需的。为了减少潜在的四舍五入法造成的错误，使用在*MasterUnits中指定的相同的精度值及字体规格(参考Unidrv字体规格文件)
*MaxCopies | 数字型值，表示打印机可以支持的最大的打印拷贝的份数 | 可选。如果没有被指定，其默认值是1
*ModeName | 加引号的字符串，表示打印机模式名称 | 如果*rcModeNameID没有被指定时是必需的，字符串必须与setup.inf中的名称相一致
*NotInstalledOptionName | 被显示的加引号的字符串，用于指明一个可以安装的特性或没有安装的选项。通常，这一字符串是“Not installed”，但是也可以指定为任何合适的字符串 | 如果对任何特征或选项*Installable?为TRUE时(参考特性属性部分)，并且*rcNotInstalledOptionNameID没有被指定时是必需的。
*Personality | 加引号的字符串，表示打印机应用的打印机语言 | 可选。如果被指定，字符串由目录服务(Directory Services)来显示，也可以参考*rcPersonalityID
*PrinterType | PAGE,SERIAL或TTY | 必需的
*PrinterRate | 数字型值，表示单色打印率，单位可以由*PrintRateUnit来指定 | 可选。如果没有被指定，默认值为0。
*PrintRatePPM | 数字型值，表示打印的速度，即每分钟的页数(pages per minute) | 可选。如果没有被指定，默认值为0。
*PrintRateUnit | PPM-Pages/min CPS-Characters/sec LPM—Lines/min IPM-Inches/min (IPM用于扫描仪) | 如果*PrinRate被指定时是必需的。指定的单位应当与打印机类型相匹配。例如，PPM应当为打印机打印页面时指定
*rcInstalledOptionNameID | 一个字符串资源的ID，用于显示可安装特性或被安装的选项。通常，这一字符串是“Installed”，但是也可以指定任何合适的字符串 | 如果对任何特征或选项*Installable?都为TRUE(参考特性属性部分)，且*InstalledOptionName没有被指定。
*rcNotInstalledOptionNameID | 一个字符串资源的ID，用于显示可安装特性或被安装的选项。通常，这一字符串是“Not Installed”，但是也可以指定任何合适的字符串 | 如果对任何特征或选项*Installable?都为TRUE(参考特性属性部分)，且*NotInstalledOptionName没有被指定。
*rcPersonalityID | 一个字符串资源的资源ID，表示打印机应用的打印机语言 | 可选。如果被指定，该字符串由目录服务来显示。(参见*Personality)
*rcPrinterIconID | 一个RC_ICON资源的资源ID，表示与打印机相关的图标 | 可选。如果没有被指定，一个默认的打印机图标将被显示，推荐所有的RC_ICON资源ID应当从1开始连续计数
*ResourceDLL | 加引号的字符串，包含名称的资源DLL，但没有路径信息 | 可选。参考在小驱动程序中使用资源DLL。

有关实例，可以参考GPD文件示例。

4.3.3.2一般打印机属性

一般打印机属性是用于指定与打印类型相关的属征，这些属性被分为以下组：

■打印机能力属性

■色彩属性

■光标属性

打印机能力属性

打印机能力属性是指定打印机属征如页边距、旋转及文本打印能力等影响所有的打印纸张尺寸和方向的一般打印属性。

属性名称 属性参数 注解

*MemoryUsage | 枚举打印机内存中存储的数据类型变量，可是FONT、RASTER VECTOR中的一种或多种，如果列出的数据类型打印机不支持，它将被忽略。 | 可选。如果没有被指定，其默认值是LIST(FONT,RASTER,VECTOR)。更多的信息，可以参考描述打印机内存配置部分
*OEMCustomData | 加引号的文本字符串，当它调用IPrintOemDriverUni::DrvGetGPDData提供一个绘制的插件 | 如果一个绘制插件调用IPrintOemDriverUni:: DrvGetGPDData函数时是必需的
*OutputOrderReversed? | TRUE或FALSE，指明是否多页文档是否从最后到第一页被排序 | 可选。如果没有被指定，默认值为FALSE。EXTERN_GLOBAL符号也不能与*OutputOrderReversed?一起使用。
*ReselectFont | 枚举常量，意指在Unidrv必需重新选择当前字体。可以是下面的一个：AFTER_GRXDATA-在任何CmdSendxxxxData光栅数据删除命令之后AFTER_XMOVE-在任意x轴光标移动命令之后AFTER_FF-在CmdFF命令之后 | 可选。如果没有被指定，Unidrv不重新选择字体
*RotateCoordinate? | TRUE或FALSE，意指打印机是否支持旋转坐标系统以匹配页面方向的命令 | 可选。如果没有被指定，默认值是FALSE。如果是TRUE，方向特性的*Option条目必须指定打印机命令。但在*Case条目中不能被替换
*RotateFont? | TRUE或FALSE,意指打印机是否自动旋转字体以匹配页面方向 | 可选。如果没有被指定，默认值为FALSE。如果是TRUE，则*RotateCooradinate?也必需是TRUE。不能在*Case条目中被置换。
*RotateRaster? | TRUE或FALSE，意指打印机是否自动旋转光栅数据以匹配页面方向 | 可选。如果没有被指定，默认值为FALSE。如果为TRUE，则*RotateCoordinate?也必须是TRUE。不能在*Case条目中被置换
*TextCapes | 枚举指明打印机的文本能力的常量。可以由一个或多个TC_XXX标记构成，在平台的SDK文档的GetDeviceCaps中有描述 | 可选。如果没有被指定，Unidrv 则假定为不支持文本能力

有关实例，可以参考GPD文件示例。

色彩属性

色彩属性是指定控制色彩打印特征的一般打印属性。

下表列出了色彩特性。

属性名称 属性参数 注解

*ChangeColorModeOnDoc? | TRUE或FALSE，意指在文档的不同页面中一个打印机的色彩模式是否可以被改变，并且不会产生负面作用 | 可选。如果没有被指定，默认值是TRUE，Unidrv用这一值来优化打印速度
*CyanInMagentaDye | 数字值，从0到1000,意指青色染紫色的百分比，该值是青色的百分比乘以100。例如，8.4%的青色指定为840，而10%的值就为1000。 | 可选。如果没有被指定，一个Unidrv提供的默认值就被使用
*CyanInYelowDye | 数字值，从0到1000,意指青色染黄色的百分比，该值是青色的百分比乘以100。例如，8.4%的黄色指定为840，而10%的值就为1000。 | 可选。如果没有被指定，一个Unidrv提供的默认值就被使用
*EnableGDIcolorMapping | TRUE或FALSE，意指GDI是否应当执行从显示器到打印机色彩空间的色彩映射 | 可选。如果没有被指定，默认值是FALSE。如果是TRUE，Unidrv则在GDIINFO结构中设置HT_FLAG_DO_DEVCLR_XFORM标志
*MagentaInCyanDye | 数字值，从0到1000,意指紫色染青色的百分比，该值是紫色的百分比乘以100。例如，8.4%的紫色指定为840，而10%的值就为1000。 | 可选。如果没有被指定，一个Unidrv提供的默认值就被使用
*MagentaInYellowDye | 数字值，从0到1000,意指紫色染黄色的百分比，该值是紫色的百分比乘以100。例如，8.4%的紫色指定为840，而10%的值就为1000。 | 可选。如果没有被指定，一个Unidrv提供的默认值就被使用
*YellowInCyanDye | 数字值，从0到1000,意指黄色染青色的百分比，该值是黄色的百分比乘以100。例如，8.4%的黄色指定为840，而10%的值就为1000。 | 可选。如果没有被指定，一个Unidrv提供的默认值就被使用
*YellowInMagentaDye | 数字值，从0到1000,意指黄色染紫色的百分比，该值是黄色的百分比乘以100。例如，8.4%的黄色指定为840，而10%的值就为1000。 | 可选。如果没有被指定，一个Unidrv提供的默认值就被使用

有关实例，可以参考GPD文件示例。

光标属性

光标属性是指定一个打印机光标的特征的一般属性。

下表列出了所有的光标属性。

属性名称 属性参数 注解

*BadCursorMoveInGrxMode | 列出在光栅图形模式中代表不合法的光标移动的值。其值可以是下边值中的一个或多个：X_PORTRAIT,X_LANDSCAPE,Y_PORTRAIT,Y_LANDSCAPE | 可选。如果没有被指定，默认值没有限制。如，LIST(X_PORTRAIT)意指X轴方向的运动对Portrait方向是不允许的。
*CursorXAfterCR | AT_PRINTABLE_X_ORIGIN或AT_CORSOR_X_ORIGIN的一种，意指在回车之后光标的x轴位置 | 可选。如果没有被指定，默认值是AT_CURSOR_X_ORIGIN，它是物理零点的位置
*EjectpageWithFF? | TRUE或FALSE。意指打印机是否用表格送纸(form-feed)式进纸。 | 可选。如果没有被指定，默认值是FALSE。
*MaxLineSpacing | 数字型值，表示在y轴的基本单位中的最大空间距 | 可选。如果没有被指定，Unidrv假定没有最大值
*UseSpaceForXMove | TRUE或FALSE。意指执行x轴方向运动时能否用空格字符 | 可选。如果没有被指定。默认值是TRUE。如果是TRUE，Unidrv用空格进行粗略移动而用NULL来进行精细移动。如果是FALSE，Unidrv用NULLs来进行所有的移动。
*XmoveThreshold | 数字型值，在x轴的基本单位中表示用CmdXMoveAbsolute时的阀值而不是CmdXMoveReLeft或CmdXMoveRelRight的阀值。 | 可选。如果没有被指定，默认值是零，意指CmdXMoveAbsolute应当总是被应用。只有所有三个x方向移动命令被指定才是可用的。
*XmoveUnit | 数字型值，即每英寸的点数，表示打印机能够进行的最小的水平移动。例如，如果运行单位是1/600th每英寸，则指定值即为600。 | 如果打印机支持水平光标运动命令时是必需的。(如果被指定，当计算基本单位时将包括该值)
*YmoveAttributes | LIST意味y轴运动的属性值，可以是下边的一个或多个：FAV_LF(favor LF spacing),SEND_CR_FIRST | 可选。如果没有被指定，则不假定任何属性。
*YMoveThreshold | 数字型值,在y轴基本单位上，表示用CmdXMoveAbsolute时的阀值而不是CmdXMoveReLeft或CmdXMoveRelRight的阀值。 | 可选。如果没有被指定，默认值是零，意指CmdYMoveAbsolute应当总是被应用。只有所有三个x方向移动命令被指定才是可用的。
*YMOveUnit | 数字型值，即每英寸的点数，表示打印机能够进行的最小的垂直移动。例如，如果运行单位是1/600th每英寸，则指定值即为600。 | 如果打印机支持垂直光标运动命令时是必需的。(如果被指定，当计算基本单位时将包括该值)

相关实例，可以参考实例GPD文件。

4.3.3.3文本打印属性

文本打印属性是指定一个打印机的文本打印能力的一般属性。这些属性可以分为以下几组：

■设备字体属性

■下载字体的属性

■仿真字体属性

设备字体属性

下表列出了描述打印机支持的设备字体的属性：

属性名称 属性参数 注解

*CharPosition | UPPERLEFT或BASELINE。意指在打印字符之前的限定于特定区域的字符在打印头的位置 | 可选。如果没有被指定，则默认值是UPPERLEFT。
*DefaultCTT | 数字型值，表示默认字符翻译表的RC_CTT资源标识符 | 可选。如果没有被指定，将没有翻译表。(这一属性只在对向后兼容的GPC文件时提供)
*DefaultFont | 数字型值，表示默认字体的资源标识符RC_FONT或RC_UFM。 | 如果打印机支持设备字体时必需。
*LookAheadRegion | 数字型值，表示在基本的Y轴单位时预准备的区域大小。 | 可选。如果没有被指定，默认值是零。只与串行打印机时使用(如HP的DeskJeT)，以绘制文本和位图数据
*MaxFontUsePerpage | 数字型值，表示打印机每一页可用的最大字体号 | 可选。如果没有被指定，就没有限制
*TextYOffset | 数字型值，代表在y轴基本单位中的垂直距离，根据它驻留的字体必需重新根据位图字体的基准线重排位置。 | 可选。如果没有指定，默认值是零(用于一些点阵打印机)。

相关实例，可以参考GPD实例文件。

下载字体的属性

下表中列出属性描述了打印机对下载字体的支持。

属性名 属性参数 注解

*DLSymbolSet | 常量(PC-8,ROMAN-8等)表示下载TrueType字体时设置的符号 | 可选。如果没有被指定，符号区间假定为由*MinGlyphID和*MaxGlyphID间指定的连续区间，可无限制的使用。
*FontFormat | 常量值，意指支持的下载字体的类型。必需是HPPCL,HPPCL_RES，HPPCL_OUTLINE，OEM_CALLBACK中的一种 | 打印机能够下载字体时是必需的。如果OEM_CALLBACK被指定，字体的回调函数必需提供。更多的信息，可以参考定制微软打印机驱动程序部分的这些回调函数
*MaxFontID | 数字型值，表示软字体的最大标识符数 | 可选。如果没有被指定，默认值是65535
*MaxGlyphID | 数字型值，表示对被下载的字体符号的最大标识符数 | 可选。如果没有被指定，并且*DLSymbolSet也没有被指定，默认值是255；如果*DLSymbolSet被指定，则将被忽略。
*MaxNumDownFonts | 数字型值，表示在同一时间可以存储于打印机内存中的软字体的最大数量 | 可选。如果没有被指定，Unidrv假定无限制的软件字体可以被存储。
*MinFontID | 数字型值，表示软件字体的最小标识符 | 可选。如果没有被指定，默认值是1
*MinGlyphID | 数字型值，表示对被下载的字体符号的最小标识符数 | 如果*DLSymbolSet没有被指定，默认值是32，如果*DLSymbolSet被指定，则将被忽略。

相关实例，可以参考GPD实例文件。

仿真字体属性

下表列中列出的属性描述了打印机对仿真字体的支持。

属性名 属性参数 注解

*DiffFontsperByteMode?

TRUE或FALSE。对支持单字节及双字节模式的打印机来说，这意味着打印机是否对每一种模式保持一种独立的字体及字体特征。

可选。如果没有被指定，默认值是FALSE。

相关实例，可以参考GPD实例文件。

4.3.3.4光栅打印属性

光栅打印属性是指定打印机的文本打印能力的一般属性，这些属性可分成以下几组：

■光栅数据压缩属性

■光栅数据发送属性

光栅数据压缩属性

没有与光栅数据压缩属性相关的属性。

更多的关于光栅数据压缩的命令，可以参考光栅数据压缩命令部分。

光栅数据发送属性

下列列出的属性描述了打印机支持的光栅数据发送属性。

属性名 属性参数 注解

*CursorXAfterSendBlockData | 常量值，意指一块光栅数据发送之后的光标的x轴位置，可以是AT_RXDATA_END,AT_GRXDATA_ORIGIN,AT_CURSOR_X_ORIGIN中的一种，意思分别是在图形块的开始的像素，最后一块图形块的像素及光标原点 | 可选。如果没有被指定，默认值是AT_GRAXDATA_END
*CursorYAfterSendBlockData | 常量值，意指一块光栅数据发送之后的光标的y轴位置，可以是NO_MOVE，AUTO_INCREMENT中的一种 | 可选。如果没有被指定，默认值是NO_MOVE，意思是光标的y轴位置没有被改变
*MirrorRasterByte? | TRUE或FALSE。意指Unidrv是否应当镜像(保留)图像的每一字节信息 | 可选。如果没有被指定，默认值是FALSE
*MoveToX0BeforeSetColor? | TRUE或FALSE。意指是否光标的X轴坐标在一个明确的色彩选择命令被发送之前，必需被设置为零 | 可选。如果没有被设定，默认值是FALSE。只有在*UseExpcolorSelectCmd?也是TRUE时其值也为TRUE。
*OptimizeLeftBound? | TRUE或FALSE。意指是否Unidrv应当删除每一个区带的左边界限的空格 | 可选。如果没有被指定，默认值是FALSE
*OutputDataFormat | H_BYTE或V_BYTE，意指是否在数据类型中的字节位被映射到水平像素或垂直像素 | 可选。如果没有被指定，默认值是H_BYTE
*RasterSendAllData? | TRUE或FALSE。意指Unidrv是否应当发送所有的光栅数据，包括扫描仪之间的空格或空行 | 可选。如果没有被指定，默认值是FALSE
*SendMultipleRows | TRUE或FALSE。意指是否由CmdSendBlockData指定的命令可以在同一时间发送多个块 |
*StripBlanks | LIST意指哪些在光栅数据块中的数据应当被删去，可以是LEADING,ENCLOSED,TRAILING中的一个或多个 | 可选。如果没有被指定，Unidrv将不去除任何空格，可以参考精度特性中的选项属性内容中的*MinStripBlankPiexels
*UseExpColorSelectCmd? | TRUE或FALSE。意指打印机是否需要分离于色彩光栅数据的明确的色彩选择命令 | 可选。如果没有被指定，默认值是FALSE。点阵打印机需要值为TRUE

关于光栅数据发送相关的命令及信息，可以参考光栅数据发送命令部分的内容。

相关实例，可以参考GPD实例文件。

4.3.3.5向量打印属性

向量打印属性是指定打印机的向量打印属征的一般属性。这些属性由一个单独的组构成：

■矩形区域填充属性

矩形区域填充属性

下表列出的属性描述了打印机对填充矩形区域的支持。

属性名 属性参数 注解

*CursorXAfterRectFill | AT_RECT_X_ORIGIN或AT_RECT_X_END，指明在打印机填充了一个矩形区域后光标的x坐标在什么地方 | 可选。如果没有被指定，默认值是AT_RECT_X_ORINGIN
*CursorYAfterRectFill | AT_RECT_Y_ORIGIN或AT_RECT_Y_END，指明在打印机填充了一个矩形区域后光标的y坐标在什么地方 | 可选。如果没有被指定，默认值是AT_RECT_Y_ORGIN
*MaxGrayFill | 数字型值，表示作为cmdRectGrayFill命令的允许的最大的灰色填充百分比 | 可选。如果没有被指定，默认值是100
*MinGrayFill | 数字型值，表示作为cmdRectGrayFill命令的允许的最小的灰色填充百分比 | 可选。如果没有被指定，默认值是1

4.4打印机命令

每一个打印机命令可以下面两种方式的任一种实现：

■可以在GPD文件中置放一个特定设备命令字符串，Unidrv将在适当的时间发送该命令字符串到打印假脱机。

■可以实现IPrintOemUni::CommandCallback COM方法，它动态地产生一个命令字符串。Undirv在它需要向假脱机发送命令时调用该方法。更多的信息可以参考第7章动态产生打印机命令中的定制微软打印机驱动程序部分的内容。

下面的主题描述了怎样在GPD文件中指定打印机命令：

■4.4.1命令条目格式

■4.4.2命令名称

■4.4.3命令属性

■4.4.4命令字符串格式

■4.4.5命令执行顺序

4.4.1命令条目格式

为了在一个GPD文件中指定打印机命令，可利用下面格式：

*Command:CommandName{CommandAttributes}

这里CommandName是一个预定义的命令名，CommandAtributes是一套命令属性。

例如，一个GPD文件可以包含下面的CmdStartPage命令的说明，它初始化一个将要打印的页面：

*Command:CmdStartPage

{

*Order:PAGE_SETUP.100

*Cmd:”<0D>”

}

如果，对于一个特殊的CommandName值，只需要指定*Cmd属性，可以使用缩略式命令条目格式，如下：

*Command:CommandName:CommnadString

这里，CommandString是一个文本字符串，表示一个打印机命令的escape序列，更多的关于指定escape序列的信息，可以参考命令字符串格式部分的内容。例如，一个GPD文件也许可以包含下面的CmdBoldOn命令的说明，它打开了对字符文本加粗(Bold)的功能：

*Command:CmdBoldOn:”<1B>(s3B”

4.4.2命令名称

GPD语言对每一个普通打印机命令定义了一个唯一的命令名称，另外，它对指定用于选择打印机选项定义了CmdSelect命令名称。

下面列出的命令名称包括以下主题：

■4.4.2.1一般打印命令

■4.4.2.2打印机配置命令

■4.4.2.3选项选择命令

■4.4.2.4文本打印命令

■4.4.2.5光栅打印命令

■4.4.2.6向量打印命令

4.4.2.1一般打印命令

一般打印命令是那些可与任意类型打印关联的命令，这些命令可以分为下面二组：

■色彩命令

■光标命令

色彩命令

下面的主题描述了色彩命令：

■选择基础背景色彩命令

■控制打印机调色板命令

■选择调色板画刷命令

所有的命令都用命令条目格式来指定。

选择基础背景色彩命令

下表中列出的打印机命令被打印机应用但不支持可编程的色彩调色板，如平面色彩打印机(例如，点阵打印机)及一些调色板打印机(例如，较早期的喷墨打印机)。

命令 描述 注解

CmdSelectBlackColor | 选择黑色背景的命令 | 可选
CmdSelectBlueColr | 选择蓝色背景的命令 | 可选
CmdSelectCyanColor | 选择青色背景的命令 | 可选
CmdSelectGreenColor | 选择绿色背景的命令 | 可选
CmdSelectMagentaColor | 选择紫色背景的命令 | 可选
CmdSelectRedColor | 选择红色背景的命令 | 可选
CmdSelectYellowColor | 选择黄色背景的命令 | 可选
CmdSelectWhiteColor | 选择白色背景的命令 | 可选

相关实例，可以参考GPD实例文件。

控制打印机调色板的命令

下表列出了用于支持对前景(文本及向量)打印或光栅打印时的打印机可编程调色板的打印命令。

命令 描述 注解

CmdBeginPaletteDef | 色彩调色板定义初始化的命令 | 可选。如果没有被指定，则不需要调色板初始化定义
CmdEndPaletterDef | 结束一个调色板定义的命令 | 可选。如果没有被指定，则不需要一个命令去结束一个调色板定义。 *Order属性可以被指定，如果没有指定，则*Order属性必需与最近执行的选项选择命令的*ColorMode属性相关。
CmdBeginPaletteReDef | 色彩调色板重定义初始化的命令 | 可选。如果没有被指定，则不需要调色板初始化的重新定义
CmdEndPaletteReDef | 结束一个调色板的重新定义 | 可选。如果没有被指定，则不需要命令来结束调色板重新定义
CmdDefinePaletteEntry | 定义一个调色板条目的命令 | 如果打印机支持调色板，则是必需的。
CmdRedefinePaletteEntry | 重新定义一个调色板条目的命令 | 可选。如果没有被指定，CmdDefinePaletteEntry则被用于重新定义调色板条目
CmdSelectPaletteEntry | 选择一个调色板条目作为当前色彩的命令 | 如果打印机支持调色板，则是必需的

相关实例，可以参考GPD实例文件。

选择调色板画刷的命令

下表列出的是被打印机用于支持下载及选择调色板画刷的打印机命令。

命令 描述 注解

CmdDownloadPattern | 发送一个调色板画刷到打印机的命令 | 可选。如果被指定，CmdSelectPattern就需被指定
CmdSelectBlackBrush | 选择一个实体黑色的画画刷为当前调色板画刷的命令 | 如果打印机支持画刷，则是必需的
CmdSelectPattern | 选择一个可下载的调色板画刷的命令 | 可选。如果被指定，CmdDownloadPattern也必需被指定
CmdSelectWhiteBrush | 选择一个实体黑色的画刷为当前调色板画刷的命令 | 可选。

光标命令

下表列出的打印机命令，用于控制光标的移动，所有的命令都用命令条目格式来指定。

命令 描述 注解

CmdBackSpace | 将光标向回移动到最后一个被打印的字符处 | 可选。可在字体加粗时应用
CmdCR | 移动光标到它的x位置的最左边 | 必需的
CmdFF | 进页命令 | 如果*EjectPageWithFF?设置为TRUE时，是必需的
CmdLF | 移动光标到下一行 | 可选。移动的总量由CmdSetLineSpacing指定
CmdPopCursor | 从栈中弹出存储的光标的最后位置 | 如果CmdPushCursor被指定，则是必需的
CmdPushCursor | 将当前的光标位置压入到栈中 | 可选。
CmdSetLineSpacing | 当涉及CmdLF时，设置光标移动的距离 | 可选。
CmdUniDirectionOff | 禁止单向打印的命令，因此允许双向打印 | 可选。
CmdUniDirectionOn | 允许单向打印的命令 | 可选。如果没有提供，以双向模式打印
CmdXMoveAbsolute | 移动光标到一个绝对的x轴位置的命令 | 可选。命令字符串可以包括一个标准变量，它用于指定距。
CmdXMoveRelLeft | 根据被指定的总量，将光标从当位x位置移动到其左边一定位置的命令 | 可选。命令字符串可以包括一个标准变量，它用于指定距离
CmdXMoveRelRight | 根据被指定的总量，将光标从当位x位置移动到其右边的一定位置的命令 | 可选。命令字符串可以包括一个标准变量，它用于指定距离
CmdYMoveAbsolute | 移动光标到一个绝对的y轴位置的命令 | 可选。命令字符串可以包括一个标准变量，它用于指定距离
CmdYMoveRelDown | 根据被指定的总量，将光标从当前y轴位置向下移动到一定位置的命令 | 可选。命令字符串可以包括一个标准变量，它用于指定距离
CmdYMoveRelUp | 根据被指定的总量，将光标从当前y轴位置向上移动到一定位置的命令 | 可选。命令字符串可以包括一个标准变量，它用于指定距离

相关实例，可以参考GPD实例文件。

4.4.2.2打印机配置命令

下面的表格列出了打印配置命令。所有的命令都由命令条目格式来指定。

命令 描述 注解

CmdStartJob | 初始化一个打印作业的命令 | 可选。命令执行顺序必需被指定
CmdStartDoc | 初始化一个文档的命令 | 可选。命令执行顺序必需被指定
CmdStartPage | 初始化一个页面并将光标位置设为坐标系原点(0,0)的命令 | 可选。命令执行顺序必需被指定
CmdEndPage | 结束打印一页的命令 | 可选。命令执行顺序必需被指定
CmdEndDoc | 结束打印一个文档的命令 | 可选。命令执行顺序必需被指定
CmdEndJob | 完成一个打印作业的命令 | 可选。命令执行顺序必需被指定
CmdCopies | 指定打印时的拷贝份数的命令 | 可选。命令执行顺序必需被指定
CmdSleepTimeOut | 指定打印机进入省电模式前等待的分钟数的命令 | 可选。命令执行顺序必需被指定

相关实例，可以参考GPD实例文件。

4.4.2.3选项选择命令

下表中列出了选项选择命令，命令由命令条目格式指定。

命令 描述 注解

CmdSelect

选择一个打印机选项的命令

对每一个选项都是必需的，且命令执行顺序必需被指定

相关实例，可以参考GPD实例文件。

4.4.2.4文本打印命令

文本打印命令是那些与打印文本相关的命令，这些命令可以分为下面几组：

■设备字体命令

■下载字体命令

■仿真字体命令

设备字体命令

没有与设备字体相关的命令。更多的关于设备字体的特性，参考设备字体属性部分。

下载字体命令

下表列出的命令用于控制下载字体，所有的命令都用命令条目格式来指定。

命令 描述 注解

CmdDeleteFont | 通过指定字体标识符来删除软字体的命令 | 可选。如果被删掉的字体相关的内存可以被立即收回，指定这一命令
CmdDeselectFontID | 撤消对当前字体ID的选定以使该字体处于非活动状态的命令 | 可选。如果不是当前状态，一种新字体被选定时，当前的字体不必要进行撤消选定
CmdSelectFontHeight | 选择被下载字体的高度的命令 | 可选。如果不是当前状态，打印机不支持可缩放、下载的TrueType轮廓字体。该命令在HPPCL_OUTLINE时是需要的
CmdSelectFontID | 选择当前的字体ID以使该字体处于活动状态的命令 | 如果打印机支持下载字体时，是需要的。
CmdSelectFontWidth | 选择下载字体的宽度的命令 | 可选。如果不是当前状态，下载字体的宽度可根据它的高度适当调整
CmdSetCharCode | 指定下一个将要被下载或被删除的字体的代码的命令 | 如果打印机支持下载字体时，是需要的。
CmdSetFonID | 设置当前字体ID的命令 | 如果打印机支持下载字体时，是需要的。

相关实例，可以参考GPD实例文件。

仿真字体命令

下表列出的命令用于控制仿真字体。所有的命令用命令条目格式来指定。

命令 描述 注解

CmdBoldOff | 禁止加粗(Bolding)的命令 | 可选。如果CmdBoldOn被指定，则它必需被指定
CmdBoldOn | 允许加粗的命令 | 可选。如果被指定，Undriv发送这一命令以允许加粗并且发送CmdBoldOff以禁止加粗
CmdClearAllFontAttribs | 单一命令，以禁止加粗、斜体、及下划线的命令 | 可选。如果打印机支持加粗、斜体或下划线时可以被指定，但是只支持一个命令来禁止加粗、斜体及下划线。而不是用CmdBoldOff,cmdItalicOff及CmdUnderlineOff
CmdItalicOff | 禁止斜体的命令 | 可选。如果CmdItalicOn被指定，则它必需被指定
CmdItalicOn | 允许斜体的命令 | 可选。如果被指定，Unidrv发送这一命令以允许斜体并发送CmdItalicOff以禁止斜体
CmdSelectDoubleByteMode | 允许双字节打印的命令 | 可选。如果CmdSelectSingleByteMode被指定，则它必需被指定
CmdSelectSingleByteMode | 允许单字节打印的命令 | 可选。如果打印机可以在单字节打印有双字节打印之间切换，则必需被指定
CmdSetFontSim | 单一命令，以设置加粗、斜体、下划线及加删除线能力的命令 | 可选。如果在每一次字体被应用时字体特征都必须被指定时必须指定。
CmdStrikeThruOff | 禁止加删除线的命令 | 可选。如果CmdStrikeThruOn指定，则必需被指定。
CmdStrikeThruOn | 允许加删除线的命令 | 可选。如果被指定，Unidrv将发送命令以允许加删除线并发送CmdStrikeThruOff以禁止加删除线
CmdUnderlineOff | 禁止下划线的命令 | 可选。如果CmdUnderlineOn被指定时必需被指定，
CmdUnderlineOn | 允许下划线的命令 | 可选。如果被指定，Unidrv发送这一命令以允许加下划线并发送CmdUnderlineOff以禁 止加下划线
CmdVerticalPrintingOff | 禁止垂直打印的命令 | 可选。如果CmdVerticalPrintingOn被指定，则必需指定
CmdVerticalPrintingOn | 允许垂直打印的命令 | 可选。如果打印机支持垂直打印则必需被指定
CmdWhiteTextOff | 禁止打印白色文本的命令 | 可选，如果CmdWhiteTextOn被指定则必需被指定
CmdWhiteTextOn | 允许打印白色文本的命令 | 可选。如果被指定，Unidrv发送这一命令以允许打印白色文本并发送CmdWhiteTextOff以禁止打印白色文本(这一命令是为与GPC3.0向后兼容提供的)

相关实例，可以参考GPD实例文件。

4.4.2.5光栅打印命令

光栅打印命令是与那些与打印光栅图形的命令相关的打印命令。

这些命令被分成为以下几组：

■光栅数据压缩命令

■光栅数据发送命令

光栅数据压缩命令

下表列出的光栅数据压缩命令，所有的命令都用命令条目格式指定。

更多的关于光栅数据压缩命令的信息，可以参考压缩光栅数据部分。

命令 描述 注解

CmdDisablecompression | 禁止打印机接受所有压缩数据类型的命令 | 可选。
CmdEnableDRC | 允许打印机接受DRC压缩的数据的命令 | 可选。如果没有被指定，Unidrv不使用DRC压缩(Delta-Row compression)
CmdEnableFE_RLE | 允许打印机接受FE_RLE压缩的数据的命令 | 可选。如果没有被指定，Unidrv不使用FE_RLE压缩
CmdEnableOEMComp | 允许打印机接受定制类型压缩的数据 | 可选。如果没有被指定，Unidrv不使用定制的数据压缩
CmdEnableTIFF4 | 允许打印机接受TIFF 4.0压缩的数据 | 可选。如果没有被指定，Unidrv不使用TIFF 4.0压缩

相关实例，可以参考GPD实例文件。

光栅数据发送命令

下表列出了光栅数据发送的命令，所有的命令都是由命令条目格式指定的。

命令 描述 注解

CmdBeginRaster | 初始化一个光栅数据传输的命令 | 可选。如果没有被指定,Unidrv认为不需要初始化
CmdEndRaster | 完成一个光栅数据传输的命令 | 可选。如果没有被指定，Unidriv认为不需要传输完成的操作(transfer-completion)
CmdSetDestBmpHeight | 设置目标位图的高度的命令 | 可选。只有在打印机支持可缩放的位图时才适用
CmdSetDestBmpWidth | 设置目标位图的宽度的命令 | 可选。只有在打印机支持可缩放的位图时才适用。
CmdSetSrcBmpHeight | 设置一个源位图的高度的命令 | 可选，只有在打印机支持可缩放位图时才适用
CmdSetSrcBmpWidth | 设置一个源位图的宽度的命令 | 可选。只有在打印机支持可缩放位图时才适用
CmdSendBolckData | 发送一个数据块到打印机的命令 | 如果*OutputDataFormat是V_BYTE时是需要的，一个块包含打印头的物理传送的数据(参考*PinsPerPhysPass)。如果*OutputDataFormat是H_BYTE,则一个块包含打印头逻辑传送的数据(参考*PinsPerLogpass)
CmdEndBlockData | 一个意味着用CmdSendBlockData发送数据时结束的命令 | 可选。如果没有被定，Undriv假定不需要什么命令来指明传送块的结束(在一些点阵打印机中用到)
CmdSendBlackData | 发送黑色平面数据到打印机的命令 | 如果使用*UseExpColorSelectCmd?属性值是FALSE时是需要的
CmdSendBlueData | 发送蓝色平面数据到打印机的命令 | 如果使用*UseExpColorSelectCmd? 属性值是FALSE时是需要的
CmdSendCyanData | 发送青色平面数据到打印机的命令 | 如果使用*UseExpColorSelectCmd? 属性值是FALSE时是需要的
CmdSendGreenData | 发送绿色平面数据到打印机的命令 | 如果使用*UseExpColorSelectCmd? 属性值是FALSE时是需要的
CmdSendMagentaData | 发送紫色平面数据到打印机的命令 | 如果使用*UseExpColorSelectCmd? 属性值是FALSE时是需要的
CmdSendRedData | 发送红色平面数据到打印机的命令 | 如果使用*UseExpColorSelectCmd? 属性值是FALSE时是需要的
CmdSendYellowData | 发送黄色平面数据到打印机的命令 | 如果使用*UseExpColorSelectCmd? 属性值是FALSE时是需要的

相关实例，可以参考GPD实例文件。

4.4.2.6向量打印命令

向量打印命令是那些与打印向量图形相关的命令。这些命令组成一个单一的组：

■矩形区域填充命令

矩形区域填充命令

以下表格列出了矩形区域填充命令，所有的命令都用命令条目格式指定。

命令 描述 注解

CmdRectBlackFill | 黑色填充一个矩形区域 | 可选的。如果没有被指定，Unidrv企图通过指定用灰色百分比*MaxGrayFill的CmdRectGrayFill命令来进行黑色填充
CmdRectGrayFill | 灰色填充一个矩形区域(不删除背景) | 可选。如果没有被指定，Unidrv认为没有灰色填充的能力，命令字符串通常包括GrayPercentage变量
CmdRectwhiteFill | 白色填充一个矩形区域(不删除背景) | 可选。如果没有被指定，Unidrv假定没有删除白色填充。这种情况下，如果应用程序请求白色填充，Unidrv将返回失败，因为灰色填充不能删除背景
CmdSetRectHeight | 设置矩形高度的命令 | 可选。如果CmdSetRectWidth被指定则必需被指定
CmdSetRectSize | 设置矩形高度和宽度的命令 不支持Windows 2000 | 可选。用于打印机设置在一个命令中设置高度和宽度
CmdSetRectWidth | 设置矩形宽度的命令 | 可选。如果CmdRectHeight被指定，就必需被指定。

相关实例，可以参考GPD实例文件。

4.4.3命令属性

当指定一个打印机的命令，使用属性来给Unidrv提供以下的信息：

■如果操作在打印机硬件实现，则引起硬件执行操作的escape序列。

■如果操作在一个绘制插件中实现时，回调标识符及IPrintOemUni::CommandCallback方法所需要的参数。

■一个命令应当被发送的、与其他命令相关的顺序。

下表列出的以字母顺序排出的命令属性描述了它们的参数：

属性名 属性参数 注解

*CallbackID | 正的数值型值，被传送到绘制插件的IPrintOemUni::CommandCallback方法作为它的dCmdCbID参数 | 在动态产生打印机命令时需要，如果*Cmd被指定，则不有效
*Cmd | 文本字符串，包含一个打印机命令的escape序列，用字符命令串格式指定 | 除非*CallbackID被指定时必需
*NoPageEject? | TRUE或FALSE。意指是否执行命令以引起打印机跳过本物理页。只有在*Order指定了DOC_SETUP区段并且允许DUPLEX打印时使用。以避免在重复的文档页面中过早跳页。如果可能，Unidrv只讨论这一特性设置为TRUE的问题 | 可选。如果没有被指定，默认值是FALSE。意指命令可能会引起跳页。 如果一个命令引起副作用(即，如果命令修改打印机设置超出将*NoPageEject?设置为TRUE的控制以外)就不能为TRUE。
*Order | 区段名以及顺序号码，如在命令执行顺序中所描述的 | 只有在与配置命令及定制的选项命令时是有效的，除非在命令描述中给出状态。
*Params | 列出标准变量，传寄给绘制插件的在EXTRAPARAM结构中的IPrintOemUni::CommandCallback方法，并作为一个pdwParams参数被传递 | 只在*CallbackID被指定时是有效的。

相关实例，可以参考GPD实例文件。

4.4.4命令字符串格式

命令字符串用于指定Unidrv必须发送给打印机硬件的escape序列。命令字符串可以由以下元素构成：

■加引号的文本字符串，它们有这样的格式：“TextString”，这里TextString是一个文本字符串。

■命令参数，它有这样的格式：%ArgumentType{StandardVariableExpression}

这里，ArgumentType是一个字母，它表示一个命令字符串参数的类型，StandardVariableExpression是一个标准的变量表示。

任何数量的加引号的文本字符串和任何数量的命令参数都可以在命令字符串中组合起来。

如一个实例，一个打印机的命令用于设置矩形的灰色填充也许可以指定如下：

Command:CmdRectGrayFill:”*<1B>c” %d{GrayPercentage}”g2p”

发送一个百分号符号(%)到打印机，则命令字符串中包括两个百分号符号。如果百分号符在命令字符串的末尾，必须使用十六进制的等制来表示，如：

“String<25 25>”

4.4.4.1命令字符串参数类型

当在命令字符串中包括参数，必须指定每一个参数的类型，每一个参数类型的说明是一个单一字母，其前放置了一个百分号。

下表列出所有的参数类型的指定符号。

参数类型指定符 结果值的描述

%<Digits>d | ASCII字符串，表示一个十进制的值，如果是负数还包括一个减号。<Digits>是一个可选的数字，意指字符串的长度
%<Digits>D | ASCII字符串，表示十进制的值，包括一个加号或减号。<Digits>是一个可选的数字，意指字符串的长度
%c | 二进制字节
%C | 附加了ASCII“0”的二进制字节
%f | 无符号ASCII字符串，表示一个十进制的值，具有一个从右边开始第三位字母是十进制的小数点，如“12.25”
%g | 2*ABS(parameter)+IS_NEGATIVE(parameter)作为一个64位数，从最小的有效位数到最多的有效位数。最多的有效位数(0-63)由192字节到254字节来表示，其他的位数由63字到126字节来表示。如果parameter为负数则“IS_NEGATIVE(parameter)”值为1，其他值时结果为0
%l | 二进制字，最小的有效字节数
%m | 二进制字，最多的有效字节数
%n | 佳能整型编码(Cannon integer encoding)。从最多的有效字数到最小的有效字节数的二进制值编码。四个最小位数被编码为001sbbb，这时s表示符号(0是负数，1是正数)，b表示整数的有效位数。下一个最大小效的6位数被编码为01bbbbbb。例如，254(11111110)表示为(01001111 00111110)
%q | ASCII字符串，表示一个对Toshiba/Qume设备的QUME十六进制数
%v | NEC VFU(Vertical Format Unit)编码，指定变量的值按1/6英寸等分，结果值是VFU被发送到打印机的次数

可以为任何参数指定可以接受的值的区间。为这样做，包括参数的最大值及最小值都可以置于一个方括号中([,]),接着是参数类型的指定符，并用一个逗号分开其值。例如，下边的命令指定0到255为可接受的LinfeedSpacing/2的值区间：

Command:CmdSetLineSpacing{*Cmd:”<1B>3”%c[0,255]{(LinefeedSpacing/2)}

4.4.4.2标准变量表达式

当指定命令中的参数时，可以作为一个表达式来指定参数的值。这一表达式可以用当前标准变量的值来执行操作。每一个在命令字符串中的标准变量表达式由圆括号分隔开来({,})。

一个标准变量表达式由下面的部分组合构成：

■0，1或更多的标准变量

■整形数值

■表达式操作符

一个标准变量表达式不能包含嵌入的宏引用。

表达式操作符包含于下面的表中。

操作符 定义

Val1+Val2 | 加法
Val1-Val2 | 减法
Val1/Val2 | 除法
Val1*Val2 | 乘法
Max(Val1,Val2) | 取最大值，其值为Val1,Val2中的最大值
Max_repeat(Val1) | 参考使用Max_repeat()
Min(Val1,Val2) | 取最小值，其值为Val1,Val2中的最小值
Mod(Val1,Val2) | 取模，其值为Val1除以Val2的余数
() | 优先操作符，如果不使用，则使用C语言的优先操作符

标准的变量表达式不修改赋给标准变量的值，计算结果的值被放置于escape序列中，利用由命令字符串类型指示符指定的格式来存放。

利用Max_repeat

max_repeat最好用一个实例来解释。假定一个GPD文件包含下面的条目：

*Command:CmdXMoveRelRight{*Cmd:”<1B>[”$d[0,9600]]{max_repeat((DestXRel/4))}”a”}

这一命令包含一个单一参数，即%d类型。它也包含一个参数的区间说明。无论何时Unidrv发送这一命令到打印机，它首它计算DextXRel/4并确定它是否在指定的区间内，如果计算的值大于9600，Unidrv将以最大值为9600重复发送命令，直到指定的值被发送为止。这样，如果DesXRel/4等于20,000，Unidrv将发送下面的命令：

<1B>[9600

<1B>[9600

<1B>[800

max_repeat操作符只有在下面的条件下才可以遇到：

■命令字符串只包括一个参数

■参数包括一个区间说明

4.4.5命令执行顺序

打印机命令必须以一个具有意义的顺序被发送到打印机硬件。对许多定义在GPD语言中的命令名称，Unidrv知道什么时候向打印机发送escape序列的命令，只有两种特例：

■选项选择命令

■打印机配置命令

对这两种类型的命令，必须指定一个命令执行的顺序。

命令执行顺序由两项组件组成——一个作业区段名称和一个序列顺序号码。Unidrv驱动程序将打印作业分成六个区段，对每一个区段，Unidrv按指定的顺序发送分配于该区段的打印机命令。下面的区段都是被定义的：

JOB_SETUP

分配于JOB_SETUP区段的命令对每个作业只发送一次，它们是一个新的作业开始时的第一批发送的命令。这些命令从Unidriv的DrvStartDoc函数实现中发出的。

DOC_SETUP

分配于DOC_SETUP区段的命令是在文档的第一页发送之前发送的。命令从Unidrv的DrvStartDoc函数实现中发出。(这些命令也可以在一个应用程序调用Win32的ResetDC函数之后发送，这一区段不能删除一个下载的信息，如软字体及调色板等)。

PAGE_SETUP

分配于区段PAGE_SETUP的命令在每一页的开始、绘制开始之前被发送的。这些命令从Unidrv的DrvStartPage函数实现中发出。

PAGE_FINISH

分配于区段PAGE_FINISH的命令在每一页的末尾、绘制完成时发送，这些命令从Unidrv的DrvSendPage函数实现中发出。

DOC_FINISH

分配于DOC_FINISH区段的命令在文档的最后一页发送完之后发送。这此命令从Unidrv的DrvEndDoc函数实现中发出。(在这一区段的命令不能去除下载信息，如软字体及调色板等)

JOB_FINISH

分配于JOB_FINISH区段的命令在每一个作业中只发送一次，它们是当一个作业结束时的最后的命令。这些命令从Unidrv的DrvEndDoc函数实现中发出。

在这每一个区段中，命令由它们的序列号指示按顺序执行。

为了指定一个命令区段及序列号，用*Order属性，它是一个命令属性，其格式为：

*Order:SectionName.SequenceNumber

这里，SectionName是JOB_SETUP,DOC_SETUP,PAGE_SETUP,PAGE_FINISH,DOC_FINISH或JOB_FINISH中的一个，SequenceNumber是一个数字型值。

序列号不需要保持连惯，但每一个区段中指定的序列号必须是唯一的。每一个区段中的命令按照所有区段序列号从最小到最大的顺序执行。例如，下面的条目示意了InputBin,PaperSize及Resolution特性的选项，并被指定于DOC_SETUP区段，以指定的顺序进行发送：

*Feature:InputBin

{

*DefaultOption:Auto

*Option:Auto

{

*Name:”Auto Tray”

*Command:CmdSelect

{

*Order:DOC_SETUP.50

*Cmd:”<1B>(1<010014>”

}

}

……

}

*Feature:PaperSize

{

*DefaultOption:Letter

*Option:Letter

{

*Name:”Letter size”

*Command:CmdSelect

{

*Order:DOC_SETUP.60

*Cmd:”<1B>(g<0300>n<01>r”

}

}

……

*Feature:Resolution

{

*DefaultOption:360dpi

*Option:360dpi

{

*Name:”360dpi x 360dip”

*Command:CmdSelect

{

*Order:DOC_SETUP.70

*Cmd:”<1B>(d<020001>”

}

}

……

}

4.5打印机特性

打印机特性是可以由Unidrv驱动程序控制的能力。通过在一个GPD文件中列出一个特性及它的特征，就可以通知Unidrv驱动程序打印机支持的特性。

每一个打印机的特性可以分配给一个或多个状态(states)，打印机选项(pinter options)被用于定义可能的状态。例如，如果打印机接收所有信纸大小尺寸及合法大小尺寸的页面，GPD文件应当在选择Letter和Legal选项时并指定PaperSize的属性。

所有的特性及相关的选项是被枚举于打印机相关的属性表单或文档属性表单上。更多的关于这些属性表单的信息，可以参考Unidrv用户接口部分。

这一部分解释GPD语言支持标准特性及定制特性，本部分队附加主题还讨论了特性条目格式、特性属性及特性冲突优先级。

4.5.1标准特性

标准特性是许多打印机都提供的打印机的一般特性。它们通过预置GPD语言能识别的名称来鉴别(字符串的资源标识符表示这些名称包含于stdnames.gpd，该DDK中即提供stdnames.gpd)。一些标准特性是必需的并且必须对每个打印机都指定，另外一些则是可选的。

下表中列出所有的标准特性，以字母排序，并指明是否每一个特性接受标准的选项或定制的选项。

特性名称 描述 标准选项 注解

Collate | 页面整理 | 参考标准选项部分内容。不允许定制选项 | 可选。如果没有被指定，Unidrv不支持页面整理
ColorMode | 色彩打印模式 | 无。所有的选项都是定制选项。参考色彩模式特性的选项属性 | 可选。如果没有被指定，Unidrv将以单一平面、每像素1位的格式绘制图像
Duplex | 双倍打印 | 参考标准选项部分内容。不允许定制选项 | 可选。如果没有被指定，Unidrv只执行单面打印
Halftone | 过渡调色的能力 | 参考标准选项部分内容。不允许定制选项。也可参考选项属性中的中间色属性 | 可选，如果没有被指定，Unidrv选择一个GDI支持的过渡调色的方法。 也可参考用Unidrv过渡调色部分的内容
InputBin | 二进制输入的类型 | 参考图形驱动程序设计与实现中的标准选项部分的内容。不允许定制选项。也可参考二进制输入特性的选项属性部分 | 必需的。定制的二进制名称必需是24个字符或以下
MediaType | 打印媒体的类型 | 参考标准选项。不允许定制选项 | 可选。如果没有被指定，则一直使用打印机的默认媒体
Memory | 打印机内存配置 | 所有的选项都是定制的。参考内存特性的选项属性部分 | 可选。如果被指定，Unidrv企图追踪内存的使用。默认的*FeatureType的值是PRINTER_PROPERTY
Orientation | 纸的方向 | 参考标准选项。不允许定制选项 | 可选。如果没有被指定，默认值是竖向
OutputBin | 输出二进制的类型 | 没有标准选项。所有的选项都是定制选项。参考二进制输出特性的选项属性 | 可选。如果没有被指定，Unidrv不企图选择二进制输出
PageProtect | 允许打前打印页的页保护 | 参考标准选项。不允许定制选项 | 可选。如果没有被指定，默认值是OFF。Unidrv只在有足够的打印机内存时才进行页面保护。默认的*FeatureType值是PRINTER_PROPERTY，也可参见*PageProtectMem
PaperSize | 页面尺寸 | 参考标准选项。不允许定制选项 可以参考纸张尺寸的选项属性部分内容 | 必需的。至少一个选项必需被指定，即CUSTOMSIZE选项允许打印机用户指定纸的尺寸
RESDLL | 资源DLL | 所有的选项都是定制的。参考在小驱动程序中使用资源DLL部分的内容 | 可选。参考*ResourceDLL
Resolution | 打印精度 | 所有的选项都是定制的。参考精度特性的选项属性部分的内容 | 必需的。至少一个选项必需被指定
Stapling | 分类能力 | 所有的选项都是定制的 | 可选。如果被指定，目录服务指明打印机支持分类

相关实例，可以参考GPD实例文件。

4.5.2定制特性

定制特性是那些特定于硬件的特性。为这些特性创建了一个唯一的名称。每一个定制的特性，必须指定一套定制的选项。例如，假设打印机提供经济模式的操作。由于GPD语言不提供描述这一能力的特性，必须定义一个定制的特性及它的选项。该特性的说明也许可以如下：

*Feature:EcomoMode

{

*Name:”Economy Mode”

*FeatureYype:PRINTER_PROPERTY

*DefaultOption:EconoModeOff

*Option:EconoModeOff

{

*Name:”Off”

*Command:CmdSelect

{

*Order:DOC_SETUP.5

*Cmd:”@PJL SET ECONOMODE=OFF<0A>”

}

}

*Option:EconoModeOn

{

*Name:”On”

*Command:CmdSelect

{

*Order:DOC_SETUP.5

*Cmd:”@PJL SET ECONOMODE=ON<OA>”

}

}

}

4.5.3特性条目格式

为在一个GPD文件中指定一个打印机特性条目，用下面的格式：

*Feature:FeatureName{FeatureAttributes}

这里，FeatureName或者是预先定义的标准特性的名称或者是一个定制的特性名称，FeatureAttributes是一套特性属性。

例如，一个GPD文件也许可以包含以下标准二进制输入特性的说明。

*Feature:InputBin

{

*Name:”Paper bin”

*DefaultOption:Upper

*Option:Upper

{

*Name:”Upper Tray”

*Command:CmdSelect

{

*Order:DOC_SETUP.10

*Cmd:”<1B>&11H”

}

*Constraints:PaperSize.env10

}

*Option:Manual

{

*Name:”Manual Feed”

*Command:CmdSelect

{

*Order:DOC_SETUP.10

*Cmd:”<1B>&12H”

}

*Installable?:TRUE

}

}

如果重复一个特性说明，如包含两个或更多的二进制输入特性条目，则必须应用下面的规则：

■将没有重复的属性及选项加到Unidrv的数据库中。

■那些重复的特性及选项被覆盖，Undriv只保存最后的说明。

可以控制顺序，在这一顺序中所有的特征按序显示给用户。参考指定特性及选项显示顺序部分的内容。

4.5.4特性属性(Feature Attributes)

当指定一个打印机的特性，使用属性来给Undriv提供以下的信息：

■一个文本字符串，以表示特性的显示的名称

■与特性相关的打印机的选项集

■一个布尔值，意指一个特性是否一直处于当前状态或可以安装

■特性的类型及优先级。如果特性是定制的，意指那一个属性表单可以被显示以及它的相关的优先级。

下表列出的以字母排序的特性属性，描述了它们的参数：

属性名称 属性参数 注解

*ConcealFromUI? | TRUE或FALSE。意指是否该特性应当在用户接口中显示。对Windows 2000不支持 | 可选。如果没有被指定，默认值是FALSE，意指特性被显示。当一个特性只有一个选项(如，一种精度值)时并且不是用户可修改的，或者如特性的选项选择由设置另外一个特性的选项来控制时，其值必需是TRUE。
*ConfictPriority | 数字型值。表示特性的优先级，1是最高优先级 | 可选。参考特性冲突优先级部分
*DefaultOption | 一种特性选项名称 | 可选。如果没有被指定，列在*Feature条目中的第一个选项是默认值
*Featuretype | DOC_PROPERTY JOB_PROPERTY PRINTER_PROPERTY 如果DOC_PROPERTY或者JOB_PROPERTY，该特性分配为言语档的属性表单，如果是PRINTER_PROPERTY，则特征分配为打印机属性表单 | 对定制的选项是必需的。对标准特性可选。如果没有被指定，对标准特性的默认值是DOC_PROPERTY，除非有其他注解。如果是PRINTER_PROPERTY，特性选项的值被保存于注册表中。如果是DOC_PROPERTY或JOB_PROPERTY，特性选项值与文档一起保存。
*HelpIndex | 数字型值，表示一个到帮助文件的索引，由*HelpFile这一根层次特性来指定 | (也是一个选项特性)
*Installable? | TRUE或者FALSE，意指该特性是否能被安装(FALSE意指一直安装着) 更多的信息可以参考处理可安装特性及选项 | 可选。如果没有被指定，默认值是FALSE，如果是TRUE，所有特性的选项也是可安装的。除了第一个是被指定的。如果是FALSE，必须至小有一个特性的选项必需一直被安装(也是一个可选特性)
*InstallableFeatureName | 文本字符串，用于显示询问用户是否是一个可安装的特性是否是实际安装的特性。 更多的信息可以参考处理可安装特性及选项 | 如果*Installable?是TRUE并且*rcInstallableFeatureNameID没有被指定(也是一个选项特性)
*Name | 文本字符串，用于在打印机的属性表单上显示该特性名称 | 可选。如果没有被指定，*rcNameID必须被指定(也是一个可选特性)
*Option | 可选参数，如在选项条目格式中所描述的 | 必需的。对每一个特性相关的选项都用一个*Option条目
*rcIconID | 与一个特性相关的图标资源的资源ID | 可选。如果没有被指定，Unidrv在打印机属性表单上不显示一个特性相关的图标。(也是一个可选特性)
*rcInstallableFeatureNameID | 一个文本字符串的资源ID，用于显示询问用户是否是一个可安装的特性是否是实际安装的特性。 更多的信息可以参考处理可安装特性及选项 | 如果*Installable?是TRUE并且*nstallableFeatureName没有被指定时是必需的。(也是一个可选特性)
*rcNameID | 一个字符串资源的ID，表示特性的名称(0是一个不有效的资源ID) | 可选。如果没有被指定，则*Name必须被指定(也是一个可选特性)
*UpdateQualityMacro? | TRUE或者FALSE，指明特性是否包含于一个指定质量设置的条件语句中(参考控制图像质量部分内容) | 可选。如果没有被指定，默认值是FALSE。(如果特性包含于一个指定质量设置的条件语句中时，该值必须是TRUE)

相关实例，可以参考GPD实例文件。

4.5.5特性冲突优先级

一个特性冲突优先级标识了一个特性在Undriv的用户接口代码实施选项约束时应当有的优先级。

GPD解析器给按下面的顺序给特性分配一个冲突优先级，从最高优先级到最低优先级：

1．实际安装着的可可安装特性。(参考处理可安装特性及选项)

2．具有*FeatureType的特性设置为PRINTER_PROPERTY。

3．具有*FeatureType的特性设置为DOC_PROPERTY或JOB_PROPERTY。

在每一个特性类型中的属性被分配给一个相关的优先级，该优先级是基于指定在特性的*ConfilictPriority属性的值。这样，例如，一个PRINTER_PROPERTY特性具有值为1的*ConfictPriority属性优先值高于一个具有DOC_PROPERTY特性具有值为3的*ConflictPriority的属性。没有*ConfilictPriority属性的特性具有比有*Confilictpriority属性更低的优先级。

更多的关于*FeatureType和*ConflictPriority属性的信息，可以参考特性属性部分的内容。

4.6打印机选项

打印机选项表示了可以分配给打印机特性的状态。例如，如果一个打印机可以用信纸大小的尺寸和法定尺寸大小的尺寸的纸张，则LETTER及LEGAL被指定为一个纸张尺寸特性的选项。

本部分讨论GPD语言支持的所有标准选项及定制选项。

本部分附加的主题讨论了选项条目格式，选项属性及选项约束等。

4.6.1标准选项

标准选项是那些与标准特性相关的选项。它们被GPD语言所识别的预定义的名称所鉴别。(这些用字符串表示的资源标识符的名称被包含于stdnames.gpd中，在DDK也提供)。

下表列出的标准选项名称对每一个标准的特性都是允许的。这些名称被作为参数用于*Options条目中。

特性名称 标准选项名称 是否允许定制选项

Collate

ON,OFF

不

ColorMode

没有标准选项

是，参考处理色彩格式及控制图像质量部分内容

Duplex

HORIZONTAL,VERTICAL,NONE

不

Halftone

HP_PATSIZE_2x2

HP_PATSIZE_2x2_M

HP_PATSIZE_4x4

HP_PATSIZE_4x4_M

HP_PATSIZE_6x6

HP_PATSIZE_6x6_M

HP_PATSIZE_8x8

HP_PATSIZE_8x8_M

HP_PATSIZE_10x10

HP_PATSIZE_10x10_M

HP_PATSIZE_12x12

HP_PATSIZE_12x12_M

HP_PATSIZE_14x14

HP_PATSIZE_14x14_M

HP_PATSIZE_16x16

HP_PATSIZE_16x16_M

HP_PATSIZE_SUPERCELL

HP_PATSIZE_SUPERCELL_M

HP_PATSIZE_ATUO

是，参考用Unidrv过渡调色部分

InputBin

AUTO

CASSETTE

ENVFEED

ENVMANUAL

FORMSOURCE

LARGECAPACITY

LARGEFMT

LOWER

MANUAL

MIDDLE

SMALLFMT

TRACTOR

UPPER

是

MediaType

GLOSSY

STANDARD

TRANSPARENCY

是，参考控制图像质量部分

Memory

无标准选项

是

Orientation

PORTRAIT，LANDSCAPE_CC90

LANDSCAPE_CC270

不

PageProtect

ON,OFF

不

PaperSize

10X11

10X14

11X17

12X11

15X11

9X11

A_PLUS

A2

A3

A3_EXTRA

A3_ExTRA_TRANSVERSE

A3_ROTATED

A4

A4_EXTRA

A4_PLUS

A4_ROTATED

A4_TRANSVERSE

A4SMALL

A5

A5_EXTRA

A5_ROTATED

A5_TRANSVERSE

A6

A6_TOTATED

B_PLUS

B4

B4_JIS-ROTATED

B5

B5_EXTRA

B5_JIS_ROTATED

B5_TRANSVERSE

B6_JIS

B6_JIS_ROTATED

CSHEET

CUSTOMSIZE

DBL_JAPANESE_POSTCARD

DBL_JAPANESE_POSTCARD_ROTATED

DSHEET

ENV_10

ENV_11

ENV_12

ENV_14

ENV_9

ENV_B4

ENV_B5

ENV_B6

ENV_C3

ENV_C4

ENV_C5

ENV_C6

ENV_C65

ENV_DL

ENV_INVITE

ENV_ITALY

ENV_MONARCH

ENV_PERSONAL

ESHEET

EXECUTIVE

FANFOLD_LGL_GERMAN

FANFOLD_SD_GERMAN

FANFOLD_US

FOLIO

ISO_B4

JAPANESE_POSTCARD

JAPANESE_POSTCARD_ROTATED

JENV_CHOU3

JENV_CHOU3_ROTATED

JENV_CHOU4

JENV_CHOU4_ROTATED

JENV_KAKU2

JENV_KAKU2_ROTATED

JENV_KAKU3

JENV_KAKU3_ROTATED

JENV_YOU4

NENV_YOU4_ROTATED

LEDGER

LEGAL

LEGAL_EXTRA

LETTER_EXTRA_TRANSVERSE

LETTER_PLUS

LETTER_ROTATED

LETTER_TRANSVERSE

LETTERSMALL

NOTE

P16K

P16K_ROTATED

P32K

P32K_ROTATED

P32KBIG

P32KBIG_ROTATED

PENV_1

PENV_1_ROTATED

PENV_10_ROTATED

PENV_2

PENV_2_ROTATED

PENV3

PENV_3_ROTATED

PENV_4

PENV_4_ROTATED

PENV_5

PENV_5_ROTATED

PENV_6

PENV_6_ROTATED

PENV_7

PENV_7_ROTATED

PENV_8

PENV_8_ROTATED

PENV_9

PENV_9_ROTATED

QUARTO

STATEMENT

TABLOID

TABLOID_EXTRA

是，定制的名称不能超过在wingdi.h中由CCHFORMNAME指定的长度

Resolution

没有标准选项

是

4.6.2定制选项

定制选项是那些没有被GPD语言中的预定义名称定义的选项。必须为这些选项选择唯一的名称。

定制选项可以与定制的特性关联。与定制特性相关的定制选项的实例，可以参考定制特性部分的内容。

也可以为一些标准特性指定定制选项。如，若打印机提供一个没有由纸张尺寸特性的标准选项描述的纸张尺寸，可以通过为该选项创建一个唯一的名称来指定一个定制的纸张尺寸选项。

定制选项名称必须在特性中是唯一的，但选项名称可以被在不同特性中重用。也可以在定制特性中用标准的选项名称。这样，例如，可以在几个*Feature条目中为定制特性使用标准的选项名称ON及OFF。

4.6.3选项条目格式

为在一个GPD文件中指定一个打印机选项条目，可以使用如下格式：

*Option:OptionName{OptionAttributes}

这里，OptionName是一个预定义标准选项的名称或者是一个定制选项的名称，OptionAttributes是一套选项属性。

与特性收相关的一套*Option条目的实例，可以参考特性条目格式部分的内容。

如果重复一个选项说明，例如通过给纸张尺寸特性的LETTER选项指定两个或更多的*Option条目，在*Options条目中每个重复的选项属性覆盖前边一个，并且GPD解析器只保留最后一次遇到的。另外一方面，如果GPD解析器遇到两上或更多的对纸张尺寸(或其他特性)的*Feature条目，*Option条目以前没有指定的将被增加到解析器数据库中。

可以控制这些选项显示给用户的顺序。参考指定特性及选项显示的顺序。

4.6.4选项属性

当指定打印机选项时，可以使用属性给Unidrv提供以下信息：

■文本字条符串表示一个选项显示的名称、用户提供及帮助信息等。

■一个布尔值意指一个选项是否总是处于当前状态或可以被安装

■能引起选项被选择的打印机命令

■不同类型的特定选项的信息

这一部分包含以下主题：

■4.6.4.1所有特性的选项属性

■4.6.4.2色彩模式特性的选项属性

■4.6.4.3过渡调色特性的选项属性

■4.6.4.4二进制输入特性的选项属性

■4.6.4.5内存特性的选项属性

■4.6.4.6二进制输出特性的选项属性

■4.6.4.7纸张尺寸特性的选项属性

■4.6.4.8精度特性的选项属性

4.6.4.1所有特性的选项属性

下表中，以字母顺序，列出了所有特性可用的选项属性，并描述了它们的参数。

属性名称 属性参数 注解

*Command | 一个cmdSelect的选项选择命令，指定了一个必须发往打印机以选择不同选项的命令字符串 | 必需的。
*DisabledFeatures | 枚举特性名称字符串时，鉴别那些那些已经被选择而被禁止的特性 | 可选。枚举的特性不能有*Installable?设置为TRUE。更多的信息，参考处理可安装特性及选项部分的内容
*HelpIndex | 数字型值，表示一个由根层次特性的*HelpFile指定的对帮助文件的索引， | (也是一个特性属性) 索引值不能是0或-1。
*Installable | TRUE或FALSE。意指是否该选项是可安装的。(FALSE意指一直安装着)更多的信息，可以参考处理可安装的特性和选项部分 | 可选。如果没有被指定，默认值是FALSE。(也是一个特性属性)
*InstallableFeatureName | 文本字符串，用于显示用询问用户是否一个可安装的特性是实际安装着的信息。可参考处理可安装特性和选项部分 | 必需的。如果*Installable?是TRUE并且*rcInstallableFeatureNameID没有被指定(也是一个特性属性)
*Name | 文本字符串，用于选项在打印机属性表单上的选项显示名称 | 可选。如果没有被指定，则*rcNameID必须被指定(也是一个特性属性)
*OptionID | 数字型值，表示一个Unidrv 存储于打印机的DEVMODE结构中的单一选项标识符，只与纸张PaperSizes,InputSlot,Halftoning,MediaType,Resolution等特性一起使用。其值分别是存储于DEVMODE结构中的dmPaperSize,dmDefaultSource,dmDitherType或dmMediaType成员。 | 可选。如果没有被指定，Unidrv分配一个标识符值(>256)以避免与Unidrv-分配的标识符的冲突，指定的值必须大于512
*PromptTime | UISETUP或PRTSTARTDOC，指明何时Unidrv应当显示由*rcPromptMsgID指定的提示信息。如果是UISETUP，显示发生于用户在打印机属性表单中选择了选项之后；如果是PRTSTARTDOC，显示发生在打印作业开始时。 | 如果*rcPromptMsgID被指定，如PRTSTARTDOC为多个选项指定，提示的信息在一个具有OK按键的对话框中显示。
*rcIconID | 与一个选项有关的图标资源的资源ID | 可选。如果没有被指定，Unidrv不在打印机属性表单上显示该选项的图标(也是一个特性属性)
*rcInstallableFeatureNameID | 一个文本字符串的资源ID，用于显示询问用户是否一个可安装的特性是实际安装着的信息。可参考处理可安装特性和选项部分。更多信息，可以参考处理可安装特性及选项部分的内容 | 如果*Installable?是TRUE并且*InstallableFeatureName没有被指定时，是必需的。 (也是一个特性属性)
*rcNameID | 一个字符串资源的ID，表示选项的名称 | 可选。如果没有被指定，则*Name必须被指定。(也是一个特性属性) 只有对纸张尺寸特性的标准选项，设置这一特性为RCID_DMPAPER_SYSTEM_NAME会引起Unidrv用一个预选定义好的选项名称字符串
*rcPromptMsgID | 字符串资源的资源ID，表示一个用户的提示信息 | 对二进制输入(InputBin)特性的MANUAL选项，指定一个消息，Unidrv可以显示请求更多的纸张。否则，可选的，Unidrv显示消息在一个有OK按键的对话框中。当Unidrv显示消息时，*rcPromptTime属性进行控制

相关实例，可以参考GPD实例文件。

4.6.4.2色彩模式特性的选项属性

下表列出的属性都与色彩模式(ColorMode)特性相关，更多的关于色彩模式的信息，可以参考标准特性部分。

属性名称 属性参数 注解

*Color | TRUE或FALSE。意指选项是否产生色彩 | 可选。如果没有被指定，对*DrvBPP>1时，默认值是TRUE。为创建灰色的缩放比例，其置设置成FALSE，且*DrvBPP>1。
*ColorPlaneOrder | 列表，意指Unidrv应当发送平面色彩数据的顺序。如： LIST(YELLOW,MAGENTA,CYAN,BLACK)，LIST(RED，GREEN，BLUE)，在列表中色彩可以重复。 | 在以下条件时必需，即如果*DevNumOfPlanes大于1，指定的色彩的数量必须等于*DevNumOfPlanes
*DevBPP | 数字型值，意指打印机支持每个像素的色彩数据的位数。 | 可选。如果没有被指定，默认值是1。
*DevNumOfPlanes | 数字型值，意指打印机支持的色彩平面的数量 | 可选。如果没有被指定，默认值是1(对彩色打印机来说，值为1也指像素格式)
*DrvBPP | 数字型值，意指Unidrv在给缓冲区绘制位图时应当使用的像素格式的位数。位图的格式是Windows DIB，有效的值是1,4,8,16,24或32 | 可选。如果没有被指定，默认值是1(对彩色打印机来说，值等于1指的是平面模式planar mode) Windows DIB总是使用一个色彩平面
*IPCallbackID | 正数字型值，被以IPrintOemUni::ImageProcessing的参数IPCallbackID传递给绘制插件的IPrintOemUni::ImageProcessing方法， | 如果提供的绘制插件包含IPrintOemUni::ImageProcessing方法时是必需的
*PaletteProgrammable? | TRUE或FALSE。意指色彩的调色板是否是可编程的 | 可选。如果没有被指定，默认值是FALSE。
*PaletteSize | 数字型值，表示在色彩调色板中应用的指定选项条目的数量 | 可选。如果没有被指定，默认值是2
*RasterMode | DIRECT或INDEXED，意指是否光栅数据被直接发送到打印机或通过一个色彩模板建索引 | 可选。如果没有被指定，默认值是INDEXED

相关实例，可以参考GPD实例文件。

关于附加的选项属性，可参考所有特性的选项部分的内容。也可以参考控制图象质量部分。

4.6.4.3过渡调色特性的选项属性

下表列出的属性与过渡调色特性相关，更多的关于过渡调色特性的信息，参考标准特性部分。

属性名称 属性参数 注解

*HTCallbackID | 正数字型值，作为IprintOemUni::HalftonePattern方法的dwCallbackID参数被传递给IprintOemUni::HalftonePattern | 如果提供了IprintOemUni::HaftonePattern方法时是必需的，可以参考用Unidrv过渡调色部分的内容
*HTNumPatterns | 数字型值，表示过渡调色模板提供的数量 可以参考用Unidrv过渡调色部分的内容 | 可选。可以是1或3，如果是3，意指对红、绿及蓝色按顺序采用各自独立的调色板。如果没有被指定，默认值是1，可以和*rcHTPatternID或*HTCallbackID一起使用
*HTPatternSize | 一对(PAIR)数字型值，表示以像素格式的调色板的宽度和高度，由*rcHTPatternID来指定 | 如果*rcHTPatternID被指定时是必需的，最大的调色板尺寸是PARI(256,256)，宽与高的乘积，必须能被4整除，作为一个DWORD型值存储。
*rcHTPatternID | RC_HTPATTERN资源标识符，表示过渡调色调色板的数据 | 如果一个过渡调色的调色板在一个资源DLL中提供。可以参考用Unidrv过渡调色

相关实例，可以参考GPD实例文件。

更多的关于使用这些属性的信息，可参考用Unidrv过渡调色部分。这些属性不能与小驱动程序提供的过渡调色一起使用。

4.6.4.4二进制输入特性的选项属性

没有与二进制输入特性相关的属性。更多的关于二进制输入的信息，可参考标准特性部分。

另外的关于选项属性的信息，参考所有特性的选项属性部分的内容。

4.6.4.5内存特性的选项属性

下表列出的属性与内存特性相关，更多的关于内存特性的信息，参考标准特性部分。

属性名称 属性参数 注解

*MemConfigKB | 一对(PAIR)数字型值，意指总的及打印机驻留的可用的内存，以千字节(Kilobytes)为单位。例如，PAIR(1024,450)意指总数为1024KB，450KB可用，具有一个GPD产生的选项名称“1024KB” | 可选。参考描述打印机内存配置
*MemConfigMB | 一对(PAIR)数字型值，意指总的及打印机驻留的可用的内存，以MB(megabytes)为单位。如，PAIR(2,1)意指总共2Mb，1Mb可用，具有GPD产生的选项名称“2MB” | 可选。参考描述打印机内存配置
*MemoryConfigKB | 一对(PAIR)数字型值，意指总的及打印机驻留的可用的内存，以千字节(Kilobytes)为单位。例如，PAIR(1024,450)意指总数为1024KB，450KB可用。 | 可选。参考描述打印机内存配置部分

相关实例，可以参考GPD实例文件。

更多的关于使用这些属性的信息及一些实例，可以参考描述打印机内存配置部分的内容。

关于附加的选项属性，参考所有特性的选项部分内容。

4.6.4.6二进制输出特性的选项属性

尽管对一个专用的输出二进制来说，包含*OutputOrderReversed?条目不是一般的情况，但没有与二进制输出特性相关的属性。更多的关于二进制特性的信息，参考标准特性部分。

关于附加的选项属性，参考所有特性的选项属性部分的内容。

4.6.4.7纸张尺寸特性的选项属性

下表列出了与纸张尺寸特性相关的属性，更多的关于纸张尺寸特性的信息，参考标准特性部分。

注意：所有的对下边的属性的纸张尺寸说明，都必须按竖向方向来表示，即使一些属性被用于描述不同的方向，如横向。

属性名称 属性参数 注解

*BottomMargin | 数字型值，表示允许的最小的底端页边值，以x基本单位，对用户指定的纸张尺寸相关的CUSTOMSIZE选项值，其值与底端的物理页面相关 | 可选，如果没有被指定，默认值是0。只与CUSTOMSIZE选项一起使用，假定为竖向方向，参考指定纸张尺寸部分的内容
*CenterPrintable? | TRUE或FALSE，意指由*MaxPrintableWidth指定的值在中间位置 | 可选。如果没有被指定，可打印区域在由*MinLeftMargin指定的右边位置。只与CUSTOMSIZE选项一起使用，参考指定纸张尺寸部分的内容
*CursorOrigin | 一对数字型值，表示以基本单位的光标原点，这时PAIR(0,0)是左上角值，对CUSTOMSIZE选项是可以变化的，用*CustCursorOriginX和*CustCursorOriginY指定这些值 | 可选。如果没有被指定，默认值是PAIR(0,0)。Unidrv假定光标原点，相对于打印机，对不同的纸张尺寸是一个常量
*CustCursorOriginX | CUSTOMSIZE参数表达式，用于创建*CursorOrigin的x轴索引的值 | 可选，只在CUSTOMSIZE选项时使用，参考指定纸张尺寸
*CustcursorOriginY | CUSTOMSIZE参数表达式，用于创建*CursorOrigin的y轴索引的值 | 可选，只在CUSTOMSIZE选项时使用，参考指定纸张尺寸部分的内容
*CustPrintableOriginX | CUSTOMSIZE参数表达式，用于创建*PrintableOrigin的x轴索引的值 | 可选，只在CUSTOMSIZE选项时使用，参考指定纸张尺寸部分的内容
*CustPrintableOriginY | CUSTOMSIZE参数表达式，用于创建*PrintalbeOrigin的y轴索引的值 | 可选，只在CUSTOMSIZE选项时使用，参考指定纸张尺寸部分的内容
*CustPrintableSizeX | CUSTOMSIZE参数表达式，用于创建*PrintableArea的x值的一个值 | 可选，只在CUSTOMSIZE选项时使用，参考指定纸张尺寸部分的内容
*CustPrintableSizeY | CUSTOMSIZE参数表达式，用于创建*PrintableArea的y值的一个值 | 可选，只在CUSTOMSIZE选项时使用，参考指定纸张尺寸部分的内容
*MaxSize | 一对数字型值，表示可允许的最大的页面长度(x)和高度(y)值，对用户专用的纸张尺寸的CUSTOMSIZE选项，用基本单位 | 对CUSTOMSIZE选项是必需的，假定为竖向方向。参考指定纸张尺寸部分部分的内容
*MaxPrintableWidth | 数字型值，表示最大的可打印宽度，对用户专用的纸张尺寸的CUSTOMSIZE选项，用基本单位用x轴基本单位 | 对CUSTOMSIZE选项是必需的，假定为竖向方向。参考指定纸张尺寸部分的内容
*MinLeftMargin | 数字型值，表示允许的最小的左边页边，对用户指定的纸张尺寸相关的CUSTOMSIZE选项用x基本单位，其值与左边的物理页面相关 | 可选。如果没有被指定，默认值是0，只与CUSTOMSIZE选项时使用，假定为PORTAIT方向。参考指定纸张尺寸部分的内容
*MinSize | 一对数字型值，表示允许的最小的页面长度(x)和宽度(y)值，对用户指定纸张尺寸与CUSTOMSIZE相关的选项用标准单 | 对CUTOMESIZE选项是必需的，假定为竖向方向。参考指定纸张尺寸部分的内容
*PageDimensions | 一对数字型值，表示页面的长度(x)及高度(y)值，对任何关于纸张尺寸的定制选项都用基本单位 | 只用于厂商定义的纸张尺寸。参考指定纸张尺寸部分的内容
*PageProtectMem | 数字型值，表示打印机内存的总量，以KB为单位，保护页面时是必需的 | 如果指定了页面保护特性时是必需的，参考指定纸张尺寸部分的内容
*PrintableArea | 一对数字型值，表示x及y平面的长度的可打印页面的长度，用基本单位。 | 对除CUSTOMSIZE之外的所有纸张尺选项都必需。参考指定页面尺寸部分内容
*printableOrigin | 一对数字型值，表示可打印区域的原点，相对于纸张的左上角，用基本单位 | 对除CUSTOMSIZE之外的所有纸张尺选项都必需。可以用*CustPrintableOriginX及*CustPrintableOriginY来指定这些值
*RotateSize? | TRUE或者FALSE，指明是否因为纸张(通常是信封纸张)是单面送纸方式，因此Unidrv应当旋转页面的维数 | 可选的。如果没有被指定，默认值是FALSE。可以和纸张尺寸特性的任何标准选项一起使用，除过CUSTOMESIZE
*TopMargin | 数字型值，表示允许的最小的页面顶端页边，对特定用户的与纸张尺寸相关的CUSTOMESIZE用y轴基本单位。其值相对于顶端的物理页边 | 可选。如果没有被指定，默认值为零，只与CUSTOMESIZE选项一起使用，假定为竖直方向打印，可以参考指定纸张尺寸部分的内容。

相关实例，可以参考GPD实例文件。

CUSTOMSIZE参数表达式

CUSTOMESIZE参数表达式是一个命令字符串的约束格式。文本字符串是不被允许的。

在表达式的ArgumentType区段，下边的约束应当被应用：

■唯一允许的ArgumentType值是%d。

■括在一起的值区间是不被允许。

在表达式的StandardVariableExpression区段，下边的约束被应用：

■只有PhysPaperWidth及PhysPaperLength标准变量可以被使用。

■Max_Repeat操作符不被允许。

下面是一些表达式的实例：

*CustCursorOgiginX:%d{((PhysPaperWidth-14040)/2)+300}

*CustCursorOriginY:%d{180}

*CustPrintableOriginX:%d{300}

*CustPrintableOriginY:%d{300}

*CustPrintableSizeX:%d{PhysPaperWidth-600}

*CustPrintableSizeY:%d{PhysPaperLength-600}

4.6.4.8精度特性的选项属性

下表列出的与精度特性相关的属性。更多的关于精度特性的信息，请参考标准特性部分的内容。

属性名称 属性参数 注解

*DPI | 一对数字型值，设置了打印机精度的x及y值，以每英寸所含的点数为单位 | 必需的。X和y的值必须等于*TextDPI中的x和y值，或者它们必须等于*TextDPI的x值和y值分别二等分相除的结果。例如，如果*TextDPI是PAIR(300,300)，则*DPI的值可以是PAIR(300,300),PAIR(150,150)或者PAIR(75,75)，但是不能为PAIR(100,100)
*MinStripBlankPiexels | 数字型值，表示Unidrv在剥离所有的空白字符之前，在一个扫描行中应当遇到的最小数字的空白字节数 | 可选。如果没有被指定，默认值是0。该属性只在*StripBlanks条目指定为ENCLOSED时相关。参考光栅数据发送特性部分
*PinsPerLogPass | 数字型值，表示一个逻辑传送的打印头中包含的最小的打印行数。必须是多个*PinsPerPhysPass，因为每一个逻辑传送是由一个或多个物理传送构成的 | 可选。如果没有被指定，默认值是1。如果打印机交替执行时是必需的。需要在跨一系列行时多个打印头的传递，以打印所有的扫描行。
*PinsPerPhysPass | 数字型值，表示打印头在页面上移动时被打印的扫描行的数量。必须是1或8的倍数 | 可选。如果没有被指定，默认值是1
*RequireUniDir? | TRUE或FALSE，指明是否指定的精度需要单一方向的打印被允许 | 可选。如果没有被指定，默认值是FALSE。
*SpotDiameter | 数字型值，表示一个点的直径大小，作为一个像素大小的百分比，由*DPI为精度指定的 | 必需的。如100，指点的直径等于像素大小，200，指点的直径是像素大小的两倍，50，指点的直径是像素大小的四倍
*TextDPI | 对值(PAIR)或数字型值，表示打印机的文本精度的x及y值，以每英寸的点数(DPI)为单位 | 必需。参考*DPI注解。该精度用于绘制字体和向量图形

相关实例，可以参考GPD实例文件。

关于附加选项的属性，参考所有特性的选项属性部分。也可参考控制图象质量部分。

4.6.5选项限制

有些时候，需要限制用户安装或选择与打印机特性相关的某些打印机选项的能力。可以指定以下类型的选项限制：

■可以限制一个选项，这样，如果一个冲突选项已经被选择，它自身就不能再被选择。这些限制叫做选择限制。

■可以限制一个可安装的选项，这样，如果一个冲突的选项已经被安装，则它自身就不能再被安装。这些限制叫做安装限制。

■可以限制一个选项，这样，如果一个冲突的可安装的选项已经被安装，则它自身就不能再被选择，或者如果必需的一个可安装选项没有被安装，自身也不能被选择。这些限制称为选择与安装之间的限制。

驱动程序的用户接口代码在用户选择了打印机属性表单的对话框上的OK键时实施限制。如果选择了不有效的选项组合，用户便给了这样一个选择，即或者是通过修改属情表单页面来改正错误，或者是让驱动程序基于特性冲突优先级自动执行错误改正。

4.6.5.1选择限制

经常，对不同打印机特征的一些选项不能同时被选择。例如，如果选择了一个信封送纸器，则非信封尺寸的纸张，如信纸尺寸或A4尺寸的纸张就不能被选择。

为指定不能同时被选择的打印机选项的组合，可以用*InvalidCombination或*Constraint条目。如果一个用户企图选择已指定为无效的选项组合，Unidrv将会拒绝这种选择。

*InvalidCombination条目有下面的格式：

*InvalidCombination:LIST(FeatureName.OptionName,FeatureName.OptionName,……)

这里，FeatureName是一个特性的名称，OptionName是与特性相关的一个选项的名称。

在单一*InvalidCombination条目中列出的选项指明一套不能用于组合的选项。例如，下面的条目指定CMYK色彩模式不能与普通纸张及720DPI一起使用。

*InvalidCombination:LIST(Resolution.720dpi,MediaType.Plain,ColorMode.CMYK)

所有的*InvalidCombination条目必须被置于GPD文件的根目录位置(即，不在括号内)。在一个条目中包括的选项数则是不限制的。

如果只是想指明两个选项之间的非法关系组合，可以用一个*Constraints条目。它的格式是：

*Constraints:FeaturneName.OptionName

这里，FeatureName是一个特性的名称而OptionName是与一个特性相关的选项名称。一个*Constraints条目必须置于*Option条目之中。例如，为了指明信纸大小及A4大小的纸张不能使用信封送纸器，可以用下列的条目：

*Feature:InputBin

{

*Option:ENVFEED

{

*Constraints:PaperSize.Letter

*Constraints:PaperSize.A4

}

}

或者，等同于：

*Feature:InpubBin

{

*Option:ENVFEED

{

*Constraints:LIST(PaperSize.Letter,PaperSize.A4)

}

}

这些实例指定如一个用户想选择一个信封送纸器及信封信纸大小的纸张或者信封送纸器A4大小的纸张，Unidrv将拒绝这些选择。

4.6.5.2安装限制

有些时候，一些专用的可安装的打印机选项不能被同时安装。例如，也不能同时安装信封送纸器及加倍单元。

为了指定可以同步被安装的打印机选项的组合，要使用*InvalidInstallableCombination条目。

*InvalisdInstallableCombination条目有下面这样的格式：

*InvalidInstallableCombinations:LIST(FeatureName.OptionName,FeatureName.OptionName,……)

这里，FeatureName是一个特性的名称而OptionName是与特性相关的一个选项的名称。这一个列表也可以包含特性及选项，这种情况时，就不能包含句号及选项名称。

在单一*InvalidInstallableCombination条目中列出的特性及选项表示一套可以用于组合的特性及选项。例如，下面的条目指定一个信封送纸器不能和双倍单元同时安装。

*InvalidInstallableCombination:LIST(InputBin,ENVFEED,Duplex)

所有的*InvalidInstallationCombination条目必须放置于GPD文件的根目录(即，不放置于括号中)，包含于条目中的特性及选项的数目则没有限制。

如果一个特性或选项被包含于*InvalidInstallationCombination条目，则该特性或选项的*Installable?属性必须被设置为TRUE。

4.6.5.3选择及安装之间的限制

有时，有必要指定一定的选项在有其他选项安装时不能被选择，或者一定的选项在其他选项没有安装时不能被选择。例如，如果一个打印机没有安装大纸张送纸托架格式，则一个用户就不能选择小报式纸张。

为指定选择与一些具有其他选项安装状态信息的选项之间的关系，需要应用*InstalledConstraints及*NotInstalledConstraints条目，他们的格式如下：

*InstalledConstraints:FeatureName.OptionName

*NotInstalledConstraints:FeatureName.OptionName

这里，FatureName是一个特性的名称，OptionName是与该特性相关的一个选项的名称，如果参数是一个特性，则不能包括句点及选项名称。

一个*InstalledConstraints或*NotInstalledConstraints条目必须被置于一个*Feature或*Option条目中。例如，为指明或一个打印机没有安装大纸张送纸托架格式，则一个用户就不能选择小报式纸张，可以用下面的条目：

*Feature:InputBin

{

*Option:LARGEFMT

{

Installable?:TRUE

NotInstalledConstraints:PaperSize.TABLOID

}

}

如果一个特性或选项包括*InstalledConstraints或者*NotInstalledConstraints条目，该特性或选项的*Installable?属性必须被设置为TRUE。

4.7打印机字体描述

GPD语言提供一些条目，这些条目描述硬件驻留字体、Cartridge字体(Cartridge Font)、TrueType字体替换为相似的硬件驻留字体或Cartridge字体。

可以选择性地提供定制代码来处理软字体。关于这些定制代码的信息，参考第7章定制的字体管理中的定制微软打印机驱动程序部分的内容。

4.7.1硬件驻留字体

如果打印机包含硬件驻留字体，必须在.ufm或.ifi文件中提供这些字体规格的说明。为创建一个.ufm或.ifi文件，使用微软小驱动程序开发工具。

每一个硬件驻留字体都在一个单独的.ufm或.ifi文件中描述，为使这些文件对Unidrv可用，需要下面这样去做：

■在打印机的资源DLL中，通过使用RC_UFM资源类型指定.ufm文件，并在.ifi文件中用RC_FONT资源类型来指定

■在打印机的GPD文件中，用*ResourceDLL特性来指定资源DLL的名称

■在打印机的GPD文件中，用*DeviceFonts条目来指定与在资源DLL中的RC_UFM及RC_FONT条目相关的资源标识符

*DeviceFonts条目的格式如下：

*DeviceFonts:LIST(FontResourceID,FontResourceID,……)

这里，FontResourceID是与一个.ufm文件相关的RC_UFM资源标识符，或者与.ifi文件相关的RC_FONT资源标识符。

下面是一个实例：

*% Assume that RC_Font_xxx id’s are references to

*% value macros defined by the GPD file creator.

*DeviceFonts:LIST(=RC_FONT_COURIER10, =RC_FONT_ARIALR,

+ =RC_FONT_ARIALI, =RC_FONT_ARIALB,

+ =RC_FONT_ARIALBI, =RC_FONT_TIMESNRR,

+ =RC_FONT_TIMESNRI, =RC_FONT_TIMESNRB,

+ =RC_FONT_TIMESNRBI)

可以在一个Unidrv小驱动程序中包括几个*DeviceFonts条目。GPD解析器连接多个条目并使所列出的字体对打印机特性的所有配置都可用。如果需要指定一些字体只对一定的配置可用，可以在条件语句中使用*DeviceFonts条目。

4.7.2Cartridge字体

如果打印机接受Cartridge字体，这些字体可以被*FontCartridge 这一GPD文件条目描述。这一条目格式是：

*FontCartridge:CartridgeName{FontCartidgeAttributes}

这里，Cartridgename是一个文本字符串，表示字体的名称，而FontCartridgeAttributes则是一个或多个Cartridge字体属性的集合。

可以选择地，由Cartridge字体提供的字体可以使用Undirv字体格式文件(.uff文件)来指定。通常，最常见的由Cartridge字体提供的都是在GPD文件中描述的，而较少的不太常用的字体则用.uff文件指定。

4.7.2.1Cartridge字体属性

下表包括有可以包含于*FontCartridge GPD文件包目的属性。

字体名称 属性参数 注解

*CartridgeName | 文本字符串，表示Cartridge字体的名称 | 必需的。除非指定了*rcCartridgeNameID
*Fonts | RC_UFM及RC_FONT资源标识符的列表，表示包含于一个Cartridge中的可以用于横向方向的字体 | 至少一个*Fonts,*PortraitFonts或者*LandscapeFonts必需被指定。字体资源标识符应当从1开始连续计数
*LandscapeFonts | RC_UFM及RC_FONT资源标识符的列表，表示包含于一个Cartridge中的可以用于横向的字体 | 至少一个*Fonts,*PortraitFonts或者*LandscapeFonts必须被指定。字体资源标识符应当从1开始连续计数
*PortraitFonts | RC_UFM及RC_FONT资源标识符的列表，表示包含于一个Cartridge中的可以用于竖向方向的字体 | 至少一个*Fonts,*PortraitFonts或者*LandscapeFonts必须被指定。字体资源标识符应当从1开始连续计数
*rcCartridgeNameID | 一个字符串资源的资源标识符，表示Cartridge字体的名称 | 必需的。除非*CartridgeName被指定

4.7.3字体替换

对提供硬件驻留字体或Cartridge字体的打印机来说，可以指定一个字体替换表。通过提供一个字体替换表，可以指定硬件驻留或Cartridge字体来替换那些必须被下载的TrueType字体。当Unidrv收到以TrueType字体的文本，它首先检查替换表是否包含用硬件驻留字体来替换该TrurType字体，如果Unidrv发现一个可替换的硬件驻留字体，并且该字体的规格(如字符集、深浅、斜体、方向等等)是兼容的，则驻留字体将被使用。

可以通过使用一系列的*TTFS条目来创建一个默认的替换表，其中每一个条目的格式为：

TTFS:FontName

{

*TTFontName:”TTFontNameString”

DevFontName:”DeviceFontNameString”

}

这时，FontName是一个指定条目名称的符号。TTFontNameString是一个文本字符串标识将要被替换的TrueType字体，DeviceFontNameString是一个标识将要用到的硬件驻留字体或Cartridge字体的文本字符串。以下是一个实例：

*TTFS:Arial

{

*TTFontName:”Arial”

*DevFontName:”Arial”

}

*TTFS:TNR

{

*TTFontName:”Times New Roman”

*DevFontName:”Times New Roman”

}

*TTFS:Curriernew

{

*TTFontName:”Courier New”

*DevFontName:”Courier New”

}

如果有重复的具有相同FontName值的*TTFS条目，解析器后边读到的一个将取代前一个。

指定的替换表是一个默认表，因为Unidrv允许用户修改该替换。

所有的*TTFS条目必须置于GPD文件的根目录层次(即不放在括号里)。为了控制是否默认状态允许字体替换，可以用*TTFSEnabled?条目。这一条目的格式为：

*TTFSEnabled?:BooleanValue

这里，BooleanValue是TRUE或FALSE。如果BooleanValue是TRUE，Unidrv允许字体替换。如果是FALSE，或者在GPD文件中没有包括*TTFSEnalbled?条目，则Unidrv不允许字体替换，直到用户允许其替换为止。

*TTFSEnalbe?条目可以重置，但*TTF条目不能。(更多的关于可重置条目的信息，参考第10章在*Switch,*Case,*Default语句中包含什么部分的内容)

默认的TrueType字体替换

TrueType字体替换的默认表在一个称为ttfsub.gpd文件里提供，为使用它，可以在GPD文件的根目录层次(不包含于括号中)加入下面的条目：

*Include: ttfsub.gpd

另外，该文件必须被安装。更多的信息，可以参考打印机INF文件安装部分的内容。

4.8条件语句

GPD语言提供一个类似于C语言的条件语句以允许描述一些打印机配置上的打印机属性的依赖性。例如，一页的页边及光标原点可以依赖于页面的方向。*Swith及*Case语句允许表达这些依赖性。这些语句的格式如下：

*Switch FeatureName

{

*Case Option1_Name

{

}

*Case Option2_Name

{

}

etc.

*Case OptionN_Name

{

}

*Default

}

FeaturnName必须是在GPD文件中用*Feature条目指定的特性的名称。选项的名称则必须是与该指定的特性名称相关的选项名。

为表达页面页边及光标原点依赖于页面的方向的选择情况，以下的条目必须用到：

*Feature:Orientation

{

*DefaultOption:Portrait

*Option:Portrait

{

*Name:”Portrait”

*rcIconID:=RC+ICON_PORTRAIT

}

*Option:Landscape_CC90

{

*Name:”Landscape”

*rcIconID:=RC+ICON_LANDSCAPE

}

}

*Feature:PaperSize

{

*DefaultOption:Letter

*Option:Letter

{

*Name:”Letter 8.5 x 11 inch”

*switch:Orientation

{

*case:Protrait

{

*PrintableArea:PAIR(4800,6324)

*PrintableOrigin:PAIR(150,150)

*CursorOrigin:PAIR(150,100)

}

*case:Landscape_CC90

{

*PrintableArea:PAIR(4860,6360)

*PrintableOrigin:PAIR(120,120)

*CursorOrigin:PAIR(100,6480)

}

}

}

}

在这一个实例中，打印机纸张大小(PaperSize)的特性依赖于打印机方向特性的选择选项。

如果不在*Case语句参数中列出所有的特性选项，可以包含一个*Default语句，如同在C语言中一样。如果不包括所有的选项，也不包括*Default语句，必须在GPD文件的别处(在*Switch语句前)估算相关的属性(如在实例中的*PrintableArea,*PrintalbeOring及*CorsorOrigin)。

指定多个依赖关系

可以在*Case和*Default语句中包括*Switch语句。这样，允许指定多个依赖关系，如下：

*Feature:feature1{*Options:optina{…}*Option:optionB{…}}

*Feature:feature2{*Options:optina{…}*Option:optionB{…}}

*Feature:feature3

{*Option:optionE

{*Switch:feature1

{*Case:optionA

{*Swtich:feature2

{*Case:optionD

{AttributesX:ValueX}

*Default

{AttributeX:ValueY}

}

}

*Default

{AttributeX:ValueZ}

}

}

*Option:optionF{…}

}

在这个例子中，AttributeX，属于feature3的optionE，依赖于feature1和feature2。

如果用户已经为feature1选择了optionA，为feature2选择了optionD,为feature3选择了optionE，则AttributesX被设置于ValueX。

如果用户已经为feature1选择了optionA，为feature2选择了optionC,为feature3选择了optionE，则attributesX被设置于ValueY。

如果用户已经为feature1选择了optionB，为feature3选择了optionE，则AttributesX被设置于ValueZ,设置与Feature2无关。

以下的规则在指定多个依赖关系时是适用的：

■多个依赖关系必须在一个*Switch条目的范围中指定。用上边的实例，如，不能使用*Switch条目来指明feature3依赖于feature1，接着，非嵌套的*Switch语句指明feature3依赖于feature2。

■在每一个嵌套的*Switch条目中，不能超过两次指定相同的特性名称。

何处放置一个*Swicth语句

在一个GPD文件中，可以将*Switch语句放置于以下位置。

■在一个*Option语句中

■在一个*Feature语句中

■在一个*Case语句中

■在一个*Default语句中

■在一个文件最顶层(即，不是在一系列括号中)

在*Switch,*Case及*Default语句中都放置什么

在一个*Switch条目中，只能放置*Case条目及*Default条目。

那些可以被置于*Case或*Default条目中的GPD文件条目被看作为可重新置位的(relocatable)条目。下面类型的GPD条目都是可重新置位的。

■大多数的打印机属性，除过根层次属性(除非*Switch条目在根层次——不是包含在括号中，一般属性必须在其前置EXTERN_GLOBAL)

■嵌套的*Switch条目，它允许指定多个依赖性

■*Command条目

■*TTFSEnalbed?，它允许字体替换

下面类型的GPD条目是不能重新置位的：

■根层次的属性

■指定字体替换的*TTFS条目

■*Constraints,*InvalidCombination,*InvalidInstallableCombination,*NotInstalledConstraints等这些定义无效选项组合的条目，如在选项限制部分中描述的。

■*Features及*Option条目(尽管特性属性及选项属性是可以重置的)

一个确定是否条目被置于*Case语句的正确位置的方法是删除所有的*Switch及*Case语句，如果在*Case语句中的条目是正确的，则在*Switch及*Case语句被删除之后仍然是正确的。

4.9压缩光栅数据

Unidrv用下面三种格式支持光栅数据压缩：

■TIFF version 4.0(Tagged Image File Format)

■DRC(Delta-Row compression)

■FE-RLE(Far East Run-Length Encoding)

这些方法都在HP PCL 5打印机语言技术参考中被定义。

可以在GPD文件中包含条目以指明打印机支持那一种压缩格式。也可以提供自已的压缩算法。

关于光栅数据压缩的更多信息，可以参考下面的主题：

■4.9.1使用Unidrv支持的压缩

■4.9.2使用定制的压缩

4.9.1使用Unidrv支持的压缩

如果在GPD文件中包含一个CmdEnableTIFF4命令条目，Unidrv将使用TIFF 4.0压缩。

如果在GPD文件中包含一个CmdEnableDRC命令条目，Unidrv将使用DRV压缩。

如果在GPD文件中包含一个CmdEnableFE_RLE命令条目，Unidrv将使用FE-RLE压缩。

如果打印机支持更多的这些压缩方法，可以对每一个支持的方法包含一个命令条目。对每一个扫描行，Unidrv将试用每种压缩算法并选择会产生最好压缩结果的压缩方法。(也可以选择一种定制的压缩方法，参考使用定制压缩方法部分)。当Unidrv发现一个最好的压缩算法，它压缩扫描行的数据，然后在压缩的数据之后，将合适的命令条目指定的命令发送到打印机。

如果指定一个CmdDisableCompression命令条目，则不管哪些压缩方法可用，Unidrv在遇到没有压缩的数据块小于它的压缩格式时，将临时禁止发送被压缩的数据。

为限制不必要的计算，当压缩算法不可能产生一个可用的结果的时，不应使用这种算法。

对许多的打印机，允许接收或禁止接收压缩数据可以通过在数据块外发送命令字符串来实现。当为这些打印机指定CmdEnableTIFF4，CmdEnableDRC，CmdEnableFE_RLE和CmdDisableCompression条目时，应当包括一个命令字符串。

对一些打印机(通常是Far East打印机)，压缩选择命令被嵌在光栅数据中并同CmdSendBlockData命令一起发送。当为这些打印机指定CmdEnableTIFF4，CmdEnbleDRC或CmdEnableFE_RLE条目时，不包括一个命令字符串。相反，应当指定一个空的加引号的字符串以表示命令，这将告诉Unidrv使用压缩但是不向它发送单独的命令允许使用压缩。对这些打印机，只有一种压缩算法可用。CmdDisableCompression条目是不需要的，因为在这种情况下，Unidrv没有方法去关掉压缩。

更多的关于CmdEnableTIFF4，CmdEnableDRC，CmdEnableFE_RLE及CmdDisableCompression条目的信息，可以参考光栅数据压缩命令部分。

关于更多的关于CmdSendBlockData的信息，可以参考光栅数据发送命令部分。

4.9.2使用定制的压缩

如果想提供一种定制的压缩算法，应当包括一个CmdEnableOEMComp命令条目以指定允许压缩方法的命令。如果打印机禁止压缩，可以选择性地包括一个CmdDisableCompression条目以指定禁止压缩的命令。也必须提供一个绘制插件，以实现IPrintOemUni::Compression方法。

如果提供一个定制的压缩算法，可以允许使用Unidrv支持的算法。对每一个扫描行，Unidrv将试验每一种压缩算法并选择产生最好压缩结果的算法(更多的关于Unidrv支持的算法，参考使用Unidrv支持的压缩部分的内容)。当Unidrv找出最好的压缩算法，它压缩扫描的行数据，然后，在压缩的数据之后，将合适的命令条目指定的命令发送到打印机。

更多的关于CmdEnableOEMComp及CmdDisableCompression条目的信息，可以参考光栅数据压缩命令部分。

更多的关于定制压缩的信息，参考第7章定制数据流压缩的定制微软打印机驱动程序部分的内容。

4.10过滤光栅数据

如果想提供定制的后处理(Post-Processing)的扫描行的光栅数据流，并在它被假脱处理之前提供，可以通过在绘制插件中实现IPrintOemUni::FilterGraphics方法。没有与这一Unidrv特性相关的GPD文件条目。

更多的信息，可参考第7章定制数据流压缩中的定制微软打印机驱动程序部分的内容。

4.11处理色彩格式

一个打印机支持的每一种色彩格式被作为一个色彩模式特性的选项来指定。通过使用色彩模式特性的选项属性，可以描述打印机能接受的每一种色彩格式。下表列出了Unidrv可以处理的色彩数据格式。

设备中的色彩平面数(*DevNumOfPlanes) 设备中的每像素的位数的数量(*DevBPP)

1 | 1(黑与白)
1 | 8
1 | 24
3 | 1(CMY and RGB)
4 | 1(CMYK)

对这些格式，Unidrv可以转换DIB数据为合适的格式并将它发送到打印机。(在这一数据上可执行的过渡调色操作将在用Unidrv过渡调色部分讲述到)。

如果打印机支持没有列在上表中的色彩格式，则必须采用如下做法：

■设置*DevnumOfPlanes及*DevBPP属性为零。这样做可以防止Unidrv发送DIB数据到打印机。

■提供一个实现IPrintOemUni::ImageProcessing方法的绘制插件。

IPrintOemUni::ImageProcessing方法必须执行以下的操作：

■转换DIB数据为打印机的色彩格式

■对数据执行过渡调色的操作

■发送数据到打印机假脱机

更多的关于提供IPrintOemUni::ImageProcessing函数的信息，可参考第7章定制色彩格式中的定制微软打印机驱动程序部分。

绘制高质量图象

对每一种色彩格式，可以指定打印机硬件可以接受的每像素的位数以及在创建DIB时想让Unidrv使用的每像素的位数。这些值被分别用*DevBPP及*DrvBPP属性来指定。有时，

对打印机绘制比打印机能处理的更高数量的每像素位数的位图却更合人的心意(如，想创建高质量的图形)。因此，它允许指定一个*DrvBPP值，它是大于由*DevNumOfPlanes值相乘的*DevBPP的值。

例如，假设想定义一个色彩模式的选项，它可以使图像以24位/像素的位图绘制，但是又想该位图以CMYK数据送往打印机。这可以如下定义这种模式：

*Feature:ColorMode

{

*Option:24toCMYK

{

*Name:”Photographic Quality”

*DrvBPP:24

*DevNumOFPlanes:4

*DevBPP:1

*ColorPlaneOrder:LIST(CYAN,MAGENTA,YELLOW,BLACK)

*IPCallbackID:1

}

other option

}

这在一个例子中，*DevBPP及*DevNumOfPlanes属性表示一个四个平面、每平面一位的CMYK格式，Unidrv可以绘制并发送给打印机。但是，在这种情况下，过渡调色操作必须在绘制图象并被打印之前执行，且必须用到小驱动程序提供的过渡调色。

4.12用Unidrv进行中间色调整

过渡调色操作可以由GDI或驱动程序软件来控制，或者通过设备硬件来控制。它所使用的类型取决于色彩的格式，下面的主题讨论了Unidrv驱动程序支持过渡调色的操作：

■4.12.1GDI提供的过渡调色

■4.12.2小驱动程序提供的过渡调色

■4.12.3设备提供的过渡调色

4.12.1GDI提供的过渡调色

如果一个指定的色彩格式是用于绘制图象(*DrvBPP)的每像素位数的数字，并且与打印机支持的(*DevBPP)乘以*DevNumOfPlanes相同，则过渡调色操作由GDI操作。一个例子是*DrvBPP的值是4，而打印机的*DevBPP等于1并且*DevNumOfPlanes等于4。

对这种情况，唯一允许的过渡调色的方法是那些GDI提供的方法。这些过渡调色的方法在GPD文件中通过标准的过渡调色的选项名称来表示，它们列于标准选项之下。为指定想要Unidrv支持打印机的并且是GDI支持的过渡调色方法，可以在*Option条目中为过渡调色特性指定它们的名称(过渡调色特性是一个标准的打印机特性)。

如果在GPD文件中指定了几个过渡调色的方法及色彩模式，并且如果想限制那些过渡调色的方法在那些色彩模式时可选，可以使用选项限制。

如果没有在GPD文件中指定过渡调色的选项，Unidrv使用标准的HP_PATSIZE_AUTO选项。HP_PATSIZE_AUTO将导致Unidrv使用对选择的精度及色彩模式最佳的标准的过渡调色的方法。这就使得用户可以在多种精度和色彩模式中间转换而不需要知道对任何特殊的组合的最好的过渡调色的选项。

当使用GDI提供的过渡调色能力时，可以提供小驱动程序提供的过渡调色模式。

更多的关于色彩格式的信息，可参考处理色彩格式部分。

4.12.1.1小驱动程序提供的过渡调色

如果用了GDI支持的过渡调色方法，GDI允许定制的过渡调色模式的说明。为指定定制的过渡调色模式，使用过渡调色特性的选项属性，如下：

■*rcHTPatternID,*HTPatternSize及*HTNumPattern属性允许描述存储于资源DLL里的过渡调色模式。过渡调色模式资源是一个三维的二进制数据的数组，以一个DWORD地址开始。可以使用下面的格式来指定，它计算正确的尺寸并提供需要的地址调整。

BYTE HTPatternResource[HTNumPatterns][(HTPatternSize.y*HTPatternSize.x+3)&~3];

在一个用于创建资源DLL的.rc文件中，该模式可以以如下模式指定：

1 RC_HTPATTERN LOADONCALL DISCARDABLE HALFTONE.BIN

这里，halftone.bin是一个包含过渡调色模式的文件

■*HTCallbackID属性允许指明在一个绘制插件中实现IPrintOemUni::HalftonePattern方法。一个唯一的*HTCallbackID值必须为每一个IPrintOemUni::HalftonePattern方法支持的模式提供。

可以提供过渡调色模式资源，一个IPrintOemUni::HalftonePattern方法，或者都提供，如下：

■如果只提供过渡调色模式，Unidrv从资源DLL中获取该模式并将它们传送到GDI。该模式不能被加密。

■如果只提供IPrintOemUni::HalftonePattern方法，该方法必须给Unidrv产生并返回过渡调色模式，Unidrv将它传送到GDI。

■如果想在资源DLL中置放加密的取中间模式，然后必须提供一个IPrintOemUni::HalftonePattern方法以解码该模式并将它们返回给Unidrv，Unidrv按次序将它们发送给GDI。

更多的关于过渡调色的信息。参考第7章定制过渡调色中的定制微软打印机驱动程序的方法。

4.12.2驱动程序提供的过渡调色

如果一个指定的色彩格式是用于绘制图象(*DrvBPP)的每像素位数的数量，并且大于打印机支持的(*DevBPP)乘以*DevNumOfPlanes的数量，则必须提供定制的过渡调色能力。

为提供定制的过渡调色能力，必须如下做：

■提供一个实现IPrintOemUni::ImageProcessing方法的绘制插件

■在GPD文件中包含一个过渡调色*Feature的条目，对每一个定制的过渡调色的方法，包括一个描述过渡调色方法的*Option条目。(不要使用任何过渡调色特性的选项属性)。

■在GPD文件中包括一个色彩模式的*Feature条目。对每一个指定的色彩格式选项，如果希望IPrintOemUni::ImageProcessing方法去为该种色彩格式过渡调色，必须包括一个*IPCallbackID属性。

下面的实例定义了两种色彩格式及四种过渡调色方法：

*Feature:ColorMode

{

*Option:ColorFormat1

{

*Name:”Color Format 1”

*DevBPP:1

*DevNumOfPlanes:4

*ColorPlaneOrder:LIST(CYAN,MAGENTA,YELLOW,BLACK)

*DrvBPP:4

*Constraints:LIST(Halftone.CustomHalftoneMethod1,

+ Halftone.CustomHalftoneMethod2)

}

*Option:ColorFormat2

{

*Name:”Color Format 2”

*DevBPP:24

*DevNumOfPlanes:1

*DrvBPP:8

*IPCallbackID:100

*Constraints:LIST(Halftone.CustomHalftoneMethod1,

+ Halftone.CustomHalftoneMethod2)

}

}

*Feature:Halftone

`*Option:StandardHalftoneMethod1

{

*Name:”Standard Halftone Method1”

}

*Option:StandardHalftoneMethod2

{

*Name:”Standard Halftone Method2”

}

*Option:CustomHalftoneMethod1

{

*Name:” CustomHalftoneMethod1”

}

*Option:CustomHalftoneMethod2

{

*Name:” CustomHalftoneMethod2”

}

在实例中，ColorFormat1及ColorFormat2的色彩模式选项表示了Unidrv可以处理的色彩格式，如在处理色彩格式部分所讲述的。对ColorFormat2来说，一个*IPCallbackID属性已经被指定，如果打印机用户选择了ColorFormat2作为色彩的格式，Unidrv将调用打印机的IPrintOemUni::ImageProcessing COM方法去处理过渡调色。该方法的一个参数是一个指定表示当前选择的过渡调色方法的字符串名称的指针。

在该例中，有两个标准的过渡调色方法及两个定制的过渡调色方法是可用的。例子中使用了选项限制来指定Unidrv应当选取那一个过渡调色方法来允许用户对每一种色彩格式选择的支持。

更多的关于过渡调色的信息，参考第7章定制过渡调色中的定制微软打印机驱动程序部分。

4.12.3设备提供的过渡调色

如果打印机提供在内部过渡调色能力，小驱动程序必须指定Unidrv发送到打印机以激活这些能力的命令。对每一种打印机支持的过渡调色选项，GPD文件过渡调色的*Feature条目对每一个设备提供的过渡调色选项必须包含*Command条目，如下：

*Feature:Halftone

{

*Option:CustomHalftoneMethod1

{

*Name:”Custom halftone Method 1”

*Command:CmdSelect:”<Printer command string>”

}

*Option:CustomHalftoneMethod2

{

*Name:”Custom Halftone Method2”

*Command:CmdSelect:”<printer command string>”

}

}

色彩模式特性条目必须被指定，并且它们必须包括*DevBPP及*DevNumOfPlans条目描述打印机期望的输入格式。

4.13控制图像质量

Unidrv的用户接口提供一套三个单选按钮(Radio button)以允许用户对打印作业选择“草图(draft)”“较好(better)”“最好(best)”三种图象质量。草图质量加强了打印机对图象质量的速度，而“最好”的质量级别则相反。

这些单选钮的目的是允许用户很容易地选择获得希望质量时必需的特性选项，没有必要单独选择必需的选项的必要。

当一个单选按钮按下时Unidrv应当选择的选项被指定在 打印机的GPD文件中，GPD语言定义了下面一个条目：

*DraftQualitySetting

*BetterQualitySettings

*BestQualitySettings

每一个条目都是与其中一个单选钮相关的，并且每一个条目接受一个选项的列表。当一个用户选择了相关的按钮，Unidrv检查列表并设置指定的选项。

每一个设备条目的格式如下：

*XXXXQualitySettings:LIST(FeatureName.Optionname,FeatureName.OptionName,FeatureName.OptionName,……)

这里，每一个FeatureName与*Feature条目相关，Optionname是与一个特性的*Option条目相关的选项名称。一个空的列表可以引起相关的单选钮变灰(不可选)。

一个附加的，必需的条目指定了默认的图象质量。其格式如下：

*DefaultQuality:DefaultQuality

这里，DefaultQuality可以是DRAFTQUALITY,BETTERQUALITY或BESTQUALITY。

这些GPD文件条目可以与任何色彩模式或媒体类型特性的选项相关。通常，它们被置于条件语句中，如下面例子所示。

*Switch:ColorMode{

*Case:Mono{

*BestQualitySettings:LIST(ColorMode.Mono,

Resolution.Option1,

TextQuality.Option3)

*BestQualitySettings:LIST(ColorMode.Mono,

Resolution.Option1,

TextQuality.Option1)

*BestQualitySettings:LIST(ColorMode.Mono,

Resolution.Option2,

TextQuality.Option2)

*DefaultQuality:BETTERQUALITY}

*Default:{

*BestQualitySettings:LIST(ColorMode.24bpp,

Resolution.Option2

TextQuality.Option3)

*BestQualitySettings:LIST(ColorMode.Color,

Resolution.Option2

TextQuality.Option1)

*BestQualitySettings:LIST(ColorMode.Color,

Resolution.Option2

TextQuality.Option2)

*DefaultQuality:BETTERQUALITY}}

如在例中所示，一个好的策略是为单一色彩模式指定一个*Case条目为，然后对所有的多色彩模式用*Default条目。这是由于Unidrv的页面设置(PageSetup)的页面表单属性给用户提供了两个选择——色彩或非色彩打印。如果用例子中的格式，当用户选择色彩打印选项时，Unidrv显示质量选择的按钮。

下面是一个更复杂的例子，它将图象质量绑定于所有的色彩模式及媒体类型：

*switch:Colormode{

*case:Mono{

*switch:MediaType{

*case:CLAYCOATED{

*DraftQualitySettings:LIST(Option List)

*BetterQualitySettings:LIST(Option List)

*BestQualitySettings:LIST(Option List)

*DefaultQuality:BESTQUALITY }

*case:GLOSSY{

*DraftQualitySettings:LIST(Option List)

*BetterQualitySettings:LIST(Option List)

*BestQualitySettings:LIST(Option List)

*DefaultQuality:BESTQUALITY }

*default:

*DraftQualitySettings:LIST(Option List)

*BetterQualitySettings:LIST(Option List)

*BestQualitySettings:LIST(Option List)

*DefaultQuality:BESTQUALITY }

*default:{

*switch:MediaType{

*case:CLAYCOATED{

*DraftQualitySettings:LIST(Option List)

*BetterQualitySettings:LIST(Option List)

*BestQualitySettings:LIST(Option List)

*DefaultQuality:BESTQUALITY }

*case:GLOSSY{

*DraftQualitySettings:LIST(Option List)

*BetterQualitySettings:LIST(Option List)

*BestQualitySettings:LIST(Option List)

*DefaultQuality:BESTQUALITY }

*default:{

*DraftQualitySettings:LIST(Option List)

*BetterQualitySettings:LIST(Option List)

*BestQualitySettings:LIST(Option List)

*DefaultQuality:BESTQUALITY }}}

}

当使用质量设置GPD条目时，必须遵守下面的规则：

■必须使用所有的四个条目。允许指定一个空的选项列表，并且导致相关的单选按钮变灰。

■所有的四个条目必须为所有的色彩模式及媒体类型组合而指定。例子中在一个条件语句中使用了一个*Default条目以达到这一目标。

■在一个质量设置的条目的选项列表中不能与在选项限制中指定的相互矛盾。

■包括一个选项列表的选项不能改变选定的媒体类型。同样，如果它被接受，例如，为最好质量设置色彩模式为24bits/pixel，为较好质量设置8bits/pixel，为草图质量设置4bits/pixel，设置为1bits/pixel(单色模式)是不能接受的。

如果一个特性被包括于指定质量设置的条件语句中，解析器设置特性的*UpdateQualityMacro?属性为TRUE(参考特性属性部分)。

4.14处理可安装的特性及选项

一些打印机的特性或选项也许可以被安装。例如，一个打印机可以接受一个可选的信封送纸器，它能被或者不被当前选用。这一从封送纸器必须在一个GPD文件中以两种方式描述：

■作为一个二进制输入特性的选项。

■作为一个可安装的“特性”(尽管它实际是一个选项)，它允许用户指明它是否被实际安装着。

第一，为指定信封送纸器，与一个自动的送纸器，作为二进制输入特征的选项，应当使用下面的GPD条目。

*Feature:InputBin

{

*Name:”Input Bin”

*Option:AUTO

{

*Name:”Automatic Feeder”

*Command:CmdSelect{Command Attributes}

}

*Option:ENVFEED

{

*Name:”Envelope Feeder”

*Command:CmdSelect{Command Attributes}

}

}

为使得信封送纸器被安装，另外的GPD条目是需要的，如下：

*InstalledOptionName:”Installed”

*NotInstalledOptionName:”Not installed”

*Feature:InputBin

{

*name:”Input Bin”

*Option:AUTO

{

*Name:”Automatic Feeder”

*Command:CmdSelect{Command Attributes}

}

*Option:ENVFEED

{

*Name:”Envelope Feeder”

*Command:CmdSelect{Command Attributes}

*Installable?:TRUE

*InstallableFeatureName:”Optional Envelope Feeder”

}

}

在一个信封送纸器的*Option条目中，增加了两个属性：

■*Installable?属性，以指明选项被安装。

■*InstallableFeatureName属性以指明一个Unidrv是显示的文本字符串，这样用户可以指明选项是否被实际安装。

无论何时当*Installable?为一个特性或选项被设置为TRUE，Unidrv对显示属性表单创建一个附加的特性(注意既使可安装的项目是选项，Unidrv也会为它在属性表单中创建一个特性表示)。这一Unidrv综合的特性是由*InstallableFeatureName提供的字符串来鉴别的。该特性提供两个选项，即“已安装(Installed)”及“未安装(Not installed)”，并允许用户选择这中间的一个选项。字符串“installed”和“Not installed”是用*InstalledOptionName及*NotInstalledOptionName特性来指定的，这样如果有更合适的文本就可以修改它们。

因此，对我们的例子来说，属性表单将会包括一个二进制输入特性，并标记为“InputBin”，它包括两个选项，标记为自动送纸器“Automatic Feeder”及信封送纸器“Enveloper Feeder”。属性表单将会包括一个附加的特性，标记为可选的信封送纸器“Optional Envelope Feeder”，并有两个选项，标记为“Intalled”及“Not Installed”。用户可以只选择在“InputBin”下的“Envolope Feeder”选项或者最初就选择“Optional Envelope Feeder”下的“Installed”选项。

有时，有必要指明一些可安装选项不能被同时安装，或者一个不能安装的选项不能在一些其他可安装的选项已被安装时被选择。为处理这些情况，可以使用GPD文件中的选项限制条目来处理。

不能使用具有可选特性的*Installable?属性，而它又需要一个*DisabledFeatures条目。对这些特性，必须明确指定可选的特性具有“Installed”或“Not installed”选项。例如，假设一个打印机具有一个可选的双倍单位。如果没有安装双倍单位，则双倍打印特征(见标准特性)必须被禁止。必须定义一个“可选的双倍打印单位”特性，并具有“Installed”及“Not installed”选项。在“Not installed”*Option条目中，应当为双倍特性包含一个*DisableFeatures条目。下面的GPD条目可以被应用：

*Feature:DuplexUnit

{

*ConfilictPriority:3 *% Make priority higher than Duplex feature

*Name:”Optional Duplexing Unit”

*Option:Installed

{

*name:”Installed”

}

*Option:NotInstalled

{

*name:”Not installed”

*DisabledFeatures:LIST(Duplex)

*Constraints:LIST(Duplex.LongEdge,Duplex.ShortEdge)

}

但是一定要指定任一相关的选项限制，如例子中所示。

4.15指定特性和选项显示的顺序

为控制特性及选项在Unidrv产生的属性表单页面上显示的顺序，应当在GPD文件中包括一个空的*Featrue及*Option条目。这些条目必须被置放于文件的开始位置，在其他的全部的*Feature及*Option条目之前，并在任何其他对该特性名或选项名的引用之前。空的条目被列出的顺序就是特性及选项应当在属性表单上显示的顺序。(需要注意的是，无论如何，纸张尺寸特性的选项总是以字母的顺序列出的，并且这个顺序不能被改变)。

下面是一个一套空的*Feature及*Option条目的实例：

*Feature:EconoMode

{

*Option:OFF{}

*Option:On{}

}

*Feature:Orientation

{

*Option:PORTRAIT{}

*Option:LANDSCAPE_CC90{}

}

*Feature:PaperSize

{

}

*Feature:Resolution

{

*Option:Option1{}

*Option:Option2{}

*Option:Option3{}

}

该例子指定的顺序为EconoMode,Orientation,PaperSize及Resolution这样的一个特性的显示顺序。另外，它也指定了在EconoMode,Orientation,PaperSize及Resolution的属性必须以字母的顺序来显示。

如果一个GPD文件不包括空的*Feature及*Option条目指定的显示的顺序，GPD解析器将决定显示的顺序。而且，一般解析器都是以特性及选项在GPD文件中的顺序来显示，这一顺序不能被保证。另外，默认情况下，解析器总是将二进制输入的特性最先显示。

由包含有空的*Feature及*Option条目明确指定的顺序的方法是推荐使用的，而不推荐用解析器创建顺序。

4.16描述打印机内存配置

Unidrv小驱动程序可以包含一个打印机的可能的及默认的内存配置说明，这样Unidrv可以试图跟踪打印机内存使用情况。每一个内存配置使用包括所有的总内存值、可用内存等。可用内存可用于下载字体、保护页面及其他由Unidrv进行的控制操作。

在一个GPD文件中，可以用两种方法去描述一个打印机的可能的内存配置。两种方法包括为内存特性(是一种标准特性)在*Featrue条目中指定属性。两种方法如下：

1.可以在一个*Feature条目的不同的*Option条目中指定每一种可能的配置。每一个*Option条目必需包含一个*MemoryConfigKB属性，该属性在内存特性的选项属性部分中被描述。

例如，为指定一个打印机可以有两种内存配置，即1M配置，其中有450K可用及2M配置其中有1350K可用，可以用下面的GPD条目：

*Feature:Memory

{

*Name:”Printer Memory”

*DefaultOption:1MB

*Option:1MB

{

*Name:”Standard 1MB”

*MemoryConfigKB:PAIR(1024,450)

}

*Option:2MB

{

*Name:”Standard 2MB”

*MemoryConfigKB:PAIR(2048,1350)

}

}

2.做为另外一种选择，*Feature条目可以包含一个或多个*MemConfigKB或*MemconfigMB属性而不是*Option条目。这是一种简单地不用包含一套*Option条目的指定内存的方法。每一个*MemConfigKB或*MemConfigMB属性表示一个内存选项。

例如，为指定上面同样的两种配置，即1M配置，其中有450K可用及2M配置其中有1350K可用，可使用下面的条目：

*Feature:Memory

{

*Name:”Printer Memory”

*DefaultOption:1024KB

*MemConfigKB:PAIR(1024,450)

*MemConfigKB:PAIR(2048,1350)

}

GPD解析器为每一种配置创建一个可显示的选项名称，基于第一个条目的PAIR语句。在上例中，选项名可以是“1024KB”及“2048KB”。*DefaultOption属性的参数必须与这两个名称之中的一个匹配。

方法1和方法2可以在一个单一的*Feature条目中使用。

如果解析器产生的选项名称与局部的需求不兼容，便只能使用方法1而不能使用方法2。

无论使用哪一种方法，Unidrv用户接口在设备的打印机属性表单上显示内存特性选项。

如果小驱动程序指定了内存配置，它也可以指定那些可以存储于打印机内存的数据的类型及用完它可用的内存空间。*MemoryUsage属性是一个打印机能力特性的属性，可以使用它向Unidrv指明是否字体、光栅或者向量数据，或者这三者的组合被存储于打印机的内存中。对每一种类型的指定，Unidrv试图跟踪到底有多少打印机内存可用。

4.17指定纸张大小

Unidrv支持三种规格的纸张尺寸——标准纸张尺寸、厂商定义的纸张尺寸、用户定义的纸张尺寸。

标准纸张尺寸

标准纸张尺寸是许多一般打印机都支持的尺寸，这些纸张大小的尺寸是预先定义好的。更多的信息，可以参考支持标准纸张尺寸部分的内容。

厂商定义的纸张尺寸

厂商定义的纸张尺寸(也称为非标准纸张尺寸non-standard paper sizes)是由打印机厂商定义的，在一个打印机的gpd文件中。更多的信息，请参考支持打厂商定义的纸张尺寸部分。

用户定义的纸张尺寸

用户定义的纸张尺寸(也称为定制的纸张尺寸)，是由系统管理员根据系统的打印文件夹来定义的，这样便是系统专用的。更多的信息，参考支持用户定义的纸张尺寸部分。

4.17.1支持标准纸张尺寸

标准的纸张尺寸由纸张尺寸特性的标准选项来表示。

打印机支持的每一种标准尺寸的纸张，在GPD文件的纸张大小特性中必须包含一个*Option条目，它的参数是标准选项名称中的一个(除过CUSTOMESIZE)。这一条目中，下面的选项特性是必需的：

*PrintableArea

*PrintableOrigin

*rcNameID

*Command

下面的选项属性可以被用到，但不是必需的：

*CursorOrigin

*RotateSize?

*PageProtectMem

对所有的标准的纸张大小尺寸，RCID_DMPAPER_SYSTEM_NAME资源标识符(在stdnames.gpd中定义)应当被用作*rcNameID的一个参数。

4.17.2支持厂商定义的纸张尺寸

厂商定义的纸张尺寸是厂商专用的并且必须由每一个打印机的GPD文件来描述。这些大小尺寸也称为非标准的纸张大小尺寸，因为它们没有包含于纸张大小特性的标准选项中。

对打印机支持的每一种厂商定义的纸张大小，GPD文件的纸张大小特性必须包括一个*Option条目，它的参数不是标准选项名中的一个。在这一条目中，下面选项属性是必需的：

*PageDimensions

*PrintableArea

*PrintalbeOrigin

*rcNameID 或*Name

*Command

下面的选项属性可以被使用，但不是必需的：

*CursorOrigin

*RotateSize?

*PageProtectMem

4.17.3支持用户定义的纸张尺寸

用户定义的纸张尺寸可以特定于一个打印机服务器并且通常对专用的应用程序是定制的。尽管他们经常称为定制的纸张尺寸。系统管理员用打印文件夹去定义定制的纸张尺寸。如果一个打印机可以处理定制的纸张尺寸，厂商必须用打印机的GPD文件去指定可接受的大小尺寸范围。

对描述可接受的定制的纸张尺寸，提供有两种方法：

■可以明确地指定大小尺寸区间。

■可以相对于打印机最大的纸张尺寸指定大小区间。

明确地指定大小尺寸区间

为使用这一方法，GPD文件的纸张大小尺寸必须包括一个*Option条目并具有一个CUSTOMSIZE参数，这一条目必需包含下面的选项属性：

*MinSize

*MaxSize

*MaxPrintableWidth

*MinLeftMargin

*TopMargin

*BottomMargin

*CenterPrintable?

*CursorOrigin

*Command

可以用这些GPD条目为具有下面特性的打印机创建定制的纸张尺寸描述：

■打印机支持明确选择定制纸张尺寸(通常是通过移动光标)的命令。

■相对于纸张的左上角，光标原点保持固定，对所有的定制的纸张尺寸。(通常对以横向及竖向模式打印或者对那些中间进纸或右边进纸的打印机是不正确的)

■顶部与底部页边相对于纸张尺寸独立。

■如果纸的宽度小于由*MinLeftmargin及*MaxPrintableWidth指定的尺寸，将没有右边页边。也就是，打印机可以打到纸张的右页边上。

如果用到了标准变量表达式，则命令参数(在*Command条目中指定)可以在打印时被计算，通常包括PhyspaperLenth及PhyspaperWidth变量。这些变量表示打印作业实际需要的尺寸，如同一个应用程序指定的那样。

相对于打印机最大的纸张尺寸指定大小区间

对那些不能明确支持指定定制纸页尺寸区间特征的打印机，提供了一个变更的方法，它相对于打印机能支持的最大纸张尺寸指定大小区间。

为使用这种方法，GPD文件纸张尺寸特性必须包含一个*Option条目并具有CUSTOMSIZE参数。这一条目必须包含下面的选项属性：

*MinSize

*MaxSize

*MaxPrintableWidth

*CustCursorOriginX

*CustCursorOriginY

*CustPrintableOriginX

*CustPrintableOriginY

*CustPrintableSizeX

*CustPrintableSizeY

*Command

当指定相对计算机能支持的最大纸张尺寸的区间时，命名用下面调整的规则：

■对左边进纸的打印机，纸张尺寸的顶部及左边的页边必须对齐。

■对右边进纸的打印机，纸张尺寸的顶部及右边的页边必须对齐。

■对中间进纸的打印机，纸张尺寸的顶部及顶部中间点必须对齐。

必须涉及下面的步骤：

1.为打印机的最大纸张尺寸确定下面的信息：

■选择最大纸张尺寸的命令。

■最大纸张尺寸可能会用到的*PageDimensions,*CursorOrigin,*PrintableOrigin及*PrintableArea等GPD条目的值，如同它们它必包含于GPD文件中。但是，不能实际将这些条目置于文件中。

2.对每一种定制的纸张尺寸，相对于打印机能支持的最大的纸张尺寸，创建一个指定或计算下列信息的公式。

■每一页纸张可打印区域的原点及大小。

■每一页纸的光标原点。

对步骤2，必须是CUSTOMSIZE参数表达式，它被指定为下面的GPD条目的值：

*CustCursorOriginX

*CustCursorOriginY

*CustPrintableOriginx

*CustPrintableOriginY

*CustPrintableSizeX

*CustPrintableSizeY

CUSTOMSIZE选项必须包含一个*Command条目，它用于指定选择最大打印机尺寸的命令。这一命令将被送往所有的定制的纸张尺寸，并且由公式指定的可打印区域的光标原点将控制打印机实际打印到纸张上的位置而不管它的尺寸。

示例计算

如一个简单的实例，假设打印机支持定制的纸张尺寸，并与打印机能支持的最大的页面的页边相同。则必须包括下面这样的步骤：

1.决定最大纸张尺寸的*PageDimensions,*CursorOrigin,*PrintableOrigin及*PrintableArea条目的值(不要把这些条目放于GPD文件中)。

2.根据这些值确定每一页最大纸张尺寸页边的宽度，如下面的一些伪表达式：

LeftMarginWidth=*PrintableOrigin.x

RightMarginwidth=*PageDimension.x-*PrintableArea.x-LeftMarginWidth

TopMarginWidth=*PrintableOrigin.y

BottomMarginWidth=*PageDimensions.y-*PrintableArea.y-TopMarginwidth

在这些伪表达式中，.x及.y表示每个条目的对值(PAIR)的水平及垂直部分。对横向打印来说，用*PrintableArea及*PrintableOrigin的横向的值。

3.创建一个指定或计算为非标准纸张的可打印区域伪表达式。

*CustPrintableOriginX:%d{LeftMarginWidth}

*CustPrintableOriginY:%d{TopMarginWidth}

*CustPrintableSizeX:%d{PhysPaperWidth-LeftMarginWidth-RightMarginWidth}

*CustPrintableSizeY:%d{PhysPaperLength-TopMarginWidth-BottomMarginWidth}

注意用两个标准变量，即PhysPaperWidth及PhysPaperLength。在运行时，这些变量包含着应用程序请求的实际纸张尺寸大小的长度及宽度值。

并且要注意，这些伪指令无论对从左边进纸、右边进纸、中间进纸，都是有效的。

4.插入真值到这些伪表达式中以创建GPD条目决定于步骤1，其例子可以是：

*CustPrintableOriginX:%d{300}

*CustPrintableOriginY:%d{300}

*CustPrintableSizeX:%d{PhysPaperWidth-600}

*CustPrintableSizeY;%d{PhyssPaperLength-600}

5.创建计算光标原点索引的伪表达式。在下面的伪表达式中，*CursorOrigin.x及*CursorOrigin.y是存放最大纸张尺寸光标原点的水平及垂直的部分的对值(PAIR)的地方。

对左边进纸的打印机：

*CustCursorOriginX:%d{*CursorOrign.x}

*CustCursorOriginY:%d{*CursorOrign.y}

对右边进纸的打印机：

*CustCursorOriginX:%d{*CursorOrign.x+PhysPaperWidth-*PageDimensions.x}

*CustCursorOriginY:%d{*CursorOrign.y}

对中间进纸的打印机：

*CustCursorOriginX:%d{*CursorOrign.x+(PhysPaperWidth-*PageDimensions.x)/2}

*CustCursorOriginY:%d{*CursorOrign.y}

6.插入步骤1决定的实型值到这些伪表达式中以创建GPD条目，其实例可能如下(对中间进纸)

*CurstCursorOriginX:%d{((PhysPaperWidth-14040)/2)+300}

*CustCursorOriginY:%d{180}

7.指定余下的三个GPD条目值——*MinSize,*MaxSize及*MaxPrintableWidth。指定于*MaxPrintableWidth的值不是实际用于该方法的值，但是解析器需要该条目的存在，因此，它的值可以被设置成为1。

一个实际的例子

下面实例是一个GPD文件片段，描述了可接受的可定制的中间进纸打印机的纸张尺寸。对纵向打印模式，所有定制纸张的尺寸的四个页边都是300个基本单位(1/4英寸)。对横向打印模式，顶部及底端的页边是240基本单位，而左边和右边的页边是200个基本单位。

*Option:CUSTOMSIZE

{

*rcNameID:=USER_DEFINED_SIZE_DISPLAY

*MinSize:PAIR(4200,9000)

*MaxSize:PAIR(14040,21240)

*MaxPrintableWidth:14040

*MinLeftMargin:100

*CenterPrintable?:FALSE

*PageProtectMem:1692

*InsertBlock:=PaperConstraints

*switch:Orientation

{

*Case:PORTRAIT

{

*CustCursorOriginX:%d{((PhysPaperWidth-14040)/2)+300}

*CustCursorOriginY:%d{180}

*CustPrintableOriginX:%d{300}

*CustPrintableOriginY:%d{300}

*CustPrintableSizeX:%d{PhysPaperWidth-600}

*CustPrintableSizeY:%d{PhysPaperLength-600}

*Command:CmdSelect

{

*Order:DOC_SETUP.13

*Cmd:”<1B>&1101a8c1e99F<1B>*px0Y<1B>*c0t8064x12528Y”

}

}

*Case:LANDSCAPE_CC90

{

*switch:Opiton20

{

*%The 8100 rotates the landscape job 180 degrees if a stapler

*%is attached ,so the staple can be placed in the top left

*%corner of the document.The printer will always rotate the

*%landscape job,even if stapling is not selected.

*case:3Kstapler

{

*CustCursorOriginX:%d{((PhysPaperWidth-14040)/2)+200}

*CustCursorOriginY:%d{PhyspaperLength}

*CustPrintableOriginX:%d{200}

*CustPrintableOriginY:%d{240}

*CustPrintableSizeX:%d{PhysPaperWidth-400}

*CustPrintableSizeY:%d{PhyspaperLength-480}

*Command:CmdSelect

{

*Order:DOC_SETUP.13

*Cmd:”<1B>&1101a8c1e63F<1B>*p0x0Y<1B>*c0t12456x8184Y”

}

}

*case:MBM5S

{

*CustCursorOriginX:%d{((PhysPaperWidth-14040)/2)+200}

*CustCursorOriginY:%d{PhyspaperLength}

*CustPrintableOriginX:%d{200}

*CustPrintableOriginY:%d{240}

*CustPrintableSizeX:%d{PhysPaperWidth-400}

*CustPrintableSizeY:%d{PhyspaperLength-480}

*Command:CmdSelect

{

*Order:DOC_SETUP.13

*Cmd:”<1B>&1101a8c1e63F<1B>*p0x0Y<1B>*c0t12456x8184Y”

}

}

*Default

{

*CustCursorOriginX:%d{((PhysPaperWidth-14040)/2)+200}

*CustCursorOriginY:%d{PhyspaperLength}

*CustPrintableOriginX:%d{200}

*CustPrintableOriginY:%d{240}

*CustPrintableSizeX:%d{PhysPaperWidth-400}

*CustPrintableSizeY:%d{PhyspaperLength-480}

*Command:CmdSelect

{

*Order:DOC_SETUP.13

*Cmd:”<1B>&1101a8c1e63F<1B>*p0x0Y<1B>*c0t12456x8184Y”

}

}

}*%switch Option20

}*% case LANDSCAPE_CC90

}*%switch Orientation

}

4.18引用场所(Referencing Locales)

GPD文件可以引用一个系统的场所。通常，场所的标识符被用于*Switch语句中，而诸如默认纸张大小尺寸及资源DLL等参数可以用手工方式在一个场所中指定。

为引用场所信息，GPD文件必须必含一个*Include语句，该语句包含了locle.gpd(这一DDK提供)这一文件，如下：

*Include:locale.gpd

这一GPD文件定义了一个名为“Locale”的特性，并对许多场所都定义了选项(访问该文件看定义了哪些场所)。下面是一个这些场所的用法实例，其实例在场所中支持默认的纸张尺寸。

*Feature:PaperSize

{

……

Option:A4

{

}

……

*switch:Locale

{

*case:English_United_States

{

*DefaultOption:Letter

}

*case:English_United_Kingdom

{

*DefaultOption:A4

}

*default:

{

*defaultOption:Letter

}

}*% End of switch

}*%End of Feature:PaperSize

在运行时，Unidrv通过调用GetsystemDefaultLCID(在平台的SDK文档中描述)确定系统的默认场所。当一个打印机被安装，GPD解析器读该打印机的GPD文件并使用与默认场所相关的在*Case语句中的信息。(注意，如果系统的场所在打印机被安装之后发生变化，基于场所的选项却没有变化)。

这里有另外一个例子，它基于场所来选择一个资源DLL。资源DLL可以包含特定场所的资源，如显示字符串等。

*switch:Locale

{

*case:English_United_States

{

*ResourceDLL:english.dll

}

*case:German_Standard

{

*ResourceDLL:german.dll

}

*default:

{

*ResourceDLL:english.dll

}

}

4.19安装一个Unidrv小驱动程序

安装一个Unidrv的小驱动程序需要一个printer.inf文件，它标识小驱动程序的文件。如果一个打印机的模式不被微软打印机的.inf文件或nfprint.inf支持，就需要一个商家提供的.inf文件。.inf文件应当引用printer.inf文件的数据部分及printer.inf文件的安装部分，printer.inf被定义于ntprint.inf中。对一个名为abc100的小驱动程序，如果打印机是双向打印，支持TrueType字体替换，并且使用了一个单一的资源DLL，下面的.inf条目通常是需要的：

[Manufacturer]

“ABC Printers”

[ABC Printers]

“ABC Printer 100”=ABC100.GPD,ABC_Printer_100

[ABC100.GPD]

CopyFiles=@ABCres.dll,@ABC100.gpd ;Resource DLL and GPD file

DataSection=UNIDRV_BIDI_DATA ;Unidrv Data Section

DataFile=ABC100.gpd

Include=NTPRINT.INF ;Include NTPRRINT.INF

Needs=TTFSUB.OEM,UNIDRV_BIDI.OEM ;Install TrueType subs,Unidrv

; and PJL Language monitor

如果提供一个用户接口的插件或一个绘制插件，需要包含这些部分的名称在.inf文件中。关于安装定制代码的信息，参考第7章安装定制驱动程序组件部分的内容。


微软PostScirpt打印机驱动程序

PostScript打印机驱动程序，即PostScript Printer Driver(Pscript)是微软公司PostScript打印机的标准打印机驱动程序。关于Pscript主要描述了下列一些主题：

■5.1Pscript能力

■5.2Pscript组件

■5.3Pscript小驱动程序

■5.4Pscript用户接口

■5.5Pscript绘制器

5.1 Pscript能力

PostScript打印机驱动程序提供了如下的能力：

■支持所有的PostScript打印机，用特定打印机的基于PPD的Pscript小驱动程序描述每一种打印机的特征。

■一个Pscript用户接口，基于树形视图控件及属性表单，对所有的打印机是一致的，但对每一种打印机的单独选项也是可以改变的。

■一个单独的Pscript绘制器，与GID图形引擎一起，将微软应用程序对Win32 GDI的调用转换成可以发送到打印假脱机的的打印机命令。

■支持文档结构转换的3.1版本标准(Document Structuring convertion V3.1)，该标准在Adobe System ，Inc公司出版的PostScript Language Reference Manual中有描述。

■为打印机提供PostScript Level 1,Level 2,Leve3特性的支持。

■下列类型字体的支持：

□增量可下载的OpenType字体，作为PostScript Type 1及 Type 2字体。

□增量可下载的TrueType字体，作为PostScript Type 1、 Type 3、Type 32、Type 42或基于CID的Type 42字体。

□增量可下载的主机驻留的光栅字体作为PostScript Type 3或者Type 32字体。

□完全可下载的主机驻留的PostScript Type 1字体。

□打印机驻留的PostScript Type1、Type2及CID字体。

□存在于打印机字符集中的每一个可被替换符号的字体，

■对ICM2.0的支持，允许在主机系统上或通过打印机硬件来执行图形色彩管理。

5.2 Pscript组件

Pscript组件由一些DLL组成，加上文本及二进制数据文件，如在下图中所示：

插入图？？？

图表中的组件包括：

应用程序

一个用户的应用程序，如可给用户提供打印能力的字处理程序。

gdi32.dll

用户模式的DLL，导出Wind32 GDI函数

内核模式的图形引擎

实现GDI功能的Windows NT 执行代码

小驱动程序文本文件

基于文本的Pscript小驱动程序，用PPD文件创建

二进制数据文件

Pscript在解析包含于小驱动程序的文本文件中的信息后创建的临时文件(具有.bpd扩展名)

ps5ui.dll

Pscript用户接口的DLL，向所有的支持Pscript的打印机提供通用的UI代码。

用户接口插件

可选，特定打印机的用户接口插件。

compstui.dll

为打印机的CPSUI用户接口

pscript5.dll

pscript 绘制器，它处理文本输出并绘制图像，然后发送文本数据及图像数据到打印假脱机。

绘制插件

可选，特定打印机的绘制插件

5.3 Pscript小驱动程序

Pscript的小驱动程序是从.ppd及.ntf文件创建的。

PPD文件

基于文本的PostScript printer Description(.ppd)文件，描述了PostScript打印机的特征。Pscript驱动程序对Windows 2000支持的.ppd文件是与Adobe Systems Inc公司的v4.3版的PPD规范说明相兼容的。Pscript读一个打印机的.ppd文件并将文本转换为二进制的格式，在本地存储为一个.bpd文件并在.ppd文件每一次改变时被重新生成。

NTF文件

Windows 2000字体文件(.ntf文件)是用于描述由Pscript支持的打印机的设备字体。

微软提供了一个默认的.ntf文件，名为pscript.ntf，它包含有对遇到的US设备字体的通用描述。对Far East打印机，微软也提供了一个默认的.ntf文件，名称为pscriptfe.ntf，它包含对遇到的Far East打印机字体的通用描述。

另外，硬件厂商可以提供没有被pscript.ntf支持的设备字体描述。这些字体描述可以由转换AFM文件到NTF文件来被创建。定制的、特定模型的打印机可以通过将它们列为独立的打印机INF文件来安装。更多的信息，可以参考安装Pscript小驱动程序部分的内容。

Pscript根据最先检查到的特定打印机模型的.ntf文件来搜索字体的规格，然后用第一次发现的字体描述来检查pscript.nft文件。

5.3.1转换AFM文件到NTF文件

对Windows 2000，Adobe Font Metrics(AFM)文件必须被转换为.ntf文件。一个名为makenft.exe的命令行工具可以执行这一转换，该工具在本DDK中也提供。

为转换一个或多个.afm文件，用下面的命令语法：

makentf NTF_FileName.ntf AFM_FileName[-v][-o]

这里，NTF_FileName是一个要被产生的.ntf文件的名称，AFM_FileName是一个或多个将被转换的AFM文件名称。

下面的命令行选项也是支持的：

-v

冗余式。该选项创建一个包含对产生的NFT文件的文本式显示的命令输出流。

-o

省略标准的西文字符集。默认情况下，在产生.ntf文件时makentf包括一个标准的西文字符集。如果正在创建多个.ntf文件，只要所有的文件是一起使用，只需要在一个文件中包括西文字符集。例如，假设想创建一个包含罗马字体规格和另外一个包含日文字体规格的.ntf文件，可以使用下面的命令：

makentf roman.ntf roman1.afm roman2.afm roman3.afm

makentf –o jpn.ntf jpn1.afm jpn2.afm jpn3.afm

如果这些文件被一起使用，西文字符集信息总可以从roman.ntf文件获得，因此，在jpn.ntf文件中重复这样的信息是不必要的，只能是浪费附加的空间。另外一方面，如果jpn.ntf单独使用，-o就必须被指定。

另一个命令语法也是被支持的，如下：

makentf filename

这里filename是一个接收输出文本的文件名，该语法导致makentf以创建一个文件，这个文件包含PostScript字符名称列表及被makentf所知的每一个代码页的Unicode值。

另外一个文件，PSFamily.dat，是该DDK提供的并必须驻留在与包含makentf.exe文件同一目录的位置。这是一个文本文件，它可以向makentf提供每一种字体的显示及家族名称。

在一个标准的.afm文件被转换之前，必须加入下面一行：

Comment UniqueID Idnumberi

这里Idnumber表示字体的唯一标识符，由字体的厂商发布的。

一个将被转换成.ntf文件的.afm文件可以包含FontBBox2关键字。这个关键字的参数与FontBBox(参考Adobe System Inc.的Adobe字体规格文件格式规范说明)的那些参数非常相似，除了FontBBox2参数为所有在指定字符集(如90ms)中的字符描述了一个限制框，而FontBBox参数为所有在.afm文件中描述的字符联合描述限制框。如果FontBBox2没有被发现，则为FonBBox指定的值将被用于限制框。

5.3.2安装Pscript小驱动程序

安装Pscript小驱动程序需要一个打印机的printer.inf文件，它标识了小驱动程序的文件。如果一种打印机模式不被微软的打印机的inf文件、ntprint.inf文件所支持，就需要有厂商提供的.inf文件。.inf文件应当引用定义于ntprint.inf中的printer.inf文件数据部分及printer.inf文件的安装部分。对一个名称为abc100的小驱动程序，下面的.inf条目是通常需要的：

[Manufacturer]

“ABC Printers”

[ABC Printers]

“ABC Printer 100 PS”=ABC100.PPD,ABC_Printer_100_PS

[ABC100.PPD]

CopyFiles=@ABC100.ppd ;PPD file

DataSection=PSCRIPT_DATA ;PSCRIPT Data Section

DataFile=ABC100.ppd

Include=NTPRINT.INF ;Include NTPRINT.INF

Needs=PSCRIPT.OEM ;Install PSCRIPT

如果提供了用户接口插件或绘制插件，需要在.inf文件中包括这些组件的名称。更多的关于安装定制代码的信息，参考第7章安装定制驱动程序组件部分的内容。

5.4 Pscript用户接口

Pscript用户接口用CPSUI以创建下面的属性表面页面：

■打印机属性表单的设备设置(Device Settings)页面，当一个用户从打印文件夹或打印机窗口选择了属性菜单项目时它就会显示。该页面列出了打印机专用的配置信息。

■文档属性表单的布局(Layout)、纸张/质量(Paper/Quality)、高级(Advanced)等页面。这些页面会在用户选择了从打印文件夹或打印机窗口的打印优选项(Printing Preference)菜单项时显示。或者一个应用程序调用PrinterProperties或DocumentProperties函数(在平台的SDK文档中描述)。这些页面列出了特定文档的配置信息。

这些属性表单页面包含了打印机的特性及由打印机的Pscript小驱动程序指定的打印机的选项。也允许用户修改这些选项值。

Pscript用户接口是作为一个用户模式的打印机接口DLL来实现的。在这一DLL中的代码，与CPSUI连接起来，指定了属性表单页面的内容。基于小驱动程序中的信息，DLL在这些打印机可组合的选项上施加限制。它也确保了用户不去选择没有安装在打印机上的选项。

可以通过提供一个用户接口的插件来修改Pscript提供的属性表单页面，这些内容在第7章中的定制微软打印机驱动程序部分描述。

5.5 Pscript绘制器

Pscript绘制器是作为一个打印机图形的DLL实现的，这样可以导出由微软设备驱动程序接口(DDI:Device Driver Interface)为图形驱动程序定义的函数。当一个应用程序调用图形设备接口(GDI:Graphics Deivce Interface)函数以向打印机设备发送文本及图像，内核模式的图形引擎调用绘制器的DDI函数。这些DDI函数帮助GDI绘制打印机作业的页面图像。

绘制器同时也负责按照打印机命令序列发送文本及绘制图像到打印假脱机，假脱机然后直接将数据流及命令发送到打印机硬件。

可以通过提供一个绘制插件来修改Pscript的绘制操作，这些内容也在第7章中的定制微软打印机驱动程序部分中描述。


微软绘图仪驱动程序

微软绘图仪驱动程序是微软公司对支持HP图形语言(Hewlet-packard Graphics Language)的绘图仪的标准打印机驱动程序。对微软绘图仪驱动程序，主要包括以下几个主题：

■6.1绘图仪驱动程序能力

■6.2绘图仪驱动程序组件

■6.3绘图仪驱动程序的小驱动程序

■6.4绘图仪驱动程序的用户接口

■6.5绘图仪驱动程序的绘制器

6.1绘图仪驱动程序能力

微软的绘图仪驱动程序(MSPlot)提供下面的能力：

■通过对用HP图形语言的HPGL/2版本的使用，利用绘图仪特定模型的绘图仪驱动程序的小驱动程序手段来提供对所有绘图仪的支持。

■一个绘图仪驱动程序的用户接口，基于树形视图控件及属性表单，对所有的绘图仪都是一致的。

■一个绘图仪驱动程序的绘制器。与GDI图形引擎一起，将应用程序的Win32 GDI调用转换成为可以发送到打印假脱机的打印机命令。

为提供对新的HPGL/2兼容设备类型的支持，需要做的只是提供一个描述该设备的小驱动程序。

6.2绘图仪驱动程序组件

MSPlot组件由DLL及二进制数据文件组成，如下面的图中所示：

插入图？？？

图中的组件包括有：

应用程序

一个提供用户打印能力的应用程序

gdi32.dll

用户模式的DLL，它导出Win32 GDI函数。

内核模式的图形引擎

实现GDI函数的Windows NT 执行代码

小驱动程序

MSPlot小驱动程序(.pcd文件)

缓存的.pcd文件数据

从.pcd文件读来的小驱动程序数据

plotui.dll

绘图仪用户接口的DLL，为所有支持MSPlot的打印机提供通用的UI代码

compstui.dll

打印机的CPSUI用户接口

plotter.dll

绘图仪驱动程序绘制器，它绘制图像并发送图像数据流到假脱机

6.3绘图仪驱动程序文件示例

本DDK包括下面的MSPlot实例代码：

■绘图仪驱动程序用户接口DLL(plotui.dll)的源代码

■绘图仪驱动程序绘制器(plotter.dll)的源代码

■plotgpc.exe的源代码，它产生.pcd文件

■一个小驱动程序的文本文件的实例，它可用于作为plotgpc.exe的输入。

这些例子都放在该DDK树形目录的实例目录的子目录中。绘图仪驱动程序绘制器(plotter.dll)可以用户模式或内核模式之任一种来编译。更多的信息，请参考第7章中的选择用户模式或内核模式部分的内容。

6.4绘图仪驱动程序的小驱动程序

微软绘图仪驱动程序的特定模型的小驱动程序，是由厂商提供的、从描述设备特征文本文件产生的二进制的.pcd文件。

PCD文件

为产生一个.pcd文件，必须首先创建一个采用PCD源文件格式的文本文件，然后运行plotgpc.exe，该文件在DDK中提供。这一程序将转换一个文本文件为二进制的.pcd文件，用下面的命令语法：

plotgpc source-file-path.txt traget-file-path.pcd

对源文件及目标文件来说，必须明确地指定其文件的扩展名，不支持默认值。

一个实例的文本文件，可作为plotgpc.exe的输入的文件被包括于实例绘图仪驱动程序文件中。

关键字 值定义 默认值

PushPopPal | 1=驱动程序必须在TRL及HPGL2之间切换时推出或弹出调色板 0=不需要推出/弹出 | 0
RasterByteAlign | 1=设备必须根据x坐标的字节分配接收所有的光栅数据 0=不需要字节分配 | 0
RasterCap | 1=光栅设备 0=笔设备 | 0
RasterDPI | 两个DWORD大小的值表示x及y的精度，以DPI为单位 对光栅绘图仪来说，这是光栅精度 对笔绘图仪来说，这时GDI提供给应用程序的理想的精度 | {300.300}
RollFeedCap | 1=设备能进行转动进纸 0=不支持 | 0
ROPLevel | ROP_LEVEL_0=不支持光栅操作 ROP_LEVEL_1=ROP1支持 ROP_LEVEL_2=ROP2支持 ROP_LEVEL_3=ROP3支持 | ROP_LEVEL_0
RTLMonoEncode5 | 1=支持光栅传输语言(RTL)单色压缩模式5 0=不支持 | 0
RTLMonoFixPal | TRL单色模板0=白色，1=黑色 | 0
RTLMonoNoCID | 1=用RTL单色板式，不需要CID命令 0=用RTL单色模式，需要CID命令 | 0
RTLNoDPIxy | 1=不支持RTL DPI X，Y等移动命令 0=支持这些命令 | 0
TransparentCap | 1=设备支持透明模式 0=不支持 | 0
WindingFillCap | 1=设备支持绕组填充 0=不支持 | 0

笔描述(Pen Discriptions)

每一个笔描述必须具有下面的格式：

PlotpenData {Pen Number,Color}

这里，Pen Number标识笔的槽数(Slot Number)而Color是一个PC_IDX_为前缀的色彩标识符。下面是笔描述的例子。

PlotPenData {1,PC_ICX_WHITE}

PlotPenData {2,PC_ICX_BLACK}

PlotPenData {3,PC_ICX_RED}

表格描述

每一个表格描述必须采用下面的格式：

FormInfo{“Form Description”,Width,Length,Left Margin,Top Margin,Right Margin,Bottom Margin}

这里，Form Description是一个描述表格工字符串，Width和Length以1/1000mm单位指定表格的大小，页边也是以1/1000mm为单位指定的。下面是三个实例：

FormInfo{“Roll Paper 24 in ”,609600,0,0,0,0,0}

FormInfo{“ANSI A 8.5 x 11 in”,216900,279400,0,0,0,0}

FormInfo{“ISO A$ 210 x 297mm”,210000,297000,0,0,0,0}

6.5绘图仪驱动程序用户接口

绘图仪驱动程序用户接口用CPSUI创建下面的属性表单：

■打印机属性表单的设备设置(Device Settings)页面，当一个用户从打印文件夹或打印机窗口选择了属性菜单项目时它就会显示。该页面列出了打印机专用的配置信息。

■文档属性表单的布局(Layout)、纸张/质量(Paper/Quality)、高级(Advanced)等页面。这些页面会在用户选择了从打印文件夹或打印机窗口的打印优选项(Printing Preference)菜单项时显示。或者一个应用程序调用PrinterProperties或DocumentProperties函数(在平台的SDK文档中描述)。这些页面列出了特定文档的配置信息。

这些属性表单包含了绘图仪的特性及由绘图仪小驱动程序指定的选项。它们也允许用户修改选项值。

绘图仪用户接口是作为用户模式的打印机接口DLL实现的。该DLL中的代码与CPSUI相连，指定了属性表单页的内容。基于小驱动程序的信息DLL对绘图仪可组合的选项实施限制，这也确保了用户不选择没有安装在绘图仪上的选项。

6.6绘图仪驱动程序绘制器

绘图仪驱动程序绘制器是作为打印机图形DLL来实现的，并对图形驱动程序导出由设备驱动程序接口(DDI)定义的函数。当一个应用程序调用图形设备接口(GDI)函数将图像发送到绘图仪，内核模式的图形引擎将调用绘制器的DDI函数。这些DDI函数帮助GDI绘制一个打印作业的页面图像。

绘制器也同时负责根据命令序列向假脱机绘制图像数据，假脱机然后再把图像及命令直接传送到绘图仪硬件。


定制微软的打印机驱动程序

微软统一驱动程序(Unidrv)及微软PostScript打印机驱动程序(Pscript)的设计都是基于Windows® NT/Windows® 2000打印机驱动程序体系结构。因此，每一个都是由两部分构成——即一个打印机接口DLL和一个打印机图形DLL。本章讲述怎样定制这些组件。

为提供对Unidrv或Pscript的打印机接口DLL的定制，必须提供一个或多个用户接口的插件。可以使用这些插件来修改驱动程序的用户接口，并对某个打印机事件提供附加的处理。

为提供对Unidrv或Pscript的打印机图形DLL的定制，必须提供一个或多个绘制插件。可以用这些插件去修改将要发送到打印机假脱机中的在打印机作业数据流中的数据。

本章也解释了怎样实现打印机驱动程序的COM接口，它被插件用于与打印机驱动程序之间的通讯，也解释了如何安装定制的驱动程序组件。

7.1用户接口插件

这一节包含如下一些解释怎样开发用户接口插件的主题：

■7.1.1用户接口插件介绍

■7.1.2UI插件的例子

■7.1.3UI插件的COM接口

■7.1.4提供DEVMODE结构附加项

■7.1.5从UI插件存取驱动程序设置

■7.1.6修改一个驱动程序提供的属性表单页面

■7.1.7增加新的属性表单页面

■7.1.8定制其他的打印机接口操作

7.1.1用户接口插件介绍

当增加对一新的微软统一驱动程序(Unidrv)或者微软Postscript打印机驱动程序(Pscript)打印机设备的支持，可以通过修改打印机的属性表单或文档的属性表单来定制驱动程序的用户接口。

可以通过提供一个用户模式的DLL来完成这些，这一DLL即指的是用户接口的插件，或称UI插件。

一个UI插件可以通过增加、删除、或者替换在属性表单中的“设备设置(Device Settings)”页面的选项来修改打印机属性表单。它也可以增加新的页面。同样，插件可以通过增加、删除、替换在属性表单的“布局(Layout)”、“纸张/质量(Paper/Quality)”、“高级(Advanced)”页面的选项来修改文档属性表单，或者它也可以增加新页。

UI插件被称为Unidrv或Pscript的打印机接口DLL，用了一套COM接口。打印机接口DLL是通过使用CPSUI来实现的，并且一个UI插件通过驱动程序的打印机接口DLL与CPSUI进行间接的交互。因此，在开发UI插件之前，阅读CPSUI一章是非常有益的。

除修改打印机驱动程序的用户接口外，一个UI插件可以执行其他的操作，如处理某个打印机事件并报告被支持的能力。更多的信息，请参考定制其他打印机接口操作部分的内容。

7.1.2 UI插件的例子

在该DDK中提供了用户接口插件的源代码实例。其代码包含在\oemui源代码目录的子目录中，并且创建了一个可以与Unidrv使用的插件。

7.1.3 UI插件的COM接口

下面的COM接口是为微软打印机驱动程序与UI插件之间的通讯定义的：

■IPrintOEMUI COM Interface——允许为Unidrv或Pscript的打印机接口DLL调用UI插件。

■IPrintOemDriverUI COM Interface——对UI插件提供实用的操作。

7.1.3.1 IPrintOemUI COM 接口

IPrintOemUI COM接口实际提供了一种Unidrv或Pscript的打印机接口DLL与UI插件通讯的一种手段。IPrintOemUI COM由每一个UI插件来实现的。

下面的表格列出并描述了所有的由IPrintOemUI接口提供的方法。UI插件必须定义所有列出来的方法。如果一种方法不需要，它可以只返回E_NOTIMPL。

方法 描述

IPrintOEMUI::CommonUIProp | 允许一个用户接口插件修改一个已经存在的属性表单页面或者文档的属性表单页面
IPrintOEMUI::DeviceCapabilities | 允许一个用户插件去指定定制的设备能力
IPrintOEMUI::DevicePropertySheets | 允许一个用户插件给一个打印机设备的打印机属性表单增加新页
IPrintOEMUI::DevMode | 在一个用户接口插件上执行操作的私有DEVMODE成员
IPrintOEMUI::DevQueryPrintEx | 允许一个用户插件帮助确定是否一个打印作业是可打印的
IPrintOEMUI::DocumentPropertysheets | 允许一个用户插件给一个打印机设备的文档属性表单增加新页面
IPrintOEMUI::DriverEvent | 在处理驱动程序专用的、需要打印机驱动程序行为的事件时，由打印假脱机调用
IPrintOEMUI::FontInstallerDlgProc | 替换Unidrv字体安装程序的用户接口
IPrintOEMUI::GetInfo | (需要实现部分)返回一个用户接口插件标识符的信息
IPrintOEMUI::PrinterEvent | 允许一个用户接口插件处理打印机事件
IPrintOEMUI::PublishDriverInterface | (需要实现部分)提供一个对Unidrv或Pscript驱动程序的IPrintOemDrvierUI COM接口的指针
IPrintOEMUI::QuerycolorProfile | 允许一个打印机接口DLL为用户的色彩管理指定一个ICC的初始化文件profile
IPrintOEMUI::UpdateExternalFonts | 允许一个打印机接口DLL修改一个打印机的Unidrv字体格式文件(.uff文件)
IPrintOEMUI::UpgradePrinter | 允许一个用户接口插件升级存储于注册表中的设备选项值

更多的信息，可参考实现打印机驱动程序COM接口部分。

7.1.3.2 IPrintOemDriverUI COM接口

IPrintOemDriverUI COM接口允许一个UI插件去查看并修改由Unidrv及Pscript打印机接口DLL管理的信息。

下表列出并描述了所有由IPrintOemDriverUI接口定义的方法：

方法 描述

IprintOemDriverUI::DrvGetDriverSetting | 允许一个用户接口插件获得当前打印机特性的状态及其他内部信息
IprintOemDriverUI::DrvUpdateUISetting | 允许一个用户接口插件通知驱动程序修改了的用户接口选项
IprintOemDriverUI::DrvUpgradeRegistrySetting | 允许一个用户接口插件修改存储于注册表中的设备设置

更多的信息，参考实现打印机驱动程序COM接口部分。

7.1.4提供DEVMODE结构附加项

用户接口插件可以向DEVMODE结构增加附加的私有成员，如图所示：

插入图？？？

一个UI插件可以使用这些私有的DEVMODE成员存储与定制的打印机选项相关的值。通过修改驱动程序提供的属性表单页面，或增加新属性表单页面时，插件使得这些选项对一个用户可用。

如果UI插件增加一个私有的DEVMODE成员，被增加的成员必须通过OEM_DMEXTRAHEADER结构来预置。

不需要向DEVMODE结构增加成员，但如果做了，插件必须实现IPrintOemUI::DevMode方法。依赖于输入的参数，这一方法的目的是返回另外增加的DEVMODE成员的初始化、转换或有效的大小。

7.1.5从UI插件存取驱动程序设置

一个用户接口插件可以获得当前打印机特性及其他内部信息的状态。IPrintOemDriverUI::DrvGetDriverSetting COM接口方法是在为微软打印机驱动程序的打印机接口DLL中实现的并能被UI插件调用。

另外，下面的方法允许UI插件修改驱动程序信息。

■IPrintOemDriverUI::DrvUpdateUISetting 允许一个UI插件在用户已经修改了驱动程序的设定时通知驱动程序。

■IPrintOemDriverUI::DrvUpgradeRegistrySetting 允许一个UI插件修改在注册表中的设备设定，这样由旧的驱动程序版本使用注册表设定就可以被修改。

7.1.6修改一个驱动程序提供的属性表单页面

一个UI插件可以直接通过实现IPrintOemDriverUI::CommonUIProp方法及一个回调函数来修改Unidrv或Pscript提供的属性表单页面。

IPrintOemDriverUI::CommonUIProg方法被UI插件使用以指定一套CPSUI可以在打印机属性表单的“设备设置(Device Settings)”页面或者文档属性表单的“布局(Layou)”“纸张/质量(Paper/Quality)”“高级(Advanced)”页面中增加、删除或替换的选项项目。

回调函数，其类型为OEMCUIPCALLBACK，用于处理用户对定制选项项目的修改。

增加选项项目

UI插件必须描述新的选项项目，这是通过将它们放置于一个由驱动程序提供的OPTITEM结构的数组中来完成的。驱动程序打印机接口DLL两次调用UI插件的IPrintOemDriverUI::CommonUIProp方法。该方法第一次被调用时，它应当返回需要OPTITEM结构的数目，驱动程序对OPTITEM数组分配空间并在一个OEMCUIPPARAM结构中描述该数组。然后，驱动程序再次调用该方法，提供OEMCUIPPARAM结构的地址，这样该方法可以用选项描述去装载OPTITEM结构。

删除选项项目

从一个Unidrv或Pscript提供的属性表单页中删除一个选项，UI插件的IPrintOemDriverUI::CommonUIProp方法可以遍历由OEMCUIPPARAM结构指向的OPTIITEM结构的数组，可以设置OPTITEM结构的OPTIF_HIDE标识(注意，这种做法实际上并没有删除选项，它只是对用户隐藏了选项，这样它默认的值就不能被改变)。

替换选项项目

为在一个Unidrv或Pscript提供的属性表单页中替换一个选项，应当根据在删除选项项目部分的教学内容去删除已经存在的选项项目，然后根据增加选项项目部分的内容以创建一个新的选项项目以替换旧的项目。

处理对定制选项值的修改

为了处理用户对定制选项项目的修改，必须至少提供一个回调函数。可以指定一个单一的回调函数用于处理对文档属性表单及打印机属性表单的处理，或者可以分别为它们各自指定一个单独的函数，这些回调函数属于OEMCUIPCALLBACK类型。

对一个回调函数的指定只需将它的地址设置在OEMCUIPPARAM结构中即可。UI插件作为对自身的IPrintOemDriverUI::CommonUIProp方法的输入收到这一结构的地址。

当一个用户打开打印机的属性表单或者文档属性表单并修改选项时，CPSUI调用打印机驱动程序的打印机接口DLL。这一DLL处理包含于它自身的OPTITEM结构中的选项值，然后，对每一个UI插件，打印机接口DLL调用由IPrintOemDriverUI::CommonUIProp 前面指定的OEMCUIPCALLBACK类型的回调函数。

7.1.7增加新的属性表单页面

如果想给打印机接口为Unidrv及Pscript提供的属性表单增加一个新的页面，UI插件必须实现下面的IPrintOemUI方法：

■IPrintOemDriverUI::DevicePropertySheets

用于给打印机属性表单增加页面，当在一个用户从打印文件夹或打印机窗口选择了“属性(Properties)”菜单项时显示，或者当一个应用程序调用了PrinterProperties函数时显示(在平台的SDK文档中有描述)。

■IPrintOemDriverUI::DocumentPropertySheets

用于给文档的属性表单增加新页，它在一个用户从打印文件夹或打印机窗口上选择了“打印机优选项(Printer Preference)”的菜单选项时或者应用程序调用了DocumentProperties/AdvancedDocumentProperties函数(在平台的SDK文档中有描述)。

如果实现了其中的一种方法，通常会提供一个_CPSUICALLBACK类型的回调函数以处理用户的修改。这一回调函数必须调用IPrintOemDriverUI::DrvUpdateUISetting以在与用户接口相关的设定值被修改后通知驱动程序，而这个设定值是要存储于驱动程序的DEVMODE结构或注册表的关键字中。

7.1.8定制其他的打印机接口操作

一个UI插件可选择性地实现下面的任一种IPrintOemDriverUI方法：

■IPrintOemDriverUI::DeviceCapabilities

■IPrintOemDriverUI::DevQueryPrintEx

■IPrintOemDriverUI::PrinterEvent

■IPrintOemDriverUI::UpgradePrint

这些方法等同于由Unidrv及Pscript使用的用户模式的打印机接口DLL导出的相同名称的函数。这些定制的方法不替换在驱动程序的打印机接口DLL中的等同函数。在每一种情况下，打印机接口DLL函数被首选调用，然后驱动程序调用插件的定制方法。

7.2绘制插件

下面的主题描述了怎样定制打印机绘制操作：

■7.2.1绘制插件介绍

■7.2.2绘制插件实例

■7.2.3绘制插件的COM接口

■7.2.4定制的DDI函数

■7.2.5定制的PDEV结构

■7.2.6从绘制插件存取驱动程序设置

■7.2.7特定Pscript的定制绘制

■7.2.8特定Unidrv的定制绘制

7.2.1绘制插件介绍

当增加对一个新的打印机设备的微软通用打印机驱动程序(Unidrv)或微软PostScript打印机驱动程序(Pscript)的支持，可以实现COM接口方法以修改打印机发送给打印假脱机的数据。

可以通过提供用户模式的DLL来完成这一定制。这个DLL即被看作为一绘制插件。

有两种类型的定制被支持：

■提供一些DDI绘制函数的定制版本

■实现特定Unidrv或特定Pscript的COM接口方法，它们在修改被绘制的图象数据或扫描行数据流，或者在专用的注入点插入Postscript代码，这些都是在数据流被发送到假脱机之前进行的。

7.2.2绘制插件实例

这一DDK也提供有绘制插件的源代码示例。代码包含于\oemuni源代码的子目录中并创建了一个可用于Unidrv的插件。

7.2.3绘制插件的COM接口

下面的COM接口是为微软打印机驱动程序及绘制插件之间的通讯定义的。

■IPrintOemUni COM接口 ——允许Unidrv的打印机图形DLL调用绘制插件。

■IPrintOemDriver COM接口 ——给Unidrv的绘制插件提供实用操作。

■IPrintOemPS COM接口 ——允许Pscript的打印机图形DLL调用绘制插件。

■IPrintOemDriverPS COM接口 ——给Pscript的绘制插件提供实用操作。

7.2.3.1 IPrintOemUni COM接口

IPrintOemUni COM接口是Unidrv的打印机图形DLL与绘制插件通讯的手段。IPrintOemUni COM接口是通过每一个绘制插件来实现的。

下面列出并描述了所有由IPrintOemUni COM接口提供的方法。绘制插件必须定义列出所有的方法，如果一个方法不需要，则它必须返回E_NOTIMPL。

方法 描述

IPrintOemUni::CommandCallback | 允许一个绘制插件提供动态产生的打印机命令
IPrintOemUni::Compression | 允许一个绘制插件提供定制的位图压缩方法
IPrintOemUni::DevMode | 在绘制插件的私有DEVMODE成员上的执行操作
IPrintOemUni::DisableDriver | 释放由绘制插件IPrintOemUni::EnableDriver方法提供的插件
IPrintOemUni::DisablePDEV | 允许一个绘制插件删除私有的由IPrintOemUni::EnablePDEV方法分配的PDEV结构
IPrintOemUni::DownloadCharGlyph | 允许一个绘制插件给打印机下载专用的软字体字符
IPrintOemUni::DownloadFontHeader | 允许一个绘制插件下载字体头信息给打印机
IPrintOemUni::DriverDMS | 允许一个绘制插件指明它将用一个设备管理的绘制表面
IPrintOemUni::EnableDriver | 允许一个绘制插件分出(hook out)一些DDI函数
IPrintOemUni::EnablePDEV | 允许一个绘制插件创建它自已的PDEV结构
IPrintOemUni::FilterGraphics | 允许一个绘制插件修改扫描行数据并将它发送到假脱机
IPrintOemUni::GetImplementedMethod | (需要实现)允许Unidrv决定哪一个IPrintOemUni接口方法已经被绘制插件所实现
IPrintOemUni::GetInfo | (需要实现)返回绘制插件的标识信息
IPrintOemUni::HalftonePattern | 允许一个绘制插件创建或修改一个过渡调色调色板，并且在它被用于过渡调色的操作之前
IPrintOemUni::ImageProcessing | 允许一个绘制插件修改图像位图数据，以执行色彩格式或过渡调色
IPrintOemUni::MemoryUsage | 允许一个绘制插件指定被IPrintOemUni::ImageProcessing方法需要的内存的总量
IPrintOemUni::OutputCharStr | 允许一个绘制插件控制字体符号的打印
IPrintOemUni::PutlishDriverInterface | (需要实现)提供一个Unidrv驱动程序的IPrintOemDriverUni COM接口
IPrintOemUni::ResetPDEV | 允许一个绘制插件重置它的PDEV结构
IPrintOemUni::SendFontCmd | 允许一个绘制插件修改一个打印机的字体选择命令并然后将它发送到打印机
IPrintOemUni::TextOutAsBitmap | 允许一个绘制插件创建一个文本字符串的位图图像
IPrintOemUni::TTDownloadMethod | 允许一个绘制插件去指明Unidrv应当为一个专用的TrueType字体使用的格式
IPrintOemUni::TTYGetInfo | 允许一个绘制插件给Unidrv提供与打印机相关的文本信息

更多的信息，可以参考实现打印机驱动程序COM接口部分。

7.2.3.2 IPrintOemDriver COM接口

IPrintOemDriver COM接口提供一个绘制插件，它能够存取Unidrv的图形DLL提供的功能操作。这些操作包括发送数据流到打印机假脱机并获取驱动程序管理的信息。

下表列出并描述所有的由IPrintOemDriverUni接口定义的方法。

方法 描述

IPrintOemDriverUni::DrvGetDriverSetting | 返回打印机特性的当前状态及其他内部信息
IPrintOemDriverUni::DrvGetGPDData | 使绘制插件获得在打印机GPD文件中定义的数据
IPrintOemDriverUni::DrvGetStandardVariable | 使绘制插件获得Unidrv的标准变量的当前值
IPrintOemDriverUni::DrvUniTextOut | 使绘制插件使用一个设备管理的绘制表面以很容易地输出文本字符串
IPrintOemDriverUni::DrvWriteSpoolBuf | 发送打印机数据到假脱机
IPrintOemDriverUni::DrvXMoveTo | 通知Unidrv关于x位置的变化
IPrintOemDriverUni::DrvYMoveTo | 通知Unidrv关于y位置的变化

更多的信息，参考实现打印机驱动程序COM接口部分。

7.2.3.3 IPrintOemPS COM接口

IPrintOemPS COM接口是Pscript的打印机图形DLL与绘制插件通讯的手段。IPrintOemPS接口是由每一个绘制插件来实现的。

下表列出并描述了所有由IPrintOemPS提供的接口。绘制插件必须定义所有的被列出的方法。如果一个方法是不需要的，则它应返回E_NOTIMPL。

方法 描述

IPrintOemPS::Command | 允许一个绘制插件插入postscript命令到打印作业的数据流中
IPrintOemPS::DevMode | 在绘制插件的DEVMODE的私有变量上的执行操作
IPrintOemPS::DisableDriver | 释放由绘制插件的IPrintOemPS::EnableDriver分配的资源
IPrintOemPS::DisablePDEV | 允许一个绘制插件删除私有的PDEV结构，该结构是由它的IPrintOemPS::EnablePDEV方法分配的
IPrintOemPS::EnableDriver | 允许一个绘制插件分出一些DDI函数
IPrintOemPS::EnablePDEV | 允许一个绘制插件创建它的PDEV结构
IPrintOemPS::GetInfo | (需要实现)返回绘制插件的标识信息
IPrintOemPS::PublishDriverInterface | (需要实现)提供一个Pscript驱动程序的指向IPrintOemPS COM接口的指针
IPrintOemPS::ResetPDEV | 允许一个绘制插件重置它的PDEV结构

更多的信息，参考实现打印机驱动程序COM接口部分的内容。

7.2.3.4 IPrintOemDriverPS COM接口

IPrintOemDriverPS COM接口提供一个可以存取Pscript的、由打印机图形DLL提供的功能进行操作的绘制插件。这些操作包括发送数据流到打印机假脱机以及获取驱动程序管理的信息。

下面的表格列出并描述了由IPrintOemDriverPS接口定义的方法：

方法 描述

IPrintOemDriverPS::DrvGetDriverSetting | 返回打印机特性的当前信息以其他内部信息
IPrintOemDriverPS::DrvWriteSpoolBuf | 发送打印机数据到假脱机

更多的信息，参考实现打印机驱动程序COM接口部分的内容。

7.2.4定制的DDI函数

一个绘制插件可以分出一些DDI函数，提供这些函数的定制实现。为提供定制的函数，绘制插件必须实现IPrintOemUni::EnableDriver或IPrintOemPS::EnableDriver方法，这两个方法被填写在DRVENABLEDDATA结构并具有每一个函数的地址。

一个绘制插件可以分出一个DDI函数，但只是在该函数是由Unidrv或Pscript驱动程序定义时。对这些函数的列表，参考DDK中包括的IPrintOemUni::EnableDriver或IPrintOemPS::EnableDriver。

如果提供一个特殊的定制的分出函数，该函数优先于驱动程序的等同DDI函数。当设计一个定制的分出函数，可以有下面的选项：

■分出函数可以完全在内部处理DDI操作。

■分出函数可以回调打印机驱动程序等同的DDI函数。

通过调用驱动程序的DDI函数，分出函数可以执行预处理或后处理的函数参数，但是仍然允许驱动程序实际执行DDI操作。对绘制插件的IPrintOemUni::EnableDriver或IPrintOemPS::EnableDriver方法的一个参数是DRVENABLEDDATA 结构包含的指向驱动程序DDI函数的指针。如果想回调这些函数，应当存储这些结构的内容。

对来说，也许有必要提供一个定制的PDEV结构。可以通过每一个分出函数作为输入接收的SURFOBJ结构指针，从一个DDI的分出函数中引用这一结构。特定地，SURFOBJ结构的dhpdev成员指向一个DEVOBJ结构，并且DEVOBJ结构的pdevOEM成员指向定制的PDEV结构。

7.2.5定制的PDEV结构

绘制插件通过实现下面的方法来支持私有的PDEV结构：

IPrintOemUni::EnablePDEV或IPrintOemPS::EnablePDEV

IPrintOemUni::DisablePDEV或IPrintOemPS::DisablePDEV

IPrintOemUni::ResetPDEV或IPrintOemPS::ResetPDEV

PDEV结构是一个一般的项目，它指向一个私有的，本地定义的结构并由定义它的模块来使用。通常，它用于存储物理设备特征。每一个打印机驱动程序，以及每一个绘制插件，定义它自已的PDEV结构。没有被定义的全局范围的“PDEV”类型的结构。

7.2.6从绘制插件存取驱动程序设置

一个绘制插件可以获取打印机特性的当前状态及其他内部驱动程序信息。下面的COM接口方法在微软打印机驱动程序内实现在方法可以被绘制插件来调用：

由Unidrv实现的方法：

IPrintOemDriverUni::DrvGetDriverSetting

IPrintOemDriverUni::DrvGetStandardVariable

IPrintOemDriverUni::DrvGetGPDData

由Pscript实现的方法：

IPrintOemDriverPS::DrvGetDriverSetting

7.2.7特定Pscript的定制绘制

Pscirpt允许特定设备的定制代码注入Postscript命令到Pscript发送给打印机设备的数据流中。如果想提供这种类型的定制代码，必须提供一个绘制插件以实现IPrintOemPS::Command方法。

Pscript在打印机作业数据流的很多点调用IPrintOemPS::Command方法。函数的参数之一指定了一个表示当前在数据流中点的位置的索引值。函数的每次调用，它可以检验索引的值并提供另外的数据流或者不提供。

7.2.8特定Unidrv的定制绘制

下面的主题提供了关于Unidrv允许绘制插件执行的操作的类型的信息：

■7.2.8.1定制的色彩格式

■7.2.8.2定制的过渡调色

■7.2.8.3定制的数据流压缩

■7.2.8.4定制的数据流过滤

■7.2.8.5定制的字体管理

■7.2.8.6动态产生的打印机命令

■7.2.8.7处理设备管理的表面

7.2.8.1定制的色彩格式

Unidrv支持好几种色彩格式，这些色彩格式在微软统一打印机驱动程序一章的处理色彩格式中列出。对这些格式来说，Unidrv在向打印机发送之前，转换GDI位图到正确的格式。如果打印机接受Unidrv不支持的格式，必须提供一个交换插件以实现IPrintOemUni::ImageProcessing方法。

如果实现IPrintOemUni::ImageProcessing方法，并且用户已经选择了Unidrv不能处理的色彩格式(色彩模式选项)，这样，每次一个缓冲区中的GDI位图数据准备打印时，Unidrv调用该方法并传寄位图的地址作为一个输入的参数。该方法必须转换位图到专用的格式，如果需要，将执行定制的过渡调色操作，并调用IPrintOemUni::DrvWriteSpoolBuf方法以发送被修改了的位图到打印机假脱机。它也必须调用IPrintOemUni::DrvXMoveTo及IPrintOemUni::DrvYMoveTo方法以修改光标的位置。更多的关于这些操作的信息，参考关于IPrintOemUni::ImageProcessing的描述。

如果一个绘制插件实现IPrintOemUni::ImageProcessing，它也能实现IPrintOemUni::MemoryUsage方法。

7.2.8.2定制的过渡调色

如在微软统一打印机驱动程序一章中的用Unidrv过渡调色部分所解释的，Unidrv允许GDI执行过渡调色操作，也可以由打印机设备、或由定制的驱动程序代码来执行。本部分解释了怎样在定制的驱动程序代码中执行过渡调色的操作。

有两种类型可用的定制：

■定制的过渡调色模式

■定制的过渡调色方法

定制的过渡调色模式

过渡调色模式可以在资源DLL中指定，或者它们可以由一个实现IPrintOemUni::HalftonePattern方法的绘制插件来产生。如在微软统一驱动程序一章用Unidrv过渡调色中所解释的，实现IPrintOemUni::HalftonePattern有两个原因：

1．定制的模式是在一个资源DLL中提供的，并且该模式是加密的。

2．定制的模式不是在资源DLL中提供的，相反，是由IPrintOemUni::HalftonePattern产生的。

IPrintOemUni::HalftonePattern方法的目的是给Unidrv返回一个可用的取中色模式，它按次序传递给GDI。该方法可以解码一个以加密格式存储于资源DLL中的模式，或者它可以在执行过程中产生一个模式。

如果实现IPrintOemUni::HalftonePattern，GPD文件必须在每一个取中色的的*Option条目中包括一个*HTCallbackID属性，这一个*Option条目是用于使用哪一种定制的模式时指定过渡调色的方法。更多的关于这一属性的信息，可参考第4章微软统一打印机驱动程序的取中色间特性的选项属性部分。

定制的过渡调色方法

对一个使用Unidrv的打印机来说，提供实现定制的过渡调色的方法的代码需要下面的步骤：

■提供一个实现IPrintOemUni::ImageProcessing方法的绘制插件。

■在打印机的GPD文件中包括一个过渡调色的*Feature条目，用每一个包含*Option的条目表示一个过渡调色方法。(也可以包括标准的或定制的过渡调色方法)

IPrintOemUni::ImageProcessing方法作为输入收到GDI位图，该方法必须执行过渡调色操作，基于当前选择的过渡调色方法，并返回结果位图到Unidrv。

如果一个绘制插件这现了IPrintOemUni::ImageProcessing，它也可以实现IPrintOemUni::MemoryUsage。

更多的关于过渡调色的信息，参考第4章微软统一打印机驱动程序中的用Unidrv过渡调色部分的内容。

7.2.8.3定制的数据流压缩

Unidrv允许数据压缩操作由定制的代码来执行。

为执行定制的压缩操作，必须做下面的内容：

■提供一个实现IPrintOemUni::Compression方法的绘制插件。

■在GPD文件中包括一个CmdEnableOEMComp命令条目。更多的关于这一命令的信息，参考第4章微软统一打印机驱动程序中的光栅数据压缩命令部分的内容。

IPrintOemUni::Compression方法作为输入收到扫描行数据。该方法必须压缩数据并将结果返回到Unidrv。CmdEnableOEMComp命令条目指定必须发送到打印机的命令，从而使打印机可以接收这些压缩数据的命令。对要发送到打印机的每一个扫描行，Unidrv调用IPrintOemUni::Compression以压缩这些扫描行，然后，如果这是唯一可用的压缩方法，Unidrv在压缩数据之后，向打印机发送由CmdEnableOEMComp命令条目指定的命令。

如果打印机小驱动程序包括允许Unidrv支持的压缩方法的GPD条目，则Unidrv将对每一个扫描行数据试用每一种压缩算法并选择产生最好结果的压缩算法。更多的关于Unidrv的压缩能力的信息，参考第4章微软统一打印机驱动程序中的压缩光栅数据部分的内容。

在同一时间，只能有一种定制压缩算法被允许。

7.2.8.4定制的数据流过滤

Unidrv允许定制的代码执行最终图像数据的后处理，并在其被送至假脱机之前。这样的处理包括删除相邻的点或者其他任何类型的Unidrv不能提供的数据过滤操作。

为执行最终的图像数据的后处理，必须提供一个实现IPrintOemUni::FilterGraphics方法的绘制插件。

IPrintOemUni::FilterGraphics方法接收扫描行数据作为输入，该方法必须处理数据并将数据通过调用IPrintOemUni::DrvWriterSpoolBuf发送到假脱机。如果IPrintOemUni::FilterGraphics方法被实现，Unidrv不支持假脱打印机数据。相反，它发送每一个数据块到IPrintOemUni::FilterGraphics方法。

7.2.8.5定制的字体管理

Unidrv对PCL打印机支持下载的软字体作为位图或TrueType字体的轮廓。对设备字体，Unidrv支持PCL、CAPSL以及PPDS打印机命令格式。对其他格式，定制的字体管理代码必须被在一个绘制插件中提供，下面的一系列IPrintOemUni方法，可以被实现。

IPrintOemUni::DownloadFontHeader

用于从Unidrv获得软字体的头信息并将信息下载到打印机

IPrintOemUni::DownloadCharGlyph

用于给打印机下载一个软件字体的字符集

IPrintOemUni::OutputCharStr

用于控制字母的打印

IPrintOemUni::SendFontCmd

用于修改一个打印机的设备字体选择命令，如果必要，然后就发送它到打印机。

IPrintOemUni::TextOutAsBitmap

用于创建文本字符串的位图图像

IPrintOemUni::TTDownloadMethod

用于指定Unidrv在向打印机发送专用的软件字体时应当使用的符号格式。

Unidrv提供了一个回调函数，UNIFONTOBJ_ GetInfo，那个绘制的插件可以调用获得字体或字符的信息。

对设备字体，字体描述必须被以Unidrv字体规格文件(.ufm文件)及翻译表文件(.gtt文件)的格式来提供。

对Cartridge字体，可以在资源DLL中提供字体描述并在GPD文件中用Cartridge字体条目来指定。字体描述可以Unidrv字体格式文件(.uff文件)的形式提供。

对可下载的PCL软件字，字体描述必须以Unidrv字体格式文件(.uff文件)来提供。

Unidrv字体规格文件

打印机支持的每一种设备字体必须用Unidrv字体规格文件(.ufm文件)来表示。.ufm文件是一个用在DDK的打印机驱动程序函数及结构一章中的Unidrv字体规格中描述的结构组建的二进制文件。.ufm中的第一个结构是UNIFM_HDR，它包含有到文件的其他结构的偏移量。

更多的关于创建.ufm文件的信息，可参考第四章中的微软小驱动程序开发工具部分的内容。

Unidrv也支持.ifi文件，是一种为Windows NT 4.0创建的字体规格文件。

字符翻译表文件

打印机支持的每一种设备字体文件必须用一个字符翻译表文件(.gtt文件)来表示，一个.gtt文件是一个用在Unidrv字符翻译表一章中的打印机驱动程序函数及结构部分中描述的结组构建的二进制文件。.gtt中的的第一个结构是UNI_GLYPHSETDATA，它包含有到文件的其他结构的偏移量。

更多的关于创建.gtt文件信息，参考第4章中的微软小驱动程序开发工具部分的内容。

Unidrv也支持为Windows NT 4.0创建的字符翻译文件，它用RLE压缩并具有一个.rle的扩展名。

Unidrv字体格式文件

对那些没有在GPD文件中用字体Cartridge条目指定的Cartridge字体来说，该字体必须以Unidrv字体格式文件(.uff文件)来描述。另外，下载的PCL软字体必须用.uff文件来指定。

一个.uff二进制文件是用下面的结构组建起来的。

■Unidrv字体格式结构。它定义了内容及.uff文件的结构。

■Unidrv字体规格结构。它定义了每种字体的规格。

■Unidrv字符翻译表结构。它定义了字体所用的字符集。

另外，对下载的PCL软字体来说，二进制数据存储于.ufm文件中。

创建.uff文件是商家提供的字体安装软件的责任。Unidrv读取一个打印机的.uff文件以获得字体及字符信息。字体安装程序应当在字体被增加或删除时修改.uff文件内容。更多的关于创建字体安装程序的信息，参考定制的Unidrv的字体安装程序部分的内容。

打印机的所有.uff文件必须被保存在%SystemRoot%\System32\Spool\Drivers\Unifont目录中。为关联单独的.uff文件与专用的打印机，安装软件必须调用SetPrinterData函数(在平台的SDK文档中有描述)以在每一个打印机的注册关键字中创建注册值，并指明每一个值的维持者。

注册表值名称及类型 值定义 维持者

“ExternalFontFile” REG_SZ | 一个.uff文件的文件名，指定当前被安装的字体。字体可以被下载或者包含在一个Cartridge中 | 字体安装程序
“ExtFontCartFile” REG_SZ | 一个.uff文件的文件名，指定所有所有包含在字体Cartridge中为“ExtFontCartNames”列出的所有字体 | 字体安装程序
“ExtFontCartNames” REG_MULTI_SZ | 所有的可以被安装在一个打印机上的Cartridge字体 | 字体安装程序
“FontCart” REG_MULTI_SZ | 当前安装在一个打印机上的所有的字体Cartridge | Unidrv用户接口

在给一个打印机增加了字体Cartridge之后，系统管理者将运行字体安装程序，它负责从由“ExtFontCartFile”指定的.uff文件中拷贝字体描述到由“ExternalFontFile”指定的.uff文件中。同样，字体安装程序必须删除.uff中在一个Cartridge字体被删除时由“ExtFontCartFile”指定的字体描述。

7.2.8.6动态产生的打印机命令

每一次在一个GPD文件中为一个Unidrv的小驱动程序指定打印机命令时，可以采用下面两种方法中的一种：

■把命令字符串放于GPD文件中

当将命令字符串放于GPD文件中时，Unidrv在适当的时间发送命令到打印机假脱机。这些命令字符串可以包括标准变量，这些变量在Unidrv发送命令之前都会进行求值。

■提供一个回调函数

如果提供一个回调函数，Unidrv在它发送命令时调用该函数，该函数主要负责发送命令到打印机假脱机。这就使得可以包括那些动态产生的命令字符串并然后将它送往打印机。

为设置一个命令字符串在GPD文件中，需要在命令的*Command条目中包括一个*Cmd属性。

为提供动态产生的命令字符串的代码，必须做下面的事情：

■提供一个绘制的插件以实现IPrintOemUni::CommandCallback方法。

■包括一个*CallbackID命令属性，并包括一个可选的*Param属性，在GPD文件的命令的*Command条目中。

当Unidrv准备发布一个打印机命令，它检查小驱动程序的数据库以确定是否该命令已经用一个*Cmd属性或一个*CallbackID属性指定。在前一种情况下，Unidrv发送命令字符串到打印假脱机；在后一种情况，Unidrv调用IPrintOemUni::CommandCallback方法，传递*CallbackID及*Params值作为输入参数。

打印机命令被在第4章微软打印机驱动程序中讨论过。更多的关于*Cmd、*Callback及*Params属性的信息，同样参考第4章中的命令属性部分。

7.2.8.7处理设备管理的表面

当Unidrv绘制器打印页面图像时，它用GDI管理的绘制表面。所有的图像被作为位图绘制。对设备提供能力不能在此种情形下采用，如绘制向量等。可以对一个设备管理的绘制表面提供定制的驱动程序支持。为支持一个设备管理的表面，必须提供一个插件以实现下面的内容：

■对所有的Unidrv支持的DDI绘制函数的一套分出函数。必须能分出下面的这些函数

DrvAlphaBlend

DrvBitBlt

DrvCopyBits

DrvDitherColor

DrvFillPath

DrvGradientFill

DrvLintTo

DrvPlgBlt

DrvRealizeBrush

DrvStretchBlt

DrvStretchBltROP

DrvStrokeAndFillPath

DrvStrokePath

DrvTextOut

DrvTransparentBlt

■IPrintOemUni::EnableDriver方法，它用于向Unidrv提供指向DDI分出函数的指针。

■IPrintOemUni::DriverDMS方法，它通知Unidrv一个设备管理的表面被应用，并指定对这一表面哪一个被定义的分出函数将被用到。

当绘制一个设备管理的表面时，分出函数不能回调GDI的以Eng-为前缀的支持服务。但是，它们可以创建一个临时的位图表面，并传递那个表面的句柄到以Eng-为前缀的函数(参考第3章中的绘制一个打印作业部分)。

一个打印作业要被绘制的每一次，都要调用IPrintOemUni::DriverDMS方法，因此绘制插件可以指定对每个作业绘制表面的类型(GDI管理或设备管理)。基于所需要的用户接口上的可选择的选项或者是提供一个用户接口插件以进行表面选择。

在设备管理的表面上绘制文本

绘制插件必须分出Unidrv的DrvTextOut函数(以及其他DDI绘制函数)。为一个设备管理的表面创建文本并包括在以下四个函数之间的相互作用：

■Unidrv的DrvTextOut函数

■绘制插件的DrvTextOut分出函数

■Unidrv的IPrintOemUni::DrvUniTextOut方法

■绘制插件的IPrintOemUni::TextOutAsBitmap方法

步骤包括在设备管理的表面上显示文本，如下：

1．GDI调用Unidrv的DrvTextOut函数。

2．Unidrv调用绘制插件的DrvTextOut分出函数。

3．分出函数发送命令到设备以指定文本刷、旋转及裁剪区域。

4．分出函数调用Unidrv的IPrintOemUni::DrvUniTextOut方法，它试图使用可下载的字体到输出文本。这一方法也处理基于字符的剪贴。

5．如果IPrintOemUni::DrvUniTextOut不能用可下载的字体(由于该字体不能获得或者旋转)，它调用插件的IPrintOemUni::TextOutAsBitmap方法，它当绘制一个位图来绘制文本。

6．在IPrintOemUni::DrvUniTextOut返回后，DrvTextOut分出函数必须绘制下划线及中横线，基于由DrvTextOut函数的prclExtra参数指定的距形，并使用矢量命令(如果支持)。

7.3实现打印机驱动程序的COM接口

这一部分解释了怎样基于DDK提供的示例代码构建一个插件。也解释了用于打印机驱动程序及插件通讯的调用顺序。提供了下面的主题：

■打印机驱动程序的接口标识符

■创建插件

■从打印机驱动程序存取插件接口

■从插件存取打印机驱动程序接口

7.3.1 打印机驱动程序的接口标识符

在prcomoem.h中定义了一套GUID结构，在这些GUID结构中的每一个都是用于打印机驱动程序(Unidrv及Pscript)及插件之间通讯的一个COM接口的接口标识符。

对Windows 2000，定义了下面的GUID：

IDD_IPrintOemUI

IDD_IPrintOemDriverUI

IDD_IPrintOemUni

IDD_IPrintOemDriverUni

IDD_IPrintOemPS

IDD_IPrintOemDriverPS

每一个GUID标识一个接口的版本。对Windows 2000，每一个接口只有一个版本，随着未来的一些版本的操作系统的发布，也许需要去修改一个或多个接口。如果定义了一个新版本的接口，一个新的GUID将被加入到这一列表中。

用户接口插件及绘制插件必须鉴别它们支持的接口版本。打印机驱动程序(Unidrv或Pscript)将调用一个插件的IUnknown::QueryInterface方法(在平台的SDK文档中有描述)，指定一个接口标识符作为输入。如果插件支持专用的版本，方法必须返回一个接口的指针并返回有S_OK的状态值。否则，它必须返回E_NOINTERFACE。驱动程序用一个对最新版本的接口标识符开始并继续使用以前版本的标识符调用QueryInterface，直到方法返回S_OK或者驱动程序穷举完列表中的版本标识符为止。

同样，Unidrv及Pscript为IPrintOemDriver、IPrintOemDriverUni及IPrintOemDriverPS COM接口提供IUnknown::QueryInterface方法。插件则应当调用合适的接口的QueryInterface方法以确定驱动程序的支持接口版本并接收一个接口指针。

7.3.2创建插件

所有的打印机驱动程序插件必须定义DllMain，DllGetClassObject及DllCanUnloadNow函数。他们也必须实现IclassFactory COM接口及IPrintOemUI，IPrintOemUni或IPrintOemPS COM接口中之一。

当创建一个用户接口插件或者一个绘制插件，必须使代码基于这一DDK提供的UI插件的实例或绘制插件的实例。

■定义一个DllMain函数(在平台的SDK文档中有描述)，这是所有Win32 DLL的入口点。

■定义并导出一个DllGetClassObject函数(在平台的SDK文档中有描述)。

打印机驱动程序调用这一函数以获得对插件实现的IClassFactory接口(在平台的SDK文档中有描述)的存取。当驱动程序调用DllGetClassObject，它指定下面一类的标识符(在prcomoem.h中定义)

CLSID_OEMNI——对UI插件

CLSID_OEMRENDER——对绘制插件

驱动程序也指定IID_IClassFactory的接口标识符。

DllGetClassObject函数必须创建一个它的IclassFactory接口的实例并为它返回一个指针，如在示例代码所示。

■实现IClassFactory COM接口

IClassFactory接口的CreateInstance方法应当创建一个下面的COM接口的插件实现的实例：

IPrintOemUI COM接口

IPrintOemUni COM接口

IPrintOemPS COM接口

CreateInstance方法的一个输入是接口的标识符。驱动程序用IID_Iunknown的一个接口标识符调用CreateInstance，意思是CreateInstance方法必须为被创建的实例的IUnknown接口(在平台的SDK文档中描述)返回一个指针，如在示例代码中所示。

■实现IPrintOemUI，IPrintOemUni或者IPrintOemPS COM接口中的一种，包括标准的IUnknown接口，如在例子代码中所示。

由驱动程序调用的第一个被实现的方法是IUnknown接口的QueryInterface方法(在平台SDK文档中有描述)。这一方法为打印机以一个输入参数收到一个接口的标识符，驱动程序调用该方法以确定插件支持那一个版本的接口并收到指向支持接口的指针。

■定义并导出DllCanUnloadNow函数(在平台的SDK文档中有描述)。

如果所有被实现的插件的IPrintOemUI，IPrintOemUni或者IPrintOemPS接口的实例已经被发布，DllCanUnloadNow函数必须返回S_OK，返回S_OK意指对驱动程序来说，插件不能被加载。

7.3.3从打印机驱动程序存取插件接口

如果一个UI插件或绘制插件已经被安装，打印机驱动程序(Unidrv或Pscript)将使用下面的调用序列以获取对插件的IPrintOemUI，IPrintOemUni或者IPrintOemPS接口的存取。

1．驱动程序调用LoadLibrary以加载插件的DLL，它引起插件的DllMain函数被调用。

2．驱动程序调用插件的DllGetClassObject函数，它返回一个指向插件的IDlassFactory接口的指针。

3．驱动程序调用IClassFactory接口的CreateInstance方法，指定一个IID_Unknown的接口标识符，它引起方法创建插件的IPrintOemUI，IPrintOemUni或者IPrintOemPS接口的实例并返回指向实例的IUnknown接口的指针。

4．驱动程序调用IUnknown接口的QueryInterface方法以确定插件支持IPrintOemUI，IPrintOemUni或者IPrintOemPS接口的那一版本，并接收一个指向支持接口的指针。

5．驱动程序调用插件的接口的PublishDriverInterface方法以使得驱动程序的IPrintOemUI，IPrintOemUni或者IPrintOemPS接口对插件可用。

6．如果插件已经实现了IPrintOemUni接口，驱动程序调用IPrintOemUni::GetImplementedMethod以确定哪一个接口方法已经被实现。

7.3.4从插件存取打印机驱动程序接口

如果一个插件能够调用属于驱动程序的IPrintOemUI，IPrintOemUni或者IPrintOemPS接口，它必须从驱动程序获得一个指针，如下：

1． 插件必须实现IPrintOemUI，IPrintOemUni或者IPrintOemPS接口的PublishDriverInterface方法。

2． 当驱动程序(Unidrv或Pscript)调用插件的PublishDriverInterface方法，它提供一个指向IPrintOemUI，IPrintOemUni或者IPrintOemPS实例的IUnknow接口。

3． 插件必须使用IUnknown接口指针去调用Iunknown::QueryInterface，指定表示是期望的版本的IPrintOemUI，IPrintOemUni或者IPrintOemPS接口的接口标识符(更多的信息，可参考打印机驱动程序的接口标识符部分)。

4． 如果插件指定一个接口的标识符表示一个驱动程序支持的接口，QueryInterface返回一个指向IPrintOemUI，IPrintOemUni或者IPrintOemPS接口的指针。注意，驱动程序在返回对插件的接口指针之前调用接口的AddRef方法(在平台的SDK文档中描述)。插件应当保存该指针，这样它可以在以后被用于调用接口方法。

5． 当IPrintOemUI，IPrintOemUni或者IPrintOemPS接口指针不再需要时，插件必须调用接口的Release方法(在平台的SDK文档中有描述)。

7.4安装定制的驱动程序组件

当为微软打印机驱动程序提供定制的组件时，必须为组件的安装提供一个.ini文件(如果打印机不被ntprint.inf支持，必须提供一个printer.inf文件)。

.ini文件必须包含一个OEMFiles部分，在这一部分，每一个定制的组件是用下面之一的条目来描述的：

OEMDriverFilen

命名一个绘制插件

OEMConfigFilen

命名一个用户接口插件

这时，n指定了安装程序安装文件的顺序，对所有类型的插件来说，为n指定的数字必须是连续的，从1开始。

例如，如果提供两个绘制插件及一个用户接口插件，打印机模型是XYZ，.ini文件必须如下所示：

[OEMFiles]

OEMDriverFile1=XYZDRV1.DLL

OEMConfigFile1=XYZUI1.DLL

OEMDriverFile2=XYZDRV2.DLL

在等号(=)之前或之后，都不允许有空格，文件名不能名含路径说明。

在例子中，指定了两个绘制插件，基于为OEMDriverFilen指定的n值。xyzdrv1.dll在xyzdrv2.dll之前被安装。Unidrv及Pscript驱动程序调用插件，并按其安装的顺序，此后，当一个驱动程序需要调用DDI分出函数及由这些插件提供的COM方法时，xyzdrv1.dll将在xyzdrv2.dll之前被调用。

.ini文件名应当反映打印机产品名称并且是唯一的以避免在.ini文件中与其他打印机名称相冲突。注意如果提供一个与Windows NT 4.0后端口(Back-Porting)通信的绘制插件或用户接口插件。.ini文件名必须与.gpd及.ppd文件匹配。(即xyz.ini必须用于xyz.gpd或xyz.ppd)。这一限制对Windows 2000及以后版本不适用。

一个.ini文件可以包含ANSI或UNICODE文本，但是推荐使用UNICODE文本。在一个.ini文件中，每行以#开始的是注解的内容。

更多的关于.inf及.ini文件的信息，参考Windows 2000驱动程序开发指南中的第一卷，即插即用，电源管理及建立设计指南部分。也可参考第4章微软统一打印机驱动程序中的安装一个Unidrv小驱动程序部分。

7.4.1Unidrv的定制的字体安装程序

对没有在打印机的GPD文件中通过字体Cartridge条目描述的Cartridge字体，需要厂商提供的字体安装程序软件。这些字体需要用Unidrv字体格式文件(.uff文件)来描述，而厂商提供的字体安装程序就负责创建.uff文件。

厂商提供的字体安装程序应当提供对下载的PCL软字体的支持。

下面两种技能为创建定制的安装程序提供：

■提供一个用户接口插件

插件必须实现下面的COM接口方法：

IPrintOemUI::FontInstallerDlgProc

IPrintOemUI::UpdateExternalFonts

■提供一个单独的执行文件

在字体安装时，可执行文件必须通过调用SetPrinterData(在平台的SDK文档中描述)存储它的名字在注册表中，并为“FontInstaller”关键字指定一个值。

Unidrv用下面的算法定位字体安装程序：

1.如果字体安装程序可执行文件的名字被存储于注册表中，Unidrv不允许系统管理员从打印机的属性表单中选择字体安装程序。相反，管理员必须运行提供的可执行文件。

2.如果一个安装程序执行文件不可用，Unidrv允许从打印机的属性表单中选择字体安装操作。Unidrv确定是否一个用户接口插件已经被安装。如果是，它的字体安装方法被调用。如果一个用户接口插件没有被安装，或者它的字体安装方法返回了E_NOTIMPL，则驱动程序使用它自已的容错安装程序。


定制打印假脱机组件

本章解释怎样定制打印机假脱机组件，并提供了以下这些主题：

■编写打印处理器

■编写打印监示器

■编写网络打印提供者

8.1编写打印处理器

本部分提供下面的主题：

■8.1.1对打印处理器的介绍

■8.1.2打印处理器示例

■8.1.3由打印处理器定义的函数

■8.1.4处理一个打印作业

■8.1.5安装一个打印处理器

8.1.1对打印处理器的介绍

打印处理器是用户模式的一些DLL，主要负责转换打印作业的假脱数据到打印机监示器的格式。也负责处理应用程序对暂停、重新开始及撤消打印作业等的请求。

打印作业的假脱数据包含在一个假脱文件中。打印处理器读取这些文件，在数据流上执行转换操作，并将转换的数据写到假脱机。假脱机然后发送数据流到合适的打印机监示器。

微软Windows 2000包括列在下表中的打印处理器。

打印处理器 输入数据类型 输出数据类型

winprint.dll | EMF RAW TEXT | RAW
stmpsprt.dll | PSCRIPT1 | RAW

关于数据类型的信息，参考下面的主题：

■EMF数据类型

■RAW数据类型

■TEXT数据类型

■PSCRIPT1数据类型

可以创建一个定制的打印处理器以支持一个Windows 2000不支持的数据类型。也可以提供一个定制的打印处理器以支持一种或更多种的被支持的数据类型，这样，就允许修改由打印处理器所提供的能力。

打印处理器与在打印机驱动程序安装时与打印机的驱动程序相关，因此，可以并存多个打印处理器支持同一种数据类型。更多的信息，参考安装打印机处理器部分的内容。

EMF数据类型

增强型元文件(EMF:Enhanced Metafile)数据类型由调用GDI函数的指令构成。打印处理器必须调用GDI函数以绘制可打印的图像。GDI函数对打印驱动程序的打印机图形DLL做调用，它绘制图像并将它作为RAW数据向假脱机发送(通过调用EngWritePrinter)。

Windows NT®/Windows 2000客户端发送EMF数据到Windows NT/Windows 2000打印服务器。EMF数据是设备独立并可以比RAW数据类型更快速地发送到服务器。当一个应用程序请求是从本地到服务器，打印作业也就被假脱处理为EMF数据，这样就允许在作业被后台假脱机绘制时能够对应用程序的快速返回。

更多的关于EMF数据类型的信息，参考Windows 2000专业版资源包(Windows 2000 Professional Resource Kit)或Windows 2000服务器版的资源包(Windows 2000 Server Resource Kit)。更多的关于增强型元文件的信息，参考平台SDK文档。

RAW数据类型

RAW数据可以不用进一步处理就被发送到打印机监示器。打印机处理器只发送这一数据到假脱机(通过调用WritePrinter，在平台的SDK文档中描述)，有时插入表格。一个RAW数据文件的实例是一个由PCL命令组成的。无论客户端或服务器端那一方不支持Windows NT/Windows 2000 EMF或者服务器管理员禁止了EMF支持，打印作业以RAW数据格式从客户端发送到服务器端。在这些情况下，客户端的图像绘制在打印作业发送到服务器端之前进行。

(Postscript命令可以被当作RAW数据，如果目标打印机支持Postscript。另一方面，stmpsprt.dll打印处理器将Postscript的输入向非Postscript的打印机进行解释，在这种情况下，Postscript不是RAW数据。)

更多的关于RAW数据类型的信息，参考Windows NT工作站资源指南或者Windows NT资源指南部分的内容。

TEXT数据类型

单纯由ANSI文本构成的。打印机处理器调用GDI以用打印机设备容错字体绘制字母，并发送RAW格式的结果输出到假脱机(通过调用Writeprinter，在平台的SDK文档中描述)。处理器等同于用写字板打开输入文件并然后打印文件(该格式用于不能打印文本字符的打印机)。

更多的关于TEXT数据类型的信息，参考Windows NT工作站资源指南或者Windows NT 服务器资源指南部分的内容。

PSCRIPT1数据类型

PSCRIPT1数据的目的是要在一个非Postscript打印机上打印的Postscript数据。打印处理器解释Postscript命令并且向输出文件写RAW格式的结果数据。

更多的关于PSCRIPT1数据类型的信息，参考参考Windows NT工作站资源指南或者Windows NT 服务器资源指南部分的内容。

8.1.2打印处理器示例

genprint.dll的源代码，一个接受EMF数据、RAW数据及TEXT数据作为输入的打印处理器的实例，包含于本DDK中。其代码位于DDK实例的目录树中的genprint的子目录中。

8.1.3由打印处理器定义的函数

打印机处理器必须导出下表列出的函数：

函数名称 描述

ClosePrintProcessor | 关闭打印处理器
ControlPrintProcessor | 提供对打印文档的控制
EnumPrintProcessorDatatypes | 枚举打印机处理器支持的数据类型
GetPrintProcessorCapabilities | 为专用的输入数据类型返回打印处理器的能力
OpenPrintProcessor | 为打印打开打印处理器
PrintDocumentOnPrintProcessor | 在一个打印处理器上打印文档

8.1.4处理一个打印作业

当一个假脱机准备发送一个打印作业到打印处理器时，它调用打印处理器的OpenPrintProcessor函数。这一函数执行初始化活动并返回一个句柄。

假脱机可以调用PrintDocumentOnPrintProcessor，它是打印处理器的一个函数，可以从输入格式到输出格式转换数据流，并返回被转换的流到假脱机。

如果输入的格式是Windows NT/Windows 2000的EMF，则PrintDocumentOnPrintProcessor函数将通过使用GDI的打印处理器函数来控制EMF记录的回放。这些函数提供了一个打印处理器与打印机驱动程序之间的接口。该接口允许打印处理器控制打印机页面的物理布局及实现诸如在一个物理页面上打印多个文档页面(“N-up”打印)、反向打印、每页打印多个拷贝等等。

一个打印处理器的输出数据流必须返回到假脱机。通常，如果数据转换需要与打印机驱动程序的打印机图形DLL(对EMF格式作为输入数据时)之间交互作用，则图形DLL通过调用EngWritePrinter。另外一方面，如果转换不调用打印机图形DLL(当输入数据是RAW类型时)，则打印处理器调用WritePrinter(在平台的SDK文档中描述)。

PrintDocumentOnPrintProcessor函数可以从假脱机到打印处理器的ControlPrintProcessor函数同步调用中断。这一函数实现应用程序的暂停、恢复及撤消一个打印作业等能力。

在PrintDocumentOnPrintProcessor结束转换数据流并返回后,假脱机调用打印机处理器的ClosePrintProcessor函数。

8.1.4.1为打印处理器使用GDI函数

一套用户模式的GDI函数被gdi32.dll导出，对被打印处理器用于处理Windows NT/Windows 2000的EMF作为输入格式。下面的表列出了可以提供的函数：

函数名称 描述

GdiDeleteSpoolFileHandle | 释放一个假脱机文件句柄
GdiEndDocEMF | 对一个打印文档完成EMF回放操作
GdiEndPageEMF | 为一个物理页面完成EMF回放操作，并从打印机跳页
GdiGetDC | 返回一个打印机设备设备环境的句柄
GdiGetDevmodeForPage | 返回一个文档页面的DEVMODE结构
GdiGetPageCount | 返回文档页面的数字
GdiGetPageHandle | 返回文档页面的句柄
GdiGetSpoolFileHandle | 返回一个假脱文件的句柄，它对其他的GDI函数是作为一个必需的输入
GdiPlaypageEMF | 播放与文档页面相关的EMF记录
GdiResetDCEMF | 重置打印机的设备设备环境
GdiStartDocEMF | 对打印作业执行初始化操作
GdiStartPageEMF | 对物理页面执行初始化操作

一个EMF打印处理器的PrintDocumentOnPrintProcessor应当调用GdiGetSpoolFileHandle以获得假脱文件的句柄及调用GdiGetDC以获得打印机设备设备环境句柄。然后它可以执行下面的操作：

■对每一个打印作业文档，GdiStartDocEMF必须在任何EMF记录被运行之前调用并且GdiEndDocEMFnt必须在EMF记录运行完之后被调用。

■对每一个要被打印的物理页面来说，GdiStartPageEMF必须在任何文档页面在页面上被绘制之前被调用，GdiEndPageEMF必须在最后一个文档页面被在物理页面上绘制完之后被调用

■对每一个将要绘制在物理页上的文档页面来说，GdiGetDevmodeForPage是否必须被调用取决于DEVMODE结构的内容是否在最后一个文档页被绘制之后改变。如果DEVMODE已经改变，一个新的物理页而必须开始(通过调用GdiEndPageEMF及GdiStartPageEMF)，打印机的设备设备环境必须通过调用GdiResetDCEMF来改变。一个文档页面必须被绘制在物理页面上，通过首先调用GdiGetPageHandle以获得文档页面句柄，并然后调用GdiPlayPageEMF以绘制页面。

在打印作业已经完成绘制后，打印处理器必须调用GdiDeleteSpoolFileHandle。

如果一个打印处理器在它开始打印页面之前需要知道总的打印页数(如反向打印时)，它可以调用GdiGetPageCount，但是这一函数将一直到假脱过程完成并且在假脱时禁止了打印能力时才返回。

如果一个打印处理器用这些GDI函数，它的EnumPrintProcessorDataTypes函数必须返回“NT EMF”作为一个支持的数据类型，它表示一般的Windows 2000 EMF格式。打印处理器不能修改EMF记录。

8.1.5安装一个打印处理器

为安装一个打印处理器，一个安装应用程序必须调用假脱机的AddPrintProcessor函数，它在平台的SDK文档中描述。

为关联一个打印机处理器与一个打印机驱动程序，它的文件名称必须通过使用PintProcessor条目而被列在.inf文件中。这一条目必须为每一个将要被与打印处理器关联的打印机驱动程序所包含。更多的信息，可以参考第10章的打印机INF文件部分的内容。

当一个安装应用程序调用假脱机的AddPrinter函数，指定一个PRINTER_INFO_2结构作为一个输入参数，它指定打印机处理器名称(从.inf文件获得)作为一个结构成员。(AddPrinter函数及PRINTER_INFO_2结构在平台的SDK文档中描述)。

8.2编写打印监示器

打印监示器负责传送打印机数据流从打印假脱机到一个合适的端口驱动程序。两种类型的打印机监示器被定义，即——语言监示器(language monitors)及端口监示器(port monitors)。这一章将描述两种监示器，并提供设计与实现的指南。

提供以下内容的主题：

■8.2.1语言监示器

■8.2.2端口监示器

■8.2.3语言和端口监示器交互

■8.2.4由打印监示器定义的函数

■8.2.5初始化一个打印监示器

■8.2.6打开及关闭一个端口

■8.2.7打印一个打印作业

■8.2.8管理端口

■8.2.9为在集群打印服务器中使用转换打印监示器

■8.2.10安装打印监示器

8.2.1语言监示器

语言监示器是一些用户模式的DLL，主要用于两个目的：

■它们在打印机假脱机与双向打印机之间提供了一个全双工的通信通道，从而具有了提供软件可存取的状态信息的能力。

■增加打印机控制信息到数据流，如由打印机作业语言定义的命令等。

微软提供了一个语言监示器pjlmon.dll，它支持打印作业语言(PJL:Printer Job Language)，并为PJL打印机提供双向通讯。这一监示器被作为语言监示器示例包含在这一DDK中。

对单向或双向打印机来说，定制的语言监示器可编写用于支持其他作业控制语言。

语言监示器是可选的，并且只与特殊的包含在打印机的.inf中的打印机类型相关，如在安装打印监示器部分所描述的。

如果一个语言监示器与打印机关联，语言监示器从打印机处理器收到打印机的数据流，修改它并将之传递到打印机的端口端示器。更多的信息，可参考语言监示器及端口监视器交互部分的内容。

8.2.1.1语言监示器示例

pjlmon.dll的源代码，支持双向PJL语言的语言监示器就包括在这一DDK中。其代码位于DDK的实例目录树中的\pjlmon子目录中。

8.2.2端口监示器

端口监示器由用户模式的一些DLL组成。它们负责在用户模式的打印假脱机及访问I/O硬件端口的内核模式的端口驱动程序之间提供通讯。一个端口监示器通常使用CreateFile，WriteFile，ReadFile及DeviceIOControl函数，在平台的SDK文档中描述的，以与内核模式的端口驱动程序通讯。端口监示器也负责管理及配置一个服务器的打印机端口，如在管理端口部分所描述的。

Windows NT/Windows 2000用户的“打印机”视图实际上是一个打印队列，是一个或多个物理打印机设备都可以连接到的队列。一个端口是一个在打印队列及一个单一打印机设备之间的物理连接。。每一个端口端示器支持一个或多个的一种或多种端口类型的实例。例如，localmon.dll这一示例的端口监示器可以支持所有的服务器的本地COM及LTP端口。(打印机文件夹)通过调用平台SDK文档中描述的AddPrinter函数来分配端口到端口监视器)。

对表示多个打印机设备(通过多个端口)的打印机队列来说，假脱机发送每个打印作业到第一个可用的端口。如果端口监示器指明一个可用端口忙或者遇到错误，假脱机将重新提交作业到队列，指定另外一个端口监示器支持的可用端口。

除了localmon.dll，Windows 2000提供几个附加的端口监示器。Windows NT服务器资源开发包中对每一个这样的端口监示器都做了描述。

可以写定制的端口监示器以支持附加类型的硬件I/O端口。

对Windows 2000来说，每一个端口监示器被分为两个DLL。

端口监示器UI DLL

端口监示器的用户接口DLL包含用户接口的功能并在打印客户系统上执行。

这一DLL必须驻留在客户系统的System32子目录中。

端口监示服务器DLL

一个端口监示器的服务器DLL包含端口通讯功能及在打印服务器上执行。它必须显示一个用户的接口。

UI DLL通过调用假脱机的Xcvdata函数与服务器的DLL通讯，在该DDK中包含一个端口监示器实例。

8.2.2.1端口监示器实例

localmonn.dll的源代码，即支持本地LPT及COM端口的端口监示器也在DDK中包含着。其代码位于DDK实例目录树的localmon及localui子目录中。

8.2.3语言和端口监示器交互

下面的图显示了打印机数据从打印处理器到打印机的路径图。其中图(a)包含一个与它相关联的语言监示器，图(b)不包含相关联的语言监示器。

插入图(a)(b)???

如果一个语言监示器在打印机的安装过程中与打印机关联，语言监示器从假脱机打印处理器中收到打印机的数据流。语言监示器修改数据流并将它传递到打印机的监示器。

许多由打印机定义的监示器对语言监示器及端口监示器是相同的。通常，如果一个语言监示器在数据流通道中，假脱机将调用端口监示器的实现函数而语言监示器将调用端口监示器的同样的实现函数。例如，在PJL语言监示器(pjlmon.dll)中的WritePort函数增加PJL命令到数据流然后调用端口监示器的WritePort并将该流发送给端口驱动程序。

如果没有安装语言监示器，假脱机调用端口监示器的函数实现。

由于语言监示器及端口监示器是打印体系中分散的组件，定制的及微软提供的监示器可以被一起应用。这样，可以提供一个与微软提供的端口监示器一起工作的定制的语言监示器，反之亦然。

也可以提供一个单一的语言和端口监示器构成(combined language and port monitor)的组合式打印监示器。

8.2.3.1组合式语言和端口监示器

专用的打印机硬件可以被一个单一的定制打印监示器支持，它实际作为一个组合式的语言和端口监示器。如果这样一个监示器需要用户的交互以获得配置参数，它必须被分成UI DLL及一个服务器的DLL，下面的模型是对端口监示器的。而语言相关的功能则属于服务器DLL。

一个组合的监示器UI DLL必须定义端口监示器客户端DLL函数。它的服务器DLL必须定义端口监示器服务器DLL的函数及语言监示器的函数。

8.2.4由打印监示器定义的函数

下面的部分列出了在打印监示器中必须定义的函数：

■8.2.4.1语言监示器函数

■8.2.4.2端口监示器客户DLL函数

■8.2.4.3端口监示器服务器DLL函数

8.2.4.1语言监示器函数

下表列出了语言监示器必须定义的函数：

函数名称 描述

DllEntryPoint | DLL入口点，通常称为DLLMain，它在平台的SDK文档中描述
ClosePort | 关闭一个端口，在没有打印机与它相连时
EndDocPort | 在一个端口上执行打印作业完成的任务
GetPrinterDataFromPort | (可选)轮询存储在注册表中的端口值
InitializePrintMonitor2 | 初始化打印监示器并返回一个实例句柄
OpenPortEx | 为一个新连接的打印机打开一个端口
ReadPort | 从一个打印机端口读数据
StartDocPort | 执行在一个端口上开始打印作业的操作
WritePort | 向打印机端口写数据

8.2.4.2端口监示器客户DLL函数

下面的表格列出了一个端口监示器UI DLL必须定义的函数：

函数名称 描述

DllEntryPoint | DLL入口点，通常称为DLLMain，它在平台的SDK文档中描述
AddPortUI | 创建一个端口并通过显示一个通话框获得配置信息
ConfigurePortUI | 配置一个前面增加的端口
DeletPortUI | 删除一个端口
InitializePrintMonitorUI | 初始化端口监示器的DLL

8.2.4.3端口监示器服务器DLL函数

下面的表中列出了端口监示器服务器DLL必须定义的函数：

函数名称 描述

DllEntryPoint | DLL入口点，通常称为DLLMain，它在平台的SDK文档中描述
ClosePort | 关闭一个端口，在没有打印机与它连接时
EndDocPort | 在端口上执行打印作业完成的任务
EnumPorts | 枚举打印机可用的服务器上的端口
GetPrinterDataFromPort | (可选)发送一个I/O控制代码到端口驱动程序并返回结果
InitializePrintMonitor2 | 初始化打印监示器并返回一个实例句柄
OpenPort | 打开一个打印机端口
ReadPort | 从一个打印机端口读数据
SetPortTimeOuts | (可选)在一个打开的端口上设置超时值
StartDocPort | 执行在一个端口上要开始打印作业的任务
WritePort | 向打印机端口写数据
XcvClosePort | 在端口管理完成之后关闭一个端口
XcvDataPort | 处理端口管理任务
XcvOpenPort | 因为管理目的打开一个端口

8.2.5初始化打印监示器

当假脱机调用LoadLibrary以装载一个打印监示器DLL，系统立即调用DLL的DllEntryPoint函数。有一个一般情况都比较好的方法是，对入口函数都调用DisableThreadLibraryCalls函数(在平台的SDK文档中有描述)，这样DLL就在线程被创建或删除时就不会没有通知到。

每一个DLL导出一个初始化函数，它在假脱机调用LoadLibrary之后调用。语言监示器DLL及端口监示器服务器DLL导出一个InitializePrintMonitor2函数。端口监示器UI DLL导出一个InitaializePrintMonitorUI函数。

这两个初始化函数负责给打印监示器定义的其余的函数返回指针，这样假脱机可以调用他们。初始化函数可以执行装载时的初始化操作。显示器的InitalizePrintMonitor2函数返回一个监示器的实例句柄。监示器应当分配本地内存以存储特定实例的信息，并使用监示器句柄作为一个分配内存的标识符。

第一次调用假脱机时，它装载所有的已经被安装的监示器DLL。在调用了监示器函数之后，假脱机调用每一个端口监示器的EnumPorts函数，它枚举监示器支持的端口。(一个监示器支持一个端口，是看是否该端口已被加到监示器的数据库中，如在增加一个端口中所描述的。)每一个支持的端口这时才被打开，如在打开及关闭端口部分中所描述的。

8.2.6打开及关闭一个端口

在增加一个端口之后，如在增加端口部分所描述的，假脱机就能通过调用合适的语言监示器的OpenPortEx函数打开它。

语言监示器用OpenPortEx函数以创建并返回端口句柄。通常，语言监示器调用它相关的端口监示器的OpenPort函数，并且语言监示器只返回从端口监示器的OpenPort函数获得的句柄。

如果语言监示器与一个端口不相关，假脱机直接调用端口监示器的OpenPort函数。

假脱机不允许在同一时间有多于一个的通道。这样，当它在一个特殊的监示器中调用OpenPortEx(或OpenPort)之后，就不会在关闭它之前企图打开同一个端口。

在一个端口被打开之后，假脱机能调用附加的函数以打印作业，如在打印一个打印作业部分中所描述的，用端口的句柄作为一个输入的参数。应当编写一个监示器，在一个端口被打开之后，假脱机可以在关闭端口之前发送多个打印作业。

若一个作业必须通过一个不同的语言监示器发送，如果没有与端口相关的打印队列或者当系统关闭时，则假脱机关闭一个端口。为关闭端口，假脱机调用语言监示器的ClosePort函数，该函数使端口被打开时创建的句柄无效。语言监示器通常调用ClosePort函数，该函数是由它相关的端口监示器定义的。

8.2.7打印一个打印作业

在一个端口被打开之后，如在打开及关闭端口部分所描述的，假脱机可以发送打印作业到端口。

每个打印作业通过假脱机调用语言或端口监示器的StartDocPort或EndDocPort函数来被分隔开。假脱机在打印处理器调用假脱机的StartDocPrinter及EndDocPrinter函数时调用这些函数，这些函数都在平台的SDK文档中有描述。在StartDocPort及EndDocPort函数的范围之间，可以出现对监示器的WritePort，ReadPort，GetPrinterDataFromPort函数的不限次数的调用。

这些函数中的每一个都需要由OpenPortEx(或OpenPort)返回的端口句柄做为一个输入参数来指定。通常，语言监示器通过调用在它的相关的端口监示器中的相同名字的函数来实现每一个函数。

当假脱机调用一个语言监示器的WritePort函数以发送数据流到一个端口，函数经常增加一些特定语言的信息(如PJL命令)到收到的数据流中，并且是在它被传送到相关联的端口的WritePort函数之前。

ReadPort函数用于获得双向打印机硬件的状态信息，双向打印机硬件的语言监示器可以通过调用SetPort向假脱机发送，在平台的SDK文档中描述。假脱机不调用ReadPort函数。

如果打印机硬件是双向的，它的语言监示器或端口监示器应当支持GetPrinterDataFromPort函数。语言监示器的GetPrinterDataFromPort函数应当接受一个注册表的值名称作为输入，并从那个名称(一般是调用相关端口的WritePort及ReadPort函数)获得一个值并将值返回给调用者。一个端口监示器的GetPrinterDataFromPort 函数应当接受一个I/O控制的代码作为输入,调用DeviceIoControl(在平台的SDK文档中有描述)以传递控制代码到端口的驱动程序,并返回结果。

8.2.8管理端口

端口管理活动由增加端口、配置端口及删除端口等对应用程序(如打印文件夹)的响应活动构成。这些端口管理活动被端口监示器处理，不是语言监示器，并需要管理员的优先权限。对存储端口配置信息有特殊的规则。

一个附加的、可选的管理活动包括设置端口超时值。

8.2.8.1增加端口

增加一个端口由存储端口的名字及用户可修改的在端口监示器服务器DLL的本地存储中或注册表中的配置信息构成。

当应用程序调用打印假脱机的AddPort函数(在平台的SDK文档中描述)，它指定端口监示器的名称作为函数的参数。假脱机调用包含在指定的端口监示器UI DLL中的AddPortUI函数。

端口监示器的UI DLL的AddPortUI函数应当执行下面的操作：

1．调用打印假脱机的OpenPrinter函数(在平台的SDK文档中有描述)，它引起端口监示器服务器DLL中的XcvOpenPort被调用。

2．多次调用打印假脱机的xcvData函数，以请求端口监示器服务器DLL增加端口并在UI DLL和服务器DLL之间传递配置信息。XcvData函数调用服务器DLL的XcvDataPort函数。AddPortUI函数通常获得从用户显示对话框栏显示配置信息。

3．调用打印假脱机的ClosePrinter函数(在平台的SDK文档中描述)，它引起端口监示器服务器DLL中的XcvClosePort函数被调用。

关于这些操作的更多信息，参考AddPortUI函数的描述，也可以参考存储端口配置部分的内容。

端口监示器的EnumPorts函数必须枚举所有被增加的端口。假脱机可以调用每一个端口监示器的EnumPorts函数以确定在一个打印机服务器上可支持的端口集合。

8.2.8.2配置端口

配置一个端口是指修改一个端口监示器服务器的DLL的以前增加的端口的存储的配置信息。

当一个应用程序调用打印假脱机的ConfigPort函数(在平台的SDK文档中有描述)。ConfigurePort函数调用包含在适当的端口监示器UI DLL中的ConfiguePortUI函数。

端口监示器的UI DLL的ConfigPortUI函数应当执行下面的操作：

1．调用打印假脱机的OpenPrinter函数(在平台的SDK文档中有描述)，它引起在端口监示器服务器DLL中的XcvOpenPort函数被调用。

2．一次或多次调用打印假脱机的XcvData函数，以在UI DLL及服务器DLL之间传输配置信息。XcvData函数调用服务器DLL的XcvDataPort函数，ConfigurePortUI函数通常通过从用户显示的对话框获得配置信息。

3．调用打印假脱机的ClosePrinter函数(在平台的SDK文档中有描述)，它导致端口监示器服务器DLL中的XcvClosePort函数被调用。

关于这些操作的更多信息，可以参考ConifigurePortUI的描述。也可以参考存储端口配置信息部分的内容。

8.2.8.3删除端口

删除端口由删除端口的名称及删除用户可修改的端口监示器服务器DLL本地存储或注册表中的配置信息构成。

当一个应用程序调用打印假脱机的DeletePort函数(在平台的SDK文档中描述)，DeletePort函数调用包含在适当的端口监示器UI DLL中的DeletePortUI函数。

端口监示器的UI DLL的DeletePortUI函数应当执行下面的操作：

1．调用打印假脱机的OpenPrinter函数(在平台的SDK文档中描述)，它导致端口监示器服务器DLL中的XcvOpenPort函数被调用。

2．一次或多次调用打印假脱机的XcvData函数，以请求端口监示器服务器DLL删除端口。XcvData函数调用服务器DLL的XcvDataPort函数。

3．调用打印假脱机的ClosePrinter函数(在平台的SDK文档中描述)，它导致端口监示器服务器DLL中的XcvClosePort函数被调用。

关于这些操作的更多信息，参考DeletePortUI的描述。

8.2.8.4设定端口超时值

如果为一个具有可修改的超时值的端口写端口监示器，超时值应当被监示器的OpenPort函数初始化。例如，在localmon.dll中的OpenPort函数，即端口监示器实例，为了这个目的调用SetCommTimeouts函数(在平台的SDK文档中描述)。

8.2.8.5存储端口配置信息

Windows 2000打印假脱机可以在集群或非集群的服务器环境中执行。当假脱机在一个服务器集群中时，打印监示器配置信息必须被存储在集群注册表中。另一方面，如果假脱机运行在一个单一的、非集群的服务器系统中，所有配置数据必须被存储在服务器的本地注册表中。

打印假脱机定义了一套注册表函数以备打印监示器使用。这些函数传送配置数据到适当的注册表中，因此，打印监示器不需要决定服务器是否集群。打印监示器不能直接使用Win32注册表API或者集群注册表API；所有的配置数据必须可以用假脱机的注册表函数来存储和存取。当假脱机调用监示器的InitializePrintMonitor2函数时，这些函数的地址在一个MONITORREG结构中被提供给打印监示器。

在一个服务器集群中，可以并存多个假脱机的实例。特别是，每个集群结点处理它自身的实例，并且一个附加的实例为集群自身存在。假脱机注册表函数的一个输入参数是假脱机句柄。这一句柄由监示器的InitializePrintMoitor2函数收到并标识假脱机已经打开的监示器的实例(结点或集群)。用假脱机句柄，假脱机注册表函数对每一个假脱机实例保持一个子键。

8.2.9为在集群打印服务器中使用转换打印监示器

打印服务器集群是Windows 2000的新特性。任一准备运行于Windows2000集群的打印机端口监示器都需要修改，这样才可以被假脱机的实例(结点的假脱机及一个集群的假脱机)所调用。下面的步骤必须执行：

■监示器的InitializePrintMonitor函数必须被InitializePrintMonitor2函数替换。后一个函数返回一个监示器实例句柄。

■全局存储变量必须被移动到本地分配的内存，并且这一内存必须与由InitializePrintMonitor2函数返回的监示器句柄相关联。

■对Win32注册表API的调用必须被对假脱机注册表的调用函数所替换，它的地址在MONITORREG结构中传递到监示器(参考存储端口配置信息)。

■端口监示器必须被分成一个端口监示器UI DLL和一个端口监示器服务器DLL。UI DLL必须通过调用假脱机的XcvData函数与服务器DLL通讯。

■必须增加一个Shutdown函数。

那些没有被转换的打印监示器只能被用于一个非集群的环境中。他们不能被用于集群服务器。

一旦一个运行在Windows2000的集群结点上的打印机端口监示器已经建立起了连接(跨网络或者本地)，端口监示器应当从假脱机在适当的时间里进行的调用返回。(默认的假脱机资源超时值是180秒，参考设置端口超时值部分的内容获得更多信息。)

当发生从一个集群结点到另外一个结点的失效情况，假脱机必须等待所有当前打印作业完成或失败。如果一个未完成的作业保持在一个端口监示器中期待着假脱机资源的超时，随着打印机的临时性丢失，假脱机也许可以在线返回到一个不完全的状态。这将会影响那些连接到这些丢失的打印机上的用户。

8.2.10安装打印监示器

这一部分描述可以用于安装打印监示器的方法(注意，如果打印机如果不被ntprint.inf支持，必须提供一个.inf文件来指定打印机。关于.inf文件的更多信息，可以参考即插即用、电源管理及建立设计向导部分的内容。)

安装语言监示器

为安装一个语言监示器，必须使用LanguageMonitor条目在.inf文件中列出它的文件名。这一条目必须被每一个控制打印机时需要使用语言监示器的打印机驱动程序都包括。更多的信息，参考第10章打印机INF文件部分。

增加驱动程序向导或者增加打印机向导将会去读这个.inf文件并安装与打印机驱动程序相关的的语言监示器。

作为选择，定制安装程序也可以通过调用假脱机的AddMonitor函数来安装语言监示器，并明确地只安装一个专用的监示器DLL。

(AddMonitor函数在平台的SDK文档中有描述)

安装一个端口监示器

为安装一个端口监示器，安装媒体必须包括一个叫做monitor.inf的打印机.inf文件。这个.inf文件必须基于%windir%\system32\printmon.inf，即默认的安装端口监示器的.inf文件。应当制做一份printmon.inf的拷贝，将它改名为monitor.inf，删除包含在该文件中的所有端口监示器，并为端口监示器增加说明。

如果提供一个.inf文件，像上面描述的那样，系统管理员可以通过使用增加打印机向导来安装端口监示器，或者通过使用在打印文件夹的文件(File)菜单中的服务器属性选中时被显示的属性表单来安装端口监示器。

作为选择，一个定制的安装程序可以通过调用AddMonitor函数来安装语言监示器DLL。

8.3编写网络打印提供者

偶尔，厂商也许想提供一个打印提供者以支持新的网络体系。提供新的打印提供者功能的可能方式就是创建一个全新的打印提供者，它替换了本地打印提供者。但是在实现中，这是一个挑战并且很可能是一个不必要的任务。另一种变更的方法是创建一个部分的打印提供者，它与本地打印提供者一起工作。

这一部分提供了下面的主题：

■8.3.1部分打印提供者概述

■8.3.2支持打印机变化的通知

■8.3.3安装一个网络打印提供者

8.3.1部分打印提供者概述

一个局部提供者DLL通常只实现管理打印队列及打印作业的提供者函数的定制版本。局部提供者只在打印客户系统上执行并依赖于驱动程序管理操作及产生打印机数据的本地打印提供者。多个局部提供者可以存在于一个客户机系统上。

在第2章中的打印提供者定义的函数一章中，某些函数被标识为“必需的”。部分打印提供者必须提供所有必需的函数。部分打印提供者一般不实现任何可选的函数。必需的函数属于下面这些函数组中：

■初始化函数

■打印队列管理函数

■打印作业创建函数

■打印作业调度函数

■端口管理函数

对部分打印提供者来说，打印机端口应当被看作与打印队列是相同的。对任何函数都接收一个PRINT_INFO_2结构(在平台的SDK文档中描述)，该结构的 pPort成员应当被设置成打印队列的名字。这样，如果打印队列名是\\Server\Printer1，端口名称应当也是\\Server\Printer1。部分打印提供者的EnumPorts函数(在平台的SDK文档中描述)的实现必须返回一个端口名\\Server\Printer1 。

如在第二章的打印提供者介绍中所讲述的，一个应用程序对OpenPrinter的调用将引起假脱机的发送器以调用每一个打印提供者，直到其中之一认出了专用的打印队列并返回一个句柄。

应该记住非常重要的一点，即一个部分打印提供者不替换本地提供者。一旦创建了一个用户连接，每一个对提供者函数的调用都通过本地提供者被发送，它或者自身处理这个呼叫或者将它重发送到局部提供者。所有的对那些标识为“必需的”提供者函数的调用都将从本地提供者重发送到适当的局部提供者。

局部提供者不产生打印作业，他们依赖于本地提供者并且它的打印处理器创建可以发送到打印机的RAW数据。当一个打印处理器调用本地提供者的StartDocPrinter函数(参考打印一个打印作业部分)，并且打印队列是由局部提供者支持的，本地提供者调用局部提供者的StartDocPrinter函数，提供RAW数据(作为一个文件)。局部提供者的StartDocPrinter，WriterPrinter及EndDocPrinter函数应当在网络上发送RAW数据到远端的打印队列。

8.3.2支持打印机变化的通知

应用程序可以请求打印队列通过调用假脱机的FindFirstPrinterChangeNotification,FindNextPrinterChangeNotification及FindClosePrinterChangeNotification函数(在平台的SDK文档中有描述)发生的事件通知。如果认为应用程序的开发者可以为部分打印提供者支持的打印队列请求事件通知，必须在提供者中如下支持事件通知：

■提供一个FindFirstPrinterChangeNotification函数

假脱机调用这一函数以给打印提供者下面这些信息：

1．一套标识应用程序请求通知的打印机事件类型的标记。

2．一个消息请求的打印队列的句柄。

3．一个应用程序请求的当事件发生时被提供的信息类型的列表。

函数必须返回指明是否提供者应当被轮询，以决定已经有变化发生的一个标记值。(非轮询的提供者在发生变化时发送信号到客户端。一个必须被轮询的提供者在发生变化时不发送信号到客户端。相反，假脱机则以规则的间隔时间向客户端发送信号，无论是否有变化发生。)

(但要注意，在提供者层，这一函数与Win32层有不同的参数。)

■保持对所有应用程序在调用FindFirstPrinterChangeNotification函数时指定的打印队列事件的跟踪。

(对一个应用程序可以请求的通知类型的列表，以及对一个可以被用于描述事件的信息类型的列表，可参考平台的SDK文档中对Win32 FindFirstPrinterChangeNotification函数的描述。应用程序可以请求的通知的事件类型包括增加或删除一个打印作业或表格。应用程序可以请求的信息类型包括作业或表格参数。)

那些没有被轮询的打印提供者必须在变化发生时调用PartialReplyPrinterChangeNotification或者ReplyPrinterChangeNotification函数，以给假脱机提供描述变化的信息。ReplyPrinterChangeNotification函数必须在一些点上被调用因为它引起假脱机向应用程序发送信号，而PartialReplyPrinterChangeNotification函数则不这样做。当应用程序收到一个从ReplyPrinterChangeNotification来的信号，它被假定为去调用FindNextPrinterChangeNotification函数。后一函数给应用程序提供假脱机以前从打印提供者那里收到的事件信息。

被轮询的打印提供者应当简单地跟踪变化。假脱机以规则的间隔向应用程序发送信息。当应用程序收到一个信号，则假定调用FindNextPrinterChangeNotification函数。这一后者的函数。对被轮询的提供者，这一函数调用提供者的RefreshPrinterChangeNotification函数。

■提供一个RefreshPrinterChangeNotificaiton函数

对专用的打印队列来说，这一函数必须返回所有被监示的打印队列选项的当前状态。当应用程序以PRINTER_NOTIFY_OPTIONS_REFRESH标记调用FindNextPrinterChangeNotification(如在平台的SDK文档中所描述)时，假脱机调用这一函数。(如果前一个对FindNextPrinterChangeNotification函数的调用返回了一个具有PRINTER_NOTIFY_INFODISCARDED标记设置的PRINTER_NOTIFY_INFO结构时，应用程序就假定设置这一个值。)所有被轮询和非轮询的提供者必须支持RefreshPrinterChangeNotification。

■提供一个FindClosePrinterChangeNotification函数(在平台的SDK文档中描述)。

8.3.3安装一个网络打印提供者

为安装一个新的网络打印提供者，必须提供一个安装程序，它拷贝提供者DLL到目标系统的\System32子目录并然后调用AddPrintProvidor(在平台的SDK文档中描述)。这一函数为提供者创建一个注册表条目并增加提供者到一个假脱机已经安装的提供者列表的末尾。函数然后装载提供者的DLL并调用提供者的InitializePrintProvidor函数。

为创建一个网络打印提供者支持的打印机连接，一个用户激活增加打印机向导并选择“网络打印机服务器(Network print server)”选项。用户用\\Server\Printer格式指定一个打印队列，并且提供者的OpenPrinter函数必须能识别打印队列的名称。


Internet打印

这一部分提供下面的主题：

■9.1从应用程序打印到URL

■9.2查看打印web页面

■9.3定制打印web页面

■9.4从web页面安装打印驱动程序

9.1从应用程序打印到URL

从应用程序的观点去看，向一个URL标识的打印队列打印等同于向一个UNC标识的打印队列打印。应用程序通常不知道哪个队列可以通过URL方式被存取。

通过视查打印web页面，一个用户可以安装并连接到一个URL标记的打印队列。当这一情况发生时，打印队列被分配给它在打印服务器上所有的相同的“友好名称(friendly name)”，这一友好名称被列在用户的打印机文件夹上。

应用程序一般根据它的友好名称引用打印队列，就像它们为UNC标识的打印队列所做的一样。对在本地打印提供者(例如，由应用程序做GDI调用引起)的OpenPrinter函数的调用。本地打印提供者，依次调用在HTTP打印提供者(inetpp.dll)中的OpenPrinter函数，指定打印队列的URL。

通过友好名称指向打印队列的应用程序，一般不知道打印队列是在本地还是在网络上，或者是否网络协议是RPC，SMB或HTTP。但是，如果必要，应用程序可以直接调用OpenPrinter函数，指定一个URL。向OpenPrinter指定一个URL，则必须用到下面的URL格式：

http://ServerName/Printer/ShareName/.Printer

这里ServerName是一个服务器名称(一个Internet连接的DNS名称或者一个Intranet连接的WINS名称)，Printers表示服务器上一个虚拟的目录。ShareName是一个打印队列的共享名称，如在它的属性表单中所指定的。(虚拟目录在平台的SDK文档中讨论)。

当一个客户假脱机组件或应用程序调用OpenPrinter并指定URL。对假脱机函数的接下来的调用，如对StartDocPrinter，WritePrinter等等，是由客户端的HTTP打印提供者来处理的。HTTP打印提供者给URL增加参数，并发送结果URL字符串到打印服务器。

Windows 2000打印服务器为接受包含URLs的打印请求，它必须运行在下面两种之一的情况下：

■Windows 2000服务器软件和微软的IIS(Internet Information Server)

■Windows 2000专业版(Professional)软件和微软Peer Web Server

在打印服务器上，IIS或Peer Web Server接收URL字符串。在客户机系统上，参数被inetpp.dll追加到字符串从而引起服务器调用HTTP打印服务器，它被包含在msw3prt.dll中。HTTP打印服务器接收RAW格式的打印数据并将它发送到本地打印假脱机。

打印机数据用Internet打印协议(Internet Printing Protocol IPP1.0)从客户端传送到服务器。IPP是由Internet工程任务组(IETF:Internet Engineering Task Force)的打印机工作组(Printer Working Group PWG)定义的。

下图示意了如果从客户端打印到一个URL标识的打印队列时，从客户应用程序向一个服务器假脱机打印的打印数据的通路。

插入图？？？

如果客户端及服务器都是Windows 2000系统，如上图所示的，则通常(但不是一直)使用RPC协议用于客户-服务器通讯。(更多的信息，参考从web页面安装打印驱动程序部分的内容)。

如果客户端和服务器端不全为Windows 2000系统，则使用HTTP，HTTP也用于那些包含内部网络接口卡并支持IPP1.0的打印机，也因此没有被连接到服务器上。

打印服务器安全是由IIS提供的，它在打印服务器上执行。安全机制由IIS支持并在IIS资源指南部分被描述。另外，资源包特别描述了与系统管理员怎样控制与打印到URL相关的安全方法。

9.2查看打印web页面

用任何平台上的Internet浏览器，用户可以查看web的页面并显示Windows 2000打印服务器以及它所连接的打印机的状态。微软提供了一套服务器驻留的HTML文件以产生这些web页面。打印服务器的web页面以及对每一个服务器安装的打印机可以被客户机浏览器用URLs来引用。另外的页面可以从这些页面的链接来引用。

Windows 2000打印服务器为支持web页面，它必须运行在Windows 2000服务器软件和微软的IIS(Internet Information Server)或Windows 2000专业版(Professional)软件和微软Peer Web Server两种之一的环境下。

为查看一个打印服务器的页面，一个用户指定下面的URL格式：

http://ServerName/Printers

这里，ServerName是一个服务器名称(一个Internet连接的DNS名称或一个Intranet连接的WINS名称)。URL指向一个产生打印服务器页面的HTML文件。

服务器页面对每一个在服务器上可用的打印队列提供一个到打印队列页面的链接。共享的打印队列可以被所有的用户存取。对共享打印机，一个用户可以通过用下面的格式来指定一个URL来引用打印队列页面：

http://ServerName/ShareName

这里，ShareName是一个打印队列的共享名称，在它的属性表单中指定。

如果一个用户在打印机文件夹中选择了打印机的链接，微软的Internet Explorer浏览器则将自动启动打印队列页面的URL被存取。可选的，如已经描述的，一个用户可以通过指定页面的URL到任何HTML浏览器来查看打印服务器页面或打印队列页面。

打印web页面可以被微软的ASP(Active Server Pages)文件解释的模板文件产生。这些模板(被称为ASP文件)包含标准的HTML标记及ASP脚本语言标记(<%和%>)。

当ASP解释器遇到了具有ASP脚本标记的文本，它调用一个合适的脚本语言解释器(如JScript®及VBScript)处理这一文本，结果的HTML数据流被送到客户端的浏览器。

微软的ASP在平台的SDK文档中描述。

一套与自动化接口相关的、基于COM的打印Web页面的ActiveX对象在oleprn.dll中提供，以获得打印机属性及SNMP信息。

当一个用户想查看一个特殊的服务器或打印机的Web页面时，将进行下面的步骤：

1．用户使用一个浏览器以指定一个合适的URL。URL指向一个指定的打印服务器上的模板文件。

2．服务器驻留的ASP解释器，是IIS的一部分，它查找ASP的脚本标记，激活适当的脚本语言解释器以解释脚本文本，并将返回的结果放置于HTML数据流中。

3．在服务器上的ASP解释器，发送结果HTML流到客户端浏览器。

下图，示意了一个打印机URL从一个客户端发送到服务器端的处理过程以及它相关的HTML流是怎样被返回到客户端的。

插入图？？？

9.3定制打印web页面

本部分提供下面的主题：

■9.3.1打印Web页面的ASP文件

■9.3.2定制打印机细节的Web页面

■9.3.3安装定制的打印Web页面

9.3.1打印Web页面的ASP文件

打印Web页面是通过使用ASP文件来产生的。微软提供ASP文件产生下面的打印web页面：

■一个打印服务器页面，它是通过http://ServerName/printers来引用的。这里，ServerName表示一个打印服务器的DNS或WINS名称。该页面包含对每一个安装在服务器上的打印机的页面的链接。

■每一个服务器打印队列的一个打印队列页面。这些页面可以通过在打印服务器页面链接来存取，或者他们可以用http://ServerName/ShareName这一URL直接从浏览器引用。

■额外的排队的文档页面、打印机属性以及特定打印机的细节。这些页面在一个打印队列页面的框架中被显示。

特定打印机细节页面可以通过替换它的ASP文件来被定制。更多的信息，可以参考定制打印机细节的web页面部分的内容。

9.3.1.1ASP文件实例

DDK中也提供了ASP文件的实例，实例被放置在DDK实例的树形目录中的QueueASP子目录中。

9.3.2定制打印机细节的Web页面

如果想替换由微软提供的打印机细节页面，可以提供一个或多个定制的ASP文件。如果替换了默认的ASP页面，也可以提供安装在服务器上的另外的定制页面的链接，以及到web上其他页面的链接。

如果微软标准TCP/IP端口监示器(tcpmon.dll)用于打印机，定制的ASP文件可以被用于替换默认的每一个打印机类型的每一个厂家的打印机细节页面。如果定制的ASP文件还没有被安装，则默认的打印机细节页面被使用。

定制的ASP文件也可以为使用其他端口的监示器的打印机替换默认的打印机细节页面，但是每一个打印机类型及每一个厂家的替换则不允许。

用于安装定制的ASP文件的方法决定于是否定制的文件为一个打印机类型、一个厂家、或一个端口监示器替换了默认的文件。更多的信息，可以参考安装定制打印机web页面部分的内容。

下面的主题提供更多的关于创建打印web页面的信息：

■9.3.2.1替换默认的打印机细节页面

■9.3.2.2哪一个打印机细节页面被显示

■9.3.2.3打印机Web页面的ASP变量

■9.3.2.4打印机Web页面的ActiveX对象

■9.3.2.5修改web页面信息

9.3.2.1替换默认的打印机细节页面

可以用以下的步骤，用一个定制的页面来替换默认的打印机细节页面。

1．为页面提供一个定制的ASP文件。

2．接着是在安装定制的打印web页面部分中描述的安装过程。

一个定制的打印机细节页面可以提供到另外的定制页面或其他URL的链接。额外的定制页面的ASP文件必须如同在安装定制的打印web页面中所指明的那样被安装。更多的关于指定链接的信息，请参考平台的SDK文档中的ASP文档。

定制的打印web页面可以用到下面的技术：

■打印Web页面的ASP变量

■打印web页面的ActiveX对象

■定制的COM对象

对创建和使用COM对象的信息，参考平台的SDK文档。

注意：

所有用ASP文件创建的打印web页面是从一个ASP应用程序创建的。

不能修改命名为globals.asp的文件，它包含在系统磁盘的打印机子目录中。

微软保留有不需通知就可修改打印web页面的权利。因此，定制的ASP文件不能依赖于微软提供的ASP文件。

9.3.2.2哪一个打印机细节页面被显示

当一个用户试图查看一个打印机的细节页面，服务器用下面的算法决定读取哪一个ASP文件。

■如果打印机使用标准的TCP/IP端口监示器：

1．服务器首先检查看是否一套特定类型的打印机ASP文件被安装，如果是，它们将被应用。

2．如果特定类型打印机的ASP文件不可用，服务器检查看是否一套特定厂家的ASP文件已经被安装，如果是，它们被应用。

3．如果特定厂家的ASP文件不可用，并且打印机支持为SNMP的MIB(RFC 1759)，则微软的默认的ASP文件被应用。

4．如果不支持SNMP，则打印机细节页面不被提供。

■如果打印机没有使用标准的TCP/IP端口监示器，服务器检查看是否一套特定监示器的ASP文件已经被安装。如果是，则它们将被应用。如果不是，则没有提供打印机细节页面。

更多的信息，请参考安装定制打印web页面部分的内容。

9.3.2.3打印机Web页面的ASP变量

微软为定制打印web页面提供了一套ASP会话的变量。下面的表格中列出了会话的变量。定制的ASP文件不能修改这些变量，如所指示的那样，一些变量只在微软的TCP/IP端口监示器被应用于打印机时才有效。

一些变量被作为会话变量传递进来，而其他的变量则用URL修饰传递进来。会话变量可以通过使用Session(“变量名称”)来存取。通过URL修饰传递进来的参数可以使用Request(“变量名称”)来存取。如果想自动刷新状态页，也许会发现有必要用页面需要的变量来重新修饰URL。既然Request变量必须在URL中传递进来，他们也许需要编码或解码从ANSI翻译为UNICODE表示。一个帮助者的对象，它的COM ProgID是“OlePrn.OleCvt”，已经提供允许在URL中使用的ANSI向UNICODE的编码和解码。这一对象上有两个方法，EncodeUnicodeName和DecodeUnicodeName，可以分别用于从ANSI向Unicode翻译及Unicode向ANSI的翻译。对Session变量，这一转换是不需要被执行的。

变量 值 是否只是TCP/IP端口监示器 变量类型 是否被编码

MS_ASP1 | 到初始化的用于描述特定打印机细节web页面的目录路径 | 不是 | 请求 | 不是
MS_Community | 打印服务器的SNMP公共名称 | 是 | 请求 | 不是
MS_Computer | 打印服务器的计算机名称 | 不是 | 会话 | 不是
MS_DefaultPage | 特定打印机细节的默认ASP文件 | 不是 | 会话 | 不是
MS_Device | 打印机的SNMP设备索引 | 不是 | 请求 | 不是
MS_DHTMLEnabled | 如果客户端支持动态HTML则为TRUE，否则为FALSE | 是 | 会话 | 不是
MS_IPAddress | 打印机的IP地址 | 是 | 请求 | 不是
MS_LocalServer | 打印服务器的标识符，这或许是一个IP地址，或者是一个计算机名称 | 不是 | 会话 | 不是
MS_Model | 打印机驱动程序的名称 | 不是 | 请求 | 是
MS_Portname | 打印机的端口名称 | 不是 | 请求 | 是
MS_Printer | 打印机的名称 | 不是 | 请求 | 是
MS_SNMP | 如果打印机用到SNMP则值为TRUE，否则为FALSE | 是 | 请求 | 不是
MS_URLPrinter | 打印机的名称，以被编码的URL格式 | 不是 | 请求 | 是

会话变量指定当前当印机的属性，也就是，为一个被唤醒的ASP页的打印机。为获得当前打印机的另外的打印机属性，或者获得不同打印机的属性，参考打印机Web页面的ActiveX对象部分的内容。

9.3.2.4打印机Web页面的ActiveX对象

为打印web页面提供了三种ActiveX®对象，即IaspHelp，ISNMP及IoleCvt。ASP文件可以通过一个自动化(Automation)接口存取每一个对象，用任何的脚本语言，如VBScript等。对象及接口在oleprn.dll中提供。

■IAspHelp自动化接口允许获取与一个特定打印机相关的属性。这一接口提供了对不能从ASP会话变量存取的信息的访问，并允许一个ASP页面获得除了页面被唤醒时信息之外的关于打印机的所有信息。

■ISNMP自动化接口允许设置并检索与SNMP OIDs相关的值，如果一个打印机支持RFC 1759。

■IOleCvt自动化接口允许从ANSI到Unicode方向转换字符串，反之亦然。为另外一个代码页转换字符串到UTF8格式及转换Unicode字符串。

ISNMP接口只可以被应用到那些用微软的TCP/IP端口监示器的打印机。这一接口本质上是一个为SNMP管理API函数的OLE自动化服务的包装器，并且在平台的SDK文档中有描述。

OID可以用数字型字符串来指定，以及通过RFC1759指定的MIB包含一个文本名称的字符串。另外的MIB名称可以在定义、编译及安装定制的MIB时被应用，如在平台的SDK文档中描述的那样。

关于ActiveX及自动化服务的更多信息，可以参考平的SDK文档。

9.3.2.5修改web页面信息

打印服务器不支持服务器对打印服务器的web页面上修改信息的推送。客户端的拖操作则被支持(假定用户的浏览器识别HTML指定的刷新参数)并且可以包括在定制页面中。(更多的关于客户端拉的信息，参考相关的HTML文档)。

9.3.3安装定制的打印Web页面

用于安装定制的打印web页面的方法，依赖于安装的页面是否特定于一种打印机类型，特定于一个生产厂家或者特定于一个端口监示器。参考下面的主题：

■9.3.3.1为一个打印机类型安装页面

■9.3.3.2为一个生产厂家安装页面

■9.3.3.3为一个端口监示器安装页面

9.3.3.1为一个打印机类型安装页面

如果打印机使用标准的TCP/IP端口监示器，可以安装一个特定于指印机类型的打印机细节页面。为了这样做，包含页面的ASP文件及所有的从属的文件(如用于链接页面的.gif文件或ASP文件)到打印机的INF文件中。下面是一个打印机INF文件的实例部分：

[Manufacturere]

“ACME”

ACME

“ACME Mega Laser”=ACML01.PPD

[ACML01.PPD]

CopyFiles=@ACML01.PPD,PSCRIPT,ACML1WEB

DataSection=PSCRIPT

[ACML1WEB]

PAGE1.ASP,ACML1.ASP;ACML1.ASP renamed to PAGE1.ASP during Installation

ACML2.ASP

ACGF001.GIF

[DestinationDirs]

DefaultDestiDir=66000

ACML1WEB=66004

当打印机类的安装程序遇到这一INF文件部分，它将执行下面的操作：

■创建一个如<Root>\<Manufacturer>\<Printer Type>格式的目录，例如，下面的子目录可以被创建：

…\ACME\ACME Mega Laser

子目录在INF文件中用打印机的DIRID值66004来指定。

■拷贝acml1.asp,asml2.asp及acgf001.fig到子目录

■将acml1.asp改名为Page1.asp(由在ACML1WEB部分的第一个语句引起)

但是要注意，必须区别第一个被查看的ASP文件，通过将它放置在名为Page1.asp文件名之前，如在实例中所示，这一安装程序在目标目录中重新改这一文件为Page1.asp。

所有的ASP文件都有一个PageN.asp的格式，这里N是这1，2，3等等，是由微软保留的。

一个实例的INF文件与ASP文件实例一起提供。

9.3.3.2为一个生产厂家安装页面

可以安装一个特定于生产厂家的打印机细节页面，对所有的厂家的使用标准的TCP/IP端口监示器的打印机类型。为了这样做，将页面的ASP文件连同所有的附属文件(如链接页面的.gif文件或ASP文件)放置于厂家的子目录中(\%windir%\web\printers\<manufacturer>)。一个定制的设置程序必须被提供以完成它。

页面的初始ASP文件必须被命名为Page1.asp，所有的ASP文件名都有一个PageN.asp的格式，其中N是1，2，3等等，是由微软自已保留的。

厂家专用的页面可以用于用标准TCP/IP端口监示器但没有特定类型打印机的页面。

系统通过在安装时读打印机的INF文件确定一个打印机的厂家。当一个用户试图查看一个打印机的细节页面，系统首先检查看是否一个名为Page1.ASP的文件存在于<Root>\<Manufacturer>\<Printer Type>,如果没有发现文件，系统检查<Root>\<Manufacturer>。

9.3.3.3为一个端口监示器安装页面

可以提供一个定制的打印机细节页面以与那些没有标准TCP/IP端口监示器的打印机使用。将页面的ASP文件连同所有的附属文件(如链接页面的.gif文件或ASP文件)放置在监示器的子目录(<Root>\<Monitor>，这里Monitor匹配于在PORT_INFO_2结构中返回的监示器的名称)中。监示器安装程序必须执行这一任务。

页面的初始ASP文件必须被命名为Page1.asp，所有的ASP文件名都有一个PageN.asp的格式，这里N是这1，2，3等等，是由微软保留的。

9.4从web页面安装打印驱动程序

在一个用户可以发送一个作业到能通过打印web页面方式可见的打印队列之前，驱动程序文件必须被从打印机服务器发送并安装到用户的系统上。这一安装操作发生在用户查看一个打印队列的web页面并且选择了它的安装页面(安装页面不能被定制页面所替代)时。

如果客户端不能用RPC连接到服务器，这通常是指Intranet连接，打印机以常见的、非HTTP方式安装。否则，安装页面调用HTTP打印服务器，它创建了一个盒式(cabinet)文件(在平台的SDK文档中描述)，该盒式文件包含了打印机的.inf文件及所有的安装需要的文件。服务器发送盒式文件到客户端，在客户端它被扩展。它启动了客户端的安装打印机向导，通过这个安装向导，完成打印机向用户打印机文件夹的增加安装工作。

作为一种选择打印机队列的安装页面的可选方法，一个用户可以通过明确地运行增加打印机向导来安装一个URL标识的打印机。当一个用户指定一个URL到增加打印机向导，客户端经常用HTTP连接到服务器。当安装包含网络接口卡并因此不连接到服务器上的打印机时，这一安装方法必须被使用到。


安装及配置打印机驱动程序

本章描述了安装及配置打印机驱动程序。提供下面的主题：

■10.1打印机的即插即用

■10.2打印机的INF文件

■10.3打印机的目录服务

■10.4支持点号及打印

10.1打印机的即插即用

两个系统组件支持打印机的即插即用：

■一个LPT枚举器

LPT枚举器具有从符合IEEE1284可扩展的端口协议及ISA接口标准能力(Extended Capabilities Port protocol and ISA Interface Standard)的LTP端口硬件获得标识信息的能力。

■一个打印机类安装程序

打印机类安装程序，ntprint.dll通过调用AddPrinterDriverEx安装打印机驱动程序，并通过调用AddPrinter使得打印机在服务器上可用。这两个函数都在平台的SDK文档中描述。

LPT枚举器是一个总线驱动程序的实例，总线驱动程序在Windows 2000驱动程序开发引用参考的第一卷，即插即用、电源管理及安装设计指南部分描述。

打印机类是Windows® 2000内置的硬件类。

当一个Windows 2000系统开始启动，配置管理器调用LPT枚举器以枚举连接到LPT端口的IEEE 1284兼容的设备。对每一个被发现的设备，配置管理器调用打印机的类安装程序。打印机类安装程序调用SetupDi-为前缀的安装函数，它从printer.inf文件获得信息。这些函数被描述在Windows 2000驱动程序开发引用参考的第一卷，即插即用、电源管理及安装设计指南部分。

10.2打印机的INF文件

对.inf的广泛讨论提供在Windows 2000驱动程序开发引用参考的第一卷，即插即用、电源管理及安装设计指南部分。

下面的主题，提供了关于打印机.inf文件特定信息：

■10.2.1打印机DIRID

■10.2.2打印机INF文件条目

■10.2.3打印机INF文件数据部分

■10.2.4打印机INF文件安装部分

■10.2.5打印机INF文件拷贝文件部分

■10.2.6打印机INF文件实例

关于打印机.inf文件的附加的信息，参考下面的主题：

■第4章中的安装一个Unidrv小驱动程序部分

■第5章中的安装Pscript小驱动程序部分

■第7章中的安装定制的驱动程序组件部分

也可以参考下面的章中的安装部分的内容：

■第8章中的定制打印机假脱机组件部分的内容

10.2.1打印机DIRID

当在一个.inf文件中指定目标目录时，目录标识符(DIRID)应当被使用。DIRID在本DDK的驱动程序开发者的指南中被描述。

下表列出了特定打印机的DIRID及每一个的目的。

DIRID 目的 目录内容

66000 | 表示由GetPrinterDriverDirectory函数返回的目录路径 | 驱动程序文件和相关的文件
66001 | 表示由GetPrintProcessorDirectory函数返回的目录路径 | 打印处理器文件
66002 | 表示一些要被拷贝到本地系统\System32的附加文件的目录路径 | 打印监示器文件及其他相关文件
66003 | 表示由GetcolorDirectory函数返回的目录路径 | ICM色彩profile文件
66004 | 表示特定类型打印机的ASP文件要被拷贝到的目录路径 | ASP文件及相关的文件(参考第9章Internet打印)

当本地体系结构的打印机驱动程序被安装在本地系统，如当x86驱动程序被安装于本地的x86系统，分配于DIRID为66022的目录中的文件被拷贝到System32子目录中。如果一个驱动程序安装在一个远程系统，这一目录中的文件被忽略。

当打印机类安装程序调用假脱机的AddPrinterDriverEx函数时，一个打印机驱动程序被安装。这一函数需要所有的驱动程序文件被放置于由GetPrinterDriverDirectory函数返回的目录中。

10.2.2打印机INF文件条目

一个安装程序在打印服务器上安装打印机，它必须调用假脱机的AddPrinterDriverEx函数以装载驱动程序文件并然后调用假脱机的AddPrinter函数以使得打印机在服务器上可用。

AddPrinterDriverEx函数需要一个DRIVER_INFO_3结构作为输入，AddPrinter函数需要一个PRINTER_INFO_2结构作为输入。默认的Windows 2000打印机类安装程序ntprint.dll，读打印机的.inf文件以获得这些函数被调用之前必须放入这些函数中的值。

AddPrinterDriverEx及AddPrinter函数，以及DRIVER_INTO_3及PRINTER_INTO_2结构，都在平台的SDK文档中描述。

为打印机的驱动程序已经定义了一套.inf文件条目，这些都是ntprint.dll可以识别的，这些条目具有如下的格式：

EntryName=Value

这里，EntryName是一个标识条目的字符串，Value是一个分配给该条目的值。

下面的表中列出了应当包含于打印机.inf文件中的.inf文件条目。对每一个条目，表中包括：

■应当被分配给条目的值

■条目没有指定时，ntprint.dll使用的默认值

■一个结构成员，ntprint.dll将指向条目值的指针放置于其中。

INF文件条目 值 默认值(如果条目没有指定) 结构成员

ConfigFile | 驱动程序打印机接口DLL的名称 | 为DriverFile指定的值 | DRIVER_INFO_3.pCinfigFile
DataFile | 驱动程序相关的数据文件名称，如.ppd文件 | 在.inf文件中的驱动程序的部分名称 | DRIVER_INFO_3.pDataFile
DefaultDataType | 在Windows NT/2000中不使用 |  |
DriverFile | 驱动程序打印机图形DLL的名称 | 在.inf文件中的驱动程序的部分名称 | DRIVER_INFO_3.pDriverPath
HelpFile | 接口DLL的帮助文件名称 | 没有。没有指定帮助文件 | DRIVER_INFO_3.pHelpFile
LanguageMonitor | 与打印机驱动程序相关联的语言监示器的名称。参考语言监示器值格式部分的内容 | 没有。没有指定语言监示器 | DRIVER_INFO_3.pMonitorName
PrintProcessor | 与打印机驱动程序相关联的打印机处理器的名称。参考打印机处理器值格式 | 没有。没有指定一个打印机处理器 | PRINT_INTO_2.pPrintProcessor
VendorSetup | 在一个厂商提供的DLL中的函数的名称，它处理定制的打印机安装操作 | 没有。没有执行定制的安装操作 | 没有

打印机的.inf文件条目是通常在printer.inf文件中的数据部分指定的。相关实例，可参考打印机.inf文件实例部分的内容。

语言监示器值(LanguageMonitor value)格式

当一个LanguageMonitor条目包含在一个打印机的.inf文件中时，其值的格式如下：

LanguageMonitor=“MonitorName,MonitorDLLName”

这里，MonitorName是一个文本字符串，表示监示器的显示的名称，而MonitorDLLName是监示器DLL的文件名。

打印机处理器值(PrintProcessor Value)格式

当一个PrintProcessor条目包含在一个打印机的.inf文件中时，其值格式如下：

PrinProcessor=“PrintProcessorName,PrintProcessorDLLName”

这里，PrintProcessorName是一个文本字符串表示打印机处理器的显示的名称，PrintProcessorDLLName，是DLL的文件名。

从属文件

对Windows 2000来说，从属文件是包含于打印机的.inf文件的安装部分并且打印机的DIRID为66000或66002的打印机驱动程序文件，但是没有分配给驱动程序的DriverFile，DataFile，ConfigFile或HelpFile条目。

对每一个打印机型号，可以指定最大64个从属文件。

10.2.2.1定制的打印机安装操作

为给那些用ntprint.dll安装的打印机提供定制的打印机安装操作，在使Windows 2000默认的打印机类安装程序时，可以在打印机的.inf文件中包括一个VendorSetup条目。

VendorSetup条目的格式如下：

VendorSetup=FileName,FunctionName

这里，FileName是一个包含一个安装函数的DLL名称，FunctionName是这个函数的名称。

由FunctionName指定的函数必须匹配下面的原型：

VOID WINAPI FunctionName(LPSTR pPrinterName)

这里，FunctionName是一个函数的名称，pPrinterName是一个指向打印机友好名称的指针。

打印机类安装程序调用该函数被作为安装操作的最后一步。

10.2.3打印机INF文件数据部分

Windows 2000默认的打印机类安装程序ntprint.dll允许打印机的.inf文件以包含数据部分。数据部分用下面的格式来指定：

DataSectionName=SectionName

这里，SectionName是一个.inf文件部分名称。

数据部分用于指定一套对多个打印机都通用的打印机的.inf文件条目。通过对这些通用的条目在名称部分列出并分组，然后对每一个使用这个条目的打印机用DataSection语句引用那个部分。条目列表只需要在.inf文件中出现一次。

微软打印机的.inf文件即ntprint.inf，定义了下面的数据部分：

■[PSCRIPT_DATA]

给微软Postscript打印机驱动程序的DriverFile，ConfigFile及HelpFile条目分配值。

■[UNIDRV_DATA]

给微软统一打印机驱动程序的DriverFile，ConfigFile及HelpFile条目分配值。

■[UNIDRV_BIDI_DATA]

给微软双向打印机的统一打印机驱动程序的DriverFile，ConfigFile，HelpFile及LanguageMonitor条目分配值。。

这些数据部分应当从厂商提供的.inf文件中引用。相应实例，可参考第4章的安装一个Unidrv小驱动程序部分和第5章的安装一个Pscript小驱动程序部分的内容。

“Previous Names”部分

Windows 2000打印机类安装程序能识别一个称为“Previous Names”的特殊的数据部分。这些部分中的每一个在.inf文件中都允许。在该部分中，驱动程序中鉴别打印机名称的标识对Windows 2000及Windows 95/98是不相同的。指定这样不相同的名称允许当Windows 95/98客户端连接到Windows 2000服务器时支持点号及打印操作。

这一部分中的每一个条目格式如下：

“Windows 2000 Printer Name”=“Windows 95/98 Printer Name”

下面是一个条目实例：

[Previous Names]

“HP Color LaserJet”=“HP Color LaserJet (MS)”

“HP DeskJet 1200C”=“HP DeskJet 1200C (MS)”

“HP DeskJet 310”=“HP DeskJet 310 (MS)”

10.2.4打印机INF文件安装部分

对Windows NT® 4.0及以前的版本，向客户提供小驱动程序的厂商也同时给客户提供适当的从微软公司获得的微软打印机驱动程序的拷贝。

对Windows® 2000，厂商不再随他们的小驱动程序一起分发微软的打印机驱动程序。相反，每一个厂商提供一个安装厂商文件的.inf文件，并然后唤醒微软的打印机.inf文件ntprint.inf，它接着安装适当的打印机驱动程序组件。

微软打印机.inf文件，即ntprint.inf包含下面一些可以被厂商的.inf文件引用的INF DDInstall 部分：

■[PSCRIPT.OEM]

安装微软Postscript打印机驱动程序(Pscript)。

■[UNIDRV.OEM]

安装微软统一打印机驱动程序(Unidrv)。

■[UNIDRV_BIDI.OEM]

安装微软统一打印机驱动程序及pjlmon.dll，pjlmon.dll是支持打印机作业语言(PJL)并为PJL打印机提供双向通讯的语言监示器。

■[TTFSUB.OEM]

安装ttfsub.gpd，它被包括在该DDK中，并为用于支持统一驱动程序打印机的普通的TrueType字体替换包含一套*TTFS条目。

■[sRGBPROFILE.OEM]

安装系统的sRGB色彩profile。

■[LOCALE.OEM]

安装locale.gpd，它包含地区标识符。(参考第4章中的引用地区部分的内容)。

为在.inf文件中引用这些安装部分，文件必须用Include及Needs指令，如下面的例子：

[Manufacturer]

“ABC Printers”

[ABC Printers]

“PBC Printer 100ex”=ABC100EX.GPD,ABC_Printer_100ex

[ABC100EX.GPD]

CopyFiles=@ABCres.dll,@ABC100EX.gpd

DataSection=UNIDRV_BIDI_DATA ;Unidrv Bi-Directional Data Section

DataFile=ABC100EX.gpd

Include=NTPRINT.INF ;Include NTPRINT.INF

Needs=TTFSUB.OEM.UNIDRV_BIDI.OEM ;Install Unidrv,TrueType subs

;and PJL Language monitor

10.2.5打印机INF文件拷贝文件部分

当打印机的.inf文件包含被一个INF的CopyFiles指令引用的文件列表部分时，在文件列表中的每一个文件必须用下面的格式来指定：

[file-list-section]

destination-file-name[,,flag]

destination-file-name[,,flag]

destination-file-name[,,flag]

……

destination-file-name区段是必须，而falg区段是可选的。文件说明不能包含可选的源文件名称及临时文件名称两个字段，即source-file-name或temporary-file-name字段，这两个字段是为文件列表部分定义的并与INF CopyFiles指令一起使用。这一限制在从web页面安装打印机时是必须的。

10.2.6打印机INF文件实例

该DDK提供了一个打印机.inf文件的实例，其例子放在DDK实例目录树的一个子目录中。

10.3打印机的目录服务

当用户安装一个网络共享的打印机。默认的，Windows®2000打印文件夹将会将该打印机发布到Windows 2000的目录服务(Windows 2000 Directory Services)。发布是通过调用假脱机的具有一个PRINTER_INTO_7的输入结构的SetPrinter函数来完成的，在平台的SDK文档中描述。

发布一个打印队列使得它对用户可见，这是通过在任务条中的“开始(Start)”菜单的“查找(Find)”选项完成的。

下面的主题提供了更多的关于打印机支持目录服务的信息：

■10.3.1打印假脱机对打印机目录服务的支持

■10.3.2打印机驱动程序对打印目录服务的支持

10.3.1打印假脱机对打印机目录服务的支持

Windows 2000打印假脱机对目录服务的支持主要由以下内容构成：

■公布打印队列

■维持三个注册表键

■允许存取假脱机维持的注册表键

■返回一个打印队列的公布状态

公布打印队列

在平台的SDK文档中描述的SetPrinter，允许调用者公布、不公布或修改一个打印队列的对象。因为这样的原因，SetPrinter函数必须被PRINTER_INTO_7输入结构来调用。

一个打印队列对象只有在它与描述用户连接的打印服务器的计算机对象相关联时才可以被公布。一个用户公布打印队列的能力决定于用户自身的存取能力，如包含在用户客户端系统的设备环境。具有管理员或者打印服务器打印队列存取的用户可以公布打印队列。

维持三个注册表键

三个注册表键包含所有在打印队列对象中公布的信息。这三个关键字是用下面的标识符来引用的，并在winspool.h中定义：

SPLDS_DRIVER_KEY | 为存储特定驱动程序的信息，可以被假脱机或驱动程序提供
SPLDS_SPOOLER_KEY | 为存储假脱机提供的、特定假脱机的信息
SPLDS_USER_KEY | 为存储应用程序提供的、特定用户的信息

假脱机用SPLDS_DRVIVER_KEY存储可以通过调用平台SDK中的DeviceCpabilities函数而获得的驱动程序能力。驱动程序负责存储假脱机不能获得的的驱动程序能力，如在打印机驱动程序对打印机目录服务部分中所描述。存储在这些关键字下的值必须由定义在wnspoool.h中定义的以SPLDS_为前缀的常量来标识。

假脱机保持对这些关键字下的因为打印队列对象最后一次被修改而引起的值的修改跟踪。假脱机每一次公布或修改打印队列对象，它把拷贝所有被修改的值到对象中。

允许存取被假脱机维持的注册表键

假脱机允许打印机驱动程序通过调用SetPrinterDataEx，GetPrinterDataEx和EnumPrinterDataEx函数存取三个由假脱机维持的注册表键，这三个函数在平台的SDK中描述。SetPrinterDataEx函数设置关键字下的函数，而GetPrinterDataEx及EnumPrinterDataEx返回当前的值。(驱动程序不设置在SPLDS_SPOOLER_KEY关键字下的值)。对这些函数的调用者不用指定一个完整的注册表路径，函数自动决定到专用的打印队列的注册条目的路径。

返回一个打印队列的公布状态

GetPrinter函数，在平台的SDK函数中描述，允许调用者决定是否一个打印队列是当前公布的。为了这一个目的，GetPrinter函数被一个具有PRINTER_INFO_7输入结构的调用者调用。该函数返回打印队列的公布状态(已公布或未公布)及对象标识符。

所有前面提到的函数都在平台的SDK文档中描述。函数不允许专用于DS相关的操作。

10.3.2打印机驱动程序对打印目录服务的支持

打印机驱动程序不负责向目录服务公布一个打印队列。Windows 2000打印文件夹创建一个打印列对象(通过调用假脱机的SetPrinter函数)，并且是在安装打印机过程中创建的。

打印队列属性被公布，这样一个用户可以根据特殊的属性，使用在任务条中的“开始(Start)”菜单的“查找(Find)”选项去查找打印机。打印文件夹公布一些但不是所有的对DriverCapabilities可用的打印机能力。

打印机驱动程序可以增加或修改一个打印队列对象的对象属性信息。那些可以被公布的、以SPLDS_为前缀的常量公布的特殊的属性，在winspool.h中定义。为增加或修改打印机属性，驱动程序必须用这些预定义的属性名称标识符。

接着进行下面的步骤：

1． 增加属性名称及值到注册表的SPLDS_DRIVER_KEY之下，并通过调用假脱机的SetPrinterDataEx函数。

2． 调用假脱机的SetPrinter函数，并具有PRINTER_INFO_7的输入结构和DSPRINT_UPDATE行为，以通知假脱机它应当修改被公布的打印队列对象(驱动程序不应当指定一个DSPRINT_PUBLISH行为)。

这些步骤应当在打印机驱动程序的DrvPrinterEvent函数中，当函数收到一个PRINTER_EVENT_INITALIZE事件时被实现。

如果一个驱动程序需要为打印机公布的属性获得当前值，它应当调用GetPrinterDataEx或EnumPrinterDataEx以从注册表获得信息，它由假脱机维持并一直是最新的。作为一种可选择的方式，可以调用GetPrinter以获得打印队列的对象标识符并然后调用ADSI函数以获得被公布的属性的值。这一技巧不是被推荐的，是因为它不仅需要大量的资源并且因为返回的数据不总是当前的数据。

10.4支持点号及打印(Point and Print)

这一部分提供下面的主题以描述Windows 2000点号及打印技术：

■10.4.1点号及打印介绍

■10.4.2在打印机安装过程中支持点号及打印

■10.4.3在打印机连接过程中支持点号及打印

10.4.1点号及打印介绍

点号及打印是一个术语，它指的是Windows 2000的这样一种能力，它允许用户在Windows 2000客户端上创建一个到远程打印机的连接，而不需要提供磁盘或其他安装媒体。所有需要的文件和配置信息将自动从打印服务器下载到客户端，不需要用户的干涉。

Windows 2000点号及打印技术提供了两种方法，通过它们可以指定那些应当从客户机器发送到打印服务器的文件：

■可以与打印机驱动程序关联的文件。这些文件与每一个使用该驱动程序的打印队列都相关联的。

■可以与单个打印队列相关联的文件。

更多的信息，参考在打印机安装过程中支持点号及打印部分的内容。

当一个用户应用程序调用AddPrinterConnection(在平台的SDK文档中描述)以做到一个打印机的连接，下面的操作必须被执行：

■驱动程序相关的及队列相关的文件被从打印服务器下载到客户端。

■打印机配置参数的当前值，它们被存储于服务器的注册表中的打印机的关键字下，被下载到客户端。

更多的信息，参考在打印机安装过程中支持句号及打印部分的内容。

10.4.2在打印机安装过程中支持点号及打印

为支持点号及打印技术，需要指明哪些驱动程序专用的及队列专用的文件需要在客户端用户连接到打印服务器时被发送到客户端。提供下面这些主题：

■10.4.2.1安装驱动程序专用的文件

■10.4.2.2安装队列专用的文件

■10.4.2.3点号及打印DLL

10.4.2.1安装驱动程序专用的文件

与打印机驱动程序关联的文件可通过将它们包含在打印机的.inf文件中实现。更多的信息，参考打印机的INF文件部分的内容。

为提供对Windows 95/98客户连接到Windows 2000服务器的点号及打印的支持，参考在打印机INF文件的“Previous Names”数据部分的描述。

10.4.2.2安装队列专用的文件

在打印机安装时，一个厂商提供的安装程序可以指定一套具有任何类型，并与特殊的打印队列相关联的文件。文件可以被下载至连接到打印服务器的任一客户。安装程序通过设置注册表中的值来指定文件。如在下表中所示：

值名称 值类型 值

Directory | REG_SZ | 到Files指定的目录路径。同时用作服务器上的源路径及客户端上的目的路径。这一路径与PRINT$环境变量相关
Files | REG_MULTI_SZ | 当客户端连接到服务器时被拷贝到客户端的文件名。文件可以是DLL，数据文件或者其他类型文件
Module | REG_SZ | 一个可选的点号及打印DLL的文件名

应用程序应当通过调用打印假脱机的SetPrinterDataEx函数来创建这些值。这一调用指定的注册表键值的格式应当如下：

CopyFiles\ComponentName

这里ComponentName是与文件相关联的软件组件的名称。例如，微软图像色彩管理(ICM:Microsoft Image Color Management)是在CopyFiles\ICM关键字下被指定的。指定了一个注册表键名称作为SetPrinterDataEx函数的参数，函数创建该关键字作为服务器上打印队列关键字的一个子关键字。

安装实例

作为一个实例，假设一个HP Color LaserJet的彩色打印机被安装在一个打印服务器上并分配了一个名为“HpColor”的队列名称。假定微软的ICM需要下面两个文件与打印队列相关联：

■一个名为hpclrsr.icm的色彩的profile文件，位于服务器的PRINT$\Color下面。

■一个名为mscms.dll的DLL文件，位于服务器的PRINT$\Color下面。

在安装时，应用程序会调用ICM API函数AssociateColorProfileWithDevice，它依次调用SetPrinterDataEx以创建下面的服务器注册条目：

CopyFiles\ICM\Directory:Color

CopyFiles\ICM\Files:hpclrsr.icm

CopyFiles\ICM\Module:mscms.dll

Mscms.dll模块是一个点号及打印的DLL，它导出GenerateCopyFilePaths及SpoolerCopyFileEvent函数。

10.4.2.3点号及打印DLL

可以选择性地提供一个特殊的点号及打印DLL，并通过将它的名称与Module注册表值相关联来实现。这一DLL必须导出下面两个函数：

GenerateCopyFilePaths

这一函数，是一个被服务器的假脱机和客户的假脱机同时调用的函数。可以用于修改Directory注册表值指定的目录路径，或者源路径(在服务器上)，或者目标路径(在客户机器)，或者两者都可以修改。

SpoolerCopyFileEvent

这一函数，也能被服务器服务器和客户的假脱机调用。它收到一个指示某一相关连接的打印机事件发生的事件代码。

一个点号及打印DLL不需要只导出这些函数。例如mscms.dll，它被微软的ICM组件所使用，同时导出一套ICM API函数。

但是，可以另外指定其他的DLL，而不是导出GenerateCopyFilePaths及SpoolerCopyFileEvent。为做到这些，分配DLL文件名到Files注册表键而不是Module注册表键(参考安装特定队列文件部分的内容)。

在打印服务器上，点号及打印DLL必须放置在一个由平台的SDK 中LoadLibrary函数搜索到的装载路径。

在安装程序通过调用SetPrinterDataEx将DLL的名称放置在服务器的注册表中时，所有接下来的对SetPrinterDataEx的调用导致一个对DLL的SpoolerCopyFileEvent函数的调用，并提供一个COPYFILE_EVENT_SET_PRINTER_DATAEX的事件代码。

不同于列在Files注册表键下的文件(参考安装特定队列的文件)，点号及打印DLL不是在客户连接到服务器时从打印服务器拷贝的。相反，当创建了一个对打印服务器的连接时，DLL被假定已经驻留客户端。结果是，DLL可以用于另外的不是与点号及打印功能相关的目的。

为确定在Module关键字下命名的DLL是否能被发现，客户端假脱机检查LoadLibrary路径，然后检查驱动程序目录。安装句号及打印DLL到客户端的一种方法是在一个Printer.inf文件中指定它的名称，作为一个相关的文件，这样这个文件可以在下载驱动程序专用的文件时被拷贝到客户端的驱动程序目录。

10.4.3在打印机连接过程中支持点号及打印

当一个用户从客户端系统连接到打印服务器，驱动程序专用的和队列专用的文件必须从服务器发送到客户端。下面的主题被提供：

■10.4.3.1下载驱动程序专用的文件

■10.4.3.2下载队列专用的文件

10.4.3.1下载驱动程序专用的文件

客户系统通过调用AddPrinterConnection创建了一个到打印服务器的连接。这一调用导致在服务器上对GetPrinterDriver的调用，它读取打印机的.inf文件以填充DRIVER_INFO_3结构，且接着调用是对具有DRVIVER_INFO_3作为输入结构的AddPrinterDriver函数的调用。AddPrinterDriver函数导致列在DRIVER_INFO_3结构下面的所有文件都被发送到客户端。

函数及DRIVER_INFO_3结构在平台的SDK文档中描述。

10.4.3.2下载队列专用的文件

如果用户决定客户创建一个到打印服务器的打印机连接，并且如果安装程序已经创建了注册表条目，该注册表条目是在打印机安装过程中支持点号及打印部分中描述的，将会发生下面的事件：

1．用户应用程序调用AddPrinterConnection，它在平台的SDK文档中描述。

2．客户端的远端打印提供者(win32spl.dll)创建一个到服务器的连接。

3．服务器的假脱机发送驱动程序文件到客户机。

4．客户端的Win32spl.dll在服务器上调用EnumPrinterKey及EnumPrinterDataEx以拷贝打印机的注册表条目。

5．当服务器假脱机在处理EnumPrinterDataEx时枚举注册表值，在每一个遇到打印机的CopyFiles关键字诸如CopyFiles\ICM这样的子关键字时，执行下面的操作：

■装载点号及打印DLL，如果被指定，调用它的GenerateCopyFilePaths函数，它可以修改源路径和/或目标路径。

■创建SourceDir和TargetDir关键字，基于由GenerateCopyFilePaths返回的源路径和目标路径，并将他们作为EnumPrinterDataEx数据返回到客户假脱机。(这些关键字实际上不存在于服务器上)。

6．客户端的Win32spl.dll缓存收到的打印机关键字作为对EnumPrinterData及EnumPrinterDataEx调用的响应。

7．对打印机的CopyFiles关键字的每一个子关键字，如CopyFiles\ICM，客户端的Win32spl.dll执行下面的操作：

■装载本地点号及打印DLL，如果提供了一个，并调用了它的GenerateCopyFilePaths函数，它可以修改源路径和/或目标路径(输入是从服务器收到的SourceDir和TargetDir关键字)。

■从服务器下载所有与Files关键字相关联的文件。

■记录一个事件，指明点号及打印文件被下载。

■调用点号及打印DLL的SpoolerCopyFileEvent函数，如果一个DLL被提供，指定一个COPYFILE_EVENT_FILE_CHANGED事件。

8．客户端假脱机调用驱动程序的DrvprinterEvent函数，指定一个PRINTER_EVENT_CACHE_REFRESH事件。

9．客户假脱机再次调用驱动程序的DrvPrinterEvent函数，指定一个PRINTER_EVENT_ADD_CONNECTION事件。

10． 如果提供一个点号及打印DLL，客户假脱机调用它的SpoolerCopyFileEvent函数，指定一个COPYFILE_EVENT_ADD_PRINTER_CONNECTION事件。

连接实例

有一个实例，假定安装程序已经定义了服务器的注册表条目，在安装实例部分描述过。另外，假定服务器被命名为NTPRINT并且客户被命名为MyClient。

为连接一个名为HpColor的打印队列到NTPRINT，一个在MyClient上的用户应用程序如下调用AddPrinterConnection：

AddPrinterConnection(\\NTPRINT\HpColor)

在服务器上，假脱机装载mscms.dll并如下调用GenerateCopyFilePaths：

GenerateCopyFilePaths(

HpColor,

Color,

&SplclientInfo1,

1,

\\NTPRINT\PRINT$\Color,

&dwSourceDirSize,

Color,

&dwDestDirSize,

COPYFILE_FLAG_SERVER_SPOOLER）

微软的ICM的mscms.dll模块不修改源路径和目标路径，因此它必须返回ERROR_SUCCESS。

服务器假脱机给MyClient返回如下的键：

Sourcedir:\\NTPRINT\PRINT$\Color

TargetDir:Color

在客户端，TargetDir的值扩展到C:\Winnt\System32\Spool\Drivers\Color下。

在MyClient上的假脱机执行下面的操作：

■下载mscms.dll并如下调用GenerateCopyFilepaths：

GenerateCopyFilePaths(

\\NTPRINT\HPColor,

Color,

&SplclientInfo1,

1,

\\NTPRINT\PRINT$\Color,

&dwSourceDirSize,

c:\Winnt\system32\Spool\Drivers\Color,

&dwDestDirSize,

COPYFILE_FLAG_CLIENT_SPOOLER)

微软的ICM的mscms.dll模块不修改源路径或目标路径，因此它只返回ERROR_SUCCESS。

■下载hpclrlsr.icm到C:\Winnt\System32\Spool\Drivers\Color目录。

■记录一个事件，指明被下载的点号及打印文件。

■调用在mscms.dll中的SpoolerCopyFileEvent函数，指定一个COPYFILE_EVENT_FILE_CHANGED事件。

■再次调用打印机驱动程序的DrvPrinterEvent函数，指定一个PRINTER_EVENT_ADD_CONNECTION事件。

■调用在mscms.dll中的SpoolerCopyFileEvent函数，指定一个COPYFILE_EVENT_ADD_PRINTER_CONNECTION事件。


CPSUI

本章描述了公用属性表单用户接口(CPSUI:Common Property Sheet User Interface)。并提供了下面的主题：

■11.1CPSUI介绍

■11.2CPSUI应用程序实例

■11.3CPSUI提供的函数

■11.4应用程序提供的回调函数

■11.5创建属性表单页面

11.1 CPSUI介绍

CPSUI是一个用户模式的动态链接库，它使得开发者可以创建公用的、具有标准外观的属性表单页面。许多用CPSUI创建的页面由以下构成：

■一个树形视图的窗口，每个树的节点表示一个可选的、用户可以修改的页面选项。

■对每一个树形节点的设备环境的菜单，可以用于显示并选择与节点相关的参数值。

设备环境菜单项是用预置的一套CPSUI支持的Windows 控件。一个用户选择树形视图窗口的一个选项，然后用设备环境菜单为选项选择期望的值。

然而CPSUI设计用于任何的应用程序，它最初的应用就是Windows® NT/Windows 2000打印子系统。因此，DDK的设计指南及参考主要针对这一应用。

CPSUI为打印机和打印机文档提供预定义的属性表单页面。CPSUI为打印机提供由DeviceSettings构成的页面，以及向文档提供由Layout，paper/Quality，Advanced构成的页面。这些页面可以从打印文件夹的Printer菜单中被查看。

与打印机接口DLL相连接的打印假脱机，用这些预定义的页面为打印机和文档创建属性表单。关于怎样打印假脱机，打印机接口DLL和CPSUI之间的交互，参考打印机驱动程序中使用CPSUI部分的内容。

为微软Unidrv和Pscript驱动程序创建的定制的用户接口代码也可以使用CPSUI。更多的信息，参考第7章用户接口插件部分。

11.2 CPSUI应用程序实例

CPSUISAM，一个CPSUI应用程序的源代码，包含在这一DDK中。应用程序导致CPSUI去调用到打印假脱机为系统默认打印机创建属性表单页面。然后应用程序创建一个另外的属性表单页面，以指明在用CPSUI创建一个新页时可用的一些新技术。

—————————————————————————————————————————

注意：打印机接口DLL不应当调用到打印假脱机。CPSUISAM指明一些CPSUI的能力但是不表示是应当被打印机接口DLL使用的技术。相反，这些DLL应当接着在打印机驱动程序中使用CPSUI部分中描述的步骤。

11.3 CPSUI提供的函数

CPSUI为应用程序提供下面二个重要的函数：

■CommonPropertySheetUI

CommonPropertySheetUI函数是CPSUI的入口点。该函数导致属性表单页面被创建及显示，然后允许他们被一个用户查看并修改。

当一个应用程序调用CommonPropertySheetUI，它提供描述页面被创建的一个页面创建的回调函数地址。CPSUI调用这一回调函数以获得页面的描述。然后显示这一页面，允许应用程序用户修改包含在页面中的值，并用页面事件回调函数发送已修改的值到应用程序。CommonPropertySheetUI函数直到用户通过按“OK”或“Cancel”键解除了属性表单时才返回。

需要注意的是，打印机接口DLL不调用这一函数，它被打印假脱机调用。

■ComPropsheet

ComPropsheet函数是应用程序向CPSUI描述属性表单页面的手段。这样CPSUI可以创建并显示它们。CPSUI应用程序从页面创建回调函数来调用这一函数。通常，一个页面描述包括一个页面事件回调函数的指针，它的CPSUI将在应用程序用户修改了页面值时调用。

什么时候调用这些函数的详细说明，参考在打印机驱动程序中使用CPSUI部分的内容。

两个附加的CPSUI提供的函数，即SetCPSUIUserData和GetCPSUIUserData，可以被应用程序提供的对话框处理，以存储和检索应用程序提供的值。

11.4应用程序提供的回调函数

CPSUI与应用程序通过回调函数通讯。一个CPSUI应用程序必须提供两个类型的回调函数。

■页面创建回调函数

■页面事件回调函数

11.4.1页面创建回调函数

当一个应用程序对CPSUI的入口点函数(CommonPropertySheetUI)做了一个初始化的调用，它必须包括一个PFNPROPSHEETUI类型的回调函数的地址。这一回调函数负责描述属性表单页面并发送这些描述给CPSUI以在创建时使用。

CPSUI的CommonPropertySheetUI函数立即回调PRPPSHEETUI类型的函数，提供一个PROPSHEETUI_INFO结构的地址。应用程序然后调用CPSUI的ComPropSheet函数，提供CPSUI可以使用的页面描述以创建页面，如下图中所描述的：

插入图？？？

更多的信息，请参考指定页面的方法部分。

11.4.2页面事件回调函数

当一个用户与一个属性表单页面交互，操作系统发送这样的窗口事件作为变化了的焦点或被修改了的值消息。一个CPSUI应用程序怎样接收一个页面的窗口事件消息依赖于应用程序怎样定义这一页面。

■如果页面是用CPSUI提供的页面和模板描述的，它可以提供一个CPSUI消息句柄。

■如果应用程序创建一个不是由CPSUI提供的定制的页面，它必须提供一个对话框过程。更多的信息，参考对话框过程及CPSUI部分的内容。

一个CPSUI应用程序在它调用ComPropSheet函数时，将页面事件回调函数的地址提供给CPSUI。

11.4.2.1 CPSUI消息句柄

CPSUI消息句柄是一个用_CPSUI-CALLBACK函数类型定义的回调函数。如果使用CPSUI-提供的这一类型的页面及模板，就推荐使用这一事件回调函数。

当一个用户与属性表单页面进行交互时，会导致一个事件发生，CPSUI解释这一事件并调用_CPSUICALLBACK类型的函数，提供一个CPSUICBPARAM结构并描述该回调函数被调用的原因。

回调函数必须处理该事件，然后给CPSUI返回一个状态值，它指明是否该页面应当被重新显示或者重新初始化。

一个对话框过程是一个处理由系统发送的窗口消息的回调函数。如果正在创建一个定制的属性表单页面并且它不被CPSUI支持时，就需要这一类型的事件回调。(也可以用CPSUI提供的页面及模板的对话框过程)。更多的关于对话框过程的信息，参考在平台的SDK文档中描述的DialogProc函数。

对话框过程的指针是用DLGPROC指针类型声明的，在平台SDK文档中有描述。

对所有用CPSUI创建的属性表单页面，窗口消息在被传递到应用程序提供的对话框过程之前首先被CPSUI解释。如果该页面是用CPSUI提供的模板定义的，应用程序提供的对话过程就可以提供一个指明CPSUI应当处理这一消息的返回值。

一个对话框过程可以用SetCPSUIUserData及GetCPSUIUserData函数去存储并检索应用程序提供的的值。

更多的关于在CPSUI应用对话框处程的信息，参考在线DDK中的DLGPAGE的Comments部分的内容。

11.5创建属性表单页面

用CPSUI创建的属性表单页面是由属性表单选项构成的，这里每一个选项表示一个用户可修改的值。属性表单选项的对话框是用一套CPSUI支持的窗口控件创建的，用以让用户修改选项的值。

CPSUI提供的窗口控件可以在CPSUI提供的页面及模板中显示，或者他们可以用于定制页面。有几种方法可以指定页面，这些方法都包括在调用CPSUI的ComPropSheet函数中。

为打印机和打印文档创建一个属性表单页面涉及打印机驱动程序CPSUI及打印驱动程序与应用程序、打印机接口DLL、CPSUI之间的交互。

11.5.1属性表单选项

属性表单选项是一个可显示的、可选择的属性表单页面上的项目。通常，用户可以修改一个选项的值。CPSUI帮助应用程序用一套预定义的CPSUI支持的窗口控件使用一个标准的格式创建选项。应用程序不为这些控件提供资源。

每一个属性表单页面通常包含好几个选项，对每一个属性表单页面选项，一个CPSUI应用程序必须用下面的CPSUI结构：

■一个OPTITEM结构，它标识选项的显示名称和其他特征。

■一个OPTTYPE结构，它标识选项的显示对话框类型(CPSUI选项类型)

■一个或多个OPTPARAM结构，它标识选项的用户可选择的参数值。

为使用这些CPSUI结构描述属性表单选项，页面包含的选项必须用COMPROPSHEETUI结构来定义。

11.5.2 CPSUI支持的窗口控件

CPSUI支持一套窗口控件以向用户提供一致的接口。在为打印机设备和文档创建属性表单页面时使用这些窗口控件是特别重要的，因为用户期望对所有的打印机有一致的接口。

CPSUI支持的windows窗口控件包括：

■包含两个或三个单选钮(radio button)的对话框。

■滚动条及轨道条。

■编辑、列表及组合框。

■一个上/下箭头框

■检查框

这一套窗口控件在指定属性表单选项时必须被经常使用到。窗口控件是用CPSUI选项类型指定的。而通常都不需要，其外观的表现可以被定制。更多的信息，可参考定制CPSUI支持的窗口控件。

CPSUI也定义了两个特殊的控件，叫做扩展的检查框(extended check box)和扩展的按键(extended push button)，这两个控件提供超过标准检查框和按键的能力，可以分别用EXTCHKBOX和EXTPUSH结构来指定。

11.5.2.1定制CPSUI支持的窗口控件

如果用CPSUI支持的窗口控件与CPSUI提供的页面和模板相连，CPSUI提供一种方式描述这些控件并使他们恰当地组在一起的窗口控制资源。因此，不需要为控件提供资源。

另一方面，如果创建一个不使用CPSUI提供的页面或模板的属性表单页面，必须定制所使用的CPSUI支持的窗口控件。为做到这些，需要为CPSUI选项类型提供窗口控制资源。必须用每一个选项的OPTTYPE结构的BegCtroID成员为这些资源指定标识符。

如果定制CPSUI支持的窗口控件，要记住，如果在OPTITEM结构中的OPTIF_HIDE标记被设定，则CPSUI将不显示一个选项。CPSUI移动余下的控件以填充一般由隐藏的选项占据的空间。因此，如果创建一个包含几个同步显示的选项的页面，则必须服从下面的规则：

■每一个选项应当占据属性表单页面的整个水平空间。

■选项对话框不应当彼此重叠。

■表示选项的单选按钮是从左边到右边排列的，按钮及图标应当按X轴排列。如果按钮是从上到下排列，则按钮及图标应当按Y轴排列。

■如果几个项目共享一个组对话框，这个组对话框必须属于第一个OPTITEM，它是在组对话框中的最顶上的项目。组对话框必须足够大以包含所有与它相关联的项目。

同样要注意的是，如果单选钮及一个图标是从上到下排列的并且一些控件是被隐藏的，CPSUI不删除结果中的y方向上的空格。

11.5.3 CPSUI提供的页面和模板

CPSUI提供一套预定义的属性表单页面及三个页面模板。预定义的属性表单页面包括以下内容：

■一套具有布局(Layout)、纸张/质量(paper/Quality)和高级(Advanced)卡片标题的三个页面。这些页面指明为打印机包含着文档属性，并可以在打印机接口DLL的DrvCocumentPropertysheets函数用于创建一个属性表单。

■一个具有卡片标题为“高级(Advanced)”的单一页面。同样，这一页面指明为打印机包含着文档的属性，并且可以用于从打印机接口DLL的DrvDocumentPropertySheets函数创建属性表单。

■一个具有卡片标题为“设备设置(DeviceSettings)”的单一页面。这一页面指明包含的打印机属性，也可以被用于从打印机接口DLL的DrvCocumentPropertySheets创建一个属性表单。

■一个单个的、一般树形视面的页面，没有预定义的标题。任何的 CPSUI应用程序都可以用于这一页面。

使用一个预定义的页面，一个应用程序必须用COMPROPSHEETUI结构的pDlgPage成员来标识它。

CPSUI也同时提供三个预定义页面的模板。CPSUI用这些模板创建它预定义的页面。应用程序可以使用他们。模板主要是由以下构成的：

■一个树形视图的页面模板，CPSUI用它去创建预定义的高级(Advanced)和设备设置(DevciceSettings)页面。这一模板由包含节点的每一个属性表单选项的树形视图控件构成。设备环境菜单与每一个树的节点相关联。每一个节点的设备环境菜单提供一种方式，通过这一方式，用户可以修改选项的值。CPSUI为这一模板提供一个对话框过程，它为所有CPSUI支持的窗口控件处理Windows消息。

■另外两个控件模板，CPSUI用来创建预定义的布局(Layout)和纸张/质量(Paper/Quality)页面。CPSUI为这一模板提供一个对话框过程，它为所有CPSUI支持的窗口控件处理Windows消息。

为使用一个预定义的页面模板，一个应用程序必须用DLGPAGE结构的DlgTemplateID成员标识它。

11.5.4指定页面的方法

一个应用程序可以使用以下三种方法中的任一种方来为CPSUI指定属性表单页面。下面的每一种方法包括调用CPSUI的ComPropSheet函数指定ComPropsheet函数的一种源代码。

■提供一个COMPROPSHEETUI结构

如果应用程序通过传递COMPROPSHEETUI结构到ComPropSheet来描述了一个属性表单页面，它可以：

1．使用一种CPSUI提供的页面及模板以指定那些预定义的、标准的打印机接口DLL能用于打印机属性表单的页面类型。

2．指定一套可显示在页面上的、用户可修改的属性表单选项。

3．指定一个当用户查看或修改页面选项时，CPSUI可以调用的页面事件的回调函数。

■提供一个PROPSHEETPAGE结构

一个PROPSHEETPAGE结构(在平台的SDK文档中描述)可以用来描述一个属性表单页面。如果该页面不能在使用COMPROPSHEETUI结构时可用的普通(标准)对话框构建，打印机接口DLL则通常不使用该方法。

■提供一个回调函数

一个应用程序可以传递给ComPropSheet一个PFNPROPSHEETUI类型的回调函数的地址，CPSUI立即调用它。回调函数负责调用ComPropsheet以创建属性表单页面。

打印假脱机用这一方法去通知CPSUI的已经存在的打印机接口DLL的DrvCocumentPropertySheets及DrvDevicePropertySheets函数。同样，Unidrv及Pscript驱动程序用这一技巧去通知CPSI的已经存在于用户接口插件中的IPrintOemUI::DocumentPropertySheets和IPrintOemUI::DevicePropertySheets的COM方法。

无论哪一个方法用于指定一个新的页面，该页面必须通过传递一个父组的句柄到ComPropSheet函数而被分配到一个父组中。

11.5.4.1父组

属性表单页面可以通过给赋单一的父组(group parent)的值来分成不同的组。可以通过调用CPSUI的具有一个CPSFUNC_INSERT_PSUIPAGE函数代码的ComPropSheet函数来创建一个父组，并指定PSUIPAGEINSERT_GROUP_PARENT作为一个INSERTPSUIPAGE_INFO结构的Type成员。

当一个新的父组被创建后，就返回一个句柄。当增加或删除属性表单页面时，然后句柄可以被用作到ComPropSheet的hComPropSheet参数。

另外，一个父组的句柄作为应用程序的PFNPROPSHEETUI类型的回调函数所接收的PROPSHEETUI_INFO结构的hComPropSheet成员而被接收。如果不创建新的父组，所有的属性表单应当被分配到这一个父组上。

可以在每一个被创建的父组下面创建另外的父组。属性表单自身被看作是顶层的父组，如果不能明确地创建出父组，所有增加的属性表单页面被分配给顶层父。

11.5.5打印机驱动程序中使用CPSUI

打印假脱机，与打印机接口的DLL相连，用CPSUI为打印文档和打印机设备创建属性表单页面。当应用程序(如微软的Word)为一个打印文档显示属性表单时，必须包含下面的步骤：

1．应用程序调用打印假脱机的DocumentProperties函数(在平台的SDK文档中描述)，指定文档要被打印到的打印机。

2．打印假脱机调用CPSUI的入口点函数CommonPropertySheetUI，以指定一个内部的PFNPROPSHEETUI类型的回调函数。

3．CPSUI调用假脱机的PFNPROPSHEETUI类型的回调函数。

4．假脱机的PFNPROPSHEETUI类型的函数调用CPSUI的ComPropSheet函数(用一个CPSFUNC_ADD_PFNPROPSHEETUI函数代码)以通知CPSUI适当的打印机接口DLL的地址。

5．CPSUI调用打印机接口DLL的DrvDocumentPropertySheets函数。

6．打印机接口DLL的DrvDocumentPropertySheets函数调用CPSUI的ComPropSheet函数(通常用一个CPSFUNC_ADD_PCOMPROPSHEETUI函数代码)以提供CPSUI属性表面页面的描述及页面事件回调(page event callbacks)。

7．CPSUI的ComPropSheet函数调用CreatePropertySheetPage(在平台的SDK文档中描述)以创建由打印机接口DLL指定的属性表单页面。CPSUI然后调用PropertySheet(在平台的SDK文档中描述)以显示属性表单页面。

下面的图示意了这些步骤：

插入图？？？

由于应用程序用户遍历属性表单页面及修改选项值，操作系统通知CPSUI页面事件及CPSUI，并按次序调用由打印机接口DLL提供的页面事件回调函数。页面事件回调函数处理页面事件并根据需要，存储内部新选择的选项值。

当一个用户通过按下OK或Cancel键撤消了属性表单，CPSUI释放该页并导致CommonPropertySheetUI函数对打印假脱机返回值，然后向应用程序返回控制。

当一个应用程序为打印机设备而不是打印文档显示了属性表单时，也使用相同的步骤，除过应用程序调用假脱机的PrinterProperties函数和假脱机传递打印机接口DLL的DrvDevicePropertySheets函数的地址到CPUSI。


打印机色彩管理

这一章描述了打印机驱动程序应当执行的支持色彩管理的操作。提供了下面这些主题：

■12.1允许使用色彩管理

■12.2控制色彩管理

■12.3放置ICC的Profile文件

■12.4安装ICC的Profile文件

12.1允许色彩管理

色彩管理可以由应用程序或打印驱动程序来允许使用。应用程序可以使用下面两种方法之一去允许色彩管理：

■调用SetICMMode(在平台的SDK文档中描述)，指定ICM_ON。

这一方法允许使用系统控制的色彩管理。

■当调用CreateDC创建一个打印作业时指定DEVMODE结构，并且设定DEVMODE结构的dmICMMethod成员为DMICMMETHOD_SYSTEM，DMICMMETHOD_DRIVER或者DMICMMETHOD_DEVICE三者中的一个。

这一方法允许应用程序选择系统控制的，驱动程序控制的或设备控制的色彩管理(假定指定的控制类型都被支持)。

打印机驱动程序可以通过驱动程序的默认DEVMODE结构的dmICMMethod成员设置为DMICMMETHOD_SYSTEM，DMICMMETHOD_DRIVER或DMICMMETHOD_DEVICE之一来允许使用色彩管理。(如果为CreateDC提供了一个DEVMODE结构，应用程序可以覆盖默认的设置。另外，驱动程序负责在执行驱动程序的DrvDocumentPropertySheets函数时为色彩管理存储用户的选择)。

12.2控制色彩管理

打印机的色彩管理可以被应用程序、系统(GDI)、驱动程序或设备硬件来控制。驱动程序通过检测被传递到实现它的DDI绘制函数的BRUSHOJB和XLATEOBJ结构中的标记以确定哪一个组件可以正确地处理色彩管理。

定义了下面的标记：

标记 定义

在BRUSHOJB中的BR_DEVICE_ICM | 色彩管理是由驱动程序或设备执行的
在XLATEOJB中的XO_DEVICE_ICM
在BRUSHOJB中的BR_HOST_ICM | 色彩管理是由应用程序或系统(GDI)执行的
在XLATEOJB中的XO_HOST_ICM

下面的主题描述了驱动程序对色彩管理的支持情况：

■12.2.1系统控制

■12.2.2驱动程序控制及设备控制

■12.2.3支持CMYK色彩空间

■12.2.4对JPEG及PNG图像的色彩管理

12.2.1系统控制

系统控制的色彩管理是默认的色彩管理类型。它也是为打印机推荐的类型。如果色彩管理被允许，GDI在发送所有DIB、笔、画刷到驱动程序打印机图形接口的DLL之前修改他们的色彩，基于输入和输出的色彩空间及安装的ICC Profile文件。

不需要ICM专用的代码增加到打印机以支持系统控制的色彩管理，不同于指明对CYMK色彩空间(如果合适)的支持，如在支持CMYK色彩空间部分所描述的。

12.2.2驱动程序控制及设备控制

如果色彩管理控制由驱动程序或打印机硬件提供，驱动程序的打印机图形DLL必须在DEVINFO结构中设置GCAPS_ICM标记。

驱动程序必须指明对CYMK色彩空间的支持(如果合适)，如在支持CMYK色彩空间部分所描述的内容。

打印机图形DLL必须定义下面三个函数：

DrvIcmCreateColorTransform

DrvIcmDeleteColorTransform

DrvIcmCheckBitmapBits

GDI调用DrvIcmCreateColorTransform函数用ICC Profile(在平台的SDK文档中描述)提供给打印机驱动程序。这些给定的Profile，函数可以创建一个内部的色彩变换以在更正色彩信息时候时使用。色彩变换是一个特定驱动程序，内部定义的从一个色彩空间到另外一个空间的映射。函数返回一个到变换的句柄，并且由GDI存储。

在BRUSHOBJ及XLATEOBJ结构中的标记指明色彩管理是否由系统(或应用程序)来执行或驱动程序(或设备)来执行。在每一个驱动程序实现的接收一种或所有函数的DDI绘制函数时，标记也必须检查。如果系统中应用程序当前正在处理色彩管理，驱动程序或设备则不处理。如果驱动程序或设备色彩管理被允许，DDI函数必须调用BRUSHJOBJ_hGetColorTransform或XLATEOBJ_hGetColorTransform函数。

处理专有色彩管理

对一些设备，不管ICM是否已经被允许，专有的色彩管理都执行(通过驱动程序或硬件)许。这些设备的驱动程序不管ICM是否已经被允许，都不许执行色彩改正。这些设备的驱动程序收到的图像数据已经被修改时不允许执行色彩改正。预先修改的数据可以被收到，如果：

■一个应用程序有已经进行了色彩修改了的图像“在设备设备环境之外(OutSide the DC)”(参考平台的SDK文档)。

■色彩管理由系统来处理。

对这两种情况的任一种，在BRUSHOBJ中的FlColorType成员的BR_HOST_ICM标记和在XLATEOBJ中的flXlate成员的XO_HOST_ICM标记必须被设定。甚至在DEVMODE的dmICMMethod成员设置为DMICMMETHOD_NONE时这些标记也可以被设定。

12.2.3支持CMYK色彩空间

不管是否一个色彩管理是由应用程序、系统、驱动程序或设备来处理，一个打印机图形DLL必须指明是否它支持CMYK色彩空间。这是由在DEVINFO结构中设定GCAPS_CMYKCOLOR标记来完成的。如查这一标记被设置并且CMYK的Profiles文件被使用，则GDI为位图、画刷及笔发送CMYK色彩数据到打印机图形DLL，而不是RGB数据。GDI也设置下面的标记：

■BRUSHOBJ结构中的flColorType成员中的BR_CMYKCOLOR标记。

■XLATEOBJ结构中的flXlate成员中的XO_FROM_CMYK标记。

注意，如果驱动程序支持CMYK色彩空间，它也必须支持过渡调色。这样，如果驱动程序在DEVINFO结构中设置GCAPS_CMYKCOLOR标记，它也必须设置GCAPS_HALFTONE标记。

12.2.4对JPEG及PNG图像的色彩管理

对提供硬件JPEG及PNG压缩图像支持的打印机来说，色彩管理必须由驱动程序或设备来处理面不是由GDI来处理。

在应用程序发送JPEG或PNG压缩图像到打印机之前，它将用CHECKJPEGFORMAT或CHECKPNGFORMAT的序列码调用ExtEscape。这将导致对驱动程序的DrvQueryDeviceSupport函数的调用，具有一个查询类型为QDS_CHECKJPEGFORMAT或QDS_CHECKPNGFORMAT及一个缓冲区包含压缩数据。

驱动程序可以检查图像数据并决定是否它能支持该图像。如果XLATEOBJ结构的XO_DEVICE_ICM标记或XO_HOST_ICM标记被设定时，支持的图像必须执行色彩转换，由于GDI不能在这些图像做色彩转换。

对这些压缩的图象，色彩空间信息通常包含在图像数据中。一个例外就是JFIF文件，它是YcbCr编码的并且对它来说，默认的sRGB空间是一个好的近似值。但是，一个JFIF文件也许可以包含一个专有的指定色彩空间的APPx标记，在这种情况下，驱动程序必须用色彩空间转换图像。

更多的关于支持JPEG和PNG压缩图像的数据，参考在线DDK中的DEVINFO的Comments部分。

12.3放置ICC的Profile文件

当色彩管理被允许，GDI使用下列步骤查询一个适当的ICC profile：

1．如果驱动程序的打印机接口DLL提供一个DrvQueryColorProfile函数，GDI客户端调用该函数以驱动程序一个机会指定profile。如果函数返回一个profile，它就被使用。

2．如果DrvQueryColorProfile不存在或者不返回一个profile，GDI查询已经为特定打印机类型安装的色彩目录的profile。GDI使用它发现的第一个匹配的DEVMODE结构中的精度、媒体类型及抖动的profile。

3．如果色彩目录不包含特定打印机类型的且与指定的DEVMODE内容匹配的任何的profile，则GDI使用系统默认的sRGB Profile。

更多的信息，参考安装ICC Profile部分的内容。另外的关于ICC Profile的信息，可以在平台的SDK文档中找到。

12.4安装ICC的Profiles文件

为给一个打印机安装ICC Profiles，文件必须被列在printer.inf文件中。

下面是一个.inf文件的实例，它使得两个ICC profile被安装。但要注意，为色彩目录所写的profile文件，具有打印机DIRID值是66003。

[Version]

Signature=”$Windows NT$”

Provider=My Company

ClassGUID={4D36E979-E325-11CE-BFC1-08002BE10318}

Class=Printer

[My Company]

My Printer Mode1=MYDRIVER,My_Printer_Model

[MYDRIVER]

DriverFile=DRVR.DRV

DataFile=DRVR.DRV

CopyFiles=@DRVR.DRV,MY_COLOR_PROFILES

DataSection=MYDATA

[MYDATA]

HelpFile=DRVR.HLP

DefaultDataType=EMF

[MY_COLOR_PROFILES]

profile1.icm

profile2.icm

[DestinationDirs]

DefaultDestDir=11

My_COLOR_PROFILES=66003


网络驱动程序设计指南的向导

这一章为网络驱动程序设计指南提供了一个导航，它将以你将编写的内核模式网络驱动程序的类型为基础，告诉你需要参见这个指南的哪些部分。

微软的Windows 2000支持三种基本的内核模式网络驱动程序：

·微端口NIC驱动程序

一个微端口的驱动程序直接控制一个网络接口卡(NIC)，并且为高层的驱动程序提供接口。

·中间层驱动程序

一个中层协议驱动程序连接了上层协议，例如早期的传输驱动程序和一个微端口。开发中层协议驱动程序的一个普遍原因是用它在早期的传输驱动程序和一个微端口之间实现转换。一个微端口控制了一个NIC，对于传输驱动程序来说，它是一个陌生的新介质类型。

·协议驱动程序

一个上层协议驱动程序向网络用户提供服务，它实现了TDI接口，或者也许是为另一个上一层的特殊应用而提供的接口。这种驱动程序在它的下边界提供了一个协议接口，用来向低层驱动程序发送和接收数据包。

另一种协议驱动程序是一种面向连接的呼叫管理器，一个呼叫管理器为面向连接的客户提供了呼叫建立和呼叫撒消服务，呼叫管理器也是协议驱动程序。

Windows 2000支持的另外一种内核模式驱动程序类型是一过滤勾子驱动程序。一个过滤勾子驱动程序用来过滤数据包，它扩展了操作系统提供的IP过滤驱动程序的功能。

无论你将编写什么类型的驱动程序，你都应该参见第一部分“网络驱动程序设计指南”的2—3章。这些章节讨论了Windows 2000的网络结构和编程要素，你也应该参见第五部分的第1章。这一章讨论了网络INF文件，它被用来安装网络组件，如果你的网络驱动程序需要一个发布对象——例如控制绑定——请参见第五部分的第2章。

为了查寻需要参见哪些额外的章节，在在线文档的下表中键入恰当的驱动程序类型，它将跳转到某一部分，来限制下一步选择驱动程序类型的范围。

微端口驱动程序

选择你将编写的微端口驱动程序类型：

无连接的微端口

如果你要为无连接网络媒体，例如Ethernet，FDDI，或Token Ring编写控制一个NIC的微端口，请参见第二部分的第1—7和第9章。

WAN微端口

如果你要编写一个微端口来控制一个WAN NIC的话，请参见第二部分的第1—9章。

面向连接的微端口

如果你要为一个面向连接网络媒体，例如ATM或ISDN，编写控制一个NIC的微端口，，请参见第二部分的第1—7和第9章及第四部分的第1章。

集成微端口呼叫管理器(MCM)

如果你要编写一个微端口来控制一个面向连接的NIC并且也提供呼叫管理服务的话，请参见第二部分的第1—7和第9章，第三部分的第2章和第四部分的第1章。

中间层驱动程序

选择你将编写的中间层驱动程序类型：

面向无连接的下层

如果你要编写一个中间层驱动程序，并在它的下层提供一个对于无连接微端口的接口，请参见第三部分第1章。

面向连接的下层

如果你要编写一个中间层驱动程序，并在它的下层为面向连接微端口提供一个接口，请参见第三部分第1章和第四部分第4章。

协议驱动程序

选择你将编写的协议驱动程序类型：

面向一个无连接的下层

如果你要编写的协议驱动程序为下层的无连接微端口提供接口，请参见第三部分第2章。

面向TDI的上一层

如果你要编写一个拥有一个TDI的上层的协议，请参见第三部分的第2，3，4和第5章。

支持Winsock

如果你要编写一个提供Winsock支持的协议，请参见第三部分的2，3，4，5，6章。

面向连接客户或呼叫管理器

如果你要编写一个面向连接的客户方，它对面向连接的微端口提供了接口，或者你要编写一个面向连接的呼叫管理器，请参见第三部分的第2章和第四部分的第1章。

过滤勾子驱动程序

请参见第六部分第1章。


内核模式驱动程序的网络结构

这一章讲述Windows 2000的内核模式驱动程序的网络结构，这章包含以下几个部分：

2.1 Windows 2000网络结构和OSI模型

2.2 NDIS网络驱动程序

2.3 TDI驱动程序

2.4 网络驱动程序环境

2.1 Windows 2000 网络结构和OSI模型

Windows 2000网络结构是以国际标准化组织(ISO)制定的七层网络模型为基础的，1978年，ISO制定的开放式系统(OSI)参考模型，将网络描述为一系列的协议层，在每个协议层中完成一系列的特定功能。每一层都向上一层提供明确的服务，同时将本层服务的实现封装起来.一个在相邻层之间完善的接口定义了下层对上层所提供的服务以及如何访问这些服务。

(应用层,表示层,会话层,传输层,网络层,链路层,物理层,物理介质 图2.1 OSI参考模型)

Windows 2000的网络驱动程序实现了这个网络结构的下面四层。

物理层

这是OSI模型中的最底层，这一层用来通过物理介质接收和发送的原始的没有结构的二进制数据流，它描述了对于物理介质的电/光，机械和功能的的接口，物理层为所有的上层传送信号，在Windows 2000中，物理层通过网络接口卡(NIC)来实现，物理层的收发是依缚于NIC介质的。使用串行端口的网络组件，物理层也同样包含了底层的网络软件，它定义了串行位流是如何分成数据包的。

数据链路层

电气电子工程师协会(IEEE)将该层进一步分成了两个子层，LLC和MAC。LLC子层用于将数据帧从一个结点无错的传输到另一个结点。LLC子层用来建立和终止逻辑链接，控制帧流，对帧排序，接收帧，并且对没有被接收的帧进行重发。LLC子层使用帧应答和帧的重发为经过链路层的上层提供了真正的无错发送。MAC子层控制物理介质的访问，检查帧的错误，并且管理接收帧的地址认证。在Windows 2000网络结构中，逻辑链路控制子层在传输驱动程序中实现，而介质访问控制子层则在网络接口卡(NIC)中实现，NIC由一个名为NIC驱动程序的设备驱动程序软件控制。Windows 2000的附带了许多常用的NIC的驱动程序。

网络层

这一层控制着子网的运作，它基于以下因素决定数据的物理路径：

·网络状况

·服务优先权

·其他因素，包括路由、流量控制、帧的分解和重组、逻辑到物理地址的映射、用户帐号。

传输层

这一层确保信息传送的无错传输，连续传输和不丢失或不重复。它使得上层协议与上层协议之间或与它同层的协议之间通讯不必关心数据的传输。传输层所在的协议栈至少应包括一个可靠的网络层，或在逻辑链路控制子层中提供一个虚电路。例如，因为Windows 2000 NetBEUI传输驱动程序包括一个与OSI兼容的LLC子层，它的传输层的功能就很小。如果协议栈不包括LLC子层，并且网络层不可靠，并且/或者支持自带地址信息(例如TCP/IP的IP层或NWLINK的IPX层)，那么传输层应能进行帧的顺序控制和帧的响应，同时要对未响应帧进行重发。

在Windows 2000网络结构中，逻辑链路层，物理层和传输层都是通过名为传输驱动程序的软件实现的，它有时也称作协议，协议驱动程序或协议模块。Windows 2000附带了TCP/IP，IPX/SPX，NetBEUI和AppleTalk传输驱动程序。

2.2 NDIS驱动程序

网络驱动程序接口说明(NDIS)库将网络硬件抽象为网络驱动程序。NDIS也说明了网络驱动程序间的标准接口，因此它将用来管理硬件的底层驱动程序抽象为上层驱动程序，例如网络传输层。NDIS也维护着状态信息和网络驱动程序的参数，包括指向函数的指针，句柄和链接时参数块的指针，以及其他系统参数。

正如图2.2所示，NDIS支持以下几种类型的网络驱动程序：

·微端口驱动程序

·中间层驱动程序

·协议驱动程序

(图2.2 NDIS驱动程序)
2.2.1 NDIS微端口驱动程序

一个NDIS微端口驱动程序(也叫微端口NIC驱动程序)有两个基本功能：

·管理一个网络接口卡(NIC)，包括通过NIC发送和接收数据。

·与高层驱动程序相接，例如中间层驱动程序和传输协议驱动程序。

一个微端口驱动程序与它的NIC通信，并且通过NDIS库与高层驱动程序通信。NDIS库对外提供了一整套的函数(NdisXXX函数)，这些函数封装了微端口需要调用的所有操作系统函数。同时，微端口必须向外提供一组入口(MiniportXxx函数)，使NDIS可以为了完成自己或高层驱动程序的任务而访问微端口。

发送和接收操作表明了NDIS与高层驱动程序和微端口NIC的相互作用。

·当一个传输驱动程序发送一个包时，它调用一个NDIS库所提供的NdisXxx函数，NDIS于是通过调用由微端口提供的合适的MiniportXxx函数将包传递给微端口，然后微端口驱动程序通过调用恰当的NdisXxx函数将包传递给NIC来发送包。

·当一个NIC接收到由NIC发给它的包时，它将产生一个由NDIS或NIC的微端口处理的中断。NDIS通过调用恰当的MiniportXxx函数指示NIC的微端口。微端口通过调用恰当的NdisXxx函数把数据从NIC传送到上层驱动程序，并且同时指示上层驱动程序接收包。

NDIS既支持无连接环境下的微端口驱动程序，也支持面向连接的微端口驱动程序。

无连接的微端口为无连接网络介质，例如Ethernt，FDDI和Token Ring，控制NIC。可将无连接微端口进一步分为以下几种子类型：

·串行化的驱动程序,它依靠NDIS对MiniportXxx函数调用进行串行化，并管理它们的发送队列。

·非串行化驱动程序，它自己对MiniportXxx函数操作进行串行化，并且在内部对进入的发送包进行排队。它的意义在于有很高的效率，例如驱动程序的临界区(在一个时间段内只有一个线程可访问的代码区)。面向连接的微端口为面向连接的网络介质，例如ATM和ISDN，控制NIC。面向连接的微端口经常非串行化的——它自己对MiniportXxx函数操作进行串行化，并且在内部对进入的发送抱进行排队。

一个NDIS微端口驱动程序可以有一个非NDIS的底层(参见图2.3)。

（图2.3 拥有非NDIS的底层NDIS微端口）

通过它的非NDIS底层，微端口使用某种类型的接口，例如通用串行总线构架(USB)或IEEEE1394(火线)来控制总线上的设备。微端口通过直接发送I/O请求包(IRPs)到总线或是发送到连接到总线的其他远程设备上来与设备通信。在它的上层，微端口提供了标准的NDIS微端口接口，它使得微端口可以与上层NDIS驱动程序通信。

NDIS也支持广域网(WAN)范围的控制WAN NIC的微端口，有关更多的WAN网络环境的信息，可参见2.4.3节。

2.2.2 NDIS中间层驱动程序

正如图2.4所示，中间层驱动程序一般位于微端口驱动程序和传输协议驱动程序之间。

因为它在驱动程序层结构的中间位置，所以既与上层协议驱动程序通信又要与下层微端口驱动程序通信。

·在它的下界，中间层驱动程序提供了协议入口点(ProtocolXxx函数)，NDIS调用这些函数传递下层微端口的请求。对于一个下层微端口驱动程序，一个中间层驱动程序这时就仿佛是一个协议驱动程序。

·在它的上界，中间层驱动程序提供了微端口的入口指针(MiniportXxx函数)，一个或多个上层协议驱动程序通过NDIS调用这些函数进行通信。对于上层协议驱动程序，一个中间层驱动程序这时就仿佛是一个微端口驱动程序。

（图2.4 在微端口驱动程序和传输驱动程序之间的中间层驱动程序）

虽然向上层提供了MiniportXxx函数的一个子集，但是事实上中间层驱动程序并不管理物理的NIC。它只是向上层协议提供了一个或多个可以绑定的虚拟适配器。对于一个协议驱动程序，一个由中间层驱动程序提供的适配器就如同一个物理的NIC。当协议驱动程序发送包或向一个实际的适配器时发出请求时，中间层驱动程序将传递这些包和请求到下层的微端口。当下层微端口向上发出接收包的指示、响应协议请求的信息或指示状态信息时，中间层驱动程序将这些包、响应、和状态指示传递给绑定在虚拟适配器上的协议驱动程序。

中间层驱动程序有如下几种典型使用方法：

·在不同的网络介质间进行转换

例如，处于Ethernet和Token Ring传输层及一个ATM微端口之间的中间层驱动程序的功能是将Ethenet和Token Ring的包转换为ATM包，反之亦然。

·过滤包

一个包调度器是中间层驱动程序用作过滤驱动程序的很好例子。包调度器读出每个传输层传下来的发送数据包和每一个由微端口指示的接收包的优先权。然后包调度器以它的优先权调度抱的接收和发送顺序。

·在多个NIC间平衡包的负载

负载平衡驱动程序向上层传输协议提供了一个虚拟的适配器，但实际上它却将包分配了给多个NIC。

2.2.3 NDIS协议驱动程序

一个网络协议在NDIS驱动程序层次结构中属于最高层驱动程序，而它经常在实现传输层协议的传输驱动程序中被用作最底层的驱动程序，例如TCP/IP或IPX/SPX。一个传输协议驱动程序分配包，从应用程序中将数据拷贝到包中，并且通过调用NDIS函数将这些包发送到低层驱动程序中。协议驱动程序也为从下层驱动程序中接收包提供了接口。一个传输协议驱动程序将接收到的数据转换成相应的客户应用数据。

在它的下层，协议驱动程序与中层网络驱动程序和微端口NIC驱动程序相连接。协议驱动程序调用NdisXxx函数来发送包，读取和设置由低层驱动程序所维护的信息，以及使用操作系统服务。协议驱动程序也提供了一套入口点(ProtocolXxx函数)， NDIS代表下层驱动程序调用这些函数为自己或向上指示接收包、指示下层驱动程序的状态、以及与协议驱动程序进行通信。

对于上层，传输协议驱动程序对高层驱动程序提供了一个私有接口。

2.3 TDI驱动程序

传输驱动程序接口(TDI)定义了一个内核模式的网络接口，它是为上层的传输协议栈提供的。(如图2.5)

（图2.5 TDI客户和传输层）

TDI客户是内核模式的驱动程序，例如Redirector和Server，它通过TDI与传输层相接。TDI简化了TDI的开发传输驱动程序的工作。同时它也通过减少必需编写的传输层相关代码的数量来简化开发客户的工作量。

传输驱动程序提供了仅可被TDI客户方所使用的TDI接口。为了提供更多的接入传输层的方法，Windows 2000为当前较通用的网络接口，Windows Socket和NetBIOS提供了仿真器模块，每个仿真器模块提供了它自己的一套函数，在用户模型中，可以通过标准的调用机制来调用它们。当调用它们时，仿真器模块将自己的函数和相关参数以及程序控制传递给TDI函数，然后通过IDL调用相关的传输驱动程序。

为了提高它的性能，TCP/IP和IPX/SPX的实现也作为TDI传输驱动程序的一部分。

2.4 网络驱动程序环境

这一节将讲述微软Windows 2000基于以下几种类型的内核模式网络驱动程序的网络环境。

·2.4.1 无连接的驱动程序

·2.4.2 面向连接的驱动程序

·2.4.3 WAN驱动程序

2.4.1 无连接环境的网络驱动程序

图2.6显示了无连接网络程序的NDIS环境。

（图2.6 无连接网络驱动程序环境）

无连接环境是为无连接介质例如Ethernet和Token Ring所提供的标准的网络驱动程序环境。参见2.2节中对这种环境下的驱动程序的描述。

2.4.2 面向连接环境下的网络驱动程序

NDIS支持以下几种面向连接的驱动程序：

·面向连接的微端口

·面向连接的客户方

·呼叫管理器

·集成的微端口呼叫管理器(MCM)

图2.7显示了一个面向连接的客户方，一个呼叫管理器，和一个面向连接的微端口的配置。

（图2.8 带MCM的面向连接环境）

面向连接的微端口控制着一个或多个网络接口卡(NIC)并且在面向连接协议(面向连接的客户方和呼叫管理器)和NIC硬件之间提供了一个接口。

呼叫管理器是一个NDIS协议，它对面向连接的客户方提供了呼叫的建立和断开服务。呼叫管理器使用面向连接微端口的发送和接收功能来与网络入口，例如网络交换机实体或远程的对等层交换信令消息。一个呼叫管理器支持一个或多个信令协议，例如，ATM论坛所提供的ATM UNI3.1。

MCM驱动程序是一个面向连接的微端口，它也对面向连接的客户方提供了呼叫管理服务。虽然一个MCM对客户提供的面向连接的服务与一个呼叫管理器伴随一个面向连接的微端口提供的服务相同，但是呼叫管理器/微端口接口是在驱动程序内部的，因此对于NDIS是不透明的。

多个呼叫管理器和/或MCM可以共存在同一个环境下，并且每个呼叫管理器或MCM可以支持多个信令协议。

一个面向连接的客户方使用呼叫管理器或MCM提供的呼叫建立和断开服务。一个面向连接的客户方同样也使用面向连接的微端口或MCM的发送和接收功能，来发送和接收数据。

面向连接的客户方与一个无连接协议的共同点在于，对于它的上层应用，它们都提供了自己的网络和传输层服务，但是，不同于无连接协议的是，面向连接的客户方对于它的下层，它使用了呼叫管理器和面向连接微端口服务或它使用了MCM服务。

一个面向连接的客户方可以是一个在早期协议和面向连接NDIS之间的一个适配层(它可以是一个中间层驱动程序)。这种适配层的例子如IP/ATM和LAN仿真器(LANE)，它们都对下层连接使用了呼叫管理器服务，但对上层非连接协议都隐藏了面向连接特性的接口。鉴于这样的面向连接客户方的上层接口定义已超出了NDIS文档的范围，如果一个客户方正作为一个适配层服务的的话，那么它的上层接口应是由适应面向连接的NDIS的上层协议定义的。

关于更多的有关面向连接驱动程序的信息请见第四部分。

2.4.3 WAN网络驱动程序的环境

Windows 2000既支持广域网(WAN)通过无连接介质连接，也支持通过面向连接介质的连接。图2.9显示了WAN环境。

下图显示了远程访问服务（RAS）结构。

（图2.9 Windows 2000 WAN环境）

WAN环境包括以下的组件：

·远程访问服务(RAS)

RAS允许用户模式的应用程序使用拨号连接，在一个RAS连接建立以后，一个用户的应用程序可以通过标准的网络接口，例如Windows Sockets、NetBIOS、Named pipe或RPC与网络服务相连。

·TAPI服务提供者

TAPI服务提供者是一个用户模式组件，它通过服务提供接口(SPI)发出的呼叫建立和断开请求，来接受来自RAS客户方和TAPI-aware应用的请求。并将TAPI请求传递给NDIS TAPI（如果呼叫是通过无连接媒体）或TAPI proxy（如果呼叫是通过面向连接媒体）。TAPI服务提供者将把SPI请求转换为TAPI请求。

·NDISTAPL

NDISTAPI是内核模式的组件，它向TAPI设备提供无连接的微端口。NDISTAPI接受来自TAPI服务提供者的呼叫建立和断开请求，并且通过NDISWAN将请求传递给适当的微端口以建立，监听和断开线路及呼叫。

·NDPROXY

TAPI代理是一个内核模式组件，它向TAPI设备提供面向连接的微端口。NOPROXY对于NDISWAN相当于一个呼叫管理器，对于面向连接的微端口相当于一个面向连接的客户方。(更多的关于面向连接的网络环境信息请看2.4.2节)

· NDISWAN

NDISWAN是一个中层NDIS驱动程序，它完成PPP协议/链接组帧，压缩和加密功能。NDISWAN将来自上层传输驱动程序的NDIS_PACKET包转换成NDIS_WAN_ PACKET包并且将转换后的包传到下层的WAN微端口驱动程序。NDISWAN既支持无连接也支持面向连接的微端口。

当工作在一个面向连接的环境下，NDISWAN对于TAPI代理驱动程序相当于一个面向连接的客户方，对一个呼叫管理器提供了NDISWAN接口。

·WAN微端口

一个WAN微端口和非WAN NDIS微端口调用许多相同的NDIS函数并提供许多相同的句柄。但是当一个WAN微端口发送包和向上层协议指示收到包时，它调用特殊的与WAN相关的NDIS函数。WAN微端口也使用NDIS_WAN_PACKET结构而不是NDIS_PACKET结构。同时，WAN微端口维护WAN相关信息并且对这种特殊的WAN的查询信息给予响应。一个WAN微端口支持无连接介质或面向连接的介质。

·串行驱动程序

串行驱动程序是一个对内部串行口或多端口串行卡的标准化设备驱动程序。为Windows 2000内置的异步WAN微端口使用内部串行驱动程序来进行调制解调通信。


网络驱动程序编程要点

这一章将讲述当编写Windows 2000的任何网络驱动程序时通常要考虑的几点问题。

为windows 2000编写的网络驱动程序应完成许多共同的目标：

·跨平台的可移植性

·对于多处理器系统的可变性

·简化软件硬件设置

·基于对象的接口

·支持异步I/O

以下是几节为Windows 2000网络驱动程序编写者讲述设计目标的实现：

3.1 可移植性

3.2 多处理器支持

3.3 IRQLs

3.4 同步和指示

3.5 包结构

3.6 使用共享内存

3.7 异步I/O和完成函数

3.1 可移植性

NDIS驱动程序应很容易在支持Windows 2000的平台间移植。一般说来，从一个硬件平台移植到另一个平台只需要将它在兼容系统的编译中重新编译即何。

驱动程序开发者应当避免调用与操作系统相关的函数，因为这将使他们的驱动程序不可移植。应用NDIS函数替换这些调用，只调用NDIS函数将使代码在支持NDIS的微软操作系统中可移植，NDIS为编写驱动程序提供了很多支持函数，并且直接调用操作系统是不需要的。

驱动程序应用C来编写。更近一步，驱动程序代码应当限制在ANSI C标准并且应当避免使用不被其他兼容系统编译器支持的语言特性。驱动程序代码编写不应当使用任何ANSI C标准指明为“implementation defined”的特性来编写。

驱动程序应当避免使用任何在不同平台上大小和结构变化的数据类型，驱动程序代码不应该调用任何C的运行时库函数，而是限于调用NDIS提供的函数。

在内核模式下漂移指针指针是不允许的。试图运行这样的操作将是一个致命的错误。

如果驱动程序代码要支持特殊平台的特性，那么这些代码应当包含在#ifdef和#endif声明之间。

3.2 多处理器支持

编写的代码应当可以在多处理器系统中安全运行。这对于编写可移植的Windows 2000驱动程序是很重要的。一个网络驱动程序必须使用NDIS函数库提供的多处理器安全保证。

在单处理器环境下，在一个时刻单处理器只运行一条机器指令，既使这样，当包到达或时间中断发生时，对于NIC或其他设备执行的中断也可能发生。典型的，当正在操纵数据结构时，例如它的队列时，驱动程序对NIC发出停用中断来操纵数据，随后再发生可用中断。许多线程在单处理器环境下表现的好像是在同一时刻运行的，但是实际上它们却是在独立的时间片上运行的。

在多处理器环境下，处理器同时可运行多条机器指令，一个驱动程序必须异步化，这使得当一个驱动程序函数正在操纵一个数据结构时，同样的在其他处理器运行的驱动程序函数不能修改共享的数据。在一个SMP机器中，所有的驱动程序代码都要重新装入，为了消除这种资源保护问题，Windows 2000驱动程序使用自旋锁。对于NDIS微端口，NDIS函数库可处理许多这些多处理器问题。NDIS库对请求进行排队和串行化调用标准的微端口函数，除非有以下情况之一：

·在设计和实现中，NIC为无连接网络介质设计，并且微端口设计为非串行化的，这或许是因为NIC有支持包排队和同步化的部分，或者因为驱动程序的编写者喜欢来管理排队和在微端口内实现同步问题。

·NIC为面向连接介质设计，NDIS认为任何面向连接的NIC驱动程序是一个非串行化的微端口。

但是大多数无连接的NIC驱动程序都是串行化的微端口，因为他们依赖NDIS来管理包排队，同步化和串行化问题。

3.3 IRQL

所有NDIS调用的驱动程序函数都运行在系统决定的IRQL下， PASSIVE_LEVEL<DLSPATCH_LEVEL<DIRQL中的一个。例如，一个微端口初始化函数、挂起函数、重启函数和有时的关闭函数通常都运行在PASSIVE_LEVEL下。中断代码运行在DIRQL下，所以NDIS中层或协议驱动程序从不运行在DIRQL下。所有其他的NDIS驱动程序函数运行在IRQL<=DISPATCH_LEVEL下。

驱动程序函数运行于的IRQL将影响调用什么样的NDIS函数。特定的函数只可在IRQL PASSIVE_LEVEL下调用，其他的函数可在DISPATCH_LEVEL或更低层调用。一个驱动程序的编写者应当检查每一个NDIS函数的TRQL限制。

任何与驱动程序的ISR共享资源的驱动程序函数必须能将它的IRQL升级到DTRQL来防止争用情况的发生，NDIS提供了这种机质。

3.4 同步和指示

当两个线程共享可被同时访问的资源时，无论是单处理机还是SMP，同步是必须的。例如，对于一个单处理机，如果一个驱动程序正在访问一个共享资源时，被一个运行在更高IRQL（例如ISR）的函数中断时，必须保护共享资源以阻止这种争用的发生而使资源处于不确定状态。在一个SMP中，两个线程可以在同一时刻运行，在不同处理器上并且试图来修改同一数据的访问必须同步。

NDIS提供了自旋锁可以用来对在同一IRQL下运行的线程间访问共享资源实现同步。当两个线程在不同IRQL下访问共享资源时，NDIS提供了一种机制来临时升高低IRQL代码的IRQL，以使得访问共享资源串行化。

当一个线程依赖于一个外部事件的发生时，指示是必须的。例如，对于驱动程序，当经过一定周期后，必须指示它以使它可以检查它的驱动设备。或者一个NIC设备驱动程序要进行周期性的操作例如轮询，时钟提供了这种机质。

事件提供了一种两个执行的线程可进行同步操作的机质。例如，一个微端口驱动程序通过向设备写数据来测试NIC上的中断，它必须待等一种中断来指示驱动程序操作是成功的。事件可以在等待中断完成的线程与处理中断的线程之间进行同步操作。

以下是这些NDIS机制的描述。

自旋锁

自旋锁提供了一个用来保护共享资源的同步机制，这种资源是单处理器或一个多处理机下的、运行在IRQL>PASSIVE_LEVEL下的、内核模式中的线程所共享使用的。一个自旋锁在同时运行在一个SMP机上不同的执行线程之间提供同步。一个线程在访问保护资源前获得一个自旋锁。自旋锁使得任务线程中只有持有自旋锁的线程可使用资源。一个等待自旋锁的线程将在试图获得锁时间内循环，直到持有锁的线程释放为止。

自旋锁的另一个特点是与IRQL有关。试图获得自旋锁将请求线程的IRQL临时升高为正在执行线程的IRQL。这阻止了运行在同一处理器上的低IRQL线程优先获得执行线程，运行在高IRQL的线程可以优先获得执行线程，但这些线程不能获得自旋锁，因为自旋锁比它们的IRQL低。因此，其他运行在同一处理器上的线程只有到自旋锁被执行线程获得并释放后才可试图获得自旋锁。一个编写很好的网络驱动程序应该会减少自旋锁持有的时间。

一个典型的使用自旋锁的例子是保护一个队列。例如，微端口发送函数MiniportSend将协议驱动程序传来的包进行排队。因为其他驱动程序函数也使用这个队列，MiniportSend必须用一个自旋锁保护这个队列使得在一个时刻只有一个线程可操纵这个队列。Miniport Send获得自旋锁，添加包到队列后释放自旋锁。使用自旋锁保证持锁线程是唯一修改队列的线程，同时使得包被安全地添加到队列中。当NIC驱动程序从队列中取走包时，通过同样的自旋锁保护这个访问。当执行指令修改队列头或任何队列组成域时，驱动程序必须用自旋锁保护队列。

一个驱动程序不能过于保护一个队列。例如，一个驱动程序可以在网络驱动程序将包进行排队之前在包的预留字段进行操作(例如，填充包含长度的字段)。驱动程序可以在自旋锁保护区域之外进行这种操作，但必须是在进行包排队之前。一旦包排入队列并且执行线程释放了自旋锁，驱动程序应认为其他线程可以立即将包从队列中取出。

避免死锁问题

Windows 2000并不限制网络驱动程序同时持有多于一个的自旋锁。但是，驱动程序的某部分在持有自旋锁B时，试图获得自旋锁A，并且其他部分在持有锁A时，试图获得自旋锁B时，死锁就会发生。如果要获得多于一个的自旋锁，驱动程序应当通过强制以某一顺序获得锁来避免死锁，这就是说，如果一个驱动程序强制在获得自旋锁A之后才可获得锁B，那么上述情况就不会发生。

总得来说，使用自旋锁将对系统性能带来负面效应，所以驱动程序不应当使用许多锁。有时，完全不同的函数(例如，发送和接收函数)有不太要紧的重叠可使用两个自旋锁。使用多于一个自旋锁是为了允许两个运行在独立处理器上的函数独立操作的折中方案。

时钟

时钟被用来轮询或进行超时操作的。一个驱动程序可以产生一个时钟并联合一个函数时钟上。当一个特定周期时钟期满时，调用相关函数。时钟可以是一次的或周期性的，一但设置了一个周期时钟，当每个周期结束时都会触发，直到它被完全清除掉为止。一次性时钟在触发后必须重新设置。

时钟通过调用NdisMInitializeTimer来产生和初始化，并且通过调用NdisMsetTimer来设置，也可调用NdisMsetPeriodicTimer设置周期时钟。如果使用了一个非周期时钟，那么通过调用NdisMSetPeriodicTimer重新设置时钟。通过调用NdisMCancelTimer可以清除时钟。

事件

事件在两个执行线程之间实现同步操作。一个事件通过一个驱动程序装入并且通过调用NdisInitializeEvent初始化。一个运行在IRQL PASSIVE_LEVEL下的线程调用NdisWaitEvent来将自身转入等侯状态。当一个驱动程序线程等待一个事件时，它指定了最大等待时间即等待事件的时间。当调用NdisSetEvent使时间得到信号量，或最大等待时间段结束时，它们两个无论是谁先发生时都将结束线程等待状态。

典型的，事件是通过相互协调的线程调用NdisSetEvent来设置的。事件被创建时是没有信号量的，但为了指示等待线程，它必须要设置信号量，事件将一直处于保持有信号状态，直到NdiResetEvent调用后为止。

3.5 包结构

通过一个协议驱动程序可以分配NDIS包、填充数据，并且将它传递到下层的NDIS驱动程序，以便将数据发送到网络上。一些最底层的NIC驱动程序分配包用来保存接收到的数据，并将包传递到对应的高层驱动程序。有时，一个协议驱动程序分配一个包，并且通过一个请求将它传给NIC驱动程序，以使NIC驱动程序将接收到的数据拷贝到提供的包中。NDIS提供函数用来分配和操纵构成包的子结构。

一个包的组成如下：

·一个包描述符包含一个为微端口NIC和协议驱动程序提供的私有区域，一系列与包有关的、由相互合作的微端口协议驱动程序定义含义的标志量，包含包的物理页的长度，包的总长，和一个指向第一个缓存描述符的指针，而这个描述符映射了包中的第一个缓存。

·一组缓存描述符，一个缓存描述符描述了每个缓存虚地址的起始位置，缓存区在页中的偏移量（字节，通过虚地址给出），缓存中字节的总数和指向下一个缓存描述符的指针（如果有的话）。

·虚拟范围，可以多于一页，它组成了由缓存描述符所描述的缓存。这些虚拟页映射到物理内存。

一个总线管理 NIC驱动程序为接收包分配共享内存，或协议驱动程序为发送包分配内存时，必须保证用来存放接收或发出包的缓存与cache对齐，对于微端口在发送包以前刷新缓存确保一致性和在指示上层数据到来前刷新接收缓存，这很重要。

3.6 使用共享内存

用作总线管理DMA设备的微端口驱动程序必须为NIC和NIC驱动程序分配共享内存。当在一个驱动程序和它的NIC之间共享cache时，特别的预防是必须的。在某种结构下，必须采取特别步骤来保证内存一致，因为NIC可以直接访问共享的物理内存，而NIC驱动程序却要通过cache访问内存。这就引起驱动程序和NIC访问内存的不同，即使它们看起来在同一位置。

使用无cache共享可以避免许多与cache共享相关的问题。但是不被cache的内存是一个很稀少的系统资源，并且对这种内存的分配也有很多限制。当必须用写一个小的数据到物理内存时，没有cache的数据将不会很快地写到物理内存中，并且不使用cache不是推荐的也是不可能的。例如，NIC驱动程序反复读数据应当使用cache来提高效率，或者，另一个例子，当要求NIC驱动程序将接收到的数据包传给许多不同的协议驱动程序时，极力推荐在这些情况下使用cache。NIC驱动程序也应当使用cache，来通过协议驱动程序传递任何数据包。

NdisMAllocateSharedMemory可由总线管理NIC驱动程序调用来为网络适配器和NIC驱动程序分配永久共享内存。这个函数返回了共享内存的虚地址和物理地址，这些地址是有效的，直到调用了NdisMFreeSharedMemory来释放内存为止。

当使用共享cache时，NDIS提供了一个必须由NIC驱动程序调用的函数来确保NIC所见到的和NIC驱动程序所见到的是一致的。在为了发送或接收访问共享内存时，NIC驱动程序必须调用NdisFlushBuffer和NdisMUPdateSharedMemory来确保cache一致性。

3.7 异步I/O和完成函数

因为在一些网络操作中有继承的因素，许多由NIC驱动程序提供的上层函数和协议驱动程序提供的下层函数被设计成支持异步操作，而不是用CPU消耗一定时间的循环来等待一个任务的完成或硬件事件的指示，网络驱动程序依赖处理许多异步操作的能力。

通过使用完成函数来支持异步网络I/O。以下的例子将说明网络的send操作如何使用一个完成函数，同样的机制也存在一个协议或NIC驱动程序的其他操作中。

当协议驱动程序调用NDIS发送一个包时，NDIS调用NIC驱动程序的MiniportSend函数发送请求，NIC驱动程序试图立即完成这个请求并且返回一个恰当的状态值。对于异步操作，可能返回NDIS_STATUS_SUCCESS作为发送成功的标志，NDIS_STATUS_RESOURCES和NDIS_STATUS_FAILURE表明有某些失败。

但是一个发送操作要花费一些时间来完成，此时NIC驱动程序(或NDIS)可将包排队并且等侯NIC指示发送操作的结果。NIC驱动程序的MiniportSend函数可以通过返回一个NDIS_STATUS_PENDING的状态值来异步处理这个操作，当NIC驱动程序完成了发送操作后，包调用完成函数NdisMSendComplete在调用中传递指向一个已被发送的包的描述符的指针。这个信息会传给协议驱动程序，指示完成了操作。

许多需要一定时间来完成的驱动程序操作用完成数来完成支持异步的操作。这种函数有同一形式的名字NidisMXxxComplete。不仅可用于发送和接收函数，完成函数也可用于查询、配置、重新设置硬件、状态指示、指示收到数据和传送收到数据。


NDIS NIC微端口驱动程序

这一章介绍了Windows 2000的网络接口卡(NIC)微端口，以下是将要讨论的主题：

·1.1 NIC微端口驱动程序类型

·1.2 网络接口卡支持

·1.3 微端口驱动程序代码的重要特征

·1.4 驱动程序例子

1.1 NIC微端口驱动程序类型

NDIS支持以下几种类型的网络接口卡(NIC)驱动程序：

·无连接微端口 为无连接网络介质控制NIC，例如Ethernet，FDDI和Token Ring。

无连接微端口进一步又可分为以下几种子类型：

·串行化驱动程序 依赖NDIS来串行调用MiniportXxx函数和管理发送队列。

·非串行化驱动程序 它通常运行在多处理器系统上来获得更高的操作性能，它串行化自己的MiniportXxx函数操作，并且在内部对所有发送包进行排队，而不是依赖NDIS来完成这些功能。这种结果显而易见地提高了效率，例如驱动程序的临界区(一个时间内只可一个线程客访问代码区)保持很小，但是，非串行化微端口必须满足额外的并且是极苛刻的设计要求，同时还要求有额外的调试和测试时间。

·面向连接的微端口 为面向连接的网络介质（例如ATM）控制NIC。

面向连接的微端口通常是非串行化的。这就是说，它们经常自己串行化自己的MiniportXxx函数操作，并且在内部进行发送包的排队。

NDIS库继续支持早期的NDIS3.0 NIC驱动程序。但是，只有NDIS4.0和NDIS5.0微端口可以使用扩充功能和当前NDIS库所支持NIC驱动程序的特性。也只有NDIS微端口NIC驱动程序可以使用为NDIS库的将来版本所制定的功能。

1.2 网络接口卡支持

Windows 2000支持以下几种类型的网络接口卡：

·Ethernet (802.3)

·Token Ring (802.5)

·FDDI

·LocalTalk

·Raw ARCNET （ARCNET 封装在 Ethernet)

·ARCNET878.2

·WAN(点对点和WAN卡)

·面向连接的WAN

·无线

·ATM

·IrDA

当NDIS调用一个NIC驱动程序的MiniportInitialize函数时，它传递了一组NDIS支持的介质。如果NIC驱动程序仅支持一种介质类型，它选择它所支持的介质。或者，如果NIC驱动程序支持多于一种的介质类型，它选择它所喜欢的那一种并且将它的选择返回给NDIS。

当一个高层NDIS协议驱动程序调用NdisOpenAdapter来绑定一个具体的NIC时，它提供了一系列它所能操作的介质类型，NDIS使用来自NIC的信息和来自协议驱动程序的信息来正确绑定它们。这个绑定提供了一条路径，通过这条路径，包可以在协议栈中往下传递并通过网络传递出去，通过这条路径，接收到的包可以传到高层驱动程序。

为ARCNET定义的介质类型是NdisMediumArcnet878_2，这种介质类的定义与为RawARCNET定义的Novell的ARCNET说明完全兼容。

Raw ARCNET格式，由Novell出版的《ARCNET Packet Header Definition standard》中定义，它由NetWare使用，支持这种格式的传输驱动程序负责从这三种不同帧格式(short，long或exception)中选择格式，所选的格式依赖于它想送出数据的长度，传输驱动程序负责执行与使用这个标准相关的必要操作，包括组帧，填充数据和设置分隔标志和顺序号等。

NdisMediumArcnet878_2格式允许传输驱动程序知道它是运行在ARCNET上的，但不需要处理帧的分段和组装细节。NdisMediumArcnet878_2的on-the-ware格式与Raw ARCNET相同。但是，许多帧格式的细节对传输驱动程序都屏避了。NIC驱动程序执行组帧操作，选择合式的帧格式，填入数据，设置分隔标志和队列号，以及其他相关字段。NDIS库对发送的包从802.3格式转换成ARCHET格式，对从ARCNET微端口接收到的包从ARCHET格式转换成802.3格式。

Windows 2000 Tcpip和Nwlink传输驱动程序使用NdisMediumArcnet878_2。

Windows 2000 Nbf传输(也叫NetBEUI)则不使用它。

NdisMediumAtm格式允许NIC驱动程序使用NDIS5.0的固有的ATM支持。

NdisMediumIrda格式允许NIC驱动程序使用NDIS5.0的固有的红外线驱动器支持。

NIC的类型

微端口用NdisXxx函数来初始化它的适配器和发送与接收包数据，这些函数依赖于它的NIC的如下特征。

1. 总线管理DMA NIC

这些NIC可以通过一个DMA控制器直接访问主机的内存，DMA控制器用来管理在网络和主机内存之间不经过主机CPU进行数据传输。

当发送时，微端口驱动程序将NIC映射到发送的缓存中。然后微端口触发设备从这个内存开始传输。NIC的DMA控制器将数据从共享的系统内存传输到网络，并且当传输结束后向CPU发出中断。当接收时，DMA控制器将接收的数据传输到主机内存后，向主机发出一个中断指示。

一个总线管理DMA NIC一般地装有一个板上的环行缓冲区，微端口可以将它映射到一系列系统内存中的缓冲区。典型地，可对NIC进行编程来有效地处理好几个包。由于NIC可以有效地处理好几个包，所以管理这种NIC的微端口驱动程序通常支持多包的发送和接收，因此这样就可以提高I/O效率。

2. 非总线管理DMA NIC

这包含三种基本的NIC类型。

·NIC包含板上的共享内存。

微端口管理的NIC必须能将NIC的共享内存映射到主机内存，并且可将发送包拷贝到NIC内存，或者将收到的帧从NIC内存拷贝到由上层协议驱动程序或其他驱动程序提供的缓冲区中。这种微端口基本上不能通过支持多包发送和接收来提高它的能力。

·从属的DMA NIC

一个微端口管理的NIC使用系统的DMA控制器来管理从网络发送和接收的数据包。传输数据要求主机CPU的合作。

·使用可编程的I/O的 NIC

微端口管理的PIO设备使用的NDIS函数以byte， word，long为单位将发送帧转移到设备寄存器，然后让设备发送数据，这种设备的驱动程序并没有从NDIS的多包发送和接收支持中受益。这种NIC类型的微端口只实现单包的发送和接收，如第四章所述。

1.3 微端口驱动程序代码的重要特征

这一节介绍微端口驱动程序代码的以下几个重要特征。

·1.3.1 MiniportXxx函数

·1.3.2 与NDIS库链接

·1.3.3 微端口适配器环境

·1.3.4 VC环境 (仅针对面向连接的微端口)

·1.3.5 网络OID

1.3.1 MiniportXxx函数

典型的微端口驱动程序仅使用很少的函数来通过NDIS与上层和硬件进行通信。NDIS为了完成自身的任务或协议驱动程序的服务,调用在2.2.1节和2.2.2节提供上层(MiniportXxx)函数列表与微端口通信；并不是所有的这些函数都是必须的。3.1.2.2节将介绍哪些函数是可选的，哪些函数是必须的，并且给出了原因。

NDIS NIC微端口和上层驱动程序使用NDIS库(ndis.sys)通过调用NdisXxx函数在彼此间通信。一个关于微端口可以调用的NdisXxx函数和宏的总结在2.3节提供。

许多微端口函数可以异步运行或同步运行。对于异步函数，当操作完成以后，必须调用NdisXxx...Complete。例如，如果一个协议驱动程序调用NdisReset来复位一个NIC，微端口的MiniportReset函数通过返回NDIS_STATUS_PENDING来挂起复位操作。最终，MiniportReset函数必须调用NdisMResetComplete来指示复位请求的最终状态。参见第二章和在线DDK的“Network Drivers Reference”来获得更多的关于异步MiniportXxx函数以及相应的完成函数的信息。

1.3.2 与NDIS库链接

NDIS库封装在ndis.sys中，这个内核模式库提供了一系列函数接口，它侧重于使功能最大化的宏。提供接口的库是一个.sys文件，它的函数与动态链接库相似。NDIS库函数在在线DDK的“Network Drivers Reference”中给予了讲述，包括协议和NIC驱动程序，以及WAN NIC驱动程序，它们都与NDIS库相链接。

Windows 2000 DDK提供了ndis.h作为微端口的主要头文件。这个文件定义了微端口入口点，NDIS库函数，和通常的数据结构。在线的DDK的“Network Drivers Reference”

讲述了微端口，协议和NdisXxx函数，以及通常的数据结构和OID。

1.3.3 微端口适配器环境

NDIS使用叫一个逻辑适配器的软件对象来代表系统中每一个NIC。这个对象由NDIS管理，并且对微端口和协议驱动程序是透明的。NDIS将这个结构的句柄传递给NIC驱动程序的MiniportInitialize函数。以后微端口驱动程序在调用NdisXxx函数时，如果这个函数与此句柄表示的NIC有关，它应在函数中传递此句柄。

当调用微端口NIC驱动程序来初始化一个它管理的NIC时，微端口驱动程序产生自己的内部数据结构来代表NIC。驱动程序使用这个结构，把它当作微端口适配器环境，来维护特定设备的状态信息，这些信息是管理NIC所必须的。当驱动程序的MiniportInitialize函数调用NdisMSetAttributes或NdisMSetAttributesEx时，它将这个结构的句柄传递给NDIS。当NDIS调用与NIC相关的微端口的MiniportXxx函数时，它将指向NIC的句柄传递给适当驱动程序。微端口适配器环境为微端口所拥用并且为微端口所管理，它对于NDIS和协议驱动程序是透明的。

1.3.4 VC环境

在建立一个呼叫之前，面向连接的客户方请求面向连接的微端口建立一个虚拟连接(VC)，通过它可以发送和/或接收包。同样，在向面向连接的客户方指示一个入站呼叫之前，呼叫管理器或集成的微端口呼叫管理器(MCM)请求微端口为入站呼叫建立一个VC。一个虚拟连接是两个面向连接实体间的一个逻辑连接。面向连接的发送和接收通常在一个特定的VC上进行。

面向连接微端口在微端口在为每个VC分配的环境区中保存状态信息。每一个VC的环境由微端口来管理，并且它对于NDIS和协议驱动程序是透明的。在它的MiniportCoCreateVC函数中，面向连接的微端口将VC环境区域的句柄传递给NDIS，NDIS将一个唯一指定的已存在的VC的NdisVcHandle回传给微端口、回传给适当的的面向连接的客户方、回传给呼叫管理器或集成微端口呼叫管理器(MCM)。

在VC上发送和接收数据之前，VC必须被激话。呼叫管理器通过调用Ndis（M）CmDeactivateVc来初始化激活的VC并且向呼叫管理器传递呼叫参数，这包括用来激活VC的参数。作为响应，NDIS调用微端口的MiniportCoActivateVc函数来激活VC。

在一个呼叫结束或因其他原因不再需要VC时，呼叫管理器通过调用Ndis(M)CmDeactivateVc来去活VC，这也将引起NDIS调用微端口的MiniportCoDeactivateVc函数。对于面向连接的客户方或呼收管理器，可以通过调用NdisCoDeleteVc来指示对VC的删除，它将引起NDIS调用微端口的MiniportCoDeleteVc函数。

更多在VC上进行微端口操作的信息，请参见第一部分的 “微端口驱动程序指南”。

1.3.5 网络OID

微端口维护有关它的性能和当前状况的信息，以及有关它所控制的每个NIC的信息。每一个信息类型都由一个对象标识(OID)确认，OID由系统定义。

NDIS和高层驱动程序可以使用OID来查询信息以及在某些情况下设置信息。

·对于无连接介质的高层驱动程序，它调用NdisRequest来查询或设置一个无连接微端口中的信息。为了执行查询操作，NDIS调用微端口的MiniportQueryInformation函数。为了执行设置操作，NDIS调用微端口的MiniportSetInformation函数。

·对于面向连接介质的高层驱动程序，它调用NdisCoRequest来查询或设置面向连接微端口中的信息。为了执行查询和设置操作，NDIS调用微端口的MiniportCoRequest函数。

NDIS将许多系统为微端口定义的OID映射为全局的唯一标识(GUIDs)。NDIS向内核模式的Windows管理检测器(WMI)来注册这些GUIDS。WMI支持用户模式下的基于Web的企业管理(WBEM)应用。当一个WMI客户方查询或设置这些GUIDS中的一个值时，NDIS将这些请求转换为一个恰当的OID查询操作或一个OID设置操作，并且把信息和状态返回给WMI。驱动程序编写者可将自定义的GUID映射为客户OID或微端口状态。一个微端口必须在初始化时向NDIS注册客户的GUID-to-OID或GUID-to-status映射。

更多有关查询和设置OID信息、创建自定义OID以及WMI的NDIS支持，参见第五章。有关系统定义的OID的描述，参见在线的DDK的“Network Drivers Reference”。

1.4 驱动程序例子

DDK包括了用来管理几种类型网络卡的微端口的代码例子。这些驱动程序的例子可以根据驱动程序开发者的要求进行组合。驱动程序的例子包含了可以改写为新的相似的驱动程序的函数。硬件相关的函数必须由驱动程序开发者重写。但是，许多函数都是很标准的。例如，与NDIS库而不是与网络接口卡通信的函数都是很标准的。对于这些驱动程序函数，在驱动程序例子中的代码可以经过很少的改动，有时甚至不经过修改就可使用。


NIC微端口操作和函数概要

本章提供了一个关于NIC微端口操作的概要，例如初始化和发送数据。这些操作涉及MiniportXxx函数和NdisXxx函数。这一章也提供了一个关于MiniportXxx函数的概要（它是由NIC微端口驱动程序提供的），以及关于NdisXxx函数的概要（它是一个由NIC微端口驱动程序调用的）。

这一章包括以下内容：

·2.1 NIC微端口操作

·2.2 微端口上层函数

·2.3 微端口调用的NDIS函数

2.1 NIC微端口操作

NIC微端口所执行的操作如下所示：

·2.1.1 初始化NDIS库和注册微端口驱动程序

·2.1.2 注册网络接口卡

·2.1.3 对查询和设置微端口信息作出响应

·2.1.4 产生、激活、去活和删除虚连接(仅对面向连接的微端口)

·2.1.5 发送数据

·2.1.6指示和传递接受的的数据

·2.1.7 向上层指示硬件状态的变化

·2.1.8 重新设置一个网络接口卡

·2.1.9 卸载和注销驱动程序

2.1.1 初始化NDIS库和注册微端口驱动程序

当装载一个微端口驱动程序时，操作系统为微端口驱动程序产生一个驱动程序对象并且调用驱动程序的DriverEntry函数。在DriverEntry函数的环境中，微端口完成以下工作：

·调用NdisMInitializeWrapper，将驱动程序对象的地址和指向确定程序相关信息的注册表路径作为参数。

·调用NdisMRegisterMiniport来将自己注册到NDIS。包括他兼容的NDIS版本号以及提供他的上层（MinportXxx）函数的入口点。NDIS库保存这些入口指针供以后调用微端口时使用。

如果NdisMRegisterMiniport或NdisIMReqisterLayerdMiniportde的调用没有返回NDIS_STATUS_SUCCESS，那么DriverEntry将返回由NdisMRegisterMiniport或NdisIMReqisterLayerdMiniport返回的值，并且调用NdisTerminateWrapper。对NdisTerminateWrapper的调用将使的NDIS清除微端口调用NdisMInitializeWrapper时分配的资源。

有关更详细的DriverEntry函数的描述，参见3.1节。

2.1.2 注册网络接口卡

在经过网络接收和发送包之前，NDIS库必须注册它所管理的每一个网络接口卡(NIC)。

MiniportInitialize函数做如下工作：

·为维护NIC运行状态的适配器相关环境区域分配内存。

·读取注册表配置数据库来确定它正初始化的NIC的参数。微端口在适配器相关环境区域，为NIC贮存这些信息。

·如果可能的话，读取由I/O总线维护的总线相关信息。有关总线相关信息的例子包括PCI或EISA信息。

·通过调用NdisMSetAttributes或NdisMSetAttributesEx向NDIS注册NIC。一个非串行化微端口必须通过设置NDIS_ATTRIBUTE_DESERIALIZE标志，来调用NdiMSetAttributesEx表明它被串行化了。

·声明操作NIC所需的系统资源。这些资源包括包所需的内存和缓冲区，用来对设备的I/O端口地址进行读写操作，总线管理器要求的映射注册，等等。

·初始化NIC

·如果NIC产生一个中断，注册一个中断

·注册一个关闭句柄

·初始化任何一个用来选取NIC的轮询时钟

有关MiniportInitialize函数的更多详情，参见3.2节。

2.1.3 对查询和设置微端口信息作出响应

为了查询无连接微端口的性能和统计信息，NDIS调用微端口的MiniportQueryInformation函数。NDIS可为完成自身任务或为上层的任务调用这个函数。查询一个面向连接微端口，NDIS调用微端口的MiniportCoRequest函数。MiniportQueryInformation或MiniportCo Request函数获取信息并且返回给NDIS。

为了设置由一个无连接微端口维护的信息，NDIS调用微端口的MiniportSetInformation函数，它设置指定的对象标识并且将操作状态返回给NDIS。为了设置由一个面向连接微端口维护的对象标识（OID），NDIS调用微端口的MiniportCoRequest函数。

微端口查询和设置函数可能是异步的。如果它们是同步完成的，它们立刻返回一个状态代码而不是NDIS_STATUS_PENDING。如果它们是异步完成的，函数返回NDIS_STATUS_PENDING。为了异步完成一个查询或设置，无连接微端口将分别为MiniportQueryInformation调用NdisMQueryInformationComplete或为MiniportSetInformation调用NdisMSetInformationComplete。一个面向连接的微端口调用NdisMCoRequestComplete来完成异步查询或设置。

NDIS库确保微端口不会有一个未完成的查询或设置请求，所以微端口不需要对查询排队。

更多有关查询和设置无连接微端口的内容，参见第五章，有关特定的OID信息，参见在线DDK的“Network Drivers Reference”。

2.1.4 产生，激活，去活，和删除虚连接

一个面向连接的微端口经常通过一个虚连接(VC)来发送和接收数据——无论这个包是为面向连接客户方的还是为一个呼叫管理器发消息。在由VC发送和接收数据之前，一个面向连接微端口必须为每一个VC建立环境，它包括分配和初始化内存，推荐用Ndis...NpagedLookasideList函数完成此项任务。然后微端口必须激活VC，这包括进行NIC所要求的各种处理以满足请求需要。当一个呼叫不再需要VC时，微端口去活VC并且也许会删除VC。

在下列情况下，一个面向连接的微端口产生一个VC。

·在呼叫管理器和微端口完成了它们的初始化操作后，呼叫管理器调用NdisCoCreateVc请求微端口建立一条呼叫管理器可用来发送信令消息的VC。作为这个调用的响应，NDIS调用MiniportCoCreateVc函数，它用来建立每个VC的环境。

·当一个网络管理员手工添加一个永久VC(PVC)时，呼叫管理器调用NdisCoCreateVc来请求微端口建立VC。

·在请求一个呼叫管理器建立一个外出呼叫之前，面向连接客户方调用NdisCoCreateVc请求微端口建立一个VC用来发送呼叫数据。

·在向一个面向连接的客户方指示一个内入呼叫之前，呼叫管理器调用NdisCoCreateVc来请求微端口建立一条VC用来接收呼叫数据。

在一个呼叫结束或因其他原因不再需要VC时，呼叫管理器或集成微端口呼叫管理器(MCM)通过调用NdisCmDeactivateVc来去活VC，它将引起NDIS调用微端口的MiniportCoDeactivateVc函数。对面向连接的客户方或呼叫管理器可以通过调用NdisCoDeleteVc来引发删除VC操作，它将引发NDIS调用微端口的MiniportCoDeleteVc函数。MiniportCoDeleteVc断开由微端口分配的每个VC状态。

更多有关在VC上的微端口操作信息和由NDIS提供的面向连接的结构的概述，参见第四部分的第四章。

2.1.5 发送数据

一个无连接微端口可以有两种函数来发送包：

·MiniportSendPackets

NDIS一次将一个或多个包作为一组指向包标识符的指针传给MiniportSendPackets。

·MiniportSend

NDIS通常一次传给MiniportSend一个包标识符。

面向连接的微端口必须有一个MiniportCoSendPackets函数用来发送包。NDIS一次将一个或多个包作为一组指向包标识符的指针传给MiniportCoSendPackets。

2.1.5.1 多包发送

无连接微端口的MiniportSendPackets或面向连接微端口的MiniportCoSendPackets接收一个指向一组指针的指针，这一组指针指向一个或多个将要发向网络的包。微端口必须保存包传递给Miniport(Co)SendPackees函数的顺序。

NIC驱动程序可以使用NDIS宏从包标识符中取出带外(OOB)数据中的介质特性和权值信息，(参见4.3节)

从串行微端口多包发送

一个串行微端口可以同步或异步地完成发送操作。在它的MiniportSendPackets函数中(如果提供这样的函数)，串行化微端口必须在MiniportSendPackets返回之前，为每个包在OOB数据中设置发送完成状态。任何一种非NDIS_STATUS_PENDING的状态都表明了微端口处理了包(即就是，同步地完成了发送操作)并且返回了包标识符的所有权和与它相关的用来重新使用或将要释放的资源。如果微端口在一个包中返回NDIS_STATUS_PENDING，那么驱动程序保持包资源的所有权，直到微端口调用NdisMSendComplete处理包为止。更多有关从串行微端口进行多包发送的操作参见4.4.1.1节。

从非串行化微端口多包发送

一个非串行化微端口的MiniportSendPackets函数并不能返回NDIS_STATUS_RESOURCES，并且必须通过调用NdisMSendComplete来异步地完成对每一个包的发送。因此，NDIS忽略由非串行微端口发送来的包中的OOB数据发送完成状态。当MiniportSendPackets没有足够资源发送一个给定的包时,非串行化微端口管理自己的内部包队列，而不是依赖NDIS来排队和重提交包，NDIS也不会要求重新发包。一个非串行化微端口负责在其内部队列中保持发送包，直到它们可以发送到网络为止。并且，一个非串行化微端口必须同步它自身函数的执行，以便MiniportSendPackets可以与其他MiniportXxx函数保持一致性执行(MiniportPortReset除外)。更多有关非串行化微端口信息参见4.5节。更多有关从非串行化微端口进行多包发送的细节参见4.4.1.2节。

通过面向连接微端口多包发送

如同一个非串行化微端口，一个面向连接的微端口：

·不能返回NDIS_STATUS_RESOURCES作为多包发送的响应

·在一个多包发送中并没有为每个包指示发送一完成状态

·必须对NDIS传递给它的包进行内部排队

·同步它自己的MiniportXxx函数的执行

面向连接微端口与无连接微端口不同点在于它使用MiniportCoSendPackets并且总是在一个虚连接(VC)上发送(接收)包。更多通过面向连接微端口多包发送的信息，参见4.4.3节。

2.1.5.2 单包发送

串行化和非串行化的非连接微端口可以使用MiniportSend函数，将单个包通过NIC发送到网络上。MiniportSent将包转换成数据帧并且执行发送操作。在微端口实际发送包之前，它可以通过调用NdisGetPacketFlags来查询包标识标，从而读取协议驱动程序传递给NIC驱动程序的包标识符头结构中的flags成员。flags成员包含着发送操作的信息（它不包括在包的数据中），这个信息被用于在相关的协议和定义标记的微端口驱动程序之间进行信息通信。如果驱动程序支持这个特性的话，驱动程序也可以查询带外(OOB)数据的权值信息和介质。

如果在返回之前MiniportSend完成了发送操作，它将返回一个状态代码而不是NDIS_STATUS_PENDING或NDLS_STATUS_RESOURCES。完成发送操作意味着微端口驱动程序完成了包标识符和它所指向的资源，并且将资源返回给调用者以重新使用或等待释放。

在异步情况下，微端口返回NDIS_STATUS_ RESOURCES，并且当它对包操作以后，必须调用NdisMSendComplete，用来表明释放了包，以便重新使用或等待删除包。

串行化微端口可以返回NDIS_STATUS_PENDING来表明它没有足够的内部资源发送包。在这种情况下，NDIS将返回的包标识符在内部排队，并且当有更多资源可用时，或微端口调用NdisMSendComplete或NdisMSendResourcesAvailable时，不管哪一个事件先发生，都促使NDIS将它再提交给NIC驱动程序。

当串行化微端口调用NdisMSendComplete或NdisMSendResourcesAvailable时，它向NDIS表明发送资源是可用的，并且微端口已作好了接收新包标识符的准备。

更多有关从无连接微端口单包发送的描述参见4.4.2节。

2.1.6 指示和传递接收的数据

一个无连接微端口可以调用以下函数中的一种来将到来的数据指示给上层：

·NdisMIndicateReceivePacket：参数为一个指向一组指向一个或多个接收数据包标识符的指针的指针。

·NdisMXxxIndicateReceive，参数为一个指向预先准备的缓存地址的指针，相关协议驱动程序将数据拷贝到这个缓存。

一个面向连接的微端口必须调用NdisMCoIndicateReceivePacket，将到来的数据传送到上层。

2.1.6.1 多包接收

当一个无连接微端口调用NdisMIndicateReceivePacket或一个面向连接的微端口调用NdisMCoIndicateReceivePacket时，微端口将一个指向已完成包的标识符的指针上传给相关协议驱动程序。向上指示的每一个包标识符的返回状态都必须独自完成设置。任一个NdisM(Co)IndicateReceivePacket返回未定状态的包标识符都将为上层所保留。包标识符所描述的资源将在以后由微端口的MiniportReturnPacket函数返回。如果NdisM(Co)IndicateReceivePacket返回任意一个非NDIS_STATUS_PENDING的状态，它所描述的包标识符和资源的所有权将返回给微端口。

一组指向已指示包的指针的所有权在从NdisM(Co)IndicateReceivePacket返回时，就已返回给了微端口驱动程序，而不用考虑任一个包标识符的返回状态。

面向连接微端口应指出包从那个VC上接收到的。对于从网络接收内入呼叫的面向连接呼叫管理器或集成微端口管理器(MCM)的请求，要求微端口提前建立和激活VC(这些操作是由NDIS调用微端口的MiniportCoCreateVc和MiniportCoActivateVc函数完成的)。微端口在NdisMCoIndicateReceivePacket中传递NDIS提供的VC句柄，有关多包接收的细节参见4.6.1。

2.1.6.2 单包接收

无连接微端口调用特定协议的NdisMXxxIndicateReceive函数来向上层指示一个单独的包。当网络接口卡(NIC)从网络接收到一个数据包，它的微端口通过为网络介质调用恰当的函数向上指示一个包：

·对于ARCNET，微端口调用NdisArcIndicateReceive

·对于Ethernet，微端口调用NdisEthIndicateReceive

·对于FDDI，微端口调用NdisFddiIndicateReceive

·对于Token Ring，微端口调用NdisTrIndicateReceive

微端口向上指示一个部分包，如果包很小的话也许会是一个完整的包。NDIS将这个指示传递给所有相关协议。如果它是一个部分包并且协议对完整包感兴趣，那么协议调用NdisTransferData请求包的剩余部分，它向下传递一个由分配协议分配的包描述符。

然后，NDIS调用微端口的MiniportTransferDate函数通知微端口将数据传递到链接在包描述符中的协议分配缓存中，MiniportTransferData将接收到的数据帧拷贝到缓存中。

如果MiniportTransferData同步完成，它仅仅将接收到的数据写入所提供的缓存中，并且返回非NDIS_STATUS_PENDING状态。如果MiniportTransferData在返回之前不能完成传递操作，它将返回NDIS_STATUS_PENDING。当数据传送完毕，微端口必须调用NdisMTransferDataComplete。

在NIC完成接收所有发送来的数据帧之后，微端口为网络媒体调用恰当的NdisMXxxIndicateReceiveComplete函数。这将导致对上层协议驱动程序的调用，指示上层协议驱动程序可以执行post—receive处理。

更多有关单包接收的描述，参见4.6.2节。

2.1.7 指示状态

一个无连接微端口调用NdisMIndicateStatus，指示它管理的NIC的状态变化，面向连接微端口调用NdisMCoIndicateStatus来指示这种变化。

NdisM(Co)IndicateStatus传递这个调用到所绑定的协议驱动程序，协议驱动程序将解释基本的状态代码。然后，协议驱动程序、NDIS库、WMI或其他OS入口可以记录状态变化，采取适当的动作，或者两者都进行。

无连接微端口在发送状态变化时，必须指示状态变化。在一个或多个NdisMIndicateStatus调用之后，无连接微端口调用NdisMIndicateStatusComplete。NDIS将完成调用传递给协议驱动程序使得它们可以进行任何必要的后续处理。当一个面向连接微端口发送状态变化时，它不用进行指示。

微端口不允许在它的MiniportInitialize，MiniportISR，MiniportHalt，或MiniportShutdown函数的环境中指示状态。

有关指示状态的细节，参见5.4节。

2.1.8 复位网络接口卡

当下列情况发生时，NDIS复位NIC：

·NIC驱动程序的MiniportCheckForHang函数返回TRUE

·当一个对微端口的请求(查询或设置)操作超时

·当一个串行微端口发送操作超时

当NDIS库决定必须重置一个NIC时，它调用微端口的MiniportReset函数。MiniportReset向NIC发布一个硬件重置，并且更新它自己的软件状态。

MiniportReset以一个成功或失败状态代码同步的完成，或以NDIS_STATUS_PENDING异步的完成。如果函数是异步的，微端口随后调用NdisMResetComplete来允许后续处理。

当一个重置正在进行时，NIC驱动程序必须保证NDIS不会发送任何请求，并且，NDIS将完成任何目前未完成的请求或发送，所以微端口不需要明确地完成它们。

有关重置串行微端口管理的NIC的细节描述，参见7.1节。

2.1.9 终止一个微端口NIC驱动程序

NDIS通过调用微端口的MiniportHalt函数来终止一个微端口。MiniportHolt函数释放由微端口所声明的系统资源。例如，如果MiniportInitialize函数通过NdisMRegisterInterrupt注册了一个中断，那么微端口的MiniportHalt函数必须通过NdisMDeregisterInterrupt注销这个中断。

如果微端口初始化一个NIC或声明所需的系统资源失败，它的MiniportInitialize函数释放所有分配给微端口的资源。否则，微端口将在它的MiniportHalt函数的环境中释放它的资源。

由于一个微端口的MiniportShutdown函数不应释放任何已分配资源，MiniportShutdown函数应当停止任何正在进行的数据传输并且将NIC置为初始状态。

有关终止一个微端口NIC驱动程序的更多信息，参见7.2节。

2.2 微端口上层函数

这一节提供了由微端口支持的上层函数的概述。NDIS为完成自身任务或其他层的网络软件的任务而调用这些函数。例如，这个高层网络软件可以是一个绑定到由NIC驱动程序管理的NIC上的高层协议驱动程序。一个微端口NIC驱动程序必须提供一些或所有的上层函数。

这里所讨论的微端口驱动程序函数都使用Miniport作为一个基本的前缀，但是为一个实际的微端口驱动程序编写函数，应当使用更多的描述前缀以使得容易调试。因为函数的地址(而不是名字)在初始化时传递到NDIS库，驱动程序开发者可自由命名MiniportXxx函数。

有一些NIC驱动程序函数本来就是同步的，而其余的可同步完成或异步完成。当一个微端口函数返回NDIS_STATUS_PENDING时，微端口必须通过调用恰当的NdisM...Complete函数来完成请求。NDIS调用其他层的用来完成异步请求的函数，与这些层相协调操作。

2.2.1 无连接微端口的上层函数

下表中总结了NDIS定义的一个无连接微端口能提供或必须提供的微端口驱动程序函数。

函数 | 描述 | 异步选项
DriverEntry | 由操作系统调用来激活和初始化微端口驱动程序 | N/A
MiniportAllocateComple | 调用它来指示以前调用的NdisMAllocateMemoryAsync已经完成 | 否
MiniportCheckForHang | 检查NIC的内部状态 | 否
MiniportDisableInterrupt | 禁止NIC产生中断 | 否
MiniportEnableInterrupt | 允许NIC产生中断 | 否
MiniportHalt | 重新分配和重新注册NIC占用的资源并且终止NIC以使它不再有用 | 否
MiniportHandleInterrupt | 延期用来完成I/O中断函数的执行 | 否
MiniportInitialize | 出始化NIC | 否
MiniportISR | 作为NIC中断服务例程以高权值运行 | 否
MiniportQueryInformation | 查询微端口驱动程序的性能和当前状态 | 否
MiniportReconfigure | 未使用 | 否
MiniportReset | 对NIC发出一个硬件重置 | 可以
MiniportReturnPacket | 从上层接收一个包，这个包是在这之前通过调用NdisMIndicateReceivePacket函数传上去的包 | 否
MiniportSend | 如果驱动程序没有MiniportSendPadcets或MiniportWanSend函数，它用来将一个包通过NIC传递到网络上 | 可以
MiniportSendPackets | 通过NIC传递一组包到网络上 | 可以
MiniportSetInformation | 变换(设置)关于微端口驱动程序或它的NIC的信息 | 可以
MiniportShutdown | 当系统暂时关闭时，将NIC恢复到初始状态 | 否
MiniportSynchronizeISR | 同步访问与MiniportISR或MiniportDisableInterrupt共享的资源。如果有运行在DIRQL下的NIC驱动程序函数与驱动程序的MiniportISR或MiniportDisableInterrupt函数共享资源，需要此函数 | 否
MiniportTimer | 如果微端口的NIC不产生中断，用于轮询NIC的状态 | 否
MiniportTransferData | 将由NIC接收到的包的内容拷贝到一个给定的包缓存中 | 可以
MiniportWanSend | 如果驱动程序控制着WAN NIC，通过NIC发送一个包到网络上 | 否

下表总结了NDIS完成函数，它用来对每个无连接微端口的异步上层函数进行响应。

函数 | 异步完成函数
MiniportSend MiniportSendPackets | NdisMSendComplete
MiniportQueryInformation | NdisMQueryInformationComplete
MiniportReset | NdisMResetComplete
MiniportSetInformation | NdisMSetInformationComplete
MiniportTransferData | NdisMTransferDataComplete

2.2.2 面向连接微端口的上层函数

下表总结了一个面向连接微端口可以或必须提供的NDIS定义下的微端口驱动程序函数。

函数 | 描述 | 异步选项
DriverEntry | 由操作系统调用来激活和初始化微端口驱动程序 | N/A
MiniportAllocateComplete | 调用它来指示以前调用的NdisMAllocateMemoryAsync已经完成 | 否
MiniportChecKForHang | 检查NIC的内部状态 | 否
MiniportCoActivateVc | 激活一个虚连接(VC) | 可以
MiniportCoCreateVc | 为一个VC建立VC状态 | 否
MiniportCoDeactivateVc | 为一个VC释放VC的状态 | 可以
MiniportCoDeleteVc | 删除一个VC | 否
MiniportCoRequest | 查询微端口驱动程序的能力和当前状态或改变(设置)有关微端口驱动程序或它的NIC的信息 | 可以
MiniportCoSendPackets | 通过一个NIC发送一组包到网络上 | 可以
MiniportDisableInterrupt | 禁止一个NIC产生中断 | 否
MiniportEnableInterrupt | 允许一个NIC产生中断 | 否
MiniporeHalt | 收回和注销NIC占用的资源并且终止NIC以使它不再使用 | 否
MiniportHandIeInterrupt | 延期用来完成I/O中断函数的执行 | 否
MiniportInitialize | 初始化NIC | 否
MiniportISR | 作为NIC中断服务例程以高权值运行 | 否
MiniportReconfigure | 未使用 | 否
MiniporeReset | 对NIC发出一个硬件重置 | 可以
MiniportReturnPacket | 从上层接收一个包，这个包是在这以前通过调用NdisMCoIndicateReceivePacket传上去的 | 否
MiniportShutdown | 一个可选择函数，当系统关闭时，将NIC恢复到它的初始状态 | 否
MiniportSynchronizpISR | 同步访问与MiniportISR或MiniportDisableInterrupt共享的资源，如果有运行在DIRQL下的NIC驱动程序函数与驱动程序的MiniportISR或MiniportDisableInterrupt函数共享资源 | 否
MiniportTimer | 如果微端口NIC不产生中断， 轮询一个NIC状态 | 否
MiniportWanSend | 如果驱动程序控制着WAN NIC ，通过网络接口卡发送一个包到网络上 | 否

下表总结了NDIS完成函数，它用来对每个对面向连接微端口可能实现的异步上层函数进行响应。

函数 | 异步完成函数
MiniportCoActivateVc | NdisMCoActivateVcComplete
MiniportCoDeactivateVc | NdisMCoDeactivateVcComplet
MiniportCoRequest | NdisCoRequestComplete
MiniportCoSendPackets | NdisMCoSendComplete
MiniportReset | NdisMResetComplete

面向连接微端口与无连接微端口关于MiniportXxx函数的不同点如下：

·面向连接微端口必须能产生，激活，去活和删除VC。MiniportCoCreateVc，MiniportCoActivateVc，MiniporeCoDeactivateVc和MiniportCoDeleteVc函数完成这些任务。

·无连接微端口的MiniportSend或MiniportSendPackets函数在面向连接微端口中由MiniportCoSendPackets函数取代。

·无连接微端口的MiniportQueryInformation和MiniportSetInformation函数在面向连接微端口中由MiniportCoRequest函数取代。

·由于面向连接微端口总是将完整的包指示给绑定的协议，所以它们不需要MiniportTransferData函数。

2.3 由微端口调用的NDIS函数

当一个NDIS微端口需要（例如自旋锁或内存缓存区的）系统资源时，它必须调用NDIS(NdisXxx)函数或宏。然后，NdisXxx函数调用内核模式的支持例程来满足微端口的请求。

NDIS定义了几百个函数和宏，它们中的许多都可被微端口调用。NDIS库可以想像成标准的C库，它为编写一个NIC驱动程序提供了丰富的框架。仅有一个NDIS子集函数可能被任何给定的NIC驱动程序调用。

NDIS库函数作为一个接口库(ndis.lib)被提供。NDIS协议和微端口都可链接到这个接口库。

NDIS库向微端口NIC驱动程序提供了以下几种类型的函数：

·初始化和注册函数

·硬件设置函数(为ISA，EISA，PCI和PC卡NICS)

·I/O端口函数

·与DMA相关联的函数

·中断处理函数

·同步函数

·状态函数

·无连接微端口的发送和接收函数

·面向连接微端口的发送和接收函数

·带外数据宏

·包和缓存处理函数

·支持函数

·媒体相关宏

以下几节总结了可被一个微端口驱动程序调用的NDIS库函数。

2.3.1 NDIS提供的初始化和注册函数

在初始化当中，微端口驱动程序对自身进行设置，并且通过调用由NDIS提供的设置函数向NDIS进行注册。有些函数访问由微端口的INF文件写入注册表的设置信息。

下表中描述了由NDIS提供的初始化和注册函数。

函数 | 定义
NdisMInitializeWrapper | 为微端口驱动程序初始化NDIS库数据结构
NdisMRegisterMiniport | 向NDIS提供有关微端口驱动程序的信息
NdisMSetAttributes | 向NDIS库报告微端口驱动程序所支持的NIC的类型并且传递指向微端口的环境区域的句柄。NDIS将在接下来的调用中把这个句柄传递给MiniportXxx函数
NdisMSetAtteributesEx | 除了如同NdisMSetAttributes一样传递同样的信息，这个函数改变了缺省的NIC超时动作，并且允许一个微端口指定一些附加的标志。一个驱动程序，例如一个NDIS中间层驱动程序或一个非 串行化微端口驱动程序，如果必须指定一个或多个这种标志的话，则必须调用这个函数而不是NdisMSetAttributes
NdisMGetDeviceProperty | 检索出设备对象，这个对象被用来通过一个总线驱动程序来建立一个与NIC的通信。例如，USB和IEEE 1394总线驱动程序
NdisMQueryAdapterResources | 返回一个NIC硬件资源列表
NdisMQueryInformationComplete | 指示NDIS先前的MiniporeQueryInformation操作完成。仅被无连接微端口调用
NdisMSetInformationComplete | 报告NDIS库，以前的MiniportSetInofrmation操作已经完成，仅被无连接微端口调用
NdisOpenConfiguration | 提供一个调用者可以用来调用NdisReadConfiguration，NdisWriteConfiguration，NdisOpenConfigurationByIndex或NdisOpenConfigurationByName的句柄
NdisOpenCanfigurationByIndex | 打开一个给定的已打开的注册表中的主键值的一个子键，而这个主键是由一个调用者所提供的句柄指定
NdisOpenConfigarationByName | 打开一个给定的已打开的注册表中的主键值的一个指定的子键，这个主键是由一个调用者所提供的句柄指定
NdisReadConfiguration | 使用由调用NdisOpenConfiguration所获得的参数句柄来读取存储在注册表中的指定键名的键值，键值将传给NdisReadConfigration
NdisWriteConfiguration | 使用由调用NdisOpenConfiguration所获得的参数句柄将键值写入注册表，键值传给NdisWriteConfiguration
NdisCloseConfiguration | 关闭一个通过调用NdisOpenConfiguration而打开的注册表句柄
NdisMRegisterAdapterShutdownHandler | 注册一个NIC驱动程序提供的Miniportshutdown函数，它在系统将要关闭时被调用
NDIS_INIT_FUNCTION | 注明一个驱动程序函数仅在初始化时开始运行
NDIS_PAGEABLE_FUNCTION | 注明一个驱动程序函数是可分页代码

2.3.2 NDIS提供的硬件设置函数

NDIS库为EISA，PCI和PC卡提供了与总线相关的函数。这些函数由MiniportInitialize调用来获取初始化网络接口卡(NIC)所需的总线相关信息。

下表描述了由NDIS提供的硬件设置函数。

函数 | 定义
NdisReadEisaSlotInformation | 读取EISA NIC插槽信息并且将它拷贝到NIC驱动程序所提供的一个缓存中
NdisReadEisaSlotInformationEx | 为一个支持可选择设置的EISA NIC读取插槽信息和有关的一系列函数的信息，并且将这些设置拷贝到NIC驱动程序所提供的缓存中
NdisImmediateReadPciSlotInformation | 从一个指定PCI设备的设置区间读取一个指定长度的字节
NdisImmediateWritePciSlotInformation | 向一个指定PCI设备的设置区间写入一个指定长度的字节
NdisMPciAssignResources | 返回一个指定PCI设备所声明的资源列表
NdisReadPciSlotInformation | 从一个指定PCI设备的设置区间读取一个指定长度的字节
NdisWritePciSlotInformation | 向一个指定PCI设备的设置区间写入一个指定长度的字节
NdisReadPcmciaAttributeMemory | 为一个PC卡的NIC从属性内存中读取与总线相关的设置参数
NdisWritePcmciaAttributeMemory | 为一个PC卡NIC向属性内存写入与总线相关的设置参数
NdisReadNetworkAddress | 返回软件可设置的网络地址，它是在NIC安装在机器中时为NIC存储在注册表中的

2.3.3 NDIS提供的I/O端口函数

NDIS库提供了一系列微端口网络接口卡(NIC)的驱动程序，可以用来访问I/O端口的函数。这些调用提供了一个标准的轻便接口，它支持NDIS驱动程序在不同的操作环境下运行。提供的函数用来映射端口，声明I/O资源，并且用来对映射和未映射I/O端口进行读和写。

下表描述了由NDIS提供的I/O端口函数

函数 | 定义
NdisMRegisterIoPortRange | 为使用NdisRawReadPortXxx和NdisRawWriteportXxx函数建立I/O访问端口
NdisMDeregisterIoProtRange | 删除早期由NdisMRegisterIoPortRange注册的I/O访问端口
NdisMMapIospace | 为随后的内存映射I/O操作映射一定范围的设备内存
NdisZeroMappedMemory | 将早期调用NdisMMapIoSpace映射的内存块以0填充
NdisMoveFromMappedMemory | 将在初始化时由NdisMMapIoSpace所映射的设备内存中的数据拷贝到一个系统空间的缓存中
NdisMoveToMappedMemory | 将数据从一个系统空间缓存中拷贝到在初始化时由NdisMMapIoSpace所映射的设备内存中
NdisMUnmapIoSpace | 释放调用NdisMMapIoSpace所映射的区域
NdisImmediateReadSharedMemory | 在驱动程序调用NdisMMapIoSpace之前从共享内存地址中读取一块数据到缓存
NdisImmediateWriteSharedMemory | 在驱动程序调用NdisMMapIoSpace之前向一个共享内存地址中写入缓存中的数据
NdisRawReadPortXxx | 从一个I/O端口读取唯一的一条数据，具体的函数包括：NdisRawReadPortUchor， NdisRawReadportUlong， NdisRawReadportUshort
NdisRawReadPortBufferXxx | 从一个I/O端口一次读取数据到缓存，具体函数包括：NdisRawReadPortBufferUchar，NdisRawReadPortBufferUlong， NdisRawReadPortBufferUshort
NdisRawWritePortXxx | 向一个I/O端口写入一条数据，具体函数包括：NdisRawWritePortUchar， NdisRawWritePortUlong， NdisRawWritePortUshourt
NdisRawWritePortBufferXxx | 将缓存中的数据写入I/O端口，具体函数包括：NdisRawWritePortBufferUch， NdisRawWritePortBufferUlong， NdisRawWritePortBufferUshort
NdisImmediateReadPortXxx | 在驱动程序调用NdisMRegisterIoPortRange映射端口之前从一个I/O端口读取一条数据。具体函数包括：NdisImmediateReadPortUcha， NdisImmediateReadPortUshort， NdisImmeduateReadPortUlong
NdisImmediateWritePortXxx | 在驱动程序调用NdisMRegisterIoPortRange映射端口之前向一个I/O端口写一条数据。具体函数包括：NdisImmediateWritePortUch， NdisImmediateWritePortUshort， NdisImmediateWritePortUlong

2.3.4 NDIS数据的与DMA相关的函数

微端口驱动程序调用NDIS函数在主机和NIC之间进行直接内存访问(DMA)操作。例如，微端口NIC驱动程序为DMA传递分配和管理主机中的实际内存缓存。下表总结了由NDIS库提供的函数，它允许驱动程序分配，构建和检查这种缓存。

下表描述了由NDIS提供的与DMA相关的函数

函数 | 定义
NdisMAllocateMapRegisters | 为使用总线管理器的DMA设备分配映射注册表
NdisMAllocateShareMemory | 分配系统与一个总线管理器DMA 和NIC共享的内存。它在驱动程序初始化时以IRQL PASSIVE_LEVEL调用
NdisMAllocateShareMemoryAsync | 调用它提升IRQL，例如一个微端口的MiniportHandleInterrupt函数为总线管理器DMA NIC分配共享内存
NdisMCompleteBufferPhysicalMapping | 释放早期为一个总线管理器DMA操作而调用NdisMStartBufferPhysicalMapping所使用的映射注册表。仅在DMA初始化时由调用NdisMAllocateMapRegisters的驱动程序调用
NdisMCompleteDmaTransfer | 指示NDIS库一个从属的DMA操作已完成
NdisMDeregisterDmaChannel | 在微端口驱动程序的DMA通道上释放它的声明
NdisFlushBuffer | 在发送数据到NIC或从NIC转移数据之前，调用它来确保在总线管理器DMA运行的cache和主机物理内存之间一致
NdisGetCacheFillSize | 返回微处理器的以字节为单位的cache边界。DMA NIC的驱动程序可以使用由这个函数返回的信息以避免在DMA传输时与cache断开
NdisMFreeMapRegisters | 释放总线管理器DMA的映射注册表，它是早期由NdisMAllocateMapRegisters分配的
NdisMFreeSharedMemory | 释放早期由NdisMAllocateSharedMemory或NdisMAllocateSharedMemoryAsync分配的内存
NdisMInitializeScatterGatherDma | 为使用DMA驱动程序保留系统资源，仅被非串行化或面向连接微端口调用
NdisMReadDmaCounter | 读取系统DMA管理器的计数器的当前值
NdisMRegistgerDmaChannel | 为将来从属的DMA操作建立一个DMA通道控制
NdisMSetupDmaTransfer | 为从属的DMA传递设置一个主机系统的DMA控制器
NdisMStarBufferPhysicalMapping | 为一个总线管理器DMA操作产生一个物理地址映射，仅在DMA初始化时调用NdisMAllocateMapRegisters的驱动程序调用
NdisMUpdateSharedMemory | 确保在总线管理器DMA操作时从共享内存区读取的数据是最新的
NdisQueryMapRegisterCount | 返回所有可能的映射注册表数量。在调用NdisMAllocateMapRegisters之前驱动程序调用NdisQueryMapRegisterCount来选择实际分配多少注册表

2.3.5 NDIS提供的中断处理函数

NDIS库为多处理器环境提供了几种中断处理函数。这些函数与一个主机处理器上的网络接口卡(NIC)中断相关。

下表描述了由NDIS提供的中断处理函数。

函数 | 定义
NdisMDeregisterInterrupt | 是一个中断处理程序停止接收中断。操作系统断开NIC中断相连的中断服务函数的联系
NdisMRegisterInterrupt | 连接微端口中断服务函数(MiniportISR)和由它的NIC所产生的中断
NdisMSynchronizeWithInterrupt | 任何与MiniportISR或MiniportDisableInterrupt函数共享资源的NIC驱动程序函数必须同步访问这些资源以防止争用发生。通过调用NdisMSynchronizeWithInterrupt和传递一个MiniportSynchronizeISR函数的地址，来使函数与MiniportISR和MiniportDisableInterrupt同步。MiniportSynchronzeISR运行在DIRQL上，所以它可以安全地访问共享资源

2.3.6 NDIS提供的同步函数

时钟函数允许微端口实现一个在驱动程序所要求的间隔后发生的操作。例如，驱动程序可以使用时钟轮询它的NIC。用来在两个线程之间进行同步操作的事件，其中至少要有一个运行在IRQL PASSIVE_LEVEL上。自旋锁被用来同步访问共享资源。

下表描述了由NDIS库为微端口驱动程序提供的同步函数。

函数 | 定义
NdisMCanceltimer | 取消早期由NdisMSetTimer设置的一个时钟
NdisMInitializeTimer | 初始化一个时钟对象并且将对象与一个MiniportTimer函数相连
NdisMSetTimer | 设置一个时钟在一个指定间隔之后停止
NdisMSetPeriodicTimer | 设置时钟每隔一个指定时间后停止，或直到调用NdisMCancelTimer后才停止
NdisMsleep | 引起调用者的线程阻塞指定的间隔。驱动程序在初始化或当停止适配器时，调用NdisMsleep。例如，当等待NIC完成初始化时。NdisMsleep仅在IRQL PASSIVEL_LEVEL下被调用
NdisStallExecution | 引起调用者的线程停止一个指定间隔，不超过50微秒。此时不能使用NdisMSleep，NdisStallExecution仅在升高的IRQL下被调用
NdisInitializeEvent | 产生和初始化一个用来同步驱动程序操作的事件
NdisSetEvent | 为指定事件设置信号量
NdisResetEvent | 重新设置指定事件为无信号状态
NdisWaitEvent | 引起调用者等待到指定事件被指示或指定时间间隔结束时为止
NdisAllocateSpinLock | 初始化一个的NDIS_SPIN_LOCK类型变量，它被用来同步访问非ISR驱动程序函数之间共享的资源
NdisFreeSpinLock | 释放一个在调用NdisAllocateSpinLock过程中初始化的自旋锁
NdisAcquireSpinLock | 获得一个自旋锁来保护在一个SMP安全方式下运行的非ISR驱动程序函数之间的共享资源的访问。运行在IRQL<DISPATCH_LEVEL下的微端口调用这个函数来获得一个自旋锁
NdisReleaseSpinLock | 释放一个早期调用NdisAcquireSpinLock获得的自旋锁
NdisDprAcquireSpinLock | 获得一个在IRQL DISPATCH_LEVEL下的自旋锁。它保护在一个SMP安全模式下运行的非ISR驱动程序函数间的共享资源访问。它比为运行在IRQL DISPATCH_LEVEL上的驱动程序函数调用NdisAcquireSpinLock要快
NdisDprReleaseSpinLock | 释放一个早期调用NdisDprAcquireSpinLock获得的自旋锁
NdisInitializeReadWriteLock | 初始化一个NDIS_RW_LOCK类型变量。NDIS_RW_LOCK变量用来限制对一个非ISR驱动程序线程的共享资源一次进行一个写访问。这个NDIS_RW_LOCK允许多个非ISR驱动程序线程同时读这些资源。这个读访问在写访问时是不允许的
NdisAcquireReadWriteLock | 获得一个调用者用来在多个驱动程序线程的共享资源间进行写或读访问的锁。运行在IRQL<DISPAICH_LEVEL下的微端口调用这个函数来获得一个读—写锁。读—写锁适用经常进行读访问但很少进行写访问的资源
NdisReleaseReadWriteLock | 释放一个在调用NdisAcquireReadWriteLock过程中获得的读—写锁
NdisMSynchronizeWithInterrupt | 任何与MiniportISR或MiniportDisableInterrupt函数共享资源的NIC驱动程序函数必须与这两个函数同步访问资源，以防止争用发生。必须与MiniportISR和MiniportDisableInterrupt同步的函数通过调用NdisMSynchronizeWithInterrupt，与一个也运行在DLRQL的MiniportSynchronizeISR函数同步访问共享资源

2.3.7 NDIS提供的状态函数

下表总结了由NDIS为微端口提供的状态函数。

函数 | 定义
NdisMCoIndicateStatus | 向绑定协议指示一个面向连接NIC的状态变化或一个在网络接口卡(NIC)上的指定虚连接(VC)的状态变化
NdisMIndicateStatus | 向NDIS库指示NIC状态已变化，仅被无连接微端口调用
NdisMIndicateStatusComplete | 向NDIS库指示状态变化已完成，仅被无连接微端口调用
NdisMQueryInformationComplete | 指示早期的MiniportQueryInformation调用已完成
NdisMSetInformationComplete | 指示早期的MiniportSetInformation调用已完成

2.3.8 NDIS为无连接微端口提供的发送和接收函数

无连接介质的微端口或者从它的MiniportSend函数发送一个单包或者从它的MiniportSendPackets函数发送一组包到网络上。

一个无连结介质的微端口驱动程序调用NdisMIndicateReceivePacket向上层传递一个或多个完整的包，或者，微端口调用过滤相关的NdisMXxxIndicateReceive函数，强制相关上层驱动程序从一个单一的接收帧中拷贝数据。如果所有的包已作为预留数据传递了，那么它以NdisMXxxIndicateReceive方式调用，如果帧比预留缓存大，那么调用它的MiniportTransferData函数。

如果NIC驱动程序向上指示一个完整的包，NDIS将调用微端口的MiniportReturnPacket函数返回包，下表描述了当发送和接收包时可被一个无连接微端口调用的NDIS库函数。

函数 | 定义
NdisMIndicateReceivePacket | 向相关上层指示一个或多个包
NdisMArcIndicateReceive NdisMEthIndicateReceive NdisMFddiIndicateReceive NdisMTrIndicateReceive | 指示一个指定介质类型的包正在接收
NdisMArcIndicateReceiveComplete NdisMEthIndicateReceiveComplete NdisMFddiIndicateReceiveComplete NdisMTrIndicateReceiveComplete | 指示一个包接收操作已完成
NdisMsendComplete | 指示早期包发送操作已完成，当早期MiniportSend函数异步操作时使用它
NdisMSendResourcesAvailable | 指示NDIS库，微端口驱动程序有可用于发送操作的资源。如果微端口完成了一个同步发送，在它的MiniportSend函数中调用这个函数，或者当它检测出一个异步发送操作已完成时，在它的MiniportHandleInterrupt函数中调用。如果驱动程序没有为一个未完的发送操作调用NdisMSendComplete时，NdisMsendResourcesAvailable才可被调用
NdisMTransferDataComplete | 指示NDIS库早期的MiniportTransferDate请求已完成，当先前调用的MiniportTransferData函数返回NDIS_STATUS_PENDING时使用这个函数

2.3.9 NDIS为面向连接微端口提供的发送和接收函数

面向连接微端口在它的MiniportCoSendPackets函数中发送一组包到网络上。

面向连接微端口NIC通过调用NdisMCoIndicateReceivePacket和传递一个完整的包或包组,向上层传递一个或多个接收包。NDIS调用微端口的MiniportReturnPacket函数来返回包。

下表描述了当发送和接收包时，面向连接微端口所调用的NDIS库函数。

函数 | 定义
NdisMCoIndicateReceivePacket | 向上层相关层指示一个或多个包，根据每一个VC指示包
NdisMCoSendComplete | 指示早期通过调用MiniportCoSendPackets所发起的包发送操作已完成

2.3.10 NDIS提供的带外数据宏

微端口可使用以下的宏来访问与一个包相关的带外(OOB)数据。有关OOB数据的更多信息，参见4.3节。

函数 | 定义
NDIS_OOB_DATA_FROM_PACKET | 返回一个指向与一个给定包描述符相连的OOB数据块的指针
NDIS_GET_PACKET_MEDIA_SPECIFIC_INFO | 从与一个给定包描述符相连的OOB数据块中返回MediaSpecificInformation指针和SizeMediaSpecificInfo值
NDIS_GET_PACKET_STATUS | 从一个与给定包描述符相连的OOB数据块中返回状态值
NDIS_GET_PACKET_TIME_TO_SEND | 从一个与给定包描述符相连的OOB数据块中返回TimeToSend值
NDIS_SET_PACKET_HEADER_SIZE | 为随后的接收指示设置一个与给定包描述符相连的OOB数据块中的HeaderSize值
NDIS_SET_PACKET_MEDIA_SPECIFIC_LNFO | 设置与一个给定包描述符相连的OOB数据块中的MediaSpecificInformation指针和SizeMediaSpecificInfo值
NDIS_SET_PACKET_STATUS | 在一个驱动程序调用NdisM(Co)IndicateReceivePacket或在一个驱动程序的Miniport(Co)SendPackets函数返回控制之前，设置与一个给定包描述符相连的OOB数据块中的状态值。
NDIS_SET_PACKET_TIME_RECEIVED | 设置与一给定包描述符相连的OOB数据块中的TimeReceived值
NDIS_SET_PACKET_TIME_SENT | 设置与一给定包描述符相连的OOB数据块中的TimeSent值

2.3.11 NDIS提供的包和缓存处理函数

微端口驱动程序必须分配缓存来接收内入的数据。向上层相关协议指示所有内入包的、总线管理器DMA设备的驱动程序必须为完成这一任务分配和管理包。如果从上层接收到的包是小的和分段的，NIC驱动程序需要为发送操作分配阶段数据的缓存空间。对于一个发送，所有的微端口NIC驱动程序接收到一个完整的包，它必须调用NDIS函数来操纵这些包和提取出要发送的数据。一个将接收到的数据向上层驱动程序传递的微端口，从上层接收一个包并且用接收到的数据来填充这个包。NDIS提供的函数使微端口驱动程序可以用来分配，构建和检测包的函数。

NDIS把包定义成为一个拥有包含网络包数据的一个或多个缓存链的包描述符。NDIS通过定义NDIS_PACKET结构来描述一个包描述符。描述符包含两部分：微端口驱动程序可见的公共成分和驱动程序不可见的私有成分。公共成分允许一个微端口驱动程序维护有关包的环境信息。微端口驱动程序可以使用这个组件来将包链入到传输队列中。

下表描述了由NDIS提供的处理包和缓存的函数。

函数 | 定义
NdisAllocatePacketPool | 分配和初始化一个不分页包池块。调用者提供请求可存放包描述符数和以字节为单位的每个固定包的大小
NdisAllocatePacketPoolEx | 分配和初始化一个不分页包池的块。并且提供请求包描述符的数量和以字节为单位的每个固定包的大小，调用者提供用来保存溢出状态下的包描述符的数量
NdisAllocatePacket | 从由NdisAllocatePacketPool返回的包池中分配一固定大小的包描述符
NdisDprAllocatePacket | 当调用者运行在IRQL DISPATCH_LEVEL下，分配和初始化一个包描述符
NdisAllocateBufferPool | 返回一个调用者可用NdisAllocateBuffer来分配缓存描述符的句柄
NdisAllocateBuffer | 在指定的已分配的不分页的内存块中创建一个映射虚存范围的缓存描述符。需给出由NdisAllocateBufferPool返回的句柄
NdisAdjustBufferLength | 修改一个给定缓存描述符指定范围的长度
NdisBufferLength | 返回一个给定映射缓存以字节为单位的长度
NdisBufferVirtualAddress | 如果给定缓存描述符所给出的物理页并没有映射到系统空间的话,返回由一个给定缓存描述符映射的缓存的基本虚地址，并且将这物理页映射到系统空间，
NdisCopyBuffer | 为指定的内存块创建缓存描述符
NdisCopyFromPacketToPacket | 从一个包向另一个包拷贝指定数量的字节，给定一个指定的源包和一个目的包及每个包的起始位置
NdisCreateLookaheadBufferFromSharedMemory | 返回一个与总线管理器DMA NIC共享内存块的缓存的虚地址。允许驱动程序以只读预分缓存方式将向上指示的接收数据的一部分映射到相关协议
NdisDestroyLookaheadBufferFromSharedMemory | 释放由调用NdisCreateLookaheadBufferFromSharedMemory而获得的缓存
NdisQueryPacket | 返回一组描述了包和链的大指针的信息
NdisQueryBuffer | 返回由一个给定缓存描述符映射的一个缓存的虚地址的基址和长度
NdisQueryBufferOffset | 返回在一个给定缓存描述符中映射的一个缓存的虚地址的基址和长度
NdisGetFirstBufferFromPacket | 返回链在包上的第一个缓存的描述符及虚地址，以及第一个缓存的长度和所有缓存的总长（以防缓存使分段的）
NdisGetNextBuffer | 返回给定缓存描述符链当前的缓存描述符
NdisGetBufferPhysicalArraySize | 对一给定缓存描述符，返回不邻接的物理块的缓存数量。总线管理DMA NIC在调用NdisMStarBufferPhysicalMapping之前调用它来决定分配和填充多少NDIS_PHYSICAL_ADDRESS结构
NdisGetPacketFlags | 如果有的话，返回在一个给定包中，由协议驱动程序设置的标志
NdisFreePacketPool | 释放由调用NdisAllocatePacketPool所分配的一个不分页池的块
NdisPacketPoolUsage | 返回一个包池中当前已分配的包描述符的数量
NdisFreePacket | 释放调用NdisAllocatePacket所分配的包
NdisDprFreePacket | 当调用者为访问包池提供内部同步，并且调用者运行在IRQL DISPATCH_LEVEL时，释放驱动程序分配的包描述符并且将它返回给空闲列表
NdisFreeBufferPool | 释放调用NdisAllocateBufferPool所获得的句柄
NdisFreeBuffer | 释放调用NdisAllocateBuffer所获得的缓存描述符
NdisChainBufferAtBack | 将一给定缓存描述符链到一个给定包的缓存描述符链表的未尾
NdisUnchainBufferAtFront | 从一给定包的缓存描述符链表中删除链表的表头并且返回指向这个缓存描述符的指针
NdisUnchainBufferAtBack | 从一给定包的缓存描述符链表中删除表尾，并且返回指向这缓存描述符的指针
NdisReinitializePacket | 从一给定包中删除所有相链的缓存并且重新初始化它以备重新使用
NdisRecalculatePacketCounts | 重新设置多个给定包的有效个数以使下一个调用NdisQueryPacket重新计算个数
NDIS_BUFFER_LINKAGE | 由一指向缓存描述符的指针，返回一个被链接的缓存的指针。这个宏允许一个驱动程序不必提供自己的缓存描述符链接情况而对已分配缓存描述符排队
NDIS_BUFFER_TO_SPAN_PAGES | 确定给定的缓存取要使用多少物理页

2.3.12 NDIS提供的支持函数

以下的NdisXxx函数类提供了支持微端口的操作：

·内存函数

·查询函数

·系统信息函数

·日志函数

·字符串函数

·文件函数

·地址函数

·变量函数

·注册函数

·工作项目函数

内存函数

函数 | 定义
NdisAllocateMemory | 在指定地址范围地将驻留(不分页)的系统空间内存以物理连续分配方式分配和/或以非cache方式分配
NdisAllocateMemorywithTag | 如同调用NdisAllocateMemory，不同在于它允许调用者提供一个标记可用来跟踪驱动程序的内存分配
NdisEqualmemory | 将一个内存块中的指定数量的字符与另一个内存块中相同数量的字符相比较
NdisFillMemory | 用给定字符填充一个调用者所提供的缓存
NdisFreeMemory | 释放早期由NdisAllocateMemory分配的内存块
NdisMoveMemory | 从一调用者提供的位置将一指定数量的字节拷贝到另一地方
NdisZeroMeomory | 用0填充内存块
NdisInitializeNPagedLookasideList | 初始化一个预先分配的列表，在初始化成功以后，可以从预先分配列表中分配或向列表释放不分页的固定大小的块
NdisAllocateFromNPagedLookasideList | 从一给定的预先分配的列表头中删除第一个项目。如果预先分配列表是空的，那么从不分页池中分配一个项目
NdisDeleteNPagedLookasideList | 将一不分页预分配列表从系统中删除
NdisFreeToNPagedLookasideList | 返回预分配列表的入口
NdisInitializeListHead | 初始化一个顺序的，互锁的，单链表的表头
NdisInterlockedInsertTailList | 插入一个项目，通常是一个包，到一个双链表的表尾，这使得可以在一个多处理器安全方式下同步访问列表
NdisInterlockedRemoveHeadList | 从表头删除一个项目，这使得可以在一个多处理器安全方式下同步访问列表
NdisInitializeslistHead | 初始化一个顺序的，互锁的，单链表的表头
NdisInterlockedPopEntrysList | 从一个顺序的，单链表中删除第一个项目
NdisInterlockedPushEntrysList | 在一个顺序的，单链表表头插入一个项目
NdisQueryDepthsList | 返回一个给定的顺序的，单链表中当前项目数目

系统信息函数

函数 | 定义
NdisSystemProcessorCount | 确定它的调用者运行在一个单处理器上还是多处理机上。一个NDIS驱动程序在分配资源之前的初始化中，调用这个函数
NdisGetCurrentProcessorCounts | 返回驱动程序当前可使用的处理器的个数，驱动程序用它来确定CPU的利用情况
NdisGetCurrentProcessorCpuUsage | 以百分比方式返回当前处理器使用率
NdisGetCurrentSystemTime | 返回当前系统时间，用来设置时间戳
NdisGetSystemUptime | 返回自从系统开始以来所经过的时间值，以微秒为单位

日志函数

函数 | 定义
NdisMCreateLog | 分配和打开一个NIC微端口可写入数据的日志文件，这个文件由一个驱动程序提供的Win32应用程序显示
NdisMWriteLogData | 为供驱动程序提供的Win32应用程序使用和显示日志，而将驱动程序提供的信息写入日志文件
NdisMFlushLog | 清除日志文件
NdisMCloseLog | 释放日志所使用的资源
NdisWriteErroLogEntry | 向系统I/O出错日志文件写一条目

字符串函数

函数 | 定义
NdisAnsiStringToUnicodeString | 将一给定数量ANSI字符串转化为一定数量的Unicode字符串
NdisEqualAnsiString | 将两个ANSI字符串比较并且返回一个表明它们是否相等的值
NdisEqualString | 在操作系统缺省的字符集下，比较两个字符串是否相等
NdisEqualUnicodeString | 比较两个Unicode字符串并且返回一个表明它们是否相等的值
NdisFreeString | 释放为缓存字符串分配的存储区
NdisInitAnsiString | 初始化一ANSI字符串
NdisInitializeString | 为一长度字符串分配存储区，并且以系统缺省的字符集初始化它
NdisInitUnicodeString | 初始化一Unicode字符串
NdisPrintString | 在调试窗口显示指定的字符串
NdisUnicodeStringToAnsiString | 将一给定的Unicode字符串转换成ANSI字符串
NdisUpcaseUnicodeString | 将一给定Unicode字符串副本转换为大写形式，并且返回转换后的字符串

文件函数

函数 | 定义
NdisOpenFile | 返回一个已打开文件的句柄
NdisMapFile | 如果文件未被映射，将一个已打开文件映射到一个调用者可访问的缓存中
NdisUnmapFile | 释放一个由NdisMapFile建立的文件的虚地址映射
NdisCloseFile | 释放由NdisOpenFile返回的句柄，并且释放文件打开时所分配给文件contex的内存

地址函数

函数 | 定义
NdisGetPhysicalAddressHigh | 返回一个给定物理地址的高位部分
NdisGetPhysicalAddressLow | 返回一个给定物理地址的低位部分
NdisSetPhysicalAddressHigh | 将一个给定物理地址的高位部分设置为一个给定的值
NdisSetPhysicalAddressLow | 将一个给定物理地址的低位部分设置为一个给定的值
NDIS_PHYSICAL_ADDRESS_CONST | 初始化一个物理地址的静态常量类型

变量函数

函数 | 定义
NdisRetriveUlong | 从源地址中取回一个ULONG值，避免对齐错误
NdisStoreUlong | 存储一ULONG值到一指定地址，避免对齐错误
NdisInterlockedAddUlong | 用一个原子操作将一个Unsigned long值与一个给定Unsigned integer相加，使用一个调用者提供的自旋锁来同步访问integer变量
NdisInterlockedDecrement | 用一个原子操作减一
NdisInterlockedIncrement | 用一个原子操作加一

注册函数

函数 | 定义
NdisReadRegisterUchar | 从一个内存映射设备注册表中读取一个UCHAR
NdisReadRegisterUlong | 从一个内存映射设备注册表中读取一个ULONG
NdisReadRegiseerUshort | 从一个内存映射设备注册表中读取一个USHORT
NdisWriteRegisterUchar | 向一个内存映射设备写入一个UCHAR
NdisWriteRegisterUlong | 向一个内存映射设备写入一个ULONG
NdisWriteRegisterUshort | 向一个内存映射设备写入一个USHORT

工作项目函数

函数 | 定义
NdisInitializeWorkItem | 当一个系统工作线程获得控制时，用一个调用者提供的环境和回调例程，初始化一个工作队列来作为执行排队
NdisScheduleWorkItem | 将一个给定工作项目插入到一个队列。一个系统工作线程删除项目，并且将控制给予驱动程序先前提供给NdisInitializeWorkItem的回调函数

2.3.13 NDIS提供的媒体相关宏

一个微端口可使用这些宏来持行媒体相关操作。

函数 | 定义
ETH_COPY_NETWORK_ADDRESS | 将一个给定Ethernet地址拷贝到一个给定位置
FDDI_IS_BROADCAST | 将一个调用者提供的变量设置为一个布尔值，它表示一个给定的FDDI地址是否是一个广播地址
FDDI_IS_MULTICAST | 将一个调用者提供的变量设置为一个布尔值，它表示一个给定的FDDI地址是否是多点传输地址
FDDI_IS_SMT | 设置一个调用者提供的变量为一布尔值，它表示是否给定的FDDI帧是SMT
TR_COMPARE_NETWORK_ADDRESS | 将一个调用者提供的变量设置为一个布尔值，表明一个给定的Token Ring地址是大于、小于或等于另一个给定Token Ring地址的值
TR_COPY_NETWORK_ADDRESSES | 将一个给定Token Ring地址拷贝到一给定位置
IR_IS_BROADCAST | 将一个调用者提供的变量设置为一个布尔值，它表示一个给定的Token Ring地址是否是广播地址
TR_IS_FUNCTIONAL | 将一个调用者提供的变量设置为一个布尔值，它表示一个给定Token Ring地址是否是函数地址
TR_IS_GROUP | 将一个调用者提供的变量设置为一个布尔值，它表示一个给定Token Ring地址是否是一组地址
TR_IS_NOT_DIRECTED | 将一个调用者提供的变量设置为一个布尔值，它表示一给定Token Ring地址是否既不是一个函数地址也不是一组地址
TR_IS_SOURCE_ROUTING | 将一个调用者提供的变量设置为一个布尔值，它表示一给定Token Ring地址是否是一源路由地址


NIC微端口驱动程序入口点和初始化

这一章讲述了微端口的DriverEntry函数和MiniportInitialize函数。任何NDIS微端口的驱动程序函数都需要它们。

这一章的内容包括：

·在3.1节讲述DriverEntry函数。在DriverEntry环境函数中，一个微端口将自身与NDIS相连，指定它所使用的NDIS版本，并且注册它的入口指针。

·在3.2节讲述MiniportInitialize函数，在它的MiniportInitialize环境函数中，一个微端口注册它所管理的每个NIC，声明系统资源(例如包，缓存，和I/O端口)，并且有可能地注册一个中断，关闭处理程序，和/或初始化轮询时钟。

3.1 NDIS微端口驱动程序入口函数

每个NIC微端口驱动程序必须提供一个DriverEntry的函数。DriverEntry由系统调用来装载驱动程序。DriverEntry产生一个微端口NIC驱动程序和NDIS库的连接，并且向NDIS注册微端口的版本和入口指针。

（图3.1 初始化NDIS库和注册一个微端口）

DriverEntry函数的声明：

NTSTAUS

DriverEntry {

IN PDRIVER_OBJECT DriverObject，

IN PUNICODE_STRING RegistryPath

};

两个参数传递给DriverEntry：

·一个指向驱动程序对象的指针，它由I/O系统产生

·一个指向注册表路径的指针，它讲明了指定驱动程序参数贮存的位置

正如图3.1所示，DriverEntry调用NdisMInitializeWrapper，然后调用NdisMRegisterMiniport。DriverEntry传递两个指针给NdisMInitializeWrappper，它返回一个包裹句柄。DriverEntry传递给NdisMRegisterMiniport。

注册表包含系统重起时永久存在的数据，它同每次系统重起时重新产生的设置信息一样。在驱动程序安装时，描述驱动程序和NIC的数据存储在注册表，注册表包含NIC驱动程序可读取的，用来初始化自身和NIC的适配器的特性，参见《Plug and Play，Power Management，and Setup Design Guide》来获取更多有关注册信息、安装驱动程序和写注册表的INF文件信息。有关网络组件INF文件的更多信息参见第五章。

3.1.1 初始化包裹

DriverEntry函数通过调用NdisMInitializeWrapper函数，（这个函数同调用DriverEntry时使用同样的参数）来使得微端口NIC驱动程序和NDIS相联系。这些参数是指向驱动程序对象的指针和指向注册表路径的指针。NdisMInitializeWrapper分配一个结构来代表这个联系，来存储NDIS库所需的微端口相关的信息，并且返回NdisWrapperHandle，它是代表这个微端口NIC驱动程序结构的句柄。当驱动程序注册它的入口指针时，它必须保留和传递这个句柄到NdisMRegisterMiniport。NDIS将使用NdisWrapperHandle来辩别微端口，微端口必须保留这个句柄，但它不应试图去访问或解释这个句柄。

3.1.2 注册微端口

在DriverEntry函数调用NdisMInitializeWrapper产生与NDIS库的一个连接后，它必须初始化一个NDIS_MINIPORT_CHARACTERISTICS类型的结构。NDIS_MINIPORT_CHARACTERISTICS结构指定了与微端口(见3.1.2.1节)兼容的NDIS版本以及要求的入口点和由微端口提供的可选上层函数(MiniportXxx) (见3.1.2.2节)。然后DriverEntry通过一个指向NDIS_MINIPORT_CHARACTERISTICS结构的指针调用NdisMRegisterMiniport。

3.1.2.1 指定NDIS版本号

NDIS_MINIPORT_CHARACTERISTICS结构中的MajorNdisVersion和MinorNdisVersion成员指定了与微端口兼容的NDIS版本。有效的NDIS版本号是5.0，4.0，或3.0。最新版本是5.0。NDIS继续支持早期为4.0和3.0版编写的驱动程序。

因此NDIS版本号，包括包含在NDIS_MINIPORT_CHARACTERISTICS中的，都必须在微端口源代码编释时指定。,下面显示的是驱动程序使用DIS_MINIPORT_CHARACTERISTICS 5.0版本条件下的包含在微端口源代码中的编译指令。

#define NDIS_MINIPORT_DRIVER

#define NDISS0_MINIPORT 1

#include <ndis.h> // AFTER Preceding directives

下面指令指定NDIS驱动程序版本：

#define NDISXX_MINIPORT

XX是NDIS版本号。

这个定义语句必须在#include <ndis.h>语句之前。

除了使用预编译指令，同样的驱动程序可以在它的源文件中设置，以下构建指令：

...

C_FLAGS=$(C_FLAGS)-DNDIS_MINIPORT_DRIVER-DNDIS50_MINIPORT_DRIVER

...

NDIS_MINIPORT_CHARACTERISTICS结构中指定的版本必须与编译器或为微端口设置的构建指令的版本一致。

3.1.2.2 注册MiniportXxx函数

NDIS_MINIPORT_CHARACTERISTICS结构中每个地址成员必须被初始化——也就是说，它必须被设置成微端口提供的函数地址或NULL。微端口必须为所有MiniportXxx函数提供入口指针;否则NDIS将不允许装载驱动程序。MiniportXxx函数在ndis.h头文件中定义。

因为微端口提供MiniportXxx函数的地址，而不是名字，驱动程序开发者可以自由的以他们喜好命名。一个合理的微端口开发策略是给这函数命名以使调试容易。例如，微端口NIC驱动程序mp.sys命名入口指针为MPISR，MpSend等。

一些MiniportXxx函数是可选择的。例如，如果一个NIC不产生中断，微端口必须轮询它的NIC。这种微端口必须有一个MiniportTimer函数，但不需要MiniportISR函数、MiniportEnableInterrupt函数或MiniportDisableInterrupt函数。

注册无连接微端口的MiniportXxx函数

无连接微端口提供的MiniportXxx函数在2.1.1节描述。

一个无连接微端口必须注册以下MiniportXxx函数：

·MiniportHalt

·MiniportInitialize

·MiniportQueryInformation

·MiniportReset

·MiniportReturnPacket或MiniportTransferData

如果LAN微端口通过调用NdisMindicateReceivePacket或NdisMCoIndicateReceivePacket支持多包接收的指示，并总是向上层驱动程序指示完整的网络包，那么它必须提供一个MiniportReturnPacket函数。如果一个LAN微端口在预分配缓存中指示接收网络包的初始部分，这个预分配缓存是相关上层拷贝接收数据的地方，那么它必须提供一个MiniportTransferData函数。WAN微端口不需要提供了一个MiniportReturnPacket函数或一个MiniportTransferData函数。

·MiniportSend或MiniportSendPackets

LAN微端口必须提供了一个SendPacketsHandler或一个SendHandler。一个WAN微端口必须有一个WanSendHandler。

·MiniportSetInformation

无连接微端口也可（在某些情况下必须）注册以下MiniportXxx函数：

·MiniportAllocateCommplete

如果微端口控制一个总线管理器DMA NIC，并且用NdisMAllocateSharedMemoryAsync在升值的IRQL下分配内存，则需要这个函数。

·MiniportISR

如果由微端口管理的NIC不产生中断，微端口用微端口提供的时钟函数轮询NIC状态，或在其他条件下应用时，则不需要这个函数。更多信息参见3.1.2.3节。

·MiniportHandleInterrupt

如果由微端口管理的一个NIC产生中断，则需要这个函数。

·MiniportEnableInterrupt和MiniportDisableInterrupt

如果由微端口管理的NIC不产生中断，则不需要这个函数。

·MiniportCheckForHang

如果没有提供，微端口依靠NDIS来判断NIC是否被挂起，依据发送和请求的超时。

一个无连接微端口不能提供以下MiniportXxx函数：

·MiniportCoActivateVc

·MiniportCoCreateVc

·MiniportCoDeactivateVc

·MiniportCoDeleteVc

·MiniportCoRequest

·MiniportCoSendPackets

注册面向连接微端口的MiniportXxx函数

由一个面向连接微端口提供的MiniportXxx函数已在第二章中讲述了。面向连接的微端口必须注册以下MiniportXxx函数：

·MiniportCoActivateVc

·MiniportCoCreateVc

·MiniportCoDeactivateVc

·MiniportCoDeleteVc

·MiniportCoRequest

·MiniportCoSendPackets

·MiniportHalt

·MiniportInitialize

·MiniportReset

·MiniportReturnPacket

一个面向连接微端口可以，并且在某些情况下必须提供以下MiniportXxx函数：

·MiniportAllocateComplete

如果微端口控制一个总线管理器DMA NIC，并且用NdisMAllocateSharedMemoryAsync在升值的IRQL下分配内存，则需要这个函数。

·MiniportISR

如果不由微端口管理的NIC产生中断，微端口用其提供的时钟函数轮询NCI状态，或其他条件下应用时，则不需要这个函数。更多信息参见3.1.2.3节。

·MiniportHandleInterrupt

如果由微端口管理的一个NIC产生中断，则需要这个函数。

·MiniportEnableInterrupt和MiniportDisableInterrupt

如果由微端口管理的NIC不产生中断，则不需要这个函数。

·MiniportCheckForHang

如果没有提供这个函数，微端口依据发送和请求的超时依靠NDIS来判断NIC是否被挂起。

一个面向连接的微端口不提供以下MiniportXxx函数：

·MiniportQueryInformation

·MiniportSetInformation

·MiniportSend

·MiniportSendPackers

·MiniportTransferData

下节讲述了注册可选MiniportXxx函数时的选择标准。

3.1.2.3 为中断支持的注册处理程序

这一节讲述了如何为微端口管理的一个NIC产生的中断，选择中断处理程序。有关中断处理的细节见4.1节。

当一个NIC中断时，NDIS产生中断；即：当NIC中断时，NDIS总是第一个被调用，NDIS判断中断属于哪一个微端口，以及是应当调用微端口的MiniportISR函数，还是应当自己为微端口处理中断（如果中断是非共享的，且微端口在调用NdisMRegisterInterrupt时指示NDIS不能调用它的MiniportISR函数）。如果NDIS没有调用微端口ISR函数，NDIS不能通过调用微端口的MiniportDisableInterrupt函数禁止进一步产生中断，然后对微端口的MiniportHandleInterrupt DPC函数进行排队。

如果微端口要处理它的设备中断，它必须在一个NDIS_MINIPORT_CHARACTERISTICS结构中注册它的ISR处理程序，并且通过将NdisMRegisterInterrupe的参数RequestISR设置为TRUE表明可以调用它的ISR处理程序。

注册一个ISR句柄

如果以下情况为真时，产生中断的NIC的微端口必须注册一个ISR处理程序。

·微端口管理了一个在微端口初始化或微端口停止操作时能产生中断的NIC

·微端口的NIC与其他设备共享一个IRQ

·微端口必须在DIRQL下从NIC注册表获取中断相关数据

当调用NdisMRegisterInterrupt时，这种微端口必须也将RequestIsr设置为TRUE。如果中断是共享的，当调用NdisMRegisterInterrupt时，必须设置SharedInterrupt为FALSE。

如果上述情况都不成立，产生中断的NIC的微端口通过将RequestISR和ShareInterrupt置为FALSE，调用NdisMRegistInterrupt指定由NDIS处理中断。

如果一个微端口管理的NIC不产生中断，微端口通常会用一个时钟来轮询NIC，因此就不需要注册ISR处理程序。这种类型的驱动程序必须在MiniportInitialize函数中初始化必须的钟。参见3.2.5节。

注册中断处理程序

如果微端口管理的NIC产生中断，它必须注册一个HandleInterrupt句柄。既使NDIS完全处理中断，它也需要对微端口的DPC进行排队，引起微端口的MiniportHandleInterrupt句柄运行。

允许和不允许中断

如果NIC不产生中断，微端口不需要提供一个MiniportEnableInterrupt或MiniportDisbleInterrupt函数。如果微端口总是在MiniportISR函数中处理它的中断，并且调度MiniportHandleInterrupt函数，那么它不需要提供特定的MiniportEnableInterrupt或MiniportDisableInterrupt函数。这种微端口可以在它的ISR中选择中断，在它的DPC中选择允许中断，此时不需要提供特定的驱动程序函数完成这个任务。如果一个微端口指明NDIS处理它的中断，它必须提供一个MiniportDisableInterrupt函数，如果微端口不在它的MiniportHandleInterrupt函数中重新允许中断，它必须有MiniportEnableInterrupt函数。

3.1.2.4 为无连接微端口选择一个发送函数

无连接介质微端口NIC驱动程序可用以下两种方式处理发送：

·提供一个MiniportSendPackets函数

·提供一个MiniportSend函数

MiniportSendPackets在一个呼叫中可以输入多个数据包。NDIS以一组指向包描述符的指针的方式将包传递给MiniportSendPackets。然而MiniportSend在一次呼叫中仅输入一个包。

基本上，如果微端口能同时向它的NIC进行多包发送，那么它提供了一个MiniportSendPackets函数。例如，一个管理着内置环的总线管理器的DMA NIC的微端口通常提供了一个MiniportSendPackets函数，对于这种NIC微端口可以用许多包建立一个环，从而在一次操作中发送它们。

即使NIC一次发送仅接收一个包，也可以通过在由MiniportSendPackets接收到的包中进行排队来提高性能。这种策略避免了早期重复调用MiniportSend。

如果微端口NIC驱动程序在NDIS_MINIPORT_CHARACTERISTICS中为MiniportSend和MiniportSendPackets都提供了入口指针，那么NDIS总是调用MiniportSendPackets向微端口传递发送请求。

3.1.2.5 为无连接微端口选择接收函数

无连接微端口向上层传递接收包的方式决定了它是否必须注册一个MiniportReturnPackets函数或MiniportTransferData函数：

·如果微端口通过调用NdisMIdicateReceivePacket来向上层指示包，那么它必须注册一个MiniportReturnPacket函数。

·如果微端口通过调用NdisMXxxIndicateReceive函数来向上层指示包，那么，它必须注册一个MiniportTransportData函数，有关微端口接收操作参见4.6节。

3.1.2.6 注册一个分配完成处理程序

如果总线管理器DMA NIC微端口调用NdisMAllocateShardMemoryAsync来分配缓存空间，那么总线管理器DMA NIC微端口必须提供一个MiniportAllocatelCOmplete函数，。可以在升高IRQL的情况下调用NdisMAllocateShardMemoryAsync—例如，从一个驱动程序的MiniportHandleInterrupt函数中。当一个驱动程序缺乏缓存时，可以调用NdisMAllocateSharedMemoryAsync，而不是强制驱动程序分配在微端口初始化时分配的缓存空间。通常只有调用NdisMIndicateReceivePacket的微端口有这种需求。

由于向上指示完整包的微端口(典型的带ring buffer 的DMA设备的微端口)放弃了向上层指示包的所有权，因此，它依靠上层迅速返回包。如果因为包不能迅速返回，微端口为接收数据导致缺乏缓存，那么它可以在升值的IRQL下调用NdisMAllocateSharedMemoryAsync，在它的MiniportHandleInterrapt函数中分配一个缓存。NDIS当异步分配完成时，它调用微端口的MiniportAllocateComplete函数。

3.1.2.7 注册一个挂起检测(CheckForHang) 处理程序

当微端口调用NdisMRegisterMiniport时，它可以有选择地提供MiniportCheckHang函数。如果一个微端口没有注册这个处理程序，它将依赖NDIS来检测NIC的挂起。

如果微端口提供了MiniportCheckForHang函数，NDIS每两秒或以驱动程序要求的超时间隔调用这个函数。MiniportCheckForHang函数判断NIC是否被挂起。如果它检测NIC挂起，那么MiniportCheckForHang返回TRUE;否则，它返回FALSE。

如果微端口没有注册MiniportCheckForHang函数，那么当下列情况出现时，NDIS判定微端口被挂起：

·一个发送包延缓了两倍超时周期。例如，向微端口传递一个包用来发送，它返回NDIS_STATUS_PENDING，且在两倍超时时间内没有调用NdisMSendComplete函数。这仅对串行微端口适用。

·一个向MiniportQueryInformation或MiniportSetInformation的发送请求在两倍超时时间内还没有完成。

一个微端口可以通过调用NdisMSetAttributesEx来改变超时参数，而不是用MiniportInitialize函数(见3.2.1.3节)中的NdisMSetAtrributes。通过NisMSetAttributeEx，微端口可以：

·将缺省的2秒超时间隔改变为一个驱动程序指定的间隔。NDIS将以这个新的间隔调用微端口的MiniportCheckForHang函数。慢速NIC的微端口可以指定适合NIC的时间超时间隔。

·指定NDIS忽略包超时。

·指定NDIS忽略请求超时。

如果微端口指定NDIS忽略包时间结束和请求时间结束，当NIC被挂起时，微端口负责检测NIC是否被挂起。

如果微端口CheckForHang返回TRUE，或一个发送或请求已经超过了2倍超时间隔，NDIS调用MiniportReset。

3.2 NDIS微端口初始化

当一个微端口DriverEntry函数返回时，NDIS立即为每个微端口管理的NIC，调用微端口的MiniportInitialize函数。

如果一个新的NIC插入到系统，将调用微端口的MiniportInitialize函数来初始化新安装的设备。因此， MiniportInitialize函数和其他调用函数，以及所有运行在IRQL PASSIVE_LEVEL的函数都可被指定为可分页的。通过使用NDIS_PAGABLE_FUNCTTON宏来指定代码为可分页的。初始化代码不能用NDIS_INIT_FUNCTION宏来指定，这是由于标记这种方式的代码在初始化系统的启动结束之际，已不再映射了。仅有DriverEntry函数和从DrireEntry调用的函数可以传递到NDIS_INTI_FUNCTLON宏，有关指定代码为可分页的或仅用来初始化的代码的更多信息，参见《Driver Write's Guide》。

初始化函数的声明如下所示：

NDIS_STATUS

MiniportInitialize {

OUT PNDIS_STATUS OpenErrorStatus，

OUT PUINT SelectMediumIndex，

IN PNDIS_MEDIUM MediumArry，

IN UINT MediumArraySize，

IN NDIS_HANDLE MiniportAdapterHandle，

IN NDIS_HANDLE ConfigurationHandle

};

以下是传递给MiniportInitialize的参数。

·一组在MediumArray中的NdisMediumXxx。微端口必须选择它所支持或兼容的介质，必须返回在SelectedMediumIndex中介质的索引。如果微端口没有在MediumArray中发现它所支持的介质，将返回一个失败状态NDIS_STATUS_UNSUPPORTED_MEDIA。

·MiniportAdapterHandle，一个可被NDIS引用的参照微端口句柄。微端口必须保留这个句柄，以使它在以后的调用中可传递给NDIS。——例如，在NdisMRegisterAdapterShutdownHandler和NdisMInitializeTimer调用中。

·一个配置句柄，它确定与这个微端口相连的指定NIC信息的注册表键。微端口必须保留和传递这个句柄给NdisOpenConfiguration，来打开描述微端口NIC的注册表的Parameters子键。有关网络INF文件注册表中参数值的信息，参见第一部分。

实验介质类型

NDIS介质类型和相关标识符包含在注册表的HKEY_LOCAL_MACHINE＼SOFTWARE＼Microsoft＼NDIS＼MediaTypes键中。这个键包含一个当前系统定义介质类型的列表。只有在此键中存在的介质，它才可在MediumArray中传递给MiniportInitialize。为了注明一个不支持的实验介质类型，驱动程序编写者可以添加实验介质类型到第一部分第三章所讲述的注册表中。

3.2.1 注册一个NIC

在微端口的MiniportInitialize函数中，微端口必须注册它的NIC。此过程包括有以下主要的三步：

1.为适配器指定的环境区域分配内存，该区域典型地保存着NIC状态。

2.微端口在适配器指定的环境区域读取配置信息和存储这个信息。

3.微端口通过NDIS注册NIC。

（图3.2 注册一个网络接口卡）

3.2.1.1 分配一个适配器指定的环境区域

在微端口的MiniportInitialize函数中，微端口通常为驱动程序内部结构分配内存，在这个结构中存储有适配器指定的如下的环境：

·向MiniportInitialize提供NDIS提供的的微端口适配器句柄

·从调用NdisReadConfiguration得到的的配置信息

·微端口所需要的其他状态信息

例如，一个在MiniportInitialize函数中为接收数据分配阶段缓存的驱动程序，可以将缓存与这个结构相连接。如果驱动程序对发送排队，那么这个队列的表头可以是这个结构的一部分。

一个微端口调用NdisAllocateMemory或NdisAllocateMemoryWithTag，来为这个结构分配存储区。微端口在初始化它之前调用NdisZeroMemory，清空这个存储区。然后，微端口传递一个指向这个结构的指针到NdisMSetAttributes或NdisMsetAttributesEx，NDIS保留指向这个与适配器有关的环境的指针，并且将它传递给微端口的MiniportXxx函数——例如， MiniportSend，MiniportISR和MiniportHandleInterrupt。微端口使用与适配器有关的环境，来保存所有与适配器有关的运行时状态，以处理发送，接收和请求。

3.2.1.2 读取配置信息

在分配和-初始化一个用来保存与适配器有关信息的结构后，微端口必须为它的NIC获得配置信息。在微端口MiniportInitialize环境函数中，微端口以以下方式为NIC获得配置信息：

1.微端口调用NdisOpenConfigration来打开和获得一个Parameters注册表键的句柄，这个注册表键包含有由安装脚本所提供的配置信息。

2.微端口调用NdisReadConfiguration来读取在Parameters键中与设备有关值的项目。微端口调用NdisReadConfiguration为每一个关键字检索相关的值，NdisReadConfiguration发现与关键字匹配的值的项目名并且向微端口返回NDIS_STATUS_SUCCESS。

3.微端口可以调用NdisOpenConfigurationKeyByIndex或NdisOpenConfigurationKeyByName来打开和获得一个存储在子键中的信息的句柄。这个子键是在由NdisOpenConfigurtion打开的Parameters键的下部，然后，微端口调用NdisReadConfiguration来读取存储在已打开键的值。

4如果与适配器相连的I/O总线类型中存在总线类型相关函数，那么微端口用它来获得总线类型指定信息。总线类型指定函数在2.3.2节列出。例如，如果总线类型是EISA，微端口调用NdisReadEisaSlotIntormation来获得信息，例如适配器主要和次要的修正版本方式有关IRQ的信息。

5.如果NIC是可编程的，微端口调用NdisReadNetWorkAddress从注册表中读取NIC的网络地址随后将地址写入到适配器的EPROM中。

6.在微端口获得它向注册表所请求的值后，它调用NdislloseConfiguration，当NdisCloseConfiguration返回时，注册表链句柄不再有效。

3.2.1.3 注册NIC

在微端口读取它所需要的描述有关NIC特征的信息，并且按要求方式将此信息存储在它的微端口适配器环境中之后，微端口调用NdisMSetAttributes或NdisMSetAttributeEx。在这个调用中，微端口作如下工作：

·传递由NDIS提供的微端口适配器句柄到微端口的MiniportInitialize函数

·传递微端口适配器环境句柄，当NDIS调用任何MiniportXxx函数时，NDIS将这个适配器指定环境区域的句柄传递给微端口

·指定它的NIC是否是一个总线管理器DMA设备

·指定调用者的NIC的I/O总线接口类型

通过NdisMSetAttributesEx，微端口可以指定有关超时和Token Ring错误的NDIS缺省行为的变化。

中间层驱动程序必须设置NDIS_ATTRIBUTE_INTERMEDLATE_DRIVER标志来调用NdisMSetAttributerEx。中间层驱动程序也必须设置NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志，来阻止微端口在系统转向低耗能(睡眠)状态时，停止驱动程序。

早期的管理一个PM不知（non-PM-aware）的NIC的微端口可以设置NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志，来阻止NDIS在系统转向睡眠(D3)状态之前停止微端口。早期微端口应设置这个标志，仅在以下情况：

·它保存它所需要的所有硬件环境

·它在睡眠状态(D3)之前将NIC置为合适的状态

·它可以唤起NIC到工作状态(D0)

有关更多信息，见6.6节

一个非串行微端口必须设置NDIS_ATTRIBUTE_DESERIALIIE标志来调用NdisMSetAttrributerEx。一个面向连接的微端口，总是非串行的，所以并不需要这么做。

3.2.2 声明资源

在微端口决定它的硬件资源并调用NdisMSetAttribute(Ex)之后，注册它将要使用的系统资源。微端口必须在声明资源，包括初始时钟之前，调用NdisMSetAttribates(Ex)。无论它所管理的NIC类型是什么，微端口必须为NIC管理设备分配内存。这个内存是附加在微端口为其适配器环境所分配的内存上的。例如，微端口应为接收到来的帧或为发送数据的阶段缓存分配内存。

下节将讲述微端口如何分配内存和注册I/O端口。这些操作以一般措辞讲述，然后分别针对总线管理器DMA NICS、编程I/O NICS、内存映射NICS和从属DMA NICS讲述。

3.2.2.1 分配内存

一般在初始化中，微端口应分配缓存、包描述符和用来请求发送和接收数据的缓存描述符。所分配内存的类型和数量依赖于NIC的类型，如同微端口所支持的接收样式类型。

内存可以共享或不共享。仅当内存由一个总线管理器DMA NIC访问时，驱动程序分配共享内存。总线管理器DMA NIC微端口必须为微端口和NIC都可访问的缓存分配共享内存。

内存可被cache或不被cache。不可被cache的内存是一种稀有的系统资源，并且它很难分配。

管理总线管理器DMA NIC的微端口必须确保它所使用的任何可被cache的内存，在从缓存读取接收到的数据或从缓存发送数据之前是一致的。这种微端口必须更新它的缓存确保发送和接收中cache一致性。

管理从属DMA NIC或非DMA NIC的微端口不需要关心与cache一致性。由微端口为从属DMA NIC调用的NdisXxx函数为微端口更新了内存，非DMA NIC的微端口并不分配cache。

以下函数是用来分配内存的。

·NdisAllocateMemory或NdisAllocateMemoryWithTag分配一个长度由调用者指定的缓存。此内存不共享但是它可被cache或不被cache，这依赖于调用者的请求。内存可以是连续的并且是在一指定物理地址界限内。

·NdisMAllocateSharedMemory分配共享内存，可被cache或不被cache，这个调用既返回一个NIC使用的物理地址指针，又返回一个微端口使用的虚地址指针。NdisMAllocateSharedMemory必须在NdisMSetAttributes(Ex)和NdisMAllocateMapRegisters或NdisMInitializeScatterGatherDma之后调用。

·NdisAllocatePacketPool或NdisAllocatePacketPoolEx从已分配包描述符中分配和初始化不分页的系统内存快

·NdisAllocatePacket从调用NdisAllocatePacketPool(Ex)所分配的包池中分配一个包描述符

·NdisAllocateBufferPool返回一个调用者可以用NdisAllocateBuffer分配的缓存描述符的句柄

·NdisAllocateBuffer从由NdisAllocateBufferPool分配的缓存池中分配一个缓存描述符

3.2.2.2 注册端口

微端口调用NdisMRegisterIoPortRange声明I/O端口地址，微端口需要这个地址使用NdisRawWrite(Read)PortXxx向端口写入或读取数据。调用NdisMRegisterIoPortRange作如下两件事：

1.确定NIC试图注册的I/O端口是否已被分配给设备了。

2.映射I/O端口以使微端口可以向使用NdisRawWrite(Read)PortXxx注册的设备写或读。NdisRawWrite(Read)Xxx函数直到NdisRegisterIoPortRange成功完成以后才不可以使用。

3.2.2.3 总线管理器DMA设备初始化

DMA初始化的可选项依赖于微端口驱动程序的类型：

· 管理总线管理器DMA NIC的串行微端口必须在MiniportInitialize函数中调用NdisMAllocateMapRegisters为DMA操作分配映射注册表。

·管理总线管理器DMA NIC的非串行或面向连接微端口应在它的MiniportInitialize函数中调用NdisMInitializeScaterGatherDma来初始化DMA资源。使用NdisMInitializeScatterGatherDma的优点在于，可以动态地为每个DMA传递分配或取消分配系统资源，例如映射注册表，并且优点还在于，通过获得映射实际范围的物理地址过程来提高DMA的性能。除了调用NdisMInitializeScatterGatherDam，管理总线管理器DMA NIC的非串行或面向连接微端口，也可调用NdisMAllocateMapRegisters来静态分配映射注册表；但是，这导致DMA性能的降低和系统资源利用率的降低，

用NdisMAllocateMapRegisters分配映射注册表

在DMA初始化时，微端口调用的NdisMAllocattMapRegisters应当首先调用NdisQueryMapRegisterrCout来确定整个系统中有多少映射注册表可用。然后微端口调用NdisMAllocateMapRegisters来声明这些可用的系统映射注册表。微端口应仅分配一个单一DMA传递所需要的映射注册表。这是因为注册表是系统共享资源，声明过多的的注册表不利于系统整体性能，并且分配比一个单一传递所需注册表多的注册表仅仅使用了本来可被其他驱动程序使用的资源，并不会提高性能。

当微端口调用NdisMAllocateMapRegisters时，它必须指定映射的最大缓存(MaximumPhysicaMapping)，以适应NIC的最大DMA能力。给这个参数指定大于0XF000的值可能会引起NdisMAllocateMapRegisters失败。如果它运行在物理地址扩展(PAE)的Microsoft Windows 2000 DataCenter Server操作系统下，指定大于0XF000将无疑地导致NdisMAllocateMapRegisters失败。

在微端口初始化时，它只一次分配映射注册表。在注册表分配成功后，它属于微端口，直到当驱动程序卸载时，它们被收回为止。

用NdisMInitializeScatterGatherDma节省系统资源

管理总线管理器DMA NIC的非串行化或面向连接的微端口可以调用NdisMInitializeScatterGatherDma，来为DMA操作初始化系统资源，这种驱动程序不需要为DMA操作分配映射注册表。

调用NdisMInitializeScatterGatherDma的驱动程序在DMA初始化时，使用合理的方法为DMA发送操作获取物理地址(见4.4.5.1节)。

NdisMInitializeScatterGatherDma仅支持32位和64位DMA地址。NdisMinitializeScatterGatherDma不支持24位DMA地址。

为缓冲区分配包池，缓存池和共享内存

为包池、缓存描述符和为缓存分配的共享内存的分配是由映射注册表（也许由微端口）是否运行在单处理器或多处理器上决定。微端口可以用NdisSystemProcessorCount来决定机器的类型。通常，多处理器的机器是一个服务器，由于服务器通常比单处理机(通常是客户端)处理更多的网络流量，所以基本上为服务器分配更多的资源。

除了为它的微端口适配器环境分配内存，一个总线管理器DMA微端口应当建立它用来指示包的包池，在初始化当中，微端口必须：

·调用NdisAllocatePacketPool为包描述符分配包池。它必须调用NdisAllocatePacket从包池中分配一个包描述符。

·调用NdisAllocateBufferPool为映射到组成包的缓存的缓存描述符分配缓存池。

·调用NdisMAllocateSharedMemory为用来从网络接收内入数据的一定量缓存分配共享内存，因为它可以被NIC和微端口共同访问，所以内存必须是共享内存。当内存用来发送和接收时，NIC使用主机内存，NIC使用内存的物理地址；微端口使用虚地址，如果调用没有先用NdisMAllocateMapRegisters分配映射注册表或用NdisMInitializeScatterGatherDma初始化系统资源，那么，调用NdisMAllocateSharedMemory会失败。

通常，DMA NIC微端口应分配可被cache的内存。这也有一些例外，在4.6.4节注明。这种NIC微端口必须更新它的缓存，以确保在发送和接收中cache与内存的一致性。

如果微端口支持向NDIS指示多包接收，它必须为使用数组传递指向包的指针分配内存，这可以是各种类型。

3.2.2.4可编程I/O设备初始化

管理可编程I/O(PIO)设备的微端口通过向端口区域写或读，来进行发送和接收操作。微端口调用NdisRawReadPortXxx从接收缓存端口将数据读入临时缓存。如果缓存属于协议，那么在它的MiniportTransferData函数中调用此函数，否则在它的MiniportHandleInterrupt函数中调用。对于发送，微端口使用NdisRaoWritePortXxx，来将数据写到MiniportSend或MiniportSendPackets函数的包中。

如果使用阶段缓存，微端口必须用NdisAllocateMemory或NdisAllocateMemoryWithTag分配缓存。除非驱动程序支持多包接收指示，任何它所需要管理的设备的附加内存都应是不可共享的，由NdisAllocateMemory分配的。如果微端口用NdisMIndicateReceivePacket指示，它必须至少分配一个如同3.2.2.1节所讲述的包。

为了建立端口范围，微端口必须以以下方式读取注册表：

1.用NdisXxxConfiguration调用或一个总线类型指定的NdisXxx调用。

2.通过调用NdisMRegisterIoPortRange来表明它所需求的I/O端口。

3.调用NdisRead(Write)PortXxx移动数据。

3.2.2.5 内存映射设备初始化

内存映射设备的微端口必须将NIC内置的内存映射到主机系统空间内存，以使它可以从卡上传递接收数据和向卡上传递发送的数据。微端口传递数据的内存是NIC的一部分；但是，驱动程序使用的是映射了的虚系统空间地址。

当接收数据时，微端口用缓存描述符所映射的虚范围指示接收数据，并且当/如果调用了MiniportTransferDate函数，微端口将数据从内置内存拷贝到协议驱动程序所提供的缓存中。如果没有协议驱动程序传递数据，当微端口从它的MiniportHandleInterrapt函数或它的轮询MiniportTimerXxx函数返回时，丢弃数据。如果微端口使用临时缓存，它用NdisAllocateMemory或NdisAllocateMemoryWithTag分配临时缓存。

如同一个PIO设备，拥有内置RAM的NIC的微端口调用NdisMRegisterIoPortRange注册它所声明的端口地址。这个调用返回一个映射了的虚端口地址，通过这个地址微端口可以访问注册了的适配器。这个函数映射的端口地址仅供NdisRawWtite(Read)PortXxx(清除中断;设置和清除请求函数)来管理设备，但是它不能象PIO设备所做的那样传递数据。

将数据移入或移出board，这种类型NIC的微端口必须映射NIC设备的内存到主机地址空间。在初始化当中，微端口通过调用NdisReadConfiguration或调用总线类型指定的NdisXxx，来从注册表中读取物理地址和内置内存大小。一旦确定了配置信息，微端口必须调用：

·NdisSetPhysicalAddressHigh和NdisSetPhysicalAddressLow设置32位物理地址，然后调用

·NdisMMapIoSpace 用这个物理地址把设备内存映射到微端口可访问的主机地址空间，NdisMMapIoSpace返回一个微端口可用的虚地址指针来参照这个设备内存。

这个驱动程序使用NdisRead(Write)RegisterXxx对NIC的注册表进行读或写。

如果驱动程序用NdisMIndicateReceivePacket或NdisMCoIndicateReceivePacket指示请求的数据，它必须分配一个如3.2.2.1节所讲述的链状缓存包。

3.2.2.6 从属DMA设备初始化

管理DMA NIC的微端口调用NdisAllocateMemory来分配内存，在发送时，微端口将数据从它发送到系统DMA控制器，在接收时，将数据传给它。

管理从属DMA设备的微端口调用以下函数：

·NdisMRegisterDmaChannel来声明一个通道用于发送和接收

·NdisMAllocateSharedMemory分配内存，通过它，微端口可以向DMA控制器写出或读入数据

当发送和接收发生时，为DMA映射内存。如果驱动程序用NdisMIndicateReceivePacket或NdisMCoIndicateReceivePacket指示接收数据时，它必须分配至少一个如3.2.2.1所讲述的包。

3.2.3 注册一个中断

在调用NdisMSetAttributes(Ex)、分配内存、声明资源之后，管理中断的NIC微端口必须调用NdisMRegisterInterrupt，来确认中断并且声明中断的向量/等级。微端口为一个透明的NDIS_MINIPORT_INIERRUPT类型变量分配永久存储区，并且传递指向这个向量的指针给NdisMRegiterInterrupt。微端口传递有关中断的信息，例如如下：

·总线相关的中断向量和中断等级

·是否在每个中断调用MiniportISR

·是否共享中断

·它的中断模式

由微端口指定的中断类型或是NdisInterruptLevelSentsitive或是NdisInterruptLatched。NdisMRegisterInterrupt代表微端口，以声明一个中断响应产生HAL调用，然后调用I/O系统与中断相连。当NDIS连接中断时，它首先向所有中断提供它自己的ISR函数。在NDIS ISR内部，做出是把中断传递给微端口的MiniportISR或MiniportDisableInterrupt函数还是在NDIS内部处理中断的决定(依据NdisMRegisterInterrupt提供的RequestIsr值)，然后对MiniportHandleInterrupt排队。

NdisMRegisterInterrupt对NIC状态没有影响。但是，微端口在调用NdisMRegisterInterrupt时就可以接收中断了，即在NdisMRegisterInterrupt返回之前。即使NIC的中断是不可用的，微端口可以一直等待中断，直到它调用NdisMDeregisterInterrupt返回为止。在调用NdisMRegisterInterrupt之前，微端口应当进行任何适配器所指定的，用来处理中断时所要进行的准备操作，即，微端口必须产生和初始化任何它需要的结构，也许初始化它所需要用来进同步访问共享数据所需的自旋锁。当微端口注册它的中断和准备处理中断时，微端口可以允许设备指定的方式中断——例如，重置NIC。

微端口在初始化当中，测试它的设备以确保设备有能力产生中断，是一个很好的习惯。当NdisMRegisterInterrupt返回时。微端口可以进行这个测试，如果设备运行不正常，微端口应当释放所有声明的资源并且从MiniportInitialize返回NDIS_STATUS_FALLURE，以使微端口可以从系统中删除。

微端口可以通过调用NdisMSetAttributesEx改变缺省的超时设置。这个函数必须在MiniportInitialize中调用，如3.1.2.3节所讲，在MiniportInitialize返回之后，驱动程序不能改变缺省的超时特征。有关微端口的CheckForHang函数的讨论见3.1.2.7节。

3.2.4 注册一个关闭函数

微端口应当调用NdisMRegisterAdapterShutdownHandler用NDIS注册一个关闭函数。

微端口向NdisMRegisterAdapterShutdownHandler传递它的MiniportShutdown函数地址、由NDIS提供的微端口适配器的MiniportInitialize的句柄和当它被调用时，传递给MiniportShutdown的环境。NdisMRegisterAdapterShutdownHandler通常在MiniportInitialize的最后，在微端口从MiniportInitialize返回之前被调用。有关关闭句柄的更多信息参见7.3节。

3.2.5 初始化轮询时钟

一些微端口控制的设备不产生中断，因此它必须被轮询。这种微端口不调用NdisRegisterInterrupt，而是由小型端口分配一个或多个用于轮询设备的时钟对象。

MiniportInitialize调用NdisMIntializeTimer来为每个微端口所需的时钟初始化一个时钟对象。NdisMinitializeTimer将初始化的时钟与微端口提供的时钟函数相连，当时钟失效时，调用MiniportTimer函数。NDIS提供了两种类型的时钟：一次性时钟和周期性时钟，当一次性时钟由NdisMSetTimer设置并且时钟终止时，它必须调用NdisMSetTimer重新设置。由于在每个终止时重新设置时钟上花费时间，所以这种请求会引起等待时间堆积。周期性时钟，设置一次后它在每个要求的时间间隔内不停止终止。这种周期性时钟很适合在一个微端口进行轮询，一旦调用NdisMSetPeriodicTimer设置了时钟，它将不停地终止，引起相关的时钟函数的调用，直到时钟在以后的调用NdisMsSetPeriodicTimer被重新设置或调用NdisMCancelTimer清除时为止。

如果时钟函数与其他微端口函数共享资源，那么必须对这些资源进行保护。如果时钟函数与运行在同一IRQL(DISPATCH_LEVEL)上的函数共享资源，那么用一个自旋锁来同步访问这些资源。为了获得早期已初始化的自旋锁，函数调用NdisDprAcquireSpinLock和NdisDprReleaseSpinLock。这些函数要比NdisAcquireSpinLock和NdisReleaseSpinLock快。

如果时钟函数与MiniportISR或MiniportDisableInterrupt共享资源，那么必须调用NdisMSynchronizeWithInterrupt同步访问。微端口在它的MiniportSynchroniczeISR函数中执行任何与时钟相关的对共享资源的操作，通常是设备注册表，MiniportSychronicISR函数的地址传递给了NdisMSynchronizeWithInterrupt。

3.2.6 在初始化当中的同步

当微端口初始化它的NIC时，它在继续执行之前需要等待状态的改变：

·MiniportInitialize调用NdisMSleep来延缓执行一个以微秒为单位的时间间隔，当MiniportInitialize调用NdisMSleep时，它将进入挂起状态。当睡眠间隔结束时，MiniportInitialize恢复执行，微端口不需要调用NdisStallExecution，函数所调用的时间间隔应小于50微秒。

·MiniportInitialize可与事件同步。MiniportInitialize可以通过调用NdisInitializeEvent产生一个事件，然后，在MiniportInitialize向它的NIC写入之后，它调用NdisWaitEvent，当一个中断发生和MiniportHandleInterrapt运行时，它清除NIC上的中断并且通过调用NdisSetEvent设置事件，等待事件的MiniportInitialize。等到事件后，调用NdisResetEvert清除事件继续它的执行。

3.2.7 在初始化时处理错误

如果在初始化中的任何时候，微端口返回一个出错状态——例如，如果它不能获得它所需要的资源或如果它不能发现它所支持的介质——它必须取消它对整个系统所进行的操作，尤其：

·如果它已分配了内存，微端口必须调用NdisFreeMemory来收回分配内存。

·如果它声明了I/O资源，微端口必须调用NdisMDeregisterToPortRange来释放这些资源。

·如果它已调用了NdisOpenConfiguration，微端口必须调用NdisCloseConfiguration。

·如果它已分配了映射注册表，微端口必须调用NdisMFreeMapRegisters。

·如果它已分配了一个DMA通道，微端口必须调用NdisMDeregisterDmaChannel。

·如果它已用NdisMAllocateSharedMemory分配了共享内存，它必须调用NdisMFreeSharedMerory。

在初始化中失败的微端口可以调用NdisWriteErrorLogEnery在出错日志中用一个出错代码和出错类型指定的值记录问题。

3.3 查询微端口信息

如果MiniportIntialize返回NDIS_STATUS_SUCCESS，NDIS查询和保存驱动程序的操作特征。这些特征由微端口管理的对象标识符(OID)来解释。微端口必须支持所有基本命令介质指定的OID，有关获取和设置微端口信息的更多信息见第五章。

3.4 减少微端口初始化时间

驱动程序初始化所花费的时间显然会影响Windows 2000启动所需的整个时间，因此，减小驱动程序初始化时间是很重要的，驱动程序初始化的时间应在10个毫秒内或更少。

为了减少初始化时间，NDIS微端口应做如下操作：

·在检测链接速度之前，进行介质检测，如果介质检测失败(因为网线没有连接到NIC)，那么微端口不进行链接速度率的检测

·如果介质检测成功，那么微端口应当使链接速率的检测服从于一个时钟函数，由于网络协议直到微端口被装载之后才被装载，微端口应当有充足的时间在一个协议发出一个OID_GEN_LINK_SPEED请求来请求链接速度之前获得链接速率

如果微端口在获得链接速率之前接收到OID_GEN_LINK_SPEED请求，OID_GEN_CURRENT_SPEED请求或发送请求时，它将挂起请求或发送操作，在它获得链接速率之后完成这示或操作。微端口对这种请求或发送操作不应让其失败。为了防止一个挂起的请求或发送操作时间结束，微端口在它的MiniportInitialize的环境函数内，使用NdisMSetAttributesEx来设置恰当的超时间隔。

下列伪代码例子说明如何：

·在初始化当中处理介质检测和链接速率检测。

·使用一个时钟函数获得链接速率以及完成任何挂起的OID请求。

·如果链接速率检测没有完成时，如何挂起OID_GEN_LINK_SPEED和OID_GEN_CURRENT_FILTER请求。

·如果链接链速率检测没有完成时，如何挂已发送操作。

在初始化当中处理介质检测和链接速率检测

下列伪代码说明如何在MinniportInitialize的环境中处理介质检测和链接速率检测：

IF (there is no link pulse on the physical layer interface)

// The cable is not connected to NIC

{

// Do not link-speed detection

}

else

{

Adapter→NegotiotionDone = FALSE;

Command NIC to start the link-speed negotiation

Start a timer to check for 完成 of link-speed dection

}

使用一个时钟函数来获得链接速率以及完成挂起OID请求和发送操作

为了减少初始化所花费的时间，微端口应当使链接速率的检测从属于一个时钟函数。下列伪代码说明如何使用一个时钟函数来获得链接速率。在时钟函数获得链接速率之后，它应当完成所有挂起的OID请求，并且重新启动所有挂起的发送操作。

IF (NIC has finished link-speed negotiation)

{

Adapter→NegotiationDone = TRUE;

if (Adapter→QueryPending)

{

// Complete the pending OID request after getting the link sped，

NdisMQueryInformationComplete ();

// if the miniport exports an NdisMCoRequestComplete

// function， call

//NdisMCoRequestComplete

//instead

}

if (Adapter->SetPending)

{

Send the set filter down to the adapter；

// The NIC generates an interrupt to indicate the 完成

// of the set-filter operation

NdisMSendResourceAvailable (); // Restars any pending send operations

// This timer dops not fire again

}

elsp

{

Restart timer;

}

挂起OID_GEN_LINK_SPEED和OID_GEN_CURRENT_PACKET_FILTER请求

如果链接速率检测没有完成，如果一个微端口没有完成链接速率检测，它应挂起它所接收到的OID_GEN_LINK_SPEEO请求或OID_GEN_CURRENT_PACKET_FILTER请求。微端口在时钟函数完成链接速率检测之后，应当完成在它时钟函数中挂起的OID的异步请求。

以下伪代码说明如果链接速率检测没有完成时如何挂起一个在MiniportQueryInformation或MiniportCoRequest中的OID_GEN_LINK_SPEED的请求：

if (!Adapter->NegotiationDone && (OID==OID_GEN_LINK_SPEED))

{

// Save the current OID request

Adapter->QueryPending = TRUE; // Timer function tests this Value

return NDIS_STATUS_PENDING;

以下伪代码说明如果链接速率检测没有完成时，如何挂起一个在MiniportQueryInformation或MiniportCoRequest中的OID_GEN_CURRENT_PACKET_FILTER：

if (!Adapter->NegotiationDone && (OID==OID_GEN_CURRENT_PACKET_FILTER))

{

// Save the current OID request

Adapter→Setpending = TRUE; // Timer function tests // this value

return NDIS_STATUS_PENDING;

}

如果链接速率检测失败延缓发送操作

如果一个串行化的微端口没有完成链接速率检测，它应当延缓任何它所接受的发送操作，并且在时钟函数完成链接速率检测后，重新在一个时间函数（参见上部分）内启动这个操作。

以下伪代码说明如果链接速率检测没有完成时，如何在MiniportSend中延缓发送操作。

if (!Adapter_>NegotionDone)

return NDIS_STATUS_RESOURCES; //Force NDIS to resubmit the send

//when link-speed negotiation is

// finished.

以下伪代码说明如果链接速率检测没有完成时，如何在MiniportSendPackets中延缓发送操作。

if (!Adapter_>NegotionDone)

Set the Status member of NDIS_PACKET_OOB_DATA block

associated with the packet descriptor to NDIS_STATUS_RESOURCE;

return;

一个串行化或面向连接的微端口必须对发送操作在内部进行排队，并且在链接速率检测完成以后完成发送操作。


数据传输

本章描述微端口（Miniport）如何发送和接收数据。包括设计下述信息：

n 4.1 中断处理

n 4.2 延时处理程序

n 4.3 带外（OOB）数据包

n 4.4 数据包传输

n 4.5 非串行微端口

n 4.6 接收数据

n 4.7 保存统计量

n 4.8 802.1p包优先性

4.1 中断处理

本节描述中断处理。更多的关于注册中断信息参见3.1.2.3节。

当网卡（NIC）产生一个中断时，NDIS捕获它，并调用初始化时为这一中断注册过的每个微端口的 MiniportISR 例程。

一般来说，MiniportISR：

n 确定并记录中断的原因

n 拷贝任何易变的特殊中断信息到一个永久结构中（通常在 MiniportAdapterContext 某处传到MiniportISR）

n 清除NIC上的中断

MiniportISR 的定义为：

VOID

MiniportISR(

OUT PBOOLEAN InterruptRecognized,

OUT PBOOLEAN QueueMiniportHandleInterrupt,

IN NDIS_HANDLE MiniportAdapterContext

);

在返回之前 MiniportISR 必须设置的两个传递参数：InterruptRecgnized和QueueMiniportHandleInterrupt。如果要识别出中断，MiniportISR 必须将InterruptRecognized设为TRUE，否则设为FALSE。若这一中断为共享的，并且此微端口没有识别出中断，则返回FALSE。NDIS就将中断传递给共享这一中断的下一个微端口的 MiniportISR 函数。NDIS反复进行这一过程，直到有一个 MiniportISR 函数识别出该中断，否则NDIS将所有（共享这一中断的每个微端口的）MiniportISR 函数调用过一遍。

若 MiniportISR 识别出中断并希望其MiniportHandleInterrupt函数排序，就必须设置 QueueMiniportHandle 为TRUE。当 MiniportISR 返回时，NDIS实际上对MiniportHandleInterrupt函数进行了排序。若将微端口的 InterruptRecognized 设置为FALSE，则 QueueMiniportHandleInterrupt 值假设为FALSE。

在执行ISR的同时，处理器上所有其他在同一IRQL级别或较低IRQL级别的中断都被屏蔽掉了。因此，使一个微端口在 MiniportISR 上所花费时间最少是很重要的。否则，系统的总体运行性能，以及整个端口I/O性能都会降低。

对于管理一个与同一总线上其他设备共享一个中断的NIC的微端口来说，重要的是迅速地检测出中断是否属于该微端口。若中断不属于该微端口，则微端口应将InterruptRecognized设为FALSE，并立即返回。

若中断共享且属于该微端口，或者未共享但却为该微端口识别（中断非假），微端口应在其NIC上禁止中断，然后从NIC的寄存器中得到所需要的特殊中断信息。如果需要，微端口立即从MiniportISR捕获中断特定的信息，或者更确切地说，微端口是在MiniportHandleInterrupt函数中捕获中断特定信息，这一函数运行在较低的IRQL上。

MiniportISR必须禁止中断，以便确保MiniportHandleInterrupt运行时，NIC上没有信息被随后的中断所覆盖。MiniportISR应当仅仅读取并保存那些在它返回后不能存取的信息，然后，MiniportISR返回。

如果微端口不希望其MiniportHandleInterrupt函数进行排序，而且如果它以前在MiniportISR中禁止中断，则在由MiniportISR返回之前，它必须使NIC重新启用中断。

ISR函数同步

微端口的MiniportISR函数及其MiniportDisableInterrupt函数在DIRQL下执行。其他微端口代码则在IRQL<=DISPATCH_LEVEL下执行。为了防止竞争状态，任何与MiniportISR或MiniportDisableInterrupt共享资源的微端口函数必须同步地调用共享的资源。与MiniportISR及MiniportDisableInterrupt同步的函数是通过调用NdisMSychronizeWithInterrupt来实现的，它提供一个MiniportSychronizeISR函数。MiniportSychronizeISR在DIRQL状态下执行并保证安全调用共享的资源。

例如，MiniportHandleInterrupt 在清除中断状态寄存器之前，调用NdisMSynchronizeWithInterrupt。这一NDIS调用得到一个 SynchronizeFunction 参数。在 NdisMSynchronizeWithInterrupt 内部，NDIS在DIRQL状态下调用微端口的 SynchronizeFunction，从而这一函数可以安全地清除中断状态寄存器，并确定MiniportISR或MiniportDisableInterrupt不会同时修改同一寄存器。SynchronizeFunction应尽快执行，就象MiniportISR和MiniportDisableInterrupt函数一样。

MiniportISR及MiniportDisableInterrupt都在DIRQL状态下运行，它们与中断对象的自旋锁同步。

中断与轮询的结合

某些时候，驱动程序开发人员可以通过将轮询NIC与中断的使用相结合的方法来改进驱动程序的运行。例如，一个驱动程序可以设计成对于接收指示使用中断，对于发送完整指示则禁止中断的模式。微端口可设一时钟，在时间停止时，检查发送完成状况。此外，当中断在接收时发生，微端口也要检查发送完成状态。当网络流通量很大时，这是一项合理的策略：中断处理量最小，并且没有遗漏数据包。

一个驱动程序可以抽样检测网络流通状况，以确定当前荷载下的最佳数据传输策略。若流通量很大，就应当采用刚刚描述的策略。如果网络上的流量低，驱动程序应启用“发送完成与接受”中断，因为所预期的中断数量要比高流通量环境下低，并且可能有对驱动程序发送性能不利的影响。

使用轮询和中断相结合的驱动程序，当使用轮询来管理其设备时禁止中断，而当使用中断来管理其设备时则启用中断。当NDIS被启用时，驱动程序也可用NDIS处理其设备的中断，但这要受到以上所描述内容的限制，即：

n 该中断不是共享的

n 端口所需的任何信息都可以在MiniportHandleInterrupt函数中捕获

n 在初始化期间及暂停过程中，其NIC从不产生中断

关于计时器的信息，参见3.2.5节。

4.2 DPC处理程序

每个管理网卡（NIC）的微端口，其中断必须具有MiniportHandleInterrupt函数。即便NDIS完全处理中断，它也总是对这一微端口延时程序调用（DPC）排序，导致微端口MiniportHandleInterrupt处理程序的运行。若MiniportISR处理一中断，它通过在InterruptRecognized和QueueMiniportHandleInterrupt参数中的MiniPortISR返回的数值，来控制MiniportHandleInterrupt是否排序。若MiniportISR都设为TRUE，则MiniportHandleInterrupt进行排序，以便在IRQL DISPATCH_LEVEL下执行；若将InterruptRecognized设为FALSE，则MiniportHandleInterrupt不进行排序。

当MiniportISR或MiniportDisableInterrupt函数在NIC上禁止中断后，MiniportHandleInterrupt被调用。MiniportHandleInterrupt应读取它所需要的任何数据以便完成处理中断驱动的I/O操作。然后MiniportHandleInterrupt重新使NIC启用中断，或让NDIS在MiniportHandleInterrupt返回控制状态后，调用微端口的MiniportEnableInterrupt函数；或由MiniportHandleInterrupt内部启用中断，这样会快一些。NIC上不会再发生中断，直到中断重新被启用。为了确保没有接收到的数据被遗漏，MiniportHandleInterrupt应当尽快启用中断。

处理中断的微端口，完全在其MiniportHandleInterrupt函数的环境中处理数据接收和发送。例如，若MiniportHandleInterrupt确定中断的原因是一个接收任务，它通过调用在DPC环境内的 NdisM(Co)IndicateReceive 或 NdisM Xxx IndicateReceiv e完成数据接收。4.6节描述了微端口如何处理接收。

若无连接微端口确定产生中断的原因是完成了发送任务，它就调用 NdisMSendComplete 发出信号，表明对MiniportSend或MiniportSendPacket的数据包发送已经完成。类似地，若一面向连接的微端口确定中断原因是完成了发送，它就调用 NdisMCoSendComplete 发出信号，指示以前送到NdisMCoSendCompletePackets的数据包已完成，这一调用返回NDIS_STATUS_PENDING状态。微端口发送操作将在4.4节中描述。

当一微端口处理一项接收任务时，它必须不能再次发生中断，直到下述情况之一发生：

n 微端口已经完成整个微端口所分配的、包含接收数据的数据包或包序列

n 若任意协议驱动程序对数据感兴趣，微端口的MiniportTransferData函数将内入的数据传送到协议驱动程序所提供的包中（仅无连接微端口有MiniportTransferData函数）

若微端口分配的包传给了协议驱动程序，而且协议驱动程序保存此包，则微端口必须为下一次接收提供一个新的空包。当它已经处理完接收信息，并为下一次接收作好准备后，端口可以重新启用中断。

4.3 带外（OOB）数据包

每个NDIS_PACKET类型的描述符都具有一相关的带外（Out-of-Band）数据块，它传递了介质相关的和优先权的信息。这一子结构定义为：

typedef struct _NDIS_PACKET_OOB_DATA {

Union {

ULONGLONG TimeToSend;

ULONGLONG TimeSent；

}

ULONGLONG TimeReceived;

UINT HeaderSize;

UINT SizeMediaSpecificInformation;

PVOID MediaSpecificInformation;

NDIS_STATUS Status;

}; NDIS_PACKET_OOB_DATA, *PNDIS_PACKET_OOB_DATA

支持OOB数据的微端口驱动程序应当使用NDIS宏来存取这类数据。它不应计算偏移量，或者根据已知包的OOB数据块的确切结构另行采取行动。

OOB数据的NDIS宏包括下述内容：

NDIS_OOB_FROM_PACKET(Packet)

返回NDIS_PACKET_OOB_DATA子结构的指针。这个一指针可以用来通过成员名来访问NDIS_PACKET_OOB_DATA的成员。

NDIS_SET_PACKET_STATUS (Packet, Status)

设置状态成员。

NDIS_SET_PACKET_TIME_TO_SEND (Packet, TimeSent)

设置TimeToSend成员。

NDIS_SET_PACKET_TIME_SENT (Packet, TimeSent)

设置TimeSent成员。

NDIS_SET_PACKET_TIME_RECEIVED (Packet, TimeReceived)

设置TimeReceived成员。

NDIS_SET_PACKET_HEADER_SIZE (Packet, HeaderSize)

设置HeaderSize成员。

NDIS_SET_PACKET_MEDIA_SPECIFIC_INFO (Packet, MediaSpecificInfo, Size-MediaSpecific)

设置MediaSpecificInformation及SizeMediaSpecificInformation成员。

NDIS_GET_PACKET_STATUS (Packet)

返回 Status 成员。

NDIS_GET_PACKET_TIME_TO_SEND (Packet)

返回TimeToSend成员。

NDIS_GET_PACKET_TIME_SENT (Packet)

返回TimeSent成员。

NDIS_GET_PACKET_TIME_RECEIVED (Packet)

返回TimeReceived成员。

NDIS_GET_PACKET_HEADER_SIZE (Packet)

返回HeaderSize成员。

NDIS_GET_PACKET_MEDIA_TO_SPECIFIC_INFO (Packet, pMediaSpecificInfo, pSizeMediaSpecificInfo)

返回介质相关信息到一个由调用程序提供的高速缓存中，并返回存储在调用程序变量中的信息字节大小。

4.3.1 等待发送的OOB数据

当一个支持介质特定信息以及/或者优先权的微端口接收到数据包时，它可以执行下列一个或多个步骤：

n 获取 TimeToSend

n 获取 SizeMediaSpecificInformaton 和 MediaSpecificInfprmation

在返回之前，串行微端口中的MiniportSendPackets为每个通过的包设置 Status 成员。若它完成了同步发送，它将 Status 设置为除NDIS_STATUS_PENDING之外的某一值。否则，MiniportSendPackets将 Status 成员设置为NDIS_STATUS_PENDING。此后，当发送完成时，发送状态必须在 Status 参数中返回给 NdisMSendComplete。MiniportSendPackets返回后，OOB数据的 Status 成员，以及所有其他成员，对于NIC驱动程序都是只读的。

在所有的提交给非串行驱动程序的MiniportSendPackets函数，或在面向连接驱动程序的MiniportCoSendPackets函数的包描述符中，NDIS库都忽略了OOB块。NDIS假定非串行微端口将每个包描述符同步地用 NdisMSendComplete 传输到MiniportSendPackets，面向连接微端口驱动程序将每个包描述符用 NdisCoSendComplete 传输到MiniportCoSendPackets。因此，非串行驱动程序的MiniportSendPackets函数，或面向连接驱动程序的MiniportCoSendPackets函数通常忽略了NDIS_PACKET_OOB_DATA块的 Status 成员，但它可以将这一成员设置为某个状态，这个状态与后来传给 NdisM（Co）SendComplete 的状态相同。

如果调用MiniportSend，而且是同步发送，则MiniportSend总是把发送状态作为MiniportSend状态返回。OOB数据从不用来返回MiniportSend操作的状态。

总之：

n 除OOB数据的 Status 成员以外，所有其他成员对于串行微端口的MiniportSendPackets函数都是只读的。通过写 Status 成员来为每个同步完成的包返回发送状态。

n NDIS忽略了包描述符的 Status 成员，描述符将其传到非串行驱动程序的MiniportSendPackets函数或面向连接驱动程序的MiniportCoSendPackets函数。对于输入序列中的每个包，非串行驱动程序的MiniportSendPackets函数必须与 NdisMSendComplete 同步完成对每个包的处理。类似地，对于输入序列中的每个包，面向连接驱动程序的MiniportCoSendPackets函数必须与 NdisMSendComplete 同步完成对每个包的处理。

n 当包被同步发送时，或者由MiniportSend或MiniportSendPackets执行时，发送的最终状态总是作为一个参数返回给 NdisMSendComplete。当包被MiniportCoSendPackets同步发送时，最终状态总是作为参数返回给 NdisMSendComplete。

n 所有OOB数据对于 MiniportSend 都是只读的。MiniportSend总是把发送状态作为MiniportSend状态返回，如果返回的状态是NDIS_STATUS_PENDING，这个状态以后将作为一个参数返回给 NdisMSendComplete。

4.3.2 接收的OOB数据

如果微端口传送介质相关的信息以及/或者其优先权，它必须用 NdisMIndicateReceivePacket 指明一个或多个数据包来指示接收数据，或者，如果微端口是面向连接的，则由 NdisMCoIndicateReceivePacket 来指示。在指示包之前，微端口必须写带外（OOB）数据的 Status 成员。微端口必须设置 Status 成员的值：在不希望高层驱动程序保存此包时，将它设置为NDIS_STATUS_RESOURCES，否则为NDIS_STATUS_SCCESS。

此外，微端口可执行下述一个或多个步骤：

n 若时间可以确定（如ATM NIC驱动程序），则设定 TimeSent

n 设定TimeReceived

n 设定HeaderSize。典型地，当包描述符被初始化时，微端口只须对每一个包描述符设定一次

n 设置指向MediaSpecificInformation及SizeMediaSpecificInfo的指针

n 若微端口指示接收包的时间戳，它必须应用NDIS_SET_PACKET_TIME_RECEIVED和/或NDIS_SET_PACKET_TIME_SENT宏在与包描述符相关的NDIS_PACKET_OOB_DATA中设置TimeReceived和/或TimeSent成员。若微端口应用系统时间作为时间戳，它可以调用一次NdisGetCurrentSystemTime来为指定的包序列中所有数据包设置接收时间戳。

n 若驱动程序通过接收指示额外OOB信息，它必须将SizeMediaSpecificInfo设为在执行MediaSpecificInformation时，调用者分配缓冲区中包含的信息的字节数。微端口可以用NDIS_SET_PACKET_MEDIA_SPECIFIC_INFO宏来设定这些数值。否则，SizeMediaSpecificInfo应为零，而MediaSpecificInfomation应当为NULL。

当 NdisM(Co)IndicateReceivePacket 返回时，OOB数据对于该微端口为只读的。因为高层已经使用过OOB，串行微端口应当读取 Status 成员，以确定哪个包可供重新使用。这类包所具有的状态与NDIS_STATUS_PENDING不同。任意带有NDIS_STATUS_PENDING Status成员的包属于更高层的驱动程序，当前不能为微端口所使用，随后将在其 MiniportReturnPacket 函数中返回给微端口。

若微端口在它所指示包序列的任意一个包中写了NDIS_ATATUS_RESOURCES状态，当NdisMIndicateReceivePacket返回时，这个包也将返回给微端口。

非串行或面向连接的微端口驱动程序在 NdisMIndicateReceivePacket 的返回时，不必检验指示包的 Status 成员。相反，非串行端口驱动程序在指明包描述符之前，必须以局部变量方式存储包的 Status 成员。当 NdisMIndicateReceivePacket 返回时，微端口驱动程序应当检查所存储包的 Status 成员。若微端口驱动程序在指明包描述符之前将 Status 成员设为NDIS_STATUS_RESOURCES，则它在 NdisMIndicateReceivePacket 返回之后，应当立即收回包描述符，这可能通过调用它所拥有的MiniportReturnPacket函数来实现。这种情况下，NDIS将不调用微端口驱动程序的MiniportReturnPacket函数来返回包描述符。若微端口驱动程序在指明包描述符之前将包的 Status 成员设为NDIS_STATUS_SUCCESS，则微端口驱动程序在NDIS将包描述符返回给微端口驱动程序的MiniportReturnPacket之前，不能收回包描述符。

4.4 发送包

无连接微端口可以采用下列两种方式的任意一种来发送包：

n MiniportSendPackets函数。NDIS每次传送给MiniportSendPackets一个或多个包描述符指针序列形式的数据包。

n MiniportSend函数。NDIS总是每次传送一个包描述符给MiniportSend。

无连接微端口要用 NdisMRegisterMiniport 注册MiniportSendPackets和MiniportSend两个函数，但NDIS仅调用MiniportSendPackets。

面向连接微端口必须使用其MiniportCoSendPackets函数来发送包。NDIS每次向MiniportCoSendPackets传送一个或多个包描述符指针序列形式的数据包。

一般来说，与分配大块不可缓冲的内存相比，为传输数据分配高速缓存更为可取。不可缓冲内存是一种稀少的系统资源。分配大块不可缓冲内存并非总是可行的，所以任何使用不可缓冲内存的微端口在驱动程序初始化期间必须分配内存。

微端口应当总是设想由协议驱动程序设定的缓冲区为可缓冲内存。

4.4.1 无连接微端口的多包传送

支持多包发送的无连接微端口声明了下述微端口函数：

VOID

MiniportSendPackets (

IN NDIS_HANDLE MiniportAdapterContext,

IN PPNDIS_PACKET PacketArray,

IN UINT NumberofPackets

) ；

PacketArray 是一个指针，它指向含有一个或多个包描述符的指针数组。微端口负责按协议驱动程序所确定的优先顺序发送包。NDIS并不对从协议驱动程序接收到的数据包进行排序，而是以从协议驱动程序接收到的次序把它们传送给微端口。

微端口假定协议驱动程序从不发送太大的包，所以微端口不检查包的大小。协议驱动程序在初始化期间查询微端口以确定微端口所支持的最大包的大小。协议驱动程序负责只传送大小为微端口所接受的包。

4.4.1.1 串行微端口的多包传送

每个包描述符都有相应的带外（OOB）数据，如4.3节所述。OOB块允许协议驱动程序给支持优先发送的协同操作微端口指定一个发送时间（TimeToSend）。在从MiniportSendPackets返回之前，串行微端口设置OOB块的 Status 成员，以指示调用的返回值。NDIS_PACKET_OOB_DATA块的其他成员对于微端口是只读的。

为一个或多个包提供MiniportSendPackets和返回NDIS_STATUS_PENDING的串行微端口必须为各个包保存指针，因为将来还要返回到这种未决的状态。当MiniportSendPackets返回时，包含指针的数组将返回给调用者。

没有发送包资源的串行微端口可将 Status 成员设为NDIS_STATUS_RESOURCES。这种情况下，当它通过调用 NdisMSendComplete 或调用 NdisMSendResourcesAvailable 完成发送时，无论哪一个先进行（见图4.1），NDIS都先将包排队，然后把它们传给微端口。

图4.1 串行微端口的多包传送──异步完成

NDIS假定，若为一个单元设定了NDIS_STATUS_RESOURCES，它也就为所有相应序列单元也设定了值。实际上，微端口暂时或永久地放弃了对包序列中所有包的所有权，在由MiniportSendPackets返回之前，它设定了不同于NDIS_STATUS_PENDING的 Status 成员。否则，微端口将拥有那些包资源，直到随后它调用 NdisMSendComplete 来放弃所有权，并将最后状态作为参数传给这一调用（见图4.1）为止。

当串行微端口没有发送包资源时，它不返回NDIS_STATUS_RESOURCES而是在内部对这一请求排序。微端口可以使用微端口在包头中的保留区来链接排序的包，这可能链接到特定适配器环境结构。若微端口对包排序，它必须返回NDIS_STATUS_PENDING作为已排序包的状态。为了增加吞吐量，微端口可以对包进行排序，以避免当包返回给NDIS后再次传递给微端口的情况发生。

串行微端口在MiniportSendPackets返回之前，为每个包设定了 Status 域，无论包的完成是同步的或是异步的。

如果串行微端口同步完成发送（见图4.2），应将包的OOB数据 Status 成员设定为发送状态，就是NDIS_STATUS_SUCCESS，NDIS_STATUS_FALURE，NDIS_STATUS_RESOURCES，或者微端口所确定的状态。如果微端口保留了此包并将随后发送此包，微端口将把 Status 成员设定为NDIS_STATUS_PENDING。如果微端口对于数组中的包返回NDIS_STATUS_RESOURCES状态，则NDIS将对此包及数组中所有随后发送的包重新排序，并在微端口指示它有可以利用的资源时，将它们传出。

图4.2 串行微端口的多包传送──同步完成

MiniportSendPackets返回后，OOB数据的 Status 域即禁止进入。这就意味着，当串行微端口完成一个先前已经返回了NDIS_STATUS_PENDING包时，微端口必须在调用 NdisMSendComplete 的 Status 参数中返回其发送状态。此微端口不必改写（通过调用 NdisMSendComplete 所完成的）任意包中OOB数据的 Status 成员。

4.4.1.2 非串行微端口的多包传送

每个包描述符有相应的带外（OOB）数据，如4.3节所述。OOB块允许协议驱动程序为支持优先发送的协同操作微端口指定一发送时间（TimeToSend）。尽管非串行微端口可以在由MiniportCoSendPackets返回之前设定OOB数据块的 Status 成员，但NDIS忽略了这一成员。对于微端口，NDIS_PACKET_OOB_DATA块的其他成员是只读的。

NDIS库忽略它传给非串行微端口驱动程序的MiniportSendPackets函数的所有包描述符中的OOB数据块。NDIS假定，非串行微端口与 NdisMSendComplete 异步地完成将每个包描述符输入到MiniportSendPackets函数中的操作。因此，非串行驱动程序的MiniportSendPackets函数通常忽略NDIS_PACKET_OOB_DATA块的 Status 状态成员，但它可以将这一成员设置为某个状态，这个状态与后来传给 NdisMSendComplete 的状态相同。

无论MiniportSendPackets多么缺乏足够的传输包的资源，也胜过依赖于NDIS对包进行排队，并重新发送数据包。非串行微端口管理其内部的包队列，驱动程序对其在内部队列中保持的传入数据包负责，直到它们通过网络传递出去为止。驱动程序也对协议检测的、传给MiniportSendPackets函数的包描述符的次序负责。非串行微端口必须用 NdisMSendComplete 来完成每一个传入的发送包（见图4.3），非串行微端口不能调用 NdisMSendResourcesAvaible。

图4.3 非串行微端口的多包传送

非串行微端口将永远不能（以一个最初提交给MiniportSendPackets函数的协议分配的包描述符的形式）把NDIS_STATUS_RESOURCES传给 NdisMSendComplete 。这种状态返回将导致协议请求的发送操作的失败，使得NDIS将以前传递过来的包描述符和所有相应资源返回给协议。

4.4.2 无连接微端口的单包发送

对于单包发送，无连接微端口声明了下述函数：

NDIS_STATUS

MiniportSend (

IN NDIS_HANDLE MiniportAdapterContext,

IN PNDIS_PACKET Packet,

IN UINT Flags

)；

参数 Flags 由共同操作的协议驱动程序和微端口定义；NDIS不对此参数做出定义或任何假定。然而，NDIS提供一个函数 NdisGetPacketFlags，这一函数由微端口调用，回收标志值。微端口保留此包，直到发送完成。

微端口不必检查包的大小，然而微端口应假定协议驱动程序永远不会发送太大的包。协议驱动程序在初始化期间查询微端口以确定它所支持最大包的大小。协议负责只传送尺寸为微端口所能支持的包。

若微端口可即刻完成发送（见图4.4），它返回NDIS_STATUS_SUCCESS，如果因任何原因失败或可能的微端口确定的错误状态，返回NDIS_STATUS_FAILURE，然后放弃包的所有权并返回给发送者。MiniportSend总是把发送状态作为MiniportSend状态返回；它从不为这类包的OOB数据设定 Status 成员。

图4.4 无连接微端口的单包发送──同步完成

若微端口不能立即完成发送任务（见图4.5），它就返回NDIS_STATUS_PENDING。这种情况下，微端口保留包的所有权，直到发送完成。例如，微端口在返回未决状态之前，在内部已经将包排序或者开始发送（但尚未完成）。当发送完成时，微端口必须调用 NdisMsendComplete，将一个指针传给已经发送完毕的包描述符，放弃包资源的所有权给发送者，以便其再次使用或者闲置它。

图4.5 串行微端口的单包发送──异步完成

串行微端口如何处理资源问题

若串行微端口缺少发送资源，它可返回NDIS_STATUS_RESOURCES。这种情况下，当微端口调用 NdisMSengResourcesAvailable 或 NdisMSendComplete 来表明它现在可以接受包（见图4.5）时，NDIS对包排序并将包重新送到MiniportSend。串行微端口仅在返回NDIS_STATUS_PENDING和调用 NdisMSendCommlete 之间的间隙里，才可调用 NdisMSendResourcesAvailable。

非串行微端口如何处理资源问题

若非串行微端口缺少发送资源，它必须在内部对包排序并返回NDIS_STATUS_PENDING（见图4.6）。非串行微端口不能返回NDIS_STATUS_RESOURCES。这种状态返回将导致协议请求的发送操作的失败，使得NDIS将以前传递过来的包描述符和所有相应资源返回给协议。

图4.6 非串行微端口的单包发送──异步完成

无论MiniportSend多么缺乏足够的资源传送数据包，也胜过依赖于NDIS对包进行排队，并重新传送数据包，非串行微端口管理自己内部包的顺序。微端口在其内部队列中对处理内入的发送包负责，直到它们通过网络被传出。
4.4.3 面向连接微端口的多包发送

在发送数据之前，面向连接微端口通常总是激活一个虚连接（VC），依靠它来发送数据。然后，微端口使用MiniportCoSendPackets函数发出数据。数据送出后，微端口去活并有时删除在其上发送数据的VC。关于建立、激活、去活、删除VC的信息，参见2.1.4节。

图4.7说明了面向连接微端口的多包发送。

图4.7 面向连接微端口的多包发送

MiniportCoSendPackets的定义为

VOID

MiniportCoSendPackets (

IN NDIS_HANDLE MiniportVcContext,

IN PPNDIS_PACKET PacketArray,

IN UINT NumberofPackets

);

MiniportVcContext 标识虚连接（VC），在其上进行包传输。VC用面向连接客户的或者呼叫管理器/MCM的调用NdisCoCreateVc来建立，NdisCoCreateVc 使得NDIS调用微端口的 MiniportCoCreateVc 函数。

每个PacketArray是指向一个或多个包描述符的指针数组的指针。微端口负责按照驱动程序设定的优先次序发送包。NDIS不对从协议驱动程序接收到的包进行排序，但将它们以接收到的次序传给微端口。

微端口不必检查包的大小，然而微端口应假设协议驱动程序永远不会发送太大的包。协议驱动程序在初始化期间查询微端口以确定它所支持的最大包的大小。协议只传递大小为微端口所支持的数据包。

每个包描述符都有相应的带外（OOB）数据，如4.3节所述。OOB块允许协议驱动程序给支持优先发送的共享微端口指定一发送时间（TimeToSend）。尽管面向连接微端口可在从MiniportCoSendPacket返回之前设定OOB块的 Status 成员，但这一成员为NDIS所忽略。NDIS_PACKET_OOB_DATA块的其他成员对于微端口是只读的。

NDIS库在提交给面向连接微端口MiniportCoSendPackets函数的所有包描述符中忽略了OOB数据块。NDIS假定，面向连接微端口与NdisMCoSendComplete函数异步地将包描述符传给 MiniportCoSendPackets 函数。因此，MiniportCoSendPackets经常忽略了NDIS_PACKET_OOB_DATA的 Status 成员，但它可以将这一成员设置为某个状态，这个状态与后来传给 NdisMCoSendComplete 的状态相同。

无论MiniportCoSendPackets多么缺乏发送包的资源，也胜过倚赖于NDIS（对包）排序，并重新发送数据包。面向连接微端口管理自己的内部包队列。这种微端口对处理进入内部队列的内入包负责，直到它们通过网络发送出去，微端口也保留协议检测的、传给MiniportCoSendPackets函数的包描述符的次序。面向连接微端口必须用 NdisMSendCoComplete 来完成每一个内入的发送包（见图4.3）。

面向连接微端口永远不能把STATUS_INSUFFICIENT_RESOURCES传给 NdisMSendComplete（以一个最初提交给MiniportCoSendPackets函数的由协议分配的包描述符的形式）。这种状态返回将导致协议请求的发送操作的失败，使得NDIS将以前传递过来的包描述符和所有相应资源返回给协议。

4.4.4 发送数据前的内存同步

如果微端口管理总线控制器DMA NIC，则它必须保证在NIC从共享内存中读取之前，传递给NIC的、用于通过网络发送的任意数据处于物理内存之中。因此，微端口必须调用NdisFlushBuffer以强迫刷新处理器的高速缓存。刷新调用的时间并不是关键的，但必须在数据写到共享内存之后以及释放到NIC之前，由MiniportSend或Miniport(Co)SendPackets来完成。

例如，在网络上发送一个包，MiniportSendPackets通常给出总线控制器包段在共享内存中的物理地址，然后调用 NdisFlushBuffer 和 NdisMUpdateSharedMemory。NdisFlushBuffer 调用确定WriteToDevice参数为TRUE，这时表明传送方向是由主机到NIC。微端口调用 NdisFlushBuffer 和 NdisMUpdateSharedMemory 之后，它通常将数据写到NIC寄存器中，释放给NIC。

由于缓冲线断裂（cache-line tearing）对于数据发送来说不是问题，微端口可以刷新数据而不涉及高速缓存的大小。

管理可编程I/O（PIO）设备的微端口保证传给NIC数据的正确性；因此，这种微端口将不调用 NdisFlushBuffer。

从属（Slave）DMA NIC的微端口保证NDIS将确保高速缓存在DMA传送数据过程中的一致性。

一般来说，最好为发送数据分配可缓冲内存。不可缓冲内存是一种稀少的系统资源。分配大块不可缓冲内存并非总是可行的。任何使用不可缓冲内存的微端口在驱动程序初始化期间必须对它分配内存。

微端口应当总是假定协议驱动程序设置的缓冲区为可缓冲内存。

4.4.5 发送步骤

微端口驱动程序发送数据包的调用顺序，取决于微端口所管理的NIC的类型。本节描述总线控制器DMA NIC，PIO设备以及使用NIC板上内存来发送包的步骤。

4.4.5.1 在总线控制器DMA NIC上发送包

在总线控制器DMA NIC上发送包所涉及的步骤取决于在DMA初始化过程中微端口如何初始化或如何保存系统资源：

n 管理一个总线控制器DMA NIC的串行微端口必须从MiniportInitialize函数中调用 NdisMAllocateMapRegisters，以便为DMA操作分配映射寄存器。

n 管理总线控制器DMA NIC的非串行或面向连接的NIC应当在其MiniportInitialize函数中调用 NdisMInitializeGatherDma，以将DMA资源初始化。使用 NdisMInitializeScatterGatherDma 的优点在于系统资源，例如映射寄存器，是动态地为每个DMA传输分配和释放的（因而更好地利用系统资源），而且DMA通过流水线进程得到了映射到虚拟范围的物理地址，从而改进了运行性能。管理总线控制器DMA NIC的非串行或面向连接的微端口可调用 NdisMAllocateMapRegisters 来静态地分配映射寄存器，而不是调用 NdisMInitializeScatterGather-Dma，然而这导致DMA运行性能较差和系统资源利用较差。

用NdisMAllocateMapRegisters保存DMA资源的微端口

一般而言，管理总线控制器DMA NIC的无连接微端口（串行的或非串行的）输出MiniportSendPackets函数以使其NIC获取最佳性能。管理总线控制器DMA NIC的面向连接微端口总是输出MiniportCoSendPackets。

在其Miniport(Co)SendPackets函数接收一个或多个包描述符之后，在DMA初始化期间，用 NdisMAllocateMapRegisters 分配映射寄存器（见3.2.2.3节）的微端口在用DMA发送包时，执行下述步骤：

1. 微端口调用 NdisQueryPacket 来读入包的长度，获取第一个缓冲描述符的指针，并确定缓冲描述符的数目及具体段数。

2. 微端口需要确定它是否有可供利用的发送缓冲区和可供利用的发送缓冲区描述符，这种描述符映射将缓冲区映射为环形。若这些资源都不能利用，串行微端口将不能以NDIS_STATUS_RESOURCES为参数调用MiniportSend或MiniportSendPackets，只能将数据包返回给NDIS；或者在其内部对包进行排序，并返回NDIS_STATUS_PENDING。非串行微端口或面向连接端口总是在内部对包排序并返回。

3. DMA设备限制物理段数目的上限，这些段可映射到单个的DMA操作。若让微端口发送一个过于细小的数据片，它必须将连接到包的缓冲区拷贝到单一的发送缓冲区中。微端口应当从它的MiniportInitialize函数分配这一分段缓冲区和缓冲描述符，以便进行映射，微端口调用 NdisMoveMemory 从协议提供的缓冲区中将数据转移到这一分段缓冲区中。只有当包过于细小时，微端口才必须拷贝发送数据，因为拷贝到分段缓冲区是一项昂贵的操作，如果做的过于频繁会对运行性能会产生负面影响。一般而言，若待发送缓冲区小于256字节，微端口可通过将数据拷贝到分段缓冲区，并将微端口分配的描述符传给 NdisMStartBufferPhysicalMapping 来改进性能。

4. 微端口调用 NdisMStartBufferPhysicalMapping 映射以前分配的共享内存缓冲区，它的内容将被发送。这一调用产生一个布尔变量，WriteToDevice，它表明数据是否由主机内存转移到NIC或由NIC转移到主机内存。对于发送，WriteToDevice设为TRUE。微端口也必须提供用于映射缓冲区的映射寄存器的索引，而且这一索引必须是在微端口初始化期间调用 NdisMAllocateMapRegisters 返回的寄存器之一。

5. 若微端口为其发送缓冲区分配缓冲内存，它将调用 NdisFlushBuffer，为发送缓冲区提供地址并指定WriteToDevice参数为TRUE，以表明传输方向是由主机到NIC。NdisFlushBuffer 刷新处理器缓冲内存线，保证NIC所见的内存内容与微端口内容相同。因为高速缓存线破裂对于发送数据不构成影响，微端口可以刷新数据而不必涉及整个缓冲内存。为保证所有平台上缓冲内存的一致性，微端口也应调用 NdisM-UpdateSharedMemory。

6. 微端口为传输数据而对NIC编程，例如，写入控制寄存器，NIC则进行DMA传输。

7. 当发送完成时，微端口调用 NdisMCompleteBufferPhysicalMapping 释放映射寄存器。如果NIC在发送完成时发生中断，那么这一调用由MiniportHandleInterrupt来执行。若NIC轮询，这一调用在时钟停止且发送结束时，由提供给 NdisMSetPeriodTimer 或 NdisMSetTimer（若端口不用周期时钟）的MiniportTimer函数来执行。

8. 微端口调用NdisMSendComplete或NdisMCoSendComplete。

用NdisMInitializeScatterGatherDma初始化DMA资源的微端口。

一般而言，管理总线控制器DMA NIC的非串行微端口输出一个MiniportSendPackets函数以使其NIC达到最佳性能。管理总线控制器DMA NIC的面向连接微端口总是输出一个MiniportCoSendPackets函数。

在其Miniport(Co)SendPackets函数中接收一个或多个包描述符后，在DMA初始化期间，用 NdisMInitializeScatterGatherDma 初始化系统资源（见3.2.2.3节）的总线控制器DMA微端口在用DMA发送包时，执行下述步骤：

1. 微端口用 ScatterGatherPacketInfo 的一个 InfoType 调用NDIS_PER_PACKET_INFO_ FROM_PACKET。NDIS_PER_PACKET_INFO_FROM_PACKET将一指针返回给SCATTER_GATHER_LIST结构。此结构设定SCATTER_GATHER_ELEMENT结构的一个数组，数组的每一个值设定了DMA数据邻近范围的基本物理地址和长度，以便发送。

2. 微端口可能需要确定它是否有可供利用的发送缓冲区和可供利用的发送缓冲区描述符，这种描述符映射将缓冲区映射为环形。若这些资源都不能利用，非串行或面向连接的微端口在内部将包排序并返回。

3. DMA设备具有物理段数目的上限，这些段可映射到单个的DMA操作。若让微端口发送一个过于细小的数据片，它必须将连接到包的缓冲区拷贝到单一的发送缓冲区中。微端口应由它的 MiniportInitialize 函数调用 NdisMAllocateSharedMemory 来分配这一分段缓冲区和缓冲描述符，以便进行映射。微端口调用 NdisMoveMemory 从协议提供的缓冲区将数据转移到这一分段缓冲区中。只有当包过于细小时，微端口才必须拷贝发送数据，因为拷贝到分段缓冲区是一项昂贵的操作，如果做的过于频繁会对运行性能产生负面影响。一般而言，若待发送缓冲区小于256字节，微端口可通过将数据拷贝到分段缓冲区来改进性能。

4. 微端口为其传输缓冲区分配缓冲内存，它调用 NdisFlushBuffer，来提供发送缓冲区的地址，并将 WriteToDevice 参数设为TRUE以指示传输的方向是从主机到NIC，NdisFlushBuffer 刷新处理器缓冲内存线，保证NIC所见的内存内容与微端口内容相同。因高速缓存线破裂对于发送数据不构成影响，微端口可刷新数据而不必涉及整个缓冲内存。为保证所有平台上缓冲内存的一致性，微端口也应调用 NdisMUpdateSharedMemory。

5. 微端口为传输数据而给NIC编程，例如，写入控制寄存器，NIC则进行DMA传输。

6. 微端口调用 NdisMSendComplete 或 NdisCoSendComplete。对Ndis(Co)SendComplete的调用释放用于DMA传输的映射资源。

4.4.5.2 在PIO设备上发送单包

对使用可编程I/O（PIO）来发送数据的NIC来说，控制它的微端口在其发送函数中执行下列步骤：

1. 调用 NdisQueryPacket 得到包的长度，然后微端口检查是否提供有用于发送数据包的传输资源，如果没有，微端口返回NDIS_STATUS_RESOURCE或者内部将包排队，并从其发送处理程序返回。如果微端口中数据包排进了队列，微端口必须为这个包返回NDIS_STATUS_PENDING。如果微端口支持带外（OOB）数据，它将使用NDIS宏从OOB块中读取任何有关的数据。如果提供了发送资源，NIC驱动程序将继续。

2. 调用 NdisQueryPacket 得到第一个包描述符缓冲区的指针。

3. 调用 NdisQueryBuffer 获得包含待发送数据缓冲区的虚拟地址以及这个缓冲区的字节数。

4. 调用 NdisRawWritePort Xxx 将缓冲区的长度写到端口上。管理PIO NIC的微端口保证将正确的数据传输给NIC，所以，这样的微端口不调用 NdisFlushBuffer。

5. 调用 NdisRawWritePortBuffer Xxx 写缓冲区——其地址是从 NdisQueryBuffer 返回的。

6. 调用 NdisGetNextBuffer 得到包的下一个缓冲区。

7. 重复3到6，直到所有的包缓冲区都被成功地送出。

8. MiniportSend总是把发送状态作为MiniportSend状态返回。如果这个状态不是NDIS_STATUS_PENDING，微端口将包描述符和资源返回给调用者。串行微端口的MiniportSendPackets函数在包描述符的OOB块 Send 成员中返回发送的状态。如果微端口对任何一个包返回未决状态，在它使用包资源而且准备将包返回给调用者时，它必须调用 NdisMSendComplete。NDIS库在它提交给非串行和面向连接微端口Miniport(Co)SendPackets函数的所有包描述符中忽略了OOB数据块。NDIS假定这样的微端口（与 NdisM(Co)SendComplete 异步）将包描述符传递给MiniportSendPackets或MiniportCoSendPackets函数。因此，Miniport(Co)SendPackets常常忽略NDIS_PACKET_OOB_DATA块的 Status 成员，但它可以将这一成员设置为某个状态，这个状态与后来传给 NdisM(Co)SendComplete 的状态相同。

4.4.5.3 使用板上内存发送包

对板上内存出现在主机的I/O空间的NIC，支持它的微端口从MiniportInitialize中调用 NdisMMapIoSpace，以将适配器的板上内存映射到主机内存中，并给微端口一个可用于引用内存的虚拟地址。在这种类型的NIC上，微端口发送数据时，先将待发送的缓冲区转移到映射内存中，然后以一种设备独特的方式，调用 NdisMRegisterIoRange 写微端口声明的端口（或寄存器），这些动作将使得NIC把数据放到网络介质上。如果微端口缺乏发送资源，它就会或者将包排队，或者将包返回给NDIS，就象以前所描述的那样。

如果微端口有可以利用的发送资源，它将调用：

1. NdisQueryPacket，得到包含待发送缓冲区的包的起始和长度。

2. NdisQueryBuffer，得到缓冲区描述符。

3. NdisMoveToMappedMemory，每次转移一个缓冲区到映射的适配器内存缓冲区。

4. NdisGetNextBuffer，从数据包中回收下一个缓冲区，然后再调用 NdisMoveToMappedMemory，重复这两个调用直到所有的缓冲区全部发送出去。

5. NdisMRawWritePort Xxx 或 NdisWriteRegister Xxx 引起设备特有方式发送的发生。

在开始拷贝之前，微端口用从 NdisQueryPacket 返回的长度来查明包含所有包缓冲区的板上内存是否有足够的空间。如果NIC内存对当前的发送没有足够的自由空间，串行微端口将返回一个NDIS_STATUS_RESOURCE状态，在这种情况下，NDIS将包排队且在以后微端口指示有可供使用的资源时重新发送。从另一个方面来说，微端口也可以返回NDIS_STATUS_PENDING，然后内部将包排队。在这种情况下，非串行微端口、面向连接的微端口以及WAN微端口必须返回NDIS_STATUS_PENDING，内部对包进行排队。

从属（Slave）DMA设备发送包

从属DMA NIC使用系统DMA控制器，将提供给发送处理程序（一般为MiniportSend）的包发送到用于网络传输的NIC上，从属DMA NIC的微端口授权NDIS在DMA传输期间确保高速缓存的一致性。

对任何一个微端口有发送资源的数据包，从属DMA MIC的微端口进行下列步骤：

1. 调用 NdisMSetupDmaTransfer 建立用于发送的系统DMA控制器。微端口通过调用 NdisMRegisterDmaChannel 传递一个句柄给在微端口初始化期间分配的DMA通道，并传递一个指向包含待发送数据缓冲区的主机内存的指针，同时传递一个缓冲区的偏移量，并将数据的长度、指示传输方向的布尔值从主机传给设备。在 NdisMSetupDmaTransfer 返回中，控制器通过编程来传输数据，也包括确保缓冲区的高速缓存一致性。

2. 执行设备相关的步骤以引起传输（例如调用 NdisRawWritePort Xxx）。

3. 当传输完成时，微端口调用 NdisMCompleteDmaTransfer。

一般来说，微端口为传输建立了完整的缓冲区。然而若缓冲区的大小大于NIC的DMA约束，微端口将用长度和偏移量参数来发送缓冲区的一段；如果待发送缓冲区小于256字节，微端口可通过将数据拷贝到分段缓冲区，并传递一个微端口分配的缓冲区包描述符到 NdisMSetuoDmaTransfer 以提高性能。

4.5 非串行微端口

通过对非串行微端口拥有的MiniportXxx函数的操作进行串行化以及对内部队列接收到的发送包进行排序，非串行微端口就能较好地完成全双工工作。提高性能的代价是非串行微端口必须符合额外的、更严格的设计要求，非串行微端口也需要额外的调试和测试时间。

注意NDIS为非串行微端口保留了为接收包而打开适配器和过滤器的方法。
4.5.1 非串行微端口的NDIS要求

与NDIS接口的非串行微端口必须符合下列要求：

n 非串行微端口在初始化期间必须能象识别NDIS一样识别出自己。其MiniportInitialize函数必须用设置在 AttributeFlag 中的NDIS_ATTRIBUTES_DESERIALIZE标志调用 NdisMSetAttributesEx。

n 非串行微端口的MiniportSendPackets函数不能完全与多包传送同步，但它必须与接收包序列中每个包的 NdisMSendComplete 异步地完成处理每个包。从这一点来看，非串行微端口类似于面向连接的微端口，它总是用 NdisMCoSendComplete 来异步完成发送多包中的每一个包。

n NDIS在它提交给非串行微端口MiniportSendPackets函数的所有包描述符中忽略了带外（OOB）数据块。因此，MiniportSendPackets常常忽略NDIS_PACKET_OOB_DATA块的 Status 成员，但它可以将这一成员设置为某个状态，这个状态与后来传给 NdisMSendComplete 的状态相同。

n 如果非串行微端口不能立即传输它所接收到的数据包，它将不能把包返回给NDIS重新排队，而是微端口必须内部对它们排队，直到有足够的资源可用来传输数据包。非串行微端口的MiniportSend或者MiniportSendPackets函数不能返回NDIS_STATUS_RESOURCE。
4.5.2 非串行微端口的驱动程序内部要求

非串行微端口必须符合下列的驱动程序内部要求：

n 非串行微端口必须用自旋锁保护其包队列，非串行微端口也应当保护其共享状态不被它所拥有的MiniportXxx函数同时访问。关于使用自旋锁的更多信息，参见“Kernel-Mode Drivers Design Guide”。

n 非串行微端口的MiniportXxx函数能运行在IRQL<=DISPATCH_LEVEL下，因此，驱动程序作者不能设想在它们处理的包序列中调用MiniportXxx，一个MiniportXxx函数可以抢先于另一个运行于低IRQL下的MiniportXxx。

n 非串行微端口负责包的队列管理。当微端口察觉到一个资源问题时，它不能将发送包返回给NDIS去重新排队，而是将内入包在自己内部进行排队，等候有足够的资源时将它们发送出去。

n 非串行微端口必须以协议确定的次序来发送每一个包数组。也就是说，微端口必须在数组第一项中传输第一个包，在数组第二项中传输第二个包，如此等等。微端口不能在给定的数组中对包进行重新排序。

4.6 接收数据

无连接的微端口使用下列方法之一给上层传递接收到的数据：

n 用一个指针调用 NdisMIndicateReceivePacket，这个指针指向一个或多个包含接收数据包的指针数组，微端口必须输出一个MiniportReturnPacket函数，用来处理接收协议返回的包。

n 用一个指向预先分配缓冲区的指针调用 NdisM Xxx IndicateReceive，有关的协议驱动程序会从缓冲区中拷贝数据。微端口必须输出一个MiniportTransferData函数，用来传输额外接收到的、不适合预先分配缓冲区的数据。

面向连接微端口必须调用NdisMCoIndicateReceivePacket来将接收到的数据传输给上层。
4.6.1 无连接和面向连接微端口的多包接收

要执行一个多包接收，微端口要分配和管理一个包描述符数组，用接收的数据填充链状缓冲区，然后通过函数调用，给有关协议驱动程序传输一个指针填充到包中，这个函数对无连接微端口是NdisMIndicateReceivePacket，面向连接微端口是NdisMIndicateReceivePacket。微端口应该为全部网络数据包而不是一个先行缓冲区分配和建立包描述符（见3.2.2.1节）。

在调用 NdisMCoIndicateReceivePacket 之前，面向连接的微端口总是要建立和激活一个在其上接收数据的虚连接（VC），当远程伙伴结束了接收调用时，微端口去活并有时是删除接收数据的VC，关于VC创建、激活、去活以及删除的描述，参见2.1.4节。

图4.8说明了无连接微端口或面向连接微端口的多包接收

图4.8 多包接收

微端口通过设置与每个包描述符相关的OOB数据块的成员，将状态（可能是包接收的时间、包发送的时间或其他介质特定信息）传递给上层，如果微端口允许协议保留数据包，并在以后将它返回给微端口的MiniportReturnPacket函数，则微端口必须将OOB块的 Status 成员设置为NDIS_STATUS_SUCCESS。如果微端口要用尽包描述符或缓冲区，会强迫所有有关的上层驱动程序在 NdisM(Co)IndicateReceivePacket 返回之前拷贝包数据，此时微端口必须将 Status 成员设置为NDIS_STATUS_RESOURCE，详情参见4.3节。

串行微端口必须在NdisM(Co)IndicateReceivePacket返回之前检查与每个包描述符有关的OOB块的 Status 成员：

n 如果 Status 是NDIS_STATUS_PENDING之外的任何一个，它所描述的包描述符和所有的资源将返回给微端口。

n 如果 Status 是NDIS_STATUS_PENDING，微端口分配的包资源的所有权转给协议驱动程序或者中间协议驱动程序，直到包描述符返回给微端口的MiniportReturnPacket函数。

n 如果微端口在调用 NdisM(Co)IndicateReceivePacket 之前，将数组中每个包的 Status 成员设置为NDIS_STATUS_RESOURCE，在高层驱动程序拷贝了指示包中的数据之后，包将返回给微端口，对所有这类包的 Status 成员将设置为NDIS_STATUS_ SUCCESS。

非串行或面向连接的微端口驱动程序不必检查 NdisMIndicateReceivePacket 返回的指示包的 Status，而实际上，非串行微端口驱动程序在指示包描述符之前，将包的 Status 保存到一个本地变量。当 NdisMIndicateReceivePacke t返回时，微端口驱动程序应当检查保存包的 Status。如果在指示包描述符之前微端口驱动程序将包的 Status 设置为NDIS_ STATUS_RESOURCES，它将在NdisMIndicateReceivePacket之后通过调用其自己的MiniportReturnPacket函数立即回收包描述符。在这种情况下，NDIS不调用微端口驱动程序的 MiniportReturnPacket 函数来返回包描述符。如果微端口驱动程序在指示包描述符之前将包的 Status 设为NDIS_STATUS_SUCCESS，直到NDIS将包描述符返回给微端口驱动程序的MinportReturnPacket函数，微端口驱动程序才收回包描述符。

管理总线控制器DMA NIC的微端口一般通过调用Ndis(Co)IndicateReceivePacket来指示包，因为其NIC通常有足够的链状缓冲区空间接收多个包。微端口通过一次处理几个包来提高性能。

通常，任何其他类型的网络接口卡（NIC），比如可编程I/O（PIO）NIC或适配器共享内存NIC，在需要传递优先权、介质特定信息或者接收时间时，只调用 NdisM(Co)-IndicateReceivePacket。一个典型的非DMA NIC每次只接收一个包，在两个单次接收之间必须重置。这类设备的微端口每次只能用NdisM(Co)IndicateReceivePacket指示一个包。

因为微端口在它调用 NdisM(Co)IndicateReceivePacket 时传递出包的所有权，它必须管好接收缓冲区以确保网络上的新数据到达时有缓冲区可用。典型地，微端口在其MiniportInitialize函数中为链状缓冲区预分配了比所要求的多一些的缓冲区，分配了足够的缓冲区描述符以映射这些缓冲区和用来进行接收指示的包描述符。

在接收指示环境中处理返回包

当非串行或面向连接微端口用 NdisM(Co)IndicateReceivePacket 指示一个包时，有可能包在 NdisM(Co)IndicateReceivePacket 返回之前，返回给微端口的MiniportReturnPacket函数。下面就是微端口如何处理这种情形的一个例子：

1. 微端口在NDIS_PACKET的 MiniportReserved 成员中维护两个标志，其中一个称为 InRcvDpc，它指出指示线程是否处在微端口接收DPC的环境中；另一个标志，称为 ReturnPacketCalled，指出微端口的MiniportReturnPacket函数是否在接收指示环境中被调用。

2. 在调用 Ndis(Co)IndicateReceivePacket 指出一个包之前，微端口设置 InRcvDpc 为TRUE，ReturnPacketCalled 为FALSE，例如：

NdisDpcAcquireSpinLock(&RcvLock);

PacketReserved->fInRcvDpc=TRUE;

PacketReserved->fReturnPacketCalled=FALSE;

NdisDpcReleaseSpinLock(&RcvLock);

3. 微端口调用 NdisM(Co)IndicateReceivePacket 指示数据包。

4. NdisM(Co)IndicateReceivePacket 返回之后，微端口设置包的 InRcvDpc 标志为FALSE，指示包不再处于接收指示的环境中。例如

NdisDpcAcquireSpinLock(&RcvLock);

Packet->fInRcvDpc=FALSE;

NdisDpcReleaseSpinLock(&RcvLock);

5. 微端口然后检查返回状态和返回包的 ReturnPacketCalled 标志。如果其状态是NDIS_STATUS_SUCCESS，NDIS_STATUS_RESOURCE或者NDIS_STATUS_ PENDING，而且 ReturnPacketCalled 为TRUE，则表示微端口的MiniportReturn-Packet函数已经在接收指示环境中被调用了，微端口重新对它所用的包进行排队。（在这种情况下，MiniportReturnPacket不对返回包重新排队。）例如：

Status = NDIS_GET_PACKET_STATUS(Packet);

If
((NDIS_STATUS_SUCCESS == Status) ||
(NDIS_STATUS_RESOURCE ==Status) ||
((NDIS_STATUS_PENDING ==Status)&&Packet->fReturnPacketsCalled)

{

ProcessReturnPacket(packet);

};

6. 在其每一个MiniportReturn函数中，微端口为每一个返回包测试 InRcvDpc 标志以检查包是否在接收指示环境被返回（也就是说，在接收指示完成以及微端口设置InRcvDpc为FALSE之前）。如果 InRcvDpc 为TRUE，微端口设置 Return-PacketCalled 为TRUE来指示协议已经完成对包的处理，微端口然后返回，以使得包能在接收指示环境中重新排队。例如：

NdisDpcAcquireSpinLock(&RcvLock);

If (Packet->fInRcvDpc)

{

Packet->fReturnPacketsCalled=TRUE;

NdisDprReleaseSpinLock(&RcvLock);

Return;

}

NdisDpcReleaseSpinLock(&RcvLock);

ProcessReturnPacket(Packet);

如果InRcvDpc为FALSE，表示在设备接收指示完成之后包被返回，MiniportReturnPacket自己释放自旋锁，并对包重新排队。

处理资源问题

如果微端口检测到它运行于较少的接收缓冲区空间资源下，它将使用下列策略中的一个：

n 当它调用 NdisM(Co)IndicateReceivePacket 时，微端口将每一个不需要协议保留的包描述符的OOB块的 Status 成员设为NDIS_STATUS_RESOURCE，这就告诉NDIS，微端口不放弃指示包的所有权。NDIS确保将包拷贝给协议驱动程序而不是传递给协议驱动程序。如果微端口是一个串行微端口，NDIS调用ProtocolReceive函数而不是ProtocolReceivePacket函数，来强迫协议驱动程序拷贝包。如果微端口是一个非串行或面向连接的微端口，NDIS调用协议驱动程序的Protocol(Co)ReceivePacket函数。当协议检测到OOB的 Status 成员设置为NDIS_STATUS_ RESOURCE时，它拷贝数据包。如果微端口支持所描述的多包接收范例，由于它不必提供MiniportTransferData函数，NDIS将截取协议驱动程序的请求并执行传输。

n 微端口为其自由缓冲区维护一个“高水位”和一个“低水位”，当缓冲区的数目接近“低水位”时，微端口调用 NdisMAllocateShareMemoryAsync 分配更多的缓冲区，然后调用 NdisAllocateBuffer 为这些缓冲区分配缓冲区描述符。不象只能在IRQL<DISPATCH_LEVEL下调用的 NdisMAllocateShareMemory，NdisM-AllocateShareMemoryAsync 能在IRQL DISPATCH_LEVEL下被调用，即使是在 MiniportHandleInterrupt 函数中。当自由缓冲区集超过“高水位”时，微端口调用 NdisMFreeShareMemory 释放超出的缓冲区，然后调用 NdisFreeBuffer 释放映射到这些缓冲区的缓冲区描述符。
4.6.2 无连接微端口的单包接收

在初始化期间，无连接微端口的MiniportQueryInformation函数被调用来检测微端口或网络接口卡（NIC）的操作特征。这些特征中包含着微端口所指示的预先分配缓冲区的最小尺寸，微端口必须至少指示出这么多的数据量，但是如果NIC提供了有更多的数据，它也能指示出来。例如，由于DMA适配器将所有的数据直接传输进内存，它通常指出一个完整的接收缓冲区，这就使得协议驱动程序可能不必调用 NdisTransferData 得到其余数据，而是直接拷贝缓冲区。

在另一方面，管理可编程IO（PIO）的无连接微端口传递一个最小预先分配缓冲区给协议驱动程序，其余数据直到微端口在MiniportTransfer函数调用微端口以前是不可读的。由于这种方式下传输数据比较慢，微端口对某些情况（例如没有协议接收包、以及调用MiniportTransferData请求最少协议数目等）进行了优化。

调用 NdisM Xxx IndicateReceive 的无连接微端口必须有一个MiniportTransferData函数，而且必须准备好被协议驱动程序调用，即拷贝内入的数据到一个链状缓冲区数据包中，MiniportTransferData在微端口 NdisM Xxx IndicateReceive 调用的环境中被调用。在 NdisM Xxx IndicateReceive 返回控制之后，微端口能接收新的数据，也就是说，它能重新接收中断。

MiniportTransferData传递一个标明单个缓冲区或缓冲区链的包描述符，微端口必须将收到的数据分配到这些缓冲区中，如果协议驱动程序提供的缓冲区不够，微端口将用一个适当的状态结束传输。然而由于微端口在其 Ndis Xxx IndicateReceive 调用中指出总的接收网络包的大小，协议驱动程序不能获得这个错误。如果所有的数据传给了MiniportTransferData，数据的传输将同步地完成（参见图4.9）。

图4.9 无连接微端口的单包发送——同步完成

另外，MiniportTransferData能返回NDIS_STATUS_PENDING。如果微端口返回一个未决状态，在所有数据被传输之后，必须调用 NdisMTransferDataComplete（参见图4.10）。而且微端口在数据传输完成前不能再中断，以使得已经接收的数据不会被新接收的覆盖掉。

图4.10 无连接微端口的单包发送——异步完成

微端口必须周期性地调用 NdisM Xxx ReceiveComplete，这导致了NDIS调用每一个绑定协议驱动程序的ProtocolReceiveComplete函数，这个对ProtocolReceiveComplete的调用向协议驱动程序指示：微端口能处理已经接收到但是没有全部处理的数据。例如，微端口能为其客户指出接收的数据，微端口应当在每十个接收包之后，或者如果不足十个处理包而要退出MiniportHandleInterrupt之前，在其MiniportHandleInterrupt函数中调用 NdisM Xxx ReceiveComplete。
4.6.3 接收数据的高速缓存（Cache）因素

如果微端口分配了缓冲内存，它应当进行下列步骤：

n 生成一个cache定位的接收缓冲区。

n 在开始接收之前，调用 NdisFlushBuffer 和 NdisMUpdateShareMemory 刷新高速缓存。

管理总线控制器DMA设备的微端口将数据接收到与网络适配卡（NIC）共享的内存区间。微端口必须保证在它开始读数据之前，接收的数据处在共享内存，而不是在处理器内存之中。要完成这些工作，这种DMA NIC的微端口必须在指示接收或者其他引起数据读操作的因素之前，调用 NdisFlushBuffer 和 NdisMUpdateShareMemory（例如，在其MiniportHandleInterrupt函数中）。

微端口也能通过分配接收缓冲区的cache定位内存来防止高速缓存线断裂（cache-line tearing）。如果微端口分配了大块内存，然后重新将它为接收的数据分为较小的缓冲区，每一个接收缓冲区的开始必须是cache定位的，这样缓冲区才不会穿过高速缓存（cache）块，微端口在初始化期间调用 NdisGetCacheFillSize，在其适配器特定的环境区保存此值。微端口在分配这样的缓冲区时，使用这个值来计算接收缓冲区的大小，微端口也必须使用高速缓存块的大小计算在预分配内存的哪里开始分配接收缓冲区。

一般来说，总线控制器DMA NIC的微端口应当分配缓冲的共享内存，因为它是一种较快、更有用的资源。有两种情况微端口必须使用非缓冲内存：

n 如果管理NIC的微端口将连贯的数据包接收到连续的内存之中，一个包的结尾占用着下一个包开始的相同的缓冲块。因此，微端口必须分配非缓冲内存，因为第一个包的读取会造成第二个包开始的坏缓冲数据。

n 如果释放与写有关的缓冲区，例如接收缓冲区可以用一个包含所有权位的头连接起来，这个头用来表明缓冲区是属于适配器还是微端口的。在这种情况下，适配器能在释放这个缓冲区之前写这一位。使用这种技术的微端口必须从非缓冲内存中分配这些缓冲区为止。
4.6.4 接收数据的步骤

微端口怎样处理接收数据依赖于其所管理的NIC类型的一些问题。本节将讨论下列各个设备类型接收数据的处理过程：

n 总线控制器DMA NIC

n PIO NIC

n 带板上内存的非DMA NIC

对这些设备有一些公共的操作，它们包括：

n 微端口调用 NdisRawWritePort Xxx 函数为其NIC编程。

n 指示一个缓冲区之前，指示一个完整包结构的微端口必须调用 NdisAdjustBuffer-Length 以调整描述缓冲区的缓冲区描述符的长度。微端口必须将缓冲区描述符的长度设为缓冲区中数据的长度。当包返回时，微端口必须再调用这个函数以将它重置为真实缓冲区的长度。

n 微端口不再启用更多的接收操作，直到当前数据已经被指示，并且一个新的缓冲区集已经被分配给接收环或者被有关的协议驱动程序拷贝为被指示的数据。

4.6.4.1 接收期间的包管理

一些微端口，比如以太网接口卡（NIC）的微端口，接收固定的、已知大小的帧。如果帧的大小总是相同的，微端口能用一个或多个链状缓冲区分配包，并能用相同包来进行接收。

然而，如果微端口控制着一个接收变长帧的NIC，所需要处理接收的包的大小各不相同。

如果微端口必须处理变长接收，它就必须进行下列步骤：

1. 对包调用 Ndis(Un)ChainBufferAtBack 或 Ndis(Un)ChainBufferAtFront 链接和短开包缓冲区，来建立较大的、并能将其改小的包指示。

2. 如果没有使用全部缓冲区，那么在指示数据包去设置链中最后一个缓冲区的缓冲区描述符大小之前，调用 NdisAdjustBufferAtLength。当包返回时，调用 NdisAdjust-BufferLength 将长度重置为缓冲区的真实长度。

从包描述符中拆除的缓冲区应当保存在一个空闲列表中，以确保它们没有“丢失”，并且当需要时它们可以再次使用。

4.6.4.2 在总线控制器DMA NIC上接收数据

当接收到一个或多个包时，微端口进行下列操作步骤：

1. 调用 NdisFlushBuffer 和 NdisMUpdateShareMemory 确保接收到的数据在微端口访问的内存区域中的一致性。

2. 调用 NdisAdjustBufferLength 调整缓冲区描述符域的大小（这个描述符域在指示之前映射网络接口卡（NIC）的接收/环状缓冲区）以匹配缓冲区中数据的实际长度。当包返回时，微端口必须再调用 NdisAdjustBufferLength 将接收/环状缓冲区回调到原来所分配的大小。

3. 如果微端口通过调用 NdisMIndicateReceivePacket 或 NdisMCoIndicateReceive-Packet 指示一个包数组，它必须设置与每个包描述符有关的OOB数据块的 Status 成员。如果微端口因为将要用尽接收缓冲区，而决定必须强迫协议驱动程序拷贝指示的数据时，它应当设置OOB块的 Status 成员为NDIS_STATUS_RESOURCE。否则微端口将把 Status 成员设为NDIS_STATUS_SUCCESS，NDIS_STATUS_ FAILURE，NDIS_STATUS_PENDING或者由驱动程序确定的状态。

4. 如果无连接微端口通过调用 NdisM Xxx IndicateReceive 指示一个单包，那么微端口指示出所有接收数据，与原先大小一样指示出其整个大小。

5. 在包指示（第3步）或拷贝（第4步）之后，微端口代替环上的缓冲区描述符。缓冲区描述符被下列之一代替：

n 一个先前分配，当前可用的包的缓冲区描述符。

n 一个协议驱动程序强迫拷贝和返回的包的缓冲区描述符。

n 一个通过调用 NdisAllocateBuffer 分配的缓冲区描述符，它是为另一个描述（调用 NdisMAllocateShareMemoryAsync 分配的）缓冲区的缓冲区描述符而使用的。

接收变长帧的NIC的微端口必须对帧进行链接和拆链，依靠接收数据的数量来构造不同大小的帧。一些微端口在使用固定长度帧的介质上接收数据，这种微端口能立即分配固定大小的缓冲区，将它们链接进包描述符，不必进行链接和拆除缓冲区就能使用它。

如果微端口调用 NdisMAllocateSharedMemoryAsync 来分配缓冲区，在分配完成时，将调用 MiniportAllocateComplete 函数。

4.6.4.3 在PIO NIC上接收数据

可编程I/O网络接口卡的微端口通过调用 NdisRawReadPort Xxx 和使用先前由 NdisMRegisterIoPortRange 声明的端口，从NIC端口读取数据。具有代表性的这样的一个设备通过调用具有过滤器特性的 Ndis Xxx IndicateReceive 函数，在 MiniportTransferData 函数中向有关的协议驱动程序传输剩余部分数据，将接收包的预先分配部分指示给绑定协议驱动程序。在数据传输之后，接收中断重新启用，数据就可以被新的数据所覆盖了。

PIO NIC的微端口不应因当前传输到上层协议驱动程序的时间延迟，而导致新的数据丢失。如果NIC有相对较小的板上FIFO缓冲区，微端口能在其MiniportInitialize函数预分配一个缓冲区，然后将接收数据转移到所指示的缓冲区，转移的过程试图保持FIFO尽可能地空，以利于接收新的数据。

换句话说，当调用微端口的MiniportTransferData函数时，微端口能指示数据直接从NIC端口拷贝，直接从NIC移动接收的剩余数据。这种接收技术招致了一个时间延迟——它等价于调用 Ndis Xxx IndicateReceive 指示接收数据到调用MiniportTransferData，将接收的数据移进MiniportTransferData提供的缓冲区之间的时间。

如果微端口调用 NdisMIndicateReceivePacket 或者 NdisMCoIndicateReceivePacket，在它作调用之前，它将数据传输到链接到预分配包的缓冲区中。

4.6.4.4 在内存映射设备上接收数据

管理带板上内存的NIC的微端口通常直接从NIC将 MiniportTransferData 接收的数据传输到调用MiniportTransferData提供的缓冲区中。象可编程I/O（PIO）NIC微端口一样，如果因为直接从NIC向协议驱动程序提供的缓冲区传输数据而引起等待时间，使得可能有新传来的数据丢失，那么微端口就能进行这样的接收。

微端口调用 NdisM Xxx IndicateReceive 指出等价于预先分配数据大小的数据量，或者如果缓冲区较小，而且微端口也已经完成了这个缓冲区，它将指示出整个缓冲区。MiniportTransferData调用 NdisMoveFromMappedMemory 从NIC的板上内存传输数据到协议驱动程序包或缓冲区中。数据被传输之后，微端口再启用中断，以使新的数据能被接收。

如果微端口调用 NdisMIndicateReceivePacket 或 NdisMCoIndicateReceivePacket，那么在调用之前，它将数据传输到链接至预分配包的缓冲区中。

4.7 保持统计量

每一个微端口网络接口卡（NIC）驱动程序应当维护强制性常规统计量和介质特性的统计对象标识符（OID）。关于更多的微端口处理查询和设置OID的信息，参见第五章。

无连接微端口的统计量

一般应用于所有无连接微端口NIC驱动程序的强制性统计量包括下列内容：

OID_GEN_XMIT_OK

OID_GEN_RCV_OK

OID_GEN_XMIT_ERROR

OID_GEN_RCV_ERROR

OID_GEN_RCV_NO_BUFFER

当接收到这样的一个OID查询时，MiniportQueryInformation函数必须返回这些统计量的每一个积累的对应值。

面向连接微端口的统计量

一般应用于所有面向连接微端口NIC驱动程序的强制性统计量包括下列内容：

OID_GEN_CO_XMIT_PDUS_OK

OID_GEN_CO_RCV_PDUS_OK

OID_GEN_CO_XMIT_PDUS_ERROR

OID_GEN_CO_RCV_PDUS_ERROR

OID_GEN_CO_RCV_PDUS_NO_BUFFER

当接收到这样的一个OID查询时，MiniportCoRequest函数必须返回它为这些统计量的每一个积累的对应值。

4.8 802.1P 包的优先权

802.1P 包的优先权是网络包中标准介质访问控制（MAC）头的一个扩展。在802网络介质中，这个扩展包含集线器和交换机用于建立包优先权的3比特的值。一般来说，当网络段拥塞时，集线器及交换机的负载导致了包的时间延迟或丢帧。在使用802.1p MAC扩展的网络上，高优先权的包优先接受处理，并且在它变成低优先级之前为它提供服务。

操作系统中负责服务质量（QoS）的部件给微端口提供802.1p优先权信息。描述每一个传输包的NDIS_PACKET结构用于发送优先权信息。QOS部件通过映射服务类型到IEEE 802.1p优先级值来得到这个优先权信息。Internet工程任务组（IETF）的集成服务工作组（Intserv）定义了这种服务类型的映射。这些802.1p优先权值的Intserv规范，也称为“用户优先权”对象，放在IETF的Internet站点上，在http://search.ietf.org执行下列步骤可以找到这些规范：

1. 单击Internet图标（Internet-Draft）

2. 单击Internet图标索引（Internet-Draft Index）

3. 单击特殊连接层的集成服务（Integrated Service over Specific Link Layers：issll）

4. 单击“IEEE 802网络上的集成服务映射”规范（“Integrated Service Mappings on IEEE 802 Networks”specification）

注意IETF有可能用较新的草案或最终的规范取代这个规范。尽管IEEE 802.1q规范描述了怎样标明包的网络优先性，这个规范还是使用IEEE802.1p优先权值描述了带优先权的包。QOS应用程序或者代表应用程序工作的QOS操作系统部件，能确定映射的 Intserv 服务类型。

除非主机正确地与网络协商了QOS，主机将只标明用“最好的”802.1p优先级值传输的包。如果主机有一个已经安装的包方案，主机将使用适当的QOS信号部件与高802.1p优先权的网络协商，包方案然后在NDIS包中传递一个适当的优先值给微端口驱动程序。

支持802.1p包优先级的微端口驱动程序使用从NDIS_PACKET得到的优先级值，然后在通过网络传输包的MAC头中生成对应的成员。这些微端口驱动程序也从接收包的MAC头中取出适当的信息。微端口驱动程序向高协议层指示包之前，优先值拷贝到NDIS_PACKET结构中。IEEE 802.1q规范描述了包怎样标明特定的网络优先性。

注意到上层的驱动程序有解释NDIS_PACKET结构的优先信息的能力。这种能力可以使得这些驱动程序对特殊的介质使用适当的优先性信息。

支持802.3优先权的特性包含在Windows NT4.0 的NDIS 4.0以及Windows 95及其后续版本之中，当802.3开始用于NDIS时，它用于PACE和100-VG的环境中。指定这个优先权的成员位于NDIS包描述符的OOB数据部分（NDIS_PACKET_OOB_DATA），这些成员仍然存在，但它们不使用802.1p优先权值。

下列章节将描述怎样实现一个支持802.1p包优先权的微端口驱动程序：

n 4.8.1 查询802.1p优先权支持

n 4.8.2 802.1优先权的包支持

n 4.8.3 为发送和接收指定包的大小

n 4.8.4 默认时禁止802.1p优先权支持
4.8.1 查询802.1p优先权支持

上层驱动程序向下层NIC微端口驱动程序发送一个查询，来检测这些微端口驱动程序或其NIC的能力。如果微端口驱动程序支持802.1p包优先权，它们将完成这些查询操作，返回所指示的支持信息。上层驱动程序对无连接微端口调用 NdisRequest 函数，对面向连接微端口调用NdisCoRequest，向查询返回一个填充着下列OID_XXX代码的NDIS_REQUEST结构。

OID_GEN_MAC_OPTION

OID_GEN_MAC_OPTION返回一个位掩码给上层驱动程序，位掩码定义了下层驱动程序或微端口NIC的可选的性质。发出查询的协议能确定出位掩码中哪一位是由下层微端口驱动程序设置的，这样的协议也利用了微端口与这一位有关的性质。例如，位掩码设置为NDIS_MAC_OPTION_8021P_PRIORITY标志的微端口驱动程序，能从高层驱动程序的NDIS_PACKET结构中接收802.1p优先权值，然后在网络传输包的MAC头中生成适当地信息，这个微端口驱动程序也支持从网络上接收的包的MAC头中提取适当的信息。信息被提取之后，微端口驱动程序使用NDIS_PACKET结构向高层驱动程序指出信息。如果微端口驱动程序或微端口NIC支持其他可选的性质，微端口驱动程序也将在位掩码中为这些性质设置标志。Windows 2000 在线DDK《Network Drivers Reference》中定义了OID_GEN_ MAC_OPTION的所有位掩码标志。
4.8.2 802.1优先权的包支持

上层驱动程序发送包给下层的NIC微端口驱动程序，微端口驱动程序也接收包，并将它们指示给连接层驱动程序。为了支持802.1p优先权，微端口驱动程序必须能访问NDIS_ PACKET传输的优先权信息。

支持802.1p包优先权的微端口驱动程序从网络接收包含着优先权值的数据包，微端口驱动程序从包的MAC头中提取有关的信息，然后微端口驱动程序在将包指示给高层协议之前，在包描述符中插入优先权的信息。要插入一个802.1p优先权值到一个包描述符中，微端口驱动程序必须调用NDIS_PER_PACKET_INFO_FROM_PACKET宏，在这个调用中，微端口驱动程序传递一个指针到描述包的NDIS_PACKET中，指示这个NDIS的 Ieee8021pPriority NDIS_PER_PACKET_INFO的值应当在包中插入802.1p优先权值。例如，在下列操作中，pPacketDesc 变量指向一个包，UserPriority 变量保持着插入到这个包的优先权值。要将优先权值插入包中，微端口驱动程序必须执行下列操作：

NDIS_PER_PACKET_INFO_FROM_PACKET(pPacketDesc,Ieee8021pPriority) =

(PVOID)(ULONG_PTR)UserPriority

支持802.1p包优先权的微端口驱动程序在从上层传下的包中得到优先权值之后，这些微端口驱动程序应当在网络传输的包的MAC头中生成相应的成员。要从包描述符中得到802.1p优先权值，微端口驱动程序调用NDIS_PER_PACKET_INFO_FROM_PACKET宏，在这个调用中，微端口驱动程序传递一个指针到描述包的NDIS_PACKET中。在这个调用中，微端口驱动程序也传递一个指示这个NDIS的 Ieee8021pPriority NDIS_PER_PACKET_ INFO的值，这个值应当是从包中得到的802.1p优先权值。例如，为了从包含在由 pPacketDesc 变量指示的包中回收优先权值，然后将优先权值分配给 UserPriority 变量，微端口驱动程序必须执行下列操作：

UserPriority = (IEEE8021PPRIORITY)

NDIS_PER_PACKET_INFO_FROM_PACKET(pPacketDesc,Ieee8021pPriority)

注意NDIS定义IEEE8021PPRIORIY数据类型为UINT。

如果从NDIS_PACKET回收的优先权值为0，微端口驱动程序在通过网络传输包之前，不应当用802.1p优先权值标识这个包。在以太网（802.3）中，不标识的包就象用“最高”优先权值标明的包一样。不标识包是为了保证与网络上与支持802.1p MAC扩展的设备向后兼容。

如果微端口驱动程序从网络上接收到的MAC包头中含有“最高”优先权，那么在将它指示给高层驱动程序之前，微端口在其NDIS_PACKET中插入0。

注意微端口驱动程序必须使用NDIS_PER_PACKET_INFO_FROM_PACKET插入或取得报包的802.1p优先权值。微端口驱动程序不必使用NDIS_PACKET_OOB_DATA结构中 MediaSpecificInformation 成员的 NdisClass802_3Priority NDIS_CLASS_ID值，来插入或取得802.1p优先权值，NDIS_PACKET_OOB_DATA结构包含着与包描述符有关的OOB信息。
4.8.3 为发送和接收指定包的大小

4.8.1节所描述的确定支持802.1p包优先权的微端口驱动程序可以对上层协议发送给它的包设定不同的大小，但对通过网络接收的包不能，而且随后会将它们指示给上层驱动程序，步骤如下：

n 微端口驱动程序可以约束上层驱动程序发送给它们的包的大小。例如，微端口驱动程序可以限制上层驱动程序发送的包最大为4字节，所以它可以给这些包追加适当的优先权值，然后通过早期的网络将这些包传输出去。在另一方面，传输的包没有预料到会穿越早期的网络设备，微端口驱动程序将不要求上层驱动程序发送的包最大为4字节。微端口驱动程序限制包的最大尺寸为4字节，因为这是IETF定义的802.1p优先权值的大小。微端口驱动程序使用OID_GEN_MAXIMUM_ FRAME_SIZE来确定其NIC支持接收上层驱动程序传来的包的最大尺寸。

n 微端口驱动程序必须确定它们从网络接收到的，然后指示给上层驱动程序的包的最大尺寸。即使微端口驱动程序在将这些通过网络接收到的包指示给上层驱动程序之前，去掉了它们的4字节优先权值，它也必须确定所有的可能指示给上层驱动程序的包的最大尺度。例如，微端口驱动程序可以接收没有标明优先权值的但仍然是下层介质支持的最大尺寸的网络包。微端口驱动程序使用OID_ GEN_MAXIMUM_TOTAL_SIZE来确定其通过网络NIC接收到的包的最大尺寸。
4.8.4 默认情况下禁止802.1p的优先权支持

微端口驱动程序应当支持NIC在默认情况下禁止802.1p优先权的性质。不能在所有网络环境中传输特定的带802.1p优先权的包，当前也没有它所需要的标准检测机制。在默认情况下，微端口驱动程序必须禁止802.1p的优先权支持。但应当能够对微端口驱动程序进行设置，用户可以通过控制面板应用中的“Network”来启用802.1p优先权支持。


获取和设置WMI的微端口信息及NDIS支持

本章描述上层及NDIS怎样查询以及设置微端口信息，微端口怎样对上层及支持Windows管理设备（WMI）的NDIS报告硬件状态的变化。

这一章包含下列内容：

n 5.1 NDIS管理信息和OID

n 5.2 查询微端口信息

n 5.3 设置微端口信息

n 5.4 报告硬件状态

n 5.5 NDIS对WMI的支持

5.1 NDIS管理信息和OID

每一个MIC微端口驱动程序都包含着它自己的MIB，这是一个由驱动程序存储动态配置的信息和静态信息，并且它也可以是由管理实体查询或设置的信息块。以太网的广播地址列表是配置信息的一个例子，接收到的广播包数目是静态信息的一个例子。MIB中每一个信息元素都被一个对象（Object）所引用。要引用这样的管理对象，NDIS定义了一个对象标识符（Object Identifier）。所以，如果管理实体要查询或设置一个特殊的管理对象，它必须为这个对象提供一个特定的OID。

MIB包含三种对象：

n 那些对所有NDIS NIC微端口通用的对象

n 那些NDIS NIC微端口为所有特定的介质类型（如以太网或令牌环网）而提供的特殊对象

n 那些厂商实现的特定对象

那些通用的和强制性介质相关的OID在DDK的在线文档“Network Drivers Reference（网络驱动程序参考）”中组织说明，为特殊的NIC驱动程序实现特定的OID在驱动程序的文档中列示并且给予了说明。

除了被分为操作特性（operational characteristics）（例如广播地址列表）和统计量（statistics）（例如接收广播数据包）之外，对象还被分为必须的（mandatory）和可选的（optional）。对一般或介质特定类的所有操作特征的对象都是必须的，但只有一些统计性对象是必须的，所有的实现特性对象都被划分为必须的分类。

图5.1显示了OID的结构。

图5.1 NDIS OID的格式

OID的前3个字节对OID的各种分类提供了主键。第4个字节标明了这种分类的特殊信息管理对象。

关于所有NDIS一般的和介质相关的OID的列表及其描述，参见DDK在线“Network Drivers Reference”。

5.2 查询微端口信息

协议驱动程序调用Ndis Xxx函数来查询微端口信息，就象用来执行这个查询的MiniportXxx一样，Ndis Xxx函数也依赖于微端口是无连接的还是面向连接微端口的。

5.2.1 无连接微端口的查询

要查询由无连接微端口维护的OID，绑定协议调用NdisRequest，传递一个标明了查询对象的NDIS_REQUEST类型的结构，它指向一个NDIS最终用来写请求信息的缓冲区。对NdisRequest的调用引起NDIS调用微端口的MiniportQueryInformation函数，它向NDIS返回请求信息。调用MiniportQueryInformation可以与调用NdisMQueyInformationComplete函数同步（或者异步）地完成（见图5.2）。

图5.2 无连接微端口的查询

NDIS也能自己调用微端口的MiniportQueryInformation函数来查询微端口的能力，状态，或者统计信息。（例如，在微端口的MiniportInitialize函数返回NDIS_STATUS_SUCCESS之后。）

无连接微端口必须的通用OID包括：

OID_GEN_SUPPORTED_LIST

OID_GEN_HARDWARE_STATUS

OID_GEN_MEDIA_SUPPORTED

OID_GEN_MEDIA_IN_USE

OID_GEN_MAXIMUM_LOOKAHEAD

OID_GEN_MAXIMUM_FRAME_SIZE

OID_GEN_LINK_SPEED

OID_GEN_TRANSMIT_BUFFER_SPACE

OID_GEN_RECEIVE_BLOCK_SIZE

OID_GEN_VENDOR_ID

OID_GEN_VENDOR_DESRIPTION

OID_GEN_VENDOR_DRIVER_VERSION

OID_GEN_CURRENT_PACKET_FILTER

OID_GEN_CURRENT_LOOKHEAD

OID_GEN_DRIVER_VERSION

OID_GEN_MAXIMUM_TOTAL_SIZE

OID_GEN_MAC_OPTIONS

OID_GEN_MEDIA_CONNECT_STATUS

OID_GEN_MAXIMUM_SEND_PACKET

特别地，微端口的MiniportQueryInformation函数必须准备对OID_GEN_MAXIMUM_ LOOKAHEAD，OID_GEN_MAXIMUM_SEND_PACKETS和OID_GEN_MAC_OPTIONS做出响应。

如果微端口通过调用Ndis Xxx IndicateReceive指示接收数据，它应当用NIC所能提供的先行数据的最大字节数来应答OID_GEN_MAXIMUM_LOOKAHEAD。如果绑定协议支持的先行缓冲区大小不一，NDIS将调用MiniportSetInformation设置微端口支持的预先缓冲区的大小为微端口和协议值的最小值。

如果驱动程序总是用NdisMIndicateReceivePacket指出完整的包，它将把这个值设置为整个包除去包头外的最大尺寸。

如果微端口注册了MiniportSendPackets函数，NDIS_GEN_MAXIMUM_SEND_ PACKETS将调用MiniportQueryInformation函数，微端口必须回答准备用于处理单包发送请求的最大包数目。因为没有资源（其设备被占满），微端口应当挑选一个这样的最大值，使得返回NDIS或内部进行排队的包数目最少。总线控制器DMA NIC的微端口应当试图挑选一个使其NIC工作于预期负载下的值。

当MinioprtQueryInformation被OID_GEN_MAC_OPTIONS调用时，它必须返回一个标明微端口执行的可选操作的位掩码，标志集包括：

n NDIS_MAC_OPTION_COPY_LOOKAHEAD_DATA

指示空闲的协议驱动程序以任意方式访问指示的数据，如果微端口指示了超出板上共享内存范围的数据，它不必设置此标志。

n NDIS_MAC_OPTION_NO_LOOPBACK

如果进行了设置，则表示微端口不回送传输给MiniportSend的包——它直接传输到本机的接收器上，微端口希望NDIS执行这个回送。如果NDIS执行了包的回送，包不传递到微端口。微端口总是设置这个标志，除非其NIC执行了硬件回送。

n NDIS_MAC_OPTION_RECEIVE_SERIALIZED

如果设置了，那么微端口在前面接收到的包被处理完之前，不指示任何新的接收包，包括传输数据。大多数微端口，除了那些通过调用NdisMIndicateReceivePacket指示包之外，都要设置这一位。

n NDIS_MAC_OPTION_TRANSFERS_NOT_PEND

如果设置了，微端口永远不从MiniportTransferData返回NDIS_STATUS_PENDING。

微端口必须永远不使用NDIS为其内部使用而保留的NDIS_MAC_OPTION_ RESERVED标志。

MiniportQueryInformation也被介质相关的OID查询以确定NIC的当前地址。例如，802.3类型NIC的微端口用OID_802_3_CURRENT_ADDRESS来查询。

确定介质类型的微端口将接收介质相关的带外OID。例如，802.3类型NIC的微端口用OID_802_3_MAXIMUM_LIST_SIZE来查询。关于一般操作的和介质相关OID的信息的列表和描述，参见DDK在线的“Network Drivers Reference”。

5.2.2 面向连接微端口的查询

要查询由面向连接微端口维护的信息对象，绑定协议调用NdisCoRequest，传递一个标明被查询对象的NDIS_REQUEST类型的结构，它提供一个NDIS最终可进行写请求信息的缓冲区。对NdisCoRequest的调用引起NDIS调用微端口的MiniportCoRequest函数，它对NDIS返回请求信息。MiniportCoRequest与一个对NdisMCoRequestComplete的调用同步或异步地完成（见图5.3）。

图5.3 面向连接微端口的查询

NDIS也能自己调用微端口的MiniportCoRequest函数来查询微端口的能力，状态，或者统计信息，例如，在微端口的MiniportInitialize函数返回NDIS_STATUS_SUCCESS之后。

面向连接微端口必须能为NIC特定的所有VC（虚连接）提供全局及部分（perVC）的基础信息，例如，如果给MiniportCoRequest提供一个非空的NdisVcHandle用来查询OID_GEN_CO_RCV_CRC_ERROR，那么微端口返回特定VC所有接收到的CRC错误数目。对一个用空NdisVcHandle的查询，微端口返回其NIC所有接收到的CRC错误数目。

面向连接微端口的一般操作OID集包括：

OID_GEN_SUPPORTED_LIST

OID_GEN_HARDWARE_STATUS

OID_GEN_MEDIA_SUPPORTED

OID_GEN_MEDIA_IN_USE

OID_GEN_LINK_SPEED

OID_GEN_VENDOR_ID

OID_GEN_VENDOR_DESRIPTION

OID_GEN_VENDOR_DRIVER_VERSION

OID_GEN_DRIVER_VERSION

OID_GEN_MAC_OPTIONS

OID_GEN_MEDIA_CONNECT_STATUS

OID_GEN_MINIMUM_SEND_PACKET

微端口的MiniportCoRequest函数必须准备对上述OID中任何一个的查询或设置做出响应。

当OID_GEN_CO_MAC_OPTIONS调用 MinioprtCoRequest时，它必须返回一个标明微端口执行的可选操作的位掩码，标志集包括：

n NDIS_MAC_OPTION_NO_LOOPBACK

如果设置了，表示微端口不回送传输给MiniportSend的包——它直接传输到本机的接收器上，微端口希望NDIS执行这个回送。如果NDIS执行了包的回送，包不传递到微端口。微端口总是设置这个标志，除非其NIC执行了硬件回送。

n NDIS_MAC_ETOX_INDICATION

如果设置了，表示微端口只在包通过NIC传输之后指示发送完成。

微端口必须从不使用NDIS为其内部使用而保留的NDIS_MAC_OPTION_ RESERVED标志。

MiniportCoQequest也被介质相关的OID查询以确定NIC的当前地址。例如，ATM类型NIC的微端口用OID_ATM_HW_CURRENT_ADDRESS来查询。

关于面向连接微端口的一般操作和介质特定OID信息的列表和描述，参见DDK在线的“Network Drivers Reference”。

5.3 设置微端口信息

协议驱动程序调用Ndis Xxx函数来设置微端口信息，就象用来进行一个设置操作的MiniportXxx函数一样，根据微端口是无连接的或面向连接的不同而不同。

5.3.1 为无连接微端口设置信息

要设置一个由无连接微端口维护的OID，绑定协议调用NdisRequest，并且传递一个标明查询对象（OID）、且指向一个包含要设置对象值的缓冲区的NDIS_REQUEST类型的数据结构。对NdisRequest的调用引起NDIS调用微端口的MiniportSetInformation函数，它用所提供的值设置对象。

MiniportSetInformation的调用可以同步或异步地完成。要异步地完成这个调用，微端口调用NdisMSetInformationComplete。

图5.4 为无连接微端口设置信息

5.3.2 为面向连接微端口设置信息

要设置一个由面向连接微端口维护的OID，绑定协议调用NdisCoRequest，传递一个标明被查询对象（OID）、且指向一个包含要设置对象值缓冲区的NDIS_REQUEST类型的数据结构。对NdisCoRequest的调用引起NDIS调用微端口的MiniportCoRequest函数，它用所提供的值设置对象。

MiniportCoRequest 的调用可以同步或异步地完成。要异步地完成这个调用，微端口调用NdisCoRequestComplete。

图5.5 为面向连接微端口设置信息

5.3.3 设置微端口信息的时机

除了在初始化期间被调用之外，无连接微端口的MinportSetInformation函数或面向连接微端口的MinportCoRequest函数在下列两种情况下被调用：

n 硬件重置期间

n 作为协议调用NdisCloseAdapter的结果

MinportSetInformation或MinportCoRequest在硬件初始化操作期间的调用，参见7.1节。在这种情况下，调用MinportSetInformation或MinportCoRequest来将微端口重置为关于其地址的初始状态。

当协议调用微端口的NdisCloseAdapter来关闭NIC时，NDIS也会调用MinportSet-Information或MinportCoRequest。这样要求微端口更新其地址信息。

5.4 报告硬件状态

无连接微端口NIC驱动程序能通过调用NdisMIndicateStatus对上层指示硬件状态的变化，面向连接微端口则使用NdisMCoIndicateStatus来指示。

NdisM(Co)IndicateStatus 取得一个一般状态码和一个包含将来定义的用于标识状态改变原因的介质信息缓冲区。NDIS给绑定协议驱动程序报告状态的变化，NDIS不解释或中途截取这些状态代码。微端口NIC驱动程序可以发出一个或多个这样的调用，然而当一个微端口完成其发送状态时，它必须调用NdisMIndicateStatusComplete以使得有关的协议驱动程序知道已经报告了的状态；面向连接微端口不必指出它已经完成了发送状态。协议驱动程序或者配置管理器能记录其状态，校正其行为。

NdisMCoIndicateStatus 任取一个有效的NDIS_STATUS_XXX值。微端口NIC驱动程序负责指示协议或高层驱动程序状态码的含义。然而，协议驱动程序忽略了所有没有意义或在其操作环境中不可理解的状态值。

例如，微端口驱动程序使用NDIS_STATUS_RING_STATUS来报告环失败。NdisMCo-IndicateStatus的StatusBuffer参数将详细说明环的状态，StatusBuffer的内容可能包含表明状态报告原因的环状态集合，比如，NDIS_RING_SIGNAL_LOSS和NDI_RING_HARD_ ERROR。

上层驱动程序或NDIS可以向微端口NIC驱动程序查询微端口的硬件状态。当无连接微端口的MiniportQueryInformation函数或面向连接微端口的MiniportCoRequest函数接收到OID_GEN_HARDWARE_STATUS时，它用任何一个在NDIS_HARDWARE_STATUS中定义的硬件状态来回答，这些硬件状态包括：

n NdisHardwareStatusReady

n NdisHardwareStatusInitializing

n NdisHardwareStatusReset

n NdisHardwareStatusClosing

n NdisHardwareStatusNotReady

微端口能被查询，以至于NDIS可以在NDIS层之间同步地操作，例如，确定NIC是否已准备好接收包。

5.5 WMI的NDIS支持

通过NDIS，WMI的客户可以查询或设置NDIS维护的OID，WMI客户也能注册用来接收微端口状态的指示。

NDIS自动用WMI注册微端口和命名VC，就象每一个微端口OID的标准集一样（参见5.6.5节）。象在5.6.7节中描述的一样，微端口能提供对定制OID和定制状态指示的支持。

NDIS不为协议驱动程序提供WMI支持。协议，比如中间层驱动程序，能为它自己创建一个设备对象，并直接用WMI注册它。

关于WMI体系结构以及微端口驱动程序怎样适应这个体系的更多信息，参见DDK在线“Kernel-Mode Drivers Design Guide”。

5.5.1 用WMI注册与注销NDIS微端口

NDIS自动向WMI注册微端口的每一个实例（即，每一个功能设备对象都是由NDIS响应一个成功的MiniportInitialize返回而创建）。微端口实例被注册为一个WMI提供的数据， WMI客户能发送它的查询，设置以及注册接收微端口状态的指示。微端口不必明确地用WMI注册，微端口初始化之后，NDIS将自动地为微端口进行这些步骤。

当微端口卸载时（即微端口的MiniportHalt函数返回之后），NDIS自动向WMI注销微端口实例，使得WMI不再对微端口发送查询或设置命令。

对每一个向WMI注册的微端口实例，NDIS注册每一个对应于特殊OID或微端口状态指示的GUID（全局统一标识符）。对每一个微端口，NDIS注册GUID为一个标准查询OID和微端口状态指示集（见5.6.5节和5.6.6节）。如果微端口支持定制的OID或状态指示（见5.6.7节），NDIS也向WMI注册这些GUID。

对面向连接微端口，NDIS也注册任何命名VC（见5.6.3节）。只有用NdisCoAssign-InstanceName命名的呼叫管理器，集成微端口呼叫管理器（MCM）或面向连接客户对WMI客户来说才是可见的。

5.5.2 OID和微端口状态的GUID映射

当微端口发送请求或设置微端口时（即，当NDIS发送一个IRP到NDIS创建的功能性设备对象时），NDIS截取这个查询或设置，映射GUID为微端口支持的OID，然后查询或设置微端口维护的OID。如果微端口是无连接微端口，NDIS调用微端口的Miniport-QueryInformation函数或MiniportSetInformation函数。如果微端口是面向连接微端口，NDIS调用微端口的MiniportCoRequest函数。NDIS返回WMI查询或设置微端口OID的结果。

如果微端口用其客户注册接收的Ndis(Co)IndicateStatus发出一个状态指示，并将它传给WMI。WMI然后传递这个状态指示给所有已注册指示的客户。

5.5.3 支持命名VC

对面向连接微端口，NDIS允许WMI客户在每VC（per-VC）基础上查询或设置OID。WMI也能列举VC，在WMI客户查询或列举相关的特殊VC OID信息之前，呼叫管理器，MCM，或者面向连接的客户必须用NdisCoAssignInstanceName来命名VC。

在用NdisCoCreateVC建立了VC之后，呼叫管理器或者面向连接的客户用NdisCoAssignInstanceName命名VC，NDIS分配VC一个实例名，并用WMI注册这个实例名。WMI客户然后列举这个VC，查询或设置有关的OID。

MCM不能用NdisCoAssignInstanceName来命名VC，然而MCM应当为VC创建一个定制的GUID和OID，并向NDIS注册这个GUID-OID映射（见5.5.7节）。

5.5.4 NDIS支持的WMI操作

NDIS支持下列WMI操作：

n 列举适配器和列举VC

NDIS向WMI注册全局的GUID（GUID_NDIS_ENUMERATE_ADAPTER和GUID_ NDIS_ENUMERATE_VC），允许WMI客户列举所有的适配器（微端口实例）和所有命名VC。由于NDIS自己保持所有载入的微端口和所有命名VC的路径，NDIS不需要查询微端口的这些信息。

n 查询单个实例（QUERY SINGLE INSTANCE）和设置单个实例（SET SINGLE INSTANCE）

通过NDIS，WMI客户能查询或设置数据块（对应于单个OID）的单个实例。对于查询，NDIS返回所有与OID相关的信息。WMI客户不能查询或设置OID中的数据项。例如，GUID_NDIS_GEN_CO_LINK_SPEED查询返回输出和输入的速度。WMI客户不能查询输出速度，但是能查询输入速度。

n 查询所有数据（QUERY ALL DATA）

NDIS通过得到适当的数据以及向WMI返回所有GUID实例的混和数据，来满足特殊GUID的一个QUERY ALL DATA请求。例如，要响应GUID_NDIS_ENUMERATE_ADAPTER的QUERY ALL DATA，NDIS给WMI返回一个所有加载微端口的列表。对映射到OID_GEN_CO_XMIT_PDUS_OK的GUID上的QUERY ALL DATA，微端口查询每一个面向连接微端口VC的OID，并返回混和数据给WMI。由于QUERY ALL DATA的开销太大，建议WMI客户只对列举适配器和VC使用QUERY ALL DATA。

n 事件指示（EVENT NOTIFICATION）

WMI客户能向NDIS注册以通知特殊微端口的状态指示。当这样的事件发生时，NDIS用适当的GUID传递这些状态信息，分配给WMI客户。

5.5.5 向WMI注册标准微端口OID

NDIS对无连接微端口自动使用下面所列示的一般OID 向WMI注册GUID：

OID_GEN_HARDWARE_STATUS

OID_GEN_MEDIA_SUPPORTED

OID_GEN_MEDIA_IN_USE

OID_GEN_MAXIMUM_LOOKAHEAD

OID_GEN_MAXIMUM_FRAME_SIZE

OID_GEN_LINK_SPEED

OID_GEN_TRANSMIT_BUFFER_SPACE

OID_GEN_RECEIVE_BUFFER_SPACE

OID_GEN_TRANSMIT_BUFFER_SIZE

OID_GEN_RECEIVE_BUFFER_SIZE

OID_GEN_VENDOR_ID

OID_GEN_VENDOR_DESCRIPTION

OID_GEN_CURRENT_PACKET_FILTER

OID_GEN_CURRENT_LOOKAHEAD

OID_GEN_DRIVER_VERSION

OID_GEN_MAXIMUM_TOTAL_SIZE

OID_GEN_MAC_OPTION

OID_GEN_MEDIA_CONNECT_STATUS

OID_GEN_MAXIMUM_SEND_PACKETS

OID_GEN_VENDORD_RIVER_VERSION

OID_GEN_XMIT_OK

OID_GEN_RCV_OK

OID_GEN_XMIT_ERROR

OID_GEN_RCV_ERROR

OID_GEN_RCV_NO_BUFFER

NDIS对面向连接微端口自动使用下面所列示的一般OID 向WMI注册GUID：

OID_GEN_CO_HARDWARE_STATUS

OID_GEN_CO_MEDIA_SUPPORTED

OID_GEN_CO_MEDIA_IN_USE

OID_GEN_CO_LINK_SPEED

OID_GEN_CO_VENDOR_ID

OID_GEN_CO_VENDOR_DESCRIPTION

OID_GEN_CO_DRIVER_VERSION

OID_GEN_CO_MAC_OPTION

OID_GEN_CO_MEDIA_CONNECT_STATUS

OID_GEN_CO_VENDORD_RIVER_VERSION

OID_GEN_CO_MINIMUM_LINK_SPEED

OID_GEN_CO_XMIT_PDUS_OK

OID_GEN_CO_RCV_PDUS_OK

OID_GEN_CO_XMIT_PDUS_ERROR

OID_GEN_CO_RCV_PDUS_ERROR

OID_GEN_CO_RCV_PDUS_NO_BUFFER

NDIS自动使用下面所列以太网OID向 WMI注册GUID：

OID_802_3_PERMANENT_ADDRESS

OID_802_3_CURRENT_ADDRESS

OID_802_3_MULTICAST_LIST

OID_802_3_MAXIMUM_LIST_SIZE

OID_802_3_MAC_OPTIONS

OID_802_3_RCV_ERROR_ALIGNMET

OID_802_3_XMIT_ONE_COLLISIONS

OID_802_3_XMIT_MORE_COLLISIONS

NDIS自动使用下面所列令牌环网OID向WMI注册GUID：

OID_802_5_PERMANENT_ADDRESS

OID_802_5_CURRENT_ADDRESS

OID_802_5_CURRENT_FUNCTIONAL

OID_802_5_CURRENT_GROUP

OID_802_5_LAST_OPEN_STATUS

OID_802_5_CURRENT_RING_STATUS

OID_802_5_CURRENT_RING_STATE

OID_802_5_LINE_ERRORS

OID_802_5_LOST_FRAMES

NDIS自动使用下面所列FDDI网络OID向WMI注册GUID：

OID_FDDI_LONG_PERMANENT_ADDRESS

OID_FDDI_LONG_CURRENT_ADDRESS

OID_FDDI_LONG_MULTICAT_LIST

OID_FDDI_LONG_MAX_LIST_SIZE

OID_FDDI_SHORT_PERMANENT_ADDRESS

OID_FDDI_SHORT_CURRENT_ADDRESS

OID_FDDI_SHORT_MULTICAT_LIST

OID_FDDI_SHORT_MAX_LIST_SIZE

OID_FDDI_ATTACHMENT_TYPE

OID_FDDI_UPSREAM_NODE_LONG

OID_FDDI_DOWNSTREAM_NODE_LONG

OID_FDDI_FRAME_ERRORS

OID_FDDI_FRAME_LOST

OID_FDDI_RING_MGT_STATE

OID_FDDI_LCT_FAILURES

OID_FDDI_LEM_REJECTS

OID_FDDI_LCONNECTION_STATE

NDIS自动使用下面所列ATM网络OID向WMI注册GUID：

OID_ATM_SUPPORTED_VC_RATES

OID_ATM_SUPPORTED_SERVICE_GATEGORY

OID_ATM_SUPPORTED_AAL_TYPES

OID_ATM_HW_CURRENT_ADDRESS

OID_ATM_MAX_ACTIVE_VCS

OID_ATM_MAX_ACTIVE_VCI_BITS

OID_ATM_MAX_ACTIVE_VPI_BITS

OID_ATM_MAX_AAL0_PACKET_SIZE

OID_ATM_MAX_AAL1_PACKET_SIZE

OID_ATM_MAX_AAL34_PACKET_SIZE

OID_ATM_MAX_AAL5_PACKET_SIZE

OID_ATM_RCV_CELLS_OK

OID_ATM_XMIT_CELLS_OK

OID_ATM_RCV_CELLS_DROPPED

5.5.6 向WMI注册的标准微端口状态

NDIS自动用下列NdisM(Co)IndicateStatus指示的NDIS_STATUS代码向WMI注册GUID：

NDIS_STATUS_RESET_START

NDIS_STATUS_RESET_END

NDIS_STATUS_MEDIA_CONNECT

NDIS_STATUS_MEDIA_DISCONNECT

NDIS_STATUS_MEDIA_SPECIFIC_INDICATION

NDIS_STATUS_LINK_SPEED_CHANGE

5.5.7 定制OID与状态指示

驱动程序开发者可以创建一个定制的OID或者（映射到开发者创建的定制GUID上的）微端口状态指示。NDIS用在微端口上起作用的WMI注册这个定制的GUID，以便WMI客户能查询或注册这个OID，或者接收定制的状态指示。

NDIS在微端口完成初始化之后通过查询微端口而得到微端口的定制信息或状态指示。要得到这样的信息，NDIS用OID_GEN_SUPPORTED_GUID查询无连接微端口，用OID_GEN_CO_SUPPORTED_GUIDS来查询面向连接微端口。

对OID_GEN_(CO_)SUPPORTED_GUIDS的查询，给NDIS返回一个NDIS_GUID结构的数组。每一个NDIS_GUID结构映射一个定制GUID到定制的OID或定制的状态指示。就象5.5.7.1节所描述的，涉及到支持定制OID和状态指示工作的主要部分是填充NDIS_GUID。就象5.5.7.2节所描述的，其他主要的任务是创建一个描述GUID的MOF文件，以及对微端口编译这个文件。

5.5.7.1 填充NDIS_GUID

NDIS_GUID结构定义如下：

typedef struct _NDIS_GUID

{

GUID Guid;

union

{

NDIS_OID Oid;

NDIS_STATUS Status;

};

ULONG Size;

ULONG Flags;

}NDIS_GUID,*PNDIS_GUID;

要得到这个结构Guid成员的GUID，驱动程序开发者必须运行uuidgen.exe程序，关于这个工具更多的信息，参见platform SDK。

Oid或Status成员是一个表示OID代码或状态码的ULONG，对这两种情况，代码中最重要的字节是0xFF，其余是由厂商定义的。这个代码在微端口范围之内必须是唯一的。

如果NDIS_GUID结构映射到一个返回数据的OID，那么返回数组的Size成员确定了每一个数据项的大小。如果返回数据项的大小是变化的，或者如果微端口不返回数据，或者NDIS_ GUID结构映射到一个状态指示，那么Size成员必须是-1。

下列Flags标志可以“或”在一起，以指示GUID是否映射到一个OID或NDIS_STATUS串，并指示出GUID所支持数据的类型：

NDIS_GUID_TO_OID

当设置了时，表示NDIS_GUID结构映射一个GUID到OID上。

NDIS_GUID_TO_STATUS

当设置了时，表示NDIS_GUID结构映射一个GUID到NDIS_STATUS串上。

NDIS_GUID_ANSI_STRING

当设置了时，表示为GUID提供一个0终结的ANSI串。

NDIS_GUID_UNICODE_STRING

当设置了时，表示为GUID提供一个UNICODE串。

NDIS_GUID_ARRAY

当设置了时，表示为GUID提供一个数据项数组。所确定的Size表示数组中每一个数据项的长度。

5.5.7.2 包括MOF文件

所有映射到微端口的定制OID和/或定制状态指示的定制GUID的描述必须包含进一个管理对象格式（MOF）文件中，这个文件必须编译和包含进微端口的资源文件（*.rc）。关于MOF文件格式的描述，参见《Driver Writer’s Guid》。

MOF源文件必须是一种MOFDATA类型，有一个.mof的扩展名。MOF源文件必须用mofcomp.exe编译进二进制文件，并用wmimofck.exe检查，这两个工具都包含在DDK之中。下面一行必须插入到微端口的*.rc文件中以包含MOF二进制：

MofResource MOFDATA filename.bmp

在这里filename是MOF源文件的文件名。


微端口的电源管理

本章描述由NDIS为NDIS微端口提供的电源管理（PM）服务，以及为支持电源管理而对微端口的要求。本章将会讨论以下主题：

6.1 电源管理的需求与可选的OID

6.2 网络设备电源状态

6.3 网络唤醒事件

6.4 处理 OID_PNP_QUERY_POWER

6.5 处理 OID_PNP_SET_POWER

6.6 早期微端口的电源管理

对电源管理的一般性讨论，参见“plug and play”，“Power Management”以及“Setup Design Guide”。

6.1 电源管理的需求与可选的OID

对微端口来说，支持电源管理就包括支持电源管理对象标识符（OIDs）。无连接的微端口用它的MiniportQueryInformation函数来处理OIDs，面向连接的微端口则用它的MiniportCoRequest来处理。关于微端口执行查询和设置OIDs的详细描述，参见第五章。

微端口的电源管理支持有两个级别：

n 微端口支持其NIC在电源状态之间的转换，这是电源管理支持的最低级别。对网络设备电源状态的描述，参见6.2节。

n 微端口也支持许多网络唤醒事件。对网络唤醒事件的描述，参见6.3节。

为支持其NIC电源状态间的转换，微端口必须支持以下状态标识符（OIDs）：

n OID_PNP_CAPABILITIES

NDIS通过查询微端口的OID，看它是否返回NDIS_STATUS_SUCCESS，从而判断其电源管理性质，如果微端口返回NDIS_STATUS_NOT_SUPPORTED，NDIS就认为这个微端口是一个早期的不支持电源管理的微端口。

n OID_PNP_QUERY_POWER

这个OID指明了被请求转换NIC的网络设备电源状态。通过返回NDIS_STATUS_ SUCCESS，微端口保证不会危及NIC对网络设备电源状态转换的请求。如果微端口不能作出这样的保证，它就必须返回一个NDIS_STATUS_SUCCESS以外的状态码。

n OID_PNP_SET_POWER

这个OID表明NIC将要转换到指定的网络设备电源状态，微端口必须在返回NDIS_STATUS_SUCCESS之前为NIC特定状态的转换作好准备，微端口必须总是对它的OID返回NDIS_STATUS_SUCCESS。

为支持网络唤醒事件，微端口也必须支持OID_PNP_ENABLE_WAKE_UP，它包含双方的协议，NDIS用这个OID来赋予NIC的网络唤醒能力，详细描述参见6.3.4节。

为支持网络唤醒帧（见6.3.2节），微端口也必须支持下列与唤醒事件有关的OID：

n OID_PNP_ADD_WAKE_UP_PATTERN

n OID_PNP_REMOVE_WAKE_UP_PATTERN

协议使用这个OID来删除一个或多个以前对微端口指定的唤醒模式。

支持网络唤醒的NDIS微端口可选择地支持下列与唤醒事件有关的统计OID：

n OID_PNP_WAKE_UP_ERROR

这个OID查询微端口NIC的失败唤醒信号的数目。

n OID_PNP_WAKE_UP_OK

这个OID查询微端口NIC的有效唤醒信号的数目。

6.2 网络设备电源状态

网络设备电源状态描述了NIC的电源消耗和计算能力的级别。

网络设备电源状态以下列标准术语定义：

n 电源消耗：NIC用了多少电能？多少电能用于NIC和特殊总线的操作（时钟等）？

n 设备环境：NIC在转换到低的电源状态之前保存多少可用的环境？

n 微端口和NIC行为：微端口请求传送包或指示接收包吗？NIC能产生中断吗？

n 恢复时间：将NIC恢复到完全操作状态要花费多少时间？NIC能用它的状态来唤醒系统吗？

有四种网络设备电源状态：D0，D1，D2和D3，D0是完全状态，D1，D2和D3是睡眠状态。状态数与电源消耗相反：较高数的状态使用较少的电能，在D3状态，电源供应可以全部从NIC中去掉。

有电源管理功能的微端口支持所有四种网络设备电源状态。

网络设备电源状态定义为：

D0

电源消耗：设备全力工作以提供全部功能与性能。

设备环境：维护硬件设备环境。

微端口和NIC行为：NIC全部适应于访问网络的请求，NIC或微端口上没有电源管理约束。

恢复时间：不用。

D1

电源消耗：D1是睡眠状态的最高级，电源消耗比D0少，大于或等于D2状态。

设备环境：硬件设备环境也许会丢失。

微端口和NIC行为：微端口并不响应协议的传输请求。因为NDIS可以通知绑定协议转换到睡眠状态，而且对于早期不支持电源管理的协议，微端口禁止这样的协议请求，所以实际上，微端口不接收这种协议请求。

在这种状态下，微端口不指示任何NIC接收的包。

NIC不产生中断，然而微端口必须能产生中断，因为共享的中断会在总线上产生。

恢复时间：NIC恢复到D0状态的时间要少于NIC在D2状态需要的时间。

D2

电源消耗：D2是中间睡眠状态，电源消耗小于D1状态，大于等于D3状态。

设备环境：硬件设备环境也许会丢失。

微端口和NIC行为：微端口并不响应协议的传输请求。因为NDIS可以通知绑定协议转换到睡眠状态，而且对于早期不支持电源管理的协议，微端口禁止这样的协议请求，所以实际上，微端口不接收这种协议请求。

在这种状态下，微端口不指示任何NIC接收的包。

NIC不产生中断，然而微端口必须能产生中断，因为共享的中断会在总线上产生。

恢复时间：NIC恢复到D0状态的时间要大于NIC在D1状态需要的时间而少于NIC在D3状态需要的时间。

D3

电源消耗：D3是最低的睡眠状态，在这一状态下，电源供应可以完全从NIC中除去。

设备环境：硬件设备环境假定已丢失。

微端口和NIC行为：微端口并不响应协议的传输请求。因为NDIS可以通知绑定协议转换到睡眠状态，而且对于早期不支持电源管理的协议，微端口禁止这样的协议请求，所以实际上，微端口不接收这种协议请求。

在这种状态下，微端口不指示任何NIC接收的包。

NIC不产生中断，然而微端口必须能产生中断，因为共享的中断会在总线上产生。

恢复时间：NIC到D0的恢复时间要大于NIC在D2状态需要的时间。

在NIC转换到D3状态之前，微端口必须关闭在其控制之下的所有事情：必须禁止中断，取消时钟，等等。在NIC被总线驱动设置到D3状态之后，微端口将不能访问到NIC硬件。

允许设备电源状态之间的转换：设备电源状态之间唯一允许的转换是从完全状态（D0）到睡眠状态（D1，D2，D3）或者是从睡眠状态到完全状态，NDIS永远不会直接命令NIC直接从一个睡眠状态转换到另一个睡眠状态。

6.3 网络唤醒事件

网络唤醒事件是一个引起NIC唤醒系统的外部事件。NIC发出一个总线特性的唤醒信号，以唤醒系统，最终导致系统从睡眠状态转换到工作状态。

NDIS定义了三种网络唤醒事件：

n NIC网线的重新连接

n 接收到网络唤醒帧，其中包含由绑定协议确定的模式

n 接收到魔包（Magic Packet TM）

在微端口完成初始化之后，NDIS和绑定协议用OID_PNP_CAPABILITIES查询微端口，确定微端口NIC的唤醒能力。NIC也支持任何网络唤醒事件的组合，包括无事件发生。如果微端口对于OID_PNP_CAPABILITIES查询返回NDIS_STATUS_SUCCESS，NDIS判断这个微端口是支持电源管理的，而不管微端口报告的唤醒能力。

依赖于NIC的能力，网络唤醒事件能从任何设备电源状态中产生，包括完全状态D0，由于D0是完全电源状态，信号唤醒事件不唤醒系统而只用作一个运行时事件。

6.3.1 连接改变唤醒

连接改变唤醒事件是在NIC检测到其网线重新连接而发出唤醒信号时产生的，连接唤醒在缺省状态下是启用的，所以协议不必启动这样的唤醒能力。NIC在电缆没连接时不产生唤醒事件，而只是在重新连接时才会产生。

6.3.2 网络唤醒帧

如果一个微端口，在它对OID_PNP_CAPABILITIES查询的响应中，指示它的NIC在接收到包含特定模式的包后产生唤醒信号，绑定协议将为NIC启用网络唤醒帧，它还标明了一个或多个唤醒模式。协议通过在OID_ENABLE_WAKE_UP中设置NDIS_WAKE_UP_ PATTERN_MATCH标志来启用这种类型的唤醒。协议用OID_PNP_ADD_WAKE_UP_ PATTERN标明了一个或多个唤醒模式，并且同时给出了一个表明哪个接收的字节应与这个模式相比较的掩码。依赖于NIC的能力，唤醒模式可以保存在NIC或主机内存中。协议能用OID_PNP_ REMOVE_WAKE_UP_PATTERN移去一个或多个唤醒模式。

关于更多的网络唤醒帧的信息，参见微软的“网络设备类电源管理参考规范”站点：

http://www.microsoft.com/hwdev/onnow.htm

6.3.3 魔包唤醒

魔包是一个包含16个邻近接收网卡的以太网地址的数据包。

6.3.4 启用唤醒事件

用OID_PNP_CAPABILITIES查询了微端口的唤醒能力之后，协议驱动程序将发送OID_PNP_ENABLE_WAKE_UP以启用一个或多个网卡的唤醒能力。NDIS并不立即由协议启用OID_PNP_ENABLE_WAKE_UP标明的唤醒能力，而是跟踪由协议启用的唤醒能力，只是在微端口转换到睡眠状态之后，给微端口发送OID_PNP_WAKE_UP_ENABLE来启用适当的唤醒事件。

在缺省情况下，如果NIC支持这样的唤醒事件，启用的是连接变化（电缆重连）的唤醒。协议不能通过在OID_PNP_ENABLE_WAKE_UP设置或清除NDIS_PNP_WAKE_UP_ LINK_CHANGE标志来启用或禁止连接改变唤醒，然而，NDIS可以设置或清除这个标志来启用或禁止连接改变唤醒，正如下面所描述的那样。

微端口转换到一个低能耗状态之后（即NDIS送一个OID_PNP_SET_POWER给微端口之后），NDIS送一个OID_PNP_ENABLE_WAKE_UP给微端口以启用适当的唤醒能力，有三种基本的方案：

n 如果系统从完全工作状态转换到睡眠状态，NDIS禁止连接改变唤醒；如果协议启用魔包唤醒和/或模式匹配唤醒，NDIS也将启用魔包唤醒和/或模式匹配唤醒。这种启用/禁止唤醒事件的配置防止了连接改变在系统处于睡眠状态时不适当地唤醒系统，但允许NIC作为响应魔包或模式匹配唤醒系统。

n 如果NIC从完全工作状态因连接改变（有人从NIC上断开了电缆）转换到睡眠状态，NDIS启用连接改变唤醒，NDIS同时也禁止魔包和模式匹配唤醒。在这种情况下，NIC的网络电缆连接导致NIC产生一个唤醒信号。

n 如果因为有人断开电缆，系统转换到低能耗状态而使得NIC能耗降低，NDIS将允许连接改变唤醒，禁止魔包和模式匹配唤醒。在这种情况下，NIC电缆的重新连接把NIC和系统两个都唤醒了。

NDIS永远不会为NIC启用所有三种唤醒事件，NDIS或者允许连接改变唤醒，禁止魔包和模式匹配唤醒，或者禁止连接改变唤醒而允许其他由协议允许的唤醒能力（魔包唤醒和/或模式匹配唤醒）。

6.3.5 处理唤醒事件

微端口并不处理由它的NIC检测到的唤醒事件。当NIC检测到一个唤醒事件之后，它声明一个特殊总线信号以指示总线驱动程序，总线驱动程序再指示NDIS，由它（NDIS）来指示NIC系统的电源管理系统，电源管理系统发送一个电源IRP给NDIS，NDIS然后给微端口发送一个OID_PNP_SET_POWER，使其转入工作状态。

6.4 处理OID_PNP_QUERY_POWER

OID_PNP_QUERY_POWER请求微端口指明：在接受到OID_PNP_SET_POWER之后，它是否能保证其NIC转换到指定的睡眠状态。通过返回NDIS_STATUS_SUCCESS，微端口承诺它将不做会危及到NIC转换到指定设备状态的任何事。

在OID_PNP_QUERY_POWER之后总是跟随着OID_PNP_SET_POWER。OID_PNP_SET_POWER也许是立即跟随OID_PNP_QUERY_POWER请求或者在OID_PNP_QUERY_ POWER请求后的一段不确定的时间间隔内到达。OID_PNP_SET_POWER请求有效的特定D0设备状态会取消前面的OID_PNP_QUERY_POWER请求。

6.5 处理OID_PNP_SET_POWER

NDIS发送一个OID_PNP_SET_POWER，向微端口指示其NIC从工作状态转换到睡眠状态或着从睡眠状态转换到工作状态。

6.5.1 转入睡眠状态

在两种情况下NDIS会送一个OID_PNP_SET_POWER给微端口，使其转入指定的睡眠状态：

n 系统转入睡眠状态

n NIC处于D0状态时，其网线断开20秒以上

在第一种情况下，OID_PNP_SET_POWER优先于OID_PNP_QUERY_POWER。在第二种情况下，OID_PNP_SET_POWER并不优先于OID_PNP_QUERY_POWER。实际上，第二种情况，只是NDIS将NIC置于不接收系统电源IRP的睡眠状态的情形。

送出OID_PNP_SET_POWER给微端口后，如果微端口支持任何唤醒事件，NDIS将再给微端口送一个OID_PNP_ENABLE_WAKE_UP请求（见6.3.4节）。

微端口必须成功地执行OID_PNP_SET_POWER。

当支持连接改变唤醒事件的NIC处于D0状态，而且其网线断开时，微端口会检测到状态的变化，用OID_STATUS_MEDIA_DISCONNECT调用Ndis(Co)IndicateStatus来报告给NDIS。如果微端口在20秒内没有用OID_STATUS_MEDIA_CONNECT调用Ndis(Co)-IndicateStatus，说明电缆已被重新连接，NDIS将送一个OID_PNP_SET_POWER给微端口，NDIS将把NIC从能产生连接改变唤醒事件状态设置为最低能耗状态。

在OID_PNP_SET_POWER请求返回NDIS_STATUS_SUCCESS之前，微端口必须执行必要的设备相关操作，来为NIC转入睡眠状态作准备，微端口也必须保存所有的以前用OID_GEN_CURRENT_PACKET_FILTER设置的包过滤器。如果其NIC转换到D3状态，微端口必须关闭其控制下的所有东西：中断必须禁止，时钟必须取消，等等。微端口在NIC被总线驱动程序设置到D3状态后不能访问NIC硬件。

6.5.2 转入工作状态

NDIS将NIC从睡眠状态（D1，D2和D3）转换到工作状态（D0）有两种情况：

n 系统从睡眠状态转换到工作状态（D0）

n NIC产生一个唤醒事件

NDIS给微端口送一个OID_PNP_SET_POWER来启动向工作状态D0的转换，然后微端口执行必须的设备相关操作以将NIC恢复到工作状态，对这个OID请求返回NDIS_STATUS_SUCCESS时，微端口及其NIC必须已经准备好了常规操作。

如果在其NIC转入睡眠状态以前，已经为微端口设置了任何一种过滤器，那么此时微端口也必须恢复这些过滤器。对支持电源管理的微端口，NDIS不恢复这样的过滤器。

6.6 早期微端口的电源管理

如果满足以下条件，NDIS将判断这个微端口是不支持电源管理的早期微端口：

n 在初始化期间，总线驱动程序指出微端口的NIC或微端口不支持电源管理

n 微端口对OID_PNP_CAPABILITES查询返回NDIS_STATUS_UNSUPPORTED

早期微端口只支持完全工作（D0）和完全关闭（D3）网络设备电源状态。

初始化期间，早期的微端口可以指示NDIS在系统转换到睡眠状态（D3）之前不能停止它。微端口是通过在传送给NdisMSetAttributesEx的AttributeFlags参数中设置NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志而作出这样的指示的。早期的微端口将只设置这个参数，如果：

n 它保存了所有需要的硬件环境

n 它能为睡眠状态（D3）而将其NIC置于一个适当的状态

n 它能将NIC重新激活至工作状态（D0）

在微端口不设置NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志时，NDIS能从总线驱动程序确定NIC（网卡）不支持电源管理，NDIS将不查询微端口的电源管理能力。然而，如果微端口设置了NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志，NDIS将对微端口发出一个OID_PNP_CAPABILITES请求。在这种情况下，微端口将用NDIS_STATUS_SUCCESS表示完成OID_PNP_CAPABILITES请求，在微端口返回的对应于这种请求的NDIS_PM_WAKE_UP_CAPABILITIES结构中，微端口必须为每一种唤醒能力指定NdisDeviceStateUnspecified的设备电源状态。

NDIS为早期微端口提供下列电源管理支持：

n NDIS完成所有（系统电源管理器送至代表NIC的设备对象）的IRP_MN_QUERY_POWER请求。此时，NDIS保证微端口的NIC转换到D3状态以响应来自于系统的查询电源请求。

n 如果微端口在初始化期间没有设置NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志，NDIS将在微端口NIC转换到D3状态之前调用微端口的MiniportHalt函数，微端口NIC将失去所有的硬件环境。

n 如果微端口在初始化期间设置了NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND标志，NDIS将在系统转换到D3状态之前不停止微端口，而是给微端口发出一个OID_PNP_SET_POWER到D3状态的请求。微端口必须保存所有它使用的硬件环境，并且将其NIC置于一个适宜于D3状态的状态上。

n 系统转入D0状态之前，如果要停止微端口，NDIS调用微端口的MiniportInitialize函数。如果NDIS不停止微端口，它就给微端口会发出一个到D0状态的OID_PNP_SET_POWER请求，微端口必须将其NIC置于一个适宜于D0状态的状态上。

n 如果在转入睡眠状态之前，已经为微端口设置了包过滤器，NDIS将通过给微端口发送一个OID_GEN_CURRENT_PACKET_FILTER来恢复这些过滤器。


重置，停止和关闭

本章描述重置，停止和关闭函数。

7.1 硬件重置（Reset）

微端口必须用NdisMRegisterMiniport注册一个MiniportReset函数。MiniportReset的定义为：

NDIS_STATUS

MiniportReset(

OUT PBOOLEAN AddressingReset，

IN NDIS_HANDLE MiniportAdapterContext

);

如果NDIS确定微端口的网络接口卡（NIC）因下列原因而被挂起，MiniportReset就会被调用：

n 微端口的MiniportcheckForHang函数返回TRUE

n NDIS检测到一个未决的发送包（只对串行微端口）

n NDIS检测到一个在指定的超时期间内未完成的未决请求

MiniportReset也可以用调用NdisMResetComplete来同步或异步地完成（见图7.1）。

图7.1 网络接口卡的重置

MiniportReset将：

n 禁止将要到来的中断。

n 清除进程中与任何发送有关的包，例如，对于总线控制器的直接内存访问（DMA）设备，发送缓冲区的指针必须被清除。串行微端口不顾其发送的状态，而放弃所拥有的未决包，对这样一个微端口，NDIS以一个适当的状态完成一次未决的（给上层的）发送。非串行和面向连接的微端口必须为任何发送包的队列返回NDIS_STATUS_REQUEST_ABORTED。

n 恢复硬件状态和微端口内部状态至与广播地址和注册过滤器有关的初始状态。微端口可以这样做，或者依赖于NDIS。

如果微端口在AddressReset参数中返回FALSE，则它将其地址值返回到其初始状态。如果微端口在AddressReset中返回TRUE，NDIS将调用无连接微端口的MiniportSetInformation函数或者面向连接的微端口的MiniportCoRequest函数，以对其初始状态设置地址和过滤器。注意，驱动程序的编写者应当确定微端口是否应保存这个地址状态，以至于在重置时能恢复到此状态，或者在重置时微端口是否依赖于NDIS以恢复其地址。

7.2 停止（Halt）处理程序

微端口必须对NdisMRegisterMiniport应用MiniportHalt函数。MiniportHalt的定义为：

VOID

MiniportHalt(

IN NDIS_HANDLE MiniportAdapterContext

);

在下列情况下，MiniportHalt会被调用：

n 建立广播地址或设置MAC_OPTIONS的请求失败之后

n 所有绑定（bind）到微端口的协议解除之后

n 卸载操作期间，比如一个NetStop命令导致一个卸载操作执行

n 系统关闭（shutdown）期间

MiniportHalt将撤销MiniportInitialize所作的任何事情，包括：

n 释放口

n 释放它所声明的所有硬件资源

n 调用NdisMDeregisterInterrupt释放中断资源

n 释放它所分配的任何内存

n 停止NIC，除非MiniportShutdown函数已将NIC恢复到其初始状态。

MiniportHalt应当在返回以前完成这些必须的卸载驱动程序的操作。图7.2显示了MiniportHalt所做的调用的集合，这些调用只是所能进行的调用的一部分。实际的调用集合依赖于先前微端口的行为。微端口如果因硬件问题，或不能得到所需的资源而不能成功地初始化网络适配卡时，也会在MiniportInitialize中作相同的调用。在这种情况下，MiniportInitialize将会通过取消以前的操作来卸载驱动程序，否则，MiniportHalt将取消MiniportInitialize的动作。

图7.2 卸载微端口驱动程序

下面列出了请求撤消微端口所做动作的调用：

n 如果微端口注册了一个中断，它应调用NdisMDeregisterInterrupt

n 如果微端口NIC驱动程序注册了关闭处理器（shutdown handler），它应调用NdisMDeregisterAdapterShutdownHandler。

n 如果微端口建立了一个或多个时钟，它应为它所创建的每一个时钟调用NdisMCancelTimer。

n 如果微端口用NdisAllocateMemory、NdisMAllocateShareMemory或NdisM-AllocateShareMemoryAsync分配了任何内存，它将调用NdisFreeMemory来释放这些内存。

n 如果微端口用NdisAllocatePacketPool分配并初始化包描述符池的存储，它将调用NdisFreePacketPool来释放这个存储。

n 如果微端口分配或保存了任何硬件资源，它们将调用NdisMDeregisterIoPortRange；如果NIC是一个总线控制的DMA NIC，则调用NdisMFree MapRegister。

7.3 关闭（Shutdown）处理程序

微端口驱动程序应当在初始化期间注册关闭处理程序。MiniportShutdown处理程序的定义为：

VOID

MiniportShutdown(

IN PVOID ShutdownContext

);

驱动程序的关闭处理程序应当只：

n 在从关闭处理程序返回之后，确保硬件不再试图从内存中拷贝任何数据。也就是说，如果进程中存在直接内存访问（DMA）操作，这个操作应当被终止。

n 重置NIC硬件到其初始状态。

关闭处理程序的调用可以作为用户操作的结果，在这种情况下，它运行于IRQL PASSIVE_LEVEL下。它也可以因为不可重获的系统错误而被调用，如果这样的话，MiniportShutdown能运行于任何IRQL之下。MiniportShutdown将不调用Ndis Xxx函数。

不象MiniportHalt，MiniportShutdown将不释放任何分配的资源。MiniportShutdown只停止NIC。


广域网微端口 NIC驱动程序

本章将介绍在标准和面向连接的NDIS（CoNDIS）环境中支持广域网 NIC微端口驱动程序所需的NDIS结构和函数。同时将详细描述如何实现标准和CoNDIS 广域网微端口驱动程序。

Microsoft® Window® 2000 同时支持标准和CoNDIS 广域网微端口驱动程序的实现，但是我们鼓励驱动程序开发者编写CoNDIS 广域网微端口驱动程序。

标准广域网微端口驱动程序：

n 大部分在非广域网无连接的微端口驱动程序中的NDIS函数仍被使用。

n 部分在非广域网无连接的微端口驱动程序中的NDIS函数被替换。

n 提供附加的广域网功能。

CoNDIS广域网微端口驱动程序：

n 提供面向连接的微端口驱动程序提供的所有函数。

n 使用面向连接的微端口驱动程序所使用的NDIS函数。

n 提供附加的广域网功能。

标准和CoNDIS环境下的广域网NIC微端口驱动程序开发时需遵循第二，三，四章描述的要求和功能，除此之外还遵循本章的要求。

远程访问服务（Remote Access Servise , RAS）使远程工作站能够拨入局域网并且透明的访问局域网上的资源。

以下节描述在标准和面向连接的环境下，广域网卡的驱动程序如何通信。同时描述了如何实现两种环境下的广域网NIC微端口驱动程序及包的组帧：

n 8.1 RAS 体系结构

n 8.2 NDISWAN 概述

n 8.3 网卡、绑定、和连接

n 8.4 广域网微端口驱动程序的实现

n 8.5 广域网包组帧

n 8.6 标准NDIS上的电话服务扩展

n 8.7 使用支持电话服务的CoNDIS扩展

8.1 RAS 体系结构

本节说明远程访问服务（RAS）各部件之间的联系，以及广域网卡（如：ISDN, X.25, Switched 56 适配器等）的驱动程序如何使用NDIS和NDISWAN服务在标准广域网环境和面向连接的广域网环境中进行通信。

图8.1说明了RAS体系结构。后面的章节中将描述NDISWAN 中间层NDIS驱动程序、NDISTAPI驱动程序、及NDPROXY驱动程序，并且给出了整个RAS系统更详细的描述。

图8.1中所示的WAN、RAS、和TAPI组件将在下文中讨论。

图8.1 RAS体系结构

RAS函数

RAS函数集允许用户模式的应用程序建立RAS连接。当RAS连接建立后，应用程序就可以使用标准网络接口（如：Windows Socket、NetBIOS、Named Pipes、和RPC等）连接到网络服务。

传输

RAS系统部件提供传输服务如PPP验证（PAP，CHAP），网络配置协议（IPCP，IPXCP，NBFCP，LCP等）。广域网微端口驱动程序仅实现PPP 介质相关组帧。.

TAPI服务

TAPI服务（tapisrv.exe）将不同服务提供者的电话服务提供者接口（TSPI）提供给使用TAPI的应用程序。应用程序使用特定的服务提供者与特定类型的设备通信。这些服务提供者是在TAPI服务进程中运行的DLLs。无论是标准的还是CoNDIS 广域网微端口驱动程序，都可以使用操作系统提供的服务提供者与用户模式驱动程序通信。

KMDDSP

本部件是一个在TAPI服务进程中运行的服务提供者DLL。kmddsp.tsp部件为TAPI应用程序提供了一个TSPI接口，以便NDISTAPI能与用户模式应用程序通信。本部件为NDISTAPI将用户模式的请求转换为相关的TAPI OIDs.

NDISTAPI

本部件实现 TAPI接口的内核模式部分。ndistapi.sys部件通过NdisRquest函数将TAPI专用的OID请求传递给适当的标准广域网微端口驱动程序，与广域网微端口进行通信。

NDPTSP

本部件是一个在TAPI服务进程中运行的服务提供者DLL。ndptsp.tsp为TAPI应用程序提供了一个TSPI接口，使NDPROXY能与用户模式应用程序通信，本部件为NDPROXY将用户模式的请求转换为相关的TAPI-CO-related OIDs。

NDPROXY

在产生和接受呼叫时本部件将TAPI参数封装到NDIS结构中。ndproxy.sys部件通过NDPTSR的TSPI接口与TAPI通信；通过NDIS与NDISWAN和CoNDIS广域网NIC微端口驱动程序进行通信。本部件为微端口驱动程序提供了一个客户接口，为NDISWAN提供呼叫管理器接口。NDISWAN为本部件提供客户接口。微端口驱动程序为本部件提供呼叫管理器接口。本部件使用TAPI-CO-related OIDs调用NdisCoRequest函数列举CoNDIS 广域网 微端口驱动程序的TAPI性能。同时，本部件注册特定的TAPI地址族、创建虚连接（VC）、产生和接受呼叫、并激活虚连接以便在虚连接上发送和接收数据。

NDISWAN

NDISWAN中间层NDIS驱动程序支持PPP协议/链路组帧、压缩、和加密。NDISWAN支持标准和面向连接的微端口驱动程序。ndiswan.sys通过两个接口与标准广域网NIC驱动程序通信：

1. NDIS广域网接口。

2. NDIS 微端口NIC驱动程序接口。

ndiswan.sys 通过面向连接的微端口NIC驱动程序接口与CoNDIS 广域网微端口驱动程序通信。

串口驱动程序

本部件是内部串行端口或多端口串行卡的标准设备驱动程序。Windows® 2000内置的异步广域网微端口驱动程序使用内部串口驱动程序进行调制解调器通信。输出与串口驱动程序相同的函数的任何驱动程序都与内置的异步WAN微端口驱动程序共同工作。

X.25厂商可以选择为X..25卡实现串口驱动程序仿真程序。在这种情况下，每个X.25卡上的虚连接都表现为一个串口（有一个X.25 PAD连在上面）。连接接口必须能正确仿真串行信号如：DTR、DCD、RTS和DSR。

选择为X..25卡实现串口驱动程序仿真程序的厂商必须在pad.inf为PAD中提供入口。这个文件里包含通过X.25 PAD建立连接所需的命令/响应脚本。pad.inf的详细信息请参见Remote Access Service Administrator’s Guide 。

广域网微端口NIC驱动程序

视情况而定，ISDN、Switched 56和X.25厂商可以选择编写标准的或CoNDIS 广域网微端口驱动程序。

8.2 NDISWAN 概述

NDISWAN是一个中间层NDIS驱动程序，它可以运行在标准的和面向连接的环境中。NDISWAN为上层协议驱动程序同时提供标准和CoNDIS 802.3微端口驱动程序接口。NDISWAN同时为下层管理广域网卡的广域网微端口驱动程序提供协议驱动程序接口。对于CoNDIS，这个广域网微端口驱动程序可以是一个集成的微端口呼叫管理器（MCM）。

NDISWAN将外出包从局域网格式转换为PPP格式。NDISWAN使用简单HDLC组帧。大多数介质相关的组帧必须在微端口驱动程序中完成。NDISWAN从微端口驱动程序对OID_WAN_MEDIUM_SUBTYPE查询请求的响应来获得组帧的信息。

广域网微端口驱动程序可以在广域网上收发包之前必须要建立连接。建立连接的方法依赖于如下所描述的广域网微端口驱动程序是在标准还是CoNDIS环境下工作：

n 在标准环境中，应用程序必须建立一个由发送节点发起的连接，或通过产生或接受呼叫接受远程节点发起的连接。连接的建立、管理、拆卸是通过TAPI完成的。TAPI请求和对TAPI的状态指示都通过NDISWAN。在标准广域网微端口驱动程序上使用TAPI服务的详细叙述见8.6节。

n 在CoNDIS环境中，须建立虚连接（VC）。NDPROXY为应用程序发起的外出呼叫创建虚连接。类似的，MCM为内入呼叫指示NDISWAN和NDPROXY建立虚连接。MCM必须通知远程团体虚连接参数，有时需与远程团体协商虚连接参数。连接的建立、管理、拆卸是通过TAPI完成的。TAPI请求和对TAPI的状态指示都通过NDISWAN。NDPROXY实现的TAPI服务详见8.7节。

在应用程序与远程节点建立连接后，就可以在广域网上交换数据包了。包的交换方式依赖于广域网微端口驱动程序是在标准还是在CoNDIS环境下运行：

n 在标准环境下，典型的从协议驱动程序到标准广域网微端口驱动程序的包传送路径是：协议驱动程序以包描述信息为参数调用NdisSend；NDISWAN检查包描述信息头以决定包将被送往哪个链接；NDISWAN将一般不连续的数据包拷贝到连续的缓冲区中，此缓冲区带有标准广域网微端口驱动程序在初始化和注册时指定的报头和报尾填充。

n 在CoNDIS环境下，在协议驱动程序以一个VC句柄和一个包描述信息指针数组为参数调用NdisCoSendPackets函数之后，包通过VC从协议驱动程序传送到CoNDIS广域网微端口驱动程序，。NDISWAN为指定VC接收包数组。

NDISWAN接收到的包有几个可能的配置选项：报头压缩、数据压缩、和加密。这些配置选项是以一定顺序加到包上的。在接收方以相反的顺序解压和解密。任何时候启用这些如压缩、加密等的配置选项时，须通知特定的广域网微端口驱动程序。

在将包发送给广域网微端口驱动程序的发送函数以前，NDISWAN进行简单PPP HDLC组帧。简单PPP HDLC组帧是指没有帧检验序列（FCS）、位/字填充、起始/结束标记的PPP HDLC组帧。有关介质相关组帧的内容详见8.5节广域网包组帧。最后，NDISWAN发送包。然后NDIS调用广域网微端口驱动程序的发送函数。NDIS根据广域网微端口驱动程序的类型（标准或CoNDIS）调用不同的函数：

n 如果是标准广域网微端口驱动程序，发送函数是MiniportWanSend ，而不是其他局域网微端口NIC驱动程序所实现的类似的函数。

n 如果是CoNDIS广域网微端口驱动程序，发送函数是 MiniportCoSendPackets ，所有面向连接的微端口驱动程序都实现这个发送函数。

广域网微端口驱动程序收到包后，它应在活动连接上发送包。对于标准环境，这个连接是指已经建立的链路。对于CoNDIS环境，这个连接是虚连接（VC）。广域网微端口驱动程序在每个连接上可以处理的未完成包的个数是由微端口驱动程序指定给协议驱动程序的。广域网微端口驱动程序指定这个数值的方法取决于它是标准或CoNDIS类型的：

n 标准广域网微端口驱动程序在NDIS_WAN_INFO结构的MaxTransmit成员中指定每个数据信道可以拥有的未完成包的缺省个数。微端口驱动程序用此结构响应协议驱动程序的OID_WAN_GET_INFO请求。然而，微端口驱动程序可以动态管理发送窗口，通过以line-up指示为每个线路调用NdisMIndicateStatus调整发送窗口。在这个调用中，微端口驱动程序为传递给函数的NDIS_MAC_LINE_UP结构的成员SendWindows 提供一个新的非零值。如果微端口驱动程序将SendWindows 置为零，NDISWAN将为此线路使用MaxTransmit 中保留的缺省值。

标准广域网微端口驱动程序可能要修改或添加包的报头和报尾（如添加FCS）。微端口驱动程序可以预先对包头和包尾进行适当填充。为了在广域网介质上发送数据，广域网微端口驱动程序可以用任何适当的方式改变包中的数据。

n CoNDIS广域网微端口驱动程序在NDIS_WAN_CO_INFO结构的成员MaxSendWindow中指定每个VC可以拥有的未完成包个数。微端口驱动程序提供此结构来响应协议驱动程序的OID_WAN_CO_GET_INFO请求。然而，微端口驱动程序可以动态调整此数字，以及为每个VC使用由微端口传输到NdisMCoIndiateStatues函数WAN_CO_LINKPARAMS结构的SendWindow成员动态调整这个数字。NDISWAN使用当前的SendWindow 值作为对未完成发送数的限制。微端口驱动程序通过将SendWindow 置为0来表示它不能处理任何未完成的包，也就是说，如果SendWindow成员设置为“0”，发送窗口将关闭。

注意，如果CoNDIS广域网微端口驱动程序将SendWindow 置零，它的关闭发送窗口将被关闭。如果标准广域网微端口驱动程序将SendWindow 置零，它的发送窗口被设置为缺省值。

8.3 网络卡、绑定、和连接

广域网微端口驱动程序在每个绑定上可管理多个连接，因此每个NIC可有多个连接。对于标准环境，这些连接是已经建立的链路；对于CoNDIS环境，这些连接是虚连接（VC）。

与其他微端口驱动程序一样，每个广域网微端口驱动程序至少应有一个NIC，为每个它分配并保留一个NIC专用的环境区域。此环境区域仅仅用于存储、检索和使用NIC硬件细节，如中断、总线类型、I/O范围、和存储器，以及维护运行时状态。微端口NIC驱动程序应为系统中它所支持的每个网卡指定一个NIC专用的环境区域。

从广域网微端口驱动程序的观点看，每个NIC只有一个绑定，尽管在微端口驱动程序之上有许多协议被绑定到某个NIC上。因为这些协议对广域网微端口驱动程序是未知的，微端口驱动程序不保留对NIC的绑定。NDISWAN处理微端口驱动程序的绑定以区分不同的协议。实际上，NDISWAN绑定到一个或多个广域网微端口驱动程序上，一个或多个协议驱动程序绑定到NDISWAN上。

标准和CoNDIS广域网微端口驱动程序在NIC上使用不同类型的的连接：

§ 标准广域网微端口驱动程序使用的连接是链路。链路是逻辑的、点对点的、双向的通信信道。每个NIC可以有多个链路。链路可以动态建立和拆除。每个连接的链路的速度和质量可以不同。然而，一个NIC上所有链路的填充和链接能力是通用的：如果一个标准广域网微端口驱动程序指定了20 byte的头填充和4 byte 的尾填充，那么这个填充对微端口驱动程序的NIC上所有的链路都保持不变。

§ CoNDIS广域网微端口驱动程序与所有面向连接的微端口驱动程序相同，使用的是虚连接（VC）。每个NIC上可以有多个虚连接。每个虚连接的速度可以不同。但是同一NIC上所有虚连接的性能是相同的。如果一个CoNDIS广域网微端口驱动程序为它可以接收和发送的任何一个包指定了最大帧长度，则这个最大帧长度对NIC上的所有的VC都保持不变。

如果某个广域网微端口驱动程序指出它不需要PPP地址和控制字段压缩，则这对NIC上所有的连接都是有效的。

图8.2 广域网微端口驱动程序的绑定

每个协议只绑定一次NDISWAN驱动程序

尽管X.25微端口只有一个绑定，NDISWAN可以将以包传递给三个协议中的任何一个

ISDN微端口有两个适配器，因此有两个绑定

协议只绑定到NDISWAN上一次，且不绑定到广域网微端口驱动程序上。这种绑定节省了内存并简化了广域网微端口驱动程序。因为在一个典型的系统中可能有多个协议和多个广域网微端口驱动程序，减少绑定数目可以节省内存。也就是说不必每个协议和驱动程序之间都有绑定。在上图中，如果每个协议都绑定到驱动程序上，会有9个绑定；如果每个协议只有一个广域网绑定，则这些协议可能被简化。

一个广域网微端口驱动程序一定是一个NDIS微端口NIC驱动程序。完全的NIC驱动程序不支持广域网NIC。本章和第二部分有关微端口NIC驱动程序的章节给出了编写广域网微端口驱动程序所需的设计信息。

在源程序中加入以下行将驱动程序标识为微端口驱动程序：

C_DEFINES=/DNDIS_MINIPORT_DRIVER

支持通过TAPI连接的标准广域网微端口驱动程序，必须在源程序中加入以下行，指定所支持的TAPI版本号:

C_DEFINES=-DNDIS_TAPI_CURRENT_VERSION=0X00010003

是集成微端口呼叫管理器（MCM）驱动程序和支持CoNDIS地址族类型CO_ADDRESS_FAMILY_TAPI_PROXY的CoNDIS广域网微端口驱动程序，必须在源程序中加入下列行以指定所支持的TAPI版本号：

C_DEFINES=-DNDIS_TAPI_CURRENT_VERSION=0x00030000

广域网微端口驱动程序的包含路径须包含ndiswan.h和ndis.h。

如果广域网微端口驱动程序支持通过TAPI的连接，则还要包含ndistapi.h。

8.4 广域网微端口驱动程序的实现

为了支持像ISDN、帧中继、Switch 56等带远程访问服务（RAS）的介质，必须要提供一个广域网微端口驱动程序。微端口驱动程序应依据NDIS的当前版本（现为5.0版）编写。对于标准广域网微端口驱动程序，现存的根据NDIS 3.0 和 NDIS 4.0 编写的驱动程序仍然可以使用。

标准广域网微端口驱动程序调用少数NDIS函数的广域网变种。标准广域网微端口驱动程序调用的部分NDIS函数与局域网微端口驱动程序调用的相同，部分局域网微端口驱动程序的NDIS调用被广域网专用的NDIS调用替换。另外，标准广域网微端口驱动程序提供了广域网专用的发送函数，它的定义与局域网微端口驱动程序实现的MiniportSend函数并不相同。本节后面将介绍这些广域网专用的调用和函数。

CoNDIS广域网微端口驱动程序与所有面向连接的微端口驱动程序调用相同的NDIS函数集，它们提供的函数集也相同。

广域网微端口驱动程序通过NDIS库与NDISWAN 中间层NDIS驱动程序通信。由于NDISWAN可以提供公用服务，如：数据压缩、加密、回送、简单PPP组帧等，因此广域网微端口驱动程序只需要与介质相关的特征（例如：ISDN需要Q931信令）。

广域网微端口驱动程序是这样的NIC微端口驱动程序：

§ 执行从NDISWAN 中间层NDIS驱动程序到广域网微端口驱动程序的连接管理操作。

§ 接受发送请求，向NDISWAN驱动程序指示接受到的数据。

下面章节定义了NDISWAN驱动程序与广域网微端口驱动程序之间的接口，并详述了实现微端口驱动程序的细节。除了本章内容，广域网微端口驱动程序的开发者还需要参考第二部分有关局域网微端口驱动程序的章节，并参考online DDK内的Network Drivers Reference获得大多数关于NDIS函数的细节。

§ 8.4.1 标准广域网微端口驱动程序与局域网微端口驱动程序的区别

§ 8.4.2 CoNDIS广域网微端口驱动程序的附加特性

§ 8.4.3 广域网微端口驱动程序提供的服务

§ 8.4.4 广域网微端口驱动程序做出的指示

8.4.1 标准广域网微端口驱动程序与局域网微端口驱动程序的区别

在前面几章我们已经描述了标准广域网微端口驱动程序与局域网微端口驱动程序的一些区别，这些区别将影响标准广域网微端口驱动程序的实现。

§ 标准广域网微端口驱动程序被禁止用NdisMRegisterMiniport注册MiniportTransferData处理程序。它总是将整个包传递给NdisMWanIndicateReceive函数。NdisMWanIndicateReceive函数返回后，包已经被复制，此时广域网微端口驱动程序可以重用分配的包资源。

§ 标准广域网微端口驱动程序提供函数MiniportWanSend代替MiniportSend函数。MiniportWanSend函数接受NDIS_WAN_PACKET类型（不是NDIS_PACKET类型）的描述信息和一个指定包将要在上发送的数据信道的附加参数。

§ 标准广域网微端口驱动程序从不返回NDIS_STATUS_RESOURCES作为MiniportWanSend或任何其他MiniportXxx函数的执行情况，且不能调用NdisMSendResourcesAvaliable函数。

§ 标准广域网微端口驱动程序支持一组广域网专用的 OID，这些OID可以用来设置和查询操作特性。

§ 标准广域网微端口驱动程序支持一组传递给NdisMIndicateStatus的广域网专用的状态指示。这些状态指示报告链接状态的变化。

§ 有两个广域网专用的NDIS函数可以被标准广域网微端口驱动程序调用，以完成用于发送和接收的广域网专用NDIS 调用。这两个完成函数是：

NdisMWanIndicateReceiveComplete

NdisMWanSendComplete

§ 标准广域网微端口驱动程序用NDIS_WAN_PACKET而不是NDIS_PACKET类型的 描述信息。

§ 标准广域网微端口驱动程序保留一组广域网专用的统计数据。

§ 标准广域网微端口驱动程序从不回送，回送总是由NDISWAN提供。

8.4.2 CoNDIS广域网微端口驱动程序的附加特性

CoNDIS广域网微端口驱动程序必须实现以下附加特性：

§ CoNDIS广域网微端口驱动程序必须支持一组用来设置和查询操作特性的CoNDIS广域网专用OID。

§ CoNDIS广域网微端口驱动程序支持一组传递给NdisMCoIndicateStatus 的CoNDIS广域网专用的状态指示。这些状态指示报告虚连接（VC）状态的变化

§ CoNDIS广域网微端口驱动程序应为驱动程序管理的NIC上的每个虚连接保留一组统计数据。

§ CoNDIS广域网微端口驱动程序使用NDIS_PACKET类型的描述符，而不是标准广域网微端口驱动程序使用的NDIS_WAN_PACKET类型的描述符。

§ CoNDIS广域网微端口驱动程序从不试图回送任何包；NDISWAN提供回送支持。

8.4.3 广域网微端口驱动程序提供的服务

标准和CoNDIS广域网微端口驱动程序对NDISWAN驱动程序提供不同的接口。标准广域网微端口驱动程序提供的接口，除发送请求外，与局域网微端口驱动程序的相同。CoNDIS广域网微端口驱动程序提供的接口与面向连接的微端口驱动程序相同。广域网微端口驱动程序接收并响应一组广域网专用 OID。对于标准广域网微端口驱动程序，用来查询和设置的OID被分别传递给MiniportQueryInformation和MiniportSetInformation函数。对于CoNDIS广域网微端口驱动程序，用来查询和设置的OID被传递给函数MiniportCoRequest 。广域网微端口驱动程序在注册时将自己标识为广域网微端口驱动程序。

以下章节描述了广域网微端口驱动程序如何标识它自己，如何处理发送操作，以及用于查询和设置标准广域网微端口驱动程序、CoNDIS广域网微端口驱动程序、和广域网NIC 特性的OID。

§ 8.4.3.1 注册为广域网微端口驱动程序

§ 8.4.3.2 查询广域网小段口驱动程序的信息

§ 8.4.3.3 设置广域网小段口驱动程序的状态

§ 8.4.3.4 在广域网微端口驱动程序上发送数据

8.4.3.1 注册为广域网微端口驱动程序

广域网微端口驱动程序在注册时可以将自己标识为标准或面向连接的微端口驱动程序。微端口驱动程序在它的函数DriverEntry内调用函数NdisMRigisterMiniport 向NDIS注册。在此调用中，标准广域网微端口驱动程序通过在NDISXX_MINIPORT_CHARACTERISTICS结构的成员Reserved中设置标志NDIS_USE_WAN_WRAPPER来指出它需要广域网服务。NDISXX_MINIPORT_CHARACTERISTICS结构的成员还指定了适当的NDIS版本号。NDIS 3.0、4.0和5.0支持标准广域网微端口驱动程序。只有NDIS 5.0支持CoNDIS广域网微端口驱动程序。在此调用中，微端口驱动程序还为驱动程序提供的MiniportXxx函数指定了入口点。

为了从NDIS接收适当的NDISXX_MINIPORT_CHARACTERISTIC结构，广域网微端口驱动程序还必须在源代码开始部分嵌入适当的编译器指令，或在源程序文件中设置适当的编译指令。这些指令取决于广域网微端口驱动程序运行的环境，是标准的还是CoNDIS环境：

§ 对于标准广域网微端口驱动程序，将以下编译器指令被放在源代码开始部分表示微端口驱动程序使用了NDIS_MINIPORT_CHARACTERISTICS结构的4.0版：

#define NDIS_MINIPORT_DRIVER

#define NDIS40_MINIPORT 1

#include <ndis.h>

如果NDIS40_MINIPORT指令被忽略了，标准广域网微端口驱动程序将使用3.0版的NDIS_MINIPORT_CHARACTERISTICS结构。

§ 对于CoNDIS广域网微端口驱动程序，将以下编译器指令被放在源代码开始部分表示微端口驱动程序使用了NDIS_MINIPORT_CHARACTERISTICS结构的5.0版：

#define NDIS_MINIPORT_DRIVER

#define NDIS50_MINIPORT 1

#include <ndis.h>

注意，在NDISXX_MINIPORT_CHARCTERISTICS的成员中指定的NDIS的版本号必须与用编译器指令为驱动程序设置的版本号一致。

在广域网微端口驱动程序的DriverEntry函数执行完之后，微端口驱动程序已经为一个或多个NIC注册了它的微端口入口点。此后，微端口驱动程序必须为上层驱动程序（如：NDISWAN、NDPROXY）提供它及其NIC性能的信息。驱动程序提供性能信息的途径取决于广域网微端口驱动程序工作的环境，是标准的还是CoNDIS环境：

§ 对于标准广域网微端口驱动程序，微端口驱动程序的MiniportInitialize函数首先为一个或多个NIC完成初始化。然后，NDISWAN驱动程序使用OID_WAN_GET_INFO查询微端口驱动程序。然后NDIS调用微端口驱动程序的MiniportQueryInformation函数并且传递此查询请求。有TAPI能力的微端口驱动程序应当设置NDIS_WAN_INFO结构的成员FrameingBits的TAPI_PROVIDER位。设置TAPI_PROVIDER位通知上层驱动程序将TAPI OID发送给微端口驱动程序。标准广域网微端口驱动程序使用TAPI OID来建立、使用、拆卸TAPI连接。如何设置使用TAPI服务的标准广域网微端口驱动程序将在8.6节详细描述。

§ 对于CoNDIS广域网微端口驱动程序，微端口驱动程序在函数MiniportInitialize内调用NdisCmRegisterAddressFamily函数，注册呼叫管理器入口点和地址族类型CO_ADDRESS_FAMILY_TAPI_PROXY。通过这样做，微端口驱动程序通告上层驱动程序它实现TAPI服务。然后NDIS将新注册的地址族通知给NDPROXY。NDPROXY确定它可以使用呼叫管理器（集成在广域网微端口驱动程序之中）提供的TAPI服务。有TAPI能力的CoNDIS广域网微端口驱动程序可以是一个集成的微端口呼叫管理器（MCM）。NDPROXY打开TAPI-proxy地址族，它联合微端口驱动程序，并向NDIS注册NDPROXY的面向连接的入口点，这些入口点被用于与微端口驱动程序的通信。一旦NDPROXY确定它可以使用微端口驱动程序的TAPI服务，它便列举微端口驱动程序的TAPI性能，然后发送封装在NDIS结构中的TAPI请求。 关于使用支持电话服务的CoNDIS扩展，详见8.7节。

8.4.3.2 查询广域网微端口驱动程序的信息

高层驱动程序用查询请求调用NdisRequest或NdisCoRequest函数，确定标准或CoNDIS广域网微端口驱动程序及其NIC的广域网性能和当前状态。在NDISWAN驱动程序转发查询请求之后，NDIS调用广域网微端口驱动程序的查询请求函数。NDIS调用的查询请求函数根据广域网微端口驱动程序的类型，是标准的或CoNDIS，而有所不同：

§ 对于标准广域网微端口驱动程序，这个查询请求函数是MiniportQueryInformation。这个函数与局域网微端口驱动程序的相同，只是广域网微端口驱动程序可以识别标准广域网OID。

§ 对于CoNDIS广域网微端口驱动程序，这个查询请求函数是MiniportCoRequest。这个函数与标准的面向连接的微端口驱动程序的相同，只是CoNDIS广域网微端口驱动程序可以识别CoNDIS 广域网OID。

若某个广域网微端口驱动程序需要通过返回状态NDIS_STATUS_PENDING异步地完成NdisRequest或NdisCoRequest，它以后还必须通过调用一个完成函数（completion function）来完成查询。微端口驱动程序使用哪个完成函数取决于此驱动程序是标准还是CoNDIS类型的：

§ 对于标准广域网微端口驱动程序，完成函数是NdisMQueryInformationComplete。

§ 对于CoNDIS广域网微端口驱动程序，完成函数是NdisCoRequestComplete。

NDIS在调用某个广域网微端口驱动程序的查询请求函数的时候，NDIS传递一个指向NDIS_REQUEST结构的指针，这个结构包含了查询OID和一个用于保留从广域网微端口驱动程序获得的信息的缓冲区。广域网微端口驱动程序在完成请求之前控制此缓冲区。如果NDIS_REQUEST的成员InformationBufferLength中指定的字节数不足以存储此OID查询的信息，广域网微端口驱动程序将使此次查询请求失败，并置NDIS_REQUEST的成员BytesNeeded（NDIS_REQUEST的一个成员）为OID需要的字节数。

当前查询请求完成之前不会有其他请求再提交给特定的广域网微端口驱动程序。

广域网微端口驱动程序必须能够识别并正确的响应一组广域网查询OID。这组OID取决于广域网微端口驱动程序的类型是标准还是CoNDIS：

§ 标准广域网查询OID（Standard WAN query OIDs）。

§ CoNDIS广域网查询OID（CoNDIS WAN query OIDs）。

标准广域网查询OID

标准广域网微端口驱动程序必须能够识别并正确的响应的标准广域网查询OID有：

OID_WAN_MEDIUM_SUBTYPE 这个OID被用来查询标准广域网微端口驱动程序支持的介质子类型。标准广域网微端口驱动程序返回的信息应该是枚举NDIS_WAN_MEDIUM_SUBTYPE的子类型之一：

NdisWanMediumHub
NdisWanMediumX_25
NdisWanMediumIsdn
NdisWanMediumSerial
NdisWanMediumFrameRelay
NdisWanAtm
NdisWanSonet
NdisWanMediumSW56K
NdisWanMediumPPTP
NdisWanMediumL2TP
NdisWanMediumIrda
NdisWanMediumParallel

OID_WAN_GET_INFO 在标准广域网微端口驱动程序初始化之后，查询此OID来确定一些关于微端口驱动程序和它的NIC的信息。这个OID的结构的说明在online DDK的Network Drivers Reference 中。标准广域网微端口驱动程序返回的信息包含在NDIS_WAN_INFO结构的成员中。这些信息包括：

参数 内容

MaxFrameSize 驱动程序将此参数置为它所能发送和接收的最大帧长度（字节）。这个参数的典型值是1500，但是驱动程序可以处理的字节数应比预测的最大值大32 字节。这个值不包括驱动程序自己的组帧开销（framing overhead）或PPP HDLC开销。

MaxTransmi t 广域网微端口驱动程序在一个线路上可以处理的未完成包(outstanding packet)数目的缺省最大值。NDISWAN驱动程序确保微端口驱动程序不超过这个数字，具体来说，如果达到最大值，NDISWAN将挂起发送包，除非微端口驱动程序在后来的line-up指示中重置此最大值。通过在line-up指示中传递一个新的非零SendWindow值，微端口驱动程序可以动态改变一条线路的最大传输值。

HeaderPadding 在最大包开始处填充所需缓冲区长度(字节)。

TailPadding 在最大包结尾处填充所需缓冲区长度(字节)。

Endpoint NIC最多所能支持的链接数。这是一个与适配器相关的特性。

MemoryFlags 如果NIC不是DMA设备则此标志为零。如果NIC支持DMA，此标志可以被设为NIDS_MEMORY_NONCACHED或/和 NDIS_MEMORY_CONTIGUOUS。

HightestAcceptableAddress 如果广域网微端口驱动程序的NIC支持DMA，置此标记为NDIS_PHYSICAL_ADDRESS_CONST(-1,-1)；否则，如果NIC支持DMA且支持24位地址，置此标记为NDIS_PHYSICAL_ADDRESS_CONST(0x1000000,0)。然后，指定范围内的内存将被分配给将要传送到NIC的包。这样可以通过DMA将内存的数据直接送到NIC，而不必通过CPU在两块缓冲区之间复制。

FramingBits 驱动程序设置此标记置指定它支持的组帧类型。

OID_WAN_PERMANENT_ADDRESS 此OID用来查询NIC的硬件地址。

OID_WAN_CURRENT_ADDRESS 此OID用来查询NIC当前正使用的地址。

标准广域网微端口驱动程序为每个NIC返回一个唯一的地址。这个地址作为以太网地址被用于绑定协议。第一字节的最小有效位不能为1，否则将被认为是以太网广播地址。如果供货商已经分配了一个以太网供货商编码，此编码应被用于确保地址不与其他供货商的地址冲突。

OID_WAN_GET_LINK_INFO 查询此OID来确定链接的当前状态。这个OID的结构的说明在online DDK的Network Drivers Reference 中。标准广域网微端口驱动程序返回先前的OID_WAN_SET_LINK_INFO设置请求设置的值。如果先前没有设置请求发生，标准广域网微端口驱动程序将返回链接的当前状态。如果链接并不存在或处于非活动状态，将返回错误NDIS_STATUS_INVALID_DATA。标准广域网微端口驱动程序返回的信息包含在结构NDIS_WAN_GET_LINK_INFO的成员中：

参数 内容

NdisLinkHandle 为此查询标识广域网微端口驱动程序的链接环境（link context）。

MaxSendFrameSize 广域网微端口驱动程序可以在此链接上发送的最大缓冲区长度（字节）。微端口驱动程序的MiniportWanSend函数可以拒绝接收大于此值的发送包。

MaxReceiveFrameSize 微端口驱动程序可以从网上接受的最大包（包括填充）。比这更大的包将被丢弃。

HeaderPadding 帧头填充长度（字节）。

TailPadding 帧尾填充的长度（字节）。

SendFramingBits 描述可以发送什么类型的帧的位掩码。

RecvFramingBits 描述可以接收什么类型的帧的位掩码。

SendCompressionBits 保留。

RecvCompressionBits 保留。

SendACCM 专用于异步介质类型。如果存在， 0-31位分别指示需要字填充（byte stuff）的字。例如：如果位0被置为1，ASCII字符0x00应被字填充。

RecvACCM 同SendACCM。

OID_WAN_GET_STATS_INFO 此OID用于查询标准广域网微端口驱动程序的NIC上的链接的状态。标准广域网微端口驱动程序应为它的链接保留统计量。这个OID的结构在online DDK的Network Drivers Reference 中说明。标准广域网微端口驱动程序返回的信息包含在结构NDIS_WAN_GET_STATS_INFO的成员中，这些信息有：

成员 内容

NdisLinkHandle 为此查询标识广域网微端口驱动程序的链接环境(link context)。

BytesSent 发送了的字节数。

BytesRcvd 接收到的字节数。

FramesSent 发送的帧(广域网包)数。

FramesRcvd 接收到的帧数。

CRCErrors 链接遇到的CRC错误个数。CRC错误是由循环冗余校验失败引起的。CRC错误表明接收到的帧中的某些字节在传输过程中有错码。

TimeoutErrors 链接遇到的超时错误数。当预期的字节未被及时接收时，发生超时错误。

AlignmentErrors 链接遇到的定位错误数。当接收到的字节与预期的不同时，发生定位错误。这种错误发生的典型情况是有字节丢失或有超时错误发生。

SerialOverrunErrors 链接遇到的串行过速错误数。当广域网NIC不能以数据接收的速度处理数据时，发生串行过速错误。

FramingErrors 链接遇到的组帧错误数。当接收到有无效开始和结束位的异步字节时，发生组帧错误。

BufferOverrunErrors 链接遇到的缓冲过速错误数。当标准广域网微端口驱动程序不能以数据接收的速度处理数据时，发生缓冲过速错误。

CoNDIS广域网查询OID

CoNDIS广域网微端口驱动程序必须能够识别并正确的响应面向连接的广域网查询OID，概述如下：

OID_WAN_CO_GET_INFO 在CoNDIS广域网微端口驱动程序初始化之后，查询此OID以确定微端口驱动程序的NIC上所有VC的信息。这个OID的结构在online DDK的Network Drivers Reference 中说明。CoNDIS广域网微端口驱动程序返回的信息包含在NDIS_WAN_CO_INFO结构的成员中。这些信息包括：

成员 内容

MaxFrameSize CoNDIS广域网微端口驱动程序将此成员设为微端口驱动程序可以接收和发送的网络包的最大帧长度（字节）。此值应包括微端口驱动程序自己的组帧开销（framing overhead）和/或PPP HDLC开销。一般的，此值在1500左右，但微端口驱动程序应能处理比预期最大值大32字节的帧。

MaxSendWindow CoNDIS广域网微端口驱动程序将此成员置为驱动程序在每个VC上能处理的未完成包（outstanding packet）的最大数。NDISWAN驱动程序确保微端口驱动程序不超过这个数字，也就是说，如果到达最大值，NDISWAN将挂起发送包，除非微端口驱动程序在后继的链接参数（link-parameters）指示中重置此最大值。通过设置WAN_CO_LINK_PARAMS结构的成员SendWindow，然后指示给NDISWAN，微端口驱动程序可以为每个VC动态改变此值。

FramingBits 一个32位掩码，CoNDIS广域网微端口驱动程序用此掩码指定微端口驱动程序支持的组帧类型。微端口驱动程序也可以指定它是否支持TAPI连接，以及是否执行它自己的组帧。

DesiredACCM CoNDIS广域网微端口驱动程序将此成员置为它需要的异步控制字符映射的类型。

OID_WAN_CO_GET_LINK_INFO 查询此OID以确定VC当前状态的PPP组帧（PPP framing）信息。这个OID的结构在online DDK的Network Drivers Reference 中说明。CoNDIS广域网微端口驱动程序返回先前用OID_WAN_CO_SET_LINK_INFO设置请求设置的值。如果先前没有设置请求发生，CoNDIS广域网微端口驱动程序将返回VC的当前状态。如果VC并不存在或处于非活动状态，将返回错误NDIS_STATUS_INVALID_DATA。CoNDIS广域网微端口驱动程序返回的信息包含在结构NDIS_WAN_CO_GET_LINK_INFO的成员中。这些信息包括：

成员 内容

MaxSendFrameSize 微端口驱动程序在此VC上可以传输的最大缓冲区长度（byte）。微端口驱动程序的MiniportCoSendPackets函数可以拒绝接收大于此长度的发送包。

MaxRecvFrameSize 可以从网络上接收的最大包长度。微端口驱动程序可以丢弃大于此长度的包。

SendFramingBits 一个32位掩码，指定微端口驱动程序可以发送的组帧类型。

RecvFramingBits 一个32位掩码，指定微端口驱动程序可以接收的组帧类型。

SendACCM 专用于异步介质类型。如果存在，位0-31分别指示需要字填充（byte stuff）的字。例如：如果位0被置为1，ASCII字符0x00应被字填充。

RecvACCM 同SendACCM。

OID_WAN_CO_GET_STATS_INFO 查询此OID以确定VC的状态。CoNDIS广域网微端口驱动程序应为它的VC保留统计量。这个OID的结构在online DDK的Network Drivers Reference 中说明。CoNDIS广域网微端口驱动程序返回的信息包含在结构NDIS_WAN_CO_GET_STATS_INFO的成员中，这些信息有：

成员 内容

BytesSent 发送的字节数。

BytesRcvd 接收的字节数。

FrameSent 发送的帧（包）数。

FrameRcvd 接收的帧数。

CRCErrors 此VC遇到的CRC错误数。CRC错误是由循环冗余校验失败引起的。CRC错误表明接收到的帧中的某些字节在传输过程中有错码。

TimeoutErrors VC遇到的超时错误数。当预期的字节未被及时接收时，发生超时错误。

AlignmentErrors VC遇到的定位错误数。当接收到的字节与预期的不同时，发生定位错误。这种错误发生的典型情况是当字节丢失或有超时错误发生时。

SerialOverrunErrors VC遇到的串行过速错误数。当WAN NIC不能以数据接收的速度处理数据时，发生串行过速错误。

FramingErrors VC遇到的组帧错误数。当接收到有无效开始和结束位的异步字节时，发生组帧错误。

BufferOverrunErrors VC遇到的缓冲过速错误数。当CoNDIS广域网微端口驱动程序不能以数据接收的速度处理数据时，发生缓冲过速错误。

8.4.3.3 设置广域网小段口驱动程序的状态

高层协议使用设置请求调用NdisReques t或NdisCoRequest函数改变标准或CoNDIS广域网微端口驱动程序的NIC的信息。在NDISWAN驱动程序转发设置请求后，NDIS调用广域网微端口驱动程序的设置请求函数。NDIS调用哪个设置请求函数取决于广域网微端口驱动程序的类型，标准或CoNDIS：

§ 对于标准广域网微端口驱动程序，这个设置请求函数是MiniportSetInformation。这个函数与局域网微端口驱动程序的相同，只是广域网微端口驱动程序可以识别标准广域网OID。

§ 对于CoNDIS广域网微端口驱动程序，这个设置请求函数是MiniportCoRequest。这个函数与标准面向连接的微端口驱动程序的相同，只是CoNDIS广域网微端口驱动程序可以识别CoNDIS 广域网OID。

在当前设置请求完成之前，其他请求不会被提交给广域网微端口驱动程序。如果广域网微端口驱动程序不能立即完成设置请求，它必须在稍后调用一个完成函数（completion function）。广域网微端口驱动程序的设置请求函数返回NDIS_STATUS_PENDING表示它未完成设置请求。微端口驱动程序调用哪个完成函数依赖于广域网微端口驱动程序的类型，标准或CoNDIS：

§ 对于标准广域网微端口驱动程序，这个完成函数是NdisMSetInformationComplete。

§ 对于CoNDIS广域网微端口驱动程序，这个完成函数是NdisCoRequestComplete。

广域网微端口驱动程序可以使用所有的NDIS 全局设置（global-set）OID。

广域网微端口驱动程序必须能够识别并正确的响应一组广域网设置 OID（WAN set OID）。这组广域网设置 OID依赖于广域网微端口驱动程序的类型，标准或CoNDIS：

标准广域网设置 OID

标准广域网微端口驱动程序必须能够识别并正确的响应以下标准广域网OID。

OID_WAN_SET_LINK_INFO 此OID用于设置链接的特性。这个OID的结构在online DDK的Network Drivers Reference 中说明。传递给标准广域网微端口驱动程序的信息包含在结构NDIS_WAN_SET_LINK_INFO的成员中。NDIS_WAN_SET_LINK_INFO结构的成员与NDIS_WAN_GET_LINK_INFO的成员相同，这在标准广域网查询OID中有描述。

CoNDIS广域网设置OID

CoNDIS广域网微端口驱动程序必须能够识别并正确的响应以下面向连接的广域网OID。

OID_WAN_CO_SET_LINK_INFO 此OID用于为特定VC改变PPP组帧信息。这个OID的结构的说明在online DDK的Network Drivers Reference 中。传送给CoNDIS广域网微端口驱动程序的信息包含在结构NDIS_WAN_CO_SET_LINK_INFO的成员中。这在CoNDIS广域网查询OID中有描述。

8.4.3.4 在广域网微端口驱动程序上发送数据

上层协议调用NdisSend或NdisCoSendPackets函数巴包发送给下层广域网微端口驱动程序。NDISWAN驱动程序转发从上层驱动程序发送来的包。在转发包之前，NDISWAN将之重新打包。对于标准广域网微端口驱动程序，在转发之前，NDISWAN将包的结构从NDIS_PACKET变为NDIS_WAN_PACKET结构。对于CoNDIS广域网微端口驱动程序，NDISWAN用新的NDIS_PACKET结构转发包。

NIDSWAN转发包之后，NDIS调用广域网微端口驱动程序的发送函数。NDIS调用哪个发送函数取决于广域网微端口驱动程序的类型是标准还是CoNDIS：

l 对于标准广域网微端口驱动程序，此发送函数是MiniportWanSend。MiniportWanSend函数将包通过NIC发送到广域网。

l 对于CoNDIS广域网微端口驱动程序，此发送函数是MiniportCoSendPacket。关于CoNDIS广域网微端口驱动程序如何发送包的更多信息，参见第四部分的1.6.7.1，面向连接的NDIS。

不论是同步还是异步，包描述信息和包数据的所有权都被传递给广域网微端口驱动程序，直到包发送完毕为止。广域网微端口驱动程序的发送函数返回NDIS_STATUS_PENDING，表示它还未完成包的传送，它需要调用一个完成函数完成发送。微端口驱动程序以后调用哪个完成函数取决于微端口驱动程序的类型是标准还是CoNDIS：

l 对于标准广域网微端口驱动程序，此完成函数是NdisMWanSendComplete。

l 对于CoNDIS广域网微端口驱动程序，此完成函数是NdisMCoSendComplete。

这些完成函数的调用不一定表时包已经被传送，尽管一般来说包已经被传送（除智能NIC外）。这些调用实际上是指广域网微端口驱动程序已经准备好释放包的所有权。

如果广域网微端口驱动程序返回一个不是NDIS_STATUS_PENDING的状态，则认为发送操作已经结束，包的所有权立刻返回到调用者。在这种情况下，微端口驱动程序不必调用完成函数。

在包被广域网微端口驱动程序接收到之后，应当在当前连接上将包发送出去。对于标准环境，此连接是一个已建立的链接。对于CoNDIS环境，此连接是一个VC。

与局域网微端口驱动程序不同的是，广域网微端口驱动程序不可以返回状态NDIS_STATUS_RESOURCES以指示它没有足够的资源处理包的传送。取而代之的是，广域网微端口驱动程序应当将包放在发送队列中等待一段时间，并可能还要降低此连接的发送窗口的值。发送窗口的值是广域网微端口驱动程序在每个连接上可以处理的未完成包的个数。发送窗口的值由微端口驱动程序为上层驱动程序指定。广域网微端口驱动程序指定发送窗口值的方法取决于广域网微端口驱动程序的类型是标准还是CoNDIS：

n 标准广域网微端口驱动程序通过NDIS_WAN_INFO结构的成员MaxTransmit指定每个数据信道可以处理的未完成包个数的缺省值。微端口驱动程序在响应协议驱动程序的OID_WAN_GET_INFO请求时提供此结构。然而，微端口驱动程序通过以line-up指示调用函数NdisMIndicateStatus，可以在每条线路的基础上动态操纵发送窗口。在此调用中，微端口驱动程序为参数NDIS_WAN_LINE_UP结构的成员SendWindow提供一个新的非零值。如果微端口驱动程序将SendWindow值置为零，NDISWAN将使用MaxTransmit中的缺省值作为此线路的值。标准广域网微端口驱动程序可能必须修改或添加包头，以及添加包尾（例如，添加FCS）。微端口驱动程序预先提供在包的头和尾的适当填充。为了在广域网介质上发送包，广域网微端口驱动程序可以随意以它认为合适的方法改变包中的数据。

n CoNDIS广域网微端口驱动程序通过NDIS_WAN_CO_INFO结构的成员MaxSendWindow，来指定每个VC可以处理的未完成包的个数。微端口驱动程序在响应协议驱动程序的OID_WAN_CO_GET_INFO请求时提供此结构。然而，通过设置传送给函数NdisMCoIndicateStatus 的WAN_CO_LINKPARAMS结构的成员SendWindow，微端口驱动程序可以在每个VC的基础上动态调节此数值。NDISWAN使用当前SendWindow值作为对未完成发送的限制。微端口驱动程序可以通过将SendWindow置零规定不可以处理任何未完成包。也就是说，把SendWindow成员置为零，关闭发送窗口并且NDISWAN停止为特定VC发送包。

广域网微端口驱动程序也不能调用NdisMSendResourcesAvailable函数。

如果已设置PPP组帧，传递给广域网微端口驱动程序的发送函数的包将包含简单HDLC PPP组帧。对于SLIP或RAS组帧，包只包含数据部分，不包含组帧数据。简单HDLC PPP组帧在后文中有消息讨论。

广域网NIC驱动程序不得试图提供软件回放或混杂模式回放（promiscuous-mode loopback）。NDISWAN驱动程序全面支持这两种回放类型。

对于标准广域网微端口驱动程序，以下内容适用于广域网包。广域网包描述信息结构的详细信息见online DDK中Network Drivers Reference中的NDIS_WAN_PACKET。

n NDIS_WAN_PACKET结构的成员MacReservedx和WanPacketQueue对标准广域网微端口驱动程序是完全可用的。

n 可用的头填充只有CurrentBuffer-StartBuffer。可用的尾填充是EndBuffer-(CurrentBuffer+CurrentLength)。头和尾填充至少应满足所需要的数量，但可以更多。

8.4.4 广域网微端口驱动程序做出的指示

标准和CoNDIS广域网微端口驱动程序有不同的方法向NDISWAN指示状态和接收到的数据。

指示广域网专用的状态：

n 标准广域网微端口驱动程序与局域网微端口NIC驱动程序相同，调用NdisMIndicateStatus函数。

n CoNDIS广域网微端口驱动程序与面向连接的微端口驱动程序相同，调用NdisMCoIndicateStatus函数。

指示从广域网接收到的数据：

n 标准广域网微端口驱动程序调用函数NdisMWanIndicateReceive，而不是调用局域网微端口驱动程序的类似函数。

n CoNDIS广域网微端口驱动程序与面向连接的微端口驱动程序相同，调用NdisMCoIndicateReceivePacket函数。

以下各节描述了标准广域网微端口驱动程序如何指示从广域网接收到的数据，并且描述了标准和CoNDIS广域网微端口驱动程序可以指示给NDISWAN驱动程序的广域网专用的状态类型。

n 8.4.4.1 指示从标准广域网微端口驱动程序接收数据

n 8.4.4.2 指示广域网微端口驱动程序状态

8.4.4.1 指示从标准广域网微端口驱动程序接收数据

标准广域网微端口驱动程序调用NdisMWanIndicateReceive函数指示包已到达且整个包已准备好检查。在此调用之后，NDISWAN将包到达的信息指示给绑定的上层驱动程序的ProtocolReceive处理程序。可以检查整个包是因为微端口驱动程序不传递任何先行数据。

因为整个包总是向上传递，微端口驱动程序永远不会接收到传输数据的请求（数据被NDISWAN复制并向上传递到下一个高层驱动程序）。因为包可能被压缩和加密，所以整个包总是向上传递。同样，因为链接为点对点的，至少应有一个绑定的协议处理包。

包的报头中的数据与NIC接收的相同。NIC驱动程序不会消除它接收到的任何报头或报尾数据。用来传送的驱动程序不能给包增加任何填充。

广域网微端口驱动程序调用函数NdisMWanIndicateReceiveComplete指示一个或多个接收指示的结束，以便协议加快处理接收到的包。作为结果，NDISWAN调用绑定协议的ProtocolReceiveComplete处理程序通知每个协议可以开始处理接收到的数据了。在协议的接收完成处理程序中，协议的处理过程不受严格的时间限制，但在接受处理程序中受此限制。

协议应该假定ProtocolReceiveComplete调用是可以被中断的。在SMP计算机中，接受处理程序和接收完成处理程序可以在不同处理器中并行运行。

注意，标准广域网微端口驱动程序的NdisMWanIndicateReceive与NdisMWanIncicateReceiveComplete可以不一一对应。微端口驱动程序可以为多个接收指示发布一个接收完毕指示。例如：一个标准广域网微端口驱动程序可以在每完成十个包或退出处理程序时，调用一个NdisMWanIncicateReceiveComplete。

8.4.4.2 指示广域网微端口驱动程序状态

广域网微端口驱动程序应该保存一组广域网专用的统计信息。广域网微端口驱动程序用唯一的状态代码和缓冲区内容指示状态。在广域网NIC的状态发生变化后，广域网微端口驱动程序调用特定的状态指示函数将这些变化指示给绑定的上层协议。广域网微端口驱动程序生成这些状态指示。绑定的协议驱动程序可以忽略这些指示。然而，一般来说执行这些指示可以改善协议和微端口驱动程序的性能。NDISWAN驱动程序将状态指示传递给绑定协议的ProtocolStatus函数或配置管理器。这些协议或配置管理器能够记录指示并可能产生校正动作。广域网微端口驱动程序调用哪个状态指示函数取决于广域网微端口驱动程序的类型是标准还是CoNDIS：

n 对于标准广域网微端口驱动程序，这个状态指示函数是NdisMIndicateStatus。此函数与局域网微端口NIC驱动程序的相同，只是广域网微端口驱动程序指示的是一组广域网专用的统计数据。

n 对于CoNDIS广域网微端口驱动程序，这个状态指示函数是NdisMCoIndicateStatus。此函数与标准面向连接的微端口NIC驱动程序中的相同，只是CoNDIS广域网微端口驱动程序为每个VC指示一组广域网专用的统计数据。微端口驱动程序使用一个显式的VC句柄调用NdisMCoIndicateStatus，将变化指示给共享此VC的上层协议驱动程序。

每个状态指示提供两部分信息：

n 表示通用状态的状态码。已定义的通用状态码的个数是有限的；这些状态码在将来可以扩充。

n 保存状态信息的缓冲区。状态信息可以是某个特定的NIC的或CoNDIS广域网微端口驱动程序的，或是NIC上某个VC的。例如，缓冲区内保存一个X.25连接的新的传输速度。对于标准广域网微端口驱动程序，此缓冲区可能是指示一个新的链接变成活动的或一个已存在的链接变为空闲状态。

广域网微端口驱动程序做出的状态指示取决于广域网微端口驱动程序的类型是标准还是CoNDIS，这将在下面两节描述：

n 标准广域网微端口驱动程序状态指示

n CoNDIS广域网微端口驱动程序状态指示

标准广域网微端口驱动程序状态指示

标准广域网微端口驱动程序状态指示的一般形式是NDIS_STATUS_WAN_XXX。三种状态指示是：

n NDIS_STATUS_WAN_LINE_UP

标准广域网微端口驱动程序调用NdisMIndicateStatus指示新的数据信道变为活动的或者是已存在的数据信道的参数发生改变。在此调用中，微端口驱动程序在参数GeneralStatus中传递NDIS_STATUS_WAN_LINE_UP，在参数StatusBuffer中传递指向NDIS_MAC_LINE_UP结构的指针。NDIS_MAC_LINE_UP为特定链接标识数据信道，并描述此链接的参数。在此指示发生之前，广域网NIC可以接受包并能够使它们成功或失败，但这样的包却未必可以被远程节点的进程接收到。在数据信道活动前，协议应当减少计时器并重新计数以便任何外出的连接企图迅速失败。

标准广域网微端口驱动程序必须在从OID_TAPI_GET_ID请求返回之前发出初始的line-up指示。此请求使用了NDIS_TAPI_GET_ID结构。如果NDIS_TAPI_GET_ID的成员DeviceClass的值是NDIS，并且成员ulSelect的值是LINECALLSELECT_CALL，必须进行line-up指示。在OID_TAPI_GET_ID请求完成之前，微端口驱动程序将NDIS_TAPI_GET_ID的成员DeviceID的值置为从line-up指示返回的NDIS_MAC_LINE_UP结构的成员NdisLinkContext的值。Line-up指示的更多信息，参见8.6.12的Line-up指示。

n NDIS_STATUS_WAN_LINE_DOWN

当链接断开时标准广域网微端口驱动程序调用NdisMIndicateStatus。在此调用中，微端口驱动程序在参数GeneralStatus中传递NDIS_STATUS_WAN_LINE_DOWN，在参数StatusBuffer中传递指向NDIS_MAC_LINE_DOWN结构的指针。NDIS_MAC_LINE_DOWN的成员NdisLinkContext指出不再有效的链接。

在此状态指示发生之后，绑定的协议应当减少计数器并重新计数，直到下一line-up指示出现。

n NDIS_STATUS_WAN_FRAGMENT

当从远程节点接收到不完全的包时，标准广域网微端口驱动程序调用NdisMIndicateStatus。在此调用中，微端口驱动程序在参数GeneralStatus中传递NDIS_STATUS_WAN_FRAGMENT，在参数StatusBuffer中传递指向NDIS_MAC_FRAGMENT结构的指针。NDIS_MAC_FRAGMENT指出特定的链接并描述接收到不完全包的原因。

当从远程对等节点接收到不完全的包时，如果标准广域网微端口驱动程序不进行FRAGMENT指示，NDISWAN将不会发现不完全包错误。在FRAGMENT指示发生后，协议应当发送帧到远程节点通知远程节点FRAGMENT指示，而不是等待超时发生。NDISWAN通过计算链接上发生的FRAGMENT指示次数记录丢弃包的数量。

标准广域网微端口驱动程序还可以指示TAPI状态。微端口驱动程序调用NdisMIndicateStatus指示TAPI状态。在此调用中，微端口驱动程序在参数GeneralStatus中传递NDIS_STATUS_TAPI_INDICATION，在参数StatusBuffer中传递指向NDIS_MAC_EVENT结构的指针。NDIS_MAC_EVENT记述发生的TAPI线路或呼叫事件。微端口驱动程序开发者关心的TAPI状态指示类型有线路状态的改变、呼叫状态的改变、内入呼叫的到达、远程节点或微端口驱动程序关闭现存线路或呼叫的事件。详见8.6节。

CoNDIS广域网微端口驱动程序状态指示

CoNDIS广域网微端口驱动程序的VC状态指示的一般形式是NDIS_STATUS_WAN_CO_XXX。两种VC状态指示类型是：

n NDIS_STATUS_WAN_CO_LINKPARAMS

CoNDIS广域网微端口驱动程序调用NdisMCoIndicateStatus指示在NIC上活动的特定VC的参数发生改变。在此调用中，微端口驱动程序在参数NdisVcHandle中传递VC的句柄，在参数GeneralStatus中传递NDIS_STATUS_WAN_CO_LINKPARAMS，在参数StatusBuffer中传递一个指向WAN_CO_LINKPARAMS结构的指针。WAN_CO_LINKPARAMS中存放VC的新参数。

n NDIS_STATUS_WAN_CO_FRAGMENT

CoNDIS广域网微端口驱动程序调用NdisMCoIndicateStatus指示它从VC的另一个端点接收到了不完全的包。在此调用中，微端口驱动程序在参数NdisVcHandle中传递VC的句柄，在参数GeneralStatus中传递NDIS_STATUS_WAN_ CO_FRAGMENT，在参数StatusBuffer中传递指向WAN_CO_FRAGMENT结构的指针。WAN_CO_FRAGMENT结构描述接收到不完全包的原因。

此指示发生后，面向连接客户应当向VC另一端的面向连接客户发送帧，这些帧通知对应端点它接收到了不完全包，而不是等超时发生。

NDISWAN通过计算每个VC上发生的FRAGMENT指示次数记录丢弃包的数量。

8.5 广域网包的组帧

本节提供了广域网包帧结构的信息。

n 8.5.1 异步帧结构

n 8.5.2 X.25帧结构

n 8.5.3 ISDN和switched-56K帧结构

8.5.1 异步帧结构

DDK提供了一个支持SLIP和RAS压缩的简单异步微端口NIC驱动程序

图8.3 关闭压缩的异步帧结构

8.5.2 X.25帧结构

图8.4 X.25帧结构（协议字段压缩开或关）

X.25不能启用地址和控制字段压缩。如果协议字段压缩被打开，NLPID将被去除。提供透明性、添加任何填充、添加FCS和添加结束标记是X.25驱动程序或硬件的责任。

8.5.3 ISDN和Switched-56K帧结构

最初应使用ISDN B信道（不是D信道）。通过NDISWAN的多链接支持支持多个B信道。最初，应使有NRZ编码的位同步HDLC帧格式。驱动程序或ISDN硬件应提供透明性。ISDN驱动程序或ISDN硬件还应提供NRZ编码、计算FCS、添加PPP结束标记（0x7E）、以及插入帧间时间填充。Switched-56K驱动程序以与ISDN的相同的方式组帧。

8.6 标准NDIS之上的电话服务扩展

本节描述了标准广域网NIC微端口驱动程序如何通过使用NDISTAPI驱动程序和NDISWAN驱动程序的功能实现电话服务。NDISTAPI驱动程序通过一个电话服务提供者与电话应用程序通信。详见Platform SDK的电话应用编程接口。

为了在标准广域网NIC微端口驱动程序上实现电话服务，以下节更详细的描述了NDISTAPI驱动程序：

n NDISTAPI概述

n 线路设备、地址和呼叫

n TAPI设置和查询请求

n 保持关于呼叫状态的信息

n 建立呼叫句柄

n TAPI注册

n TAPI初始化

n 打开一个呼叫线路（call line）

n 接受内入呼叫

n 产生TAPI呼叫

n 主动事件处理

n 指示呼叫线路状态

n 关闭呼叫线路

n NDISTAPI接口

8.6.1 NDISTAPI概述

NDISTAPI驱动程序定义了标准广域网NIC微端口驱动程序如何实现电话服务。它与Windows Telephony的服务提供接口有关，为兼容TAPI的NDIS，微端口NIC驱动程序为用户模式TAPI服务提供者提供核心模式支持。本节介绍了兼容TAPI的标准广域网微端口驱动程序如何初始化、建立线路、使用TAPI和WAN OIDs建立呼叫和关闭呼叫。在这些介绍中简单的介绍了Windows Telephony中的一些概念，但读者应参考Telephony SDK中的文档得到更深入的了解。

兼容TAPI的NDIS微端口NIC驱动程序将自己同时注册和初始化为广域网和TAPI服务的用户。在注册和初始化完成之后，用户层的应用程序可以向用户模式的KMDDSP服务提供者模块提出电话请求，此模块将SPI请求转换为相应的OID。KMDDSP模块将OID传递给TAPI的内核模式组件——NDISTAPI驱动程序。为了建立、监视、拆除线路和呼叫，NDISTAPI将OID发送给恰当的NIC驱动程序。NDISTAPI在NdisRequest调用中将OIDs传递给NDISWAN中间驱动程序。NDISWAN继续传递这些NDIS请求，这导致了对兼容TAPI的标准广域网微端口驱动程序的MiniportQueryInformation函数或MiniportSetInformation函数的调用。

兼容TAPI的标准广域网微端口驱动程序通知线路和呼叫状态的变化，如从进行中状态变为连接状态、内入呼叫的到达、或者远程断连。通过将适当的NDIS_STATUS_TAPI_XXX和NDIS_STATUS_WAN_XXX状态消息传递给NdisMIndicateStatus，微端口驱动程序将状态变化通知上层。

本节描述的各部件之间的关系参见8.1节的RAS体系结构图。内核模式通信的流动方向是从NDISTAPI到NDISWAN，然后通过NDIS到兼容TAPI的标准广域网微端口驱动程序。

8.6.2 线路设备、地址和呼叫

兼容TAPI的标准广域网NIC微端口驱动程序模拟线路、地址和呼叫。线路设备是对一个拥有一个或多个通信信道（用于传送信号或/和信息）的物理设备的抽象。如何模拟线路设备由广域网微端口NIC驱动程序开发者自行决定。例如，考虑一个有两个64K ”B” 信道的BRI-ISDN线路。标准广域网微端口驱动程序可以选择模拟以下的几种选择：

n 一个线路设备，它有两个信道

n 两个线路设备，每个线路设备都有一个信道

n 两个线路设备，每个都支持一个或两个信道，从公用池中选择

n 两个单信道线路设备和一个双信道线路设备

在后两种模型中，信道可以在不同时间被分配给不同线路设备。

每个线路设备可以分配一个或多个地址。每个地址相当于一个电话号码。地址ID是一个介于0到此线路上的地址数减一之间的数字。

从TAPI客户的观点来看，线路设备的数目和相应的地址数目始终保持在客户调用lineInitialize（初始化TAPI客户会话）的次数和调用lineShutdown（结束TAPI客户会话）的次数之间。然而，线路设备和地址的状态常常在客户的电话会话的生命期中改变。例如，线路设备可能会因某些原因停止服务，并稍后又变正常。一些请求和通知消息允许客户应用追踪线路和地址的状况和性能。

一个呼叫代表两个或多个地址间的连接。与线路设备和地址不同的是，呼叫可以动态的创建和销毁。同线路设备和地址相同的是，它们都定义了请求和通知消息，允许客户应用程序追踪呼叫状态和信息的变化。

8.6.3 设置和查询请求

TAPI请求以OID_TAPI_XXX的形式，连同相关的结构，传送给标准广域网微端口驱动程序。如果微端口NIC驱动程序应当给调用者返回信息，在NdisRequest调用中应指定NdisRequestQueryInformation请求类型。对于除完成状态外不返回任何信息的请求，应指定NdisRequestSetInformation请求类型。NDIS库发送来自NDISWAN的NdisRequest调用给目标微端口驱动程序的MiniportQueryInformation函数或MiniportSetInformation函数。

所有发送给标准广域网微端口驱动程序的请求都既可以同步完成也可以异步完成。如果NIC驱动程序不能立即完成请求，它只返回NDIS_STATUS_PENDING,并在它完成请求后调用NdisMQueryInformationComplete或NdisMSetInformationComplete。

8.6.4 保持状态信息

标准广域网微端口驱动程序必须跟踪它管理的链接上的状态的变化，如当前呼叫、地址、线路的状态，并通过调用NdisMIndicateStatus向NDISWAN报告这些变化。

当呼叫状态改变时，微端口驱动程序必须总是发送呼叫状态改变消息。报告呼叫状态消息的格式是LINECALLSTATE_XXX和LINECALLINFOSTATE_XXX。所有的呼叫状态消息都不会被过滤。

微端口驱动程序报告的一些典型的呼叫状态包括LINECALLSTATE_IDLE、LINECALLSTATE_CONNECTED、LINE_CALLSTATE_OFFERING、LINECALLSTATE_DISCONNECT、LINECALLINFOSTATE_CALLERID和LINECALLINFOSTATE_MEDIAMODE。

只有在被OID_TAPI_SET_STATUS_MESSAGE请求使能时，微端口驱动程序才指示线路状态或地址状态的改变。

对于地址状态的改变，微端口驱动程序用LINEADDRESSSTATE_XXX格式表示，例如LINEADDRESSSTATE_INUSEMANY和LINEADDRESSSTATE_FORWARD状态。

对于线路状态的改变，微端口驱动程序用LINEDEVSTATE_XXX格式表示，例如INEDEVSTATE_RISING、INEDEVSTATE_CONNECTED、INEDEVSTATE_OPEN、和LINEADDRESSSTATE_CLOSED状态。

标准广域网NIC微端口驱动程序可以接收一个或多个OID_TAPI_SET_STATUS_MESSAGE请求，此请求通知微端口驱动程序哪个状态的改变必须报告。最初，微端口驱动程序应当认为除LINEDEVSTATE_REINIT（永远不会失效）外的所有线路和地址状态指示都时无效的。

当标准广域网微端口驱动程序调用NdisMIndicateStatus报告状态改变时， GeneralStatus参数总是NDIS_STATUS_TAPI_INDICATION。StausBuffer参数是一个指向NDIS_TAPI_EVENT结构的指针。微端口驱动程序在NDIS_TAPI_EVENT结构的成员htLine和htCall中标识状态被改变的线路和呼叫。NDIS_TAPI_EVENT结构的成员uMsg被置为像LINE_CALLSTATE这样的值，ulParaml描述被报告的状态。

8.6.5 建立句柄

在NDISTAPI驱动程序将标准广域网微端口驱动程序初始化为提供者之后，微端口驱动程序可以接受一个打开线路的请求，此请求使用OID_TAPI_OPEN和适当参数调用MiniportQueryInformation函数。接收到打开请求后，微端口驱动程序一般来说应当作以下操作：

n 在微端口驱动程序的内部结构中将线路设备标记为开放的

n 保存NDISTAPI驱动程序的线路设备句柄（在htLine成员中传递的），这将在后续的事件通知中使用。

n 返回它自己的线路设备句柄（在hdLine成员中传回）。

再一次强调，此请求的发起者是调用lineOpen的客户进程。

在客户进程成功的打开线路后，它可以通过调用lineMakeCall在此线路上放置一个呼叫。这将导致NDISTAPI驱动程序用OID_TAPI_MAKE_CALL调用NdisRequest，在此调用中，在htLine中指定NDISTAPI提供的线路句柄，在htCall中指定NDISTAPI提供的呼叫句柄。此请求被传递给微端口驱动程序的MiniportQueryInformation函数（指定OID_LINE_MAKE_CALL）。如果NIC驱动程序能成功创建此呼叫，它在hdCall中传回它自己的呼叫句柄。

两级ht Xxx/hdXxx（“TAPI对象XXX的句柄/驱动程序对象XXX的句柄”）句柄方案为NDISTAPI驱动程序和标准广域网微端口驱动程序提供了为同一个逻辑对象定义它们自己的含义的灵活性。到NIC驱动程序的请求指定驱动程序句柄（hdLine和hdCall），到NDISTAPI驱动程序的通知包含TAPI句柄（htLine和htCall）。在以上情况下微端口NIC驱动程序为某些对象定义了驱动程序句柄(例如：hdLine和hdCall)，一般来说指定一个指向适当的代表对象的驱动程序内部结构的指针作为句柄，这对NIC驱动程序是有利的。

当客户进程完成呼叫后，它将调用lineDrop丢弃呼叫，然后调用lineDeallocateCall收回呼叫实例。这将导致NDISTAPI驱动程序分别发出两个请求，OID_TAPI_DROP和OID_TAPI_CLOSE_CALL，这两个请求通过NDISWAN传递到标准广域网微端口驱动程序以丢弃呼叫和释放呼叫实例。微端口驱动程序也有可能收到OID_TAPI_CLOSE_CALL请求，而在这之前没有收到OID_TAPI_DROP请求，在这种情况下，微端口驱动程序应当丢弃呼叫和释放呼叫实例，就像两个OID都收到一样。

最后，当最后一个TAPI客户调用了lineClose，NDISTAPI驱动程序将在它的函数MiniportSetInformation中调用NdisRequest发送给微端口驱动程序一个OID_TAPI_CLOSE请求。OID_TAPI_CLOSE也可以在一个活动线路上收到。在这种情况下，微端口驱动程序必须完成或中断此设备上的所有的未完成呼叫和异步请求，并关闭线路。带有有效的hdLine的OID_TAPI_CLOSE请求不会失败。

8.6.6 TAPI注册

标准广域网微端口驱动程序通过两个步骤设置它自己可以接受TAPI命令。当微端口驱动程序在它的函数DriverEntry中通过调用NdisMRegisterMiniport向NDIS注册时，它通过在NDISXX_MINIPORT_CHARCTERISTICS结构的成员Reserved中设置标记NDIS_USE_WAN_WRAPPER指出它需要广域网服务。

在运行DriverEntry函数和在MiniportInitialize函数完成一个或多个适配器的初始化之后，NDISWAN驱动程序将用OID_WAN_GET_INFO请求，对标准广域网微端口驱动程序的MiniportQueryInformation函数进行NdisRequest调用。兼容TAPI的微端口驱动程序应将给此调用中传递的NDIS_WAN_INFO结构的成员FramingBits的TAPI_PROVIDER位置位。

兼容TAPI的标准广域网微端口驱动程序将NDIS_WAN_INFO结构的成员MaxTransmit置为微端口驱动程序可接收未完成包的最大数。NDIS在到达此最大值时挂起发送包。然而，兼容TAPI的微端口驱动程序可以动态提供一个不同于最初缺省值的新的发送窗口。在微端口驱动程序向NdisMIndicateStatus指示line-up状态时，它通过设置NDIS_WAN_LINE_UP结构的SendWindow可以为此线路提供一个新的非零值。如果微端口驱动程序传递了一个为零的SendWindow，NDISWAN将使用缺省值——在此线路初始化时设置的MaxTransimit。

就像广域网驱动程序的MiniportInitialize函数被用来初始化它管理的每个适配器，NDISWAN调用NDISTAPI注册每个TAPI兼容的适配器。这样就为特定的注册过的适配器建立了一个联系，NDISTAPI可以通过NDISWAN向微端口驱动程序发送电话请求。

8.6.7 TAPI初始化

在标准广域网微端口驱动程序注册、初始化、并指示自己是一个TAPI提供者之后，将使用OID_TAPI_PROVIDER_INITIALIZE请求调用MiniportQueryInformation函数。在提供者和TAPI兼容适配器之间有一对一关系。传递给调用的NDIS_TAPI_PROVIDER_INITIALIZE结构中的ulDeviceIDBase表示偏移量，若在后续的OID响应中涉及到设备，微端口驱动程序应当将它以零为基准的线路设备标识加上此偏移量作为设备标识。如果微端口驱动程序管理两个设备，并且ulDeviceIDBase为n，这些设备在后续的调用中为n和n+1。

在从MiniportQueryInformation返回之前，标准广域网微端口驱动程序应当填写OID结构的两个成员。UlNumLineDevs被置为适配器支持的线路设备数。ulProviderID应被置为一个唯一（每个适配器）的值。因为当前无法保证ulProviderID值的唯一性，将NDIS句柄作为ulProviderID传递给MiniportInitialize是一个好的策略。此结构的ulRequestID成员保留给NDISTAPI，并且对微端口驱动程序不是透明的。

发送给已注册的标准广域网微端口驱动程序的OID_TAPI_PROVIDER_INITIALIZE请求总是由第一个客户进程载入和初始化的高层电话模块产生。对每个适配器，微端口驱动程序只在第一个客户进程作出lineInitialize请求时接收一次OID_TAPI_PROVIDER_INITIALIZE请求。

通信设备特性

在标准广域网微端口驱动程序返回它支持的设备数后，它的MiniportQueryInformation函数将以参数OID_TAPI_GET_DEV_CAPS被调用，以返回设备的性能；以参数OID_TAPI_GET_DEV_CONFIG被调用，以返回设备的配置。这些信息描述了广域网微端口驱动程序如何模拟它的设备。例如，同一ISDN适配器可被模拟为：

n 一个线路，此线路上有一个地址，每个地址支持两个呼叫

n 一个线路，此线路上有两个地址，每个地址支持一个呼叫

n 两个线路，每个线路上有一个地址，每个地址支持一个呼叫

使用这些OID可以查询微端口驱动程序的特性，每个从OID_TAPI_PROVIDER_INITIALIZE请求返回的设备都被查询一次。

8.6.8 打开线路

作为应用的lineOpen请求的结果，线路被打开。这个请求通过TAPI到NDISTAPI到NDISWAN，再通过NDIS以OID_TAPI_OPEN请求的形式传递给标准广域网微端口驱动程序。此请求传递NDIS_TAPI_OPEN结构，成员ulDeviceID指定要被打开的线路的标识。另外，此结构包含htLine，NDISTAPI内部涉及的线路句柄。微端口驱动程序必须保留此线路句柄并在对此线路的所有状态指示中和对查询的响应时返回，例如在用NdisMIndicateStatus报告状态改变时。

标准广域网微端口驱动程序创建一个结构来代表线路，此结构保存线路相关的环境。在从MiniportQueryInformation返回之前，微端口驱动程序应当在OID_TAPI_OPEN的成员hdLine中返回代表此环境的值。hdLine中的线路相关值将在后续的有关此线路的调用中（如OID_TAPI_MAKE_CALL）传递给微端口驱动程序。

设置线路的模式

如果应用程序用所有者特权发出lineOpen请求，应用表示它想接收内入呼叫。应用必须指定在此线路上它愿意接受的内入呼叫的有效类型。例如，一个应用可以请求接收这样的类型：g3fax、交互语音、datamodem等。可以使用OID_SET_DEFAULT_MEDIA_DETECTION请求多次调用MiniportSetInformation，以设置微端口驱动程序代表客户可以接受的类型。

如果多于一个的应用设置了介质类型，并且一个应用的类型列表与另一个的列表不同，两个应用以这些请求调用微端口驱动程序以设置介质检测类型。如果一个内入呼叫不在以前成功完成的SET_DEFAULT_MEDIA_DETECTION请求指定的介质模式类型中，微端口驱动程序将不会把此呼叫指示给TAPI。如果微端口驱动程序没有收到对某个线路的SET_DEFAULT_MEDIA_DETECTION请求，它将不会指示任何内入呼叫；这个线路只用于外出呼叫。

标准广域网微端口驱动程序接收的OID_TAPI_CONDITIONAL_MEDIA_DETECTION请求用于判断微端口驱动程序是否支持对特定介质模式类型的内入呼叫指示。如果微端口驱动程序能够监控指示的介质模式集，能够支持LineCallParams中指出的性能，能够放置指定类型的呼叫，它将返回NDIS_STATUS_SUCCESS。如果这样，微端口驱动程序很有可能会接收到后续请求如OID_TAPI_OPEN，OID_SET_DEFAULT_MEDIA_DETECTION等等。

8.6.9 接受内入呼叫

在标准广域网微端口驱动程序成功完成OID_TAPI_PROVIDER_INITIALIZE、OID_TAPI_OPEN和OID_SET_DEFAULT_MEDIA_DETECTION请求之后，它就可以在已经打开的线路上接收内入呼叫了。如果内入呼叫的介质类型与某个已设置的类型匹配，微端口驱动程序应调用NdisMIndicateStatus通知上层此呼叫，它在GeneralStatus中传递NDIS_TAPI_INDICATION，在StatusBuffer中传递指向NDIS_TAPI_EVENT的指针。

NDIS_TAPI_EVENT的ulMsg被置为LINE_NEWCALL，ulParam1被置为hdCall——驱动程序的呼叫句柄。htCall、ulParam2、 ulParam3被置为零。在从NdisMIndicateStatus返回时，NDISWAN在ulParam2中放htCall。微端口驱动程序应保留此句柄并在后续的属于此呼叫实例的指示中使用。例如，在向NDISWAN指示line-up时微端口驱动程序将ConnectionWrapperID置为此值。

微端口驱动程序给出LINE_NEWCALL状态指示之后，它应用LINECALLSTATE_XXX消息，一般是LINECALLSTATE_OFFERING消息，调用NdisMIndicateStatus。此消息的ulParam3置为内入呼叫的介质模式。

如果呼叫被应用接受，应以OID_TAPI_ACCEPT调用 MiniportSetInformation，然后用一个OID_TAPI_ANSWER请求调用 MiniportSetInformation，或只用OID_TAPI_ANSWER请求调用MiniportSetInformation。

如果微端口驱动程序接收到OID_TAPI_ACCEPT请求，它应为此呼叫分配并初始化一个呼叫状态环境，但在接收到OID_TAPI_ANSWER请求之前不能应答此呼叫。然后，如果微端口驱动程序接收到OID_TAPI_ANSWER请求，它应做如下操作：

n 应答此呼叫

n 将线路的状态置为已连接，若允许报告此状态则调用NdisMIndicateStatus报告此新状态。

如果微端口驱动程序在未收到OID_TAPI_ACCEPT请求时收到OID_TAPI_ANSWER请求，它应认为这两个请求被同时收到。

有可能广域网NIC驱动程序永远收不到对某一线路的OID_TAPI_ANSWER请求或OID_TAPI_ACCEPT请求。这种情况发生在客户忙于处理其他呼叫时，或客户对指示的呼叫类型不感兴趣（如对介质模式），等等。等待某一接受或应答请求一定时间后，或检测到远程部分断连，广域网NIC驱动程序应通知NDISTAPI驱动程序呼叫被转换为空闲状态。然后，一段时间后，一个OID_TAPI_CLOSE_CALL请求应被发送给广域网NIC驱动程序来释放呼叫实例。微端口驱动程序也有可能在新呼叫超时之前收到OID_TAPI_CLOSE_CALL。

8.6.10 产生TAPI呼叫

拥有一个开放线路的标准广域网微端口驱动程序可以接收以下两个请求之一以发起呼叫：

n OID_TAPI_MAKE_CALL请求，包含目标地址或使用预定义的缺省号码。

n OID_TAPI_MAKE_CALL请求，不包含目标地址。这种情况下OID_TAPI_MAKE_CALL后紧跟一个包含目标地址的OID_TAPI_DIAL请求

当以OID_TAPI_MAKE_CALL请求调用MiniportQueryInformation时，NDISTAPI在NDIS_TAPI_MAKE_CALL结构的成员 hdLine中传递线路的标识，标识要产生呼叫的线路。HdLine的值是微端口驱动程序从OID_TAPI_OPEN请求返回的值。

微端口驱动程序应保存通过请求传递的htCall值，此值在该呼叫实例后续的状态指示和响应中将被传递。例如，在向NDISWAN指示line-up时微端口驱动程序将此值作为ConnectionWrapperID。

对于非拨号的地址，ulDestAddressSize可以为零，例如热线电话总是自动连接到预定义的号码上；当用后续的ID_TAPI_DIAL请求拨号时，ulDestAddressSize也可以为零。

若NDIS_TAPI_MAKE_CALL结构的成员bUseDefaultLineCallParams为FALSE，微端口驱动程序应使用LineCallParams中的呼叫参数。若bUseDefaultLineCallParams为TRUE，则使用预先设置的缺省呼叫参数。

微端口驱动程序必须检查在其上进行建立呼叫请求的线路的状态，以确定它是否可以生成请求的呼叫。若不能，微端口驱动程序应使此呼叫失败。

MiniportQueryInformation在以下情况下返回：

n 它已产生了呼叫

n 否则，若在后续的OID_TAPI_DIAL请求中拨号，微端口驱动程序分配一个结构并将它自己设置为一个状态，可能是dialtone状态，以便以后完成此呼叫。

若MiniportQueryInformation返回NDIS_STATUS_SUCCESS，微端口驱动程序必须在hdCall中返回一个呼叫相关的环境值。此唯一的hdCall值将在后续与此呼叫相关的请求中传递给微端口驱动程序。

若呼叫是通过两步完成的，第一步传递给MiniportQueryInformation OID_TAPI_MAKE_CALL请求；第二步传递给MiniportSetInformation OID_TAPI_DIAL请求，目标号码在NDIS_TAPI_DIAL结构中传递给微端口驱动程序。OID_TAPI_DIAL请求需要在NDIS_TAPI_DIAL结构中设置hdCall，它的值与微端口驱动程序从先前的OID_TAPI_MAKE_CALL请求返回的句柄相同。

8.6.11 主动事件处理

在电话会话过程中，标准广域网微端口驱动程序很可能会需要向NDISTAPI通知驱动程序主动事件。最典型的主动事件是在一个已打开线路上的进入呼叫，现有呼叫的状态转变（例如从振铃到连接），或线路设备状态的改变（从运行状态到运行结束，反之亦然）。

在向NDISWAN驱动程序自发地报告线路或呼叫上发生的事件时，使用NDIS_STATUS_TAPI_INDICATION状态。微端口驱动程序调用NdisMIndicateStatus，指定GeneralStatus为NDIS_STATUS_TAPI_INDICATION，StatusBuffer参数指向一个已初始化的NDIS_TAPI_WAN结构。

此结构包含以下成员

结构成员 意义

htLine 事件发生的线路的NDISTAPI驱动程序线路句柄。

htCall 对于没有呼叫的线路相关事件，此参数应为零；否则，是与报告的事件相关的NDISTAPI驱动程序呼叫句柄。

ulMsg 指定事件类型，格式为LINE_XXX。

ulParam1 与事件有关的参数，例如，指向描述特定事件的结构的指针。

ulParam2 同上

ulParam3 同上

与标准广域网微端口驱动程序开发者有关的通知消息有：

LINE_ADDRESSSTATE

LINE_CALLINFO

LINE_CALLSTATE

LINE_CLOSE

LINE_DEVSPECIFIC

LINE_NEWCALL

LINE_CALLDEVSPECIFIC

8.6.12 Line-Up指示

从电话的观点看，呼叫的生命期就是一系列状态转变。对于外出呼叫，典型的状态序列是：拨号，进行中，振铃，连接，断连，最后是空闲。对于内入呼叫，这个序列是：提交，振铃，连接，断连，最后是空闲。无论在哪种情况下，对向连接状态的转化都特别感兴趣，因为连接状态是数据可以流过线路的状态。

标准广域网微端口驱动程序以参数GengealStatus为NDIS_STATUS_WAN_LINE_UP调用NdisMIndicateStatus函数，指示一个新的数据信道已变为活动的。StatusBuffer指向的缓冲是NDIS_MAC_LINE_UP结构。在指示line-up之前，适配器可以接受帧，并能使他们成功或失败，但这些帧未必会被远程系统接收。在line-up指示前，协议应当减少计时器并重新计数以便任何外出的连接企图迅速失败。

NDIS_STATUS_WAN_LINE_UP状态必须在从OID_TAPI_GET_ID请求返回之前完成。从NDIS_STATUS_WAN_LINE_UP指示返回的NdisLinkContext在NDIS_TAPI_GET_ID结构中作为DeviceID传递回去。

NDIS_STATUS_WAN_LINE_UP必须在向NDISWAN指示LINECALLSTATE_CONNECTED之前完成。否则，连接的用户模式客户可能会在NDISWAN意识到此线路存在之前试图发送数据。

Lint-up指示完成后，NDISWAN和微端口驱动程序已经交换了句柄，其中一个句柄由NDISWAN用于以后直接向数据信道发送包，另一个由微端口驱动程序用于为到NDISWAN的发送和状态指示标识正确的数据信道。在NDISWAN得到line-up指示和为特定呼叫实例设立数据信道之前，NDISWAN不会用微端口驱动程序的MiniportWanSend函数发送包。

如果下列某种情况发生，微端口驱动程序在OID_TAPI_GET_ID请求的环境之中指示line-up状态：

n NDIS_TAPI_GET_ID结构的成员ulSelect被置为LINECALLSELECT_CALL。并且

n 此请求是NDIS_TAPI_GET_ID结构成员hdCall的第一个OID_TAPI_GET_ID请求。

或

n NDIS_TAPI_GET_ID结构的成员ulSelect被置为LINECALLSELECT_CALL。并且

n 此请求不是NDIS_TAPI_GET_ID结构成员hdCall的第一个OID_TAPI_GET_ID请求，但DeviceClass不同于先前任何对此呼叫实例的get-id请求。并且

n 微端口驱动程序支持对不同设备类型有各自的数据信道。

传递给NdisMIndicateStatus的StatusBuffer参数应该指向NDIS_MAC_LINE_UP结构的缓冲区。此结构定义是：

typedef struct _NDIS_MAC_LINE_UP{

IN ULONG LinkSpeed;

IN NDIS_WAN_QUALITY Quality;

IN USHORT SendWindow;

IN NDIS_HANDLE ConnectionWrapperID;

IN NDIS_HANDLE NdisLinkHandle;

IN OUT NDIS_HANDLE NdisLinkContext;

} NDIS_MAC_LINE_UP, *PNDIS_MAC_LINE_UP;

标准广域网微端口驱动程序应当将NDIS_MAC_LINE_UP的ConnectionWrapperID的值置为此呼叫的htCall（TAPI呼叫句柄），因为它对所有兼容TAPI的微端口驱动程序是唯一的。在微端口驱动程序发布line-down指示之前ConnectionWrapperID都是有效的。

NdisLinkHandle 表示在此呼叫上的数据信道的微端口驱动程序句柄。一般来说，它引用微端口驱动程序为此呼叫保存的状态。NDISWAN保留此句柄，并且如果要在此数据信道发送，NIDSWAN将它返回给微端口驱动程序。在微端口驱动程序调用NdisMIndicateStatus 时，NdisLinkContext被置为NULL。NDISWAN在从NdisMIndicateStatus函数返回前将NdisLinkContext置为NDISWAN提供的值。微端口驱动程序必须保留此不透明的句柄，并在随后的有关此链接的NDISWAN呼叫中提供此句柄，如NdisMWanIndicateReceive。

当MiniportQuerytInformation从OID_TAPI_GET_ID请求返回时，必须在NDIS_TAPI_GET_ID的成员DeviceID中返回NdisLinkContext。从NDISTAPI来看，此环境用一个句柄将呼叫连接和设备类型绑定起来，该句柄是NDISWAN可以识别并已绑定到特定数据信道的。NDISWAN用此句柄确保TAPI客户的数据包直接通过NDISWAN发送到正确的微端口驱动程序数据信道。

改变发送窗口

在line-up指示中指定的SendWindow（NDIS_MAC_LINE_UP结构的成员）充当节流阀的作用，在开始对包排队之前对于特定线路，它控制NDISWAN可以向MiniportWanSend函数传递的包的数量。OID_WAN_GET_INFO请求返回的NDIS_WAN_INFO结构的MaxTransmit成员表示适配器支持的所有线路的发送窗口的缺省值。当微端口驱动程序为一个新线路作第一次line-up指示时，它可以为此线路重置发送窗口值。它也可以通过在后续line-up指示中给NDISWAN一个新的非零SendWindow动态的为某个线路改变发送窗口值。也就是说，微端口驱动程序在没有内入或外出呼叫到达时也可以产生line-up指示。如果微端口驱动程序将SendWindows 置为零，NDISWAN将使用缺省的MaxTransmit值作为发送窗口的大小。

如果微端口驱动程序发出line-up指示改变SendWindow，它必须填写NdisLinkHandle和NdisLinkContext成员。LinkSpeed和Quality被置为零或者是一个新的值。通过作line-up指示并重置这些值，LinkSpeed和Quality都可以被动态的独立的改变。

8.6.13 关闭呼叫线路

在标准广域网微端口驱动程序成功完成OID_TAPI_PROVIDER_INITIALIZE请求时一次会话开始。在一次会话中，可以通过OID_TAPI_OPEN请求打开一个或多个线路，并且在一个线路上，可以通过OID_TAPI_MAKE_CALL、OID_TAPI_DIAL、OID_TAPI_ANSWER请求建立一个或多个呼叫。在一个线路启动期间，许多呼叫可以被建立，然后关闭或放弃。在一个会话期间，一个或多个线路可以经历多次从开放到关闭的转变。微端口驱动程序如何处理这些转变描述如下：

关闭一个呼叫

一个正在运行的呼叫可以被本地或远程节点关闭。呼叫在本地节点被关闭，是因为拥有此呼叫的句柄的最后一个应用关闭了这个句柄，或因为微端口驱动程序的MiniportHalt或MiniportRest函数被调用。如果远程节点中止了一个正在运行的呼叫，微端口驱动程序必须向上层报告此状态改变。

如果本地节点的应用程序要关闭呼叫，应以OID_TAPI_DROP请求并随后以OID_TAPI_CLOSE_CALL调用MiniportSetInformation，或仅仅发送一个OID_TAPI_CLOSE_CALL请求。

如果微端口驱动程序接收到OID_TAPI_DROP请求，它必须作以下操作：

1．丢弃呼叫

2．将呼叫的状态变为LINECALLSTATE_IDLE并调用NdisMIndicateStatus报告状态的改变。

已丢弃呼叫的句柄——htCall和hdCall保持有效。微端口驱动程序不能释放、重新使用或者换句话说更改与hdCall（为呼叫实例创建的）有关的状态，因为微端口驱动程序可能接收到与此呼叫有关的请求，例如有关此呼叫的统计数据的请求。如果MiniportHalt或MiniportRest函数被调用，微端口驱动程序应当将此呼叫断连，但为此呼叫实例保留状态直到接收到OID_TAPI_CLOSE_CALL请求。

当拥有此呼叫实例的打开句柄的最后一个应用关闭此句柄时，MiniportSetInformation也可以接收OID_TAPI_CLOSE_CALL请求。微端口驱动程序不能使此关闭呼叫请求失败。在此请求从MiniportSetInformation返回后，htCall将不再有效。

微端口驱动程序必须准备处理类似这样的时序：在接收到OID_TAPI_DROP之前接收OID_TAPI_CLOSE_CALL。如果微端口驱动程序接收到一个没有OID_TAPI_DROP的OID_TAPI_CLOSE_CALL请求，它就像接收到了丢弃和关闭呼叫两个请求那样进行处理。如果微端口驱动程序在关闭呼叫请求后收到丢弃请求，因为呼叫的句柄不再有效，微端口驱动程序应当返回适当的错误信息。

如果微端口驱动程序检测到远程系统已经丢弃了呼叫，它应将呼叫的状态变为LINECALLSTATE_DISCONNECTED并调用NdisMIndicateStatus（传递此呼叫实例的htCall）指示此状态改变。在此状态改变信息被传播到本地节点上的应用程序后，微端口驱动程序将接收到OID_TAPI_DROP随后是OID_TAPI_CLOSE_CALL，或者仅仅接收到OID_TAPI_CLOSE_CALL请求。微端口驱动程序应以上面所讲的方法处理丢弃和关闭呼叫请求。

关闭线路

在拥有此线路的开放句柄的最后一个应用关闭了这个句柄后，线路将被关闭。当微端口驱动程序接收到OID_TAPI_CLOSE时，它应当拆除线路实例，也就是说，微端口驱动程序中所有涉及此线路的状态可以被重用或释放，因为这些信息不再有效。

如果传递给一个OID_TAPI_CLOSE调用的线路句柄htLine存在并依然有效，此调用必须成功。如果还有呼叫在线路上，微端口驱动程序不会接收到对此线路的OID_TAPI_CLOSE请求。在做出线路关闭请求前，应使用OID_TAPI_CLOSE_CALL关闭所有呼叫。

当线路被关闭时所有存在的呼叫都必须被关闭；微端口驱动程序应当丢弃这些呼叫并清除它们的状态。微端口驱动程序还应当将线路的状态改变为空闲状态。微端口驱动程序拥有的所有此线路的句柄都不再有效。如果微端口驱动程序在收到关闭请求后又收到传递htLine句柄或htCall句柄的请求，它必须使这些请求失败。

关闭会话

上层或广域网微端口NIC驱动程序都可以发起会话的终止操作。当最后一个客户进程与高层电话模块分离时，NDISTAPI驱动程序将被通知它需要终止每一个已注册适配器的会话。NDISTAPI通过用OID_TAPI_PROVIDER_SHUTDOWN调用NdisRequest完成上述行为。一接收到此请求，NDISWAN就调用微端口驱动程序的MiniportSetInformation函数，这将结束指定适配器上所有进行中的活动并释放所有相关资源。

驱动程序发起的会话终止可以在微端口NIC驱动程序被它的函数MiniportHalt卸载时发生。一般来说，微端口NIC驱动程序应完成NDISTAPI驱动程序的所有未完成请求并通过调用NdisMIndicateStatus通知NDISWAN线路设备正在关闭。然后NDIWAN应调用NDISTAPI驱动程序注销适配器。若微端口NIC驱动程序在后来又被重新加载，它应经历与先前描述相同的初始化过程。

若标准广域网微端口驱动程序经历了一些迫使所有客户和设备完成重新初始化的动态重配置，它也有可能发起会话的终止。例如，如果一个适配器的线路设备模型（如支持的线路设备数量）被改变，微端口驱动程序应设置LINEDEVICESTATE_REINIT标记并用LINE_LINEDEVICESTATE调用NdiaMIndicateStatus。

8.6.14 NDISTAPI接口

本节将描述定义NDISTAPI驱动程序、NDISWAN和之下的标准广域网NIC微端口驱动程序之间接口的OID请求。此处描述的每个OID都在Platform SDK 中的Telephony Software Development Kit中定义了一个相应的SPI功能。

标准广域网微端口驱动程序开发者在设计和实现驱动程序时应参考SPI规范。注意NDISTAPI规范和SPI规范的函数定义和参数可能有一些不同，在这种情况下开发者应遵守NDISTAPI驱动程序接口规范。

下表总结了定义NDISTAPI接口的OID请求。

OID 可选或必需

OID_TAPI_ACCEPT 可选

OID_TAPI_ANSWER 必需

OID_TAPI_CLOSE 必需

OID_TAPI_CLOSE_CALL 必需

OID_TAPI_CONDITIONAL_MEDIA_DETECTION 必需

OID_TAPI_CONFIG_DIALOG 可选

OID_TAPI_DEV_SPECIFIC 可选

OID_TAPI_DIAL 可选

OID_TAPI_DROP 必需

OID_TAPI_GET_ADDRESS_CAPS 必需

OID_TAPI_GET_ADDRESS_ID 必需

OID_TAPI_GET_ADDRESS_STATUS 必需

OID_TAPI_GET_CALL_ADDRESS_ID 必需

OID_TAPI_GET_CALL_INFO 必需

OID_TAPI_GET_CALL_STATUS 必需

OID_TAPI_GET_DEV_CAPS 必需

OID_TAPI_GET_DEV_CONFIG 可选

OID_TAPI_GET_EXTENSION_ID 可选

OID_TAPI_GET_ID 必需

OID_TAPI_GET_LINE_DEV_STATUS 必需

OID_TAPI_MAKE_CALL 必需

OID_TAPI_NEGOTIATE_EXT_VERSION 可选

OID_TAPI_OPEN 必需

OID_TAPI_PROVIDER_INITIALIZE 必需

OID_TAPI_PROVIDER_SHUTDOWN 必需

OID_TAPI_SECURE_CALL 可选

OID_TAPI_SELECT_EXT_VERSION 可选

OID_TAPI_SEND_USER_USER_INFO 可选

OID_TAPI_SET_APP_SPECIFIC 必需

OID_TAPI_SET_CALL_PARAMS 必需

OID_TAPI_SET_DEFAULT_MEDIA_DETECTION 必需

OID_TAPI_SET_DEV_CONFIG 可选

OID_TAPI_SET_MEDIA_MODE 必需

OID_TAPI_SET_STATUS_MESSAGES 必需

这些OID的定义在ntddndis.h中。NDIS_TAPI_EVENT消息的定义在ndistapi.h中。

一个给定的标准广域网微端口驱动程序支持此接口的范围，主要依赖于开发者的判断和下层硬件的性能。一些请求的功能要求非常简单，例如OID_TAPI_SET_APP_SPECIFIC请求只要求驱动程序联合一个ULONG值到特定呼叫实例，此值可以被后来的OID_TAPI_GET_CALL_INFO请求检索到。

NIC驱动程序开发者决定不支持的请求应当以NDIS_STATUS_TAPI_OPERATIONUNAVIL状态码结束。NIC驱动程序可以通过对存在于某些OID_TAPI_GET_XXX请求中的结构的成员（例如，LINE_ADDRESS_CAPS结构的成员ulAddrCapFlags）置位或清零，以指示它支持的请求类型（依赖于线路、地址、或呼叫状态，在不同时间有所变化）。

以下节将描述NDISTAPI接口上的查询和设置请求：

n 8.6.14.1 查询信息请求

n 8.6.14.2 设置信息请求

8.6.14.1 查询信息请求

查询信息类型的请求通过在结构定义中加OUT前缀，表示它与设置信息类型的请求在请求结构的一个或多个成员上的差异，OUT前缀表示要传递回调用进程的信息。有IN前缀的成员应被认为是只读的。所有查询信息请求的第一个成员，ulRequestID，为将来的使用保留，可以被忽略。

以下结构的定义可以在ndistapi.h中找到：

OID_TAPI_DEV_SPECIFIC

此请求用于产生扩展机制，使广域网NIC驱动程序提供对其他请求未描述特征的访问。扩展的意义与设备有关，应用程序要完全知道这些扩展才能利用它们。此函数使用NDIS_TAPI_DEV_SPECIFIC结构。

OID_TAPI_GET_ADDRESS_CAPS

此请求查询指定线路设备上的指定地址，以测定它的电话性能。此请求使用NDIS_TAPI_GET_ADDRESS_CAPS结构。

OID_TAPI_GET_ADDRESS_ID

此请求返回指定线路设备上与地址相关的地址ID（因线路不同而有不同的格式）。此请求使用NDIS_TAPI_GET_ADDRESS_ID结构。

OID_TAPI_GET_ADDRESS_STATUS

此请求查询指定地址，以得到它的当前状态。使用NDI _TAPI_GET_ADDRESS_STATUS结构。

OID_TAPI_GET_CALL_ADDRESS_ID

此请求为指定呼叫检索地址ID，使用NDIS_TAPI_GET_CALL_ADDRESS_ID结构。

OID_TAPI_GET_CALL_INFO

此请求返回指定呼叫的详细信息，使用NDIS_TAPI_GET_CALL_INFO结构。

OID_TAPI_GET_CALL_STATUS

此请求返回指定呼叫的当前状态，使用NDIS_TAPI_GET_CALL_STATUS结构。

OID_TAPI_GET_DEV_CAPS

此请求检索指定的线路以测定它的电话性能。返回信息对此线路设备上所有的地址都有效。使用NDIS_TAPI_GET_DEV_CAPS结构。

OID_TAPI_GET_DEV_CONFIG

此请求返回一个数据结构对象，它的内容与线路（广域网微端口NIC驱动程序）和设备类型有关，给出与线路设备一对一联系的设备的当前配置。此请求使用NDIS_TAPI_GET_DEV_CONFIG结构

OID_TAPI_GET_EXTENSION_ID

此请求为指定线路设备返回广域网微端口NIC驱动程序支持的扩展ID。此请求使用NDIS_TAPI_GET_DEV_CONFIG结构。

OID_TAPI_GET_ID

此请求为联合选定线路、地址、或呼叫的指定设备类型返回设备ID。使用NDIS_TAPI_GET_ID结构。

OID_TAPI_GET_LINE_DEV_STATUS

此请求查询指定的已打开线路设备的当前状态。返回的信息对此线路上所有地址是公用的。此请求使用NDIS_TAPI_GET_LINE_DEV_STATUS结构。

OID_TAPI_MAKE_CALL

此请求在指定线路上向指定目标地址发出呼叫。如果未要求使用缺省呼叫建立参数，可以指定呼叫参数。此请求使用NDIS_TAPI_MAKE_CALL结构。

OID_TAPI_NEGOTIATE_EXT_VERSION

此请求返回微端口设备希望在其上运行的最高版本扩展号，给出可能的扩展版本范围。此请求使用NDIS_TAPI_NEGOTIATE_EXT_VERSION结构。

OID_TAPI_OPEN

此请求打开指定的线路设备（给定设备ID），返回此设备的NIC驱动程序句柄。NIC驱动程序必需保留此设备的NDISTAPI驱动程序句柄。此请求使用NDIS_TAPI_OPEN结构。

OID_TAPI_PROVIDER_INITIALIZE

此请求初始化广域网微端口驱动程序，以便处理TAPI操作。此请求使用NDIS_TAPI_PROVIDER_INITIALIZE结构。

8.6.14.2 设置信息请求

除完成状态外，设置信息请求不返回任何信息。请求结构的所有成员应被认为是只读的，不能被标准广域网微端口驱动程序修改。所有查询信息请求的第一个成员，ulRequestID，保留在将来使用，可以被忽略。

以下结构的定义可以在ndistapi.h中找到：

OID_TAPI_ACCEPT

此请求接受指定的呼叫。它可以向呼叫方发送特定的用户到用户信息。此请求使用NDIS_TAPI_ACCEPT结构。

OID_TAPI_ANSWER

此请求应答指定的呼叫。它可以向呼叫方发送特定的用户到用户信息。此请求使用NDIS_TAPI_ANSWER结构。

OID_TAPI_CLOSE

此请求在完成或中止设备上所有未完成呼叫和异步请求后关闭指定的线路设备。此请求使用NDIS_TAPI_CLOSE结构。

OID_TAPI_CLOSE_CALL

此请求在完成或中止呼叫上所有未完成异步请求后释放指定的呼叫。此请求使用NDIS_TAPI_CLOSE_CALL结构

OID_TAPI_CONDITIONAL_MEDIA_DETECTION

当客户应用程序使用LINEMAPPER作为dwDeviceID调用lineOpen函数请求驱动程序搜索线路，以找出一个支持期望的介质模式和呼叫参数的线路时， NDISTAPI发出此请求。

NDISTAPI驱动程序的搜索基于期望的介质模式和此线路上其他正被监视的介质模式的并集，如果微端口驱动程序不能同时监视所有请求的介质模式，这就给微端口驱动程序指示的机会。在那时，如果微端口驱动程序能监视指定的介质模式集，支持LineCallParams中指定的性能，支持指定类型的呼叫，它将以成功指示回复。微端口驱动程序保持线路的活动介质监视模式不变。此请求使用NDIS_TAPI_CONDITIONAL_MEDIA_DETECTION结构。

OID_TAPI_DIAL

此请求在指定呼叫上拨指定号码。此请求使用NDIS_TAPI_DIAL结构。OID_TAPI_DROP

此请求丢弃或断连指定的呼叫。可以选择用户到用户信息作为呼叫断连的一部分传送。应用可以在任何时间发起此请求。成功返回OID_TAPI_DROP后，此呼叫变为空闲状态。此请求使用NDIS_TAPI_DIAL结构。

OID_TAPI_PROVIDER_SHUTDOWN

此请求关闭微端口驱动程序。微端口驱动程序应终止所有正在进行的活动，并为所有开放线路向NDISWAN作line-down指示。此请求使用NDIS_TAPI_PROVIDER_SHUTDOWN结构。

OID_TAPI_SECURE_CALL

此请求使呼叫免受所有可能影响呼叫的介质流的中断或冲突的影响。此请求使用NDIS_TAPI_SECURE_CALL结构。

OID_TAPI_SELECT_EXT_VERSION

此请求为指定线路设备选择指定的扩展版本。后续的请求操作应符合此扩展版本。此请求使用NDIS_TAPI_SELECT_EXT_VERSION结构。

OID_TAPI_SEND_USER_USER_INFO

此请求向指定呼叫的远程方发送用户到用户信息。此请求使用NDIS_TAPI_SEND_USER_USER_INFO结构。

OID_TAPI_SET_APP_SPECIFIC

此请求设置指定呼叫的LINECALLINFO结构的应用相关成员。此请求使用NDIS_TAPI_SET_APP_SPECIFIC结构。

OID_TAPI_SET_CALL_PARAMS

此请求为一个已存在的呼叫设置某些呼叫参数。此请求使用NDIS_TAPI_SET_CALL_PARAMS参数。

OID_TAPI_SET_DEFAULT_MEDIA_DETECTION

此请求通知微端口驱动程序新的介质模式集（替换原先的集合）以监测指定的线路。此请求使用NDIS_TAPI_SET_DEFAULT_MEDIA_DETECTION结构。

OID_TAPI_SET_DEV_CONFIG

此请求用先前用OID_TAPI_GET_DEV_CONFIG获得的数据恢复与线路设备一对一联系的设备的配置。此数据结构的内容与线路（广域网NIC驱动程序）和设备类型有关。此请求使用NDIS_TAPI_SET_DEV_CONFIG结构。

OID_TAPI_SET_MEDIA_MODE

此请求改变存储在呼叫的LINE_CALL_INFO结构中的呼叫的介质模式。此请求使用NDIS_TAPI_SET_DEV_CONFIG结构。

OID_TAPI_SET_STATUS_MESSAGE

此请求指定NIC驱动程序应为指定线路或它的任何地址的哪些状态改变事件产生通知信息。在缺省情况下，一个线路的地址和线路状态报告最初是停用的。此请求使用NDIS_TAPI_SET_STATUS_MESSAGE请求。

OID_TAPI_CONFIG_DIALOG

此请求检索用户模式动态连接库（用于配置指定设备）的名字。这个配置DLL应导出以下函数：

LONG

WINAPI

ConfigDialog(

IN HWND hwndOwner,

IN ULONG ulDeviceID,

IN LPCSTR lpszDeviceClass

);

此请求使用NDIS_TAPI_CONFIG_DIALOG结构。

8.7 使用支持电话服务的CoNDIS扩展

本节描述了在面向连接的环境中，CoNDIS广域网NIC微端口驱动程序如何使用NDIS函数实现电话服务。CoNDIS广域网微端口驱动程序通过NIDS与NDPROXY和NDISWAN驱动程序通信。NDISPROXY驱动程序通过电话服务提供者与电话应用程序通信。详见Platform SDK中的电话应用程序编程接口（TAPI）。

以下节更全面的描述了NDISPROXY驱动程序。以下节还描述了CoNDIS广域网微端口驱动程序如何注册和列举它的TAPI性能、建立线路、建立和关闭由TAPI请求发起的呼叫。这些描述中简单的提及了TAPI中的一些概念，读者应参阅Platform SDK以获得有关TAPI更详细的信息。TAPI如何模拟线路设备以及广域网微端口驱动程序如何保持连接的状态在8.6.2和8.6.4节中已有详细的描述。

n 8.7.1 NDPROXY概述

n 8.7.2 CoNDIS TAPI注册

n 8.7.3 CoNDIS TAPI初始化

n 8.7.4 建立外出呼叫

n 8.7.5 接受内入呼叫

n 8.7.6 CoNDIS TAPI关闭

n 8.7.7 语音流对呼叫管理器的要求

n 8.7.8 在面向连接NDIS之上支持电话服务的非广域网专用的扩展

8.7.1 NDPROXY概述

NDPROXY驱动程序是Windows Telephony服务的提供者。应用程序发出TAPI请求，Windows Telephony服务将此请求传递给NDPROXY。NDPROXY为Windows Telephony服务实现电话服务提供者接口（TSPI），NDPROXY通过NDIS与NIDSWAN驱动程序和CoNDIS广域网微端口驱动程序通信。这些CoNDIS广域网微端口驱动程序必需提供TAPI能力。

NDPROXY驱动程序通过将TAPI参数封装在NDIS结构中传递TAPI请求。NDPROXY向CoNDIS广域网微端口驱动程序呈现一个客户接口，向NDISWAN呈现一个呼叫管理者接口。NDISWAN向NDPROXY呈现一个客户接口。CoNDIS广域网微端口驱动程序向NDPROXY呈现一个呼叫管理者接口，向NDISWAN呈现一个CoNDIS微端口接口。

一个有TAPI能力的CoNDIS广域网微端口驱动程序将自己注册并初始化为广域网和TAPI服务二者的用户。在注册和初始化完成之后，用户层的应用就可以向Windows电话服务模块发出电话请求，此模块将TAPI请求转化为TSPI请求。Windows Telephony服务模块将这些请求传递给NDPROXY驱动程序。NDPROXY将TAPI请求的参数封装到NDIS结构中，并将请求发送给适当的CoNDIS广域网微端口驱动程序。传送这些请求的目的是建立、监视、拆卸线路和呼叫。

一个有TAPI能力的CoNDIS广域网微端口驱动程序还可以通知线路和呼叫状态的改变，例如内入呼叫的到达或远程断连。

8.7.2 CoNDIS TAPI注册

本节讨论CoNDIS广域网微端口驱动程序如何注册以及如何建立与NDISWAN和NDPROXY驱动程序的TAPI相关通信。

在CoNDIS广域网微端口驱动程序为一个或多个NIC注册微端口驱动程序入口后，以下操作将使NDISWAN和NDPROXY驱动程序与这些NIC以TAPI相关方式联合起来。

n CoNDIS广域网微端口驱动程序在它的MiniportInitialize函数内部调用NdisMCmRegisterAddressFamily函数，注册它的呼叫管理器入口指针和地址族类型CO_ADDRESS_FAMILY_TAPI_PROXY。通过这个操作，微端口驱动程序通知它实现TAPI服务。

n NDIS调用NDPROXY的ProtocolCoAfRegisterNotify函数通知NDPROXY新注册的地址族。NDPROXY的ProtocolCoAfRegisterNotify检查地址族数据并确定它可以使用集成到CoNDIS广域网微端口驱动程序内的呼叫管理者提供的TAPI服务。有TAPI能力的CoNDIS广域网微端口驱动程序是一个集成微端口呼叫管理器（MCM）。

n NDPROXY调用NdisClOpenAddressFamily函数打开联合CoNDIS广域网微端口驱动程序的TAPI-proxy地址族。NdisClOpenAddressFamily向NDIS注册NDPROXY的面向连接的入口指针。这些入口指针用于与CoNDIS广域网微端口驱动程序通信。

n NDPROXY调用NdisCmRegisterAddressFamily函数注册它的呼叫管理器入口指针和地址族类型CO_ADDRESS_FAMILY_TAPI。通过这样做，NDPROXY通知它实现TAPI服务。

n NDIS调用NDISWAN的ProtocolCoAfRegisterNotify函数通知NDISWAN新注册的地址族。NDISWAN的ProtocolCoAfRegisterNotify函数检查地址族数据并确定NDISWAN可以使用NDPROXY提供的TAPI服务。

n NDISWAN调用NdisClRegisterSap函数通知NDPROXY，NDISWAN可以在特定服务访问点（SAP）上接受内入呼叫。在此调用中，NDISWAN传递一个描述SAP的CO_SAP结构。NDISWAN将CO_SAP结构的成员SapType置为AF_TAPI_SAP_TYPE，指定SAP将用于TAPI呼叫。为一个特定的TAPI设备，NDISWAN将CO_SAP结构的成员Sap置为一个字符串。在TAPI应用程序调用lineGetID函数时，应用程序提供此串。NDPROXY应向NDISWAN通知所有定位到SAP上的内入呼叫。

8.7.3 CoNDIS TAPI初始化

本节讨论CoNDIS广域网微端口驱动程序如何为应用列举它的TAPI性能。这些TAPI性能包括：

n 微端口驱动程序支持的线路设备数。如modem、fax卡、以及ISDN卡等。

n 特定线路的信息。例如线路标识符和线路支持的信道地址（电话号码）等。

n 特定信道地址的信息。例如呼叫者的标识符（Caller ID）和活动呼叫的数目。

为了得到线路和信道地址的性能，NDPROXY发出请求检索底层硬件的信息。也就是说，NDPROXY查询CoNDIS广域网微端口驱动程序的TAPI性能。NDPROXY驱动程序调用NdisCoRequest函数查询微端口驱动程序的TAPI性能。在此调用中，NDPROXY传递一个NDIS_REQUEST结构。NDPROXY在NDIS_REQUEST结构中指定以下信息：

n 在RequestType成员中保存值NdisRequestQueryInformation。

n 在Oid成员保存对象标识符（OID），指定要从微端口驱动程序检索哪个TAPI性能。

n 返回TAPI性能的缓冲区保存在InformationBuffer成员中。

NDPROXY发送给CoNDIS广域网微端口驱动程序的所有请求都可以被异步或同步的完成。如果CoNDIS广域网微端口驱动程序确定不能立即结束查询，它将仅返回一个NDIS_STATUS_PENDING，并在完成请求后在它的ProtocolCoRequest函数内调用NdisMCmRequestComplete函数。

在CoNDIS广域网微端口驱动程序通知NDPROXY关于一个新地址族注册的消息后，NDPROXY查询以下OID以确定CoNDIS广域网微端口驱动程序和微端口驱动程序的NIC的TAPI相关性能。

n NDPROXY用OID_CO_TAPI_CM_CAPS查询微端口驱动程序，以确定微端口驱动程序的设备（提供TAPI服务的设备）支持的线路数。此OID还要求微端口驱动程序指出这些线路是否有不同的性能。

n NDPROXY然后用OID_CO_TAPI_LINE_CAPS查询微端口驱动程序，以确定指定线路的电话性能。此OID还要求微端口驱动程序指出此线路上的地址是否有不同的地址性能。

n 若先前的OID_CO_TAPI_CM_CAPS查询发现微端口驱动程序的设备只支持一个线路，或设备支持具有相同性能的多条线路，NDPROXY只需要查询一次OID_CO_TAPI_LINE_CAPS以获得设备的线路性能。在此情况下，微端口驱动程序返回的线路性能适用于此设备上所有的线路。

n 若设备支持有不同性能的多条线路，NDPROXY必须为每条线路查询一次OID_CO_TAPI_LINE_CAPS以获得每条线路的线路性能。

n 最后，NDPROXY用OID_CO_TAPI_ADDRESS_CAPS查询微端口驱动程序，以确定指定线路上指定地址的电话性能。

n 若先前的OID_CO_TAPI_LINE_CAPS查询发现线路只支持一个地址，或此线路上的所有地址具有相同的地址性能，NDPROXY只需要查询一次OID_CO_TAPI_ADDRESS_CAPS以获得此线路上所有地址的性能。

n 若线路支持有不同性能的多个地址，NDPROXY必须为此线路上的每个地址查询一次OID_CO_TAPI_LINE_CAPS。

NDPROXY驱动程序使用由TAPI列举OID获得的信息做以下工作：

n 为后续TAPI呼叫创建TAPI参数。

n 决定应接受还是拒绝后续的内入TAPI呼叫。

n 注册一个或多个TAPI服务访问点（SAP）。

8.7.4 建立外出呼叫

如果一个应用程序试图发起外出呼叫，它首先应打开一个线路。应用调用TAPI lineOpen函数打开线路。应用调用TAPI lineMakeCall函数并传递一个指向指定目标地址的指针，将一个电话呼叫放到先前打开的线路上。如不要求使用缺省的呼叫建立参数，应用还要传递一个指向LINECALLPARAMS结构的指针。如应用使用缺省的呼叫建立参数，这些参数由lineMakeCall在LINECALLPARAMS结构中返回。此结构的成员指定电话呼叫应如何建立。

这些TAPI函数调用导致NDPROXY作如下操作：首先通过CoNDIS广域网微端口驱动程序建立虚连接（VC），然后为了产生外出呼叫将TAPI参数封装在NDIS结构中。微端口驱动程序将使用这些TAPI参数建立外出呼叫。外出呼叫的建立过程如下：

n NDPROXY调用NdisCoCreateVc指示微端口驱动程序建立VC。在NDPROXY调用NdisCoCreateVc后，作为一个同步操作，NDIS调用集成在微端口驱动程序中的呼叫管理器的ProtocolCoCreateVc函数。NDIS向ProtocolCoCreateVc传递一个代表VC句柄。如果NdisCoCreateVc调用成功，NDIS会返回此句柄。ProtocolCoCreateVc执行所有必需的动态资源和结构的分配，微端口驱动程序的呼叫管理器需要这些结构以执行此VC上的后续操作。这些资源包括（并不只限于）内存缓冲区、数据结构、事件、以及其他类似资源。

n NDPROXY在CO_AF_TAPI_MAKE_CALL_PARAMETERS结构中为外出呼叫指定TAPI参数。NDPROXY用TAPI lineMakeCall函数传递来的信息填充此结构的成员：

n DestAddress 中存放目标地址。

n ulLineID 中存放线路标识符。

n LineCallParams 中存放LINECALLPARAMS结构。

n NDPROXY 将CO_AF_TAPI_MAKE_CALL_PARAMETERS结构放到CO_SPECIFIC_PARAMETERS结构的成员Parameters中，并将CO_SPECIFIC_PARAMETERS结构的成员Length置为CO_AF_TAPI_MAKE_CALL_PARAMETERS结构的长度。

n NDPROXY将CO_SPECIFIC_PARAMETERS结构放到CO_MEDIA_PARAMETERS结构的成员MediaSpecific中。

n NDPROXY将一个指向CO_MEDIA_PARAMETERS结构的指针放到CO_CALL_PARAMETERS结构的成员MediaParameters中。

n 一旦NDPROXY封装完TAPI参数，它就调用NdisClMakeCall函数指示外出呼叫。在此函数调用中，NDPROXY传递了一个指向填充了的CO_CALL_PARAMETERS结构的指针。NDIS然后调用CoNDIS广域网微端口驱动程序的呼叫管理器的ProtocolCmMakeCall函数。微端口驱动程序只需要检查嵌入在CO_CALL_PARAMETERS结构中的CO_AF_TAPI_MAKE_CALL_PARAMETERS结构，其他呼叫参数都没有意义。如果微端口驱动程序随后为外出呼叫激活了VC，它调用NdisMCmActiveVc函数并传递一个指向填充了的CO_CALL_PARAMETERS结构的指针。

n 微端口驱动程序与网络协商，为VC确定电话呼叫参数并为那些呼叫参数设置NIC之后，它调用NdisCmMakeCallComplete函数指出它已经准备好在VC上进行数据传输。在此调用中，微端口驱动程序必需传递此VC的句柄和对电话呼叫参数的修改。

n 微端口驱动程序必需修改CO_CALL_PARAMETERS结构的成员CallMgrParameters，以指定传输包的服务质量（QoS），例如带宽。为了设置CallMgrParameters，微端口驱动程序填充CO_CALL_MANAGER_PARAMETERS结构的成员并将CallMgrParameters指向此结构。例如，为了标识VC的传输和接收速度，微端口驱动程序必需CO_CALL_MANAGER_PARAMETERS结构的成员Transmit和Receive的PeakBandWidth成员。Transmit和Receive的类型是FLOWSPEC结构。FLOWSPEC结构的详细信息参见Platform SDK。

n 如果微端口驱动程序修改了电话呼叫参数，它必须将CO_CALL_PARAMETERS结构的Flags置为CALL_PARAMETERS_CHANGED。作为微端口驱动程序调用NdisCmMakeCallComplete函数的结果，NDIS调用NDPROXY的ProtocolClMakeCallComplete函数完成由NdisClMakeCall发起的异步操作。

n 在微端口驱动程序成功完成外出呼叫后，NDPROXY通知TAPI应用程序呼叫已连接。然后此TAPI应用程序调用TAPI lineGetID函数通知NDPROXY定位适当的CoNDIS客户。在此lineGetID调用中，TAPI应用程序为特定TAPI设备（应用程序需要它的句柄）提供了一个字符串。NDPROXY使用此字符串定位CoNDIS客户（已经为特定TAPI注册了SAP）。如果这个CoNDIS客户是NDISWAN，此字符串是NDIS。如果NDPROXY定位到一个与TAPI应用程序传递的字符串匹配的SAP，它就调用NdisMCmCreateVc建立一个连接边界点，在此边界点上它可以发出外出呼叫通知。NDIS然后调用NDISWAN的ProtocolCoCreateVc函数并传递一个代表VC的句柄。

n 在NDPROXY建立连接边界点后，它调用NdisCmDispatchIncomingCall函数通知NDISWAN外出呼叫。在此调用中，NDPROXY传递了一个包含外出呼叫参数的CO_AF_MAKE_CALL_PARAMETER结构。NDIS然后调用NDISWAN的ProtocolClIncomingCall函数，在此调用中NDISWAN可以接受或拒绝请求的连接。如果NDISWAN改变了传递给它的呼叫参数，它必须将CO_CALL_PARAMETERS结构的Flags设置为CALL_PARAMETERS_CHANGED。

n 在决定是否接受连接并可能改变呼叫参数后，NDISWAN调用ProtocolClIncomingCallComplete函数。NDIS然后调用微端口驱动程序的ProtocolCmIncomingCallComplete函数。微端口驱动程序调用NdisCmDispatchCallConnected还是NdisCmDispatchIncomingCloseCall函数，取决于NDISWAN是否接受了外出呼叫以及微端口驱动程序是否接受NDISWAN对呼叫参数的改变，。NdisCmDispatchCallConnected通知NDISWAN可以在NDISWAN为外出呼叫建立的VC上开始数据传输。NdisCmDispatchIncomingCloseCall通知NDISWAN 和NDISWAN拆卸外出呼叫。

n 在NDISWAN接受外出呼叫之后，NDPROXY调用NdisCoGetTapiCallId函数检索标识VC的NDISWAN环境的字符串。NDPROXY将此字符串传递回TAPI应用程序。TAPI应用程序用此VC环境字符串完成对lineGetID的调用。

8.7.5 接受内入呼叫

在应用可以接受内入呼叫之前，它必须要打开一个线路。应用调用TAPI lineOpen函数打开线路。对此TAPI函数的调用导致下层驱动程序为了准备接受内入呼叫将TAPI参数封装到NDIS结构中。CoNDIS广域网微端口驱动程序接收到一个内入呼叫后，首先微端口驱动程序必须与NDPROXY驱动程序建立一个虚连接（VC），然后向NDPROXY通知内入呼叫。NDPROXY然后通过TAPI通知应用程序。内入呼叫过程如下：

n NDPROXY在CO_AF_TAPI_SAP结构中为内入连接指定TAPI参数。NDPROXY用从TAPI lineOpen函数传递来的以下信息填充此结构：

n ulLineID 中存放开放线路的标识。

n ulAddressID 中存放内入连接的地址。

n ulMediaModes 中存放内入连接的信息流的介质模式。

n NDPROXY用CO_AF_TAPI_SAP结构覆盖CO_SAP结构的成员Sap，并置CO_SAP结构的成员SapLength为CO_AF_TAPI_SAP结构长度。NDPROXY还应将CO_SAP结构的SapType置为AF_TAPI_SAP_TYPE。

n 在NDPROXY封装完TAPI参数后，NDPROXY调用NdisClRegisterSap函数使它自己准备好接收内入呼叫。在此函数调用中，NDPROXY传递一个指向CO_SAP结构的指针，此结构指定NDPROXY可以在其上接收内入呼叫的服务访问点（SAP）。NDIS将此CO_SAP结构传递给CoNDIS广域网微端口驱动程序的呼叫管理器（MCM）的ProtocolCmRegisterSap函数。ProtocolCmRegisterSap根据需要与网络控制设备或其他介质相关代理通信，来为NDPROXY向网络注册SAP。在微端口驱动程序注册完SAP后，它就可以接受直接向此SAP提出的内入呼叫。

n 从网络来的信号消息通知CoNDIS广域网微端口驱动程序内入呼叫。微端口驱动程序为呼叫从这些信号消息中提取呼叫参数，包括内入呼叫寻址的SAP。

n 在通知NDPROXY内入呼叫之前，微端口驱动程序调用NdisMCmCreateVc函数通知NDPROXY 建立VC。NDPROXY分配并初始化VC需要的资源并存储VC的句柄。

n CoNDIS广域网微端口驱动程序在CO_AF_TAPI_INCOMING_CALL_PARAMETERS结构中为内入呼叫设置TAPI参数。微端口驱动程序用从信号消息中提取的信息填充此结构的成员：

n ulLine 中存放线路标识符。

n ulAddressID 中存放内入呼叫的地址。

n ulFlags 中的CO_TAPI_FLAG_INCOMING_CALL位。ulFlags的其他位被保留且必需被置为0。

n LineCallInfo 中存放LINECALLPARAMS结构。LINECALLPARAMS的成员为内入呼叫指定TAPI呼叫参数。

n 微端口驱动程序用CO_AF_TAPI_INCOMING_CALL_PARAMETERS结构覆盖CO_SPECIFIC_PARAMETERS结构的Parameters成员，并置CO_SPECIFIC_PARAMETERS结构Length成员为CO_AF_TAPI_INCOMING_CALL_PARAMETERS的长度。

n 微端口驱动程序将CO_SPECIFIC_PARAMETERS结构放到CO_MEDIA_PARAMETERS结构的成员MediaSpecific中。

n 微端口驱动程序将一个指向CO_MEDIA_PARAMETERS结构的指针放到CO_CALL_PARAMETERS结构的MediaParameters成员中。

n 微端口驱动程序还必须设置CO_CALL_PARAMETERS结构的CallMgrParameters成员以指定传送包的服务质量（QoS），如带宽。为了设置CallMgrParameters，微端口驱动程序填充CO_CALL_MANAGER_PARAMETERS结构的成员并将CallMgrParameters指向此结构。例如，为了标识VC的传输和接收速度，微端口驱动程序必需CO_CALL_MANAGER_PARAMETERS结构的成员Transmit和Receive的PeakBandWidth成员。Transmit和Receive的类型是FLOWSPEC结构。FLOWSPEC结构的详细信息参见Platform SDK。

n 在微端口驱动程序封装TAPI参数和填充CO_CALL_MANAGER_PARAMETERS的CallMgrParameters成员后，它调用NdisMCmDispatchIncomingCall函数向NDPROXY指示内入呼叫。在此调用中，微端口驱动程序传递以下参数：

n 标识内入呼叫寻址的SAP的句柄。

n 标识VC的句柄。

n 指向CO_CALL_PARAMETERS结构的指针。

n NDPROXY返回微端口驱动程序NDIS_STATUS_PENDING，这样NDPROXY就可以异步的完成NdisMCmDispatchIncomingCall。

n 在TAPI应用程序用lineAnswer函数应答内入呼叫后，NDPROXY调用NdisClIncomingCallComplete函数。NDIS然后调用微端口驱动程序的ProtocolCmIncomingCallComplete函数。NDPROXY返回NDIS_STATUS_SUCCESS表示接受呼叫参数。如果NDPROXY发现呼叫参数不可接受，它可以通过将CO_CALL_PARAMETERS结构的Flags置为CALL_PARAMETERS_CHANGED并提供修改了的呼叫参数，请求改变呼叫参数。如果NDPROXY接受内入呼叫，微端口驱动程序应发送信号消息指示正在呼叫的实体呼叫被接受。否则，微端口驱动程序应发送信号消息指示呼叫被拒绝。如果NDPROXY请求改变呼叫参数，微端口驱动程序发送信号消息请求对呼叫参数的改变。

n 微端口驱动程序激活VC并调用NdisMCmActivateVc函数通知NDPROXY微端口驱动程序已准备好在VC上传输数据。

n 如果拒绝呼叫，微端口驱动程序调用NdisMCmDeactivateVc函数去活微端口驱动程序为内入呼叫创建的VC。然后微端口驱动程序调用NdisMCmDeleteVc函数删除VC。

n 根据NDPROXY是否接受内入呼叫和端到端连接是否被成功建立，微端口驱动程序调用NdisMCmDispatchCallConnected或NdisMCmDispatchIncomingCloseCall函数。注意，如果远程正在呼叫的实体拆卸了呼叫，它将发送信号消息指示端到端连接没有被成功建立。NdisMCmDispatchCallConnected通知NDPROXY可以在VC上开始数据传输。NdisMCmDispatchIncomingCloseCall通知NDPROXY拆除内入呼叫。

n 如果NDPROXY被指示拆除内入呼叫，它调用NdisClCloseCall函数确认它不会在此VC上试图发送或接收数据。NDIS然后调用微端口驱动程序的ProtocolCmCloseCall函数。微端口驱动程序然后调用NdisMCmDeactivateVc函数去活VC。在VC被去活后，微端口驱动程序调用NdisMCmDeleteVc函数删除VC。

n 在TAPI应用程序接受内入呼叫和NDPROXY通知应用程序呼叫已连接后，应用调用TAPI lineGetID函数通知NDPROXY定位适当的CoNDIS客户。在此lineGetID调用中，TAPI应用程序为特定TAPI设备（应用程序需要它的句柄）提供了一个字符串。NDPROXY使用此字符串定位CoNDIS客户（已经为特定TAPI注册了SAP）。如果这个CoNDIS客户是NDISWAN，则此字符串是NDIS。如果NDPROXY定位到一个与TAPI应用程序传递的字符串匹配的SAP，它就调用NdisMCmCreateVc建立一个连接边界点，在此边界点上它可以发出内入呼叫通知。NDIS然后调用NDISWAN的ProtocolCoCreateVc函数并传递一个代表VC的句柄。

n 在NDPROXY建立连接边界点后，它调用NdisCmDispatchIncomingCall函数向NDISWAN通知内入呼叫。在此调用中，NDPROXY传递了一个包含内入呼叫参数的CO_AF_MAKE_CALL_PARAMETER结构。NDIS然后调用NDISWAN的ProtocolClIncomingCall函数，在此调用中NDISWAN可以接受或拒绝请求的连接。

n 在决定是否接受连接并可能改变呼叫参数后，NDISWAN调用ProtocolClIncomingCallComplete函数。NDIS然后调用微端口驱动程序的ProtocolCmIncomingCallComplete函数。根据NDISWAN是否接受了内入呼叫以及微端口驱动程序是否接受NDISWAN对呼叫参数的改变，微端口驱动程序调用NdisCmDispatchCallConnected或NdisCmDispatchIncomingCloseCall函数。NdisCmDispatchCallConnected通知NDISWAN可以在NDISWAN为内入呼叫建立的VC上开始数据传输。NdisCmDispatchIncomingCloseCall通知NDISWAN 和NDISWAN拆卸内入呼叫。

n 在NDISWAN接受内入呼叫之后，NDPROXY调用NdisCoGetTapiCallId函数检索标识NDISWAN 的VC环境的字符串。NDPROXY将此字符串传递回TAPI应用程序。TAPI应用程序用此VC环境字符串完成对lineGetID的调用。

8.7.6 CoNDIS TAPI关闭

TAPI会话在CoNDIS广域网微端口驱动程序为应用程序列举完它的TAPI性能后开始。在一个会话中，可以打开一个或多个线路，并且可以发布一个或多个呼叫。在一个线路的开放期内，可以发布，然后关闭或丢弃多个呼叫。在会话期间，一个或多个线路可以经历多次从开放到关闭的转换。本节描述了微端口驱动程序如何处理这样的转换。

关闭呼叫

一个进行中的呼叫可以被本地或远程节点关闭。呼叫在本地节点被关闭，是因为拥有此呼叫的句柄的最后一个应用关闭了这个句柄，或可能因为微端口驱动程序的MiniportHalt或MiniportRest函数被调用。如果远程节点终止了一个正在运行的呼叫，微端口驱动程序必须通知上层拆卸此呼叫。

如果本地节点的应用程序要关闭呼叫，它必须断开呼叫。应用程序调用TAPI lineDrop函数断开呼叫。此TAPI函数调用导致NDPROXY调用NdisClCloseCall函数并传递一个代表VC的句柄。NDIS然后调用CoNDIS广域网微端口驱动程序的ProtocolCmCloseCall函数。微端口驱动程序应向NDPROXY返回NDIS_STATUS_PENDING，这样微端口驱动程序就可以异步的完成NdisClCloseCall。

微端口驱动程序的ProtocolCmCloseCall必须与网络控制设备通信以终止本地节点和远程节点间的连接。然后微端口驱动程序必需调用NdisMCmDeactivateVc函数指示用于此呼叫的VC的失效。

在微端口驱动程序终止连接后，ProtocolCmCloseCall调用NdisMCmCloseCallComplete函数完成呼叫关闭。

如果远程节点中止了一个正在运行的呼叫，微端口驱动程序调用NdisCmDispatchIncomingCloseCall函数，通知NDISWAN和NDPROXY拆除内入呼叫。

关闭线路

在拥有此线路的开放句柄的最后一个应用关闭了这个句柄后，线路将被关闭。应用程序调用TAPI lineClose函数关闭线路。此TAPI函数调用导致NDPROXY发起对此线路上所有呼叫的关闭操作。微端口驱动程序应丢弃这些呼叫并清除它们的状态。

关闭会话

上层模块或CoNDIS广域网微端口驱动程序都可以发起会话的终止。在最后一个进程与高层电话模块分离后，应通知NDPROXY终止每个注册适配器的会话。为了完成这些操作，NDPROXY驱动程序调用NdisClCloseAddressFamily函数并传递TAPI地址族的句柄。NDIS然后调用微端口驱动程序的ProtocolCmCloseAf函数。微端口驱动程序应终止指定适配器上进行的相关活动并释放所有相关资源。在调用NdisClCloseAddressFamily后，客户应认为TAPI地址族的句柄已无效。

驱动程序发起的会话终止可以在微端口驱动程序被MiniportHalt函数卸载时发生。一般情况下，微端口驱动程序应完成所有未完成的NDPROXY请求并通知NDISWAN呼叫正在被关闭。如果后来微端口驱动程序又被重新加载，它应经历与先前描述相同的初始化过程。

如果CoNDIS广域网微端口驱动程序进行某些迫使所有客户和驱动程序完全重新初始化的动态重配置，它也可能会发起会话终止。例如，如果适配器的线路设备模块（如支持的线路设备数）被改变。

8.7.7 语音流对呼叫管理器的要求

本节描述了呼叫管理器或集成的微端口呼叫管理器（MCM）为了在面向连接介质上支持语音流必需达到的要求。这些需求可被分为以下几类：

n 8.7.7.1 响应OID_CO_TAPI_LINE_CAPS查询

n 8.7.7.2 为外出呼叫指定参数

n 8.7.7.3 为内入呼叫指定参数

8.7.7.1 响应OID_CO_TAPI_LINE_CAPS查询

为了响应OID_CO_TAPI_LINE_CAPS查询，呼叫管理器或MCM返回一个包含LINE_DEV_CAPS的CO_TAPI_LINE_CAPS结构。为了支持语音流，呼叫管理器或MCM必须在LINE_DEV_CAPS结构中指定下列值：

n ulMediaModes

此字段应包含LINEMEDIAMODE_AUTOMATEDVOICE，它在TAPI 3.0中映射为TAPIMEDIAMODE_AUDIO。

n ulAddressTypes

必须适当地填充此字段。参见Platform SDK中对dwAddressTypes的描述。此字段不能为零。

n ulGenerateDigitModes

用LINEDIGITMODE常数的按位或填充此字段。LINEDIGITMODE常数指定可以在线路上产生的数字模式。LINEDIGITMODE常数的详细信息参见Platform SDK中对dwGenerateDigitModes的描述。

n ulMonitorDigitModes

用LINEDIGITMODE常数的按位或填充此字段。LINEDIGITMODE常数指定可以在线路上被检测到的数字模式。LINEDIGITMODE常数的详细信息参见Platform SDK中对dwMonitorDigitModes的描述。

8.7.7.2 为外出呼叫指定参数

在产生外出呼叫时，支持语音流的呼叫管理器或MCM必须为CO_CALL_MANGET_PARAMETERS结构提供下列值：

n 最大发送SDU长度（CallMgrParameters->Transmit.MaxSduSize）。

n 最大接收SDU长度（CallMgrParameters->Receive.MaxSduSize）。

一个支持除CO_ADDRESS_FAMILY_TAPI_PROXY之外其他地址族的呼叫管理器或MCM，在响应OID_CO_TAPI_TRANSLATE_TAPI_CALLPARAMS请求将TAPI呼叫参数转换成NDIS呼叫参数时，填充这些值。

支持CO_ADDRESS_FAMILY_TAPI_PROXY地址族的呼叫管理器或MCM，在函数ProtocolCmMakeCall的中将这些值写入CO_CALL_MANGER_PARAMETERS结构。注意传递给ProtocolCmMakeCall函数的最大SDU长度是错误的。ProtocolCmMakeCall必须用正确值覆盖相应的错误值。

8.7.7.3 为内入呼叫指定参数

在用Ndis(M)CmDispatchIncomingCall指示内入呼叫时，支持语音流的呼叫管理器或MCM必须在CO_CALL_MANGET_PARAMETERS结构中指定以下值：

n 最大发送SDU长度（CallMgrParameters->Transmit.MaxSduSize）。

n 最大接收SDU长度（CallMgrParameters->Receive.MaxSduSize）。

另外，呼叫管理器或MCM在LINE_CALL_INFO结构中指定以下值：

n ulMediaMode

此字段应包含LINEMEDIAMODE_AUTOMATEDVOICE，它在TAPI 3.0中变换成TAPIMEDIAMODE_AUDIO。

n ulCallerIDFlags

n ulCallerIDSize

n ulCallerIDOffset

n ulCallerIDNameSize

n ulCallerIDNameOffset

n ulCalledIDFlags

n ulCalledIDSize

n ulCalledIDOffset

n ulCalledIDNameSize

n ulCalledIDNameOffset

n ulCallerIDAddressType

n ulCalledIDAddressType

一个支持除CO_ADDRESS_FAMILY_TAPI_PROXY之外其他地址族的呼叫管理器或MCM，在响应OID_CO_TAPI_TRANSLATE_TAPI_CALLPARAMS请求时，指定上述LINE_CALL_INFO成员。

一个支持CO_ADDRESS_FAMILY_TAPI_PROXY地址族的呼叫管理器或MCM，在提供给Ndis(M)CmDispatchIncomingCall 的CO_CALL_MANGER_PARAMETERS结构的介质相关部分，指定上述LINE_CALL_INFO成员。

对LINE_CALL_INFO成员的描述详见Platform SDK中的LINECALLINFO结构。

8.7.8 在面向连接NDIS之上支持电话服务的非广域网专用的扩展

本节描述了在面向连接NDIS之上提供TAPI的非广域网专用的扩展。此扩展是一些NDIS/TAPI转换OID。这些扩展允许非广域网专用的呼叫管理器和集成微端口呼叫管理器（MCM）将TAPI参数转换成NDIS参数，或将NDIS参数转换成TAPI参数。例如，这些扩展允许支持ATM的呼叫管理器和MCM提供面向连接介质上TAPI访问。在面向连接NDIS上提供TAPI支持的广域网专用的扩展的信息参见8.7使用支持电话服务的CoNDIS扩展中的8.7.2到8.7.6。

这些NDIS/TAPI转换OID不能用于分别用NdisCmRegisterAddressFamily或NdisMCmRegisterAddressFamily注册CO_ADDRESS_FAMILY_TAPI_PROXY的呼叫管理器或MCM。这些呼叫管理器和MCM，与TAPI客户一样，应将TAPI参数封装到面向连接结构中。参见8.7使用支持电话服务的CoNDIS扩展。

NDIS/TAPI转换OID有：

n OID_CO_TAPI_TRANSLATE_TAPI_CALLPARAMS

此OID请求呼叫管理器或MCM将客户提供的TAPI呼叫参数转换成NDIS呼叫参数。客户一般使用呼叫管理器或MCM返回的NDIS呼叫参数作为NdisClMakeCall的输入（以CO_CALL_PARAMETERS格式）。客户使用NdisClMakeCall发起面向连接的呼叫。

n OID_CO_TAPI_TRANSLATE_NDIS_CALLPARAMS

此OID请求呼叫管理器或MCM为一个内入呼叫将NDIS呼叫参数（在CO_CALL_PARAMETERS结构中传递给客户的ProtocolClIncomingCall函数）转换为TAPI呼叫参数。客户使用呼叫管理器或MCM返回的TAPI呼叫参数决定是否接受内入呼叫。

n OID_CO_TAPI_TRANSLATE_SAP

此OID请求呼叫管理器或MCM用客户提供的TAPI呼叫参数准备一个或多个NDIS SAP。客户一般将呼叫管理器或MCM返回的NDIS SAP作为NdisClRegisterSap的输入（以CO_SAP格式），客户通过此函数注册一个接受内入呼叫的SAP。


任务卸载

为了显著的提高性能，Microsoft TCP/IP传输层可以将以下任务推卸给拥有适当任务卸载能力的NIC：

n 校验和任务

TCP/IP传输层能够卸载对IP和/或TCP校验和的计算和/或验证。Window® 2000的最初版本不支持卸载UDP校验和；将来Window® 2000的服务包和升级版本可能会支持卸载UPD校验和。

n IP安全性任务

TCP/IP传输层能够卸载对身份验证报头（authentication headers，AH）和/或封装安全负载（encapsulating security payloads，ESP）的加密校验和的计算和/或验证。TCP/IP传输层还能够卸载对ESP负载的加密和解密。

n 大TCP包分段

TCP/IP传输层能够卸载对大TCP包的分段处理。

Window® 2000中的TCP/IP传输层可以处理IPv4包（但不能处理IPv6包），因此只对IPv4支持任务卸载。

从TCP/IP传输层卸载任务到NIC包括两个主要操作：

n 查询并启动NIC的任务卸载功能（参加9.1和9.2）。

n 传递涉及卸载任务的per-packet信息（扩展的带外数据）。参见9.4、9.5、9.6、9.7节。

9.1 查询任务卸载能力

TCP/IP传输层通过查询OID_TCP_TASK_OFFLOAD确定NIC的任务卸载能力。在此查询中，TCP/IP传输层在InformationBuffer中提供一个NDIS_TASK_OFFLOAD_HEADER，定义如下：

typedef struct _NDIS_TASK_OFFLOAD_HEADER

{

ULONG Version;

ULONG Size;

ULONG Reserved;

UCHAR OffsetFirstTask;

NDIS_ENCAPSULATION_FORMAT EncapsulationFormat;

} NDIS_TASK_OFFLOAD，*PNDIS_TASK_OFFLOAD;

NDIS_TASK_OFFLOAD_HEADER结构指定以下内容：

n TCP/IP传输层支持的任务卸载版本。

n NDIS_TASK_OFFLOAD_HEADER结构的长度（用于检查此结构的版本）。

n 从InformationBuffer的开始到第一个NDIS_TASK_OFFLOAD结构的偏移量（字节）。

n 在TCP/IP传输层和微端口之间传递的发送和接收包的封装格式。

使用OID_TASK_OFFLOAD_HEADER提供的信息，微端口或它的NIC能够在发送或接收包中定位第一个IP报头的开始。这是执行卸载任务的先决条件。

作为对OID_TCP_TASK_OFFLOAD查询的响应，微端口在InformationBuffer中返回NDIS_TASK_OFFLOAD_HEADER，后跟一个或多个NDIS_TASK_OFFLOAD结构。NDIS_TASK_OFFLOAD结构定义如下：

typedef struct _NDIS_TASK_OFFLOAD

{

ULONG Version;

ULONG Size;

NDIS_TASK task;

ULONG OffsetNextTask;

ULONG TaskBufferLength;

UCHAR TaskBuffer[1];

} NDIS_TASK_OFFLOAD，*PNDIS_TASK_OFFLOAD;

NDIS_TASK_OFFLOAD结构指定以下内容：

n 指定卸载任务的版本。

n NDIS_TASK_OFFLOAD结构的长度（用于检查此结构的版本）。

n 要卸载的任务。

n 从InformationBuffer的开始到下一个NDIS_TASK_OFFLOAD结构的偏移量（字节）。

n TaskBuffer 的长度。

n 一个包含任务相关信息的变长数组。

每个NDIS_TASK_OFFLOAD结构指定一个微端口的NIC支持的任务卸载能力。如果微端口的NIC支持特定任务卸载能力的多个版本，它将为每个版本返回一个NDIS_TASK_OFFLOAD结构。

9.1.1 报告NIC的校验和性能

如果微端口的NIC能够计算和/或验证IP，TCP，和/或UDP校验和，微端口将在NDIS_TASK_TCP_IP_CHECKSUM结构中指出上述性能。微端口用NDIS_TASK_TCP_IP_CHECKSUM结构覆盖NDIS_TASK_OFFLOAD结构的TaskBuffer，并在InformationBuffer中返回NDIS_TASK_OFFLOAD结构作为对OID_TCP_TASK_OFFLOAD查询的响应。

NDIS_TASK_TCP_IP_CHECKSUM结构的定义如下：

typedef struct _NDIS_TASK_TCP_IP_CHECKSUM

{

struct

{

ULONG IpOptionSupported:1’

ULONG TcpOptionSupported:1;

ULONG TcpChecksum:1;

ULONG UdpChecksum:1;

ULONG IpChecksum:1;

}V4Transmit;

struct

{

ULONG IpOptionSupported:1’

ULONG TcpOptionSupported:1;

ULONG TcpChecksum:1;

ULONG UdpChecksum:1;

ULONG IpChecksum:1;

}V4Receive;

struct

{

ULONG IpOptionSupported:1’

ULONG TcpOptionSupported:1;

ULONG TcpChecksum:1;

ULONG UdpChecksum:1;

}V6Transmit;

struct

{

ULONG IpOptionSupported:1’

ULONG TcpOptionSupported:1;

ULONG TcpChecksum:1;

ULONG UdpChecksum:1;

}V6Receive;

微端口为IPv4发送包和IPv4接收包指示以下校验能力：

n NIC是否能够为在IP报头中包含IP选项的包计算和/或验证校验和。

n NIC是否能够为在TCP报头中包含TCP选项的包计算和/或验证校验和。

n NIC为发送包计算为接收包验证的校验和（IP，TCP，和/或UDP）的类型。Window® 2000的最初版本不支持卸载UDP校验；将来Window® 2000的服务包和升级版本可能会支持卸载UPD校验。

微端口可以为IPv6接收包和发送包指示类似的校验和能力；但是因为Window 2000的最初版本只能处理IPv4包，它从不为IPv6包激活校验能力。

9.1.2 报告NIC的IP安全性性能

微端口在NDIS_TASK_IPSEC结构中指定它的NIC的安全性性能。微端口用此结构覆盖NDIS_TASK_OFFLOAD结构的TaskBuffer，然后为了响应OID_TCP_TASK_OFFLOAD查询，微端口在InformationBuffer中返回NDIS_TASK_OFFLOAD结构。

NDIS_TASK_IPSEC结构定义如下：

typedef struct _NDIS_TASK_IPSEC

{

struct

{

ULONG AH_ESP_COMBIND:1;

ULONG TRANSPORT_TUNNEL_COMBINE:1;

ULONG V4_OPTION:1;

ULONG RESERVED:1;

}Supported;

struct

{

ULONG MD5:1;

ULONG SHA_1:1;

ULONG Transport:1;

ULONG Tunnel:1;

ULONG Send:1;

ULONG Receive:1;

}V4AH;

struct

{

ULONG DES:1;

ULONG RESERVED:1;

ULONG TRIPLE_DES:1;

ULONG NULL_ESP:1;

ULONG Transport:1;

ULONG Tunnle:1;

ULONG Send:1;

ULONG Receive:1;

}V4ESP

}NDIS_TASK_IPSEC，*PNDIS_TASK_IPSEC；

微端口在NDIS_TASK_IPSEC结构中指定下列通用性能：

n 它的NIC是否能够在包上执行组合的IP安全操作。也就是说，处理既包含身份验证报头（AH）又包含封装安全负载（ESP）的包，这个包的格式是[IP][AH][ESP][包的剩余部分]。

n 它的NIC是否在接收和发送包的传输模式和隧道模式两部分都可以进行IP安全处理。包的传输模式部分属于端到端安全。包的隧道模式部分属于隧道安全。

n 它的NIC是否能够在IP报头中包含IP选项的包上执行IP安全操作。

为了给AH负载和身份确认信息计算和/或验证加密校验和，微端口为其NIC指定以下性能：

n NIC可以使用的完全性算法（MD5或SHA 1）。

n NIC是否可以为包的传输模式部分处理AH安全性负载。

n NIC是否可以为包的隧道模式部分处理AH安全性负载。

n NIC是否可以为发送包处理AH安全性负载。

n NIC是否可以为接收包处理AH安全性负载。

为了处理ESP负载，微端口为其NIC指定以下性能：

n NIC可以使用的机密性算法（DES和/或三重DES）。

n NIC是否支持空加密（null encryption），即没有加密但有身份验证散列的ESP负载。

n NIC能否为包的传输层模式部分进行ESP处理。

n NIC能否为包的隧道模式部分进行ESP处理。

n NIC能否为发送进行ESP处理。

n NIC能否为接收进行ESP处理。

9.1.3 报告NIC的TCP包分段性能

微端口在NDIS_TASK_TCP_LARGE_SEND结构中指定它的NIC的包分段性能。微端口用此结构覆盖NDIS_TASK_OFFLOAD结构的TaskBuffer，然后为了响应OID_TCP_TASK_OFFLOAD查询，微端口在InformationBuffer中返回NDIS_TASK_OFFLOAD结构。

NDIS_TASK_TCP_LARGE_SEND结构的定义如下：

typedef struct _NDIS_TASK_TCP_LARGE_SEND

{

ULONG MaxOffloadSize;

ULONG MinSegmentCount;

BOOLEAN TcpOptions;

BOOLEAN IpOptions;

} NDIS_TASK_TCP_LARGE_SEND，*PNDIS_TASK_TCP_LARGE_SEND；

微端口必需在NDIS_TASK_TCP_LARGE_SEND结构中指定以下信息：

n MaxOffLoadSize，在一个大TCP包中，TCP/IP传输层可以传递给微端口的用户数据的最大字节数。

n MinSegmentCount，在TCP/IP传输层可以将分段推卸给NIC之前，大TCP必须被分割成的最小段数。

n NIC是否可以分割包含TCP选项的TCP包。

n NIC是否可以分割包含IP选项的TCP包。

9.2 启用任务卸载能力

TCP/IP传输层通过设置OID_TCP_TASK_OFFLOAD启用NIC的任务卸载能力。在此设置操作中，TCP/IP传输层在InformationBuffer中传递一个NDIS_TASK_OFFLOAD_HEADER结构，在此结构后紧跟若干NDIS_TASK_OFFLOAD结构，每个NDIS_TASK_OFFLOAD对应要卸载的任务。

微端口必需检查InformationBuffer中的每个NDIS_TASK_OFFLOAD以确定哪个卸载任务正被启用。微端口还必需检查每个NDIS_TASK_OFFLOAD的TaskBuffer中的任务相关结构（NDIS_TASK_TCP_IP_CHECKSUM，NDIS_TASK_IPSEC，或NDIS_TASK_TCP_LARGE_SEND），以确定正在启用特定卸载任务的哪个能力。

TCP/IP传输层只用一个OID_TCP_TASK_OFFLOAD设置为NIC启用所有卸载能力。微端口必需使后续所有设置OID_TCP_TASK_OFFLOAD的试图失败。

9.3 停用任务卸载能力

为了停用NIC的所有任务卸载能力，TCP/IP传输层设置OID_TCP_TASK_OFFLOAD，在InformationBuffer中只传递一个NDIS_TASK_OFFLOAD_HEADER结构。TCP/IP传输层将NDIS_TASK_OFFLOAD_HEADER结构的OffsetFirstTask成员置为0。

9.4 访问Per-Packet信息

TCP/IP传输层和微端口之间用包的Per-Packet信息互相传递关于任务卸载操作的信息。Per-Packet信息是用NDIS_PACKET_EXTENSION结构表示的带外信息，此结构的定义如下：

typedef struct _NDIS_PACKET_EXTENSION

{

PVOID NdisPacketInfo[MaxPerPacketInfo];

} NDIS_PACKET_EXTENSION，*PNDIS_PACKET_EXTENSION;

用NdisAllocatePacket分配的每个包描述信息都有一个相关的NDIS_PACKET_EXTENSION结构。此结构包含一个指针数组（PVOID），每个数组包含或指向一个与包描述信息相关的Per-Packet信息的特定类型。

驱动程序使用NDIS_PACKET_EXTENSION_FROM_PACKET宏或NDIS_PER_PACKET_INFO_FROM_PACKET宏访问Per-Packet信息。

NDIS_PACKET_EXTENSION_FROM_PACKET宏的定义如下：

PNDIS_PACKET_EXTENSION

NDIS_PACKET_EXTENSION_FROM_PACKET（

IN PNDIS_PACKET Packet

）；

此宏返回一个指向与给定包描述信息相关的NDIS_PACKET_EXTENSION结构的指针。当驱动程序需要访问与包描述信息相关的多个类型的Per-Packet信息时，它应调用此宏。在获得指向NDIS_PACKET_EXTENSION结构的指针后，驱动程序可以使用适当的数组索引访问特定类型的Per-Packet信息。属于卸载任务的索引值有：

TcpIpCheckSumPacketInfo

索引一个指向NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构的指针。此结构为卸载的校验和操作向微端口指定Per-Packet信息。

IpSecPacketInfo

索引一个指向NDIS_IPSEC_PACKET_INFO结构的指针。此结构为IP安全性操作向微端口指定Per-Packet信息。

TcpLargeSendPacketInfo

索引指向一个ULONG值的指针。此值用于卸载大TCP包分段。

NDIS_PER_PACKET_INFO_FROM_PACKET宏定义如下：

PVOID

NDIS_PER_PACKET_INFO_FROM_PACKET（

IN PNDIS_PACKET Packet，

IN NDIS_PER_PACKET_INFO InfoType

）；

此宏为指定包返回一个指向或包含Per-Packet信息的特定类型的指针。属于卸载任务的InfoType值有TcpIpCheckSumPacketInfo、IpSecPacketInfo，和TcpLargeSendPacketInfo。这些值与上面描述的索引值一致。例如，若指定的InfoType是TcpIpCheckSumPacketInfo，此宏返回一个指向与给定包描述信息相关的NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构的指针。在驱动程序需要访问Per-Packet信息的一个类型时，它应调用NDIS_PER_PACKET_INFO_FROM_PACKET宏。

9.5 卸载TCP/IP校验和任务

负载平衡微端口不能卸载TCP/IP校验和任务。负载平衡的更多信息参见第十章。

在向微端口传递一个包描述信息（微端口将要在此包上执行一个或多个校验任务）之前，TCP/IP传输层指定与此包描述信息相关的校验信息。此信息用NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构表示，它是与包描述信息相关的Per-Packet信息（扩展的带外信息）的一部分。

NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构的定义如下：

typedef struct _NDIS_TCP_IP_CHECKSUM_PACKET_INFO

{

union

{

struct

{

ULONG NdisPacketChecksumV4:1;

ULONG NdisPacketChecksumV6:1;

ULONG NdisPacketTcpChecksum;

ULONG NdisPacketUdpChecksum;

ULONG NdisPacketIpChecksum;

} Transmit;

struct

{

ULONG NdisPacketTcpChecksumFailed:1;

ULONG NdisPacketUdpChecksumFailed:1;

ULONG NdisPacketIpChecksumFailed:1;

ULONG NdisPacketTcpChecksumSucceeded:1;

ULONG NdisPacketUdpChecksumSucceeded:1;

ULONG NdisPacketIpChecksumSucceeded:1;

ULONG NdisPacketLoopback:1;

} Receive;

ULONG Value;

};

}NDIS_TCP_IP_CHECKSUM_PACKET_INFO，

*PNDIS_TCP_IP_CHECKSUM_PACKET_INFO;

在将对TCP包的校验和的计算卸载之前，TCP/IP传输层为TCP虚拟报头计算校验和。TCP/IP传输层的虚拟报头的校验和计算跨越了虚拟报头的所有字段，包括源IP地址、目标IP地址、协议字段、和TCP包的TCP长度。TCP/IP传输层将虚拟报头校验和放到TCP报头的校验和字段中。

TCP/IP传输层提供的虚拟报头校验和是NIC为发送包计算的实TCP校验和的提前注入量。为了计算实TCP校验和，NIC计算TCP校验和（对TCP报头和负载）的可变部分，将此校验和加到TCP/IP传输层计算的虚拟报头校验和上，然后为此校验和计算16位的补码。（计算这样的校验和的更多信息，参见RFC 793和RFC 1122）。

在从MiniportSend、MiniportSendPacket、和MiniportCoSendPacket函数收到包描述信息后，微端口一般进行以下校验和处理：

1．微端口调用NDIS_GET_PACKET_PROTOCOL_TYPE宏确定包的协议。

2．如果宏返回的协议ID不是NDIS_PROTOCOL_ID_TCP_IP，微端口的NIC不应在此包上进行任何校验操作。

3．如果返回的协议ID是NDIS_PROTOCOL_ID_TCP_IP，微端口调用NDIS_PER_PACKET_INFO_FROM_PACKET宏（其中InfoType值为TcpIpCheckSumPacketInfo），获得一个指向NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构的指针。（或者微端口调用NDIS_PACKET_EXTENSION_FROM_PACKET宏获得一个指向NDIS_PACKET_EXTENSION结构的指针。然后微端口使用TcpIpCheckSumPacketInfo数组索引获得一个指向NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构的指针。）

4．微端口检查NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构中的NdisPacketChecksumV4标记。若未设置NdisPacketChecksumV4标记，微端口的NIC不应在此包上进行任何校验操作。

5．若已设置NdisPacketChecksumV4标记，微端口检查MdisPacketTcpChecksum、MdisPacketUdpChecksum、和MdisPacketIpChecksum标记，确定它的NIC应为包计算哪些校验和。

6．微端口将包传递给它的NIC，NIC将为包计算适当的校验和。如果包既有一个隧道IP报头，又有一个传输IP报头，那么支持IP校验卸载的NIC只在隧道报头上执行IP校验任务。TCP/IP传输层在传输IP报头上执行IP校验任务。

在指示一个接收包之前，微端口验证适当的校验和并在NDIS_TCP_IP_CHECKSUM_PACKET_INFO结构中置NdisPacketXxxChecksumFailed和/或NdisPacketXxxChecksumSucceeded标记。如果此包是一个回送包，微端口设置NdisPacketLoopback标记和所有的NdisPacketXxxChecksumSucceeded标记。

9.6 卸载IP安全任务

本节假定读者对在RFC中和在Internet Engineering Task Force（IETF）的IP Security Working Group发布的草稿中指定的IP安全性有一定了解。

IPSec体系结构：

n Security Architecture for the Internet Protocol（RFC 2401）

身份验证报头（AH）：

n IP Authentication Header（RFC 2402）

n The Use of HMA-MD5-96 within ESP and AH（RFC 2403）

n The Use of HMA-SHA-1-96 within ESP and AH（RFC 2404）

n HMAC-MD5 IP Authentication with Replay Prevention（RFC 2085）

封装安全负载（ESP）：

n IP Encapsulating Security Payload（ESP）（RFC 2406）

n The ESP CBC-Mode Cipher Algorithms（RFC 2451）

n The ESP DES-Mode Cipher Algorithms with Explicit IV（RFC 2405）

n The NULL Encryption Algorithm and its Use with IPSec（RFC 2410）

对IP安全性卸载的需求和限制

对IP安全性卸载有以下需求和限制：

n NIC必须保持一个安全性联系表（Security Association SA）。这使在发送包中不需要包含密钥或其他AH和ESP需要的信息，因此改善了性能。

n NIC应能同时为单个包处理AH和ESP负载。在此情况下，NIC必须为AH和ESP支持以下完整性（身份验证）算法的组合。

AH ESP

MD5 MD5

SHA 1 SHA 1

MD5 SHA 1

SHA 1 MD5

MD5 Null（只有在NIC支持空加密时）

SHA 1 Null（只有在NIC支持空加密时）

n [image] 支持DES算法的NIC必须生成这些算法需要的IV。

n NIC执行的IP安全性任务处理加密的AH校验和和/或ESP校验和，以及加密和解密ESP负载。对于发送包，TCP/IP传输层创建所有的报头、填充、和重发编号，并选择只有目标地址/IP安全性协议对才有的SPI值。对于接收包，TCP/IP传输层执行内入策略检查，处理重发检测和预防，并处理审计事件。

n 对于发送包，TCP/IP传输层不提供显式的偏移量——例如，指示加密数据的开始的偏移量——因为卸载驱动程序能够非常容易的从用于处理包的特定安全性联系（SA）中确定这些信息。

n 使用IP安全协议的包在身份验证报头（AH）和/或封装安全负载（ESP）中必须有验证信息。决不允许IP安全包不带验证信息的。

n 不能为需要IP分段的发送包，或为需要装配IP分段的接收包卸载IP安全性任务。

n 不能为通过负载平衡微端口发送和接收的包卸载IP安全性任务。负载平衡的详细信息参见第十章。

为NIC添加安全性联系

在TCP/IP传输层确定一个NIC可以执行IP安全性操作后（参见9.1.2），传输层必需请求NIC的微端口在传输层将IP安全性任务推卸给NIC之前为NIC添加一个或多个内入和外出安全性联系（SA）。TCP/IP传输层用OID_TCP_TASK_IPSEC_ADD_SA请求微端口为它的NIC添加一个或多个SA。

从NIC删除安全性联系

如果必要，TCP/IP传输层设置OID_TCP_TASK_IPSEC_DELETE_SA以请求微端口从它的NIC删除安全性联系（SA）。

为了给NIC上其他SA腾出空间，微端口可以为一个接收包在NDIS_IPSEC_PACKET_INFO结构中设置SA_DELETE_REQ。TCP/IP传输层随后发出OID_TCP_TASK_IPSEC_DELETE_SA删除内入安全性联系（SA），然后再一次发出OID_TCP_TASK_IPSEC_DELETE_SA删除与内入SA对应的外出SA。在接收到相应的OID_TCP_TASK_IPSEC_DELETE_SA请求之前，微端口的NIC不得删除任何SA。微端口对SA_DELETE_REQ的设置与CRYPTO_DONE无关。

卸载发送方的IP安全性任务

在为一个包向微端口发送一个包描述信息之前，TCP/IP传输层更新与此包描述信息联系的IP安全性信息。TCP/IP传输层在一个NDIS_IPSEC_PACKET_INFO结构中指定此信息，NDIS_IPSEC_PACKET_INFO结构是与此包描述信息联系的Per-Packet数据（扩展的带外数据）。

NDIS_IPSEC_PACKET_INFO结构的定义如下：

typedef struct _NDIS_IPSEC_PACKET_INFO

{

union

{

struct

{

NDIS_HANDLE OffloadHandle;

NDIS_HANDLE NextOffloadHandle;

} Transmit;

struct

{

ULONG SA_DELETE_REQ:1;

ULONG CRYPTO_DONE:1;

ULONG NEXT_CRYPTO_DONE:1;

ULONG CryptoStatus;

} Receive;

};

} NDIS_IPSEC_PACKET_INFO，*PNDIS_IPSEC_PACKET_INFO ；

TCP/IP传输层提供一个OffloadHandle，为发送包的传输（端到端连接）部分指定外出SA的句柄。如果包将通过隧道传输，TCP/IP传输层还提供NextOffloadHandle，为发送包的隧道部分指定外出SA的句柄。

在微端口从它的MiniportSend、MiniportSendPacket、或MiniportCoSendPacket函数接收到包描述信息之后，它以参数InfoType为IpSecNdisTask调用NDIS_PER_PACKET_INFO_FROM_PACKET宏，以获得与此包描述信息联系的NDIS_PACKET_EXTENSION结构。然后微端口就可以用IpSecPacketInfo数组索引获得指向NDIS_IPSEC_PACKET_INFO结构的指针。

当在发送包上执行IP安全性处理时，如果此包包含一个ESP负载并还要加密此包，NIC为包计算AH和/或ESP加密校验和。TCP/IP传输层已经构建、填充了此包，并且在必要时为它分配了顺序号和SPI。

卸载接收方的IP安全任务

当在接收包上执行IP安全性处理时，NIC为包含ESP负载的包解密并为包计算AH和/或ESP加密校验和。在向TCP/IP传输层指示此包前，微端口以参数InfoType为IpSecNdisTask调用NDIS_PER_PACKET_INFO_FROM_PACKET宏以获得一个指向与此包相关的NDIS_IPSEC_PACKET_INFO结构的指针，或调用NDIS_PACKET_EXTENSION_FROMPACKET宏以获得一个指向与包描述信息相关的NDIS_PACKET_EXTENSION结构的指针。

微端口通过在NDIS_IPSEC_PACKET_INFO结构中设置CRYPTO_DONE标记指出它的NIC至少在接收包的一个IP安全性负载上执行了IP安全检查。如果微端口的NIC在接收包的隧道和传输部分都执行了IP安全检查，微端口还要在NDIS_IPSEC_PACKET_INFO结构中设置NEXT_CRYPTO_DONE标记。只有在包同时具有隧道和传输IP安全性负载时才能设置NEXT_CRYPTO_DONE标记；否则，NEXT_CRYPTO_DONE被置为零。微端口还必须在NDIS_IPSEC_PACKET_INFO结构中提供CryptoStatus的值，表示IP安全检查的结果。如果NIC求校验和或解密时发生错误，无论此包的格式如何，微端口必须向上指示此接收包，并指定适当的CryptoStatus值。

在微端口将包指示给TCP/IP传输层后，传输层检查由NIC执行的IP安全性检查的结果、为包检查顺序号、并决定如何处理校验失败和/或顺序号检查失败的包。

网络接口的变化对IP安全卸载的影响

网络接口的以下改变会影响IP安全任务的卸载：

n NIC被删除

在从系统中删除一个执行被卸载任务的NIC之前，它的微端口应从NIC上删除所有的安全性联系（SA）。微端口不必请求TCP/IP传输层删除SA。

n 路由接口被改变

当网络通信通过一个新的接口发送时，TCP/IP栈暂时由它自己执行IP安全性任务，直到它在新的接口上将适当的SA添加到NIC上。TCP/IP栈通过发布OID_TCP_TASK_IPSEC_ADD_SA将SA添加到NIC上。NIC上用于旧接口的SA失效后，TCP/IP传输层发布多次OID_TCP_TASK_IPSEC_DELETE_SA，请求NIC的微端口从NIC上删除SA。

9.7 卸载大TCP包分段

支持对大TCP包（大于网络介质的最大传输单元的TCP包）进行分割的NIC必须能够：

n 为包含IP选项的发送包计算IP校验和。

n 为包含TCP选项的发送包计算TCP校验和。

如果微端口指出它的NIC支持对大TCP包进行分段，TCP/IP栈假定NIC可以对包含IP选项和/或TCP选项的大TCP包执行此分段操作。

TCP/IP传输层只对符合以下标准的大TCP包卸载分段操作：

n 必须是一个TCP包。TCP/IP传输层不卸载大UDP包的分段操作。

n 包必需至少应被分为微端口指定的最小分段数。（参见9.1.3）

n 不能是回送包。

n 此包不能通过隧道发送。

n 此包不能被送往负载平衡微端口。负载平衡参见第十章。

在为大TCP包卸载分段操作之前，TCP/IP传输层做以下操作：

n 更新与包描述信息有关的大包分段信息。此信息是一个ULONG值，是与包描述信息有关的Per-packet信息的一部分。（关于Per-packet信息的详见online DDK中Network Driver Reference中的NDIS_PACKET_EXTENSION）。TCP/IP传输层将此ULONG值置为最大段长度（MSS），相当于当前的最大传输单元（MTU）。MTU是微端口当前可以在一个包内包含的用户数据的最大字节数。

n 将大TCP包的总长度写入包的IP报头的总长度字段。总长度包括：IP报头的长度、IP选项的长度（如果存在）、TCP报头的长度、TCP选项的长度（如果存在）、TCP负载的长度。

n 为TCP虚拟报头计算校验和，并将此虚拟报头校验和写入TCP报头的校验和字段。TCP/IP传输层在虚拟报头的以下字段上计算虚拟报头校验和：源IP地址、目标IP地址、和协议字段。TCP/IP传输层提供的虚拟报头校验和为NIC对每个包进行的实TCP校验和计算提供了一个提前注入量，这样NIC就直接从TCP包计算而不必检查IP报头。

n 将当前的顺序号写入TCP报头的顺序号字段。顺序号标识TCP负载的第一个字节。

在微端口从它的MiniportSend、MiniportSendPacket、或MiniportCoSendPacket函数中获得包描述信息之后，它调用NDIS_PER_PACKET_INFO_FROM_PACKET宏，其中将InfoType置为TcpLargeSendPacketInfo，以获得TCP/IP传输层写入的MSS值。或者，微端口调用NDIS_PACKET_EXTENSION_FROM_PACKET宏获得一个指向NDIS_PACKET_EXTENSION结构的指针，然后微端口就可以使用TcpLargeSendPacketInfo数组索引获得MSS值。

微端口从包的IP报头获得包的总长度，并使用MSS值将大TCP包分段为较小的包。每个较小的包都包含MSS或较少的数据字节。

微端口将IP和TCP报头附加到每个从大包分出的段上。微端口必须为这些包的报头计算IP和TCP校验和。为了计算TCP校验和，NIC计算TCP校验和的可变部分（为TCP报头和TCP负载），将此校验和加到TCP/IP传输层计算的虚拟报头校验和上，然后为此校验和计算16位的补码。（计算这样的校验和的更多信息，参见RFC 793和RFC 1122）。

以下假定和限制应用于对IP和TCP报头的处理：

n TCP/IP传输层卸载的大TCP包的IP报头中的MF位不应被置位，且IP报头中的分段偏移量应为0。

n 大TCP包的IP报头中的URG、RST和SYN标记不应被置位，且IP报头的紧急偏移量应为0。

n 如果大TCP包的IP报头中的FIN位被置位，微端口必须在从大TCP包创建的最后一个包的TCP报头中设置此位。

n 如果大TCP包的IP报头中的PUSH位被置位，微端口必须在从大TCP包创建的最后一个包的TCP报头中设置此位。

n 如果大TCP包包含IP选项和/或TCP选项，微端口将把这些选项原封不动的复制到从大TCP包分出的每个包中。特别是，NIC不会使时间戳选项增值。

微端口必须以从TCP/IP传输层接收的顺序发送包。然而，从大TCP包分出来的包可以与其他包交错。

微端口还必须保持ACK（对从大TCP包分出的包的应答）的顺序。微端口必须以接收的顺序向上层指示包含ACK的包，这样TCP/IP传输层可以正确执行快速中继算法、快速恢复算法、和选择ACK算法。

在为大包完成发送操作（例如，使用NdisMSendComplete或NdisMCoSendComplete完成发送操作）之前，微端口将表示发送的用户数据总字节数的ULONG值（大TCP包负载的per-packet信息）写入从大TCP包分出的所有包中。

9.8 卸载组合

对特定包而言，TCP/IP传输层实际上将哪些任务推卸给了NIC依赖于NIC的微端口报告的卸载能力以及IP安全策略在系统中是否有效。

n 如果在系统中没有有效的IP安全策略，TCP/IP栈将卸载校验和任务和/或大TCP包分段任务（如果NIC的微端口表示NIC支持这样的任务）。

n 如果系统中有一个IP安全策略有效，TCP/IP栈不卸载校验和任务和/或大TCP包分段。TCP/IP栈将卸载IP安全任务（如果NIC的微端口表示NIC支持这样的任务）。

n 如果系统中有一个IP安全策略变为有效，TCP/IP栈立即停止卸载校验和任务和/或大TCP包分段并开始卸载IP安全任务。

n 如果系统中的IP安全策略变为无效，TCP/IP栈恢复卸载校验和任务和/或大TCP包分段。

Windows 2000的将来版本和服务包可能会对同一包同时支持校验和卸载和IP安全卸载。因此，如果NIC支持校验和卸载和IP安全卸载，它必须能够在同一包上同时执行校验和和IP安全操作。

下表总结了在不同的任务卸载能力集合下可以卸载的任务：

卸载能力 卸载的任务

IP校验和 | TCP校验和 | 大TCP包 | IP安全性 | IP校验和 | TCP校验和 | 大TCP包 | IP安全性
Off | Off | Off | Off | No | No | No | No
Off | Off | Off | On | No | No | No | Yes2
Off | Off | On | Off | No | No | Yes1 | No
Off | Off | On | On | No | No | Yes1 | Yes2
Off | On | Off | Off | No | Yes1 | No | No
Off | On | Off | On | No | Yes1 | No | Yes2
Off | On | On | Off | No | Yes1 | Yes1 | No
Off | On | On | On | No | Yes1 | Yes1 | Yes2
On | Off | Off | Off | Yes | No | No | No
On | Off | Off | On | Yes1 | No | No | Yes2
On | Off | On | Off | Yes | No | Yes1 | No
On | Off | On | On | Yes1 | No | Yes1 | Yes2
On | On | Off | Off | Yes | Yes1 | No | No
On | On | Off | On | Yes1 | Yes1 | No | Yes2
On | On | On | Off | Yes | Yes1 | Yes | No
On | On | On | On | Yes1 | Yes1 | Yes1 | Yes2

1. 如果系统中没有有效的IP安全策略，任务被卸载。

2. 对需要IP分段的发送包和需要组装IP分段的接收包不卸载IP安全。只有在NIC激活了适当的IP安全能力时才卸载IP安全。例如，如果系统和对等节点之间的ISAKMP/Oakley（IKE）协商为NIC不支持的安全性联系指定了一个算法组合，则不卸载IP安全性。详见9.6节。

9.9 使用注册表键值激活和禁止任务卸载

在调试驱动程序的任务卸载功能时，激活或停用任务卸载可能是有用的。有两个注册表键控制此功能：

n HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\TCPIP\Parameters\
DisableTaskOffload

将此键置为0会禁止从TCP/IP传输层卸载所有任务。将它置为1激活所有任务卸载。

n HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Ipsec\EnableOffload

将此键置为0禁止从TCP/IP传输层卸载IP安全。但不影响对TCP/IP校验和任务和大TCP包分段任务的卸载。将此键置为1激活IP安全卸载。


负载平衡和失效替换

负载平衡和失效替换（LBFO）功能可以提高微端口驱动程序的网络适配器的可靠性和性能。微端口驱动程序可以使用LBFO将工作量分布到它的适配器束中，从而平衡工作量。也就是说微端口驱动程序可以使用LBFO将任务从主适配器推卸给任何次适配器。例如，假设一个协议驱动程序请求微端口驱动程序发送包，为了平衡包发送工作量，微端口驱动程序可以使用它的主适配器传送一些包，而将另一些包的传送卸载给次适配器。LBFO功能还可以使微端口驱动程序在主适配器发生故障时用一个次适配器接管包传输和信息请求。

以下部分描述了LBFO以及如何实现支持LBFO的微端口驱动程序：

n 10.1 关于LBFO

n 10.2 指定对LBFO的支持

n 10.3 在微端口驱动程序上实现LBFO

关于在微端口驱动程序上添加LBFO功能的信息，参见Passthru driver的自述文件。这个微端口驱动程序是Microsoft® Windows® 2000 DDK中的一个微端口驱动程序示例。

10.1 关于LBFO

微端口驱动程序可以实现对LBFO的支持。这样的微端口驱动程序可以将工作量分布到它的微端口实例束中，以平衡包传输的工作量，并可以在主适配器发生故障时，用一个次适配器接管包传输和信息请求。微端口驱动程序管理的微端口实例束可以包括单端口和多端口NIC。

NDIS只向传输层暴露微端口驱动程序的主微端口。如果传输层请求向微端口驱动程序发送包，或请求设置或查询微端口驱动程序的信息，NDIS将这些请求传递给主微端口。

只有被同一微端口驱动程序初始化的微端口实例才能成为LBFO方案的一部分。为了使多个被不同微端口驱动程序初始化的微端口实例成为一个LBFO方案的一部分，必须实现一个中间层微端口驱动程序为这些微端口实例提供LBFO。

次微端口只能使用主微端口的句柄向绑定的传输层指示包。

主微端口处理传输层产生的一切信息请求。然而，NDIS可以将指定请求发送给次微端口。例如，NIDS可能会查询所有微端口的介质连接性（media connectivity）。次微端口必须处理并响应此查询。

在安装时微端口驱动程序必须指定它是否实现了LBFO。为了指定LBFO支持，微端口驱动程序的信息文件（*.INF）必须包含BuddleId关键字和一个字符串值以标识微端口的适配器束。注册表就是用这些信息配置的。

在初始化期间，微端口驱动程序必须确定正在初始化的微端口实例是否属于一个已存在的束。为了确定微端口是否是束的成员，微端口驱动程序从微端口的注册表键下的BundleId关键字中检索一个字符串值。每个微端口实例都应将此BuddleId值复制到微端口的内部结构中。

然后微端口应搜索所有已初始化的微端口的内部结构，查找与它有相同束标识（BuddleId）值的微端口。如果没有找到，它就成为束的主微端口。在此微端口是束中第一个被初始化的微端口时，发生上述情况。如果微端口找到了一个与它有相同束标识值的微端口，它应将自己置为主微端口对应的次微端口。微端口驱动程序可以设置多个次微端口，也就是说，一个束内可以有多个次微端口。

微端口驱动程序必须为包传输和信息请求提供适当的LBFO。以下描述了提供LBFO的微端口的例子：

n 微端口驱动程序将包传输和信息请求重新选择给次微端口。

n 如果束的主微端口失效，微端口驱动程序可以从束中将主微端口去除，并提升次微端口担任主要任务。

10.2 指定对LBFO的支持

微端口驱动程序在安装时指定它实现LBFO。为了指定对LBFO的支持，微端口驱动程序的信息文件（INF）必须包含一个BundleId关键字和一个字符串值（REG_SZ）。此字符串值标识微端口驱动程序的适配器束。束标识符信息还用于配置注册表。

微端口驱动程序的束标识符值必须作为适配器属性暴露给网络应用程序、控制面板应用程序，所以如果用户改变了此属性，NIC将被停机然后重新启动。用户可以通过改变束标识符属性调整从属关系。

以下是一个微端口的INF文件中的add-registry-section，它在Ndi\params键下添加BundleId子键，并给BundleId的ParamDesc（参数说明）设置一个字符串值”Bundle1”。

[a1.params.reg]

HKR，Ndi\params\BundleId，ParamDesc，0，”Bundle1”

向微端口的INF文件添加键和值的信息详见1.2.7节。指定适配器配置参数和值的信息详见1.2.7.11和1.2.7.12。

10.3 在微端口驱动程序上实现LBFO

为了支持LBFO，微端口驱动程序必须能够执行以下操作：

n 10.3.1 初始化微端口束

n 10.3.2 平衡微端口驱动程序的工作量

n 10.3.3 在主微端口失效后提升一个次微端口

10.3.1 初始化微端口束

NDIS为驱动程序管理的每个网络适配器调用一次微端口驱动程序的MiniportInitialize函数。为了支持LBFO，MiniportInitialize必须确定被初始化的微端口是否属于一个已存在的束。

为了确定微端口是否是束的成员，微端口首先调用NdisOpenConfiguratiom函数获得注册表键（存储着微端口的配置参数）的句柄。然后微端口调用NdisReadConfiguration函数从微端口的注册表键下的BundleId关键字中检索它的字符串值（REG_SZ）。每个微端口都应将此束标识符复制到它的内部结构中。此内部结构还被认为是一个MiniportAdapterContext。每个微端口还应将它的MiniportAdapterHandle复制到MiniportAdapterContext中。NDIS使用MiniportAdapterHandle引用相关的微端口。

在微端口检索束标识符后，微端口应搜索所有已初始化微端口的内部结构，查找与它有相同束标识符值的微端口。为了执行此搜索，微端口检查每个微端口的MiniportAdapterContext。微端口驱动程序可以将此微端口置为主微端口或次微端口：

n 此微端口是束中第一个被初始化的微端口。换句话说，此微端口没有查找到与它有相同束标识符值的微端口。在此情况下，此微端口默认成为主微端口。

n 此微端口查找到一个相同的束标识符值。它应调用NdisMSetMiniportSecondary函数将它自己置为与主微端口关联的次微端口。在此调用中，微端口在MiniportAdapterHandle参数中传递他自己的句柄，在PrimaryMiniportAdapterHandle参数中传递已被初始化的主微端口的句柄。微端口从主微端口的MiniportAdapterContext中检索主微端口的句柄。

一个束中可以存在多个次微端口。在每个实例初始化的过程中，微端口驱动程序可以为多个微端口调用NdisMSetMiniportSecondary，将它置为与主微端口关联的次微端口。

只有在微端口实例潜在的属于一个被同一微端口驱动程序初始化的实例束时，它才能调用NdisMSetMiniportSecondary。在微端口实例调用NdisMSetMiniportSecondary之前，它不能被认为是束的一部分。只有在成功调用NdisMSetMiniportSecondary之后，微端口实例才能成为束的一部分。

10.3.2 平衡微端口驱动程序的工作量

先前初始化了多个微端口并使它们成为一个束的成员的微端口驱动程序，可以使用这些微端口平衡驱动程序的工作量。

NDIS只将主微端口实例暴露给传输层。如果传输层请求向微端口驱动程序发送包或请求查询或设置微端口驱动程序的信息，NDIS应将这些请求传递给主微端口实例。为了平衡微端口驱动程序的工作量，驱动程序应实现将一些请求卸载给次微端口实例的功能。微端口驱动程序可以通过将这些请求发送给次微端口请求次微端口执行与请求相关的任务。不管实际上是主微端口还是次微端口执行了请求，在完成这些请求时微端口驱动程序只应用主微端口的句柄。

然而，如果NDIS向次微端口发送了一个请求，那个次微端口应使用它自己的句柄完成请求。

10.3.3 在主微端口失效后提升一个次微端口

支持LBFO的微端口驱动程序可以在主适配器失效时提升一个次微端口担任主要角色。如果微端口驱动程序的主适配器失效，驱动程序可以调用NdisMRemoveMiniport函数将主微端口从束中去除。然后驱动程序调用NdisMPromoteMiniport函数将次微端口提升为主微端口。NDIS应使用新的微端口作为主微端口将传输层的后续请求传递给微端口驱动程序。


快速转发路径

快速转发路径（Fast Forwarding Path，FFP）是网络接口卡（NIC）的一个特性，它可以改善网络的性能。FFP用来与路由和过滤代码协作，从同一个NIC（或类似的NIC）上的一个适配器端口到另一个适配器端口转发和过滤包，而不用通过主机处理器发送包。

协议驱动程序可以将转发和过滤包的任务推卸给支持FFP的NIC。为了卸载这些任务，协议驱动程序请求NIC的微端口驱动程序在NIC上激活FFP。NIC的微端口驱动程序可以设置它的NIC以使此NIC执行这些任务，一般情况下这些任务是由协议驱动程序的路由和过滤代码执行的。

以下节描述了FFP和如何实现支持FFP的NIDS驱动程序：

n 11.1 关于FFP

n 11.2 NIDS中的FFP支持

n 11.3 为IP转发在微端口实现FFP

11.1 关于FFP

NIC微端口驱动程序可被实现成通过快速路径转发包，而不中断主机处理器。例如，一个NIC有两个适配器端口，分别叫NET1和NET2。假设最初的包到达NET1，NIC的微端口驱动程序取回包并将它指示给路由代码。然后路由代码检查包，让它通过包过滤代码，并通过微端口驱动程序将它发送到NET2。NIC的微端口驱动程序能将发送给NET2的包的报头信息同从NET1取回的包的报头信息相比较，以确定它们是否相同。利用此路由和比较的方法，微端口驱动程序和它的NIC确定是否它们应把后来从NET1接收的、与最初包有相同报头信息的包快速转发到NET2，而不中断主机处理器。

包中的头信息也被称为包标识元组（packet-identifier tuple）。包标识元组是唯一地标识包的包元素。

如果NIC的微端口驱动程序确定从NET1取回的初始包与发送给NET2的包相同，微端口驱动程序可以将包的标识元组，连同外出包的硬件地址，添加到NIC的快速转发缓存（fast-forward cache，FFC）中。随后，每当有包到达NET1，NIC将此包的标识元组与FFC中所有的标识元组进行比较。如果发现匹配其中一个，它将快速转发此包。

以下节更详细的描述了FFP：

n 11.1.1 使用一个NIC的FFP

n 11.1.2 使用多个NIC的FFP

n 11.1.3 IP转发

n 11.1.4 FFP和包过滤

11.1.1 使用一个NIC的FFP

图11.1描述了使用一个NIC的FFP。

图11.1 使用一个网络卡的FFP

在使用一个NIC的FFP中，第一个包最初通过慢速路径路由；也就是说，NET1的第一个包通过微端口驱动程序、绑定的协议驱动程序、和路由代码，到达NET2。然后，与第一个包有相同的报头信息的后续包通过快速路径路由；也就是说，它不通过微端口驱动程序和路由代码直接从NET1到NET2。

11.1.2 使用多个NIC的FFP

NIC微端口驱动程序可被实现成通过跨越多个相同类型的NIC的快速路径转发包。例如，NIC1有一个适配器端口NET1，NIC2有一个适配器端口NET2。假设最初的包到达NET1，这些NIC的微端口驱动程序取回此包并将它指示给路由代码。同只有一个NIC时的方法相同，路由代码检查并将此包传递给NET2，然后微端口驱动程序进行包比较。这些NIC的微端口驱动程序确定发送给NET2的包是否与从NET1取回的初始包相同。如果两个包相同，微端口驱动程序可以将包的标识元组添加到两个NIC共享的FFC中。随后，每当有包到达NET1，NIC1将此包的标识元组与共享的FFC中所有的标识元组进行比较。如果发现一个匹配，它将通过到NET2的共享BUS快速转发此包，而不中断主机处理器。此FFP方案需要微端口驱动程序保持一个多个NIC共享的内入包缓存。本节不描述如何实现微端口驱动程序，以在多个NIC之间共享内入包缓存。

图11.2 使用两个或更多网络卡的FFP

图11.2描述了使用多个NIC的FFP。第一个包最初通过慢速路径路由；也就是说，NIC1上NET1的第一个包通过微端口驱动程序、绑定的协议驱动程序、和路由代码，到达NIC2上的NET2。然后，与第一个包有相同的报头信息的后续包通过快速路径路由；也就是说，它不通过微端口驱动程序和路由代码直接从NET1到NET2。

11.1.3 IP转发

如果包的包标识元组与任何存储在NIC的FFC中的元组匹配，NIC可以通过快速路径转发包。

如果到达NIC的适配器端口的包是通过使用IP协议的网络传输的，则那些包在包标识元组中包含以下信息：

IP源（SRC）地址 （IP报头偏移量12，4字节）

IP目标（DEST）地址 （IP报头偏移量16，4字节）

IP协议类型 （IP报头偏移量 9，1字节）

如果到达NIC的适配器端口的包是通过使用IP高层协议的网络传输的，则那些包可能会在包标识元组中包含追加信息。高层协议包括传输控制协议（TCP）、用户数据报协议（UDP）、和网间控制报文协议（ICMP）等。

如果包使用TCP/IP传输，那些包应在包标识元组中包含以下关于TCP的追加信息：

TCP 源端口 （TCP报头偏移量0，2字节）

TCP目标端口 （TCP报头偏移量2，2字节）

如果包使用UDP/IP传输，那些包应在包标识元组中包含以下关于UDP的追加信息：

UDP源端口 （UDP报头偏移量0，2字节）

UDP目标端口 （UDP报头偏移量2，2字节）

如果包使用ICMP/IP传输，那些包应在包标识元组中包含以下关于ICMP的追加信息：

ICMP类型 （ICMP消息偏移量0，1字节）

ICMP编码 （ICMP消息偏移量1，1字节）

IP的包标识元组的定义如下：

<IPSRC，IPDEST，IPPROTOCOL>

TCP/IP的包标识元组的定义如下：

<IPSRC，IPDEST，IPPROTOCOL，[TCPSRC+TCPDEST]>

UDP/IP的包标识元组的定义如下：

<IPSRC，IPDEST，IPPROTOCOL，[UDPSRC+UDPDEST]>

ICMP/IP的包标识元组的定义如下：

<IPSRC，IPDEST，IPPROTOCOL,[ICMPTYPE+ICMPCODE]>

11.1.4 FFP和包过滤

被要求过滤某些包的NIC微端口驱动程序，应将这些包指示给绑定协议驱动程序的包过滤逻辑。微端口驱动程序将包传送给过滤逻辑会消除FFP的优势，因为NIC将不得不中断主机处理器将包发送到路由代码。然而，包标识元组可以包含一些信息，NIC通过检查这些信息确定是否过滤此包。这样NIC只需向绑定的协议驱动程序指示那些包标识元组先前未经过过滤逻辑的包。另外，NIC可被设为快速转发或滤出带有特定包标识元组的包。

使用包过滤的高层协议驱动程序可以请求在NIC的FFC中把有特定包标识元组类型的缓存项目类型置为FFP_DISCARD_PACKET。这样,NIC就可以执行包过滤，而不中断主机处理器。也就是说，NIC将丢弃那些有被置为FFD_DISCARD_PACKET的包标识元组的包。如果NIC发现一个内入包的包标识元组与一个在FFC中被置为FFD_DISCARD_PACKET的包标识元组匹配，NIC将立即丢弃此包而不用将它指示给绑定的协议驱动程序。详见online DDK中的Network Drivers Reference，FFP Objects中的OID_FFP_DATA。

如果协议驱动程序不使用基于高层协议的活动IP过滤，协议驱动程序可以通知NIC。这些高层驱动程序包括，传输控制协议（TCP）、用户数据报协议（UDP）、和网间控制报文协议（ICMP）等。NIC一旦接收到这个信息，它就可以全速快速转发每个包，而不必考虑每个包内高层协议报头信息。在这种情况下，NIC检查的转发信息可以用以下元组表示：

IP 源地址，IP 目标地址，IP 协议

详见online DDK中的Network Drivers Reference，FFP Objects，OID_FFP_SUPPORT或OID_FFP_CONTROL。

11.2 NIDS中的FFP支持

为了在NIC上支持FFP，协议驱动程序必须可以执行以下操作：

n 协议驱动程序必须能够获得一个拥有特定适配器端口的NIC微端口驱动程序的句柄。协议驱动程序首先调用NdisOpenAdapter函数得到一个绑定适配器端口的句柄。然后调用NdisGetDriverHandle函数得到拥有绑定适配器端口的NIC微端口驱动程序的句柄。协议驱动程序可以使用NdisGetDriverHandle函数检查一个特定的微端口驱动程序是否拥有两个或多个被协议驱动程序打开的绑定适配器端口。如果一个协议驱动程序需要对一个NIC微端口驱动程序的所有适配器端口的FFP状态进行刷新，它可以只给NIC微端口驱动程序发送一个OID_FFP_FLUSH。

n 协议驱动程序必须能够使用包含FFP对象标识符（OID）的请求为支持FFP的NIC设置或取出操作特性。包含FFP对象标识符（OID）的请求，详见online DDK中的Network Drivers Reference中的FFP Objects。

为了在NIC上支持FFP，NIC微端口驱动程序必须可以执行以下操作：

n NIC微端口驱动程序必须能处理FFP OID。

n NIC微端口驱动程序必须能够基于协议驱动程序中的过滤器设置它的NIC使它丢弃包。微端口驱动程序通过，在NIC的快速转发缓存中，将包含特定包标识元组的包的缓存项目类型置为FFD_DISCARD_PACKET，使NIC丢弃包。

n 如果没有基于高层协议的活动IP过滤，NIC微端口驱动程序必须能够设置它的NIC使它忽略每个包的高层协议报头中的附加信息。然后NIC就可以全速的快速转发每个接收包。

11.3 为IP转发在微端口实现FFP

本节将描述在IP网络中如何在NIC微端口驱动程序上实现FFP。在实现这样的微端口驱动程序时必须要考虑一下项目：

n 如果到达NIC的适配器端口的包包括任何选项，微端口驱动程序和驱动程序的NIC不应当快速转发此包。此包应被指示给绑定的协议驱动程序。

n 如果微端口驱动程序和驱动程序的NIC快速转发特定包，在转发之前必须要将这些包的IP报头中的存活时间（time to live，TTL）减1。由于那些包没有被指示给路由代码，因此没有被路由代码减1，所以微端口驱动程序和驱动程序的NIC必须减少那些包的TTL。一个包的IP报头通过IPHeader结构描述。TTL的成员是iph_ttl。IPHeader结构的定义参见online DDK中Network Drivers Reference中的FFP Objects中的OID_FFP_DATA。

n 如果微端口驱动程序和驱动程序的NIC改变了一个包的IPHeader结构的iph_ttl，它们还必须重新计算包的IP报头的校验和。计算完此校验和后必须将它放到IPHeader结构的iph_xsum中。微端口驱动程序应使用增量技术计算此校验和。

n 如果微端口驱动程序和驱动程序的NIC不能处理一个包，它们不能丢弃或快速转发此包，但必须将此包指示给绑定的协议驱动程序。以下列举了一些微端口的NIC不能处理包的原因：

n IPHeader 中的iph_xsum成员的值不是0xffff。

n IPHeader 中的iph_ttl被置为0。

n 正如在FFP和包过滤中描述的，一个协议驱动程序可以使用高层协议信息设置NIC使它过滤包而不中断主机处理器。高层协议协议包括，传输控制协议（TCP）、用户数据报协议（UDP）、和网间控制报文协议（ICMP）等。通过IP网络传输的包能回分段到达NIC的适配器端口。包的报头中的高层协议信息只存在于第一个传输分段中。NIC应能从到达的包中过滤出第一个分段以及所有的后续分段。在包的第一个分段到达后，NIC应保持一些状态信息。NIC将这些状态信息用于确定是否过滤或快速转发包的后续分段。


带WDM低级接口的微端口驱动程序

本章描述如何实现一个NDIS-WDM微端口驱动程序，这样的微端口驱动程序：

n 使用Windows 驱动程序模型（WDM）低级接口

n 能调用NDIS和非NDIS函数

n 能初始化用来控制连接到一个特殊总线的远程设备，而且通过总线与远程设备通信的微端口实例

例如，一个控制通用串行总线（USB）或IEEE 1394总线的微端口要求一个对上层提供标准的NDIS微端口接口，在下层为特殊总线使用类接口的驱动程序。这样的微端口就可以通过发送I/O请求包（IRP）给总线或直接给与总线相连接的远程设备，而完成与远程设备的相互通信。

以下章节描述如何用WDM低层实现一个微端口驱动程序：

n 一个带WDM低级接口的微端口驱动程序范例的图解

n WDM低层微端口驱动程序的注册函数

n 用WDM低层初始化微端口驱动程序

n 发布命令与远程设备通信

n WDM低层微端口驱动程序的实现要点

n WDM低层微端口驱动程序的编译标志

12.1 WDM低层微端口

图12.1展示了一个与USB驱动程序接口的微端口驱动程序

图12.1 USB的微端口驱动程序

12.2 注册WDM低层的微端口函数

WDM低层的微端口驱动程序必须在其DriverEntry例程中指定明确的入口点函数。然后DriverEntry调用NdisMRegisterMiniport函数用NDIS库注册这些入口点，这些入口点函数组成了微端口驱动程序的上层，其描述如下：

n MiniportInitialize

微端口驱动程序初始化函数建立一个用于输入输出（I/O）操作的微端口实例，“Initializing a miniport driver with a WDM lower edge”描述了它必须执行初始化函数，并通过使用WDM低层的微端口来建立通信操作。

n MiniportHalt

微端口驱动程序的停止（Halt）函数释放资源，停止I/O操作。实际上，MiniHalt将恢复由MiniportInitialize为特殊微端口实例所作的所有事情。

n MiniportReset

微端口驱动程序的重置（Reset）函数将驱动程序的软件状态重置。

n MiniportReturnPacket

微端口驱动程序的返回包（return-packet）函数为随后的接收指示准备返回包。较高层的驱动程序调用这个函数向微端口驱动程序返回包的所有权。

n 微端口驱动程序的发送例程

这些函数通过网络传送协议提供的包。对无连接的微端口驱动程序，这个发送函数是MiniportSend，对面向连接的微端口驱动程序，这个发送函数是MiniportCoSendPackets。微端口驱动程序同时只能传送单个包描述符。

n 执行信息请求的微端口驱动程序例程

这些函数处理协议发起的请求，从微端口驱动程序得到信息，或设置微端口驱动程序的信息。无连接的微端口驱动程序为查询或设置信息提供两个独立的函数：MiniportQueryInformation和MiniportSetInformation，而面向连接的微端口驱动程序提供MiniportCoRequest函数来处理查询和设置两种操作。

WDM低层微端口驱动程序在其DriveEntry或初始化例程中不设置确定的入口点函数。例如，下列入口点函数会以下述原因而不设置：

n MiniportISR，MiniportHandleInterrupt，MiniportEnableInterrupt，MiniportDisable：

这些入口点在DriveEntry中没有确定。因为微端口驱动程序不从物理NIC接收中断，它将不请求这些例程，当包到达微端口驱动程序想要的总线时，特定总线的驱动程序接收这些中断，总线驱动程序然后指示微端口驱动程序。

n MiniportTransferData

这个入口点在DriveEntry中没有确定。当微端口驱动程序指出数据包时，它整个将它们都通过。高层的驱动程序从来不会调用这些例程来传递保留包的内容，所以它不是必须的。

n MiniportAllocateComplete

这个入口点在DriveEntry中没有确定。由于微端口驱动程序不需要为物理NIC分配内存，以及将内存映射到物理内存。所以也不需要一个完整的例程。

n MiniportCheckForHang

这个入口点在DriveEntry中没有确定。微端口驱动程序只能依赖于NDIS，根据它的发送和请求超时以判断其微端口是否挂起，所以这个例程也是不必要的。

n MiniportShutdown

微端口驱动程序在其初始化例程期间并不建立关闭（shut-down）例程，因为微端口实例没有必须关掉的硬件。

12.3 初始化WDM低层微端口

微端口驱动程序在被操作系统装载之后，NDIS调用微端口驱动程序的MiniportInitalize函数初始化其管理的微端口实例。要通过WDM低层的微端口驱动程序通信，微端口驱动程序必须收回建立联系的特殊的信息。在微端口实例的初始化期间，微端口驱动程序必须调用NdisMGetDeviceProperty函数，以回收其在与微端口实例建立联系（通过WDM接口）时请求的设备对象。在这个调用中，微端口驱动程序将处理以MiniportAdapterHandle参数和回收DEVICE_OBJECT结构的缓冲区的形式传递给微端口实例。这些结构代表了微端口（或许也包括微端口驱动程序控制的总线的设备对象）。微端口驱动程序使用回收的设备对象指针创建IRP，并将它们提交给接口。更多的创建和提交IRP的信息，参见《Kernel-mode Drivers Design Guide》。

WDM低层微端口驱动程序必须是一个非串行微端口驱动程序，也就是说，这样的微端口驱动程序管理着它自己内部的发送请求队列，无论它有没有足够的资源，立即传输所接收到的发送包。在微端口实例的初始化期间，非串行微端口驱动程序应当控制NDIS不将微端口实例的未决发送请求排入队列。要确定这个特征，微端口驱动程序应在调用NdisMSetAttributesEx回收时，设置NDIS_ATTRIBUTE_DESERIALIZE标志。带WDM低层的微端口驱动程序必须确定微端口是非串行的，因为它是在NDIS环境以外接收和发送数据包，也就是说，包是通过总线接口传送和接收的；NDIS不牵扯到传输过程。

12.4 发布命令与远程设备通信

控制总线的微端口驱动程序对总线接口发布命令以及与远程设备通信。为了给总线接口发布命令，这样的微端口驱动程序使用特殊总线类的IOCTL_Xxx控制码,发布IRP_MJ_INTERNAL_DEVICE_CONTROL中断请求包（IRP）。例如微端口驱动程序对USB发布控制码IOCTL_INTERNAL_USB_SUBMIT_URB，对IEEE 1394发布控制码IOCTL_ 1394_CLASS。

微端口驱动程序将命令的参数打包成一个与特殊总线相联系的请求块，并且通过中断请求包（IRP）I/O堆栈位置（IO_STACK_LOCATION）的Parameters.Others.Argument1数据成员，将指针传递给请求块。请求块的函数代码或者识别出特殊的请求，或者确定操作的类型，请求块的其他成员描述了请求和操作。例如，微端口驱动程序将IEEE 1394的命令参数打包为IEEE1394请求块（IRB），将USB的打包为USB请求块（URB）。

更多的关于创建和提交IRP的信息，参见《Kernel-mode Drivers Design Guide》。更多关于特殊总线的函数代码和请求块格式的信息，参见总线驱动程序接口的文档。

微端口驱动程序能发布命令以发送数据包给远程设备，也可以指示自己远程设备所发送的如下所述的数据包：

n 在总线上发送包

n 在总线上接收包

12.4.1 在总线上发送包

给微端口驱动程序传送包的传输请求导致NDIS微端口调用微端口驱动程序的发送例程，这个例程发布一个命令给特定总线或者给与总线相连的远程设备，这个命令了确定了通过网络发送的数据包。

12.4.2 在总线上接收包

微端口驱动程序为了能接收到通过网络传输过来的包，必须给特定总线发布命令。这个命令确定总线在接收到要传给微端口驱动程序的数据包后，指示微端口驱动程序。微端口驱动程序被指示后，它从传输中指示出那些数据包。为了指示数据包，无连接的微端口驱动程序需要调用NdisMIndicateReceivePacket函数，面向连接的微端口驱动程序需要调用NdisMCoIndicateReceivePacket函数。

12.5 WDM低层的实现要点

这一节讨论用WDM低层接口实现一个NDIS-WDM微端口驱动程序的要点。这样的一个微端口驱动程序能调用NDIS和非NDIS两种函数。这些非NDIS函数包括，WDM内核模式支持的特定总线驱动接口的函数和例程。NDIS-WDM微端口驱动程序仅限于使用WDM内核模式支持的例程（它只是Microsoft Windows 2000内核模式支持例程的一个子集）。要得到更多的WDM内核模式支持例程的信息，参见《Windows 2000 Drivers Development Reference》的第二卷，在这一卷中将WDM内核模式支持的例程等同于wdm.h头文件中所描述的内核模式例程，如果内核模式例程只出现在ntddk.h头文件中，它不是一个WDM内核模式支持的例程。

当实现一个NDIS-WDM微端口驱动程序时，必须考虑到下列问题：

n 构造一个NDIS-WDM微端口驱动程序要求在包括（Include）ndis.h头文件以前，NDIS_WDM标志已被定义。NDIS_WDM标志的定义将导致自动包括wdm.h头文件。NDIS_WDM标志应当嵌入微端口驱动程序源代码的开始，或者放在微端口驱动程序的源（source）文件中。NDIS_WDM微端口驱动程序需要wdm.h来调用象IoCallDriver和IoAllocateIrp这样的内核模式的函数。

n 特定总线驱动接口的函数调用需要总线驱动程序的头文件。

n 建议不要将NDIS和非NDIS头文件包括（include）进同一个源文件，因为它们不能很好地在一起工作，也就是说，调用NDIS函数的代码应当被创建为一个独立的源文件，调用非NDIS的也一样。

n NDIS_WDM微端口驱动程序应当调用适当的NDIS函数分配和释放资源，除非为下列之一的情况：

n 资源，典型的如内存资源，由NDIS_WDM微端口驱动程序分配，后来被一个非NDIS实体如总线驱动程序接口释放。

n 资源，典型的如内存资源，由非NDIS实体分配，后来被一个NDIS-WDM微端口驱动程序释放。

对上述情形，NDIS_WDM微端口驱动程序将调用适当的WDM函数为非NDIS实体分配或释放资源。

12.6 WDM低层的编译标志

本节列出了为建造NDIS-WDM微端口驱动程序而必须包括进源文件的编译标志，下列编译标志必须被包括：

n -DNDIS_WDM=1

指导NDIS包含wdm.h头文件。

n -DUSE_KLOCKS=1

指导NDIS为自旋锁（spin lock）使用KSPIN_LOCK类型而不是NDIS_SPIN_LOCK的变量，使用WDM内核模式函数初始化、分配和释放这些自旋锁。

作为一种选择，编译标志能在ndis.h头文件被包括之前，嵌入到微端口驱动程序源代码的开始。


IrDA微端口NIC驱动程序

红外数据协会（the Infrared Data Association：IrDA）已经制定了一种用红外线脉冲对数据编码的半双工协议。制造红外收发器用来传输和接收这些红外线脉冲，这些红外收发器由光发射二极管（LED）和接收二极管组成。接收二极管对一定距离内红外LED发出的红外线传播很敏感，发射LED必须向同样距离之外的远程接收二极管发送红外线脉冲。

红外适配器是一种由支持硬件对帧进行编码和解码的红外收发器组成的网络接口卡（NIC）。IrDA微端口NIC驱动程序是实现用来控制红外NIC。

下面的章节描述IrDA微端口NIC驱动程序的体系结构、IrDA微端口驱动程序的特征以及IrDA微端口驱动程序所必须支持的IrDA介质种类的帧格式：

n 13.1 IrDA微端口驱动程序简述

n 13.2 IrDA体系结构

n 13.3 IrDA协议驱动程序

n 13.4 IrDA介质特征

n 13.5 IrDA帧格式

n 13.6 IrDA微端口驱动程序包编码方案

n 13.7 接收和发送包序列

n 13.8 即插即用

13.1 IrDA微端口驱动程序简述

IrDA硬件（红外收发器）和核心IrDA协议栈之间的接口是通过网络驱动程序接口规范（NDIS）和IrDA微端口驱动程序来实现的，IrDA协议栈期待IrDA硬件和IrDA微端口驱动程序来处理编帧、透明、错误检测、支持感知介质行为的命令以及改变速度。IrDA微端口驱动程序负责丢弃坏的循环冗余校验（CRC）的接收包，这些帧永远不会提交到IrDA协议栈。

操作系统提供了一个核心IrDA协议栈的实现，在IrDA微端口驱动程序安装时，IrDA协议栈被即插即用的系统动态地载入。IrDA 协议栈的IrDA小传输协议（TinyTP）层通过支持用户空间部件wshirda.dll的Windows套接字（WinSock）来支持应用程序。应用程序只能使用Winsock与IrDA微端口驱动程序通信。关于WinSock的更多信息，参见platom SDK文档。

为达到更多的用途，操作系统将IrDA微端口驱动程序看作只绑定于IrDA协议栈上的NIC微端口驱动程序。IrDA 协安装程序将这些驱动程序（象IrDA适配器、处理特殊IrDA用户接口和设置等）问题显示给用户。

不象其他介质的NDIS驱动程序，IrDA微端口驱动程序不能同时绑定一个以上的协议，而且它总是绑定到同一个协议。这是因为，不象NDIS其他介质，IrDA协议栈只实现了介质控制算法的一部分，这种算法排除了多个控制器。

系统提供的和厂商提供的INF文件用来安装IrDA微端口驱动程序和IrDA协议栈，这些INF文件必须符合Net INF和IrDA INF协定。

操作系统支持的一个IrDA微端口驱动程序：irsir.sys，是用于外部连接SIR适配器的通用异步收发器（UART）和内部SIR IrDA适配器间的通信的。厂商应使它们的SIR硬件正确地被irsir.sys支持。

高质量的IrDA微端口驱动程序和INF文件能被操作系统包含在内。应当鼓励这种习惯，因为它改进了用户的IrDA安装经验。

IrDA协议栈能同时支持多个IrDA网络接口卡（NIC），希望开发这种特征的厂商应当通过其IrDA微端口驱动程序来支持多个IrDA NIC。每一个NIC应代表一个独立的收发器和支撑硬件——能进行独立的红外连接。

在为多种IrDA硬件配置实现IrDA微端口驱动程序时，有多个即插即用的问题应当被考虑到。13.8节，“即插即用”，将讨论这些问题。

13.2 IrDA体系结构

图13.1显示了IrDA协议层的组成，以及IrDA微端口驱动程序是怎样通过NDIS库与其NIC及应用程序通信的。

图13.1 IrDA体系结构

13.3 IrDA协议驱动程序

IrDA协议驱动程序，irda.sys，是打开IrDA微端口驱动程序的唯一的协议驱动程序。这个协议驱动程序是IrDA红外连接访问协议（IrLAP）的一个实现，协议驱动程序负责检测红外设备、在本地和远程设备之间建立连接以及可靠地在本地和远程设备之间传送数据。IrDA协议驱动程序使用IrDA微端口驱动程序，在本地和远程设备之间收发数据。

IrDA协议驱动程序的服务类似于TCP/IP协议表现出来的服务。使用这些服务，运行于两个不同计算机上的应用程序能很容易地打开多个用于发送和接收数据的可靠连接。IrDA协议驱动程序提供下列服务：

n 小传输协议（TinyTP）是通过Winsock提供给应用程序的。应用程序能使用TinyTP建立连接和收发数据。

n 红外连接管理协议（IrLMP）为IrLAP增加多个会话支持。多个应用程序能使用IrLMP来监视相互没有接口的连接。单个应用程序也能使用IrLMP同时打开一个控制连接和一个数据连接。IrLMP也能为单个IrDA连接提供的流控制增加每个连接流的控制。IrLMP使得应用程序能得到传给IrDA协议栈的大块数据，IrDA协议栈然后将这些数据以优化的速度送出。使用IrLMP，应用程序不需要监视丢失的数据或者进行流控制。

n 信息访问服务（IAS）直接在IrLMP的顶部运行。IAS将一个ASCII服务名映射为一个LSAP-SEL。LSAP-SEL是用于从运行于服务器上多个可能的应用程序中挑选出一个应用程序的协议元素。这个协议的名字是提供给应用程序的一个友好的抽象。

n 红外连接打印传输（IrLPT）与打印机通信。TinyTP不用于IrLPT。IrLPT使用一种只支持单个IrLMP连接的专有模式，这个单个IrLMP连接使用了IrLAP的流控制特性。IrLMP定义了这种专有模式的服务。

IrDA协议驱动程序绑定到了所有支持NdisMediumIrDA介质类型的IrDA微端口驱动程序。IrDA微端口驱动程序只绑定到IrDA协议驱动程序，因为，不象其他NDIS介质，IrDA IrDA协议层实现了介质控制算法的一部分，这种算法排除了多个控制器。

13.4 IrDA介质特征

与早期的NDIS介质比起来，通过IrDA介质控制和通信，NdisMediumIrDA会涉及到一些独特的问题。IrDA微端口驱动程序解决了这些问题。下列各节将描述红外介质的特征：

n 13.4.1 通信连接速度

n 13.4.2 通信连接回转时间

n 13.4.3 接收器同步

13.4.1 通信连接速度

不象典型的NDIS介质，红外介质支持许多不同的传输和接收比特的速度。当前定义的操作速度从2 400比特每秒（bps）直到4 M比特每秒（Mbps）。在将来，IrDA将会定义更高的速度。各种不同速度的设计目的导致了三种不同的帧编码方式：SIR，MIR和FIR。这些不同的帧编码方式必须都由IrDA微端口驱动程序处理，然后传给协议。

当前定义的IrDA速度及帧编码方式列表如下：

速度（bps） | 帧编码方式
2 400 9 600 19 200 38 400 57 600 115 200 576 000 1.152 Mbps 4 Mbps | SIR SIR SIR SIR SIR SIR MIR MIR FIR

关于帧编码方式的定义，参见13.6节，“IrDA微端口驱动程序包编码方案”。

特定的红外适配器不需要支持上表列出的所有速度。然而，对于两个相互通信的IrDA设备，在支持的速度上必须有重叠部分。IrDA标准规定所有的IrDA设备必须支持 9 600 bps的速率。这样的IrDA规范保证了所有的IrDA设备至少能以9 600bps通信。通过以9 600bps速率通信，IrDA设备可能协商达到一个更高的速度，例如，两个IrDA设备必须以9 600 bps的速度协商，但如果两个设备都支持 115.2 k比特每秒（Kbps），它们就会以这个较高的速度来传输数据。注意在很少的情况下，两个IrDA设备以9 600 bps协商，但两个IrDA设备共享的通信速度是唯一的2 400bps的低速。

IrLAP协议能通过给驱动程序发送一个OID_IRDA_SUPPORTED_SPEEDS查询以检测IrDA微端口驱动程序支持什么速度。

IrLAP协议在传输包以前，使用OID_IRDA_LINK_SPEED和OID_IRDA_SUPPORTED_ SPEEDS的返回值来设置IrDA微端口驱动程序的连接速度。连接速度指红外适配器向外传输帧的速度以及可能接收传入帧的速度。关于怎样设置OID_IRDA_ LINK_SPEED来控制传入帧的速度，参见OID_IRDA_RATE_SNIFF的描述信息。

13.4.2 通信连接回转时间

红外适配器由支持硬件对帧编码和解码的红外收发器组成。红外收发器包含许多靠得很近的发射LED和接收二极管。接收二极管对红外线相当敏感，因为它必须接收到至少一米之外的远程红外LED传输来的信号。发射LED必须是大功率的，因为它必须向同样距离之外的远程接收二极管传输信息。

在传输期间，本地的LED必须发射足以使本地接收二极管饱和的光线。就象人们盯着太阳看时看不清楚一样，本地LED向外发出传输帧之后，本地接收二极管很难立即正确地接收到传过来的帧。

本地LED接收二极管从饱和状态变得能再接收来帧所允许的时间，即为IrDA协议定义的回转时间。回转时间确定了以毫秒计的接收二极管从饱和状态恢复的时间。对一些IrDA设备，回转时间可以忽略不计，有些IrDA设备则是相对较长的一段时间。

本地接收二极管的回转时间将不会影响本地收发器的行为。然而本地接收二极管的回转时间会影响远程收发器的期望行为。例如，本地收发器从其LED完成传输到其接收二极管能接收信号需要1毫秒的时间延迟，在开始传输一个新帧之前，远程的工作站在接收到上一帧的最后一位之后就必须等待1毫秒的时间，远程工作站执行这个等待以兑现本地收发器的回转时间。

IrDA微端口驱动程序必须被告知其本地收发器的回转时间。IrDA微端口驱动程序使用OID_IRDA_TURNAROUND_TIME来确定这个时间值，IrLAP能在任何时候给IrDA微端口驱动程序发送OID_IRDA_TRUNAROUND_TIME查询来得到本地收发器的回转时间。

考虑到远程收发器的回转时间，IrDA协议有时会规定延迟包的传输，如果要这样做，IrLAP协议须确定包传输后的时间长短。IrDA微端口驱动程序在等待请求的时间数之前不必传输数据包，尽管驱动程序可以等待较长的时间。IrDLAP协议在数据包的带外（OOB）数据块的介质相关成员中规定了包的传输延迟时间。关于更多的信息，参见13.7节，“发送和接收帧序列”。

13.4.3 接收器同步

一些红外收发器使用自己红外信号中的信息来与信号同步，以便能接收到传过来的帧。由于红外收发器使用不同的硬件，这些收发器同步到它们能可靠地接收数据的位置也需要不同的时间。典型的情况，红外收发器在接收到位于每一个传入包起始的帧开始（BOF）标志时与红外信号同步。由于一些硬件需求，红外收发器也许需要一个或多个BOF标志以完成接收器同步。

IrDA微端口驱动程序必须一直持有（其收发器在接收的红外信号中请求同步时所使用的）额外的BOF标志数目的信息。IrDA微端口驱动程序使用OID_IRDA_EXTRA_RCV_ BOFS来确定请求额外的BOF标志。IrLAP可以在任何时候给IrDA微端口驱动程序发送一个OID_IRDA_EXTRA_RCV_BOFS来得到本地收发器的额外BOF标志的数目。IrDA微端口驱动程序也必须被告知远程接收工作站所请求的额外BOF标志。在IrLAP与远程接收工作站协商连接速度并设置IrDA微端口驱动程序的连接速度之后，IrDA微端口驱动程序必须使用下列换算表来检测额外BOF标志的请求数目。

在OID_IRDA_EXTRA_RCV_BOFS中规定的值应用于115.2 Kbps的串行红外（SIR）连接速度，其他SIR连接速度的值由此值换算而来。IrDA微端口驱动程序使用下列换算表为那些驱动程序从连接速度中确定所需要的额外BOF标志。

速度 | 请求的额外BOF标志
2 400 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0
9 600 | 4 | 2 | 1 | 0 | 0 | 0 | 0 | 0
19 200 | 8 | 4 | 2 | 1 | 0 | 0 | 0 | 0
38 400 | 16 | 8 | 4 | 2 | 1 | 0 | 0 | 0
57 600 | 24 | 12 | 6 | 3 | 1 | 1 | 0 | 0
115 200 | 48 | 24 | 12 | 6 | 3 | 2 | 1 | 0

为确定OID_IRDA_EXTRA_RCV_BOFS的值，IrDA微端口驱动程序应当指示IrDA协议驱动程序：

1. 在最左一列中找到其NIC收发器的连接速度；

2. 在这一行，找出收发器以这种速度工作时的额外BOF标志的请求数目。

3. 往下找到115.2 Kbps行的那一列，换算表为115.2Kbps规定的额外BOF标志数就是微端口驱动程序应当指示给IrDA协议驱动程序的额外BOF标志数。

下面的例子展示了IrDA微端口驱动程序怎样使用上述换算表确定适当的OID_IRDA_ EXTRA_RCV_BOFS值的：

n 以2 400bps速度传输，如果NIC的收发器为了同步请求1个额外BOF标志，IrDA微端口驱动程序必须将OID_IRDA_EXTRA_RCV_BOFS的值确定为48；

n 以57.6 Kbps速度传输，如果NIC的收发器为了同步请求8个额外BOF标志，IrDA微端口驱动程序必须将OID_IRDA_EXTRA_RCV_BOFS的值确定为24，因为8处于表中的6和12之间，IrDA微端口驱动程序使用较大的12来确定OID_ IRDA_EXTRA_RCV_BOFS的值。

n 以115.2 Kbps速度传输，如果NIC的收发器为了同步请求1个额外BOF标志，IrDA微端口驱动程序必须将OID_IRDA_EXTRA_RCV_BOFS的值确定为1；

注意MIR和FIR红外收发器的连接速度必须以此速度下BOF标志准确的数目来工作。

为了兑现远程工作站所要求的额外BOF标志数，IrLAP必须在送往IrDA微端口驱动程序传输的每一个数据包中确定额外BOF标志的数目，这个值总是为当前连接速度下的额外BOF标志数。因为IrLAP在每个包中确定了额外BOF标志数，IrDA微端口驱动程序不需要在每次准备发送包之前使用上述的换算表。IrDLAP协议在数据包的带外（out-of-band OOB）数据块的介质相关成员中规定了额外BOF标志数目。关于更多的信息，参见13.7节，“发送和接收帧序列”。

13.5 IrLAP帧格式

IrDA协议栈的协议最低层指的是红外连接访问协议（IrLAP）。IrLAP定义了红外介质上发送和接收的帧的格式。下列各节将描述IrLAP帧格式的内容：

n 13.5.1 帧格式简述

n 13.5.2帧信息的使用

n 13.5.3 地址成员

13.5.1 帧格式简述

IrLAP定义了红外介质上发送和接收的帧的格式。每一个IrLAP帧由下列元素组成：

n 一个或多个表示帧开始的帧起始（BOF）标志。BOF成员长度大小依赖于速度而不同，就象在“接收器同步”节描述的那样。

n 识别第二个联系地址的地址（A）成员。地址成员为8位，地址成员确定了属于特定IrDA微端口驱动程序设备的地址，IrDA微端口驱动程序通过这个设备传输或接收包含其地址的帧。更多的地址成员的信息，参见13.5.3节“地址成员”。

n 确定特殊帧功能的控制（C）成员。控制成员为8位。

n 包含信息数据的可选信息（I）成员。信息成员是八位字节的整数。

n 帧检查序列（FCS）允许接收站检查帧的传输准确性。FCS依赖于速度为16位或32位。

n 帧结束（EOF）标志表示帧的结束。EOF成员的大小随随速度变化而不同。

下列IrLAP帧的例子显示了上述元素的顺序：

BOF

A

C

I

FCS

EOF

包的编码和BOF、EOF以及FCS成员随红外介质的操作速度而变化。关于物理帧的完整文档能在IrDA的发布的《Infrared Data Association Serial Physical Layer Link Specification》中找到。13.6节 “IrDA微端口驱动程序包编码方案”提供了各种不同编码方案的简介。

13.5.2 帧信息的使用

IrDA协议驱动程序调用NdisSendPackets函数发送一个包描述符序列（NDIS_PACKET结构）到下层IrDA微端口驱动程序。这些包描述符只给出了帧从“地址元素”到“信息元素”的信息，IrDA在传输此帧之前必须给它加上其他所有的帧元素。同样地，IrDA微端口驱动程序在将包指示给IrDA协议驱动程序之前，必须将接收包的“地址元素”到“信息元素”之外的所有元素剥离。

IrDA微端口驱动程序必须进行一个用来转换传递到红外介质上的包的编码操作，同样的，IrDA微端口驱动程序也必须进行解码操作，将从红外介质上接收的包转换为其原始形式。

如果从红外介质上接收的包是无效的，IrDA微端口驱动程序将不不把它提交给IrDA协议驱动程序。一个包可以因为下列原因而变得无效：

n 包不完整

n 包的循环冗余校验（CRC）无效

n 包包含一个拒绝序列

13.5.3 地址成员

下列例子显示了IrLAP帧中的地址成员的8比特信息：

C/R

A

A

A

A

A

A

A

地址成员的第一位用于命令与响应（C/R）。如果这一位被设置了，表示这个包从主站送到从站。如果C/R位被清除，表示包从从站送往主站。因为IrDA微端口驱动程序不需要C/R位的信息，它并不监视这一位。注意主站启动了到从站的连接，并运行使这些连接有效的时钟。

地址成员中剩下第7个信号位表示连接访问协议（LAP）的地址，LAP的地址是一个属于特殊IrDA微端口驱动程序的设备地址。IrDA微端口驱动程序通过这个设备传送和接收数据包。

IrLAP协议能指定一个能接收包的设备地址清单，从而限制所有其他的设备接收数据包。为了指定这个清单，IrLAP协议请求IrDA微端口驱动程序用OID_IRDA_UNICAST_LIST在UNICAST列表中设置地址，UNICAST列表是一个IrDA微端口驱动程序能通过它接收数据包的设备地址的数组。如果UNICAST列表设置了，IrDA微端口驱动程序将只能从指定地址接收有效的红外数据帧；如果UNICAST列表没有设置或包含0元素，IrDA微端口驱动程序将必须接收所有有效的红外帧，然后将数据包提交给IrLAP协议。

13.6 IrDA微端口驱动程序包编码方案

实际在红外介质上传输的包的格式是随着包传输和接收的速度不同而变化的，IrDA微端口驱动程序形成包的方式依赖于其红外NIC的硬件实现。下列各节将简要描述不同类型的红外NIC的帧编码的方法：

n 13.6.1 SIR编码

n 13.6.2 MIR编码

n 13.6.3 FIR编码

如果红外NIC的硬件允许开发者进行红外介质的编码，他们能得到出版物：《Infrared Data Association Serial Physical Layer Link Specification》和《Infrared Data Association Serial Infrared Link Access Protocol （IrLAP）》，这些出版物详细地解释IrDA提供的所有类型的红外NIC帧的编码。

13.6.1 SIR编码

本节描述IrDA微端口驱动程序或者其红外NIC怎样对串行IrDA（SIR）连接速度下的传输帧进行编码。SIR规范用1个起始位、8个数据位和1个结束位定义了一个小范围的红外异步串行传输模式。最大的数据传输率为115.2 Kbps（半双工）。以SIR对帧进行编码的最大益处是已存在的串行硬件非常便宜，使用串行硬件的低花费是红外SIR设备广泛应用的一个原因。

SIR速度的BOF标志定义为0xC0，EOF的值定义为0xC1。为了避免包含BOF和EOF的帧含糊不清，为帧中的其他部分出现的0xC0和0xC1定义了一个转义序列，转义字符定义为0x7D。

对遇到的每一个象BOF、EOF或转义序列这样的字符，发送器将执行下列步骤：

1. 在每一个这样的字节之前插入一个控制序列字节（0x7D）

2. 对每一个象BOF、EOF或转义序列这样的字符的第五位求反，也就是用0x20对其进行一个或（OR）运算。

在驱动程序插入任何一个控制序列字节之前，IrDA微端口驱动程序计算出帧的FCS成员。

在FCS测定之前，接收站也将测试BOF和EOF之间的整个帧环境。接收站对遇到的每一个转义字节执行下列步骤：

1. 丢弃转义字节

2. 对转义字节以后字节的第五位求反

IrDA微端口驱动程序用地址（A）、控制（C）和信息（I）成员来计算帧的FCS序列。对SIR连接速度，具有代表性的，是用IrDA微端口驱动程序而不是驱动程序的硬件来计算CRC值。计算16位CRC的算法可参见IrDA发布的《Infrared Data Association Serial Physical Layer Link Specification》和《The Internet Working Group Request For Comments（RFC）1171》。

对SIR连接速度，IrDA微端口驱动程序也能在包中编入一个拒绝序列。拒绝序列指示红外收发器接收到这样一个包后丢弃它，拒绝序列为0x7D 0xC1。

13.6.2 MIR编码

本节讨论IrDA微端口驱动程序或者其红外NIC对以中间IrDA（MIR）连接速度传输的帧的编码。MIR速率为0.576 Mbps和1.152 Mbps（半双工）。

对MIR连接速度，BOF和EOF的定义是相同的，都定义为0x7E。为了避免带BOF和EOF的帧含糊不清，MIR速率在BOF和EOF之外的所有成员中每5个连续的“1”之后插入1个“0”，而不是象SIR那样使用转义序列。因为在比特级插入和去除“0”的过程是很消耗处理器的，所以强烈推荐在硬件中实现这个逻辑。在MIR连接速度中，每一个帧需要两个BOF标志。

对MIR连接速度，象SIR速度一样也使用了CRC，也就是说，对MIR连接速度，是IrDA微端口驱动程序而不是驱动程序的NIC来计算CRC值。

13.6.3 FIR编码

本节讨论IrDA微端口驱动程序或者其红外NIC对以快IrDA（FIR）连接速度传输的帧的编码。FIR规范定义了一个4Mbps下的小范围低能耗的操作（半双工）。所有的FIR设备都需要支持SIR操作。

对FIR连接速度，使用了一个完全不同的编码方案——称为脉冲位置调制（PPM）。PPM为BOF和EOF定义了一个特殊的标志。PPM编码方案应当在硬件实现。

IrDA微端口驱动程序也需要计算CRC以检验帧的有效性。对FIR连接速度，使用了一个32位的CRC。计算32位CRC的算法在IrDA出版的《Infrared Data Association Serial Physical Layer Link Specification》中提供。

13.7 发送和接收帧序列

IrDA微端口驱动程序应当支持以下所描述的发送和接收帧序列。

IrDA微端口驱动程序应当实现一个MiniportSendPackets函数来通过网络传输帧的序列。IrLAP调用NdisSendPackets函数发送一个帧标识符的序列给下层的IrDA微端口驱动程序。每一个帧标识符都关联着带外（OOB）信息。IrDA微端口驱动程序能从OOB数据块回收和使用（NDIS_PACKET_OOB_DATA）指定给红外介质的信息。

包从远程站传过来后，NIC的收发器接收这些数据包，然后发出中断给NDIS，再依次调用特殊的例程处理这些中断。NIC的IrDA微端口驱动程序应当实现MiniportIsr和MiniportHandleInterrupt函数为接收到的包处理中断。为将数据包指示给IrDA微端口驱动程序，IrDA微端口驱动程序为每一个包设置（已在OOB数据块中指定的）红外介质的信息，然后调用NdisMIndicateReceivePacket函数。

IrDA微端口驱动程序应当使用NDIS提供的设置和回收OOB数据的宏。关于OOB数据和访问OOB数据的信息，参见第4章4.3节“带外（OOB）数据包”。

OOB数据块的MediaSpecificInformation成员指向一个MEDIA_SPECIFIC_ INFORMATION类型的结构，而这种结构有一个为NDIS_IRDA_PACKET_INFO类型结构的ClassInformation成员。NDIS_IRDA_PACKET_INFO定义如下：

typedef struct _NDIS_IRDA_PACKET_INFO{

UINT ExtraBOFs;

UINT MinTurnAroundTime;

}NDIS_IRDA_PACKET_INFO,*PNDIS_IRDA_PACKET_INFO;

ExtraBOFs 成员表示增加到传输包中的额外帧起始（BOF）数目，关于更多的信息，参见13.4.3节“接收器同步”。

MinTurnAroundTime表示帧的最小延迟时间，关于更多的信息，参见13.4.2节“通信连接回转时间”。

13.8 即插即用

当为各种各样硬件配置的NIC红外收发器实现IrDA微端口驱动程序时，应该考虑到几个即插即用问题。这些配置包括，支持即插即用、所连接系统的类型以及连接速度。下列章节将描述这些问题：

n 13.8.1 非即插即用外部串行连接SIR适配器

n 13.8.2 非即插即用内部SIR适配器或者象串口一样错误地呈现于外的内部SIR适配器

n 13.8.3 即插即用外部串行连接SIR适配器

n 13.8.4 即插即用内部SIR适配器

n 13.8.5 非即插即用总线连接FIR适配器

n 13.8.6 即插即用总线连接FIR适配器

13.8.1 非即插即用外部串行连接SIR适配器

连接到外部串口并且不支持任何即插即用功能的红外SIR设备由操作系统提供的IrDA微端口驱动程序irsir.sys来支持。系统提供的IrDA微端口驱动程序使用串口驱动程序提供的服务来访问串口。串口驱动程序可以是操作系统提供的一个serial.sys，或者是一个兼容的第三方串口驱动程序。

外部串口在“控制面板/设备管理”列表的“端口（COM&LPT）”单元中对用户可见。系统提供的IrDA微端口驱动程序忽略了串口的特殊设置。所以，用户不能正确地配置串口以至于连接到外部串口的红外设备不能工作。

用户应当通过“硬件向导（Wizard）”的“增添新硬件”功能手工安装这些设备。用户应当选择：

1. IrDA适配器设备类型

2. 从支持适配器类型的列表中找到的IrDA适配器设备

3. 适配器连接的串口，串口通过其名字来选择

系统提供的IrDA微端口驱动程序打开用户通过其名字而选择的串口。请求支持这个端口的资源是不可见的，用户可以通过IrDA适配器的用户接口来配置它。用户只能配置串口名。

需要操作系统支持硬件厂商应当请求这样的支持、提供特定设备初始化和速度变化的程序。

13.8.2 非即插即用内部SIR适配器或者象串口一样错误地呈现于外的内部SIR适配器

内部连接的和任何非即插即用功能的红外SIR设备不象即插即用的SIR设备那样通过系统BIOS呈现于外。这些设备被检测到以后再进行安装，或者它们被象常规串口那样手工安装。

如果红外SIR设备的IrDA UART只象常规的串行设备那样通过BIOS呈现于外，这个设备就象这样安装。在这种情况下，用户的感觉就象在《Non-Plug ang Play External Serial-Attached SIR Adapters》中描述的那样。

推荐不要使用这种配置。

13.8.3 即插即用外部串行连接SIR适配器

当前没有连接到外部串口的红外SIR设备支持串行计算，所以不支持这种配置。

13.8.4 即插即用内部SIR适配器

内部连接的和支持任何即插即用功能的红外SIR设备，象即插即用的SIR设备那样通过系统BIOS（PNP0510或PNP0511）呈现于外。这些设备象常规的UART一样呈现给IrDA微端口驱动程序，这是由操作系统的irsir.sys和一个串口驱动程序提供的。这个串口驱动程序可以是操作系统提供的一个serial.sys，或者是一个兼容的第三方串口驱动程序。

这些设备并不在“控制面板/设备管理”列表的“端口（COM&LPT）”单元可见。因为它们象串行设备那样并不是通过BIOS呈现出来，这就意味着在这个端口上串口驱动程序不能自动调入，另外，这些设备象常规串口一样对用户和应用程序不可见。

一旦安装了这些设备通过即插即用，它们就在“设备管理器”列表的“红外适配器”单元中呈现给用户，而且，安装了系统提供的IrDA微端口驱动程序，它使用下层IrDA UART描述的资源。为了使用IrDA UART资源，系统象低层过滤驱动程序一样通过IrDA微端口驱动程序载入串口驱动程序。

SIR IrDA UART请求的资源必须以常规即插即用的方式呈现出来。即插即用试图找到一个相互不冲突的资源的集合。为了使得所有的系统硬件一起工作，内置的或OEM包装的硬件必须总是支持一定的资源集。操作系统不允许中断共享，所以，SIR UART硬件必须配置到3和4以外的中断上工作，这两个中断是早期的程序中断。这个中断配置必须通过BIOS提供出来。

注意，在很多情况下，提供给SIR IrDA UART的中断3和4的请求不能工作。

13.8.5 非即插即用总线连接FIR适配器

本节讨论连接到总线而且不支持任何即插即用功能的红外FIR设备。

在这种情况下，即插即用支持的缺乏表明了FIR硬件不响应总线计算或不通过系统BIOS呈现出来。如果用户试图安装多个原始的FIR硬件，它们将不可能成功。例如，多个原始FIR硬件包括，带许多硬件跳线的FIR硬件。

所有必需的FIR硬件也要支持SIR协议和速度。FIR硬件除FIR的DMA机制以外通常为SIR提供一个UART，来满足这个要求。这种配置通常要求IrDA微端口驱动程序和安装INF文件都必须复合在一起。SIR和FIR部件两者都必须都是可编程的。而且，SIR必须支持中断3和4以外的中断，必须有能力至少使用早期的COM 3或COM 4的I/O空间。这种资源需求对避免与早期的串口相冲突是十分必要的。这些冲突对诊断也许是微妙的和困难的。

这些FIR设备必须由IrDA微端口驱动程序和安装INF文件来提供。INF文件必须列出它所支持的资源组合，必须当用户选择了一个不冲突的资源集时就能对FIR硬件进行编程。INF文件必须描述所需要的其他任何一个参数，而且用户必须能配置这些参数。其他参数包括，红外收发器的类型。

一旦安装了这些FIR设备，它们就在“控制面板”“设备管理器”列表的“红外适配器”单元中呈现给用户，而且，安装了FIR设备的IrDA微端口驱动程序，这些驱动程序使用FIR资源以及下层IrDA UART描述的资源。

为了使用IrDA UART资源，IrDA微端口驱动程序能象低层过滤驱动程序一样载入串口驱动程序，serial.sys，但是IrDA微端口驱动程序不要求这么做。

建议不要使用这种配置。

13.8.6 即插即用总线连接FIR适配器

本节讨论连接到总线而且支持即插即用功能的红外FIR设备。

同样的应用于非即插即用FIR设备的限制也应用于即插即用的FIR设备。也就是说，SIR和FIR部件必须都是可编程的，而且SIR必须支持诊断3和4以外的中断，必须至少使用COM3和COM4的早期I/O空间。这些资源必须或者通过系统BIOS正确报告或者在安装INF文件中被描述。

支持即插即用的FIR设备必须报告一个唯一的精确描述芯片组和收发器组合的即插即用标识符。即插即用标识符的标准当前并不存在。请留意能发布已知的FIR硬件组合及其适当的即插即用标识符的组织，这个组织可能是IrDA、芯片组或收发器厂商集团。

这些FIR设备必须由IrDA微端口驱动程序和安装INF文件来提供。INF文件必须为IrDA微端口驱动程序列出能支持即插即用的SIR和FIR资源的组合。

一旦FIR设备被安装，它们就在“控制面板”“设备管理器”列表的“红外适配器”单元中呈现给用户，而且，安装了FIR设备的IrDA微端口驱动程序，这些驱动程序使用FIR资源以及下层IrDA UART描述的资源。

FIR设备的IrDA微端口驱动程序象低层过滤驱动程序一样，不能调入系统提供的串口驱动程序serial.sys，FIR设备通常提供FIR功能扩充了的标准UART寄存器。使用过滤器一样的串口驱动程序排除了对硬件或IRQ直接访问。

支持即插即用的FIR设备能以一个唯一的即插即用标识符和标准SIR即插即用别名呈现出来，如果操作系统能将FIR即插即用标识符与IrDA微端口驱动程序匹配起来，操作系统将安装这个驱动程序；否则，操作系统将安装串口驱动程序。

象常规串口一样不正确呈现出来的FIR设备只支持手工安装。在这种情况下，用户的经历如同13.8.2节“非即插即用内部SIR适配器或者象串口一样错误地呈现于外的内部SIR适配器”所描述的。


NDIS中间层驱动程序

NDIS（网络驱动器接口标准）中间层驱动程序在其上边界导出MiniportXxx函数，在其下边界导出ProtocalXxx函数。该驱动程序在其上边界仅提供面向无连接通信支持，而在其下边界，则即可支持面向无连接通信，也可支持面向连接通信 （关于面向连接通信方面更多的信息，可参阅本书的第四部分）。

中间层驱动程序的微端口部分（上边界）必须是非串行的，系统将依赖这些非串行驱动程序，而不是NDIS对MiniportXxx函数的操作进行串行化处理和对内部生成的输出包进行排队操作提供良好界导出TDI驱动程序__________________________________________________________________________________________________________________，这样驱动程序只要保持很小的临界区（每次只能有一个线程执行该代码）就能提供性能良好的全双工操作。但是这些非串行Miniport要受到更多也更严格的设计要求的限制，往往要为此付出更多的调试和测试时间（关于非串行驱动程序的更多的信息可参阅第二部分第四章4.5节）。

中间层驱动程序是一种典型的层次结构程序，它基于一个或多个NDIS NIC驱动程序，其上层是一个向上层提供TDI（传输驱动程序接口）支持的传输驱动程序（也可能是多层结构）。从理论上讲，一个中间层驱动程序也可以是基于其他中间层驱动程序或作为其他中间层驱动程序的低层出现的，尽管这种方案未必能展现更好的性能。

中间层驱动程序的一个示例是LAN仿真中间层驱动程序，其上层是一个早期传输驱动程序，下层是一个非LAN介质的微端口 NIC驱动程序。该驱动程序从上层接收LAN格式的数据包并将其转换为本地网卡的介质格式，然后将其发送到那个NIC的NDIS 微端口。接收数据时，该驱动程序将低层网卡驱动程序送来的数据包转换为LAN兼容格式，最后向上层传输驱动程序提交这些转换过的数据包。

例如，NDISWAN就具有一些上述特征。NDISWAN将数据包从上层的传输LAN格式转换为WAN数据包格式，或者将数据包从低层的网卡驱动WAN格式转换为LAN数据包格式。另外，如果低层NIC硬件不支持这些功能，那么NDISWAN也可提供诸如压缩、加密和端对端协议（PPP）等的数据格式化功能。NDISWAN为在NDISAPI和网卡驱动程序之间进行通信提供了一个专用接口，同时，NDISWAN也将协议绑定映射为活动连结请求。

另一个中间层驱动程序的例子是ATM LANE （LAN仿真）驱动程序，它将数据包从上层无连接的传输格式转换为下层面向连接的网卡支持的ATM格式。

图1.1说明了中间层驱动程序结构

图1.1中间层驱动程序结构

NDIS中间层驱动程序在NDIS中起着转发上层驱动程序送来的数据包，并将其向下层驱动程序发送的接口功能。当中间层驱动程序从下层驱动程序接收到数据包时，它要么调用NdisMXxxIndicateReceive函数，要么调用NdisMindicateReceivePacket函数向上层指示该数据包。

中间层驱动程序通过调用NDIS打开和建立一个对低层NIC驱动程序或者NDIS中间层驱动程序的绑定。中间层驱动程序提供MiniportSetInformation和MiniportQueryInformation函数来处理高层驱动程序的设置和查询请求，某些情况下，可能还要将这些请求向低层NDIS驱动程序进行传递，如果其下边界是面向无连接的可通过调用NidsRequest实现这一功能，如果其下边界是面向连接的则通过调用NidsCoRequest实现该功能。

中间层驱动程序通过调用NDIS提供的函数向网络低层NDIS驱动程序发送数据包。例如，下边界面向无连接的中间层驱动程序必须调用NdisSend或NdisSendPackets来发送数据包或者包数组，而在下边界面向连接的情况下就必须调用NdisCoSendPackets来发送包数组数据包。如果中间层驱动程序是基于非NDIS NIC驱动程序的，那么在调用中间层驱动程序的MiniportSend或Miniport(Co)SendPackets函数之后，发送接口对NDIS将是不透明的。

NDIS提供了一组隐藏低层操作系统细节的NdisXxx函数和宏。例如，中间层驱动程序可以调用NdisMInitializeTimer来创建同步时钟，可以调用NdisInitializeListHead创建链表。中间层驱动程序使用符合NDIS标准的函数，来提高其在支持Win32接口的微软操作系统上的可移植性。

可分页和可丢弃代码

就像在第一部分中所描述的，每一个MiniportXxx函数或ProtocolXxx函数都运行在一个特定的IRQL上，在中间层驱动程序中这些函数可使用的IRQL从PASSIVE_LEVEL一直到DISPATCH_LEVEL(包括DISPATCH_LEVEL)。

总是运行在IRQL PASSIVE_LEVEL上的中间层驱动程序函数可通过调用NDIS_PAGEABLE_FUNCTION宏将其标记为可分页代码。驱动程序设计者应尽可能的将程序代码设计为可分页的，为那些必须驻留内存代码释放系统空间。运行在IRQL PASSIVE_LEVEL的驱动程序函数，当其既不调用运行在IRQL >=DISPATCH_LEVEL的任何函数，也不被运行在IRQL >=DISPATCH_LEVEL的任何函数调用时，可将其标注为可分页的。例如，一个获取自旋锁的函数，而获取自旋锁将促使获取线程提升到IRQL DISPATCH_LEVEL上运行。一个运行在IRQL PASSIVE_LEVEL的函数，如ProtocolBindAdapter，如果被标注为可分页的，就不能再调用运行在IRQL >=DISPATCH_LEVEL NDIS的函数。关于运行在IRQL 上的NDIS函数的更多信息，请参阅在线DDK的“Network Drivers Reference”，其中列出了每一个NdisXxx函数的IRQL。

中间层驱动程序的DriverEntry函数以及只在DriverEntry中调用的代码，应该用NDIS_INIT_FUNCTION宏将其设定为仅用作系统初始化功能。假定NDIS_INIT_FUNCTION宏标识的代码仅在系统初始化时运行，这样该部分代码将只有在初始化时才会被映射，在DriverEntry返回后，NDIS_INIT_FUNCTION宏标识的这部分代码将被丢弃。

共享资源的访问同步

如果驱动程序分配的资源能够被两个驱动程序函数同时共享，或者中间层驱动程序能够运行在SMP（对称多处理）机器上，这样相同的驱动程序函数能够从多个处理器同时访问该资源，那么对这些共享资源的访问必须进行同步。例如，驱动程序维持一个共享队列，使用自旋锁来对队列的访问进行同步，自旋锁在队列创建之前调用NdisAllocateSpinLock进行初始化。

当然，也不必过分地保护共享资源，例如，对于队列，一些读操作不进行串行化也是可以成功执行的。但任何针对队列链接的操作都必须进行同步。自旋锁应该尽量少使用，并且每次都要尽可能缩短其使用的时间。关于自旋锁更深入的讨论可参阅“Kernel_Mode Drivers Design Guide”。

1.1中间层驱动程序的DriverEntry函数

为了使加载程序能够准确地识别，必须将中间层驱动程序的初始入口点明确地指定为DriverEntry的形式。所有其他的驱动程序导出函数，像这里所描述的MiniportXxx和ProtocolXxx函数，由于其地址传给了NDIS，因此在设计时可由开发者任意指定名称。任何内核模式驱动程序DriverEntry的定义具有以下形式：

NTSTAUS

DriverEntry(

IN PDRIVER_OBJECT DriverObject,

IN PUNICODE_STRING RegistryPath

);

如果除了ProtocolXxx之外，驱动程序还导出一组标准的内核模式驱动程序函数，那么必须将这些标准函数的地址写入要传给DriverEntry的驱动程序对象中。

在中间层驱动程序中，DriverEntry至少应该完成以下工作：

1、调用NdisMInitializeWrapper并保存在NdisWrapperHandle中返回的句柄；

2、传递上一步保存的句柄，调用NdisIMRegisterLayeredMiniport注册驱动程序的MiniportXxx函数；

3、如果驱动程序随后要绑定到低层NDIS驱动程序上，则调用NdisRegisterProtocol注册驱动程序的ProtocolXxx函数；

4、如果驱动程序导出了MiniportXxx和ProtocolXxx函数，那么调用NdisIMAssociateMiniport向NDIS通告有关驱动程序的微端口低边界和协议高边界信息；

DriverEntry能够为中间层驱动程序分配的所有共享资源初始化自旋锁，例如驱动程序用于跟踪运行中的连接和发送任务的构件和内存区。

当DriverEntry不能为驱动程序分配用于网络I/O操作的所有资源时，它就应该释放先前已经分配的任何资源并返回一个适当的错误状态。例如，如果DriverEntry已经调用了NdisMInitializeWrapper函数，那么当后续操作出错时必须调用NdisTerminateWrapper复位系统状态。

中间层驱动程序的DriverEntry函数能够执行一些全局初始化操作。然而，如果驱动程序提供了在1.2节所描述的，实现对低层设备的打开和绑定功能的ProtocolBindAdapter函数，那么驱动程序就能够让ProtocolBindAdapter来分配绑定相关的系统资源，ProtocolBindAdapter根据要求为DeviceName设备进行资源分配和绑定操作。DriverEntry必须初始化该包裹程序并注册微端口驱动程序。如果中间层驱动程序导出了一组ProtocolXxx函数的话，也要注册协议驱动程序。

如果中间层驱动程序仅向NDIS导出了一组MiniportXxx函数，只要向NDIS库注册这些函数即可，如下所述。

1.1.1 注册NDIS中间层驱动程序

NDIS中间层驱动程序必须在DriverEntry函数的环境中向NDIS注册其MiniportXxx函数和ProtocolXxx函数。驱动程序通过调用NdisIMRegisterLayeredMiniport对MiniportXxx函数进行注册，该调用导出中间层驱动程序的MiniportXxx函数。当虚拟NIC被初始化时，以及随后当驱动程序将要基于该NIC接收和发送数据包时，注意驱动程序的控制过程。

对于指定的虚拟NIC，NDIS将在NdisIMInitializeDeviceInstance的环境中调用中间层驱动程序的MiniportInitialize函数对其进行初始化操作。如果中间层驱动程序导出了多个虚拟NIC，那么为使其可用于网络请求，驱动程序必须为每一个NIC调用NdisIMInitializeDeviceInstance函数进行初始化。这样可根据网络业务量，生成相应的数量的虚拟NIC。

如果NDIS中间层驱动程序也导出了一组ProtocolXxx函数，则必须调用相应的NdisRegisterProtocol函数向NDIS库注册这些函数。

1.1.1.1注册中间层驱动程序的Miniport

中间层驱动程序通过调用NdisIMRegisterLayeredMiniport导出MiniportXxx函数。

NdisIMRegisterLayeredMiniport以如下方式进行声明：

NDIS_STATUS

NdisIMRegisterLayeredMiniport(

IN NDIS_HANDLE NdisWrapperHandle,

IN PNDIS_MINIPORT_CHARACTERISTICS MiniportCharacteristics,

IN UINT CharacteristicsLength,

OUT PNDIS_HANDLE DriverHandle

);

中间层驱动程序必须保存NdisIMRegisterLayeredMiniport返回的DriverHandle句柄，并且在驱动程序中调用NdisIMInitializeDeviceInstance函数，请求中间层驱动程序的MiniportInitialize函数对虚拟NIC进行初始化时，将该句柄输入NDIS。当中间层驱动程序成功的绑定到一个或多个低层NIC驱动程序上或者当其绑定在一个非NIC设备驱动程序上后，将调用NdisIMInitializeDeviceInstance函数，使得中间层驱动程序可以初始化Miniport组件来接受虚拟NIC上的I/O请求。

NdisWrapperHandle 句柄是由先前的NdisMInitializeWrapper函数返回的。

中间层驱动程序必须完成以下操作：

1. 用NdisZeroMemory函数零初始化一个NDIS_MINIPORT_CHARACTERISTICS类型的构件；

2. 保存所有驱动程序导出的强制性的和非强制的MiniportXxx函数的地址，并将所有非强制的MiniportXxx入口指针设为NULL；

当其他类型的NDIS驱动程序的有效主版本是0x03、0x04、0x05时，如果要导出任何新的V4.0或 V5.0的MiniportXxx函数，中间层驱动程序的主版本必须是4.0并提供4.0或5.0版的MiniportCharacteristics构件。

对于MiniportCharacteristics，如果驱动程序不用导出MiniportXxx函数，则必须将其设为NULL，但是如果要导出函数的话，则必须将其设为某个有效的MiniportXxx函数地址值。

HaltHandler

当低层NIC超时并且NDIS已经中止了网卡驱动程序时，或者操作系统正在执行一个可控的系统关闭操作时，NDIS将调用该函数。

InitializeHandler

作为中间层驱动程序调用NdisIMInitializeDeviceInstance初始化微端口的结果，调用该函数对虚拟网卡进行初始化。

QueryInformationHandler

该函数接收OID_XXX请求，这个请求来自于高层驱动程序（用NdisRequestQueryInformation请求类型作参数，调用NdisRequest）。

ResetHandler

在高层协议驱动程序调用NdisReset的指示下，NDIS能够调用中间层驱动程序的MiniportReset函数。然而，协议驱动程序并不启动复位功能，通常，NDIS启动低层网卡驱动程序复位操作并调用中间层驱动程序的ProtocolStatus和ProtocolStatusComplete函数通知中间层驱动程序低层微端口正在复位网卡。

SetInformationHandler

该函数接收OID_XXX请求，这个请求来自于高层驱动程序（用NdisRequestSetInformation请求类型作参数，调用NdisRequest）。

SendHandler

NDIS调用该函数向低层网卡（或设备）驱动程序发送单个数据包。如果中间层驱动程序不支持MiniportSendPackets函数，那么MiniportSend函数（或MiniportWanSend函数）是必须提供的。除非中间层驱动程序总是基于那些每次只发送单一数据包或将自身绑定到低层WAN NIC的驱动程序，否则MiniportSendPackets函数应该总是以SendPacketsHander方式提供，而不是以该SendHander处理程序方式提供，关于这方面的更多讨论请参阅1.6节。

SendPacketsHander

该函数接收用于指定网上传输数据包的包描述符指针数组。除非中间层驱动程序绑定到低层WAN NIC驱动程序上并提供了MiniportWanSend函数，否则驱动程序应提供对MiniportSendPackets而不是MiniportSend函数支持。换句话说，不管中间层驱动程序是基于每次只能传送单个数据包的网卡驱动程序；还是基于每次可以传送多个数据包的网卡驱动程序，也不管中间层驱动程序是基于每次只能传送单个数据包的协议驱动程序；还是基于每次可以传送多个数据包的协议驱动程序，MiniportSendPackets函数都能实现最好的性能，关于这方面的更多讨论可参阅1.6节。

TransferDataHandler

该函数用于传输在前视缓冲区中没有指示的接收数据包的剩余部分，该前视缓冲区由中间层驱动程序传递给NdisMXxxIndicateReceive函数。这个被指示的数据包可以是中间层驱动程序的ProtocolReceive函数或者是ProtocolReceivePackets处理程序接收的转换数据包。如果中间层驱动程序通过调用（除NdisMWanIndicateReceive之外）NdisMXxxIndicateReceive函数向上层驱动程序指示接收数据包，那么该处理程序是必须提供的。如果中间层驱动程序总是通过调用NdisMIndicateReceivePacket向上层驱动程序指示接收数据包，则不必要提供MiniportTransferData函数。

ReturnPacketHandler

该函数接收返回包描述符（该包描述符先前通过NdisMIndicateReceivePacket调用向高层指示），从而释放指示给高层驱动程序的资源的控制权。在高层驱动程序处理完所有指示之后，中间层驱动程序分配的描述符及所描述的资源将返回给MiniportReturnPacket函数。当然，如果中间层驱动程序总是通过调用介质相关的NdisMXxxIndicateReceive函数向上层指示数据包，或者在调用NdisMIndicateReceivePacket之前总是将OOB数据块（与每一个描述符相关的）状态设置为NDIS_STATUS_RESOUCES，则不必提供MiniportReturnPacket函数。

CheckForHangHandler

该函数以NDIS规定的时间间隔调用，或者以中间层驱动程序规定的时间间隔运行，二者只居其一。如果提供了该处理程序，那么MiniportCheckForHang函数每两秒钟被调用一次（或者按驱动程序要求的间隔调用）。关于MiniportCheckForHang函数的更多信息可参见在线DDK的“Network Drivers Reference”或者该手册的第二部分。通常情况下，由于驱动程序无法确定低层网卡是否悬挂，NDIS中间层驱动程序并不提供对MiniportCheckForHang函数的支持。如果驱动程序基于状态不能到达NDIS的非NDIS驱动程序，中间层驱动程序可能会提供对该处理程序的支持。

由于驱动程序并不管理中断设备，不为使用中的IRQL分配缓冲区，或者因为NDIS并不调用MiniportReconfigure函数（在ReconfigureHandler情况下），因此中间层驱动程序不会提供以下的几个微端口处理程序函数。

n DisableInterruptHandler

n EnableInterruptHandler

n HandleInterruptHandler

n ISRHandler

n AllocateCompleteHandler

n ReconfigureHandler

1.1.1.2注册中间层驱动程序的协议

中间层驱动程序通过调用NdisRegisterProtocol向NDIS注册ProtocolXxx函数。

NdisRegisterProtocol以如下方式进行声明：

VOID

NdisRegisterProtocol(

OUT PNDIS_STATUS Status,

OUT PNDIS_HANDLE NdisProtocolHandler,

IN NDIS_PROTOCOL_CHARACTERISTICS ProtocolCharacteristics,

IN UINT CharacteristicsLength

);

在调用NdisRegisterProtocol函数之前，中间层驱动程序必须完成以下操作：

1. 零初始化一个NDIS_PROTOCOL_CHARACTERISTICS类型的结构。中间层驱动程序能够使用4.0或者5.0版的ProtocolCharacteristics结构。协议驱动程序必须支持即插即用功能，因此NDIS不再支持3.0版的协议驱动程序。

2. 保存所有驱动程序支持的强制性的和非强制的ProtocolXxx函数的地址；

该调用的返回句柄NdisProtocolHandler对中间层驱动程序是不透明的，中间层驱动程序必须保存该句柄，并在将来NDIS中间层驱动程序的协议部分的函数调用中作为输入参数传递，例如，打开低层适配器的函数调用。

注册下边界面向无连接的中间层驱动程序的ProtocolXxx函数

下边界面向无连接中间层驱动程序能够导出的（包括可选的和必须的）协议处理程序函数如下所列：

BindAdapterHandler

这是一个必须提供的函数。NDIS调用该函数请求中间层驱动程序绑定到低层NIC或虚拟NIC上，NIC名作为该处理程序的一个参数传递。关于动态绑定的更多的信息参见1.2节.

UnbindAdapterHandler

这是一个必须提供的函数。NDIS调用ProtocolUnbindAdapter释放对低层NIC或虚拟NIC的绑定，网卡名作为该处理程序的一个参数传递。当绑定成功关闭时，ProtocolUnbindAdapter将调用NdisCloseAdapter函数并释放相关资源。

OpenAdapterCompleteHandler

这是一个必须提供的函数。如果中间层驱动程序对NdisOpenAdapter的调用返回NDIS_STATUS_PENDING，则接着调用ProtocolOpenAdapterComplete来完成绑定。

CloseAdapterCompleteHandler

这是一个必须提供的函数。如果中间层驱动程序对NdisCloseAdapter的调用返回NDIS_STATUS_PENDING，则接着调用ProtocolCloseAdapterComplete来完成绑定释放。

ReceiveHandler

这是一个必须提供的函数。ProtocolReceive函数以指向包含网络接收数据的前视缓冲区的指针为参数被调用执行。如果该缓冲区包含的不是完整的网络数据包，ProtocolReceive以数据包描述符作为参数，调用NdisTransferData接收该数据包的剩余部分。如果低层驱动程序调用NdisMIndicateReceivePacket指示接收数据包，那么传给ProtocolReceive函数的前视缓冲区将总是完整的网络数据包。

ReceivePacketHandler

这是一个可选函数。如果中间层驱动程序所基于的NIC驱动程序指示的是数据包描述符指针数组，或者调用NdisMIndicateReceivePacket函数指示接收带外数据，那么驱动程序应提供ProtocolReceivePacket函数。如果开发者不能确定中间层驱动程序的执行环境，也应提供函数，因为在能够产生多包指示的低层NIC驱动程序上，中间层驱动程序将获得更好的性能。

ReceiveCompleteHandler

这是一个必须提供的函数。当先前指示给ProtocolReceive 函数的数据包被处理后，将调用ProtocolReceiveComplete函数。

TransferDataCompleteHandler

如果ProtocolReceive要调用NdisTransferData函数，则必须提供该处理程序。如果复制接收数据包剩余部分的NdisTransferData函数调用返回NDIS_STATUS_PENDING，那么当传输操作完成后，将调用ProtocolTransferDataComplete函数。

ResetCompleteHandler

这是一个必须提供的函数。当NdisReset函数（返回NDIS_STATUS_PENDING）调用启动的复位操作完成时，ProtocolResetComplete函数将被调用。通常情况下，中间层驱动程序并不调用NdisReset，但由于上层驱动程序可能会调用该函数，所以中间层驱动程序可能只是向低层NDIS驱动程序转发复位请求而已。

RequestCompleteHandler

这是一个必须提供的函数。当NdisRequest函数（返回NDIS_STATUS_PENDING）调用启动的查询或设置操作完成时，ProtocolRequestComplete函数将被调用。

SendCompleteHandler

这是一个必须提供的函数。对每一个调用NdisSend函数传输的数据包，当其返回NDIS_STATUS_PENDING作为发送状态时，将调用ProtocolSendComplete函数完成发送操作。如果调用NdisSendPackets发送一组数据包，那么对于每一个传送给NdisSendPackets的数据包，ProtocolSendComplete将被调用一次。中间层驱动程序仅仅根据送给ProtocolSendComplete的状态参数就能确定调用NdisSendPackets函数的发送操作的状态。

StatusHandler

这是一个必须提供的函数。NDIS用低层NIC驱动程序发起的状态通知来调用ProtocolStatus函数。

StatusCompleteHandler

这是一个必须提供的函数。NDIS调用ProtocolStatusComplete函数来指示状态改变操作已经完成，该状态先前被指示给ProtocolStatus函数。

PnPEventHandler

这是一个必须提供的函数。NDIS调用ProtocolPnPEvent来指示即插即用事件或电源管理事件。更多信息可参见1.7节。

UnloadHandler

这是一个可选函数。NDIS调用ProtocolUnload函数来响应用户卸载中间层驱动程序的请求。对于每一个绑定的适配器，NDIS在调用ProtocolUnbindAdapter之后，将调用ProtocolUnload函数卸载驱动程序。ProtocolUnload执行驱动程序决定的清除操作。

注册下边界面向连接的中间层驱动程序的ProtocolXxx函数

下边界面向连接的中间层驱动程序必须注册如下的面向无连接的和面向连接的微端口所共有的协议处理程序函数：

n BindHandler

n UnbindHandler

n OpenAdapterCompleteHandler

n CloseAdapterCompleteHandler

n ReceiveCompleteHandler

n ResetCompleteHandler

n RequestCompleteHandler

n StatusCompleteHandler

n PnPEventHandler

这些函数已经在上一小节作了汇总。

下边界面向连接的中间层驱动程序还必须注册如下的面向连接协议函数：

CoSendCompleteHandler

这是一个必须提供的函数。对于传给NdisCoSendPackets的每一个数据包都要调用一次ProtocolCoSendComplete函数。中间层驱动程序仅仅根据送给ProtocolCoSendComplete的状态参数就能确定调用NdisCoSendPackets的发送操作的状态。

CoStatusHandler

这是一个必须提供的函数。NDIS用低层NIC驱动程序发起的状态通知来调用ProtocolCoStatus函数。

CoReceivePacketsHandler

这是一个必须提供的函数。当绑定的面向连接NIC驱动程序或者集成微端口呼叫管理器（MCM）通过调用NdisMCoIndicateReceivePackets指示一个指针数组时，NDIS将调用中间层驱动程序的ProtocolCoReceivePacketHandler函数。

CoAfRegisterNotifyHandler

如果中间层驱动程序是一个使用呼叫管理器或MCM驱动程序的呼叫管理服务的面向连接客户，那么就必须注册ProtocolCoAfRegisterNotify函数，该函数用来确定中间层驱动程序能否使用呼叫管理器或MCM（已经通过注册地址族，公布其服务）的服务。

1.2 中间层驱动程序的动态绑定

中间层驱动程序必须提供ProtocolBindAdapter和ProtocolUnbindAdapter函数以支持对低层NIC的动态绑定。当NIC可用时，NDIS调用中间层驱动程序（能够绑定到NIC）的ProtocolBindAdapter函数实现动态绑定操作。

VOID

ProtocolBindAdapter(

OUT PNDIS_STATUS Status,

IN NDIS_HANDLE BindContext,

IN PNDIS_STRING DeviceName,

IN PVOID SystemSpecific1,

IN PVOID SystemSpecific2

);

BindContext句柄代表绑定请求的NDIS环境，中间层驱动程序必须保存该句柄，并且在中间层驱动程序完成绑定相关操作，并准备接受发送请求时，将该句柄作为NdisCompleteBindAdapter的参数返回NDIS。

绑定时的操作包括为该绑定分配NIC相关的环境区域并进行初始化，接着调用NdisOpenAdapter绑定到DeviceName参数指定的适配器。DeviceName可以是低层NIC驱动程序管理的NIC，也可以是介于被调用的中间层驱动程序和管理适配器的NIC驱动程序之间，由中间层NDIS驱动程序导出的控制传输请求的虚拟NIC。通常情况下，可能仅有一个基于NIC驱动程序的中间层NDIS驱动程序，实现早期的高层协议驱动程序支持的介质格式和低层NIC驱动程序支持的介质格式之间的转换。

注意，中间层驱动程序向NdisOpenAdapter传递的DeviceName必须与先前向ProtocolBindAdapter函数传递的DeviceName（一个Unicode字符串缓冲区指针）相同，驱动程序不能复制该指针并将指针副本传递给NdisOpenAdapter函数。

中间层驱动程序能够在已分配的绑定相关环境区域或者另一个驱动程序可访问位置保存BindContext。如果NdisOpenAdapter返回NDIS_STATUS_PEDDING，则必须保存BindContext值，在这种情况下，中间层驱动程序直到打开适配器的操作完成，并且其ProtocolOpenAdapterComplete函数已调用完成，才能调用NdisCompleteBindAdapter函数完成绑定工作。BindContext必须从某个已知位置获得，并由ProtocolOpenAdapterComplete传递给NdisCompleteBindAdapter函数。

如果中间层驱动程序将适配器相关信息存入注册表，那么SystemSpecific1将指向注册表路径，该值将被传给NdisOpenProtocolConfiguration函数以获取用于读写适配器信息的句柄。

SystemSpecific2预留系统使用。

如果中间层驱动程序要将内入数据包从一种介质格式转化为另一种格式，那么ProtocolBindAdapter能够分配数据包描述符池和每一个绑定所需的缓冲描述符。关于分配和管理数据包的要求可参阅1.3节。另外，如果中间层驱动程序仅仅用Protocol(Co)Receive函数接收内入数据，那么驱动程序应该分配数据包池和缓冲池来复制接收的数据。

1.2.1 打开中间层驱动程序下层的适配器

ProtocolBindAdapter函数通过DeviceName参数值打开低层NIC或虚拟网卡，从而建立到低层NIC驱动程序的绑定，它也能够从注册表中读取所要求的附加配置信息。NdisOpenProtocolConfiguration用于获取指向中间层驱动程序存储适配器相关信息的注册表主键句柄。中间层驱动程序通过调用NdisOpenConfigurationKeyByIndex函数或者NdisOpenConfigurationKeyByName函数打开并获取主键（由NdisOpenProtocolConfiguration函数打开）下的子键句柄。然后，中间层驱动程序能够调用NdisRead(Write)Configuration函数读写注册表主键或子键下的相关信息。NdisRead(Write)Configuration函数在在线DDK的“Network Drivers Reference”中有详细的描述。

典型地，ProtocolBindAdapter使用环境区域（代表对DeviceName绑定）存储所有绑定相关信息（与绑定适配器相关联的）。

绑定操作最终由NdisOpenAdapter函数调用来实现，该函数声明如下：

VOID

NdisOpenAdapter（

OUT PNDIS_STATUS Status,

OUT PNDIS_STATUS OpenErrorStatus,

OUT PNDIS_HANDLE NdisBindingHandle,

OUT PUNIT SelectedMediumIndex,

IN PNDIS_MEDIUM MediumArray,

IN UINT MediumArraySize,

IN NDIS_HANDLE NdisProtocolHandle,

IN NDIS_HANDLE ProtocolBindingContext,

IN PNDIS_STRING AdapterName,

IN UINT OpenOptions,

IN PSTRING AddressingInformation

);

中间层驱动程序在ProtocolBindingContext中传递代表绑定相关环境区域（已经分配并初始化）的句柄。NDIS在未来与绑定相关的调用中，将向中间层驱动程序返回该环境。例如，在对Protocol(Co)Receive或Protocol(Co)Status函数的调用中。相似地，当NdisOpenAdapter 调用返回时，NDIS将向中间层驱动程序传递该NdisProtocolHandle句柄。驱动程序必须保存该句柄，通常保存在绑定相关的环境区域，在以后与该绑定相关的调用中，中间层驱动程序将向NDIS传送该句柄，例如NdisSend或Ndis(Co)SendPackets函数调用。

ProtocolBindAdapter也能够通过MediumArray传递所支持的介质类型。如果NdisOpenAdapter函数调用成功，低层NIC驱动程序将选择一种其所支持的介质类型，并通过SelectedMediumHandle返回其从MediumArray中所选介质的索引。

ProtocolBindAdapter可以通过NdisProtocolHandle传递前面对NdisRegisterProtocol函数成功调用所返回的值。

如果NdisOpenAdapter返回了一个错误，那么中间层驱动程序应该收回为绑定相关环境区域分配的内存空间，并释放为绑定分配的其他所有资源。典型地，ProtocolBindAdapter通过调用NdisWriteErrorLogEntry，用适当的描述信息记录任何失败的绑定操作。

1.2.2 微端口初始化

当成功打开低层NIC并准备在虚拟NIC或NIC上接收请求和发送数据之后，ProtocolBindAdapter将对NdisIMIntializeDeviceInstance进行一次或多次调用来请求对一个或多个网卡进行初始化操作。NdisIMIntializeDeviceInstance调用中间层驱动程序的MiniportInitialize函数执行指定网卡的初始化。当MiniportInitialize函数返回后，上层NDIS驱动程序就能够进行对中间层驱动程序的虚拟NIC(s)的绑定操作了。

MiniportInitialize函数必须分配和初始化虚拟NIC相关的环境区域。作为初始化操作的一部分，MiniportInitialize必须用相应的环境句柄调用NdisMSetAttributeEx函数，NDIS将在以后对MiniportXxx函数调用中传递该环境句柄。MiniportInitialize也必须设置AttributeFlags参数（将被传递给NdisMSetAttributeEx函数）中的NDIS_ATTRIBUTE_INTERMEDIATE_DRIVER标记。中间层驱动程序通过设置NDIS_ATTRIBUTE_INTERMEDIATE_DRIVER标记来标识NDIS驱动程序类型。

另外，如果当中间层驱动程序队列中的发送和请求操作超时时，不想让NDIS调用MiniportCheckForHang(或MiniportReset)函数，那么MiniportInitialize必须对AttributeFlags参数（将被传递给NdisMSetAttributeEx函数）中的NDIS_ATTRIBUTE_IGNORE_PACKET_TIMEOUT和NDIS_ATTRIBUTE_IGNORE_REQUEST_TIMEOUT标记进行设置。通过设置超时标记来通知NDIS将由中间层驱动程序负责处理虚拟NIC超时操作。因为中间层驱动程序并不操纵低层NIC，因此它无法控制到底花了多长时间完成未决发送和请求操作，驱动程序通常既不提供MiniportCheckForHang函数也不处理虚拟NIC超时。

然而，如果中间层驱动程序已经注册了CheckForHangHandler句柄的入口点，并且没有请求NDIS忽略数据包和请求超时，也没有改变超事间隔，那么，在默认情况下，将每隔两秒对MiniportCheckForHang函数进行一次调用。如果MiniportCheckForHang函数返回TRUE，驱动程序将调用MiniportReset函数。如果驱动程序支持MiniportCheckForHang函数，那么可以通过调用NdisMSetAttributeEx函数来明确指定一个不同的TimeInSeconds值，改变默认的两秒调用间隔设置。

中间层驱动程序必须像一个非串行驱动程序那样进行操作，并且通过设置将要传递给NdisMSetAttributeEx函数的AttributeFlags参数中的NDIS_ATTRIBUTE_DESERIALIZE标记来进行注册。非串行驱动程序对MiniportXxx函数的操作进行串行化，并且对所有引入的发送数据包在内部进行排队，而不是依靠NDIS来保存发送队列。关于非串行驱动程序的更多信息可参阅第二部分第四章4.5节。

中间层驱动程序也必须设置AttributeFlags参数（将被传给NdisMSetAttributeEx函数）中的NDIS_ATTRIBUTE_NO_HALT_SUSPEND标记，防止NDIS在低层微端口过渡到低功耗状态之前中断驱动程序。

中间层驱动程序要确保所保持的状态信息是完全初始化过的。如果中间层驱动程序请求发送相关的资源（例如MiniportSend或MiniportSendPackets将要向相邻低层发送的数据包的包描述符），那么如果在调用NdisIMInitializeDeviceInstance之前，ProtocolBindAdapter还没有分配数据包池，则分配数据包池。

1.2.3 中间层驱动程序查询和设置操作

当成功绑定到低层NIC并完成虚拟NIC(s)的初始化操作之后，中间层驱动程序就可以查询低层NIC驱动程序的操作特性，设置其内部状态，也可以协商一些参数（如为低层NIC驱动程序预留的缓冲区大小等）。下边界面向无连接的中间层驱动程序通过调用NdisRequest实现该功能，下边界面向连接的中间层驱动程序则通过调用Ndis(Co)Request函数实现该功能。

中间层驱动程序也能够接收来自协议驱动程序的MiniportQueryInformation和MiniportSetInformation函数的查询和设置请求，它要么响应这些请求要么将这些请求传给低层驱动程序。

在在线DDK的“Network Drivers Reference”中包含了中间层驱动程序开发者所关心的全部通用的、面向连接的、非介质相关的OID，以及介质相关的OID的详细信息。接下来将讨论几个标准且常用的通用分类OID、面向连接OID以及一些介质相关OID。

发布设置和查询请求

典型地，下边界面向无连接的中间层驱动程序通过发布OID_GEN_MAXIMUM_FRAME_SIZE请求，查询低层NIC驱动程序所支持的帧最大长度，该请求返回值不包括帧头部分的长度。

下边界面向无连接的中间层驱动程序能够用OID_GEN_MAXIMUM_TOTAL_SIZE请求，查询绑定从而确定低层NIC驱动程序所管理的NIC所能接纳的最大数据包，中间层驱动程序必须对发送数据包进行设置，使其满足这一尺寸要求。如果上层驱动程序发送一个超出NIC驱动程序（中间层驱动程序所绑定的）所能支持尺寸的数据包，那么将会出现错误。

下边界面向无连接的中间层驱动程序能够用OID_GEN_CURRENT_LOOKAHEAD请求，查询前视数据缓冲区的大小。如果中间层驱动程序提交这一查询请求，NDIS将返回对低层NIC驱动程序的给定绑定的最新前视缓冲区尺寸。如果中间层驱动程序进行相应的设置请求，那么它将指示所提出的前视缓冲区尺寸，但中间层驱动程序并不能保证低层NIC驱动程序能够按照所指示尺寸设置前视缓冲区。

下边界面向无连接的中间层驱动程序用OID_GEN_LINK_SPEED请求，查询低层NIC驱动程序的链接速率，并用该请求的返回值修改其保存的内部超时设置。下边界面向连接的中间层驱动程序用OID_GEN_CO_LINK_SPEED请求，查询低层NIC驱动程序的链接速率，并且也能够用OID_GEN_CO_LINK_SPEED请求，设置低层NIC驱动程序的链接速率。

如果中间层驱动程序绑定到WAN NIC驱动程序上的，那么直到接收到一个连结指示（指示本地节点和远程节点连结建立）时才能确定链接速率。关于链接指示的描述请参阅第二部分第八章的“广域网微端口驱动程序做出的指示”。

中间层驱动程序也必须确定低层NIC驱动程序的操作特性的设置，下边界面向无连接的中间层驱动程序用OID_GEN_MAC_OPTIONS请求来实现这一功能，下边界面向连接的中间层驱动程序用OID_GEN_CO_MAC_OPTIONS请求来实现这一功能。

下边界面向无连接的中间层驱动程序通常发布的是OID_GEN_MAXIMUM_SEND_PACKETS查询（特别是在中间层驱动程序导出了MiniportSendPackets函数情况下），驱动程序能够在以后响应高层驱动程序的OID_GEN_MAXIMUM_SEND_PACKETS查询时，向上层传递该查询的返回值。

中间层驱动程序也能够通过介质相关OID查询相关介质的当前地址，例如，下边界面向无连接的中间层驱动程序可以发布OID_WAN_CURRENT_ADDRESS、OID_802_3_CURRENT_ADDRESS、OID_802_5_CURRENT_ADDRESS或者OID_FDDI_LONG_CURRENT_ADDRESS查询，下边界面向连接的中间层驱动程序可以OID_ATM_WAN_CURRENT_ADDRESS查询。

如果必要，中间层驱动程序能够发布一个设置请求，来通知NDIS其操作特性的有关信息。下边界面向无连接的中间层驱动程序用OID_GEN_PROTOCOL_OPTIONS调用NdisRequest函数来实现这一功能，而下边界面向连接的中间层驱动程序用OID_GEN_CO_PROTOCOL_OPTIONS调用NdisRequest来实现这一功能。

绑定到支持WAN的NIC的中间层驱动程序同时必须完成以下设置信息请求：

n 用OID_WAN_PROTOCOL_TYPE请求，通知低层NIC驱动程序其协议的类型，该类型以单字节的网络层协议标识符形式提供；

n 用OID_WAN_HEADER_FORMAT请求，通知低层NIC驱动程序其发送数据包的头格式。

响应设置和查询请求

因为NDIS中间层驱动程序可被高层NDIS驱动程序绑定，所以它也可以接收MiniportQueryInformation和MiniportSetInformation函数的查询和设置请求。在某些情况下，中间层驱动程序所起的作用仅仅是将这些请求传递给低层驱动程序。另外，当这些请求是关于其在上边界导出的介质时，也能够对这些查询和设置请求进行响应。注意中间层驱动程序必须将其从上层NDIS驱动程序接收到的OID_PNP_XXX请求，传递给低层Miniport驱动程序处理。

通常情况下，中间层驱动程序所接收到的通用OID，与其向低层NIC驱动程序提交的OID是相似甚至相同的，中间层驱动程序所接收到的介质相关OID将是高层驱动程序所期望的介质类型。

1.2.4作为面向连接客户程序注册中间层驱动程序

下边界面向连接的中间层驱动程序必须作为面向连接客户程序进行注册。面向连接客户程序使用呼叫管理器或集成微端口呼叫管理器（MCM）的安装调用（call-setup）和卸载（tear-down）服务完成相关功能，也可以使用面向连接的微端口或MCM的接收和发送功能进行发送和接收数据操作。关于面向连接通信的更多信息请参阅第一部分第四章。

当呼叫管理器或MCM从ProtocolBindAdapter函数中调用Ndis(M)CmRegisterAddressFamily注册地址族时，NDIS将调用绑定上的所有协议驱动程序的ProtocolCoRegisterAfNotify函数。如果中间层驱动程序在注册协议时调用了ProtocolCoRegisterAfNotify函数，那么NDIS将对中间层驱动程序的ProtocolCoRegisterAfNotify函数进行调用。

如果ProtocolCoRegisterAfNotify函数确定中间层驱动程序能够使用呼叫管理器或者MCM（注册地址族的）的服务，那么它将为客户的每一个AF分配相应的环境区域并调用NdisClOpenAddressFamily函数注册一组客户提供的函数。

NdisClOpenAddressFamily定义如下：

NDIS_STATUS

NdisClOpenAddressFamily(

IN NDIS_HANDLE NdisBindingHandle,

IN PCO_ADDRESS_FAMILY AddressFamily,

IN NDIS_HANDLE ProtocolAfContext,

IN PNDIS_CLIENT_CHARACTERISTICS ClCharacteristics,

IN UINT SizeOfClCharactertistics,

OUT PNDIS_HANDLE NdisAfHandle

);

在调用NdisClOpenAddressFamily之前，中间层驱动程序必须完成以下操作：

1. 将PNDIS_CLIENT_CHARACTERISTICS类型的ClCharacteristics结构体置零，该结构的最新版本为5.0；

2. 存储驱动程序支持的ProtocolXxx客户函数的地址。

该调用的返回值NdisAfHandle对中间层驱动程序是不透明的，中间层驱动程序必须保存该句柄并在以后中间层驱动程序的协议部分调用中作为参数传递给NDIS。例如，注册SAP的NdisClRegisterSap函数调用。

中间层驱动程序必须用NdisClOpenAddressFamily注册的客户函数有：

ClCreateVcHandler

设定呼叫器的ProtocolCoCreateVc函数的入口点。

ClDeleteVcHandler

设定呼叫器的ProtocolCoDeleteVc函数的入口点。

ClRequestHandler

设定呼叫器的ProtocolCoRequest函数的入口点。

ClRequestCompleteHandler

设定呼叫器的ProtocolCoRequestComplete函数的入口点。

ClOpenAfCompleteHandler

设定呼叫器的ProtocolClOpenAfComplete函数的入口点。

ClCloseAfCompleteHandler

设定呼叫器的ProtocolClCloseAfComplete函数的入口点。

ClRegisterSapCompleteHandler

设定呼叫器的ProtocolClRegisterSapComplete函数的入口点，客户程序用该函数接收远程机器的呼叫。

ClDeRegisterSapCompleteHandler

设定呼叫器的ProtocolClDeRegisterSapComplete函数的入口点。

ClMakeCallCompleteHandler

设定呼叫器的ProtocolClMakeCallComplete函数的入口点，客户程序用该函数对远程机器作外出呼叫。

ClModifyCallQoSCompleteHandler

设定呼叫器的ProtocolClModifyCallQoSComplete函数的入口点，客户程序用该函数对已经建立的VC服务质量进行动态修改，或者当准备一个内入呼叫时，用该函数和呼叫管理器协商建立QoS。

ClCloseCallCompleteHandler

设定呼叫器的ProtocolClCloseCallComplete函数的入口点。

ClAddPartyCompleteHandler

设定呼叫器的ProtocolClAddPartyComplete函数的入口点，客户程序用该函数为对远程机器的外出呼叫建立点对多点的VCs。

ClDropPartyCompleteHandler

设定呼叫器的ProtocolClDropPartyComplete函数的入口点。

ClIncomingCallHandler

设定呼叫器的ProtocolClIncomingCall函数的入口点，客户程序用该函数接收远程机器的呼叫。

ClIncomingCallQoSChangeHandler

设定呼叫器的ProtocolClIncomingCallQoSChange函数的入口点，客户程序用该函数接收来自远程机器的调用，在该远程机器上发送客户程序可以动态地改变QoS。

ClIncomingCloseCallHandler

设定呼叫器的ProtocolClIncomingCloseCall函数的入口点。

ClIncomingDropPartyHandler

设定呼叫器的ProtocolClIncomingDropParty函数的入口点。

ClCallConnectedHandler

设定呼叫器的ProtocolClCallConnected函数的入口点，客户程序用该函数接收远程机器的呼叫。

即使中间层驱动程序不支持内入呼叫、外出呼叫或者“点—多点”连接，当调用NdisClOpenAddressFamily时，也必须设置ProtocolCl/CoXxx函数调用的NDIS_CLEINT_CHARACTERISTICS结构的每一个ClXxx参数成员，对于那些中间层驱动程序不支持的面向连接函数子集，ProtocolCl/CoXxx函数将只是简单地返回NDIS_STATUS_NOT_SUPPORTED值。

1.3中间层驱动程序数据包管理

中间层驱动程序从高层驱动程序接收数据包描述符，并在网络上发送，该包描述府与一个或多个链式数据缓冲区相关联。中间层驱动程序能够对数据进行重新打包，并使用新的数据包描述符进行数据传输，也可以直接将数据包传递给低层驱动程序，如果驱动程序下边界面向无连接，可调用NdisSend或NdisSendPackets函数完成该功能，如果驱动程序下边界是面向连接的，可调用NdisCoSendPackets函数完成此项功能。中间层驱动程序也可以进行一些操作改变链式缓冲区的内容，或者调整内入数据包相对于其他发送任务的发送次序或发送定时。但是，即使中间层驱动程序只是向下层传递上层引入的数据报，例如，仅仅只是对数据包进行计数，也必须分配新的数据包描述符，并且要管理部分或者全部新的包结构。

每一个中间层驱动程序都必须分配自己的包描述符来代替高层的数据包描述符。如果中间层驱动程序要把数据包从一种格式转化为另一种格式，也必须分配缓冲区描述符来映射用于复制转配数据的缓冲区，该缓冲区由中间层驱动程序进行分配。如果有与复制的包描述符相关的OOB数据，那么可以将这些数据复制到与包描述符（中间层驱动程序分配的）相关的新OOB数据块，其过程是，首先，用NDIS_OOB_DATA_FROM_PACKET宏获取OOB数据区的指针，然后，调用NdisMoveMemory将其内容移入与新包描述符相关的OOB数据区。该驱动程序也能够用NDIS_GET_PACKET_XXX或NDIS_SET_PACKET_XXX宏从与老的包描述符相关的OOB数据区中，读取相关的内容，并写入与新包描述符相关的OOB数据区。

包描述符通过调用以下NDIS函数进行分配：

1. 调用NdisAllocatePacketPool或者NdisAllocatePacketPoolEx，为固定尺寸包描述符（由呼叫器指定数量）分配并初始化一组非可分页池；

2. 调用NdisAllocatePacket函数，从NdisAllocatePacketPool(Ex)已经分配的池中分配包描述符；

根据中间层驱动程序目的的不同，驱动程序能够对引入包描述符连接的缓冲区进行重新打包。例如，中间层驱动程序可以在接下来的情况下分配包缓冲池、对引入包数据重新打包：

n 如果中间层驱动程序从高层协议驱动程序接收到的数据缓冲区，比低层介质能够发送的单个缓冲区更大，那么中间层驱动程序必须将引入的数据缓冲分割成更小的、满足低层发送要求的数据缓冲。

n 中间层驱动程序在将发送任务转交低层驱动程序之前，可以通过压缩或加密数据方式来改变内入数据包的长度。

调用以下NDIS函数分配上面所要求的缓冲区：

1. 用NdisAllocateBufferPool获取用于分配缓冲区描述符的句柄；

2. 用NdisAllocateMemory或NdisAllocateMemoryWithTag分配缓冲区；

3. 用NdisAllocateBuffer分配和设置缓冲区描述符，映射由NdisAllocateMemory(WithTag)分配的缓冲区，并链接到NdisAllocatePacket分配的包描述符上。

驱动程序可以通过调用NdisChainBufferAtBack或NdisChainBufferAtFront函数，将缓冲区描述符和包描述符进行链接。调用NdisAllocateMemory(WithTag)返回的虚拟地址和缓冲区长度，将被传递给NdisAllocateBuffer函数来初始化其所映射的缓冲区描述符。

符合典型要求的包描述符能够在驱动程序初始化时根据要求进行分配，也可以通过ProtocolBindAdapter函数调用来实现。如果必要或者出于性能方面的考虑，中间层驱动程序开发者可以在初始化阶段，分配一定数量的包描述符和由缓冲区描述符映射的缓冲区，这样，就为ProtocolReceive复制内入数据（将向高层驱动程序指示）预先分配了资源，也为MiniportSend或MiniportSendPackets向相邻低层驱动程序传递引入的发送数据包，准备了可用的描述符和缓冲区。

如果在中间层驱动程序复制接收/发送数据到一个或多个缓冲区时，最末的一个缓冲的实际数据长度比缓冲区的长度小，那么，中间层驱动程序将调用NdisAdjustBufferLength把该缓冲区描述符调节到数据的实际长度。当该包返回到中间层驱动程序时，应再次调用该函数将其长度调节到完整缓冲区的实际尺寸。

下边界面向无连接的中间层驱动程序能够通过ProtocolReceivePacket函数，从低层NIC驱动程序以完整数据包形式接收内入数据，该数据包由NDIS_PACKET类型的包描述符指定，也能够通过将内入数据指示给ProtocolReceive函数，并将数据复制到中间层驱动程序提供的数据包中。下边界面向连接的中间层驱动程序总是用ProtocolCoReceivePacket函数，从低层NIC驱动程序接收数据作为一个完整的数据包。

在如下情况下，中间层驱动程序能够保持对接收数据包的所有权：

n 当下边界面向无连接的中间层驱动程序向ProtocolReceivePacket函数指示完整数据包时；

n 当下边界面向连接的中间层驱动程序向ProtocolCoReceivePacket函数指示数据包时，其中NDIS_PACKET_OOB_DATA的Status成员设置为除NDIS_STATUS_RESOURCES以外的任何值。

在这些情况下，中间层驱动程序能够保持对该包描述符和其所描述的资源的所有权，直到所接收数据处理完毕，并调用NdisReturnPackets函数将这些资源返还给低层驱动程序为止。如果ProtocolReceivePacket向高层驱动程序传递其所接收的资源，那么至少应该用中间层驱动程序已经分配的包描述符替代引入包描述符。

根据中间层驱动程序目的的不同，当其从低层驱动程序接收完整数据包时，将有几种不同的包管理策略。例如，以下是几种可能的包管理策略：

n 复制缓冲区内容到中间层驱动程序分配的缓冲区中，该缓冲区被映射并链接到一个新的包描述符，向低层驱动程序返回该输入包描述符，然后可以向高层驱动程序指示新的数据包；

n 创建新的包描述符，将缓冲区（与被指示包描述符相关联）链接到新的包描述符，然后将新的包描述符指示给高层驱动程序。当高层驱动程序返回包描述符时，中间层驱动程序必须拆除缓冲区与包描述符间的链接，并将这些缓冲区链接到最初从低层驱动程序接收到的包描述符，最后向低层驱动程序返还最初的包描述符及其所描述的资源。

即使下边界面向无连接的中间层驱动程序支持ProtocolReceivePacket函数，它也提供ProtocolReceive函数。当低层驱动程序不释放包描述符所指示资源的所有权时，NDIS将调用ProtocolReceive函数，当这类情况出现时，中间层驱动程序必须复制所接收的数据到它自己的缓冲区中。如果中间层驱动程序同时也指示了带外数据，ProtocolReceive函数能够用NDIS_GET_ORIGINAL_PACKET调用NdisGetReceivePacket，获取接收指示关联的带外数据。

对于下边界面向连接的中间层驱动程序，当低层驱动程序不释放包描述符所指示资源的所有权时，则将数据包的NDIS_PACKET_OOB_DATA的Status成员设为NDIS_STATUS_RESOURCES，然后驱动程序的ProtocolCoReceivePacket函数必须将接收到数据复制到自己的缓冲区中。

重用数据包

前面已经讲过，NdisSend为数据包描述符（中间层驱动程序提交的）返回NDIS_STATUS_SUCCESS状态标志后，不是将包描述符返回给ProtocolSendComplete函数，就是将中间层驱动程序分配的数据包返回给MiniportReturnPacket函数。包描述符的所有权及其所描述的资源都将返回给中间层驱动程序。如果高层驱动程序提供缓冲区（链到返回包描述符），那么中间层驱动程序应当能够准确地向分配资源的驱动程序返回这些资源。

如果最初是中间层驱动程序分配了包描述符和链接的缓冲区，那么它就能够回收这些资源，并可以在接下来的发送和数据接收过程中使用这些资源。对中间层驱动程序来说，比起先释放这些资源，然后在需要时再进行重新分配，重新初始化并重用其所分配的包描述符、重用任何中间层驱动程序分配的链接缓冲区描述符和缓冲区，将是一种更为有效的方法。

中间层驱动程序通过调用NdisReinitializePacket函数对包描述符进行重新初始化，然而，中间层驱动程序必须首先确定已经调用NdisUnchainBufferAtXxx移去了所有链接缓冲区及其缓冲描述符。因为NdisReinitializePacket将清除指向缓冲区链的成员，如果没有预先释放或存储链接缓冲区，该调用将导致内存泄漏。同样地，如果与包描述符相关的MediaSpecificInformation中包含OOB数据，在重新初始化该包描述符之前，也必须回收内存。

1.4 中间层驱动程序的限制

前面各章节已经描述了中间层驱动程序必须按序执行的各项操作，现总结如下：

1. 当中间层驱动程序在MiniportInitialize函数中调用NdisMSetAttributesEx时，必须设定NDIS_ATTRIBUTE_INTERMEDIATE_DRIVER标识，NDIS将仅仅通过该标识的当前值鉴别中间层驱动程序类型，并采取一定的措施确保延期操作不会导致死锁，例如向中间层驱动程序传送内部发送队列数据包。

2. 中间层驱动程序至少应该用其分配的新数据包描述符代替内入数据包描述符，不管该数据包是要向下传递给低层驱动程序进行发送，还是要向上传递给高层驱动程序进行接收。其后，还必须用原始包描述符（最初与传递给中间层驱动程序的数据包相关联）替换其自己的包描述符，例如当完成一个发送或完成一个接收指示时。另外，中间层驱动程序还必须通过返回包描述符及其所指定的资源，及时地返还其从高层或低层驱动程序借入的资源。

3. 特别地，中间层驱动程序必须遵循接下来的准则，该准则适用于所有非串行微端口驱动程序：

如果驱动程序的所有内部资源被中间层驱动程序发送函数和其他的MiniportXxx函数所共享，那么该资源必须通过自旋锁保护，这些函数包括MiniportSend或MiniportSendPackets以及其他MiniportXxx函数，唯一的例外是MiniporReset函数，它由NDIS进行串行化。

对于非串行微端口驱动程序，中间层驱动程序将其每个绑定环境区域的共享资源（仅受自旋锁保护）组织为离散的专用接收区、专用发送区和共享区域，这样相对于那种过分保护那些分布没有规律的发送、接收和共享变量的驱动程序，将能够获得更好的性能。

1.5 中间层驱动程序接收数据

这一节将讨论下边界面向无连接以及下边界面向连接的中间层驱动程序如何进行数据接收。

1.5.1下边界面向无连接的中间层驱动程序接收数据

低层面向无连接的NIC驱动程序可通过下面两种方式指示数据包:

n NIC驱动程序调用非过滤相关的NdisMIndicateReceivePacket，传递指向数据包描述符的指针数组的指针，向高层驱动程序转让所指示包资源的所有权。当高层驱动程序处理完相应数据后将向NIC驱动程序返回那些包描述符及其所指向的资源。

n NIC驱动程序调用过滤相关的NdisMXxxIndicateReceive函数，传递前视缓冲区指针及数据包的大小值。

下边界面向无连接的中间层驱动程序必须提供ProtocolReceive函数，另外，它也可包含ProtocolReceivePacket函数，这依赖于其具体的运行环境而定。

n 对于下边界面向无连接的中间层驱动程序来说， ProtocolReceive函数是必须提供的。该函数传递前视缓冲区指针，如果面向无连接中间层驱动程序检查完前视数据之后认为该数据包是高层驱动程序所需要的，那么必须将数据复制到已分配的数据包，该数据包将指示给高层驱动程序。如果前视缓冲区尺寸小于接收到的包大小，中间层驱动程序必须首先在ProtocolReceive的环境中调用NdisTransferData复制接收数据包的其余部分。

为了让ProtocolReceive尽可能快的执行，中间层驱动程序应该为此目的预分配包描述符、缓冲区及缓冲区描述符。ProtocolReceive被调用通常是因为低层驱动程序调用了NdisMXxxIndicateReceive函数，然而，如果低层NIC驱动程序用NdisMIndicateReceivePacket函数指示接收数据包时，被指示的数据包描述符的OOB数据块状态设为NDIS_STATUS_RESOUCES，那么ProtocolReceive也可以被调用。因为那些NdisMIndicateReceivePacket指示的数据包被传递给ProtocolReceive函数，所以前视缓冲的大小总是与数据包的大小相等，因此中间层驱动程序不会为那些指示，调用NdisTransferData进行大小不相等包的处理。但是，如果低层NIC驱动程序也指示OOB数据，那么中间层驱动程序在ProtocolReceive处理中必须以NIDS_GET_ORIGINAL_PACKET为参数调用NdisGetReceivePacket找到这些信息。

n 对于下边界面向无连接的中间层驱动程序来说， ProtocolReceivePacket函数是可选的。ProtocolReceivePacket接收描述完整网络数据包的包描述符指针。如果低层面向无连接的NIC是DMA总线控制设备，驱动程序也必须相应的调用非过滤相关的NdisMIndicateReceivePacket函数指示接收数据包，并且，如果驱动程序绑定到低层NIC驱动程序，那么中间层驱动程序应提供ProtocolReceivePacket函数。另外，支持OOB数据的低层NIC驱动程序在大多数情况下，会在接收数据过程中向NdisMIndicateReceivePacket函数传递包描述符，以使中间层驱动程序能够访问与该描述符相关的OOB数据。

ProtocolReceivePacket对数据包进行检查，如果它认为该包是高层驱动程序所需要的，那么它能够通过返回一个非零值的方式保持该包的所有权。如果一个非零值被返回，中间层驱动程序接下来必须以包描述符指针为参数调用NdisReturnPackets，而且，对于一个特定的包描述符，该函数被调用的次数应与接收指示时ProtocolReceivePacket函数返回的非零值的数目相等。

当中间层驱动程序按指定次数调用NdisReturnPackets之后，将向低层驱动程序转让最初指示接收数据的包描述符的所有权及相关的缓冲区，所以中间层驱动程序应尽可能快的调用NdisReturnPackets函数进行相应处理。

另一方面，如果中间层驱动程序从NdisReturnPackets中返回零值，这表示将立即释放数据包及相关资源。例如，如果中间层驱动程序复制指示数据到自己的缓冲区，并且在向高层驱动程序提交之前内部进行数据的相应处理，这种情况将会发生。

1.5.1.1 在中间层驱动程序中实现ProtocolReceivePacket处理程序

当低层NIC驱动程序通过调用NdisMIndicateReceivePacket函数指示可能包含OOB数据的包数组时，NDIS通常将以每一个包描述符为参数调用中间层驱动程序的ProtocolReceivePacket函数，并且在返回之前允许中间层驱动程序保持包描述符指定的资源，并对数据进行相关的处理。以包数组为参数调用NdisMIndicateReceivePacket函数的两类典型的NIC驱动程序如下：

n 管理能够接收多个网络数据包到缓冲环的DMA总线控制适配器的NIC驱动程序；

n 在与包描述符相关的NDIS_PACKET_OOB_DATA数据块中，向高层驱动程序提供包含介质相关信息的带外数据（如包优先级等）的NIC驱动程序。当然，这些驱动程序并不一定非要是DMA总线控制设备的驱动程序。

如果中间层驱动程序被绑定到NIC驱动程序，就像前面提及的，那么它应该提供ProtocolReceivePacket函数。这使得驱动程序可以完成接下的操作：

1. 在每一个接收指示中，接收完整数据包；

2. 调用NDIS宏，读取与包描述符相关的OOB数据，而不是调用NdisGetReceivePacket和NDIS_GET_ORIGINAL_PACKET接收和复制数据;

3. 保持对内入数据包描述符的所有权，并通过这些包描述符对缓冲数据进行直接读访问，然后，在对每一个包描述符的处理中，可能要为客户程序制作该数据的多个副本；

4. 通过NdisReturnPackets函数返回包描述符及其所描述资源，另外还有其他可能的包描述符。

即使中间层驱动程序提供了ProtocolReceivePacket处理程序，NIC驱动程序对NdisMIndicateReceivePacket函数的调用也可能导致对中间层驱动程序ProtocolReceivePacket函数的调用。当NIC驱动程序调用NdisMIndicateReceivePacket暂时释放了驱动程序分配资源的所有权之后，将依赖于这些包使用者调用NdisReturnPackets及时向低层驱动程序返还该部分资源。换句话说，NIC驱动程序能够撇开接收资源运行，像NIC中的接收缓冲空间等。NIC驱动程序通过向OOB数据块（与被传给NdisMIndicateReceivePacket的包数组中的包描述符相关联）写入NDIS_STATUS_RESOURCES状态标识来完成该项功能，该状态指示的数据包将导致NDIS以该包及数组中相继的其他包为参数调用高层驱动程序的ProtocolReceive函数，这将强制中间层驱动程序复制包数据而不是获取包的所有权。

如果中间层驱动程序想要通过调用ProtocolReceive函数来获取与包描述符关联的OOB数据，那么它必须用NDIS_GET_ORIGINAL_PACKET 调用NdisGetReceivePacket，复制特定的介质信息到中间层驱动程序所分配的缓冲区，另外，如果低层NIC驱动程序提供了时间戳，那么还必须复制TimeSent和TimeReceived信息。

1.5.1.2 在中间层驱动程序中实现ProtocolReceive处理程序

如果NIC驱动程序调用了NdisMXxxIndicate函数，那么驱动程序将总是调用ProtocolReceive函数来处理接收数据包。如果中间层驱动程序接受该数据包的话，ProtocolReceive必须以包描述符为参数调用NdisTransferData函数，复制前视缓冲区及包的其余部分到中间层驱动程序已分配的缓冲区。NdisTransferData必须在ProtocolReceive环境中调用，而且只能调用一次。中间层驱动程序应该设置拥有足够尺寸的链式缓冲区包描述符来保存所有的接收数据。当NdisTransferData返回之后，从低层NIC驱动程序接收来的数据将不再有用。

如果传给ProtocolReceive的数据是通过调用NdisMXxxIndicateReceive进行指示的，那么传给ProtocolReceive函数的前视缓冲区尺寸将不会超过用OID_GEN_CURRENT_LOOKAHEAD调用NdisRequest返回的值。对于中间层驱动程序来说，所有的前视缓冲区中的数据都是只读的。如果对ProtocolReceive函数的调用是由于在调用NdisMIndicateReceivePacket之前，低层NIC驱动程序把包数组中的一个或多个包状态设置为NDIS_STATUS_RESOURCES，那么前视缓冲区的尺寸将总是等于整个网络数据包的大小，所以中间层驱动程序将不必再调用NdisTransferData函数。

ProtocolReceive函数必须尽可能快的返回资源的控制权，因此在中间层驱动程序收到接收指示之前，应确保拥有可利用的包描述符、缓冲区及缓冲区描述符。如果中间层驱动程序检查前视数据后认为该包不是其要复制的那个，驱动程序应返回NDIS_STATUS_NOT_ACCEPT标识。

在接收包被复制时，ProtocolReceive函数不能处理接收数据，因为这将严重影响系统性能以及低层NIC从网络中接收内入数据包的能力。作为替代，中间层驱动程序在以后的ProtocolReceiveComplete函数中对接收数据包进行处理，该函数在随后能够进行数据包后期处理时被调用。典型地，当低层NIC驱动程序已经指示了NIC驱动程序确定的所有数据包时或者在退出DPC层接收句柄之前，以上操作将发生。中间层驱动程序必须对ProtocolReceive复制的数据包进行排队，以使ProtocolReceiveComplete函数能够过它们进行后期处理。

1.5.1.3 下边界面向无连接中间层驱动程序接收OOB数据信息

如果接收到的网络数据包被指示给ProtocolReceive函数，那么驱动程序必须将接收数据复制到中间层驱动程序所提供的缓冲区。如果包描述符相关的数据包OOB数据中包含特定介质信息和（或）时间戳信息，中间层驱动程序将调用NdisGetReceivePacket 和NDIS_GET_ORIGINAL_PACKET获取介质信息以及TimeSent和TimeReceived时间戳（如果低层NIC驱动程序提供了那些信息）。

如果接收数据包被传给ProtocolReceivePacket函数，那么中间层驱动程序必须以下面的方式用NDIS宏保存与包关联的OOB数据信息：

n 用NDIS_GET_MEDIA_SPECIFIC_INFO 读取介质相关信息,用NDIS_SET_MEDIA_SPECIFIC_INFO写入介质相关信息;

n 用NDIS_GET_TIME_SENT读TimeSent信息,用NDIS_SET_TIME_TO_SEND写TimeSent信息;

n 用NDIS_GET_TIME_RECEIVED读TimeReceived信息。

TimeSent时间戳是远程节点NIC发送数据包的时间，如果可能的话，它将被低层NIC驱动程序获取并保存。TimeReceived时间戳是内入数据包被低层NIC驱动程序接收的时间。

1.5.2下边界面向连接的中间层驱动程序接收数据

面向连接的NIC驱动程序通过调用NdisMIndicateReceivePacket指示数据包，传递参数为指向数据包描述符的指针数组的指针。如果中间层驱动程序基于NIC驱动程序之上，NDIS接下来将调用中间层驱动程序的ProtocolCoReceivePacket函数。

ProtocolReceivePacket接收描述完整网络数据包的包描述符指针。ProtocolReceivePacket检查该数据包，如果认为该包是高层驱动程序需要的，将通过返回非零值的方式保持对该包的所有权。如果对该包返回了非零值，中间层驱动程序接着必须以相应包描述符指针为参数调用NdisReturnPackets函数，而且，对于一个特定的包描述符，该函数被调用的次数应与接收指示时ProtocolCoReceivePacket函数返回的非零值的个数相等。

当中间层驱动程序以指定次数调用NdisReturnPackets之后，将向低层驱动程序转让最初指示接收数据的包描述符的所有权及相关的缓冲区，所以中间层驱动程序应尽可能快地调用NdisReturnPackets函数进行相应处理。

另一方面，如果中间层驱动程序从NdisReturnPackets中返回零值，这表示将立即释放数据包及相关资源。例如，如果中间层驱动程序复制指示数据到自己的缓冲区，并且在向高层驱动程序提交之前内部进行数据的相应处理，该种情况将会发生。

1.5.2.1在中间层驱动程序中实现ProtocolCoReceivePacket处理程序

当低层NIC驱动程序通过调用NdisMCoIndicateReceivePacket函数指示可能包含OOB数据的包数组时，NDIS以每一个包描述符为参数调用中间层驱动程序的ProtocolCoReceivePacket函数。为了获得相关包的OOB 数据块状态，ProtocolCoReceivePacket必须对每一个包描述符调用一次NDIS_GET_PACKET_STATUS宏。

如果NIC驱动程序在调用NdisMCoIndicateReceivePacket之前，将数据包描述符相关的OOB 数据块状态设为NDIS_ STATUS_SUCCESS，NIC驱动程序将暂时释放与该包描述符相关的资源的所有权。在这种情况下，NIC驱动程序将依赖于这些包的使用者及时返还该部分资源。换句话说，NIC驱动程序能够撇开接收资源运行，像NIC中的接收缓冲空间等。当撇开这些资源运行时，NIC驱动程序向与包描述符相关的OOB数据块写入NDIS_STATUS_RESOURCES标识，该状态指示的数据包将强制中间层驱动程序的ProtocolCoReceivePacket函数立即复制包数据，而不是保持NIC驱动程序分配的包资源。在这种情况下，ProtocolCoReceivePacket必须返回零。

如果低层NIC驱动程序没有将OOB数据块（与被传给NdisMCoIndicateReceivePacket的包数组中的包描述符相关的）设置为NDIS_STATUS_RESOURCES，那么将允许中间层驱动程序保持该包描述符及其指定的资源，直到中间层驱动程序、高层协议及高层协议的客户程序处理完数据并返回包描述符为止。

1.5.2.2在下边界面向连接的中间层驱动程序中接收OOB数据信息

中间层驱动程序必须以下面的方式用NDIS宏保存与包关联的OOB数据信息：

n 用NDIS_GET_MEDIA_SPECIFIC_INFO 读取介质相关信息,用NDIS_SET_MEDIA_SPECIFIC_INFO写入介质相关信息;

n 用NDIS_GET_TIME_SENT读TimeSent信息,用NDIS_SET_TIME_TO_SEND写TimeSent信息;

n 用NDIS_GET_TIME_RECEIVED读TimeReceived信息。

TimeSent时间戳是远程节点NIC发送数据包的时间，如果可能的话，它将被低层NIC驱动程序获取并保存。TimeReceived时间戳是导入数据包被低层NIC驱动程序接收的时间。

1.5.3向高层驱动程序指示接收数据包

在面向连接的中间层驱动程序处理完所接收的数据（可能已将其转化为高层驱动程序所要求的格式，并已将相关数据复制到链向中间层驱动程序分配的包描述符的缓冲区）之后，可以通过调用NdisMIndicateReceivePacket，将数据包指示给相邻的高层驱动程序，即使中间层驱动程序的微端口是面向无连接的。

1.6通过中间层驱动程序传输数据包

在1.1.1节已经提到，中间层驱动程序必须提供MiniportSendPackets函数，并通过NdisIMRegisterLayeredMiniport函数进行注册。如果中间层驱动程序是位于两个支持多包发送的NDIS驱动程序之间的，那么MiniportSendPackets函数能够导入包数组的转发。如果驱动程序下边界是面向无连接的，通过调用NdisSendPackets实现转发；如果驱动程序下边界是面向连接的，那么通过调用NdisCoSendPackets实现转发。如果驱动程序是位于一次只能用NdisSend发一个数据包的传输器下的，MiniportSendPackets函数能够用NdisSend或Ndis(Co)SendPackets进行单一数据包传输（对系统性能没有任何负面影响）。

上述的中间层驱动程序不会成为性能的瓶颈。如果中间层驱动程序位于可向MiniportSendPackets发送包数组的传输器和一次只能处理一个包的微端口之间，不考虑低层微端口的性能，MiniportSendPackets可用NdisSendPackets发送接收到的包数组。NDIS先对数组中的数据包进行排队，然后当微端口可以接受发送请求时，将数据包独立的逐个发送给低层面向无连接的微端口的MiniportSend函数，当然这些操作对中间层驱动程序是透明的。

因为对MiniportXxx的调用是由NDIS管理的，同步是有保证的，因此当高层驱动程序对低层驱动程序的转发请求产生时，不必像1.4节描述的那样进行NdisIMXxx的调用。

作为最起码的要求，必须用中间层驱动程序自己的包描述符代替每一个内入包描述符。驱动程序必须保留高层驱动程序的原始的描述符（和链接的缓冲区，如果它们被复制到了新的缓冲区的话），当发送完成、返回高层驱动程序之前，驱动程序应该返回原始的包描述符和用于发送包的数据缓冲区。关于如何分配包资源以及将信息从一个包复制到另一个包的更多的信息，请参阅1.3节。

MiniportSendPackets接收按序组织的包描述符数组，该顺序由NdisSendPackets的调用者确定。在多数情况下，当将这些引入包数组传给低层NIC驱动程序时，中间层驱动程序应该维持该包描述符顺序，仅仅在将它们传给低层驱动程序之前，中间层驱动程序向内入数据包增加带外信息时才可能重排该引入包数组的顺序。

当向NdisSendPackets 传递包数组时，NDIS将一直保持包描述符指针的顺序。低层NIC驱动程序假定：传给MiniportSendPackets函数的包指针数组隐含包将以同样的顺序发送。

下边界面向无连接的中间层驱动程序能够以包描述符指针为参数调用NdisSend传输单个数据包，也可以通过调用NdisSendPackets并传递指向包描述符（驱动程序已经分配的）指针数组的指针来传输单个或多个数据包。下边界面向连接的中间层驱动程序能够通过调用NdisCoSendPackets并传递指向包描述符指针数组的指针，传输单个或多个数据包。

通常情况下，下边界面向无连接的中间层驱动程序开发者，应该根据驱动程序的自身要求和低层面向无连接NIC驱动程序的已知特征，决定是使用NdisSend还是NdisSendPackets函数。下边界面向无连接的中间层驱动程序可以通过以NdisQueryInformation（RequeryType类型）和OID_GEN_MAXIMUM_SEND_PACKETS为参数调用NdisRequest，获取低层驱动程序能够接受的发送包数组的最大包数量。如果低层驱动程序返回值‘1’或NDIS_STATUS_NOT_SUPPORTED，那么中间层驱动程序可以选择使用NdisSend而不是NdisSendPackets函数。如果低层驱动程序返回大于‘1’的值，那么在发送包数组时使用NdisSendPackets函数会使两种驱动程序的性能都变得更好。

如果低层面向无连接的NIC驱动程序返回大于‘1’的值，那么下边界面向无连接的中间层驱动程序也应该提供MiniportSendPackets函数。另外，中间层驱动程序应该用与低层NIC驱动程序相等大小的值响应OID_GEN_MAXIMUM_SEND_PACKETS查询。

如果OOB数据在中间层驱动程序和NIC驱动程序之间传递，两个发送函数都可能被调用，因为在任何一种情况下，低层驱动程序都能使用NDIS提供的宏读取与包描述符相关的OOB数据。

如果中间层驱动程序发送超出低层NIC驱动程序内部资源承受能力的数据包：

n 非串行或面向连接的NIC驱动程序将在其内部队列中对超量的数据包进行排队，在条件允许的时候再发送它们；

n 串行NIC驱动程序可以对超量的数据包在内部队列中进行排队，在条件允许的时候再发送它们，也可以用NDIS_STATUS_RESOURCE状态返回超量数据包。在后一种情况下，NDIS将在内部队列中保存那些数据包并在NIC驱动程序下一次调用NdisMSendResourceAvailable或NdisMSendComplete时，重新提交这些数据包。

当下边界面向无连接的中间层驱动程序从MiniportSend调用NdisSend函数时，将以同步或异步方式转让包描述符的所有权及其所描述的所有资源（直到发送操作完成为止）。如果NdisSend返回的状态不是NDIS_STATUS_PENDING，则调用是以同步方式完成的，并且在NdisSend返回时包资源的所有权就将返还给中间层驱动程序。中间层驱动程序应该返还所有高层驱动程序分配的发送资源，并传送NdisSend调用的结果作为MiniportSend的返回状态。

如果NdisSend返回的状态是NDIS_STATUS_PENDING，当接下来发送操作完成的时候，发送操作的最终状态和包描述符将返回给ProtocolSendComplete。中间层驱动程序应该从ProtocolSendComplete函数中调用NdisSendComplete传送发送状态给相邻的高层驱动程序。

当下边界面向无连接的中间层驱动程序通过调用NdisSendPackets发送一个或多个数据包时，或者当下边界面向连接的中间层驱动程序通过调用NdisCoSendPackets发送一个或多个数据包时，发送操作默认是异步的。直到每一个包描述符和该报发送的最终状态都返回给ProtocolCoSendComplete之后，呼叫器才释放这些包描述符的所有权。ProtocolCoSendComplete必须像前一段描述的那样，向相邻的高层驱动程序传送每一个包的发送状态。

作为一个结论，如果中间层驱动程序用NdisSendPackets发送数组中的数据包，那么在NdisSendPackets返回时，它不能企图读取相关的OOB数据块的Status成员。该成员被NDIS用于跟踪转换中的发送请求的进程，它是易变的。中间层驱动程序仅仅能通过检查传送给Protocol(Co)SendComplete函数的Status参数来获取传送请求的状态。

如果在发送之前，中间层驱动程序通过重组从上层驱动程序接收的数据包，请求不同优先级的包数组传送，那么应将最高优先级的数据包放在数组的开始位置。当将这些数据包传给低层NIC驱动程序的MiniportSend或Miniport(Co)SendPackets函数时，NDIS将保持该顺序，即使在内部要对一些数据包进行排队。对于每一个Ndis(Co)SendPackets调用，NDIS将维持该顺序。

NDIS永远不会企图对与包描述符相关的OOB数据块进行排队或检查。除非中间层驱动程序对NIC驱动程序处理包优先级的方式有特别的了解，否则，应假定NIC驱动程序按接收到的顺序发送数据包，保持接收时的顺序。

NDIS_PACKET类型描述符的私有（Private）成员的结构对中间层驱动程序是不透明的，并且允许使用NDIS提供的宏或函数对其进行读访问，在某些情况下，也可进行写访问。例如，在发送一个数据包之前，中间层驱动程序可以调用NdisSetPacketFlags设置描述符的NDIS私有部分的中间判定标识。这些标识并不是NDIS定义的，而是协议和低层NIC驱动程序合作定义的。

1.6.1传递介质相关信息

中间层驱动程序可以通过与每一个NDIS_PACKET描述符相关的OOB数据块，传送更多的介质相关信息。以下是OOB数据块的定义：

typedef struct _ NDIS_PACKET_OOB_DATA{

union{

ULONGLONG TimeToSend;

ULONGLONG TimeSend;

};

ULONGLONG TimeReceived;

UINT HeaderSize;

UINT SizeMediaSpecificInfo;

PVOID MediaSpecificInformation;

NDIS_STATUS Status;

} NDIS_PACKET_OOB_DATA, * PNDIS_PACKET_OOB_DATA;

缓冲区中的单个记录结构MediaSpecificInformation定义如下：

typedef struct MediaSpecificInformation{

UINT NextEntryOffset;

NDIS_CLASS_ID ClassId;

UINT Size;

UCHAR ClassInformation[1];

}MEDIA_SPECIFIC_INFORMATION;

ClassId成员是NDIS定义的枚举变量（ClassInformation[1]中发现的信息类型）。目前，支持win32的微软操作系统中为介质提供了四个Class Ids：NdisClass802_3Priority、NdisClassWirelessWanMbxMailbox、NdisClassIrdaPacketInfo 和NdisClassAtmAALInfo。关于更详细的信息请参阅在线“the Network Drivers Reference”。

如果中间层驱动程序知道发送数据包的低层NIC驱动程序要使用OOB数据，它就能够设定相应的OOB结构成员。例如，中间层驱动程序能够完成以下操作：

n 通过使用NDIS_SET_PACKET_TIME_TO_SEND宏设置TimeToSend成员，请求数据包在特定的时间发送。该宏在系统时间单元中传递请求时间。驱动程序可以调用NdisGetCurrentSystemTime获取系统时间，可用该值计算请求发送时间。

n 可以使用NDIS_PACKET_SET_MEDIA_SPECIFIC_INFO宏在MediaSpecificInformation中传递呼叫器缓冲区中的介质相关信息。例如，如果中间层驱动程序绑定到要求优先级的低层NIC，那么可以设置MediaSpecificInformation结构的ClassId成员为NdisClass802_3Priority，并通过ClassInformation传递优先级相关信息以及该信息的字符大小值。

1.7处理中间层驱动程序的PnP事件和PM事件

中间层驱动程序必须能够处理即插即用（PnP）事件和电源管理事件（PM）。特别地：

n 中间层驱动程序必须在传给NdisMSetAttributeEx的AttributeFlags参数中设置标识，参阅1.2.2节;

n 中间层驱动程序的微端口部分必须处理OID_PNP_XXX请求;

n 中间层驱动程序的协议部分必须传送准确的OID_PNP_XXX请求给低层的微端口。中间层驱动程序的微端口部分必须向最初产生请求的协议驱动程序传递低层微端口对该请求的响应；

n 中间层驱动程序的协议部分必须提供ProtocolPnPEvent处理程序。

1.7.1处理OID_PNP_XXX查询和设置

中间层驱动程序的上边界必须导出MiniportQueryInformation函数和MiniportSetInformation函数。当上层驱动程序（绑定到中间层驱动程序导出的虚设备实例的）调用NdisRequest设置和查询对象信息（OID_XXX）时，NDIS将相应地调用MiniportQueryInformation或MiniportSetInformation函数实现该功能。NDIS也可为实现自己的某项功能调用MiniportQueryInformation或MiniportSetInformation。关于微端口设置和查询处理的更多信息请参阅第二部分第五章。

中间层驱动程序能够使用NdisRequest（如果中间层驱动程序下边界是面向无连接的）或NdisCoRequest（如果中间层驱动程序下边界是面向连接的）查询或设置低层微端口保存的OID_XXX。

在绑定到低层NIC之后，中间层驱动程序应该通过查询OID_PNP_CAPABILITIES确定低层NIC的电源管理能力。如果NIC能够进行电源管理，低层微端口对OID_PNP_CAPABILITIES查询返回NDIS_STATUS_SUCCESS。Miniport也能够设定NIC唤醒能力。如果NIC不具备电源管理能力，低层微端口对OID_PNP_CAPABILITIES查询返回NDIS_STATUS_NOT_SUPPORTED。

中间层驱动程序处理查询和设置上边界保存的即插即用对象信息的方式，在一定程度上依赖于低层NIC是否具有电源管理能力:

n OID_PNP_CAPABILITIES

如果低层NIC具有电源管理能力，中间层驱动程序对OID_PNP_CAPABILITIES查询必须返回NDIS_STATUS_SUCCESS。在该OID返回的OID_PM_WAKE_UP_CAPABILITIES结构中，中间层驱动程序必须为每一个唤醒功能指定NdisDeviceStateUnspecified的设备电源状态。该响应表示中间层驱动程序具有电源管理能力但不能唤醒系统。

如果低层NIC不具备电源管理能力，低层微端口对OID_PNP_CAPABILITIES查询返回NDIS_STATUS_NOT_SUPPORTED。

n OID_PNP_QUERY_POWER和OID_PNP_SET_POWER

中间层驱动程序对OID_PNP_QUERY_POWER查询和OID_PNP_SET_POWER设置返回NDIS_STATUS_SUCCESS。中间层驱动程序不能将这些OID请求传递给低层微端口驱动程序。

n Wake-up OIDs

如果低层NIC具有电源管理能力，中间层驱动程序向低层微端口传递（调用Ndis(Co)Request）如下的关于唤醒事件的OID_PNP_XXX：

OID_PNP_ ENABLE_WAKE_UP

OID_PNP_ ADD_WAKE_UP_PATTERN

OID_PNP_ REMOVE_WAKE_UP_PATTERN

OID_PNP_ WAKE_UP_PATTERN_LIST

OID_PNP _WAKE_UP_ERROR

OID_PNP _WAKE_UP_OK

中间层驱动程序也必须把低层微端口对这些OID的响应传递给上边的协议驱动程序。

如果NIC不具备电源管理能力，中间层驱动程序对这些OID查询和设置应该返回NDIS_STATUS_NOT_SUPPORTED。

1.7.2中间层驱动程序ProtocolPnPEvent处理程序的实现

如果操作系统向代表NIC的目标设备对象发布即插即用IRP或电源管理IRP，NDIS截取该IRP，然后通过调用驱动程序的ProtocolPnPEvent处理程序向每一个绑定的中间层驱动程序和协议驱动程序指示该事件，NDIS传递描述被指示的PnP事件或PM事件的NET_PNP_EVENT结构的指针。

就像NET_PNP_EVENT结构中NetEvent节点所指示的，有六个可能的PnP和PM事件：

n NetEventSetPower

指示电源设置请求，该请求指定将NIC过渡到特定电源状态。中间层驱动程序应该总是通过返回NDIS_STATUS_SUCCESS说明成功处理该事件。关于处理电源设置请求的更多信息请参阅1.7.3。

n NetEventQueryPower

指示电源查询请求，该请求查询NIC能否过渡到特定电源状态。中间层驱动程序应该总是通过返回NDIS_STATUS_SUCCESS说明成功处理该事件。中间层驱动程序应该总是能够成功执行NetEventQueryPower。不应通过使NetEventQueryRemoveDevice失败的方式阻止系统过渡到睡眠状态。注意NetEventQueryPower之后总是要调用NetEventSetPower。对设备当前电源状态的NetEventSetPower调用实际上相当于撤销了NetEventQueryPower。

n NetEventQueryRemoveDevice

指示设备删除查询请求，该请求查询NIC能否在不中断操作的情况下删除NIC。如果中间层驱动程序不能释放设备（例如，因为设备正在使用），那么必须以返回NDIS_STATUS_FAILURE的方式使NetEventQueryRemoveDevice操作失败。

n NetEventCancelRemoveDevice

指示取消设备删除操作请求，该请求取消NIC的删除操作。中间层驱动程序应该总是通过返回NDIS_STATUS_SUCCESS说明成功处理该事件。

n NetEventReconfigure

指示网络部件的配置已经改变。例如，如果用户改变了TCP/IP的IP地址，NDIS用NetEventReconfigure向TCP/IP协议指示该事件。中间层驱动程序应该总是通过返回NDIS_STATUS_SUCCESS说明成功完成该事件。

n NetEventBindList

向TDI客户指示绑定列表已经改变。绑定列表是传输协议导出的一个或多个设备的列表，TDI客户可绑定到上面。

n NetEventBindComplete

指示中间层驱动程序已经绑定到所有可以绑定的NIC上。除非有即插即用NIC装入系统，否则NDIS不会向中间层驱动程序指示更多的NIC。

NET_PNP_EVENT的Buffer成员指向包含被指示事件特定信息的缓冲区。更多的信息请参阅在线DDK“Network Drivers Reference”。

中间层驱动程序能够以NdisCompletePnPEvent异步地完成ProtocolPnPEvent调用。

1.7.3处理规定的电源请求

中间层驱动程序处理电源设置请求的方式依赖于低层NIC是否具有电源管理能力。

睡眠状态的电源设置请求

1. NDIS调用IM驱动程序和每一个绑定到虚设备（IM驱动程序导出的）的协议驱动程序的ProtocolPnPEvent函数。该调用指定NetEventSetPower为睡眠状态（D1、D2、D3网络设备的电源状态）。绑定的协议驱动程序停止向中间层驱动程序发送数据包和进行OID请求。中间层驱动程序停止向下边的微端口发送数据包和进行OID请求。

2. NDIS向中间层驱动程序的微端口(上边)(如果该NIC支持电源管理，还包括低层的NIC 微端口)发出OID_PNP_SET_POWER请求。当成功完成请求时，中间层驱动程序和低层微端口都返回NDIS_STATUS_SUCCESS。中间层驱动程序不能向低层 微端口传递OID_PNP_SET_POWER请求。

3. 通常情况下，中间层驱动程序不会清除虚NIC的初始化。尤其是，如果低层NIC具有电源管理能力并支持唤醒事件，中间层驱动程序不能清除虚NIC的初始化。如果中间层驱动程序调用NdisIMDeinitialDeviceInstance清除虚NIC的初始化，那么NDIS调用每一个绑定的协议驱动程序的ProtocolUnbindAdapter函数解除到虚NIC的绑定。在TCP/IP协议解除绑定之前，通过向低层微端口发送OID_PNP_ REMOVE_WAKE_UP_PATTERN清除所有低层微端口的唤醒模式，这实际上禁止了唤醒匹配模式。

工作状态的电源设置请求

n 如果低层NIC支持电源管理，NDIS可向低层微端口发出OID_PNP_SET_POWER请求设置工作状态。NDIS也可向中间层驱动程序的微端口(上边)发出OID_PNP_SET_POWER请求。对电源设置请求，中间层驱动程序仅仅返回NDIS_STATUS_SUCCESS。中间层驱动程序不能向低层微端口传递OID_PNP_SET_POWER请求。

n 如果中间层驱动程序清除了虚NIC的初始化，那么必须调用NdisIMInitialDeviceInstance重新初始化该NIC。在这种情况下，为了将协议驱动程序绑定到虚NIC上，NDIS调用每一个上层协议驱动程序的ProtocolBindAdapter函数。

n NDIS调用IM驱动程序和每一个绑定到虚设备（IM驱动程序导出的）的协议驱动程序的ProtocolPnPEvent函数。该调用指定NetEventSetPower为工作状态（D0网络设备的电源状态）。绑定的协议驱动程序可以开始向中间层驱动程序发送数据包和进行OID请求。中间层驱动程序可以开始向下边的微端口发送数据包和进行OID请求。

注意，在向低层微端口发送OID_PNP_SET_POWER之前，NDIS可向中间层驱动程序微端口发出该OID设置D0状态。在调用中间层驱动程序的ProtocolPnPEvent函数指示D0状态之前，NDIS也可调用绑定协议的ProtocolPnPEvent函数指示NIC过渡到D0状态。

在上述情况下，在低层NIC真正完全行使职能之前，中间层驱动程序的微端口将在功能上完全代表NIC，中间层驱动程序必须具备处理这种情况的能力，例如，如果绑定的协议驱动程序向中间层驱动程序的微端口发送一个数据包，中间层驱动程序应该立即在内部对该包进行排队直到低层NIC完全行使职能。

1.8中间层驱动程序复位操作

中间层驱动程序必须提供由于低层NIC复位而导致的发送（低层驱动程序未处理的）撤销能力。

典型地，低层驱动程序复位NIC是由于当NDIS队列发送超时或有NIC的绑定请求时，NDIS调用NIC驱动程序的MiniportReset函数。如果高层驱动程序调用NdisReset，微端口也可复位NIC。如果低层NIC要复位，NDIS以NDIS_STATUS_RESET_START状态调用每一个绑定协议和中间层驱动程序的Protocol(Co)Status函数，然后调用同一个绑定驱动程序的ProtocolStatusComplete函数。当NIC驱动程序复位完成时，NDIS用NDIS_STATUS_RESET_END再一次调用Protocol(Co)Status函数，并接着调用ProtocolStatusComplete函数。

当NIC复位时，如果中间层驱动程序有任何已发送而该NIC又未处理掉的数据包，NDIS将向中间层驱动程序返回这些包的准确状态并结束这些数据包。复位完成后，中间层驱动程序必须重新发送这些数据包。

当中间层驱动程序收到NDIS_STATUS_RESET_START状态时，将进行以下操作：

n 挂起发送就绪的数据包直到Protocol(Co)Status收到NDIS_STATUS_RESET_END通知并且已调用ProtocolStatusComplete函数为止;

n 挂起已经准备好向高层驱动程序指示的接收数据包直到Protocol(Co)Status收到NDIS_STATUS_RESET_END通知并且已调用ProtocolStatusComplete函数为止;

n 清除其保持的所有处理中的操作的内部状态和NIC状态。

在Protocol(Co)Status收到NDIS_STATUS_RESET_END并且ProtocolStatusComplete函数已被调用之后，中间层驱动程序可继续发送数据包、产生请求、向高层驱动程序作指示。

因为中间层驱动程序通常在NDIS调用NdisMSetAttributeEx时，就将发送和请求超时禁止掉了，所以MiniportReset很少被调用。如果MiniportReset被调用，有可能是因为高层驱动程序调用了NdisReset，那么如果有必要的话，中间层驱动程序应复位内部状态并应该在返回之前设置AddressingReset为TRUE。当低层NIC复位时，如果低层NIC处在继续发送和接收数据包状态，NDIS将请求MiniportSetInformation，为该NIC复位中间层驱动程序的内部地址状态。MiniportReset不必完成为未处理完的发送，NDIS将以适当的方式使高层驱动程序的未完成发送失效。不过，中间层驱动程序保存的一些相关状态可能应该清除。

中间层驱动程序能够通过调用NdisReset启动复位操作。如果复位请求返回NDIS_STATUS_PENDING，那么当低层NIC或虚NIC复位时调用了ProtocolResetComplete并且驱动程序为NIC调用了ProtocolMResetComplete。除非中间层驱动程序知道低层NIC不能正常运行，否则很少调用NdisReset。例如，如果中间层驱动程序发现对于相当数量的发送或请求没有收到完成调用，而且如果有足够的关于低层NIC的知识断定有问题发生，则可以调用NdisReset。然而，通常情况下，NIC复位要求的检测和启动是通过NDIS和NIC驱动程序使用超时逻辑来完成的。

1.9中间层驱动程序拆除绑定操作

当低层NIC不再可用时，中间层驱动程序从NDIS调用的NdisUnbindAdapter函数中调用NdisCloseAdapter来从低层NIC驱动程序解除绑定。例如，如果低层NIC超时并且NDIS为故障NIC调用了NIC驱动程序的MiniportHalt函数，那么接下来NDIS将调用上层的中间层驱动程序的NdisUnbindAdapter函数。

中间层驱动程序也能发起解除绑定的操作。例如，当在ProtocolStatus中接收到的GeneralStatus状态值为NDIS_STATUS_CLOSING时。如果初始化期间某个操作并发地使用适配器，例如分配所要求的资源失败，那么中间层驱动程序也必须解除到适配器的绑定，因为绑定时不能执行网络操作。

中间层驱动程序的NdisUnbindAdapter函数要调用NdisCloseAdapter。中间层驱动程序不必释放预绑定资源，除非NdisCloseAdapter函数返回NDIS_STATUS_SUCCESS或者驱动程序调用NdisCompleteUnbindAdapter。如果NdisCloseAdapter函数返回NDIS_STATUS_PENDING，那么中间层驱动程序不能够释放预绑定的资源（直到NdisCloseAdapterComplete函数被调用为止）。

在该对绑定上的所有请求处理完后，NDIS才能调用NdisCloseAdapterComplete。当NdisCloseAdapterComplete返回控制权后，中间层驱动程序分配的代表绑定的ProtocolBindingContext句柄将变成无效的。

在同步地从NdisUnbindAdapter返回之前，或者用NdisCompleteUnbindAdapter异步地完成解除绑定操作之前，中间层驱动程序必须完成以下操作：

n 清除其为绑定保存的任何状态值；

n 释放其分配的用于建立绑定的所有资源；

n 调用NdisCloseAdapter。

如果正在被结束的绑定被映射到中间层驱动程序导出的设备，并且该设备是通过调用NdisIMInitializeDeviceInstance初始化的，那么中间层驱动程序能够通过调用NdisIMDeInitializeDeviceInstance或NdisIMInitializeDeviceInstanceEx函数关闭这些设备。这样做的结果将导致对高层驱动程序来的发送和请求，中间层驱动程序的虚NIC将不再响应。

当中间层驱动程序调用NdisCloseAdapter之后，应该以合适的错误状态值使该绑定的所有发送请求失效。

1.10中间层驱动程序状态指示

下边界面向无连接的中间层驱动程序被要求提供ProtocolStatus和ProtocolStatusComplete函数。当低层面向无连接的NIC驱动程序调用NdisMIndicateStatus报告硬件状态时，NDIS调用ProtocolStatus。当状态改变开始的时候，ProtocolStatus被调用。如果当ProtocolStatus调用时状态节点指示的操作没有完成，低层NIC驱动程序紧跟着将调用NdisMIndicateStatusComplete。当上面情况发生时，调用ProtocolStatusComplete为状态改变执行后期操作。

面向连接的中间层驱动程序被要求提供ProtocolCoStatus函数和ProtocolStatusComplete函数。当低层面向连接的NIC驱动程序调用NdisMCoIndicateStatus报告硬件状态时，NDIS调用ProtocolCoStatus。当状态改变开始的时候，ProtocolCoStatus被调用。

被送给ProtocolStatus 的GeneralStatus例子包括：

n NDIS_STATUS_CLOSING

在1.9节讨论了该状态和Protocol(Co)Status的操作;

n NDIS_STATUS_RESET_START和NDIS_STATUS_RESET_END

就像1.8节中所说的,这两个状态都将送给Protocol(Co)Status 和ProtocolStatusComplete函数；

n NDIS_STATUS_LINE_UP

该状态表明中间层驱动程序是位于已经与远程节点建立连接的支持WAN功能的NIC驱动程序之上。关于WAN驱动程序的更多信息请参阅第二部分第八章；

n NDIS_STATUS_RING_STATUS

对于该状态StatusBuffer提供了更详细的信息，例如关于令牌环介质的问题。

当中间层驱动程序收到状态指示的时候，如果状态指示导致中间层驱动程序以影响MiniportXxx函数操作的方式改变其内部状态，那么它将通过调用NdisMIndicateStatus向上层驱动程序指示该状态。即，如果指示给中间层驱动程序的状态使得驱动程序的发送或请求失效，那么中间层驱动程序能够向高层驱动程序（可能暂停提交发送和请求的）指示该状态。然而，如果中间层驱动程序通过内部等待队列继续接收发送和请求，那么不应向上传递该状态信息。


NDIS协议驱动程序

本章描述在下边界导出一组ProtocolXxx函数的NDIS驱动程序的特征。协议驱动程序和NDIS进行通讯来发送和接收网络数据包，并绑定和使用低层微端口 NIC驱动程序或中间层NDIS驱动程序（它在上边界导出MiniportXxx接口）。

上述的NDIS协议驱动程序可能在上边界支持TDI，或者可以通过驱动程序的传送栈，其中包括栈顶支持TDI的栈，向高层核心模式驱动程序导出私有接口。例如，NDIS协议驱动程序可能是多模块传输实现的标准协议的最低层模块，如最高层模块支持TDI的TCP/IP协议。

和低层NDIS驱动程序通信来发送和接收数据包的协议驱动程序总是使用NDIS提供的函数来进行通信。例如，下边界面向无连接的协议驱动程序（和面向无连接介质的低层驱动程序进行通信，如以太网、令牌环网）必须调用NdisSend或者NdisSendPackets向低层NDIS驱动程序发送数据包，也必须调用NdisRequest来产生或传送查询，以及用网络相关的低层面向无连接驱动程序支持的OID_XXX的设置信息请求。

NDIS也提供一组隐藏低层操作系统细节的NdisXxx函数。例如，协议驱动程序调用NdisInitializeEvent为同步目的创建事件，调用NdisInitializeListHead创建链表。使用那些函数的NDIS版本的协议驱动程序在支持WIN32接口的微软操作系统中可移植性更好。协议驱动程序也可调用OS专用的核心模式支持的例程，像用KeInitializeEvent创建事件、用KeWaitForSingleObject同步两个执行线程。核心模式支持例程的文档可查阅“Kernel-Mode Drivers Reference”。

可分页代码和可丢弃代码

每一个ProtocolXxx运行的IRQL是从PASSIVE_LEVEL到DISPATCH_LEVEL。

独占地运行在IRQL PASSIVE_LEVEL下的函数应该用NDIS_PAGEABLE_FUNCTION宏标识为页面模式。驱动程序开发者应尽可能地将代码设计为可分页的，释放内存驻留代码的系统空间。运行在IRQL PASSIVE_LEVEL下的驱动程序函数只要其既不调用也不被运行在IRQL〉= DISPATCH_LEVEL下的任何函数调用——例如要求自旋锁的函数，就可设计为可分页。获取自旋锁导致获取线程的IRQL被提升到IRQL PASSIVE_LEVEL。运行在IRQL PASSIVE_LEVEL下的驱动程序函数，如ProtocolBindAdapter，如果被设计为可分页的，那么就不能调用任何运行在IRQL〉= DISPATCH_LEVEL下的NdisXxx函数。关于每一个NdisXxx函数的IRQL，请参阅在线DDK的“the Network Drivers Reference”。

协议驱动程序的DriverEntry函数以及其中调用的代码，应该用NDIS_INIT_FUNCTION宏指定为仅仅用于初始化的代码。该宏标识的代码默认情况下只在系统初始化时运行一次，并且作为结果，该部分代码只在那时被映射。在函数代码被标识为仅用于初始化时，该代码将成为可丢弃的。

如果驱动程序分配的共享资源可以被两个驱动程序函数同时访问，或者如果协议驱动程序能够运行在SMP机器上，使得同一个协议驱动程序函数可以从多个处理器上同时访问某些资源，那么这些访问必须被同步。例如，如果驱动程序维护共享队列，自旋锁可用来串行对该队列的访问，该自旋锁应在队列创建的时候初始化。

然而，也不应过分保护共享资源，如内部驱动程序队列。有些只读操作对队列访问可不必串行化，但任何变换队列连接的操作都必须串行化。自旋锁应该尽可能少地使用，并尽可能地减少其存在时间。关于自旋锁的深入讨论请参阅“the Kernel-Mode Drivers Design Guide”。

2.1 协议DriverEntry及其初始化

对于驱动程序初始化要求的入口点，为了使引导程序能够识别，必须被明确地命名为DriverEntry形式。所有其他的被描述为ProtocolXxx的导出函数，由于其地址被传给了NDIS，则可由开发者指定任何确定的名字。任何内核模式驱动程序的DriverEntry定义具有以下形式：

NTSTAUS

DriverEntry(

IN PDRIVER_OBJECT DriverObject,

IN PUNICODE_STRING RegistryPath

);

如果除了NDIS 定义的ProtocolXxx之外，驱动程序还导出一组标准的内核模式驱动程序例程，如（Tdi）DispatchXxx和卸载例程，那么协议驱动程序必须将这些标准例程的地址以DriverObject的形式传入DriverEntry，就像任何其他的内核模式中间层驱动程序一样。

为了和NDIS库建立通信，协议的DriverEntry必须调用NdisRegisterProtocol作为协议驱动程序注册，稍候将作详细描述。

DriverEntry也能够初始化协议要求的自旋锁，如保护用于跟踪连接和运行中的发送任务或驱动程序分配的队列等的状态变量。

如果DriverEntry分配某个协议要求的资源失败，那么它应该释放先前为其分配的任何资源，如果必要，甚至可以包括调用NdisDeregisterProtocol，然后返回一个适当的错误状态。

当协议驱动程序能够分配DriverEntry中要求的所有资源时，如果驱动程序提供了像2.3节描述的ProtocolBindAdapter函数，那么可以推迟对低层NIC（或虚NIC）驱动程序的断开和绑定，并保留系统资源管理绑定直到出现初始的网络I/O请求。然后，当ProtocolBindAdapter调用NdisOpenAdapter时，将分配低层NIC（打开的）要求的资源。

2.1.1注册NDIS协议驱动程序

驱动程序在DriverEntry环境中通过调用NdisRegisterProtocol向NDIS注册ProtocolXxx函数。NdisRegisterProtocol定义如下：

VOID

NdisRegisterProtocol(

OUT PNDIS_STATUS Status,

OUT PNDIS_HANDLE NdisProtocolHandler,

IN NDIS_PROTOCOL_CHARACTERISTICS ProtocolCharacteristics,

IN UINT CharacteristicsLength

);

该调用的返回句柄NdisProtocolHandler对协议驱动程序是透明的，协议驱动程序必须保存该句柄并在将来对NDIS的调用中作为输入参数传递，例如，打开低层适配器。

在调用NdisRegisterProtocol之前，DriverEntry必须完成以下操作：

1. 零初始化一个NDIS_PROTOCOL_CHARACTERISTICS类型的结构，例如调用NdisZeroMemory函数。这将确保可选入口点的尚未使用的成员设置为NULL。如果该结构没被置零，那么在调用NdisRegisterProtocol之前，任何不再使用的成员必须置为NULL;

2. 像2.1.1.1节中描述的那样，在NDIS_PROTOCOL_CHARACTERISTICS结构中指定协议兼容的NDIS版本;

3. 像2.1.1.2节和2.1.1.3节中描述的那样，在NDIS_PROTOCOL_CHARACTERISTICS结构中保存驱动程序导出的强制性的和非强制的ProtocolXxx函数的地址。

2.1.1.1 为协议驱动程序指定NDIS版本号

NDIS_PROTOCOL_CHARACTERISTICS结构中的MajorNdisVersion和MinorNdisVersion成员指定了协议兼容的NDIS版本。对协议有效的NDIS版本号为5.0和4.0。最新的版本是5.0。NDIS继续支持早期的为4.0版设计的驱动程序。

协议驱动程序必须支持即插即用功能，所以NDIS不再支持3.0版的协议驱动程序。

2.1.1.2 注册下边界面向无连接的协议驱动程序的ProtocolXxx函数

面向无连接驱动程序必须和可能要求的导出协议函数如下所列：

BindAdapterHandler

这是一个必须提供的函数。NDIS调用该函数请求协议驱动程序绑定到低层网卡或虚拟网卡上，网卡名作为该处理程序的参数传递。关于动态绑定的更多的信息参见2.3节.

UnbindAdapterHandler

这是一个必须提供的函数。NDIS调用ProtocolUnbindAdapter释放对低层网卡或虚拟网卡的绑定，网卡名作为参数传递。当绑定成功解除时，ProtocolUnbindAdapter函数调用NdisCloseAdapter并释放资源。

OpenAdapterCompleteHandler

这是一个必须提供的函数。如果协议驱动程序对NdisOpenAdapter的调用返回NDIS_STATUS_PENDING，则接着调用ProtocolOpenAdapterComplete来完成绑定操作。

CloseAdapterCompleteHandler

这是一个必须提供的函数。如果协议驱动程序对NdisCloseAdapter的调用返回NDIS_STATUS_PENDING，则接着调用ProtocolCloseAdapterComplete来完成解除绑定操作。

ReceiveHandler

这是一个必须提供的函数。ProtocolReceive函数以前视缓冲区的指针为参数被调用执行。如果该缓冲区包含的不是完整的接收到的网络数据包，ProtocolReceive以协议分配的数据包描述符作为参数，调用NdisTransferData指定协议分配缓冲区接收数据包的其余部分。

ReceiveCompleteHandler

这是一个必须提供的函数。ProtocolReceiveComplete用来指出：以前指示给ProtocolReceive 的接收数据包现在可以延期处理。

TransferCompleteHandler

这是一个必须提供的函数，除非协议排它地绑定到低层NIC驱动程序（以NdisMIndicateReceivePacket指示数据包）上。当NdisTransferData调用返回NDIS_STATUS_PENDING，并且剩余数据已复制到与给定包描述符相连的协议分配的缓冲区时，ProtocolTransferDataComplete函数被调用。

ReceivePacketHandler

这是一个可选函数。如果协议驱动程序绑定到NIC驱动程序（通过调用NdisMIndicateReceivePacket指示包数组），那么ProtocolReceivePacket函数应被提供。

SendCompleteHandler

这是一个必须提供的函数。对用NdisSend函数（返回NDIS_STATUS_PENDING作为发送状态）调用来传输的每一个数据包，ProtocolSendComplete函数被调用。如果包数组被发送，那么对于每一个传给NdisSendPackets的数据包，ProtocolSendComplete被调用一次，不管是否返回了NDIS_STATUS_PENDING。

ResetCompleteHandler

这是一个必须提供的函数。当返回值为NDIS_STATUS_PENDING的NdisReset函数调用开始的启动协议复位操作完成时，ProtocolResetComplete被调用。

RequestCompleteHandler

这是一个必须提供的函数。当以返回为NDIS_STATUS_PENDING的NdisReset函数调用开始的启动协议查询和设置操作完成时，ProtocolRequestComplete被调用。

StatusHandler

这是一个必须提供的函数。ProtocolStatus函数用来处理低层NIC驱动程序指示的状态改变。

StatusCompleteHandler

这是一个必须提供的函数。NDIS调用ProtocolStatusComplete函数和ProtocolStatus函数来报告发起的NDIS驱动程序和NIC驱动程序复位操作的开始和结束。

PnPEventHandler

这是一个必须提供的函数。NDIS调用ProtocolPnPEvent来指示即插即用事件或电源管理事件。更多信息参见2.6节。

UnloadHandler

这是一个可选函数。NDIS调用ProtocolUnload函数来响应用户卸载中间层驱动程序的请求。对于每一个绑定的适配器，在调用NDIS ProtocolUnbindAdapter之后，调用ProtocolUnload函数卸载驱动程序。ProtocolUnload执行驱动程序决定的清除操作。

协议驱动程序应该设置ProtocolCharacteristics的TranslateHandler成员为NULL，该成员预留未来使用。

2.1.1.3 注册面向连接协议驱动程序的ProtocolXxx函数

面向连接的协议可以或者必须注册如下的，对于面向无连接的和面向连接的协议所共有的协议处理程序函数：

n BindHandler(必须)

n UnbindHandler(必须)

n OpenAdapterCompleteHandler(必须)

n CloseAdapterCompleteHandler(必须)

n ReceiveCompleteHandler(必须)

n ResetCompleteHandler(必须)

n RequestCompleteHandler(必须)

n StatusCompleteHandler(必须)

n PnPEventHandler(必须)

这些函数已经在2.1.1.2节已作了总结。

面向连接的协议也必须注册如下的面向连接的协议函数，说明这些入口点需要ProtocolCharacteristics类型的5.0版的结构。

CoSendCompleteHandler

这是一个必须提供的函数。对于传给NdisCoSendPackets的每一个数据包ProtocolCoSendComplete都要被调用一次。中间层驱动程序仅仅根据送给ProtocolCoSendComplete的状态参数就能确定调用NdisCoSendPackets的发送操作的状态。

CoStatusHandler

这是一个必须提供的函数。NDIS用低层NIC驱动程序初始化的状态标志信息来调用ProtocolCoStatus函数。

CoReceivePacketsHandler

这是一个必须提供的函数。当一个绑定的面向连接的NIC驱动程序或MCM通过调用NdisMCoIndicateReceivePackets指示一个指针数组时，NDIS调用中间层驱动程序的ProtocolCoReceivePacketHandler。

CoAfRegisterNotifyHandler

如果协议驱动程序是一个使用呼叫管理器或MCM驱动程序的呼叫管理服务的面向连接的客户程序，则必须注册ProtocolCoAfRegisterNotify函数。当呼叫管理器或MCM驱动程序调用Ndis(M)CmRegisterNotify注册地址族时，NDIS调用呼叫绑定的每一个协议的ProtocolCoAfRegisterNotify函数。ProtocolCoAfRegisterNotify确定协议驱动程序是否可以使用已经发布其服务的呼叫管理器或MCM驱动程序的服务。独立于呼叫管理器的协议驱动程序也必须注册CoAfRegisterNotifyHandler处理程序，当被NDIS调用呼叫时，呼叫管理器的ProtocolCoAfRegisterNotify函数简单的返回控制权。

2.1.2 打开中间层驱动程序低层的适配器

协议驱动程序读取安装期间存储的注册信息，建立将要绑定的适配器名列表。注册信息包括网络配置时写入的绑定信息。DriverEntry读取这些注册信息，其中包括可以绑定的适配器名。

协议驱动程序调用NdisOpenProtocolConfiguration获取协议驱动程序能够存储适配器相关信息的注册表主键句柄。然后，协议驱动程序可以调用NdisOpenConfigurationKeyByIndex或者NdisOpenConfigurationKeyByName函数，获取由NdisOpenProtocolConfiguration函数打开的主键下的子键句柄。

在注册表中存储适配器相关信息的协议驱动程序，必须在ProtocolName下存储该信息并使用NDIS函数访问这些信息。一旦获取了该键的句柄，协议驱动程序就可以调用NdisRead(Write)Configuration函数读写这些信息。NdisRead(Write)Configuration函数在在线DDK的“Network Drivers Reference”中有详细的描述。

如果协议驱动程序提供ProtocolBindAdapter函数，那么传给该函数的SystemSpecific1参数将在ProtocolSection中被连续地传给NdisOpenProtocolConfiguration。如果协议驱动程序没有提供ProtocolBindAdapter函数，那么假定驱动程序知道其打开的适配器的名字和ProtocolSection中传递的字符串值。

在协议驱动程序已经从注册表中获得了其要求的信息，并已调用NdisRegisterProtocol完成注册之后，在协议驱动程序能够发送数据包和接收导入数据包之前，必须绑定到微端口或中间层NDIS驱动程序管理的低层NIC。协议通过调用NdisOpenAdapter函数将自身绑定到低层NIC和管理它的微端口。NdisOpenAdapter函数描述如下：

VOID

NdisOpenAdapter（

OUT PNDIS_STATUS Status,

OUT PNDIS_STATUS OpenErrorStatus,

OUT PNDIS_HANDLE NdisBindingHandle,

OUT PUNIT SelectedMediumIndex,

IN PNDIS_MEDIUM MediumArray,

IN UINT MediumArraySize,

IN NDIS_HANDLE NdisProtocolHandle,

IN NDIS_HANDLE ProtocolBindingContext,

IN PNDIS_STRING AdapterName,

IN UINT OpenOptions,

IN PSTRING AddressingInformation

);

协议驱动程序向用于为绑定存储状态信息的适配器相关环境区域传递ProtocolBindingContext句柄，在接下来从属于绑定的调用中，NDIS将向协议驱动程序返回该句柄，例如，对ProtocolReceive或ProtocolStatus函数的调用。NDIS返回NdisProtocolHandle句柄给协议驱动程序，该句柄必须被协议所保存，通常放在该适配器相关环境区域，在未来与该绑定相关的调用中，协议驱动程序将向NDIS传送该句柄。

协议驱动程序以AdapterName形式向NdisOpenAdapter传送从注册表读取的或者通过DeviceName输入到ProtocolOpenAdapter函数中的适配器名。在MediumArray中传送协议驱动程序支持的介质类型。如果对ProtocolOpenAdapter函数的调用成功，低层NIC驱动程序将从MediumArray中选择一种介质并用SelectedMediumIndex参数返回其索引。成功调用NdisRegisterProtocol返回的值以NdisProtocolHandle形式返回给协议驱动程序。

如果协议驱动程序没能成功绑定到低层适配器，将释放以前为该适配器分配的任何资源。如果协议驱动程序不能打开可能存在的适配器中的任一个，应该释放协议以前分配的所有全局资源并返回恰当的错误状态，在这种情况下，协议驱动程序将被卸载。典型地，DriverEntry或者ProtocolBindAdapter函数应该用恰当的描述信息，记录失败的绑定操作，这可以通过NdisWriteErrorLogEntry或者OS专门支持的例程（像IoWriteErrorLogEntry）来实现。

2.1.3 协议驱动程序查询和设置操作

当协议驱动程序确定打开哪一个适配器并已成功地绑定到一个或多个适配器上之后，就可以调用Ndis(Co)Request查询低层NDIS驱动程序特征信息，也可以设置协议自身的操作特征。协议驱动程序也可协商绑定的某些参数。

驱动程序可以进行通用的OID_GEN_(CO_)XXX类型的独立于低层介质的OID请求。驱动程序也能够进行为介质相关类型定义的介质相关的OID请求。在线DDK的“Network Drivers Reference”有关于这两种类型的OID的详细描述。有一些OID是强制要求的，低层驱动程序必须支持它们，而其他的则是可选的。

设置和查询请求的处理

面向无连接协议调用NdisRequest来查询或设置低层微端口的特征信息。

面向无连接协议驱动程序使用通用的OID_GEN_MAXIMUM_FRAME_SIZE，查询用字节表示的低层NIC驱动程序支持的最大帧尺寸，返回的尺寸大小不包括包头部分。

面向无连接协议驱动程序使用通用的OID_GEN_MAXIMUM_TOTAL_SIZE，查询绑定确定的低层NIC驱动程序管理的NIC所能提供的最大数据包尺寸，协议驱动程序必须限制发送包大小以满足这一尺寸要求。如果协议驱动程序向低层NIC驱动程序发送一个超出其所能支持的尺寸的数据包，将会产生错误。

面向无连接协议驱动程序应该使用OID_GEN_MAXIMUM_SEND_PACKETS，确定低层NIC驱动程序每次调用NdisSendPackets所能接受的发送包的数量。关于该请求怎样影响协议驱动程序的下一次发送操作，请参阅2.5节关于这方面的讨论。

面向无连接协议驱动程序可以查询或设置低层驱动程序提供的前视缓冲区尺寸（协议驱动程序的ProtocolReceive函数被调用时），使用的OID是OID_GEN_CURRENT_LOOKAHEAD。如果协议驱动程序将其作为查询请求提交，NDIS将返回为低层NIC驱动程序的给定绑定分配的最新前视缓冲区尺寸。如果协议驱动程序用该OID产生设置请求，将设定所指定的前视缓冲区，但协议驱动程序并不保证低层驱动程序能够与该请求相一致。如果对于先前的OID_GEN_MAXIMUM_SEND_PACKETS查询低层驱动程序返回大于‘1’的值，那么该驱动程序将总是用包含完整网络数据包的前视缓冲区指示接收数据包。

面向无连接协议驱动程序可以用OID_GEN_LINK_SPEED来向低层NIC驱动程序查询链接速率，并用返回值设定其维护的内部超时值设置。如果协议驱动程序绑定到NDISWAN，那么直到从WAN NIC驱动程序接收到一个指示本地节点和远程节点连结建立的连结指示，才能确定链接速率。

面向无连接协议驱动程序也必须通过OID_GEN_MAC_OPTIONS查询，确定低层NIC驱动程序的操作特性，该查询返回低层驱动程是否支持双工操作等方面的信息。关于该查询可以返回的NDIS定义的标识组请参阅在线DDK的“the Network Drivers Reference”。

如果必要，面向无连接协议驱动程序能够通过OID_GEN_PROTOCOL_OPTIONS产生设置请求，向NDIS通知它的有关操作特性。

2.1.3.2 面向连接协议驱动程序的查询和设置

面向连接协议驱动程序调用NdisCoRequest查询和设置低层面向连接微端口的特征信息。

面向连接协议驱动程序可以用OID_GEN_CO_LINK_SPEED来向低层NIC驱动程序查询链接速率，并用返回值设定其维护的内部超时值设置。如果协议驱动程序绑定到NDISWAN，那么直到从WAN NIC驱动程序接收到一个指示本地节点和远程节点连结建立的连结指示，才能确定链接速率。面向连接协议驱动程序也可以用OID_GEN_CO_LINK_SPEED设置低层NIC的速率。

面向连接协议驱动程序也必须通过OID_GEN_MAC_OPTIONS查询，确定低层NIC驱动程序的操作特性。关于该查询可以返回的NDIS定义的标识组，请参阅在线DDK的“the Network Drivers Reference”。

如果必要，面向连接协议驱动程序可以通过OID_GEN_PROTOCOL_OPTIONS产生设置请求，向NDIS通知它的有关操作特性。

2.1.3.3 对WAN Miniports的查询和设置

绑定到支持WAN功能NIC上的面向无连接或者面向连接的协议驱动程序也必须产生如下的设置信息请求:

n OID_WAN_PROTOCOL_TYPE通知NDISWAN驱动程序关于协议驱动程序的类型，该类型值以单字节网络层协议标识符形式给出。

n OID_WAN_HEADER_FORMAT通知NDISWAN驱动程序关于发送数据包的头格式。

2.1.3.4 介质相关查询

协议驱动程序用介质相关OID查询相关介质的当前地址，例如，面向无连接协议驱动程序能够处理OID_WAN_CURRENT_ADDRESS、OID_802_3_CURRENT_ADDRESS、OID_802_5_CURRENT_ADDRESS或者OID_FDDI_LONG_CURRENT_ADDRESS查询。面向连接协议驱动程序能够处理OID_WAN_CURRENT_ADDRESS查询或者OID_ATM_HW_CURRENT_ADDRESS查询。

2.1.4作为呼叫管理器或者面向连接客户程序进行注册

面向连接协议驱动程序必须作为一个呼叫管理器或者面向连接客户程序进行注册。关于呼叫管理器和面向连接客户程序的描述请参阅第一部分第四章。

2.1.4.1作为呼叫管理器注册

协议驱动程序通过调用NdisCmRegisterAddressFamily为呼叫管理器注册。当驱动程序调用NidsOpenAdapter建立到低层微端口的绑定之后，协议驱动程序从ProtocolBindAdapter函数中调用NdisCmRegisterAddressFamily进行注册。每当协议驱动程序的ProtocolBindAdapter函数被调用时，它都要调用NdisCmRegisterAddressFamily函数，为每一个向低层面向连接客户程序提供呼叫管理服务的NIC有效地注册地址族。

通过调用NdisCmRegisterAddressFamily函数，协议驱动程序注册其呼叫管理函数并通告为绑定的每一个客户提供的具体的信号服务。当呼叫管理器的ProtocolBindAdapter函数成功注册独立呼叫管理器并返回之后，NDIS调用绑定上的所有面向连接客户的ProtocolCoRegisterAfNotify函数（参见2.1.4.2节）。

NdisCmRegisterAddressFamily定义如下：

NDIS_STATUS

NdisCmRegisterAddressFamily(

IN NDIS_HANDLE NdisBindingHandle,

IN PCO_ADDRESS_FAMILY AddressFamily,

IN PNDIS_CLIENT_CHARACTERISTICS ClCharacteristics,

IN UINT SizeOfClCharactertistics,

);

在调用NdisCmRegisterAddressFamily之前，中间层驱动程序必须完成以下操作：

1. 对一个PNDIS_CLIENT_CHARACTERISTICS类型的结构体置零，ClCharacteristics结构体的最新版本为5.0；

2. 存储驱动程序支持的ProtocolXxx呼叫管理函数的地址。

协议驱动程序必须用NdisCmRegisterAddressFamily注册的呼叫管理函数有：

CmCreateVcHandler

设定呼叫器的ProtocolCoCreateVc函数的入口点。

CmDeleteVcHandler

设定呼叫器的ProtocolCoDeleteVc函数的入口点。

CmOpenAfHandler

设定呼叫器的ProtocolCmOpenAf函数的入口点。

CmCloseAfHandler

设定呼叫器的ProtocolCmCloseAf函数的入口点。

CmRegisterSapHandler

设定呼叫器的ProtocolCmRegisterSap函数的入口点。

CmDeRegisterSapHandler

设定呼叫器的ProtocolCmDeRegisterSap函数的入口点。

CmMakeCallHandler

设定呼叫器的ProtocolCmMakeCall函数的入口点。

CmCloseCallHandler

设定呼叫器的ProtocolCmCloseCall函数的入口点。

CmIncomingCallCompleteHandler

设定呼叫器的ProtocolCmIncomingCallComplete函数的入口点。

CmAddPartyHandler

设定呼叫器的ProtocolCmAddParty函数的入口点。

CmDropPartyHandler

设定呼叫器的ProtocolCmDropParty函数的入口点。

CmActivateVcCompleteHandler

设定呼叫器的ProtocolCmActivateVcComplete函数的入口点。

CmDeActivateVcCompleteHandler

设定呼叫器的ProtocolDeCmActivateVcComplete函数的入口点。

CmMakeCallQoSHandler

设定呼叫器的ProtocolCmMakeCallQoS函数的入口点。

CmRequestHandler

设定呼叫器的ProtocolCoRequest函数的入口点。

CmRequestCompleteHandler

设定呼叫器的ProtocolCoRequestComplete函数的入口点。

即使协议驱动程序不支持内入呼叫、外出呼叫或者“点—多点”连接，协议驱动程序也必须设置NDIS_CLEINT_CHARACTERISTICS结构中的每一个CmXxx成员，对于那些呼叫管理器不支持的面向连接函数子集，占位符ProtocolCmXxx函数只是简单地返回NDIS_STATUS_NOT_SUPPORTED。

当独立呼叫管理器成功调用NdisCmRegisterAddressFamily函数之后，NDIS将忽略CM以前在NDIS_CLEINT_CHARACTERISTICS结构（传给NdisRegisterProtocol的）的RequestHandler和RequestCompleteHandler成员中设定的每一个入口点。

2.1.4.2作为面向连接客户注册

当呼叫管理器或MCM从ProtocolBindAdapter函数中调用Ndis(M)CmRegisterAddressFamily注册地址族时，NDIS将调用绑定的所有协议驱动程序的ProtocolCoRegisterAfNotify函数。如果当协议驱动程序作为协议注册时调用了ProtocolCoRegisterAfNotify函数，那么NDIS将调用协议驱动程序的ProtocolCoRegisterAfNotify函数。

如果ProtocolCoRegisterAfNotify函数判定协议驱动程序能够使用呼叫管理器或MCM注册到地址族的服务，它将为客户的每一个ProtocolCoRegisterAfNotify函数分配环境区域，并调用NdisClOpenAddressFamily函数为中间层驱动程序注册客户支持的函数集。

NdisClOpenAddressFamily定义如下：

NDIS_STATUS

NdisClOpenAddressFamily(

IN NDIS_HANDLE NdisBindingHandle,

IN PCO_ADDRESS_FAMILY AddressFamily,

IN NDIS_HANDLE ProtocolAfContext,

IN PNDIS_CLIENT_CHARACTERISTICS ClCharacteristics,

IN UINT SizeOfClCharactertistics,

OUT PNDIS_HANDLE NdisAfHandle

);

在调用NdisClOpenAddressFamily之前，中间层驱动程序必须完成以下操作：

1. 对一个PNDIS_CLIENT_CHARACTERISTICS类型的结构体置零，ClCharacteristics结构体的最新版本为5.0；

2. 存储驱动程序支持的ProtocolXxx客户函数的地址。

该调用的返回值NdisAfHandle对中间层驱动程序是不透明的，这个句柄必须由该驱动程序保存，并在以后驱动程序协议部分调用时作为输入参数传递给NDIS，例如，用NdisClRegisterSap函数注册一个SAP的调用。

必须用NdisClOpenAddressFamily注册的协议驱动程序客户函数有：

ClCreateVcHandler

设定呼叫器的ProtocolCoCreateVc函数的入口点。

ClDeleteVcHandler

设定呼叫器的ProtocolCoDeleteVc函数的入口点。

ClRequestHandler

设定呼叫器的ProtocolCoRequest函数的入口点。

ClRequestCompleteHandler

设定呼叫器的ProtocolCoRequestComplete函数的入口点。

ClOpenAfCompleteHandler

设定呼叫器的ProtocolClOpenAfComplete函数的入口点。

ClCloseAfCompleteHandler

设定呼叫器的ProtocolClCloseAfComplete函数的入口点。

ClRegisterSapCompleteHandler

设定呼叫器的ProtocolClRegisterSapComplete函数的入口点，客户程序用该函数接收远程机器的调用。

ClDeRegisterSapCompleteHandler

设定呼叫器的ProtocolClDeRegisterSapComplete函数的入口点。

ClMakeCallCompleteHandler

设定呼叫器的ProtocolClMakeCallComplete函数的入口点，客户程序用该函数对远程机器进行输出调用。

ClModifyCallQoSCompleteHandler

设定呼叫器的ProtocolClModifyCallQoSComplete函数的入口点，客户程序用该函数对已经建立的VC服务质量进行动态修改，或者当准备建立一个内入呼叫时，用该函数和呼叫管理器协商建立QoS。

ClCloseCallCompleteHandler

设定呼叫器的ProtocolClCloseCallComplete函数的入口点。

ClAddPartyCompleteHandler

设定呼叫器的ProtocolClAddPartyComplete函数的入口点，客户程序用该函数为对远程机器的输出调用建立点对多点的VC。

ClDropPartyCompleteHandler

设定呼叫器的ProtocolClDropPartyComplete函数的入口点。

ClIncomingCallHandler

设定呼叫器的ProtocolClIncomingCall函数的入口点，客户程序用该函数接收远程机器来的调用。

ClIncomingCallQoSChangeHandler

设定呼叫器的ProtocolClIncomingCallQoSChange函数的入口点，客户程序用该函数接收远程机器来的调用，在该远程机器上，发送客户程序可以动态修改QoS。

ClIncomingCloseCallHandler

设定呼叫器的ProtocolClIncomingCloseCall函数的入口点。

ClIncomingDropPartyHandler

设定呼叫器的ProtocolClIncomingDropParty函数的入口点。

ClCallConnectedHandler

设定呼叫器的ProtocolClCallConnected函数的入口点，客户程序用该函数接收远程机器来的调用。

即使中间层驱动程序不支持内入呼叫、外出呼叫或者“点—多点”连接，当调用NdisClOpenAddressFamily时，协议驱动程序也必须为呼叫器的ProtocolCl/CoXxx函数设置NDIS_CLEINT_CHARACTERISTICS结构的每一个ClXxx参数成员，对于那些中间层驱动程序不支持的面向连接函数子集，ProtocolCl/CoXxx函数仅仅返回NDIS_STATUS_NOT_SUPPORTED。

2.2 协议驱动程序数据包管理

协议驱动程序从客户程序接收一个或多个网上传送数据的缓冲区。协议驱动程序必须分配并初始化客户数据缓冲区链接的包描述符。包描述符必须像接下来描述的这样从包池中分配：

1. 在驱动程序初始化或者每个绑定初始建立时，调用NdisAllocatePacketPool或NdisAllocatePacketPoolEx为固定大小的包描述符（呼叫器其指定数量）分配并初始化一组非可分页池；

2. 调用NdisAllocatePacket从NdisAllocatePacketPool已经分配的池中分配包描述符；

通过调用NdisChainBufferAtBack或者NdisChainBufferAtFront函数，将映射到缓冲区的缓冲区描述符链接到包描述符。如果协议驱动程序从客户程序接收到必须以几个更小缓冲区发送的数据缓冲区，协议驱动程序能够将这些数据复制到协议已经分配的缓冲区，用以前已经分配的缓冲区描述符映射这些缓冲区，然后将这些缓冲区描述符链接到协议已经分配的包描述符。这些缓冲区可以通过调用核心模式支持的例程，如NdisAllocateMemory或者NdisAllocateMemoryWithTag来分配，并可像以下描述的这样，映射到协议驱动程序已经分配的缓冲区描述符：

1. 在驱动程序初始化或者每个绑定初始建立时，调用NdisAllocateBufferPool获取用于分配缓冲区描述符的句柄；

2. 调用NdisAllocateMemory（WithTag）分配缓冲区，并将其链接到NdisAllocatePacket分配的包描述符上；

3. 调用NdisAllocateBuffer分配和设置一个缓冲区描述符，该描述符映射到由NdisAllocateMemory(WithTag)调用分配缓冲区。

调用NdisAllocateMemory(WithTag)返回的基准虚地址和长度将传递给NdisAllocateBuffer，来初始化缓冲区描述符。

满足典型传送要求的包描述符可以在驱动程序初始化和（或）绑定时根据需要分配。协议驱动程序开发者可以在初始化时分配一定数量的包描述符和链接的缓冲区描述符，以保存协议绑定到低层NIC驱动程序时出现的接收数据包，接着，ProtocolReceive应尽可能快地返回控制权给低层驱动程序，否则，紧接着的接收数据包就可能丢失。就像稍后将要讲的，面向无连接协议驱动程序要么在ProtocolReceivePackets函数中以包描述符（指定完整网络数据包）从低层NIC驱动程序接收内入数据，要么向ProtocolReceive函数指示接收数据，这样就必须将指示数据复制到缓冲区（链到协议分配的包描述符）。而面向连接协议驱动程序总是用ProtocolCoReceivePackets函数接收内入数据。

每一个面向无连接的协议驱动程序都必须提供ProtocolReceive函数。当该函数被调用时，协议驱动程序必须将所指示的前视数据复制到协议驱动程序分配的已链到预分配包描述符的缓冲区，该包描述符将传给NdisTransferData函数。如果除了指示的前视数据之外还有其他接收数据，以上调用将向低层驱动程序传递包描述符。

如果协议驱动程序只是绑定到用NdisM(Co)IndicateReceivePackets指示包数组的低层NDIS驱动程序，那么Protocol(Co)ReceivePacket函数不必为内入数据提供包描述符、缓冲区和缓冲区描述符。当协议驱动程序向Protocol(Co)ReceivePacket函数指示完整数据包时，那么可以赋予客户对内入数据包描述符描述的缓冲数据直接只读访问能力，直到该数据处理完毕并释放了其所指定的包描述符及所有的相关资源为止。当协议驱动程序获得了该数据包的所有权后就不必再将这些数据复制到预分配包，也不必再调用NdisTransferData函数获取数据包的剩余部分。

如果协议驱动程序向NdisTransferData、NdisSend(Packets)或者NdisCoSendPackets函数提供链向多个缓冲区的包描述符，并且最末的一个缓冲区的实际数据长度小于分配的缓冲区空间，那么协议驱动程序应该调用NdisAdjustBufferLength函数在缓冲区描述符中设置实际数据长度。当包描述符返回给协议驱动程序时，驱动程序应重新将缓冲区描述符指定的长度调整到完整缓冲区的大小。

重用数据包

当Ndis(Co)Send返回除NDIS_STATUS_PENDING之外的任何值或者当驱动程序的Ndis(Co)SendComplete被调用时，协议驱动程序分配的用于发送的包资源的所有权将返还给协议驱动程序。然后协议驱动程序可以随后的发送请求，重定义这些返回的包资源，或者如果其是无连接的驱动程序，可以重定义这些资源，来复制ProtocolReceive接收的数据包。

比起释放那些返还资源并在随后的发送或者数据传输操作中重新分配资源，协议驱动程序重新初始化并重用这些包描述符和缓冲区将是一种更为有效的方式。如果保存这些没被链接的缓冲区描述符和缓冲区以供将来需要时重用，而不是释放并重新分配相应的资源，那么协议驱动程序通常能够展现更好的性能。

协议驱动程序通过调用NdisReinitializePacket函数重新初始化包描述符。首先，协议驱动程序应该确定：已经通过调用NdisUnchainBufferAtXxx释放缓冲描述符及其映射的缓冲区的方式，删除了所有的链接缓冲区及它们的缓冲描述符，换句话说，NdisReinitializePacket将设置所有指向链接缓冲区的成员为NULL，所以如果没有预先释放存储链接缓冲区，那么重新初始化包描述符将导致内存泄漏。同样地，如果协议驱动程序和低层驱动程序使用带外信息，那么与每一个包描述符相关的OOB数据块所指定的资源在调用NdisReinitializePacket函数之前也必须被收回。

2.3 协议驱动程序的动态绑定

支持即插即用功能的协议驱动程序通过提供ProtocolBindAdapter函数和ProtocolUnbindAdapter函数就可以支持对低层NIC的动态绑定。

如果驱动程序向NDIS注册了这些函数，那么将可以延迟打开和绑定低层NIC而不必在DriverEntry函数中实现该功能，只要用ProtocolBindAdapter函数就可代替执行该操作。如果协议驱动程序提供了这些函数，那么只要低层NIC可用，NDIS将调用能够将自己绑定到该适配器的任何协议驱动程序的ProtocolBindAdapter函数。并且只要低层NIC被关闭，那么NDIS将可以调用互逆的ProtocolUnbindAdapter函数。

ProtocolBindAdapter函数定义如下

VOID

ProtocolBindAdapter(

OUT PNDIS_STATUS Status,

IN NDIS_HANDLE BindContext,

IN PNDIS_STRING DeviceName,

IN PVOID SystemSpecific1,

IN PVOID SystemSpecific2

);

NDIS通过DeviceName向ProtocolBindAdapter函数提供最近可用的适配器名。NDIS在BindContext中为绑定请求传递代表其环境的句柄。协议驱动程序必须保存该句柄并在驱动程序完成绑定相关的操作，并准备传输和接收数据包时将该句柄作为NdisCompleteBindAdapter的参数传递给NDIS。

绑定时的操作包括为绑定分配适配器相关的环境空间并初始化，以DeviceName为参数调用NdisOpenAdapter打开适配器。DeviceName可以指低层NIC，或者也可以是介于协议驱动程序和NIC驱动程序之间，由NDIS中间层驱动程序导出的管理控制传输请求的适配器的虚拟NIC。例如，NDIS中间层驱动程序在旧版本协议驱动程序支持的介质格式和低层NIC驱动程序支持的介质格式之间进行转换。BindContext值应该存储在协议驱动程序分配的环境区域或者其他类似的位置。

因为NdisOpenAdapter调用可能返回NDIS_STATUS_PEDDING，所以BindContext值必须保存。如果NdisOpenAdapter调用返回NDIS_STATUS_PEDDING，那么直到适配器打开操作完成并且协议驱动程序的ProtocolOpenAdapterComplete函数已被调用之后，协议驱动程序才能调用NdisCompleteBindAdapter函数。在这种情况下，BindContext必须从某个已知位置获取并传给NdisCompleteBindAdapter函数。

SystemSpecific1是一个必须传给NdisOpenProtocolConfiguration函数的字符串。该字符串对协议驱动程序是不透明的。

SystemSpecific2预留系统使用。

关于动态绑定的更多信息请参阅2.8节。

2.4 协议驱动程序接收数据

这一节将讨论面向无连接以及面向连接的协议驱动程序是如何接收数据的。

2.4.1下边界面向无连接的中间层驱动程序接收数据

低层面向无连接的NIC驱动程序可以下面两种方式指示数据包:

n NIC驱动程序调用NdisMIndicateReceivePacket函数，传递指向完整数据的包描述符数组指针并且向高层驱动程序（在稍后能够处理数据并能返还包资源）转让这些数据包资源的所有权。

n NIC驱动程序调用过滤相关的NdisMXxxIndicateReceive函数，传递前视缓冲区指针、该缓冲区尺寸及其接收数据包的总尺寸值。

每一个面向无连接的协议驱动程序必须至少提供这两个接收句柄中的一个，同时也可能提供如下的两个函数：

1. ProtocolReceive函数是必须提供的。该函数通过前视缓冲区指针调用。如果ProtocolReceive函数检查前视数据并认为该数据包是客户程序所需要的，那么必须将该数据复制到协议驱动程序分配的可能链到包描述符的缓冲区。如果前视缓冲区大小小于接收数据包的总尺寸，那么ProtocolReceive函数必须以该包描述符为参数调用NdisTransferData函数，获取低层驱动程序复制到协议驱动程序提供的缓冲区中的接收数据包的其余部分。

2. ProtocolReceivePacket函数是可选的，接收描述完整网络数据包的包描述符指针。ProtocolReceive函数也检查该数据包，并确定该数据包是否是客户程序所需要的。如果该数据包客户程序所需要的，那么协议驱动程序能够以从ProtocolReceivePacket函数返回一个非零值（协议驱动程序向其转发接收指示的客户数量）的方式，赋予客户程序对被指示包资源的所有权，包括对缓冲区网络数据的直接只读访问能力。当然，以后协议驱动程序的客户必须向低层驱动程序返还包描述符及其所指定的资源。对于该接收指示，除非所有客户程序的调用从ProtocolReceivePacket函数返回的值总计非零，否则每个客户都必须返回该包描述符。

当协议驱动程序的客户程序调用返回包描述符达到要求的次数之后，将向最初指示接收数据包的低层NIC驱动程序转让包资源的所有权。

另一方面，如果协议驱动程序从NdisReturnPackets函数中返回零值，那么将立即释放数据包及相关资源。例如，如果对于那个数据包，连接已关闭或者处于其他的某种不可用方式，或者如果在向客户指示之前已将指示数据复制到自己的缓冲区，并在内部处理了数据，那么立即释放该接收数据包的情况将出现。

2.4.1.1 在中间层驱动程序中实现ProtocolReceivePacket处理程序

当低层面向无连接的NIC驱动程序通过调用NdisMIndicateReceivePacket函数指示包描述符数组时，NDIS通常将以每一个包描述符为参数调用绑定的协议驱动程序的ProtocolReceivePacket函数，从而让协议驱动程序（或者其客户）保存包描述符及其所有的资源，直到协议驱动程序或者其客户处理完数据并返回包描述符。典型的以包数组为参数调用NdisMIndicateReceivePacket函数的两类面向无连接NIC驱动程序如下：

1. 管理能够一次接收多个网络数据包到缓冲环的DMA总线控制适配器的NIC驱动程序；

2. 在与包描述符相关的NDIS_PACKET_OOB_DATA块中提供包含介质西相关信息的带外数据，像包优先级等的NIC驱动程序。

当然，一个驱动程序并不要求必须是DMA总线控制设备驱动程序。

如果协议驱动程序意识到被绑定（或者可能被绑定）到上面所说的NIC驱动程序上，那么它就应该提供ProtocolReceivePacket函数。这使得协议驱动程序可以完成接下的所有操作：

n 从低层NIC驱动程序接收完整数据包的指示；

n 用NDIS提供的宏，读取与包描述符相关的OOB数据;

n 保持对内入数据包描述符的所有权和对这些包描述符指定的缓冲数据的直接读访问能力，或者甚至在选择了对客户重要的包数据的范围之后向客户转发所指示的包描述符；然后，在对每一个包描述符的处理中，可能要为客户程序制作该数据的多个副本；

n 如果协议驱动程序保存着内入数据包描述符的所有权，那么当协议驱动程序处理完指示数据之后，通过NdisReturnPackets函数返回包描述符及所描述资源，另外可能还有其他被保存的包描述符。换句话说，即要客户（协议驱动程序向其转发接收指示）返回包描述符。

即使协议驱动程序提供了ProtocolReceivePacket处理程序，NIC驱动程序对NdisMIndicateReceivePacket函数的调用也可能导致对协议驱动程序ProtocolReceivePacket函数的调用。因为NIC驱动程序调用NdisMIndicateReceivePacket函数时暂时释放了驱动程序分配资源的所有权，所以低层驱动程序将依赖于这些数据包的使用者及时返还该部分资源。换句话说，NIC驱动程序能够撇开接收资源运行，像NIC中的接收缓冲空间等。NIC驱动程序通过向与传给NdisMIndicateReceivePacket的包数组中的包描述符相关的OOB数据块写入NDIS_STATUS_RESOURCES状态标识，来完成该项功能，该状态指示的数据包将导致NDIS以该包及数组中相继的其他包为参数，调用高层驱动程序的ProtocolReceive函数，这将强制协议驱动程序立即复制包数据而不是保持NIC驱动程序分配的这些包资源。

如果协议驱动程序想要通过调用ProtocolReceive函数来获取与包描述符关联的OOB数据，那么它必须调用NdisGetReceivePacket及NDIS_GET_ORIGINAL_PACKET复制介质相关信息到中间层驱动程序分配的缓冲区，另外，如果低层NIC驱动程序提供了时间戳，那么还必须复制TimeSent和TimeReceived信息。

2.4.1.2 在协议驱动程序中实现ProtocolReceive处理程序

如果低层面向无连接的NIC驱动程序调用了与过滤相关的NdisMXxxIndicateReceive函数，那么驱动程序将总要调用每一个绑定协议驱动程序的ProtocolReceive函数。

如果协议驱动程序的一个或者多个客户成为数据包的传送目标，而其前视缓冲区尺寸小于该包总尺寸，那么ProtocolReceive就必须完成以下操作：

1. 复制前视缓冲区数据到协议驱动程序分配的包描述符映射的内部缓冲区；

2. 将映射到足够容纳网络包数据的其余部分的缓冲区（由协议驱动程序分配的）的缓冲区描述符链接到协议驱动程序分配的包描述符；

3. 以包描述符为参数调用NdisTransferData函数，使低层驱动程序复制接收数据包的其余部分到协议驱动程序缓冲区。

当NdisTransferData函数返回STATUS_SUCCESS或者ProtocolTransferDataComplete函数被调用时，前视缓冲区可以被链接包含该传输数据的包描述符，并指示给任何对其感兴趣的客户。

对每一个接收指示，NdisTransferData仅能被调用一次。协议驱动程序应创建包含链式缓冲区的包描述符，该缓冲区应能够容纳完整网络数据包。当NdisTransferData返回之后，从低层NIC驱动程序接收来的数据将不再有用。

传给ProtocolReceive函数的前视缓冲区长度是用OID_GEN_CURRENT_LOOKAHEAD调用NdisRequest的返回尺寸和数据包总长度中较小的一个。对于ProtocolReceive函数来说，所有的前视缓冲区中的数据都是只读的。

如果前视缓冲区长度大于或者等于与过滤相关的NdisMXxxIndicateReceive函数指示的数据包总尺寸，那么ProtocolReceive应该调用NdisTransferData函数将前视缓冲数据复制到内部缓冲区。

如果对ProtocolReceive函数的调用是由于在调用NdisMIndicateReceivePacket之前，低层NIC驱动程序设置了包数组中的一个或多个包状态为NDIS_STATUS_RESOURCES，那么前视缓冲区的尺寸将总是等于整个网络数据包的大小。在这种情况下，由于ProtocolReceive可以立即复制完整指示到内部缓冲区，所以协议驱动程序不会再调用NdisTransferData函数。

如果在响应OID_GEN_MAC_OPTIONS请求时，NDIS_MAC_OPTIONS_COPY_LOOKAHEAD_DATA被置位，那么协议驱动程序可以使用任何方法将前视数据复制到内部缓冲区，如NdisMoveMemory等。如果以上标识没有被置位，那么协议驱动程序就必须调用NdisCopyLookaheadData函数来复制指示数据，否则复制操作的结果将是不确定的。

ProtocolReceive函数必须尽可能快地执行。协议驱动程序必须确保在接收内入指示之前有可用的由其分配的包资源。在协议驱动程序检查数据包确定其不是要复制的数据包之后，ProtocolReceive函数应立即返回NDIS_STATUS_NOT_ACCEPT标识。

在复制接收包时，ProtocolReceive函数不能处理接收数据，因为这将严重影响系统性能以及低层NIC接收导入数据包的能力。作为替代，中间层驱动程序在以后的ProtocolReceiveComplete函数中对接收数据包进行处理，该函数在随后能够进行数据包后期处理时被调用。典型地，当低层NIC驱动程序已经接收并指示了微端口确定的所有数据包时，或在其退出接收处理程序之前，以上操作将发生。协议驱动程序必须对ProtocolReceive接收的数据包进行排队，以使ProtocolReceiveComplete函数能够过它们进行后期处理。

2.4.1.3 从面向无连接协议驱动程序中访问OOB数据信息

当接收到的网络数据包指示给ProtocolReceive函数时，将强制驱动程序复制接收数据到协议驱动程序提供的缓冲区。如果该指示在包描述符相关的数据包OOB数据中包含介质相关信息和（或）时间戳信息，那么协议驱动程序可以调用NdisGetReceivePacket 和NDIS_GET_ORIGINAL_PACKET获取介质相关信息以及TimeSent和TimeReceived时间戳。

如果接收数据包传给ProtocolReceivePacket函数，那么协议驱动程序可以以如下的方式使用NDIS提供的宏，保存相关的OOB数据信息：

n 用NDIS_GET_MEDIA_SPECIFIC_INFO 复制介质相关信息;

n 用NDIS_GET_TIME_SENT复制TimeSent信息;

n 用NDIS_GET_TIME_RECEIVED复制TimeReceived信息。

TimeSent时间戳是远程节点NIC发送数据包的时间，并且如果可能的话，它将被本地节点的低层NIC驱动程序获取并保存。TimeReceived时间戳是内入数据包被本地节点的低层NIC驱动程序接收的时间。

如果协议驱动程序将以上时间戳转换为另一种格式，那么当前系统时间可以通过NdisGetCurrentSystemTime或者KeQuerySystemTime函数确定。

2.4.2 面向连接协议驱动程序接收数据

低层面向连接的NIC驱动程序通过调用NdisMIndicateReceivePacket指示数据包，传递参数为指向完整数据包的包描述符指针数组的指针，向在稍后能够处理数据并返还包资源的上层驱动程序转让这些数据包资源的所有权。

面向连接的协议驱动程序通过调用ProtocolCoReceivePacket函数接收指示数据包。该函数接收指定缓冲的完整网络数据包的包描述符指针。

ProtocolCoReceivePacket函数也检查该数据包，并确定该数据包是否是客户程序所需要的。如果该数据包是客户程序所需要的，那么协议驱动程序能够以从ProtocolCoReceivePacket函数返回一个非零值（协议驱动程序向其转发接收指示的客户数量）的方式赋予客户程序对被指示包资源的所有权，包括对缓冲区网络数据的直接只读访问能力。当然，以后协议驱动程序的客户必须向低层驱动程序返还包描述符及其所指定的资源。对于该接收指示，除非所有客户程序的调用从ProtocolCoReceivePacket函数返回的值总计非零，否则每个客户都必须返回该包描述符。

当协议驱动程序的客户程序调用返回包描述符达到要求的次数之后，将向最初指示接收数据包的低层NIC驱动程序转让包资源的所有权。

另一方面，如果协议驱动程序从ProtocolCoReceivePacket函数中返回零值，那么将立即释放数据包及相关资源。例如，如果对于那个数据包，连接已关闭或者处于其他的某种不可用方式，或者如果在向客户指示之前已将指示数据复制到自己的缓冲区并在内部处理了数据，那么将立即释放该接收数据包。

2.4.2.1 ProtocolCoReceivePacket处理程序实现

当低层面向连接的NIC驱动程序通过调用NdisMCoIndicateReceivePacket函数指示包数组时，NDIS以每一个包描述符为参数，调用绑定的协议驱动程序的ProtocolCoReceivePacket函数。为了获得相关包的OOB 数据块状态，ProtocolCoReceivePacket必须对每一个包描述符调用一次NDIS_GET_PACKET_STATUS宏。

如果NIC驱动程序在调用NdisMCoIndicateReceivePacket之前，将数据包描述符相关的OOB 数据块状态设为NDIS_ STATUS_SUCCESS，NIC驱动程序将暂时释放该包描述符相关的资源的所有权。在这种情况下，NIC驱动程序将依赖于这些包的使用者及时返还该部分资源。换句话说，NIC驱动程序能够撇开接收资源运行，像NIC中的接收缓冲空间等。

当撇开这些资源运行时，NIC驱动程序设置包描述符相关的OOB数据块状态为NDIS_STATUS_RESOURCES，该状态指示的数据包将强制协议驱动程序的ProtocolCoReceivePacket函数立即复制包数据而不是保持NIC驱动程序分配的包资源。在这种情况下，ProtocolCoReceivePacket必须返回零值。

如果低层NIC驱动程序没有将OOB数据块（与传给NdisMCoIndicateReceivePacket的包数组中的包描述符相关）置为NDIS_STATUS_RESOURCES，那么将允许协议驱动程序保持该包描述符及其指定的资源，直到协议驱动程序或者其客户程序处理完数据并返回包描述符为止。

2.4.2.2 从面向连接协议驱动程序中访问OOB数据信息

当接收到的网络数据包指示给ProtocolReceive函数时，将强制驱动程序复制接收数据到协议驱动程序提供的缓冲区。如果该指示在包描述符相关的数据包OOB数据中包含介质相关信息和（或）时间戳信息，那么协议驱动程序可以调用NdisGetReceivePacket 和NDIS_GET_ORIGINAL_PACKET获取介质相关信息以及TimeSent和TimeReceived时间戳。

面向连接协议驱动程序可以以如下的方式使用NDIS提供的宏，复制指示接收包相关的OOB数据块信息：

n 用NDIS_GET_MEDIA_SPECIFIC_INFO 复制介质相关信息;

n 用NDIS_GET_TIME_SENT复制TimeSent信息;

n 用NDIS_GET_TIME_RECEIVED复制TimeReceived信息。

TimeSent时间戳是远程节点NIC发送数据包的时间，并且如果可能的话，它将被本地节点的低层NIC驱动程序获取并保存。TimeReceived时间戳是内入数据包被本地节点的低层NIC驱动程序接收的时间。

如果协议驱动程序将以上时间戳转换为另一种格式，那么当前系统时间可以通过NdisGetCurrentSystemTime函数确定。

面向连接微端口能够以NIC本地时间指示数据包TimeReceived时间戳。为了查询面向连接微端口或者MCM的本地定时能力，协议驱动程序通过OID_GEN_CO_TIME_CAPS调用NdisCoRequest函数。为了获得NIC的本地时间，协议驱动程序通过OID_GEN_CO_ GET_NETCARD_TIME调用NdisCoRequest函数。

2.5 发送协议驱动程序创建的数据包

这一节来讨论从面向无连接协议驱动程序和面向连接驱动程序如何发送数据包。

2.5.1 从面向无连接协议驱动程序发送数据包

面向无连接协议驱动程序可以通过传递包描述符指针来调用NdisSend函数传输单个数据包，该包描述符指向链式缓冲区描述符（映射要发送缓冲数据）。作为一种替代方案，面向无连接协议驱动程序也可以通过传递指向包描述符指针数组的指针来调用NdisSendPackets函数传输多个数据包。

通常，面向无连接协议驱动程序开发者应根据驱动程序的需求和低层NIC驱动程序的特征，选择是调用NdisSend还是NdisSendPackets函数进行数据包传输。

当绑定到低层NIC驱动程序时，面向无连接协议驱动程序应通过OID_GEN_MAXIMUM_SEND_PACKETS调用NdisRequest，获取低层驱动程序能够接受的发送包数组的最大包数量。如果NIC驱动程序仅支持单个包的发送，不管是通过NdisSend函数还是NdisSendPackets函数，那么以上调用的返回值将是‘1’或者NDIS_STATUS_NOT_SUPPORTED，这两个返回之中的任一个都可能表示协议驱动程序将要调用的是很可能是NdisSend而不是NdisSendPackets函数。如果低层驱动程序返回大于‘1’的值，那么在发送包数组时使用NdisSendPackets函数会使两种驱动程序的性能都变得更好。如果OOB数据在协议驱动程序和NIC驱动程序之间传递，那么该两个发送函数都可以被调用，因为在任何一种情况下，低层驱动程序都能使用NDIS提供的宏读取相关的OOB数据。

当协议驱动程序调用NdisSend函数时，将以同步或异步方式释放给定包资源的所有权直到发送操作完成为止。如果NdisSend返回状态不是NDIS_STATUS_PENDING，则调用以同步方式完成，并且将包资源的所有权返还协议驱动程序。如果NdisSend返回状态是NDIS_STATUS_PENDING，则当调用完成时，发送操作的最终状态和协议驱动程序分配的包描述符将传给NdisSendComplete函数。

当协议驱动程序通过调用NdisSendPackets发送一个或多个数据包时，发送操作总是异步的。协议驱动程序释放其分配的包资源的所有权，直到每一个包描述符和对该包发送操作的最终状态都返回ProtocolCoSendComplete为止。

作为一个结论，在NdisSend(Packets)函数调用返回时，协议驱动程序不能读取包描述符相关的OOB数据块的Status成员。协议驱动程序不能够以这种方式知道发送请求的状态，因为该成员被NDIS用于跟踪转换中的发送请求的进程，在包描述符返回给ProtocolSendComplete函数之前它是易变的。协议驱动程序要么通过检查NdisSend函数的返回值，要么通过传给Protocol(Co)SendComplete函数的Status参数来获取传送请求的状态信息。

如果在发送之前，中间层驱动程序通过重组从客户程序接收的数据包，请求不同优先级的包数组传送，那么应将最高优先级的数据包放在数组的开始位置。NDIS将保持这些包数组（将被传递给NdisSendPackets函数）中的数据包的顺序，即使在内部要对一些数据包进行排队。

NDIS不能企图对与传给NdisSendPackets函数（或者NdisSend函数）的包描述符相关的任何OOB数据块进行排队或检查。除非协议驱动程序对NIC驱动程序处理包优先级或TimeToSend时间戳的方式有特别的了解，否则，应假定NIC驱动程序按接收到的顺序发送数据包，保持接收时的顺序。因此，协议驱动程序应该根据数据包将在网络上传输的顺序，对将要发送的包数组进行排序。

2.5.1.1面向无连接协议驱动程序传递介质相关信息

在发送数据包之前，协议驱动程序能够调用NdisSetPacketsFlags函数在包描述符的NDIS私有部分设置协议规定的标识。该标识并不是NDIS定义的，而是协议和低层NIC驱动程序合作定义的。NDIS_PACKET的私有（Private）成员的结构对所有的NDIS驱动程序都是可访问的，允许使用NDIS提供的宏或函数对其进行读访问，在某些情况下，也可进行写访问。

协议驱动程序可以通过OOB数据块（与每一个NDIS_PACKET类型描述符相关的）传送更多的介质相关信息。以下是OOB数据块的定义：

typedef struct _ NDIS_PACKET_OOB_DATA{

union{

ULONGLONG TimeToSend;

ULONGLONG TimeSend;

};

ULONGLONG TimeReceived;

UINT HeaderSize;

UINT SizeMediaSpecificInfo;

PVOID MediaSpecificInformation;

NDIS_STATUS Status;

} NDIS_PACKET_OOB_DATA, * PNDIS_PACKET_OOB_DATA;

驱动程序缓冲区中的单个记录结构MediaSpecificInformation定义如下：

typedef struct MediaSpecificInformation{

UINT NextEntryOffset;

NDIS_CLASS_ID ClassId;

UINT Size;

UCHAR ClassInformation[1];

}MEDIA_SPECIFIC_INFORMATION;

ClassId成员是NDIS定义的枚举变量（ClassInformation[1]中发现的信息类型）。目前，支持win32的微软操作系统中为无连接介质提供了三个Class Id：NdisClass802_3Priority、NdisClassWirelessWanMbxMailbox和NdisClassIrdaPacketInfo。关于更详细的信息请参阅在线“the Network Drivers Reference”。

如果协议驱动程序知道要发送数据包的低层NIC驱动程序使用OOB数据，就能够设定相应的OOB结构成员：

n 通过使用NDIS_SET_PACKET_TIME_TO_SEND宏设置TimeToSend成员，请求数据包在特定的时间发送。该宏在系统时间单元中传递请求时间。协议驱动程序可以调用NdisGetCurrentSystemTime获取用于计算请求发送时间的系统时间；

n 使用NDIS_ PACKET_SET_ MEDIA_SPECIFIC_INFO宏设置该成员为缓冲区地址，在MediaSpecificInformation中传递协议缓冲区中的介质相关信息。例如，如果协议驱动程序绑定到要求优先级的低层NIC，那么可以设置MediaSpecificInformation结构的ClassId成员为NdisClass802_3Priority，并通过ClassInformation传递优先级相关信息以及通过Size传递该信息的字符大小值。协议驱动程序应该分配缓冲区来保存任何介质相关数据记录，也应该通过MediaSpecificInformation设置该缓冲区的指针。

2.5.2 面向连接协议驱动程序发送数据包

面向连接协议驱动程序可以通过向NdisCoSendPackets传送指向包描述符指针数组的指针的方式进行函数调用，传送一个或者多个数据包。

当协议驱动程序通过调用NdisCoSendPackets发送一个或多个数据包时，发送操作总是异步的。协议驱动程序释放其分配的包资源的所有权，直到每一个包描述符和对该报发送操作的最终状态都返回ProtocolCoSendComplete为止。

作为一个结论，在NdisCoSendPackets函数调用返回时，协议驱动程序不能读取包描述符相关的OOB数据块的Status成员。协议驱动程序不能够以这种方式知道发送请求的状态，因为该成员被NDIS用于跟踪转换中的发送请求的进程，在包描述符返回给ProtocolCoSendComplete函数之前它是易变的。协议驱动程序总是通过传给ProtocolCoSendComplete函数的Status参数来获取传送请求的状态信息。

如果在发送之前，中间层驱动程序通过重组从客户程序接收的数据包，请求不同优先级的包数组传送，那么应将最高优先级的数据包放在数组的开始位置。NDIS将保持这些将要传给NdisCoSendPackets函数的包数组中的数据包的顺序，即使在内部要对一些数据包进行排队。

NDIS不能企图对与传给NdisCoSendPackets函数（或者NdisSend函数）的包描述符相关的任何OOB数据块进行排队或检查。除非协议驱动程序对NIC驱动程序处理包优先级或TimeToSend时间戳的方式有特别的了解，否则，应假定NIC驱动程序按接收到的顺序发送数据包，保持接收时的顺序。因此，协议驱动程序应该根据数据包将在网络上传输的顺序对将要发送的包数组进行排序。

2.5.2.1面向连接协议驱动程序传递介质相关信息

在发送数据包之前，协议驱动程序能够调用NdisSetPacketsFlags函数在包描述符的NDIS私有部分设置协议规定的标识。该标识并不是NDIS定义的，而是协议和低层NIC驱动程序合作定义的。NDIS_PACKET的私有（Private）成员的结构对所有的NDIS驱动程序都是可访问的，允许使用NDIS提供的宏或函数对其进行读访问，在某些情况下，也可进行写访问。

协议驱动程序可以通过与每一个NDIS_PACKET类型描述符相关的OOB数据块，传送更多的介质相关信息。以下是OOB数据块的定义：

typedef struct _ NDIS_PACKET_OOB_DATA{

union{

ULONGLONG TimeToSend;

ULONGLONG TimeSend;

};

ULONGLONG TimeReceived;

UINT HeaderSize;

UINT SizeMediaSpecificInfo;

PVOID MediaSpecificInformation;

NDIS_STATUS Status;

} NDIS_PACKET_OOB_DATA, * PNDIS_PACKET_OOB_DATA;

驱动程序缓冲区中的单个记录结构MediaSpecificInformation定义如下：

typedef struct MediaSpecificInformation{

UINT NextEntryOffset;

NDIS_CLASS_ID ClassId;

UINT Size;

UCHAR ClassInformation[1];

}MEDIA_SPECIFIC_INFORMATION;

ClassId成员是NDIS定义的枚举变量（ClassInformation[1]中发现的信息类型）。目前，支持win32的微软操作系统中提供了一个Class Id：NdisClassAtmAALInfo。关于更详细的信息请参阅在线“the Network Drivers Reference”。

如果协议驱动程序知道要发送数据包的低层NIC驱动程序使用OOB数据，就能够设定相应的OOB结构成员：

n 通过使用NDIS_SET_PACKET_TIME_TO_SEND宏设置TimeToSend成员，请求数据包在特定的时间发送。该宏在系统时间单元中传递请求时间。协议驱动程序可以调用NdisGetCurrentSystemTime获取用于计算请求发送时间的系统时间；

n 使用NDIS_ PACKET_SET_ MEDIA_SPECIFIC_INFO宏设置该成员为缓冲区地址，在MediaSpecificInformation中传递协议缓冲区中的介质相关信息。

面向连接协议驱动程序能够用NIC本地时间调度发送任务并标记发送包时间。为了查询面向连接微端口或者MCM的本地定时能力，协议驱动程序通过OID_GEN_CO_TIME_CAPS调用NdisCoRequest函数。为了获得NIC的本地时间，协议驱动程序通过OID_GEN_CO_ GET_NETCARD_TIME调用NdisCoRequest函数。

2.6处理协议驱动程序的PnP事件和PM事件

当操作系统向代表NIC的目标设备对象发出即插即用IRP或电源管理IRP时，NDIS将截取该IRP，然后通过调用驱动程序的ProtocolPnPEvent处理程序向每一个绑定的协议驱动程序和中间层驱动程序指示该事件。在对ProtocolPnPEvent的调用中，NDIS传递描述被指示的PnP事件或PM事件的NET_PNP_EVENT结构的指针。

就像NET_PNP_EVENT结构中NetEvent节点所指示的，有六个可能的PnP和PM事件：

n NetEventSetPower

指示电源设置请求，该请求指定NIC过渡到特定电源状态。一个支持电源管理的协议驱动程序应该总是通过返回NDIS_STATUS_SUCCESS说明该事件成功处理。早期的协议驱动程序可以通过NDIS_STATUS_NOT_SUPPORT指示NDIS应该解除到NIC的绑定。

n NetEventQueryPower

指示电源查询请求，该请求查询NIC能否过渡到特定电源状态。协议驱动程序应该总是能够成功执行NetEventQueryPower。在建立一个有效连接之后，协议驱动程序可以调用PoRegisterSystemState注册一个持续忙状态，只要状态注册还有效，电源管理器就不会企图让系统进入睡眠状态。当连接不可用时，协议驱动程序可以调用PoUnregisterSystemState撤消状态注册。协议驱动程序不应通过使NetEventQueryRemoveDevice失败的方式阻止系统过渡到睡眠状态。注意NetEventQueryPower之后总是要调用NetEventSetPower。对设备当前电源状态的NetEventSetPower调用实际上相当于撤销了NetEventQueryPower。

n NetEventQueryRemoveDevice

指示一个删除设备查询请求，该请求查询NIC能否在不中断操作的情况下删除NIC。如果中间层驱动程序不能释放设备（例如，因为设备正在使用），那么必须以返回NDIS_STATUS_FAILURE的方式使NetEventQueryRemoveDevice操作失败。

n NetEventCancelRemoveDevice

指示撤销删除设备请求，该请求撤销NIC删除操作。协议驱动程序应该总是通过返回NDIS_STATUS_SUCCESS说明该事件成功处理。

n NetEventReconfigure

指示网络部件的配置已经改变。例如，如果用户改变了TCP/IP的IP地址，NDIS用NetEventReconfigure向TCP/IP协议指示该事件。协议驱动程序应该总是通过返回NDIS_STATUS_SUCCESS说明成功完成该事件。

n NetEventBindList

向客户指示绑定列表已经改变。绑定列表是传输协议导出的客户程序可绑定的一个或多个设备的列表。

n NetEventBindComplete

指示协议驱动程序已经绑定到所有可以绑定的NIC上。除非有即插即用NIC装入系统，否则NDIS不会向协议驱动程序指示更多的NIC。

NET_PNP_EVENT的Buffer成员指向包含被指示事件特定信息的缓冲区。更多的信息请参看NET_PNP_EVENT。

协议驱动程序能够以NdisCompletePnPEvent异步地完成ProtocolPnPEvent调用。

2.7协议驱动程序复位操作

协议驱动程序可以通过调用NdisReset的方式启动复位操作。如果复位请求返回NDIS_STATUS_PENDING，那么当复位操作完成时，ProtocolResetComplete函数将被调用。

除非协议驱动程序知道低层NIC不能正常运行，否则很少调用NdisReset。例如，如果协议驱动程序发现对于相当数量的发送或请求没有收到完成调用，而且如果有足够的关于低层NIC的知识断定有硬件问题发生，那么就可以调用NdisReset启动复位操作。然而，通常情况下，NIC复位要求的检测和启动是通过NDIS和NIC驱动程序使用超时逻辑来完成的。任何绑定到以NdisMediumWan方式报告介质类型的低层NDIS驱动程序的协议驱动程序，都不能调用NdisReset函数。

典型地，低层驱动程序复位NIC是因为发送或请求操作中NIC超时，这导致NDIS调用NIC驱动程序的MiniportCheckForHang函数，并随后调用其MiniportReset函数，或者因为NIC驱动程序确定NIC接收能力异常。如果NDIS启动了复位操作并且NdisReset函数返回NDIS_STATUS_PENDING，那么首先NDIS将以NDIS_STATUS_RESET_START状态调用每一个绑定协议驱动程序的Protocol(Co)Status函数，然后调用相同绑定驱动程序的ProtocolStatusComplete函数。当NIC驱动程序调用ProtocolResetComplete函数时， NDIS将以NDIS_STATUS_RESET_END状态再一次调用Protocol(Co)Status函数并接着调用ProtocolStatusComplete函数。

协议驱动程序必须处理未完成的发送任务（在对低层NIC的绑定上）因为NIC复位而被撤销的情况。如果绑定的协议驱动程序有任何未完成的发送数据包，NDIS将向协议驱动程序返回这些包的准确状态并结束这些数据包。当复位操作完成时，假设NIC恢复可用状态，那么协议驱动程序必须重新发送这些数据包。

当中间层驱动程序收到NDIS_STATUS_RESET_START状态时，应进行以下操作：

n 挂起发送就绪的数据包直到Protocol(Co)Status收到NDIS_STATUS_RESET_END通知;

n 不做任何对低层微端口操作的NDIS调用，除了那些返回资源的调用，像NdisReturnPackets返回数据包调用。

在Protocol(Co)Status收到NDIS_STATUS_RESET_END并且ProtocolStatusComplete函数已被调用之后，协议驱动程序可以继续发送数据包和处理请求。

2.8协议驱动程序拆除绑定操作

协议驱动程序可以通过调用NdisCloseAdapter函数拆除对低层NIC驱动程序的绑定。如果协议驱动程序注册了NdisUnbindAdapter函数，那么当低层NIC不再可用时，NDIS将调用该函数。例如，如果低层NIC超时，复位NIC的尝试不能使其正常工作，并且NDIS为故障NIC调用了NIC驱动程序的MiniportHalt函数，那么接下来NDIS将调用上层协议驱动程序的NdisUnbindAdapter函数。

如果协议驱动程序没有注册该函数，作为在GeneralStatus中接收到NDIS_STATUS_CLOSING状态值的结果，拆除绑定操作也可由驱动程序的Protocol(Co)Status函数产生。

中间层驱动程序也能发起解除绑定的操作。例如，当在ProtocolStatus中接收到的GeneralStatus状态值为NDIS_STATUS_CLOSING时。如果初始化期间某个操作并发地使用适配器，例如分配所要求的资源失败，那么协议驱动程序应该拆除绑定，因为该绑定上已不能执行网络操作。

如果协议驱动程序注册了NdisUnbindAdapter函数，那么该函数将要调用NdisCloseAdapter函数。无论如何，如果协议驱动程序调用了NdisCompleteUnbindAdapter函数，那么必须释放所有与绑定相关的资源。如果协议驱动程序没有提供NdisUnbindAdapter函数，但调用了NdisCloseAdapter函数，那么当NdisCloseAdapter函数返回NDIS_STATUS_SUCCESS或者如果NdisCloseAdapter函数返回了NDIS_STATUS_PENDING ，当调用NdisCloseAdapterComplete函数时，可以释放以前分配的资源。

NDIS推迟调用NdisCloseAdapterComplete函数，直到绑定上的所有未处理请求都完成为止。一旦关闭请求完成，协议驱动程序分配的代表绑定的ProtocolBindingContext句柄将无效。

在同步地从NdisUnbindAdapter返回之前或者用NdisCompleteUnbindAdapter异步地完成解除绑定操作之前，中间层驱动程序必须完成以下操作：

n 清除其为绑定保存的任何状态值；

n 释放其分配用于建立绑定的所有资源；

n 调用NdisCloseAdapter。

2.9协议驱动程序状态指示

面向无连接协议驱动程序被要求提供ProtocolStatus和ProtocolStatusComplete函数。面向连接协议驱动程序被要求提供ProtocolCoStatus和ProtocolCoStatusComplete函数。

当低层面向无连接的NDIS驱动程序调用NdisMIndicateStatus报告NIC上的改变时，NDIS将调用ProtocolStatus函数。当低层面向连接的NDIS驱动程序调用NdisMCoIndicateStatus时，NDIS将调用ProtocolCoStatus函数。

当状态改变开始的时候，Protocol(Co)Status被调用。如果当ProtocolStatus函数或者面向无连接协议被调用时，状态信息指示的操作没有完成，在这种情况下低层面向无连接NIC驱动程序紧接着将调用NdisMIndicateStatusComplete函数，那么调用ProtocolStatusComplete执行状态改变所指示的任何延期操作。

被送给ProtocolStatus 的GeneralStatus实例包括：

n NDIS_STATUS_CLOSING

在2.8节已经讨论了该状态和Protocol(Co)Status的操作;

n NDIS_STATUS_RESET_START和NDIS_STATUS_RESET_END

就像2.7节中所说的,这两个状态都将送给Protocol(Co)Status 和ProtocolStatusComplete函数；

n NDIS_STATUS_LINE_UP

该状态表明协议驱动程序是基于已经与远程节点建立连接的支持WAN功能的NIC驱动程序的。协议驱动程序交换活动连接的句柄，在该连接上，可以进行数据发送和接收，也可查询连接获取相关呼叫信息。关于WAN驱动程序的更多信息请参阅第二部分第八章；

n NDIS_STATUS_RING_STATUS或者其他介质相关的状态值

对于该状态StatusBuffer提供了更详细的信息，例如关于令牌环介质的问题。


TDI传输器及其客户

总的来说，这一章主要描述TDI传输驱动程序和TDI核心模式客户之间的联系，如使用系统定义的TDI接口和传输协议栈通信的高层网络接口仿真器、转向器和服务器。即本章将提供对以下问题的综述：

n 传输驱动程序接口（TDI）

n 支持即插即用的TDI传输器如何使用代表传输器和NIC绑定的设备对象，以及它们如何用TDI通过这些绑定动态注册客户可用的网络地址

n TDI如何使用代表网络实体的文档对象，包括以下的三种基本TDI实体：

1. 传输地址

2. 连接端点

3. 控制信道

n TDI要求的传输驱动程序调度例程集合

n TDI客户和其低层传输器间的交互

n TDI请求和事件之间的区别

3.1 传输驱动程序接口（TDI）

图3.1展示了TDI客户和传输驱动程序之间的总体关系。

图3.1 TDI客户和传输提供者

如图中所示，传输驱动程序接口（TDI）定义了在传输协议栈上边界提供的核心模式网络接口。上述每一个栈中的最高层协议驱动程序支持为更高层核心模式网络客户提供的TDI接口。

该接口包括以下内容：

n 一组由每个传输驱动程序导出的标准核心模式中间层驱动程序的调度例程，客户通过调用支撑例程向传输驱动程序提交I/O请求（IRP），如Zw..File例程和(或)IoCallDriver；

n 一组由每个TDI客户导出的ClientEventXxx回调例程，通过向低层传输驱动程序注册这些例程来接收特定的网络事件通知；

n 一组由TDI客户导出的ClientPnPXxx回调例程，通过向TDI注册这些例程来接收有关动态绑定、网络地址和电源状态改变等来自支持PnP的Windows 2000传输器的通知；

n 与TDI传输、ClientEventXxx例程和ClientPnPXxx例程相关的参数、结构、IOCTL和程序规则；

n 一组系统提供的TdiXxx函数，通过调用这些函数，传输器和客户进行相互通信；

n 一组系统提供的TdiBuildXxx宏和函数，客户用它们生成提交给低层传输器的I/O请求。

要求所有传输驱动程序提供单一公共接口（TDI）简化了传输驱动程序开发的任务，这样所有传输器只要支持这些预定的单一接口就行了。由于减少了必须编写的传输器相关的代码量，这也简化了客户开发的任务。

Windows 2000提供了几种通用网络接口的接口模块，如Windows Sockets和NetBIOS。每个接口模块对外提供一组本地的原语函数，在用户代码中通过标准的调用就可以对其进行访问，接口模块将这些本地的原语函数及其相关参数和程序规则映射为对低层TDI传输驱动程序的一个或者多个服务的调用。

TDI的主要特征包括：

n 高粒度级别

因为在本质上有粒度的相关性，TDI可以现存的通用网络接口为标准，用几个TDI定义的请求（它们可以进行混合和匹配调节现存的网络接口函数的映射），调整所有的原语和约定。

n 异步操作

多数核心模式TDI操作是异步的，用客户提供的回调例程指示异步网络事件，异步地完成大部分客户发起的作为IRP提交的操作。

n 32位的地址和值

就像所有的Windows 2000核心模式组件一样，TDI传输器和其客户是以32位编码的。TDI定义的结构和参数使用32位指针和值。

n 灵活的寻址模式

TDI并不要求什么特定的地址格式，如以前操作系统定义的16字符的NetBIOS名称。相反其使用一种扩展的机制使得多种地址格式都能被识别和使用。

n 扩展通信

TDI定义了TDI_ACTION_IOCTL请求，通过使用它，TDI传输器可以支持一组由传输器确定的由其客户的请求发起的操作。这允许客户向低层传输驱动程序（TDI没有明确定义）提交特定传输请求。

n 事件通知

TDI定义了一种模式，当其在网络上出现的时候（不要求客户提交显式的I/O请求），通过它传输器能够向客户通知其感兴趣的事件。对于某些种类的事件，像连接、断连和接收事件等，当传输驱动程序通知客户时，可以将相关数据一起指示给客户。

n 即插即用事件通知

TDI定义了一种模式，通过它Windows 2000传输器能够通知其客户相关的PnP事件，如低层NIC的可用性以及基于本地网址的连接创建和删除。

n 系统电源状态改变通知

TDI定义了一种模式，通过它Windows 2000传输器能够通知客户其所提名的系统电源状态改变事件，这样使得客户有可能维持有用连接。

接下来的特征被TDI传输驱动程序作为附加选择支持：

n 数据传输方式

TDI支持以离散消息（消息模式）方式或者字节流（字节流模式）的方式发送和接收数据。支持哪一种模式（或者两种都支持）由传输驱动程序设计者和（或）协议的性质确定。

n 内部缓冲

TDI传输器能够在内部对其客户的发送和接收进行缓冲。传送其内部缓冲，从而允许TDI客户查询和设置驱动程序内部缓冲区的尺寸、请求非块式发送操作、接收可用缓冲区空间的通告和在接收之前浏览缓冲数据。

n 管理选项

所有的传输器都维持着一些关于其各自的特征、限制和运行时统计信息的由TDI定义的状态。这允许每个客户动态地查询，在某些情况下也可设置传输提供者的静态信息、统计信息和配置参数。另外，前面提到的可扩展TDI_ACTION IOCTL允许TDI传输驱动程序实现独特的网络管理特性（可以被其客户通过对传输器的请求操作访问的）。

n 服务质量（QoS）

TDI传输器能够提供网络连接建立时的QoS协商。另外，该驱动程序也能对面向无连接的数据报传输提供QoS支持。为了提供上述QoS支持，TDI定义的连接建立和数据报发送请求包括Options和OptionsLength参数，这使得TDI客户可以包括一个传输相关的可变长字符串来指定选项。

事实上，Options和OptionsLength参数不仅仅可以指定QoS，而且能够用于向传输驱动程序传递由驱动程序设计者决定的传输器相关连接或者有关数据报的选项。

n 加速传送

当发送消息时，客户可以标识特定消息为要加速的。在发送端，低层传输器在非加速消息之前发送这些被标识的消息。在接收端，标识为加速的消息被提前和（或者）独立于非加速消息指示给客户

n 链式接收指示

如果低层NDIS 微端口支持多包接收指示，那么TDI传输器可以赋予其客户在单个调用中对完整TSDU的直接只读访问能力，并且客户能够保持包含TSDU的缓冲区的控制，直到其处理完指示数据。该特性通过降低接收指示的调用开销，提高了传输器和客户的性能。

3.2 TDI设备对象

Windows 2000传输器通过调用代表各自对低层NIC绑定的设备对象，支持即插即用和系统电源管理。每个那样的传输器在系统安装时作为一个网络提供者向TDI注册，这使得在低层网络硬件联机时，潜在客户可以使用即插即用传输器。

这些具有即插即用功能的传输器的潜在客户向TDI注册各自的ClientPnPXxx回调函数，来接收关于传输器对NIC活动绑定的可用性的通告和关于传输器建立的网络地址的通告，当该网络地址有效时，客户就可以在网络上发送和接收数据。

当传输器通过调用由传输栈中的最低层模块（或者整体式驱动程序）向NDIS注册的ProtocolBindAdapter函数创建对低层NIC的绑定时，TDI传输驱动程序创建指定的设备对象，用来代表对低层NDIS 微端口的绑定。在系统安装期间，支持PnP的传输器向TDI注册其创建的每个指定的设备对象，并设置每个绑定的状态。传输器也向TDI注册每个绑定上其已知的网络地址，并设置每个注册地址的状态。当NDIS在传输栈底部通过PnP事件代码NetEventBindsComplete调用ProtocolPnPEvent函数时，如果网络上至少有一个传输器-NIC绑定准备好传送数据，那么接着该传输器将通知TDI。

TDI使得每个已经注册了ClientPnPXxx处理程序的网络客户有可能通过从注册客户的ClientPnPBindingChange例程调用ZwCreateFile绑定到传输器创建的设备对象。接着TDI对于每个网络地址（低层传输器已经为其设备对象注册的）一次或者多次调用ClientPnPAddNetAddress例程。当某个特定的传输器指示其准备好进行网络发送时，并且当所有安装时传输器-NIC绑定都已建立时，TDI将通过调用已注册的ClientPnPBindingChange例程通知其客户。

运行时，当动态绑定发生改变时，不管改变是由于NIC被启动或禁止，还是由于终端用户导致了绑定改变，或者当传输器像如下所述的那样创建或断开一个对远程节点网络地址的连接时，这些支持PnP的传输器（或者NDIS）都将继续调用TDI。

n 如果一个新的NIC被启动，NDIS调用传输栈底部的ProtocolBindAdapter函数，使得传输器有可能绑定到新的NIC并创建新的设备对象代表其新的绑定，然后向TDI注册新的设备对象。当每个传输器创建的设备对象注册时，TDI将调用最新注册的ClientPnPBindingChange例程并提供客户打开新的绑定的机会。

n 在注册新的设备对象后，传输器也将向TDI注册绑定上的已知的网络地址。当传输器注册新的绑定（或者已存在的绑定）上已知网络地址时，TDI将调用ClientPnPAddNetAddress例程。该调用也使得客户开启对已注册地址（甚至绑定）的连接成为可能。当传输器断开对远程节点的连接时，将调用TDI注销相应的网络地址。

n 在现存的NIC被禁止之前，典型地，NDIS将调用传输栈底部的ProtocolPnPEvent函数询问删除NIC是否安全。接着，该传输器通知TDI关于该删除NIC的请求，然后TDI通过调用已注册的ClientPnPPowerChange例程通知客户。如果没有客户反对NIC的删除，那么接下来NDIS将调用传输栈底部的ProtocolUnbindAdapter函数并且传输器将拆除该绑定。在这个过程中，传输器调用TDI注销以前正式注册的绑定上的所有网络地址，它也调用TDI注销以前创建的代表绑定的设备对象。按顺序，接下来TDI通过调用ClientPnPDelAddress例程和ClientPnPBindingChange例程通知客户关于这些拆除绑定操作。

n 如果终端客户促使网络连接文件夹中的绑定发生改变，NDIS将直接调用TDI，把相关绑定的改变通知注册网络客户。如果没有客户反对（即用户并不企图取消绑定改变操作），那么NDIS将选择传输栈底部ProtocolPnPEvent、ProtocolBindAdapter或者ProtocolUnbindAdapter中的适当函数进行相关的处理。

TDI以相似的方式把低层NIC的电源状态改变通知给支持PnP的传输器的客户。当系统电源管理器调用NDIS来提供NIC电源或者切断NIC电源时，NDIS将调用每个当前绑定该NIC的协议栈底部的ProtocolPnPEvent函数。接着，每个支持PnP的传输器通知TDI相关电源状态的改变，并且TDI将向传输器的客户转发该通知以征求赞成或拒绝意见。

3.3 TDI文件对象

所有I/O请求都被直接指示给一个打开的文件对象，该文件对象代表一个物理的或虚拟的设备、一个数据文件或者任何I/O请求的逻辑目标，该文件对象能够通过特定步骤打开。打开文件对象是进程相关的。例如，I/O管理器为每个打开特定数据文件的进程创建一个文件对象。

TDI用文件对象代表所有网络通信环境中的实体。特别地，TDI用文件对象代表如下TDI定义实体类型：

n 代表传输地址（address），该地址由网络节点上的特定进程或者一组进程打开；

n 代表连接端点，该端点标识与传输地址相关的特定端点，它被打开用于设置和远程节点的对等进程的端端连接；

n 代表控制信道，该信道标识传输服务提供者，打开它来查询和设置低层传输器维持的全局配置、特性、限制和（或）统计信息。

TDI传输驱动程序和低层NDIS驱动程序提供了一种机制，使得数据可以从网络节点的一个进程传送到本节点或者其他网络节点上的一个或多个进程。该传输以打开文件对象所代表的前述地址和连接端点实体的方式出现。

例如，Windows 2000转向器进程打开两个文件对象，来向远程节点上的服务器进程发送SMB消息和接收来自远程节点上服务器进程的返回消息，该两个文件对象其中一个代表传输地址，另一个代表传输地址相关的连接端点。

3.3.1代表传输地址的文件对象

TDI支持不可靠的面向无连接传输和可靠的面向连接传输两种数据传输。不可靠的面向无连接数据可被发送到远程节点上已经打开特定传输地址的一个或多个进程。当作为数据报发送不可靠的面向无连接数据时，发送者仅需要标识接收数据报的进程或者进程组的远程节点地址。

如果端端连接（也叫虚电路）在两个进程间已经建立，那么可靠的面向连接的数据就能够在该两个进程间传送。一个端端连接是二个且仅有两个进程之间的一对一联系。为了建立那样一个连接，一个进程必须标识其要建立连接的进程，每个进程都必须在各自的网络节点打开一个传输地址和一个连接端点，并用打开的传输地址联系连接端点。关于连接端点的更多信息请参阅3.2.2节。

用于标识特定进程或进程组的TDI实体是一个或多个打开的代表特定传输地址的进程相关文件对象。该文件对象包含传输器提供的指向驱动程序维持的标识特定进程和进程驻留节点状态的指针。这些具有路由能力的传输器，像TCP/IP、Mcsxns、AppleTalk和NWLink，保存的关于那些地址的状态也包括指定节点所在的网络（或者子网）的信息。

TDI定义的传输地址类型能够容纳关于地址标识单个进程（单值地址）还是一组进程（组地址）的显式的或者隐式的指示。在组地址情况下，TDI定义的地址能够包含标识相关进程即进程驻留节点和节点所在网络（或者子网）的信息。

TDI支持许多地址类型，接下来描述三种通用TDI地址类型的格式和用法：

n TDI_ADDRESS_NETBIOS

NetBIOS类型地址包括标准16字符的NetBIOS名和所指示的成员。如果该名以单值名称注册，则指示该名标识单个进程；或者如果该名作为组名注册，则指示该名标识一组进程。

因为传输驱动程序能够确保每次网络上仅有一个进程使用单值名，所以该名称不仅能够标识进程，而且能够隐含地标识进程驻留节点。

如果被注册的是一个组名，那么该地址对多个终端的进程都是可用的，这样该组名标识了注册该名的所有进程以及进程驻留节点。

n TDI_ADDRESS_IP

IP类型地址包含一个端口号和标准的IP地址。由于TCP/IP允许相同的端口号可被多个节点上的进程注册使用，所以要求IP地址能够独立地标识节点并且要求端口号能够独立地标识该节点上的进程。

对于无连接数据传输（使用TCP/IP的UDP协议），相同的端口号可以被同一节点上的多个进程注册使用。另外，某些IP地址也能被多个节点使用。送给TDI地址的包含端口号和IP地址的数据可以被该IP地址标识的所有节点接收，并且这些数据将被送给注册指定端口号的所有进程。

n TDI_ADDRESS_IPX

IPX类型地址包含四字节的网络号、六字节的节点号和两字节的端口号。因为IPX允许多个节点上进程注册相同的端口号，所以要求IPX地址能够独立地标识网络和节点，而端口号用来独立地标识该节点上的进程。

3.3.2代表连接端点的文件对象

代表打开连接端点的文件对象标识一个特定的连接，在该连接上，本地和远程节点的对等进程通过网络相互进行通信，或者当本地节点进程建立和远程节点对等进程的端端连接之后在二者之间进行通信。

以上所述的任何文件对象必须建立与其他打开的代表特定传输地址的文件对象之间的联系。3.2.1节已经讨论过，传输地址可以标识进程。

相似地，传输器使用代表本地连接端点的文件对象，保存关于远程节点对等进程的状态，像远程节点传输地址，本地节点客户可以建立到它的端端连接。

客户进程能够和远程节点进程建立多个端端连接。例如，转向器和远程Windows 2000服务器建立独立的连接进行相互通信。这样客户就能够拥有多个打开且相互独立的与打开的传输地址相关的连接端点。

当某个进程想在端端连接上发送数据时，必须指定数据要发送到现存的哪一个连接上。每一个打开的代表连接端点的文件对象用于区分同一个进程已经建立的多个端端连接。

3.3.3代表控制信道的文件对象

网络管理进程能够查询和设置与特定传输提供者相关的全局配置或统计信息。为了进行上述查询和设置，该进程必须在多个可能的传输服务提供者之间指定特定的传输器。

为了查询相应的传输器，客户应该打开代表控制信道的文件对象。在实际运行中，当客户打开控制信道时，客户将通过调用ZwCreateFile或者IoGetDeviceObjectPointer，打开代表低层传输驱动程序创建的设备对象的文件对象。

通过网络发送和接收数据的其他TDI客户也能够打开控制信道，对低层传输器进行查询。例如，客户能够生成一个查询，确定传输器对数据报长度的限制，使得客户可以有效地分配用于发送和接收数据报的缓冲区大小。

总之，TDI传输器维持的是关于其特征和最新的统计值的全局状态信息，而不是依赖于特定打开文件对象的进程相关状态信息。例如，客户传递指向代表该地址的特定客户文件对象的指针，来查询其打开的传输地址的最新状态，传输器返回当前所有打开相同地址的客户的公共信息。

3.4 TDI传输驱动程序例程

每个TDI兼容的传输驱动程序都必须是一个导出许多可以被I/O管理器调用的入口点的标准中间层驱动程序。

在TDI传输器的标准驱动程序例程中，一部分用于完成初始化和卸载驱动程序的工作，其他的是标准的调度例程，当TDI客户调用系统支持例程时，像ZwCreateFile或者IoCallDriver，I/O管理器可以调用这些调度例程。

像其他的核心模式驱动程序一样，TDI传输器的DriverEntry例程设置一个或多个调度例程来处理各种类型的作为IRP传入的I/O请求。TDI驱动程序能够导出单个调度例程处理所有引入的IRP，或者导出独立的DispatchXxx例程处理驱动程序支持的IRP_MJ_XXX。调度例程的一般要求的详细讨论可参阅Kernel-mode Driver Guide。调度例程的TDI相关要求在第四章将进行汇总，其详细描述也可参阅在线DDK的“the Network Drivers Reference”。

当TDI完成其客户的请求操作时，I/O管理器调用客户提供的IoComplete例程，其在该客户向低层传输器提交该请求之前在IRP中设置。

另外，当特定网络事件出现时，上述传输驱动程序必须在TDI客户代码中的预注册入口点调用TDI客户。这些由客户提供的事件处理程序也将在第四章进行汇总，其详细描述也可参阅在线DDK的“the Network Drivers Reference”。

在下边界，整体式的TDI驱动程序必须导出一组ProtocolXxx函数供代表低层NDIS 中间层驱动程序和（或）NIC驱动程序的NDIS库调用。这些NDIS驱动程序下边界函数前面已在该本书中进行了描述，也可参阅在线DDK的“the Network Drivers Reference”中的函数相关方法。

3.5 TDI核心模式客户交互

下图显示了TDI客户是如何向其低层TDI传输器发送I/O请求的，以及传输器是怎样回调其客户的。

图3.2 TDI客户/传输器交互

TDI客户以以下方式和其低层传输驱动程序进行交互：

n 创建TDI文件对象

客户调用ZwCreateFile创建或打开代表传输地址、连接端点或者控制信道的文件对象。该调用使得I/O管理器分配IRP，将客户提供的参数整理到IRP，向低层传输驱动程序的TdiDispatchCreate例程传送IRP。当传输驱动程序为新创建的文件对象设置了其维护的所有状态之后，将以IRP和STATUS_SUCCESS调用IoCompleteRequest（或者TdiCompleteRequest）。IoCompleteRequest将向TDI客户返回文件对象句柄。

每个客户进程对ZwCreateFile的调用将创建一个独立的文件对象，即使两个客户在对ZwCreateFile的调用中已指定了相同的传输地址。

对ZwCreateFile的成功调用依赖于调用中客户传递的EaXxx参数打开传输地址、连接端点或者控制信道。

n 提交请求

文件对象创建之后，客户就可以提交引用这些对象的请求。例如，在打开代表特定传输地址的文件对象之后，该客户就可以提交地址信息查询或者从该地址发送数据报的请求。

上述客户用标准I/O系统机制和约定提交如下请求：

n 客户用IRP_MJ_XXX操作码（指定客户希望传输驱动程序执行的操作）设置IRP。客户为给定的IRP_MJ_XXX设置所有相关参数，并且作为一种选择也可设置IoComplete例程，该例程在传输器完成请求时被调用。

Windows 2000 DDK包含一组TdiBuildXxx宏（在tdikrnl.h中），该宏可被链入客户代码，也可用于为TDI定义的IOCTL请求设置IRP，另外，也提供了用于分配IRP的TdiBuildInternetDeviceControl函数。

n 当IRP设置好后，客户用指向IRP的指针、指向代表地址的文件对象、连接端点或者控制信道的指针、以及指向传输驱动程序设备对象的指针调用IoCallDriver。I/O管理器直接向传输驱动程序的适当TdiDispatch（Xxx）例程传递该IRP。

n 当传输驱动程序完成请求操作之后，将调用TdiCompleteRequest或者IoCompleteRequest。然后I/O管理器调用客户提供的IoCompletion例程，如果对该IRP客户提供了这个例程的话。

n 处理事件通知

如果客户已预先为一个或多个事件处理程序注册了入口点，那么当相应网络事件发生时，传输驱动程序将调用相关例程。例如，客户在端端连接相关的地址上注册了ClientEventReceive处理程序，那么当远程节点对等进程发送的数据被本地节点接收时，传输器将调用该处理程序。

n 删除TDI对象

当地址、连接端点或者控制进程不再为客户需要时，该客户调用ZwClose删除相关的文件对象。删除请求被转发给传输器的TdiDispatchCleanUp，接着转发给TdiDispatchClose例程。

关于以上所提的核心模式支持例程的更多信息，请参阅在线DDK的“the Kernel-Mode Drivers Reference”。

3.6 TDI请求及事件

许多低层网络接口，如NetBIOS和Windows Sockets，主要都是单行道的。客户在任何时候都可以调用低层传输驱动程序，反之，传输驱动程序就不能调用其客户。传输驱动程序调用其客户的唯一方式是通过从客户发起的请求返回特定错误码。例如，在NetBIOS接口中，传输器通过随特定的错误代码（例如，对会话关闭用0x0A）一起返回未决的NCB_Receive (或者NCB_Receive_Any)通知其客户关于连接挂起信息。

TDI提供一种事件通知机制，在特定网络事件（例如，数据报的接收）发生时，允许TDI传输器调用其受到影响的核心模式客户，只要每个客户为该类型事件都已向传输器注册了ClientEventXxx处理程序。然后TDI客户提供的回调执行相应的操作，并将控制返回传输驱动程序。

TDI也为支持PnP的Windows 2000传输器定义了一种方式，当该传输器绑定到低层NIC或拆除绑定时，当传输器在特定绑定上建立或断开到远程节点的连接时，或者当电源管理器改变（和/或影响）系统电源状态时，允许传输器通知其客户。就像在3.2节中所说的，该传输器的客户向TDI注册一组ClientPnPXxx处理程序来接收这些种类的通知。

当TDI传输驱动程序调用客户注册的ClientEventXxx处理程序时，能够传递一定数量的数据作为该调用的参数。该特征使得客户不必分配缓冲区就可接收来自传输器的消息。

例如，转向器就利用了该TDI特征。转向器向服务器发送的许多SMB请求，仅要求用标准SMB报头存放相应的SMB响应信息，该报头很小，典型的只有100字节，报头存放信息包括状态指示器、多路复用ID等。

当低层TDI传输驱动程序用先前发送的SMB响应信息调用转向器注册的ClientEventReceive处理程序时，该转向器仅仅需要察看（不必复制）指示消息，记录SMB响应的状态指示器并返回传输驱动程序。在该处理中，转向器不必分配缓冲区就可接收SMB响应信息。


TDI例程、宏和回调

本章讨论TDI传输驱动程序的例程和请求和TDI核心模式客户导出的回调例程，以及系统提供给传输器和客户使用的宏和TDI函数。

以下主题是对每种信息的汇总：

n 传输驱动程序

4.1、 TDI驱动程序初始化

4.2、 TDI驱动程序调度例程

n TDI定义的核心模式客户能够向其低层传输器发布的一组IOCTL

4.3、 TDI IOCTL请求

n 客户能够向低层传输器注册的事件处理程序

4.4、 TDI客户回调

n TDI传输器和其客户能够使用的由系统提供的一组函数和宏

4.5、 TDI库函数和宏

关于TDI客户和传输期间的交互序列（当使用前面所述的宏、函数和例程时）的更多信息请参阅第五章。

要想获取关于每个请求、宏或例程的详细信息，请参阅在线DDK的“the Network Drivers Reference”。

4.1 TDI驱动程序初始化

每个TDI传输驱动程序必须提供DriverEntry显式命名的初始化例程。另外，传输驱动程序应提供足够核心模式客户I/O请求需要的TdiDispatchXxx例程和内部驱动程序函数。每个TDI传输驱动程序或者传送栈中的某个低层协议驱动程序也必须提供前面第二章所描述的NDIS驱动程序下边界函数，关于其详细信息可参阅在线DDK的“the Network Drivers Reference”。

每个传输驱动程序必须提供在传输器载入时供I/O 管理器调用的DriverEntry例程。DriverEntry必须在驱动程序代码中声明，使得传输器能够自动装载。

当载入驱动程序时，I/O 管理器创建代表传输器的驱动程序对象，并在调用TDI传输器的DriverEntry例程时传递该驱动程序对象的指针。然后，DriverEntry例程完成如下的工作：

n 如果可能，读取注册表获取传输器INF文件写入注册表的配置信息。根据需要，用注册表配置信息进行相关配置。

n 在驱动程序对象中设置驱动程序所有的TdiDispatchXxx入口点。以后，I/O 管理器处理TDI客户请求时将会调用这些TdiDispatchXxx例程。

n 调用TdiRegisterProvider通知TDI关于传输器的即插即用和电源管理支持能力。

n 使用IoCreateDevice为自己创建至少一个设备对象。每个驱动程序设备对象的名字就是存在注册表Export入口中的Linkage键下的设备名。每个传输驱动程序的Export入口决定了系统安装时每个特定的传输驱动程序创建的设备对象。

有些传输驱动程序，像NWLink，不管其绑定的NIC有多少，都只创建一个设备对象。有些传输驱动程序，像TCP/IP，可以创建一个或者多个设备对象并且对每个设备对象导出一组设备接口。其他的传输驱动程序则为每个绑定的NIC创建一个独立的设备对象，如Nbf_Elnkkii1，Nbf_Elnkkii2等。

n 执行其他必须的初始化任务，像绑定到低层NDIS中间层驱动程序和（或）NIC驱动程序。

n 当创建对每个低层NIC的绑定时，支持即插即用的Windows 2000传输器还要用传输器创建的设备对象调用TdiRegisterDeviceObject。接着，一次或者多次调用TdiRegisterNetAddress注册所有已知的与设备对象（传输器为其最近建立的绑定创建的）相关的网络地址（包括本地和远程节点）。

当该支持PnP的传输器已至少完成了一个绑定的初始化，从而可以在网络上传输数据时，传输器必须调用TdiProviderReady。典型地，作为NDIS在传输栈低边通过NET_PNP_EVNET编码NetEventBindsComplete调用ProtocolPnPEvent函数的结果，支持PnP的传输器调用TdiProviderReady。

TDI传输驱动程序必须提供卸载例程，除非系统可用时驱动程序不能卸载。多数TDI传输驱动程序声明了卸载例程并且DriverEntry例程在驱动程序对象中设置其入口点。关于驱动器卸载例程的更多信息请参阅4.1.2节。

关于TDI驱动程序所用的核心模式支持例程的更多信息，请参阅在线DDK的“the Kernel-Mode Drivers Reference”。

4.1.1 注册TDI传输驱动程序

当TDI载入时，配置记录获取每个TDI传输驱动程序和核心模式客户在特定网络上的操作入口。使用注册表中的绑定信息，操作系统导入网络驱动程序。

接着，I/O 管理器创建驱动程序对象并调用每个TDI传输驱动程序的DriverEntry例程直到网上所有TDI驱动程序都被载入并注册为止。

如果要绑定到低层NDIS NIC驱动程序，那么TDI传输驱动程序或者传送栈的低层协议组件就必须调用NdisRegisterProtocol向NDIS库注册。当PnP管理器调用NDIS初始化本地节点上的每个NIC时，NDIS在相应TDI传输驱动程序或传送栈的下边界调用已注册的ProtocolBindAdapter函数，使得每个传输器都能够绑定到相应的NDIS 微端口。

当TDI传输驱动程序绑定到NDIS 微端口或者使自己位于已绑定到该微端口的NDIS协议驱动程序之上时，TDI驱动程序应准备好响应已注册的TDI客户，该客户向TDI传输器已经向该绑定注册的网络地址提交请求。

最后，当TDI传输器（支持PnP）为每个初始化时调用（TdiRegisterDeviceObject 和TdiRegisterNetAddress）设置了各个设备对象状态和各个地址状态之后，必须调用TdiProviderReady。

注意上述TDI传输器每次初始化时只调用TdiRegisterProvider 和TdiProviderReady一次。然而，传输器可以对Windows 2000 TDI提供的PnP和电源管理函数进行多次运行时调用，例如，TdiRegister/DeregisterDeviceObject、TdiRegister/DeregisterNetAddress和TdiPnPPowerRequest等。

关于Windows 2000 PnP和电源管理支持的更多信息，请参阅Plug and Play，Power Management，and Setup Design Guide以及the Network Drivers Guide中关于NDIS库如何指示PnP和电源管理事件的详细资料。

4.1.2 卸载和注销TDI传输驱动程序

在NIC从机器中删除之前，绑定到该NIC的Windows 2000 TDI传输器能够首先通知其客户：NIC将被禁止或删除。即，除非意外删除NIC，NDIS将在每个绑定到该NIC的传输器的下边界用NetEventQueryRemoveDevice输入码，调用ProtocolPnPEvent函数，使得每个TDI传输器都能够通过调用TdiPnPPowerRequest向其客户转发该通知。

直到NDIS在传送栈下边界调用ProtocolUnbindAdapter函数，上述支持PnP的传输器才真正拆除绑定并调用TdiDeregisterNetAddress 和TdiDeregisterDeviceObject。而且，也直到释放了所有对低层NIC的绑定之后，传输驱动程序才被卸载。

不管怎样，当NIC从机器中删除之后，系统就可以卸载TDI传输驱动程序了，这些驱动程序要么直接绑定到上述低层NIC驱动程序，要么建立在绑定到NIC驱动程序的NDIS协议驱动程序之上。为了实现该功能，系统调用TDI传输器的卸载例程。

当服务控制器从内存卸载该驱动程序之后，I/O管理器将调用传输器的卸载例程。传输驱动程序的卸载例程释放所有保留的驱动程序分配的资源。

支持PnP的TDI传输驱动程序必须在其卸载例程中进行互逆的TdiDeregisterProvider调用。如果TDI驱动程序导出了一组NDIS定义的ProtocolXxx函数，那么在其卸载例程中也必须调用TdiDeregisterProtocol。

关于标准中间层驱动程序卸载例程的更多信息，请参阅the Kernel-Mode Drivers Design Guide。关于Windows 2000 PnP和电源管理支持的更多信息，请参阅Plug and Play，Power Management，and Setup Design Guide以及the Network Drivers Guide中关于NDIS库如何指示PnP和电源管理事件的详细资料。

4.2 TDI驱动程序调度例程

I/O请求由I/O管理器或者由TDI客户显式地格式化为IRP，并通过调用IoCallDriver提交给传输驱动程序。当传输驱动程序调用IoCompleteRequest或TdiCompleteRequest时，完成的IRP返回给呼叫器。在调用IoCallDriver之前，任何核心模式TDI客户都可以为IRP设置IoCompletion例程。

有五个IRP_MJ_XXX编码用于向TDI传输驱动程序发送I/O请求。传输器处理的引入IRP，其中的MajorFunctionCode是以下之一：

n IRP_MJ_CREATE

打开一个传输器创建的指定的设备对象，接着打开低层传输器中的传输地址、连接端点和控制信道。

该请求由客户对ZwCreateFile的调用发起。

n IRP_MJ_INTERNEL_DEVICE_CONTROL

指定核心模式客户请求（TDI IOCTL），对于该请求，内部传输函数处理除打开和关闭文件对象之外的操作。

该请求通常由客户对TdiBuildXxx宏的调用发起，该宏之后紧接着调用IoCallDriver。

n IRP_MJ_ DEVICE_CONTROL

指定用户可见模式IOCTL请求，该请求可由传输器专门应用发起。除了传输驱动程序定义的私有IOCTL之外，前述请求通常都能被转发给相同的处理内部设备控制请求的内部驱动程序函数。

该请求由传输器专门用户模式应用对DeviceIoControl的调用引起。

n IRP_MJ_CLEANUP

当NT执行程序关闭相应文件对象的最后一个句柄时，关闭打开的相应地址、连接端点和控制信道。

该请求由客户对ZwClose的调用引起。

n IRP_MJ_CLOSE

如果NT执行程序取消对文件对象句柄的最后的引用，关闭打开的相应地址、连接端点和控制信道。当未完成的文件对象句柄都已关闭时，关闭传输驱动程序创建的设备对象。

在同一个文件对象上，该请求紧跟着IRP_MJ_CLEANUP请求发起。

TDI驱动程序中的入口点是一个或者多个处理IRP_MJ_XXX请求的调度例程。因为TDI客户通过IRP和驱动程序进行通信，所以驱动程序要有一个或多个确定执行什么操作的TdiDispatchXxx例程。通常，为了更进一步的处理，这些TdiDispatchXxx例程向相应的内部驱动程序函数传递客户请求。

TDI传输驱动程序通过在传给DriverEntry例程的驱动程序对象中进行设置，导出其所有TdiDispatchXxx例程的入口点。当客户提交I/O请求时，I/O管理器调用DriverEntry例程。传输驱动程序能够用分离的TdiDispatchXxx，处理每个可能的IRP_MJ_XXX操作码，用单个TdiDispatchXxx例程处理所有可能IRP_MJ_XXX操作码的IRP，或者用一组TdiDispatchXxx处理IRP_MJ_XXX操作码的分立子集。

既然所有调度入口点都是通过驱动程序对象中的地址而不是名字导出的，因此TDI传输驱动程序对这些例程的命名是不受限制的。在该TDI文档中，相对于前述的IRP_MJ_XXX ，TdiDispatchXxx例程有如下的后缀名，每一个都描述了其基本的功能：

IRP_MJ_CREATE

TdiDispatchCreate

IRP_MJ_INTERNEL_DEVICE_CONTROL

TdiDispatchInternelDeviceControl

IRP_MJ_ DEVICE_CONTROL

TdiDispatchDeviceControl

IRP_MJ_CLEANUP

TdiDispatchCleanUp

IRP_MJ_CLOSE

TdiDispatchClose

4.3 TDI IOCTL请求

前一节已经提到，核心模式客户通过调用IoCallDriver在IRP中向低层传输驱动程序传递TDI IOCTL。这些请求通过IRP_MJ_INTERNEL_DEVICE_CONTROL 的MajorFunctionCode和指定如下TDI定义的操作之一的MinorFunctionCode进行传递：

TDI_ACCEPT

允许本地节点客户接受来自远程节点对等实体的引入连接提议，使得能够在网络上发送和接收面向连接数据。

TDI_ACTION

为地址、连接端点或者控制信道启动传输相关的扩展操作。

TDI_ASSOCIATE_ADDRESS

用打开的本地传输地址联系开放连接端点，为了和远程节点对等实体建立端端连接这是必要的。

TDI_CONNECT

启动从一端到远程节点的另一端的连接提议。

TDI_DISASSOCIATE_ADDRESS

对一个无效连接，使连接端点与相连的地址对象分离。

TDI_DISCONNECT

为本地节点客户终止一个已建立的端对端连接，或者证实远程节点引发的连接中断。

TDI_LISTEN

被动地侦听远程节点对等进程的引入连接提议。

TDI_QUERY_INFORMATION

查询低层TDI传输器的特定类型信息。

TDI_RECEIVE

返回端端连接上接收的TSDU部分或全体。

TDI_RECEIVE_DATAGRAM

返回打开传输地址上的数据报TSDU。

TDI_SEND

向端端连接上的远程节点对等实体发送标准的或者加速的TSDU。

TDI_SEND_DATAGRAM

向指定的远程节点地址发送数据报。

TDI_SET_EVENT_HANDLER

注册ClientEventXxx处理程序。当相应网络事件发生时，传输器将调用该处理程序。

TDI_SET_INFORAMTION

在低层传输器中设置特定类型信息。

4.4 TDI 客户回调

多数TDI客户例程是易变的并且是依赖于环境的。然而，TDI定义了一组由客户向低层TDI传输器注册的回调例程，用于在相关网络事件发生时接收通知。如果TDI驱动程序要通知客户关于所发生的特定类型网络事件，那么核心模式TDI客户能够注册上述的所有ClientEventXxx例程。作为一种选择，客户甚至能够使用注册的ClientEventXxx处理程序向驱动程序发布确定的TDI_XXX IOCTL请求。

就像前面所描述的TdiDispatchXxx例程一样，客户提供的事件处理程序任意选择其名称。这儿所说的每个ClientEventXxx注册为一个接口，该接口被传递给由客户提供的向TdiDispatchInternelDeviceControl例程提交的IRP。

一个TDI客户可以有一个或者多个以下所列的回调例程，这些回调例程在打开传输地址上的网络操作开始时，通过发布连续的TDI_SET_EVENT_HANDLER请求，向TDI传输驱动程序注册，并用TdiBuildSetEventHandler进行设置。

ClientEventConnect

通知客户关于远程节点对等进程的连接提议。

ClientEventDisconnect

通知本地节点客户：远程节点对等实体正在终止它们之间的端端连接。

ClientEventError和/或ClientEventErrorEx

通知客户关于其低层传输驱动程序、传送栈的某个低层协议或者低层NDIS驱动程序的错误状态，这使得随后在客户打开传输地址上的I/O请求变得不可靠或者不可能。

ClientEventReceive

通知客户：传输器已从端端连接的远程节点对等实体上接收到标准数据，并且该接收数据的部分或全体已可被客户复制。

ClientEventChainReceive

通知客户：传输器已从端端连接的远程节点对等实体上接收到标准TSDU，并且客户已可处理该完整TSDU。

ClientEventReceiveExpedited

通知客户：传输器已从端端连接的远程节点对等实体上接收到加速数据，并且该接收数据的部分或全体已可被客户复制。

ClientEventChainReceiveExpedited

通知客户：传输器已从端端连接的远程节点对等实体上接收到加速TSDU，并且客户已可处理该完整TSDU。

ClientEventReceiveDatagram

通知客户：TDI驱动程序已在客户打开的传输地址上接收到数据报。

ClientEventChainReceiveDatagram

通知客户：TDI驱动程序已在客户打开的传输地址上接收到数据报，并且客户已可处理该完整TSDU。

ClientEventSendPossible

通知客户：传输器（在内部对发送数据进行缓冲）重新拥有发送缓冲空间。

Windows 2000 TDI客户也可向TDI注册一组ClientPnPXxx回调例程来接收支持PnP的TDI传输器的动态PnP绑定改变、网络地址改变和电源状态改变事件的通知。就像ClientPnPXxx处理程序一样，这些客户提供的PnP事件处理程序也可以任意选择其名称。

该Windows 2000 TDI客户有以下所列的一组回调例程，这些回调例程在客户初始化是通过调用TdiRegisterPnPHandlers进行注册。

ClientPnPBindingChange

系统启动时，TDI调用该例程通知客户所有可用的由传输器创建的、代表传输器对NIC绑定的设备对象。当每个支持PnP的TDI传输器指示其绑定已准备好进行网络数据传输时，再一次调用该例程。当所有可能的TDI传输器对NIC的绑定都已建立之后，也要调用该例程。

运行期间，当支持PnP的传输器注册另一个代表最近建立的传输器对NIC绑定的设备对象时，或者当注销现存设备对象时，TDI将调用该客户例程。另外，如果终端用户使网络连接文件夹中的绑定列表顺序发生改变，也要调用该例程。

ClientPnPAddNetAddress

系统启动时，TDI调用该例程通知客户其绑定上的所有当前网络地址，该网络地址由支持PnP的传输器向TDI注册。接着，当支持PnP的传输器在绑定上注册新的网络地址（通常是为新近建立的对远程节点的连接注册的）时，TDI将调用该客户。当所有可能的TDI传输器对NIC的绑定都已建立之后，也要调用该例程。

ClientPnPDelNetAddress

当支持PnP的传输器从注册表中撤销绑定上的现存网络地址（通常在断开对远程节点连接时发生）时，TDI将调用该例程。

ClientPnPPowerChange

TDI调用该例程通知客户相关NIC的删除和相关系统电源状态的改变，这将影响特定绑定上的客户在网络上进行传输数据的能力。

典型地，向TDI注册这组ClientPnPXxx处理程序的客户将保存关于所有有效网络地址的动态状态，通常作为一个或多个内部列表，这些网络地址是依附于用于网上通信的所有TDI传输器的绑定的。

4.5 TDI 库函数和宏

Windows 2000以核心模式动态连接库的方式提供了一组TDI库函数，通过这些库函数，TDI驱动程序和核心模式客户可以进行链接。然而，系统提供的供核心模式客户调用的多数TdiBuildXxx是作为宏在tdikrnl.h头文件中实现的，该头文件可被客户和传输器包含。

就像4.3节已经提到的，任何核心模式TDI客户在准备TDI_XXX IOCTL IRP时，都可以根据需要使用TdiBuildXxx宏。当客户使用TdiBuildXxx宏设置好了该IRP之后，将通过向IoCallDriver传递IRP的方式，向低层TDI驱动程序提交其请求。这些宏将填充客户提供的IRP的相关成员，除了Status和Information成员之外，这两个成员在处理完客户请求之后由低层传输器填充。在使用TdiBuildXxx IOCTL宏之前，如果高层网络组件还没有分配并赋予客户相应的IRP，客户能够调用TdiBuildInternetDeviceControlIrp分配IRP。

传输驱动程序不能使用TdiBuildXxx宏。然而，在处理客户请求过程中，作为辅助，TDI驱动程序能够使用其余的TDI库中的某些例程，例如TdiCompleteRequest和TdiCopyBufferToMdl等。

TDI库例程和宏包括以下所列：

TdiBuildInternetDeviceControlIrp

分配IRP，如果客户没有收到较高网络层的IRP的话。

TdiBuildAccept

为客户提交TDI_ACCEPT请求设置IRP。

TdiBuildAction

为客户提交TDI_ACTION请求设置IRP。

TdiBuildAssociateAddress

为客户提交TDI_ASSOCIATE_ADDRESS请求设置IRP。

TdiBuildConnect

为客户提交TDI_CONNECT请求设置IRP。

TdiBuildDisassociateAddress

为客户提交TDI_DISASSOCIATE_ADDRESS请求设置IRP。

TdiBuildListen

为客户提交TDI_LISTEN请求设置IRP。

TdiBuildQueryInformation

为客户提交TDI_QUERY_INFORMAITON请求设置IRP。

TdiBuildReceive

为客户提交TDI_RECEIVE请求设置IRP。

TdiBuildReceiveDatagram

为客户提交TDI_RECEIVE_DATAGRAM请求设置IRP。

TdiBuildSend

为客户提交TDI_SEND请求设置IRP。

TdiBuildSendDatagram

为客户提交TDI_SEND_DATAGRAM请求设置IRP。

TdiBuildSetEventHandler

为客户提交TDI_SET_EVENT_HANDLER请求设置IRP。

TdiBuildSetInformation

为客户提交TDI_SET_INFORMATION请求设置IRP。

TdiBuildNetbiosAddress

为客户设置NetBIOS地址。

TdiBuildNetbiosAddressEa

设置缓冲的NetBIOS地址，客户接下来将其传递给ZwCreateFile来打开该地址。

TdiReturnChainReceives

在客户处理完接收的TSDU之后，释放传给ClientEventChainReceive（Xxx）处理程序的缓冲区的控制权。

TdiCopyBufferToMdl

复制一定范围的缓冲数据到给定MDL映射的目标缓冲区。

客户和传输器都可使用该函数。

TdiCopyMdlToBuffer

从给定MDL映射的缓冲区复制数据到呼叫器提供的目标缓冲区。

客户和传输器都可使用该函数。

TdiCopyLookaheadData

安全地复制NIC驱动程序指示给传输器的接收数据，不管NIC正在使用的内存（包括映射的设备内存）的特征是什么。

TdiMapUserRequest

如果TdiMapUserRequest认可输入IRP中指定的局部函数代码，将传给传输器DisPatchDeviceControl例程的IRP转换为TDI_XXX IOCTL IRP。

TdiCompleteRequest

以系统为传输驱动程序定义的网络相关优先权作为辅助完成IRP。

TdiRegisterPnPHandler

向TDI注册一组ClientPnPXxx例程接收来自低层支持PnP的TDI 传输器的PnP绑定改变、网络地址改变和电源状态改变事件的通知。

TdiDeregisterPnPHandler

注销一组ClientPnPXxx例程，该例程接收关于PnP绑定改变、网络地址改变和电源状态改变事件通知。

TdiRegisterProvider

系统启动时，向TDI注册支持Windows 2000 PnP的传输驱动程序。

TdiProviderReady

指示支持PnP的传输驱动程序准备好执行一个或多个新近建立的传输器-NIC绑定上的网络I/O。

TdiDeregisterProvider

卸载之前注销支持PnP的传输驱动程序。

TdiRegisterDeviceObject

注册一个指定的由传输器创建的设备对象，该支持PnP传输器的客户能够为网络I/O打开该设备对象。该支持PnP的传输器调用该TDI函数通知其潜在客户它刚刚建立了一个新的传输器-NIC的绑定。

TdiDeregisterDeviceObject

当支持PnP的传输器拆除绑定时，注销传输器创建的设备对象。

TdiRegisterNetAddress

注册一个有效网络地址，该地址由支持PnP的传输器为本地机器创建或者是在特定绑定上建立对远程节点链接时获得的。支持PnP的传输器调用该TDI函数通知潜在客户：以后可使用该协议相关地址进行网络通信。

TdiDisregisterNetAddress

从特定的支持PnP的传输器-NIC绑定的有效地址列表中，注销一个网络地址。

TdiPnPPowerRequest

向支持PnP的传输器客户转发确定的NET_PNP_EVENT类型通知。支持PnP的传输器调用该TDI函数，使其客户有可能通知它们的更高层客户传输器依赖的低层NIC的绑定发生改变或电源状态发生改变。

TdiPnPPowerComplete

指示客户已经完成了对绑定改变或电源状态改变通知（先前返回了STATUS_PENDING）的处理。

TdiEnumerateAddresses

向调用客户返回所有有效网络地址的列表。该TDI函数很少被调用，因为该调用对调用程序的性能有严重的负面影响。


TDI操作

本章讨论TDI运行时的基本操作。当支持PnP的Windows 2000传输器的客户开始这里所说的操作时，它们将使用当前已向TDI注册的协议相关网络地址，在网络上建立连接。关于支持PnP的传输器和其客户的更多信息，可参阅前面的第三章和第四章。

很多TDI运行操作都包括本地节点客户和远程节点客户之间的端端连接，这些TDI操作是面向连接的并且包括以下内容：

n 打开本地节点连接断点

n 请求连接

n 接受连接

n 发送面向连接数据

n 接收面向连接数据

n 断开连接

n 关闭本地节点连接断点

TDI也允许网络节点间的无连接通信。这种类型的操作不要求本地节点客户建立和远程节点客户的端端连接。它们比面向连接通信速度更快但可靠性降低。TDI无连接操作包括以下内容：

n 发送数据报

n 接收数据报

对面向连接和面向无连接通信，以下TDI操作是公用的：

n 打开本地节点传输地址，该地址用一个文件对象作为代表

n 打包和发送请求

n 设置和查询信息

n 接收错误通知

n 如果低层传输器对其客户支持扩展TDI接口，请求相关传输操作

n 关闭前面打开的传输地址

接下来这一节讨论TDI的主要操作，其内容将尽可能按这些操作可能出现的顺序安排。关于这里所提的TDI、传输器和客户例程请参阅在线DDK的“the Network Drivers Reference，Part 3”。

关于这里所说的支撑例程，如ZwCreateFile和IoCallDriver等，请参阅在线DDK的“the Kernel-Mode Drivers Reference”。

5.1打开传输地址

图5.1显示了核心模式客户是怎样在低层传输驱动程序中打开传输地址的。

图5.1 打开传输地址

当为绑定打开传输器创建的设备对象之后，TDI客户通常通过打开代表传输地址的文件对象和本地节点传输器进行通信。为了实现该功能，客户调用ZwCreateFile并在其EA（扩展属性）缓冲区参数中指定相应地址。在EA缓冲区中，客户设置EaName成员为系统定义的TdiTransportAddress，其后紧跟的是TDI定义的TRANSPORT_ADDRESS类型EA值，该值由客户设置指定将被打开的相关传输地址。

当调用ZwCreateFile时，客户可以指定特定传输地址是可与其他客户共享的还是该客户自己独占使用的。当初始客户为独占使用成功打开特定传输地址之后，其他客户直到该客户关闭该地址时才能再次打开该地址。

为了在网上进行无连接通信，假设传输器支持广播数据报，客户能够在EA缓冲区中指定低层传输器的广播地址。任何客户都不能独占使用该地址。

客户对ZwCreateFile的调用促使I/O管理器创建代表该地址的客户进程相关文件对象，并用包含客户向ZwCreateFile提供的参数的IRP，调用TDI传输驱动程序的TdiDispatchCreate例程。TdiDispatchCreate对EA信息进行分析，并且如果调用成功，传输器将为该打开地址和客户设置相应的内部状态。

当ZwCreateFile调用成功并向客户返回一个文件句柄，并且该客户也通过ObReferenceObjectHandler获得了该文件句柄的指针之后，就能够向低层传输器提交确定的TDI_XXX IOCTL请求。例如，客户能够在该打开地址上发送和接收数据报。

通常，客户必须首先确定是否要注册ClientEventXxx处理程序，是否将使用该打开地址和远程节点对等进程进行通信，如果要进行通信，客户也必须打开一个连接断点。

如果客户想接收不同网络事件的通知，可以通过向低层传输器提交TDI_SET_EVENT_HANDLER请求的方式来为每类事件注册相关ClientEventXxx处理程序，该请求用TdiBuildSetEventHandler进行设置。关于设置IOCTL请求和向低层传输器提交该请求的更多信息请参阅5.3节。

当客户在打开的传输地址上注册了ClientEventXxx处理程序之后，就能够像5.2节所说的那样打开一个连接端点进行面向连接通信，也可像5.7节中将要讨论的进行无连接通信。

当打开的传输地址上的网络通信完成后，客户必须关闭代表该传输地址的文件对象，该操作通过传递ZwCreateFile返回句柄调用ZwClose来完成，后面5.13节有详细描述。

5.2打开连接端点

图5.2显示了核心模式客户是怎样在低层传输驱动程序中打开连接端点的。

图5.2 打开连接端点

如果本地节点客户请求和远程节点对等进程进行面向连接通信，那么本地节点客户必须首先在低层传输驱动程序中打开一个传输地址，并且接着打开一个连接端点。即，该客户要两次调用ZwCreateFile，第一次打开一个地址，其次打开一个连接端点。

当打开连接端点时，客户在EA（扩展属性）缓冲区参数中向ZwCreateFile传递客户提供的连接环境。在EA缓冲区中，客户设置EaName成员为系统定义的TdiConnectionContext，其后紧跟的EA值通常是客户分配的环境区域地址，客户将在该区域保存要建立的端端连接的状态。

当ZwCreateFile调用成功并向客户返回一个文件句柄，并且该客户也通过ObReferenceObjectHandler获得了该文件句柄的指针之后，就能够向低层传输器提交确定的TDI_XXX IOCTL请求和远程节点对等实体建立端端连接。

首先，客户必须通过向低层传输器提交TDI_ASSOCIATION_ADDRESS请求的方式，在连接端点和其打开的传输地址之间建立联系，该请求用TdiBuildAssociation进行设置。

关于设置TDI_XXX IOCTL IRP的更多信息可参阅5.3节。关于如何建立端端连接可参阅5.5节。

当端端连接已经断开并且客户不再进行任何面向连接通信时，客户必须关闭连接端点，该操作通过传递ZwCreateFile返回的文件句柄，调用ZwClose来完成，后面5.12节有详细描述。

5.3打包并提交IOCTL请求

图5.3显示了TDI客户是如何设置并提交对其低层TDI传输器的TDI_XXX IOCTL请求的。

图5.3 准备IOCTL IRP并产生请求

核心模式客户使用第四章讲到的TdiBuildXxx宏，为与低层传输驱动程序进行通信，准备其IOCTL IRP。客户能够从网络高层获取IRP，也可以调用TdiBuildInternalDeviceControlIrp为自己分配IRP。

接下来调用TdiBuildXxx宏在IRP的MinorFunctionCode中设置合适的TDI_XXX，并将MajorFunctionCode设为IRP_MJ_INTERNEL_DEVICE_CONTROL ，另外，也要为每个TdiBuildXxx宏调用设置IRP中的客户提供的以及与IOCTL相关的其他参数。例如，为了建立前面提到的TDI_ASSOCIATION_ADDRESS请求，当使用TdiBuildAccept时，客户将要传递代表连接端点的文件对象指针和代表传输地址的文件对象句柄。

IRP设置好后，客户调用IoCallDriver向其低层传输器提交该IOCTL请求。传输器的TdiDispatchInternelDeviceControl例程接收客户通过IoCallDriver提供的IRP，通常，为了进行进一步的处理，还要向内部驱动程序函数转发该客户的请求。传输器处理请求操作，用操作结果设置IRP的I/O状态区，并在客户请求已被满足时用IRP调用TdiCompleteRequest（或者IoCompleteRequest）。

如果客户在调用TdiBuildXxx宏时提供了IoComplete例程的入口点，那么当传输器完成请求操作时，将调用客户的IoComplete例程并随同客户提供的TDI_XXX IOCTL请求一起调用TdiCompleteRequest（或者IoCompleteRequest）。

5.4设置和查询信息

图5.4显示了TDI客户是如何查询其低层传输器的特征的，以及如何请求其低层传输器将其状态数据设为客户指定值的。

图5.4 设置和查询传输器信息

TDI客户能够查询传输驱动程序信息，像连接状态信息、特定传输地址的活动性、传输相关的数据报大小和数量限制、驱动程序统计数据以及内部发送接收缓冲区大小（如果传输器在内部对数据进行缓冲的话）。客户也能设置低层传输器的一些状态信息，尽管客户不能请求设置超出低层传输器确定的尺寸限制的值。

如果客户想要查询关于打开传输地址和连接端点的信息，可以向低层传输器提交TDI_QUERY_INFORMAION请求，该请求用TdiBuildQueryInformation进行设置。当使用该宏时，客户将传递指向代表地址的文件对象或者连接端点的指针，以及指向客户提供缓冲区的指针，传输器在该缓冲区中返回请求信息。

为了设置传输器信息，客户发布TDI_SET_INFORMAION请求，该请求用TdiBuildSetInformation进行设置。

对于上述操作，客户也可将系统定义的TDI_QUERY_XXX值作为TdiBuild..Information 宏的Qtype或者Stype参数传递，来区分查询和设置的信息类型。

要查询和设置传输地址或连接端点以外的信息，客户在提交其查询或设置信息请求之前必须首先打开TDI驱动程序中的控制信道。例如，查询低层传输器的用于无连接数据通信的广播地址信息或者查询传输器的最新统计数据信息的客户要使用控制信道。

为了打开控制信道，客户用空的EaBuffer指针（NULL）调用ZwCreateFile。该调用促使I/O管理器创建客户进程相关的代表控制信道的文件对象，并用包含客户向ZwCreateFile提供的参数的IRP来调用TDI传输驱动程序的TdiDispatchCreate例程。TdiDispatchCreate给出空缺EA信息，并且如果调用成功，传输器将为打开的控制信道和该客户设置内部状态。

作为打开代表控制信道的文件对象的另一选择，客户可以调用IoGetDeviceObjectPointer，该调用返回指向指定设备对象和相关文件对象的指针。

当ZwCreateFile调用成功向客户返回一个文件句柄，并且该客户也通过ObReferenceObjectHandler获得了该文件句柄的指针之后，就能够向低层传输器提交TDI_QUERY_INFORMAION或者TDI_QUERY_SET_INFORMAION IOCTL类型的请求。

当查询和设置信息操作完成之后，客户必须关闭打开的控制信道，该操作通过传递ZwCreateFile返回的文件句柄调用ZwClose来完成，也可通过向ObDereferenceObject传递IoGetDeviceObjectPointer返回的文件对象指针来实现，在后面5.13节进行讨论。

5.5建立端端连接

网络上分立的TDI客户和其各自的传输器相互合作在它们之间建立端端连接。在连接建立之前，各自节点上的客户必须完成以下操作：

1、 打开一个传输地址

2、 打开一个连接端点

3、 通过向低层传输器提交TDI_ASSOCIATION_ADDRESS IOCTL请求的方式在打开的连接和打开的传输地址之间建立联系

然后，通常当一个客户正在侦听（被动地）即将建立的连接提议时，另一个客户向该侦听客户发送建立连接提议。客户也可以向其低层传输器注册ClientEventConnect处理程序（参阅5.1节）。当从远程节点发向客户开放传输地址的连接提议到达时，传输驱动程序将调用ClientEventConnect。

请求对远程节点的连接

图5.5显示了本地节点TDI客户是如何发起对远程节点对等进程的连接提议的。

图5.5 请求连接

本地节点客户通过向低层传输器提交TDI_CONNECT请求，向远程节点对等进程发送连接提议，该请求通过TdiBuildConnect进行设置。

本地节点传输器根据其客户的连接请求确定远程节点目标地址并向相应的远程节点传动器发送该连接提议。

远程节点传输器通知客户关于内入连接提议信息，这可以通过响应客户提交的TDI_LISTEN请求或者调用已注册的ClientEventConnect处理程序来实现。

如果远程节点没有侦听或者没有响应连接请求，本地节点传输器将使该连接IRP失效。如果两边的传输器都支持连接延迟接受功能，远程节点将既可以接受该连接提议也可以拒绝该连接提议。

接受远程节点的连接提议

图5.6显示了本地节点客户是如何侦听远程节点对等进程的连接提议的。

图5.6 接受连接提议（侦听操作）

为了建立端端连接，一个客户发布连接提议，另一个客户则指示其低层传输器它正在等待连接提议。

本地节点客户可以通过向低层传输器提交TDI_LISTEN请求的方式被动地侦听引入连接提议，该请求通过TdiBuildListen进行设置。当设置侦听IRP时，客户能够指定发送连接提议的远程节点传输地址。如果传输器支持连接延迟接受功能，那么客户能够指示传输器要么立即接受特定远程节点地址来的连接提议，要么允许客户检查该提议并决定是够接受。

当传输器收到TDI_LISTEN请求之后，将监视来自远程节点的连接提议，该连接提议被发向客户打开的传输地址。当收到指定远程节点地址的连接提议时，传输器将复制提议信息到相应的缓冲区，该缓冲区是本地节点客户通过TDI_LISTEN请求提供的，同时结束IRP返回正在侦听客户。

如果客户指示传输器立即接受引入连接提议，那么当其传输器通知远程节点传输器，和远程节点客户的端端连接已经建立时，侦听请求就完成了。实际上，甚至在知道侦听IRP完成之前，本地节点客户就已经建立了和远程节点对等实体的端端连接，并且该客户立刻就可在该端端连接上接收数据。换句话说，传输器的侦听请求完成操作通知本地节点客户关于连接提议信息，客户要么通过发布TDI_ACCEPT请求接受该提议，要么通过发布TDI_DISCONNECT请求拒绝该提议。本地节点客户使用TdiBuildAccept或者TdiBuildDisconnect为连接延迟接受操作，设置将要提交给低层传输器的IRP。

如果本地节点客户通过事件处理和低层TDI传输驱动程序进行通信，接受连接提议操作甚至将会更加简单。当传输器接收到一个远程节点客户发给本地客户打开的传输地址的连接提议时，传输器调用本地节点客户注册的ClientEventConnect处理程序，并传递提议客户的传输地址和接收到的连接数据。然后，ClientEventConnect将决定立即接收还是拒绝该连接提议。

5.6发送和接收面向连接数据

网络分立节点上的TDI客户必须首先建立端端连接，才能发送和接收面向连接数据。在一个客户向另一客户发送数据之前，各自节点的每个客户必须完成以下操作：

1、 打开一个传输地址

2、 打开一个连接端点

3、 通过向低层传输器提交TDI_ASSOCIATION_ADDRESS IOCTL请求的方式，在打开的连接和打开的传输地址之间建立联系

4、 一个客户发布连接提议，另一个接受连接提议，建立两者之间的端端连接

事实上，在TDI_LISTEN IRP完成并返回之前，只要本地节点传输器向远程节点传输器发送连接接受帧，接受连接提议的客户就可以从远程节点对等实体接收数据了。

当低层传输器建立端端连接之后，客户就能够在网络相互发送数据了。

在端端连接上发送数据

图5.7显示了核心模式客户是如何通过其低层传输器在端端连接上向远程节点对等实体发送数据的。

图5.7 向远程节点对等实体发送数据

本地节点TDI客户通过发布发送请求，实现从其连接端点向远程节点连接端点的数据传送。为实现该功能，本地节点客户向其传输器提交TDI_LISTEN IOCTL请求。该IRP由客户调用TdiBuildSend进行设置，其中包含有一个指向客户提供缓冲区的指针，缓冲区容纳面向流或面向消息的TSDU，其长度可以是小于TDI传输器允许的最大尺寸的任意值。如果传输器支持加速传送，那么其客户就能够请求将TSDU作为加速数据传输，该加速数据将先于已经提交的但当前又尚未完成的标准数据进行发送。如果传输驱动程序支持内部缓冲，那么其客户可以请求非块式发送。

客户用TDI_LISTEN IRP调用IoCallDriver，向低层传输器的TdiDispatchInternelDeviceControl例程转发IRP。该例程检查传输器I/O栈中IRP的MinorFunction代码，通常要调用驱动程序内部与发送相关的函数对该IRP进行进一步的处理。对于发送请求，如果此前客户已经提交了其他的发送请求，而该请求又尚未通过网络向远程节点传输，那么驱动程序内部函数通常会对新的IRP进行排队，同时传输器总是将加速数据发送请求排在标准TSDU发送请求之前。不管是标准的还是加速的发送请求，传输器总是以先入先出（FIFO）的原则传输队列中客户请求的发送任务。在完成客户提交的TDI_SEND IRP之前，传输器要么将客户提供的数据复制到内部缓冲区，要么在网上发送指定的数据。

如果由于内部缓冲区空间不足使得非块式发送请求失败，那么当传输器又有可用的发送缓冲空间时，驱动程序将调用客户注册的ClientEventSendPossible处理程序。ClientEventSendPossible将再次提交前面失败的TDI_SEND请求。

从端端连接上接收数据

图5.8显示了核心模式客户是如何通过其低层传输器从端端连接上的远程节点对等实体接收数据的。

图5.8 从远程节点对等实体接收数据

本地节点客户可以通过向低层传输器提交TDI_RECEIVE请求接收标准的或者加速的TSDU。该IRP由客户调用TdiBuildReceive进行设置，其中包含有一个指向客户提供缓冲区的指针，该缓冲区用来复制接收来自远程节点对等实体的TSDU的全部或部分数据，其长度可以是小于TDI传输器允许的最大尺寸的任意值。

客户用TDI_RECEIVE IRP调用IoCallDriver，向低层传输器的TdiDispatchInternelDeviceControl例程转发该IRP。该例程检查传输器I/O栈中IRP的MinorFunction代码，通常要调用驱动程序内部与接收相关的函数对该IRP进行进一步的处理。驱动程序内部函数向客户提供的缓冲区复制接收数据，直到缓冲区满或者接收的TSDU数据复制完毕。

接收时，加速数据优先于标准数据。如果当传输器处理客户提交的对标准数据的接收请求时，有远程节点的加速TSDU到达，那么传输器将结束该IRP并和已经复制到客户提供缓冲区的标准TSDU一起返回客户，接着传输器对接收的加速数据进行处理，加速数据处理完后，客户必须发布新的TDI_RECEIVE请求获取标准TSDU的其余部分。

客户也可以以事件通知的方式从低层TDI传输驱动程序接收来自远程节点对等实体的数据。对这些通知，驱动程序删除从远程节点接收到的TSDU的传输层报头，并调用客户注册的ClientEventReceive、ClientEventChainedReceive、ClientEventReceiveExpedited或者ClientEventChainedReceiveExpedited处理程序。然后，客户的事件处理程序能够尽可能的复制更多的数据。如果ClientEventReceive或者ClientEventReceiveExpedited没有接收完全部数据，可以执行以下的某一个操作：

n 立即返回没被接收状态，实际上是告诉传输器接收到的TSDU不是客户需要的。

n 产生另一个TDI_RECEIVE请求获取TSDU数据的其余部分。

n 依靠接下来的驱动程序接收事件通知获取接收数据的其余部分。

低层传输器总是只赋予ClientEventChainedReceive和ClientEventChainedReceiveExpedited处理程序对完整TSDU的只读访问能力，因此这些例程不必向低层传输器发布一系列的TDI_RECEIVE请求，也不必处理接收到的TSDU的局部指示。然而客户应该尽可能快的调用ClientReturnChainedReceives向NDIS 微端口返还该指示相关的资源。

关于注册ClientEventXxx处理程序的更多信息，请参阅5.1节。

5.7发送和接收无连接数据

只要核心模式客户成功地打开了低层TDI传输器的一个传输地址，就能够在网上进行无连接通信。

为了接收无连接数据，客户必须向低层传输器注册ClientEventReceiveDatagram处理程序和可能的ClientEventChainedReceiveDatagram处理程序，或者向低层传输器发布显式的TDI_RECEIVE_DATAGRAM请求。

关于打开传输地址和注册该地址的事件处理程序的更多信息，请参阅5.1节。

发送数据报

图5.9显示了TDI客户是如何向远程节点地址发送数据报的。

图5.9 发送数据报

如图中所示，发送数据报和发送面向连接数据是相似的。然而，为发送数据，本地节点客户将向其低层传输器提交依附于一个开放传输地址的TDI_SEND_DATAGRAM请求，代替前面5.6节所述的在端端连接上的发送请求。

TDI_SEND_DATAGRAM IRP请求低层传输器向特定远程节点传输地址上的数量未知的客户发送客户提供的数据报。

本地节点客户（拥有打开的传输地址）能够使用TdiBuildSendDatagram对上述请求进行设置。客户提供的缓冲数据长度可以是小于TDI传输驱动程序为数据报发送提供的最大缓冲空间的任意值（包括目标节点地址）。客户能够通过像5.4节所述的那样，查询低层传输器来确定该最大值限制。

接收数据报

图5.10显示了核心模式客户是如何通过其低层传输器接收来自远程节点的数据报的。

图5.10 接收数据报

如图中所示，接收数据报与接收面向连接数据是相似的。然而，为接收数据，本地节点客户将向其低层传输器提交依附于一个开放传输地址的TDI_RECEIVE_DATAGRAM请求，代替前面5.6节所述的在端端连接上的接收请求。

然而，本地客户能够接收任何远程节点客户发送的数据报，该远程客户指定本地客户打开的传输地址作为数据报的发送目标，其他打开相同传输地址的客户也能够接收该数据报。

TDI_RECEIVE_DATAGRAM IRP请求低层传输器返回来自远程节点客户的数据报，该远程客户向本地节点客户打开的传输地址发送了数据，本地节点客户能够使用TdiBuildReceiveDatagram对该请求进行设置。客户提供的缓冲区长度可以是小于TDI传输驱动程序为数据报接收提供的最大缓冲空间的任意值（包括代表打开的传输地址的文件对象指针）。因为TDI传输器不会对数据报进行分段，所以本地节点客户通常只要发布一次接收数据报请求就能接收一个数据报。

当上述请求完成时，传输驱动程序复制接收数据报到客户提供的缓冲区，并随同完成的IRP返回远程节点发送数据报的客户的传输地址。如果接收数据报长度超出缓冲区大小，那么传输器将返回尽可能多的数据并丢弃剩余数据。

客户也能够以低层TDI传输驱动程序的事件通知方式接收来自远程节点的数据报。对这些通知，驱动程序删除从远程节点接收的TSDU报头并调用客户注册的ClientEventReceiveDatagram或者可能的ClientEventChainedReceiveDatagram处理程序。ClientEventReceiveDatagram能够执行以下的某一个操作：

n 立即返回没被接收状态，实际上是告诉传输器接收到的TSDU不是客户需要的。

n 向内部缓冲区复制尽可能多的数据并返回控制，如果传输器仅仅提供了部分数据，则为TSDU的其余部分再生成一个TDI_RECEIVE_DATAGRAM请求。

n 复制TSDU的全部数据到内部缓冲区并返回控制。

如果接收数据报指示发生之后，客户没有复制数据或者没有返回IRP，那么非缓冲式传输驱动程序能够丢弃接收数据，而缓冲式传输驱动程序则保存一定数量的数据报信息，客户随后可通过显式的TDI_RECEIVE_DATAGRAM请求获取该信息。

低层传输器总是赋予ClientEventChainedReceiveDatagram处理程序对完整TSDU的只读访问能力，因此该例程不必向低层传输器发布TDI_RECEIVE_DATAGRAM请求，也不必处理接收到的TSDU的局部指示。然而客户应该尽可能快的调用ClientReturnChainedReceives向NDIS 微端口返还该指示相关的资源。

关于注册ClientEventXxx处理程序的更多信息，请参阅5.1节。

5.8面向连接和面向无连接传输

TDI在传输层提供面向连接服务，以最小的代价提供最高质量的服务。根据相关驱动程序协议的特点，该服务包括客户数据打包、排序、应答、重发、流量控制和错误恢复。

TDI也提供无连接的数据报服务。然而，该服务是轻量的：不提供纠错重发和流量控制功能。通常，传输层一般不对数据报进行分割或重组。仅就TDI传输器来说，无连接数据传输效率是最高的。

数据报发送本质上是一种不可靠数据通信方式。发送客户无法确定有多少或者哪一个远程节点客户已经打开了远程节点目标地址，也无法确定当前是否有远程节点传输器在接收数据报。此外，本地节点TDI驱动程序可以任由传输驱动程序设计者确定对数据报进行丢弃或者复制操作。相反，当端端连接有效时，本地节点传输器要负责在端端连接上重发数据，直到发送数据被远程节点接收并且已由远程节点传输器进行确认为止。

就像数据报发送一样，数据报接收操作也是不可靠的。即本地节点TDI驱动程序可以任由传输驱动程序设计者确定对数据报进行丢弃或者复制操作。相反，当端端连接有效时，本地节点传输器要负责提供端端连接上的接收数据，直到发送数据被其客户接收（或者拒绝）并向远程节点传输器进行确认为止。

端端连接将一直保持有效，直到出现后面5.11节要讨论的断连操作或者低层传输器的超时逻辑确定远程节点不能响应为止。

5.9请求传输相关操作

图5.11显示了TDI客户是如何向低层TDI驱动程序进行传输相关请求的，如果传输器定义了这些TDI扩展操作的话。

图5.11 请求传输相关扩展操作

TDI客户能够向TDI传输驱动程序发送特定的或专有的扩展请求，TDI传输驱动程序为这些扩展操作定义了一组传输相关操作代码。这些关于打开地址、开放连接端点或者打开的控制信道的扩展操作仅仅适用于呼叫客户，对其他的任何TDI传输器客户或驱动程序都是不适用的。

为了请求上面的传输器定义操作，客户首先必须像5.1节、5.2节或者5.4节所说的那样打开相关地址、连接端点或者控制信道，然后，客户使用TdiBuildAction，与客户提供的包含传输器定义操作代码和请求操作的相关参数一起，设置TDI_ACTION请求。

5.10接收错误通知

在对客户提交的IRP进行响应时，传输驱动程序在返回的状态码中将出错情况通知TDI客户。当向客户提供相关请求的错误信息时，客户将很难保存失败IRP的计数，也很难使用内部逻辑确定低层驱动程序是否处在一种将会使客户以后的网络I/O操作不可靠的状态。

为了接收关于低层驱动程序或者低层物理介质的意外错误状态的通知，客户可以像5.1节所说的那样向低层传输器注册ClientEventError或者ClientEventErrorEx处理程序。然后，如果传输器自身或者传输器依赖的任何低层驱动程序进行网上客户通信时发生错误，传输器将调用ClientEventError(Ex)。该调用将通知其客户，客户打开的传输地址上的网络I/O操作不再可靠（或者有时是可能变得不可靠）。接着，客户能够通知其更高层客户关于网络失效情况，同时释放所有TDI客户为相关传输地址上的未决操作分配的资源。

5.11断开端端连接

图5.12显示了TDI客户是如何释放端端连接的。

图5.12 缺

断开连接操作本质上是与特定传输器相关的。当面相连接TDI发起断连请求时，连接上的两个节点可能都要参与断连操作。即，当一个客户发起断连请求时，远程节点客户可能必须响应该请求。

断连操作期间，TDI传输器通常将拒绝该开放连接端点上的内入请求，并终止指定连接端点上的所有活动，除非两个传输器都支持可控断连功能，并且发起客户请求向其中一个提交断连请求。

如图5.12所示，端端连接上的一个客户能够向低层传输器通过提交TDI_DISCONNECT请求的方式发起断连操作，该请求可用TdiBuildDisconnect进行设置。当传输器完成对该客户请求的处理时，将通知远程节点传输驱动程序进行断连操作，同时该传输器将对端端连接上的客户提交的I/O请求返回一个合适的状态码。

如果响应客户注册了ClientEventDisconnect处理程序，那么当断连请求发生时，将调用该处理程序通知其客户。然后，ClientEventDisconnect通过向低层传输器提交TDI_DISCONNECT请求来确认断连操作。该通知使得客户能够及时地清除为端端连接分配的状态。

然而，断连操作并没有关闭客户打开的连接端点或者传输地址，当TDI_DISCONNECT请求完成之后，客户能够重用这些代表其低层传输器打开资源的文件对象，客户也可以像5.5节所说的那样再次向网络上的远程节点发送连接提议。除非客户像5.12节和5.13节所说的那样，关闭了代表其各自连接端点的文件对象和相关的传输地址，否则对客户向低层传输器提交的IOCTL请求来说，相关资源仍然是可用的。

5.12关闭连接端点

图5.13显示了TDI客户是如何关闭连接端点的。

图5.13 关闭本地节点的连接端点

当端端连接断开后，客户就可以关闭连接端点了。

当客户不再使用连接端点时，必须进行如下所述操作关闭连接端点：

n 用ObReferenceObjectByHandler返回的文件对象指针来调用ObDereferenceObject。

n 用在连接端点打开时ZwCreateFile返回的文件句柄调用ZwClose。

然后，I/O管理器向传输器的TdiDispatchCleanUp例程和随后的TdiDispatchClose例程提交IRP。

这些传输器例程将立即关闭连接端点，并释放所有相关的传输驱动程序资源。TdiDispatchCleanUp也将通过向相应的远程节点发送断连请求的方式终止与该连接端点相关的任何活动连接。

就像前一段讨论所表示的，在产生关闭连接端点请求之前，TDI客户没有必要将连接端点和相关传输地址分离。如果必要，低层传输器可以模拟该分离效果。

然而，在关闭连接端点之前，客户也能够通过向传输器提交TDI_DISCONNECT_ADDRESS请求的方式，显式的对连接端点和相关传输地址进行分离，该请求可通过TdiBuildDisconnectAddress进行设置。

例如，客户可以生成分离地址请求，并将开放连接端点与其他打开的传输地址进行重新联合。

5.13关闭传输地址和控制信道

在关闭所有相关连接之后，TDI客户就可以关闭当前打开的传输地址了。

当客户不再使用打开的传输地址或者控制信道时，必须像下面所述的这样释放该对象：

n 用ObReferenceObjectByHandler返回的文件对象指针来调用ObDereferenceObject。

n 用在连接端点打开时ZwCreateFile返回的文件句柄来调用ZwClose。

相似的，TDI客户也能够关闭低层传输驱动程序中打开的任何控制信道。

如果客户是调用IoGetDeviceObjectPointer来打开代表控制信道的文件对象的，那么就必须向ObDereferenceObject调用传递ObReferenceObjectByHandler返回的文件对象指针，从而释放该文件对象。

然后，I/O管理器向传输器的TdiDispatchCleanUp例程和随后的TdiDispatchClose例程提交IRP。

这些传输器例程将立即关闭传输地址或者控制信道，并释放所有相关的传输驱动程序资源。例如，TdiDispatchCleanUp撤销将要关闭的传输地址上的所有未决请求，注销该地址上的所有ClientEventXxx处理程序，清除该地址的客户相关状态。如果该客户释放了为该传输地址分配的所有文件对象句柄，那么传输器也将释放该传输地址的内部状态。

当ZwClose返回客户之后，就不能再为前面打开的传输地址或者控制信道向低层传输器提交请求了。代表该地址或者控制信道的客户相关文件对象也将不复存在。


Windows Sockets的传输助手DLLS

    作为TCP／IP的应用程序编程接口，Windows Sockets已经成为Windows 2000网络传输的主要用户模式接口。除了TCP／IP，Windows Sockets动态链接库还与其他传输形式相连系，这些传输器以传输器特有的用户模式形式向Windows Sockets Helper DLLS(WSH)提供扩展。

本章描述了一个新的传输驱动程序所支撑的WSH DLLS，通过Windows Sockets支持应用程序的调用：

·Windows Sockets、传输驱动程序和特殊传输WSH DLL之间的结构关系总览。

·Windows Sockets和特殊传输WSH DLLS通信概况。

·建立WSH DLLS的配置注册。

·管理WSH DLLS的同步。

·WSH DLLS在连接和断开时如何支持网络中发送的额外信息。

·必需的WSHxx和可选的WSHxx功能总结。

6.1 Windows Sockets Helper DLL结构

WSH DLLS是由传输驱动程序开发者为方便Windows Sockets的使用而提供的用户模式组件。图3.1显示了这个结构的总貌。

图3.1　Windows Sockets Helper DLL结构

Windows 2000提供了一个DLL,msafd.dll,它是Socket服务的提供者。当传输驱动程序安装时，同时也会安装特殊传输WSH DLL，网络设置将自动把msafd.dll配置为该WSH DLL的服务提供者。在应用程序调用Windows Sockets函数时(如WSASocket), 作为服务提供者 ,MSAFD将处理这个调用，并调用合适的WSH DLL以寻求必要的帮助。一些Windows Sockets函数不需要任何WSH DLL的帮助。例如，在连接已建立的情况下发送和接受数据不需要WSH DLL。在此情况下，MSAFD通过调用Win32函数和传输驱动程序进行通信。

    然而对于依赖于特殊传输特性的函数调用，或传输驱动的实现不统一时，传输器专用的WSH DLL可以用来消除这些不确定性。例如，WSAJoinLeaf将一个Socket加入到已建立的多点会话中。每个传输器各自实现多点会话中一个连接的加入。结果，MSAFD调用合适的WSH DLL，该DLL根据特殊传输实现支持Socket接口，为了支持对WSAJoinLeaf的调用，MSAFD调用WSH DLL的WSAJoinLeaf函数验证请求的选项，并用合适的信息调用传输器，从而把一个socket增加到多点会话中。

    如果一个传输器支持Windows Sockets2的新特性，包括多点会话Socket，Socket集群，和Sockets地址的逻辑表示，相应的WSH DLL必须输出Windows Sockets2函数，这将在6.3节中总结。

6.2用WSH DLL通信

每个支持Windows Sockets应用程序的传输器都支持一个用户模式的WSH DLL，用以解释网络地址和处理6.1节所列结构的Socket选项。例如，一个应用程序调用Socket时，它说明了地址族、Socket类型和协议。这三个参数必须确定唯一的传输驱动程序来支持此Socket。Windows Sockets搜索应用程序提供的参数和注册表中WSH DLL的标准化配置是否匹配，如果它发现一个匹配，Windows Sockets通过MSAFD调用由WSH DLL输出的WSHXxx函数，否则，应用程序调用Socket失败。当找到一个匹配时，Windows Sockets调用WSH DLL中的LoadLibrary,然后调用GetProcAddress取回WSHXxx函数的入口。在查询WSH DLL支持的协议时，这些WSHXxx函数被调用，用以翻译地址，并调用getsockopt和setsockopt处理WSH支持选项。例如，当一个应用程序将选项传递给setsockopt（Windows Sockets不显式支持）时，将会调用WSHDLL处理这个选项。WSHDLL作必要的操作，对每个选项提供相应的传输器支持。

下面的协定允许socket地址（即应用程序定义的SOCKADDR结构）和传输驱动程序使用的TDI-defined地址转变。

·TDI地址类型，由TA_ADDRESS结构中的AddressType说明，它等同于socket地址结构中的地址族。

·TA_ADDRESS结构中的Address［］和socket地址结构中的sa_family的值相等。

6.3配置WSH DLL

注册表中有两个地方必须设置成WSH DLL的标准配置信息。

1.在HKEY_LOCAL_MACHINE＼SYSEM＼CurrentControlSet＼Services＼Winsock＼Parameters下是说明协议(传输驱动程序)列表的REG_MULTI_SZ入口值。存储在列表中的值和..＼CulrentControlSet＼Services下相应的传输驱动程序的键名相匹配。

2.对每个传输驱动程序来说，HKEY_LOCAL_MACHINE＼SYSTEM＼CurrentControlSet＼Sevices＼TransportDriverName＼Parameters有一个Winsock子键。Windows Sockets从Service键下它自己的键的路径中，..＼Winsock＼Parameters下列表中描述的传输器名字中，和传输器的..Parameters\Winsock键中形成一个名字串，从而打开那个键，该键必须包括以下值的项：

·Mapping 一个描述地址族，socket类型和协议参数三元组的REG_BINARY值。这个二进制数据格式是WINSOCK_MAPPING结构，在wsahelp.h中定义。

·HelperDllName 一个描述WSH DLL路径的REG_EXPAND_SZ值。

·MinSockaddrLength 以字节为单位，说明SOCKADDR的最小效值，它的类型是REG_DWORD。

·MaxSockaddrLength 以字节为单位,说明SOCKADDR的最大有效值, 它的类型是REG_DWORD。

把这个信息存储在..＼CurrentControlSet＼Services下两个不同的地方。将位于驱动程序自己的Parameters键下的信息本地化。..\Services＼Winsock＼Paramters下的信息提供了每个传输WSH DLL信息的指针，使得安装程序更容易地相互合作。

Windows 2000安装工具提供了一些函数，这些函数为传输驱动程序建立WSH DLL注册信息做了绝大多数工作。这些例程包括AddWinsockInfo和RemoveWinsockInfo.传输驱动程序安装脚本能调用AddWinsockInfo,传送传输驱动程序Services下的键名、特殊传输WSH DLL的完全路径名、SOCKADDR的最小和最长长度。AddWinsockInfo存储注册表中的标准信息。

传输器调用安装函数的例子，请看oemnxptc.inf,这是TCP／IP的安装脚本，也位于System目录下。

6.4WSH DLL同步

对每个socket，Windows Sockets都保持与WSH DLL的同步，也就是说，两个线程不能对同一个socket同时执行WSHXxx函数。从而，WSH DLL本身不需要做任何同步工作。

如果一个WSH DLL支持对全局状态信息的存取进行同步，它必须在内部对状态信息存取进行同步。Windows 2000提供了几个用户模式的同步机制，包括信号量，事件，和关键段。具体哪个同步机制取决于WSH DLL开发者。

6.5用WSH DLL支持连接和断开数据

一些传输器，如DECNet和OSITP4，支持连接和断开数据：不在正常的网络数据流中的额外数据。它随着连接和断开请求而发生。通常，连接和断开数据被用于应用程序级的版本协商。

TDI用TDI_CONNETION_INFORMATION结构支持连接和断开数据的传送，该结构包括UserDataLength,UserData,OptionsLength和Options。该结构在由核心模式的TDI客户发出的TDI连接、接受和断开请求中被传输。

然而，Windows Sockets不向connect函数提供连接数据的输入参数，也不向Shutdown和closesocket函数提供断开数据的输入参数。WSH DLL为调用Windows Sockets 的setsockopt和getsockopt函数的应用程序,增加对连接和断开数据的支持。

setsockopt 函数用来说明发送到远程结点的连接和断开数据。getsockopt函数，取到来自远程结点的连接和断开数据。详情请参见Windows.h中的SO_XXX选项。

一个应用程序如何在getsockopt和setsockopt中使用这些选项，取决于应用程序是个服务器还是客户(参见6.4.1节和6.4.2节)，以及应用程序如何使用连接数据。对于传输器而言，如DECNet有一个与上面不同的、预置的连接数据定义，WSH DLL对传输语义和Windows Sockets语义进行互译。

从Windows Sockets来看，连接和断开选项与连接和断开数据一样有效。这实际上是在连接和断开之前传送给传输器的数据缓冲区，这个缓冲区包括随着连接和断开而得到的数据。结果，应用程序常常把连接或断开选项和连接或断开数据以同一方式使用。仅有的不同是选项名多被作为传送给getsockopt和setsockopt的参数。例如，SO_CONNOPT代替了SO_CONNDATA。

6.5.1客户应用程序和连接数据

一个应用程序使用Windows Sockets和支持连接数据的传送器通信，此Windows Sockets使用支持连接数据的传输器，该应用程序在调用setsockopt时，应使用SOL_SOCKET作为级别，SO_CONNDATA作为选项名，一个指向连接数据缓冲区的指针作为选项值，这个连接数据是由传输器传送到远程结点的，数据与连接请求一同发送。所以在一个应用程序连接之前必须调用setsockopt。当应用程序调用connect时，这些连接数据将被发送。

1．在调用connect前，应用程序必须通知Windows Sockets为预期的连接响应数据预留多大空间。为做到这一点，应用程序使用选项级别SOL_SOCKET，选项名字SO_CONNDATALEN，和指向一个整数的选项来调用setsockopt,该整数说明了应用程序所期望的最大字节数。WindowsSockets分配这么大的缓冲区以容纳来自服务器应用程序的连接数据。

2．在connect成功完成后，应用程序能以选项组别SOL_SOCKET，选项名字SO_CONNDATA，和指向容纳响应连接数据的缓冲区的指针，调用getsockopt。然后，Windows Sockets将响应连接数据拷入应用程序缓冲区。

在应用程序创建响应连接缓冲区之后，Windows Sockets在此socket的生命周期中一直使用那个缓冲区。那就是说，在一个socket连接后和关闭前，应用程序在任何时候都可以取到该socket的响应连接数据。

6.5.2服务器应用程序和连接数据

服务器应用程序以两种重要方式使用来自客户应用程序的连接数据。

1．在发送响应之前，服务器应用程序接收客户的连接数据。

2．因为Windows Sockets不支持延迟连接接受(TDI可以)，服务器应用程序必须向所有客户发送相同的响应连接数据。

为了取到来自客户的连接数据，服务器应用程序用SO_CONNDATALEN选项调用setsockopt，使用Windows Sockets为连接数据保留缓冲区空间。连接数据缓冲区的大小对所有连接都是一样的，但Windows Sockets为每个连接分配独立的缓冲区。

在一个新连接的socket被accept后，服务器应用程序用SO_CONNDATA选项和一个足以容纳期望数据的、由服务器分配的缓冲区来调用getsockopt。Windows Sockets用来自客户的连接数据填充这个缓冲区。

为向客户发送响应连接数据，服务器应用程序用SO_CONNDATA选项和一个数据缓冲区，在连接被接受之前调用setsockopt(通常在listen之前)。此后，连接服务器的客户都会收到服务器提供的响应连接数据。服务器的响应连接数据在任何时候都可以用setsockopt来改变，用新的数据代替老数据。

6.5.3断连（disconnect）数据

客户和服务器应用程序以类似于发送和接收连接数据的方式发送断连数据。

客户应用程序使用SO_DISCDATA选项和一个断开数据（发送到远程结点服务器）的缓冲区，来调用setsockopt。客户应用程序调用shutdown断开socket的发送方时，或者应用程序调用closesocket时，发送这个数据。

为了取到断开数据，客户应用程序在远端关闭连接以前，必须用SO_DISCDATALEN选项调用setsockopt。这个选项说明了应用程序所期望的断开数据的数量。

在远程结点断开后，服务器提供的断开数据能以SO_DISCDATA选项调用getsockopt来获得。客户应用程序传递一个足以容纳断开数据的缓冲区，Windows Sockets将收到的断开数据拷入该缓冲区。

6.6WSH DLL函数总览

现在总结每个WSH DLL都必须提供的WSHXxx函数集。每个新传输器的供应者能在与传输特性相符的WSH DLL中实现这些函数。Windows Sockets信任所有WSH DLL。所以，WSHXxx函数中的bug会表现为Windows Sockets的bug.

下面概括了所有传输器专用的WSHDLL必须提供的函数集：

·WSHEnumProtocols返回WSH DLL支持的协议列表(Windows Sockets PROTOCOL_INFO结构))

·WSHGetsockAddrType解析一个socket地址。

·WSHGetsocketInformation在向getsockopt传递一个Windows Sockets不显式支持的选项时被调用。

·WSHGetWildCardSockAddr在Windows Sockets需要执行socket自动捆绑时被调用。

·WSHGetWinsockMapping返回地址族，socket类型和WSH DLL支持的协议参数三元组。

·WSHNotify把状态转换通知助手DLL。

·WSHOpenSocket在一个socket打开时调用。

·WSHSetsocketInfomation在setsockopt被传送一个Windows sockets不显式支持的选项时调用。为支持新的Windows Sockets2的叶\根、Socket、Socket群和Socket的逻辑表示，应实现下面可选的函数：

·WSHAddressToString返回一个用来显示的socket地址的逻辑字符串表示。

·WSHGetBroadcastSockaddr得到socket的有效广播地址。

·WSHGetProviderGuid返回助手DLL的协议的GUID

·WSHGetWSAProtocolInfo返回助手DLL支持的协议信息的指针

·WSHIoctl得到基于一般特殊控制码的操作的执行信息。

·WSHJoinJeaf执行任何协议专用的操作，这些操作在把一个socket加入多点会话时必须执行。

·WSHOpenSocket2为创建一个新Socket而执行协议专用的操作。如果这个函数被输出，它将代替WSHOpenSocket函数。

·WSHStringToAddress将一个socket的逻辑表示转成SOCKADDR结构。WSHXxx函数的更多信息，请参见在线“Network Drivers Reference”。


面向连接的网络驱动程序接口标准(NDIS)

网络驱动程序接口标准(NDIS)提供了对面向连接介质的支持，如异步传输模式(ATM)和ISDN。下面几节描述面向连接的NDIS：

·1.1面向连接环境

·1.2使用AFs，VCs，SAPs和Parties

·1.3服务质量

·1.4MCM与呼叫管理有何不同

·1.5面向连接的时间特性

·1.6面向连接的操作

1.1面向连接环境

NDIS支持如下面向连接的驱动程序：

·面向连接客户

·呼叫管理器

·集成微端口呼叫管理器(MCM)

·面向连接的微端口

图1.1显示了面向连接客户，呼叫管理器和微端口的结构。

图1.1 呼叫管理器的面向连接环境

图1.2显示了面向连接客户和集成微端口呼叫管理器(MCM)的结构

图1.2 集成微端口呼叫管理器的面向连接环境

一个面向连接微端口控制一个或多个网络接口卡(NICs)，并提供了面向连接协议(面向连接客户和呼叫管理器)和NIC硬件的接口。面向连接微端口执行的面向连接操作的总结，请参见1.6.1.3节。

呼叫管理器是为面向连接客户提供呼叫建立和断开服务的NDIS协议驱动程序。呼叫管理器使用面向连接微端口的发送和接收功能在网络实体之间交换信令消息，这些实体包括交换机和远程对等体。呼叫管理器支持1个或多个的信令协议，如由ATM论坛规范的ATM UNI3.1。呼叫管理器执行的面向连接的操作总结，请参见1.6.1.2节。

集成微端口呼叫管理器(MCM)是一个向面向连接客户提供呼叫管理器服务的面向连接微端口。虽然MCM向客户提供的功能和基于面向连接微端口的呼叫管理器一样，但由于MCM的呼叫管理器／微端口接口是内置的，因此对于NDIS来说是不可见的。MCM和呼叫管理的详细比较参见1.4节。

多个呼叫管理器和／或MCM在同一环境中并存，每个呼叫管理器或MCM能支持多个信令协议。

一个面向连接的客户使用呼叫管理器或MCM的呼叫建立和断开服务。面向连接的客户也能用面向连接的微端口或MCM的发送和接收功能发送和接收数据，由面向连接客户执行的面向连接的操作的总结，参见1.6.1.1。

一个面向连接的客户类似于无连接协议，因为它能在其上边界，向更高层应用程序提供它自己的网络和传输层服务。然而，不象无连接协议，面向连接的客户在其低层使用呼叫管理器和面向连接的微端口的服务，或者使用MCM的服务。

一个面向连接的客户可以是位于合法协议和面向连接NDIS之间的适配层。如IP／ATM和LAN Emulation(LANE)，两者调用呼叫管理器的服务建立连接特性。注意如此一个面向连接客户上层接口的定义已超过了NDIS文献的范围范畴。如果客户用作适配层，它的上层接口由协议定义，该协议需要由其提供对NDIS的适配功能。

1.2使用AFs，VCs，SAP和Parties

面向连接的驱动程序创建和使用

·1.2.1地址族(AFs)

·1.2.2虚连接(VCs)

·1.2.3服务访问点(SAPs)

·1.2.4Parties

当一个面向连接驱动程序注册一个AF或创建一个VC，SAP或Party时，它将为那个NDIS实体传递本地环境区的指针。NDIS向这个驱动程序，返回一个新注册的AF或新创建的VC，SAP或Party的句柄。

1.2.1地址族

一个地址族(AF)表示了如下驱动程序集之间的联系。

·面向连接客户，呼叫管理器，和下层的面向连接的微端口

·面向连接客户和信令协议专用的的MCM

地址族也说明了特殊信令协议，如ATM UNI3.1。

呼叫管理器或MCM通过在NDIS中注册地址族，向特殊信令协议公布其呼叫管理器服务。在能使用呼叫管理器或MCM提供的呼叫管理器服务前，面向连接的客户必须用呼叫管理器或MCM打开地址族。

地址族操作的更多信息，请参见1.6.2.1节和1.6.2.4节。

1.2.2虚连接

在一个本地系统中，虚连接(VC)是客户，呼叫管理器或MCM，微端口之间的一个端点。网络中，一个VC指两个通信端点的连接，如两个面向连接客户。

一块网络接口卡上同时可有许多VCs处于激活状态，允许网络接口卡同时向多个呼叫提供服务。每个连接可连到不同计算机的不同端点。

网络中的VCs在向客户提供的服务的类型上是不同的。例如，一个VC能提供单向或双向服务。服务质量(QoS)参数保证了特殊的性能阈值，如带宽和响应时间。在信令协议基础上VC的QoS是可协商的。更多的NDIS对QoS的支持信息，请参见1.3节。网络中的VC可以是交换VC(SVC)或永久VC(PVC)

·SVC在一个特殊呼叫需要时生成。例如，面向连接客户为了建立外出呼叫而创建一个VC。类似地，呼叫管理器或MCM为了向面向连接客户指示一个内入呼叫而建立一个VC。呼叫管理器或MCM必须和远端通信并协商VC参数。

·PVC由手动建立，并最终由使用配置工具的操作员删除，NDIS并不支持这个配置工具，。监视PVCS手动生成和删除的客户能使用OID_CO_ADD_PVC和OIO_CO_DELETE_PVC请求呼叫管理器或MCM从PVCs配置列表中增加或删除一个PVC。PVC的QoS由操作员定义，网络中是不可协商的。

在NDIS中，一个VC由微端口分配的资源组成，该资源保持了网络中一个VC的状态信息。这些资源包括存储缓冲区，事件和数据结构,但并不限于此。经面向连接客户请求，微端口为外出呼叫建立一个VC的环境；经呼叫管理器请求，微端口为内入呼叫建立一个VC的环境。更多的VCs生成信息，请参见1.6.3.1节。

    在一个VC能传输数据前，它必须由呼叫管理器或MCM激活。为激活一个VC，微端口或MCM为VC建立资源，并与NIC进行通信，使NIC准备接收或传送VC中的数据。更多的VC激活信息，请参见1.6.3.2节。

    当断开一个呼叫时，呼叫管理器或MCM使该呼叫使用的VC去活。VC去活的更多信息，参见1.6.3.3节。

    在一个呼叫断开后，VC的生成者(面向连接客户，呼叫管理器，或MCM)能或者删除ＶＣ，或者将其用于其他呼叫。关于删除ＶＣ的更多信息，请参见1.6.3.4节。

1.2.3 SAPs

服务访问点(SAP)定义了面向连接客户感兴趣的内入呼叫集，通过向呼叫管理器或MCM注册一个SAP，客户指示呼叫管理器或MCM向客户通知那个SAP的所有内入呼叫。

SAP总是用客户已开放的地址族注册。客户能向一个呼叫管理器或MCM注册多个SAPs。

注册或注销SAPs的更多信息，参见1.6.2.2节和1.6.2.3节

1.2.4 Parties

一个Party代表了一对多连接中多个叶节点的1个。当进行一个外出呼叫时，面向连接客户可以说明一个Party。这使得该呼叫成为一个多点呼叫，客户作为呼叫根，远程Party作为叶节点。然后，客户能请求其他远程Parties作为叶节点结点加入该呼叫。

关于增加Parties到一对多呼叫的更多信息，参见1.6.6.1节。从一对多呼叫中删除Parties的信息，参见1.6.6.2节。

1.3服务质量

SVC上一个呼叫发起者能说明该呼叫的服务质量(QoS)参数，根据所用的信令协议，呼叫管理器或MCM能协商网络实体的QoS，如网络交换机或远程客户。如果信令协议允许，面向连接客户也可以在决定，是否在接受一个内入呼叫时，请求改变QoS。

呼叫的QoS参数是CO_CALL_PARAMETRS结构。CO_CALL_PARAMETERS指向两个其他结构：

·CO_CALL_MANAGER_PARAMETERS,说明呼叫管理器参数，呼叫管理器和MCM用这个参数建立呼叫。

·CO_MEDIA_PARAMETERS，说明介质参数，呼叫管理器和MCM用这个参数建立呼叫。

    CO_CALL_MANAGER_PARAMETERS和CO_MEDIA_PARAMETERS都包含了种类参数(flags),此参数适用于所有使用这些参数的驱动程序。这些结构也都指向一个CO_SPECIFIC_PARAMETERS结构，这个结构说明了特殊呼叫管理器参数(在CO_CALL_MANAGER_PARAMETERS结构指向它时)或者特殊介质参数(在CO_MEDIA_PARAMETERS指向它时)。

QoS操作的更多信息，参见1.6.5.1节和1.6.5.2节。

1.4MCM和呼叫管理器有何不同

集成微端口(MCM) 也是一个面向连接微端口，同时向面连接客户提供呼叫管理器服务。MCM执行了面向连接微端口和呼叫管理器的所有面向连接功能。象所有的微端口一样，MCM必须调用NdisXxx和NIC硬件通信。MCM主要在两个方面不同于呼叫管理器:

·呼叫管理器是一个增加了呼叫管理器功能的NDIS面向连接协议驱动程序。MCM是增加了呼叫管理器功能的NDIS面向连接微端口驱动程序。

·呼叫管理器和微端口完全暴露于NDIS--那就是说，所有呼叫管理器和微端口之间的通信都经过NDIS。除了客户VCs的激活去活(VCs用来传送外出和内入的客户数据)，MCM的呼叫管理器部分和微端口部分的接口对于NDIS来说是不可见的。客户VCs的激活和去活必须通过NDIS来完成，因为NDIS保持着客户VCs的跟踪。

MCM和呼叫管理器的差异在下面几节中作进一步描述。

1.4.1初始化的不同

1.4.2对NdisXxx函数调用的不同

1.4.3虚连接的不同

1.4.1初始化的不同

呼叫管理器是一个NDIS协议，它遵守面向连接协议的初始化顺序，并有一个额外的步骤：在它的ProtocolBindAdapter处理程序中,在完成面向连接协议初始化步骤后(见第三部分)，呼叫管理器必须立即通过调用NdisCmRegisterAddressFamily注册地址族。在NdisCmRegisterAddressFamily函数中，呼叫管理器必须为每个绑定的NIC注册地址族。

由于MCM是一个微端口，它遵守面向连接微端口的初始化顺序，并有一个额外的步骤：在它的MiniportInitialize函数中，在完成微端口初始序列后(参见第二部分)，MCM必须通过调用NdisMCmRegisterAddressFamily来注册地址族。在NdisMCmRegisterAddressFamily中，MCM注册了它的呼叫管理器功能，将MCM与通常的面向连接微端口区分出来。虽然，在初始化过程中，MCM在调用NdisMCmRegesterMiniport时，仅对微端口处理程序注册一次，但是，它必须为每个NIC调用NdisMcmRegisterAddressFamily一次。

1.4.2对NdisXxx函数调用的不同

呼叫管理器和MCM调用不同的呼叫管理器函数集。呼叫管理器调用NdisCmXxx函数，而MCM调用NdisMCmXxx函数。MCM不调用面向连接客户和呼叫管理器调用的NdisCoXxx函数。相反，MCM调用下面的NdisMCmXxx函数：

·NdisMCmCreateVc而不是NdisCoCreateVc

·NdisMCmDeleteVc而不是NdisCoDeleteVc

·NdisMCmRequest而不是NdisCoRequest

·NdisMCmRequestComplete而不是NdisCoRequestComplete

MCM不需类似NdisCoSendPackets的调用，由于呼叫管理器和微端口的发送界面内置于MCM，因而对NDIS来说不可见。

1.4.3虚连接的不同

呼叫管理器用信令VCs向/从网络实体(如交换机)，发送／接收信令消息。呼叫管理器的信令VCs对NDIS是可见的。呼叫管理器必须通过调用NDIS创建，激活，去活，删除VCs。然而，一个MCM的信令VCs对NDIS是不可见的。MCM在内部执行这个操作。MCM必须调用NDIS执行VCs上的操作，从而发送和接受客户数据。NDIS保持着对客户VCs的追踪。

MCM既是呼叫管理器又是微端口的事实，意味着某些面向连接操作是冗余的。MiniportCoCreateVC和MiniportCoDeleteVC是冗余的，因此MCM不支持。对VC的操作通过如下函数进行：

·客户请求VC的生成和删除时，调用MCM的ProtocolCoCreateVC和ProtocolCoDeleteVC函数。

·MCM创建或删除VC时，调用NdisMCmCreateVC和NdisMCmDeleteVC

·MCM激活或去活VC时，调用NdisMCmActivateVc和NdisMCmDeactivateVc

MCM必须支持MiniportCoRequest函数以使客户可以查询或设置微端口信息，同时支持MiniportCoSendPackets函数处理来自客户的发送操作。

1.5面向连接的时间特性

面向连接的NDIS使用NIC的本地时间安排包传输和发送与接收包的时间戳。

面向连接的协议能使用OID_GEN_CO_GET_TIME_CAPS调用MiniportCoRequest,查询面向连接

微端口或MCM的本地计时能力。作为对此查询的反应，微端口或MCM返回如下信息：

·NIC上是否有个就绪的时钟

·NIC是否从网络连接中得到它的时间

·本地时钟的精确度

·NIC是否能用本地时间对接受到的包加时间戳

·NIC能否根据本地时间安排发送包的传输。

·NIC能否用本地时间对传送的包加盖时间戳。

为得到NIC的本地时间，面向连接协议可以用OID_GEN_CO_GET_NETCARD_TIME调用NdisCoRequest来查询面向连接微端口或MCM。面向连接微端口或MCM同步的返回本地时间，面向连接协议可以用来安排包传送。

发送或接收包的时间信息包含在包的带外(OOB)数据中。更多的信息，参见在线“Network Drivers Reference”中的NDIS_PACKET_OOB_DATA。

1.6面向连接操作

这节从全局角度描述面向连接操作。在每个面向连接操作描述中，包含了所有牵涉到的面向连接驱动程序，并总结了在那个操作中，驱动程序和NDIS调用的所有面向连接函数。

1.6.1面向连接操作总结

这节描述了由面向连接客户，呼叫管理器，微端口执行的面向连接操作。集成微端口呼叫管理器(MCM)执行呼叫管理器和面向连接微端口两者的面向连接操作。

1.6.1.1由客户执行的面向连接操作

面向连接客户执行如下的面向连接操作

·打开和关闭地址族

接收到来自NDIS的通知(表明呼叫管理器或MCM已经注册了一个地址族)时，面向连接驱动程序能用呼叫管理器或MCM打开那个地址族(参见1.6.2.1节)。客户然后就能使用呼叫管理器或MCM提供的呼叫管理器服务了。客户通过关闭地址族,释放它和呼叫管理器或MCM的联系（参见1.6.2.4节）。

·注册和注销SAPs

在用呼叫管理器或MCM打开一个地址族后，面向连接客户能用呼叫管理器或MCM注册一个或更多的SAPs(参见1.6.2.2节)。呼叫管理器或MCM将向客户指示所有与该SAP相关的内入呼叫。客户通过注销(deregister)来释放SAPs(参见1.6.2.3节)。

·增加和删除PVCs

当一个操作员手动配置或解除一个永久VC(PVC)时，面向连接客户能进行监测。作为对这个操作的响应，客户请求呼叫管理器或MCM将PVCs加入PVCs列表，或者从列表中删去一个PVC(参见在线DDK“Network Drivers Reference”的OID_CO_ADD_PVC，DDK_OID__CO_ADD_PVC_NR和OID_CO_DELETE_PVC)。

·进行外出呼叫

在进行外出呼叫之前，客户必须为此呼叫建立一个VC(参见1.6.3.1节)。然后客户可以进行外出呼叫（见1.6.4.1节）。为了进行点-多点呼叫，客户在进行呼叫时必须说明一个party。

·从一对多呼叫中增加或删除一个party

客户能将一个party加入一对多呼叫(参见1.6.6.1节)，并能从一对多呼叫中删除一个party(参见1.6.6.2节)。客户也能对一个内入呼叫作出反应，从而从一对多呼叫中删除一个party(参见1.6.6.3节)。

·接受或拒绝内入呼叫

客户能接受或拒绝某个SAP的内入呼叫，该SAP是客户先前向呼叫管理器或MCM注册的(参见1.6.4.2节)。

·为一个激活的VC协商呼叫参数

在信令协议允许的基础上，客户可以为一个激活的VC协商呼叫参数。客户能请求改变服务质量(QoS)(参见1.6.5.1节)，也能响应一个激活VC的QoS的改变请求。客户也能对来自远程party的QoS改变请求作出响应（参见1.6.5.2节）。

·发送和接受包

客户能通过面向连接微端口或MCM发送包，(参见1.6.7.1节)

客户能通过面向连接微端口或MCM接收包，(参见1.6.7.2节)

·发起VC的删除

客户能发起删除它所创建的VC。

·发起断开呼叫

客户能发起断开外出呼叫或它所接收的内入呼叫（参见1.6.8.1节）。

·查询或设置信息

客户通过一个绑定的呼叫管理器或MCM的呼叫管理器部分，来查询或设置信息。客户也能对来自呼叫管理器或MCM的查询和设置作出响应。(参见1.6.9.1节)

另外，客户可以通过绑定的微端口或MCM的微端口部分来查询或设置信息（参见1.6.9.1节）。

·输入微端口状态指示

客户能输入由面向连接微端口或MCM驱动程序指示的状态。(参见1.6.9.2节)

·重置NIC

面向连接客户可以使面向连接微端口或MCM重置它的NIC(参见1.6.10节)。

1.6.1.2由呼叫管理器执行的面向连接操作

呼叫管理器执行如下的面向连接操作

·注册和注销1个或多个地址族(参见1.6.2.1节)。通过注册一个地址族，呼叫管理器用NDIS注册了它的呼叫管理器函数入口，并且向面向连接客户公布了它的呼叫管理器服务(一个信令协议)。

·在面向连接客户的请求下注册和注销SAPs

呼叫管理器接收来自绑定的面向连接客户的请求，这些请求要求注册SAPs(参见1.6.2.2节)和注销SAPs(参见1.6.2.3节)。呼叫管理器代表客户在网络上发送信令消息以注册或注销SAPs。

·在面向连接客户的请求下创建一个外出呼叫

当面向连接客户作一个外出呼叫时，呼叫管理器为了建立一个连接，在必要时同网络控制设备进行通信。(参见1.6.4.1节)。如果远程party接受呼叫，呼叫管理器激活VC(参见1.6.3.2节)。

·向面向连接客户创建和指示一个内入呼叫

呼叫管理器向面向连接客户，指示所有该客户注册的SAP的呼叫。(参见1.6.4.2节)。在向客户指示内入呼叫前，呼叫管理器为这呼叫发起创建VC(参见1.6.3.1节)，然后发起这个VC的激活(参见1.6.3.2节)。

·传递改变QoS的请求

在信令协议基础上，呼叫管理器能传递来自本地客户的请求，该请求要求改变外出或内入呼叫的QoS(参见1.6.5.1节)，也能传递来自远程party关于呼叫QoS的改变请求(参见1.6.5.2节)。

·传递增加或删除parties的请求

呼叫管理器传递本地客户请求，该请求要求从一个一对多呼叫中增加或删除一个party(参见1.6.6.1节和1.6.6.2节)。呼叫管理器也传递远程party的内入请求，该请求要求将其从一对多呼叫中删除。(参见1.6.6.3节)。

·断开呼叫

应面向连接客户请求，呼叫管理器与网络控制设备进行通信来关闭呼叫。(参见1.6.8.1节)。应远程party请求，呼叫管理器向面向连接客户指示远程party关于关闭一个呼叫的请求(参见1.6.8.2节)。在断开呼叫过程中，呼叫管理器使VC去活(参见1.6.3.3节)。如果是呼叫管理器创建的VC(对一个内入呼叫而言)，呼叫管理器也能删除此VC(参见1.6.3.4节)

·查询和设置信息

呼叫管理器能查询或设置面向对象客户支持的信息(参见1.6.9.1节)。呼叫管理器也能响应来自绑定的面向连接客户的查询和设置操作。

另外，呼叫管理器也能查询或设置由微端口或MCM的微端口部分支持的信息。(参见1.6.9.1节)。

·输入微端口状态指示

呼叫管理器能从一个面向连接微端口输入状态信息(参见1.6.9.2节)。

·重置NIC

呼叫管理器可以使面向连接微端口重置它的NIC(参见1.6.10节)。

1.6.1.3由微端口执行的面向连接操作

除了控制NIC硬件，面向连接微端口执行如下的面向连接功能：

·发送和接收包

面向连接微端口代表面向连接客户或呼叫管理器发送和接收包(参见1.6.7.1节和1.6.7.2节)。

·创建(建立)VCs

应面向连接客户请求，面向连接微端口为一个外出呼叫VC分配和初始化资源。(参见1.6.3.1节)。应呼叫管理器请求，面向连接微端口为内入呼叫VC，或呼叫管理器用于发送／接收信令消息的VC，分配并初始化资源(参见1.6.3.1节)。

·激活VCs

应呼叫管理器请求，面向连接微端口与它的NIC通信，使NIC准备好通过VC接受或传递数据(参见1.6.3.2节)。

·去活VCs

应呼叫管理器请求，面向连接微端口和它的NIC通信，使NIC终止一个VC的所有通信(参见1.6.3.3节)。

·删除VCs

应面向连接客户请求，面向连接微端口收回客户建立的VC的资源(参见1.6.3.4节)。应呼叫管理器请求，面向连接微端口收回由呼叫管理器发起建立的VC的资源(参见1.6.3.4节)。

·查询或设置信息

面向连接微端口对面向连接客户或呼叫管理器的查询或设置操作作出响应。(参见1.6.9.1节)。

·指示状态

面向连接微端口能向面向连接客户和呼叫管理器指示它的状态变化或是NIC的状态变化。（参见1.6.9.2节）

·重置NIC

应面向连接客户，呼叫管理器，或NDIS的请求，面向连接微端口重置它的NIC(参见1.6.10节)。

1.6.2地址族和SAPs上的操作

呼叫管理器或MCM驱动程序必须用NDIS注册它的呼叫管理器入口,并且向面向连接客户发布呼叫管理器服务。呼叫管理器或MCM通过注册地址族来实现上述操作。

为了用呼叫管理器或MCM的呼叫管理器服务，面向连接客户必须用那个呼叫管理器或MCM打开一个地址族。为了接收内入呼叫，客户必须用呼叫管理器MCM注册一个或多个SAPs.

下面是关于地址族和SAPs的面向连接操作：

·1.6.2.1注册并打开一个地址族

·1.6.2.2注册一个SAP

·1.6.2.3注销一个SAP

·1.6.2.4关闭一个地址族

1.6.2.1注册并打开一个地址族

呼叫管理器必须为每个NIC注册一个地址族，通过NIC向面向连接客户提供呼叫管理器服务。类似地，MCM驱动程序必须向它管理的NIC注册一个地址族。通过注册一个地址族，呼叫管理器或MCM用NDIS注册了入口，并使得NDIS向所有绑定在这块适配卡上的客户，公布呼叫管理器或MCM服务。

如果面向连接客户可以使用呼叫管理器或MCM公布的服务，它就能用呼叫管理器或MCM打开一个地址族。

从呼叫管理器中注册地址族。

在呼叫管理器的ProtocolBindAdapter函数用NdisOpenAdapter绑定NIC微端口后，呼叫管理器调用NdisCmRegisterAddressFamily为此绑定注册一个地址族。(见图1.3)。

图1.3用呼叫管理器注册并打开一个地址族

调用NdisCmRegisterAddressFamily为呼叫管理器注册呼叫管理器入口，并公布管理器的特殊信令服务。每当呼叫管理器在调用ProtocolBindAdapter函数时，并用NdisOpenAdapter成功绑定一块NIC时，必须注册地址族。

呼叫管理器在它绑定的所有NIC驱动程序中，可以支持多个的地址族。呼叫管理器也能在它所绑定的单个NIC上绑定多个的地址族。在这种情况下，呼叫管理器必须为该绑定上的每个地址注册相同的入口。对于一个绑定某个NIC驱动程序的客户，只有一个呼叫管理器能为其提供某一种特殊地址族。

从MCM驱动程序中注册地址族

MCM驱动程序在用NdisMCmRegisterMiniport注册入口后，从MiniportInitiallize函数中调用NdisMCmRegisterAddressFamily一次，用NDIS注册它的呼叫管理器入口，并且发布它对面向连接客户提供的服务(参见图1.4)。

图1.4 用MCM注册和打开地址族

NIC微端口驱动程序有一个内置的面向连接信令支持，即使呼叫管理器是可利用的,也能把自己注册为一个MCM驱动程序。这时，MCM驱动程序取代那个NIC的呼叫管理器。

打开一个地址族

呼叫管理器或MCM调用Ndis(M)CmRegisterAddressFamily，使得NDIS调用面向连接客户的ProtocolCoAfRegisterNotify函数(如图1.3和1.4)。

ProtocolCoAfRegisterNotify 检查地址族以确定是否客户能使用CM或MCM服务,以及在呼叫管理器或MCM支持的协议基础上，客户是否能修改地址族数据。

如果客户可以利用呼叫管理器服务，ProtocolCoAfRegisterNotify为这个客户的每个地址族分配一个环境并调用NdisClOpenAddessFamily。NdisClOpenAddessFamily向NDIS注册了客户的面向连接入口点。

对NdisClOpenAddressFamily的调用,使NDIS调用呼叫管理器或MCM的ProtocolCmOpenAf函数(如图1.3、1.4所示)。ProtocolCmOpenAf保证客户传递一个有效的地址族，并分配和初始化资源，该资源对客户是必须的。ProtocolCmOpenAf也存储了一个由NDIS支持的NdisAfHandle,它代表了呼叫管理器和打开地址族的客户之间的联系。

ProtocolCmOpenAf能同步或异步地完成。为异步完成，呼叫管理器ProtocolCmOpenAf函数调用NdisCmOpenAddressFamilyComplete。MCM的ProtocolCmOpenAf函数调用NdisMCmRegisterAddressFamilyComplete。对于Ndis（M）CmRegisterAddressFamilyComplete的调用,使NDIS调用客户的ProtocolClOpenAfComplete函数，进而使该客户调用NdisClOpenAddressFamily。

如果客户成功调用NdisClOpenAddressFamily，NDIS将一个NdisAfHandle返回给客户，该NdisAfHandle代表了呼叫管理器和客户之间的联系。

如果一客户接受了内入呼叫，在ProtocolClOpenAfComplete函数中，它通常在成功调用NdisCIOpenAddressFamily之后，调用NdisClRegisterSap来注册一个或多个SAPs(参见1.6.6.2节)。

如果一客户作外出呼叫，根据外出呼叫的请求，在其ProtocolClOpenAfComplete函数中创建一个或多个VCs(参见1.6.3.1节)。

1.6.2.2注册一个SAP

如果一个客户接受了内入呼叫，它的ProtocolClOpenAfComplete函数(参见1.6.2.1节)通常通过NdisClRegisterSap，向呼叫管理器注册一个或多个SAPs。

图1.5呼叫管理器的客户注册一个SAP

图1.5用呼叫管理器注册一个SAP

图1.6MCM的客户注册一个SAP

图1.6用MCM注册一个SAP

通过调用NdisClRegisterSap，客户请求在某个SAP上的内入呼叫的指示。NDIS将客户支持的SAP信息传给呼叫管理器或MCM的ProtocolCmRegisterSap函数来验证其有效性。如果某个SAP已经正在使用，或者呼叫管理器或MCM客户不支持这个SAP说明，请求就会失败。

ProtocolCmRegisterSap与网络控制设备或其他特殊介质代理通信，为面向连接客户注册SAP。ProtocolCmRegisterSap也存储了一个代表SAP的NdisSapHandle（由NDIS支持）。ProtocolCmRegisterSap能异步或同步完成。为了能异步完成，呼叫管理器的ProtocolCmRegisterSap函数调用NdisCmRegisterSapComplete。MCM的ProtocolCmRegisterSap调用NdisMCmRegisterSapComplete.对函数Ndis(M)CmRegisterSapComplete的调用，使NDIS调用了客户的ProtocolClRegisterSapcomplete函数。

如果客户调用NdisClRegisterSap成功，NDIS向客户返回一个代表该SAP的NdisSapHandle。

在呼叫管理器注册了一个代表面向连接客户的SAP之后，将通过调用NdisCmDispatchIncomingCall,向客户通知那个SAP的内入呼叫。MCM调用NdisMCmDispatchIncomingCall(参见1.6.4.2节)。当SAP注册未决时（那是调用ProtocolClRegisterSapcomplete函数以前）客户就能接收那个SAP的内入呼叫了。

1.6.2.3注销SAP

面向连接客户用NdisClDeregisterSap注销一个SAP。

图1.7显示了一个呼叫管理器的客户注销SAP

图1.7 用呼叫管理器注销SAP

图1.8展示一个MCM客户注销SAP.

图1.8 用MCM注销SAP

对NdisClDeregisterSap的调用使得NDIS调用呼叫管理器或MCM的ProtocolClDeregisterSap函数。ProtocolClDeregisterSap与网络控制设备和其他特殊介质代理通信以注销SAP。另外，ProtocolClDeregisterSap必须释放所有动态分配给该SAP的资源。ProtocolClDeregisterSap可以同步或异步完成。为了能异步完成，呼叫管理器的ProtocolClDeregisterSap调用NdisCmDeregisterSapComplete。MCM的ProtocolClDeregisterSap调用NdisMCmDeregisterSapComplete。Ndis(M)CmDeregisterSapComplete通知NDIS和客户，呼叫管理器已经完成注销Sap的请求，这时ProtocolClDeregisterSap返回NDIS_STATUS_PENDING。

调用Ndis(M)CmDeregisterSapComplete使得NDIS调用客户的ProtocolClDeregisterSapComplete函数。调用ProtocolClDeregisterSapComplete指示，客户先前对NdisCmDeregisterSap的调用已经被呼叫管理器或MCM处理。

注意，客户可以注销SAP,而不影响那个SAP已被接受的内入呼叫，也不影响那个呼叫的VC。

1.6.2.4关闭一个地址族

面向连接客户调用NdisClCloseAddressFamily删除自身、呼叫管理器、NIC之间的联系。

图1.9显示了一个呼叫管理器客户关闭一个地址族。

图1.9 通过呼叫管理器关闭一个地址族。

图1.10显示了MCM客户关闭一个地址族。

图1.10 通过MCM关闭一个地址族。

客户在关闭某个绑定上所有打开的VCs(参见1.6.8.1节)和注销所有SAPs(参见1.6.2.3节)后，客户将从ProtocolUnbindAdapter函数中调用NdisClCloesAddressFamily。无论何时，当客户绑定的NIC被删除或重新配置时，NDIS调用客户的ProtocolUnbindAdapter函数。

调用NdisClCloesAddressFamily使得NDIS调用呼叫管理器或MCM的ProtoclCmCloseAf释放所有资源和／或使其去活，这些资源由呼叫管理器在其ProtocllCmOpenAf函数中分配。在客户关闭那个地址族后，呼叫管理器也应该撤消所有代表面向连接客户所作的操作。ProtocllCmCloseAf可以同步或异步完成。为能异步完成，呼叫管理器的ProtocllCmCloseAf函数调用了NdisCmCloesAddressFamilyComplete。MCM的ProtocllCmCloseAf函数调用NdisMCmCloesAddressFamily。如果在一个地址族上，有请求或连接处于打开状态，呼叫管理器对客户关闭地址族的要求作以下两种响应：

·呼叫管理器或MCM用NDIS_STATUS_NOT_ACCEPTED驳回请求。

·呼叫管理器或MCM返回NDIS_STATUS_PENDING。在客户关闭所有呼叫且注销的所有SAPs后，呼叫管理器或MCM可以关闭地址族，并调用Ndis(M)CmCloesAddressFamilyComplete

通知客户。这是用户所期望的响应。

对Ndis(M)CmCloesAddressFamilyComplete的调用,使NDIS调用面向连接客户的ProtocolClCloseAfComplete函数，该客户先前调用了NdisClCloseAddressFamily。

1.6.3 VCs上的操作

网络上的所有面向连接的通信都发生在虚连接（VCs）之上。VC操作在下述章节中描述：

·1.6.3.1创建VC

·1.6.3.2激活VC

·1.6.3.3使VC去活

·1.6.3.4删除VC

1.6.3.1创建VC

在作一个外出呼叫前，面向连接客户发起一个虚连接(VC)的创建。在向面向连接客户指示一个内入呼叫时，呼叫管理器或MCM也发起一个VC的创建。在VC创建并激活后，客户数据能在此VC上传递和／或接收。

呼叫管理器和MCM也能发起创建一个VC，在此VC上，网络成员(如网络交换机)之间可以交换信令消息。

由客户发起的VC创建

在调用NdisClMakeCall(参见1.6.4.1节)前,面向连接客户调用NdisCoCreateVC以发起VC的创建。图1.11显示了呼叫管理器的客户发起一个VC的创建。

图1.11 由客户发起的VC的创建（呼叫管理器）

图1.12显示了一个MCM客户发起VC的创建。

图1.12 由客户发起的VC的创建。(MCM)

当面向连接客户调用NdisCoCreateVC时，作为一个同步操作，NDIS调用呼叫管理器的ProtocolCoCreateVC函数和微端口MiniportCoCreateVC函数。(见图1.11)。NDIS向ProtocolCoCreateVC和MiniportCoCreateVC传递一个代表VC的NdisVCHandle.如果调用NdisCoCreateVc成功，NDIS向NdisCoCreateVc返回NdisVCHandle。

ProtocolCoCreateVC分配并初始化任何动态资源和结构，这些资源和结构被呼叫管理器用于即将激活的VC的后续操作上。MiniportCoCreateVc分配并初始化任何资源，这些资源被微端口用来维持VC的状态信息。ProtcolCoCreateVc和MiniportCoCreateVc贮存NdisVcHandle。

一个MCM的面向连接客户对NdisCoCreateVc的调用,使得NDIS调用ProtocolCoCreateVC函数(见图1.12)。在这种情况下，ProtocolCoCreateVC执行了VC资源的分配和初始化。这里没有MiniportCoCreateVC的调用(也有可能是内部的)，MCM不支持这个函数。

呼叫管理器发起的VC创建

在用NdisCmDispatchIncomingCall向面向连接客户指示一个内入呼叫前(参见1.6.4.2节)，呼叫管理器调用NdisCoCreateVC以发起一个VC的创建(参见图1.3)。

图1.3呼叫管理器发起的VC创建

当呼叫管理器调用NdisCoCreateVC时，作一个同步操作，NDIS调用注册SAP(呼叫正是通过这个SAP接收的)的面向连接客户的ProtocolCoCreateVC函数。NDIS也调用了微端口的MiniportCoCreateVC函数。NDIS向ProtocolCoCreateVC和MiniportCoCreateVC传递一个代表VC的NdisVCHandle。如果NdisCoCreateVC的调用成功，NDIS向NdisCoCreateVC返回NdisVCHandle。

MCM发起的VC创建

在用NdisMcmDispatchIncomingCall向面向连接客户指示一个内入呼叫前(参见1.6.4.2节)，MCM调用NdisMCmCreateVC以发起VC的创建(见图1.14)

图1.14 MCM发起的VC创建

当MCM调用NdisMCmCreateVC时，作为NdisMCmCreateVC返回前的同步操作，NDIS将调用面向连接客户的ProtocolCoCreateVC函数。NDIS向ProtocolCoCreateVC传递一个代表VC的NdisVCHandle。如果对NdisMCmCreateVc的调用成功，NDIS向NdisMCmCreateVc返回一个NdisVCHandle。

ProtocolCoCreateVc 分配并初始化所有动态资源和结构，客户用这些资源和结构来执行VC上的后续操作。ProtocolCoCreateVC也贮存NdisVCHandle。

注意，当MCM创建一个VC,用于网络成员间交换信令消息时，它并不使用NdisXxx创建VC。实际上，MCM不用NdisXxx来创建、激活，去活，或删除这样的VCs。相反，MCM在内部执行这些操作。这样的VCs对NDIS来说是不可见的。

1.6.3.2激活VC

在虚连接创建后(参见1.6.3.1节)，必须在能进行数据传递和接收前，激活该VC。呼叫管理器调用NdisCmActivateVC发起的激活(见图1.15)。

图1.15 呼叫管理器发起VC的激活

MCM通过调用NdisMCmActivateVC发起VC的激活(见图1.16)。

图1.16 MCM发起VC的激活

如果本地客户和远程party成功地商定了VC参数的改变，呼叫管理器或MCM可以再次激活一个活动VC。(参见1.6.5.1节和1.6.5.2节)。为了改变活动VC的呼叫参数，呼叫管理器或MCM可以为单个VC多次调用Ndis(M)CmActivateVc。

对于一个客户发起的外出呼叫，呼叫管理器或MCM在和远程party交换证实商定协议的包后，或者在交换机上成功建立呼叫之后，将立即调用Ndis(M)CmActivateVC。在调用Ndis(M)CmMakeCallComplete通知NDIS(和客户)外出呼叫已完成之前(参见1.6.4.1节)，呼叫管理器或MCM先调用Ndis(M)CmActivateVC。对于一个内入呼叫，呼叫器或MCM通常在成功调用NdisCo(M)CmCreateVC之后，以及调用Ndis(M)cmDispatchIncomingCall之前(参见1.6.4.2)，调用Ndis(M)CmActivateVC。

呼叫管理器对NdisCmActivateVc的调用,使NDIS调用NIC微端口函数MiniportCoActivateVc函数。MiniportCoActivateVc函数必须检查该VC的呼叫参数,以证实适配器能够支持请求的呼叫。如果呼叫参数是可接受的，MiniportCoActivateVc必须和适配器通信，从而使其准备好(例如，程序的接收缓冲区)通过虚连接传送或接收数据。如果呼叫参数不被支持，微端口驳回请求。

MiniportCoActivateVc 可以同步或异步地完成。调用NdisMCoActivateVCComplete使得NDIS调用呼叫管理器的ProtocolCmActivateVcComplete函数，ProtocolCmActivateVcComplete必须检查NdisMCoActivateVcComplete返回的状态,以确认虚连接已成功激活。如果微端口没有成功激活VC，呼叫管理器无法在VC上通信。ProtocolCmActivateVcComplete必须处理网络介质请求，在将控制返回给NDIS之前，确保虚连接准备好传送数据。

MCM对NdisMCmActivateVC的调用通知NDIS，它已在一个新生成的VC上创建了呼叫和介质参数，或者改变了一个已存在VC的呼叫参数。这通知NDIS，MCM已使NIC准备好传送该VC的数据。NDIS通过调用MCM的ProtocolCmActivateVcComplete函数，完成激活序列。

MCM调用NdisMCmActivateVC激活传送和／或接收客户数据的VCs，但不能激活用于MCM和网络成员(如交换机)之间交换信令消息的VCs。MCM激活一个信令VC时，不调用任何NdisXxx函数，所以任何用于信令交换的VC对于NDIS来说是不可见的。

1.6.3.3使VC去活

作为关闭一个外出或内入呼叫的基本步骤，通常在和断开呼叫的网络成员进行包交换之后，呼叫管理器调用NdisCmDeactivateVC (参见1.6.8.1和1.6.8.2节)。MCM调用NdisMCmDeactivateVC来做相同的工作。

对NdisCmDeactivateVC的调用使得NDIS调用NIC驱动程序的MiniportCoDeactivateVC（参见图1.17）. MiniportCoDeactivateVC与它的网络适配器通信,以终止这个VC上的所有通信(例如，清除该适配器上的发送和接收缓冲区)。

图1.17 呼叫管理器发起的VC去活

在使VC去活之前，微端口必须先完成该VC上未完成的传输。那就是说，得等到所有正在发送和已经指示的包的接收完成。在VC去活后，微端口不能再在这个VC上指示接收或传输发送。

注意，MiniportCoDeactivateVC并不删除VC。一个不再使用的VC的生成者(如客户，呼叫管理器，或MCM)调用NdisCoDeleteVC来删除VC(参见1.6.3.4节)。去活的VC可以被面向连接客端，呼叫管理器，或MCM重新激活。(参见1.6.3.2节)。

MiniportCoDeactivateVC 能同步或异步完成。对NdisMCoDeactivateVcComplete的调用，使NDIS调用呼叫管理器的ProtocolCmDeactivateVcComplete函数。去活的完成意味着VC激活时所用的所有参数不再有效。除非用新的参数集重新激活VC，否则无法使用此VC。

MCM对NdisMCmDeactivateVC的调用通知NDIS,它已经使一个VC去活，或改变了一个VC的呼叫参数（见图1.8）。NDIS通过调用MCM的ProtocolCmDeactivateVcComplete完成去活操作序列。

图1.18 MCM发起的VC去活

1.6.3.4删除VC

只有发起VC创建的面向连接客户、呼叫管理器和MCM才能启动那个VC的删除操作。客户删除先前为一个外出呼叫而创建的VC，呼叫管理器或MCM删除先前为一个内入呼叫而创建的VC，呼叫管理器删除先前为交换信令消息而创建的VC。(MCM不调用NDIS来删除它先前为交换信令消息而创建的VC。MCM用内部操作删除该VC，这对NDIS是不可见的。)面向连接客户或呼叫管理器用NdisCoDeleteVC发起一个VC的删除。图1.19显示了呼叫管理器的客户发起VC的删除。

图1.19 面向连接客户通过呼叫管理器发起的VC的删除

图1.20显示了MCM客户发起的VC删除。

图1.20 面向连接客户通过MCM发起的VC的删除。

图1.21显示了呼叫管理器发起的VC删除。

图1.21呼叫管理器发起的VC删除。

当客户或呼叫管理器调用NdisCoDeleteVC，或者当MCM调用NdisMCmDeleteVC时，在此VC上肯定已经不存在呼叫，而且VC肯定已经去活了(参见1.6.3.3节)。达到这些要求表示下面的条件已经满足：

·如果呼叫断开由本地客户发起，那么那个客户已经用NdisVCHandle调用NdisClCloseCall并且已成功地完成(参见1.6.8.1节)。

·如果呼叫断开由远程客户发起，那么呼叫管理器已经用NdisVCHandle调用了NdisCmDeactivateVC，或者MCM已经用NdisVCHandle调用了NdisMCmDeactivateVC。并且已成功地完成(参见1.6.8.2节)。

客户或呼叫管理器调用NdisCoDeleteVC,使NDIS调用NIC驱动程序的MiniportCoDeleteVC函数和客户或呼叫管理器的ProtocolCoDeleteVC函数。调用者与客户或呼叫管理器共享同一个NdisVCHandle(参见图1.19，1.20和1.21)。MinportCoDeleteVC释放所有分配给VC的资源，包括该VC的微端口环境。ProtocolCoDeleteVC释放客户或呼叫管理器用来执行操作或跟踪VC状态的所有资源。

MinportCoDeleteVC 和ProtocolCoDeleteVC都是不可能返回NDIS_STATUS_PENDING的同步函数。

MCM用NdisMCmDeleteVC发起一个VC删除(参见图1.22)。

图1.22 MCM发起VC的删除

MCM对NdisMCmDeleteVC的调用使得NDIS调用客户的ProtocolCoDeleteVC函数，该客户和MCM分享同一个NdisVCHandle。

当NdisCoDeleteVC或NdisMCmDeleteVC返回控制时，NdisVCHandle将不再有效。

1.6.4创建呼叫

在作一个外出呼叫时，面向连接客户发起一个呼叫的创建。当向面向连接客户指示一个内入呼叫时，呼叫管理器或MCM发起呼叫的创建。

1.6.4.1进行呼叫

图1.23显示一个客户通过呼叫管理器作一个外出呼叫。

图1.23 通过呼叫管理器作一个呼叫

图1.24显示一个客户通过MCM作一个外出呼叫

图1.24 通过MCM作一个呼叫

在作外出呼叫之前，面向连接客户必须完成以下操作：

·在类型CO_CALL_PARAMETERS结构中初始化呼叫参数。呼叫管理器或MCM通常用客户说明的呼叫参数，创建呼叫并衍生出微端口使用的介质参数。

·用NdisCoCreateVC发起VC的创建（参见1.6.3.1）。

在NdisCoCreateVC成功返回时，客户调用NdisClMakeCall创建呼叫(参见图1.23和1.24)。

在对NdisClMakeCall调用中，客户传递一个初始化过的指向CO_CALL_PARAMETERS结构的指针。客户同时也传递一个NdisVCHandle(由NdisCoCreateVC返回的)，这个NdisVCHandle指出了客户用来为呼叫传送数据的VC。如果客户作一个多点呼叫(具有多个远程party呼叫)，它也传递一个PotocolPartyContext，此参数以说明客户分配的常驻环境区的句柄，在这个环境区中客户为多点VC上的每个party维护状态信息。

对NdisClMakeCall的调用使NDIS将这个请求传给呼叫管理器或MCM的ProtocolCmMakeCall函数，呼叫管理器或MCM与客户共享这个NdisVCHandle。ProtocolCmMakeCall必须验证由客户创建的呼叫参数。ProtocolCmMakeCall同网络控制设备进行通信(交换信令消息)，来创建一个连接。呼叫管理器调用NdisCoSendPackets发起这个交换(参见1.6.7.1节)。而MCM不会调用NdisCoSendPackets。相反，它通过网络直接送数据。

呼叫管理器或MCM在与相关网络成员协商过程中，可以修改客户支持的呼叫参数，返回不同于客户最初传给NdisClMakeCall的传输参数。

一个传给ProtocolCmMakeCall的NdisPartyHandle指示客户创建的VC将用于多点呼叫。呼叫管理器或MCM必须分配并初始化的所有必需的资源，那些资源用于维护每个Party的状态信息和多点呼叫控制。

在呼叫管理器已经作了介质所要求的所有与网络硬件的通信后，它必须调用NdisCmActivateVc发起VC的激活，呼叫数据将在这个VC上发送或接收。(参见1.6.3.2节)。MCM必须调用NdisMCmActivateVc。

当NIC微端口准备好在一个VC上作数据传输时(在VC被激活之后)，呼叫管理器调用NdisCmMakeCallComplete,MCM调用NdisMCmMakeCallComplete。在这时，呼叫管理器或MCM应已经完成了VC的激活。

在对Ndis(M)CmMakeCallComplete的调用中，呼叫管理器或MCM把VC的呼叫参数，以指向结构类型CO_CALL_PARAMETERS的指针来传递。如果呼叫管理器已经修改了呼叫参数，它会在CO_CALL_PARAMETERS结构中设置CALL_PARAMETERS_CHANGED标志通知客户。

对Ndis(M)CmMakeCallComplete的调用,使NDIS调用发出外出呼叫的客户的ProtocolClMakeCallComplete函数。ProtocolClMakeCallComplete指示:呼叫管理器已经完成了客户建立虚连接的请求(用NdisClMakeCall提出的)。

如果客户建立外出呼叫成功，ProtocolClMakeCallComplete应该检查CALL_PARAMETERS_CHANGED标志，确定客户最初说明的呼叫参数是否已被修改。如果标志被设置，说明呼叫参数已被修改，ProtocolClMakeCallComplete应该检查返回的参数，以确定参数是否可接受。如果呼叫参数是可接受的，ProtocolClMakeCallComplete返回控制。如果呼叫参数不可接受并且信令协议允许再协商，客户可以调用NdisClModifyCallQoS来请求改变呼叫参数(参见1.6.5.1节)。如果信令协议不允许再协商，则ProtocolClMakeCallComplete必须用NdisClCloseCall断开呼叫(参见1.6.8.1节)。

1.6.4.2指示内入呼叫

来自网络的信令消息提醒呼叫管理器或MCM有一个内入呼叫。呼叫管理器或MCM从这些信令消息中,抽取呼叫参数，包括内入呼叫的SAP。

图1.25显示呼叫管理器指示一个内入呼叫

图1.25 通过呼叫管理器指示内入呼叫

图1.26显示了MCM指示内入呼叫

图1.26 通过MCM指示内入呼叫

如果对于呼叫管理器或MCM来说，内入呼叫参数是不可接受的，那么如果信令协议允许协商，它就可以和远程party协商改变呼叫参数。内入呼叫所导向的客户在收到来自呼叫管理器或MCM的呼叫指示后，也可以协商呼叫参数。(参见1.6.5.1节)。如果呼叫中管理器或MCM无法和远程party商定一个可接受的呼叫参数，可能会拒绝这次呼叫。在上述情况下，根据具体的信令协议进行相应处理。

在向客户指示内入呼叫前，呼叫管理器或MCM必须签别呼叫所导向的SAP。这个SAP必须已被客户注册(参见1.6.2.2节)。呼叫管理器或MCM必须发起虚连接的创建(参见1.6.3.1节)并发起激活此VC (1.6.3.2节)。然后，呼叫管理器或MCM向注册这个SAP的客户指示内入呼叫。呼叫管理器用NdisCmDispatchIncomingCall指示一个内入呼叫。在调用Ndis(M)CmDispatchIncomingCall时，呼叫管理器或MCM传递了如下的信息：

·用于指出呼叫所针对的SAP的NdisSapHandle

·用于指出内入呼叫的VC的NdisVCHandle

·指向结构类型CO_CALL_PARAMETERS的指针，该结构中包含了呼叫参数。

对Ndis(M)CmDispatchInComingCall的调用使得NDIS调用客户的ProtocolClInComingCall函数，在这个函数中，客户接受或者拒绝该连接请求。ProtocolClInComingCall应该验证SAP、VC和呼叫参数的有效性。

ProtocolClInComingCall 能同步完成，或者，它能用NdisClInComingCallComplete异步完成，并返回NDIS_STATUS_PENDING。对NdisClInComingCallComplete的调用,使NDIS调用呼叫管理器或MCM的ProtocolCmInComingCallComplete函数。

由ProtocolClInComingCall同步完成所返回的NDIS_STATUS码，或是由NdisClInComingCallComplete支持的NDIS_STATUS码,都指示了客户对内入呼叫的接受或拒绝。客户也在缓冲区的CO_CALL_PARAMETERS结构中，返回呼叫参数。如果客户发现呼叫参数不可接受，只要信令协议允许，它能在CO_CALL_PARAMETERS结构中设置CALL_PARAMETERS_CHANGED标志来请求改变呼叫参数，同时在缓冲区的CO_CALL_PARAMETERS结构中提供修订过的呼叫参数。

如果客户接受一个内入呼叫，呼叫管理器或MCM应发送信令消息，以指示呼叫实体，此次呼叫已被接受。否则，呼叫管理器或MCM应该发送信令消息指示呼叫已被拒绝。如果客户正在请求呼叫参数，呼叫管理器或MCM发送信令消息请求改变呼叫参数。

如果客户接受呼叫，或者远程party接受客户改变呼叫参数的请求，呼叫管理器调用NdisCmDispatchCallConnected,MCM调用NdisMCmDispatchCallConnected。调用Ndis(M)CmDispatchCallConnected使NDIS调用客户的ProtocolClCallConnnected函数。如果客户拒绝呼叫，并且呼叫管理器或MCM已经为此次内入呼叫激活VC，那么呼叫管理器或MCM调用Ndis(M)CmDeactivateVC使VC去活。呼叫管理器用NdisCoDeleteVC发起VC的删除。MCM用NdisMCmDeleteVC发起VC的删除。

如果客户接受呼叫，但端到端的连接没有成功建立(例如，远程party断开链接)，那么呼叫管理器或MCM不调用Ndis(M)CmDispatchCallConnected。相反，它调用Ndis(M)CmDispatchIncomingCloseCall,这使得NDIS调用客户的PtotocolClIncomingCloseCall函数。然后，客户必须调用NdisClCloseCall，完成呼叫的断开。接着，呼叫管理器或MCM调用Ndis(M)CmDeactivateVC使VC去活(参见1.6.3.3节)。呼叫管理器用NdisCoDeleteVC发起VC的删除，MCM用NdisMCmDeleteVC发起VC的删除(参见1.6.3.4节)。

1.6.5改变活动VC的QoS

面向连接客户能请求改变一个活动VC的QoS。远程客户也能作如此请求。在这种情况下，呼叫管理器或MCM驱动程序指示远程客户的QoS改变请求。

1.6.5.1客户发起的改变呼叫参数请求。

客户用NdisClModifyCallQoS请求改变活动VC的QoS。

图1.27显示了呼叫管理器的改变QoS的客户请求。

图1.28显示了MCM的客户的改变QoS的请求。

图1.27客户通过呼叫管理器发起呼叫参数的改变请求

图1.28 客户通过MCM发起呼叫参数的改变请求

在对NdisClModifyCallQoS的调用中，客户提供了如下的信息：

·指明VC的NdisVCHandle

·指向CO_CALL_PARAMETERS结构的指针，该结构容纳了客户正在请求的呼叫参数。

客户可否请求改变QoS的环境是由信令协议决定的。

对NdisClModifyCallQoS的调用使得NDIS调用呼叫管理器或MCM的ProtocolCmModifyQoS函数，该函数输入NdisVCHandle和由客户传给与NdisClModifyCallQoS的缓冲区的CO_CALL_PARAMETERS结构。ProtocolCmModifyQoS与网络控制设备或其他介质成员通信，为一个虚连接修改介质呼叫参数。

在和网络通信并断定改变成功之后，呼叫管理器必须调用NdisCmActivateVC(MCM必须调用NdisMCmActivateVC)，用新的呼叫参数激活VC。

如果网络不接收新的呼叫参数或者微端口不接受参数，呼叫管理器或MCM将VC状态恢复到修改以前，并返回NDIS_STATUS_FAILURE。

为了指示客户改变QoS请求的状态，呼叫管理器调用NdisCmModifyQoSComplete,MCM调用NdisMCmModifyCallQoSComplete.

在调用中，呼叫管理器或MCM传递如下信息：

·指示请求状态的NDIS_STATUS

·指明VC的NdisVCHandle

·指向CO_CALL_PARAMETERS结构的指针，该结构容纳了VC的参数。

如果信令协议允许，呼叫管理器或MCM可以将修改过的呼叫参数返回给客户。这些修改可以是网络协商的结果，或者也可由呼叫管理器和MCM本身提供。呼叫管理器或MCM在CO_CALL_PARAMETERS结构中设置CALL_PARAMETERS_CHANGE标志，指示已修改了呼叫参数。对Ndis(M)CmModifyCallQoSComplete的调用使NDIS调用客户的ProtocolClModifyCallQoSComplete函数。NDIS向ProtocolClModifyCallQoSComplete传递信息如下：

·指示客户改变QoS请求状态的NDIS_STATUS

·指明VC的ProtocolVCContext的句柄

·指向CO_CALL_PARAMETERS结构的指针，该结构包含了呼叫管理器或MCM传给Ndis(M)CmModifyCallQoSComplete的呼叫参数。

如果在CO_CALL_PARAMETERS结构中，CALL_PARAMETERS_CHANGED标志被设置，客户必须检查返回的呼叫参数，并确定是否接受修改。如果客户的NdisClModifyCallQOS调用成功，ProtocolClModifyCallQoSComplete接收QoS修改并返回控制。否则，在信令协议允可的条件下，并且客户开发者已对协商次数作了合理限制时，ProtocolClModifyCallQoSComplete和呼叫管理器作进一步协商。当呼叫管理器拒绝改变QoS的请求，并且以前的QoS不再可接受时，ProtocolClModifyCallQoSComplete调用NdisClCloseCall断开一个呼叫(参见1.6.8.1节)。

1.6.5.2改变呼叫参数的内入请求

网络信令消息提醒呼叫管理或MCM，有远程party的内入请求，该请求要求改变活动VC的呼叫参数。信令协议来决定是否呼叫管理器或MCM支持活动VC、QoS的动态改变。例如，ATM UNI3.1不支持活动VC的动态QoS改变。

图1.29显示一个通过呼叫管理器修改呼叫参数的请求

图1.29 通过呼叫管理器改变QoS的内入请求

图1.30显示一个通过MCM改变呼叫参数的内入请求

图1.30 通过MCM改变呼叫参数的内入请求

在收到改变呼叫参数的内入请求之后，呼叫管理器将修改过的呼叫参数传给NdisCmActivateVC,把QoS的变化通知给NIC驱动程序。MCM将修改过的呼叫参数传给NdisMCmActivateVC(参见1.6.3.2节)。如果NIC驱动程序接受改变后的呼叫参数，呼叫管理器调用NdisCmDispatchIncomingCallQoSChange(参见图1.29)。MCM调用NdisMCmDispatchIncomingCallQoSChange(参见图1.30)。呼叫管理器或MCM向Ndis(M)CmDispatchIncomingCallQoSChange传递NdisVCHandle和缓冲区的CO_CALL_PARAMETERS结构。

客户接受QoS的改变，并更新它所维护的QoS的状态信息，然后返回控制。如果不接受修改，在信令协议允可的条件下，客户可用NdisClModifyCallQoS来重新协商呼叫参数。(参见1.6.5.1节)否则，客户用NdisClCLoseCall断开呼叫。(参见1.6.8.1节)。在ProtocolClIncomingCallQoS返回后，呼叫管理器或MCM把客户的接受或拒绝反应传给远程party。

1.6.6增加和删除Parties

面向连接客户可以在一个由其通过NdisClMakeCall发起的外出多点呼叫中增加一个Party。面向连接客户可以自己或应远程party请求，从多点呼叫中删去一个Party。

1.6.6.1把一个Party加入到多点呼叫

客户用NdisClAddParty请求将Party加入到多点呼叫中。客户只能将一个Party加入到已存在的多点各叫中。

图1.31显示了呼叫管理器客户请求将一个Party加入到多点呼叫中的过程。

图1.31 通过呼叫管理器将一个Party加入到多点呼叫中。

图1.32显示了MCM客户请求将一个Party加入到多点呼叫中的过程。

图1.32 通过MCM将一个Party加入到多点呼叫中。

在调用NdisClAddParty之前，客户必须为加入的Party分配和初始化环境区。在调用NdisClAddParty时，客户传递指向环境区的指针ProtocolPartyContext和指向环境区内一个变量的指针NdisPatyHandle.除了NdisVCHandle和ProtocolPartyContext，客户向NdisClAddParty传递呼叫参数(缓冲区的CO_CALL_PARAMETERS)。下面的网络介质决定了,客户能是否需要说明多点呼叫中每个party的传输参数。例如，多点VC中的传输参数对于ATM中的所有parties来说都是相等的。

对NdisClAddparty调用，使NDIS将此请求传给呼叫管理器或MCM的PotocolCmAddParty函数，用此函数共享NdisVCHandle.NDIS将下边的信息传给ProtocolCmAddParty:

·向呼叫指示VC的CallMgrVCContext

·包含由客户传给NdisClAddParty的呼叫参数的CO_CALL_PARAMETERS结构的指针。

·指出所增加Party的NdisPartyHandle

ProtocolCmAddParty 分配并初始化增加Party所需的任何动态资源。从ProtocolCmAddParty中，呼叫管理器或MCM和网络控制设备或其他网络介质通信，从而在多点呼叫中增加一个Party.如果客户传递的呼叫参数并不与已创建的多点VC相匹配，呼叫管理器或MCM会：

·只要网络介质支持，就为每个Party单独创建传输参数。

·重置已创建的VC的传输参数

·改变呼叫参数

·拒绝客户增加一个Party

ProtocolCmAddParty 可以同步完成。在使用呼叫管理器的情况下，可以用NdisCmAddPartyComplete异步完成；在使用MCM的情况下，可以用NdisMCmAddPartyComplete异步完成。无论是呼叫管理器或MCM采用同步或异步方式完成操作，都要向NDIS传递缓冲区中的呼叫参数。

对Ndis(M)CmAddPartyComplete的调用，使NDIS调用客户的ProtocolClAddPartyComplete函数。如果客户增加Party的请求成功，并且如果信令协议允许呼叫管理器或MCM修改呼叫参数，ProtocolClAddPartyComplete应该检查缓冲区中的CO_CALL_PARAMETERS结构的CALL_PARAMETERS_CHANGED标志，从而确定是否呼叫参数已改变。如果发现CO_CALL_PARAMETERS的改变是不可接受的，则由信令协议决定客户应采取的措施。在这种情况下，客户通常调用NdisClDropParty(参见1.6.6.2节)

1.6.6.2从多点呼叫中删除Party

作为多点呼叫根的面向连接客户，最后必须用NdisClDropParty或NdisClCloseCall删除呼叫中的每一个Party.

在如下情况下，客户从呼叫中删除一个Party：

·在用NdisClCloseCall发起多点呼叫的关闭操作前(1.6.8.1节)，客户必须使用一系列NdisClDropParty的调用,将Party删到只剩一个。客户用NdisClCloseCall删除最后一个Party。

·作为对远程Party从多点呼叫中删除请求的响应（参见1.6.6.3）, 客户在它的ProtocolClIncomingDropParty函数中调用NdisClDropParty。

·在客户增加一个Party的尝试失败后，客户必须调用NdisClDropParty。

客户对NdisClDropParty的调用，使NDIS调用呼叫管理器或MCM的ProtocolCmDropParty函数，该呼叫管理器或MCM与多点VC共享同一NdisVCHandle。

图1.33显示了呼叫管理器的客户请求删除一个Party

图1.33 通过呼叫管理器从多点呼叫中删除一个Party

图1.34显示了MCM的客户请求删除一个Party

图1.34 通过MCM从多点呼叫中删除一个Party

ProtocolCmDropParty 和网络控制设备通信，从现存的多点呼叫中删除一个Party。NDIS向ProtocolCmDropParty传递一个含有数据的缓冲区的指针(这些数据在对NdisClDropParty的调用中，由客户提供)。在连接删除前，ProtocolCmDropParty必须发送这些数据。

ProtocolCmDropParty 可以同步完成，在使用呼叫管理器情况下，也可用NdisCmDropPartyComplete异步完成，在使用MCM情况下，也可用NdisMCmDropPartyComplete异步完成。对Ndis(M)CmDropPartyComplete的调用，使NDIS调用客户的ProtcolClDropPartyComplete函数。如果客户处于断开多点VC的过程中，PorotcolClDropPartyComplete可以用任何有效的NdisPartyHandle来调用NdisClDropParty,该NdisPartyHandle指向活动多点VC的一个Party.如果多点VC中只有一个Party了，客户将其NdisPartyHandle传给NdisClCloseCall,删除这个Party(参见1.6.8.1节)。

1.6.6.3从多点呼叫中删除一个Party的内入请求

来自网络的信令消息提醒呼叫管理器或MCM，有一个来自远程party的内入请求，该请求要求从多点呼叫中删除那个Party。呼叫管理器或MCM检测到数据在VC上传输有网络阻碍时，会发出内入请求删除一个Party.

如果删除的Party不是VC中的最后一个，呼叫管理器调用NdisCmDisPatchIncomingDropParty。MCM调用NdisMCmDisPatchIncomingDropParty。如果删除的Party是VC中的最后一个，呼叫管理器调用NdisCmDisPatchIncomingCloseCall，MCM调用NdisMCmDisPatchIncomingCloseCall(参见1.6.8.2节)。

对Ndis(M)CmDisPatchIncomingDropPart y的调用使得NDIS调用客户的ProtocolClIncomingDropParty函数。

图1.35显示了一个通过呼叫管理器删除Party的内入请求

图1.35 通过呼叫管理器从多点呼叫中删除Party的内入请求

图1.36显示了一个通过MCM删除Party的内入请求

图1.36 通过MCM从多点呼叫中删除Party的内入请求

ProtocolClIncomingDropParty 为从客户的多点VC中删除Party而执行所有协议确定的操作。如果被删去的Party不是VC中最后一个Party，ProtocolClIncomingDropParty必须调用NdisClDropParty(参见1.6.6.2节)。如果被删除的Party是VC中的最后一个Party, ProtocolClIncomingDropParty必须调用NdisClCloseCall(参见1.6.8.1节)。

1.6.7发送并接收数据

在一个已创建并激活的VC上传输数据包括发送接收包。

1.6.7.1在VC上发送包

为了在网络上发送包，面向连接客户或呼叫管理器调用NdisCoSendPackets.与MCM相连的面向连接客户也调用NdisCoSendPackets。因为呼叫管理器和MCM的接口在MCM的内部,所以MCM不调用NdisCoSendPackets，MCM不经NDIS，而是直接将包传给NIC。

图1.37显示了客户或呼叫管理器通过微端口发送包

图1.37 客户或呼叫管理器通过微端口发送包

图1.38显示了客户通过MCM发送包

图1.38 客户通过MCM发送包

在调用NdisCoSendPackets之前，客户或呼叫管理器应该按相应的发送顺序创建一个包指针数组。然后客户或呼叫管理器调用NdisCoSendPackets，同时传递一个指向包指针数组的指针和指明所用VC的NdisVCHandle。

当客户或呼叫管理器调用NdisCoSendPackets时，它就放弃了以下资源的所有权：

·包数组中所包描述符

·与这些描述符相链的缓冲区指示器所指的缓冲区

·与包描述符相关的带外数据块(OOD)，包括OOB块中所有介质相关缓冲区。

对NdisCoSendPackets的调用，使NDIS调用微端口的MiniportCoSendPackets函数。MiniportCoSendPackets根据数组中原有的包次序，顺序发送数组中的每一个数据包。MiniportCoSendPackets调用NdisQueryPacket抽取信息，如缓冲区描述符的数目和要求传输的字节总数。

MiniportCoSendPackets 可以调用NdisGetFirstBufferFromPacket,NdisQueryBuffer或NdisQueryBufferOffset来抽取单个缓冲区的信息。MiniportCoSendPackets通过使用NDIS_GET_PACKET_XXX宏，取回协议支持的每个包的OOB信息。MiniportCoSendPackets函数通常忽略NDIS_PACKET_OOB_DATA块的status成员，但它可以把这个成员设置为传给NdisMCoSendComplete的状态。

当MiniportCoSendPackets没有足够的资源传送包时，面向连接微端口自己管理内部包排队，而不靠NDIS排队和重新提交发送包。微端口总是在它的内部队列中保存发送包，一直到包被发送为止。队列保存着协议确定的包描述符次序，这些包描述符将被传给MiniportCoSendPackets函数。

面向连接微端口必须用NdisMCoSendComplete来完成每个发送包的发送。它不可调用NdisMSendResourcesAvailable.

面向连接微端口不能用协议分配的包描述符向NdisMCoSendComplete传送STATUS_INSUFFICIENT_RESOVRCES，这些包描述符最初被传给MiniportCoSendPackets函数。对NdisMCoSendPackets的调用,使NDIS调用客户的ProtocolCoSendComplete函数。ProtocolCoSendComplete为完成传输操作执行所有善后处理，如通知客户。

发送操作的完成表示NIC驱动程序已经完成包的传输。然而，“智能”NIC驱动程序在把包下放到NIC时就认为发送已完成。

在调用ProtocolCoSendComplete后，驱动程序重新获得了协议分配的资源的所有权：

·包描述符

·与包描述符相链的缓冲区器，这些包描述符映射容纳包数据的缓冲区，所有协议分配的缓冲区都由这些描述符映射。

·所有与包描述符相关的OOB

·所有由OOB描述的协议分配缓冲区

虽然在对NdisCoSendPackets的调用中，NDIS总按协议确定的顺序向微端口提交协议支持的包数组，但是下面的驱动程序可以随机地发送。那就是说每个绑定协议依靠NDIS，以FIFO次序低层驱动程序提交包，但协议不能指望这个驱动程序以同样的次序调用NdisMCoSendComplete。

1.6.7.2接收VC上的包

面向连接微端口或MCM调用NdisMCoIndicateReceivePacket,向面向连接客户或呼叫管理器指示接收到的包。如果微端口或MCM处理中断，它在MiniportHandleInterrupt函数中调用NdisMCoIndicateReceivePacket。

图1.39显示了一个指示接收包的微端口

图1.39 通过微端口接收包

图1.40显示了一个指示接收包的MCM

图1.40 通过MCM接收包

在对NdisMCoIndicateReceivePacket的调用中，微端口或MCM将传递一个指向包描述符指针数组的指针。微端口或MCM也要传送指示收到包的VC的NdisVCHandle。在调用NdisMCoIndicateReceivePacket之前，微端口或MCM必须创建一个包数组(参见第二部分，4.6节)。

对NdisMCoIndicateReceivePacket的调用，使NDIS调用协议驱动程序的ProtocolCoReceiveComplete函数，这个驱动程序和微端口共享VC。ProtocolCoReceiveComplete函数处理接收指示。

在调用NdisMCoIndicateReceivePacket之后，微端口必须调用NdisMCoReceiveComplete来指示接收的完成，这些接收是由NdisMCoIndicateReceivePacket指示的。对NdisMCoReceiveComplete的调用,使NDIS调用面向连接客户或呼叫管理器的ProtocolReceiveComplete函数。

如果协议不立即返还微端口分配的资源，微端口或MCM可以用NDIS_STATUS_RESOURCES调用NdisMCoIndicateStatus，来提醒协议：微端口或MCM缺少可利用的包或缓冲区描述符。

接收包的详细情况，参见第二部分，4.6节。

1.6.8断开呼叫

面向连接客户能发起一个呼叫的断开操作，这个呼叫或者是它发出的，或者是接受的内入呼叫。类似地，远程party也可以发起一个呼叫的断开。

1.6.8.1客户发起的关闭呼叫请求

如果客户正在关闭含有一个或多个party的多点呼叫，它首先必须多次调用NdisClDropParty，删除除最后一个Party外的所有Party（参见1.6.6.2）.

客户调用NdisClCloseCall发起呼叫的关闭。图1.4.1显示了客户通过呼叫管理器发起呼叫的关闭。图1.42显示了客户通过MCM发起呼叫关闭。

图1.41 客户通过呼叫管理器发起呼叫关闭请求。

图1.42 客户通过MCM发起呼叫关闭请求。

面向连接客户在如下情况中，调用NdisClCloseCall

·关闭一个已创建的外出或内入呼叫

·从ProtocolClIncomingCloseCall函数中断开一个呼叫(参见1.6.8.2节)

·如果远程party提出的QoS的改变不能被接受，则在ProtocolClIncomingCallQoSChange函数中断开呼叫。

·如果客户提出的QoS改变，远程party不能接受，则在函数ProtocolClModifyCallQoSComplete中断开呼叫。

客户对NdisClCloseCall的调用使得NDIS调用呼叫管理器或MCM的ProtocolCmCloseCall函数。ProtocolCmCloseCall必须与网络控制设备通信,以终止本地结点与远程结点之间的通信。

如果传送给ProtocolCmCloseCall的是一个显式的CallMgrPartyContext,那么终止的呼叫是一个多点呼叫。呼叫管理器或MCM必须与网络硬件通信，来终止这个多点呼叫。

在调用NdisClClose过程中，NDIS向ProtocolCmCloseCall传递一个缓冲区指针，该缓冲区中包含客户提供的数据。这些数据可以是指明为什么关闭呼叫的诊断数据，也可以是信令协议需要的其他数据。在完成关闭呼叫之前，ProtocolCmCloseCall必须把这些数据送出去。如果不能在关闭连接的同时发送数据，呼叫管理器MCM返回NDIS_STATUS_INVALID_DATA。

ProtocolCmCloseCall 可以同步或异步完成，呼叫管理器用NdisCmCloseCallComplete异步完成,MCM用NdisMCmCloseCallComplete异步完成。对Ndis(M)CmCallComplete的调用使NDIS调用客户的ProtocolClCloseComplete函数。

通过调用NdisCmDeactivateVC或NdisMCmDeacticateVC，呼叫管理器或MCM使VC去活(参见1.6.3.3节)。然后,VC的创建者(客户呼叫管理器或MCM)可以有选择地删除VC(参见1.6.3.4节)。

1.6.8.2关闭呼叫的内入请求

当远程客户调用NdisClCloseCall关闭一个呼叫时，本地呼叫管理器或MCM必须向本地客户指示这个请求。为此，呼叫管理器用被设置为NDIS_STATUS_SUCCESS的CloseStatus来调用NdisCmDispatchIncomingCloseCall.(参见图1.43)。

图1.43 通过呼叫管理器关闭呼叫的内入请求

MCM调用NdisMCmDispatchIncomingCloseCall，来指示关闭呼叫的请求(参见图1.44)。

图1.44 通过MCM关闭呼叫的内入请求

呼叫管理器或MCM也能在如下情况下，调用Ndis(M)CmDispatchIncomingCloseCall：

·如果MCM确定面向连接客户正在请求改变呼叫参数，而这个改变是不可接受的，那么可以从ProtocolCmIncomingCallComplete中调用。

· 如果异常网络条件迫使呼叫管理器断开活动呼叫。对Ndis(M)CmDispatchIncomingCloseCall的调用，使NDIS调用ProtocolClIncomingCloseCall函数。ProtocolClIncomingCloseCall执行所有协议确定的操作，诸如通知客户连接已经断开。如果关闭的呼叫是由客户创建的多点呼叫，ProtocolCmIncomingCloseCall必须调用NdisClCloseCall(用指向VC中最后一个Party的包柄)，来确认客户不再在这个VC上发送或接收数据。如果是呼叫管理器或MCM创建了这个VC，ProtocolCmIncomingCloseCall在调用NdisClCloseCall之后，返回控制。呼叫管理器或MCM必须使VC去活(参见1.6.3.3节)。

如果是客户创建的VC，并且CloseStatus 是NDIS_STATUS_SUCCESS，那么ProtocolClIncomingCloseCall可以有选择地用NdisCoDeleteVC断开VC(参见1.6.3.4节)。或者将VC重用于另一呼叫。如果CloseStatus不是MDIS_STATUS_SUCCESS，ProtocolCmIncomingCloseCall必须调用NdisCoDeleteVC.

如果是呼叫管理器或MCM创建的VC，呼叫管理器或MCM分别调用各自的NdisCmDeleteVC或NdisMCmDeleteVC,有选择地删除VC(参见1.6.3.4节)。

1.6.9获取并设置信息

呼叫管理器，面向连接客户或MCM可以查询或设置绑定上其他驱动程序所维护的信息。另外，微端口或MCM可以向其他驱动程序指示微端口状态。

1.6.9.1查询或设置信息

面向连接客户或呼叫管理器调用NdisCoReqeuest来查询或设置信息，这些信息由绑定上的其他驱动程序维护或由低层的微端口维护。

在调用NdisCoReqeuest之前，客户或呼叫管理器为其分配了一个缓冲区并初始化了一个NDIS_REQUEST结构。这个结构说明了请求类型(查询或设置)，指明了查询或设置的信息(OID)，以及用于传送OID数据的缓冲区指针。

如果面向连接客户或呼叫管理器传递了一个有效的NdisAfHandle(参见1.2.1节)，那么NDIS将调用每个协议驱动程序的ProtocolCoRequest函数(参见图1.45)。

图1.45 查询或设置客户或呼叫管理器信息

如果低层微端口是MCM，那么NDIS将调用MCM的ProtocolCoReqcuest(参见图1.46)

图1.46 查询或设置微端口的呼叫管理器信息

如果面向连接客户或呼叫管理器传递空的地址族句柄，NDIS调用微端口或MCM的MiniportCoReqcuest。图1.47显示了客户或呼叫管理器查询或设置微端口信息。

图1.47 查询或设置微端口信息。

图1.48显示了客户查询或设置MCM的微端口信息。

图1.48 查询或设置MCM的微端口信息。

为查询客户维护的信息，MCM调用NdisMCmReqeuest(参见图1.49)。

图1.49 MCM发起的客户信息查询

通过说明VC句柄或Party句柄，NdisCoReqeuest或NdisMCmReqeuest的调用者可以缩小请求范围。无论请求本身是给客户、呼叫管理器、微端口还是MCM的，传递一个空的NdisVcHandle，都将使请求成为成为全局的。ProtocolCoReqcuest或MiniportCoReqcuest能同步完成，或者也可用NdisCoReqcuestComplete异步完成。对NdisCoReqcuestComplete地调用使NDIS调用驱动程序的ProtocolCoRequestComplete函数，正是这个驱动程序调用了NdisCoReqeuest。

1.6.9.2指示微端口状态

为了报告面向连接NIC的状态变化或是NIC上某个活动VC的状态变化，面向连接微端口调用NdisMCoIndicateStatus.如果微端口报告某个VC的状态变化，它须提供指明此VC的NdisVCHandle.图1.50显示了指示一个微端口状态.图1.51显示了指示一个MCM状态

图1.50 指示微端口状态

图1.51 指示MCM的微端口状态

如果没有提供NdisVCHandle,对NdisMcoIndicateStatus的调用，使NDIS调用了所有绑定到该NIC上的协议驱动程序的ProtocolCoStatus函数。如果微端口提供NdisVCHandle,NDIS调用共享该VC的协议驱动程序的函数。当MCM调用NdisMCoIndicateStatus时，由于这里没有呼叫管理器,NDIS仅调用绑定的面向连接客户的ProtocolCoStatus函数。

ProtocolCoStatus 函数能记录状态变化并采取正确的行动。例如，在微端口调用NdisMCoIndicateSatus报告NIC中操作问题时，协议可以调用NdisReset(或NDIS自己调用微端口的MiniportReset函数)，使NIC恢复到工作状态(参见1.6.10节)。

1.6.10重置

绑定到面向连接微端口或MCM的协议，能调用NdisReset，从而使微端口或MCM重置NIC。NDIS自己也可以调用微端口或MCM的MiniportReset重置NIC。

图1.5.2显示了一个客户向微端口提出重置请求的过程。

图1.5.2 通过微端口提出重置NIC请求。

图1.5.3显示了一个客户向MCM提出重置请求的过程。

图1.5.3 通过MCM提出重置NIC请求。

当面向连接驱动程序重置NIC时，NDIS用NDIS_STATUS_RESET_START调用协议的ProtocolCoStatus函数，通知每个绑定的协议。NDIS接着调用ProtocolStatusComplete.在微端口或MCM的NIC重置时，NDIS不接受协议的发送请求和对微端口或MCM的信息请求。当重置进行时，协议驱动程序无法用NdisCoSendPackets发送给微端口，也不能用NdisCoRequest向微端口请求信息。

MiniportReset 执行所有与设备相关的操作，这些操作对重置NIC是必须的。MiniportReset可以同步完成，也能用NdisMResetComplete异步完成。

˙如果同步完成重置，NDIS用NDIS_STATUS_RESET_END调用每个绑定协议的ProtocolCoStatus函数。然后,NDIS调用协议的ProtocolStatusComplete函数。

˙如果异步完成重置，NDIS用NDIS_STATUS_RESET_END调用每个绑定协议的ProtocolCoStatus函数，然后NDIS调用协议的ProtocolStatusComplete函数。如果重置由协议发起，NDIS也调用该协议的ProtocolResetComplete函数。


安装网络组件

这章包括：

·涉及网络组件安装的组件和文件总结

·关于创建网络组件信息(INF)文件的细节化信息

1.1用于安装网络组件的组件和文件

Windows 2000网络组件的安装涉及如下几个方面：

·类安装器和协作安装程序

网络组件由Windows 2000网络类安装器，或者供应商创建的定制类安装器实现安装。类安装器是一个动态链接库，用于安装、配置或删除某个类的设备。如果网络类安装器没有提供所需的特性，供应商自己可以写一个协作安装程序来定制安装过程。协作安装程序可以是一个Win32 DLL,它在Windows 2000系统中协助设备安装。协作安装程序作为类安装器的助手或过滤器，由设备安装器调用。

·信息(INF)文件

每个网络组件必须有信息(INF)文件，网络类安装器可以用来安装组件。网络INF文件基于通用的INF文件格式。

为网络组件创建INF文件的细节化信息，参见1.2节

·可选的通知对象

软件组件，如网络协议、客户或服务，可以有一个通知对象。通知对象实现一个用户接口，向组件通知绑定事件，使组件实现对某个绑定过程的控制，并提供条件安装和条件删除。通知对象在第二章中描述。

网络适配器也能支持一个用户接口，并对绑定事件、条件安装和条件删除实现一些控制。这些通过INF文件或协作安装程序来实现。

·可选的网络移植DLL和相关文件

如果网络供应商的驱动程序没有随Windows 2000一起发布，供应商应该提供这些组件的升级支持。网络升级处理将网络组件的参数只从Windows NT 3.51 或Windows NT 4.0移到Windows 2000 上。关于升级网络组件的更多信息，参见在线DDK“NetWork Drivers Reference”的第六部分，第一章。

除了以上组件外，供应商也提供如下文件：

·设备的驱动程序

驱动程序通常由驱动程序印象(.sys文件)和驱动程序库(.dll文件)构成。

·可选的驱动程序目录文件

供应商向Windows硬件质量库(WHQL)提交驱动程序，用来测试和签名，进而得到一个数字签名。WHQL将一个目录文件(.cat文件)随同包返回。供应商必须在设备的INF文件中列出所有目录文件。

·可选的文本模式安装信息文件(txtsetup.oem).

如果网络设备要求启动机器，操作系统包中必须包括驱动程序，或者该设备供应商必须提供一个txtsetup.oem文件。txtsetup.oem文件包含一些信息，在启动处理的早期阶段，系统安装组件用这些信息来安装设备(在文本模式安装期间)。

1.2创建网络INF文件

网络INF文件基于标准INF文件格式，但也包括网络特殊项，如网络特殊节、说明、节项和值。下面对于网络INF文件的描述，假设读者已经理解基本INF文件。在尝试创建网络INF文件之前，应先读一下基本INF文件的描述。

微软Windows 2000的网络INF文件与Windows NT 4.0和更早版本的NT的网络INF文件是不兼容的。为了使网络组件可以在Windows 2000和NT 4.0平台上安装，应分别创建不同的INF文件。

同样的INF文件可以在Windows 2000和Windows 95/98上用来安装网络组件。参见《Windows 2000 Driver Development Reference》第一卷的INF文献。

INF文件需求因网络类型不同而变化。不同类型网络对INF需求的总结，参见1.2.11。

1.2.1网络INFS文件名的约定

和Windows 2000一同出售的INF文件的文件名不得超过8个字符。其他INF文件不受这个限制。所有INF文件的扩展名为.inf。所有网络INF文件名以net开头，用来指示由网络类安装器处理这个文件。IrDA设备的INF文件以ir开头。

文件名的其余部分用来指示网络组件生产商或描述这些组件。下面是一个有效的网络INF文件名的例子：

网络INF文件名 文件名组件

netMst net+制造商

netDlc net+产品描述

net999 net+产品型号

1.2.2网络INF文件的版本节

网络INF文件的版本节有一些网络特有性质的描述，如下：

Class

类版本节中应该包括类项，使读者容易确定该文件要安装的网络组件的类别。

有4个网络类

·Net

说明物理或虚拟的网络适配器。NDIS中间层驱动程序包含在Net类中，该程序输出虚拟网络适配器。

·NetTrans

说明网络协议，如TCP/IP，IPX，面向连接客户或面向连接呼叫管理器。

·NetClient

说明网络客户，如微软网络客户或NetWare客户。NetClient组件被认为是网络提供者，如果它提供网络打印服务，它也被认为是一个打印提供者。

·NetService

说明网络服务，如文件服务或打印服务。

虽然IrDA设备由类安装器安装，但IrDA并不归于上面4个网络类中的任何一个。用来安装IrDA设备的INF文件应该有一个Infrared的Class值。这个类包括Serial-IR和Fast-IR设备。

ClassGuid

版本节中必须包含ClassGuid项。网络类安装器用ClassGuid项确定安装的网络组件类。

有4个ClassGuid值，每个值对应于一个网络类：

网络类 ClassGuid

Net {4D36E972-E325-11CE-BECl-08002BE10318}

NetTrans {4D36E973-E325-11CE-BFCl-08002BE10318}

NetClient {4D36E974-E325-11CE-BFCl-08002BE10318}

NetService {4D36E975-E325-11CE-BFCl-08002BE10318}

IrDA设备的INF文件应该有一个ClassGuid:{66ddlfc5-81do-bec7-08002be2092f}。

签名和操作系统项

签名项有三个值：

·$Windows 95$

·$Windows NT$

·$Chicago$

如果INF文件仅用于Windows 95/98，正确的签名应力$Windows 95$。有$Windows 95$签

名的INF文件不能运行于Windows 2000.

如INF仅用于Windows 2000，正确的签名是$Windows NT$。有$Windows NT$签名的INF文件不能在Windows 95/98上运行。

如INF在Windows 95/98以及Windows 2000上运行，正确的签名是$Chicago$。有$Chicago$签名的网络INF文件必须在版本节中有如下一行：

Compatible=1

如果没有这一行，就不能在Windows2000上运行。

版本节例子

下面是安装网络适配器的INI文件的版本节：

［Version］

Signature=$Chicago$

Compatible=1

Class=Net

ClassGuid={4D36E972-E325-11CE-BECl-08002BE0318}

Provider=%Msft%

Driverver=08/20/1999

Provider项指示谁开发了INF文件,不是谁开发了INF文件安装的组件。

1.2.3网络INF文件的模型节

INF文件的模型节为每个安装的组件,包含如下格式的项：

［device-description=install-section.name,hw-id[,compatible-id...］

对这个项的细节化描述，参见《Windows 2000 Driver Development Reference》第1卷的INF文档。

网络适配器的hw-id(也称为设备、硬件、或组件ID)必须和适配器提供给PnP管理器的硬件ID相匹配。网络软件组件的hw-id应由提供者名字组成，后面跟着下划线和制造商名称或产品名称。提供者名称指示了INF文件的提供者。制造商的名字指示了软件组件的制造商。产品名字指明了软件组件。例如，下面的hw-id包含了后跟产品名称的提供者名称：

MS_DLC

MS_IBMDLC

1.2.4 INF文件的DDInstall节

网络INF文件的DDInstall节有如下的网络特殊属性：

Characteristics

每个DDInstall节必须有Characteristics项。Characteristics项说明了网络组件的某个特征，可能限制用户对组件所作的操作。例如，Characteristics可以说明组件是否支持用户接口，是否可删除，或是否对子用户隐藏。

Characteristics 项可以有1个或多个如下的值(多值应计算总和)：

十六进制值 名字 描述

ox1 NCF_VIRTUAL 说明组件是个虚拟适配器

ox2 NCF_SOFTWARE_ENUMERATED 说明组件是一个软件模拟的适

配器

ox4 NCF_PHYSICAL 说明组件是一个物理适配器

ox8 NCF_HIDDEN 说明组件不显示用户接口

ox10 NCF_NO_SERVICE 说明组件没有相关的服务(设

备驱动程序)

ox20 NCF_NOT_USER_REMOVABLE 说明不能被用户删除(例如，

通过控制面板或设备管理器)

ox40 NCF_MULTIPORT_INSTANCED_ADAPTER 说明组件有多个端口，每个端

口作为单独的设备安装。每个

端口有自己的hw_id(组件ID)

并可被单独安装，这只适合于

EISA适配器

ox80 NCF_HAS_UI 说明组件支持用户接口(例如，

Advanced Page或Customer

Properties Sheet)

ox400 NCF_FILTER 说明组件是一个过滤器

下面Characteristics值的组合是不被允许的：

·NCF_VIRTUAL,NCF_SOFTWARE_ENUMERATED,和NCF_PHYSICAL是相互排斥的。

·NCF_NO_SERVICE不能和NCF_VIRTUAL,NCF_SOFTWARE_ENUMERATED,或NCF_PHYSICAL同时使用。虚拟的、软件仿真的适配器或物理适配器必须有一个相关的服务(设备驱动程序)。

下面是支持用户接口的物理适配器的Characteristics项例子：

Characteristics =Ox84;NCF_PHYSICAL,NCF_HAS_UI

BusType

物理适配器的DDInstall节必须包含BusType项，该项说明了总线类型(PCI，ISA，等等)。

BusType 项可能的值由INTERFACE_TYPE枚举说明：

总线类型 值

ISA 1

EISA 2

MicroChannel 3

TurboChannel 4

PCIBus 5

VMBus 6

NuBus 7

PCMCIABus 8

Cbus 9

MPIBus 10

MPSABus 11

PNPISABus 14

PNPBus 15

如果适配器可用于多种总线类型，那么安装该适配器的INF文件对每种总线类型都应有一个DDInstall节。例如，如果适配器可用于ISA总线和PnPISA总线，INI文件应该有ISA的DDInstall节和PnPISA的DDInstall节。在DDInstall节中的BusType说明该节的总线类型。例子如下：

［al.isa］

BusType=1

［al.pnpisa］

BusType=14

EisaCompressedId和AdapterMask

安装EISA网络适配器的INF文件的DDInstall节必须包括EisaCompressedId项，该项说明EISA压缩ID和适配器掩码。例子如下：

EisaCompressedId=Ox24322432

AdapterMask=Oxffff

PortlDeviceNumber和Port1FunctionNumber

安装多端口网络适配器的INF文件的DDInstall节，包括Port1DeviceNumber项或Port1FunctionNumber项。说明了这个项，当鼠标在网络适配器名字或图标上时，适配器端口信息显示于连接属性对话框（这个通过网络和拨号文件夹来访问）中。如果适配器端口号映射到PCI设备号，则要使用Port1DeviceNumber项。将Port1DeviceNumber设置为第一个PCI设备号。例如，如果PCI设备号4映射到端口1，PCI设备号5映射到端口2，PCI设备号6映射到端口3。用如下项：

Port1DeviceNumber=4

如果适配器端口号顺序映射到PCI函数号，则要使用Port1FunctionNumber项。将Port1FunctionNumber设置成第一个PCI函数号。例如，如PCI函数号2映射到端口1，PCI函数号3映射到port2,PCI函数号4映射于端口3，等等。用如下项：

Port1FunctionNumber=2

PCI设备号或PCI函数号到端口的映射被认为是静态的。同时也认为适配器端口是顺序编码的。

Port1DeviceNumber 和Port1FunctionNumber项是互相排斥的。如果两个项存在于同一DDInstall节中，则只使用Port1DeviceNumber项。

1.2.5删除节

删除NetClient,NetTrans,和NetService组件支持的节，但不删除Net组件(适配器)。网络类安装器不对适配器实例进行跟踪。删除节导致删除其他网络适配器和适配器的其他实例共享的文件，使适配器或适配器实例无法工作。

如果必须删除一个Net组件所使用的驱动程序文件，则要用协作安装程序跟踪所有使用该文件的驱动程序。这种协作安装程序既可跟踪同一设备的多个实例，也可跟踪多个设备的驱动程序。协作安装程序的更多信息，参见《Windows 2000 Driver Development Reference》的第一卷的INF文献。

1.2.6 ControlFlags节

ControlFlags 节通常有1个或多个ExcludeFromSelect项。每个ExcludeFromSelect项说明一个网络组件，该组件不作为手动安装的一个选项显示给最终用户。ControlFlags节必须为如下项包括一个ExcludeFromSelect项：

·每个安装的即插即用适配器

·每个由程序自动增加（而非用户手动增加）的软件组件

非即插即用的适配器必须由用户手动增加，因此不应在ControlFlags节中列出。例如，ISA和EISA适配器必须由用户手动安装。

ExcludeFromSelect 项的功能和DDInstall节中的Characteristics项的NCF_HIDDEN的功能是不同的。

ExcludeFromSelect 项防止适配器或软件组件在选择安装组件对话框中显示。然而，适配器或组件仍能在连接对话框中列出。NCF_HIDDEN防止适配器或组件在任何用户界面中出现，包括连接对话框。更多的信息，参见《Windows 2000 Driver Development Reference》的第一卷。

1.2.7网络INF文件的add-registry-sections

INF对每个安装的组件都包含1个或多个add-registry-sections。Add-registry-section向注册表增加键和值。INF的DDInstall节包含AddReg说明，它引用1个或多个add-registry-sections。关于add-registry-section和AddReg说明，参见《Windows 2000 Driver Development Reference》卷1中的INF文档。

向组件的实例键中增加键和值

一个或多个add-registry-sections能向实例键中增加键和值：

·为组件设置静态参数(不能通过用户接口修改的配置参数)。参见1.2.7.1节

·说明端口数目(如信道，电路或bearer信道)参见1.2.7.2节

·说明ISDN适配器的键和值。参见1.2.7.3节

·请求另一个网络组件的安装。参见1.2.7.4节

·说明支持定制属性页的值。参见1.2.7.12

向NetClient组件增加键和值

一个NetClient组件的add-registry-section必须将一个NetworkProrider键加入到该组件的service键中。

NetworkProvider 键有2个值：说明网络提供者名字的Name和描述网络提供者DLL完全路径的ProviderPath。参见1.2.7.6。

生成Ndi键

每个网络INF文件必须包括至少一个add-registry-section,用来为该文件安装的组件增加Ndi键。Ndi键是一个特殊网络键，在组件的实例键内。加入到Ndi键中的键和值根据网络组件类型和相容性而不同。Ndi键支持如下信息：

·为NetTrans,NetClient,或NetService组件说明HelpText值。参见1.2.7.7。

·为通知对象说明值。参见1.2.7.8

·说明相关服务值。参见1.2.7.9

·说明绑定接口。参见1.2.7.10

·为高级页说明适配器配置参数。参见1.2.7.11

·为过滤器服务说明值。参见1.2.7.13。

·说明束成员关系。参见1.2.7.14

在Windows 95/98中可用，在Window 2000中不再使用的Ndi注册键和值的列表参见1.2.7.15。

1.2.7.1设置静态参数

静态参数只能用INF文件设置一次，不能通过属性页重新配置。

add-registry-section 将一个REG_S2值，作为静态参数加入到组件的实例键中。下面是一个例子，将两个静态参数加入到组件实例键中。

［al.staticparams.reg］

HKR,,MediaType,0,"1"

HKR,,InternalId,0,"232"

add-registry-section 可以将一些供应商定义的静态参数加入到组件实例键中。

1.2.7.2 为WAN适配器说明WAN端点

WAN适配器的INF文件必须向适配器实例键中增加WanEndpoionts值。WanEndpoints是一个REG_DWORD值，说明了WAN适配器支持的端点数目(如信道，电路或bearer channels)。例如，BRI(基本速率接口)ISDN适配器的WanEndPoints值是2，而PRI(主速率)ISDN适配器的WanEndPoints值是23。

下面是add-registry-section的例子，为一个BRIISDN适配器增加WanEnelpoionts,其值为2。

［al.reg］

HKR,,WanEndpoints,Ox00010001,2

1.2.7.3为ISDN适配器说明ISDN键和值

除了WanEndpoints值(参见1.2.7.2节)，ISDN适配器的INF文件还必须向适配器实例键中加入如下键和值(通过add-registry-section)。

IsdnNumDChannels

这个REG_DWORD类型的值说明了ISDN适配器支持的D-Channels的数目。

IsdnAutoSwitchDetect(Optional)

这个可选的REG_DWORD类型值说明,是否ISDN适配器支持自动交换检测。值1表示支持，值0不支持。

IsdnSwitchType

这个REG_DWORD值说明了ISDN适配器支持的交换类型：

ISDN_SWITCH_AUTO Auto Detect(NorthAmericalonly)

ISDN_SWITCH_AUT O ESS5(AT&T,NorthAmerica)

ISDN_SWITCH_NI1 National ISDN1(NI_1)

ISDN_SWITCH_NI2 National ISDN2(NI_2)

ISDN_SWITCH_NT1 Northern Telecom DMS 100(NT_1)

ISDN_SWITCH_INS64 NTT INS64(Japan)

ISDN_SWITCH_ITR6 Germal National(ITR6).此类型很少使用

ISDN_SWITCH_VN3 French National(VN3).此类型很少使用

ISDN_SWITCH_NET3 European ISDN(DSS1)

ISDN_SWITCH_DSS1 European ISDN(DSS1)

ISDN_SWITCH_AUS Australian National此类型很少使用

ISDN_SWITCH_BEL Belgium National此类型很少使用

ISDN_SWITCH_VN4 French National(VN4)

ISDN_SWITCH_SWE Swedish National

ISDN_SWITCH_ITA Italian National

ISDN_SWITCH_TWN TaiWan National

为说明多个交换类型，只要将交换类型值相加即可。ISDN向导（在安装ISDN组件时自动运行）允许用户选择由IsdnSwithTypes说明的交换类型。选择的交换类型决定了随后显示的ISDN配置参数。这些参数包括电话号码，SPID(service profile identifier),子地址和多用户号。

IsdnNumBchannels(增加到D-Chanel键)

D-Channel值是一个0～9的索引，用于指明D-Channel。

IsdaNumBChannels 是一个REG_DWORD值，该值被添加到D-channel键中。IsdnNumBchannels说明了由D-Channel支持的B-Channel的数目。

下面是向ISDN适配器实例键增加ISDN键和值的例子。说明了两个D-Channels，每个D-Channel中说明了两个B-Channel。

［ISDNadapter,reg］

HKR,,WanEndpoint,Ox00010001,4

HKR,,IsdnNumDChannels,Ox00010001,2

HKR,,IsdnAutoSwitchDetect,Ox00010001,1

HKR,,IsdnSwitchType,Ox00010001,Ox00000004;NI1

HKR,0,IsdnNumBChannels,Ox00010001,2

HKR,1,IsdnNumBChannels,Ox00010001,2

ISDN向导自身也根据用户指定的参数值向ISDN适配器的实例键中增加ISDN键和值。ISDN向导增加如下的键和值：

·IsdnSwitchType

这个REG_DWORD值指示用户选择的交换类型

·对每个D-Channel,有一个IsdnMultiSubscriberNumbers值.这个REG_MULTI_S2值指明了用户说明的multi_subscriber数目。

·每个B-Channel有一个B-Channel键、IsdnSpid、IsdnPhoneNumber和/或一个IsdnSubaddress值。

· B-Channel键是指示B-Channel的由0开始的索引。B-Channel键值的最大值比IsdnNumBChannels的值小1。

·IsdnSpid是指示SPID的一个REG_SZ值。如果有，就由用户来说明。

·IsdnPhoneNumber是电话号码.如果有，由用户说明

下面是ISDN适配器注册表节的布局示例。每个注册键由方括号括起，例如：［keyname］。粗体ISDN键和值由ISDN适配器的INF文件添加。非粗体的ISDN键和值由ISDN向导添加。

［Enum\emumeratorID\device-instance-id］ ;ISDN适配器实例键

WanEndPoints=4

IsdnNumDChannels=2

IsdnAutoSwitchDetect=1

IsdnSwitchType=Ox4 ;NationalISDN1

［Enum\emumeratorID\dervice-instance-id\0］ ;D-Channel0

IsdnNumBChannels=2

IsdnMultiSubscriberNumbers=123456723456783456789

［Enum\emumeratorID\device-instance-id\0\0］ ;D-channel0的B-Channel0

IsdnSpid=00555121200

IsdnPhoneNumber=5551212

IsdnSubaddress=

［Enum\emumeratorID\device-instance-id\0\1］ ;D-Channel0的B-Channel1

IsdnSpid=00555121300

IsdnPhoneNumber=5551213

IsdnSubaddress=

［Enum\emumeratorID\dervice-instance-id\1］ ;D-Channel1键

IsdnNumBChannels=2

IsdnMultiSubscriberNumbers=867530923901257658156

［Enum\emumeratorId\dervice-instance-id\1\0］ ;D-Channel1的B-Channel0

IsduSpid=00555987600

IsdnPhoneNumber=5559876

IsdnSubaddress=

［Enum\emumeratorID\dervice-instance-id\1\0］ ;D-Channel1的B-Channel1

Isdnspid=00555876500

IsdnPhoneNumber=5558765

IsdnSubaddress=

1.2.7.4安装多协议WAN NICs

多协议WAN NIC提供多于1种的WAN协议。例如，NIC可能允许用户选择ISDN，帧中继或T1信道。在安装NIC或配置NIC期间，用户可以选择WAN协议。

多协议WAN NIC的供应商必须提供协作安装程序，以安装向导页。(协作安装程序详细信息，参见《Plug and Play,Power Management,and Setup Design Guide》，以及《Windows 2000 Driver Development Reference》卷1)。向导页提示用户选择WAN协议。

·如果用户选择ISDN，则显示ISDN向导。ISDN向导提示用户输入ISDN交换类型和其他ISDN参数值。(参见1.2.7.3节)

·如果用户选择WAN协议，向导在WAN NIC的实例键中增加ShowIsdnPages值。在这种情况中，向导将ShowIsdnPages设置成0，从而阻止ISDN向导的显示。

在安装WAN NIC后，用户可以使用NIC的属性页重新配置NIC。

·如果用户将协议从ISDN改为WAN协议，属性页将ShowIsdnPage值加入WAN NIC的实例键中。属性页设置ShowIsdnPages为0，阻止ISDN向导的显示。

·如果用户将协议改为ISDN，WAN NIC的属性页显示一个对话框提示用户确认改变。当用户确认改变后，属性页将ShowIsdnPages设置成1。当用户再次打开属性页时，显示ISDN向导。如果多协议WAN NIC支持ISDN，绑定接口中的LowerRange必须设置成isdn(参见1.2.7.10节)。如果showIsdnPage注册值不存在，且NIC的LowerRanger被设置为isdn，安装和配置时显示ISDN向导。如果showIsdnPages设置成0，ISDN向导不显示。ShowIsdnPage设置成1，ISDN向导在NIC配置时显示。

1.2.7.5请求安装另一个网络组件

为正常运作，网络组件可能需要安装1个或多个其他网络组件。网络INF文件用RequiredAll值说明这些依赖性。RequiredAll值被加入到需要安装其他组件的组件的Ndis键中。(通过add-registry-section)。

下面的例子显示了add-registry-section的RequiredAll项：

［ndi.reg］

HKR,Ndi,RequiredAll,0,"Component id"

组件ID是所需要的网络组件的hw-id(参见1.2.3节)。如果网络组件要安装多个其他组件，对每个组件都使用一个RequiredAll项，如下所示：

［HKR,Ndi,RequireAll］,0,"component2 id"

RequiredAll 仅用于安装那些不能由用户安装的隐藏网络组件。这种组件不为用户接口所支持。

由RequiredAll说明的组件只有在通过RequiredAll请求安装该组件的组件被删除后，才能被删除。例如，如果组件A的INF文件使用RequiredAll说明了对组件B的依赖，组件B在组件A删除时才能删除。

RequiredAll 仅安装在组件运行时，必须需要的其他组件。例如，Net组件(适配器)的INF文件中，使用RequiredAll说明安装TCP/IP，用户在适配器删除前不能删除TCP/IP。由于适配器可以不需要TCP/IP，适配器的INF文件不应该使用RequiredAll说明对TCP/IP的依赖。

说明RequiredAll依赖性的INF文件，必须确保所需网络组件的INI文件在inf目录下。它通常有一个CopyFiles节。CopyFiles节的更多信息，参见《Plug and Play,Power Management,and Setup Design Guide》，以及《Windows 2000 Driver Development Reference》卷1。

如果由RequiredAll说明的网络组件安装失败，依赖于该组件的网络组件也无法成功安装。

1.2.7.6说明NetCLient组件的名字和提供者

在用户接口中可见的安装NetClient组件的INF文件必须在该组件的Service键中增加一个NetworkProvider

键。INF文件通过add-registry-section增加NetworkProvider键，这在该组件的Service-install节中用AddReg引用。

NetWorkProvider 键有2个值：一个描述网络提供者字的Name和描述网络提供者DLL完全路径的ProviderPath。

下面是一个add-registry-section的例子，用以向组件实例键增加NetworkProvider键。

［NWCWorkStation.AddReg］

HKR,NetworkProvider,Name,0,"NetWare or Compatible Network"

HKR,NetworkProvider,ProviderPath,Ox2000,"%11%\nwprovau.dll"

安装NetClient组件的INF文件不必修改组件的...Control\Network\Provider\Order键下的ProviderOrder值。这由网络类安装器自动完成。

1.2.7.7增加HelpText值

NetTrans,NetClient,NetService网络组件的INF文件应该在组件Ndi键中增加HelpText值(REG_SZ)。HelpText值是一个字符串，说明组件的功用。例如，NetClient组件的HelpText值不应只简单地指明这个客户，而且还需指出客户允许用户和什么连接。在连接属性对话框的Genenal页中，当页中组件被选择时，HelpText值出现在页底部。Net组件(适配器)和IrDA组件不支持HelpText值。下面是add-registry-section的例子，用来向Ndi键中增加HelpText值：

［ms_Protocol.ndi_reg］

HKR,Ndi,HelpText,0,%MyTransport_Help%

HelpText 值是一个%strkey%形式的标志，这个在INF文件的Strings节中定义。Strings节的更多信息参见《Plug and Play,Power Management,and Setup Design Guide》，以及《Windows 2000 Driver Development Reference》卷1。

1.2.7.8为通知对象增加注册值

NetTrans,NetClient或NerService组件可以有一个通知对象，用来实现以下功能：

·显示组件用户接口

·将绑定事件通知组件，使组件能实现绑定过程上的一些控制。

·提供条件安装。

详情参见第2章。

Net组件(适配器)不支持通知对象，它使用的是协作安装程序。协作安装程序的详情，参见《Plug and Play,Power Management,and Setup Design Guide》，以及《Windows 2000 Driver Development Reference》的卷1的协作安装程序文档。

如果组件提供通知对象，则此组件的INF文件必须将下面两个值加进组件Ndi键。

·ClsID

说明组件对象的GUID。通过运行uuidgen.exe得到GUID。详情参见 Platform SKD。

·ComponentDll

说明通知对象DLL的路径。如果DLL不在Window\system32目录下，则应说明为完全路径。

下面是将ClsID和ComponentDll加入到Ndi键中的add-registry-section例子：

［MS_Protocol.ndi.reg］

HKR,Ndi,ClsID,O,“GUID”

HKR，Ndi,ComponentDll,O,"notifyobject.dll"

有通知对象的组件的DDInstall节必须包含CopyFiles指令，来引用file-list-section,该节将通知对象DLL复制到DirectionDirs节说明的目的目录中。关于CopyFiles指令和DirectionDirs节的详情参见《Plug and Play,Power Management,and Setup Design Guide》，以及《Windows 2000 Driver Development Reference》的卷1的INF文档。

1.2.7.9向Ndi键增加服务相关值

如果组件有一相连的服务(设备驱动程序)，add-registry-section必须将Service值加入到Ndi键中。该值是个REG_SZ值，说明了与组件相连系的主要服务。Service值必须和AddService指令中的ServiceName参数一致，该指令引用了Serivce-install-section(参见1.2.8节)。

如果组件有1个或多个相连系的服务，由DDInstall节引用的add-registry-section必须将CoServices值加入到Ndi键中。CoService值是MULTI_SZ值，说明了组件安装的所有服务，包括Service值说明的主要服务。所有NetTrans,NetClient和NetService组件都需要CoServices值。由于仅有1个服务能连系于1个适配器，Net组件(适配器)不应支持CoServices值。除了关闭服务，所有服务相关的操作按它们在Coservices中所列的顺序执行。例如，服务按它们所列的顺序开始，关闭时却是逆序的。只有当服务在CoServices中列出时，服务相关的操作才能被执行。

如果在CoServices中列出的服务不想在组件安装时启动，这些服务应该在ExcludeSetupStartServices值(MULTI_SZ)中列出，该值被加入到Ndi键中。

下面是一个add-registry-section,用来将服务相关值加入到Ndi键中：

［Ms_Protocol.ndi.reg］

HKR,Ndi,Service,O,"MYT3"

HKR,Ndi,CoService,Ox10000,"MYT3","MYT3CO"

HKR,Ndi,ExcludeSetupStartService,Ox10000,"MYT3CO"

1.2.7.10说明绑定接口

对安装的每个网络组件，网络INF文件必须为此组件说明向上和向下的接口，这可以通过向NDi键中加入Interface键来达到。

Interface 键至少有2个值：

·1个UpperRange值(REG_SZ)，用以定义组件可以绑在其上边界的接口。

·1个LowerRage值(REG_SZ)，用以定义组件可以绑定在其下边界的接口。(对物理适配器来说，这个接口是网络介质，如以太网或信令环网)。

Windows 95/98的网络INF文件中的DefUpper和DefLower，Windows 2000 的INF文件不支持。

下表列出了微软支持的UpperRange值：

UpperRange 值 描述

Netbios NetBIOS

Ipx IPX

Tdi TCP/IP的TDI接口

ndis5 NDIS 5.x(ndis2,ndis3和ndis4不应再用)。对于非ATM

网络组件这个值必须说明，如非ATM适配器，

它的上边界与NDIS接口。

Ndisatm ATM支持的NDIS 5.x。这个值在ATM网络组件中是必须

的，如ATM适配器，它的上边界和NDIS接口相接。

ndiswan WAN适配器的上边界。这个值的说明导致操作系统自动

使WAN适配器用于RAS。

Ndiscowan WAN适配器的上边界，面向连接的NDIS在上面运行。

Noupper 所有不暴露上边界给绑定用途的组件的上边界。例如

有一个private接口的组件。

Winsock Windows socket接口

ndis5_atalk NDIS 5.x Net组件(适配器)的上边界，在其上边界仅绑

定AppleTalk接口

ndis5_dlc NDIS5.xNet组件适配器的上边界，在其上边界仅绑定

DLC接口

ndis5_ip NDIS5.xNet组件(适配器)的上边界，在其上边界仅绑定

TCP/IP接口

ndis5_ipx NDIS5.xNet组件(适配器)的上边界，在其上边界仅绑定

IPX接口

ndis5_nbf NDIS 5.x Net组件(适配器)的上边界，在其上边界仅绑

定NetBEUI接口

ndis5_strems NDIS5.xNet组件(适配器)的上边界，在其上边界仅绑定

streams接口

下表是微软支持的LowerRange值的列举：

LowerRange值 描述

ethernet 以太网适配器的下边界

atm ATM适配器的下边界

tokenring 令牌环网的下边界

serial serial 适配器的下边界

fddi FDDI适配器的下边界

baseband baseband适配器的下边界

arcnet Arcinet适配器的下边界

localtalk LocalTalk适配器的下边界

isdn ISDN适配器的下边界

wan WAN适配器的下边界

nolower 某些组件的下边界，这些组件都不将下边界暴露给绑

定用途。

ndis5 NDIS 5.x(ndis2,ndis3,ndis4不再使用)。对于所

有下边界通过NDIS与非ATM组件接口的组件都必须

说明。

Ndisatm 由ATM支持的Ndis5.x。所有下边界通过NDIS与ATM

组件接口的组件都必须说明。

UpperRange 和LowerRange说明了组件可绑定的接口类型，而不是实际的组件。绑定引擎将网络组件绑定到所有提供相应接口（在适当的边界上）的组件。例如，LowerRange值为ndis5的协议，绑定到所有UpperRange值为ndis5的组件，如物理或虚拟适配器。

如果NDIS 5.x Net组件(适配器)和一个或多个协议工作，那么它的UppeRange应该赋予一个或多个协议值，如ndis5_atalk,nids5_dlc,ndis5_ipx,ndis5_nbf或ndis5_streams。这样的Net类组件不应将其UpperRange值赋为ndis_5，因为这将使该组件绑定于所有提供ndis5下边界的协议。INF文件开发者可以对private绑定接口使用供应商的特定UpperRange和LowerRange值。例如，如果供应商只想将其适配器绑定到自己的私有协议驱动程序，那么INF文件开发者可以将适配器的UpperRange说明为XXX，私有协议的LowerRanger说明为XXX。Windows 2000绑定引擎将所有UpperRange值为XXX的组件(此例中是适配器)绑定到所有LowerRange值为XXX的组件(此例中是私有协议)上。

下面是add-registry-section的一个例子，用于为ATM适配器增加UpperRange和LowerRange。

［addreg-section］

HKR,Ndi\Interfaces,UpperRange,0,"ndisATM"

HKR,Ndi\Interfaces,LowerRange,0,"atm"

1.2.7.11为高级属性页说明配置参数

安装Net组件(适配器)的INF文件可以说明适配器配置参数，这些参数将在组件的高级属性页中显示。用户在高级属性页中说明的配置值被写到此组件的根实例键中。

如果适配器支持高级属性页，适配器的DDInstall节中的Characteristics项必须包括NCF_HAS_UI值。网络INF文件通过add-registry-section说明在高级属性页中显示的配置参数，add-registry-section在该组件的DDInstall节中引用。这种add-registry-section在Ndi\params键中增加一个或多个子键。配置参数的子键格式为Ndi\params\SubkeyName, SubkeyName是说明供应商参数名字的REG_SZ值。例如，说明transceiver类型参数的键，可以命名为Ndi\params\TransceiverType。

以下的键保留，不能作为Ndi\params\SubkeyName使用。这些键包括BundleId,Characteristics,ComponentId,Description,DriverDesc,InfPath,InfSection,InfSectionExt,Manufacturer,NetCfgInstanceId,Provider和ProviderName。

对于每个加入到Ndi\params中的参数子键，add-registry-section必须加入ParamDesc(参数描述)和Type值。add-registry-section也可以为这个参数增加Default和Optional值，并且如果参数式数字的，也可增加Min,Max和Step值。下表描述了可被加入到Ndi\params键中的值:

值名 |  | 值 | 描述
 | ParamDesc | String | 说明在高级属性页中显示的参数名
 | Type | int,long,word,dword, edit,或enum | 说明参数类型int,long,word说明是数字参数。edit,enum说明是文本参数。
 | Default | 默认值 | 为参数说明默认值。对数字参数，默认值必须是数值(int,long,word或dword)。对文本参数，默认值必须是字符串。必选参数必须设默认值，可选参数不应设默认值。
 | optional | 0或1 | 0说明参数是必需的。1说明参数是可选的。在高级属性页中用户可以将可选参数设置为Not Present。但对于必需参数，用户必须说明一个值或使用默认值。
 | Min | 数字值 | 说明数字参数的最小值
 | Max | 数字值 | 说明数字参数的最大值
 | Step | 数字值 | 说明数字参数有效值之间的步长。以最小值为起点。

enum 参数的值域由如下形式的子键说明：

Ndi\params\SubkeyName\enum

每个枚举值都必须提供这个键。每个enum子键说明一个数字值(从0开始)和对该值的描述。

下面是add-registry-section的例子，用来增加名为TransType的配置参数。

［al.params.reg］

HKR,Ndi\params\TransType,ParamDesc,0,"TranseiverType"

HKR,Ndi\params\TransType,Type,0,"enum"

HKR,Ndi\params\TransType,Default,0,"0"

HKR,Ndi\params\TransType,optional，0,"0"

HKR,Ndi\params\TransType\enum,"0",0"Auto-Connector"

HKR,Ndi\params\TransType\enum,"1",0,"Thick Net(AUI/DIX)"

HKR,Ndi\params\TransType\enum,"2",0,"Thin Net(BNC/COAX)"

HKR,Ndi\params\TransType\enum,"3",0,"Twisted-Pair(TPE)"

1.2.7.12为网络适配器说明定制属性页

如果高级属性页没有为Net组件(适配器)提供合适的配置选择，可以生成一个或多个定制属性页，如下所示：

1.生成Microsoft Win32属性页。参见Platform SDK

2.生成属性页扩展DLL，该DLL提供AddPropSheetPageProc和ExtensionPropSheetPageProc回调函数。参见Platform SDK。

3.用add-registry-section将EnumPropPages32键加入到适配器的实例键中。EnumPropPages32键有两个REG_SZ值：输出AddPropSheetPageProc函数的DLL名字和ExtensionPropSheetPageProc函数的名字。下面是一个add-registry-section例子，用来增加EnumPropPages32键：

HKR，EnumPropPages32,0,"DLL name,ExtensionPropSheetPageProc function name"

4.在适配器的INF文件中，包括一个CopyFiles节，将属性页扩展DLL复制到Windows/systems32目录下。关于Copyfile节的详情，参见《Plug and Play,Power Management,and Setup Design Guide》，以及《Windows 2000 Driver Development Reference》的卷1的INF文档。

5.在适配器的DDInstall节中，将NCF_HAS_UI说明为一个Characteristics值(参见1.2.4节)，用以指明适配器支持的一个用户接口。

6.当用户确认属性页中所作的改动后，属性页扩展DLL必须调用SetupDiGetDeviceInstallParams,并在在参数SP_DEVINSTALL_PARAMS结构中设置DI_FLAGEX_PRORCHANGE_PENDING标志，然后用该结构调用SetupDiSetDeviceInstallParams,重装驱动程序，从而读入改变后的参数值。

1.2.7.13说明过滤器服务值

安装网络过滤器组件的INF文件必须说明过滤器服务值。本节将描述怎样定义一个过滤器服务。网络过滤器组件包括以下两个部分：

·过滤器服务

·过滤器设备

一个网络过滤器的服务和设备属于同一过滤器驱动程序。安装网络过滤器既需要过滤器服务的INF文件也需要过滤器设备的INF文件。过滤器服务INF文件必须说明Ndi键下的过滤器组件的Service值。过滤器设备INF文件，包含了AddService指令，该指令引用了Service-install节，在该节中说明了过滤器服务在何时、怎样被装载。AddService指令的ServiceName值必须和过滤器组件的服务（Service）名相匹配。过滤器设备INF文件中的Service-install节的ServiceBinary项说明了过滤器驱动程序的二进制路径。在过滤器安装过程中，用过滤器服务INF文件的CopyFiles指令，将过滤器驱动程序传输到目标计算机中。参见1.2.7.9节和1.2.8节。

下面是过滤器服务INF文件说明过滤器组件服务名字的一个例子：

HKR，Ndi,service,,sfilter

这个过滤器组件服务名字也就是要提交给NDIS的名字。过滤器组件服务名字不需要和过滤器驱动程序的二进制名字相同，但在通常情况下它们是相同的。

当INF文件增加过滤器组件服务时，过滤器设备INF文件是怎样引用过滤器组件服务名的呢?

示例如下：

[SFilterMP.ndi.Services]

AddService=Sfilter,2,SfilterMP.AddService

［SFilterMP.AddService］

DisplayName=%SFilter_Desc%;"Sample Filter Miniport"

ServiceType=1;SERVICE_KERNEL_DRIVER

StartType=3;SERVICE_DEMAND_START

ErrorControl=1;SERVICE_ERROR_NORMAL

SericeBinary=%12%\passthru.sys;filterdriver

LoadOrderGroup=PNP_TDI

过滤器服务INF文件的Ndi键必须支持如下注册表项，用以定义过滤器服务：

FilterClass

FilterClass 说明了过滤器服务类。过滤器类可以是下表值中的一个：

值 含义

scheduler 包调度过滤服务。这个过滤器类在链中是最高的。包调度程序检测

802.1p优先权，这优先权是由QoSsignaling组件赋给包的。

包调度程序根据优先级将这些包发送给低层的驱动程序。

loadbalance 负载平衡过滤服务。这个过滤器类在包调度和失败结束过滤器之

间。负载平衡过滤器通过将工作负载分配给微端口实例束，平衡

包传输的工作负载。

failover 失败结束过滤器。这个过滤器类是链中最低的。那就是说没有其他

过滤器可以处于这个过滤器设备和适配器之间。

FilterClass 值确定过滤器在过滤器栈中的位置。

注意：每个过滤器服务类只有一个过滤器存在于过滤器分层栈中。例如，两个调度过滤器不能同时在栈中出现。

FilterDeviceInfFile

FilterDeviceInfFile 为过滤器设备说明INF文件的名字。过滤器设备INF文件定义了过滤器设备，过滤器服务用它为某个网络适配器过滤信息。过滤器设备是一个虚拟NIC。

FilterDeviceInfId

FilterDeviceInfId 说明了过滤器设备的标识符。标识符在过滤器设备的INF文件的Models节中列出。过滤器设备是由过滤器驱动程序为每个物理适配器初始化的微端口实例。

FilterMediaTypes

FilterMediaTypes 说明了过滤器服务过滤的介质类型。介质类型列表，参见1.2.7.10节中的微软支持的LowerRange值列表。过滤器服务INF在Ndi键下Interfaces键中说明了这些介质类型。

注意：为FilterMediaTypes说明合适的介质类型是很关键的。过滤器设备只能安装在这种物理适配器上，即该适配器所连的网络介质至少要和过滤器介质类型中的一个相匹配。物理适配器的LowerRonge项说明了网络介质，如以太网或信令网。

下面是过滤器服务INF文件定义过滤器服务的值的一个例子：

HKR,Ndi,FilterClass,failover

HKR,Ndi,FilterDeviceInfFile,netsf_m.inf

HKR,Ndi,FilterDevviceInfId,ms_sfiltermp

HKR,Ndi,\Interfaces,FilterMediaTypes,"ethernet,tokenring,fddi"

虽然其他网络INF文件可能为安装的组件说明向上和向下绑定接口，但是过滤器服务INF文件必须说明,它不是出于绑定目的而暴露上、下边界。过滤器服务INF文件在Interface键中说明这个特性。下面是说明这个特性的例子：

HKR,Ndi\Interfaces,UpperRange,,noupper

HKR,Ndi\Interfaces,LowerRange,,noloner

1.2.7.14说明束成员关系

安装Net组件(物理或虚拟适配器)的INF文件可说明这些适配器属于同一个适配器束。NDIS中间层驱动程序和过滤器驱动程序(输出虚拟网络适配器)包括在Net类中。NDIS驱动程序通过在适配器束上分配工作负载，来平衡负载。

关于负载平衡的更多信息，参见《Miniport NIC Driver》的第2部分第10章。

为了说明适配器属于某个适配器束，安装适配器的驱动程序INF文件必须包含BundleId键和一个大小写敏感的字符串值(REG_SZ)。这个字符串值说明了适配器的驱动程序束。这个注册值用束标志符信息来配置。

下面是一个驱动程序INF文件的add-registry-section例子，用来将BundleId子键加入到Ndi\params键中，并赋给BundleId的ParamDesc(参数描述)一个字符串值“Bundle1”。

［al.params.reg］

HKR,Ndi\Params\BundeId,ParamDesc,0,"Bundle1"

1.2.7.15 Window 2000中不用的 Window 95/98 Ndi值和键

下面列出的Ndi注册键和值在Windows 95/98中使用，但在Windows 2000中不再使用。这些信息帮助开发者把驱动程序从95/98上移植到Windows 2000上。

·DevLoader

·DeviceVxD

·DriverDesc

·Ndi\DeviceID

·Ndi\NdiInstaller

·InfFile

·InfSelection

·Ndi\InstallInf

·Ndi\CardType

·StaticVxD

·Ndi\Interfaces\DefUpper

·Ndi\Interfaces\DefLower

·Ndi\Interfaces\RequireAny

·Ndi\Compability

·Ndi\Install

·Ndi\Remove

·Ndi\Params\Param_key_name\flag

·Ndi\Params\Param_key_name\location

·Ndi\param_key_name\resc

·Ndi\filename\...

·NDIS\...

Windows 2000不支持Ndi\param_key_name\resc和Ndi\Params\param_key_name\flag值。这意味着用户不能通过高级页配置适配器资源。

1.2.8 DDInstall.Service节

DDInstall.Services节包括一个或多个AddService指令，每个指令引用了INF作者定义的Service-install-section,它说明了组件的驱动程序何时以及如何加载。

在安装Net组件(适配器)的INF文件中必需一个DDInstall.Service s节。即在安装NetTrans,NetClient或NetService组件的INF文件中该节是可选的。

DDInstallService 节中的AddService指令也能引用为组件安装错误日志的error-log-install-section。错误日志对所有网络组件是可选的。DDInstall.Services节和AddService指令的细节化描述参见《Plug and Play,Power Management,and Setup Design Guide》以及《Windows 2000 Driver Development Reference》。第1卷的INF文件格式描述，。

下面是DDInstall.Services节，service-install-section,error-log-install-section和add-registry-section(被error-log-install-section中的AddReg指令引用)的例子：

［al.ndi.NT.Services］

AddService=a1,2,al.AddService,al.AddEventLog

［a1.AddService］

DisplayName=%Adapter1.DispName%

ServiceType=1;SERVICE_KERNEL_DRIVER

StartType=2;SERVICE_AUTO_START

ErrorControl=1;SERVICE_ERROR_NORMAL

ServiceBinary=%12%\al.sys

LoadOrderGroup=NDIS

［a1.AddEventlog］

AddReg=al.AddEventLog.reg

［a1.AddEventlog.reg］

HKR,,EventMessageFile,Ox00020000,"%%SystemRoot%%\system32\netevent.dll"

HKR,TypeSupported,Ox00010001,7

AddService 指令的ServiceName参数（上例中的a1）,必须和组件的Ndi\Service值相匹配。参见1.2.7.9节。

1.2.9 NetworkProrider和PrintProvider节

由于NetClient组件向用户提供网络服务，它们被认为是网络提供者。NetClient组件如Microsoft Client for Networks和NetWare Client。

除了作为网络提供者外，NetClient组件还可以是一个打印提供者。打印提供者向网络中的用户应用程序提供打印服务。

NetClient组件总是作为网络提供者来安装。安装NetClient组件的INF文件不需要NetworkProvider节，除非下述条件之一成立：

·说明了这个组件的替代设备名字。

·为使用net view命令，为组件指定短名(参见1.2.9.1节)。

安装作为打印提供者的NetClient组件的INF文件，必须为该组件包含一个PrinterProvider节(参见1.2.9.2节) 。安装NetClient组件的INF文件也必须包含一个add-registrg-section(通过service-install-section中的AddReg指令引用)，用来向组件service键中增加一个NetworkProvider键(参见1.2.7.6节)。

1.2.9.1包含一个NetworkProvider节

NetworkProvider节说明了1个或两个如下信息：

NetClient组件的替代设备名和用于net view命令的短名。为了创建一个NetworkProrider节，将.NetworkProvider扩展名加入到DDInstall节中，示例如下：

［DDInstall］ ;InstallSection

［DDInstall.NetworkProvider］ ;NetworkProvidersection

说明一个设备名

正常情况下，网络类安装器通过将组件的Ndi\Service值(参见1.2.7.9节)复制到组件service键下的NetworkProvider键中，来创建网络提供者的设备名。为了给组件说明不同的设备名，在NetworkProvider节中包含DeviceName项，如下例所示：

［DDInstall-section.NetworkProvider］

DeviceName="nwrdr"

DeviceName是可选的，仅在Ndi\Service值作为网络提供者的设备名不够用时，才会说明。

说明一个短名

为使用NetWare的net view命令而为网络提供者说明一个短名，将在NetworkProvider节中包含一个ShortName项，如下例所示：

[DDInstall-section.NetworkProvider]

ShortName=”nw”

下面是net view命令所用短名的例子：

net view /n:nw

ShortName 比网络提供者的全名容易记，也容易打印。

ShortName 是可选的，仅在需要的时候说明。

1.2.9.2 包括一个PrintProvider节

安装NetClient组件（这里是一个打印提供者）的INF文件必须包括PrintProvider节。为了生成一个PrintProvider节，在DDInstall节中加入.PrintProvider扩展名，如下例所示：

［DDInstall-section］;InstallSection

［DDInstall-section.printProvider］;PrintProvidersection

PrintProvider 节必须包括如下项：

·PrintProviderName

一个说明打印提供者的非本地字符串

·PrintProviderDll

打印提供者DLL的文件名

·DisplayName

说明打印提供者名字的本地字符串。DisplayName可以不同于PrintProviderName。

PrintProviderName 和PrintProviderDll项提供一些信息，这些信息被用作AddPrintProvider函数的输入值(PROVIDER_INFO_1结构)。AddPrintProvider函数将打印提供者组件作为打印提供者加入。关于AddPrintProvider函数的更多信息，参见Platform SDK。

下面是PrintProvider节的一个例子：

［DDInstall-section.PrintProvider］

PrintProviderName="NetWare or Compatible Network"

PrintProviderDll="nwprovau.dll"

DisplayName="%NWC_Network_Display_Name%"

1.2.10 Winsock节

提供Winsock接口的NetTrans组件的INF文件必须说明Winsock的依赖性。这个INF文件必须包含一个Winsockinstall节。为创建一个Winsockinstall节，将.Winsock扩展名增加到该协议的DDInstall节名中。例如，如果DDInstall节命名为Ipx，该协议的Winsockinstall节必须命名为Ipx.Winsock。

Winsock安装节必须包含AddSock指令。AddSock指令说明供应商命名的节，该节中包括了增加到组件的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControl\Services\TransportDriver-

Name\Params\Winsock 键中的值。

由AddSock指令引用的供应商命名的节必须包含如下值：

值名 描述

TransportService 说明协议服务名的REG_SZ值。这必须和该协议

Ndi\Servce值相同。(参见1.2.7.9节)

HelperDllName 一个REG_EXPAND_SZ值，说明该协议的

WindowsSocketsHelper(WSH)DLL。参见第3部分。

MaxSockAddLength 一个REG_DWORD值，说明了最大有效SOCKADDR的

尺寸，以字节为单位。

MinSockAddLength 一个REG_DWORD值，说明了最小有效SOCKADDR

的尺寸，以字节为单位

如果说明了名字空间提供的可选ProviderId，下面的值也必须说明：

值名 描述

ProviderId 说明GUID的REG_SZ值，用来签别名字空间提供者。GUID

作为所有名字空间提供者的键。通过运行

uuidgen.exe得到GUID。详情参参见platform SDK。

LibraryPath 一个REG_EXPAND_SX值，说明名字空间提供者DLL的完全路

径。

DisplayString 一个本地化的字符串，说明名字空间提供者在用户接口中的

显示名。

SupportedSpace 一个REG-DWORD值，说明一个由名字空间提供者支持的名字

空间。下面是在Winsock2.h中定义的名字空间值：

名字空间 值

NS_ALL 0

NS_SAP 1

NS_NDS 2

NS_PEER_BROWSE 3

NS_TCPIP_LOCAL 10

NS_TCPIP_HOSTS 11

NS_DNS 12

NS_NETBT 13

NS_WINS 14

NS_NBP 20

NS_MS 30

NS_STDA 31

NS_CAIRO 32

NS_X500 40

NS_NIS 41

NS_WRQ 50

Version 一个可选的REG_DWORD值，说明了名字空间提供者的版本号。

如果此值没有说明，则用缺省值(1)。名字空间

提供者的详情参见Platform SDK。

下例显示了IPX协议的Winsock节：

［Ipx.Winsock］

AddSock=Install.IpxWinsock

［Install.IpxWinsock］

TransportService=nwlinkpipx

HelperDllName="%%SystemRoot%%\System32\wshisn.dll"

MaxSockAddrLength=Ox10

MinSockAddrLength=Oxe

ProviderId="GUID"

LibraryPath="%SystemRoot%\\System32\\nwprovaa.dll"

DisplayString=%NwlnkIpx_Desc%

SupportedNameSpace=1

Version=2

通过包括一个Winsockremove节，INF文件为一个协议删除Winsock依赖性。为创建一个Winsock-remove节，将.Winsock扩展名加入到协议的Remove节名中。例如，协议的Remove节名是Ipx.Remove,那么该Winsock-remove节名必须为Ipx.Remove.Winsock。

Winsock-remove 节包含一个DelSock指令，该指令说明了一个INF-writer-named节。INF-Writer-named节必须说明需删除的传输服务。如果ProviderId已经注册，vendor-named节也必须说明要删除的ProviderId.下例显示了删除Ipx协议Winsock依赖性的两个节：

［Ipx.Remove.Winsock］

DelSock=Remove.IpxWinsock

TransportService=nwlinkipx

ProviderId="GUID"

1.2.11网络组件安装需求总结

本节总结如下类型的网络组件安装需求：

·网络适配器

·网络协议驱动程序

·中间层网络驱动程序

·网络过滤器驱动程序

·网络客户

·网络服务

1.2.11.1网络适配器的安装需求

本节总结了网络适配器的安装需求。

一般需求

INF文件节 | 状态 | 注释
Version | 需要 | Class=Net ClassGUID={4D36E972-E325-11CE-08002BE10318}
SourceDisksNames 和 SourceDiskFiles | 如果.., 则需要 | 如果INF文件没有随Windows 2000发布时需要。如果INF文件随Windows 2000发布，在version节中必须说明一个LayoutFile项，并且不用SourceDiskNames和SourceDiskFiles节。
DestinationDirs | 需要 | 没有特殊网络需求。
ControlFlags | 需要 | 在安装即插即用(PnP)适配器的INF文件中必须包含ExcludeFromSelect项。对非PnP适配器，无需列出。
Manufacturer | 需要 | 无特殊网络需求
Models | 需要 | hw-id必须和PnP管理器的适配器提供的硬件ID相匹配。
DDInstall | 需要 | Characteristics 项可用的值： NCF_VIRTUAL,NCF_SOFTWARE_ENUMERATED,NCF_PHYSICAL,NCF_MULTIPORT_INSTANCED_ADAPTER,NCF_HAS_UI,NCF_HIDDEN,NCF_NOT_USER_REMOVABLE。 NCF_VIRTUAL,NCF_SOFTWARE_ENUMERATED和NCF_PHYSICAL是相互排斥的。 对于物理适配器，BusType项是必需的。对EISA适配器，EisaCompressedID项是必需的。这个项说明了EISA压缩ID和适配器掩码。多端口网络适配器需要Port1DeviceNumber或Port1FunctionNumber项。
DDInstall.Services | 需要 | 无特殊网络需求
add_reg_sections | 需要 | 需要： 创建Ndi键 说明服务相关值 说明束成员关系(仅对LBFO微端口) 说明绑定接口 可用的绑定接口： UpperRange: ndis5,ndisatm,ndiswan,ndiscowan,noupper,ndis5_atalk,ndis5_dlc,nids5_ip,ndis5_ipx,ndis5_nbf,ndis5_streams LowerRange: ethernet,atm,tokenring,serial,fddi,baseband,broadbond,arcnet,isdn,localtalk,Wan 可选项： 为组件设置静态参数 需要其他网络组件的安装 为高级属性页说明配置参数 生成定制属性页
Strings | 需要 | 无特殊网络需求

WAN适配器的额外需求

以下部分描述WAN适配器的额外安装需求：

·说明端点数目(如信道，电路或bearer Channels)

·为ISDN适配器说明键和值

·安装多协议WAN适配器

不支持

Remove节

通知对象

1.2.11.2网络协议安装要求

本节总结了网络协议的安装需求

一般需求

INF文件节 | 状态 | 注释
Version | 需要 | Class=NetTrans classGuid={4D36E973-E325-11CE-BFC1-08002BE10318}
SourceDiskNames 和SourceDiskFiles | 如果.., 则需要， | 如果INF文件没有随Windows 2000发布则需要。如果INF文件随Windows 2000发布，在Version节中必须说明一个LayoutFile项，不使用SourceDiskNames和SourceDisksFiles节。
DestinationDirs | 需要 | 无特殊网络需求
ControlFlags | 可选 | 无特殊网络需求
Manufacturer | 需要 | 无特殊网络需求
Model | 需要 | hw_id由提供者名字，下划线和制造商名或产品名组成如：MS_DLC。
DDInstall | 需要 | Characteristics 项值： NCF_HIDDEN,NCF_NO_SERVICE,NCF_NOT_USER_REMOVABLE,NCF_HAS_UI
DDInstall.Services | 可选 | 无特殊网络需求
add_reg_sections | 需要 | 需要： 创建Ndi键 说明绑定接口 允许的绑定接口： UpperRange: netbios,ipx,tdi,Winsock,noupper LowerRange: ndis5,ndisatm,nolower 可选： 为组件设置静态参数 需要其他网络组件的安装 说明相关服务值 说明HelpText值 为一个通知对象说明值
Remove | 可选 |
WinsockSections | 可选 | 对于一个提供Winsock接口的协议，Winsock_install是必需的，Winsock-remove节是可选的。
Strings | 需要 | 无网络特殊需求

1.2.11.3中间层网络驱动程序的安装需求

本节总结了中间层网络驱动程序的安装需求。

INF文件节 | 状态 | 注释
Version | 需要 | Class=Net ClassGuid={4D36E972-E325-11CE-BFC10802BE-10318}
SourceDiskNames 和 SourceDisksFiles | 如..， 则需要 | 如果INF文件没有随Windows 2000发布，这个节是必需的。如果INF文件随Windows 2000发布，Version节中的Layoutfile项必须说明，不使用SourceDiskNames和SourceDisksFiles节。
DestinationDirs | 需要 | 无特殊网络需求
ControlFlags | 可选 | 无特殊网络需求
Manafacturer | 需要 | 无特殊网络需求
Models | 需要 | hw_id由提供者名字，下划线和制造商名字或产品名字组成如：MS_DLC
DDInstall | 需要 | Characteristics项： NCF_VIRTUAL是必需的。 NCF_HIDDIN和NCF_NOT_USER_REMOVABLE是可选的。
DDInstall.Services | 需要 | 无网络特殊需求
Add-reg-sections | 需要 | 需要： 创建Ndi键 说明相关服务值 说明绑定接口 可用的绑定接口： UpperRange: ndis5,ndisatm,ndiswan,ndiscowan,noupper,nids5_atalk,ndis5_dlc,ndis5_ip,ndis5_ipx,ndis5_dbf,ndis5_streams LowerRange: ethernet,atm,tokenring,serial,fddi,baseband,broadband,arcnet,isdn,localtalk,wan 可选： 为组件设置静态参数 需要安装其他网络组件
strings | 需要 | 无网络特殊需求

1.2.11.4网络过滤器驱动程序的安装需求

本节总结了网络过滤器驱动程序的INF文件需求。安装网络过滤器驱动程序需要2个INF文件：

·一个是为过滤器服务提供的(Class=NetService)

·一个是为过滤器设备提供的(Class=Net)

网络过滤器驱动程序的ServiceINF文件

INF文件节 | 状态 | 注释
Version | 需要 | Class=NetService Classguid={4D36E975-E325-11CE-BFC1-08002BE10318}
SourceDiskNames 和SourceDisksFiles | 需要，如果.. | 如果INF不随Windows 2000发布，该节是必需的。如果INF文件随Windows 2000发布，Version节中的LayoutFile项必须说明，不使用SourceDisNames和SourceDisksFiles节。
DestinationDirs | 需要 | 无特殊网络需求
ControlFlags | 可选 | 无特殊网络需求
Manufacturer | 需要 | 无特殊网络需求
Models | 需要 | hw_id由提供者名字，下划线和制造商名字或产品名组成。如：MS-DLC
DDInstall | 需要 | Characteristics 项： NCF-FILTER是必需的。NCF_HAS_UI和NCF_NO_SERVICE是可选的。
DDInstall.Services | 可选 | 需要： 生成Ndi键 说明过滤器服务值： FilterClass,FilterDeviceInfFile,FilterDeviceInfId,FilterMediaTypes 说明绑定接口： UpperRange:noupper LowerRange:nolower 可选： 为组件设置静态参数 需要安装其他网络组件 说明一个HelpText值 说明一个通知对象的值
Remove | 可选 |
Strings | 需要 | 无特殊网络需求

网络过滤器驱动程序的DeviceINF文件

INF文件节 | 状态 | 注释
Version | 需求 | Class=Net ClassGuid={4D36E972-E325-11CE-BFC1-08002BE10318}
ControlFlags | 需要 | 这个节必须包含ExcludeFromSelect项
Manufacturer | 需要 | 无特殊网络要求
Models | 需要 | hw_id由提供者名字，下划线和制造商名字或产品名组成。如：MS_DLC。
DDInstall | 需要 | Characteristics 项： NCF_VIRTUAL是必须的。 NCF_HIDDEN和NCF_NOT_USER_REMOVABLE是可选的。
DDInstall.Service | 需要 | AddService 指令的ServiceName值必须和Ndi键下的过滤器组件服务值相匹配。
add_reg_sections | 需要 | 需要： 创建Ndi键 说明服务相关值 可选： 为组件设置静态参数 需要其他网络组件的安装
Strings | 需要 | 无特殊网络需求

1.2.11.5网络客户的安装需求

本节总结了网络客户的安装需求

INF文件节 | 状态 | 注释
Version | 需要 | Class=NetClient ClassGuid={4D36E974-11CE0-BFC1-08002BE10318}
SourceDisksNames 和 SourceDisksFiles | 需要， 如果.. | 如果INF文件没有随Windows 2000发布，该节是必需 的。如果INF文件随Windows 2000发布，Version节中 的LayoutFiles项必须说明，不使用SourceDiskNames和SourceDisksFiles节。
DestinationDirs | 需要 | 无特殊网络需求
ControlFlags | 可选 | 无特殊网络需求
Manufacturer | 需要 | 无特殊网络需求
Models | 需要 | hw_id由提供者名字，下划线和制造商名字或产品名组 成。如：MS_DLC。
DDInstall | 需要 | Characteristics 项可用值： NCF_HIDDEN，NCF_NO_SERVICE，NCF_NOT_USER_REMOVABLE，NCF_HAS_UI
DDInstall.Services | 可选 | 无特殊网络需求
Add_reg_section | 需要 | 需要： 创建Ndi键 说明绑定接口 可用的绑定接口： UpperRange:noUpper LowerRange:ipx,tdi,Winsock,netbios,nolower 可选： 为组件设置静态参数 需要安装其他组件 说明服务相关值 说明HelpText值 说明通知对象的值
Remove | 可选 |
NetworkProvider | 如..， 则需要。 | 如果说明了网络客户的可替代设备名字，或者为使用net view命令而为组件说明了一个短名时，该节是必需的。
PrintProvider | 如..， 则需要。 | 如网络客户是一个打印提供者，该节名是必须的。
Strings | 需要 | 无特殊网络需求。

1.2.11.6网络服务的安装请求

本节总结了网络服务的安装请求。

INF文件节 | 状态 | 注释
SourceDisksNames 和 SourceDisksFiles | 如果.., 则需要. | 如果INF文件没有随Windows 2000发布，该节是必需的。 如果INF文件随Windows 2000发布，在Version节中的 LayoutFile项必须说明，不使用SourceDiskNames和 SourceDisksFiles节。
DestionationDirs | 需要 | 无特殊网络需求
ControlFlags | 可选 | 无特殊网络需求
Manufacturer | 需要 | 无特殊网络需求
Models | 需要 | hw_id由提供者名字，下划线和制造商名字或产品名组 成。如：MS_DLC。
DDInstall | 需要 | Characteristics 项可用值： NCF_HIDDEN,NCF_NO_SERVICE,NCF_NOT_USER_REMOVABLE,NCF_HAS_UI
DDInstall.Service | 可选 | 无网络特殊需求
add-reg-sections | 需要 | 需要： 创建Ndi键 说明绑定接口 可用的绑定接口： UpperRane:noupper LawerRange:ipx,tdi,Winsock,netbios,nolower Optional: 为组件设置静态参数 需要安装其他网络组件 说明服务相关值 说明HelpText值 说明通知对象的值
Remove | 可选 |
Strings | 需要 | 无特殊网络需求


网络组件的通知对象

通知对象处理通知信息，这些通知是由网络配置子系统发送给代表相关网络组件的对象的。通知对象由一个DLL实现。通知对象被用来显示网络组件的属性页并给出组件网络配置的编程控制。

注意：如果下述两个条件同时成立，网络组件可能不需要通知对象。

·如果网络组件通过它的信息(INF)文件安装和删除。

·如果对网络配置信息的反应不是必需的。

下面章节描述了通知对象并解释怎样开放。

下面章节也提供了支持这种对象的接口方法的参考信息。

·2.1关于通知对象

·2.2创建通知对象

·2.3通知对象参考

2.1关于通知对象

通知对象处理通知，这些通知是由网络配置子系统，发送给代表某个网络组件（它拥有通知该对象）的对象。可以拥有通知对象的网络组件有：

·传输，如协议驱动程序

·服务，如中间层驱动程序

·客户，如拨号网络

注意网卡不支持也不能拥有通知对象。如果物理和虚拟网卡需要涉及安装/删除过程或配置网络，可以使用INF文件或设备协作安装程序机制。(协作安装程序的更多信息，参见《Plug and Play,Power Management,and Setup Design Guide》和《Windows 2000 Driver Development Reference 》卷1的INF文档)。

通知对象是一个组件对象模型(com)类对象，由作为com组件服务器的DLL实现。每种类型的网络组件和一个类安装器相连系。那个类安装器用来安装这种类型的网络组件，并注册网络组件拥有的com类对象。注册在网络组件的主要安装阶段结束之后进行。类安装器调用对象的入口函数，注册一个COM类对象。

当操作系统安装、升级或删除网络功能模块时，或当应用程序配置网络时，操作系统或该应用程序必须启动网络配置子系统。一旦网络配置子系统启动，创建一个通知对象实例，这个通知对象执行某些特定操作。

以下部分描述了通知对象接收的通知类型和通知对象执行的操作：

·2.1.1通知对象图

·2.1.2通知类型

·2.1.3网络组件的安装

·2.1.4网络组件的删除

·2.1.5升级网络组件

·2.1.6显示并改变属性

·2.1.7网络配置

2.1.1 通知对象图

图2.1显示了安装或控制网络的客户应用程序是如何调用网络配置子系统的。子系统调用网络类安装器安装网络组件并为这些组件注册通知对象。通知对象代表这些组件，回调子系统来配置网络。

图2.1 网络配置子系统

2.1.2通知类型

网络配置子系统向通知对象发送通知：

·在网络安装期间。包括安装操作系统，在先前没有网络功能的操作系统上安装网络，升级操作系统和删除网络。

·在网络配置期间。包括增加、删除、激活、去活网络组件，改变网络组件，改变网络配置子系统和网络组件绑定的方式。

·在应用程序指导子系统显示网络组件（它拥有通知对象）的属性之后。

2.1.3网络组件的安装

网络配置子系统安装网络组件如下：

1.网络配置子系统为某个组件类型调用类安装器。

2.类安装器调用Setup　API，从组件INF文件中获取信息，并安装组件。

3.如果组件拥有通知对象，那么类安装器取回实现通知对象的DLL名。这个DLL在组件INF文件中出现，如下所示：

HKR,Ndi,ComponentDll,0,"notifyobject.dll"

4.类安装器调用DLL的入口函数以注册通知对象。

5.网络配置子系统创建一个通知对象的实例，并调用该对象的INetCfgComponentControl::Initialize方法。

6.网络配置子系统调用通知对象的INetCfgComponentSetup::Install方法，执行安装组件所需的操作。

7.如果组件的安装是自动的，网络配置子系统调用通知对象的InetCfgComponentSefup::ReadAnswerFile方法,从一个自动安装文件中取出组件参数，该文件称为answer文件。

8.一旦网络配置子系统创建一个实例，并初始化这个通知对象，那么子系统将调用通知对象的INetCfgComponentNotifyGlobal::GetSupportedNotifications方法,来取回对象所需的通知类型。用这个信息，子系统将通知发送给对象。对象可以用这些通知控制网络安装和配置，这些安装和配置可能会影响拥有这个对象的组件。例如，如果子系统调用INetCfgComponentNotifyGlobal::SysNotifyComponent方法来通知对象:子系统要安装或删除其他网络组件，对象就有机会执行相关的操作。

9.一旦网络配置子系统创建通知对象的一个实例并将其初始化，子系统也将调用通知对象INetCfgComponentNofifyBinding接口中的任一方法,把有关变化（子系统将其他组件绑定到拥有通知对象的组件的绑定方式的变化）通知给对象。

10.当网络配置子系统准备将组件属性应用到操作系统中时，它调用对象的INetCfgComponentControl::ApplyRegisryChanges方法给组件注册键下的组件参数赋值。通知对象调用它组件的INetCfgComponent::OpenParamKey方法来打开和取回组件注册键。

11.为配置组件的驱动程序，网络配置子系统调用通知对象的INetCfgComponentControl::ApplyPnpChanges方法,并传递INetCfgPnpReconfigCallback接口。通知对象调用INetCfgPnpReconfigCallback：：SendPnpRecconfig方法,将配置信息发送到其组件驱动程序。

2.1.4删除网络组件

网络配置子系统以如下描述的方式删除一个网络组件：

子系统创建通知对象的一个实例并调用对象的INetCfgComponentControl::Initialize方法。该方法初始化对象并提供对组件和网络配置的访问功能。

为执行删除组件所需的操作，网络配置子系统调用通知对象的INetCfgComponentControl::Removing方法。Removing方法执行清除操作以准备删除组件。

为删除组件，网络配置子系统调用通知对象的INetCfgComponentControl::ApplyRegistryChange方法从注册表中删除网络组件信息。

2.1.5升级网络组件

网络配置子系统升级网络组件，描述如下。

子系统创建一个通知对象的实例，并调用通知对象的INetCfgComponentControl::Initialize方法。这个方法初始化对象并提供对组件和网络配置访问功能。

当操作系统改变为新的、改进的或其他版本时，网络配置子系统调用通知对象的INetCfgComponentSefup::Upgrade方法，执行所需的操作。

为了升级组件，网络配置子系统调用通知对象的INetCfgComponentSefup：：ApplyRetistryChanges方法,来修改注册表中网络组件的信息。为了用升级后的信息配置组件驱动程序，网络配置子系统调用通知对象的InetCfgComponentSefup::ApplyPnpReconfigCallback方法,并传递INetCfgPnpReconfigCallback接口。

2.1.6显示并改变属性

网络配置子系统显示网络组件的属性页，并改变组件的参数，如下所述。

一个用户打开网络控制面板应用程序，来显示和修改组件的属性。当用户打开该应用程序时，应用程序启动网络配置子系统。然后，网络配置子系统创建通知对象的一个实例，并调用对象的INetCfgComponentControl::Initialize方法。这个方法初始化对象并提供对组件和网络配置的访问功能。

为显示组件属性，网络控制面板应用程序调用组件的INetCfgComponent::RaisePropertyUi方法。RaisePropertyUi方法调用通知对象的如下方法：

·INetCfgComponentPropertyUi::QueryPropertyUi。确定某一个环境对显示组件属性是否合适。

·INetCfgComponentPrpertyUi::SetContext方法。指导组件的通知对象在某环境中显示组件属性。

·INetCfgComponentPrpertyUi::MergePropPages方法。创建和归并定制属性页。

如果用户在一个定制属性页中改变了组件的某个属性，RaisePropertyUi调用通知对象的INetCfgComponentPropertyUi::ApplyProperties方法,在内存中保存此变化。

为使改变生效，网络配置子系统调用通知对象的INetCfgComponentControl::ApplyRetistryChanges方法,来修改注册表中网络组件的信息。为了用修改后的信息配置驱动程序，网络配置子系统调用通知对象的INetCfgComponentControl::ApplyPnpChanges方法,同时传递INetCfgPnpReconfigCallback接口。

2.1.7网络配置

通知对象向拥有它的网络组件，提供网络配置的编程控制，描述如下。

用户可以打开网络控制面板应用程序，配置网络的某些特性。打开后，应用程序启动网络配置子系统。然后该子系统创建一个通知对象的实例，并调用对象的INetCfgComponentControl::Initialize方法。这个方法初始化对象并提供对组件和网络配置的存取。一旦网络配置子系统创建并初始化这个通知对象，子系统调用通知对象的INetCfgComponentNotifyGlobal::GetSupportedNotifications方法,来取回对象所需的通知类型。用这个信息，子系统向对象发送所需的通知。对象用这些通知控制网络安装和配置，这些安装或配置能够影响拥有对象的组件。例如，如果子系统调用通知对象的INetCfgComponentNotifyGlobal::SysQueryBindingPath方法通知对象:子系统增加其他网络组件的绑定路径。从而使这个对象有机会请求子系统：去活这个绑定路径。另外，子系统可以调用通知对象INetCfgComponentNotifyBinding接口的所有方法。这些方法将绑定方式变化（子系统将其他网络组件绑定到拥有通知对象的组件上）通知对象。

2.2创建通知对象

如果网络组件要执行如下操作，则应为网络组件创建通知对象：

·需要在网络安装和配置上进行一些控制。

·需要显示定制属性页，用户能用定制属性页修改组件属性。以下部分描述了创建通知对象的过程：

.2.2.1装载通知对象DLL和类对象

.2.2.2定义通知对象

.2.2.3创建并初始化通知对象的一个实例

.2.2.4安装、升级、删除组件

.2.2.5为组件创建属性页

.2.2.6为显示属性设置环境

.2.2.7评价网络配置的变化

.2.2.8将组件变化加到注册表中

.2.2.9配置组件驱动程序

2.2.1装载通知对象DLL和类对象

网络组件的通知对象应该用Com类对象实现，它位于作为Com服务器的DLL中。

特定通知对象的DLL应该提供一些入口函数

DllMian 函数允许网络配置子系统将DLL装载进子系统的虚地址空间。

DllRegisrterServer 和DllUnregisterServer函数将DLL类对象的信息放入注册表中。网络配置子系统用注册表信息定位并装载网络组件的通知对象。

DLLCanUnloadNow 函数允许网络配置子系统确定DLL是否正在使用。如果当前DLL不在使用，子系统可以安全地将其从内存中卸载。

为使通知对象的DLL成为一个Com服务器，通知对象类必须暴露一个类工厂。这个类工厂允许网格配置子系统创建一个通知对象的实例。类工厂应该从IClassFactory接口中继承。

2.2.2定义通知对象

通知对象类从INetCfgComponentControl接口继承。然而，如果通知对象执行某些特殊操作，它们的通知类也必须从如下接口中继承实现：

.如果通知对象执行一些操作，这些操作与拥有该对象的组件的安装、升级和删除相关，则通知类必须从INetCfgComponentSetup接口中继承。

.如果通知对象为拥有对象的组件显示定制属性页，则通知类必须从INetCfgComponentPropertyUi接口中继承。

.如果一个通知对象评价一些变化（网络配置子系统把组件（拥有对象的）绑定到其他组件的方式的变化），则相应通知类必须从INetCfgComponentNotifyBinding接口中继承。

·如果通知对象评价影响拥有对象的组件的网络配置的变化，则相应的通知对象类必须从INetCfgComponentNotifyGlobal接口中继承。

在通知类中的某些数据成员对所有通知对象是一样的。而另一些数据成员随组件不同而变化的。

所有通知对象都应定义的数据成员包括：

·指向网络组件实例的INetCfgComponent接口指针，该组件拥有通知对象。通知对象实例用此指针存取和控制组件

·指向网络配置对象实例的INetCfg接口指针。通知对象实例用这个指针存取网络配置各个特性。

·为拥有通知对象的组件贮存参数信息的变量。

·说明通知对象先前执行的操作的变量。定义常量来指示通知对象可能执行的不同操作。当网络配置子系统调用通知对象的INetCfgComponentControl::ApplyRegistryChanges方法，将配置变化加入注册表。ApplyRegistryChanges用这个变量确定注册表的变化。例如，如果通知对象在它的INetCfgComponentSetup::Install方法中，执行与安装组件相关的操作(该组件拥此通知对象)，Install将这个变量设置为install.

·HKEY类型的注册键。通知对象调用组件的INetCfgComponent：：OpenParamKey方法打开和获取注册键，它包含组件参数。通知对象然后将该键设置为HKEY成员。

为通知类定义构造函数和析构函数。也要考虑定义只有通知类能用的私有方法。

通知类应该实现IUnknown接口的所有方法。如果通知类从前面列表中提到的任一所选接口继承而来，那么该接口的所有方法都应实现。对通知对象接口的任一方法，E_NOTIMPL都不是一个有效返回值。如果通知对象不需要某个方法的实现，则该方法只需返回S_OK即可。

2.2.3创建并初始化通知对象实例

在子系统能够做如下操作之前,网络配置子系统必须创建一个通知对象实例并初始化对象：

·提醒通知对象网络配置发生变化

·为拥有对象的组件显示定制属性页

子系统通过DLL的类工厂中创建通知对象实例。类工厂调用通知类的构造函数。

类构造函数应该将初始值分配给类数据成员。构造函数应该分配的初始值包括：

·构造函数应该将指向网络组件实例INetCfgComponent的接口指针赋为NULL值。

·构造函数应该将指向网络配置对象实例的接口（INetCfg）指针赋为NULL值，

·构造函数应该将一个变量设置成表示未知操作的常量，该变量用来说明通知对象先前所执行的操作。此变量的详情，参见2.2.2节的“定义通知对象”。

一旦网络配置子系统创建一个通知对象实例，这个子系统将调用INetCfgComponentControl::Initialize方法来初始化对象实例。在这个调用中，子系统传递了INetCfgComponent接口指针。INetCfgComponent向通知对象提供对象组件的实例，对象能用这个实例访问和控制组件。在这个调用中，子系统也传递INetCfg接口指针,向通知对象提供网络配置对象实例，通知对象可用来访问网络配置的所有特性。

Initialize 方法应该将网络配置子系统提供的INetCfgComponent和INetCfg接口指针赋给通知类的数据成员。Initialize然后调用：

·INetCfg::AddRef方法，用来增加网络配置对象的引用数。

·INetCfgComponent::AddRef方法，用来增加拥有通知对象的网络组件的引用数。

在Initialize返回前，不可调用其他的通知对象的其他接口方法。

2.2.4安装，升级和删除组件

当网络配置子系统安装、升级或删除网络组件时，子系统也调用组件的通知对象来完成安装、升级和删除操作。实现组件的通知对象，来执行组件所需的操作。例如：

·当子系统安装组件时，组件通知对象安装组件需要的其他网络组件。通知对象的INetCfgComponentSetup::Install方法调用网络配置的INetCfgClassSetup::Install方法, 安装一个网络组件。在这个调用中，组件通知对象的Install方法传递所要安装的组件的标识符。

·通知对象将删除其他网络组件。为删除一个网络组件，组件通知对象的INetCfgComponentSetup:: Removing方法调用网络配置的INetCfgClassSetup:: DeInstall。在这个调用中Removing传递要删除组件的INetCfgComponent接口指针。

·实现组件通知对象，当子系统升级一个组件时，组件通知对象可以改变组件绑定路径的顺序。为改变这个顺序，通知对象的INetCfgComponentSetup::Upgrade方法将调用INetCfgComponentBinding::MoreBefore或INetCfgComponentBinding::MoreAfter方法。

2.2.5为组件生成属性页

在网络配置子系统调用通知对象的INetCfgComponentPropertyUi::MergePropPages方法之后，通知对象生成定制属性页。可以用MergePropPages方法将定制属性页并到组件属性单的页缺省集中。MergePropPages返回定制属性页合并后的缺省页数目。

为了生成定制属性页，MergePropPages调用COM的CoTaskMemAlloc函数为PRORSHEETPAGE结构的句柄分配空间。每个句柄代表了要创建的属性页。如果CoTaskMemAlloc为句柄成功分配了空间，MergePropPages将为每个属性页声明和填写PRORSHEETPAGE结构。一旦MergePropPages填写了这些结构，它将为每个属性页调用Win 32的CreatePropertySheetPage函数。在这个调用中，MergePropPages传递要生成的PROPSHEETPAGE结构地址。

MergePropPages 生成的每个属性页都实现一个对话框回调函数。对话框回调函数处理操作系统发送给与其相联系的属性页的消息。为将属性页和对话框函数连系起来，MergePropPages应该把每个属性页的PROPSHEETPAGE结构中的pfnDlgProc成员指向该页对话框函数。

对话框函数处理如下信息：

˙WM_INITDIALOG消息，在操作系统显示属性页之前，把这个消息发送给对话框函数。对话框函数通常用这个消息初始化属性页，并执行影响属性页外观的任务。

˙WM_NOTIFY消息，把属性页中发生的一个事件发送到对话框函数。随该消息发送的其他信息用来鉴别发生了什么事件。事件信息包含在指向NMHDR结构的指针中。NMHDR中包含的信息如下：

˙PSN_APPLY事件，指示用户点击了属性页上的OK、Apply或Close按钮。如果用户点击OK、Close或Apply,那么对话框函数将调用PropSheet_Changed宏通知属性单，页中信息已经改变。在这个调用中，对话框函数传递属性单和属性页的句柄。对话框函数可以把页句柄作为参数，调用Win 32的GetParent函数，来获取属性单句柄。

在对话框函数将变化通知属性单后，网络配置子系统调用INetCfgComponentPropertyUi::ValidateProperties方法检查变化的合法性。如果变化是有效的，子系统调用通知对象的INetCfgComponentPropertyUi::ApplyProperties方法，使改变生效。网络配置子系统在操作系统关闭对话框之前,调用ApplyProperties。ApplyProperties获取用户输入的信息，并给通知对象的数据成员设置信息。

˙PSN_RESET事件，指示操作系统将销毁(destroy)属性页。用户可以点击属性页的Cancel，发起这个操作。如果用户点击了Cancel,网络配置子系统调用INetCfgComponentPropoertyUi::CancelProperties方法，丢弃所有修改。在对话框关闭以前，网络配置子系统调用CancelProperties。

˙PSN_KILLACTIVE事件，指示属性页将变成非活动的。这个事件在用户激活其他属性页或点击OK时发生。

属性页回调函数也可以由MergePropPages创建的不同属性页分别实现。该回调函数执行该页的初始化和消除操作。为了将属性页和属性页回调函数连系起来，MergePropPages应该在PRORSHEETPAGE结构中，把pfnCallback成员指向该页的回调函数。

2.2.6设置环境来显示属性

通知对象可以设置环境，在这个环境中为拥有该对象的网络组件显示属性。在网络配置子系统调用对象的INetCfgComponentPropertyUi::SetContext方法之后，且在子系统调用对象的INetCfgComponentPropertyUi::MergePropPages方法之前，通知对象设置显示环境。当网络配置子系统调用SetContext时，它传递一个IUnknown接口。SetContext调用该接口的QueryInterface方法来确定子系统提供的特定对象的接口。

例如，网络配置子系统在调用SetContext时，能够提供INetLanConnectionUiInfo接口。SetContext可以用INetLanConnectionUIInfo的GetDeviceGuid方法,取回LAN设备的GUID。通知对象随后在LAN设备的环境中,显示网络组件的属性。例如，TCP/IP协议的通知对象在适配器的环境中，显示一个和LAN适配器连系在一起的IP地址。这样用户可以为这个适配器配置IP地址。

2.2.7评价网络配置的变化

在网络配置子系统调用通知对象的INetCfgComponentNotifyGlobal和INetCfgComponentNotifyBinding接口后，通知对象应该评价子系统发送的网络配置的变化，并执行与这些变化相关的操作。通知对象的INetCfgComponentNotifyGlobal和INetCfgComponentNotifyBinding接口的方法，处理对拥有对象的组件有影响的变化。

例如，网络配置子系统调用通知对象的INetCfgComponentNotifyGlobal::SysNotifyComponent方法并传递NCN_ADD,提醒通知对象：子系统安装了其他网络组件。如果拥有对象的组件要绑定指定的安装组件，则通知对象将执行有助于绑定的操作。

2.2.8将组件变化加入注册表

在网络配置子系统调用通知对象的InetCfgComponentControl::ApplyRegistryChanges方法后，通知对象根据先前做的操作，从注册表中设置、修改或删除信息。在通知对象执行特定操作后(这个操作与拥有对象的组件的安装，删除或参数修改有关)，通知对象设置某个数据成员来指示已执行的操作。在子系统调用ApplyRegistryChanges将配置变化加入注册表后，ApplyRegistryChanges用这个数据成员确定如何修改注册值。

例如：如果通知对象先前执行了与安装组件（拥有该对象的）有关的操作，通知对象将数据成员设置为”install”。在子系统调用ApplyRegistryChanges将配置变化加入到注册表时，ApplyRegistryChanges在注册表中设置组件信息。

如果通知对象先前执行有关删除组件（拥有对象的）的操作，通知对象应该将数据成员设置为”remove”。在子系统调用ApplyReyistryChanges时，ApplyRegistryChanges从注册表中删除该组件的信息。

如果一个用户在组件的定制属性页中修改了一个属性，组件通知对象应该将数据成员设置为”modifyparameter"。在子系统调用ApplyRegistryChanges时，ApplyRegistryChanges在注册表中修改组件参数信息。

为了打开并取回组件的注册表键，修改组件信息，ApplyRegistryChanges方法应该调用组件的INetCfgComponent::OpenParamKey方法。为了在组件的注册键下设置值，ApplyRegistryChanges用Win 32函数向注册表写数据。例如，ApplyRegistryChanges可以调用RegCreateKeyEX函数来生成子键，用RegSetValueEx函数创建并设置值。

2.2.9配置组件驱动程序

在网络配置子系统调用通知对象的INetCfgComponentControl::ApplyPnpChanges方法后，通知对象应该将配置信息发送给组件（拥有该对象的）的驱动程序。在网络配置子系统调用INetCfgComporentControl::ApplyRegistryChanges方法后，并且网络组件的服务和驱动程序已经启动后，网络配置子系统调用ApplyPnpChanges。在ApplyPnpChanges调用中，网络配置子系统传递了INetCfgPnpReconfigCallback接口。组件的通知对象可以用INetCfgPnpReconfigCallback接口,把配置信息发送给组件驱动程序。这个驱动程序必须是TDI提供者或NDIS微端口。通知对象在它的ApplyPnpChanges中调用INetCfgPnpReconfigCallback::SendPnpReconfig,将配置信息发送给组件驱动程序。SendPnpReconfig向驱动程序传送配置信息。

通知对象也能调用Win 32的CreateFile函数打开与组件驱动程序的连接。通知对象可以调用Win 32 DeviceIoControl函数,向其组件驱动程序直接发送控制码和输入数据。

并不要求通知对象一定要使用INetCfgPnpReconfigCallback。但是，如果通知对象使用INetCfgPnpReconfigCallback,则用户无需重新启动操作系统，就可以使配置变化在驱动程序中生效。