读书笔记
🚤《Operating System:Three Easy Pieces》第三十六章 I/O设备
00 分钟
2023-1-29
2023-4-15
type
status
date
slug
summary
tags
category
icon
password
Property
Apr 15, 2023 03:22 PM

系统架构

先看一个典型系统的架构。
其中,CPU 通过某种内存总线(memory bus)或互连电缆连接到系统内存。图像或者其他高性能 I/O 设备通过常规的I/O 总线(I/O bus)连接到系统,在许多现代系统中会是 PCI 或它的衍生形式。最后,更下面是外围总线(peripheral bus),比如 SCSI、SATA 或者 USB。它们将最慢的设备连接到系统,包括磁盘、鼠标及其他类似设备。
notion image
为什么要用这样的分层架构?简单回答:因为物理布局造价成本
越快的总线越短,因此高性能的内存总线没有足够的空间连接太多设备。另外,在工程上高性能总线的造价非常高。所以,系统的设计采用了这种分层的方式,这样可以让要求高性能的设备(比如显卡)离 CPU 更近一些,低性能的设备离 CPU 远一些。将磁盘和其他低速设备连到外围总线的好处很多,其中较为突出的好处就是你可以在外围总线上连接大量的设备

标准设备

现在来看一个标准设备(不是真实存在的),通过它来帮助我们更好地理解设备交互的机制。
notion image
第一部分是向系统其他部分展现的硬件接口(interface)。同软件一样,硬件也需要一些接口,让系统软件来控制它的操作。因此,所有设备都有自己的特定接口以及典型交互的协议。
第二部分是它的内部结构(internal structure)。这部分包含设备相关的特定实现,负责具体实现设备展示给系统的抽象接口。非常简单的设备通常用一个或几个芯片来实现它们的功能。更复杂的设备会包含简单的 CPU、一些通用内存、设备相关的特定芯片,来完成它们的工作。例如,现代 RAID 控制器通常包含成百上千行固件(firmware,即硬件设备中的软件),以实现其功能。

标准协议

一个(简化的)设备接口包含 3 个寄存器:
  • 一个状态(status)寄存器,可以读取并查看设备的当前状态;
  • 一个命令(command)寄存器,用于通知设备执行某个具体任务;
  • 一个数据(data)寄存器,将数据传给设备或从设备接收数据。通过读写这些寄存器,操作系统可以控制设备的行为。
我们现在来描述操作系统与该设备的典型交互,以便让设备为它做某事。协议如下:
  • 第 1 步,操作系统通过反复读取状态寄存器,等待设备进入可以接收命令的就绪状态。我们称之为轮询(polling)设备(基本上,就是问它正在做什么)。
  • 第2 步,操作系统下发数据到数据寄存器。例如,你可以想象如果这是一个磁盘,需要多次写 入操作,将一个磁盘块(比如 4KB)传递给设备。如果主CPU参与数据移动(就像这个示 例协议一样),我们就称之为编程的 I/O(programmed I/O,PIO)。
  • 第 3 步,操作系统将命令写入命令寄存器;这样设备就知道数据已经准备好了,它应该开始执行命令。
  • 最后一步,操作系统再次通过不断轮询设备,等待并判断设备是否执行完成命令(有可能得到一个指示成功或失败的错误码)。
轮询过程比较低效,在等待设备执行完成命令时浪费量 CPU 时间,如果此时操作系统可以切换执行下一个就绪进程,就可以大大提高 CPU 的利用率。

利用中断减少 CPU 开销

有了中断后,CPU 不再需要不断轮询设备,而是向设备发出一个请求,然后就可以让对应进程睡眠,切换执行其他任务。
当设备完成了自身操作,会抛出一个硬件中断,引发 CPU 跳转执行操作系统预先定义好的中断服务例程(Interrupt Service Routine,ISR),或更为简单的中断处理程序(interrupt handler)。
中断处理程序是一小段操作系统代码,它会结束之前的请求(比如从设备读取到了数据或者错误码)并且唤醒等待 I/O 的进程继续执行。
中断允许计算与 I/O 重叠(overlap),这是提高 CPU 利用率的关键。如果我们利用中断并允许重叠,操作系统就可以在等待磁盘操作时做其他事情:
notion image
在这个例子中,在磁盘处理进程 1 的请求时,操作系统在 CPU 上运行进程 2。磁盘处理完成后,触发一个中断,然后操作系统唤醒进程 1 继续运行。这样,在这段时间,无论CPU 还是磁盘都可以有效地利用。
使用中断并非总是最佳方案。
  • 假如有一个非常高性能的设备,它处理请求很快:通常在 CPU 第一次轮询时就可以返回结果。此时如果使用中断,反而会使系统变慢:切换到其他进程,处理中断,再切换回之前的进程代价不小。因此,如果设备非常快,那么最好的办法反而是轮询。如果设备比较慢,那么采用允许发生重叠的中断更好。
  • 如果设备的速度未知,或者时快时慢,可以考虑使用混合(hybrid)策略,先尝试轮询一小段时间,如果设备没有完成操作,此时再使用中断。这种两阶段(two-phased)的办法可以实现两种方法的好处。
  • 另一个最好不要使用中断的场景是网络。网络端收到大量数据包,如果每一个包都发生一次中断,那么有可能导致操作系统发生活锁(livelock),即不断处理中断而无法处理用户层的请求。例如,假设一个 Web 服务器因为“点杠效应”而突然承受很重的负载。这种情况下,偶尔使用轮询的方式可以更好地控制系统的行为,并允许 Web 服务器先服务一些用户请求,再回去检查网卡设备是否有更多数据包到达。
  • 另一个基于中断的优化就是合并(coalescing)。设备在抛出中断之前往往会等待一小段时间,在此期间,其他请求可能很快完成,因此多次中断可以合并为一次中断抛出,从而降低处理中断的代价。当然,等待太长会增加请求的延迟,这是系统中常见的折中。

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

如果使用编程的 I/O 将一大块数据传给设备,CPU 又会因为琐碎的任务而变得负载很重,浪费了时间和算力,本来更好是用于运行其他进程。
notion image
进程 1 在运行过程中需要向磁盘写一些数据,所以它开始进行 I/O 操作,将数据从内存拷贝到磁盘(其中标示 c 的过程)。拷贝结束后,磁盘上的 I/O 操作开始执行,此时 CPU 才可以处理其他请求。
💡
关键问题:如何减少 PIO 的开销 使用 PIO 的方式,CPU 的时间会浪费在向设备传输数据或从设备传出数据的过程中。如何才能分离这项工作,从而提高 CPU 的利用率?
使用 DMA(Direct Memory Access)。DMA 引擎是系统中的一个特殊设备,它可以协调完成内存和设备间的数据传递,不需要 CPU 介入。 DMA 工作过程如下:为了能够将数据传送给设备,操作系统会通过编程告诉 DMA 引擎数据在内存的位置,要拷贝的大小以及要拷贝到哪个设备。在此之后,操作系统就可以处理其他请求了。当 DMA 的任务完成后,DMA 控制器会抛出一个中断来告诉操作系统自己已经完成数据传输。修改后的时间线如下:
notion image
从时间线中可以看到,数据的拷贝工作都是由 DMA 控制器来完成的。因为 CPU 在此时是空闲的,所以操作系统可以让它做一些其他事情,比如此处调度进程 2 到 CPU 来运行。因此进程 2 在进程 1 再次运行之前可以使用更多的 CPU。

设备交互的方法

随着技术的不断发展,主要有两种方式来实现与设备的交互:
  • I/O 指令—特权指令(privileged)
    • 这些指令规定了操作系统将数据发送到特定设备寄存器的方法,从而允许构造上文提到的协议。
      例如在 x86 上,in 和 out 指令可以用来与设备进行交互。当需要发送数据给设备时,调用者指定一个存入数据的特定寄存器及一个代表设备的特定端口。执行这个指令就可以实现期望的行为。
      这些指令通常是特权指令(privileged)。操作系统是唯一可以直接与设备交互的实体。例如,设想如果任意程序都可以直接读写磁盘:完全混乱(总是会这样),因为任何用户程序都可以利用这个漏洞来取得计算机的全部控制权。
  • 内存映射 I/O(memory- mapped I/O)
    • 通过这种方式,硬件将设备寄存器作为内存地址提供。当需要访问设备寄存器时,操作系统装载(读取)或者存入(写入)到该内存地址;然后硬件会将装载/存入转移到设备上,而不是物理内存。
两种方法没有一种具备极大的优势。内存映射 I/O 的好处是不需要引入新指令来实现设备交互,但两种方法今天都在使用。

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

每个设备都有非常具体的接口,如何将它们纳入操作系统,而我们希望操作系统尽可能通用。
例如文件系统,我们希望开发一个文件系统可以工作在SCSI 硬盘、IDE 硬盘、USB 钥匙串设备等设备之上,并且希望这个文件系统不那么清楚对这些不同设备发出读写请求的全部细节。
💡
关键问题:如何实现一个设备无关的操作系统 如何保持操作系统的大部分与设备无关,从而对操作系统的主要子系统隐藏设备交互的细节?
这个问题可以通过古老的抽象(abstraction)技术来解决。在最底层,操作系统的一部分软件清楚地知道设备如何工作,我们将这部分软件称为设备驱动程序(device driver),所有设备交互的细节都封装在其中。
这种封装也有不足的地方。例如,如果有一个设备可以提供很多特殊的功能,但为了兼容大多数操作系统它不得不提供一个通用的接口,这样就使得自身的特殊功能无法使用。
因为所有需要插入系统的设备都需要安装对应的驱动程序,所以久而久之,驱动程序的代码在整个内核代码中的占比越来越大。查看 Linux 内核代码会发现,超过 70%的代码都是各种驱动程序。在 Windows 系统中,这样的比例同样很高。

案例研究:简单的 IDE 磁盘驱动程序

看 xv6 源码中一个简单的、能工作的 IDE 驱动程序实现。 IDE 硬盘暴露给操作系统的接口比较简单,包含 4 种类型的寄存器,即控制、命令块、状态和错误。在 x86 上,利用 I/O 指令 in 和 out 向特定的 I/O 地址(如下面的 0x3F6)读取或写入时,可以访问这些寄存器,如图 36.4 所示。
notion image
下面是与设备交互的简单协议,假设它已经初始化了,如图 36.5 所示。
  • 等待驱动就绪。读取状态寄存器(0x1F7)直到驱动 READY 而非忙碌。
  • 向命令寄存器写入参数。写入扇区数,待访问扇区对应的逻辑块地址(LBA),并将驱动编号(master=0x00,slave=0x10,因为 IDE 允许接入两个硬盘)写入命令寄存器(0x1F2-0x1F6)。
  • 开启 I/O。发送读写命令到命令寄存器。向命令寄存器(0x1F7)中写入 READ-WRITE 命令。
  • 数据传送(针对写请求):等待直到驱动状态为 READY 和 DRQ(驱动请求数据),向数据端口写入数据。
  • 中断处理。在最简单的情况下,每个扇区的数据传送结束后都会触发一次中断处理程序。较复杂的方式支持批处理,全部数据传送结束后才会触发一次中断处理。
  • 错误处理。在每次操作之后读取状态寄存器。如果 ERROR 位被置位,可以读取错误寄存器来获取详细信息。
xv6 的 IDE 硬盘驱动程序(简化的):

小结

中断和 DMA,用于提高设备效率。
访问设备寄存器的两种方式,I/O 指令和 内存映射 I/O。
设备驱动程序的概念,展示了操作系统本身如何封装底层细节,从而更容易以设备无关的方式构建操作系统的其余部分。

参考


评论
Loading...