I/O原理

I/O原理

输入输出

输入输出

对于操作系统设计人员来说,从高层设计来看,关于输入输出我们要问的问题有两个:

  • 输入输出要达到什么目的?
  • 操作系统是如何实现输入输出功能的?

输入输出的目的

输入输出的目的,简单来说,就是提供一个人机交互的通道,使得人与计算机能够进行沟通。但这是抽象的层次。具体来说,输入输出的目的是什么呢?

操作系统输入输出的目的是:

  • 屏蔽输入输出设备的差异。
  • 在不同设计之间进行数据表示的转换。

达到上述目的需要的机制,仿照我们前面的模式就是设备独立与设备保护。

  • 设备独立指的是输入输出不以设备的不同而转移,即不管输入输出设备如何更好或更新,我们进行输入输出的模式和方法保持不变。
  • 设备保护则是一个输入输出设备的操作不会影响对另一个输入输出设备的操作。

输入输出硬件

从程序员或操作系统设计人员的视角来看,所有的输入输出设备可以(大概)划分为两个大类:块设备和字符设备。这种划分是以设备存储和传输数据的方式来决定。

  • 块设备,顾名思义,就是以数据块为单位存储和传输数据的输入输出设备,如磁盘、光盘、U盘、磁带等。
  • 字符设备自然是将数据按照字符(字节)为单位来存放和传输的设备,如鼠标、键盘、打印机、网络界面等。

当然,上述分类并不是绝对的。有时,一个设备可以同时作为块设备和字符设备。例如,网络界面通常被认为是一个字符输入输出设备,但在某些时候可以与内存进行DMA,从而看上去更像一个块设备。而另外的设备,如时钟,则不属于这两种中的任何一种。(时钟是输入输出设备吗?)

字符设备和块设备的最大不同表现在寻址上。块设备的数据按数据块为单位进行处理,而每个数据块都有一 个唯一的磁盘地址,也就是说数据块是可寻址的。而字符设备里的字符是不可寻址的。当然,由于一个字符占一个字节(对于ASCII码来说),而字节是可以寻址的,很多人会认为字符因此也是可寻址的。但这个理
解是不正确的。

输入输出设备的差异性

输入输出设备由于种类不同,制造商不同,技术标准不同,其特性可以有巨大的不同。而这种不同越是明显,对操作系统的设计的挑战就越大。因为屏蔽这些巨大的不同,使得不同的设备相互共存并不是一件容易的事情。其中最为明显的一种差异是其数据传输的速度。

设备控制器

输入输出设备本身并不是一个不可分割的整体,而是由不同的部件构成。一般来说,一个输入输出设备至少可以分为两部分:机械部分和电子部分。

机械部分自然是设备的物理硬件部分,而电子部分则是设备的控制器。控制器有时也称为适配器,通常为一 块印刷电路板。控制器可以处理多个设备,或者说多个同类的设备可以共用一个控制器。

设备控制器的任务可以简单地分为如下几项:

  • 控制设备的物理运行。
  • 将序列字位流转化为字节块流。
  • 进行纠错操作。

设备控制器与CPU之间的数据交互通过设备寄存器进行。设备寄存器附着在设备控制器上。通过向这些寄存器进行写入,操作系统可以向设备发出输入输出命令,如把设备关闭或打开。而通过读取这些寄存器的内容,操作系统可以获得设备的状态信息。

为了提高与CPU交互数据的效率,输入输出设备通常还备有数据缓冲区

物理I/O模式

根据CPU访问I/O设备的方式进行分类:

  1. 专有通道I/O
    在专有通道I/O模式下,I/O与内存是完全脱离的。每个控制寄存器被赋予一个I/O端口。这个I/O端口就是一 个8位或者16位的整数。这个整数与内存地址没有任何关系。而正是由于I/O端口地址与内存地址没有任何关系,或者说I/O端口地址不是内存地址,操作系统必须使用专门的输入输出特殊指令来进行数据的读写。

    专有通道模式的优点是与内存分开,输入输出操作不会影响或干扰内存操作。尤其是输入输出软件的可靠性通常不如内存管理软件,这种分离就显得更加有价值。

    这个优点恰恰也是其缺点。由于与内存分开,输入输出指令与内存访问指令自然也不相同。因此,进行内存访问与进行输入输出的指令互不相同。事实上,正如上一段所描述的,这种模式下的输入输出需要使用专门的IN/OUT指令来进行。

    由于高级语言不支持这种IN/OUT低级指令,如果程序员想要进行I/O,则必须在程序中直接使用这些低级语言指令,而这将增大程序设计的难度,并打破人们不懈追求的封装,导致程序可靠性降低。专有通道I/O还有一个问题,即保护问题。这是因为使用低级语言的保护作用非常有限。例如,低级语言一般不具备访问控制和授权能力,使用这种模式进行访问的主体可以对I/O设备进行任意操作,包括故意制造故障。

    早期的计算机使用专有通道I/O模式,这些计算机包括IBM360、IBM370等。

  2. 内存映射的I/O

    内存映射的I/O,顾名思义,就是将I/O映射到内存里面。从而使得I/O和内存管理得到统一。具体来说,就是将I/O设备的每个控制寄存器和设备缓冲区寄存器赋予一个唯一的内存地址。对这些地址的访问就是对I/O设备的访问。对这些地址的访问从逻辑上看,就是内存访问。

    讨论操作系统和用户程序在内存里面的相对位置时,有一种模式是操作系统处于内存地址的两端,用户程序处于内存空间的中间。在这种模式下,内存顶端的操作系统部分就是内存映射的输入输出,即设备的控制寄存器的地址在可用物理内存地址的上方。

    内存映射虽然具有I/O与内存访问统一的优点,但是也存在诸多问题。

    • 第1个问题是使用缓存而产生的问题。由于使用内存映射,I/O控制寄存器被当做内存来看待,而内存里面的内容则有可能缓存起来。对于一般的数据来说,缓存提供的是优势。但对于控制寄存器来说,则是一个大缺点。因为操作系统在读控制寄存器时需要的是最新的内容,而不是缓存的内容。I/O设备的任何变化都将在控制寄存器内容上体现。但这个内容却不一定在缓存中体现。这样,如果读操作在缓存命中,操作系统获得的系统状态将是以前的状态,这将直接影响I/O操作的执行。
    • 第2个问题是总线竞争。在单总线系统里,内存和设备均需要对总线上的数据进行监听,以确认命令是否针对自己。这样将产生总线竞争而降低系统效率,如图19-3a所示。如果是多总线系统,则I/O设备有可能看不到总线上的地址。

    对于第1个问题,我们可以使用缓存禁止位。

    对于多总线数据迷失问题,解决的办法有多个:

    • 失败与再试:如果一个请求没有得到内存响应,则将数据发到其他的总线。
    • 窃听:在总线上安装一个窃听装置,负责对数据进行分发。
    • 地址过滤:使用一个过滤装置把地址进行自动过滤到合适的地方。

    但对于单总线竞争问题,则尚无有效的解决办法。

  3. 复合I/O模式

    当然也可以使用上述两种方式的组合,即复合I/O模式。在这种复合模式下,数据缓冲区为内存映射,但是I/O端口为分开的部分。即设备控制器寄存器需要使用特殊命令来访问。

根据CPU在I/O过程中的涉入程度进行分类:

  1. 繁忙等待访问

    不管是否使用内存映射的输入输出,处理器均需要与I/O控制器和数据缓冲区进行数据交换。而这种交换既可以按字节进行,也可以按数据块进行。如果按字节进行,CPU当然需要在整个过程中介入,即CPU在I/O过程中一直处于繁忙状态。

    显然,让CPU在I/O过程中一直保持繁忙不是一件好事情。如果要传输的数据是一片连续的内存空间,则我们就可以把CPU从I/O中解脱出来。解脱出来的办法就是直接内存访问(即Direct Memory Access, DMA)。

  2. 直接内存访问(DMA访问)

    DMA的工作原理是,如果按数据块进行I/O,即需要传输大量数据时,就无须CPU的介入。在这种情况下, 我们可以让I/O设备与计算机内存进行直接数据交换。

    问题是,将CPU从繁忙等待中解脱出来,难道DMA的整个数据读写过程不需要使用处理器的功能吗?当然不是。数据传输当然使用CPU,只不过这里使用的CPU不是计算机里面所有进程共享的CPU,而是由另外一 个CPU来负责数据传输。这个另外的CPU就是DMA控制器。

    这里的关键是DMA里面的CPU可以比通用CPU简单,而且价格便宜很多,它只需要能够以不慢于I/O设备的速度进行数据读写即可。其他复杂功能,如算数运算、移位、 逻辑运算等功能皆可以不要。

    DMA控制器既可以构建在设备控制器里面,也可以作为独立的实体挂在计算机主板上。而以独立形式存在的DMA控制器更为常见。

    DMA输入输出的过程如下所示:

    1. CPU对DMA进行设置,告诉其I/O的起始地址和数据长度。
    2. 启动DMA过程。
    3. DMA进行数据传输。
    4. DMA结束后发出中断。
    5. CPU响应中断并处理结束事宜。
  3. DMA模式的考虑因素

    • 如何访问总线:由于DMA需要使用内存总线,而CPU也需要使用内存总线,这样将形成内存总线竞争。那么DMA使用总线的模式将依赖于CPU使用内存的模式。根据这些模式的不同,我们可以选择周期盗用(cycle stealing)或者爆发模式(burst mode)。
    • 数据存放何处:由于DMA传输的数据量通常较大,数据存放何处就是一个需要考虑的因素。我们既可以将数据直接存入内存,也可以存入DMA缓冲区,然后通过中断让CPU一次性将DMA缓冲区的数据拷入内存。
    • 内存寻址模式:如何寻址内存。是使用虚拟地址还是物理地址?使用物理地址的好处是速度快,缺点是绕过MMU存在安全和可靠性风险。
  4. DMA模式的优缺点

    优点就是将主CPU从I/O中解脱出来。

    缺点是增加了成本和复杂性。而且,由于DMA需要与CPU竞争内存总线,其效率的提高不如理论上的期望。

    因此,并不是任何I/O都应该使用DMA。在下述情况下,应该选择使用主CPU:

    • 设备I/O速度非常高。
    • 主CPU没有其他事情可做。
    • 成本考虑。

I/O软件

I/O软件的目的

具体来说,其目标有如下几项:

  • 设备独立:指的是程序对I/O设备的访问不依赖于设备的物理特征,且在I/O程序的编写时无须事先指定I/O设备。
  • 统一命名:指的是设备或文件的命名不依赖于具体的计算机,这样使用名字将使程序可以在任何机器上运行
  • 错误处理:指的是对输入输出过程中产生的数据错误进行侦测与纠正,而且纠错应该在最靠近硬件的层面上进行
  • 数据传输:指的是实际操控数据在主机和外设之间的传递,例如支持同步(阻塞传输)和异步(中断驱动)数据传输
  • 缓冲:为数据传输提供一个临时存放地,然后在方便的时候将数据拷贝到最后目的地。
  • 共用与独享:指的是将设备尽量变为共享,以增大资源利用率和降低死锁发生的概率。例如,将磁盘、打印机变为共享

逻辑I/O模式

从CPU的涉入程度来分,可以分为可编程I/O和中断驱动I/O,这两种分别对应硬件原理的繁忙等待I/O和直接内存访问。

  1. 可编程I/O原理

    在可编程模式下,CPU等待I/O的完成,即CPU涉入程度很深。这种模式也称为轮询或者繁忙等待。

    显然,在等待打印机状态变为就绪的过程中,CPU不能干任何事情。而每次等待后做的事情只不过是将一个字符发送到打印机的数据寄存器里面。最严重的缺陷是还有可能造成优先级倒挂,进而造成死锁。

    解决的方法就是不繁忙等待,而是使用中断。

  2. 中断驱动I/O原理

    中断驱动就是将CPU从繁忙等待的繁琐中解脱出来。I/O设备处理完这批数据后,向CPU发出中断。CPU响应中断后再发送下一批数据。具体来说,中断驱动I/O的过程如下:

    1)CPU初始化I/O并启动第一次I/O操作。

    2)CPU去忙别的事情。

    3)当I/O完成时,CPU将被中断。

    4)CPU处理中断。

    5)CPU恢复被中断的程序。

  3. 直接内存访问I/O原理

    中断驱动I/O由于在I/O设备工作时无须繁忙等待,其效率比可编程I/O要高。但是它毕竟需要CPU周期性地中断来发送或接收后续的数据。这种频繁中断对于系统效率来说并不是什么好事。

    当然是降低CPU响应中断的频率:从一个I/O任务多次中断变为一个I/O只有一次中断,这就是直接内存访问,我们在硬件角度看I/O时已经讲过了DMA的原理。现在从软件的角度看就更加清楚了。由于一个I/O任务不一定可以一次将数据传输完毕,因此中间还需要CPU进行处理,但为了将主CPU从I/O的繁琐事情中解脱出来,我们另外设立一个CPU来响应一个任务中间的中断。而这个CPU就是DMA控制器。

    更进一步来看,既然有一个单独的CPU来处理I/O,也就没有必要使用中断。就让这个额外的CPU轮询也不会降低系统效率。因此DMA模式下的I/O通常是轮询。

I/O软件分层

从前面所述可以看出,I/O是一件颇为繁琐的事情。它涉及用户空间和内核空间的数据交换、I/O设备的设置与启动、中断响应与返回,而且整个I/O需要提供一个与I/O设备无关的统一界面。为了完成一个繁琐的工作,人们通常会将其分为更小的任务来处理。在I/O软件上自然也不例外。

I/O软件通常按照I/O功能进行分层,每一层有提供独特的功能,并与相邻的层面设计有标准界面。当然,在不同的操作系统中这种分层也是不同的。但一般来说都会有下面几层。

中断服务程序

由于大多数I/O均为中断驱动,中断驱动服务程序就成为绝大部分I/O软件的不可分割的部分。中断服务程序由于直接与硬件相关,针对不同的I/O硬件,中断响应的处理也不尽相同。因此,中断服务程序是I/O软件系统分层里面的最底层。为了降低操作系统的复杂性,中断服务程序的暴露窗口应该越小越好。也就是说,知道中断服务程序或者与其打交道的操作系统部分越小越好。

降低暴露窗口的最好办法是让设备驱动程序负责中断响应。即设备驱动程序启动I/O操作后阻塞,然后等待中断。而一个设备驱动程序可以通过操作信号量或睡觉来进行阻塞。当收到中断请求后,中断服务程序先执行,然后将处于阻塞状态的设备驱动程序解锁。这种解锁可以通过发送信号或者对信号量进行操作。下面为中断处理的步骤:

1)保存没有被中断硬件保存的相关寄存器。

2)设置中断服务程序的上下文。

3)设置中断服务程序的栈。

4)回应中断控制器。

5)重开中断。

6)从保存处恢复寄存器。

7)执行服务程序。

8)设置MMU以执行下一个进程。

9)设置新进程的寄存器。

10)开始执行新进程。

设备驱动程序

设备驱动程序,顾名思义,就是直接驱动I/O设备进行输入或输出操作的软件。它属于与设备控制器直接联系的I/O软件部分。它与具体的I/O设备直接相关,并针对每个特定的I/O设备进行优化。设备控制程序通常由设备制造商提供,但归属于操作系统内核。正因为这一属性,设备驱动程序是操作系统安全的一个巨大隐患。

由于需要直接驱动设备的运行,设备驱动程序必须清楚设备的所有细节。

设备驱动程序需要完成的任务包括:

  • 从上层接收抽象的读写请求。
  • 确保读写操作完成。
  • 初始化设备,开关设备。
  • 对设备的功能进行管理。

多数操作系统都定义了一个标准的块设备界面和字符设备界面。每种界面由一组子程序组成。这组子程序可
以由操作系统的其他部分调用。

在收到一个I/O请求后,设备驱动程序做的第一件事情是检查输入参数是否合法。如果不合法,则返回错误。如果参数正常,则将I/O请求的抽象表示转换为设备能够认识的具体表示,如将线性数据块号转换为磁头、磁道、扇面等。然后,设备驱动程序需要检查设备状态以确认其处于闲置状态。如果设备繁忙,则将I/O请求送入该设备的等待队列里面以待处理。如果设备没有开机,则驱动设备进行并启动电机。

在做完上述各项工作后,剩下的就是真正的I/O操作了。设备驱动程序通过发出一系列的I/O命令到设备控制寄存器里面来进行真正的数据传输工作。如果需要,设备驱动程序需要阻塞并等待中断。而每次从中断醒过来,则需要检查I/O是否正确完成,再将数据传输到上面调用设备驱动程序的应用。

在所有数据传输完毕后,设备驱动程序将返回到调用者那里。

设备独立的操作系统软件

一般来说,设备驱动程序并不直接从用户处接收I/O请求,而是通过另外一层中介获得用户请求。这层介于设备驱动程序与用户程序之间的中介就是设备独立的操作系统软件。操作系统在设计时之所以有这层软件是因为I/O软件的一部分与设备有关,一部分与设备无关。而如果与设备无关,就可以将这部分共用起来,放置在设备驱动程序之上,为用户提供一个统一的I/O界面。

这种对于所有I/O设备都共同的操作包括诸如缓冲、错误报告、分配与释放独享设备、提供设备独立的数据块尺寸等。设备独立的操作系统软件与设备驱动软件之间的分界自然与每个设备有关。

  1. 统一界面

    设备独立的操作系统软件的一个重要目标是提供一个统一的I/O界面。即让所有的I/O设备看上去一样或者相似,这是操作系统惯常扮演的角色。采用的手段则是为设备驱动界面规定一个标准:在下层规定设备驱动程序必须提供的功能清单,在上层规定内核为设备驱动程序提供的功能清单和界面。

  2. 缓冲

    缓冲是几乎所有I/O设备都需要的一种操作。缓冲的目的有两个:一是桥接速度不同的设备,使之可以沟通同步;二是提供灵活的健壮机制,因为在每个缓冲层都可以进行一些健壮性、可靠性和安全性处理。而第1点又有两层意思:一是提高数据传输速度,因为快速设备不必等待慢速设备;二是防止溢出,因为慢速设备来不及处理的数据可以存放在缓冲区而不会丢失。

    当然,缓冲也有缺点,就是降低了数据传输的时效性。因为数据层层缓冲处理是需要时间的。如果一个系统的时效性非常重要,则最好不要使用缓冲,而是在通信双方之间创建一个没有缓冲的直接通道,这样,数据从一方发出后,另一方将马上收到。例如,Mach操作系统的x-kernel就提供这种无缓冲的直接数据通道。自然,这种行为是比较危险的,必须非常小心来避免数据溢出和丢失。

  3. 错误报告

    在I/O操作中,错误是难免的。这是因为I/O需要与计算机外面的世界打交道。而外面的世界自然不如计算机内部的世界井然有序。统计数据表明,计算机壳里面发生数据传输错误的概率很低,而在计算机壳外部进行数据传输发生错误的概率相当高。二者可以相差几百万倍。因此,在I/O中进行错误处理是件十分重要的事情。

    如何进行错误处理则取决于错误的类型。一般来说,错误可以分为程序错误和真正的I/O错误。程序错误就是用户要求设备做一件该设备无法做到的事情,例如从输出设备上读数据。真正的I/O错误当然是指数据读写过程中发生的错误,例如数据读错了,或者磁盘盘片损坏了等。

    对于程序设计错误来说,I/O软件除了将错误报告给用户外,似乎不能做任何别的事情了。但如果是I/O错误,那I/O软件则需要进行适当的纠错操作,看能否消除错误。如果不能消除错误,可以询问调用者如何处理,或者干脆返回一个错误码给用户。

用户层I/O软件

前面说过,设备驱动程序从设备独立的操作系统软件层接受I/O请求。而设备独立的操作系统软件则从用户或应用软件处接收指令。但是用户也好,应用软件也好,如果需要向I/O软件发出指令,得需要一个发出指令的界面。这个用户层可以直接使用的I/O界面就是用户层I/O软件。这一层既然可由用户直接操控,它自然运行在用户空间。

例如,很多读者都见过的count=write(fd, buffer, nbytes)命令就是用户层I/O软件的一部分。这条指令里面的write被很多人误认为是操作系统的系统调用,而实际上并不是。这是一个由高级语言提供的库函数,它将操作系统的相关(在这个例子里是相同的)系统调用包裹起来。用户与这个库函数打交道,而这个库函数在编译出来后,会变成一系列指令,来完成系统调用过程。

除此之外,放在用户层I/O软件里面的I/O功能包括格式化、假脱机等。

posted @ 2021-05-27 19:05  睿阳  阅读(405)  评论(0编辑  收藏  举报