输入输出设备与操作系统设备抽象
输入输出设备连接计算机的数字状态与外部物理世界。输入设备使计算机感知外部状态,输出设备使计算机对外实施动作;存储设备则进一步允许数据跨越进程退出、系统重启和断电而留存。因此,I/O 是操作系统进入“持久化”部分的硬件入口。
本篇关注的是操作系统如何控制设备、传输数据并向应用程序提供统一接口。磁盘介质、文件系统和崩溃一致性属于后续的存储与文件系统主题。
输入输出设备的硬件接口模型
寄存器化的标准设备模型
从 CPU 的视角看,再复杂的设备最终也表现为若干个具有约定含义的寄存器。典型设备控制器至少包含三类寄存器:
- 数据寄存器:保存输入或输出的数据;
- 状态寄存器:表示设备是否就绪、是否出错、是否完成操作;
- 命令或控制寄存器:接收 CPU 发出的操作和配置。
设备控制器使用地址译码器识别 CPU 发出的地址,并把访问转发到相应寄存器。CPU 访问设备寄存器主要有两种方式:
Port I/O:使用专门的 in/out 指令访问独立的 I/O 地址空间MMIO:把设备寄存器映射到物理地址空间,使用普通 load/store 访问因此,标准设备模型可以概括为:
CPU/驱动程序 │ 读写状态、命令和数据寄存器 ▼设备控制器 │ 控制信号与数据流 ▼物理设备驱动程序的核心职责,就是把应用程序提出的通用操作翻译成对这些设备寄存器和设备协议的具体操作。
设备中断与完成通知
CPU 不必持续轮询设备是否完成。设备控制器可以在事件发生后提出中断请求:
设备完成操作或收到数据 → 控制器设置状态寄存器 → 发送 IRQ 或 MSI/MSI-X → 中断控制器进行路由和优先级仲裁 → CPU 在允许中断时保存现场并进入内核处理程序 → 驱动读取状态、处理结果并确认中断中断只负责通知“设备有事件发生”,通常不负责搬运大量数据。数据可能仍由 CPU 读取,也可能已经由 DMA 传入内存。
典型设备的控制方式
GPIO 与串口控制器
GPIO(General Purpose Input/Output)是最简单的 I/O 设备。一个 MMIO 寄存器中的某一位可以直接对应引脚电平:读寄存器获得外部状态,写寄存器改变输出电平。例如,程序可以通过写 GPIO 寄存器控制 LED。
UART(Universal Asynchronous Receiver/Transmitter)负责串行通信。传统 PC 中,COM1 是第一个串口的逻辑名称,通常以 0x3f8 为 I/O 端口基址;Linux 常将其抽象为 /dev/ttyS0。
#define COM1 0x3f8
outb(COM1, data); // 向串口发送一个字节int ready = inb(COM1 + 5) & 0x1; // 查询是否收到数据int data = ready ? inb(COM1) : -1;程序写入一个字节后,UART 控制器再按波特率和帧格式把它逐位发送出去。因此,COM1 表示一条由操作系统和 UART 管理的串行通信通道,不是某种具体的高级命令。
键盘与磁盘控制器
IBM PC/AT 的 8042 PS/2 键盘控制器把数据、状态和命令分别映射到约定端口。例如,0x60 用于数据,0x64 用于状态或命令;不同命令可以设置键盘 LED、重复速度等属性。
传统 ATA 磁盘控制器也遵循同一模型。CPU 先写入扇区数量、扇区地址和读写命令,再等待设备就绪,最后从数据端口读取扇区。早期 PIO(Programmed I/O)模式中,每个数据字都由 CPU 主动搬运;现代高吞吐设备通常使用 DMA 完成数据传输。
打印设备与页面描述语言
打印程序通常不会直接向打印机发送电机脉冲或“喷嘴打开”之类的机械控制信号。打印路径分为多个抽象层次:
应用程序:绘制线段、文字和图像 → 绘图/打印 API → 打印驱动与打印队列 → PostScript、PCL、IPP 或光栅数据 → 打印机解释器与固件 → 喷头、激光器、感光鼓和电机动作PostScript 是一种页面描述语言,也是一种面向图形状态机的编程语言。它描述页面上的路径、文字、颜色和图像,而不是直接描述打印机的机械动作。例如:
100 100 moveto300 100 linetostroke这段代码表示从 (100, 100) 到 (300, 100) 绘制一条线。打印机或光栅化程序将其转换为像素,再由打印机固件产生具体机械动作。某些绘图仪协议确实包含“抬笔、落笔、移动坐标”等命令,但这仍然是设备协议层,不是应用程序直接操作电机。
摄像设备与 UVC 协议
UVC(USB Video Class)规定了 USB 视频设备的通用接口。主机通常按照以下过程使用摄像头:
枚举设备 → 查询并协商格式、分辨率和帧率 → 分配传输缓冲区 → 启动视频流 → 逐帧接收未压缩数据或 MJPEG/H.264 数据Linux 的 V4L2 子系统通常把摄像头表示为 /dev/videoX:应用程序通过 ioctl 查询和设置参数,通过 mmap 或用户缓冲区接收视频帧。标准化协议使大量摄像头能够复用通用驱动,但底层仍然是在设备与主机之间交换字节流。
设备数据传输与协处理机制
CPU 直接搬运与 DMA 传输
如果没有 DMA,设备和内存之间的数据必须由 CPU 逐字节或逐字搬运:
设备寄存器 → CPU 寄存器 → 内存这种 PIO 模式实现简单,但大量 CPU 时间消耗在重复的数据复制上。DMA(Direct Memory Access)可以看作一个专门执行受限 memcpy 的硬件协处理器。CPU 只负责设置传输方向、内存地址、设备端点和数据长度;DMA 引擎完成搬运,并在结束后通过中断或完成队列通知 CPU。
CPU:配置描述符并启动传输 → DMA:在设备与内存之间搬运数据 → 设备/控制器:记录完成状态并通知 CPUDMA 并不意味着 CPU 完全退出:缓冲区分配、地址映射、权限设置、提交命令、同步以及错误处理仍由 CPU 和驱动程序负责。
共享内存、DMA 与协处理器
这三个概念属于不同维度:
| 概念 | 本质 | 解决的问题 |
|---|---|---|
| 共享内存或共享缓冲区 | CPU 与设备都能访问的数据区域 | 数据和命令放在哪里 |
| DMA | 设备与内存之间的硬件搬运机制 | 大量数据由谁搬运 |
| 协处理器 | 能独立执行特定程序或计算的处理单元 | 计算任务由谁执行 |
它们通常同时出现。以 GPU 为例:
CPU 在共享命令队列中准备任务 → CPU 通过 MMIO/ioctl 写入 doorbell → DMA 搬运代码和数据,或 GPU 直接访问共享缓冲区 → GPU 执行 kernel → GPU 写回结果 → 中断、完成队列或 fence 通知 CPU共享内存只表示双方可以访问同一数据区域,并不自动保证缓存一致性和操作顺序。驱动仍需使用所有权协议、内存屏障、缓存维护或硬件一致性协议,确保一方看到另一方已经完成的写入。集成 GPU 常与 CPU 共享主存;独立 GPU 通常拥有显存,并通过 PCIe DMA 与主存交换数据。
总线与设备互连体系
总线的注册与转发功能
总线为大量异构设备提供统一的连接、寻址和转发机制。设备在总线上注册地址范围;总线或桥控制器收到地址和数据后,将请求转发到目标设备。
严格地说,总线是互连基础设施,而不是像磁盘、键盘那样的功能端点。但从 CPU 和操作系统的视角看,总线控制器本身也具有配置寄存器、状态和管理逻辑,需要被发现、配置和驱动;因此讲义将总线称为“特殊的 I/O 设备”。它管理的不是某一种物理功能,而是其他设备的枚举、路由、仲裁、中断和数据传输。
PCIe 与 USB 的体系差异
PCIe 和 USB 都是设备互连标准,但面向的目标不同:
| 特性 | PCIe | USB |
|---|---|---|
| 主要场景 | 主机内部的高性能设备 | 外部通用外设 |
| 典型设备 | GPU、网卡、NVMe、FPGA | 键盘、鼠标、U 盘、打印机、摄像头 |
| 通信结构 | 高速点对点链路,经交换结构连接 | 主机控制器统一调度,经 Hub 扩展 |
| 数据传输 | 设备可进行总线主控和 DMA | 业务传输通常由 USB 主机控制器发起和调度 |
| 中断 | 常用 MSI/MSI-X | USB 主机控制器完成事务后中断 CPU |
| 设备管理 | 配置空间、BAR、总线枚举 | 热插拔枚举、描述符和设备类协议 |
USB 设备通常不是直接连接 CPU。USB 主机控制器本身往往是一个 PCIe 设备:
USB 外设 → USB Hub/链路 → xHCI 主机控制器 → PCIe → CPU/内存因此,USB 强调通用性、热插拔和统一设备类,PCIe 强调低延迟、高带宽以及设备对内存的高效访问。
CXL 的缓存一致互连
CXL(Compute Express Link)在 PCIe 物理连接基础上增加了面向内存和缓存一致性的协议,主要连接 CPU、内存扩展设备和异构加速器:
CXL.io:设备发现、配置和普通 I/O;CXL.cache:设备以缓存一致方式访问主机内存;CXL.mem:主机访问设备侧内存。
普通 PCIe 数据共享常表现为“准备缓冲区—DMA 搬运—完成通知”;CXL 进一步允许 CPU 和设备围绕同一内存区域建立硬件支持的一致性语义,从而减少重复的数据复制和软件缓存维护。不过,缓存一致性只保证各缓存对同一内存值达成一致,并不替代锁、原子操作和内存顺序等并发同步机制。
操作系统的设备抽象
设备访问的隔离与复用
应用程序不应直接访问设备寄存器和总线,主要原因包括:
- 多个程序可能并发访问同一设备,需要仲裁与同步;
- 设备寄存器通常具有特权和破坏性,错误写入可能造成系统故障;
- 设备命令、DMA 地址和中断管理依赖具体硬件;
- 操作系统需要实施权限检查、资源隔离、缓冲、调度和错误恢复。
因此,内核通过驱动程序拥有设备,再把设备虚拟化成应用程序可访问的操作系统对象。
文件描述符与文件操作接口
UNIX 的“Everything is a File”并不是说所有对象都存储在磁盘上,而是说许多内核对象都实现了一组类似文件的操作接口。文件描述符可以理解为指向内核对象的受保护句柄。
struct file_operations { loff_t (*llseek)(...); ssize_t (*read)(...); ssize_t (*write)(...); long (*unlocked_ioctl)(...); int (*mmap)(...); ...};应用程序调用 read、write、mmap 或 ioctl 后,内核根据文件描述符找到对应对象,再调用该驱动实现的操作函数:
应用程序 API → 系统调用 → 文件描述符与 file_operations → 设备驱动程序 → Port I/O、MMIO、DMA 和设备协议read/write 主要表达数据面的传输,mmap 可以让用户态和驱动或设备共享缓冲区,减少大块数据在用户态与内核态之间的重复复制。
ioctl 的设备控制接口
ioctl(I/O control)用于表达普通字节流读写无法自然描述的设备控制操作:
int ioctl(int fd, unsigned long request, ...);其中 fd 指定设备对象,request 指定设备相关命令,第三个参数通常是整数或指向参数结构体的指针。调用链为:
用户程序 → ioctl(fd, request, arg) → 内核检查文件描述符、权限和参数 → 对应驱动解释 request → 修改驱动状态或设备寄存器典型用途包括设置串口波特率、查询磁盘健康状态、配置网卡、申请摄像头缓冲区以及提交 GPU 命令。/dev/kvm 也通过一组 ioctl 创建虚拟机和虚拟 CPU,并控制其运行直到发生 VM Exit。
数据命令与控制命令的分离
设备协议可以规定特殊字符或特殊字节序列。例如终端转义序列、调制解调器 AT 命令、PCL 和 PostScript 都可以通过 write 发送。这种方式适用于“控制命令本身就是设备数据协议的一部分”的场景。
但它不能取代通用的 ioctl:
- 任意二进制数据都可能包含所谓的特殊字符,需要额外转义和分帧;
- 复杂参数需要自行编码、解析和版本管理;
- 改变驱动内部状态或硬件寄存器并不等于向设备数据通道发送字符;
- 内核难以对字符串协议实施结构化参数检查、权限检查和原子状态变更;
- 查询操作还需要设计独立的返回值和错误报告协议。
因此可以把两类接口理解为:
read/write:数据平面,传输字节流ioctl:控制平面,配置和查询设备ioctl 的代价是各驱动的命令编号、参数结构和语义并不统一,形成了大量设备相关的“隐藏规范”。这正反映了设备功能本身无法被一个极小接口完全消除的复杂性。
输入输出操作的完整路径
一个现代 I/O 操作通常经历以下过程:
应用程序调用 read/write/ioctl/mmap → 系统调用进入内核 → 内核根据 fd 找到设备对象和驱动 → 驱动准备命令、缓冲区和 DMA 描述符 → 通过 MMIO/Port I/O 写命令或 doorbell → 总线把请求路由到设备 → 设备执行操作并与内存交换数据 → 设备或主机控制器发送中断 → 驱动确认完成、更新内核对象并唤醒进程 → 系统调用返回应用程序这条路径体现了操作系统设备抽象的核心:硬件侧仍然是寄存器、总线、DMA 和中断;应用侧看到的则是可读、可写、可映射、可控制的文件描述符对象。
持久化语义的边界
I/O 只说明数据已经沿设备路径发生传输,不自动等价于数据已经可靠持久化。对于存储设备,write 返回时数据可能仍停留在用户缓冲区、内核页缓存或设备缓存中;是否已经到达非易失介质,还取决于文件系统、缓存刷新、写入顺序和设备保证。
因此可以区分:
I/O 完成:一次设备操作已经完成到接口承诺的位置持久化完成:数据已经到达断电后仍能保留的介质,并满足所需一致性语义后续存储设备、文件系统和数据库部分将继续回答:数据如何组织、何时真正落盘,以及系统在写入过程中崩溃后如何恢复。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






