Windows之磁盘的设备驱动堆叠

 磁盘的设备驱动堆叠

本文节选自《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理解成对于“磁盘”这个子类的驱动,则也还可以说得通。
    现在我们可以读代码了。

你可能感兴趣的:(Windows之磁盘的设备驱动堆叠)