长春抗癌研究所地址:驱动编程学习笔记之IO处理

来源:百度文库 编辑:九乡新闻网 时间:2024/04/28 00:19:42
驱动编程学习笔记之IO处理(转)关键词: i/o    IRP    IOCTL                                          

典型的i/o处理过程
=================

操作系统将所有的i/o请求都抽象成针对一个虚拟文件的操作,从而掩盖了"一个i/o操作的目标可能不是一个文件结构的设备"这样的事实。这一抽象也使得应用程序对待设备的接口变得泛化。

用户模式api
   |
i/o系统服务api(Ntxxx)
   |
i/o管理器(Ioxxx)
   |
内核模式
设备驱动程序------驱动程序支持例程(Io, Ex, Ke, Mm, Hal, FsRtl等等)
   |
HAL i/o访问例程
   |
i/o端口和寄存器


驱动程序对象和设备对象
======================

驱动程序对象(driver object)代表了系统中一个单独的驱动程序。i/o管理器从该驱动程序对象中获得其每一个分发例程(入口点)的地址;

设备对象(device object)代表了系统中一个物理的或逻辑的设备,并且描述了它的特征,比如它要求的缓冲区的对齐特性,以及它的设备队列(用于存放进来的irp)的位置。

当一个驱动程序被加载到一个系统中时,i/o管理器创建一个驱动程序对象,然后它调用该驱动程序的初始化例程(DriverEntry),该例程会利用该驱动程序的入口点来填充此对象的属性。

设备对象指回到它的驱动程序对象上,这正是当i/o管理器接收到一个i/o请求时,它如何知道该调用哪一个驱动例程的原因。它利用该设备对象来找到一个驱动程序对象,此对象代表了负责为该设备提供服务的驱动程序。然后,它利用原始请求中所提供的功能代码,索引到该驱动程序对象中;每个功能代码对应于驱动程序的一个入口点。

驱动程序对象通常有多个与之关联的设备对象。这些设备对象的列表代表了该驱动程序所控制的物理和逻辑设备。例如,一个硬盘的每个分区都有一个单独的设备对象,该设备对象包含了与此分区相关的信息。然而,同样的硬盘驱动程序被用于访问所有这些分区。当一个驱动程序被从系统卸载时,i/o管理器使用设备对象的队列来决定哪些设备将会受到"该驱动程序被移除"的影响。

 

IRP结构
=======

MdlAddress (PMDL)域指向一个内存描述符表(MDL),该表描述了一个与该请求关联的用户模式缓冲区。如果顶级设备对象的Flags域为 DO_DIRECT_IO,则I/O管理器为IRP_MJ_READ或IRP_MJ_WRITE请求创建这个MDL。如果一个 IRP_MJ_DEVICE_CONTROL请求的控制代码指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,则 I/O管理器为该请求使用的输出缓冲区创建一个MDL。MDL本身用于描述用户模式虚拟缓冲区,但它同时也含有该缓冲区锁定内存页的物理地址。为了访问用户模式缓冲区,驱动程序必须做一点额外工作。

Flags(ULONG)域包含一些对驱动程序只读的标志。但这些标志与WDM驱动程序无关。

AssociatedIrp (union)域是一个三指针联合。其中,与WDM驱动程序相关的指针是AssociatedIrp.SystemBuffer。 SystemBuffer指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中。对于IRP_MJ_READ和IRP_MJ_WRITE操作,如果顶级设备指定DO_BUFFERED_IO标志,则I/O管理器就创建这个数据缓冲区。对于IRP_MJ_DEVICE_CONTROL操作,如果 I/O控制功能代码指出需要缓冲区(见第九章),则I/O管理器就创建这个数据缓冲区。I/O管理器把用户模式程序发送给驱动程序的数据复制到这个缓冲区,这也是创建IRP过程的一部分。这些数据可以是与WriteFile调用有关的数据,或者是DeviceIoControl调用中所谓的输入数据。对于读请求,设备驱动程序把读出的数据填到这个缓冲区,然后I/O管理器再把缓冲区的内容复制到用户模式缓冲区。对于指定了METHOD_BUFFERED 的I/O控制操作,驱动程序把所谓的输出数据放到这个缓冲区,然后I/O管理器再把数据复制到用户模式的输出缓冲区。

IoStatus(IO_STATUS_BLOCK) 是一个仅包含两个域的结构,驱动程序在最终完成请求时设置这个结构。IoStatus.Status域将收到一个NTSTATUS代码,而 IoStatus.Information的类型为ULONG_PTR,它将收到一个信息值,该信息值的确切含义要取决于具体的IRP类型和请求完成的状态。Information域的一个公认用法是用于保存数据传输操作,如IRP_MJ_READ,的流量总计。某些PnP请求把这个域作为指向另外一个结构的指针,这个结构通常包含查询请求的结果。

RequestorMode将等于一个枚举常量UserMode或KernelMode,指定原始I/O请求的来源。驱动程序有时需要查看这个值来决定是否要信任某些参数。

PendingReturned(BOOLEAN)如果为TRUE,则表明处理该IRP的最低级派遣例程返回了STATUS_PENDING。完成例程通过参考该域来避免自己与派遣例程间的潜在竞争。

Cancel(BOOLEAN)如果为TRUE,则表明IoCancelIrp已被调用,该函数用于取消这个请求。如果为FALSE,则表明没有调用IoCancelIrp函数。取消IRP是一个相对复杂的主题,我将在本章的最后详细描述它。

CancelIrql(KIRQL)是一个IRQL值,表明那个专用的取消自旋锁是在这个IRQL上获取的。当你在取消例程中释放自旋锁时应参考这个域。

CancelRoutine(PDRIVER_CANCEL)是驱动程序取消例程的地址。你应该使用IoSetCancelRoutine函数设置这个域而不是直接修改该域。

UserBuffer(PVOID) 对于METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。该域还用于保存读写请求缓冲区的用户模式虚拟地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO标志的驱动程序,其读写例程通常不需要访问这个域。当处理一个METHOD_NEITHER控制操作时,驱动程序能用这个地址创建自己的MDL。

Tail.Overlay是Tail联合中的一种结构,它含有几个对WDM驱动程序有潜在用途的成员。图5-2是Tail联合的组成图。在这个图中,以水平方向从左到右是这个联合的三个可选成员,在垂直方向是每个结构的成员描述。Tail.Overlay.DeviceQueueEntry(KDEVICE_QUEUE_ENTRY)和 Tail.Overlay.DriverContext(PVOID[4])是Tail.Overlayare内一个未命名联合的两个可选成员(只能出现一个)。I/O管理器把DeviceQueueEntry作为设备标准请求队列中的连接域。当IRP还没有进入某个队列时,如果你拥有这个IRP你可以使用这个域,你可以任意使用DriverContext中的四个指针。Tail.Overlay.ListEntry(LIST_ENTRY)仅能作为你自己实现的私有队列的连接域。

CurrentLocation (CHAR)和Tail.Overlay.CurrentStackLocation(PIO_STACK_LOCATION)没有公开为驱动程序使用,因为你完全可以使用象IoGetCurrentIrpStackLocation这样的函数获取这些信息。但意识到CurrentLocation就是当前I/O堆栈单元的索引以及CurrentStackLocation就是指向它的指针,会对驱动程序调试有一些帮助。

&nbp;


I/O堆栈
=======


irp是由两部分组成的:一个固定的头以及一个或者多个栈单元。固定部分包含一些诸如以下的信息:该请求的类型和大小,该请求是同步的还是异步的,一个用于缓冲类型i/o的缓冲区指针,以及一些该请求处理过程中会被改变的状态信息。

irp的栈单元包含一个功能代码(由一个主码和一个次码组成)与功能相关的参数,以及一个指向调用者文件对象的指针。

MajorFunction (UCHAR)是该IRP的主功能码。这个代码应该为类似IRP_MJ_READ一样的值,并与驱动程序对象中MajorFunction表的某个派遣函数指针相对应。如果该代码存在于某个特殊驱动程序的I/O堆栈单元中,它有可能一开始是,例如IRP_MJ_READ,而后被驱动程序转换成其它代码,并沿着驱动程序堆栈发送到低层驱动程序。我将在第十一章(USB总线)中举一个这样的例子,USB驱动程序把标准的读或写请求转换成内部控制操作,以便向 USB总线驱动程序提交请求。

MinorFunction(UCHAR)是该IRP的副功能码。它进一步指出该IRP属于哪个主功能类。例如,IRP_MJ_PNP请求就有约一打的副功能码,如IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE,等等。

Parameters (union)是几个子结构的联合,每个请求类型都有自己专用的参数,而每个子结构就是一种参数。这些子结构包括Create (IRP_MJ_CREATE请求)、Read(IRP_MJ_READ请求)、StartDevice(IRP_MJ_PNP的 IRP_MN_START_DEVICE子类型),等等。

DeviceObject(PDEVICE_OBJECT)是与该堆栈单元对应的设备对象的地址。该域由IoCallDriver函数负责填写。

FileObject(PFILE_OBJECT)是内核文件对象的地址,IRP的目标就是这个文件对象。驱动程序通常在处理清除请求(IRP_MJ_CLEANUP)时使用FileObject指针,以区分队列中与该文件对象无关的IRP。

CompletionRoutine (PIO_COMPLETION_ROUTINE)是一个I/O完成例程的地址,该地址是由与这个堆栈单元对应的驱动程序的更上一层驱动程序设置的。你绝对不要直接设置这个域,应该调用IoSetCompletionRoutine函数,该函数知道如何参考下一层驱动程序的堆栈单元。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实需要一个完成例程,但通常没有自己的堆栈单元。这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因。

Context(PVOID)是一个任意的与上下文相关的值,将作为参数传递给完成例程。你绝对不要直接设置该域;它由IoSetCompletionRoutine函数自动设置,其值来自该函数的某个参数。

 

I/O请求派遣机制
===============

Win2000的I/O请求是包驱动的,当一个I/O请求开始,I/O管理器先创建一个IRP去跟踪这个请求,另外,它存储一个功能代码在IRP的I/O堆栈区的MajorField域中来唯一的标识请求的类型。

MajorField 域是被I/O管理器用来索引驱动程序对象的MajorFunction表,这个表包含一个指向一个特殊I/O请求的派遣例程的功能指针,如果驱动程序不支持这个请求,MajorFunction表就会指向I/O管理器函数_IopInvalidDeviceRequest,该函数返回一个错误给原始的调用者。驱动程序的作者有责任提供所有的驱动程序支持的派遣例程。


启用特殊的功能代码
==================

如果想要启用特殊的功能代码,驱动程序必须声明一个响应这个请求的派遣例程,声明机制是很简单的,只需要在DriverEntry例程中存储派遣例程函数的地址到驱动程序对象的MajorFunction表的适当的位置,I/O功能代码是这个表的索引。如下列代码片段所示:

NTSTATUS DriverEntry (IN PDRIVER_OBJECT pDO, IN PUNICODE_STRING pRegPath)
{        :

    pDO->MajorFunction[ IRP_MJ_CREATE ]  = DispCreate;
    pDO->MajorFunction[ IRP_MJ_CLOSE ]   = DispClose;
    pDO->MajorFunction[ IRP_MJ_CLEANUP ] = DispCleanup;
    pDO->MajorFunction[ IRP_MJ_READ ]    = DispRead;
    pDO->MajorFunction[ IRP_MJ_WRITE ]   = DispWrite;

        :
    return STATUS_SUCCESS; 
}

注意,每一个I/O功能代码(表的索引)是被一个IRP_MJ_XXX形式的符号所标识,它们被NTDDK.h和WDM.h文件定义,这些符号常量总是被用在固定的地方。

声明方法也允许一个单一的例程被用来处理多个请求类型。DriverEntry可以放置公共的派遣例程,因为IRP命令包含请求代码,公共的函数可以被作为合适的例程而被调用。

最后,驱动程序不支持的例程应该被DriverEntry例程忽略,I/O管理器在调用DriverEntry例程之前就已经将整个的MajorFunction表填充为_IopInvalidDeviceRequest。


决定需要支持那些功能代码
========================

所有的驱动程序必须支持IRP_MJ_CREATE功能代码,因为这个功能代码是用来响应Win32用户模式的CreateFile调用,如果不支持这功能代码,Win32程序就没有办法获得设备的句柄,类似的,驱动程序必须支持IRP_MJ_CLOSE功能代码,因为它用来响应Win32用户模式的 CloseHandle调用。顺便提一下,系统自动调用CloseHandle函数,因为在程序退出的时候,所有的句柄都没有被关闭。

其它的功能代码是否支持依赖它控制的设备的性质,当编写分层的驱动程序的时候,高层的驱动程序必须支持连接所有的低层驱动程序的例程,因为用户请求先通过高层的驱动程序。


IRP功能代码                           意义                           调用者
----------------------------------------------------------------------------------------
IRP_MJ_CREATE                    请求一个句柄                      CreateFile
IRP_MJ_CLEANUP                   在关闭句柄时取消悬挂的IRP         CloseHandle 
IRP_MJ_CLOSE                     关闭句柄                          CloseHandle
IRP_MJ_READ                      从设备得到数据                    ReadFile
IRP_MJ_WRITE                     传送数据到设备                    WriteFile
IRP_MJ_DEVICE_CONTROL            控制操作                          DeviceIoControl
IRP_MJ_INTERNAL_DEVICE_CONTROL   控制操作(只能被内核调用)          没有Win32调用
IRP_MJ_QUERY_INFORMATION         得到文件的长度                    GetFileSize
IRP_MJ_SET_INFORMATION           设置文件的长度                    SetFileSize
IRP_MJ_FLUSH_BUFFERS             写输出缓冲区或者丢弃输入缓冲区    FlushFileBuffers
                                                                   FlushConsoleInputBuffer                                            &nbp;                                          PurgeComm
IRP_MJ_SHUTDOWN                  系统关闭                          InitiateSystemShutdown
-------------------------------------------------------------------------------------------
                表7.1常用的功能调用

 


扩展派遣例程
============

大部分的I/O管理器的操作支持一个标准的读写提取,请求者提供一个缓冲区和缓冲区的长度和传送到或者从设备的数据。不是所有的操作都适合这个抽象。例如,磁盘的格式化或者重新分区操作不适合一般的读或者写操作。这种请求使用扩展的I/O请求代码处理,这些代码允许任何数量的驱动程序指定的操作,不被读写抽象所约束。

1. IRP_MJ_DEVICE_CONTROL允许扩展的I/O请求,使用用户模式的DeviceIoControl函数来调用,I/O管理器创建一个 IRP,这个IRP的MajorFunction和IoControlCode是被DeviceIoControl函数指定其内容。

2.  IRP_MJ_INTERNAL_DEVICE_CONTROL允许内核模式的请求,用户模式没有权力使用这个操作,它主要是用于在一个分层的堆栈中其它驱动程序传递特殊的请求。这两个版本的设备操作的其它的地方是相同的。请求者放置了一个IoControlCode值到IRP。

结果,这两个中的任何一个的执行都需要一个于IRP的IoControlCode值有关的二级派遣例程。这个值叫做设备I/O控制代码(IOCTL)。因为二级派遣例程机制完全包含驱动程序的私有例程。IOCTL值的意义是由驱动程序指定的。下面详细介绍设备控制接口。


定义私有的IOCTL
===============

传递给驱动程序的IOCTL遵循一个特殊的结构,它有32-bit大小,DDK包含一个方便的产生IOCTL值的机制的宏,CTL_CODE。

  31-16    |     15-14      |     13-2    |     1-0
DeviceType | RequiredAccess | ControlCode | TransferType


管理IOCTL缓冲区
===============

IOCTL请求可以在同一个调用中指定同时指定一个输入缓冲区和一个输出缓冲区。这样,提供了一个写之后再读的抽象给调用者。有两个访问用户缓冲区的方法。

缓冲区传输机制是用IOCTL的ControlCode定义,独立与整个的设备对象策略之外。
有两个相关的缓冲区,一个是输入缓冲区,一个是输出缓冲区。
以下的部分描述不同的缓冲区策略


METHOD_BUFFERED
===============

I/O 管理器从非分页的缓冲池分配一个单一的临时缓冲区,它足够容纳调用者的输入或者输出缓冲区中的数据,这个缓冲池的地址放在IRP的 AssociatedIrp.SystemBuffer域。然后复制请求者的出入缓冲区到缓冲池,再设置IRP的UserBuffer域为用户空间输出缓冲区的地址。

完成IOCTL请求之后,I/O管理器复制系统缓冲池中的数据到请求者的用户空间缓冲区,注意只有一个内部的缓冲池提供给驱动程序代码,甚至用户指定了独立的输入和输出缓冲区。驱动程序代码必须在向缓冲池中写数据之前将收到的数据全部从缓冲池中读出。

METHOD_IN_DIRECT
================

I/O管理器检查请求者的输入缓冲区,然后将它锁定到物理存储空间中,为输入缓冲区创建一个MDL,存储指向MDL的指针到IRP的MdlAddress域。

也从非分页的缓冲池中分配一个临时的输出缓冲区,存储这个缓冲区的地址到IRP的AssociatedIrp.SystemBuffer域。IRP的 UserBuffer域设置为调用者的输出缓冲区地址。当IOCTL IRP完成,系统缓冲区中的内容被复制到调用者原始的输出缓冲区。

METHOD_OUT_DIRECT
=================

I/O管理器检查请求者的输出缓冲区,然后将它锁定到物理存储空间中,为输出缓冲区创建一个MDL,存储指向MDL的指针到IRP的MdlAddress域。

也从非分页的缓冲池中分配一个临时的输入缓冲区,存储这个缓冲区的地址到IRP的AssociatedIrp.SystemBuffer域。调用者原始的输入缓冲区的内容被复制到系统缓冲区,设置IRP的UserBuffer域为NULL。

METHOD_NEITHER
==============

I/O管理器放置调用者输入缓冲区的地址到IRP的当前I/O堆栈单元的Parameters.DeviceIoControl.Type3InputBuffer域,存储输出缓冲区的地址到IRP的UserBuffer域中,两个都是用户空间地址。


IOCTL参数传递方法
=================

扩展的功能用IOCTL值来定义,它存储在驱动程序常常需要的输入或者输出的缓冲区中,驱动程序常使用IOCTL值来汇报执行的数据,这个数据通过用户提供的缓冲区传输。确实,DeviceIoControl函数为两个缓冲区定义参数,一个用来输入,另一个用来输出。I/O管理器提供的缓冲区传输机制在 IOCTOL值中定义着,可以是缓冲区I/O或者是直接I/O。像以前介绍的,对于缓冲区I/O,I/O管理器复制用户缓冲区到(假如是写操作)系统中非分页的缓冲池,这时驱动程序代码才可以方便的操作。对于直接I/O驱动程序直接访问用户缓冲区。

有趣的是,驱动程序整个的缓冲区处理策略(在DriverEntry例程中定义)对于IOCTL传输不是强制的,也就是说,可以使用也可以不使用。确实,I/O管理器使用IOCTL结构的一个域来确定传输方法,这样每个IOCTL有不同的传输方法。这给执行DeviceIoControl操作最大的灵活性。


CTL_CODE宏参数                      意义
-------------------------------------------------------------------
DeviceType                  指定给IoCreateDevice的FILE_DEVICE_XXX值
                            0x0000到0x7FFF-保留给Microsoft
                            0x8000到0xFFFF-用户定义
--------------------------------------------------------------------
ControlCode                 驱动过程定义的IOCTL代码
                            0x000 to 0x7FF -保留给Microsoft
                            0x800 to 0xFFF -用户定义
--------------------------------------------------------------------
TransferType                这个控制代码的缓冲区传输机制
                            ETHOD_BUFFERED,ETHOD_IN_DIRECT,
                            ETHOD_OUT_DIRECT,ETHOD_NEITHER
--------------------------------------------------------------------
RequiredAccess              调用CreateFile的访问条件
                            ILE_ANY_ACCESS,
                            ILE_READ_DATA, 
                            ILE_WRITE_DATA,
                            ILE_READ_DATA | FILE_WRITE_DATA
---------------------------------------------------------------------
                表7.3 CTL_CODE宏参数

IOCTL的TransferType域有two-bits宽,它定义了下面的其中一个:

METHOD_BUFFERED. I/O管理器使用一个中介的非分页的缓冲池在用户缓冲区和驱动程序间交换数据。 
METHOD_IN_DIRECT. I/O管理器提供一个绕用户缓冲区的页表。驱动程序使用着个列表提供从设备
                  到用户空间(这是读操作)直接的I/O。
 
METHOD_OUT_DIRECT. I/O管理器提供一个环绕用户缓冲区的页表。驱动程序使用着个列表提供从用
                    户空间到设备(这是写操作)直接的I/O。 

METHOD_NEITHER.  I/O管理器不援助缓冲区传输,给驱动程序的是用户的原始的缓冲区地址(大概来
                  自分页的存储空间)。
 
因为TransferType域是IOCTL代码的一部分,Microsoft定义了一些指定I/O缓冲机制的公共的IOCTL代码。可以定义任何适当的传输机制(私有的驱动过程定义的IOCTL代码)。缓冲区I/O适用于小的,慢的数据传输。直接I/O适用于快的,大量的数据传输。

 

处理IOCTL请求
=============

一旦驱动程序声明了IRP_MJ_DEVICE_CONTROL或者IRP_MJ_INTERNAL_DEVICE_CONTROL功能代码的派遣例程,严格的解释IOCTL设备控制代码是驱动程序的职责。I/O管理器不检验IOCTL代码的各个域,请求者传递的任何随机数都可以传递到驱动程序,所以驱动程序必须作检验的工作。

因此,设备控制派遣例程的典型的结构是一个大的switch语句。如以下的例子:

NTSTATUS DispatchIoControl (IN PDEVICE_OBJECT pDO, IN PIRP pIrp)
{
    NTSTATUS status = STATUS_SUCCESS;

    PDEVICE_EXTENSION pDE;
    PVOID userBuffer;
    ULONG inSize;   
    ULONG outSize;
    ULONG controlCode;                 // IOCTL请求代码
   PIO_STACK_LOCATION pIrpStack;      //堆栈区域存储了用户缓冲区信息

   pIrpStack = IoGetCurrentIrpStackLocation (pIrp);

    // 取出IOCTL请求代码
    controlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;

    // 得到请求缓冲区大小
    inSize  = pIrpStack-> Parameters.DeviceIoControl.InputBufferLength;
    OutSize = pIrpStack-> Parameters.DeivceIoControl.OutputBufferLength;

    //现在执行二次派遣  
    switch (controlCode)
    {
    case IOCTL_MISSLEDEVICEAIM:
        // 检验参数
        if (inSize < sizeof(AIM_IN_BUFFER) ||
           (outSize < sizeof(AIM_OUT_BUFFER) )
        {
            status = STATUS_INVALID_BUFFER_SIZE;
            break;
        }

        IoMarkIrpPending (pIrp);              // 有效的IRP值- 准备设备
        IoStartPacket (pDO, pIrp, 0, NULL);   // calls the driver's StartIo routine
        return STATUS_PENDING;

    case IOCTL_DEVICE_LAUNCH:
         if (inSize > 0 || outSize > 0)
         {
             // Is it really an error to pass buffers
             // to a function that doesn't use them? Maybe not, but the caller is
             //now forced to re-think the purpose of the call.
             status = STATUS_INVALID_PARAMETER;            
             break;
         }

         // Same kind of processing start the device
         return STATUS_PENDING; 

    default:    // 驱动程序收到了未被承认的控制代码

        status = STATUS_INVALID_DEVICE_REQUEST;  
        break;
    }

    // Valid control code cases returned above. Execution here means an error
    // occurred. Fail the IRP request...pIrp->IoStatus.Status = status;

    pIrp->IoStatus.Information = 0;  // 数据没有传输
    IoCompleteRequest (pIrp, IO_NO_INCREMENT) ;     
    return status;
}

 

编写IOCTL头文件
===============

因为驱动程序工程和所有的驱动程序的客户需要IOCTL代码的符号定义,通常,驱动程序作者提供一个单独的驱动过程控制代码定义的头文件。这个头文件应该包含描述特殊控制操作的缓冲区内容和所有的结构定义。一个WIN32程序需要在包含驱动程序的IOCTL头文件之前包含WINIOCTL.h文件,驱动程序工程需要在包含驱动程序指定的IOCTL头文件之前包含DEVIOCTL.h文件。这些文件中定义了CTL_CODE宏,下前是一个IOCTL头文件的例子:

#define IOCTL_MISSLEDEVICE_AIM CTL_CODE(  \
              FILE_DEVICE_UNKNOWN, 0x801, \
              METHOD_BUFFERED, FILE_ACCESS_ANY )

                              
// IOCTL_MISSLEDEVICE_AIM使用的结构
typedef struct _AIM_IN_BUFFER
{
    ULONG Longitude;
    ULONG Latitude;
} AIM_IN_BUFFER, *PAIM_IN_BUFFER;

typedef struct _AIM_OUT_BUFFER

    ULONG ExtendedStatus; 
} AIM_OUT_BUFFER, *PAIM_OUT_BUFFER;


#define IOCTL_MISSLEDEVICE_LAUNCH CTL_CODE( \
             FILE_DEVICE_UNKNOWN,    0x802, \   
             METHOD_NEITHER,  FILE_ACCESS_ANY )

 

IRP 缓冲区管理
==============

当一个应用程序或者设备驱动程序间接地利用NtReadFile, NtWriteFile或NtDeviceIoControlFile系统服务(或者对应于这些服务的Windows API函数,即ReadFile,WriteFile和DeviceIoControl)来创建一个IRP时,


处理读写请求
============

最基本的I/O请求是在用户缓冲区和设备之间交换数据,I/O管理器提供了以传统的读写抽象来请求这样的数据传输,给驱动程序majorfunction域是IRP_MJ_READ或者IRP_MJ_WRITE的IRP的形式来传递请求,另一个IRP的域指定请求者的缓冲区地址。I/O管理器分配和维护的缓冲区地址是否是一个直接的虚拟地址或者一个中介的非分页缓冲池是由设备对象的Flags域决定的。无论如何,在这个缓冲区和设备件的数据传送是读写派遣例程的工作。

缓冲的I/O
==========

在读或者写操作的开始,I/O管理器确认用户缓冲区的所有的虚拟存储页,对于缓冲区I/O,它然后分配一个足够用户请求所使用的大小的非分页缓冲池,这个暂时的非分页缓冲区地址被存放在IRP的 AssociatedIrp.SystemBuffer域中,这个地址在数据传输过程中保持有效(也就是直到IRP被标记上完成)。

对于读操作,I/O管理器在IRP的UserBuffer域中纪录原始的用户缓冲区地址。然后使用这个保持的地址完成从非分页的缓冲池到用户存储空间的数据拷贝工作。

对于写操作,I/O管理器在调用写派遣例程之前拷贝用户缓冲区中的数据到非分页的缓冲池中。然后设置IRP的UserBuffer域为NULL,因为没有必要保持这个状态。

直接I/O
======

在开始操作之前,I/O管理器确认用户缓冲区使用的整个页表,然后创建一个存储器描述符表(MDL)的数据结构,然后存储MDL的地址到IRP的 MdlAddress域,AssociatedIrp.SystemBuffer域和UserBuffer域被设置为NULL。

对于 DMA操作,MDL结构被直接用来和adapter对象一起执行数据传输。对于过程控制I/O,MDL结构被用来和 MmGetSystemAddressForMdl函数一起去得到用户缓冲区的系统映射地址,通过这个地址可以直接访问用户缓冲区。使用这个技术,用户缓冲区被锁定到了物理存储器(也就是强制被变成非分页地址),当I/O请求完成,用户缓冲区被解除锁定。


其它的方法
==========

在设备对象的Flags域中有两个bits,它指定DO_BUFFERED_IO或者DO_DIRECT_IO。如果都没有设定,I/O管理器不会执行上面的两种方式。代替的是,它简单的将用户空间的请求的缓冲区地址放到IRP的UserBuffer域, AssociatedIrp.SystemBuffer和MdlAddress域被设置为NULL。

一个简单的用户模式的地址不是十分有用,驱动程序中的大多数例程这时不能确定映射的这个原始的页表。因此,用户空间地址通常是无用的。只有一个例外:在高层驱动程序的派遣例程被调用的时候,执行使用原始的请求线程,这时用户空间地址是有效的。中间驱动程序或者任何DPC或者中断服务例程从来都不依赖用户空间缓冲区。

读写请求实例
============

这是一个有趣的例子,虽然它很简单。通过保留一个分页的缓冲池和拷贝用户的数据到这个暂时的缓冲区,来完成写操作,这个缓冲区一直保持到读请求产生,这时这个暂时的缓冲区中的数据将被复制到用户缓冲区,然后释放暂时的缓冲区。下面例子示范了读写派遣例程和用户缓冲区的访问:

NTSTATUS DispatchWrite (IN PDEVICE_OBJECT pDO, IN PIRP pIrp)
{
    NTSTATUS status = STATUS_SUCCESS;      
    PDEVICE_EXTENSION pDE;
    PVOID userBuffer;                         
    ULONG xferSize;  

    // 堆栈中包含用户缓冲区的信息
    PIO_STACK_LOCATION pIrpStack;
    pIrpStack = IoGetCurrentIrpStackLocation (pIrp);  

    // 假定使用缓冲区I/O
    userBuffer = pIrp->AssociatedIrp.SystemBuffer;
    xferSize = pIrpStack->Parameters.Write.Length;

    // 暂时的缓冲区指针在DEVICE_EXTENSION 中
    pDE = (PDEVICE_EXTENSION) pDO->DeviceExtension;  

    // 如果已经有一个缓冲区的话,释放它
    if (pDE->deviceBuffer != NULL)
    { 
        ExFreePool (pDE->deviceBuffer);
        PDE->deviceBuffer = NULL;     
        xferSize = 0;    
     }
 
     pDE->deviceBuffer = ExAllocatePool (PagedPool, xferSize);

    if (pDE->deviceBuffer == NULL)
   {
         // 没有分配缓冲区
         status = STATUS_INSUFFICIENT_RESOURCES;  
         xferSize = 0; 
    }
    else // 复制数据
    {    
         pDE->deviceBufferSize = xferSize;
         RtlCopyMemory (pDE->deviceBuffer, userBuffer, xferSize);  
    }

    // 完成IRP 
    pIrp->IoStatus.Status = status;    
    pIrp->IoStatus.Information = xferSize;

    IoCompleteRequest (pIrp, IO_NO_INCREMENT);     
    return status;    
}

 

NTSTATUS DispatchRead (IN PDEVICE_OBJECT pDO, IN PIRP pIrp)

    NTSTATUS status = STATUS_SUCCESS;  
    PDEVICE_EXTENSION pDE;
    PVOID userBuffer;                      
    ULONG xferSize;

    //堆栈中包含用户缓冲区的信息
    PIO_STACK_LOCATION pIrpStack;
 
    pIrpStck = IoGetCurrentIrpStackLocation (pIrp);
    userBuffer = pIrp->AssociatedIrp.SystemBuffer;
    xferSize = pIrpStack->Parameters.Read.Length;  

    // 暂时的缓冲区指针在DEVICE_EXTENSION 中
    pDE = (PDEVICE_EXTENSION) pDO->DeviceExtension;

    // 仅传输用户请求数量的数据
    xferSize = (xferSize < pDE->deviceBufferSize) ?
                  xferSize : pDE->deviceBufferSize;

    // 复制临时缓冲区到用户空间
    RtlCopyMemory (userBuffer, pDE->deviceBuffer, xferSize);

    // 释放临时分页缓冲池
    ExFreePool (pDE->deviceBuffer);  
    pDE->deviceBuffer = NULL;
    pDE->deviceBufferSize = 0;          

    // 完成I/O请求
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = xferSize;
    IoCompleteRequest (pIrp, IO_NO_INCREMENT);  
    return status;       
}

 

 


创建IRP
=======

IRP 开始于某个实体调用I/O管理器函数创建它。在上图中,我使用术语"I/O管理器"来描述这个实体,尽管系统中确实有一个单独的系统部件用于创建IRP。事实上,更精确地说,应该是某个实体创建了IRP,并不是操作系统的某个例程创建了IRP。但是你的驱动程序有时也会创建IRP。

可以使用下面任何一种函数创建IRP:

IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。
IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。
IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。
IoAllocateIrp 创建上面三个函数不支持的其它种类的IRP。
 
前两个函数中的Fsd表明这些函数专用于文件系统驱动程序(FSD)。虽然FSD是这两个函数的主要使用者,但其它驱动程序也可以调用这些函数。DDK还公开了一个IoMakeAssociatedIrp函数,该函数用于创建某些IRP的从属IRP。WDM驱动程序不应该使用这个函数。


操作IRP
=======

一些IRP访问函数在IRP头,其它的是处理特殊的IRP堆栈函数。了解访问函数需要指向整个IRP,或者仅仅指向IRP堆栈是非常重要的。

I/O管理器提供多个处理IRP的函数,表4.3列出了常用的一些。

函数                  描述                      调用者
----------------------------------------------------------------
IoStartPacket      发送IRP到Start I/O例程        Dispatch
IoCompleteRequest  表示所有的处理完成            DpcForIsr
IoStartNextPacket  发送下一个IRP到Start I/O例程  DpcForIsr
IoCallDriver       发送IRP给另一个驱动程序       Dispatch
IoAllocateIrp      请求另外的IRP                 Dispatch
IoFreeIrp      &nbp;   释放驱动程序分配的IRP         I/O Completion
-----------------------------------------------------------------
                     表4.3处理整个IRP的函数

I/O管理器同样提供多个函数,驱动程序可以用它们访问IRP的堆栈部分,这些函数列在表4.4中:

函数                           描述                                        调用者
----------------------------------------------------------------------------------- 
IoGetCurrentIrpStackLocation  得到调用者堆栈的指针  
IoMarkIrpPending              为进一步的处理标记调用者I/O堆栈             Dispatch
IoGetNextIrpStackLocation     得到下一个低层的驱动程序的I/O堆栈单元的指针 Dispatch
IoSetNextIrpStackLocation     将I/O堆栈指针压入堆栈                       Dispatch
IoSetCompleteRoutine          把I/O Completion例程连接到下一个低层的驱动
                              程序的I/O堆栈单元                           Dispatch
------------------------------------------------------------------------------------ 
                        表4.4  IO_STACK_LOCATION访问函数

 


发往派遣例程
============

创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序:

PDEVICE_OBJECT DeviceObject;   

//something gives you this

PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation (Irp);
stack->MajorFunction = IRP_MJ_Xxx;

NTSTATUS status = IoCallDriver(DeviceObject, Irp);
 

IoCallDriver函数的第一个参数是你在某处获得的设备对象的地址。我将在本章的结尾处描述获得设备对象指针的两个常用方法。在这里,我们先假设你已经有了这个指针。

IRP 中的第一个堆栈单元指针被初始化成指向该堆栈单元之前的堆栈单元,因为I/O堆栈实际上是IO_STACK_LOCATION结构数组,你可以认为这个指针被初始化为指向一个不存在的"-1"元素,因此当我们要初始化第一个堆栈单元时我们实际需要的是"下一个"堆栈单元。IoCallDriver将沿着这个堆栈指针找到第0个表项,并提取我们放在那里的主功能代码,在上例中为IRP_MJ_Xxx。然后IoCallDriver函数将利用 DriverObject指针找到设备对象中的MajorFunction表。IoCallDriver将使用主功能代码索引这个表,最后调用找到的地址 (派遣函数)。

你可以把IoCallDriver函数想象为下面代码:

NTSTATUS IoCallDriver(PDEVICE_OBJECT device, PIRP Irp)
{
    IoSetNextIrpStackLocation(Irp);
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    stack->DeviceObject = device;
    ULONG fcn = stack->MajorFunction;
    PDRIVER_OBJECT driver = device->DriverObject;
    return (*driver->MajorFunction[fcn])(device, Irp);
}

 

管理自己的IRP
=============

现在,我已经解释完IRP处理的所有底层结构,我们可以返回到创建IRP的主题上。我曾提到过有四种不同的服务函数可以用来创建IRP。但我不得不推迟到现在才讨论如何选择它们。下面事实供你在选择时参考:

IoBuildAsynchronousFsdRequest和IoBuildSynchronousFsdRequest函数仅能用于创建主功能码在表5-3中列出的IRP。
IoBuildDeviceIoControlRequest仅能用于创建主功能码为IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL的IRP。
 
当调用IoCompleteRequest时,有些代码可能会释放IRP占用的内存。
你应该事先做好安排以便该IRP能被IoCancelIrp调用取消。
表5-3.  适用于IoBuildXxxFsdRequest的IRP类型。

     主功能码
---------------------
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_FLUSH_BUFFERS
IRP_MJ_SHUTDOWN
IRP_MJ_PNP
IRP_MJ_POWER
----------------------

 

使用IoBuildSynchronousFsdRequest
================================

IoBuildSynchronousFsdRequest函数的调用格式如下:

PIRP Irp = IoBuildSynchronousFsdRequest(MajorFunction,
     DeviceObject,
     Buffer,
     Length,
     StartingOffset,
     Event,
     IoStatusBlock);
 

MajorFunction (ULONG)是新IRP的主功能码(见表5-3)。DeviceObject(PDEVICE_OBJECT)是该IRP最初要发送到的设备对象的地址。对于读写请求,你必须提供Buffer(PVOID)、Length(ULONG)、StartingOffset(PLARGE_INTEGER) 参数。Buffer是一个内核模式数据缓冲区的地址,Length是读写操作的字节长度,StartingOffset是读写操作在目标文件中的定位。对于该函数创建的其它请求,这些参数将被忽略。(这就是为什么该函数在WDM.H中的原型将这些参数定为"可选的",但对于读写请求它们则是必需的) I/O管理器假定你给出的缓冲区地址在当前进程上下文中是有效的。

Event(PKEVENT)是一个事件对象的地址, IoCompleteRequest应该在操作完成时设置这个事件,IoStatusBlock(PIO_STATUS_BLOCK)是一个状态块的地址,该状态块用于保存IRP结束状态和信息。在操作完成前,事件对象和状态块必须一直存在于内存中。

如果创建的是读写IRP,那么在提交该 IRP前你不需要做任何事。如果创建的是其它类型的IRP,你需要用附加的参数信息完成第一个堆栈单元;MajorFunction已经被设置,不能设置未公开的Tail.Overlay.OriginalFileObject域,这样做将使文件对象在IRP完成时被错误地废除。同样也不可能设置 RequestorMode,它已经被初始化成KernelMode。(我提到这两点是因为我回想起曾读过该函数的一个公开讨论,该讨论认为应该做这两件事,但我认为没有必要) 现在,你可以提交该IRP并等待其完成:

PIRP Irp = IoBuildSynchronousFsdRequest(...);
NTSTATUS status = IoCallDriver(DeviceObject, Irp);
if (status == STATUS_PENDING)
  KeWaitForSingleObject(Event, Executive, KernelMode, FALSE, NULL);
 

IRP完成后,你可以通过察看你的I/O状态块来了解该IRP的结束状态和相关信息。
很明显,在等待操作完成前你应该运行在PASSIVE_LEVEL级上和非任意线程上下文中。


使用IoBuildAsynchronousFsdRequest
=================================

IoBuildAsynchronousFsdRequest是用于创建表5-3中列出的IRP的另一个例程。该函数的原型如下:

PIRP IoBuildAsynchronousFsdRequest(ULONG MajorFunction,
       PDEVICE_OBJECT DeviceObject,
       PVOID Buffer,
       ULONG Length,
       PLARGE_INTEGER StartingOffset,
       PIO_STATUS_BLOCK IoStatusBlock);
 

这个原型与IoBuildSynchronousFsdRequest不同,它没有Event参数,并且IoStatusBlock指针可以为NULL。你仍需要为该函数创建的IRP安装一个完成例程,这个完成例程的工作就是调用IoFreeIrp并返回 STATUS_MORE_PROCESSING_REQUIRED。

我想知道这两个函数在创建IRP上有什么不同之处,因此我稍微深入了一些。这两个函数的代码基本上是相同的。实际上,IoBuildAsynchronousFsdRequest是 IoBuildSynchronousFsdRequest的子函数。IoBuildSynchronousFsdRequest仅有的额外操作就是保存事件指针到IRP中(I/O管理器需要找到这个事件对象并把它置成信号态),并把该IRP放到当前线程的IRP队列中。这可以使IRP线程死亡时能被取消。

你可能在这样两种情况下需要调用IoBuildAsynchronousFsdRequest:第一种情况,当你发现自己执行在任意线程上下文中并需要创建一个IRP时,IoBuildAsynchronousFsdRequest函数就是理想选择,因为当前线程(任意线程)的结束过程不能取消这个新创建的IRP。第二种情况,当你运行在APC_LEVEL级的非任意线程上下文时,你需要同步执行一个IRP。 IoBuildSynchronousFsdRequest不能满足这个要求,因为这种IRQL将阻塞设置事件的APC。所以你应该调用 IoBuildAsynchronousFsdRequest并在某个事件上等待,而这个事件将由你的完成例程去置成信号态。第二种情况不经常出现在设备驱动程序中。

通常情况下,与IoBuildAsynchronousFsdRequest配合使用的完成例程不仅仅是调用 IoFreeIrp。实际上,你需要实现I/O管理器用于清除已完成IRP的内部例程(IopCompleteRequest)。你不能依赖I/O管理器来清除IoBuildAsynchronousFsdRequest创建的IRP。因为在当前版本的Windows 98和Windows 2000中清除操作需要一个APC,并且在任意线程下执行APC是错误的,所以I/O管理器不能为你做清除工作,你必须自己做全部的清除工作。

如果IRP的目标设备对象设置了DO_DIRECT_IO标志,IoBuildAsynchronousFsdRequest将创建一个MDL,这个MDL必须由你自己释放,如下面代码:

NTSTATUS CompletionRoutine(...)
{
  PMDL mdl;
  while ((mdl = Irp->MdlAddress))
  {
    Irp->MdlAddress = mdl->Next;
    IoFreeMdl(mdl);
  }
  ...
  IoFreeIrp(Irp);
  return STATUS_MORE_PROCESSING_REQUIRED;
}
 

如果IRP的目标设备对象设置了DO_BUFFERED_IO标志,IoBuildAsynchronousFsdRequest将分配一个系统缓冲区,但这个缓冲区需要你来释放。如果你正在做一个输入操作,那么在释放这个缓冲区之前你还需要把输入数据从系统缓冲区复制到你自己真正的输入缓冲区。并且要保证这个真正的缓冲区在非分页内存中,因为完成例程需要在DISPATCH_LEVEL级上运行。你还需要确保你得到的是缓冲区的内核地址,因为完成例程运行在任意线程上下文中。如果这些限制还不足以使你在使用IoBuildAsynchronousFsdRequest(用于DO_BUFFERED_IO设备)时感到泄气,那么想一想你还必须在完成例程中检测未公开标志位IRP_BUFFERED_IO、IRP_INPUT_OPERATION、 IRP_DEALLOCATE_BUFFER。我不将给出关于这个函数的代码,因为我曾保证过不在本书中使用未公开的内部技术。

我的建议是,仅在你知道IRP的目标设备不使用DO_BUFFERED_IO标志时,才使用IoBuildAsynchronousFsdRequest。

 

使用IoAllocateIrp
=================

如果你想更深入一些,你可以使用IoAllocateIrp函数创建任何类型的IRP:

PIRP Irp = IoAllocateIrp(StackSize, ChargeQuota);
 
StackSize (CCHAR)是与IRP一同分配的I/O堆栈单元的数目,ChargeQuota(BOOLEAN)指出该内存分配是否应充入进程限额。通常,你可以从该IRP对应的设备对象中获得StackSize参数,ChargeQuota参数指定为FALSE,例如:

PDEVICE_OBJECT DeviceObject;
PIRP Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
 
当你使用IoAllocateIrp时,必须为它创建的IRP安装一个完成例程,并且该完成例程必须返回 STATUS_MORE_PROCESSING_REQUIRED。另外,你还要负责释放该IRP以及任何相关的对象。如果你不打算取消该IRP,你的完成例程应该象这样:

NTSTATUS OnComplete(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)
{
  IoFreeIrp(Irp);
  return STATUS_MORE_PROCESSING_REQUIRED;
}
 
如果IRP的发起线程提前结束,则IoAllocateIrp创建的IRP不能被自动取消。


设备对象指针从哪来?
===================

IoCallDriver函数需要一个PDEVICE_OBJECT作为它的第一个参数。你也许想知道我是从哪得到设备对象的指针的。

一个最明显的获得设备对象指针的方式是调用IoAttachDeviceToDeviceStack函数,这也是每个WDM驱动程序的AddDevice函数应做的一步。在本书的所有例子驱动程序中,你都将看到下面一行代码:

pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);
 
任何时候我们需要沿着设备堆栈向下传送一个IRP时,我们就使用这个设备对象指针。
另一个常用的定位设备对象的方法是使用对象名称:

PUNICODE_STRING DeviceName;   //  something gives you this
PDEVICE_OBJECT DeviceObject;  //  an output from this process
PFILE_OBJECT FileObject;      //  another output
NTSTATUS status = IoGetDeviceObjectPointer(DeviceName, , &FileObject, &DeviceObject);

通过指定设备对象名称和与其相关的文件对象指针你可以得到这个设备对象的指针。文件对象就是文件句柄指向的对象。最后,你还需要解除文件对象参考,如下:

ObDereferenceObject(FileObject); //  DeviceObject now poison!

解除文件对象参考后,你还要释放设备对象的隐含引用。如果你要继续使用该设备对象,应该先做设备对象引用:

ObReferenceObject(DeviceObject);
ObDereferenceObject(FileObject); //  DeviceObject still okay
 
然而,你不应该机械地把前两行代码加入到你的驱动程序中。实际上,当你向地址由IoGetDeviceObjectPointer获得的设备对象发送IRP时,你应该带着文件对象:

PIRP Irp = IoBuildXxxRequest(...);
PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);
stack->FileObject = FileObject;
IoCallDriver(DeviceObject, Irp);
 
这里是这个额外语句的解释。在内部,IoGetDeviceObjectPointer打开设备对象的一个常规句柄,驱动程序应创建与这个文件对象相关联的某些辅助数据结构,处理IRP_MJ_CREATE之后的IRP也许会用到这些结构。处理IRP_MJ_CLOSE时将销毁这些结构。因此,你需要在发往驱动程序的每个IRP的第一个堆栈单元中设置FileObject指针。

你不必总在新IRP中设置文件对象指针,如果你在 IRP_MJ_CREATE的对应例程中获得了文件对象,那么你下面的驱动程序就没有必要查看该文件对象。而在前面描述的情况中,文件对象的所有者是你用 IoGetDeviceObjectPointer函数获得设备对象的驱动程序,在这种情况下,你必须设置文件对象指针。

 

寻址数据缓冲区
==============

当应用程序发起一个读或写操作时,通过给出一个用户模式虚拟地址和长度,应用程序向I/O管理器提供了一个数据缓冲区。正如我在第三章中提到的,内核模式驱动程序几乎从不使用用户模式虚拟地址访问内存,因为你不能把线程上下文确定下来。Windows 2000为驱动程序访问用户模式数据缓冲区提供了三种方法:

在buffered方式中,I/O管理器先创建一个与用户模式数据缓冲区大小相等的系统缓冲区。而你的驱动程序将使用这个系统缓冲区工作。I/O管理器负责在系统缓冲区和用户模式缓冲区之间复制数据。
在direct方式中,I/O管理器锁定了包含用户模式缓冲区的物理内存页,并创建一个称为MDL(内存描述符表)的辅助数据结构来描述锁定页。因此你的驱动程序将使用MDL工作。
在neither方式中,I/O管理器仅简单地把用户模式的虚拟地址传递给你。而使用用户模式地址的驱动程序应十分小心。

指定缓冲方式
============

为了指定设备读写的缓冲方式,你应该在AddDevice函数中,在创建设备对象后,立即设置其中的标志位:

NTSTATUS AddDevice(...)
{
  PDEVICE_OBJECT fdo;
  IoCreateDevice(..., &fdo);
  fdo->Flags |= DO_BUFFERED_IO;
          
  fdo->Flags |= DO_DIRECT_IO;
          
  fdo->Flags |= 0;  // i.e., neither direct nor buffered
}
 
这之后你不能该变缓冲方式的设置,因为过滤器驱动程序将复制这个标志设置,并且,如果你改变了设置,过滤器驱动程序没有办法知道这个改变

Buffered方式
============

当I/O 管理器创建IRP_MJ_READ或IRP_MJ_WRITE请求时,它探测设备的缓冲标志以决定如何描述新IRP中的数据缓冲区。如果 DO_BUFFERED_IO标志设置,I/O管理器将分配与用户缓冲区大小相同的非分页内存。它把缓冲区的地址和长度保存到两个十分不同的地方,见下面代码片段中用粗体字表示的语句。你可以假定I/O管理器执行下面代码(注意这并不是Windows NT的源代码):

PVOID uva;             //  user-mode virtual buffer address
ULONG length;          //  length of user-mode buffer

PVOID sva = ExAllocatePoolWithQuota(NonPagedPoolCacheAligned, length);
if (writing)
  RtlCopyMemory(sva, uva, length);

Irp->AssociatedIrp.SystemBuffer = sva;

PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);
if (reading)
  stack->Parameters.Read.Length = length;
else
  stack->Parameters.Write.Length = length;

if (reading)
  RtlCopyMemory(uva, sva, length);

ExFreePool(sva);

可以看出,系统缓冲区地址被放在IRP的AssocatedIrp.SystemBuffer域中,而数据的长度被放到stack-> Parameters联合中。在这个过程中还包含作为驱动程序开发者不必了解的额外细节。例如,读操作之后的数据复制工作实际发生一个APC期间,在原始线程的上下文中,由一个与构造该IRP完全不同的子例程执行。I/O管理器把用户模式虚拟地址(uva变量)保存到IRP的UserBuffer域中,这样一来复制操作就可以找到这个地址。但你不要使代码依赖这些事实,因为它们有可能会改变。IRP最终完成后,I/O管理器将释放系统缓冲区所占用的内存。

 


Direct方式
==========

如果你在设备对象中指定DO_DIRECT_IO方式,I/O管理器将创建一个MDL用来描述包含该用户模式数据缓冲区的锁定内存页。MDL结构的声明如下:

typedef struct _MDL {
  struct _MDL *Next;
  CSHORT Size;
  CSHORT MdlFlags;
  struct _EPROCESS *Process;
  PVOID MappedSystemVa;
  PVOID StartVa;
  ULONG ByteCount;
  ULONG ByteOffset;
} MDL, *PMDL;
 

图7 -3显示了MDL扮演的角色。StartVa成员给出了用户缓冲区的虚拟地址,这个地址仅在拥有数据缓冲区的用户模式进程上下文中才有效。 ByteOffset是缓冲区起始位置在一个页帧中的偏移值,ByteCount是缓冲区的字节长度。Pages数组没有被正式地声明为MDL结构的一部分,在内存中它跟在MDL的后面,包含用户模式虚拟地址映射为物理页帧的个数。

用于访问MDL的宏和访问函数

   宏或函数                                   描述
-----------------------------------------------------------------------------
IoAllocateMdl                 创建MDL
IoBuildPartialMdl             创建一个已存在MDL的子MDL
IoFreeMdl                     销毁MDL
MmBuildMdlForNonPagedPool     修改MDL以描述内核模式中一个非分页内存区域
MmGetMdlByteCount             取缓冲区字节大小
MmGetMdlByteOffset            取缓冲区在第一个内存页中的偏移
MmGetMdlVirtualAddress        取虚拟地址
MmGetSystemAddressForMdl      创建映射到同一内存位置的内核模式虚拟地址
MmGetSystemAddressForMdlSafe  与MmGetSystemAddressForMdl相同,但Windows 2000首选
MmInitializeMdl               (再)初始化MDL以描述一个给定的虚拟缓冲区
MmPrepareMdlForReuse          再初始化MDL
MmProbeAndLockPages           地址有效性校验后锁定内存页
MmSizeOfMdl                   取为描述一个给定的虚拟缓冲区的MDL所占用的内存大小
MmUnlockPages                 为该MDL解锁内存页
--------------------------------------------------------------------------------

对于I/O管理器执行的Direct方式的读写操作,其过程可以想象为下面代码:

KPROCESSOR_MODE mode;      //  either KernelMode or UserMode
PMDL mdl = IoAllocateMdl(uva, length, FALSE, TRUE, Irp);
MmProbeAndLockPages(mdl, mode, reading ? IoWriteAccess : IoReadAccess);

MmUnlockPages(mdl);
ExFreePool(mdl);
 

I/O 管理器首先创建一个描述用户缓冲区的MDL。IoAllocateMdl的第三个参数(FALSE)指出这是一个主数据缓冲区。第四个参数(TRUE)指出内存管理器应把该内存充入进程配额。最后一个参数(Irp)指定该MDL应附着的IRP。在内部,IoAllocateMdl把Irp-> MdlAddress设置为新创建MDL的地址,以后你将用到这个成员,并且I/O管理器最后也使用该成员来清除MDL。

这段代码的关键地方是调用MmProbeAndLockPages。该函数校验那个数据缓冲区是否有效,是否可以按适当模式访问。如果我们向设备写数据,我们必须能读缓冲区。如果我们从设备读数据,我们必须能写缓冲区。另外,该函数锁定了包含数据缓冲区的物理内存页,并在MDL的后面填写了页号数组。在效果上,一个锁定的内存页将成为非分页内存池的一部分,直到所有对该页内存加锁的调用者都对其解了锁。

在Direct方式的读写操作中,对MDL你最可能做的事是把它作为参数传递给其它函数。例如,DMA传输的MapTransfer步骤需要一个MDL。另外,在内部,USB读写操作总使用MDL。所以你应该把读写操作设置为DO_DIRECT_IO方式,并把结果MDL传递给USB总线驱动程序。

顺便提一下,I/O管理器确实在stack->Parameters联合中保存了读写请求的长度,但驱动程序应该直接从MDL中获得请求数据的长度。

ULONG length = MmGetMdlByteCount(mdl);


内核同步对象
============

Windows NT提供了五种内核同步对象(Kernel Dispatcher Object),你可以用它们控制非任意线程(普通线程)的流程。表4-1列出了这些内核同步对象的类型及它们的用途。在任何时刻,任何对象都处于两种状态中的一种:信号态或非信号态。有时,当代码运行在某个线程的上下文中时,它可以阻塞这个线程的执行,调用KeWaitForSingleObject或 KeWaitForMultipleObjects函数可以使代码(以及背景线程)在一个或多个同步对象上等待,等待它们进入信号态。内核为初始化和控制这些对象的状态提供了例程。

表4-1. 内核同步对象

对象                   数据类型                 描述
------------------------------------------------------------------------------
Event(事件)           KEVENT            阻塞一个线程直到其它线程检测到某事件发生 
Semaphore(信号灯)     KSEMAPHORE        与事件对象相似,但可以满足任意数量的等待 
Mutex(互斥)           KMUTEX            执行到关键代码段时,禁止其它线程执行该代码段 
Timer(定时器)         KTIMER            推迟线程执行一段时期
Thread(线程)          KTHREAD           阻塞一个线程直到另一个线程结束
--------------------------------------------------------------------------------

 

何时阻塞和怎样阻塞一个线程
==========================

为了理解WDM驱动程序何时以及如何利用内核同步对象阻塞一个线程,你必须先对线程有一些基本了解。通常,如果在线程执行时发生了软件或硬件中断,那么在内核处理中断期间,该线程仍然是"当前"线程。而内核模式代码执行时所在的上下文环境就是指这个"当前"线程的上下文。为了响应各种中断,Windows NT调度器可能会切换线程,这样,另一个线程将成为新的"当前"线程。

术语"任意线程上下文(arbitrary thread context)"和"非任意线程上下文(nonarbitrary thread context)"用于精确描述驱动程序例程执行时所处于的上下文种类。如果我们知道程序正处于初始化I/O请求线程的上下文中,则该上下文不是任意上下文。然而,在大部分时间里,WDM驱动程序无法知道这个事实,因为控制哪个线程应该激活的机会通常都是在中断发生时。当应用程序发出I/O请求时,将产生一个从用户模式到内核模式的转换,而创建并发送该IRP的I/O管理器例程将继续运行在非任意线程的上下文中。我们用术语"最高级驱动程序"来描述第一个收到该IRP的驱动程序。

通常,只有给定设备的最高级驱动程序才能确切地知道它执行在一个非任意线程的上下文中。这是因为驱动程序派遣例程通常把请求放入队列后立即返回调用者。之后通过回调函数,请求被提出队列并下传到低级驱动程序。一旦派遣例程挂起某个请求,所有对该请求的后期处理必须发生在任意线程上下文中。

解释完线程上下文后,我们可以陈诉出关于线程阻塞的简单规则:

当我们处理某个请求时,仅能阻塞产生该请求的线程。

 


中断请求级
==========

Windows NT为每个硬件中断和少数软件事件赋予了一个优先级,即中断请求级(interrupt request level - IRQL)。IRQL为CPU上的活动提供了同步方法,它基于下面规则:

一旦某CPU执行在高于PASSIVE_LEVEL的IRQL上时,该CPU上的活动仅能被拥有更高IRQL的活动抢先。

图4 -1显示了x86平台上的IRQL值范围。(通常,这个IRQL数值要取决于你所面对的平台) 用户模式程序执行在PASSIVE_LEVEL上,可以被任何执行在高于该IRQL上的活动抢先。许多设备驱动程序例程也执行在 PASSIVE_LEVEL上。第二章中讨论的DriverEntry和AddDevice例程就属于这类,大部分IRP派遣例程也属于这类。

某些公共驱动程序例程执行在DISPATCH_LEVEL上,而DISPATCH_LEVEL级要比PASSIVE_LEVEL级高。这些公共例程包括 StartIo例程,DPC(推迟过程调用)例程,和其它一些例程。这些例程的共同特点是,它们都需要访问设备对象和设备扩展中的某些域,它们都不受派遣例程的干扰或互相干扰。当任何一个这样的例程运行时,上面陈述的规则可以保证它们不被任何驱动程序的派遣例程抢先,因为派遣例程本身执行在更低级的 IRQL上。另外,它们也不会被同类例程抢先,因为那些例程运行的IRQL与它们自己的相同。只有拥有更高IRQL的活动才能抢先它们。

注意

派遣例程(Dispatch routine)和DISPATCH_LEVEL级名称类似。之所以称做派遣例程是因为I/O管理器向这些函数派遣I/O请求。而存在派遣级 (DISPATCH_LEVEL)这个名称是因为内核线程派遣器运行在这个IRQL上,它决定下一次该执行哪个线程。(现在,线程调度程序通常运行在 SYNCH_LEVEL级上)

----------------
HIGH_LEVEL    31
POWER_LEVEL   30
IPI_LEVEL     29
CLOCK2_LEVEL  28
CLOCK1_LEVEL  28
PROFILE_LEVEL 27
    ...
DISPATCH_LEVEL 2
APC_LEVEL      1
PASSIVE_LEVEL  0
----------------

在DISPATCH_LEVEL 级和PROFILE_LEVEL级之间是各种硬件中断级。通常,每个有中断能力的设备都有一个IRQL,它定义了该设备的中断优先级别。WDM驱动程序只有在收到一个副功能码为IRP_MN_START_DEVICE的IRP_MJ_PNP请求后,才能确定其设备的IRQL。设备的配置信息作为参数传递给该请求,而设备的IRQL就包含在这个配置信息中。我们通常把设备的中断级称为设备IRQL,或DIRQL。

其它IRQL级的含义有时需要依靠具体的CPU结构。这些IRQL通常仅被Windows NT内核内部使用,因此它们的含义与设备驱动程序的编写不是特别密切相关。例如,我将要在本章后面详细讨论的APC_LEVEL,当系统在该级上为某线程调度APC(异步过程调用)例程时不会被同一CPU上的其它线程所干扰。在HIGH_LEVEL级上系统可以执行一些特殊操作,如系统休眠前的内存快照、处理bug check、处理假中断,等等。

IRQL的变化
==========

为了演示IRQL的重要性,参见图 4-2,该图显示了发生在单CPU上的一系列事件。在时间序列的开始处,CPU执行在PASSIVE_LEVEL级上。在t1时刻,一个中断到达,它的服务例程执行在DIRQL1上,该级是在DISPATCH_LEVEL和PROFILE_LEVEL之间的某个DIRQL。在t2时刻,另一个中断到达,它的服务例程执行在DIRQL2上,比DIRQL1低一级。我们讨论过抢先规则,所以CPU将继续服务于第一个中断。当第一个中断服务例程在t3时刻完成时,该中断服务程序可能会请求一个DPC。而DPC例程是执行在DISPATCH_LEVEL上。所以当前存在的未执行的最高优先级的活动就是第二个中断的服务例程,所以系统接着执行第二个中断的服务例程。这个例程在t4时刻结束,假设这之后再没有其它中断发生,CPU将降到DISPATCH_LEVEL 级上执行第一个中断的DPC例程。当DPC例程在t5时刻完成后,IRQL又落回到原来的PASSIVE_LEVEL级。

基本同步规则
============

遵循下面规则,你可以利用IRQL的同步效果:

所有对共享数据的访问都应该在同一(提升的)IRQL上进行。

换句话说,不论何时何地,如果你的代码访问的数据对象被其它代码共享,那么你应该使你的代码执行在高于PASSIVE_LEVEL的级上。一旦越过 PASSIVE_LEVEL级,操作系统将不允许同IRQL的活动相互抢先,从而防止了潜在的冲突。然而这个规则不足以保护多处理器机器上的数据,在多处理器机器中你还需要另外的防护措施--自旋锁(spin lock)。如果你仅关心单CPU上的操作,那么使用IRQL就可以解决所有同步问题。但事实上,所有WDM驱动程序都必须设计成能够运行在多处理器的系统上。

IRQL与线程优先级
================

线程优先级是与IRQL非常不同的概念。线程优先级控制着线程调度器的调度动作,决定何时抢先运行线程以及下一次运行什么线程。然而,当IRQL级高于或等于DISPATCH_LEVEL级时线程切换停止,无论当前活动的是什么线程都将保持活动状态直到IRQL降到DISPATCH_LEVEL级之下。而此时的"优先级"仅指IRQL本身,由它控制到底哪个活动该执行,而不是该切换到哪个线程的上下文。


IRQL和分页
==========

执行在提升的IRQL级上的一个后果是,系统将不能处理页故障(系统在APC级处理页故障)。这意味着:

执行在高于或等于DISPATCH_LEVEL级上的代码绝对不能造成页故障。

这也意味着执行在高于或等于DISPATCH_LEVEL级上的代码必须存在于非分页内存中。此外,所有这些代码要访问的数据也必须存在于非分页内存中。最后,随着IRQL的提升,你能使用的内核模式支持例程将会越来越少。

DDK文档中明确指出支持例程的IRQL限定。例如,KeWaitForSingleObject例程有两个限定:

调用者必须运行在低于或等于DISPATCH_LEVEL级上。
如果调用中指定了非0的超时,那么调用者必须严格地运行在低于DISPATCH_LEVEL的IRQL上。
上面这两行想要说明的是:如果KeWaitForSingleObject真的被阻塞了指定长的时间(你指定的非0超时),那么你必定运行在低于 DISPATCH_LEVEL的IRQL上,因为只有在这样的IRQL上线程阻塞才是允许的。如果你所做的一切就是为了检测事件是否进入信号态,则可以执行在DISPATCH_LEVEL级上。但你不能在ISR或其它运行在高于DISPATCH_LEVEL级上的例程中调用 KeWaitForSingleObject例程。

 

内核同步对象
============

Windows NT提供了五种内核同步对象(Kernel Dispatcher Object),你可以用它们控制非任意线程(普通线程)的流程。表4-1列出了这些内核同步对象的类型及它们的用途。在任何时刻,任何对象都处于两种状态中的一种:信号态或非信号态。有时,当代码运行在某个线程的上下文中时,它可以阻塞这个线程的执行,调用KeWaitForSingleObject或 KeWaitForMultipleObjects函数可以使代码(以及背景线程)在一个或多个同步对象上等待,等待它们进入信号态。内核为初始化和控制这些对象的状态提供了例程。

表4-1. 内核同步对象
-----------------------------------------------------------------------------
对象               数据类型                       描述
-----------------------------------------------------------------------------
Event(事件)        KEVENT         阻塞一个线程直到其它线程检测到某事件发生 
Semaphore(信号灯)  KSEMAPHORE     与事件对象相似,但可以满足任意数量的等待 
Mutex(互斥)        KMUTEX         执行到关键代码段时,禁止其它线程执行该代码段 
Timer(定时器)      KTIMER         推迟线程执行一段时期
Thread(线程)       KTHREAD        阻塞一个线程直到另一个线程结束
------------------------------------------------------------------------------
在下几段中,我将描述如何使用内核同步对象。我将从何时可以调用等待原语阻塞线程开始讲起,然后讨论用于每种对象的支持例程。最后讨论与线程警惕(thread alert)和提交APC(异步过程调用)相关的概念。


何时阻塞和怎样阻塞一个线程
==========================

为了理解WDM驱动程序何时以及如何利用内核同步对象阻塞一个线程,你必须先对线程有一些基本了解。通常,如果在线程执行时发生了软件或硬件中断,那么在内核处理中断期间,该线程仍然是"当前"线程。而内核模式代码执行时所在的上下文环境就是指这个"当前"线程的上下文。为了响应各种中断,Windows NT调度器可能会切换线程,这样,另一个线程将成为新的"当前"线程。

术语"任意线程上下文(arbitrary thread context)"和"非任意线程上下文(nonarbitrary thread context)"用于精确描述驱动程序例程执行时所处于的上下文种类。如果我们知道程序正处于初始化I/O请求线程的上下文中,则该上下文不是任意上下文。然而,在大部分时间里,WDM驱动程序无法知道这个事实,因为控制哪个线程应该激活的机会通常都是在中断发生时。当应用程序发出I/O请求时,将产生一个从用户模式到内核模式的转换,而创建并发送该IRP的I/O管理器例程将继续运行在非任意线程的上下文中。我们用术语"最高级驱动程序"来描述第一个收到该IRP的驱动程序。

通常,只有给定设备的最高级驱动程序才能确切地知道它执行在一个非任意线程的上下文中。这是因为驱动程序派遣例程通常把请求放入队列后立即返回调用者。之后通过回调函数,请求被提出队列并下传到低级驱动程序。一旦派遣例程挂起某个请求,所有对该请求的后期处理必须发生在任意线程上下文中。

解释完线程上下文后,我们可以陈诉出关于线程阻塞的简单规则:

当我处理某个请求时,仅能阻塞产生该请求的线程。

通常,仅有设备的最高级驱动程序才能应用这个规则。但有一个重要的例外,IRP_MN_START_DEVICE请求,所有驱动程序都以同步方式处理这个请求。即驱动程序不排队或挂起该类请求。当你收到这种请求时,你可以直接从堆栈中找到请求的发起者。正如我在第六章中讲到的,处理这种请求时你必须阻塞那个线程。

下面规则表明在提升的IRQL级上不可能发生线程切换:

执行在高于或等于DISPATCH_LEVEL级上的代码不能阻塞线程。

这个规则表明你只能在DriverEntry函数、AddDevice函数,或驱动程序的派遣函数中阻塞当前线程。因为这些函数都执行在 PASSIVE_LEVEL级上。没有必要在DriverEntry或AddDevice函数中阻塞当前线程,因为这些函数的工作仅仅是初始化一些数据结构。

在单同步对象上等待
==================

你可以按下面方法调用KeWaitForSingleObject函数:

ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
LARGE_INTEGER timeout;
NTSTATUS status = KeWaitForSingleObject(object, WaitReason, WaitMode, Alertable, &timeout);
 
ASSERT语句指出必须在低于或等于DISPATCH_LEVEL级上调用该例程。

在这个调用中,object指向你要等待的对象。注意该参数的类型是PVOID,它应该指向一个表4-1中列出的同步对象。该对象必须在非分页内存中,例如,在设备扩展中或其它从非分页内存池中分配的数据区。在大部分情况下,执行堆栈可以被认为是非分页的。

WaitReason 是一个纯粹建议性的值,它是KWAIT_REASON枚举类型。实际上,除非你指定了WrQueue参数,否则任何内核代码都不关心此值。线程阻塞的原因被保存到一个不透明的数据结构中,如果你了解这个数据结构,那么在调试某种死锁时,你也许会从这个原因代码中获得一些线索。通常,驱动程序应把该参数指定为Executive,代表无原因。

WaitMode是MODE枚举类型,该枚举类型仅有两个值:KernelMode和UserMode。

Alertable 是一个布尔类型的值。它不同于WaitReason,这个参数以另一种方式影响系统行为,它决定等待是否可以提前终止以提交一个APC。如果等待发生在用户模式中,那么内存管理器就可以把线程的内核模式堆栈换出。如果驱动程序以自动变量(在堆栈中)形式创建事件对象,并且某个线程又在提升的IRQL级上调用了KeSetEvent,而此时该事件对象刚好又被换出内存,结果将产生一个bug check。所以我们应该总把alertable参数指定为FALSE,即在内核模式中等待。

最后一个参数&timeout是一个 64位超时值的地址,单位为100纳秒。正数的超时表示一个从1601年1月1日起的绝对时间。调用KeQuerySystemTime函数可以获得当前系统时间。负数代表相对于当前时间的时间间隔。如果你指定了绝对超时,那么系统时钟的改变也将影响到你的超时时间。如果系统时间越过你指定的绝对时间,那么永远都不会超时。相反,如果你指定相对超时,那么你经过的超时时间将不受系统时钟改变的影响。

指定0超时将使 KeWaitForSingleObject函数立即返回,返回的状态代码指出对象是否处于信号态。如果你的代码执行在DISPATCH_LEVEL级上,则必须指定0超时,因为在这个IRQL上不允许阻塞。每个内核同步对象都提供一组KeReadStateXxx服务函数,使用这些函数可以直接获得对象的状态。然而,取对象状态与0超时等待不完全等价:当KeWaitForSingleObject发现等待被满足后,它执行特殊对象要求的附加动作。相比之下,取对象状态不执行任何附加动作,即使对象已经处于信号态。

超时参数也可以指定为NULL指针,这代表无限期等待。

该函数的返回值指出几种可能的结果。STATUS_SUCCESS结果是你所希望的,表示等待被满足。即在你调用 KeWaitForSingleObject时,对象或者已经进入信号态,或者后来进入信号态。如果等待以第二种情况满足,则有必要在同步对象上执行附加动作。当然,这个附加动作还要参考对象的类型,我将在后面讨论具体对象类型时再解释这一点。(例如,一个同步类型的事件在你的等待满足后需要重置该事件)

返回值STATUS_TIMEOUT指出在指定的超时期限内对象未进入信号态。如果指定0超时,则函数将立即返回。返回代码为 STATUS_TIMEOUT,代表对象处于非信号态,返回代码为STATUS_SUCCESS,代表对象处于信号态。如果指定NULL超时,则不可能有返回值。

其它两个返回值STATUS_ALERTED和STATUS_USER_APC表示等待提前终止,对象未进入信号态。原因是线程接收到一个警惕(alert)或一个用户模式的APC

在多同步对象上等待
==================

KeWaitForMultipleObjects函数用于同时等待一个或多个同步对象。该函数调用方式如下:

ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
LARGE_INTEGER timeout;
NTSTATUS status = KeWaitForMultipleObjects(count,
        objects,
        WaitType,
        WaitReason,
        WaitMode,
        Alertable,
        &timeout,
        waitblocks);
 

在这里,objects指向一个指针数组,每个数组元素指向一个同步对象,count是数组中指针的个数。count必须小于或等于 MAXIMUM_WAIT_OBJECTS值(当前为64)。这个数组和它所指向的所有对象都必须在非分页内存中。WaitType是枚举类型,其值可以为WaitAll或WaitAny,它指出你是等到所有对象都进入信号态,还是只要有一个对象进入信号态就可以。

waitblocks参数指向一个KWAIT_BLOCK结构数组,内核用这个结构数组管理等待操作。你不需要初始化这些结构,内核仅需要知道这个结构数组在哪里,内核用它来记录每个对象在等待中的状态。如果你仅需要等待小数量的对象(不超过THREAD_WAIT_OBJECTS,该值当前为3),你可以把该参数指定为 NULL。如果该参数为NULL,KeWaitForMultipleObjects将使用线程对象中预分配的等待块数组。如果你等待的对象数超过 THREAD_WAIT_OBJECTS,你必须提供一块长度至少为count * sizeof(KWAIT_BLOCK)的非分页内存。

其余参数与KeWaitForSingleObject中的对应参数作用相同,而且大部分返回码也有相同的含义。

如果你指定了WaitAll,则返回值STATUS_SUCCESS表示等待的所有对象都进入了信号态。如果你指定了WaitAny,则返回值在数值上等于进入信号态的对象在objects数组中的索引。如果碰巧有多个对象进入了信号态,则该值仅代表其中的一个,可能是第一个也可能是其它。你可以认为该值等于STATUS_WAIT_0加上数组索引。你可以先用NT_SUCCESS测试返回码,然后再从其中提取数组索引:

NTSTATUS status = KeWaitForMultipleObjects(...);
if (NT_SUCCESS(status))
{
  ULONG iSignalled = (ULONG) status - (ULONG) STATUS_WAIT_0;
  ...
}
 

如果KeWaitForMultipleObjects返回成功代码,它也将执行等待被满足的那个对象的附加动作。如果多个对象同时进入信号态而你指定的WaitType参数为WaitAny,那么该函数仅执行返回值指定对象的附加动作。

内核事件
========

表4-2列出了用于处理内核事件的服务函数。为了初始化一个事件对象,我们首先应该为其分配非分页存储,然后调用KeInitializeEvent:

ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
KeInitializeEvent(event, EventType, initialstate);
 
event 是事件对象的地址。EventType是一个枚举值,可以为NotificationEvent或SynchronizationEvent。通知事件 (notification event)有这样的特性,当它进入信号态后,它将一直处于信号态直到你明确地把它重置为非信号态。此外,当通知事件进入信号态后,所有在该事件上等待的线程都被释放。这与用户模式中的手动重置事件相似。而对于同步事件(synchronization event),只要有一个线程被释放,该事件就被重置为非信号态。这又与用户模式中的自动重置事件相同。而KeWaitXxx函数在同步事件对象上执行的附加动作就是把它重置为非信号态。最后的参数initialstate是布尔量,为TRUE表示事件的初始状态为信号态,为FALSE表示事件的初始状态为非信号态。

表4-2. 用于内核事件对象的服务函数
----------------------------------------------------------------
服务函数                        描述
----------------------------------------------------------------
KeClearEvent       把事件设置为非信号态,不报告以前的状态
KeInitializeEvent  初始化事件对象
KeReadStateEvent   取事件的当前状态
KeResetEvent       把事件设置为非信号态,返回以前的状态
KeSetEvent         把事件设置为信号态,返回以前的状态
----------------------------------------------------------------
调用KeSetEvent函数可以把事件置为信号态:

ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
LONG wassignalled = KeSetEvent(event, boost, wait);
 

在上面代码中,ASSERT语句强制你必须在低于或等于DISPATCH_LEVEL级上调用该函数。event参数指向一个事件对象,boost值用于提升等待线程的优先级。wait参数的解释见文字框"KeSetEvent的第三个参数",WDM驱动程序几乎从不把wait参数指定为TRUE。如果该事件已经处于信号态,则该函数返回非0值。如果该事件处于非信号态,则该函数返回0。

多任务调度器需要人为地提升等I/O操作或同步对象的线程的优先级,以避免饿死长时间等待的线程。这是因为被阻塞的线程往往是放弃自己的时间片并且不再要求获得CPU,但只要这些线程获得了比其它线程更高的优先级,或者其它同一优先级的线程用完了自己的时间片,它们就可以恢复执行。注意,正处于自己时间片中的线程不能被阻塞。

用于提升阻塞线程优先级的boost值不太好选择。一个较好的笨方法是指定IO_NO_INCREMENT值,当然,如果你有更好的值,可以不用这个值。如果事件唤醒的是一个处理时间敏感数据流的线程(如声卡驱动程序),那么应该使用适合那种设备的boost值(如IO_SOUND_INCREMENT)。重要的是,不要为一个愚蠢的理由去提高等待者的优先级。例如,如果你要同步处理一个IRP_MJ_PNP请求,那么在你要停下来等待低级驱动程序处理完该IRP时,你的完成例程应调用KeSetEvent。由于PnP请求对于处理器没有特殊要求并且也不经常发生,所以即使是声卡驱动程序也也应该把boost参数指定为 IO_NO_INCREMENT。


使用完成例程
============


通常,你需要知道发往低级驱动程序的I/O请求的结果。为了了解请求的结果,你需要安装一个完成例程,调用IoSetCompletionRoutine函数:

IoSetCompletionRoutine(Irp,
         CompletionRoutine,
         context,
         InvokeOnSuccess,
         InvokeOnError,
         InvokeOnCancel);
 

Irp就是你要了解其完成的请求。CompletionRoutine是被调用的完成例程的地址,context是任何一个指针长度的值,将作为完成例程的参数。InvokeOnXxx参数是布尔值,它们指出在三种不同的环境中是否需要调用完成例程:

InvokeOnSuccess 你希望完成例程在IRP以成功状态(返回的状态代码通过了NT_SUCCESS测试)完成时被调用。
InvokeOnError 你希望完成例程在IRP以失败状态(返回的状态代码未通过了NT_SUCCESS测试)完成时被调用。
InvokeOnCancel 如果驱动程序在完成IRP前调用了IoCancelIrp例程,你希望在此时调用完成例程。IoCancelIrp将在IRP中设置取消标志,该标志也是调用完成例程的条件。一个被取消的IRP最终将以STATUS_CANCELLED(该状态代码不能通过NT_SUCCESS测试)或任何其它状态完成。如果IRP以失败方式完成,并且你也指定了InvokeOnError参数,那么是InvokeOnError本身导致了完成例程的调用。相反,如果 IRP以成功方式完成,并且你也指定了InvokeOnSuccess参数,那么是InvokeOnSuccess本身导致了完成例程的调用。在这两种情况中,InvokeOnCancel参数将是多余的。如果你省去InvokeOnSuccess和InvokeOnError中的任何一个参数或两个都省去,并且IRP也被设置了取消标志,那么InvokeOnCancel参数将导致完成例程的调用。
这三个标志中至少有一个设置为TRUE。注意,IoSetCompletionRoutine是一个宏,所以你应避免使用有副作用的参数。这三个标志参数和一个函数指针参数在宏中被引用了两次。