持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
IO设备
I/O(Input/Output),即输入/输出是系统的重要组成部分,计算机通过IO存取设备,通过IO和外界交互。程序通过IO执行功能。因此IO与操作系统结合方式是系统是否高效的关键。首先我们需要了解系统IO架构是怎样的。
根据IO数据的来源不同,系统IO架构可分为三层,一是高速内存的IO流,二是高速设备如显卡、高速网卡的IO流,三是慢速设备如磁盘、U盘、键盘鼠标等IO流。现代IO架构也会有专门的芯片组和点对点互连用于加速IO操作,例如x86平台。下图展示了IO架构的传统模型和现代系统的IO架构。
而对于单个设备来说,包含两个部分,一是暴露给操作系统的接口,二是硬件的抽象内部结构。设备接口简化来讲可以分为三个寄存器,分别是状态寄存器、命令寄存器、数据寄存器。操作系统对设备的调用,也可以由这三个寄存器来抽象为以下过程。
通过轮询状态寄存器,我们能够实现设备的长期访问,但是我们也知道,很多情况下轮询总是低效的,因此我们可以用中断来代替轮询,具体的,操作系统可以发出一个请求,将调用进程置于睡眠状态,并将上下文切换到另一个任务,而不是重复轮询设备。因此,IO和计算可以重叠。
但中断并不总是最好的解决方案,一个设备执行任务非常快时第一个轮询通常会找到要完成任务的设备。在这种情况下使用中断实际上会降低系统的速度,并且中断的方法可能造成活锁的问题。所以如果不知道设备的速度,或者有时快,有时慢,最好使用一个混合轮询器,轮询一段时间后,如果设备还没有完成,就使用中断。这种分两阶段的方法可以两全其美。
并且中断是可以合并的,在这样的设置中,需要触发中断的设备首先要等待一段时间,然后才将中断发送给CPU。这样能够降低中断的总体开销。
当我们使用programmed IO(PIO)来操作大块数据进入设备时。CPU会忙于搬运数据,浪费很多CPU时间。解决方案就是我们所说的直接内存访问(DMA)。它本质上是系统中的一种非常特定的设备,它可以在不需要太多CPU干预的情况下协调设备和主存之间的传输。为了将数据传输到设备,操作系统将通过告诉DMA引擎数据在内存中的位置、复制多少数据以及将数据发送到哪个设备来编程。此时,操作系统完成了传输,可以继续进行其他工作。当DMA完成时,DMA控制器引发一个中断,这样OS就知道传输完成了。修改后的时间表。有DMA介入的CPU timpline如下图所示。
让我们再深入设备的内部,看看操作系统是怎么与设备直接交互的,具体来说,有两种方式。
第一种,也是最古老的方法(IBM大型机使用了多年)是使用显式的I/O指令。这些指令为操作系统指定了一种将数据发送到特定设备寄存器的方式。例如x86系统下的in和out指令,要将数据发送到一个设备,调用者指定一个包含数据的寄存器,以及一个指定设备名称的特定端口。
第二种方法称为内存映射I/O。通过这种方法,硬件使设备寄存器可用,就像它们是内存位置一样。为了访问一个特定的寄存器,操作系统发出一个加载(读)或存储(写)地址;然后硬件将加载/存储路由到设备而不是主存储器。也就是像访问内存一样访问设备。
这两种方法都没有很大的优势。内存映射方法很好,因为不需要新的指令来支持它,但这两种方法今天仍然在使用。
但是设备内部到底是如何呢,这要求我们继续讨论设备驱动的概念,这是操作系统能够与设备交互的最底层方式,任何与设备交互的细节都封装在里面。
通过Linux文件系统软件堆栈,我们可以了解这种抽象如何帮助操作系统的设计和实现。
可以看到,文件系统(当然,上面的应用程序)完全不知道它使用的是哪个磁盘类;它只是向通用块层发出块读和写请求,通用块层将它们路由到适当的设备驱动程序,后者处理发出特定请求的细节。上面图还有一个Raw接口,它允许特殊的应用程序(如文件系统检查器,稍后[AD14]描述,或磁盘碎片整理工具)直接读写块,而不使用文件抽象。
上面看到的封装也有其缺点。例如,如果有一个设备具有许多特殊的功能,但是必须向内核的其他部分提供一个通用接口,那么这些特殊功能将不会被使用。例如,在带有SCSI设备的Linux中,有非常丰富的错误报告;因为其他块设备(如ATA/IDE)有更简单的错误处理,所有更高级别的软件曾经接收到的是通用EIO错误代码,因此更详细的错误消息就消失了。
因为任何插入系统的设备都需要设备驱动程序,随着时间的推移,它们已经占据了内核代码的很大比例。对Linux内核的研究表明,超过70%的操作系统代码存在于设备驱动程序中[C01];对于基于windows的系统,这个数字可能也相当高。因此,当人们告诉你操作系统有数百万行代码时,他们实际上是在说操作系统有数百万行设备驱动程序代码。当然,对于任何给定的安装,大多数代码可能都不是活动的。
我们以一个IDE驱动的实际例子来解释操作系统怎么通过驱动程序和设备交互,如下图所示。
- ide_rw():将一个请求排入队列,或者直接发送到磁盘
- ide_start_request():向磁盘发出请求,用in和out指令来读写设备寄存器
- ide_wait_ready():用于确保启动器就绪
- ide_intr():中断处理,发送切换进程信息
我们来做一个小结,这次学习了操作系统和设备架构和设备驱动程序,一个顶层设计,一个是底层原理。对于操作系统设备调用方式的优化,我们讨论了中断,对于CPU调度的优化,我们讨论了DMA。有关操作系统如何与设备交互,我们讨论了显式IO指令和内存映射两种方式,通过这一连串的方法,我们能够以以一种与设备无关的方式来构建操作系统的其余部分(这就是设备驱动程序的核心功能)。