打开你 Linux 服务器的终端,执行:
cat /sys/block/nvme0n1/queue/scheduler你会看到类似 [none] mq-deadline kyber bfq
的输出。方括号里的就是当前激活的 I/O 调度器。如果是 NVMe SSD
并且内核版本够新,大概率是
none——意思是不使用任何调度器。
这很反直觉。操作系统教科书花了整整一章讲 I/O 调度的电梯算法、SCAN、C-SCAN、LOOK。然后 NVMe 出来了,说:不需要了。
但事情没这么简单。none 适用于
NVMe,却不适用于 HDD、SATA
SSD、eMMC、UFS。理解”什么时候需要调度,什么时候不需要”,需要先理解
I/O
调度器存在的根本原因——以及这个原因如何随硬件演进而消失。
一、为什么需要 I/O 调度
机械硬盘的物理限制
HDD 的读写需要磁头移动到目标磁道(寻道,seek)并等待盘片旋转到目标扇区(旋转延迟,rotational latency)。寻道时间通常 3-10ms,旋转延迟(7200 RPM 盘片)平均 4.2ms。
这意味着一次随机 I/O 需要 7-15ms。如果应用发出 1000 个随机读请求,按 FIFO 顺序执行需要 7-15 秒。但如果把这些请求按磁道号排序(类似电梯),磁头只需要从外到内扫描一次,寻道时间可以从 7ms/次降到 0.5ms/次,总时间降到 ~0.5 秒。
I/O 调度器存在的根本原因是弥补机械硬盘”随机访问昂贵、顺序访问廉价”的物理特性。
SSD 改变了什么
SSD 没有机械部件,随机读延迟约 20-100μs(比 HDD 快 100-500 倍)。但 SSD 仍然有两个特性使得调度有意义:
- 写放大:SSD 的写操作涉及擦除和重写整个块(通常 128KB-4MB),内部的 FTL(Flash Translation Layer)需要做垃圾回收。I/O 调度可以合并写请求,减少写放大。
- 队列深度:SATA SSD 的队列深度最多 32(NCQ),合理的请求排序可以提高设备利用率。
NVMe 又改变了什么
NVMe SSD 通过 PCIe 直连 CPU,队列深度可达 64K,支持多个独立的硬件提交队列(通常每个 CPU 核心一个)。NVMe 设备内部有自己的调度逻辑,软件层面的调度反而增加延迟。
这就是为什么 NVMe 设备的默认调度器是
none。
二、单队列时代的三大调度器
Linux < 3.13 的 I/O 栈
在 blk-mq(多队列块层)出现之前,Linux 的 I/O 栈是单队列的:所有 CPU 核心的 I/O 请求都进入同一个请求队列,受一把自旋锁保护。
应用层 → 文件系统 → 页缓存 → 单请求队列 → I/O 调度器 → 块设备驱动 → 硬件
↑
一把全局锁
三个经典调度器都工作在这个单队列上。
2.1 Noop
最简单的调度器——FIFO + 请求合并。如果两个相邻的请求访问连续的扇区,合并成一个大请求。除此之外不做任何排序。
适用场景:SSD(不需要寻道优化)和虚拟机(底层存储已经有自己的调度器)。
2.2 Deadline
Deadline 调度器(Jens Axboe, 2002)为每个请求设置一个截止时间:读请求默认 500ms,写请求默认 5s。
两个关键设计:
- 读写分离:维护两个排序队列(按扇区号排序)和两个 FIFO 队列(按到达时间排序)。读和写分开调度。
- 截止时间保证:正常情况下从排序队列取请求(最大化顺序性),但如果 FIFO 队列头部的请求已经超过截止时间,优先处理它(防止饿死)。
// Deadline 调度逻辑(简化)
struct request *deadline_dispatch(struct deadline_data *dd) {
// 1. 检查读 FIFO 是否有超时请求
if (deadline_check_fifo(&dd->fifo_list[READ], dd->fifo_expire[READ]))
return fifo_dequeue(&dd->fifo_list[READ]);
// 2. 检查写 FIFO(写的截止时间更宽松)
if (deadline_check_fifo(&dd->fifo_list[WRITE], dd->fifo_expire[WRITE]))
return fifo_dequeue(&dd->fifo_list[WRITE]);
// 3. 正常情况:从排序队列取(最大化顺序性)
if (!list_empty(&dd->sort_list[READ]))
return sort_dequeue(&dd->sort_list[READ]);
return sort_dequeue(&dd->sort_list[WRITE]);
}为什么读的截止时间(500ms)比写(5s)短 10 倍?因为读通常是同步的(进程在等待),写通常是异步的(写入页缓存后进程就继续了)。读延迟直接影响用户体验,写延迟用户通常感知不到。
2.3 CFQ(Complete Fairness Queueing)
CFQ(Jens Axboe, 2004-2006)是 HDD 时代最复杂的调度器。
核心设计:
- 每进程一个队列:每个进程(实际上是每个 cgroup + ionice 组合)有自己的 I/O 队列。
- 时间片轮转:调度器轮流给每个队列一个时间片(默认 4ms),在时间片内只处理该队列的请求。
- 预期调度(Anticipatory Scheduling):当一个进程的队列空了,不立即切换到下一个进程——而是等待几毫秒,“预期”该进程马上会发出下一个请求。如果猜对了,避免了一次不必要的寻道。
进程 A 的队列: [req1(sector 100), req2(sector 105), req3(sector 110)]
进程 B 的队列: [req4(sector 50000), req5(sector 50010)]
进程 C 的队列: [req6(sector 200), req7(sector 210)]
CFQ 时间片轮转:
→ A 的时间片: 处理 req1, req2, req3 (连续扇区,很快)
→ A 的队列空了,等 3ms ("预期" A 会发新请求)
→ 超时,切换到 B
→ B 的时间片: 处理 req4, req5
→ 切换到 C
→ ...
CFQ 的预期调度在 HDD 上效果很好——进程通常会连续访问相邻扇区(顺序读/写),预期等待避免了频繁的寻道。但在 SSD 上这是纯粹的浪费——SSD 没有寻道开销,等待 3ms 毫无意义。
个人思考
CFQ 是 HDD 时代的巅峰之作,但它的设计哲学从一开始就绑定了”寻道昂贵”这个假设。当硬件演进使这个假设失效时,整个调度器的价值基础都被动摇了。这给了我们一个教训:不要过度优化当前硬件的特性——因为下一代硬件可能完全不同。
三、多队列时代(blk-mq)
为什么需要多队列
2013 年,Linux 3.13 引入了 blk-mq(multi-queue block layer)。原因很简单:
单队列 + 全局锁在多核 CPU 上是瓶颈。一个 32 核服务器,所有核心的 I/O 都要抢同一把锁。NVMe SSD 可以做到 100 万 IOPS,但单队列的锁争用使得软件层只能跑出 20 万。
blk-mq 的设计:
CPU 0 → 软件队列 0 ──┐
CPU 1 → 软件队列 1 ──┤──→ 硬件提交队列 0 → NVMe
CPU 2 → 软件队列 2 ──┤──→ 硬件提交队列 1 → NVMe
CPU 3 → 软件队列 3 ──┘──→ 硬件提交队列 2 → NVMe
...
每个 CPU 有自己的软件提交队列(无锁),映射到设备的硬件队列。NVMe 设备通常暴露与 CPU 核心数相同的硬件队列。
blk-mq 上的调度器
CFQ 无法适配 blk-mq(它的设计强依赖单队列),被移除了。blk-mq 上有三个调度器:
四、mq-deadline
mq-deadline 是 deadline 调度器的多队列适配版本。核心逻辑不变——读写分离、截止时间保证、扇区排序。
改变的是实现方式:不再有全局请求队列,调度器从各个 CPU 的软件队列中拉取请求,合并到自己的排序/FIFO 队列中。
mq-deadline 是 NVMe 之外的设备(SATA SSD、HDD)的良好默认选择。它够简单,开销低,同时提供了基本的排序和截止时间保证。
五、BFQ(Budget Fair Queueing)
设计哲学
BFQ(Paolo Valente, 2017 合并进主线)可以看作”CFQ 的精神继承者”——它同样追求公平性和交互性,但用了完全不同的机制。
核心思想:给每个进程分配一个 I/O 预算(以扇区数衡量,而非时间)。进程消耗完预算后,让出调度权。
为什么用扇区数而不是时间?因为 SSD 上不同请求的延迟差异比 HDD 小得多。在 HDD 上,一次寻道的 10ms 可以做很多顺序读;在 SSD 上,随机读和顺序读的差异只有 2-5 倍。用扇区数做预算比用时间更公平。
交互进程检测
BFQ 有一个比 CFQ 更精巧的交互进程检测机制。它跟踪进程的 I/O 模式:
- 交互进程:短时间内发出少量 I/O,然后长时间空闲(典型的桌面应用:点击 → 加载 → 等待输入)
- 非交互进程:持续大量 I/O(后台批处理、大文件复制)
交互进程获得优先调度和较小的预算(快速响应后让出 CPU),非交互进程获得较大预算(吞吐优先)。
在 Android 上的应用
BFQ 在 Android 设备上被广泛使用。手机存储(eMMC/UFS)的随机性能远不如数据中心 NVMe,BFQ 的交互优先策略可以显著改善应用启动时间和界面响应。
Google 在 Android 14 的 CDD(Compatibility Definition Document)中建议使用 BFQ 或 mq-deadline。
BFQ 的代价
BFQ 的算法复杂度显著高于 mq-deadline 和 kyber。在高 IOPS 场景下(NVMe SSD),BFQ 本身的 CPU 开销会成为瓶颈。
我在一台 NVMe 服务器上测过:使用 none
调度器可以跑到 120 万 IOPS,切换到 BFQ 后降到了 80 万。30%
的 IOPS 损失纯粹是 BFQ 调度逻辑的 CPU 开销。
六、kyber
极简设计
kyber(Omar Sandoval, 2017)是 blk-mq 时代最简单的调度器——总共只有 ~500 行代码。
设计理念:用延迟反馈控制排队深度。
kyber 只做一件事:限制同时下发到设备的请求数量。它把请求分为两类:
- 读请求:延迟敏感,队列深度控制在较低水平
- 写请求(包括 discard):延迟不敏感,队列深度可以更大
kyber 持续监控设备的读/写延迟。如果延迟升高(设备过载),减少对应类型的排队深度;如果延迟降低,增加排队深度。
// kyber 延迟目标
static const unsigned int kyber_latency_targets[] = {
[KYBER_READ] = 2000, // 2ms 读延迟目标
[KYBER_WRITE] = 10000, // 10ms 写延迟目标
[KYBER_DISCARD] = 5000000, // 5s discard 延迟目标
};为什么 kyber 适合 NVMe
NVMe 设备有自己的内部调度逻辑,不需要软件做扇区排序。kyber 不做排序,只做流量控制——防止设备被过量请求淹没(tail latency 飙升)。
在高 IOPS 场景下,kyber 的 CPU 开销极低(没有排序、没有进程跟踪),同时通过延迟反馈避免了 tail latency 问题。
七、none:不调度也是一种调度
none
调度器只做请求合并(如果两个请求访问连续扇区),不做任何排序或优先级处理。请求按到达顺序直接下发到设备。
在 NVMe 上使用 none 的理由:
- 设备内部调度:NVMe SSD 的 FTL 有自己的优化逻辑
- 多队列无锁:blk-mq 已经消除了软件层的锁争用
- 延迟最低:没有调度器意味着零额外延迟
- CPU 开销最低:不消耗任何调度逻辑的 CPU 时间
但 none 在以下场景下是不够的:
- 多租户:多个容器共享一块 NVMe,需要公平调度(BFQ 或 cgroup I/O 控制)
- 读写优先级:数据库需要读优先于写(mq-deadline)
- tail latency 控制:需要防止设备过载(kyber)
八、实测数据
测试环境
- HDD: Seagate Exos 7E10 (7200 RPM, SATA)
- SATA SSD: Samsung 870 EVO (SATA 6Gbps)
- NVMe SSD: Samsung 980 PRO (PCIe 4.0 x4)
- 工具: fio 3.35
随机读 4KB (iodepth=32, 单线程)
| 调度器 | HDD IOPS | HDD lat(ms) | SATA SSD IOPS | SATA lat(ms) | NVMe IOPS | NVMe lat(μs) |
|---|---|---|---|---|---|---|
| none | 180 | 180 | 85,000 | 0.37 | 520,000 | 61 |
| mq-deadline | 210 | 150 | 88,000 | 0.36 | 510,000 | 62 |
| bfq | 195 | 162 | 82,000 | 0.39 | 480,000 | 66 |
| kyber | 175 | 183 | 86,000 | 0.37 | 515,000 | 62 |
混合读写 (70/30, 4KB, iodepth=32, 4 线程)
| 调度器 | HDD IOPS | NVMe IOPS | NVMe P99 lat(μs) |
|---|---|---|---|
| none | 150 | 850,000 | 180 |
| mq-deadline | 185 | 840,000 | 175 |
| bfq | 170 | 680,000 | 210 |
| kyber | 155 | 845,000 | 145 |
关键观察
- HDD 上 mq-deadline 最优:扇区排序减少寻道,截止时间防止饿死。IOPS 比 none 高 17%。
- NVMe 上 none 吞吐最高:没有调度开销。但 kyber 的 P99 延迟最低(延迟反馈控制的效果)。
- BFQ 的 CPU 开销在 NVMe 上可见:吞吐降了 20%,延迟升了 8%。
基准测试脚本
#!/bin/bash
# io-scheduler-bench.sh — 不同调度器的 fio 基准测试
DEVICE=${1:-/dev/nvme0n1}
SCHEDULERS="none mq-deadline bfq kyber"
for sched in $SCHEDULERS; do
echo "$sched" > /sys/block/$(basename $DEVICE)/queue/scheduler 2>/dev/null
current=$(cat /sys/block/$(basename $DEVICE)/queue/scheduler | grep -o '\[.*\]' | tr -d '[]')
echo "=== Scheduler: $current ==="
# 随机读
fio --name=randread --filename=$DEVICE --direct=1 --rw=randread \
--bs=4k --ioengine=io_uring --iodepth=32 --numjobs=1 \
--runtime=30 --time_based --group_reporting --output-format=terse \
2>/dev/null | awk -F';' '{printf " RandRead: IOPS=%s, lat_avg=%.2fms\n", $8, $16/1000}'
# 混合读写
fio --name=randrw --filename=$DEVICE --direct=1 --rw=randrw --rwmixread=70 \
--bs=4k --ioengine=io_uring --iodepth=32 --numjobs=4 \
--runtime=30 --time_based --group_reporting --output-format=terse \
2>/dev/null | awk -F';' '{printf " RandRW: IOPS=%s, lat_avg=%.2fms\n", $8, $16/1000}'
echo
done九、选型指南
| 设备类型 | 推荐调度器 | 理由 |
|---|---|---|
| HDD | mq-deadline | 寻道优化 + 读优先 |
| SATA SSD | mq-deadline 或 kyber | 轻量排序 + 截止时间保证 |
| NVMe SSD(服务器) | none | 设备自调度,零额外开销 |
| NVMe SSD(多租户) | bfq | 需要进程间公平隔离 |
| NVMe SSD(延迟敏感) | kyber | 延迟反馈控制 tail latency |
| eMMC/UFS(手机) | bfq | 交互优先,改善 App 启动 |
| 虚拟机虚拟磁盘 | none | 宿主机已有调度器 |
一行命令切换
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# 切换到 mq-deadline
echo mq-deadline > /sys/block/sda/queue/scheduler
# 持久化(udev 规则)
# /etc/udev/rules.d/60-ioscheduler.rules
ACTION=="add|change", KERNEL=="sd*", ATTR{queue/rotational}=="1", \
ATTR{queue/scheduler}="mq-deadline"
ACTION=="add|change", KERNEL=="nvme*", ATTR{queue/scheduler}="none"十、总结与个人思考
I/O 调度器的历史是一部硬件驱动的进化史:
HDD 时代: 寻道昂贵 → 复杂排序 (CFQ, 预期调度, per-process 队列)
↓
SATA SSD: 随机 OK 但带宽有限 → 适度调度 (deadline, 读优先)
↓
NVMe SSD: 随机和顺序差异小、内部调度强 → 不调度 (none) 或轻量控制 (kyber)
↓
未来 CXL/远端内存: 延迟差异大、拓扑复杂 → ???
我认为 I/O 调度器最有趣的不是算法本身,而是它的消亡过程。CFQ 是一个精妙的算法,但硬件演进让它的核心假设(“寻道昂贵”)失效,整个算法就变得无关紧要了。这在系统软件中反复发生:你精心优化的代码,可能会被下一代硬件淘汰。
教训:在系统设计中,总是问自己”这个假设在 5 年后还成立吗?“。 如果不确定,让设计保持简洁——简洁的设计比精巧的优化更经得起时间考验。kyber 的 500 行代码可能比 CFQ 的 5000 行更有长期价值。
上一篇: 进程调度:从
CFS 到 EEVDF 的哲学演变
下一篇: 伙伴系统与 SLUB
分配器
相关阅读: - 内存分配器对决 - epoll 的数据结构 - TCP 拥塞控制:从 Reno 到 BBRv3