Modern Operating System 4th 中译

I/O设备

CPU和内存并不是操作系统必须管理的唯一资源。I/O设备也占了操作系统很大的比重。如图1-6,I/O设备通常包含两部分:控制器和设备本身。控制器是一枚或一系列用于控制设备的芯片。它从操作系统接受命令,例如从设备读取数据,并且执行它们。

许多情况下,设备的实际控制是非常复杂繁琐的,控制器的工作就是为操作系统提供一个简单(事实上仍然非常复杂)的接口。例如磁盘控制器接收的命令可能是读取磁盘2的11206扇区。控制器必须将这个线性扇区地址转换成柱面,扇区,磁头。由于外圈柱面比内圈有更多的扇区,以及某些坏扇区被重映射至其他扇区等,这个转换可能会非常复杂。之后控制器必须判断当前磁头臂在哪个柱面,然后命令它移动到目标柱面。之后必须等待要读取的扇区旋转至磁头下,之后需要在它转离磁头前完成读写和校验。最后将数据组合成字存入内存。为完成这些工作,控制器一般包含一个小的嵌入式计算机专门完成这项工作。

另一部分是设备本身。设备的接口非常简单,一方面因为它们并不能做复杂的工作,另一方面也因为这是标准需要。后者非常重要,这样一个SATA磁盘控制器就可以控制任何一个SATA磁盘。SATA代表Serial ATA,ATA代表AT Attachment。顺便一提AT标准,这是IBM于1984年推出的第二代“Personal Computer Advanced Technology”,基于6MHz的80286处理器。我们从中可以看出计算机厂商总喜欢在现有的缩略词上缝缝补补。我们也可以注意到一些形容词,比如“advanced”应当极其谨慎地使用,否则30年后看起来就会非常愚蠢。

SATA是目前计算机磁盘接口的标准。因为真实的设备接口被控制器隐藏,所以操作系统看到的是控制器的接口,这可能和设备本身的接口有很大区别。

因为每种控制器都是不一样的,所以就需要不同的软件负责和控制器交互,发送命令并且读取回复。这种软件称为设备驱动(device driver)。每个控制器厂家都提供针对每个支持的操作系统的驱动。因此一台扫描仪可能有OS X,Windows 7,Windows 8和Linux的驱动程序。

为使用驱动程序,它就必须被放入操作系统以运行在内核模式。事实上驱动程序也可以运行在内核外,操作系统如Linux和Windows都对这种做法提供了一些支持,但是驱动的大部分依旧运行在内核。只有极少数的操作系统,例如MINIX 3,会把所有的驱动程序运行在用户态。用户态的驱动程序必须有办法在受控的情况下访问硬件,这种方法并不简洁。

将驱动放入内核有三种方式。第一种办法就是将内核与新驱动程序重新链接,再重启系统。许多老旧的UNIX系统使用这种方式。第二种是在操作系统中创建一个文件告诉系统需要一个新驱动,之后重启系统。在系统引导时,操作系统找到需要的驱动程序并且载入它们。Windows采用这种方式。第三种方式是操作系统可以在运行时动态无缝安装新的驱动程序而无须重新启动。尽管使用这种方式的不多但是现在正在逐渐普及。热插拔设备,例如USB或者IEEE 1394总线设备(下文将讨论)需要操作系统有动态加载驱动的能力。

每个控制器都有一系列用于通讯的寄存器。例如,一个最简单的磁盘控制器有用于区分磁盘地址,内存地址,扇区数和方向(读or写)的寄存器。如果需要激活控制器,驱动程序从操作系统处接受命令,翻译成合适的寄存器值并且写入设备寄存器。所有的设备寄存器组成I/O接口空间,我们将会在第五章介绍。

在一些计算机中,设备寄存器被映射到操作系统的地址空间,这样访问它们就可以像访问普通内存一样。在这种计算机中不需要特殊的I/O指令,并且用户程序可以通过不允许访问这些内存地址(例如,通过使用基地址和限制寄存器)的方式来禁止其对硬件的访问。在另外的电脑上,设备寄存器被放在专门的I/O接口空间,每个寄存器都有专门的接口地址。在这些机器上需要专门的IN和OUT指令允许驱动程序在内核态下访问寄存器。前者不需要专门的I/O指令但是耗费了一些地址空间,后者不使用地址空间但是需要专门的指令。两种方式都被广泛采用。

输入输出任务有三种完成方式。最简单的办法是,当一个应用程序发起系统调用时,内核将其翻译为对合适驱动程序的调用。之后驱动程序开始读写并且不停地查询操作是否完成(一般有专门的bit指示设备是否忙碌)。当I/O操作完成后,驱动程序得到数据并且返回。之后操作系统将控制权返还调用程序。这种方式被称为忙等待(busy waiting)并且有很明显的缺点,就是在操作完成之前会一直占用CPU。

第二种方式是驱动程序发起I/O,并且要求操作完成后给出中断信号。在驱动程序返回之前,操作系统阻塞调用程序,并且去做别的事情。当控制器检测到传输完成后,它产生一个完成的中断。

在操作系统中,中断机制非常重要。因此我们有必要更详细解释一下。图1-11(a)显示了一个三步的I/O处理过程。第一步,驱动程序通过写入设备寄存器告诉控制器需要做什么。之后控制器开始操作设备。第二步,控制器完成读取或写入工作之后就会通过特定的总线发送信号给中断控制器。第三步,如果中断控制器可以接受中断(可能由于正在处理优先级更高的中断而繁忙),它就会通过处理器的一个引脚通知CPU。在第四步,中断控制器告诉CPU产生中断的设备号,这样CPU可以知道哪个设备产生了中断(可能同时运行着多个设备)。

当CPU决定处理中断时,当前的PC和PSW一般被压入当前的栈并且CPU进入内核模式。设备号可能被用于设备中断例程(interrupt handler)内存地址的索引。这部分内存被称为中断向量(interrupt vector)。开始中断例程(产生中断设备的驱动程序的一部分)后,它会移除栈中的PC和PSW并且保存它们,再查询设备状态。中断例程最终完成后,系统返回原来的用户程序将要运行的指令。如图1-11(b)所示。

第三种I/O操作方式需要特定的硬件:一个DMA(Direct Memory Access)芯片,它可以不经CPU而控制内存与某些控制器间的数据流。CPU设置好DMA芯片,设定好需要传送的比特数,涉及到的设备和内存地址和方向,之后就放任不管。DMA芯片完成工作后产生一个中断,然后同上处理。DMA和I/O硬件会在第五章详述。

有时(事实上经常)中断可能在一个非常不合适的时间产生,例如,当另一个中断例程正在运行时。因此,CPU可以选择关闭中断,之后再重新打开。中断关闭时,任何完成的设备不停地产生中断信号,但是直至CPU重新使能中断之前都不会得到响应。如果中断关闭时多个设备请求中断,中断控制器就必须决定首先通过哪个,通常取决于每个设备所静态指定的优先级。优先级高的设备最先得到响应,其他设备必须等待。