磁盘的设备驱动堆叠
本文节选自《Windows 内核情景分析--采用开源代码ReactOS》一书
读者已经在前几节中看到,设备的驱动常常分成“类设备驱动”和“端口设备驱动”两层。例如鼠标器就成为一个设备的类,而具体又有PS/2鼠标器、串口鼠标 器以及基于USB的HID鼠标器,所以鼠标器的驱动就分为一种类设备驱动和三种端口设备驱动。其中PS/2鼠标器的端口驱动是直接与硬件打交道的。不过端 口驱动也可能不直接驱动硬件,而只是对虚拟的硬件进行操作。HID鼠标器的端口驱动就是这样,因为它与实际的硬件之间还隔着USB总线这一层,因而需要把 HID的端口设备驱动堆叠在USB总线驱动的上面,USB总线驱动下面又是USB的类驱动和端口驱动。至于类设备驱动与端口设备驱动之间的关系,则既可以 是一对一的,也可以是一对多的。
类设备驱动与端口设备驱动的划分有三方面的好处。一方面,像鼠标器驱动那样,可以把不同种类鼠标器驱动中的公共部分抽取出来,避免开发鼠标器驱动时的重复 劳动。这种重复劳动不仅是浪费的问题,而且还容易引起差错和不兼容。另一方面,即使实际上某种设备并不成为“类”,把本来可以是“整块式”的设备驱动按其 逻辑和机理分成两块也有好处,因为那样可以增进软件的模块化,从而使这些模块的来源多样化。再说,类设备驱动与端口设备驱动的划分,以及这二者在接口界面 形式上的规范性,还使得按需要在二者之间插入“过滤”模块成为可能。
所以类设备驱动与端口设备驱动的划分是个重要的概念,也是一项重要的技术。
但是,就磁盘的驱动而言,类驱动和端口驱动的分离仍旧是不充分的,因为磁盘有逻辑和物理之分,逻辑磁盘很可能就是物理磁盘上的一个分区,还可能实际上不是 磁盘。所以,在这样的条件下,还会把类驱动再分成两层,成为上下两个设备对象,上面的称为“功能(性)设备驱动(Functional Device Object)”即FDO,下面的称为“物理设备驱动(Physical Device Object)”即PDO。不过PDO未必就是直接与物理设备打交道的设备对象,而是(向上)代表着某种物理设备的设备对象,“代表着”不一定就是“直接 操作着”。有时候,FDO和PDO实际上就只是“上、下”之分。那么,端口设备驱动是否还可以进一步分解呢?
对于同一种设备,来自不同厂家的硬件接口也会有些不同,但是这种不同只是局部的、少量的,一般都集中在底层直接与硬件有关的地方,而离具体的硬件接口稍远 就又都一样了。以磁盘为例,首先“块存储设备”形成一个类,属于这个类的有磁盘、磁带、光盘,可能还有U盘、Ramdisk等。而磁盘又有逻辑和物理之 分。因为一个物理的磁盘可以分成好几个分区,而每个分区就是一个逻辑的磁盘。然后,物理的磁盘又有IDE磁盘和SCSI磁盘之分。最后,即使同为IDE磁 盘,不同厂家的产品也会有些不同和特殊之处,例如IDE接口的寄存器可以表现在只能通过in、out指令访问的I/O地址空间,也可以表现在通过mov指 令访问的内存地址空间;有些厂家还提供IDE磁盘阵列,有些则可能还有特殊的操作要求等。如果要求每个磁盘厂家都必须提供全套的驱动即整个磁盘驱动堆叠, 那当然不现实。即使只要求提供整个端口驱动,那也会造成许多重复开发,并且也对磁盘厂家提出了更高的要求,增加了许多负担。而如果能把其中公共的部分提取 出来,做成一个共用的模块,使磁盘厂家可以只做与具体产品密切相关的那一部分驱动程序,那么磁盘厂家的负担就可以降到最低。这就是Miniport驱动的 由来,Mini既有“小”的意思,也有“最小化(Minimalized)”的意思。而提取出来的公共部分,则仍称为Port驱动,本质上就是操作系统内 核的一部分,所以一般是由微软自己提供的。“Miniport”这个词,按字面意义称之为“小端口”或“最小端口”固然可以,但也不尽合适,因为没有反映 出这种驱动模块的本质;笔者觉得称之为“末梢端口”或许还可以,因为Miniport都是在最底层直接与硬件打交道的。
再看上下层设备对象之间的界面。在“类驱动+端口驱动”的模型中,二者之间的界面就是常规的由IRP和IoCallDriver()所构成的界面。当然, 载运在IRP上的数据或数据结构是根据具体情况而定的,这需要上下两个模块之间有个协议。而若把类驱动分为FDO和PDO两层,则两层之间的交互一般远较 类驱动与端口驱动之间的交互更为复杂和紧密,此时光靠由IRP和IoCallDriver()构成的界面就不够了。为此,在类驱动的FDO和PDO之间往 往有另外一个界面,要由下层向上层提交一个数据结构,通过该数据结构向上层“登记”有关的函数指针和数据。可是这样一来又有了问题,因为下层驱动模块的装 入理应在上层模块之前,既然如此,那下层模块又如何向尚未装入的上层模块登记呢?于是,FDO中又得划分出一部分,这一部分是需要在下层模块之前装入的, 起着PDO与FDO之间的中介作用,使下层模块在初始化时可以预先登记。这一部分当然是个可以独立装入的模块,但是却不创建自己的设备对象,因为它的设备 对象(如果要说实质上有的话)就是其所属的类驱动的设备对象,要到装入时才会创建。如果我们以是否有设备对象为依据把设备驱动模块分成“有形”和“无形” 两种,那么这就是属于无形的设备驱动模块。就其本质而言,无形的驱动模块就相当于系统空间的DLL,实质上成为内核的扩充。此外,应用软件在打开某个设备 时只能以(有命名的)设备对象为目标,所以无形的驱动模块不能作为应用软件的打开目标。
实际上,从类驱动中划分出一个无形的模块并不只是为了解决下层模块(PDO)向上层登记的问题,这里面也有“提取公因子”的问题。比方说,磁盘本身构成一 类设备即磁盘类,但是同时它又属于一个更大的类即块存储设备类;而磁带类同样也属于块存储设备类。这二者显然存在着一些共性。如果分别加以实现,则磁盘的 类驱动和磁带的类驱动中势必有一部分程序是共同的。既然有共同的部分,那就不如把它提取出来。事实上,classpnp.sys就是从Windows的块 存储设备的类驱动中抽取出来的公共部分。
类似的原理也适用于端口驱动与小端口驱动之间,这二者的设备对象之间的界面既保留了IRP+ IoCallDriver(),又增加了基于由下层向上层“登记”的扩充界面。
所以,Windows的这个以IRP和IoCallDriver()为特征的模型,简洁固然是简洁,实际上却并不能贯彻始终,对于比较复杂的设备就不能愉快胜任了。而由下层向上层登记一个数据结构,以提供函数指针和数据,则正是Linux所采用的方法。
可以这样来理解,基于IRP和IoCallDriver()的设备对象堆叠构成特定设备驱动的骨架,体现着总体上的层次关系;但是此骨架中的某些层次又可 以进一步划分成“子层”,子层之间的界面并不完全遵循Irp+IoCallDriver()的模型。不过,“过滤驱动(Filter Driver)”只能插入在堆叠中的骨干层次之间。
下面介绍SCSI磁盘(不包括光盘)驱动的堆叠,但是我们把注意力集中在堆叠的构成以及类、端口、小端口驱动的相互关系,而不是集中在具体的操作细节上, 因为许多细节是因具体硬件而异的。我们假定所用的是SCSI磁盘,而插在计算机总线上的接口即“适配器(Adapter)”,是Adaptec的 1540B接口板。DDK提供了这种接口板的小端口驱动,但是却并未提供SCSI的端口驱动,所以下面的有些代码取自DDK,有些却取自ReactOS。 而在类驱动这一层上,DDK倒是既提供了disk.sys的代码,也提供了classpnp.sys的代码。
对于SCSI磁盘,其驱动模块堆叠的构成为:
disk.sys。这是磁盘的类驱动,分为FDO和PDO两个子层,这两个子层各有自己的设备对象。二者的结合把对于逻辑磁盘的操作映射到对于物理磁盘的 操作。如前所述,磁盘和磁带等同属于块存储设备类,所以部分本来可以放在disk.sys中的公共代码被提取了出来,成为classpnp.sys。 DDK提供了这个模块的源码。
classpnp.sys。这是个无形的模块,没有自己的设备对象,只是起着函数库的作用。DDK提供了这个模块的源码。
scsiport.sys。SCSI磁盘的端口驱动,这又是个无形的模块,没有自己的设备对象。DDK并未提供这个模块的源码,但是ReactOS已经实现了这个模块。
aha154x.sys。这是Adaptec 1540B接口板的小端口驱动。DDK提供了这个模块的源码。
disk.sys和classpnp.sys这两个驱动模块合在一起相当于一个类驱动模块,可是分成两块以后称为什么呢?在DDK的源码中,前者的路径是 src"storage"class"disk,似乎应该算是类驱动;可是后者既然名为classpnp.sys,就更应该是类驱动。然而,这二者的特性 显然不同,前者是有形的,其所创建的设备对象在设备对象堆叠中,而后者只是个无形的函数库,可见微软对于什么样的驱动是类驱动并无明确的定义。不过,从后 面的代码中可以看出,如果把classpnp.sys理解成对于“块设备”这个大类的驱动,而把disk.sys理解成对于“磁盘”这个子类的驱动,则也 还可以说得通。
现在我们可以读代码了。