毕业后一直在学操作系统, 有时候觉得什么都懂了,有时候又觉得好像什么都不懂,但总体来说自认为对操作系统实现机制的了解比周围的人还是要多一些。去年曾花了几个星期的晚上时间断断续续翻译了这篇对Linux和Windows驱动架构进行比较的论文。原文在这里。
Linux和Windows设备驱动架构比较
1. 概述
这篇论文中,我们将考查目前最为广泛使用的两种操作系统,即Linux和Windows系统的设备驱动架构。为每种操作系统实现设备驱动所需要的驱动组件将被展示并进行比较,同时也展示每种操作系统中执行I/O到内核缓冲的驱动的实现过程。最后将以对每种操作系统为开发者所提供的开发环境和辅助设施的考查收尾。
2. 引言
现代操作系统中包含多个模块,诸如内存管理器、进程调度器、硬件抽象层和安全管理器。欲了解Windows内核的细节,可参考[Russinovich, 98],Linux内核则参考[Rusling, 99], [Beck et al, 98]。内核可被看作一个黑盒,并且它应该知道如何与现存的不同种类的以及还没出现的更多硬件设备进行交互。实现一个内核,使其能够与所有被熟知的硬件设备进行交互是可能的,但并不现实,因为这会消耗太多的系统资源,没有必要。
内核模块化
在内核被创建的时候,并不期望它能知道如何与未出现的设备进行交互。现代操作系统内核允许通过在运行时添加设备驱动模块的方式来扩展系统功能,这个模块的功能使得内核可以与某种特定的新设备进行交互。每个模块都提供一个例程供内核在模块被加载时进行调用,还有一个例程在模块被移除时调用。每个模块还实现各种不同的例程以便实现将数据传送到设备或从设备接收数据的I/O功能,同时还有一个例程供发送I/O控制指令到设备。以上所述对Linux和Windows两种驱动架构均适用。
本论文的组织结构
本论文分成下面几节:
l 两种操作系统大致的驱动架构(第二节)
l 每种操作系统驱动架构组件(第三节)
l 实现一个驱动执行I/O到内核缓冲(第四节)
l 两种操作系统为开发者提供的驱动开发环境和辅助设施(第五节)
相关工作
Windows设备驱动架构相关文档可在Windows驱动开发包中找到,更有甚者,Walter Oney [Oney, 99] 和Chris Cant [Cant, 99] 对Windows驱动架构进行了详细的展示。Linux设备驱动架构则由Rubini et al [Rubini et al,01]作了很好的描述,可免费获取。
3. 设备驱动架构
设备驱动通过暴露编程接口的方式使得应用程序和操作系统可以对设备进行控制来达到对硬件的操作。这一节将展示当前最常用的两个操作系统,即Windows和Linux的驱动架构,以及这些架构的起源。
Linux驱动架构的起源
Linux可以说是Unix操作系统的一个克隆,首先由Linus Travolds创造 [Linus FAQ, 02], [LinuxHQ,02]。Linux沿用了类似于Unix的系统架构。Unix系统将设备看作是文件系统的节点。设备以特殊文件节点的方式呈现在目录中,该目录通常包含设备文件系统的节点入口[Deitel, 90]。用文件系统节点来表示设备的目的是使得应用程序能以设备无关的方式访问各种设备[Massie, 86],[Flynn et al, 97]。应用程序仍然可以通过I/O控制操作进行特定于设备的操作。设备由主设备号和次社保号进行标识。主设备号用来作为驱动数组的索引下标,而次设备号将相似的物理设备归组[Deitel, 90]。Unix有两种类型的设备,即字符设备和块设备。字符设备驱动管理没有缓冲并需要顺序访问的设备,块设备驱动管理那些可随机访问的设备,数据以块的方式被访问。另外,块设备驱动还用到缓冲区。块设备必须以文件系统节点的方式挂载之后才能被访问[Beck et al, 98]。Linux保留了Unix的很多架构设计,区别在于,Unix系统中,每个块设备需要创建一个对应的字符设备,而在Linux中,虚拟文件系统(VFS)接口使得字符设备和块设备的区分变得模糊[Beck et al, 98]。Linux还引入了第三种设备,叫网络设备。访问网络设备驱动的方式和访问字符设备、块设备的方式不同,它使用了不同于文件系统I/O接口的一个接口集。比如socket接口,就是用来访问网络设备的。
Windows驱动架构的起源
1980年,微软从贝尔实验室获取Unix操作系统的许可,之后作为XENIX操作系统发布。1981年,MS DOS第一版随着IBM PC发布,并具有和基于XENIX的Unix系统类似的驱动架构[Deitel, 90]。和Unix操作系统不同的是,这个系统内嵌了常规设备所需的驱动程序。设备入口不以文件系统节点的形式呈现,而是给设备赋予预留的名称,如CON表示键盘或屏幕,PRN表示打印机,AUX表示串口。应用程序不能像对待文件系统节点那样通过打开设备获取和驱动程序关联的设备句柄从而对设备进行I/O操作。操作系统透明地将预留的设备名称对应到驱动程序所管理的设备。MS DOS第二版引进了可加载驱动的概念。由于Microsoft公开了驱动架构的接口,这也促进了第三方设备制造商生产更多设备 [Davis, 83]。硬件制造商可以为这些新设备提供运行时可加载到内核或从内核移除的驱动程序。
之后,Microsoft又发布了Windows 3.1,它支持更多的设备并使用基于MS DOS的架构。之后的Windows 95、98和NT,Microsoft引入了WDM(Windows Driver Mode)。WDM的出现是因为Microsoft想要驱动程序代码和后面所有新的操作系统兼容[Microsoft WDM, 02]。因此,驱动遵守WDM规范的好处是,驱动程序只需编写一次,在Microsoft之后所有新版操作系统上使用时只需要重新编译该驱动即可。
Windows驱动架构
Windows驱动分两类,分别为遗留驱动和即插即用驱动。这里我们只将重点放在PnP驱动上,所有提及的驱动都大可认为是PnP驱动。PnP驱动不需费什么力气就能安装好,因此它对用户是友好的。另外一个使驱动程序支持PnP的好处是它们只会在需要的时候被操作系统加载,因此它们不会无端的耗尽系统资源。遗留驱动是为Microsoft早期的操作系统实现的,它们的架构已经过时。WDM是Microsoft指定的标准驱动模型[Microsoft DDK, 02]。WDM驱动适用于Microsoft近期所有的操作系统(Windows 95和之后的)。
WDM驱动架构
WDM驱动分三类,分别为过滤驱动、功能驱动和总线驱动[Oney, 01]。它们形成了图2.3所示的栈式结构。另外,WDM驱动必须是具有PnP感知的,支持电源管理和Windows管理规范(Windows Management Instrumentation)。图2.3显示了各个驱动如何交互数据和消息。一个叫I/O请求包(IRP,I/O Request Package)的标准结构被用来进行通信。任何时候,应用程序向驱动程序发送请求时,I/O管理将创建IRP并下传到驱动程序,驱动程序处理完毕后,“完成”这个IRP [Cant, 99]。不是所有的IRP都被下发到总线驱动,有些IRP被上层的驱动处理后直接返回到I/O管理器。对设备硬件的访问需要通过硬件抽象层。
Figure 2.3 The WDM Driver Architecture
Linux驱动架构
Linux下的驱动以模块的形式呈现,这些模块就是扩展了Linux内核功能的一个个代码块[Rubini et al, 01]。模块可形成图2.4那样的层次结构。模块之间的通信通过函数调用实现。模块在加载时,将导出模块中所有对Linux内核所维护的符号表公开的函数,之后这些函数对所有的内核模块都是可见的。对设备的访问需要通过硬件抽象层,硬件抽象层的实现依赖于内核编译时所针对的硬件平台,如x86或SPARC。
Figure 2.4 The Linux Driver Architecture
Linux和Windows驱动架构的比较
如图2.3和2.4所示,两个操作系统有很多的相似之处。两个系统中,驱动程序都是作为扩展内核功能的模块化组件。在Windows系统中,驱动层级之间的通信是通过将IRP作为标准系统函数或驱动程序自定义函数的参数来实现,Linux下函数调用的参数则是根据具体的驱动而不同。Windows有单独的内核模块来管理PnP、I/O和电源,这些组件在适当的时候将IRP发送到驱动程序。
Linux系统中,模块没有明显的层级关系,比如没有区分总线、功能、过滤驱动。内核没有明确定义的PnP、电源管理器以便在适当的时候将特定的信息发送给内核模块。内核可能会加载具有PnP、电源管理功能的内核模块,但内核模块暴露给驱动程序的接口并没有规定。
这些功能一般会合并到新版的Linux内核中,因为Linux内核总是处于发展状态。每当内核将数据发送给栈式模块中的某个驱动程序时,通过这些驱动所指定的某个接口,该数据可被分享给栈式模块中的其他驱动程序。
在这两种系统环境中,对硬件的访问都会通过硬件抽象层接口,硬件抽象层接口根据内核编译时所针对的特定平台(如X86,SPARC等)而实现。两种架构的相同之处在于,驱动程序都是运行时可加载的模块,每个模块都包含一个入口点使内核知道从哪里开始执行模块的代码。一个模块还包含这样一些例程,即该模块所管理的设备接收到I/O操作请求时供内核调用的例程。这使得内核可以向应用层提供设备无关的接口。在后面的第3.3节中,将对两种架构中的驱动组件作更深入的比较。
驱动组件
编写驱动程序时需要对硬件设备如何被操作有了解。比如说,几乎所有的设备都允许用户读取和写入数据。这一节中将展示所有驱动程序都应该包含的驱动组件,同时对两种操作系统的驱动组件进行比较,并展示如何实现一个对内核缓冲区进行I/O操作的驱动程序。本节将以对每种操作系统为驱动程序开发所提供的环境和辅助设施的考究来收尾。
Windows驱动组件
Windows驱动程序由各种不同的例程组成,其中有一些是必须的,其它的则是可选的。这一节展示所有驱动程序都必须实现的例程。Windows中的设备驱动以一个叫DriverObject的结构体表示。用一个结构体诸如驱动对象来表示一个驱动是有必要的,因为内核实现了可被所有驱动对象使用的各种例程。这些例程对一个驱动对象进行操作,这部分内容将在下一节进行讨论。
驱动程序初始化
Windows中每个设备驱动程序都包含一个叫DriverEntry的例程。顾名思义,这个例程在驱动程序被加载时执行,驱动所管理的设备对象的初始化也在这个例程中进行。Microsoft’s DDK [Microsoft DDK, 02] 是这样描述的:驱动对象表示当前被加载的驱动程序,设备对象则表示一个物理、逻辑或虚拟的设备。一个被加载的驱动程序(用驱动对象表示)可以管理多个设备(用设备对象表示)。初始化过程中,设备对象中用以指定驱动程序的卸载例程、添加设备例程和分发例程都将被设置。卸载例程用于驱动程序被卸载时做一些清除操作,例如释放从内核堆中的分配的内存。添加设备例程仅在当驱动程序作为PnP驱动加载时,在DriverEntry例程之后被调用,而分发例程用于实现I/O操作。
AddDevice例程
PnP驱动程序需要实现AddDevice例程。在这个例程中,一个设备对象被创建,为该设备保存全局数据的空间被分配。设备资源的分配和初始化也在这里进行。设备对象根据其被创建的位置而拥有不同的名称。如果一个设备在当前加载的驱动程序中创建并用于管理该驱动,则该设备叫功能设备对象(FDO)。如果一个设备对象是由驱动栈中位于下方的驱动程序创建,则该设备叫物理设备对象(PDO)。如果一个设备对象是由位于上方的驱动程序所创建,则叫过滤驱动对象(FIDO)。
创建一个设备对象
一个设备对象对应于在AddDevice例程中调用I/O管理器中名为IoCreateDevice的例程所创建的设备。对IoCreateDevice来说,最重要的是设备对象的名称和设备类型。这个名称使得应用程序和其他的内核驱动可获取到该驱动的句柄,从而可进行I/O操作。设备类型指定了驱动程序管理的设备的类型,如存储设备。
全局驱动数据
当一个设备对象被创建时,可以将一个内存块与之关联,该内存块叫DeviceExtension,也即在Windows中驱动程序数据保存的地方。这是一个挺重要的东东,它使得在驱动程序代码中使用难于维护的全局数据结构变得没有必要。例如,要是错误的声明了一个和全局变量具有相同名称的局部变量,驱动程序编写者会发现难以跟踪这样的bug。这也使得维护特定于设备对象的数据变得简单,尤其是当多个设备对象存在于一个驱动程序中的时候,比如总线驱动程序在管理总线上出现的多个设备的物理设备对象时。
设备命名
设备的名称可在设备对象被创建的时候赋予,这个名称可以用来访问驱动的句柄,句柄又被用来进行I/O操作。Microsoft建议不要给在过滤驱动和功能驱动中创建的功能设备对象命名。Oney [Oney, 99]指出,若一个设备对象具有名称,则任意用户都能打开设备对象并对其进行I/O操作,即使是对非磁盘设备驱动。这是因为Windows默认就给了非磁盘设备对象毫无限制的访问状态。另外一个问题是这些名称不需要遵循任何命名规范,指定的名称往往不是经过挑选的。例如两个驱动程序开发者可能给他们的设备对象赋予相同的名称,这样就会引起冲突。Windows还支持另外一种设备对象命名方式,即设备接口。设备接口是由128比特位构成的全局唯一标识符[Open Group, 97]。GUID可用Microsoft DDK中提供的工具生成,生成之后可对外发布。驱动程序通过在AddDevice例程中调用I/O管理器的名为IoRegisterDeviceInterface的例程注册设备接口。一旦注册,驱动程序必须调用I/O管理器的IoSetDeviceInterfaceState例程来使能设备接口。注册过程中一个接口数据入口被添加到Windows注册表中,应用程序接着就可以访问到。
从应用程序访问驱动
应用程序欲对设备驱动执行I/O操作前,必须先通过调Win32 API CreateFile获取到设备驱动的一个句柄,这个API需要设备的路径作为参数,如\device\devicex。具有名称的设备将出现在命名空间“\\device”中,因此先前的路径表示设备devicex。CreateFile同时需要指定对设备的访问标志,如读、写和共享方式。对注册了设备接口而没有名称的设备的访问则不同于图3.1.2.4展示的例程,它需要使用驱动程序的GUID,调用Win32 API SetupDiGetClassDevs获取一个指向设备信息结构的句柄。这种方式只适用于驱动程序已经注册了设备接口、应用程序需要访问设备(叫设备接口类)的情况。每次驱动程序调用I/O管理器例程IoRegisterDeviceInterface时,一个新的设备接口类的实例就被创建。一旦应用程序获取到了设备信息句柄,对Win32 API SetupDiEnumDeviceInterfaces的多个调用将会为每个设备接口类实例返回设备接口数据。最后,通过调用Win32 API SetupGetDeviceInterfaceDetail,并根据之前返回的接口数据,可为每个设备接口类实例获取到一个设备路径。接着,对感兴趣的设备,使用设备路径为参数调用CreateFile来获取句柄以便执行I/O操作。
Figure 3.1.2.4 Obtaining a handle an application can use for I/O from a device GUID.
设备对象栈
当PnP管理器调用AddDevice例程时,它其中的一个参数是来自下层驱动的一个设备对象(PDO)。设备对象在AddDevice例程中完成堆叠,因为发往下层驱动的IRP可被当前加载的驱动获取到。如图 3.1.2.5所示,设备堆叠是通过调用I/O管理器例程IoAttachDeviceToDeviceStack 来完成。在调用IoAttachDeviceToDeviceStack时,需要一个位于栈中新创建设备对象下方的物理设备对象。这个例程将新创建的设备附加到设备栈的顶层,并将当前位于其下方的设备对象返回,图 3.1.2.5中下方设备为设备对象X。下层的物理设备可位于新设备下方的任何位置,而IoAttachDeviceToStack 返回的是紧邻着当前设备的下层设备。
Figure 3.1.2.5 Attaching a device object to the top of a device object stack.
Windows应用层到内核和内核到应用层数据传输模式
从内核空间到用户空间以及从用户空间到内核空间传送数据的模式是在设备对象的flag域中设置。共有三种模式,分别为buffer I/O,direct I/O和neither I/O。图3.1.2.6 阐述了这三种模式。在buffer I/O模式中,操作系统分配了一个内核缓冲区来处理请求。在写操作中,操作系统首先验证用户空间提供的缓冲区,然后从用户空间将数据拷贝到新分配的内核缓冲区,接着讲内核缓冲区传送给驱动程序。读操作时,操作系统验证用户缓冲区然后将数据从新分配的内核缓冲区中拷贝到用户缓冲区。驱动程序可通过IRP的AssociatedIrp.SystemBuffer域访问到内核缓冲区。当使用buffer I/O模式时,驱动程序通过读取或写入内核缓冲区来实现与应用层的通信。
Direct I/O是用于应用层和驱动程序交换数据的第二种模式。应用层提供的缓冲区被操作系统在内存中锁定,这样它就不会被交换出去,并将被锁定内存的内存描述列表(Memory Description List,MDL)传送给驱动程序。内存描述列表是一个不透明的结构体,它的实现对驱动程序是不可见的。驱动程序之后通过MDL对用户空间缓冲区进行DMA操作。驱动程序通过IRP的MdlAddress域访问MDL。使用direct I/O的好处是它比buffer I/O速度要快,因为不需要在用户层和内核层之间拷贝任何数据,而是直接对用户缓冲区进行I/O操作。
第三种I/O模式既不使用buffer也不使用MDLs,操作系统直接将用户空间缓冲区的虚拟地址传送给驱动程序。驱动程序在使用之前负责检查缓冲区的有效性。此外,只有在当前线程上下文环境和应用程序的上下文环境一致时,用户空间缓冲区才能被访问,否则会出现页错误,因为虚拟地址只有在应用程序所对应的进程处于激活状态时才有效。
Figure 3.1.2.6 The three ways in which data from kernel to user and user to kernel
space is exchanged.
分发例程
分发例程用来处理接收到的I/O请求包,即IRPs(I/O request packets)。当一个IRP到来(如当一个应用程序发起I/O操作)时,一个适当的例程被从驱动对象的MajorFunction域中指定的例程数组中选出来,如图3.1.3。这些例程在驱动程序的入口函数中被初始化。每个IRP在创建时就与一个I/O stack location结构体(用于存储IRP的参数)关联。这个结构体有一个域,指定了IRP需要执行的分发例程和分发例程需要的相关参数。I/O管理器根据IRP决定将IRP发往哪个分发例程。
Figure 3.1.3 dispatching IRP’s to dispatch routines.
因此,IRPs被路由到适当的驱动例程进而得到处理。分发例程ID如表3.1.3所示,它们作为例程数组的索引,这个数组是在驱动对象的MajorFunction域中指定。分发例程的名称是驱动程序所实现的例程的名称,这些例程都将一个IRP和该IRP被发送到的设备对象作为参数。
Table 3.1.3 Required Windows driver dispatch routines
Windows驱动程序安装
Windows根据一个INF文件中的安装信息来安装驱动。驱动程序的编写者负责为驱动程序提供一个INF文件。Windows DDK提供一个叫GenInf的GUI应用程序,来为驱动程序生成INF文件。这个工具需要提供一个公司名称和一个Windows设备类,驱动程序将被安装到该设备类下。Windows为驱动程序预定义了各种不同的设备类。从系统控制面板进入到设备管理器面板,可看到显示的所有按设备类分类的已安装驱动程序。已有的设备类如1394和PCMCIA设备类。可在INF文件中添加一个ClassInstall32节来添加一个自定义的设备类。对PnP感知的设备,还需要在INF文件中指定一个硬件ID,在该设备被添加到系统中时,系统将用该ID来标识设备。硬件ID是一个标识字符串,PnP管理器在设备添加到系统中用硬件ID来标识设备。Microsoft为Windows系统会用到的各种设备发布了硬件ID。硬件ID保存在硬件设备中,操作系统在设备添加到系统中时从设备读取。一旦新设备的INF文件成功安装到系统中,每当具有指定硬件ID的设备被添加到系统中,为该设备编写的驱动程序都被加载,并在设备移除时被卸载。
Windows获取驱动程序使用信息
系统控制面板上的设备管理器给用户提供驱动的相关信息。它列出了所有当前已加载的驱动,每个驱动提供者的相关信息和驱动资源使用情况。同时还显示驱动无法加载时的失败信息以及错误码。
Linux驱动架构组件
Linux下的设备驱动和Windows设备驱动的相似之处在于它们都是由一些执行I/O及控制操作的例程组成。驱动程序没有对应的驱动对象,而是由内核直接管理。
驱动程序初始化
Linux下的美国各驱动程序包含一个驱动注册例程和反注册例程。驱动注册例程类似于Windows的驱动入口例程。驱动程序编写者使用内核定义的两个宏module_init和module_exit来指定自定义的例程作为注册和反注册例程。
3.2.1.1. 驱动注册和反注册
module_init声明的注册例程是驱动程序被加载时第一个执行的例程。在这个例程中,用一个内核字符设备注册例程register_chrdev注册驱动。这个例程需要一个驱动名称、主驱动编号(将在3.2.2节中讨论)和一系列执行文件操作的例程。其它特定于驱动的初始化也必须在这个例程中完成。反注册函数在驱动程序被卸载时执行,它的主要功能是做一些清除操作。反注册之前使用register_chrdev注册的驱动程旭时会调用内核例程unregister_chrdev,并需以设备名和主编号为参数。
设备命名
Linux下,设备命名使用0到255的数字,叫主设备编号。这意味着最多只能有256个可用的设备,也即应用程序可获取到句柄的设备。但这样一个主设备的每个驱动程序可以管理额外的256个设备。这些驱动程序管理的设备也使用0到255的数字标识,叫次设备编号。因此,应用程序可访问多达65535(256*256)个设备。主设备编号赋给一些熟知的设备,如IEEE1394的编号为171。Linux内核源码树中的文件Documentation/devices.txt包含了所有主设备编号的分配情况和编号注册中心的联系地址。当前,主设备编号240-254为实验所用。一个驱动程序通过指定0作为主设备编号来请求一个自动分配的主编号(若当前还有可用的主设备编号的话)。这种指定0为主设备编号的方式并不会有什么问题,因为它是为null设备预留的,而没有一个新的驱动程序会将自己注册为null设备驱动。
应用程序访问驱动
应用程序通过文件系统入口(nodes)访问驱动。按照惯例,驱动程序目录为/dev。需要对驱动执行I/O操作的应用程序使用open系统调用来获取某个特定驱动的句柄。Open系统调用需要一个设备节点名称如/dev/tty和访问标识(flags)。获取句柄之后,应用程序使用该句柄来调用其他的I/O系统调用如read、write和IOCTL。
文件操作
Windows下,分发例程是在驱动入口例程中设置。Linux下,这些分发例程就是所谓的文件操作并使用结构体file_operations来表示。一个典型的驱动程序会实现如表3.2.3列出的文件操作例程。
Table 3.2.3 Most commonly defined driver file operations in Linux
这些文件操作在驱动程序注册时指定。每当应用程序请求一个设备句柄时,内核会创建一个叫file的结构体,并在某个驱动例程被调用时将其传递给驱动程序。文件操作例程被多个用户调用,每个都对应一个file结构体。File结构体有一个f_op域,这个域是一个指针,指向驱动注册时指定的文件操作例程集。因此,在调用任何一个文件操作例程时,都可以通过改变f_op域的值来指向新的文件操作例程集。
驱动程序全局数据
每当应用程序对/dev下的设备文件节点发起一个open系统调用时,应用程序从操作系统获得设备的一个句柄。这个时候驱动程序的open函数被调用,并给它传递为open系统调用所创建的file结构体。任何一个文件操作例程执行时,内核都将file结构体传递给驱动程序。File结构体的private_data域可以是驱动程序指定的任意自定义结构体。驱动程序的私有数据通常在文件open操作函数中被设置,即为它分配内存,之后在文件的release操作函数中释放该内存。File结构体的私有数据域可用来指向驱动程序的全局数据,避免了使用全局变量。
驱动主编号和次编号如何工作
3.2.4.1. 问题
Linux下只有一个驱动程序可通过注册一个特定的主编号来管理一个设备,也就是说,驱动程序的注册只能使用一个主编号。举个例子,存在两个设备节点/dev/device1(主编号4次编号1)和设备/dev/device2(主编号4次编号2),只有一个驱动程序能够处理应用程序对两个节点的请求。这种限制的存在是因为Linux没有提供一种注册机制使得驱动程序能自注册一个主编号和一个次编号以便能管理一个设备。
3.2.4.2.解决办法
· 加载一个驱动程序来管理主编号为4的设备。这个驱动程序将自己在内核中注册(节3.2.1.1会看到这是如何完成的)。
· 分别加载两个驱动,一个管理主编号4次编号1的设备,另一个管理主编号4次编号2的设备。这两个驱动程序没有在内核中注册,而是向管理主设备编号4的另外一个驱动程序注册。这个驱动程序负责实现注册机制并跟踪管理向它注册的所有驱动程序。
· 应用程序打开任意一个设备节点(/dev/device1或/dev/device2)时,注册为管理主设备4的驱动程序的open例程将被内核调用。一个用来表示被打开设备的file结构体作为参数传递给这个open例程。
· 这时候,管理设备主编号4的驱动程序修改文件操作函数指针(file结构体的f_op成员)来指向管理被打开设备的驱动程序所实现的I/O例程。应用程序打开次设备时,管理主编号4的驱动程序以下列方式区分:
o 一个叫inode的结构体被传递给open例程。这个结构体包含一个叫i_rdev的域,该域指定了open操作的目标设备对应的主编号和次编号。内核的MINOR和MAJOR宏可用来从i_rdev域提取住次编号。这个例子中,主编号为4,次编号为1或2。管理主编号4的驱动程序就可以通过这个信息从它的注册数据库中定位到次设备驱动程序。
用户到内核和内核到用户空间数据传输模式
Linux下,用户到内核和内核到用户空间的数据交换方式有三种,分别是buffer I/O,direct I/O和mmap。在buffer I/O模式中,内核将数据从用户空间拷贝到内核空间供驱动程序使用。和Windows不同,Linux没有自动对I/O进行缓冲,而是提供访问用户和内核空间的例程,驱动程序使用这些例程来完成用户和内核空间之间数据的拷贝。Direct I/O模式中,驱动程序可对用户空间缓冲区直接读和写。这是通过kiobuf接口完成,它将用户空间缓冲区映射到调用系统调用kiobuf时定义的结构体。这个操作会锁定用户空间缓冲区,这样该空间不会被换出以便满足设备的I/O操作。第三种方式是mmap,它是由驱动程序使用mmap内核调用将内核的空间块映射到用户空间,应用程序因而可以对映射的内核内存进行I/O操作[Rubini et al, 01].。
Linux驱动安装
Linux下驱动程序的安装是将驱动文件放置到特定的系统目录下。在RedHat发行版中[Redhat, 02],模块位于目录/lib/modules/kernel_version,kernel_version指定当前内核版本,如2.4.19。一个叫modules.conf的配置文件位于系统配置文件目录如/etc中,这个文件在加载模块时被内核使用。通过修改该文件,可对某个驱动程序放置位置进行覆盖。还可以定义其它一些模块加载选项,如驱动被加载时给它传递的参数。模块加载和卸载使用系统自带的内核模块工具包,叫insmod,modprobe和rmmod。Insmod和modprobe将驱动程序二进制镜像加载到内核,rmmod则移除模块。另一个叫lsmod的程序列出当前所有已加载的模块。Insmod尝试加载一个模块,若该模块依赖于其他模块,则返回一个错误码。Modprobe则尝试着满足模块依赖关系,它试图将当前模块所依赖的其它模块先进行加载。模块依赖关系信息可从一个叫modules.dep的文件获取,该文件位于系统模块目录(system’s modules directory)中。在驱动程序可被应用程序访问前,这个驱动的一个附有主次设备编号的设备节点(见2.1和3.2.2.1节,Linux如何在系统中表示设备)首先要在设备目录/dev中被创建。系统程序mknod就是为这个目的准备的。在创建一个设备节点时,指定节点为字符设备还是块设备是有必要的。
Linux获取驱动使用信息
我们时常需要获取系统已加载驱动的状态信息。Linux下,proc文件系统是用来将内核信息向应用程序发布。Proc文件系统和其他的文件一样,它也包含目录和文件节点供应用程序访问和执行I/O操作。Proc文件系统中的文件和普通文件的区别在于,对proc文件执行I/O操作的数据是被传递到内核内存而不是磁盘存储。Proc文件系统是应用程序和内核组件之间的通信媒介。例如,读取/proc/modules将返回当前所有已加载模块和它们的依赖关系。在获取驱动状态信息和发布驱动程序数据到应用程序时,proc文件系统就尤为有用。
Windows和Linux驱动架构组件比较
Windows和Linux的驱动程序都是由一系列执行I/O操作的例程组成的可动态加载的模块。当加载一个模块时,内核将定位到被系统标记为驱动程序入口的例程作为驱动代码执行的起点。
驱动例程
两个系统中驱动程序都具有初始化和反初始化例程。在Linux中,这两个例程的名称可自定义,在Windows中,初始化例程的名称固定(叫DriverEntry)但反初始化例程可自定义。Windows为每个驱动程序维护一个驱动对象,驱动程序的多个实例用多个驱动对象表示。Linux下,内核为每个管理一个设备主编号的驱动维护信息,即每个主设备驱动。两个操作系统都要求驱动程序实现标准的I/O例程,Windows中叫分发例程,Linux下叫文件操作。Linux下,可为每个应用程序获取到的设备句柄设置一个不同的文件操作例程集。Windows下,分发例程在驱动对象的一部分,并且是一次性地在DriverEntry例程中定义。由于每个被加载的驱动都有一个驱动对象,因此不建议在应用程序使用系统调用请求一个句柄时修改驱动对象的分发例程。Windows有个叫AddDevice的例程,在PnP感知的设备添加到系统时被PnP管理器调用。Linux没有PnP管理器,也就不存在这样一个例程。
Windows的分发例程对设备对象和IRPs进行操作,Linux下,文件操作针对file结构体。自定义的驱动全局数据保存在Windows的设备对象中,而Linux下则保存在file结构体中。Windows下,设备对象在驱动加载时被创建,Linux下,file结构体是应用程序通过系统调用open向驱动请求句柄时被创建。这就意味着Linux下每个应用程序的全局数据可保存在file操作结构体中。Windows下,全局数据只能出现在驱动管理的功能设备对象(FDO)中。Windows下每个应用程序的全局数据必须保存在功能设备对象(FDO)自定义结构体的列表结构中。
设备命名
Windows下的驱动使用驱动自定义的字符串命名并显示在\\device命名空间下。Linux下,驱动被赋予文本形式的名称,但应用程序并不需要知道这些名称,驱动是通过主-次编号对来标识。主-次编号的范围是0-255,因为是用16比特位来表示主-次编号对,所以最大允许65535个设备安装到系统中。Linux下的设备通过文件系统节点供应用程序访问。在大部分的Linux发行版中,目录/dev包含设备文件系统节点。每个节点创建时带有驱动的主编号和次编号。应用程序获得驱动的一个句柄,用来对系统调用open的目标设备节点进行I/O操作。Windows还有另一种驱动命名方式,是给每个驱动注册的128位GUID。应用程序访问注册表,通过GUID获得\\device命名空间下的文本形式的名称。这个名称通过使用Win32 API CreateFile来获取驱动的一个句柄以便进行I/O操作。
用户-内核空间数据交换
两种操作系统中,数据来自或去往用户空间的方式是类似的,都允许缓冲区数据传送,在Windows下是有I/O管理器执行,Linux下则由驱动执行。两种操作系统都可以进行direct I/O到用户空间缓冲区,通过锁定用户空间缓冲区以使得该缓冲区一直存在于物理内存中。这个起因是驱动程序并不总能直接访问用户空间缓冲区,因为它不能保证一直运行在和拥有该用户空间缓冲区的应用程序一致的进程上下文中。应用程序有它自己的虚拟地址空间,该地址空间只在它自己的进程上下文中有效。因此,当驱动程序访问某些应用程序的一个虚拟地址但不在该应用程序的进程上下文中时,就会访问了无效的地址。
驱动安装和管理
Windows驱动的安装是通过一个叫INF文件的文本文件。一旦安装之后,一个设备的驱动程序在设备出现在系统中时会自动被PnP管理器加载。Linux系统中,使用程序工具来加载驱动二进制镜像到内核。需要手动将一些条目添加到系统启动文件中,这样驱动加载程序如modprobe就以驱动程序镜像路径或驱动程序的别名为参数执行。驱动程序的别名在文件/etc/modules.conf中定义,modprobe等类似程序在加载驱动之前会查看该文件。Modules.conf中一个定义别名的条目的例子可类似于“alias sounddriver testdriver”,这是将sounddriver作为testdriver驱动二进制镜像的别名。这样一来,用户可通过使用标准的更简单的名称如sounddriver来加载音频驱动程序而不需要知道音频卡的某个特定驱动程序的名称。Windows下驱动程序的状态信息可在设备管理面板中看到,也可以直接从系统注册表中读取相关数据。Linux下,驱动信息可通过proc文件系统节点获取,如文件/proc/module包含了已加载模块的一个列表。
一个内核缓冲驱动
这一节展示一个执行I/O操作到内核内存块(虚拟磁盘)的简单驱动程序的实现。我们将讨论为使驱动程序能够同时在Windows和Linux下工作所需要的各种组件,这样它们所需要驱动组件的相似和不同之处也得到了突显。驱动程序所管理的虚拟设备如图4.0.所示,它由若干内核内存块组成。应用程序可对虚拟设备进行I/O操作。驱动程序可以选择某个内存块和内存块的偏移位置进行访问。
Figure 4.0 A simple virtual device
需要的驱动组件
Windows和Linux驱动程序都将实现read,write和IOCTL驱动例程。每个操作系统所需要的例程如图4.1.所示。驱动程序的名称可随意指定。图4.1种不同操作系统的例程也可以被赋以相同的名称,这里只是根据平台的惯用法来命名。
Figure 4.1 The Windows and Linux basic driver routines
驱动加载和卸载例程
Windows下,驱动加载例程DriverEntry中所执行的步骤是设置I/O分发例程,如图4.1.1a所示。
Figure 4.1.1a Initialisation of a driver Object in the driver entry routine
Linux下,驱动加载例程RegisterDriver中所进行的是驱动主编号的注册,如图4.1.1b。Tagged文件操作的初始化,只针对GCC编译器,如图4.1.1b,是在对结构体fops的声明中,当然这不是ANSI C的有效语法。编译器将使用驱动程序实现的例程名称初始化file_operation结构体(fops)中的各个不同的域。如open是结构体一个域的名称而Open是驱动实现的一个例程,编译器将Open函数指针赋给open域。
Figure 4.1.1b Registration of a driver major number in Linux
Linux驱动的卸载程序中,已注册驱动必须进行反注册,如图4.1.1c。
Figure 4.1.1c Driver major number deregistration in Linux
驱动全局结构
必须定义一个结构体来保存驱动全局数据,这些数据在驱动的各例程中被使用。对这个内存设备,同样的结构使用在Windows和Linux驱动程序中,其定义如图4.1.2。
Figure 4.1.2 Structure used to store global data for generic driver
memoryBank是包含4个内存块,每个块为1K大小的数组。currentBank表示当前选中的内存块,offsets记录了每个内存块内部的偏移量。
添加设备例程
添加设备例程只针对Windows。Linux没有添加设备例程,所有的初始化必须在驱动加载例程里完成。Windows下addDevice例程所执行的操作如图4.1.3所示。在调用I/O管理器例程IoCreateDevice例程时,一个设备对象被创建。供应用程序使用来获取驱动句柄的接口也被创建,这通过调用I/O管理器例程IoRegisterDeviceInterface。这个例程的一个参数是使用系统工具guidgen手动生成的GUID。Windows下,驱动和应用程序之间的不同数据交换方式在3.1.2.6节中有说明。驱动程序通过设置设备对象的flags域(见3.1和3.2关于设备对象的讨论)来表明其要使用的数据交换方法。这里例子中flags被设置成使驱动使用buffered I/O方式。内存设备使用的每个内存块通过其中一个叫ExAllocatePool的内核内存分配例程来分配。这个内存从内核的非页内存池中分配,这样设备的内存总是存在于物理内存中。
Figure 4.1.3 Operations performed in the Windows driver’s add device routine
打开和关闭例程
Windows驱动的大部分初始化操作都已经在添加设备例程中完成,因此不需要在打开例程中做任何初始化。Linux下的打开例程如图4.1.4a所示。首先,用于保存驱动全局数据的内存被分配,然后将文件结构体的private_data域指向该内存。之后内存设备所使用的内存块通过和Windows下完全一样的方式分配,只是内存分配函数的名字不同,Windows下是ExAllocatePool而Linux下是kmalloc。
Figure 4.1.4a Operations performed in Linux’s generic driver open routine
在Linux的关闭例程中,为驱动全局数据和内存设备分配的内存被释放,如图4.1.4b。Windows下,内存的释放是在响应PnP移除消息的时候,这个在本节后面会有讨论。
Figure 4.1.4b Operations performed in Linux’s generic driver close routine
读和写例程
Read和write例程将数据传送到或取自当前选中的内核内存块。Windows下,读例程的执行如图4.1.5a所示。要读取数据的长度值从IRP的I/O栈位置(见3.1.3节什么是I/O stack location)获取,该域名称为Parameters.Read.Length。所请求长度的数据将被从当前选中的内存块(后面会讨论应用程序通过驱动IOCTL例程选择内存块)中读取,使用的是内核运行时例程RtlMoveMemory。RtlMoveMemory将数据从内存设备的内存空间搬移到I/O管理器为buffered I/O分配的缓冲区,也就是IRP的AssociatedIrp.SystemBuffer域。这个IRP算完成了,就通知I/O管理器驱动程序已完成IRP的处理,I/O管理器将IRP返回给其发起者。
Figure 4.1.5a Performing a read operation in the Windows driver
写例程对上述内存搬移进行反操作,如图4.1.5b。
Figure 4.1.5b Performing a write operation in the Windows driver
Linux下,读例程如图4.1.5c所示。对驱动全局数据的引用从文件结构体的private_data域获取,从全局数据中,又获取到对memoryBank的引用。接着数据就从这个内存区被传送到用户空间,使用内核访问用户空间例程copy_to_user。
Figure 4.1.5c Performing a read operation in the Linux driver
写例程执行和上面同样的操作,只是这一次数据是从用户空间传送到内核空间,如图4.1.5d。
Figure 4.1.5d Performing a write operation in the Linux driver
设备控制例程
设备控制例程用来设置设备的各种状态。应用程序使用win32例程DeviceIoControl来对驱动程序进行IOCTL调用。这个例程需要一个由驱动程序定义的IOCTL码。一个IOCTL码告诉驱动程序应用程序要执行的操作。在这个例子中,驱动实现IOCTL例程用来选择当前内存块号(current bank number)。驱动的IOCTL码使用之前必须先被定义。Windows下IOCTL码的定义如图4.1.6a。CTL_CODE宏用来定义一个设备的IOCTL码[Oney, 99]。CTL_CODE的第一个参数是设备ID,ID数值范围为0-65535,其中0-32767为系统预留,32768-65535的使用可自定义。所选的IOCTL码必须和驱动的addDevice例程中为例程IoCreateDevice指定的设备编码一致(参见节4.1.3addDevice例程所做的事情)。第二个参数为表示功能码的12比特位长数值。0到2047为Microsoft预留,因此功能码应为大于2047而小于2^12。这个通常用来表示哪个控制码被定义,即将两个IOCTL码区分开,如图4.1.6a。第三个参数值指定用于从用户空间传送参数到内核空间的方法,第四个参数表示应用程序对设备的访问权限。
Figure 4.1.6a IOCTL code definition in Windows
Linux下,应用程序使用系统例程ioctl来对驱动进行IOCTL调用。IOCTL码在文件Documentation/ioctl-numbers.txt中指定,可在Linux内核源码树中找到。用于试验的驱动选择一个未使用的号码,目前是大于0xFF的值。这个驱动的IOCTL码的定义如图4.1.6b。_IOWR表示数据将被传送到或取自内核空间。其它的宏如_IO表示没有任何参数,_IOW表示数据仅将从用户空间被传送到内核空间,最后的_IOR表示数据仅将从内核空间被传送到用户空间。以上的宏需要一个表示内核和用户空间所需要交换数据的大小的值。据Rubini et al [Rubini et al, 01]建议,为使驱动程序可移植性更好,这个值应被设置为255(8比特),虽然依赖于当前架构的数值为8-14比特位。第二个参数和Windows下的函数编号类似,8位宽,从0-255。
Figure 4.1.6b IOCTL code definition in Windows
一旦IOCTL码被选定,IOCTL例程就可以被定义。Windows下,IOCTL例程的定义如图4.1.6c。两个IOCTL码被处理。第一个IOCTL_SELECT_BANK,设置当前内存块号,第二个IOCTL_GET_VERSION_STRING,返回驱动版本字符串。从IOCTL例程返回的数据和read、write请求返回的数据一样被调用者处理。
Figure 4.1.6c IOCTL routine definition in Windows
Linux下IOCTL例程的定义如图4.1.6d。对IOCTL码的处理和Windows一样,不同的只是语法上。数据的处理和read、write请求一样。
Figure 4.1.6d IOCTL routine definition in Linux
PnP消息处理例程
Windows下,PnP消息在适当的时候被分发给驱动程序,如当设备被添加到系统或被从系统移除时。这些消息被驱动程序实现的PnP分发例程处理。Linux下,内核并没有发送PnP消息给驱动程序,因此也就没有PnP例程。Windows下PnP消息处理例程如图4.1.7所示。这个例子中,内存设备驱动只处理其中一个PnP消息。移除设备的消息是在驱动程序被系统卸载时被发送。这个时候,通过调用I/O管理器例程IoSetDeviceInterface禁用驱动程序的接口,驱动程序的功能设备对象和驱动程序分配的那些内存块也一并被删除。
Figure 4.1.7 PnP Message handler routine
驱动开发环境
为到此为止所讨论的两种操作系统,即Microsoft Windows和Linux开发驱动需要使用特定于每个平台的一些软件开发工具。Windows和Linux操作系统的内核都是使用C语言编写,这使得为两个系统编写的驱动程序也跟着使用C语言编写。Windows支持使用面向对象的编程语言C++来编写驱动程序,Linux却没有支持。
Windows驱动开发环境
Microsoft Windows是一个具有所有权的商用的操作系统,即它需要被购买来使用。针对Windows,存在若干商用的驱动程序开发环境。举个例子,如NuMega DriverStudio™ Suit [Compuware, 01],带有类库和驱动构造向导以辅助驱动开发,同时还集成了一个调试器允许驱动代码的调试。
Windows设备驱动开发包
Windows下驱动开发的标准途径是从Microsoft获取设备驱动开发包(DDK)和利用一些辅助工具进行开发。最新版的DDK可从MSDN得到(The latest version of the DDK is available to Microsoft Software Development Network (MSDN) subscribers),DDK包含开发驱动所需要的程序。DDK安装程序会安装一些批处理文件,这些批处理文件会建立一个外壳窗口使得可以为Microsoft每个版本的操作系统开发驱动。这次对Windows驱动架构考查所使用的DDK 3590,具有为Windows ME、2000、XP、.NET开发驱动的环境。每个平台都有两个版本的开发环境,一种叫checked版,调试符号被添加到驱动代码中,另外一种叫发布版,这个版本的驱动程序没有调试符号。发布版开发环境也是驱动产品最后使用的编译环境。
Windows驱动的Makefile
DDK外壳窗口打开后,一个简单的build命令就可以编译驱动程序。Makefile定义了用来生成驱动程序的源代码文件。Makefile的条目在一个叫sources的文件中指定,放在build命令所处的当前目录下。图5.1.2显示了用来生成一个简单驱动程序的Makefile的格式。环境变量TARGETNAME指定了生成驱动的名称。 这个例子中,驱动程序将叫mydrivers.sys,Windows下的驱动都以.sys为后缀。TARGETPATH指定驱动程序赖以生成的目标代码文件。 目录obj下有一个文件_objects.mac,定义了额外的目标文件路径。Windows 2000中,checked版默认的目标文件路径为objchk_w2k,发布版默认的目标文件路径为objfre_w2k。INCLUDES指定了编译驱动所需要的包含文件路径,SOURCES指定驱动赖以生成的驱动源码文件。
Figure 5.1.2 A Makefile used for building a WDM driver with the Windows DDK
Windows DDK文档和工具
Windows DDK包含组织良好的API文档和驱动程序例子代码。初学者可从这里学到如何创建驱动程序。DDK还包含一些辅助驱动开发的实用工具程序。其中一个是设备树应用程序,列出了当前所有已加载的在\\device命名空间以层级方式列出的驱动(见3.1.2.4关于设备命名空间的讨论),显示每个驱动栈、驱动所实现的例程和驱动对象内存地址。Windows DDK提供的其它工具中有一个用来生成INF文件的叫geninf,它生成驱动程序安装需要的INF文件,还有一个PnP驱动测试应用程序用来测试驱动是否支持PnP。
Linux驱动开发环境
Linux驱动开发环境和Windows不同,Linux下没有和Windows DDK对应的东西,也就是说内核创建者没有提供Linux设备驱动开发包,而是将内核源码对所有人公开。内核源码的头文件就是开发驱动所需要的所有东西。驱动程序使用GNU C编译器,即GCC,它也被用来编译应用程序。和Windows类似,通过Makefile文件指定驱动如何被编译生成。
Linux驱动开发Makefile
一旦定义了Makefile,使用简单的make命令来生成驱动。图5.2.1显示了一个用来生成叫mydriver的驱动程序的Makefile文件示例,源代码文件为mydriver.c。第一个条目是KERNELDIR,定义一个环境变量,指定了内核头文件的位置。后面一行包含了当前内核的配置信息。在内核和驱动程序被编译生成之前,外部定义的内核变量在.config文件中指定,该文件位于内核源码树的根目录,这样内核的头文件可以使用这些信息。CFLAGS用来设置GCC编译器额外的标志,‘-O’ 打开代码优化开关,‘-Wall’打印所有的代码警告。‘all’节是make命令执行时默认会去检查的节。一个目标叫mydriver,依赖于目标文件mydriver.o,mydriver.o由GCC生成。环境变量LD指定用来生产最后的驱动模块的GNU链接器。选项‘-r’指定输出可被重定位,即里面的内存内置应该是相对于某个基地址的偏移,这个基地址在编译的时候是不知道的。‘$^’ 是mydriver.o的别名,‘$@’ 是mydriver的别名,也就是说,它要求链接器从mydriver.o目标文件生成可重定位的代码然后生成输出文件mydriver。
Figure 5.2.1 Makefile used to build a driver in Linux
内核模块管理程序如insmod和lsmod分别用来将驱动程序加载到内核和查看当前已加载的所有内核模块。
Linux驱动开发文档
Linux内核源码树下有个叫“Documentation”的目录,这个目录下有一些关于Linux内核方面的文档,但还是没有Windows DDK文档那样完整和生动。Rubini et al[Rubini et al, 01]编写Linux驱动书籍对设备驱动开发者来说是个更好的信息来源。Linux内核没有自带任何驱动程序例子,但有在实际环境中被使用的驱动程序源码,这些代码可以作为开发新设备驱动的基础。然而,这对设备驱动开发新手来说并不是一个好的引导素材。
驱动程序调试
每一个软件部件在其开发周期内总是不时地需要调试,因为总存在一些难以靠检查源码就能发现的晦涩的bug。对驱动程序来说更是如此。应用程序的bugs最坏情况下会是应用进程不稳定,而驱动程序中严重的bug会使整个系统不稳定。调试应用程序很直观,即在调试器的帮助下,在感兴趣的源代码位置设置一个中断语句。这个因调试器而异。Windows下,使用Microsoft Visual Studio调试器,设置断点只需要在源码所在行点击一下鼠标。DDD(Linux下的GUI调试器,使用了最流行的命令行调试器GDB)调试器也是一样的操作。程序在调试模式运行时,遇到断点程序会暂停执行使得可以单步跟踪,即从那个地方开始的指令逐条执行并可观察执行的效果。调试器中一般可以看到被调试程序中变量的内存地址和变量的值。到此为止我们所讨论的调试方式也适用于驱动程序的调试,某种程度上还适用于每种操作系统。
Windows下驱动调试
Windows下驱动程序的调试有若干不同的方法。最简单的就是使用DbgPrint调试例程将消息打印到Windows调试器缓冲区。比如使用Windbg调试器时,可从调试器界面看到那些消息,否则需要一个特定的程序从调试器缓冲区接收那些消息,如SysInternals公司免费提供的DebugView程序[Russinovich, 01]。DbgBreakPoint例程在程序中设置一个断点,当被执行时,系统停下来并将驱动执行代码传递给系统调试器。Assert宏基于条件测试的结果,将驱动执行转移到系统调试器。使用Microsoft内核调试器Windbg,需要两个PC。第一个PC是驱动代码的开发和调试机器,第二个PC通过串口连到开发驱动的PC。开发者使用第二个PC,通过串口控制台连接到第一个PC,就能和在第一个PC上的调试器交互。NuMega DriverStudio ™ [Compuware, 01]提供的调试器允许驱动调试在单个PC内部,这个PC可以作为驱动开发机器,并作为应用调试器。它提供了一个console窗口,命令行可从这里输入以便进行控制。
Linux下驱动调试
和Windows一样,Linux驱动调试可使用内核提供的调试例程printk,对应于Windows的DbgPrint例程。它和C的标准I/O例程printf类似,只是需要一个额外的参数来指定消息将被打印到的位置。内核调试器可作为内核源码的一个patch被获取到。Linux内核调试器(kdb)的patch可从KDB项目页面获取[KDB, 02]。它允许标准调试器一样的操作,即设置断点、单步执行驱动代码和观察驱动内存。
总结
Windows和Linux是当今最为普遍流行的操作系统。Windows的市场份额最大,Linux知名度则在不断增长。硬件设备制造商每发布一种新设备,都配备有一个能使新设备在Windows下使用的驱动程序。两种操作系统的驱动架构有很多的不同,但也有一些类似的地方。
设备驱动架构
通过对两种操作系统驱动架构的比较,可以看到Windows系统的架构更为成熟。这并不意味着Windows架构提供更好的功能,而是它有一个定义更为良好的的驱动模型让驱动开发者去遵循。虽然驱动程序的编写者可以忽视Windows驱动模型开发自己的驱动,但很少有驱动开发者这么做。Linux下没有正式定义的驱动模型。Linux驱动程序编写者基于他们个人的设计来开发驱动。除非两个驱动开发组合作来开发能一起工作的驱动程序,不同开发者开发的驱动程序在Linux系统下不能协同工作。Windows下,两个或多个驱动开发组开发的驱动可以协同工作,只要他们都遵循WDM来构建驱动程序。Windows驱动架构支持PnP和电源管理,是通过在适当的时候将这些消息发送到实现了消息处理函数的驱动。目前的Linux驱动架构没有提供这样的机制。
设备驱动程序设计
设计驱动程序时应该对操作系统提供的辅助设施进行评估。Windows和Linux是两个现代化的操作系统。它们提供了对数据结构如栈(stack)、队列(queue)和自旋锁(spin locks),还有完成硬件无关操作的硬件抽象层(HAL,Hardware Abstraction Layer)例程的实现。这使得驱动程序可以在不同的处理器架构下运行,如IA64 (Intel’s 64 bit platform) 和SPARC。两种操作系统下的驱动程序都可以被分成多个模块,然后以栈式结构堆叠,使用标准化的数据结果进行通信。Windows这种标准化的数据结构就是IRP,Linux下则可是任何驱动程序自定义的结构,因为操作系统没有提供任何标准化的结构。
设备驱动程序实现
两种操作系统下的驱动程序都由一系列各操作系统期望驱动程序实现的例程组成。包括标准I/O如读写设备的例程、发送I/O控制命令到设备的例程。两种系统中,每个驱动程序都要实现一个驱动被加载时执行的例程和驱动被卸载时执行的例程,不同驱动程序的例程可使用相同的名称,但一般来说每种操作系统都使用惯用的命名方式。Windows下设备驱动的的命名方式(设备接口)比当前Linux下设备驱动的命名方式更便捷。Linux使用GUID为设备命名,相比Windows,驱动名称冲突的现象在Linux下更易于出现。
驱动程序开发环境
Windows操作系统提供DDK,其中包含相关的文档和开发工具,大大减少了开发驱动需要的学习时间。Linux下没有DDK,因此刚开始时设备驱动开发者需要收集其他的一些资源来辅助驱动的开发。一旦花时间熟悉了两种驱动开发环境后,开发者会发现创建Linux驱动程序比创建Windows驱动程序更容易,因为所有的Linux内核源码对他们是可见的。这使得驱动开发者能更深入到他们的驱动程序所依赖的内核代码中去跟踪解决驱动程序的问题。Windows,只有debug版二进制组件可用,里面包含调试符号如函数名称和变量名,但不如拥有操作系统源码的用处那么大。
结束语
驱动程序应该被设计成需要终端用户很少的交换就能使用,并且应用程序可以访问驱动的所有功能。第一点是Windows的一个要点,它支持了PnP。Linux是一个开源项目,它还在不断地改进中。以后Linux的驱动架构很有可能像Windows驱动架构那样正式化,如具有一个WDM那样的驱动模型。随着越来越多的个人和组织采用了Linux,支持Linux的硬件厂商也会增加。
致谢
This research was made possible through the Andrew Mellon Foundation scholarship at Rhodes University,
Grahamstown, South Africa.
参考书目
[Beck et al, 98] Beck, Bohme, Dziadzka, Kunitz, Magnus, Verworner, Linux Kernel Internals,
Addison Wesley, 1998.
[Cant C, 99] Cant C, Writing Windows WDM Device Drivers, CMP Books, 1999.
[Compuware, 01] Compuware, NuMega DriverStudio Version 2.5, http://www.compuware.com, 2001.
[Compuware, 01] Compuware, Using Driver Works, Version 2.5, Compuware, 2001.
[Deitel, 90] Deitel HM, Operating Systems, 2nd Edition, Addison Wesley, 1990.
[Davis, 83] Davis W, Operating Systems, 2nd Edition, Addison Wesley, 1983.
[Flynn et al, 91] Flynn IM, McHoes AM, Understanding Operating Systems, Brooks/Cole, 1991.
[Katzan, 73] Katzan H, Operating Systems: A Pragmatic Approach, Reinhold, 1973.
[KDB, 02] KDB, The Linux Built in Kernel Debugger, http://oss.sgi.com/projects/kdb, 2002.
[Laywer, 01] Lawyer D S, Plug and Play HOWTO/Plug-and-Play-HOWTO-1.html,
http://www.tldp.org/HOWTO, 2001.
[Linus FAQ, 02] The Rampantly Unofficial Linus Torvalds FAQ,
http://www.tuxedo.org/~esr/faqs/linus/index.html, 2002.
[Linux HQ, 02] The Linux Headquarters, http://www.linuxhq.com, 2002.
[Lorin et al, 81] Lorin H, Deitel HM, Operating systems, Addison Wesley, 1981.
[Microsoft DDK, 02] Microsoft ,DDK- Kernel Mode Driver Architecture, Microsoft, 2002.
[Microsoft WDM, 02] Microsoft, Introduction to the Windows Driver Model,
http://www.microsoft.com/hwdev/driver/wdm, 2002.
[Oney, 99] Oney W, Programming the Microsoft Windows Driver Model, Microsoft, 1999.
[Open Group, 97] Open Group, Universal Unique Identifier,
http://www.opengroup.org/onlinepubs/9629399/apdxa.htm, 1997.
[Redhat,02] Redhat, http://www.redhat.com,2002.
[Rubini et al, 01] Rubini A, Corbet J, Linux Device Drivers, 2nd Edition, Oreilly, 2001.
[Russinovich, 98] Russinovich M, Windows NT Architecture,
http://www.winnetmag.com/Articles/Index.cfm?ArticleID=2984, 1998.
[Russinovich, 01] Russinovich M, SysInternals, http://www.sysinternals.com, 2001.
[Rusling, 99] Rusling D A, The Linux Kernel, http://www.tldp.org/LDP/tlk/tlk.html, 1999.