操作系统-5-I/O

《现代操作系统》的笔记,这一篇是I/O。

I/O简介

IO的本质其实就是除去CPU之外的设备,在单片机中我们成为外设。像磁盘,USB,鼠标,键盘,甚至显示屏都是外设。

I/O设备

IO设备大体上被分为三种:

  • 块设备:以块为单位进行读写的设备,比如磁盘。块设备的特点是不能一字节一字节地读写,而必须一个块一个块读写。
  • 字符设备:和块设备相反的设备,只能以一个字符一个字符的方式读写,比如键盘。
  • 其他设备:既不属于块设备也不属于字符设备的设备,比如时钟。

设备控制器

IO不能直接接到CPU上,而必须通过设备控制器接入:

通过控制器的IO

设备控制器提供了一些操作设备的方法,让CPU可以更好地操作设备。

设备控制器中会有一些寄存器,以及一个缓冲区(有些有),比如和硬盘打交道的控制器,并不是磁盘传递一个数据之后就将数据转发给CPU,而是将数据放在缓冲区中,等到缓冲区满了再转发数据。一般块设备的控制器都有缓冲区,并且在读取了一个块到缓冲区后会通过校验和检查数据是否出错。

操作设备的方式

通过端口操纵

设备控制器会暴露端口,软件可以通过端口操控。如在大部分汇编语言中,可以使用inout指令读和写端口。

通过内存映射

有些设备有一个CPU可以读写的缓冲区,通过将这些缓冲区直接映射到内存中就可以像访问内存一样访问缓冲区,十分方便。比如在保护模式下,显卡的显存(VRAM)就被映射到0xB800:0x0000地址处,直接向地址写数据就可以在屏幕上显示字符,而不需要通过端口。

现代CPU一般都是通过两种混合的方式:

端口和内存映射的IO

内存映射是这样工作的:CPU在对内存工作的时候,必须将数据放在地址总线上,然后发起一个READ信号。内存接收到这个信号后,会判断是否在IO映射区,如果在,则对应的IO响应,否则内存响应。

直接存储器存取

如果所有的外设操作都由CPU完成的话,由于需要等待外设完成操作,将会耗费很多CPU时间。有些计算机有专门的直接存储器(DMA)硬件和IO打交道。DMA能够独立于CPU访问IO。DMA一般是可编程的,CPU告诉DMA要打交道的IO和读出的数据应当存放的内存地址后,就由DMA和IO交互,DMA会等待IO的数据,然后放到指定的内存处,CPU之后就可以直接访问内存了:

使用DMA的方式

DMA会发送和CPU一样的读写指令,所以内存并不知道是谁发送了指令,它只管运行。

有些DMA还有如图的做法:不是自己存储数据放入内存,而是直接告诉控制器内存地址,让控制器直接放入内存。

DMA完成IO操作之后,会产生一个中断给CPU,让CPU回来处理数据。

中断的底层实现

中断的底层实现

一般IO都挂载到中断控制器上,由中断控制器来向CPU发送中断。CPU也可以对中断控制器进行设置,比如cli汇编指令就是告诉中断控制器忽略可屏蔽中断。

中断控制器一次只能发送一个中断。一般CPU在运行中断处理程序时会先关闭中断,运行完之后会打开中断。如果IO发现CPU没有响应中断,可能会重复发起中断。

精确和不精确中断

精确中断是指满足一下三个条件的中断:

  • 程序计数器(PC)保存在一个已知的地方
  • PC所指指令之前的所有指令都执行完毕
  • PC所指指令之后的所有指令都没执行
  • PC所指指令的执行状态已知

在实模式下的中断都是精确中断,但是保护模式下就不是了。因为保护模式下新增了指令流水线微操作等特性。

指令流水线是指:将一条指令的执行过程分为很多指令,并传给不同单元执行,每个单元的执行是并行的:

指令流水线基本原理

而为了实现流水线技术,必须将指令分成更小的称为微操作的指令。一旦分解完毕,CPU就可以使用乱序执行技术,将微操作打乱,以自己认为更合理的方式执行。比如指令

1
2
3
4
mov eax, [mem1]
shl eax, 5
add eax, [mem2]
mov [mem3], eax

这里对add eax, [mem2]就可以拆分为读mem2内存微操作和将读出结果写入eax微操作,那么我们就可以在执行shl eax, 5指令的同时将mem2的读操作一并执行,然后再写入。这样指令的顺序就变化了。

有了这两种特性,当产生中断的时候指令执行的状态就不是那么容易获得,并且在流水线的情况下很难明确目前究竟执行到哪一条指令。所以这种中断就称为不精确中断

I/O软件原理

接下来简要探讨IO软件的目标和几种实现方法

设备独立性

存在IO软件或者IO库的目的显然在于封装底层IO的不同操作方式,并且提供统一的接口。比如Unix可以通过open()函数操作硬盘,USB,甚至是网卡(打开Socket),而且在访问的时候不需要指定设备。

Unix下的做法是统一命名,所有的设备都映射为文件,只需要通过常规的文件操作就可以对设备操作了。

IO软件还需要做到错误处理:当IO出错了需要报错或者使用某些手段处理错误。

IO软件还需要提供同步异步操作,也就是我们常听说的异步IO和同步IO。

实现方式

程序控制IO

第一种方式是程序控制IO,用户程序必须通过系统调用访问IO,这将导致代码进入内核空间。内核空间会创建一个缓冲区等待IO数据,填充满之后将缓冲区返回给用户程序。

这里需要注意的是,程序需要一直等待IO数据,如果IO阻塞了需要进行等待,也就是说存在类似下面的代码:

1
2
3
4
5
6
7
8
void read_disk(int disk_num){
  char buffer[1024];
  int i = 0;
  char c;
  while((c=read(disk_num))!=READY){
    buffer[i++] = c;
  }
}

这种方式叫做轮询(Poll)或者忙等待(busy wait)。用高级语言的话说叫做同步IO

中断控制IO

显然程序控制IO很浪费CPU,使用中断的话更好。也就是说当IO完成操作时向CPU发送中断,从而让CPU处理数据。这种就叫做异步IO

使用DMA的IO

最后一种就是使用DMA,如果你的电脑存在DMA硬件的话。

设备驱动程序

一般来说每一类设备都会有一个设备驱动程序,专门用于和这个设备打交道的。设备的厂商也会自己开发设备驱动程序以供给知名的操作系统。

使用设备驱动程序的IO

与设备无关的IO软件

设备驱动程序的统一接口

每次增加新类型的驱动的话就需要新编写驱动程序,并且需要定义和操作系统交互的接口。这还少很麻烦的事情。所以需要统一设备驱动程序的统一接口。Unix就做的很好:将设备全部映射为文件。

缓冲

用户通过IO系统调用进入内核后,内核一般都有一个缓冲区用于缓冲。有些内核甚至存在双缓冲:一个缓冲用于将读好的数据交付给用户程序,一个用于接收从设备来的数据。

有些时候内核没有缓冲,用户程序可以自己定义缓冲。

带有缓冲的IO软件

错误报告

当设备发送错误的时候必须向用户程序报告错误。

分配与释放专用设备

如果一个设备一次性只能被一个进程占用,就需要对设备使用进行检查。这也就是为什么在高级语言中设备是通过文件句柄交互的原因。

与设备无关的块大小

对于块设备来说,不同设备有不同的块大小。IO软件需要定义一个和设备无关大小的块来方便和用户程序交互。

I/O硬件

接下来看一些几乎每个电脑上都有的IO硬件

磁盘

磁盘是典型的块设备。其内部结构如下:

磁盘内部结构

磁盘由多个盘片组成,每个盘片被划分为扇区和磁道。磁道就是用多个同心圆之间的区域,而扇区就是在同心圆区域之间,被从圆心向外发出的直线所分割的区域。每个盘一般有两个盘面。多个盘的同一组磁道在空间上组成了一个圆柱,就被称为柱面。读写磁头就是用于读写磁盘的磁头。

需要注意的是,读写磁头必须一起移动,不能分开移动。

对于磁盘的读写,一般来说都是通过指定柱面,磁道,扇区(CHS方法)三个坐标来读取,但是也有称为LBA直读的方式,其将柱面,磁道,扇区编码成一列,你可以通过读取数组一样的方式来读取。

LBA的优点是软件读取十分方便,但是没有办法利用好磁盘的特点,比如你要读第一LBA和第二LBA,那么磁头必须进行一次移动,从一扇区移动到二扇区。而CHS方法则可以很好地利用磁盘物理结构,如果你想要读取不同盘面的同一扇区的话,磁头只需要移动到对应扇区即可(因为各个盘面的扇区在同一柱面上),这样读取起来十分块。

现在有些机器还是用RAID(廉价磁盘冗余阵列),其实就是讲多个磁盘叠在一起组成大磁盘,然后由专门的RAID设备驱动程序控制。一般用在服务器上。

磁盘臂调度算法

由于磁盘臂只能一起移动,所以需要想一些调度算法。

磁盘的读取速度一般由以下因素决定:

  • 寻道时间(将磁盘臂移动到指定柱面上的时间)
  • 旋转延时(等待指定扇区旋转到指定磁头下面的时间)
  • 实际读取时间

先来先寻道(FCFS)算法

这个算法也是最容易想到的:先来的读写请求先执行。这种算法就属于无脑算法,因为磁盘臂可能需要移动很长的距离。比如读取请求按顺序是这样的(为了简化,假设只有一个盘,盘只有一个磁道):3扇区,5扇区,1扇区,6扇区。那么这样移动顺序为3->5->1->6,一共移动了11个扇区的距离。

最短寻道优先(SSF)算法

这种算法总是优先处理距离当前位置最短的请求,比如存在请求:3,5,1,6。那么移动顺序为3->5->6->1一共走了7个扇区的距离。这种算法的缺点在于,如果近距离的读写请求不断被发送,那么远距离的扇区可能需要等待很长时间。

电梯算法

这个算法是同时也是现实中电梯的算法:先往一个方向移动,直到这个方向上没有扇区时向反方向移动。比如存在3,5,1,6,2扇区的请求,并且现在的方向是向右移动,那么首先读取3->5->6三个扇区,在这个方向上一节没有要读写扇区了,那么向反方向移动,变为6->2->1读取三个扇区。这种算法目前是应用最广的。

时钟

时钟既不属于块设备,也不属于字符设备。时钟由晶振组成,在早期的电脑中,只需要将时钟两端通上电,时钟就可以以一定频率进行振动,从而计时。现在在单片机中我们仍然能看到这样的时钟。

现代计算机一般都是可编程时钟:

可编程时钟

晶振连接着一个计数器,并且还存在一个存储寄存器。当晶振开始工作的时候,存储寄存器将值放入计数器中,晶振每振动一次,计数器的值就减1,如果计数器值为0,时钟向CPU产生时钟中断,并且再次将存储寄存器的值放入技术器中。由于可以编程指定存储寄存器的值,所以是可编程的。这样中断频率就可以通过软件设置了。

一般时钟软件都有如下功能:

  • 维护系统时间
  • 防止进程运行超时
  • 对CPU使用情况进行记账
  • 处理用户提出的alarm请求
  • 为系统各个部分提供监视器
  • 完成概要剖析,统计和监视功能

键盘和鼠标

关于键盘要提的就是系统接收到的键盘按键信息并不是ASCII码,而是按键扫描码(Scancode)。一般键盘不超过128个键,所以使用一个8位寄存器即可记录扫描码,最高位可以用来记录键是否按下的状态。

鼠标没什么可说的,就是返回按键信息和x,y方向上的偏移量。

显示器

也没什么可说的,这里科普一下:显示器有两种模式:字符模式和图像模式(VGA模式),这两种模式的IO内存映射也不一样。字符模式下只能显示字符,不能显示图像,但是显示字符很方便(只需要将字符的ASCII码和颜色信息发送给显卡即可),而图像模式下可以绘制图像,但是显示字符也是通过绘制的方式,比较麻烦。

updatedupdated2023-06-082023-06-08