读书频道 > 系统 > windows > Windows内核原理与实现
6.5.3 驱动程序的代码结构
2013-05-20 08:32:37     我来说两句 
收藏    我要投稿   

本文所属图书 > Windows内核原理与实现

本书从操作系统原理的角度,详细解析了Windows如何实现现代操作系统的各个关键部件,包括进程、线程、物理内存和虚拟内存的管理,Windows中的同步和并发性支持,以及Windows的I/O模型。在介绍这些关键部件时,本...  立即去当当网订购

这一节我们首先来看一个驱动程序的基本代码结构,然后通过toaster 例子中的总线驱动程序、功能驱动程序和过滤驱动程序,来学习这些驱动程序的设计要点。图6.13 显示了一个驱动程序应该暴露的各种例程。


 

虽然设备驱动程序中有明确的对象概念,包括驱动程序对象和设备对象等,但是,绝大多数设备驱动程序是用C 语言来编写的,包括上一小节介绍的toaster 例子中的驱动程序。在图 6.13 中,驱动程序与 I/O 系统之间的接口,即图中所示的各种例程,实际上是驱动程序实现的一组C 风格的函数。驱动程序本身是一个动态链接库,扩展名为.sys ,系统中已安装的驱动程序位于Windows 目录的system32\drivers 子目录下。驱动程序的主入口函数也被称为初始化例程,这是驱动程序被初始化时首先获得控制权的代码。其他的例程都是通过此初始化例程来指定的。下面逐一介绍驱动程序暴露的例程。

•  初始化例程,是驱动程序 DRIVER_OBJECT 对象的DriverInit域。在驱动程序中,该例程通常被命名为DriverEntry 。此例程带两个参数:一个 DRIVER_OBJECT 对象指针和一个针对该驱动程序的注册表路径。在初始化例程中,驱动程序需要将自己实现的接口例程填充到第一个参数所指定的DRIVER_OBJECT 对象中,这样相当于将这些接口例程告诉了I/O 系统。

正如6.2.1 节中所介绍的那样,系统在加载驱动程序时,无论是通过IopInitializeBuiltinDriver 函数,还是IopLoadDriver 函数,都会调用初始化例程。

 •  增加设备例程,是驱动程序DRIVER_OBJECT 对象的扩展部分DriverExtension 的AddDevice 域。当即插即用管理器检测到一个由此驱动程序负责的设备时,即调用此例程来增加一个设备。通常,该例程调用IoCreateDevice 创建一个DEVICE_OBJECT对象,并将它加入到设备栈中。

•  一组分发例程,是驱动程序DRIVER_OBJECT 对象的MajorFunction数组域。通过I/O管理器发送的I/O 请求最终由该数组中的函数来处理。在Windows Server 2003中,此数组共包含27项,其中有IRP_MJ_READ 、IRP_MJ_WRITE,以及前文看到过的IRP_MJ_PNP 和IRP_MJ_POWER 等,具体定义请参考base\ntos\inc\io.h 文件中的IRP_MJ_< XXX >宏定义。这些数组项与驱动程序能处理的 I/O 请求是对应的,其中有些I/O 请求还有子命令,参见io.h 文件中的 IRP_MN_< XXX > 定义。如果设备驱动程序没有为一个I/O 请求提供相应的处理例程,那么,I/O 管理器将该I/O 请求对应的数组项设置为IopInvalidDeviceRequest 函数,该函数只是简单地设置I/O 请求包(IRP )中的状态,指明这是一个无效操作,然后完成此I/O 请求。

•  卸载例程,是驱动程序DRIVER_OBJECT 对象的 DriverUnload域。该函数释放当前驱动程序所用到的系统资源,之后I/O 管理器将驱动程序移除出内存。通常卸载例程与初始化例程相对应,也就是说,在初始化例程中申请的资源,应该在卸载例程中释放。正确的卸载例程可使得驱动程序被动态地卸载而不会造成任何资源泄漏。驱动程序的卸载是在系统线程(System 进程)中由 IopUnloadDriver 函数来完成,或者在一个工作项目(WorkItem)中由 IopLoadUnloadDriver 函数来完成的,参见 WRK中这两个函数的代码。

•  一些可选的例程。通常驱动程序可能会涉及以下的例程:

o  ReInitialize 例程,这是第二阶段初始化例程。在驱动程序的初始化例程和增加设备例程被调用,并且其他的驱动程序被加载和初始化以后,ReInitialize例程将被调用。通常,一个驱动程序在初始化例程DriverEntry 中调用 IoRegisterDriverReinitialization函数可以登记一个ReInitialize 例程。

o  StartIo 例程,是驱动程序 DRIVER_OBJECT 对象的DriverStartIo域。驱动程序通过一个StartIo 例程来发起一次数据传输,通常对于最底层的设备驱动程序有意义。这使得驱动程序可以并发地处理多个I/O 请求。

o  中断服务例程(ISR )。若设备以中断方式通知处理器,则驱动程序必须编写并注册至少一个中断服务例程。Windows 提供了中断对象机制,允许驱动程序在不操纵IDT (中断描述符表)的情况下,将一个例程与特定的中断向量关联起来,参见5.2.3节的介绍。中断服务例程通常只是保存设备的状态,然后插入一个DPC对象,以便在相对较低的IRQL 上完成I/O 任务。
 
o  DPC 例程。驱动程序在DPC 对象的延迟例程中完成I/O 任务,参见 5.2.4节的介绍。

o  SynchCritSection例程,用于同步访问设备硬件资源或驱动程序数据的例程。驱动程序通过KeSynchronizeExecution函数来同步一个SynchCritSection例程的执行,从而可以确保该例程与ISR 不会并发执行。

o  AdapterControl 例程。DMA设备往往有一个AdapterControl 例程,由它负责发起 DMA操作。驱动程序调用IoGetDmaAdapter 函数获得一个适配器对象(adapter object),以后调用适配器对象中的 AllocateAdapterControl 函数来准备DMA 传输,而AllocateAdapterControl会调用参数中指定的AdapterControl 例程来执行 DMA操作。

o   IoCompletion 例程,即I/O 完成例程。驱动程序调用IoSetCompletionRoutine 函数可以设置一个I/O 请求的完成通知,设备栈中的下层驱动程序一旦调用了IoCompleteRequest 函数,I/O 管理器就会依次调用上层驱动程序设置的完成例程,最后返回至I/O 请求的发起者。参考6.6.4节介绍的I/O 完成过程。

o  Cancel 例程,即 I/O 取消例程。与完成例程的情形类似,一个驱动程序可以调用IoSetCancelRoutine 函数,来设置一个I/O 请求的取消例程。当驱动程序调用IoCancelIrp 函数时,取消例程将被调用。与完成例程情形不同的是,取消例程并不针对设备栈层次中特定的驱动程序,而是针对一个I/O 请求,所以,一个I/O请求只有一个取消例程。

o  定时器例程(IoTimer)。由于硬件操作的不确定性,设备驱动程序有时候需要周期性地做一些检查工作,比如查看一个I/O 是否超时、定期刷新驱动程序中的某些变量(比如计数器),或者为某些内部操作进行计时等。驱动程序通过IoInitializeTimer/IoStartTimer/IoStopTimer 函数来使用 I/O 定时器。I/O 定时器一旦被
注册,则每秒钟被调用一次。I/O 定时器例程实际上是 DPC 例程,只不过它关联了一个设备对象,并且 I/O 管理器每秒钟调用它一次。设备对象 DEVICE_OBJECT结构中的 Timer 成员记录了与之关联的定时器对象。I/O 管理器将所有的 I/O 定时器组织成一个链表,然后在一个1 s的系统定时器(全局的定时器变量IopTimer )中依次触发所有已经被启动的 I/O 定时器。读者可以参考 IoInitializeTimer 、IoStartTimer、IoStopTimer 以及 IopTimerDispatch 函数的代码实现。 因此,我们可以看到,驱动程序本质上是以上这些接口例程的集合。这些例程分别参与到与设备相关的I/O 请求的处理中,有些例程的意图是直接操纵硬件设备,而有些例程是为了参与到I/O 管理器的管理框架中。下面介绍toaster 例子中主要驱动程序的基本实现,如表6.4所示。



 

在总线驱动程序 BusEnum.sys中,对 IRP_MJ_PNP 命令的处理是在 Bus_PnP 函数中完成的。总线上插入、拔除和弹出设备的动作是由IRP_MJ_DEVICE_CONTROL 命令中的三个子命令来模拟的。在接收到插入一个 toaster 设备的 I/O 请求时,驱动程序创建一个设备对象(成为该设备的 PDO ),并执行初始化,然后调用 IoInvalidateDeviceRelations 函数通知即插即用管理器此设备插入进来;拔除设备的过程类似,它首先设置设备对象的存在标志(设备对象 DeviceExtension 的Present 域,由 BusEnum.sys驱动程序来定义)为 FALSE,然后调用 IoInvalidateDeviceRelations 函数,通知即插即用管理器重新检查总线上的设备节点。弹出设备的处理是通过调用即插即用管理器的IoRequestDeviceEject 函数来完成的。

总线驱动程序BusEnum.sys 负责两种类型的设备对象:一种是总线设备的FDO,另一种是该总线上附载的设备的PDO。设备对象的 DeviceExtension 成员的IsFDO 域用于区分这两种情形。在Bus_PnP 函数中,我们可以看到这两种设备对象的即插即用命令分别由Bus_FDO_PnP和Bus_PDO_PnP函数来处理。对于电源命令IRP_MJ_POWER 的处理也类似,Bus_Power 依据不同的设备对象类型,分别调用 Bus_FDO_Power 或Bus_PDO_Power函数。关于这两种设备对象的即插即用命令和电源命令的具体实现,请参考toaster 例子的bus\pnp.c 和bus\power.c 文件中的代码。Toaster 例子的说明文档(toaster.htm )解释了电源子命令IRP_MN_QUERY_POWER 和IRP_MN_SET_POWER 在几个驱动程序中的传递路径。

Toaster.sys 是toaster 设备的功能驱动程序,它演示了即插即用命令的处理、电源命令的处理(支持D0、D1和D3设备电源状态)和WMI支持,同时它也支持设备的读写操作,允许应用程序通过API 函数DeviceIoControl 来操纵设备。Toaster例子中的应用程序toast.exe 可以测试 toaster 设备的读操作和DeviceIoControl 操作。

Toaster例子提供了 6 个过滤驱动程序,这些驱动程序只是一个基本框架,它们共用同一份代码,区别仅在于它们的编译配置和.inf 文件。过滤驱动程序把所有的非即插即用I/O 请求直接传递给下层的驱动程序(命令处理函数为 FilterPass );而对于绝大多数即插即用命令,它基本上也是忽略不予处理,但不管怎么样,它都会将I/O 请求传递给下层驱动程序。过滤驱动程序实现了一把自定义的锁(设备对象的DeviceExtension->RemoveLock成员),以保护设备对象在I/O 请求进行过程中不会被卸载(detach )和删除。

Toaster例子中的驱动程序并没有实现实质性的功能,但是,其中对于即插即用命令和电源命令的实现代码演示了一个驱动程序在通常情形下应如何处理这些命令。结合例子中给出的一些辅助工具,并观察调试输出信息(即代码中的DbgPrint 输出),这对于理解这些驱动程序与I/O 管理器、即插即用管理器以及电源管理器的交互非常有帮助。建议读者在调试器或者可观察调试信息的工具(例如DebugView )中检查I/O 请求的执行情况。

 

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:6.5.2 例子驱动程序toaster
下一篇:6.5.4 toaster 设备的设备栈
相关文章
图文推荐
3.4.4 进程生命期管
3.4.2 Windows应用商
3.4.1 Windows应用商
3.4 进程生命期管理
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站