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 请求的提交与完成
请求的生命周期从块层提交开始,到驱动完成回调结束。下图展示了完整流程:
4.3.1 请求提交路径
当文件系统下发I/O时,块层通过以下路径将bio转化为request:
- bio提交:
submit_bio()→__submit_bio() - bio合并:尝试将bio合并到已有request中(向前合并/向后合并)
- request分配:若无法合并,调用
blk_mq_get_request()分配新request - 调度入队:request进入调度器队列(如CFQ、deadline、mq-deadline)
- 驱动派发:调度器通过
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中调用。 - 队列限值必须准确反映硬件能力,否则会导致块层做出错误的合并决策。