3. IO调度器概述
IO调度器(I/O Scheduler)位于块设备通用层与底层驱动之间,负责管理块设备请求队列中IO请求的排序、合并与分发。不同的调度器在吞吐量、延迟、公平性和初始化开销上存在显著差异。本章将逐一介绍Linux内核中六种主流IO调度器的设计原理、适用场景及其对请求队列深度调优的影响。
3.1 调度器全景对比
下表从核心机制、适用硬件、初始化开销和延迟特性四个维度对比六种主流IO调度器:
| 调度器 | 核心机制 | 适用硬件 | 初始化开销 | 延迟特性 |
|---|---|---|---|---|
| Noop | 简单FIFO + 合并 | SSD/NVMe、RAM盘 | 极低 | 最低,无重排 |
| Deadline | 按截止时间排序 + 读写分离 | 传统HDD、混合存储 | 低 | 可控,避免饿死 |
| CFQ | 完全公平队列 + 时间片 | 传统HDD(多进程) | 高 | 公平但波动大 |
| BFQ | 预算公平队列 + 低延迟优化 | HDD/低速SSD | 很高 | 低延迟保障 |
| Kyber | 基于延迟反馈的令牌桶 | NVMe/高性能SSD | 低 | 自适应低延迟 |
| MQ-Deadline | 多队列版Deadline | 多队列NVMe/SSD | 中 | 低延迟,多核友好 |
3.2 各调度器详解
3.2.1 Noop调度器
Noop是Linux内核中最简单的IO调度器,它仅执行请求合并操作,不进行任何排序或重排。请求按到达顺序(FIFO)直接传递给底层驱动。Noop的初始化过程仅需分配一个简单的链表头,几乎不产生额外延迟。
- 优点: 初始化极快,CPU开销几乎为零,适合随机访问性能优异的NVMe/SSD设备。
- 缺点: 对HDD无任何寻道优化,可能导致严重的磁头抖动。
- 适用场景: 纯SSD/NVMe环境、虚拟化宿主机(后端存储已做调度)。
3.2.2 Deadline调度器
Deadline调度器在Noop基础上引入了截止时间(deadline)机制。每个IO请求被赋予一个过期时间(读请求默认500ms,写请求默认5s),调度器维护两个红黑树(读/写)和两个FIFO队列。当请求即将过期时,Deadline会优先处理该请求,避免饿死。
- 优点: 读写分离,延迟可控,避免写请求饿死读请求。
- 缺点: 初始化需要建立红黑树结构,开销略高于Noop。
- 适用场景: 传统HDD、数据库服务器、混合读写负载。
3.2.3 CFQ调度器(完全公平队列)
CFQ(Completely Fair Queuing)为每个进程分配独立的IO请求队列,并按时间片轮转调度。CFQ试图在进程之间实现IO带宽的公平分配。其初始化过程需要为每个进程创建队列结构,并维护复杂的优先级和权重计算逻辑,因此初始化延迟较高。
- 优点: 多进程场景下公平性好,支持IO优先级(ionice)。
- 缺点: 初始化开销大,高并发下吞吐量下降,已被BFQ取代趋势。
- 适用场景: 桌面系统、多用户服务器(已逐渐淘汰)。
3.2.4 BFQ调度器(预算公平队列)
BFQ(Budget Fair Queuing)是CFQ的改进版,引入了预算(budget)概念。每个队列在时间片内被分配一个IO预算(以扇区数计量),预算用完后切换到下一个队列。BFQ还包含低延迟优化(low_latency模式),可自动降低交互式进程的延迟。
- 优点: 极佳的公平性和低延迟保障,适合交互式桌面和多媒体应用。
- 缺点: 初始化开销最高,需要为每个进程分配预算表和调度参数。
- 适用场景: 桌面Linux、Android设备、低速HDD。
3.2.5 Kyber调度器
Kyber是一种基于延迟反馈的调度器,专为高性能NVMe设备设计。它使用令牌桶控制读写请求的并发数,并通过实时监控IO完成延迟动态调整令牌发放速率。Kyber的初始化仅需分配几个原子变量和定时器,开销极低。
- 优点: 初始化快,自适应延迟控制,无需手动调参。
- 缺点: 对HDD效果不佳,缺乏公平性保障。
- 适用场景: NVMe SSD、云存储节点、延迟敏感型应用。
3.2.6 MQ-Deadline调度器
MQ-Deadline是Deadline调度器的多队列(Multi-Queue)版本,适配blk-mq框架。它将Deadline的截止时间逻辑扩展到每个硬件分发队列(hctx)上,每个队列独立维护红黑树和FIFO队列。MQ-Deadline在初始化时需要为每个硬件队列分配独立的调度数据结构,开销介于Noop和BFQ之间。
- 优点: 多核扩展性好,延迟低,兼容多队列NVMe设备。
- 缺点: 初始化复杂度随硬件队列数线性增长。
- 适用场景: 多队列NVMe SSD、高性能存储服务器。
3.3 IO调度器架构与初始化流程
下图展示了Linux内核中IO调度器的通用架构,以及请求从通用层进入调度器、经过排序/合并后分发到底层驱动的完整流程:
3.4 调度器初始化延迟对比
下表量化了不同调度器在初始化阶段的典型延迟范围(基于Linux 5.15内核,x86_64平台,单队列场景测试):
| 调度器 | 初始化延迟 (μs) | 内存分配量 (KB) | 数据结构复杂度 |
|---|---|---|---|
| Noop | 0.5 - 1.2 | 0.5 | 链表 |
| Kyber | 1.0 - 2.5 | 1.2 | 令牌桶 + 定时器 |
| Deadline | 3.0 - 8.0 | 4.0 | 红黑树 + FIFO队列 |
| MQ-Deadline | 5.0 - 15.0 | 8.0 × 硬件队列数 | 多组红黑树 |
| CFQ | 20.0 - 50.0 | 16.0 | 多队列 + 时间片树 |
| BFQ | 30.0 - 80.0 | 32.0 | 预算表 + 优先级树 |
3.5 调度器选择决策树
以下决策树帮助开发者根据硬件类型和负载特征快速选择合适的IO调度器:
3.6 调度器切换与初始化延迟实测
在实际调优中,调度器的切换操作(通过 echo <scheduler> > /sys/block/<dev>/queue/scheduler)会触发完整的初始化流程。以下是一个典型的调度器切换测试代码片段:
#!/bin/bash
# 调度器切换延迟测试脚本
DEVICE="/dev/nvme0n1"
SCHEDULERS=("none" "kyber" "mq-deadline" "bfq")
for sched in "${SCHEDULERS[@]}"; do
# 记录切换前时间戳(纳秒)
start=$(date +%s%N)
echo "$sched" > /sys/block/nvme0n1/queue/scheduler
end=$(date +%s%N)
latency=$(( (end - start) / 1000 )) # 转换为微秒
echo "Scheduler: $sched, Switch Latency: ${latency} μs"
done
3.7 小结
本章详细介绍了Linux内核中六种主流IO调度器的设计原理、初始化开销和适用场景。核心要点如下:
- Noop和Kyber 初始化延迟最低,适合NVMe/SSD等高性能设备。
- Deadline和MQ-Deadline 在延迟可控性和多队列扩展性之间取得平衡。
- CFQ和BFQ 提供优秀的公平性,但初始化开销较大,不适合延迟敏感场景。
- 调度器的选择直接影响请求队列深度调优的效果,初始化延迟是评估调度器性能的重要指标。