4. 请求队列与 I/O 调度:blk_init_queue、blk_cleanup_queue、I/O 调度器类型(noop/deadline/cfq/mq-deadline)原理
块设备驱动的核心是管理 I/O 请求队列。Linux 内核通过 请求队列(Request Queue) 将文件系统下发的通用 I/O 请求进行排队、合并与排序,再交由底层驱动处理。本章深入剖析请求队列的初始化与销毁流程,并详细对比四种经典 I/O 调度器的设计原理与适用场景。
4.1 请求队列生命周期:blk_init_queue 与 blk_cleanup_queue
每个块设备在注册时都需要创建一个请求队列。内核提供 blk_init_queue 进行初始化,并在设备移除时通过 blk_cleanup_queue 进行销毁。
4.1.1 blk_init_queue 初始化流程
blk_init_queue 的原型如下:
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
参数说明:
- rfn:驱动提供的请求处理函数(request_fn),当队列中有待处理请求时被调用。
- lock:用于保护队列操作的自旋锁。
初始化过程中,内核会完成以下关键步骤:
- 分配
request_queue结构体内存。 - 设置默认的 I/O 调度器(通常为 CFQ 或 mq-deadline)。
- 初始化队列的等待队列、完成队列、统计计数器。
- 绑定驱动提供的
request_fn回调。 - 设置队列的软/硬限制(如最大扇区数、最大请求数)。
blk_init_queue 的返回值。若返回 NULL,通常表示内存不足或参数无效,此时应返回 -ENOMEM 并中止设备注册。
4.1.2 blk_cleanup_queue 销毁流程
当设备被移除或驱动卸载时,必须调用 blk_cleanup_queue 来释放队列资源:
void blk_cleanup_queue(request_queue_t *q);
销毁过程包括:
- 标记队列为“死亡”状态,阻止新请求入队。
- 等待所有未完成的请求完成或超时。
- 释放调度器相关的内部数据结构。
- 释放队列自身占用的内存。
blk_cleanup_queue 之前,必须确保没有线程正在访问该队列。通常需要在移除设备前先调用 del_gendisk 断开设备与队列的关联,否则可能导致内核崩溃。
4.1.3 请求队列生命周期 SVG 流程图
4.2 I/O 调度器类型与原理
I/O 调度器位于通用块层与驱动之间,负责对请求进行合并、排序和分发。Linux 内核支持多种调度器,每种调度器针对不同的工作负载进行了优化。
4.2.1 调度器类型对比表
| 调度器名称 | 核心算法 | 适用场景 | 主要特点 |
|---|---|---|---|
| Noop | FIFO + 简单合并 | SSD、NVMe、RAM 盘 | 开销最低,不进行寻道优化 |
| Deadline | 按截止时间排序 | 数据库、实时应用 | 保证读写请求的最大延迟 |
| CFQ | 完全公平队列 | 传统 HDD、桌面系统 | 按进程分配时间片,公平性好 |
| mq-deadline | 多队列 Deadline | 多队列块设备(NVMe) | Deadline 的多队列版本,支持 blk-mq |
4.2.2 Noop 调度器
Noop 调度器是最简单的调度器,它只做两件事:
- 请求合并:将相邻扇区的请求合并为一个更大的请求。
- FIFO 处理:按请求到达顺序提交给驱动。
Noop 不进行任何寻道优化,因此对于没有寻道开销的设备(如 SSD、NVMe)非常高效。对于 HDD,Noop 会导致严重的寻道延迟。
4.2.3 Deadline 调度器
Deadline 调度器为每个请求设置一个截止时间(默认读 500ms,写 5s),并维护两个队列:
- 排序队列(Sorted Queue):按扇区号排序,用于合并和顺序提交。
- FIFO 队列:按截止时间排序,用于保证延迟。
调度策略:
- 优先从排序队列中提交请求,以最大化顺序吞吐。
- 如果 FIFO 队列头部的请求即将超时,则立即提交该请求(即使破坏顺序性)。
- 读请求的截止时间更短,因此读操作优先于写操作。
/sys/block/<dev>/queue/iosched/writes_starved 参数,控制读请求连续提交的次数上限。
4.2.4 CFQ 调度器
CFQ(Completely Fair Queuing)为每个进程分配一个独立的 I/O 队列,并按时间片轮转调度。核心机制:
- 进程隔离:每个进程的 I/O 请求放入独立队列。
- 时间片分配:每个队列获得一个时间片,时间片内可以提交一定数量的请求。
- 优先级支持:通过 I/O 优先级(ionice)调整进程的权重。
CFQ 在传统 HDD 上表现优异,但在 SSD 上由于时间片切换开销较大,已被 mq-deadline 或 none 取代。
mq-deadline 或 none(即 Noop 的多队列版本)。
4.2.5 mq-deadline 调度器
mq-deadline 是 Deadline 调度器针对多队列块设备(blk-mq)的适配版本。它与传统 Deadline 的核心区别:
- 多队列支持:每个硬件提交队列(hardware dispatch queue)独立维护排序队列和 FIFO 队列。
- 无全局锁:使用每队列锁,减少锁竞争。
- 批量提交:支持一次从多个队列中批量提取请求。
mq-deadline 是目前 NVMe 和高端 SSD 的默认调度器,兼顾了延迟保证与多核扩展性。
4.2.6 I/O 调度器架构对比 SVG 图
4.3 调度器切换与调试
在运行时可以通过 sysfs 接口动态切换 I/O 调度器,无需重启系统:
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# 切换调度器(例如切换到 deadline)
echo deadline > /sys/block/sda/queue/scheduler
输出示例:
[mq-deadline] none
方括号内的调度器为当前激活的调度器。对于 blk-mq 设备,可选的调度器通常为 none(即 Noop)和 mq-deadline。
none 和 mq-deadline 两种调度器下的吞吐量和延迟分布。使用 fio 工具配合不同的调度器,可以快速定位调度器引入的性能瓶颈。
4.4 请求队列与调度器交互流程
当文件系统下发一个 I/O 请求时,完整的交互流程如下:
- 请求生成:文件系统调用
submit_bio提交 bio 结构体。 - 请求合并:通用块层尝试将 bio 合并到现有请求中(合并条件:相邻扇区、相同方向)。
- 请求入队:如果无法合并,则创建新请求并插入调度器的排序队列。
- 调度决策:调度器根据自身策略(FIFO、截止时间、时间片等)选择下一个要提交的请求。
- 请求分发:调度器将选中的请求放入驱动层的分发队列(dispatch queue)。
- 驱动处理:驱动的
request_fn从分发队列中取出请求,提交给硬件。
/sys/block/<dev>/queue/ 下的统计文件观察调度器的行为,例如 nr_requests(队列深度)、iosched/ 目录下的调度器特定参数。
4.5 本章小结
- 请求队列是块设备驱动的核心数据结构,
blk_init_queue和blk_cleanup_queue管理其生命周期。 - Noop 适用于无寻道开销的设备,Deadline 保证延迟上限,CFQ 提供进程公平性(已废弃),mq-deadline 是现代多队列设备的默认选择。
- 通过 sysfs 可以在运行时切换调度器,便于性能调优和问题定位。
- 理解调度器原理是分析块设备 I/O 延迟和吞吐量问题的前提。