荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: 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软件 网络书店