4. request结构体与请求队列

在Linux块设备层,struct request是I/O请求的核心载体,而request_queue则是驱动与块层之间的调度枢纽。本章深入剖析request结构体的关键成员、请求队列的生命周期管理,以及请求从提交到完成的完整路径。

4.1 request结构体核心成员

struct request定义在<linux/blk-mq.h>中,是块层与驱动交互的最小单元。每个request可以携带多个连续的bio,形成一个完整的I/O操作。

成员 类型 说明
q struct request_queue * 指向所属请求队列
cmd_flags unsigned int 操作标志,如REQ_OP_READ/REQ_OP_WRITE
__sector sector_t 起始扇区号
__data_len unsigned int 数据总长度(字节)
bio struct bio * 链表头,指向第一个bio
biotail struct bio * 链表尾,用于追加bio
rq_disk struct gendisk * 目标磁盘设备
end_io rq_end_io_fn 请求完成回调函数
elv.priv[0] void * 调度器私有数据
关键理解: request与bio的关系是“一对多”。一个request可以合并多个相邻扇区的bio,减少中断次数和DMA开销。驱动通过遍历request->bio链表获取所有数据段。

4.2 请求队列的创建与销毁

请求队列是块设备驱动的核心数据结构,驱动必须正确管理其生命周期。

4.2.1 创建请求队列

现代块设备驱动使用blk-mq(多队列)框架,通过以下API创建队列:

/* 分配请求队列 */
struct request_queue *blk_mq_init_queue(struct blk_mq_tag_set *set);

/* 或使用更细粒度的版本 */
struct request_queue *blk_mq_init_queue_data(
    struct blk_mq_tag_set *set, void *queuedata);

/* 设置队列的限值参数 */
void blk_queue_max_hw_sectors(struct request_queue *q, unsigned int max_sectors);
void blk_queue_max_segments(struct request_queue *q, unsigned short max_segments);
void blk_queue_logical_block_size(struct request_queue *q, unsigned int size);

创建队列前必须先初始化blk_mq_tag_set:

static struct blk_mq_tag_set my_tag_set = {
    .ops        = &my_mq_ops,
    .nr_hw_queues = 1,
    .queue_depth  = 128,
    .numa_node    = NUMA_NO_NODE,
    .flags        = BLK_MQ_F_SHOULD_MERGE,
};

/* 注册tag_set */
blk_mq_alloc_tag_set(&my_tag_set);

/* 创建队列 */
struct request_queue *q = blk_mq_init_queue(&my_tag_set);
if (IS_ERR(q)) {
    blk_mq_free_tag_set(&my_tag_set);
    return PTR_ERR(q);
}
q->queuedata = my_device;

4.2.2 销毁请求队列

在驱动卸载或设备移除时,必须按顺序释放队列资源:

/* 1. 清理队列中的未完成请求 */
blk_mq_freeze_queue(q);
blk_mq_quiesce_queue(q);

/* 2. 注销磁盘(会触发队列清理) */
del_gendisk(disk);

/* 3. 释放请求队列 */
blk_cleanup_queue(q);

/* 4. 释放tag_set */
blk_mq_free_tag_set(&my_tag_set);
常见错误: 先释放tag_set再清理队列会导致use-after-free。必须确保队列完全停止后,才能释放tag_set。另外,在blk_cleanup_queue()之后不能再访问队列的任何成员。

4.3 请求的提交与完成

请求的生命周期从块层提交开始,到驱动完成回调结束。下图展示了完整流程:

请求提交与完成生命周期 块层提交request 调度器排队/合并 驱动获取request DMA数据传输 调用blk_mq_end_request 驱动在queue_rq回调中收到request,完成硬件操作后调用blk_mq_end_request()通知块层

4.3.1 请求提交路径

当文件系统下发I/O时,块层通过以下路径将bio转化为request:

  1. bio提交:submit_bio() → __submit_bio()
  2. bio合并:尝试将bio合并到已有request中(向前合并/向后合并)
  3. request分配:若无法合并,调用blk_mq_get_request()分配新request
  4. 调度入队:request进入调度器队列(如CFQ、deadline、mq-deadline)
  5. 驱动派发:调度器通过blk_mq_dispatch_rq_list()将request派发给驱动

驱动侧接收request的入口是queue_rq回调:

static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
                                const struct blk_mq_queue_data *bd)
{
    struct request *rq = bd->rq;
    struct my_device *dev = hctx->queue->queuedata;

    /* 设置DMA方向 */
    if (op_is_write(req_op(rq)))
        dma_dir = DMA_TO_DEVICE;
    else
        dma_dir = DMA_FROM_DEVICE;

    /* 遍历bio链表,映射DMA地址 */
    __rq_for_each_bio(bio, rq) {
        /* 为每个bio段建立DMA映射 */
    }

    /* 启动硬件DMA传输 */
    start_hw_dma(dev, rq);

    return BLK_STS_OK;
}

4.3.2 请求完成路径

硬件完成I/O后,驱动必须在中断上下文或任务队列中调用完成函数:

/* 中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_device *dev = data;
    struct request *rq = dev->current_rq;

    /* 检查硬件状态 */
    if (readl(dev->regs + STATUS) & ERR_BIT) {
        /* 设置错误码 */
        rq->errors = -EIO;
    }

    /* 通知块层请求完成 */
    blk_mq_end_request(rq, rq->errors ? BLK_STS_IOERR : BLK_STS_OK);

    /* 触发下一个请求的派发 */
    blk_mq_start_stopped_hw_queues(dev->queue, true);

    return IRQ_HANDLED;
}
性能要点: 在blk-mq框架中,blk_mq_end_request()会调用request的end_io回调,进而唤醒等待的进程。如果完成路径在中断上下文中执行,务必确保回调函数不会睡眠。对于高速设备,建议使用IRQ threaded handler或workqueue来减少中断关闭时间。

4.4 请求队列的限值与配置

驱动必须向块层报告硬件能力,以便块层做出合理的合并和调度决策:

API 作用 典型值
blk_queue_max_hw_sectors 单次请求最大扇区数 256 (128KB) ~ 2048 (1MB)
blk_queue_max_segments 最大分散-聚集段数 128 ~ 256
blk_queue_max_segment_size 单个段最大字节数 65536 (64KB)
blk_queue_logical_block_size 逻辑块大小(字节) 512 或 4096
blk_queue_physical_block_size 物理块大小(字节) 512 或 4096
blk_queue_io_min 最小I/O大小 逻辑块大小
blk_queue_io_opt 最优I/O大小(条带对齐) 4096 ~ 1048576
/* 典型NVMe SSD的队列配置示例 */
blk_queue_max_hw_sectors(q, 2048);          /* 1MB */
blk_queue_max_segments(q, 256);
blk_queue_max_segment_size(q, 131072);      /* 128KB */
blk_queue_logical_block_size(q, 512);
blk_queue_physical_block_size(q, 4096);
blk_queue_io_min(q, 4096);
blk_queue_io_opt(q, 131072);                /* 128KB条带 */

4.5 请求与bio的遍历操作

驱动在处理request时,经常需要遍历其包含的所有bio和所有数据段:

/* 遍历request中的所有bio */
struct bio *bio;
__rq_for_each_bio(bio, rq) {
    /* 处理每个bio */
}

/* 遍历bio中的所有段(page+offset+len) */
struct bio_vec bvec;
struct bvec_iter iter;
bio_for_each_segment(bvec, bio, iter) {
    /* bvec.bv_page, bvec.bv_offset, bvec.bv_len */
}

/* 更高效的直接遍历(跳过bio_vec迭代器) */
struct req_iterator iter;
struct bio_vec bvec;
rq_for_each_segment(bvec, rq, iter) {
    /* 直接访问每个DMA段 */
}
最佳实践: 对于高性能驱动,建议使用rq_for_each_segment()宏,它内部已经优化了bio链表的遍历,减少了函数调用开销。同时,在queue_rq回调中应尽早完成DMA映射,避免在中断处理中执行耗时操作。

4.6 本章小结

  • request结构体是块层I/O的核心载体,通过bio链表管理多个连续数据段。
  • 请求队列使用blk_mq_init_queue()创建,销毁时必须先冻结队列再释放tag_set。
  • 请求提交经过bio合并、request分配、调度入队、驱动派发四个阶段。
  • 请求完成通过blk_mq_end_request()通知块层,驱动需在中断或workqueue中调用。
  • 队列限值必须准确反映硬件能力,否则会导致块层做出错误的合并决策。