八:操作系统设备管理之I/O 操作方法
深入理解操作系统:I/O 操作方法(Polling, Interrupts, DMA)
在上一篇文章中,我们探讨了操作系统进行 I/O 操作所依赖的硬件基础:设备控制器、端口和总线,以及 CPU 与设备通信的两种方式(内存映射 I/O 和端口映射 I/O)。硬件提供了与设备交互的能力,但操作系统如何高效地利用这些硬件来实现实际的 I/O 操作呢?这就涉及到不同的 I/O 操作方法。
本篇文章将详细介绍操作系统与设备控制器协同完成 I/O 操作的三种主要方法:程序控制 I/O (Polling)、中断驱动 I/O (Interrupt-Driven I/O) 和 直接内存访问 (Direct Memory Access - DMA)。理解这些方法对于把握操作系统如何管理设备、如何平衡 CPU 利用率与 I/O 效率至关重要。
核心问题:CPU 如何得知设备状态?
无论哪种 I/O 方法,都绕不开一个基本问题:CPU 如何知道设备是否准备好接收数据、是否已经完成了请求的操作、或者是否有数据可供读取?设备的速度通常比 CPU 慢几个数量级,CPU 不能只是发出一个命令然后立刻期望结果。它必须等待。如何有效地等待和协调,就是这些不同方法解决的问题。
方法一:程序控制 I/O (Programmed I/O, Polling)
这是最简单、也最原始的 I/O 方法。在这种方式下,CPU 发出 I/O 命令后,会不断地检查设备控制器的状态寄存器,直到设备准备好或操作完成。这个过程就像 CPU 不停地去敲设备的门,问:“你好了吗?你好了吗?”
工作流程:
- 操作系统(通过设备驱动程序)将 I/O 命令(写入命令寄存器)、数据(写入数据寄存器)和/或状态读取请求(读取状态寄存器)发送给设备控制器。
- CPU 进入一个忙等待循环 (Busy-Waiting Loop)。
- 在循环中,CPU 反复读取设备控制器的状态寄存器,检查特定的状态位(例如,“忙碌”位、 “完成”位、 “数据准备好”位、 “发送缓冲区空闲”位等)。
- CPU 一直循环,直到状态寄存器中的某个位表明设备已经完成操作或准备就绪。
- 一旦状态位符合要求,CPU 就可以进行下一步操作(例如,从数据寄存器读取数据,或者写入更多数据)。
优点:
- 简单易实现: 控制逻辑相对直接,不需要复杂的中断处理机制。
- 可预测性: 如果知道设备的响应时间范围,可以相对确定地知道何时数据可用。
- 适用于简单、快速的操作: 对于那些快速完成且 CPU 可以承担少量等待的设备,可能勉强可用。
缺点:
- 浪费 CPU 周期: 这是其主要缺点。在忙等待期间,CPU 无法执行任何其他有用的计算任务。如果设备很慢或者 CPU 需要等待很长时间,CPU 的利用率会非常低。
- 响应延迟: 如果设备完成操作的速度很快,但在 CPU 下一次检查状态之前完成,CPU 仍然需要等待直到下一次检查才能发现。如果轮询频率太低,可能会错过快速发生的事件。
- 不适用于多任务环境: 在一个多任务操作系统中,一个进程进行轮询会导致整个系统性能下降,因为 CPU 被一个进程的等待所独占。
举例:
想象一个非常简单的,一次只能处理一个字符的串口设备。CPU 想要发送一个字符 ‘A’。
- 驱动程序将 ‘A’ 写入串口控制器的数据寄存器。
- 驱动程序进入一个循环:
While (串口状态寄存器的 '发送缓冲区空闲' 位 == 0) { /* 什么也不做,继续循环 */ }
- CPU 会不断地读取串口状态寄存器,检查那个特定的位。
- 串口控制器发送完 ‘A’ 后,会将 ‘发送缓冲区空闲’ 位设置为 1。
- CPU 在下一次检查时发现该位为 1,退出循环。
- 现在 CPU 可以发送下一个字符了。
在这个过程中,CPU 在等待串口发送 ‘A’ 的这段时间内,什么别的也做不了,一直在循环检查状态。这对于比 CPU 慢得多的设备来说是极度低效的。
方法二:中断驱动 I/O (Interrupt-Driven I/O)
为了克服程序控制 I/O 浪费 CPU 周期的问题,引入了中断驱动 I/O。在这种方式下,CPU 发出 I/O 命令后,可以立即去做其他事情,而设备控制器在完成操作后,会主动通知 CPU。这个通知机制就是中断 (Interrupt)。
工作流程:
- 操作系统(通过设备驱动程序)将 I/O 命令和任何必要的数据发送给设备控制器。
- CPU 不再等待,立即切换去执行其他进程或任务。
- 设备控制器独立执行 I/O 操作。
- 操作完成后,设备控制器通过中断控制器向 CPU 发送一个中断请求信号。
- CPU 当前正在执行的任务被打断。
- CPU 完成当前指令后,保存被打断任务的上下文(寄存器状态等)。
- CPU 跳转到一个预定义的中断服务程序 (Interrupt Service Routine, ISR) 的入口地址。每个设备类型通常有对应的 ISR。
- ISR 执行:它与设备控制器通信,读取状态寄存器以确定中断原因,如果是数据可用,就从数据寄存器读取数据(或将数据写入数据寄存器进行发送)。ISR 清除中断标志,并通知操作系统 I/O 操作的状态(例如,成功完成,或发生错误)。
- ISR 执行完毕后,CPU 恢复之前保存的被打断任务的上下文,并从被打断的地方继续执行。
优点:
- 高效利用 CPU: CPU 不需要在 I/O 等待期间忙碌,可以执行其他有用的工作。
- 响应性好: 设备完成操作后能及时通知 CPU,系统能更快地响应外部事件。
- 适用于多任务环境: CPU 可以在等待 I/O 完成时调度和运行其他进程。
缺点:
- 中断处理开销: 每次中断都需要保存和恢复 CPU 状态,以及执行 ISR,这本身会消耗 CPU 时间。对于产生大量频繁中断的设备(如高速网络接口),中断开销可能变得显著。
- 数据传输效率问题: 即使是中断驱动,实际的数据传输(从设备数据寄存器到内存,或反之)通常仍由 CPU 在 ISR 中通过读写端口或内存映射地址来完成。对于大量数据的传输,每次传输少量数据(如一个字节或一个字)并产生中断仍然效率不高。
举例:
想象一个键盘设备。用户按下 ‘B’ 键。
- 键盘控制器检测到按键,将 ‘B’ 的扫描码存入其内部数据寄存器。
- 键盘控制器向中断控制器发送一个中断请求信号。
- 中断控制器将该信号转发给 CPU。
- CPU(例如正在运行一个文字处理器进程)当前执行的任务被打断。
- CPU 保存文字处理器进程的状态,跳转到键盘设备的中断服务程序 (ISR)。
- 键盘 ISR 执行:它读取键盘控制器的状态寄存器,确认是按键中断。然后从数据寄存器读取扫描码 ‘B’。ISR 将 ‘B’ 放入操作系统的键盘输入缓冲区,并清除键盘控制器的中断标志。
- 键盘 ISR 执行完毕。
- CPU 恢复文字处理器进程的状态,继续执行被打断前的任务。文字处理器进程稍后会从操作系统缓冲区读取按下的 ‘B’ 字符。
通过中断,CPU 不必浪费时间轮询键盘,只有在按键发生时才会被通知。
方法三:直接内存访问 (Direct Memory Access, DMA)
尽管中断驱动 I/O 提高了 CPU 的利用率,但对于需要传输大量数据的设备(如硬盘、固态硬盘、网络接口卡、图形处理器等),中断开销和 CPU 参与数据传输本身的开销仍然很高。每次传输一小块数据(如一个字节或一个字)并在 ISR 中处理效率低下。DMA 解决了这个问题。
DMA 允许设备控制器直接与主内存交换数据,而不需要 CPU 的干预。CPU 只需要设置好 DMA 传输,然后就可以去做其他事情了。数据传输由一个专门的硬件单元——DMA 控制器 (DMA Controller) 或设备控制器中集成的 DMA 逻辑来完成。
工作流程:
- 操作系统(通过设备驱动程序)准备好数据:
- 对于读取操作(从设备到内存),驱动程序分配一个内存缓冲区来接收数据。
- 对于写入操作(从内存到设备),驱动程序确保要写入的数据已准备好在内存缓冲区中。
- 操作系统告诉 DMA 控制器(或具有 DMA 功能的设备控制器):
- 要传输的数据的内存地址(源地址或目标地址)。
- 要传输的数据量(字节数)。
- 数据传输是读取还是写入。
- 涉及的设备。
- 操作系统向设备控制器发出命令,启动 I/O 操作,并通知设备使用 DMA 进行数据传输。
- CPU 不再等待,立即切换去执行其他进程或任务。
- DMA 控制器接管数据传输:它直接与设备控制器通信,并通过系统总线访问主内存。DMA 控制器通常会成为总线主控器 (Bus Master),暂时控制地址总线、数据总线和部分控制总线,以便在设备和内存之间传输数据块,而无需 CPU 的参与(CPU 可能需要短暂地暂停访问总线,但这比 CPU 亲自传输数据效率高得多)。
- DMA 控制器传输整个数据块。
- 数据传输完成后,DMA 控制器向 CPU 发送一个中断请求信号,通知传输已完成。
- CPU 收到 DMA 完成中断,保存当前上下文,跳转到相应的 ISR(DMA 完成 ISR 或设备 ISR)。
- ISR 检查传输状态(是否成功,是否有错误),并根据需要处理后续操作(例如,通知等待数据的进程)。
- ISR 执行完毕,CPU 恢复被打断任务的上下文,继续执行。
优点:
- 极高的传输效率: 特别适合传输大量数据块,因为 CPU 仅在传输开始和结束时介入。
- CPU 利用率最高: CPU 在数据传输的大部分时间内是空闲的,可以充分用于计算任务。
- 降低总线负载(对于 CPU 来说): 数据直接在设备和内存之间传输,减少了 CPU 通过总线来回搬运数据的次数。
缺点:
- 硬件复杂性: 需要专门的 DMA 控制器或支持 DMA 的设备控制器,硬件成本更高。
- 设置开销: 设置 DMA 传输比简单的端口读写更复杂,CPU 需要向 DMA 控制器写入多个参数。
- 缓存一致性问题 (Cache Coherency): 如果 CPU 的数据缓存中保留了将要被 DMA 写入的内存区域的旧数据,或者 CPU 缓存了刚刚被 DMA 写入的内存区域的数据而没有意识到内存已被更新,就会出现缓存不一致的问题。现代系统有硬件机制(如缓存嗅探 Snooping)或软件机制(如缓存刷新 Cache Flushing/Invalidation)来处理这个问题,但这增加了系统的复杂性。
举例:
用户程序请求读取硬盘上的一个文件块到内存。
- 操作系统分配一个内存缓冲区,例如在地址
0x20000
处,大小 4KB。 - 操作系统告诉磁盘控制器:“请读取硬盘上的扇区 X,并准备通过 DMA 传输数据。”
- 操作系统告诉 DMA 控制器:“请将来自磁盘控制器的数据,通过 DMA 方式传输到内存地址
0x20000
处,共传输 4KB。” - 操作系统命令磁盘控制器启动读取操作 并使用 DMA。
- CPU 继续执行其他任务。
- 磁盘控制器读取扇区数据,将数据通过总线直接发送给 DMA 控制器。
- DMA 控制器接收数据,并将其写入内存地址
0x20000
到0x20000 + 4KB
的范围。DMA 控制器管理总线访问,确保数据正确传输。 - 传输完 4KB 数据后,DMA 控制器向 CPU 发送一个中断。
- CPU 接收中断,执行 DMA 完成 ISR。
- ISR 检查传输是否成功。如果成功,通知等待数据的进程,数据已经在
0x20000
的内存缓冲区中准备好了。
在这个例子中,CPU 不需要亲自一个字节一个字节地从磁盘控制器的数据寄存器读取数据再写入内存,整个 4KB 的数据传输是由 DMA 控制器和磁盘控制器合作完成的,极大地提高了效率。
总结与联系
这三种 I/O 方法代表了效率和复杂度的不同权衡:
- Polling (程序控制 I/O): 最简单,但 CPU 效率最低,浪费大量 CPU 周期等待。适用于非常简单的控制器或对实时性要求不高且 CPU 负载极轻的场景(现代操作系统极少用于常规设备 I/O)。
- Interrupt-Driven I/O (中断驱动 I/O): 提高了 CPU 效率,CPU 无需忙等,可以在等待时做其他事情。适合处理单个事件或传输少量数据的设备(如键盘、鼠标)。
- DMA (直接内存访问): 效率最高,尤其适合批量数据传输。CPU 仅在开始和结束时参与设置和处理完成通知。是现代高速 I/O 设备(硬盘、网卡、显卡)的标准方式。
在实际系统中,这几种方法 often are used in combination. 例如,启动一个磁盘读写操作可能涉及:
- Polling 或 Programmed I/O: CPU 写入命令到磁盘控制器的命令/状态/数据端口,设置 DMA 传输的参数。
- DMA: 实际的大块数据传输(从磁盘到内存,或从内存到磁盘)由 DMA 控制器完成,CPU 在此期间执行其他任务。
- Interrupt: DMA 传输完成后,DMA 控制器或磁盘控制器产生一个中断,通知 CPU 操作完成。CPU 通过中断服务程序来处理完成事件。
操作系统通过设备驱动程序,根据设备的特性和 I/O 请求的需求,选择和协调使用这些不同的硬件交互方法,以实现高效、灵活的 I/O 管理。理解这些基本操作方法,是理解整个操作系统设备管理子系统的重要一步。