Lecture 18: I/O - General I/O, Disk, and SSD
导读
- 本讲从操作系统为什么需要 I/O 子系统开始,解释 CPU、bus、controller、driver、DMA、interrupt/polling 如何协作。
- 应用只想调用
read()/write(),但真实设备在速度、粒度、访问模式和失败方式上差异巨大。 - HDD 的随机访问慢在机械定位,SSD 的复杂性则来自 erase-before-write、FTL、GC 和 wear leveling。
- 读这讲要抓住一条链路:用户请求如何穿过 kernel,到设备,再回到用户线程。
本讲地图
| 层次 | 核心问题 | 关键词 |
|---|---|---|
| I/O abstraction | 如何统一千差万别的设备 | block, character, network device |
| Hardware connection | CPU 如何和设备通信 | bus, controller, register, MMIO |
| Data transfer | 数据如何进出内存 | programmed I/O, DMA |
| Notification | OS 如何知道设备完成 | interrupt, polling |
| Driver path | 驱动如何分工 | top half, bottom half, ioctl() |
| Storage media | HDD/SSD 为什么性能差异大 | seek, rotation, FTL, GC |
正文
I/O 路径可以从设备一路看回 OS:bus 负责连接,controller 暴露寄存器,DMA 搬数据,interrupt 或 polling 把完成事件送回来。
I/O Subsystem
没有 I/O,计算机不能和外界、存储、网络交互。OS 面对的难点不是“有没有设备”,而是设备差异太大。同样叫 I/O,速度可以从极慢输入设备跨到高速网络/存储;数据粒度可能是 byte、block,也可能是 packet;访问模式可能是 sequential、random,也可能只支持特定顺序。再加上设备会失败,完成时间也未必可预测,内核就必须在统一接口和设备特性之间做一层翻译。
OS 的目标是给用户提供稳定接口,同时在快设备上不产生过高 per-byte overhead,在慢设备上不让 CPU 白等。
Bus / PCIe
bus 是硬件设备之间通信的一组 wires 加 protocol。它包含 control lines、address lines、data lines,还需要仲裁、寻址和握手协议。好处是多个设备可以共享一套连接;代价是同一时刻通常只有一个 transaction,其他设备必须等待。
传统 PCI 是 parallel bus,多设备共享地址/数据线。PCI Express 名字仍叫 bus,但更像一组 fast serial lanes:设备可按需要使用多个 lane,慢设备不必和快设备强行共享同一条并行总线。这也是 OS 抽象的力量:底层 interconnect 从 PCI 变为 PCIe,上层设备 API 仍能保持稳定。
CPU 通常不直接理解设备细节,而是和 device controller 交互。controller 提供 control/status/data registers 或 request queues;CPU 访问这些寄存器时,可以走 Port-mapped I/O,也就是使用专门的 in/out 指令,例如 x86 的 out 0x21, AL;也可以走 Memory-mapped I/O (MMIO),把设备寄存器映射到物理地址空间,再用普通 load/store 访问。
MMIO 不是“真的内存”。写这些地址会触发设备行为,所以必须由内核控制映射和权限。
PIO 与 DMA

这页展示了 CPU 通过 controller registers 与各种 I/O devices 交互的整体结构。
Programmed I/O (PIO) 中,每个 byte/word 都经由 CPU 的 in/out 或 load/store 搬运。它硬件简单、编程直接,但 CPU cycles 消耗和数据量成正比,大块 I/O 很亏。
Direct Memory Access (DMA) 则让 controller 直接在设备和 main memory 之间搬数据。OS/driver 负责告诉 controller 内存地址、长度、方向和命令;数据搬运完成后,controller 设置状态并通知 OS。
DMA 的标准流程可以写成:
| |
DMA 不是不需要 CPU,而是让 CPU 不再逐字节搬运;CPU 仍要配置 controller、处理完成事件,并保证内存一致性和权限安全。
Interrupt / Polling
OS 需要知道 I/O operation 何时完成、是否出错。Interrupt 让设备主动打断 CPU,适合不可预测、低频事件;缺点是 interrupt overhead 高。Polling 让 OS 定期读取 status register,单次检查开销低,适合高频或短时间内连续事件;但事件稀疏时会浪费 CPU cycles。
实际系统常混用。高速网卡可能对第一个 packet 发 interrupt 唤醒内核,随后内核 poll hardware queue,直到 queue 清空。这样既避免长期空转,又能在 burst 中减少 interrupt 次数。
| 场景 | 更合适的方式 | 原因 |
|---|---|---|
| 键盘输入、偶发 I/O 完成 | interrupt | 事件稀疏,polling 浪费 |
| 高速网卡连续收包 | interrupt + polling | 第一个事件唤醒,后续批量处理 |
| 设备很快且马上完成 | polling | 等 interrupt 可能更贵 |
| 高频完成队列 | polling 或混合 | 降低 interrupt storm |
Device Driver

一次 I/O request 从用户态到 driver top half、设备、bottom half,再回到用户线程。
device driver 是内核中直接和设备硬件交互的 device-specific code。它向 kernel I/O subsystem 提供标准内部接口,所以同一个 OS 可以面对不同硬件。
一次请求的生命周期通常是:
| |
Top half 在系统调用路径中运行,处理 open()、close()、read()、write()、ioctl() 等,发起 I/O,并在需要时让线程 sleep。Bottom half 在 interrupt routine 或 deferred handler 中运行,处理完成事件、继续输出下一块、唤醒等待 I/O 的线程。
ioctl() 用于设备特有配置,但不能替代通用 read/write 接口。OS 仍需要 block device、character device、network device 等标准接口来隐藏设备差异。
Timing 接口
I/O timing 对用户程序有三种常见表现。Blocking 接口让调用者等待,直到数据 ready 或设备 ready;Non-blocking 接口会立即返回,告诉用户完成了多少,也可能只是说明暂时没有数据;Asynchronous 接口同样立即返回,但之后由 kernel 完成传输并通知用户。
这三类接口的本质差异在于等待发生在哪里:用户线程里、用户事件循环里,还是 kernel 后台路径里。理解这一点后,read() 看起来就不只是“读数据”,而是一次关于等待位置的 API 选择。
HDD

HDD 的 sector、track、cylinder、head/arm 结构解释了为什么随机访问要付 seek 和 rotation。

这页把 7200 RPM、seek time、transfer rate 放进同一个随机读延迟计算中。
HDD 的基本结构包括 sector、track、cylinder、head/arm。一次读写通常先付 seek time,让磁头移动到正确 track/cylinder;接着付 rotational latency,等待目标 sector 转到磁头下;最后才是 transfer time,也就是 sector 经过磁头并被传输的时间。
总延迟常写成:
| |
如果模型忽略 queueing/controller,就只算后三项。RPM 转换为旋转时间的公式是:
| |
例如 7200 RPM 的一圈约 60000 / 7200 = 8.33 ms,平均 rotational latency 约 4.17 ms。如果 seek time 是 5 ms,4KB block 在 50MB/s 下 transfer 只约 0.08 ms。随机读慢的主因不是 transfer,而是 seek + rotation。
HDD controller 还会隐藏很多复杂性:ECC 修复小错误,sector sparing 把坏扇区透明映射到备用扇区,slip sparing 尽量保留顺序性,track skewing 让换 track 后不必等一整圈。
SSD

这页图把 SSD 的 FTL 映射和 copy-on-write 更新路径放在一起。
SSD 没有 seek 和 rotational delay,随机读可以很快。问题在写入:NAND flash 通常只能写空 page,erase 的单位是更大的 block,而且 erase 很慢、次数有限。
OS 看到的仍常是类似 HDD 的 4KB block interface,但 SSD 内部不能对小 page 原地覆盖。它的解决办法可以概括成两条系统原则:Layer of Indirection 让 Flash Translation Layer (FTL) 把 OS 的 logical/virtual block number 映射到 flash physical page;Copy on Write 则让更新写到新的 free physical page,再更新 mapping,并把旧 page 标为 invalid。
后台 garbage collection (GC) 会回收 invalid pages 所在的 erase block;wear leveling 把写入分散到不同 blocks,避免热点块过早磨损。
因此 SSD 的一句话不是“随机访问都快”,而是:读快在无机械延迟,写复杂在 erase-before-write 和有限擦写寿命。