#### IO概述

操作系统的一个主要功能就是控制I/O设备，I/O代码占了操作系统相当大的部分。操作系统必须向设备发出命令、捕获中断，处理错误。操作系统需要在设备和系统的其它部分之间提供接口，这个接口应该简单易用。在可能的范围内，所有设备的接口都应是一样的(设备无关性)。

#### I/O软件的原理

##### 中断处理例程

中断是生活中令人不快的事实。尽管无法避免，但它们应该被隐藏起来，深藏在操作系统的深处，让操作系统尽可能少的部分知道它们。隐藏它们的最好方法是让驱动程序启动一个I/O操作的时候阻塞，直到I/O完成然后中断发生。驱动程序可以通过信号量的down操作、条件变量的wait操作、消息的receive操作，或类似的操作来阻塞自己。

当中断发生时，中断过程做任何它必须做的事情来处理中断。然后它可以对启动它的驱动程序解除阻塞。在某些情况下，它只是在一个信号量上完成up操作。在其他情况下，它在管程的条件变量上执行signal操作。还有一些其他情况下，它会向被阻塞的驱动发送一条消息。在所有情况下，中断的最终效果将是之前被阻塞的驱动程序现在能够运行。如果驱动程序的结构是独立的进程，有它们自己的状态、堆栈和程序计数器，这个模型就会发挥最好的作用。

##### 设备驱动程序

每个设备控制器都有寄存器，用于向它发出命令或读出它的状态，或两者兼而有之。寄存器的数量和命令的性质因设备而异。

因此，每个连接到计算机的I/O设备都需要一些特定于设备的代码来控制它。这些代码被称为**设备驱动程序**，通常由设备制造商编写，并随设备一起放在CD-ROM中。由于每个操作系统都需要自己的驱动程序，设备制造商通常会为几种流行的操作系统提供驱动程序。

每个设备驱动程序通常处理一种设备类型，或一类密切相关的设备。

为了访问设备的硬件(即控制器的寄存器)，设备驱动程序通常是系统内核的一部分。这种方法提供了最好的性能和最差的可靠性，因为任何设备驱动程序中的一个bug都可能导致整个系统崩溃。为了提高可靠性，minix3脱离了这个模型。在minix3中，每个设备驱动程序现在都是一个单独的用户模式进程。

操作系统通常将驱动程序分为**块设备**(如磁盘)或**字符设备**(如键盘和打印机)。大多数操作系统都定义了一个所有块驱动程序都必须支持的标准接口，和一个所有字符驱动程序都必须支持的另一个标准接口。这些接口由许多过程（procedures）组成，操作系统的其他部分可以调用这些过程来让驱动程序为其工作。

一般来说，设备驱动程序的工作是接受来自其上层与设备无关的软件的抽象请求，并确保请求被执行。对磁盘驱动器的一个典型请求是读取块n。如果在请求进入时驱动器是空闲的，它会立即开始执行请求。但是，如果它已经忙于处理某个请求，它通常会将新请求放入待处理请求队列中，以便尽快处理。

实际执行I/O请求的第一步是检查输入参数是否有效，如果无效则返回错误。如果请求有效，下一步是将其从抽象术语转换为具体术语。简而言之，驱动程序必须决定需要哪些控制器操作以及操作的顺序。

一旦驱动程序确定了要向控制器发出哪些命令，它就开始通过写入控制器的设备寄存器来发出这些命令。简单控制器一次只能处理一个命令。更复杂的控制器则接受一串命令，然后它们自己执行这些命令，而无需操作系统的进一步帮助。

发出一个或多个命令后，将产生两种情况。在许多情况下，设备驱动程序必须等待直到控制器为它做一些工作，所以它阻塞自己，直到中断进来解除阻塞。然而，在其他情况下，操作不会延迟完成，因此驱动程序不需要阻塞。作为后一种情况的例子，在某些显卡上滚动屏幕只需要向控制器寄存器写入几个字节。不需要机械运动，因此整个操作可以在几微秒内完成。

在上面的操作完成后，则必须检查错误。如果一切正常，驱动程序可能有数据传递给设备无关的软件(例如，一个刚刚读出的块)。最后，它返回一些状态信息，以便向调用者报告错误。如果有任何其他请求正在排队，现在可以选择并启动其中一个请求。如果队列中没有请求，驱动程序将阻塞等待下一个请求。

处理读写请求是驱动程序的主要功能，但可能还有其他需求。例如，驱动程序可能需要在系统启动或第一次使用时初始化设备。此外，可能需要管理电源需求、处理即插即用设备或记录事件。

##### 与设备无关的I/O软件

大部分的软件都是设备无关的，Minix3里的大部分设备无关的软件都是文件系统的一部分。设备无关软件的典型功能有：

- 设备驱动程序的统一接口

  有了标准接口，只要符合驱动程序接口，插入新驱动程序就会容易得多。

  使用统一接口的另一个方面是I/O设备的命名方式。设备无关软件负责将符号设备名称映射到正确的驱动程序上。例如，在UNIX和minix3中，一个设备名(例如/dev/disk0)是一个有唯一索引结点号的特殊文件，这个索引结点包含**主设备号**，用于定位适当的驱动程序。它还包含**次设备号**，它作为参数传递给驱动程序，以便指定要读取或写入的单元。所有设备都有主号和次号，所有驱动都通过主号来选择驱动。

  与命名密切相关的是保护。设备在文件系统中以命名对象的形式出现，这意味着通常的文件保护规则也适用于I/O设备。

- 缓冲

  缓冲区对于块设备和字符设备也是一个问题。对于块设备，硬件通常坚持一次读写整个块，但用户进程可以自由地读写任意单位的块。如果一个用户进程写了半个块，操作系统通常会在内部保存数据，直到其余的数据被写完，此时块可以放到磁盘上。对于字符设备，用户向系统写入数据的速度要快于其输出速度，这就需要缓冲。在需要之前到达的键盘输入也需要缓冲。

- 错误报告

  错误在I/O上下文中比在其他任何上下文中都要常见得多。当它们发生时，操作系统必须尽可能地处理它们。许多错误是特定于设备的，所以只有驱动程序知道要做什么(例如，重试，忽略，或恐慌)。一个典型的错误是由磁盘块损坏而不能再读取引起的。当驱动尝试读取该块一定次数后，它放弃并通知与设备无关的软件。从这里开始如何处理错误是与设备无关的。如果在读取用户文件时发生了错误，那么向调用方报告错误就足够了。但是，如果它发生在读取关键的系统数据结构时，比如读取包含位图的块，在这个位图上显示哪些块是空闲的，操作系统可能不得不显示一条错误消息并终止。

- 专用设备的分配与释放

  有些设备，如CD-ROM录音机，在任何给定时刻只能由单个进程使用。操作系统需要检查设备使用请求，并根据设备是否可用来接受或拒绝这个请求。处理这些请求的一种简单方法是要求进程对这个特殊文件执行open操作。如果设备不可用，则open失败。关闭这样一个专用设备，然后释放它。

- 提供设备无关的块大小

  并非所有磁盘都具有相同的扇区大小。这是由独立于设备的软件来隐藏这一事实，并向更高的层提供统一的块大小，例如，通过将几个扇区视为单个逻辑块。通过这种方式，较高层只处理所有使用相同逻辑块大小的抽象设备，与物理扇区大小无关。类似地，一些字符设备每次传送一个字节的数据(例如，调制解调器)，而其他设备以更大的单位传送数据(例如，网络接口)。这些差异也是可以隐藏起来的。

##### 用户空间的I/O软件

有一种用户空间的I/O软件是通过库使用了I/O系统调用。

另一种用户级的I/O软件是多任务缓冲系统(spooling system)。**多任务缓冲**（Spooling)是在多程序系统(multiprogramming system)里处理专用I/O设备的一种方法。

比如说管理打印机。有一个守护进程，只有它有权限让打印机打印文件。守护进程会从多任务缓冲目录(spooling directory)里找文件来打印。一个进程只需把要打印的文件放到多任务缓冲目录就好。这就可以避免用户进程直接管理专用设备而导致的设备使用时间不合理的问题。

多任务缓冲也可用于其它情况，比如电子邮件。提交的邮件先被放在邮件多任务缓冲目录，随后邮件守护进程尝试发送它。如发送失败，邮件会继续留在多任务缓冲目录，并在稍后重发。