《现代操作系统》读书笔记——I/O管理

发布于 2020-07-19  254 次阅读


输入/输出

除了提供抽象(例如,进程(和线程)、地址空间和文件)以外,操作系统还要控制计算机的所有I/O(输入/输出)设备。

I/O硬件原理

设备分类(按照信息交换基本单位分类)

块设备:

块设备把信息存储在固定大小的块中,每个块有自己的地址。通常块的大小在512字节至32 768字节之间。所有传输以一个或多个完整的(连续的)块为单位。块设备的基本特征是每个块都能独立于其他块而读写(可寻址)。硬盘、CD-ROM和USB盘是最常见的块设备。

字符设备:

字符设备以字符为单位发送或接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操作。打印机、网络接口、鼠标(用作指点设备)、老鼠(用作心理学实验室实验),以及大多数与磁盘不同的设备都可看作是字符设备。在输入/输出的时候一般采用中断驱动的方式。

设备控制器

image-20200725104450595

I/O设备一般由机械部件电子部件两部分组成。通常可以将这两部分分开处理,以提供更加模块化和更加通用的设计。电子部件称作设备控制器(device controller)或适配器(adapter)。设备控制器用于控制机械部件工作。

设备控制器的功能
  • 接受和识别CPU发出的命令(控制寄存器中存放CPU命令的参数)
  • 向CPU报告设备的状态(设置状态寄存器
  • 数据交换(数据寄存器
  • 地址识别(有两种编址方式:内存映射I/O单独分配

控制器与设备之间的接口通常是一个很低层次的接口。例如,磁盘可以按每个磁道10 000个扇区,每个扇区512字节进行格式化。

控制器的任务是把串行的位流转换为字节块,并进行必要的错误校正工作。字节块通常首先在控制器内部的一个缓冲区中按位进行组装,然后在对校验和进行校验并证明字节块没有错误后,再将它复制到主存中。

I/O寄存器编址方式

每个控制器有几个寄存器用来与CPU进行通信。通过写入这些寄存器, 操作系统可以命令设备发送数据、接收数据、开启或关闭,或者执行某些其他操作。通过读取这些寄存器,操作系统可以了解设备的状态,是否准备好接收一个新的命令等。

除了这些控制寄存器以外,许多设备还有一个操作系统可以读写的数据缓冲区。例如,在屏幕上显示像素的常规方法是使用一个视频RAM,这一RAM基本上只是一个数据缓冲区,可供程序或操作系统写入数据。(显存)

CPU如何与设备的控制寄存器和数据缓冲区进行通信

image-20200719094334141

寄存器独立编址

在第一个方法中,每个控制寄存器被分配一个I/O端口(I/O port)号,这是一个8位或16位的整数。所有I/O端口形成I/O端口空间(I/O port space),并且受到保护使得普通的用户程序不能对其进行访问(只有操作系统可以访问)。

缺点

  • 如果需要特殊的I/O指令读写设备控制寄存器,那么访问这些寄存器需要使用汇编代码,因为在C或C++中不存在执行IN或OUT指令的方法。调用这样的过程增加了控制I/O的开销。
内存映射I/O

第二个方法是PDP-11引入的,它将所有控制寄存器映射到内存空间中,如图5-2b所示。每个控制寄存器被分配惟一的一个内存地址,并且不会有内存被分配这一地址。这样的系统称为内存映射I/O(memory-mapped I/O)。通常分配给控制寄存器的地址位于地址空间的顶端。

优点

  • 设备控制寄存器只是内存中的变量,在C语言中可以和任何其他变量一样寻址。
  • 不需要特殊的保护机制来阻止用户进程执行I/O操作。操作系统必须要做的全部事情只是避免把包含控制寄存器的那部分地址空间放入任何用户的虚拟地址空间之中。更为有利的是,如果每个设备在地址空间的不同页面上拥有自己的控制寄存器,操作系统只要简单地通过在其页表中包含期望的页面就可以让用户控制特定的设备而不是其他设备。这样的方案可以使不同的设备驱动程序放置在不同的地址空间中,不但可以减小内核的大小,而且可以防止驱动程序之间相互干扰。
  • 对于内存映射I/O,可以引用内存的每一条指令也可以引用控制寄存器。

缺点

  • 缓存带来的一致性问题(状态不准确):现今大多数计算机都拥有某种形式的内存字高速缓存。对一个设备控制寄存器进行高速缓存可能是灾难性的。为了避免这一情形,硬件必须针对每个页面具备选择性禁用高速缓存的能力。操作系统必须管理选择性高速缓存,所以这一特性为硬件和操作系统两者增添了额外的复杂性。

  • 现代计算机多级总线的结构下,在内存映射的机器上具有单独的内存总线的麻烦是I/O设备没有办法查看内存地址,因为内存地址旁路到内存总线上,所以没有办法响应。此外,必须采取特殊的措施使内存映射I/O工作在具有多总线的系统上。

    image-20200719095507751

    一种可能的方法是首先将全部内存引用发送到内存,如果内存响应失败,CPU将尝试其他总线。这一设计是可以工作的,但是需要额外的硬件复杂性。

    第二种可能的设计是在内存总线上放置一个探查设备,放过所有潜在地指向所关注的I/O设备的地址。此处的问题是,I/O设备可能无法以内存所能达到的速度处理请求。

    第三种可能的设计是在PCI桥芯片中对地址进行过滤,这正是图1-12中Pentium结构上所使用的。该芯片中包含若干个在引导时预装载的范围寄存器。

混合方案

图5-2c所示是一种混合的方案,这一方案具有内存映射I/O的数据缓冲区,而控制寄存器则具有单独的I/O端口。Pentium处理器使用的就是这一体系结构。在IBMPC兼容机中,除了0到64K-1的I/O端口之外,640K到1M-1的地址保留给设备的数据缓冲区。

直接存储器存取

无论一个CPU是否具有内存映射I/O,它都需要寻址设备控制器以便与它们交换数据。CPU可以从I/O控制器每次请求一个字节的数据,但是这样做浪费CPU的时间,所以经常用到一种称为直接存储器存取(Direct Memory Access,DMA)的不同方案。

只有硬件具有DMA控制器时操作系统才能使用DMA,而大多数系统都有DMA控制器。有时DMA控制器集成到磁盘控制器和其他控制器之中,但是这样的设计要求每个设备有一个单独的DMA控制器。更加普遍的是,只有一个DMA控制器可利用(例如,在主板上),由它调控到多个设备的数据传送,而这些数据传送经常是同时发生的。

image-20200719095805655

无论DMA控制器在物理上处于什么地方,它都能够独立于CPU而访问系统总线,如图5-4所示。它包含若干个可以被CPU读写的寄存器,其中包括一个内存地址寄存器、一个字节计数寄存器和一个或多个控制寄存器。控制寄存器指定要使用的I/O端口、传送方向(从I/O设备读或写到I/O设备)、传送单位(每次一个字节或每次一个字)以及在一次突发传送中要传送的字节数。

没有DMA时如何读取磁盘
  • 控制器从磁盘驱动器串行地、一位一位地读一个块(一个或多个扇区),直到将整块信息放入控制器的内部缓冲区中。
  • 计算校验和,以保证没有读错误发生。
  • 控制器产生一个中断。当操作系统开始运行时,它重复地从控制器的缓冲区中一次一个字节或一个字地读取该块的信息,并将其存入内存中。 (消耗cpu)
使用DMA时如何读取磁盘
  • CPU通过设置DMA控制器的寄存器对它进行编程,所以DMA控制器知道将什么数据传送到什么地方(图5-4中的第1步)。
  • DMA控制器还要向磁盘控制器发出一个命令,通知它从磁盘读数据到其内部的缓冲区中,并且对校验和进行检验。
  • 如果磁盘控制器的缓冲区中的数据是有效的,那么DMA就可以开始了。 DMA控制器通过在总线上发出一个读请求到磁盘控制器而发起DMA传送(第2步)。
  • 这一读请求看起来与任何其他读请求是一样的,并且磁盘控制器并不知道或者并不关心它是来自CPU还是来自DMA控制器。一般情况下,要写的内存地址在总线的地址线上,所以当磁盘控制器从其内部缓冲区中读取下一个字的时候,它知道将该字写到什么地方。写到内存是另一个标准总线周期(第3步)。
  • 当写操作完成时,磁盘控制器在总线上发出一个应答信号到DMA控制器(第4步)。
  • DMA控 制器步增要使用的内存地址,并且步减字节计数。如果字节计数仍然大于0,则重复第2步到第4步,直到字节计数到达0。
  • DMA控制器 将中断CPU以便让CPU知道传送现在已经完成了。当操作系统开始工作时,用不着将磁盘块复制到内存中,因为它已经在内存中了。 (仅仅从内核空间搬运到用户空间需要消耗cpu)

DMA控制器在复杂性方面的区别相当大。最简单的DMA控制器每次处理一路传送,如上面所描述的。复杂一些的DMA控制器经过编程可以一次处理多路传送,这样的控制器内部具有多组寄存器,每一通道一组寄存器。CPU通过用与每路传送相关的参数装载每组寄存器而开始。每路传送必须使用不同的设备控制器。在图5-4中,传送每一个字之后,DMA控制器要决定下一次要为哪一设备提供服务。DMA控制器可能被设置为使用轮转算法,它也可能具有一个优先级规划设计,以便让某些设备受到比其他设备更多的照顾。假如存在一个明确的方法分辨应答信号,那么在同一时间就可以挂起对不同设备控制器的多个请求。出于这样的原因,经常将总线上不同的应答线用于每一个DMA通道。

许多总线能够以两种模式操作:每次一字模式块模式。某些DMA控制器也能够以这两种模式操作。在前一个模式中,操作如上所述:DMA控制器请求传送一个字并且得到这个字。如果CPU也想使用总线,它必须等待。这一机制称为周期窃取(cycle stealing),因为设备控制器偶尔偷偷溜入并且从CPU偷走一个临时的总线周期,从而轻微地延迟CPU。在块模式中,DMA控制器通知设备获得总线,发起一连串的传送,然后释放总线。这一操作形式称为突发模式(burst mode)。它比周期窃取效率更高,因为获得总线占用了时间,并且以一次总线获得的代价能够传送多个字。突发模式的缺点是,如果正在进行的是长时间突发传送,有可能将CPU和其他设备阻塞相当长的周期

在我们一直讨论的模型——有时称为飞越模式(fly-bymode)中,DMA控制器通知设备控制器直接将数据传送到主存。某些DMA控制器使用的其他模式是让设备控制器将字发送给DMA控制器,DMA控制器然后发起第2个总线请求将该字写到它应该去的任何地方。采用这种方案,每传送一个字需要一个额外的总线周期,但是更加灵活,因为它可以执行设备到设备的复制甚至是内存到内存的复制(通过首先发起一个到内存的读,然后发起一个到不同内存地址的写)。

大多数DMA控制器使用物理内存地址进行传送。使用物理地址要求操作系统将预期的内存缓冲区的虚拟地址转换为物理地址,并且将该物理地址写入DMA控制器的地址寄存器中。在少数DMA控制器中使用的一个替代方案是将虚拟地址写入DMA控制器,然后DMA控制器必须使用MMU来完成虚拟地址到物理地址的转换只有在MMU是内存的组成部分(有可能,但罕见)而不是CPU的组成部分的情况下,才可以将虚拟地址放到总线上

我们在前面提到,在DMA可以开始之前,磁盘首先要将数据读入其内部的缓冲区中。你也许会产生疑问:为什么控制器从磁盘读取字节后不立即将其存储在主存中?换句话说,为什么需要一个内部缓冲区?有两个原因。首先,通过进行内部缓冲,磁盘控制器可以在开始传送之前检验校验和。如果校验和是错误的,那么将发出一个表明错误的信号并且不会进行传送

第二个原因是,一旦磁盘传送开始工作,从磁盘读出的数据就是以固定速率到达的,而不论控制器是否准备好接收数据。如果控制器要将数据直接写到内存,则它必须为要传送的每个字取得系统总线的控制权。此时,若由于其他设备使用总线而导致总线忙(例如在突发模式中),则控制器只能等待。如果在前一个磁盘字还未被存储之前下一个磁盘字到达,控制器只能将它存放在某个地方。如果总线非常忙,控制器可能需要存储很多字,而且还要完成大量的管理工作。如果块被放入内部缓冲区,则在DMA启动前不需要使用总线,这样,控制器的设计就可以简化,因为对DMA到内存的传送没有严格的时间要求。(事实上,有些老式的控制器是直接存取内存的,其内部缓冲区设计得很小,但是当总线很忙时,一些传送有可能由于超载运行错误而被终止。)

重温中断

在硬件层面,中断的工作如下所述。当一个I/O设备完成交给它的工作时,它就产生一个中断(假设操作系统已经开放中断),它是通过在分配给它的一条总线信号线上置起信号而产生中断的。该信号被主板上的中断控制器芯片检测到,由中断控制器芯片决定做什么。

image-20200719101334829

如果没有其他中断悬而未决,中断控制器将立刻对中断进行处理。如果有另一个中断正在处理中,或者另一个设备在总线上具有更高优先级的一条中断请求线上同时发出中断请求,该设备将暂时不被理睬。在这种情况下,该设备将继续在总线上置起中断信号,直到得到CPU的服务。

为了处理中断,中断控制器在地址线上放置一个数字表明哪个设备需要关注,并且置起一个中断CPU的信号。

中断信号导致CPU停止当前正在做的工作并且开始做其他的事情。地址线上的数字被用做指向一个称为中断向量(interrupt vector)的表格的索引,以便读取一个新的程序计数器。这一程序计数器指向相应的中断服务过程的开始。一般情况下,陷阱和中断从这一点上看使用相同的机制,并且常常共享相同的中断向量。中断向量的位置可以硬布线到机器中,也可以在内存中的任何地方通过一个CPU寄存器(由操作系统装载)指向其起点。

中断服务过程开始运行后,它立刻通过将一个确定的值写到中断控制器的某个I/O端口来对中断做出应答。这一应答告诉中断控制器可以自由地发出另一个中断。通过让CPU延迟这一应答直到它准备好处理下一个中断,就可以避免与多个几乎同时发生的中断相牵涉的竞争状态。说句题外的话,某些(老式的)计算机没有集中的中断控制器,所以每个设备控制器请求自己的中断。

在开始服务程序之前,硬件总是要保存一定的信息。哪些信息要保存以及将其保存到什么地方,不同的CPU之间存在巨大的差别。作为最低限度,必须保存程序计数器,这样被中断的进程才能够重新开始。在另一个极端,所有可见的寄存器和很多内部寄存器或许也要保存。

将这些信息保存到什么地方是一个问题。一种选择是将其放入内部寄存器中,在需要时操作系统可以读出这些内部寄存器。这一方法的问题是,中断控制器之后无法得到应答,直到所有可能的相关信息被读出,以免第二个中断重写内部寄存器保存状态。这一策略在中断被禁止时将导致长时间的死机,并且可能丢失中断和丢失数据。

因此,大多数CPU在堆栈中保存信息。然而,这种方法也有问题。首先,使用谁的堆栈?如果使用当前堆栈,则它很可能是用户进程的堆栈。堆栈指针甚至可能不是合法的,这样当硬件试图在它所指的地址处写某些字时,将导致致命错误。此外,它可能指向一个页面的末端。若干次内存写之后,页面边界可能被超出并且产生一个页面故障。在硬件中断处理期间如果发生页面故障将引起更大的问题:在何处保存状态以处理页面故障?

如果使用内核堆栈,将存在更多的堆栈指针是合法的并且指向一个固定的页面的机会。然而,切换到核心态可能要求改变MMU上下文,并且可能使高速缓存和TLB的大部分或全部失效。静态地或动态地重新装载所有这些东西将增加处理一个中断的时间,因而浪费CPU的时间。

I/O软件原理

I/O软件目标

在设计I/O软件时一个关键的概念是设备独立性(device independence)。它的意思是应该能够编写出这样的程序:它可以访问任意I/O设备而无需事先指定设备。

与设备独立性密切相关的是统一命名(uniform naming)这一目标。一个文件或一个设备的名字应该是一个简单的字符串或一个整数,它不应依赖于设备。

I/O软件的另一个重要问题是错误处理(error handling)。一般来说,错误应该尽可能地在接近硬件的层面得到处理。当控制器发现了一个读错误时,如果它能够处理那么就应该自己设法纠正这一错误。如果控制器处理不了,那么设备驱动程序应当予以处理,可能只需重读一次这块数据就正确了。

另一个关键问题是同步(synchronous)(即阻塞)和异步(asynchronous)(即中断驱动)传输。大多数物理I/O是异步的——CPU启动传输后便转去做其他工作,直到中断发生。如果I/O操作是阻塞的,那么用户程序就更加容易编写——在read系统调用之后,程序将自动被挂起,直到缓冲区中的数据准备好。正是操作系统使实际上是中断驱动的操作变为在用户程序看来是阻塞式的操作。

I/O软件的另一个问题是缓冲(buffering)。数据离开一个设备之后通常并不能直接存放到其最终的目的地。例如,从网络上进来一个数据包时,直到将该数据包存放在某个地方并对其进行检查,操作系统才知道要将其置于何处。此外,某些设备具有严格的实时约束(例如,数字音频设备),所以数据必须预先放置到输出缓冲区之中,从而消除缓冲区填满速率和缓冲区清空速率之间的相互影响,以避免缓冲区欠载。缓冲涉及大量的复制工作,并且经常对I/O性能有重大影响。

image-20200725115243211

程序控制I/O

I/O的最简单形式是让CPU做全部工作,这一方法称为程序控制I/O(programmed I/O)。

image-20200719103227419

首先,数据被复制到内核空间。然后,操作系统进入一个密闭的循环,一次输出一个字符。在该图中,清楚地说明了程序控制I/O的最根本的方面,这就是输出一个字符之后,CPU要不断地查询设备以了解它是否就绪准备接收另一个字符。这一行为经常称为轮询(polling)或忙等待(busy waiting)。

程序控制I/O十分简单但是有缺点,即直到全部I/O完成之前要占用CPU的全部时间。如果“打印”一个字符的时间非常短(因为打印机所做的全部事情就是将新的字符复制到一个内部缓冲区中),那么忙等待还是不错的。此外,在嵌入式系统中,CPU没有其他事情要做,忙等待也是合理的。然而,在更加复杂的系统中,CPU有其他工作要做,忙等待将是低效的,需要更好的I/O方法。

中断驱动的I/O

这种允许CPU在等待打印机变为就绪的同时做某些其他事情的方式就是 使用中断。当打印字符串的系统调用被发出时,如我们前面所介绍的,字符串缓冲区被复制到内核空间,并且一旦打印机准备好接收一个字符时就将第一个字符复制到打印机中。这时,CPU要调用调度程序,并且某个其他进程将运行。请求打印字符串的进程将被阻塞,直到整个字符串打印完。

使用DMA的I/O

image-20200725113252541

image-20200725113918584

中断驱动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也许更好。

通道控制

image-20200725115205045

I/O软件层次

image-20200725110303594

I/O软件通常组织成四个层次,如图5-11所示。每一层具有一个要执行的定义明确的功能和一个的定义明确的与邻近层次的接口。功能与接口随系统的不同而不同,所以下面的讨论并不针对一种特定的机器。我们将从底层开始讨论每一层。

软件分层:

  • 用户级I/O软件一般属于用户空间
  • 后三个层次属于操作系统内核部分,即I/O系统

image-20200719103631313

中断处理程序

image-20200725111721082

虽然程序控制I/O偶尔是有益的,但是对于大多数I/O而言,中断是令人不愉快的事情并且无法避免。应当将其深深地隐藏在操作系统内部,以便系统的其他部分尽量不与它发生联系。隐藏它们的最好办法是将启动一个I/O操作的驱动程序阻塞起来,直到I/O操作完成且产生一个中断。驱动程序阻塞自己的手段有:在一个信号量上执行down操作、在一个条件变量上执行wait操作、在一个消息上执行receive操作或者某些类似的操作

当中断发生时,中断处理程序将做它必须要做的全部工作以便对中断进行处理。然后,它可以将启动中断的驱动程序解除阻塞。在一些情形中,它只是在一个信号量上执行up操作;其他情形中,是对管程中的条件变量执行signal操作;还有一些情形中,是向被阻塞的驱动程序发一个消息。在所有这些情形中,中断最终的结果是使先前被阻塞的驱动程序现在能够继续运行如果驱动程序构造为内核进程,具有它们自己的状态、堆栈和程序计数器,那么这一模型运转得最好

当然,现实没有如此简单。对一个中断进行处理并不只是简单地捕获中断,在某个信号量上执行up操作,然后执行一条IRET指令从中断返回到先前的进程。对操作系统而言,还涉及更多的工作。我们将按一系列步骤给出这一工作的轮廓,这些步骤是硬件中断完成之后必须在软件中执行的。应该注意的是,细节是非常依赖于系统的,所以下面列出的某些步骤在一个特定的机器上可能是不必要的,而没有列出的步骤可能是必需的。此外,确实发生的步骤在某些机器上也可能有不同的顺序。

  • 保存没有被中断硬件保存的所有寄存器(包括PSW)。
  • 为中断服务过程设置上下文,可能包括设置TLB、MMU和页表。
  • 为中断服务过程设置堆栈。
  • 应答中断控制器,如果不存在集中的中断控制器,则再次开放中断。
  • 将寄存器从它们被保存的地方(可能是某个堆栈)复制到进程表中。
  • 运行中断服务过程,从发出中断的设备控制器的寄存器中提取信息。
  • 选择下一次运行哪个进程,如果中断导致某个被阻塞的高优先级进程 变为就绪,则可能选择它现在就运行。
  • 为下一次要运行的进程设置MMU上下文,也许还需要设置某个TLB。
  • 装入新进程的寄存器,包括其PSW。
  • 开始运行新进程。

由此可见,中断处理远不是无足轻重的小事。它要花费相当多的CPU指令,特别是在存在虚拟内存并且必须设置页表或者必须保存MMU状态(例如R和M位)的机器上。在某些机器上,当在用户态与核心态之间 切换时,可能还需要管理TLB和CPU高速缓存,这就要花费额外的机器周期。

设备驱动程序

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

每个设备驱动程序通常处理一种类型的设备,或者至多处理一类紧密相关的设备。

不过在有些时候,机器不同的设备却基于相同的底层技术。众所周知的例子可能是USB,这是一种串行总线技术。此处的技巧是USB驱动程序通常是堆栈式的,就像网络中的TCP/IP栈(分层结构)。不同的设备共享这个协议栈中的某些部分。

为了访问设备的硬件(意味着访问设备控制器的寄存器),设备驱动程序通常必须是操作系统内核的一部分,至少对目前的体系结构是如此。实际上,有可能构造运行在用户空间的驱动程序,使用系统调用来读写 设备寄存器。这一设计使内核与驱动程序相隔离,并且使驱动程序之间相互隔离,这样做可以消除系统崩溃的一个主要源头——有问题的驱动程序以这样或那样的方式干扰内核。

image-20200719105317478

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

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

从MS-DOS开始,操作系统转向驱动程序在执行期间动态地装载到系统中的另一个模型。不同的操作系统以不同的方式处理驱动程序的装载工作。

驱动程序的功能

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

驱动程序结构

许多设备驱动程序具有相似的一般结构。

  • 典型的驱动程序在启动时要检查输入参数,检查输入参数的目的是搞清它们是否是有效的,如果不是,则返回一个错误。如果输入参数是有效的,则可能需要进行从抽象事项到具体事项的转换。

  • 接着,驱动程序可能要检查设备当前是否在使用。如果在使用,请求将被排入队列以备稍后处理。如果设备是空闲的,驱动程序将检查硬件状态以了解请求现在是否能够得到处理。

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

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

驱动程序必须是重入的(reentrant),这意味着一个正在运行的驱动程序必须预料到在第一次调用完成之前第二次被调用。

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

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

与设备无关的I/O软件

虽然I/O软件中有一些是设备特定的,但是其他部分I/O软件是与设备无关的。设备驱动程序和与设备无关的软件之间的确切界限依赖于具体系统(和设备),因为对于一些本来应按照与设备无关方式实现的功能,出于效率和其他原因,实际上是由驱动程序来实现的。图5-13所示的功能典型地由与设备无关的软件实现。

image-20200719110226561

设备驱动程序的统一接口

操作系统的一个主要问题是如何使所有I/O设备和驱动程序看起来或多或少是相同的。

image-20200719110602756

设备驱动程序与操作系统其余部分之间的接口是这一问题的一个方面。图5-14a所示为这样一种情形:每个设备驱动程序有不同的与操作系统的接口。这意味着,可供系统调用的驱动程序函数随驱动程序的不同而不同。这可能还意味着,驱动程序所需要的内核函数也是随驱动程序的不同而不同的。综合起来看,这意味着为每个新的驱动程序提供接口都需要大量全新的编程工作。

相反,图5-14b所示为一种不同的设计,在这种设计中所有驱动程序具有相同的接口。这样一来,倘若符合驱动程序接口,那么添加一个新的驱动程序就变得容易多了。这还意味着驱动程序的编写人员知道驱动程序的接口应该是什么样子的。实际上,虽然并非所有的设备都是绝对一样的,但是通常只存在少数设备类型,而它们的确大体上是相同的。

如何给I/O设备命名是统一接口问题的另一个方面。与设备无关的软件要负责把符号化的设备名映射到适当的驱动程序上。例如,在UNIX系统中,像/dev/disk0这样的设备名惟一确定了一个特殊文件的i节点,这个i节点包含了主设备号(major device number),主设备号用于定位相应的驱动程序。i节点还包含次设备号(minor device number),次设备号作为参数传递给驱动程序,用来确定要读或写的具体单元。所有设备都具有主设备号和次设备号,并且所有驱动程序都是通过使用主设备号来选择驱动程序而得到访问。

缓冲

image-20200725150833101

输入缓冲

image-20200719110858462

  • 用户进程在用户空间中提供了一 个包含n个字符的缓冲区,并且执行读入n个字符的读操作。中断服务过 程负责将到来的字符放入该缓冲区中直到缓冲区填满,然后唤醒用户进程。这一方案比前一种方案的效率要高很多,但是它也有一个缺点:当一个字符到来时,如果缓冲区被分页而调出了内存会出现什么问题呢? 解决方法是将缓冲区锁定在内存中,但是如果许多进程都在内存中锁定页面,那么可用页面池就会收缩并且系统性能将下降。
  • 在内核空间中创建一个缓冲区并且让中断处理程序将字符 放到这个缓冲区中,如图5-15c所示。当该缓冲区被填满的时候,将包含用户缓冲区的页面调入内存(如果需要的话),并且在一次操作中将内核缓冲区的内容复制到用户缓冲区中。这一方法的效率要高很多。 即使这种方案也面临一个问题:正当包含用户缓冲区的页面从磁盘调入内存的时候有新的字符到来,这样会发生什么事情?因为缓冲区已满,所以没有地方放置这些新来的字符。
  • 一种解决问题的方法是使用 第二个内核缓冲区。第一个缓冲区填满之后,在它被清空之前,使用第 二个缓冲区,如图5-15d所示。当第二个缓冲区填满时,就可以将它复制给用户(假设用户已经请求它)。当第二个缓冲区正在复制到用户空间的时候,第一个缓冲区可以用来接收新的字符。以这样的方法,两个缓冲区轮流使用:当一个缓冲区正在被复制到用户空间的时候,另一个缓冲区正在收集新的输入。像这样的缓冲模式称为双缓冲(double buffering)。
  • 广泛使用的另一种形式的缓冲是循环缓冲区(circular buffer)。它由一 个内存区域和两个指针组成。一个指针指向下一个空闲的字,新的数据可以放置到此处。另一个指针指向缓冲区中数据的第一个字,该字尚未被取走。在许多情况下,当添加新的数据时(例如刚刚从网络到来),硬件将推进第一个指针,而操作系统在取走并处理数据时推进第二个指针。两个指针都是环绕的,当它们到达顶部时将回到底部。
输出缓冲

image-20200719111245617

错误报告

错误在I/O上下文中比在其他上下文中要常见得多。当错误发生时,操作系统必须尽最大努力对它们进行处理。许多错误是设备特定的并且必须由适当的驱动程序来处理,但是错误处理的框架是设备无关的。

  • 一种类型的I/O错误是编程错误

  • 另一种类型的错误是实际的I/O错误

软件要做的事情取决于环境和错误的本质。如果是一个简单的读错误并且存在一个交互式的用户可利用,那么它就可以显示一个对话框来询问用户做什么。选项可能包括重试一定的次数,忽略错误,或者杀死调用进程。如果没有用户可利用,惟一的实际选择或许就是以一个错误代码让系统调用失败。

分配与释放专用设备

image-20200725123443754

image-20200725121928514

某些设备,例如CD-ROM刻录机,在任意给定的时刻只能由一个进程使用。这就要求操作系统对设备使用的请求进行检查,并且根据被请求的设备是否可用来接受或者拒绝这些请求。处理这些请求的一种简单方法 是要求进程在代表设备的特殊文件上直接执行open操作。如果设备是不可用的,那么open就会失败。于是就关闭这样的一个专用设备,然后将其释放

分配方式

静态分配

进程运行前为其分配所有需要的资源,运行结束后归还

动态分配

进程运行过程中动态申请设备资源

数据结构

  • 设备控制表

image-20200725122538794

  • 控制器控制表

image-20200725122818105

  • 系统设备表

imag

分配步骤改进

image-20200725123235933

image-20200725123336959

与设备无关的块大小

不同的磁盘可能具有不同的扇区大小。应该由与设备无关的软件来隐藏这一事实并且向高层提供一个统一的块大小,例如,将若干个扇区当作一个逻辑块。这样,高层软件就只需处理抽象的设备,这些抽象设备全都使用相同的逻辑块大小,与物理扇区的大小无关。类似地,某些字符设备(如调制解调器)一次一个字节地交付它们的数据,而其他的设备(如网络接口)则以较大的单位交付它们的数据。这些差异也可以被隐 藏起来。

用户空间的I/O软件

假脱机技术

image-20200725120840996

image-20200725121456850


面向ACG编程