通过安装DDK和相应的开发软件,我们构造好了WDM驱动程序的开发环境。接着,我们就要深入进行设计与开发工作了。
■WDM 驱动程序的运作流程
WDM本身的PNP管理器被抽象地提升到了ROOT的地位。PNP管理器负责所有的总线驱动程序的加载。总线驱动程序则负责遍历所有位于总线上的设备,并且为每个设备创建相应的设备对象。当PNP管理器发现一个设备对象,就查找该对象对应的Driver。并调用该Driver的ADD DEVICE例程。如果Driver不在内存中,就先加载,然后调用ADD DEVICE例程。
当然,总线本身并没有发出任何信号告诉PNP管理器自己的存在,所以,总线Driver是在NT的安装时设定的。而ISA设备并没有规范,因为需要KMD自己检查硬件存在及状态,所以它是老式KMD存在的惟一理由。这也是微软极力在新规范里取消ISA总线的理由之一。WDM支持PNP协议和PM协议,而且实现时仅仅需要在MAJOR FUNCTION里加入一些对PNP和PM事件响应的例程即可。
■驱动程序设计
设计一个设备驱动程序,应该支持和其他相同类型设备的NT驱动程序相同的IRP_MJ_XXX和IOCTL请求代码。如果设计一个中间层NT驱动程序,应该首先确认下层驱动程序所管理的设备,因为一个高层的驱动程序必须具有低层驱动程序绝大多数IRP_MJ_XXX例程入口。高层驱动程序在接到I/O请求时,在确定自身IRP当前堆栈单元参数有效的前提下 ,设置好IRP中下一个低层驱动程序的堆栈单元,然后再调用IoCallDriver将请求传递给下层驱动程序处理。一旦决定好了驱动程序应该处理哪些IRP_MJ_XXX,就可以开始确定驱动程序应该有多少个Dispatch例程。当然也可以考虑把某些RP_MJ_XXX处理的例程合并为同一例程处理。例如在ChangerDisk和VDisk里,对IRP_MJ_CREATE和IRP_MJ_CLOSE处理的例程就是同一函数。
一个驱动程序必须为它所管理的每个可能成为I/O请求的目标的物理和逻辑设备创建一个Device对象。一些低层的驱动程序还可能要创建一些不确定数目的Device对象。例如一个硬盘驱动程序必须为每一个物理硬盘创建一个Device对象,同时还必须为每个物理磁盘上的每个逻辑分区创建一个Device对象。
一个高层驱动程序必须为它所代表的虚拟设备创建一个Device对象,这样更高层的驱动程序才能连接它们的Device对象到这个驱动程序的Device对象。另外,一个高层驱动程序通常为它低层驱动程序所创建的Device对象创建一系列的虚拟或逻辑Device对象。
尽管可以分阶段来设计驱动程序,从而使一个处在开发阶段的驱动程序不必一开始就创建出所有它将要处理的所有Device对象,但从一开始就确定好最终要创建的所有Device对象将有助于设计者所要解决的任何同步问题。另外,确定所要创建的Device对象还有助于定义Device对象的Device Extension的内容和数据结构。
■驱动程序开发
驱动程序的开发是一个从粗到细逐步求精的过程。NT DDK的src/目录下有一个庞大的模板代码,几乎覆盖了所有类型的设备驱动程序、高层驱动程序和过滤器驱动程序。在开始开发驱动程序之前,应该先在这个样板库下面寻找是否有和所要开发的类似类型的例程。
例如若要开发光盘塔驱动程序,虽然DDK对光盘塔没有任何描述,但光盘塔是符合SCSI-Ⅱ规范的SCSI设备,可以在src/storage/class目录中发现很多和SCSI设备有关的驱动程序,例如SCSI Tape、SCSI Disk、SCSI CDROM等驱动程序开发时,可以参考类似驱动程序,从而减化开发难度。
下面笔者将进一步介绍开发驱动程序的基本步骤:
l.编写驱动程序框架
(1)首先编写一个DriverEntry例程,并在该例程里调用IoCreateDevice来创建一个Device对象。
(2)写一个处理IRP_MJ_CREATE请求的Dispatch例程的基本框架。如果驱动程序创建了多于一个的Device对象,则必须为IRP_MJ_CLOSE请求写一个例程,该例程通常情况下可以和DispatchCreate共用一个例程。
(3) 编译连接驱动程序。
2.测试驱动程序
(1)首先在系统中安装好驱动程序,具体编译安装驱动程序请见下版的《编译安装篇》。
(2)为NT逻辑设备名称和目标Device对象名称之间建立起符号连接,在前面已经知道Device对象名称对Win32用户模式是不可见的,是不能直接通过API来访问的,Win32 API只能访问NT逻辑设备名称。可以通过修改注册表来建立这两种名称之间的符号连接。运行Regedt32.exe在/HKEY_LOCAL_MACHINE/ System/ CurrentControlSet/ Control/ Session Manager/ DOS Devices下建立起符号连接,这种符号连接也可以在驱动程序里调用函数IoCreateSymbolicLink来创建。
(3)完成以上所有的设置并检查无误后,我们必须重新启动Windows系统。
(4)编写一个简单的测试程序调用Win32 API中的CreateFile函数,并以刚才命名的NT逻辑设备名打开这个设备。如果打开成功,则成功地写出了一个最简单的驱动程序了。支持更多的设备I/O请求,例如驱动程序可能需要对IRP_MJ_READ请求做出响应(完成后可用ReadFile 函数进行测试)。如果驱动程序需要能够手工卸载,那么还必须对IRP_MJ_CLOSE做出响应。为所需要处理的IRP_MJ_XXX写好处理例程,并在DriverEntry里面初始化好这些例程入口。一个低层的驱动程序需要一个StartIo、ISR和DpcForIsr例程,可能还需要一个SynchCritSection例程,如果设备使用了DMA,那么可能还需要一个AdapterControl例程。
对于高层驱动程序可能需要一个或多个IoCompletion例程,最起码完成检查I/O状态块然后调用IoCompleteRequest的工作。如果需要,还要对Device Extension数据结构和内容做些修改。有一点必须很清楚的,就是代码运行级别的问题,即IRQL,最常见的级别是PASSIVE_LEVEL、APC_LEVEL、DISPATCH_LEVEL和DIRQL。
在看NT DDK HELP中的函数说明的时候,要注意函数的可运行级别,比如有的函数只能在PASSIVE_LEVEL下运行,有的函数则可以在DISPATCH_LEVEL以下级别运行,级别越高的时候,对代码的要求就越严格,比如在DISPATCH_LEVEL的时候,就不能使用分页内存。通常情况下应该尽可能让代码在低运行级别如PASSIVE_LEVEL下运行,在高级别下运行过长时间将导致系统效率降低、影响系统响应的实时性。但有时候自己无法控制运行的级别,例如在调用低层Driver时使用IoCallDriver,低层Driver响应完毕后会执行completion例程,该例程运行的级别就是由低层Driver来决定。因此在编写completion例程时,应尽量将这个函数设计成能在DISPATCH_LEVEL级别运行。
依照以上开发步骤,我们可以设计出全新的WDM设备驱动程序。
■WDM 驱动程序的运作流程
WDM本身的PNP管理器被抽象地提升到了ROOT的地位。PNP管理器负责所有的总线驱动程序的加载。总线驱动程序则负责遍历所有位于总线上的设备,并且为每个设备创建相应的设备对象。当PNP管理器发现一个设备对象,就查找该对象对应的Driver。并调用该Driver的ADD DEVICE例程。如果Driver不在内存中,就先加载,然后调用ADD DEVICE例程。
当然,总线本身并没有发出任何信号告诉PNP管理器自己的存在,所以,总线Driver是在NT的安装时设定的。而ISA设备并没有规范,因为需要KMD自己检查硬件存在及状态,所以它是老式KMD存在的惟一理由。这也是微软极力在新规范里取消ISA总线的理由之一。WDM支持PNP协议和PM协议,而且实现时仅仅需要在MAJOR FUNCTION里加入一些对PNP和PM事件响应的例程即可。
■驱动程序设计
设计一个设备驱动程序,应该支持和其他相同类型设备的NT驱动程序相同的IRP_MJ_XXX和IOCTL请求代码。如果设计一个中间层NT驱动程序,应该首先确认下层驱动程序所管理的设备,因为一个高层的驱动程序必须具有低层驱动程序绝大多数IRP_MJ_XXX例程入口。高层驱动程序在接到I/O请求时,在确定自身IRP当前堆栈单元参数有效的前提下 ,设置好IRP中下一个低层驱动程序的堆栈单元,然后再调用IoCallDriver将请求传递给下层驱动程序处理。一旦决定好了驱动程序应该处理哪些IRP_MJ_XXX,就可以开始确定驱动程序应该有多少个Dispatch例程。当然也可以考虑把某些RP_MJ_XXX处理的例程合并为同一例程处理。例如在ChangerDisk和VDisk里,对IRP_MJ_CREATE和IRP_MJ_CLOSE处理的例程就是同一函数。
一个驱动程序必须为它所管理的每个可能成为I/O请求的目标的物理和逻辑设备创建一个Device对象。一些低层的驱动程序还可能要创建一些不确定数目的Device对象。例如一个硬盘驱动程序必须为每一个物理硬盘创建一个Device对象,同时还必须为每个物理磁盘上的每个逻辑分区创建一个Device对象。
一个高层驱动程序必须为它所代表的虚拟设备创建一个Device对象,这样更高层的驱动程序才能连接它们的Device对象到这个驱动程序的Device对象。另外,一个高层驱动程序通常为它低层驱动程序所创建的Device对象创建一系列的虚拟或逻辑Device对象。
尽管可以分阶段来设计驱动程序,从而使一个处在开发阶段的驱动程序不必一开始就创建出所有它将要处理的所有Device对象,但从一开始就确定好最终要创建的所有Device对象将有助于设计者所要解决的任何同步问题。另外,确定所要创建的Device对象还有助于定义Device对象的Device Extension的内容和数据结构。
■驱动程序开发
驱动程序的开发是一个从粗到细逐步求精的过程。NT DDK的src/目录下有一个庞大的模板代码,几乎覆盖了所有类型的设备驱动程序、高层驱动程序和过滤器驱动程序。在开始开发驱动程序之前,应该先在这个样板库下面寻找是否有和所要开发的类似类型的例程。
例如若要开发光盘塔驱动程序,虽然DDK对光盘塔没有任何描述,但光盘塔是符合SCSI-Ⅱ规范的SCSI设备,可以在src/storage/class目录中发现很多和SCSI设备有关的驱动程序,例如SCSI Tape、SCSI Disk、SCSI CDROM等驱动程序开发时,可以参考类似驱动程序,从而减化开发难度。
下面笔者将进一步介绍开发驱动程序的基本步骤:
l.编写驱动程序框架
(1)首先编写一个DriverEntry例程,并在该例程里调用IoCreateDevice来创建一个Device对象。
(2)写一个处理IRP_MJ_CREATE请求的Dispatch例程的基本框架。如果驱动程序创建了多于一个的Device对象,则必须为IRP_MJ_CLOSE请求写一个例程,该例程通常情况下可以和DispatchCreate共用一个例程。
(3) 编译连接驱动程序。
2.测试驱动程序
(1)首先在系统中安装好驱动程序,具体编译安装驱动程序请见下版的《编译安装篇》。
(2)为NT逻辑设备名称和目标Device对象名称之间建立起符号连接,在前面已经知道Device对象名称对Win32用户模式是不可见的,是不能直接通过API来访问的,Win32 API只能访问NT逻辑设备名称。可以通过修改注册表来建立这两种名称之间的符号连接。运行Regedt32.exe在/HKEY_LOCAL_MACHINE/ System/ CurrentControlSet/ Control/ Session Manager/ DOS Devices下建立起符号连接,这种符号连接也可以在驱动程序里调用函数IoCreateSymbolicLink来创建。
(3)完成以上所有的设置并检查无误后,我们必须重新启动Windows系统。
(4)编写一个简单的测试程序调用Win32 API中的CreateFile函数,并以刚才命名的NT逻辑设备名打开这个设备。如果打开成功,则成功地写出了一个最简单的驱动程序了。支持更多的设备I/O请求,例如驱动程序可能需要对IRP_MJ_READ请求做出响应(完成后可用ReadFile 函数进行测试)。如果驱动程序需要能够手工卸载,那么还必须对IRP_MJ_CLOSE做出响应。为所需要处理的IRP_MJ_XXX写好处理例程,并在DriverEntry里面初始化好这些例程入口。一个低层的驱动程序需要一个StartIo、ISR和DpcForIsr例程,可能还需要一个SynchCritSection例程,如果设备使用了DMA,那么可能还需要一个AdapterControl例程。
对于高层驱动程序可能需要一个或多个IoCompletion例程,最起码完成检查I/O状态块然后调用IoCompleteRequest的工作。如果需要,还要对Device Extension数据结构和内容做些修改。有一点必须很清楚的,就是代码运行级别的问题,即IRQL,最常见的级别是PASSIVE_LEVEL、APC_LEVEL、DISPATCH_LEVEL和DIRQL。
在看NT DDK HELP中的函数说明的时候,要注意函数的可运行级别,比如有的函数只能在PASSIVE_LEVEL下运行,有的函数则可以在DISPATCH_LEVEL以下级别运行,级别越高的时候,对代码的要求就越严格,比如在DISPATCH_LEVEL的时候,就不能使用分页内存。通常情况下应该尽可能让代码在低运行级别如PASSIVE_LEVEL下运行,在高级别下运行过长时间将导致系统效率降低、影响系统响应的实时性。但有时候自己无法控制运行的级别,例如在调用低层Driver时使用IoCallDriver,低层Driver响应完毕后会执行completion例程,该例程运行的级别就是由低层Driver来决定。因此在编写completion例程时,应尽量将这个函数设计成能在DISPATCH_LEVEL级别运行。
依照以上开发步骤,我们可以设计出全新的WDM设备驱动程序。