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 调度器选择策略

  1. HDD(机械硬盘):推荐 mq-deadline。其按扇区排序可减少磁头寻道,截止时间机制保证读请求不被写请求饿死。
  2. NVMe / 高性能 SSD:推荐 none 或 Kyber。NVMe 设备内部已实现高效并行调度,内核层调度反而增加延迟。
  3. 虚拟化 / 容器场景:推荐 BFQ。BFQ 可为每个 cgroup 分配权重,保证多租户 I/O 公平性。
  4. 实时 / 低延迟系统:推荐 Kyber。其基于延迟反馈动态调整并发度,避免队列堆积。
性能调优提示: 在 NVMe 驱动中,如果使用 none 调度器,务必确保驱动实现了高效的 blk_mq_ops(多队列接口),否则可能因缺乏调度导致 I/O 顺序混乱。

4.3 请求队列与 I/O 调度器架构

下图展示了请求队列、I/O 调度器与块设备驱动之间的数据流关系。上层文件系统通过 submit_bio 提交 bio,通用块层将其转换为 request 并插入调度器队列,调度器排序后通过 request_fn 回调传递给驱动。

请求队列与 I/O 调度器架构 文件系统 (VFS / ext4) 通用块层 (submit_bio) 请求队列 (request_queue) I/O 调度器 (mq-deadline / none) 排序 合并 request_fn (驱动回调) 块设备驱动 (DMA 传输) 硬件设备 (NVMe / SATA) 调度器核心职责 • 合并相邻扇区请求 • 按 LBA 排序减少寻道 • 读优先避免饥饿

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;
}
blk-mq 调度器选择建议:
  • 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 能力。