1、三种类型的WDM驱动程序
总线驱动程序(bus driver)
功能驱动程序(function driver)
过滤驱动程序(filter driver)
2、其他分类方法
类驱动程序(class driver)
端口驱动程序(port driver)
小端口驱动程序(miniort driver)
3、驱动程序对象(DRIVER_OBJECT)主要成员
DeviceObject: 指向一个设备对相链表,每个设备对象代表一个设备。
DriverExtension: 一个结构体, 该结构只有AddDevice成员可以直接访问。
DriverStartIo: 指向驱动程序中处理I/O请求的函数。
DriverUnload: 指向驱动程序中的清除函数。
MajorFunction: 为一个函数指针表, 指向存在于驱动程序中的各个IRP处理函数, 它定义了I/O请求如何进入驱动程序。
4、设备对象(DEVICE_OBJECT)主要成员
DriverObject: 指向与该设备对象相关的驱动程序对象。过滤驱动程序有时需要用这个指针来寻找被过滤设备的驱动程序对象。
CurrentIrp: 指向最近发往驱动程序StartIo函数的I/O请求包。
Flags: 包含一组标志位
DO_BUFFERED_IO: 读写操作使用缓冲方式(系统复制缓冲区)访问用户模式数据
DO_EXCLUSIVE: 一次只允许一个线程打开设备句柄
DO_DIRECT_IO: 读写操作使用直接方式(内存描述符表)访问用户模式数据
DO_DEVICE_INITIALIZING: 设备对象正在初始化
DO_POWER_PAGABLE: 必须在PASSIVE_LEVEL级上处理IRP_MJ_PNP请求
DO_POWER_INRUSH: 设备上电期间需要大电流
Characteristics: 包含另一组标志位,描述设备的可选特征
FILE_REMOVABLE_MEDIA: 可更换媒介设备
FILE_READ_ONLY_DEVICE: 只读设备
FILE_FLOPPY_DISKETTE: 软盘驱动器设备
FILE_WRITE_ONCE_MDEIA: 只写一次设备
FILE_REMOTE_DEVICE: 通过网络连接访问的设备
FILE_DEVICE_IS_MOUNTED: 物理媒介已在设备中
FILE_DEVICE_SECURE_OPEN: 在打开操作中检查设备对象的安全属性
DeviceType: 一个枚举常量,描述设备类型。
FILE_DEVICE_PRINTER: 打印机
FILE_DEVICE_SCANNER: 扫描仪
...
FILE_DEVICE_UNKNOWN: 未知设备
5、驱动程序对象与设备对象的关系
一方面, 驱动程序对象通常有多个与它相关的设备对象,因此它利用DeviceObject指针指向一个设备对象列表,该列表表示驱动程序可以控制的物理设备。
另一方面, 设备对象反过来指向它自己的驱动程序对象, 这样I/O管理器就知道在接收一个I/O请求时应该调用哪个驱动程序。每个功能码都对应一个驱动程序的入口点。
6、I/O请求包(IRP)
IRP是I/O系统用来存储处理I/O请求所需信息的地方, I/O管理器在IRP中保存一个指向调用者文件对象的指针。从编程的角度看, IRP是I/O管理器在响应一个I/O请求时从非分页系统内存中分配的一块可变大小的数据结构内存, I/O管理器每收到一个来自用户的请求就创建一个该结构,并将其作为参数传给驱动程序的DispatchXxx、StartIo等例程。该结构中存放有请求的类型、用户缓冲区的首地址、用户请求数据的长度等信息。驱动程序处理完这个请求后, 也在该结构中添加处理结果的有关信息, 然后调用IoCompleteRequest将其返回给I/O管理器, 用户程序的请求随即返回。
每个IRP可以被看成由两部分组成: 固定部分和一个I/O堆栈。IRP的固定部分包含关于请求的信息, I/O堆栈则包含一系列I/O堆栈单元(I/O Stack location), 单元的数目应与驱动程序堆栈中处理这一请求的驱动程序数目相同, 每个单元对应一个将处理该IRP的驱动程序。
IRP固定部分的域
MdlAddress(Memory Descriptor List, MDL): 指向一个内存描述表。当驱动程序使用直接I/O时, MDL用来描述一个与该请求相关联的用户模式缓冲区
AssociatedIrp: 该域是一个三指针联合, 其中与WDM驱动程序相关的指针是AssociatedIrp.SystemBuffer。如果设备执行缓冲I/O, 则SystemBuffer指针指向系统空间缓冲区, 否则为NULL。
RequestorMode: 取值为一个枚举常量UserMode或KernelMode, 指定请求初始化的模式为用户模式还是核心模式。驱动程序有时需要查看这个值来决定是否需要信任某些参数
Cancel: 该域为BOOLEAN类型。如果为TRUE, 则表明IoCancelIrp已被调用, 该函数用于取消这个请求。如果为FALSE, 则表明没有调用IoCancelIrp函数
CancelIrql: 一个IRQL(I/O Request Query Level)值, 表明那个专用的取消自旋锁是在这个IRQL上获取的。当驱动程序在取消例程中释放自旋锁时应该参考这个域
CancelRoutine: 指向驱动程序取消例程的地址。应该使用IoSetCancelRoutine函数设置CancelRoutine域而不是直接修改该域
IRP堆栈单元中的域
任何内核模式程序在创建一个IRP时, 同时还创建了一个与之关联的I/O堆栈。堆栈中的I/O堆栈单元由IO_STACK_LOCATION结构定义, 每个堆栈单元都对应一个将处理的IRP的驱动程序。为了在一个给定的IRP中确定当前的IRP I/O堆栈单元, 驱动程序可以调用IoGetCurrentIrpStackLocation函数, 该函数返回当前I/O堆栈单元的指针。
MajorFunction: 该IRP的主要功能代码, 它指出所要执行的I/O操作类型。例如: 主功能代码IRP_MJ_READ表示通过Win32 API函数CreateFile发送的请求。主功能代码与驱动程序对像的MajorFunction表中的某个分发函数指针相对应。
MinorFunction: 该IRP的副功能代码, 它进一步指出该IRP属于哪个主功能类。例如: IRP_MJ_PNP请求有十几个副功能代码, 包括IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE等。
DeviceObject: 指向该堆栈单元对应的设备对象地址, 该域由IoCallDriver函数负责填写。
FileObject: 指向与一个I/O请求有关的文件对象地址。
I/O功能代码
IRP_MJ_CREATE: 打开设备 CreateFile
IRP_MJ_CLEANUP: 在关闭设备时, 取消挂起的I/O请求 CloseHandle
IRP_MJ_CLOSE: 关闭设备 CloseHandle
IRP_MJ_READ: 从设备获得数据 ReadFile
IRP_MJ_WRITE: 向设备发送数据 WriteFile
IRP_MJ_DEVICE_CONTROL: 对用户模式或内核模式客户可用的控制操作 DeviceControl
IRP_MJ_INTERNAL_DEVICE_CONTROL: 只对内核模式客户程序可用的控制操作 没有对应的Win32 API
IRP_MJ_QUERY_INFORMATION: 得到文件的长度 GetFileLength
IRP_MJ_SET_INFORMATION: 设置文件的长度 SetFileLength
IRP_MJ_FLUSH_BUFFERS: 写输出缓冲区或丢弃输入缓冲区 FlushFileBuffers FlushConsoleInputBuffer PureComm
IRP_MJ_SHUTDOWN: 系统关闭 InitialSystemShutdown
7、单层驱动程序的I/O请求
I/O请求经过子系统DLL
子系统DLL调用I/O管理器的NtWriteFile服务
I/O管理器分配一个描述该请求的的IRP, 并通过调用IoCallDriver函数向驱动程序(这里指设备驱动程序)发送请求
驱动程序将IRP中的数据传输到设备并启动I/O操作
通过中断CPU, 驱动程序发信号进行I/O完成操作
在设备完成操作并且中断CPU时, 设备驱动程序服务于中断
驱动程序调用IoCompleteRequest函数表明它已经处理完IRP请求, 接管I/O管理器完成I/O请求
驱动程序可用的内核态例程:
Ex... 执行支持
Hal... 硬件抽象层(仅NT/Windows 2000)
Io... I/O管理器(包括即插即用例程)
Ke... 内核
Mm... 内存管理器
Ob... 对象管理器
Po... 电源管理
Ps... 进程结构
Rtl... 运行时库
Se... 安全引用监视
Zw... 其他例程
总线驱动程序和类特定的例程:
BatteryClass... 小类驱动程序的电池类例程
Hid... 人工输入设备(HID)例程
Pc... 用于小端口驱动程序的SCSI Tape类例程
Usb... 用于USB客户驱动程序的通用串行总线驱动程序接口例程
IRP_MJ_CREATE 创建或打开设备文件
IRP_MJ_CLOSE 关闭句柄
IRP_MJ_READ 读
IRP_MJ_WRITE 写
IRP_MJ_CLEANUP 取消文件句柄上的任何等待的IRP
IRP_MJ_DEVICE_CONTROL 设备I/O控制
IRP_MJ_INTERNAL_DEVICE_CONTROL 来自高层驱动程序的I/O控制
IRP_MJ_SYSTEM_CONTROL WMI
IRP_MJ_POWER 电源管理请求
IRP_MJ_PNP 即插即用消息
IRP_MJ_SHUTDOWN 关闭通知
UNICODE_STRING结构定义:
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
Buffer域指向一个宽16位字符缓冲区,该字符串通常不是NULL终止的,而是由Length域指定字符串的当前大小(字符数)
UNICODE_STRING函数:
RtlAnsiStringToUnicodeString: 把ANSI字符串转换成Unicode字符串,可选地分配一个缓冲区
RtlAppendUnicodeStringToString: 把一个Unicode字符串追加到另一个Unicode字符串,直到目标缓冲区的最大长度
RtlAppendUnicodeToString: 追加一个宽字符串到Unicode字符串,直到目标缓冲区的最大长度
RtlCompareUnicodeString: 比较两个Unicode字符串,可选择不去分大小写
RtlCopyUnicodeString: 把一个Unicode字符串复制到另一个Unicode字符串,直到目标缓冲区的最大长度
RtlEqualUnicodeString: 如果两个Unicode字符串相等,返回TRUE,可选择不区分大小写
RtlInitUnicodeString: 设置Unicode字符串缓冲区指向指定的宽字符串,并设置长度域为匹配值
RtlIntegerToUnicodeString: 把ULong值转换成指定基的Unicode字符串,字符串缓冲区必须提前初始化
RtlPrefixUnicodeString: 检查一个Unicode字符串是否是另一个Unicode字符串的前缀,可选择不区分大小写
RtlUnicodeStringToAnsiString: 把Unicode字符串转换成ANSI,如果分配目标ANSI缓冲区,最终使用RtlFreeAnsiString释放它
RtlUnicodeStringToInteger: 把Unicode字符串转换成一个整数
RtlUpcaseUnicodeString: 把Unicode字符串转换成大写,可选地分配缓冲区
IoCreateDevice函数:
------------------------------------------------
NTSTATUS IoCreateDevice IRQL == PASSIVE_LEVEL
------------------------------------------------
IN PDRIVER_OBJECT DriverObject 驱动程序对象
IN ULONG DeviceExtensionSize 要求的设备扩展的大小
IN PUNICODE_STRING DeviceName 设备名称,或者NULL
IN DEVICE_TYPE DeviceType 设备的类型,标准头文件WDM.H或NTDDK.H中列出的FILE_DEVICE_XXX值之一
IN ULONG DeviceCharacteristics 各种常量用OR组合在一起,指示可删除介质、只读等
IN BOOLEAN Exclusive 如果一次只有一个线程可以访问该设备,为TRUE
OUT PDEVICE_OBJECT* DeviceObject 返回的设备对象
------------------------------------------------
IoRegisterDeviceInterface函数
------------------------------------------------
NTSTATUS IoRegisterDeviceInterface IRQL == PASSIVE_LEVEL
------------------------------------------------
参数 描述
------------------------------------------------
IN PDEVICE_OBJECT PhysicalDeviceObject 设备PDO
IN CONST GUID* InterfaceClassGuid 被注册的GUID
IN PUNICODE_STRING ReferenceString 通常是NULL,引用字符串或成为接口名称的一部分,所以可以用于区分同一设备的不同接口
OUT PUNICODE_STRING SymbolicLinkName 输出接口符号连接名,完成使用时,不要忘记使用RtlFreeUnicodeString释放这个Unicode字符串
------------------------------------------------
IoRegisterDeviceInterface函数
------------------------------------------------
NTSTATUS IoRegisterDeviceInterface
------------------------------------------------
参数 描述
------------------------------------------------
IN PDEVICE_OBJECT PhysicalDeviceObject 设备PDO
IN CONST GUID* InterfaceClassGuid 被注册的GUID
IN PUNICODE_STRING ReferenceString 通常是NULL,引用字符串成为接口名称的一部分,所以可以用于区分统一设备的不同接口
OUT PUNICODE_STRING SymbolicLinkName 输出接口符号链接名。完成使用时,不要忘记使用RtlFreeUnicodeString释放这个Unicode字符串
设备名:
符号链接:这个名字向内核标识设备,而不是向Win32标识设备。根据约定,内核设备名从0开始编号,而符号链接名从1开始编号。用IoCreateSymbolicLink创建。
设备接口:驱动程序使用设备接口使它的设备对Win32程序可见,主要思想是:每个设备使一个定义的应用程序编程接口(API)可用,全局唯一标识符(GUID)用于标识这个接口。
操作系统版本的确定:
可以使用注册表设置在运行时确定是否正运行Windows 98。在NT和Windows 2000中,可用以下注册表值:HKLM\System\CurrentControlSet\Control\ProductOptions\ProductType。对Workstation/Professional Windows版本,这个值是"WinNT";对于Server/Enterprise版本,这个值是"LanmanNT"或者是"ServerNT"。在Windows 98中,这个注册表值不可用。
驱动程序出错的方式:
1、操作系统崩溃( 访问不存在的内存,在DISPATCH_LEVEL或以上的中断级访问分页内存 )
2、内核转储
3、驱动程序不启动
4、挂起
5、遗漏资源
6、时间依赖性
检查版本:
#if DGB
DbgPrint( "Wdm1 checked" );
#else
DbgPrint( "Wdm1 free" );
可重入性:就是说它可以被"同时"调用处理连个不同的IRP。在多处理器的Windows 2000系统中,一个处理器可能是用一个IRP调用Wdm1Read,另一个CPU上的第二个进程也同时使用另一个IRP调用Wdm1Read。
使一个例程是可重入的第一个技术是使用局部变量,
DebugPrint格式说明符
------------------------------------------------
符号 格式说明符 类型
------------------------------------------------
%c ANSI字符 char
%C 宽字符 wchar_t
%d,%i 十进制有符号整数 int
%D 十进制_int64 _int64
%I IRP主功能代码和次功能代码 PIRP
%L 十六进制的LARGE_INTEGER LARGE_INTEGER
%s NULL终止的ANSI字符串 char*
%S NULL终止的宽字符串 wchar_t*
%T UNICODE_STRING PUNICODE_STRING
%u 十进制的ULONG ULONG
%x 十六进制的ULONG ULONG
------------------------------------------------
一些IRP首部结构域
------------------------------------------------
域 描述
------------------------------------------------
IO_STATUS_BLOCK IoStatus IRP的完成状态
PVOID AssociatedIrp.SystemBuffer 系统空间缓冲区(用于缓冲I/O)
BOOLEAN Cancel 设置IRP是否已经被取消
ULONG Flags IRP标志
------------------------------------------------
CTL_CODE宏参数
------------------------------------------------
参数 描述
------------------------------------------------
DeviceType 指定IoCreateDevice的FILE_DEVICE_XXX值
ControlCode IOCTL功能代码
0x000 ~ 0x7FF 为Microsoft保留
0x800 ~ 0xFFF是私有代码
TransferType METHOD_BUFFERED
METHOD_IN_DIRECT
METHOD_OUT_DIRECT
METHOD_OUT_DIRECT
METHOD_NEITHER
RequiredAccess FILE_ANY_ACCESS
FILE_READ_DATA
FILE_WRITE_DATA
FILE_READ_DATA | FILE_WRITE_DATA
------------------------------------------------
缓冲I/O:
驱动程序可以使用两个主要的方法访问用户缓冲区,这两个方法是缓冲I/O和直接I/O。在创建设备时,必须设置新设备对象的Flags域中的DO_BUFFERED_IO位来使用缓冲I/O,或设置DO_DIRECT_IO位来使用直接I/O。
如果使用缓冲I/O,内核使用户的缓冲区在某个非分页内存中可用,并在IRP首部的AssociatedIrp.SystemBuffer域中存储合适的指针。在驱动程序中简单地读或写这个内存。
这个技术是驱动程序开发者最容易使用的一个技术。但是,它总体上速度要慢些,因为操作系统通常必须要把用户缓冲区复制到非分页内存或者从非分页内存复制出来。
直接I/O:
使用内存描述符列表(MDL)速度要快些,但这仅可用于执行直接内存访问(DMA)的硬件。用户缓冲区的MDL放在IRP首部的MdlAddress域中。
DeviceIoControl缓冲区
自旋锁:内核自旋锁在BufferLock提供这个保护。自旋锁可以用在代码需要短时访问某种资源的地方。
KSPIN_LOCK BufferLock; // 声明一个自旋锁对象
KeInitializeSpinLock( &BufferLock ); // 初始化
KeAcquireSpinLock( &BufferLock, XXX ); // 获得自旋锁
KeReleaseSpinLock( &BufferLock, XXX ); // 释放自旋锁
"自旋(spin)"是指KeAcquireSpinLock不断进行监视。由于这个原因,我们只能持有一个自旋锁很短的时间。DDK建议不要持有自旋锁超过25微秒。
我们在KeAcquireSpinLock的调用中,必须提供一个指向KIRQL变量的指针,这个变量存储在提升到DISPATCH_LEVEL之前的原始IRQL级。在必要时,KeReleaseSpinLock的调用降低IRQL。如果肯定代码在DISPATCH_LEVEL IRQL运行,可以使用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel例程得到更好的性能。
在持有一个自旋锁时,不要访问分页代码或数据,因为系统几乎肯定会崩溃。在持有一个自旋锁时,千万不要退出主分发例程。
设备驱动程序工作的次序:
1) 一个标准总线驱动程序检测何时添加一个设备
2) 设备标识符用于发现驱动程序。
3) 驱动程序被装入,并告诉它添加了一个新设备。
4) 进一步的消息告诉用户使用了哪些资源。
5) 然后驱动程序与设备通信,这可能要使用一个标准驱动程序的服务来实现。
当一个设备拔出时,Windows检测到这个事件,并告诉驱动程序该设备已经不存在。
即插即用设备检测
------------------------------------------------
目标 在加电时检测全部设备
装入最合适的驱动程序
为每个驱动程序分配资源
对于可热插拔的设备,在添加或删除设备时处理
解决方案 提供枚举器总线驱动程序,发现新设备,并检测何时添加或删除设备
每个设备提供标识符,用于发现最合适的驱动程序
总线驱动程序或INF文件提供设备的资源分配
提供仲裁器确定给每个设备分配哪些资源
发送消息指示设备事件
缺点 正在运行的设备可能必须停止,这样才可以重新分配它们资源
------------------------------------------------
过滤驱动程序只是在设备第一次安装时安装,所以在设备栈构造后就不能插入过滤驱动程序。
单一驱动程序和分层驱动程序:键盘驱动程序为单一驱动程序,因为它接管处理键盘要求的所有处理(即它不是用其它的驱动程序)。相反,USB驱动程序使用分层方法,为了使用USB键盘,键盘驱动程序需要使用USB类驱动程序的服务。
即插即用次功能代码
------------------------------------------------
常用PnP IRP 功能
------------------------------------------------
IRP_MN_START_DEVICE 分配资源并启动一个设备
IRP_MN_QUERY_REMOVE_DEVICE 询问一个设备是否可以被删除
IRP_MN_CANCEL_REMOVE_DEVICE 取消查询删除请求
IRP_MN_REMOVE_DEVICE 设备被拔出或卸下取消资源分配并删除设备
IRP_MN_SURPRISE_REMOVAL(仅Windows 2000) 用户在意外的情况下拔出设备
IRP_MN_QUERY_STOP_DEVICE 取消查询停止请求
IRP_MN_STOP_DEVICE 停止设备进行资源重新分配
不常用PnP IRP
IRP_MN_QUERY_DEVICE_RELATIONS 查询有特定特征的PDO
IRP_MN_QUERY_INTERFACE 让驱动程序导出直接调用接口
IRP_MN_QUERY_CAPABILITIES 查询设备功能(如它是否可以被锁定货弹出)
IRP_MN_QUERY_RESOURCES 取出设备的引导配置资源
IRP_MN_QUERY_RESOURCE_REQUIREMENTS 查询设备的资源要求
IRP_MN_QUERY_DEVICE_TEXT 得到设备的描述或位置字符串
IRP_MN_FILTER_RESOURCE_REQUIREMENTS 让过滤驱动程序和功能驱动程序过滤设备的资源要求
IRP_MN_READ_CONFIG 读配置空间信息
IRP_MN_WRITE_CONFIG 设置配置空间信息
IRP_MN_EJECT 从设备槽弹出设备
IRP_MN_SET_LOCK 设置设备锁定状态
IRP_MN_QUERY_ID 设置设备的硬件、兼容性和实例ID
IRP_MN_QUERY_PNP_DEVICE_STATE 设置设备状态位映射中的位
IRP_MN_QUERY_BUS_INFORMATION 得到副总线的类型和实例号
IRP_MN_DEVICE_USAGE_NOTIFICATION 通知设备是在分页文件、休眠文件还是崩溃转储文件的路径中
IRP_MN_QUERY_LEGACY_BUS_INFORMATION 返回遗留总线信息(仅Windows 2000)
------------------------------------------------
当一个设备被添加到系统时,Windows查找正确的驱动程序,并调用它的DriverEntry例程,告诉它添加了一个设备。就是这个时候,驱动程序创建他自己的的设备对象,即功能设备对象(FDO)。但是驱动程序还不试图访问它的设备硬件。
在处理的过程中,驱动程序收到一个IRP_MN_START_DEVICE IRP,包括设备被分配的资源的信息,然后他开始与设备硬件进行合适的对话。
如果一个设备要被拔出,Windows使用IRP_MN_REMOVE_DEVICE IRP查询驱动程序设备是否可以被删除。如果驱动程序同意,则发送一个IRP_MN_REMOVE_DEVICE IRP删除设备。如果驱动程序不希望设备被删除(如它正处于一个长的传输过程中),它将拒绝删除请求,然后发送IRP_MN_CANCEL_REMOVE_DEVICE IRP,使它回到开始的状态。
如果用户偶然拔出一个设备,在Windows 98中给驱动程序发送一个IRP_MN_REMOVE_DEVICE IRP,在Windows 2000中发送IRP_MN_SUPPRISE_REMOVE IRP。我们必须处理好中断的传输。
在删除请求的响应中,IRP_MN_QUERY_STOP_DEVICE IRP询问是否可以停止设备。如果可以停止设备,则发出IRP_MN_STOP_DEVICE IRP,使设备进入停止状态。如果不能停止设备,则发出IRP_MN_CANCEL_STOP_DEVICE IRP,使设备回到启动状态。当设备停止时,驱动程序不能访问它的设备,一个IRP_MN_START_DEVICE IRP通知驱动程序设备的新资源并再次启动设备。
注意:当设备处于停止状态或等待状态时可能会收到删除设备消息。
PnP配置管理器为驱动程序将基本的系统资源分类为:I/O端口、内存地址、DMA通道和中断。
当Windows启动时,它并不知道哪些设备连接到计算机上。它可以发现自己的一些基本信息,如有多少内存,但是如何发现其他的信息呢?
Windows使用驱动程序枚举可用的硬件。枚举是指查找并列出任何可用的设备,然后使用仲裁器调整所有的资源要求。对每个设备插找合适的驱动程序,然后告诉这些驱动程序使用哪些资源,并运行这些驱动程序。
查找
根设备 |________ 串行设备
|
|查找
|________ 键盘
|查找 查找 查找
|________ PCI总线 _______ PnP ISA 总线 ________ 声卡
|
|
|查找 查找
|______ USB 总线 _____________ USB 键盘
|
|
| 查找
|____________ USB 打印机
有两类电路可以用于PC到物理USB总线的接口:开放主机控制接口(OpenHCI)和通用主机控制器接口(UHCI)。Windows选择OpenHCI.sys驱动程序或者UHCI.sys驱动程序作为到这些电路接口的驱动程序层。
为一个设备服务的主驱动程序称为功能驱动程序。安装INF文件可能指定多个功能驱动程序放在一个设备栈中。
每个功能驱动程序或过滤驱动程序中的AddDevice例程在创建新的设备栈时调用。对每个AddDevice例程传递一个指向相同总线驱动程序PDO的指针,然后AddDevice创建一个FDO,挂接到该设备栈。AddDevice例程的调用顺序确定设备栈中驱动程序的顺序。这样,设备栈自底向上构造。类似地,当一个设备被删除时,设备栈通过先删除最上面的驱动程序来析构。PDO作为这个设备栈的固定点,因为设备栈中的每个驱动程序有相同的PDO指针。
USB 键盘设备栈
------------------------------------------------
驱动程序 设备栈
------------------------------------------------
HID键盘驱动程序 HID键盘设备FDO
HID类设备FDO
PDO(由USB集线器总线驱动程序创建)
USB集线器 USB集线器设备FDO
PDO(由USB主机控制器总线驱动程序创建)
USB主机控制器 USB主机控制器设备FDO
FDO(由PCI总线驱动程序创建)
PCI适配器 PCI设备FDO
PDO(由根总线驱动程序创建)
------------------------------------------------
支持即插即用主要是指实现一个AddDevice例程和一个IRP_MJ_PNP处理程序。这个PnP IRP有8个次功能代码,大多数的WDM驱动程序需要支持这些次功能代码:
IRP_MN_START_DEVICE(启动设备)
IRP_MN_QUERY_REMOVE_DEVIE(查询删除)
IRP_MN_REMOVE_DEVICE(删除设备)
IRP_MN_CANCEL_REMOVE_DEVICE(取消删除)
IRP_MN_STOP_DEVICE(停止设备)
IRP_MN_QUERY_STOP_DEVICE(查询停止)
IRP_MN_CANCEL_STOP_DEVICE(取消停止设备)
IRP_MN_SURPPISE_REMOVE(意外删除)
PnP的处理:
处理设备的添加和删除
得到分配的资源
处理查询停止和查询删除消息
处理停止设备消息
处理意外删除消息。
但是,上面只是一些基本的处理功能,驱动程序显然还要做以下的工作:
仅在设备启动时允许I/O请求
在有任何打开的句柄时不允许删除设备
在设备每次启动时,排队I/O请求
在处理删除请求前,等待任何I/O请求完成
在低层设备启动后处理启动设备消息
沿设备栈向下传递不支持的IRP
AddDevice例程的工作是创建和初始化一个设备对象供当前驱动程序使用,并把该对象连接到设备栈。如果一个驱动程序的AddDevice例程失败,将给它下面的任何驱动程序(其AddDeivice已成功)发送一个删除设备消息。在AddDevice例程完成后,准备好接收删除设备消息。
当添加一个Wdm2设备时,Unknown总线驱动程序为它创建一个PDO。PnP管理器把这个PDO传递给Wdm2。下面给出的Wdm2AddDevice例程调用IoCreateDevice创建Wdm2功能设备对象,并最后调用IoAttachDeviceToDeviceStack,把该设备对象挂接到设备栈。在这个过程中,FDO和它的设备扩展被初始化,并为它建立一个设备接口。
删除设备消息处理程序的最终工作是停止设备,并删除FDO。在Wdm2中,最终调用IoDetachDevice从设备栈卸下该设备,并调用IoDeleteDevice删除设备对象,即它的设备扩展内存。注意删除设备请求是驱动程序卸载前的最后一个IRP。
考虑各种PnP停止消息。当某个新硬件添加到系统或者新设备插入时,发生PnP停止消息。PnP管理器可能决定,它只有通过重新分配现有的设备目前在使用的资源,才能接纳新的设备。它首先发出一个“查询停止”消息。如果设备栈中的所有设备都同意停止,则PnP管理器发出一个“停止设备”请求。如果设备栈中的任何驱动程序不希望设备停止,PnP管理器发出“取消停止”消息,并可能通知用户目前没有足够的资源可用,所以需要重新启动。当资源被重新分配时,它发送“启动设备”消息,带有新的资源分配。
在这整个过程中,已存在的设备上的I/O请求似乎肯定正常执行。实际上,这意味着不开始任何新的请求,并在一个队列中保存这些请求,当设备再次启动时处理。所以用户可能注意到资源重新分配时有短暂的停顿,但I/O请求不会失败。
当用户想删除一个设备时,PnP管理器总是使用“查询删除”请求询问删除这个设备是否合适。所以,对于主“删除设备”请求,驱动程序可以确定设备上没有在处理的I/O请求。
但是,一些设备是可热插拔的(即用户可以带点插拔设备)。在这种情况下,PnP驱动程序接受不到“查询删除”消息,在Windows 98中,它只是得到一个“删除设备”消息。当所有打开的句柄关闭时,Windows 2000首先发送“意外删除”消息,然后发送“删除设备”请求。
记录有多少I/O请求正在处理过程中的最好方法是什么呢?答案是在设备扩展中的UsageCount域中记录打开的I/O请求的数目。在每个IRP请求的开始时,进行对LockDevice的调用,对UsageCount计数器增1。在每个IRP完成时,调用UnlockDevice,对UsageCount计数器减1。
Windows 2000提供标准的例程替我们的LockDevice和UnLockDevice例程,并提供相关的变量。在设备扩展中必须提供一个IO_REMOVE_LOCK域,在AddDevice例程中使用IoInitializeRemoveLock函数初始化这个域。使用IoAcquireRemoveLock替换LockDevice的所有调用,使用IoReleaseRemoveLock替换对UnLockDevice的所有调用。DDK建议在传递出对代码的引用(如定时器、DPC或任何其他的回调例程)时,要调用IoAcquireRemoveLock。当这些回调例程禁止时,调用IoReleaseRemoveLock,这通常在“删除设备”处理程序中发生。