5.3.2 设备驱动程序

在本章前面的内容中,我们介绍了设备控制器所做的工作。我们注意到每一个控制器都设有某些设备寄存器用来向设备发出命令,或者设有某些设备寄存器用来读出设备的状态,或者设有这两种设备寄存器。设备寄存器的数量和命令的性质在不同设备之间有着根本性的不同。例如,鼠标驱动程序必须从鼠标接收信息,以识别鼠标移动了多远的距离以及当前哪一个键被按下。相反,磁盘驱动程序可能必须要了解扇区、磁道、柱面、磁头、磁盘臂移动、电机驱动器、磁头定位时间以及所有其他保证磁盘正常工作的机制。显然,这些驱动程序是有很大区别的。

因而,每个连接到计算机上的I/O设备都需要某些设备特定的代码来对其进行控制。这样的代码称为设备驱动程序(device driver),它一般由设备的制造商编写并随同设备一起交付。因为每一个操作系统都需要自己的驱动程序,所以设备制造商通常要为若干流行的操作系统提供驱动程序。

每个设备驱动程序通常处理一种类型的设备,或者至多处理一类紧密相关的设备。例如,SCSI磁盘驱动程序通常可以处理不同大小和不同速度的多个SCSI磁盘,或许还可以处理SCSI CD-ROM。而另一方面,鼠标和游戏操纵杆是如此的不同,以至于它们通常需要不同的驱动程序。然而,对于一个设备驱动程序控制多个不相关的设备并不存在技术上的限制,只是这样做并不是一个好主意。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

为了访问设备的硬件(意味着访问设备控制器的寄存器),设备驱动程序通常必须是操作系统内核的一部分,至少对目前的体系结构是如此。实际上,有可能构造运行在用户空间的驱动程序,使用系统调用来读写设备寄存器。这一设计使内核与驱动程序相隔离,并且使驱动程序之间相互隔离,这样做可以消除系统崩溃的一个主要源头——有问题的驱动程序以这样或那样的方式干扰内核。对于建立高度可靠的系统而言,这绝对是正确的方向。MINIX 3就是一个这样的系统,其中设备驱动程序就作为用户进程而运行。然而,因为大多数其他桌面操作系统要求驱动程序运行在内核中,所以我们在这里只考虑这样的模型。

因为操作系统的设计者知道由外人编写的驱动程序代码片断将被安装在操作系统的内部,所以需要有一个体系结构来允许这样的安装。这意味着要有一个定义明确的模型,规定驱动程序做什么事情以及如何与操作系统的其余部分相互作用。设备驱动程序通常位于操作系统其余部分的下面,如图5-12所示。

阅读 ‧ 电子书库
图 5-12 设备驱动程序的逻辑定位。实际上,驱动程序和设备控制器之间的所有通信都通过总线

操作系统通常将驱动程序归类于少数的类别之一。最为通用的类别是块设备(block device)和字符设备(character device)。块设备(例如磁盘)包含多个可以独立寻址的数据块,字符设备(例如键盘和打印机)则生成或接收字符流。

大多数操作系统都定义了一个所有块设备都必须支持的标准接口,并且还定义了另一个所有字符设备都必须支持的标准接口。这些接口由许多过程组成,操作系统的其余部分可以调用它们让驱动程序工作。典型的过程是那些读一个数据块(对块设备而言)或者写一个字符串(对字符设备而言)的过程。

在某些系统中,操作系统是一个二进制程序,包含需要编译到其内部的所有驱动程序。这一方案多年以来对UNIX系统而言是标准规范,因为UNIX系统主要由计算中心运行,I/O设备几乎不发生变化。如果添加了一个新设备,系统管理员只需重新编译内核,将新的驱动程序增加到新的二进制程序中。

随着个人计算机的出现,这一模型不再起作用,因为个人计算机有太多种类的I/O设备。即便拥有源代码或目标模块,也只有很少的用户有能力重新编译和重新连接内核,何况他们并不总是拥有源代码或目标模块。为此,从MS-DOS开始,操作系统转向驱动程序在执行期间动态地装载到系统中的另一个模型。不同的操作系统以不同的方式处理驱动程序的装载工作。

设备驱动程序具有若干功能。最明显的功能是接收来自其上方与设备无关的软件所发出的抽象的读写请求,并且目睹这些请求被执行。除此之外,还有一些其他的功能必须执行。例如,如果需要的话,驱动程序必须对设备进行初始化。它可能还需要对电源需求和日志事件进行管理。

许多设备驱动程序具有相似的一般结构。典型的驱动程序在启动时要检查输入参数,检查输入参数的目的是搞清它们是否是有效的,如果不是,则返回一个错误。如果输入参数是有效的,则可能需要进行从抽象事项到具体事项的转换。对磁盘驱动程序来说,这可能意味着将一个线性的磁盘块号转换成磁盘几何布局的磁头、磁道、扇区和柱面号。

接着,驱动程序可能要检查设备当前是否在使用。如果在使用,请求将被排入队列以备稍后处理。如果设备是空闲的,驱动程序将检查硬件状态以了解请求现在是否能够得到处理。在传输能够开始之前,可能需要接通设备或者启动马达。一旦设备接通并就绪,实际的控制就可以开始了。

控制设备意味着向设备发出一系列命令。依据控制设备必须要做的工作,驱动程序处在确定命令序列的地方。驱动程序在获知哪些命令将要发出之后,它就开始将它们写入控制器的设备寄存器。驱动程序在把每个命令写到控制器之后,它可能必须进行检测以了解控制器是否已经接收命令并且准备好接收下一个命令。这一序列继续进行,直到所有命令被发出。对于某些控制器,可以为其提供一个在内存中的命令链表,并且告诉它自己去读取并处理所有命令而不需要操作系统提供进一步帮助。

命令发出之后,会牵涉两种情形之一。在多数情况下,设备驱动程序必须等待,直到控制器为其做某些事情,所以驱动程序将阻塞其自身直到中断到来解除阻塞。然而,在另外一些情况下,操作可以无延迟地完成,所以驱动程序不需要阻塞。在字符模式下滚动屏幕只需要写少许字节到控制器的寄存器中,由于不需要机械运动,所以整个操作可以在几纳秒内完成,这便是后一种情形的例子。

在前一种情况下,阻塞的驱动程序可以被中断唤醒。在后一种情况下,驱动程序根本就不会休眠。无论是哪一种情况,操作完成之后驱动程序都必须检查错误。如果一切顺利,驱动程序可能要将数据(例如刚刚读出的一个磁盘块)传送给与设备无关的软件。最后,它向调用者返回一些用于错误报告的状态信息。如果还有其他未完成的请求在排队,则选择一个启动执行。如果队列中没有未完成的请求,则该驱动程序将阻塞以等待下一个请求。

这一简单的模型只是现实的粗略近似,许多因素使相关的代码比这要复杂得多。首先,当一个驱动程序正在运行时,某个I/O设备可能会完成操作,这样就会中断驱动程序。中断可能会导致一个设备驱动程序运行,事实上,它可能导致当前驱动程序运行。例如,当网络驱动程序正在处理一个到来的数据包时,另一个数据包可能到来。因此,驱动程序必须是重入的(reentrant),这意味着一个正在运行的驱动程序必须预料到在第一次调用完成之前第二次被调用。

在一个热可插拔的系统中,设备可以在计算机运行时添加或删除。因此,当一个驱动程序正忙于从某设备读数据时,系统可能会通知它用户突然将设备从系统中删除了。在这样的情况下,不但当前I/O传送必须中止并且不能破坏任何核心数据结构,而且任何对这个现已消失的设备的悬而未决的请求都必须适当地从系统中删除,同时还要为它们的调用者提供这一坏消息。此外,未预料到的新设备的添加可能导致内核重新配置资源(例如中断请求线),从驱动程序中撤除旧资源,并且在适当位置填入新资源。

驱动程序不允许进行系统调用,但是它们经常需要与内核的其余部分进行交互。对某些内核过程的调用通常是允许的。例如,通常需要调用内核过程来分配和释放硬接线的内存页面作为缓冲区。还可能需要其他有用的调用来管理MMU、定时器、DMA控制器、中断控制器等。