除了提供抽象以外,操作系统还需要控制计算机的所有I/O设备,操作系统必须向设备发送命令,捕捉中断,处理设备的各种错误,I/O部分的代码是整个操作系统中的重要组成部分。
1. I/O硬件原理
对于程序员来说,I/O硬件是提供给软件的接口,我们的讨论仅限于如何对硬件编程,而不是其内部工作原理。
1.1 I/O设备
I/O设备可以分成两类:块设备和字符设备。
块设备把信息存储在固定大小的块中,每个块有自己的地址,所有的传输以一个或多个完整的块为单位。块设备的基本特征就是每个块都能独立于其他块而读写。硬盘,CD-ROM和USB盘都是块设备。
另一类I/O设备是字符设备,字符设备以字符为单位发送或接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操作。打印机,网络接口,鼠标等设备都是字符设备。
1.2 设备控制器
I/O设备一般由机械部件和电子部件两部分组成,电子部件称为设备控制器,控制器的任务是把串行的位流转换为字节块,并进行必要的错误校正工作。字节块通常在控制器内部的一个缓冲区中按位进行组装,然后在对校验和进行校验并证明字节块没有错误后,再将它复制到主存中。控制器和设备之间的接口通常是一个很低层次的接口。
1.3 内存映射I/O
每个控制器都有几个寄存器用于和CPU进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据,接收数据,开启或关闭,或执行某些操作。通过读取这些寄存器,操作系统可以了解设备的状态,是否准备好接收一个新的命令。
除了这些控制寄存器之外,许多设备还有一个操作系统可以读写的数据缓冲区。CPU如何与设备的控制寄存器和数据缓冲区进行通信?
第一个方法中,每个控制寄存器被分配一个I/O端口号,所有的I/O端口形成I/O端口空间,并且受到保护使得普通的用户程序不能对其进行访问。
第二个方法是将所有的控制寄存器映射到内存空间中,每个寄存器被分配唯一的内存地址,并且不会有内存被分配给这一地址,系统被称为内存映射I/O,通常被分配给控制寄存器的地址位于地址空间的顶端。
当CPU需要读入一个字的时候,不论从内存中读入还是从I/O端口中读入,都要将需要的地址放到总线的地址线上,然后在总线的一条控制线上置一个READ信号,还要用第二条信号线来表明需要的是I/O空间还是内存空间。如果是内存空间,内存响应请求;如果是I/O空间,I/O设备响应请求。如果只有内存空间,那么每个内存模块和I/O设备都会将地址线和所服务的地址设备进行比较,寻找对应的内存或I/O设备进行响应。
对于内存映射I/O,设备控制寄存器只是内存中的变量,可以和任何其他变量一样寻址,如果用第一种方法就要用到某些汇编代码;内存映射I/O不需要特殊的保护机制来阻止用户进程执行I/O操作。
当然,内存映射I/O也有缺点,现今的计算机都拥有某种形式的内存字高速缓存,对一个设备控制寄存器进行高速缓存有可能是灾难性的,硬件必须对每个页面具备选择性禁用高速缓存的能力,操作系统必须管理选择性高速缓存。
1.4 直接存储器存取
无论一个CPU是否具有内存映射I/O,都需要寻址设备控制器以便和它们交换数据,CPU可以从I/O控制器每次请求一个字节的数据,但是这样做非常浪费CPU的时间,所以经常会用到直接存储器存取(Direct Memory Access, DMA)。只有硬件具有DMA控制器时操作系统才能使用DMA,一般情况下,只有一个DMA控制器可用,由它调控到多个设备的数据传送。
DMA控制器独立于CPU访问系统总线,包含若干个可以被CPU读写的寄存器,其中包含一个内存地址寄存器,一个字节计数寄存器和一个或多个控制寄存器,控制寄存器指定要使用的I/O端口,传送方向,传送单位以及一次在传送中要传送的字节数。
在没有使用DMA时,控制器从磁盘驱动器中串行地按位读一个块,直到将整块信息放入控制器的内部缓冲区中,接着计算校验和以保证没有读错误发生,然后控制器产生一个中断。
使用DMA时,首先CPU通过设置DMA控制器的寄存器对其进行编程,DMA控制器便知道将什么数据放到什么地方。DMA控制器还要向磁盘控制器发出一个命令,通知它从磁盘读数据到其内部的缓冲区中,并对校验和进行校验。
DMA控制器通过在总线上发出一个读请求到磁盘控制器而发起DMA传送,磁盘控制器并不知道,也不关心它是来自CPU还是DMA控制器。磁盘控制器从其内部缓冲区中将数据写入到内存地址上,当写操作完成后,磁盘控制器在总线上发出一个应答信号到DMA控制器。于是,DMA控制器步增要使用的内存地址,并且步减字节计数,如果字节计数仍然大于0,重复上一步骤,直到字节计数达到0为止。此时DMA控制器将中断CPU以便让CPU知道传送已经完成。
并不是所有的计算机都使用DMA,反对的论据是主CPU通常比DMA控制器快很多,做同样的工作可以更快。如果CPU没有其他工作要做,让快速的CPU等待慢速的DMA控制器完成工作是无意义的,这一点在嵌入式计算机上非常重要。
1.5 重温中断
当一个I/O设备完成交给它的工作时,就产生一个中断,它是通过在分配给它的一条总线信号线上置起信号而产生中断的,该信号被主板上的中断控制器芯片检测到,由中断控制器芯片决定做什么。设备与中断控制器之间的连接实际上使用的是总线上的中断线而不是专用连线。
为了处理中断,中断控制器在地址线上放置一个数字表明哪个设备需要关注,并且置起一个中断CPU的信号。中断信号导致CPU停止当前做的工作并且开始做其他的事情,地址上的数字被用做指向一个称为中断向量的表格索引,以便读取一个新的程序计数器,这个程序计数器指向相应的中断服务开始。
在开始服务程序之前,硬件总是要保存一定的信息。作为最低限度,必须保存程序计数器,以便中断的进程能够重新开始,此外所有可见的寄存器也许需要保存。
2. I/O软件原理
2.1 I/O软件的目标
在设计I/O软件时一个关键的概念是设备独立性,应该能够编写这样的程序:可以访问任意I/O设备而无需事先指定设备。与设备独立性密切相关的是统一命名这一目标。一个文件或一个设备的名字应该是一个简单的字符串或一个整数,它不应该依赖于设备。
I/O软件的另一个重要问题是错误处理,错误应该尽可能地接近硬件的层面得到处理。当控制器发现一个读错误,如果它能够处理就应该自己设法纠正该错误,如果控制器处理不了,设备驱动程序应该予以处理。很多情况下,错误恢复可以在低层透明地得到解决,高层软件甚至不知道存在该错误。
另一个关键问题是同步(阻塞)和异步传输(中断驱动)。大多数物理I/O是异步的——CPU启动传输后做其他工作,直到中断发生。如果I/O操作是阻塞的,用户程序就更加容易编写——在read系统调用之后,程序自动被挂起,直到缓冲区的数据准备好。正是操作系统使实际上是中断驱动的操作变为用户程序看来是阻塞式操作。
I/O软件的另一个问题是缓冲,数据离开一个设备之后通常并不能直接存放在其最终目的地。
最后一个概念是共享设备和独占设备的问题,有些I/O设备能够同时让多个用户使用,有些I/O设备则必须由单个用户使用,直到该用户使用完。操作系统必须能够处理共享设备和独占设备,以避免问题的发生。
2.2 程序控制I/O
程序控制I/O中,其操作过程简要概括如下:首先,数据被复制到内核空间,然后,操作系统进入一个循环由程序控制一次输入/输出字符,这个过程中,CPU需要不断地查询设备以了解它是否就绪准备接收另一个字符,轮询或忙等待。
程序控制I/O十分简单但有缺点,即直到全部I/O完成之前需要占用CPU的全部时间。在嵌入式系统中,CPU没有其他事情可以做,忙等待是合理的,然而,更复杂的系统,CPU有其他工作要做,忙等待是低效的,需要更好的I/O方法。
2.3 中断驱动I/O
允许CPU在等待I/O设备就绪过程中做其他事情的方式就是使用中断。例如,在打印机(I/O,通常其速度比较慢)设备打印完并准备接收下一个字符时,将产生一个中断,这个中断将停止当前进程并保存其状态,然后,打印机中断服务过程将运行。
2.4 使用DMA的I/O
中断驱动I/O的最大缺点是中断发生在每个I/O上,中断需要花费时间,浪费一定的CPU时间,这个问题的一种解决方法是使用DMA。让DMA控制器给打印机提供字符,而不必打扰CPU,本质上DMA是程序控制I/O,只是由DMA而不是CPU做全部工作。
这个策略需要特殊的硬件(DMA控制器),但是使CPU获得自由从而可以在I/O期间做其他工作。DMA重大的成功是将中断的次数从打印每个字符一次减少到打印每个缓冲区一次,如果有多个字符并且中断十分缓慢,那么采用DMA可能是重要的改进。
另一方面,DMA控制器通常比主CPU慢很多,如果DMA不能以全速驱动设备,或者CPU在等待DMA中断的同时没有其他事情可以做,那么采用中断驱动I/O甚至采用程序控制I/O会更好。
3. I/O软件层次
I/O软件通常分成4个层次,每一层具有一个要执行的定义明确的功能和一个定义明确的与邻接层次的接口,功能和接口随系统的不同而不同。
3.1 中断处理程序
大多数I/O来说,中断是令人不愉快的事情而且不可避免,应该将其深深地隐藏在操作系统内部,以便系统的其他部分尽量不与其发生联系。隐藏的最好办法是将启动一个I/O操作的驱动程序阻塞起来,直到I/O操作完成且产生一个中断。
对一个中断进行处理并不只是简单地捕获中断,在某个信号量上执行up操作,然后执行一条IRET指令从中断返回到先前的进程。对操作系统而言,还涉及更多的工作,下面列出一系列此类工作的步骤:
- 保存没有被中断硬件保存的所有寄存器;
- 为中断服务过程设置上下文,可能包括TLB,MMU和页表;
- 为中断服务过程设置堆栈;
- 应答中断控制器,如果不存在集中的中断控制器,则再次开放中断;
- 将寄存器从它们被保存的地方复制到进程表中;
- 运行中断服务程序,从发出中断的设备控制器的寄存器中读取信息;
- 选择下一次运行哪个进程,如果中断导致某个被阻塞的高优先级进程变为就绪,则可能选择它现在就运行;
- 为下一次要运行的进程设置MMU上下文,也许还需要设置某个TLB;
- 装入新进程的寄存器,包括其PSW;
- 开始运行新的进程;
可见,中断处理要花费相当多的CPU指令,特别是存在虚拟内存并且必须设置页表或者必须保存MMU状态(R和W位)的机器上。某些机器上,当在用户态和内核态之间切换时,可能还需要管理TLB和CPU高速缓存,花费额外的CPU周期。
3.2 设备驱动程序
每个连接到计算机上的I/O设备都需要某些设备特定的代码来对其进行控制,这样的代码被称作设备驱动程序,一般由设备的制造商编写并随同设备一起交付,设备制造商通常要为若干流行的操作系统提供驱动程序。
为了访问设备的硬件,设备驱动程序通常必须是操作系统内核的一部分。操作系统的设计者知道其他人写的驱动程序代码片段将被安装在操作系统的内部,事先需要有一个体系结构来允许这样的安装。需要有一个定义明确的模型,规定驱动程序做什么事以及如何与操作系统的其他部分相互作用。
设备驱动程序有若干功能,最明显的功能就是接收来自其上方与设备无关的软件所发出的抽象读写请求,并目睹这些请求被执行。
驱动程序不允许进行系统调用,但是经常需要与内核的其余部分进行交互,对某些内核过程的调用通常是允许的。
3.3 与设备无关的I/O软件
设备驱动程序和与设备无关的I/O软件之间的确切界限依赖于具体系统(和设备),与设备无关的软件的基本功能是执行对所有设备公共的I/O功能,并且向用户层软件提供一个统一的接口。
设备驱动程序的统一接口
操作系统的一个主要问题是如何使所有I/O设备和驱动程序看起来或多或少是相同的。如果具有统一的标准的驱动程序接口,添加一个新的驱动程序就变得容易了,这也意味着驱动程序的编写人员知道驱动程序的接口形式。实际上,虽然并非所有的设备都是绝对一样的,但是通常只存在少数设备类型,并且的确大体相同。
这种设计的工作方式如下:对于每一种设备类型,操作系统定义一组驱动程序必须支持的函数。
如何给I/O设备命名是统一接口问题的另一个方面。与设备无关的软件负责把符号化的设备名映射到适当的驱动程序上,所有设备都具有主设备号和次设备号,主设备号用于定位相应的驱动程序,次设备号作为参数传递给驱动程序,用来确定要读或写的具体单元。
缓冲
无论对于块设备还是对于字符设备,缓冲也是一个重要问题。如果一个从外部设备读取字符的进程在每个字符到来时,都引起中断并启动用户进程,对于短暂的数据流量让一个进程运行多次效率会很低,并不是一个良好的设计。
使用缓冲区可以对原来的模型进行改进,中断服务过程负责将到来的字符放入该缓冲区中直到缓冲区填满,然后唤醒用户进程。
广泛使用的一种形式的缓冲是循环缓冲区,由一个内存区域和两个指针组成,一个指针指向下一个空闲的字,新的数据可以放置到此处;另一个指针指向缓冲区中数据的第一个字,该字尚未被取走。当添加数据时,推进第一个指针,当操作系统取走数据并处理时推进第二个指针,两个指针是环绕的(这种模型也让人联想到Java中的NIO模型)。
错误报告
错误在I/O上下文中比其它上下文中要常见得多,当错误发生时,操作系统必须尽最大努力对它们进行处理。
一种类型的I/O错误是编程错误,发生在一个进程请求某些不可能是事情时,比如无效缓冲区,无效设备等等。这种错误上采取的行动是直截了当的,只是将错误代码报告返回给调用者。
另一种类型错误是实际的I/O错误,试图写一个已经被破坏的磁盘块,读一个已经关机的摄像机。这种情形中,应该由驱动程序决定做什么,如果驱动程序不知道怎么做,将问题向上传递,返回给设备无关的软件。
分配与释放专用设备
某些设备在任意给定时刻只能由一个进程使用,这就要求操作系统对设备使用的请求进行检查,根据被请求的设备是否可以来接收或者拒绝请求。如果设备不可用,可以对调用的进程进行阻塞,或者直接失败。
与设备无关的块大小
不同的磁盘可能具有不同的扇区大小。应该由与设备无关的软件来隐藏这一事实,并向高层提供一个统一的块大小。这样高层软件就只需处理抽象的设备,这些抽象的设备全都使用相同的逻辑块大小,与物理扇区大小无关。
3.4 用户空间的I/O软件
尽管大部分I/O软件都在操作系统内部,但是仍有一小部分在用户空间,包括与用户程序连接在一起的库,甚至完全运行于内核外的程序。
比如创建一个特殊进程,称为守护进程,以及一个特殊的目录,称为假脱机目录。一个进程需要打印一个文件时,首先生成要打印的文件,将其放在假脱机目录下,由守护进程打印该目录下的文件,该进程是允许访问打印机特殊文件的唯一进程。通过保护特殊文件来防止用户直接使用,可以解决某些进程长期空闲占用打印机的问题。
我们对I/O系统进行一个简单的总结,给出所有层次以及每一层的主要功能。当一个用户程序试图从一个文件中读一个块时,操作系统被调用以实现这一请求。与设备无关的软件在缓冲区高速缓存中查找有无要读的块。如果需要的块不在其中,调用设备驱动程序,向硬件发出一个请求,让它从磁盘中获取该块。然后,该进程被阻塞直到磁盘操作完成。
当磁盘操作完成时,硬件产生一个中断。中断处理程序就会运行,要查明发生了什么事情,关注的设备。然后,中断处理程序从设备提取状态信息,唤醒休眠的进程以结束此次I/O请求,并让用户进程继续使用。