4. I/O 调度器选择:CFQ、Deadline、NOOP、BFQ 调度器原理与适用场景

文件系统调优里,I/O 调度器是个容易被忽略的环节。很多人觉得选默认的就行,其实不然。我见过不少嵌入式设备,换个调度器,读写性能直接翻倍。今天咱们就把这几个调度器掰开揉碎讲清楚。

4.1 调度器到底在干什么?

说白了,I/O 调度器就是块设备层的“交通警察”。多个进程同时发读写请求,谁先谁后?怎么合并?怎么排队?调度器说了算。

Linux 内核里,调度器位于通用块层和块设备驱动之间。它接收上层的 bio 请求,经过排序、合并、分发,再交给驱动去处理硬件。

核心职责:

  • 请求合并:把相邻的扇区请求合并成一个,减少寻道次数
  • 请求排序:按扇区顺序重排,减少磁头移动
  • 公平调度:保证多个进程都能分到 I/O 时间片
  • 延迟控制:确保关键请求不被饿死
I/O 调度器在块设备层的位置 应用进程 (APP) VFS + 文件系统 通用块层 (bio) I/O 调度器 CFQ / Deadline / NOOP / BFQ 块设备驱动 调度器核心工作 ① 请求合并 ② 请求排序 ③ 公平调度 ④ 延迟控制

4.2 四大调度器逐个拆解

4.2.1 CFQ(完全公平队列)

CFQ 是 Linux 内核用了很多年的默认调度器。它的思路很朴素:给每个进程分配一个 I/O 时间片,大家轮流用。有点像操作系统的进程调度,只不过这里调度的是磁盘访问权。

工作原理:

  • 每个进程有自己的 I/O 请求队列
  • 按时间片轮转方式处理各队列
  • 支持优先级(ionice 命令就是跟它配合的)
  • 同步请求优先于异步请求

适用场景:

  • 通用桌面系统、多用户环境
  • 需要公平分配 I/O 带宽的场景
  • 机械硬盘(HDD)上表现不错

不适用场景:

  • SSD/NAND Flash(寻道时间几乎为零,排序合并意义不大)
  • 高并发数据库(延迟抖动较大)
  • 实时性要求高的嵌入式系统

我的经验: 在某个工业控制项目中,CFQ 导致数据采集进程偶尔出现 200ms 以上的 I/O 延迟。换成 Deadline 后,最大延迟降到了 30ms 以内。如果你的系统对 I/O 延迟敏感,CFQ 可能不是最佳选择。

4.2.2 Deadline(截止时间调度器)

Deadline 的设计目标很明确:保证每个请求都不会被饿死太久。它给每个请求设置了一个“截止时间”,默认读请求 500ms,写请求 5s。一旦超时,这个请求会被优先处理。

工作原理:

  • 维护两个队列:排序队列(按扇区排序)和 FIFO 队列(按截止时间排序)
  • 正常情况下按排序队列处理,提高吞吐量
  • 当有请求接近截止时间时,切换到 FIFO 队列优先处理
  • 读请求优先级高于写请求(读饥饿保护)

适用场景:

  • 数据库服务器(对读延迟敏感)
  • 嵌入式实时系统
  • 混合读写负载
  • SSD 和 HDD 都适用

关键参数:

参数 默认值 说明
read_expire 500 ms 读请求截止时间
write_expire 5000 ms 写请求截止时间
fifo_batch 16 每次批量处理的请求数
writes_starved 2 读优先处理次数后,才处理一次写

4.2.3 NOOP(电梯调度器)

NOOP 是最简单的调度器。它几乎不做排序和合并,只维护一个简单的 FIFO 队列。新请求来了直接插到队尾,或者跟相邻的请求合并一下。

你可能会问:这么简单有什么用?

嗯,对于 SSD 和 NAND Flash 这类随机访问性能很好的设备,排序和合并带来的收益微乎其微。反而调度器本身的 CPU 开销成了负担。这时候 NOOP 就是最佳选择。

适用场景:

  • SSD、NVMe、eMMC、UFS 等闪存设备
  • CPU 资源紧张的嵌入式系统
  • 底层硬件自己做了 I/O 调度(如某些 RAID 卡、NVMe 控制器)

注意: 我曾经在一个使用 NAND Flash 的物联网设备上测试过,NOOP 相比 CFQ 能降低约 15% 的 CPU 占用。但要注意,如果设备同时有大量顺序读写和随机读写混合,NOOP 可能会导致吞吐量下降。建议先做压力测试再决定。

4.2.4 BFQ(预算公平队列)

BFQ 是 CFQ 的升级版,也是目前比较新的调度器。它把 I/O 调度做得更精细——不仅分配时间片,还分配“预算”(budget)。预算就是每次可以传输的数据量。

工作原理:

  • 每个进程有独立的 I/O 预算(默认约 4MB)
  • 预算用完后,切换到下一个进程
  • 低延迟进程(如交互式应用)会获得更小的预算,更频繁地被调度
  • 支持 cgroup 的 I/O 带宽控制

适用场景:

  • 桌面系统(交互式体验好)
  • 多媒体播放、音视频处理
  • 需要细粒度 I/O 控制的场景
  • 机械硬盘和 SSD 都适用

我的建议: BFQ 在 Android 系统上用得比较多,因为能明显提升 UI 操作的流畅度。但在纯后台服务的嵌入式设备上,BFQ 的调度开销比 Deadline 高 5%-10%,不一定划算。

4.3 如何选择和切换调度器

查看当前调度器很简单:

# 查看所有块设备的调度器
cat /sys/block/sda/queue/scheduler

# 输出示例:[mq-deadline] none
# 方括号里的是当前使用的调度器

临时切换:

echo deadline > /sys/block/sda/queue/scheduler

永久生效(以 root 身份):

# 修改内核启动参数,在 /etc/default/grub 中添加
GRUB_CMDLINE_LINUX="elevator=deadline"

# 或者使用 udev 规则
echo 'ACTION=="add|change", KERNEL=="sd*", ATTR{queue/scheduler}="deadline"' > /etc/udev/rules.d/60-iosched.rules

选择建议速查表:

设备类型 推荐调度器 理由
机械硬盘(HDD) Deadline 或 BFQ 减少寻道时间,保证延迟
SATA SSD Deadline 或 NOOP 延迟可控,开销低
NVMe NOOP(none) 硬件调度足够好,软件调度是累赘
eMMC / NAND Flash NOOP 或 Deadline 减少 CPU 开销
实时系统 Deadline 截止时间保证
桌面/多媒体 BFQ 交互体验好

4.4 ionice 命令:进程级 I/O 优先级控制

ionice 可以给单个进程设置 I/O 优先级。它只对 CFQ 和 BFQ 调度器生效,Deadline 和 NOOP 不支持。

优先级等级:

  • Idle(空闲,优先级 3): 只有磁盘空闲时才执行 I/O,适合后台备份、日志清理
  • Best-effort(尽力而为,优先级 0-7): 默认等级,0 最高,7 最低
  • Real-time(实时,优先级 0-7): 优先于所有 Best-effort 进程,慎用

常用用法:

# 查看当前 I/O 优先级
ionice -p 1234

# 以 Best-effort 等级 0 启动程序
ionice -c 2 -n 0 ./my_app

# 将正在运行的进程改为 Idle 等级
ionice -c 3 -p 1234

# 实时等级(谨慎使用)
ionice -c 1 -n 0 ./critical_app

避坑指南: 我曾经在一个视频监控项目中,把录像存储进程设成了 Idle 等级。结果磁盘压力大时,录像直接丢帧了。后来改成 Best-effort 等级 4,既保证了录像不丢,又不影响其他进程。记住:Idle 等级真的会被饿死。

4.5 块设备队列深度调整

队列深度(queue_depth)决定了块设备驱动一次能处理多少个未完成的 I/O 请求。这个参数对性能影响很大。

查看和调整:

# 查看当前队列深度
cat /sys/block/sda/device/queue_depth

# 临时调整(以 NVMe 为例)
echo 64 > /sys/block/nvme0n1/device/queue_depth

# 对于 SCSI 设备
echo 32 > /sys/block/sda/device/queue_depth

队列深度对性能的影响:

队列深度 随机读(IOPS) 延迟(ms) 适用场景
1-4 低 低(稳定) 实时系统、单线程应用
8-16 中等 中等 通用嵌入式系统
32-64 高 较高(波动大) 高并发服务器、数据库
128+ 极高 高(可能超时) NVMe 高性能场景

我的调优经验: 队列深度不是越大越好。我在一个使用 eMMC 的工控板上测试过,队列深度从 32 降到 8,随机读 IOPS 虽然下降了 20%,但 99% 延迟从 50ms 降到了 8ms。对于实时性要求高的场景,牺牲一点吞吐量换延迟稳定性,很值得。

调优步骤建议:

  1. 先用 fio 测试当前队列深度下的性能基线
  2. 逐步增大队列深度(2 倍递增),观察 IOPS 和延迟变化
  3. 找到 IOPS 不再明显增长的拐点
  4. 在拐点附近,根据延迟要求微调
  5. 固化到启动脚本或 udev 规则中

4.6 本章小结

调度器选择和队列深度调整,说白了就是吞吐量和延迟之间的权衡。没有银弹,只有适合你场景的方案。

我个人习惯的做法是:先确定设备类型(HDD/SSD/NVMe/Flash),再根据负载特征(随机/顺序、读/写比例、实时性要求)选调度器,最后用 fio 压测验证。队列深度从默认值开始,逐步调整到性能拐点附近。

记住一点:调优不是一次性的。系统跑起来后,用 iostat、blktrace 持续监控,发现异常及时调整。这才是嵌入式工程师该有的态度。