《操作系统导论》第四部分 持久性 P1 I/O设备

C1 I/O设备

在了解持久性部分的主要内容前,先介绍I/O设备的概念,并展示操作系统如何与它们交互,当然I/O对计算机系统非常重要,设想一个程序没有任何输入(每次运行都产生相同的结果),或者一个程序没有任何输出(得不到想要的结果),对于计算机系统来说,输入和输出都是必要的

1.1 系统架构

先看一个典型系统的架构:
在这里插入图片描述
CPU通过内存总线连接到系统内存,图像或者其它高性能I/O设备通过常规的I/O总线连接到系统,外围总线(SCSI,SATA,USB)将最慢的设备连接到系统中

采用这样的布局,是因为越短的总线越快,因此高性能的内存总线没有足够的空间连接太多设备,且高性能总线的造价很高所以采用这种分层的布局,让要求高性能的设备(显卡)离CPU更近一点,低性能的设备离CPU远一点,将磁盘和其它低速设备连接到外围总线的好处有很多,如你可以在外围总线上连接大量的设备

1.2 标准设备

在这里插入图片描述
这是一个标准设备,通过它可以理解设备交互的机制,这个标准设备包含两部分重要组件

硬件接口:同软件一样,硬件也需要一些接口,让系统来控制它的操作,所有的设备都有自己的特定接口以及特有的交互协议

内部结构包含设备功能的实现,一些非常简单的设备通常用一个或几个芯片来实现它们的功能,更复杂的设备会包含简单的CPU,一些通用内存,设备相关的特定芯片,来完成它们的工作,如现代RAID控制器通常包含上千行固件(硬件中的软件)

1.3 标准协议

在标准设备的示意图中,设备接口包含了3个寄存器,一个状态寄存器(用于读取并查看当前设备的状态),一个命令寄存器(用于通知设备执行某项任务),一个数据寄存器(将数据传给设备或者从设备接收数据),通过读写标准设备的这些寄存器,操作系统就可以控制该设备的行为

一个简单的交互协议:

//轮询设备当前状态
while(STATUS == BUSY) {
    
    
	;
}
//向数据寄存器和命令寄存器写入数据
Write data to DATA register
Write Command to COMMAND register
//轮询设备是否成功执行命令
while(STATUS == BUSY) {
    
    
	;
}

这个简单的标准协议包含4步:
1,操作系统反复读取状态寄存器,等待设备进入可以直接接收命令的就绪状态,称为轮询设备
2,操作系统下发数据到数据寄存器
3,操作系统将命令写入命令寄存器,这时设备就知道数据已经准备好了,它开始执行命令
4,操作系统不断轮询设备,等待并判断设备是否完成了命令(可能得到一个代表执行成功或失败的数据)

这个协议简单且有效,但难免有些低效和不方便,第一个问题就是轮询比较低效,在等待设备执行完成命令时浪费了大量CPU时间,如果此时操作系统切换到下一个就绪进程,就可以大大提高CPU的利用率

1.4 利用中断减少CPU开销

利用中断可以减少CPU的开销,在上述的标准协议中,有了中断,CPU可以不用通过轮询设备来判断设备是否成功执行命令,而是向设备发出一个请求,然后让当前发起I/O的进程睡眠,切换执行其它进程,当设备执行完命令后,会抛出一个硬件中断,引发CPU跳转执行系统预先定义好的中断服务例程或中断处理程序,它会唤醒先前发起I/O的进程继续执行

通过轮询的方式:在这里插入图片描述
通过中断的方式:
在这里插入图片描述

因此中断允许计算与I/O重叠,这是提高CPU利用率的关键

但是使用中断也并非是最佳方案,考虑下面两个场景:
1,如果有一个非常高性能的设备,它处理请求很快,通常在CPU第一次轮询就能返回结果,如果此时使用中断,反而会让系统变慢,使用中断切换到其它进程,处理中断再切换回来带来了进程切换的开销,如果设备很快,那么最好的方法反而是轮询,如果设备较慢,那么采用允许发生重叠的中断更好,如果设备速度时慢时快,那么可以采用混合策略,先轮询一小段时间,设备还没有完成命令时,再使用中断
2,在网络中,网络端收到大量数据包,如果每个包引发一次中断,那么可能导致操作系统不断处理中断而无法处理用户的请求,这种情况下,采用轮询可以更好控制系统的行为,让服务器先处理一些请求,再轮询网卡是否有数据包到达

对于中断的处理也可以优化,通过合并,设备在抛出中断前先等待一小段时间,在此期间其它请求可能也会完成,就可以将多个中断合并成一次中断抛出,从而降低处理中断的代价

1.5利用DMA进行更高效的数据传送

标准协议还有一点需要注意,如使用编程的I/O(CPU参与数据移动)
将一大块数据传给设备,CPU又会因为琐碎的任务而变得负载很重,浪费了时间和算力
在这里插入图片描述
进程1在运行时需要向磁盘写入一些数据,所以它开始进行I/O操作,CPU将数据从内存拷贝到磁盘,CPU拷贝结束后,磁盘上的I/O操作才开始执行,此时CPU才可以处理其它请求

解决方案就是使用DMA(直接内存访问),DMA引擎是操作系统中的一个特殊设备,它可以协调完成内存和设备间的数据传递,不需要CPU介入

DMA的工作过程:为了将数据传送给设备,操作系统通过编程告诉DMA引擎需要的数据所在内存的位置,要拷贝的大小以及要拷贝到哪个设备,之后操作系统就可以处理其他请求了,当DMA的任务完成后,DMA控制器会抛出一个中断告诉操作系统自己已经完成了数据传输
在这里插入图片描述
数据的拷贝都是由DMA完成的,因此CPU在此时是空闲的,所以操作系统可以让它做一些其它事情,如调度其它进程

1.6 设备交互的方法

操作系统应该如何和设备进行通信,以进行I/O操作?

主要有两种方法实现与设备的交互:
1,使用明确的I/O指令:这些I/O指令规定了操作系统将数据发送到特定设备寄存器的方法,从而允许构造上文提到的协议

如在x86上,in和out指令都可以用来与设备进行交互,当需要发送数据给设备时,调用in指令指定一个存入数据的特定寄存器和一个代表设备的特定端口,执行这个指令就可以实现期望的行为

这些指令通常是特权指令,操作系统是唯一可以直接与设备交互的实体,不允许其它的程序直接读写磁盘,控制外设,这样会变得一团糟

2,内存映射I/O:通过这种方式,硬件将设备寄存器作为内存地址提供,当需要访问设备寄存器时,操作系统装载(读取)或者存入(写入)到该内存地址,然后硬件会将装载/存入转移到设备上,而不是物理内存

这两种方式没有一种具有极大的优势,内存映射I/O的好处是不需要引入新指令来实现设备交互,但两种方法都在使用

1.7 纳入操作系统:设备驱动程序

每个设备都有非常具体的接口,如何将它们纳入操作系统?

我们希望操作系统尽可能地通用,例如文件系统,我们希望开发一个文件系统可以工作在SCSI硬盘,IDE硬盘,USB设备等设备之上,并且希望这个文件系统不那么清楚对这些不同设备发出读写地全部细节

这个问题可以通过抽象来解决,在最底层,操作系统的一部分软件清楚地知道设备如何工作,将这部分软件称为设备驱动程序所有设备交互的细节都封装在其中

Linux的文件系统栈:在这里插入图片描述
文件系统完全不清楚它使用的是什么类型的磁盘,它只需要简单地向通用块设备层发送读写请求即可,块设备层将这些请求交给设备驱动,然后设备驱动来完成真正的底层操作

这种封装也有不足的地方
1,如果一个设备可以提供很多特殊的功能,但因为兼容了大多数操作系统,它不得不提供一个通用的接口,这样就使得自身的特殊功能无处使用

2,因为所有需要插入系统的设备都需要安装对应的驱动程序,所以久而久之,驱动程序在内核代码中占的比重越来越大,Linux内核中70%都是各种驱动程序,因为驱动程序的开发者不是专业的内核开发人员,所以他们更容易写出缺陷,因此他们是内核崩溃的主要贡献者

1.8 简单的IDE磁盘驱动程序

看一个真实的设备-IDE磁盘驱动程序,IDE磁盘暴露给操作系统的接口比较简单,包含4种类型的寄存器,即控制,命令块,状态,错误,在x86上,利用I/O指令in和out向特定的I/O地址读取或写入时,可以访问这些寄存器

下面是与设备交互的简单协议,假设它已经初始化了:
1,等待驱动就绪,读取状态寄存器,直到驱动READY而非忙碌
2,向命令寄存器写入参数,写入扇区数,待访问扇区对应的逻辑块地址,并将驱动编号写入命令寄存器
3,开启I/O,发送读写命令到命令寄存器
4,数据传送(针对写请求),等待驱动状态为READY
5,中断处理,完成后触发中断,恢复进程
6,错误处理,每次操作后读取状态寄存器,如果ERROR被置位,就可以读取错误寄存器获取详细信息

IDE读写主要通过4个函数实现:
1,ide_rw(),它会将一个请求加入队列,调用它的进程睡眠
2,ide_wait_ready(),确保驱动处于就绪状态
3,ide_start_request(),将请求发送到磁盘,进行in/out指令
4,ide intr(),完成后,发生中断,唤醒发起I/O的进程
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43541094/article/details/111483852