驱动程序基本结构
本章概述了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”。
http://www.blog.edu.cn/user2/60402/cmd.shtml?do=blogs&id=76423&uid=60402