4. 请求队列与 I/O 调度:blk_init_queue、blk_cleanup_queue、I/O 调度器类型与选择
块设备驱动的核心任务之一是高效管理上层文件系统下发的 I/O 请求。Linux 内核通过 请求队列(Request Queue) 机制将通用块层与具体设备驱动解耦,并引入 I/O 调度器(I/O Scheduler) 对请求进行排序、合并与分发,以优化磁盘寻道时间与吞吐量。本章深入剖析请求队列的生命周期管理、核心 API 以及主流 I/O 调度器的原理与选型策略。
4.1 请求队列生命周期管理
请求队列是块设备驱动与通用块层之间的核心数据结构(struct request_queue)。驱动通过 blk_init_queue 创建队列,通过 blk_cleanup_queue 销毁队列。队列中挂载了 I/O 调度器、软/硬中断处理函数、DMA 映射回调以及限速参数。
4.1.1 blk_init_queue:队列初始化
blk_init_queue 是驱动注册块设备时最常用的队列初始化函数。它分配 request_queue 结构体,绑定 I/O 调度器,并注册请求处理函数(request_fn)。
#include <linux/blkdev.h>
static struct request_queue *q;
static DEFINE_SPINLOCK(my_lock);
/* 请求处理函数原型 */
static void my_request_fn(struct request_queue *q)
{
struct request *req;
while ((req = blk_fetch_request(q)) != NULL) {
/* 处理单个请求:解析 bio、映射 DMA、启动硬件传输 */
blk_end_request_all(req, 0);
}
}
/* 模块初始化时调用 */
int __init my_init(void)
{
/* 分配请求队列,绑定处理函数和自旋锁 */
q = blk_init_queue(my_request_fn, &my_lock);
if (!q)
return -ENOMEM;
/* 设置队列参数:最大扇区数、段数、DMA 对齐等 */
blk_queue_max_hw_sectors(q, 256);
blk_queue_max_segments(q, 128);
blk_queue_dma_alignment(q, 512 - 1);
/* 注册磁盘(gendisk)并关联队列 */
return 0;
}
- request_fn:驱动必须实现的回调,当 I/O 调度器将请求下发到驱动时被调用。该函数运行在软中断上下文,不能睡眠。
- lock:自旋锁,用于保护请求队列的并发访问。驱动在 request_fn 中操作队列时必须持有该锁。
- blk_queue_max_hw_sectors:限制单次 DMA 传输的最大扇区数,避免硬件限制导致传输失败。
4.1.2 blk_cleanup_queue:队列销毁
当设备被移除或驱动卸载时,必须调用 blk_cleanup_queue 安全销毁请求队列。该函数会等待所有未完成的 I/O 请求完成或错误返回,然后释放队列资源。
void __exit my_exit(void)
{
/* 先注销磁盘,再清理队列 */
del_gendisk(my_gendisk);
put_disk(my_gendisk);
/* 清理请求队列:等待所有请求完成,释放调度器资源 */
blk_cleanup_queue(q);
}
- 在仍有未完成 DMA 传输时调用
blk_cleanup_queue,导致内存损坏或设备挂起。 - 未先调用
del_gendisk就清理队列,导致 sysfs 条目泄漏。 - 在 request_fn 中持有锁时调用
blk_cleanup_queue,造成死锁。
4.2 I/O 调度器类型与选择
I/O 调度器位于通用块层与设备驱动之间,负责对 bio/request 进行排序、合并和分发。Linux 内核支持多种调度器,每种针对不同的硬件特性(HDD、SSD、NVMe)和负载模式进行了优化。
4.2.1 主流 I/O 调度器对比
| 调度器 | 核心算法 | 适用设备 | 优势 | 劣势 |
|---|---|---|---|---|
| mq-deadline | 按扇区排序 + 截止时间(读优先) | HDD、混合负载 | 避免请求饥饿,读延迟低 | 对纯顺序写场景优化不足 |
| BFQ | 基于权重的公平队列 | 桌面、交互式系统 | 低延迟,公平性好 | CPU 开销较高 |
| Kyber | 基于延迟的令牌桶 | NVMe、SSD | 极低开销,自适应 | 对 HDD 优化不足 |
| none | 无调度,直接下发 | NVMe、高性能 SSD | 零开销,最大吞吐 | 无 QoS 保证 |
4.2.2 调度器选择策略
- HDD(机械硬盘):推荐 mq-deadline。其按扇区排序可减少磁头寻道,截止时间机制保证读请求不被写请求饿死。
- NVMe / 高性能 SSD:推荐 none 或 Kyber。NVMe 设备内部已实现高效并行调度,内核层调度反而增加延迟。
- 虚拟化 / 容器场景:推荐 BFQ。BFQ 可为每个 cgroup 分配权重,保证多租户 I/O 公平性。
- 实时 / 低延迟系统:推荐 Kyber。其基于延迟反馈动态调整并发度,避免队列堆积。
none 调度器,务必确保驱动实现了高效的 blk_mq_ops(多队列接口),否则可能因缺乏调度导致 I/O 顺序混乱。
4.3 请求队列与 I/O 调度器架构
下图展示了请求队列、I/O 调度器与块设备驱动之间的数据流关系。上层文件系统通过 submit_bio 提交 bio,通用块层将其转换为 request 并插入调度器队列,调度器排序后通过 request_fn 回调传递给驱动。
4.4 多队列(blk-mq)与调度器适配
现代 NVMe 和高速 SSD 使用 blk-mq(Block Multi-Queue) 框架,将单队列拆分为多个硬件分发队列(hctx),每个队列可绑定独立 CPU 核心,实现无锁并发。blk-mq 对 I/O 调度器的接口进行了重构:
- mq-deadline:为 blk-mq 重写的 deadline 调度器,支持每 hctx 独立排序。
- none:直接透传,不进行任何调度,适合 NVMe 设备。
- Kyber:原生支持 blk-mq,基于每 hctx 的延迟令牌桶。
/* blk-mq 驱动中设置调度器示例 */
static int my_blk_mq_init(struct my_device *dev)
{
struct request_queue *q;
struct blk_mq_tag_set *set;
set = kzalloc(sizeof(*set), GFP_KERNEL);
set->ops = &my_mq_ops;
set->nr_hw_queues = num_online_cpus(); /* 每 CPU 一个硬件队列 */
set->queue_depth = 256;
set->numa_node = NUMA_NO_NODE;
set->flags = BLK_MQ_F_SHOULD_MERGE;
q = blk_mq_init_queue(set);
if (IS_ERR(q))
return PTR_ERR(q);
/* 设置调度器为 none(NVMe 推荐) */
blk_queue_flag_set(QUEUE_FLAG_NO_SCHED, q);
dev->queue = q;
return 0;
}
- NVMe 设备:使用
none,避免调度器引入额外延迟。 - SATA SSD:使用
mq-deadline,兼顾延迟与吞吐。 - HDD 多队列:使用
mq-deadline,排序可显著减少寻道。
4.5 请求队列参数调优
驱动可以通过 blk_queue_* 系列函数调整队列行为,以匹配硬件特性:
| API | 作用 | 典型值 |
|---|---|---|
blk_queue_max_hw_sectors |
限制单次 DMA 最大扇区数 | 256 (128KB) |
blk_queue_max_segments |
限制 scatter-gather 最大段数 | 128 |
blk_queue_dma_alignment |
设置 DMA 缓冲区对齐要求 | 511 (512 字节对齐) |
blk_queue_logical_block_size |
设置逻辑块大小(通常 512 或 4096) | 512 |
blk_queue_physical_block_size |
设置物理块大小(4K 对齐) | 4096 |
- 将
max_hw_sectors设置过大可能导致 DMA 超过硬件 PRD 表长度,造成传输截断。 - 未设置
dma_alignment可能导致 bounce buffer 频繁分配,降低性能。 - 在 blk-mq 驱动中,
queue_depth设置过大会导致内存浪费,过小则降低吞吐。
4.6 本章小结
- 请求队列是块设备驱动的核心数据结构,通过
blk_init_queue创建、blk_cleanup_queue销毁。 - I/O 调度器负责请求排序与合并,HDD 推荐
mq-deadline,NVMe 推荐none,交互式系统推荐BFQ。 - blk-mq 框架通过多硬件队列提升并发,调度器需适配
mq-deadline或none。 - 驱动应通过
blk_queue_*参数调优队列,匹配硬件 DMA 能力。