荔园在线
荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀
[回到开始]
[上一篇][下一篇]
发信人: zzt (少年仲永), 信区: Hacker
标 题: Windows NT驱动程序基础
发信站: BBS 荔园晨风站 (Wed Apr 7 17:12:15 1999), 转信
Windows NT驱动程序基础
关于Windows NT设备驱动程序编程的绝大多数内容可以参考DDK的帮助,下面说说我在做NT
设备驱动程
序的过程中遇到的一些问题和解决方法。
DDK的安装
正确安装DDK以及到后面的编译,需要首先安装WIN32 SDK,但如果并不需要编译而只想安装
DDK,可以用下面修改注册表的方法绕过WIN32 SDK直接安装DDK。 首先在创建如下子项
\HKEY_CURRENT_USER\Software\Microsoft\Win32SDK\Directories\
然后在该子项下添加如下键值
Install Dir="X:\Win32Directory"
X:\Win32Directory可以是你任意指定的目录。
如果想正确顺利地编译出设备驱动程序,系统上应该 安装WIN32 SDK,DDK和一个C语言编译
器,我安装的是 Visual C++ 5.0。
设备驱动程序的编译。
安装DDK后,在DDK程序组下有check和Free两个编译环境, check环境是编译带调试信息的
驱动程序的,Free则是编 译正式发布版本的环境。 通常情况下设备驱动程序的编译采用命
令行的方式。通过 一定的设置可以在VC++的集成环境下编译,关于如何设置
我将在另一篇单独的文章中讲述。 一般来说,成功编译一个最基本的设备驱动程序需要四
个 文件,第一个当然是你的驱动程序C语言源程序文件(例如
vdisk.c,注意下面所有的例子都是以vdisk来说明)。第二个是RC文件(例如vdisk.rc)。第
三个是sources文件。第四个文件是makefile。RC文件很简单,它主要用来确定一些驱动程
序的信息。makefile很简单,只有一行。sources文件和make文件类似,用来指定需要编译
的文件以及需要连?
拥目馕募U馊龈ㄖ募己芗虻ィ贒DK samples的每个例程里都有三个这样的文件,
依样画瓢就能理解它们的结构和意义。
下面分别举一例
/*vdisk.rc*/
#include
#include
#define VER_FILETYPE VFT_DRV
#define VER_FILESUBTYPE VFT2_DRV_SYSTEM
#define VER_FILEDESCRIPTION_STR "SCSI VDisk Driver"
#define VER_INTERNALNAME_STR "vdisk.sys"
#define VER_ORIGINALFILENAME_STR "vdisk.sys"
#include "common.ver"
/*end of vdisk.rc*/
设备驱动程序一般都使用BUILD实用程序来进行,BUILD只是
NMAKE外面的一个外包装程序。BUILD本身其实相当简单,编
译的大部分工作实际上由BUILD传递给NMAKE来进行。
/*SOURCES*/
TARGETNAME=vdisk
TARGETTYPE=DRIVER
TARGETPATH=$(BASEDIR)\lib
TARGETLIBS=$(BASEDIR)\lib\*\$(DDKBUILDENV)\scsiport.lib
INCLUDES=..\..\inc
SOURCES=vdisk.c vdisk.rc
/*end of SOURCES*/
注意SOURCES的文件名就是SOURCES(没有任何扩展名)。关于
sources文件的详细语法,可以参考DDK帮助。
# makefile
#
# DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source
# file to this component. This file merely indirects to the real make file
# that is shared by all the driver components of the Windows NT DDK
#
!INCLUDE $(NTMAKEENV)\makefile.def
# end of makefile
注意对所有驱动程序的makefile都是一样的,Microsoft也警
告不要编辑这个文件,如果需要,可以编辑修改sources文件
达到同样的效果。
对于设备驱动程序,所使用的C编译器基本上无一例外地选用
Microsoft的Visual C++。
编译的基本步骤是:
1、首先进入check或free编译环境。初始化DDK编译环境。
2、运行VC安装目录下bin目录下的vcvars32.bat。初始化VC++编译环境。
3、运行build.exe进行编译。关于build的参数可见它的帮助。
注意上面的第二步在DDK文档中没有说明,是我在实际过程中
尝试出来的。
设备驱动程序的安装和启动。
Windows NT在引导的时候,通过扫描注册表构造驱动程序列
表。这个列表既包括自启动的程序程序,也包括需要手工启
动的驱动程序。这个列表其实就是控制面板中设备applet所
列出来的所有设备。
所有的设备驱动程序应该在注册表的HKEY_LOCAL_MACHINE\
System\CurrentControlSet\Services\下有相应的键值。
下面以vdisk为例来说明如何添加键值。
首先在HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\
下添加一个子项vdisk,注意这里的名称应该和你的驱动程序
名称一致,例如我的驱动程序名称是vdisk.sys,那么这里的
子项名称就是vdisk。
然后在vdisk下添加以下键值。
名称 数据类型 说明
Type REG_DWORD 这是什么种类的驱动程序*
1 ?内核模式驱动程序
2 ?文件系统驱动程序
Start REG_DWORD 在系统启动过程中的何时启动本驱动程序(见下面)*
ErrorControl REG_DWORD 如果驱动程序装入失败,系统如何响应*
0 ?日志记录错误并忽略
1 ?日志记录错误并显示一个对话框
2 ?日志记录错误,并用最后已知的好配置重新启动系统
3 ?日志记录错误,如果已经使用了最后已知的好配置,失败。
Group REG_SZ 驱动程序的组名(见下面)
DependOnGroup REG_MULTI_SZ 本驱动程序所依赖的其他驱动程序(见下面)
Tag REG_BINARY 同组内的驱动程序的装入顺序(见下面)
Parameters (key) 本驱动程序特定的参数键
注意打*的项是必需的。
控制驱动程序的装入次序
有时候控制多个驱动程序的装入次序是必要的,例如我们
开发的光盘塔驱动程序一共有三个驱动程序,分别是
jbChanger.sys,changerDisk.sys和vdisk.sys。jbChanger
和changerDisk是两个SCSI类驱动程序,它们都依赖SCSI小
端口(mini port驱动程序),同时changerDisk必须在
jbChanger启动之后启动。Vdisk是虚拟的磁盘驱动程序,
它必须在jbChanger和changerDisk都启动之后才能启动成功。
驱动程序的Start值
上面注册表中驱动程序的Start值控制驱动程序在系统启动
的何时启动。目前,Start可以取以下值。
(1) 0x0 (SERVICE_BOOT_START)。这个值指定本驱动程序应
该由操作系统装入程序启动。一般的驱动程序不会采用本值,
因为系统在这个时候几乎还没有启动,大部分系统尚不可用。
(2) 0x1 (SERVICE_SYSTEM_START)。该值表示在操作系统装
入后但同时初始化它自己时启动驱动程序。
(3) 0x2 (SERVICE_AUTO_START)。该值表示在整个系统启动
并运行后由服务控制管理器装入。
(4) 0x3 (SERVICE_DEMAND_START)。该值表示该驱动程序必
须手工启动。可以通过控制面板的设备applet或者使用WIN32
API编程来启动。
(5) 0x4 (SERVICE_DISABLED)。表示本驱动程序被禁用。
这里说的Start值其实就是和控制面板的设备applet的启动设
置是一一对应的,如图所示。
注意我们在调试驱动程序的时候,最好将Start值设置为3来
手工启动,这是因为如果设置为自动启动,而驱动程序在启
动的过程中又发生了异常错误的话,可能导致系统不能启动。
在我的实际过程中就出现过这样的问题,如果没有紧急恢复
盘,首先可以尝试在启动的时候选择用已知的上一次好的配
置来启动系统,看是否能启动成功。如果失败(我当时就失败
了),可以用DOS启动后到\%SystemRoot%\System32\Drivers
目录下将出现问题的驱动程序删除,然后系统就可以启动了。
不过如果NT安装在NTFS分区,DOS启动后将看不到这个分区,
这样就必须将硬盘挂到另一NT系统上来删除这个文件了。
建立驱动程序间的依赖关系
通过设置Start可以控制驱动程序在不同的时候启动。但如果
要解决上面说到依赖性问题,则需要使用Group和DependOnGroup值。
首先要确定自己的驱动程序使用的Group名,系统有一些定义
好的组名,对于当前系统存在的组名,可以观察注册表的
\HKEY_LOCAL_MACHINE\ System\ CurrentControlSet\
Control\ ServiceGroupOrder\ List的键值。例如我的机器
上目前该值为:
…
SCSI miniport
port
Primary disk
SCSI class
SCSI CDROM class
filter
boot file system
…
这里每一行都是一个Group名,一般来说某个驱动程序都属
于某一个Group。系统启动时安装该List下组的顺序依次启
动各组里的驱动程序。例如jbChanger和changerDisk都属于
SCSI Class组。如果你觉得该表中的组名都不合适,可以在
该List的适当位置中添加新的组名。
DependOnGroup值控制本驱动程序启动的时候必须先启动另
一组的驱动程序,例如jbChanger和changerDisk的启动就依
赖于SCSI miniport组。因此jbChanger和changerDisk的
DependOnGroup值都为SCSI miniport。
控制组内的装入次序
有时候同一组内的驱动程序也必须按一定的先后次序启动,
例如jbChanger 和changerDisk 都属于SCSI Class组,但
changerDisk必须在jbChanger之后启动。这样就要使用驱
动程序的Tag键值。关于如何设置Tag值,请参考《Windows
NT 设备驱动程序指南》P330或DDK帮助。
添加修改注册表的方法
在注册表里添加这些值可以手工修改,也可以自己编程利
用WIN32 API进行添加,同时也可以用ini文件的方式来添
加。下面是一个ini(文件名为vdisk.ini)文件的例子。
\Registry\Machine\System\CurrentControlSet\Services\VDisk
Type = REG_DWORD 0x00000001
Start = REG_DWORD 0x00000003
ErrorControl = REG_DWORD 0x00000001
Group = SCSI Class
Parameters
DriveLetter = N:
然后以vdisk.ini为参数运行REGINI.EXE。就会自动在注册
表里添加相应的项。
在注册表里添加好这些项后,必须重新启动系统,这样所
添加的设备驱动程序才能在控制面板的设备applet中列出
来,再可以进行其他操作。
启动设备驱动程序
在添加修改好注册表后,重新启动系统,如果上面选择的
Start值是0,1,2,如果一切正常,驱动程序就应该已经启
动起来了。可以观察控制面板的设备applet中的设备列表。
如果Start选择是3,则可以在这里启动它。
调试工具
目前NT驱动程序的调试工具只有WINDBG和SOFTICE,WINDBG
的使用需要双机环境,我没有用过,强力推荐使用SOFTICE,
注意目前国内FTP服务器上的SOFTICE 3.2 FOR NT的Setup.ins
文件是错误的,它将导致安装程序不认识你的NT,可以用
3.0的setup.ins文件替代3.2的setup.ins,这样就可以安装
成功。
--
风过耳,带来你的声音。。。
※ 来源:·武汉白云黄鹤站 s1000e.whnet.edu.cn·[FROM: 202.103.33.26]
--------------------------------------------------------------------------------
[返回首页] [分类讨论区] [全部讨论区]
发信人: HaHa (持子之手), 信区: Programming
标 题: NT Driver Programming(2)
发信站: 武汉白云黄鹤站 (Sat Aug 29 13:56:25 1998) , 转信
Windows NT驱动程序设计基础
NT驱动程序设计当然远不是能在短短的一篇文章里说清楚的,
也不是我能说明白的,毕竟我也只是最近一段时间才开始接
触设计NT驱动程序。在本文里只是将自己在做驱动程序最开
始的时候比较模糊的东西组织到了一起。设计NT驱动程序,
最重要的资料当然是NT DDK文档。
NT驱动程序的分层结构
在大多书操作系统中,驱动程序是指管理某个外围设备的一
段代码。NT采用更灵活的方法,允许杂应用程序和硬件之间
存在几个驱动程序层次。这个分层允许NT更加广泛地定义驱
动程序,包括文件系统、逻辑卷管理器和各种网络组件,以
及各种物理设备驱动程序。
1、 设备驱动程序
这些是管理实际数据传输和控制特定类型的物理设备的操作
的驱动程序,包括开始和完成I/O操作,处理中断和执行特定
设备要求的任何差错处理。
2、 中间驱动程序
NT允许在物理设备驱动程序上分层任意树木的中间驱动程序。
这些中间层次提供扩展I/O系统的功能一种方法,而不必修改
底层的驱动程序。在我们所开始的光盘塔驱动程序中,就使
用了这种分层结构,在设计过程中,我们并不用关心如何去
与SCSI卡打交道,SCSI卡厂商和Microsoft提供的SCSI小端口
程序和SCSI端口程序为我们解决了这个问题。我们需要做的
就是在这些端口程序的基础上增加新的驱动程序层次,满足
我们对设备进行控制的要求。
3、 文件系统驱动程序(FSD)
FSD是一类比较特殊的驱动程序,通常负责维护各种文件系统
所需要的磁盘结构。注意我们并不能使用DDK来开发FSD,而
必须使用Microsoft的文件系统开发人员工具包,对此我们了
解甚少,但我们想要对光盘塔开发出独特而且更有价值的应
用,我们应该针对光盘塔开发我们自己的文件系统。
SCSI驱动程序
针对我们的实际光盘塔应用,有必要对SCSI驱动程序先做一些
介绍。SCSI驱动程序在整个NT设备驱动程序中都是比较特使的
一类驱动程序。NT SCSI体系结构使用分层的驱动程序分开特定
设备的管理与SCSI总线适配器(HBA)的控制。其结构如下所示。
SCSI端口程序是由Microsoft提供的组件,通常处理常见的SCSI
工作和隐藏本地操作系统的细节。SCSI小端口程序提供任何HBA
特定的控制操作的例程,一般由提供HBA产品的厂商提供。
SCSI类驱动程序城关特定类型的所有SCSI设备,而不管它们连
接到什么样的HBA上。例如有磁带机、磁盘、CD-ROM等的类驱
动程序。我们开发的jbChanger就是SCSI类驱动程序,管理所
有的媒质交换设备(光盘塔)。
一般比较少写SCSI过滤驱动程序,SCSI过滤驱动程序和NT的其
它类型的过滤驱动程序一样,它截获和修改高层发送给SCSI类
驱动程序的请求。这样就允许利用现有类驱动程序的功能,而
不必从头开始写所有程序。
NT内核模式对象
在我们的实际开发过程中的对象是SCSI设备,由于SCSI端口驱
动程序已经隐藏了硬件控制操作,因此我在这里不讲述跟硬件
相关的部分。如果今后的开发对象不同,需要对硬件进行操作
的时候,可能会对中断、DMA等有比较详细的了解,这些内容
可以参考DDK帮助。
NT使用对象技术管理所有的数据,下面分别对一般驱动程序所
涉及的一些对象做一介绍。不过在介绍这些对象之前,有必要
先对驱动程序的结构做一介绍。
驱动程序结构
NT驱动程序和一般的DOS/Windows C语言程序不一样,它没有
main()或者WinMain()函数入口。和DLL类似地,它向操作系
统显露一个名称为DriverEntry()的函数,在启动驱动程序的
时候,操作系统将调用这个入口。DriverEntry除了做一些必
要的设备初始化工作外,还初始化一些Dispatch例程入口。
我们知道,NT和设备驱动程序打交道主要是通过CreateFile、
ReadFile、WriteFile 和DeviceIoControl等Win32 API来进行
的。这些API其实都对应着驱动程序的一些Dispatch例程。而
驱动程序除了DriverEntry以外,主要就是由这些Dispatch例
程组成的。例如调用Win32 API CreateFile的时候,操作系
统最终转化为对驱动程序IRP_MJ_CREATE功能代码所对应的
Dispatch例程的调用,如果驱动程序没有提供该例程,
CreateFile调用就会失败。
NT中一些常用的功能代码和Win32 API的对象关系如下所示。
功能代码 说明
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
对用户模式或内核模式客户程序可用的控制操作
DeviceIoControl
IRP_MJ_INTERNAL_DEVICE_CONTROL
只对内核模式客户程序可用的控制操作
没有对应的Win32 API
IRP_MJ_QUERY_INFORMATION 得到文件的长度GetFileLength
IRP_MJ_SET_INFORMATION 设置文件的长度SetFileLength
IRP_MJ_FLUSH_BUFFERS 写输出缓冲区或丢弃输入缓冲区
FlushFileBuffers
FlushConsoleInputBuffer
PurgeComm
IRP_MJ_SHUTDOWN 系统关闭InitialSystemShutdown
和上面的驱动程序支持的功能代码相对应,一般的驱动程序
看起来就象下面的样子。
DriverEntry(…) // 驱动程序入口
{
…
DeviceObject->MajorFunction[IRP_MJ_CREATE] = XXDriverCreateClose;
DeviceObject->MajorFunction[IRP_MJ_CLOSE] = XXDriverCreateClose;
DeviceObject->MajorFunction[IRP_MJ_READ] = XXDriverReadWrite;
DeviceObject->MajorFunction[IRP_MJ_WRITE] = XXDriverReadWrite;
…
}
XXDriverCreateClose(…) // 对应IRP_MJ_CREATE和IRP_MJ_CLOSE的例程
{
//……….
}
XXDriverDeviceControl(…)// 对应IRP_MJ_DEVICE_CONTROL的例程
{
//……….
}
XXDriverReadWrite(…) // 对应IRP_MJ_READ和IRP_MJ_WRITE的例程
{
//……….
}
一个驱动程序并不需要支持所有的功能代码,比如如果一个
驱动程序根本就不必要与用户模式客户程序交互,那么就不
用支持IRP_MJ_CREATE和IRP_MJ_CLOSE。又如设备不支持设备
读写,就不用支持IRP_MJ_READ和IRP_MJ_WRITE。
驱动程序对象
驱动程序对象是在操作系统启动驱动程序、在调用驱动程序
入口DriverEntry之前就已经创建好了的,并且作为DriverEntry
函数的参数传递给驱动程序。如果驱动程序启动失败,操作
系统将删除该对象。该对象的数据结构如下。注意下表并不
是完整地列出了ntddk.h中的DEVICE_OBJECT结构体的所有数
据项,这里仅列出了一般驱动程序可能使用到的数据项。
Driver对象数据项 说明
PDEVICE_OBJECT DeviceObject
由本驱动程序创建的Device对象的链表
ULONG Flags
PDRIVER_INITIALIZE DriverInit
驱动程序初始化例程(一般较少用)
PDRIVER_STARTIO DriverStartIo
StartIo例程入口,一般该例程对低层设备驱动程序用得较多,
高层驱动程序较少使用本例程。
PDRIVER_UNLOAD DriverUnload
卸载驱动程序例程,如果想在控制面版的设备Applet里停止该设
备,应该提供本例程。
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]
驱动程序的Dispatch例程表
Driver对象的结构图如下图所示。
在上面提到过驱动程序是管理同类型的所有设备,所以上面的
结构中DeviceObject指向的就不是单个的设备对象,而是一个
对象链表,这个链表的维护在下面介绍Device对象时可以看到。
Device对象与Device Extension
驱动程序在调用IoCreateDevice函数成功后就创建了一个Device
对象。下面对Device对象几个比较重要的数据做一介绍。
Device对象数据项 说明
PVOID DeviceExtension
指向Device Extension结构的指针(见下面)
PDRIVER_OBJECT DriverObject
指向这个设备的Driver对象的指针,IoCreateDevice会
自动填写本数据。
ULONG Flags 指定这个设备的缓冲策略。
DO_DIRECT_IO
DO_BUFFERED_IO
PDEVICE_OBJECT NextDevice
指向属于这个驱动程序的下一个设备对象,依靠本数据来维护设
备对象链表。
CCHAR StackSize
发送到这个设备的IRP需要的I/O堆栈单元的最小数目,一般对
分层驱动程序来说,本数据应该比其下层设备的大1。
ULONG AlignmentRequirement
缓冲区要求的内存对齐,一般对分层驱动程序来说,本值应该
和其下层设备的对齐一致。
Device对象的结构如下图所示。
Device记录着设备的特征和状态信息,对系统上的每个虚拟
的、逻辑的和物理的设备都有一个Device对象。例如对一个
硬盘驱动程序,对一个物理硬盘有一个名称为Partition0的
Device对象,对应整个物理磁盘,同时对硬盘的每个分区,
也都有一个Device对象,它们的名称分别为PartitionX(X从
1开始,每个分区对应一个数字)。
Device Extension是连接到Device对象的一个很重要的数据
结构,它的数据结构是由驱动程序设计者自己来确定的,在
调用IoCreateDevice的时候应该指定它的大小,Device
Extension其实是由操作系统在非分页内存池中为每个Device
对象分配的一块内存。由于驱动程序必须是完全可重入的,
因此使用任何全局变量和静态变量都不是好的办法,一般来
说和设备有关的任何需要保持的信息都应该放到Device
Extension里去。
设备的缓冲策略也必须提一下,这里的Flag的缓冲策略主要
决定设备读写(功能代码IRP_MJ_READ和IRP_MJ_WRITE)时候的
缓冲策略,另外功能代码IRP_MJ_DEVICE_CONTROL时候的缓冲
策略是由IOCTL控制代码本身来决定的。两者不能混为一谈。
在下面我将专门用一节来讨论I/O的缓冲策略。
I/O请求包(IRP)
在上面的结构里面已经出现了IRP了,在这里对它做一说明。
在NT中,几乎所有的I/O都是包驱动的,可以说驱动程序和
操作系统其他部分都是通过I/O请求包来进行交互的。我们
来看看一个I/O请求的执行过程。
(1) 操作系统的I/O管理器从非分页内存分配一个IRP,响应
一个I/O请求。基于由客户指定的I/O函数,I/O管理器将该
IRP传递给合适的驱动程序的Dispatch例程。
(2) Dispatch例程检查请求的参数是否有效,如果有效,
驱动程序根据请求的内容进行一系列的操作。否则设置错
误状态信息直接返回。
(3) 操作完成时,将数据(如果有)和状态信息存放到IRP中
并返回给I/O管理器。
(4) I/O管理器对返回的IRP进行适当的处理后将最后状态和
数据(如果有)返回给用户。
一个IRP的主要数据项如下表所示。
IRP主要数据项 说明
IO_STATUS_BLOCK IoStatus
存放I/O请求的状态
PVOID AssociatedIrp.SystemBuffer
如果设备执行缓冲I/O,则为指向系统空间缓冲区的指针。
否则为NULL。
PMDL MdlAddress
如果设备执行直接I/O,指向用户空间缓冲区的内存描述表的指针
PVOID UserBuffer
I/O缓冲区的用户空间地址
BOOLEAN Cancel 指示IRP已被取消
关于AssociatedIrp.SystemBuffer、MdlAddress和UserBuffer将在
下面的I/O缓冲区策略里面更详细地讨论。
NT还有更多其他的对象,例如中断对象、Controller对象、定时器
对象等等,但在我们开发的光盘塔驱动程序中并没有用到,因此在
这里不做介绍。
I/O缓冲策略
很明显的,驱动程序和客户应用程序经常需要进行数据交换,但我
们知道驱动程序和客户应用程序可能不在同一个地址空间,因此操
作系统必须解决两者之间的数据交换。这就就设计到设备的I/O缓
冲策略。
读写请求的I/O缓冲策略
前面说到通过设置Device对象的Flag可以选择控制处理读写请求的
I/O缓冲策略。下面对这些缓冲策略分别做一介绍。
1、 缓冲I/O(DO_BUFFERED_IO)
在读写请求的一开始,I/O管理器检查用户缓冲区的可访问性,然
后分配与调用者的缓冲区一样大的非分页池,并把它的地址放在
IRP的AssociatedIrp.SystemBuffer域中。驱动程序就利用这个
域来进行实际数据的传输。
对于IRP_MJ_READ读请求,I/O管理器还把IRP的UserBuffer域设置
成调用者缓冲区的用户空间地址。当请求完成时,I/O管理器利用
这个地址将数据从驱动程序的系统空间拷贝回调用者的缓冲区。对
于IRP_MJ_WRITE写请求,UserBuffer被设置为NULL,并把用户缓冲
区的数据拷贝到系统缓冲区中。
2、 直接I/O(DO_DIRECT_IO)
I/O管理器首先检查用户缓冲区的可访问性,并在物理内存中锁定
它。然后它为该缓冲区创建一个内存描述表(MDL),并把MDL的地址
存放在IRP的MdlAddress域中。AssociatedIrp.SystemBuffer和
UserBuffer都被设置为NULL。驱动程序可以调用函数
MmGetSystemAddressForMdl得到用户缓冲区的系统空间地址,从而
进行数据操作。这个函数将调用者的缓冲区映射到非分页的地址空
间。驱动程序完成I/O请求后,系统自动从系统空间解除缓冲区的
映射。
3、 这两种方法都不是
这种情况比较少用,因为这需要驱动程序自己来处理缓冲问题。
I/O管理器仅把调用者缓冲区的用户空间地址放到IRP的UserBuffer
域中。我们并不推荐这种方式。
IOCTL缓冲区的缓冲策略
IOCTL请求涉及来自调用者的输入缓冲区和返回到调用者的输出
缓冲区。为了理解IOCTL请求,我们先来看看WIN32 API
DeviceIoControl函数的原型。
BOOL DeviceIoControl (
HANDLE hDevice, // 设备句柄
DWORD dwIoControlCode, // IOCTL请求操作代码
LPVOID lpInBuffer, // 输入缓冲区地址
DWORD nInBufferSize, // 输入缓冲区大小
LPVOID lpOutBuffer, // 输出缓冲区地址
DWORD nOutBufferSize, // 输出缓冲区大小
LPDWORD lpBytesReturned, // 存放返回字节数的指针
LPOVERLAPPED lpOverlapped // 用于同步操作的Overlapped结构体指针
);
IOCTL请求有四种缓冲策略,下面一一介绍。
1、 输入输出缓冲I/O(METHOD_BUFFERED)
I/O管理器首先分配一个非分页池,它足够大地存放调用者的输
入或输出缓冲区(不管哪个更大)。非分页缓冲区的地址放在IRP的
AssociatedIrp.SystemBuffer域中,然后把IOCTL的输入数据拷贝
到这个非分页缓冲区中,并把IRP的UserBuffer域设置成调用者输
出缓冲区的用户空间地址。当驱动程序完成IOCTL请求时,I/O管理
器将这个非分页缓冲区中的数据拷贝到调用者的输出缓冲区。
注意这里同一个非分页池同时用于输入和输出缓冲区,因此驱动
程序在向缓冲区写东西之前应该把输入的所有数据读出来。
2、 直接输入缓冲输出I/O(METHOD_IN_DIRECT)
I/O管理器首先检查调用者输入缓冲区的可访问性,并在物理内存
中将其锁定。然后为该输入缓冲区创建一个MDL,并把指定该MDL的
指针存放到IRP的MdlAddress域中。
同时,I/O管理器还在非分页池中分配一输出缓冲区,并把这个缓冲
区的地址存放在IRP的AssociatedIrp.SystemBuffer域中,并把IRP
的UserBuffer域设置成调用者输出缓冲区的用户空间地址。当驱动
程序完成IOCTL请求时,I/O管理器将非分页缓冲区中的数据拷贝到
调用者的输出缓冲区。
3、 缓冲输入直接输出I/O(METHOD_OUT_DIRECT)
I/O管理器首先检查调用者输出缓冲区的可访问性,并在物理内存中
将其锁定。然后为该输出缓冲区创建一个MDL,并把指定该MDL的指针
存放到IRP的MdlAddress域中。
同时,I/O管理器还在非分页池中分配一输入缓冲区,并把这个缓冲
区的地址存放在IRP的AssociatedIrp.SystemBuffer域中, 同时把
调用者用户输入缓冲区中的数据拷贝到系统缓冲区中,并把IRP的
UserBuffer域设置为NULL。
4、 上面三种方法都不是(METHOD_NEITHER)
I/O管理器把调用者的输入缓冲区的地址放到IRP当前I/O堆栈单元的
Parameters.DeviceIoControl.Type3InputBuffer域中,把输出缓冲
区的地址存放到IRP的UserBuffer域中。这两个地址都是用户空间地
址。
从上面的说明可以看出,在执行缓冲I/O时,I/O管理器将在非分页池
中分配内存,如果调用者的缓冲区比较大时,分配的非分页池也将
比较大。非分页池是系统比较宝贵的资源,因此,如果调用者的缓
冲区比较大时,我们一般采用直接I/O的方式(例如磁盘读写请求等),
这样不仅节省系统资源,另一方面由于省去了I/O管理器在系统缓冲
区和调用者缓冲区之间的数据拷贝,也提高了效率,这对存在大量
数据传送的驱动程序尤其明显。
可以注意到DDK中的Samples下,几乎所有的例程的读写请求都是直
接I/O的,而对于IOCTL请求则是缓冲区I/O的居多。
下面以changerDisk的IRP_MJ_DEVICE_CONTROL例程中的一段程序来
加强IOCTL缓冲策略的认识和用法。在该例中所有的IOCTL请求的缓
冲策略都是METHOD_BUFFERED。
NTSTATUS
ChangerDiskDeviceControl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
ULONG ControlCode;
ULONG InputLength, OutputLength;
PVOID InputBuffer, OutputBuffer;
…
ControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
// IOCTL请求功能代码存放在IRP当前I/O堆栈单元的
// Parameters.DeviceIoControl.IoControlCode中。;
InputLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;
// 输入缓冲区的大小存放在IRP当前I/O堆栈单元的
// Parameters.DeviceIoControl.InputBufferLength中。
OutputLength = irpStack->Parameters.DeviceIoControl.OutputBufferLength;
// 输出缓冲区的大小存放在IRP当前I/O堆栈单元的
// Parameters.DeviceIoControl. OutputBufferLength中。
InputBuffer = OutputBuffer = Irp->AssociatedIrp.SystemBuffer;
// 由于本例中所有的IOCTL请求都是输入输出缓冲的,所以输入输出缓冲区
// 的地址就是IRP的AssociatedIrp.SystemBuffer域。
….
}
NT和Win 32设备名
NT设备有多个名称,在函数IoCreateDevice创建Device对象时
指定的名称是设备对Windows NT Executive知道的名字。如果
要时设备对Win32 子系统和DOS虚拟机可用,还必须给设备一个
DOS名字。
这两个名字位于对象管理器的名字空间的不同部分。NT设备名在
树的\Device下面。而Win32名则出现在\DosDevices下面。同一
Device对象的NT设备名和DOS设备名通过符号连接相联系。运行
Win32 SDK带的WINOBJ.EXE可以比较清楚地看到这种关系。
上图中我们看到的就是当前系统所有的NT设备名。请注意一下图
中右边列表中的Harddisk0、Harddisk1、Harddisk2和windfs,
它们并不是NT设备名,它们是目录对象,
在他们下面包含的才是一个个的NT设备名。这里稍微做一下解释,
图中的HarddiskX分别对象系统的第0、1和2个硬盘。对于硬盘来
说,驱动程序应该为它所管理的每个物理硬盘创建一个Device对
象,同时也必须为磁盘的每个逻辑分区创建一个Device对象,那
么每个物理硬盘会有好几个Device对象,为了更好地组织这种关
系,可以在NT的名字空间里创建一个目录对象,然后再在这个目
录下创建每个Device对象。上图所示的是所有的设备的Win32名
字。我们可以调用Win32 API CreateFile函数来打开列出的这些
名字的设备。例如图中的PhysicalDriverX所对应的就是系统中的
每个物理硬盘。而图中的c:,d:,。。。对应的就是硬盘的逻辑
分区了。这就是通过符号连接建立起来的联系,我们在下面的图
中可以看到这中联系。
上图是用WINOBJ查看WIN32设备名d:时的情况,它清楚地表明WIN32
设备名d:对应的NT设备名为\Device\Harddisk0\Partition2,说
明逻辑盘号D对应的是第0个硬盘的第2个分区。注意NT设备名
Partition0并不是一般意义上的分区,它对应整个物理硬盘设备
对象。
开始驱动程序设计
下面的文字是从Microsoft的DDK帮助中节选出来的,它让我们明
白在开始设计驱动程序应该注意些什么问题,这些都是具有普遍
意义的开发准则。应该支持哪些I/O请求在开始写任何代码之前,
应该首先确定我们的驱动程序应该处理哪些IRP例程。
? 如果你在设计一个设备驱动程序,你应该支持和其他相同类型
设备的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处理的例程就是同一函数。
对IRP_MJ_READ和IRP_MJ_WRITE处理的例程也是同一个函数。
应该有多少个Device对象?
一个驱动程序必须为它所管理的每个可能成为I/O请求的目标的物
理和逻辑设备创建一个命名Device对象。一些低层的驱动程序还可
能要创建一些不确定数目的Device对象。例如一个硬盘驱动程序必
须为每一个物理硬盘创建一个Device对象,同时还必须为每个物理
磁盘上的每个逻辑分区创建一个Device对象。
一个高层驱动驱动程序必须为它所代表的虚拟设备创建一个Device
对象,这样更高层的驱动程序才能连接它们的Device对象到这个驱
动程序的Device对象。另外,一个高层驱动程序通常为它低层驱动
程序所创建的Device对象创建一系列的虚拟或逻辑Device对象。
尽管你可以分阶段来设计你的驱动程序,因此一个处在开发阶段的
驱动程序不必一开始就创建出所有它将要处理的所有Device对象。
但从一开始就确定好你最终要创建的所有Device对象将有助于设计
者所要解决的任何同步问题。另外,确定所要创建的Device对象还
有助于你定义Device对象的Device Extension的内容和数据结构。
开始驱动程序开发
驱动程序的开发是一个从粗到细逐步求精的过程。NT DDK的src\
目录下有一个庞大的样板代码,几乎覆盖了所有类型的设备驱动
程序、高层驱动程序和过滤器驱动程序。在开始开发你的驱动程
序之前,你应该在这个样板库下面寻找是否有和你所要开发的类
似类型的例程。例如我们所开发的光盘塔驱动程序,虽然DDK对
光盘塔没有任何描述,但我们知道光盘塔是符合SCSI-II规范的
SCSI设备,我们可以在src\storage\class目录发现很多和SCSI
设备有关的驱动程序,例如SCSI Tape,SCSI Disk,SCSI CDROM
等的驱动程序。下面我们来看开发驱动程序的基本步骤。
最简的驱动程序框架
1、 写一个DriverEntry例程,在里面调用IoCreateDevice创建
一个Device对象。
2、 写一个处理IRP_MJ_CREATE请求的Dispatch例程的基本框架
(参见DDK Kernel-Mode Drivers 4.4.3描述的一个DispatchCreate
例程所要完成的最基本工作。当然写了DispatchCreate例程后,
要在DriverEntry例程为IRP_MJ_CREATE初始化例程入口)。如
果驱动程序创建了多于一个Device对象,则必须为IRP_MJ_CLOSE
请求写一个例程,该例程通常情况下可以和DispatchCreate共
用一个例程,参见参见DDK Kernel-Mode Drivers 4.4.3。
3、 编译连接你的驱动程序。
用下面的方法来测试你的驱动程序。
? 首先按上面介绍的方法安装好驱动程序。
? 其次我们还得为NT逻辑设备名称和目标Device对象名称之间建立
起符号连接,我们在前面已经知道Device对象名称对WIN32用户模式
是不可见的,是不能直接通过API来访问的,WIN 32 API只能访问NT
逻辑设备名称。我们可以通过修改注册表来建立这两种名称之间的符
号连接。运行REGEDT32.EXE在\HKEY_LOCAL_MACHINE\ System\ CurrentControlSet\
Control\ Session Manager\ DOS Devices下建
立起符号连接(这种符号连接也可以在驱动程序里调用函数
IoCreateSymbolicLink来创建)。
? 重新启动系统。
? 编写一个简单的测试程序调用WIN 32 API CreateFile函数以刚才
你命名的NT逻辑设备名打开这个设备。如果打开成功,那么你也就成
功地写出了一个最简单的驱动程序了。
支持更多的设备I/O请求
例如你的驱动程序可能需要对IRP_MJ_READ请求做出响应(完成后可
用WIN32 API ReadFile 函数进行测试)。如果你的驱动程序需要能
够手工卸载,那么还必须对IRP_MJ_CLOSE做出响应。为你所需要处
理IRP_MJ_XXX写好处理例程,并在DriverEntry里面初始化好这些例
程入口。
一个低层的驱动程序可能需要最起码一个StartIo,ISR和DpcForIsr
例程,可能需要一个SynchCritSection例程,如果设备使用了DMA,
那么可能还需要一个AdapterControl例程。关于这些例程,请参考
DDK相应文档。
对于高层驱动程序可能需要一个或多个IoCompletion例程,最起码
完成检查I/O状态块然后调用IoCompleteRequest的工作。
如果需要,还要对Device Extension数据结构和内容做些修改。
运行级别的问题
做NT Driver,有点必须很清楚的,就是代码运行级别的问题,
即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级别运行。
中断响应例程在DIRQL下运行。
有一点要注意,我到现在也没有搞太明白,不能在DISPATCH
_LEVEL及以上级别调用KeWaitForSingleObject等函数。我的
印象是在DISPATCH_LEVEL及以上级别调用KeWaitForSingleObject
等函数时会立即返回,达不到真正等待事件的目的,很奇怪地
是,事件确实被置位了,可我不知道这是谁干的。对这一点我
的看法是伪软不想让我们在高级别的时候停下来不干活去等待,
因为在高级别下的等待会阻塞高级别任务的运行。
--
日出东方,唯我不败;
天上地下,唯我独尊。
※ 来源:.BBS 荔园晨风站 bbs.szu.edu.cn.[FROM: 192.168.0.101]
[回到开始]
[上一篇][下一篇]
荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店