第 9 章 · 设备管理

Linux 有句老话:一切皆文件。看看 /dev 目录就明白这不是鸡汤:

$ ls -l /dev | head -5
brw-rw----  1 root disk      8,  0  nvme0n1      ← 你的整块硬盘
crw-rw-rw-  1 root tty       4, 64  ttyS0        ← 串口终端
crw-rw----  1 root kvm      10, 232 kvm          ← 虚拟化接口

b 开头是块设备,c 开头是字符设备,8 和 0 是主次设备号。你在磁盘上 dd、对串口 cat、往声卡里写数据,用的全是同一套 open/read/write 系统调用。把千奇百怪的硬件统一成「文件」,是设备管理的核心思想;而统一背后,是本章要展开的层次:中断和 DMA(计组第 9 章讲过硬件侧,这里看内核怎么用它们)、设备驱动、磁盘调度算法、以及 SSD 时代的新格局。

9.1 内核眼中的设备:三层皮

从上到下,内核把设备分三层管理:

  • 统一接口层:VFS 给所有设备 open/read/write/ioctl 的文件接口,应用只见文件不见硬件。
  • 驱动层:每个设备一份驱动,把「读这个文件」翻译成「往那些寄存器写这些命令、等这个中断」。驱动是内核里 bug 最密集的地方(Linux 内核代码九成以上是驱动),因为硬件手册良莠不齐,而驱动一旦越界就是内核崩溃。
  • 中断与 DMA 层:计组第 9 章的机制在这层服役。内核把中断处理拆成两半:上半部(top half)关中断、抢时间、把数据从硬件薅走,几微秒;下半部(bottom half,softirq/tasklet/workqueue)开中断慢慢处理耗时活。这样高频中断(万兆网卡)不至于把响应延迟吃光,这层设计思想只有一句:在中断里待的时间越短越好。

一个实用知识点:块设备和字符设备的差别。块设备(磁盘)可以随机寻址、以块为单位读写、可挂文件系统;字符设备(串口、键盘)是字节流,来了就得收。鼠标是字符设备,SSD 是块设备,别看 SSD 没有磁头,抽象不变。

9.2 磁盘:寻道是原罪

机械硬盘的物理结构:盘片、磁道、扇区(512B 或 4KB,最小读写单位),磁头移动靠机械臂。一次磁盘访问的耗时拆成三段:

T=T‘寻道‘+T‘旋转‘+T‘传输‘T = T_`{\text{寻道}}` + T_`{\text{旋转}}` + T_`{\text{传输}}`

典型值:寻道几毫秒(磁头臂要移动),旋转延迟平均半圈(7200rpm 的盘约 4ms),传输微秒级。寻道是大头,所以磁盘调度的全部目标就一句话:让磁头少跑路。

设磁头在 50 号磁道,请求队列 98, 183, 37, 122, 14, 124, 65, 67,把四个算法各算一遍(这是本章的必考手算题):

FCFS:按请求原序跑,总移动 = |98-50|+|183-98|+... = 48+85+146+85+108+110+59+2 = 640。磁头在盘面上反复横跳,公平但荒唐。

SSTF(最短寻道优先):每步挑最近的。50→65→67→37→14→98→122→124→183,总计 236。好很多,但有饥饿病:远端请求永远排不进「最近」,两头的老请求可能被无限晾着。

SCAN(电梯):磁头只朝一个方向扫,扫到头再折返,像电梯。50→37→14→0(到底)→65→67→98→122→124→183,总计 208。无饿死,但中间磁道待遇最好,刚被扫过的边缘要等整整一个来回。

C-SCAN(循环扫描):单向服务,扫到头直接空驶回起点再扫,把「服务」限定在单方向,等待时间分布最均匀。总移动约 382(多付了空驶),换来公平。

教材到这就算完了,但真实世界有两层后续值得知道。其一,现代磁盘自己有缓存和队列(NCQ),会偷偷重排 OS 下发的请求,OS 级调度的意义被稀释;其二,SSD 没有磁头,「寻道」不存在,这套算法在 SSD 上的意义归零——但它们的思想搬进了 SSD 内部的固件(FTL 层)和调度器(如 mq-deadline 保证读请求不被写请求憋死),电梯还是那部电梯,只是换了司机。

9.3 缓冲与 SPOOLing:和速度差做斗争

CPU 快、设备慢、各设备之间也互不匹配,缓冲(buffering)是标准药方:在内存里放缓冲区,快的一方写完就走,慢的一方慢慢消化。你写的每一行 printf 其实都进了 libc 的缓冲区,攒够了才 syscall;数据库的 WAL、日志系统的批量落盘,同源同理。缓冲还顺带解决「生产者和消费者速率抖动」问题,吸收瞬时洪峰。

SPOOLing(假脱机)是另一味药,专治「独占设备虚拟成共享」:打印机一次只能打一份,多个进程同时打印就会穿插出废纸。SPOOLing 把打印请求排队写进磁盘队列,打印机守护进程按序取活。打印机变成了「共享的」,每个进程感觉自己独占着一台。这招的思想(排队 + 守护进程 + 磁盘做中转)在打印、队列系统、消息中间件里到处都是。

9.4 零拷贝:设备管理的现代答卷

留一个现代话题收尾。传统的「读文件发网络」要四次拷贝:磁盘→页缓存→用户缓冲区→socket 缓冲区→网卡。数据在内核和用户态之间反复横跳,CPU 当搬运工。Linux 的 sendfile(或 mmap)把中间两跳省掉,数据从页缓存直送网卡(DMA 配合),拷贝从四次降到两次、CPU 从全程参与变成旁观。Nginx 静态文件快、Kafka 吞吐高,这个系统调用功不可没。它提醒我们设备管理的终极目标和整门课一致:让 CPU 从搬运中解放,只做决策。

常见错误

  • 在 SSD 上还纠结磁道调度。物理模型都不存在了,调优方向换成对齐(4K)、TRIM、队列深度;老教材的 SCAN 计算题当历史课听。
  • 把 iostat 的 %util 当「磁盘满负荷」。SSD 并行度高,%util 100% 可能远没到瓶颈;看 IOPS、延迟、队列深度才是正路。
  • 忽略写入放大。SSD 覆盖写要先擦除整块(块远大于页),小随机写性能极差且伤寿命,所以数据库都爱顺序写日志(WAL)而怕随机写。

练习

  1. 手算:磁头 53,请求 98,183,37,122,14,124,65,67(教材经典原题),分别算 FCFS、SSTF、SCAN(向大号方向)、C-SCAN 的总移动距离。
  2. 手算:磁盘转速 15000rpm,平均寻道 4ms,每磁道 200 扇区。读一个扇区的平均时间多少?读同一磁道连续 100 个扇区呢?算完你就懂为什么日志顺序写是刚需。
  3. 动手:iostat -x 1 观察 NVMe 盘的 r/s、await、%util;跑 fio 或简单 dd 分别做顺序写和 4K 随机写,亲测性能差多少倍。
  4. 思考:「一切皆文件」在 socket 上也成立(Linux 可以对 fd read/write),那为什么网络编程没人用 read/write,都用 send/recv?提示:TCP 有带外数据、标志位,文件接口的表达力够不够?