第 22 讲:I/O 请求、驱动、DMA 与缓冲

I/O 难在两点:设备种类、寄存器和时序各不相同;设备速度又常比 CPU 慢许多。OS 要向上提供尽量统一的接口,向下则必须精确操作控制器,并让 CPU 在等设备时去做别的事。

设备怎么分类

按数据组织

  • 块设备:按固定大小块传输,通常可随机寻址,如磁盘;
  • 字符设备:按字节流顺序收发,通常不能任意跳到某“块”,如串口和终端;
  • 网络设备:以帧/数据报为主,另有协议栈和异步到达语义。

按分配方式

  • 独占设备:一段时间只分配给一个进程;
  • 共享设备:多进程的请求可被设备/驱动排队交错服务;
  • 虚拟设备:用 SPOOLing 等方法把物理独占设备包装成多个逻辑设备。

硬件视角:总线、控制器和寄存器

设备通常不直接让 CPU 理解它的电气细节,而由控制器/适配器暴露:

  • 命令/控制寄存器;
  • 状态寄存器;
  • 数据寄存器或 FIFO;
  • 中断线、DMA 通道等。

这些寄存器有两种常见寻址:

  • 独立 I/O 编址:有专用 I/O 指令和端口地址空间,不占内存地址,但指令/编程模型不统一;
  • 内存映射 I/O:控制器寄存器占物理地址区间,用普通 load/store 访问;页表权限可阻止用户进程乱写,但要正确处理缓存、顺序和副作用属性。

总线理论带宽可粗略看作频率乘每次传输字节宽度,但真实有协议开销、仲裁和等待周期,实际吞吐低于理论值。

软件视角:一次 I/O 经过哪些层

用户程序:read(fd, buf, n)
       ↓ 系统调用
设备无关 I/O / 文件系统:权限、偏移、缓存、设备选择
       ↓
设备驱动:把通用请求转成该控制器命令
       ↓
中断处理/DMA 完成处理:确认状态,完成请求,唤醒等待者
       ↓
控制器 ↔ 物理设备

分层的目的是将“打开、读、写”这些稳定语义与“向某款控制器哪个寄存器写哪一位”隔离。设备无关层还实现命名、保护、缓冲、错误报告和逻辑设备到物理设备的映射。

四种 I/O 控制方式

1. 程序查询/PIO

CPU 发命令后循环读状态寄存器,设备就绪时再传数据。

  • 优点:路径简单,不需要中断配置;
  • 缺点:CPU 在设备慢时持续忙等,并且常需亲自搬每个字。

适合极短、可预期很快就绪或早期启动尚无中断环境的操作。

2. 中断驱动

CPU 发起命令后去运行别的工作,设备在就绪/完成时中断:

  1. CPU 保存必要现场,进中断处理;
  2. 驱动读状态,确认中断源和错误;
  3. 传送本次数据或继续下一步;
  4. 请求完成则唤醒等待进程,返回被中断执行流或重新调度。

比轮询节省 CPU 等待,但如果每个字节/小数据都中断一次,中断开销会过高。

3. DMA

CPU 先设置 DMA 的内存起址、方向、字节数与设备,DMA 控制器随后在设备与内存之间成批搬运,整块完成再中断 CPU。

中断驱动 PIODMA
CPU 常要处理每个数据单元DMA 成批搬运
每小单元可中断常整块完成中断
CPU 传数据开销高CPU 主要负责启动与收尾

DMA 与 CPU 共享内存总线,可产生周期偷窃/带宽竞争。如果 CPU Cache 不与 DMA 自动一致,驱动还必须正确做 Cache 同步和内存屏障。

4. I/O 通道

通道是更独立的 I/O 处理器,有自己的通道指令/程序,能执行一串设备操作。DMA 的起址、方向、长度主要由 CPU 逐次设置;通道可解释通道程序,进一步减少 CPU 干预。

课件介绍:

  • 字节多路通道:按字节交叉服务多台低速设备;
  • 数组选择通道:一次专为一台高速设备传整块;
  • 数组多路通道:按数据块交叉服务多台高速设备。

驱动程序在做什么

驱动是内核中与具体设备密切相关的代码,通常包含:

  • 探测/初始化和卸载;
  • open/close/read/write/ioctl 等标准内核接口的设备实现;
  • 请求队列、启动 I/O 和超时/重试;
  • 中断处理与 DMA 完成回收;
  • 电源、错误和并发访问管理。

驱动没有普通应用那样的 main,而是在初始化时向内核注册回调入口,之后由系统调用路径、中断或热插拔事件调用。它运行在高权限内核环境,越界错误可影响整个系统。

设备独立性与 LUT

应用使用逻辑名/文件描述符,不应把程序写死为某块控制器的物理地址。课件中的 LUT(Logical Unit Table)将逻辑设备映射到当前分配的物理设备:

  • 更换设备时应用不用改;
  • 可从同类多台设备中灵活分配;
  • 设备驱动细节被限定在底层。

映射可是全系统一张表,也可每进程/会话各有逻辑名空间,后者更容易让多个用户各自使用同名逻辑设备。

缓冲为什么能提高并行性

设:

  • TT:设备把一块输入缓冲区的时间;
  • MM:内核缓冲区与用户区之间搬运时间;
  • CC:进程计算一块的时间。

单缓冲

设备填充下一块可与 CPU 处理上一块的 CC 并行,但 MM 占用缓冲的搬运阶段需串行。稳态每块时间:

max⁡(C,T)+M.\max(C,T)+M.

双缓冲

设备填 A 时 CPU 可处理 B,然后交换角色,稳态每块可近似:

max⁡(C+M,T).\max(C+M,T).

若设备和 CPU 速度差距不大,两个缓冲可让两边连续工作;若一边长期快得多,有限缓冲最终仍会空或满,只能吸收短期抖动,不能改变长期吞吐瓶颈。

环形缓冲和缓冲池

多个等大缓冲构成环,至少要记录空缓冲、已满缓冲和正在处理的位置。本质上是生产者—消费者问题,需要空/满计数与队列互斥。

为每对 I/O/计算进程预留专用环会浪费内存,所以全系统可用共享缓冲池,维护:

  • emq:空缓冲队列;
  • inq:已填入输入数据的队列;
  • outq:已填入待输出数据的队列。

“收容输入”是从 emq 取空块、填满放 inq;“提取输入”是从 inq 取数据、用完还 emq。输出在 emq 和 outq 之间对称流动。

设备分配的数据结构

课件从设备到通道列出四类表:

  • DCT:每设备的类型、忙闲、等待 PCB 队列、重试次数与所连控制器;
  • COCT:每控制器的状态、中断号、DMA 等配置;
  • CHCT:每 I/O 通道的状态与连接;
  • SDT:全系统设备总表,将设备标识映射到 DCT 并记录占用者。

在单通路系统里,分配一台设备还可要逐级获得设备→控制器→通道;任一层忙都要进对应等待队列。多通路可从多个控制器/通道中选空闲路径,可用性更高,数据结构也更复杂。

SPOOLing:把独占设备改造成队列服务

SPOOLing 在磁盘上建输入井/输出井,内存中建缓冲和请求表,由专门输入/输出进程真正操作设备。

以打印为例:应用把输出写入自己的 spool 文件后就可继续;后台管理器按队列将作业交给物理打印机。对应用看,自己似乎拥有一台可快速写入的虚拟打印机;物理打印仍被后台进程串行。

五种 I/O 等待模型

将读 I/O 分成两阶段:

  1. 等数据从设备到内核缓冲;
  2. 把数据交付/复制到用户缓冲。
模型等设备阶段交付用户缓冲阶段
阻塞 I/O调用线程睡眠调用中完成
非阻塞 I/O立即返回未就绪,应用重试就绪后调用复制
I/O 多路复用select/poll 等一次等多个 fd就绪后再 read
信号/事件驱动就绪时内核通知通知后再读
异步 I/O提交后继续数据已交付用户缓冲才通知完成

多路复用并不是让一个 I/O 更快,而是让一个执行流不要在某个未就绪 fd 上卡死,可及时处理任意一个已就绪 fd。非阻塞轮询若没有就绪通知或退避,可把睡眠浪费变成 CPU 忙等浪费。

UNIX 式设备管理实例

UNIX 将设备通过特殊文件纳入文件接口,块设备和字符设备分别通过驱动开关表暴露标准操作。

块设备缓存通常每项有 buffer header,记逻辑设备号、物理块号、锁定/延迟写等状态与多组链指针。同一缓存可同时在:

  • 按 (device, block) 快速查找的哈希队列;
  • 可回收缓存的空闲队列。

“在空闲队列”不一定表示内容无效,它仍可作为某设备块的 cache,直到被重分配。延迟写块在重分配前必须先写回。

字符设备可将小块 cblock 串成字符队列,每个字符从队首读走后就消费,与可重复命中的块 cache 语义不同。课件还介绍了 STREAMS 用消息模块和多路复用器减少字符/网络驱动的重复功能,以及 Windows NT 用 I/O 请求包 IRP 在分层驱动中向下传递。

一次 I/O 问题的诊断顺序

  1. 请求是块、字符还是网络 I/O?
  2. 用户线程是阻塞、轮询、等就绪事件还是等完整异步完成?
  3. 数据由 CPU 搬还是 DMA 搬,中断频率是每字/每块还是批量?
  4. 数据经过了几次缓冲和拷贝,哪一层拥有它的生命期?
  5. 瓶颈是设备服务、排队、总线、拷贝还是过多中断?

先走清请求链,才能判断加缓冲、改 DMA、改异步接口或换调度算法中哪一件真的会帮忙。

评论