一篇围绕操作系统 I/O 子系统展开的学习笔记。
前面三章分别讨论了进程、内存与文件系统。这三者有一个共同前提:它们都依赖 I/O 子系统与外部世界交互——进程通过 I/O 获得输入、产生输出;内存通过 I/O 与磁盘交换页面;文件系统本身就是构建在 I/O 之上的抽象。
本篇将 I/O 的硬件基础、软件接口和内核服务汇集在一起,给出 I/O 子系统的完整图景。
导览
1. 核心问题
I/O 子系统的核心问题可以压缩为三组:
| 层级 | 核心问题 | 关键对象 |
|---|---|---|
| 硬件层 | 设备连接、控制与数据传输 | 端口、总线、控制器、中断、DMA |
| 软件接口层 | 统一访问种类繁多的设备 | 块设备接口、字符流接口、网络 socket、时钟 |
| 内核服务层 | 调度、缓冲、缓存、保护 I/O 操作 | 请求队列、内核缓冲区、页缓存、设备状态表 |
本篇按这三层的顺序展开:先介绍硬件层的端口、总线、控制器以及三种交互方式(轮询、中断、DMA);再整理应用程序看到的 I/O 接口分类;然后讨论内核 I/O 子系统提供的调度、缓冲、缓存、假脱机等服务;最后给出 I/O 请求从系统调用到硬件信号的完整生命周期,以及性能相关考量。
I/O 硬件
1. 端口、总线与控制器
设备与计算机的通信通过连接点(端口,port)进行。 如果多个设备共享一组通用线路,则称为总线(bus)——总线是一组线路加上严格定义的传输协议。 典型的 PC 总线结构如下:
| 总线 | 连接对象 | 特点 |
|---|---|---|
| PCI / PCIe | 处理器-内存子系统 + 快速设备 | 高吞吐量,如磁盘、显卡 |
| 扩展总线 | 相对较慢的设备 | 键盘、串口、USB |
| SCSI / SATA | 磁盘与控制器 | 存储设备专用 |
控制器(controller)是操作端口、总线或设备的一组电子器件。简单控制器(如串口控制器)可能是单个芯片;复杂控制器(如 SCSI 控制器)通常有独立处理器、微代码和专用内存。磁盘驱动器上附着的电路板本身就是磁盘控制器。
CPU 通过读写控制器的寄存器来发送命令和数据。有两种寄存器访问方式:
| 方式 | 原理 | 特点 |
|---|---|---|
| 端口映射 I/O | 通过专用 I/O 指令(如 in/out)访问端口地址 | 地址空间独立,需要特殊指令 |
| 内存映射 I/O(MMIO) | 设备寄存器映射到 CPU 物理地址空间,通过普通访存指令读写 | 编程简单,但易受野指针误写影响 |
大多数系统混合使用两种技术。I/O 端口通常由四类寄存器组成:
| 寄存器类型 | 方向 | 用途 |
|---|---|---|
| 数据输入 | 主机 ← 设备 | 主机读出以获取数据 |
| 数据输出 | 主机 → 设备 | 主机写入以发送数据 |
| 状态寄存器 | 主机 ← 设备 | 反映当前命令是否完成、是否有数据可读、是否有故障 |
| 控制寄存器 | 主机 → 设备 | 启动命令或更改设备模式 |
2. 轮询
主机与控制器之间的基本握手协议通过两个位完成:控制器的忙位(busy bit,位于状态寄存器)和主机的命令就绪位( command-ready bit,位于命令寄存器)。典型握手流程:
| 步骤 | 执行者 | 动作 |
|---|---|---|
| 1 | 主机 | 重复读取忙位,直到清零 |
| 2 | 主机 | 设置写命令位,写出数据到数据输出寄存器 |
| 3 | 主机 | 设置命令就绪位 |
| 4 | 控制器 | 检测到命令就绪位,设置忙位 |
| 5 | 控制器 | 读取命令寄存器,执行 I/O 操作 |
| 6 | 控制器 | 清除命令就绪位,清除故障位,清除忙位 |
步骤 1 就是轮询(polling)—— 主机在循环中反复读取状态寄存器直到忙位清除。 对于短暂等待,轮询开销可低至三条 CPU 指令(读寄存器、位与、条件跳转),完全可接受。但如果设备响应慢,CPU 将浪费大量时间空转等待。
3. 中断
中断(interrupt)让设备在准备好服务时主动通知 CPU,而不是让 CPU 反复轮询。
基本中断机制:CPU 硬件有一条中断请求线(IRL,Interrupt-Request Line);每条指令执行后 CPU 检测 IRL。当控制器在 IRL 上发出信号(引发中断),CPU 保存当前状态并跳转到内存固定位置的中断处理程序(interrupt handler); 处理程序确定中断原因、执行处理、恢复状态,最后通过返回中断指令回到原执行流。
现代操作系统需要更复杂的中断机制:
| 需求 | 实现方式 |
|---|---|
| 在关键处理时延迟中断 | 可屏蔽中断线 + 中断优先级 |
| 高效分派到对应处理程序 | 中断向量(interrupt vector)表,按编号索引处理程序 |
当设备数量超过中断向量条目时,使用中断链(interrupt chaining):向量中的每个元素指向一个中断处理程序链表,逐个调用直到找到能处理请求的那个。
中断不仅是设备通知机制。操作系统还将中断机制用于:
| 用途 | 对应机制 | 说明 |
|---|---|---|
| 异常处理 | 除以零、缺页、非法访问 | 触发预先注册的处理程序 |
| 系统调用 | 软中断 / 陷阱 | 用户态切换到内核态的入口 |
| 内核控制流 | 多优先级中断线程 | 如 Solaris 的中断线程 |
其中,陷阱(trap,也称软中断)由软件指令主动触发,而非来自外部硬件信号。当用户进程执行陷阱指令(如 int 0x80 或 syscall)时,CPU 保存用户态上下文、切换到内核模式,并根据系统调用号分派到对应的内核服务程序。 陷阱的优先级通常低于硬件中断——外部设备的数据可能因处理不及时而丢失,而系统调用来自用户进程的主动请求,短暂的排队不会导致数据丢失。
4. 直接内存访问
对于需要传输大量数据的设备(如磁盘),如果让 CPU 逐字节地将数据从控制器数据寄存器搬到内存(程序控制 I/O,PIO),会大量消耗 CPU 时间。直接内存访问(DMA,Direct Memory Access)把传输任务交给专用 DMA 控制器:
| 步骤 | 执行者 | 动作 |
|---|---|---|
| 1 | 主机 | 在内存中构建 DMA 命令块(源地址、目标地址、传输字节数) |
| 2 | 主机 | 将命令块地址写入 DMA 控制器,然后继续其他工作 |
| 3 | DMA 控制器 | 独立操作内存总线,将地址放到总线,执行传输 |
| 4 | 设备控制器 | 通过 DMA-request / DMA-acknowledge 线与 DMA 控制器握手 |
| 5 | DMA 控制器 | 传输完成后中断 CPU |
DMA 控制器同样在传输完成后通过中断通知 CPU。二者的不同之处在于:普通设备中断通常表示”设备就绪,可以开始下一字节/块的传输”,CPU 需要介入每次小粒度的数据搬运;DMA 中断则表示”整个批量传输已完成”,CPU 只在起始和结束时介入。换言之,DMA 将中断从”逐字节通知”压缩为”整批通知”,大幅减少了中断频率和 CPU 干预。
DMA 控制器占用内存总线时,CPU 暂时不能访问主存,但仍可访问 CPU 缓存。这种周期窃取(cycle stealing)通常会带来净性能提升。
| 概念 | 含义 |
|---|---|
| PIO | CPU 逐字节执行 I/O 传输,消耗 CPU 周期 |
| DMA | 专用控制器接管总线,CPU 只需发起和收尾 |
I/O 硬件的四个核心概念可以整理为下表:
| 概念 | 作用 |
|---|---|
| 总线 | 连接设备与计算机 |
| 控制器 | 操作端口、总线或设备 |
| I/O 端口 | 状态/控制/数据寄存器,CPU 与控制器通信的接口 |
| 握手 | 主机与控制器之间的生产者-消费者协议 |
三种握手执行方式的对比:
| 方式 | 触发方式 | 适用场景 | 代价 |
|---|---|---|---|
| 轮询 | CPU 主动循环检查状态位 | 设备响应极快 | 忙等待 CPU |
| 中断 | 设备通过 IRQ 通知 CPU | 设备响应慢或不可预测 | 频繁上下文切换开销 |
| DMA | DMA 控制器接管总线传输 | 大量数据传输(磁盘、网络) | 周期窃取 |
I/O 软件接口
1. 设备分类
操作系统将种类繁多的设备按特征维度进行分类,以便提供统一接口:
| 维度 | 分类 |
|---|---|
| 数据传输模式 | 字符流 / 块 |
| 访问方式 | 顺序 / 随机 |
| 传输时序 | 同步 / 异步 |
| 共享性 | 专用 / 共享 |
| 操作方向 | 只读 / 只写 / 读写 |
| 速度 | 每秒数字节 ~ 每秒数 GB |
差异封装在内核模块(设备驱动程序)中。驱动程序一方面定制以适应特定设备,另一方面向内核 I/O 子系统提供统一接口。
内核 I/O 结构如下:
\begin{array}{c}
\text{应用程序} \\
\hline
\text{内核 I/O 子系统(调度、缓冲、缓存、假脱机)} \\
\hline
\text{设备驱动程序层(SCSI 驱动、键盘驱动、鼠标驱动……)} \\
\hline
\text{设备控制器层(SCSI 控制器、键盘控制器……)} \\
\hline
\text{硬件设备}
\end{array}2. 块设备与字符设备
| 接口类型 | 单位 | 典型 API | 典型设备 |
|---|---|---|---|
| 块设备 | 字节块 | read()、write()、seek() | 磁盘、SSD |
| 字符流 | 字节 | get()、put() | 键盘、鼠标、串口 |
块设备接口支持随机访问,通常通过文件系统接口访问。字符流接口适合顺序产生数据或消费数据的设备。
三种特殊的设备访问模式:
| 模式 | 含义 | 适用场景 |
|---|---|---|
| 原始 I/O | 绕过文件系统,直接以线性块数组访问块设备 | 数据库管理系统、交换空间 |
| 直接 I/O | 禁止操作系统缓冲和锁定,但仍通过文件系统访问 | 应用程序自行管理缓存的场景 |
| 内存映射 I/O | 通过内存字节数组访问磁盘存储,按需分页实际传输 | 程序加载、共享内存 |
3. 网络设备
网络 I/O 的寻址和性能特征与磁盘 I/O 明显不同,因此操作系统通常提供独立的网络套接字(socket)接口:
| 系统调用 | 作用 |
|---|---|
socket | 创建套接字 |
connect | 连接远程地址 |
listen/accept | 等待/接受远程连接 |
send/recv | 通过连接发送和接收数据 |
select | 管理一组套接字,获知哪些可读/可写 |
select 消除了网络 I/O 中对轮询和忙等的需要——调用者知道哪个套接字已有接收数据或可发送数据时才执行操作。
4. 时钟与定时器
硬件时钟和定时器提供三种基本功能:
| 功能 | 对应 API | 用途 |
|---|---|---|
| 获取当前时间 | gettimeofday 等 | 时间戳、日志 |
| 获取经过时间 | clock_gettime 等 | 性能测量 |
| 定时触发操作 | setitimer、alarm | 调度抢占、缓冲刷新、网络超时 |
可编程间隔定时器(programmable interval timer)可设置为等待一段时间后触发中断(单次或周期)。内核在此基础上模拟虚拟时钟,维护按时间排序的定时器列表,支持比硬件信道数更多的定时请求。
5. 阻塞与非阻塞、同步与异步
首先需要区分两组正交的概念:
| 维度 | 含义 | 关注点 |
|---|---|---|
| 阻塞/非阻塞 | 系统调用是否挂起调用线程,直到完成才返回 | 调用线程的控制流 |
| 同步/异步 | I/O 完成的通知方式:同步由调用者主动检查,异步由内核主动通知 | 数据就绪的通知机制 |
四者组合后表现为:
| 组合 | 行为 | 调用返回时 |
|---|---|---|
| 同步阻塞 | 调用线程挂起,I/O 完成后内核将其唤醒 | I/O 完整完成后 |
| 同步非阻塞 | 调用不挂起,立即返回已传输的字节数;调用者需反复重试直到完成 | 立即返回,可能只传输了部分数据 |
| 异步阻塞 | 调用不挂起,立即返回;I/O 在后台执行,完成时通过信号/回调通知;调用者阻塞等待该通知 | 立即返回,但调用者可另外阻塞等待完成通知 |
| 异步非阻塞 | 调用立即返回,I/O 在后台执行,完成时通过信号/回调通知,调用者继续执行 | 立即返回,最终完成时异步通知 |
实践中,POSIX 提供的异步 I/O 接口(aio_read/aio_write)属于"异步非阻塞"模型。大多数文件 I/O 系统调用(如 read/write )默认属于"同步阻塞"模型;设置 O_NONBLOCK 后变为"同步非阻塞"。select/poll/epoll 则提供了在多个 fd 上同步非阻塞地等待就绪事件的机制。
内核 I/O 子系统
1. I/O 调度
调度一组 I/O 请求即决定它们的执行顺序。操作系统为每个设备维护一个请求等待队列,I/O 调度程序重新排列队列以改善整体性能。 状态信息保存在设备状态表(device-status table)中:
调度策略与设备类型紧密相关。磁盘调度已在文件系统硬件 中详细展开(FCFS、SSTF、SCAN/C-SCAN、LOOK/C-LOOK),此处仅指出调度在 I/O 子系统中的位置:它位于设备驱动程序之上,按公平性和优先级统一管理所有进程对同一设备的访问请求。
2. 缓冲
缓冲区(buffer)是内存中用于暂存两个设备之间或设备与应用程序之间传输数据的区域。缓冲的三个理由:
| 理由 | 说明 | 例子 |
|---|---|---|
| 速度不匹配 | 生产者和消费者速度差异大 | 调制解调器(~56Kbps)→ 硬盘(~100MB/s) |
| 传输大小不一致 | 设备传输单元大小不同,需要分段与重组 | 网络分组的分段和重组 |
| 复制语义 | 保证写入磁盘的是系统调用瞬间的缓冲区版本,而非应用程序后续修改后的版本 | write() 时内核先复制用户缓冲区到内核缓冲区 |
常见缓冲策略:
| 策略 | 做法 | 效果 |
|---|---|---|
| 单缓冲 | 一个缓冲区交替供生产者和消费者使用 | 二者不能同时操作 |
| 双缓冲 | 两个缓冲区轮换,生产者填一个时消费者处理另一个 | 解耦生产者与消费者,允许一定程度的重叠 |
| 循环缓冲 | 多个缓冲区组成环形队列 | 适合突发性数据流 |
3. 缓存
缓存(cache)是保存数据副本的高速内存区域。缓冲与缓存的区别:
| 概念 | 含义 |
|---|---|
| 缓冲 | 可以保存数据项的唯一现有副本 |
| 缓存 | 只提供位于其他地方的数据项的更快存储副本 |
实践中,同一个内存区域可兼用于缓冲和缓存。例如内核中的缓冲区缓存(buffer cache):既作为磁盘 I/O 的缓冲区(满足复制语义),又作为文件数据的缓存(共享文件可快速命中,写入可在数秒内合并为大块传输)。
4. 假脱机与设备预留
有些设备(如打印机、磁带机)不能交错接收多个应用程序的输出。假脱机(spool)是处理这类设备的方式:
| 步骤 | 动作 |
|---|---|
| 1 | 应用程序的输出被拦截,写入单独的磁盘假脱机文件 |
| 2 | 应用程序完成后,假脱机系统排序所有假脱机文件 |
| 3 | 逐个将假脱机文件复制到设备 |
处理并发设备访问的另一种方式是提供显式的互斥协调——分配空闲设备,使用完毕后释放。
5. 错误处理与 I/O 保护
I/O 故障分两类:
| 类型 | 原因 | 处理方式 |
|---|---|---|
| 瞬态故障 | 网络过载、暂时信号干扰 | 重试(read 重试、send 重发) |
| 永久故障 | 控制器缺陷、磁盘物理损坏 | 返回错误码给应用程序 |
Unix 通过 errno 变量返回约 100 种错误代码表示失败的大致性质。
I/O 保护的核心机制:
| 保护手段 | 做法 |
|---|---|
| I/O 指令特权化 | 所有 I/O 指令定义为特权指令,用户态必须通过系统调用 |
| 内存保护 | 阻止用户进程访问内存映射 I/O 端口区域 |
| 访问控制 | 文件系统和设备文件的所有权与权限检查 |
I/O 请求的生命周期
下面以同步阻塞 read(fd, buf, n) 为典型路径,追踪一个 I/O 请求从用户态系统调用到硬件信号再返回的完整过程。非阻塞与异步路径的区别在最后单独说明。
| 步骤 | 执行者 | 动作 |
|---|---|---|
| 1 | 用户进程 | 对已打开的 fd 调用 read();CPU 执行陷阱指令,切换到内核态 |
| 2 | 内核系统调用代码 | 检查参数;若数据已在缓冲区缓存命中,直接复制到用户空间并返回;否则继续 |
| 3 | I/O 子系统 | 将进程从运行队列移到该设备的等待队列(阻塞);按调度策略排列请求,发送到设备驱动程序 |
| 4 | 设备驱动程序 | 分配内核缓冲区用于 DMA 接收;将读写命令写入设备控制器的控制寄存器 |
| 5 | 设备控制器 | 控制设备硬件执行数据传输(磁头寻道、扇区读取等) |
| 6 | DMA 控制器/设备 | DMA 控制器接管内存总线,将数据从设备传输到步骤 4 分配的内核缓冲区;传输完成时产生中断 |
| 7 | 中断处理程序 | 通过中断向量表接收中断,保存现场,通知设备驱动程序;若采用多优先级中断,此处可能只完成关键处理,其余部分以低优先级中断线程延后执行 |
| 8 | 设备驱动程序 | 确定请求状态(成功/失败),向内核 I/O 子系统发送完成信号 |
| 9 | I/O 子系统 | 将数据从内核缓冲区复制到用户空间 buf;将进程从等待队列移到就绪队列(唤醒) |
| 10 | 调度程序 | 为进程分配 CPU 时,进程从 read() 返回用户态继续执行 |
如果是同步非阻塞路径:步骤 3 不挂起进程,若数据未就绪则立即返回 -1 并设 errno = EAGAIN。如果是异步非阻塞路径:步骤 1-2 之后直接返回到用户态,步骤 3-10 在后台执行,完成时通过信号(SIGIO)或 aio_error/aio_return 通知用户进程。
整个过程涉及两次上下文切换(步骤 2→3 阻塞,步骤 9→10 唤醒)、至少一次中断和至少一次跨内核边界的数据复制——这些正是 I/O 性能代价的核心来源。
性能
I/O 是系统性能的主要因素。开销来源:
| 开销来源 | 说明 |
|---|---|
| 上下文切换 | 进程阻塞/唤醒导致的状态保存与恢复 |
| 中断处理 | 每次中断需要状态保存、分派处理程序、恢复状态 |
| 数据复制 | 设备 → 内核缓冲 → 用户空间的多重复制 |
| 内存总线竞争 | DMA 与 CPU 争用总线带宽 |
改善 I/O 效率的几种方法:
| 方法 | 原理 |
|---|---|
| 减少上下文切换 | 使用内核线程实现 I/O 处理,避免频繁进出内核 |
| 减少数据复制 | 利用虚拟内存映射和写时复制,避免显式 memcpy |
| 减少中断频率 | 大块传输、智能控制器、在忙等待更有效时使用轮询 |
| 将处理原语移至硬件 | 将简单复制等操作交给硬件 |
每一层都有取舍:在应用程序级别实现算法最灵活但性能最低;在内核中实现性能更好但开发更复杂;在硬件中实现性能最高但灵活性最低且成本最高。
小结
I/O 子系统的主干按三层组织:
| 层级 | 关键词 | 操作 |
|---|---|---|
| 硬件层 | 端口、总线、控制器、中断、DMA | 轮询 / 中断握手、DMA 传输 |
| 软件接口层 | 块设备、字符流、网络 socket、时钟、定时器 | read/write、send/recv、select |
| 内核服务层 | 请求队列、缓冲区、缓存、设备状态表 | 调度、缓冲、缓存、假脱机、错误处理与保护 |
理解 I/O 系统的关键是追踪一个请求如何穿过这几层——从应用程序的系统调用,到内核 I/O 子系统的调度与缓冲,再到设备驱动程序的寄存器写入,最后到硬件层面的中断或 DMA 传输。每一步的转换都有明确的代价和取舍,这些代价和取舍即是 I/O 性能优化的出发点。