4. 请求队列与 bio:块设备I/O的核心数据结构
在块设备驱动中,I/O请求的传递与处理依赖于一套精心设计的数据结构。本章深入剖析 bio、request 和 request_queue 三个核心对象,理解它们如何协同工作,将文件系统的逻辑块访问转化为物理设备的实际传输。
核心概念速览: bio 描述单个I/O段(segment),request 聚合多个 bio 形成一次设备操作,request_queue 则作为请求的调度与分发中心。
4.1 bio 结构体详解
bio 是块设备I/O的最小描述单元,它代表一个内存缓冲区与块设备之间的数据传输请求。每个 bio 可以包含多个不连续的物理内存页,支持分散/聚集(scatter-gather)I/O。
4.1.1 bio 核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
bi_next |
struct bio * | 指向下一个 bio,用于将多个 bio 链接成链表 |
bi_disk |
struct gendisk * | 指向目标块设备(通用磁盘结构) |
bi_sector |
sector_t | 起始扇区号(512字节为单位) |
bi_size |
unsigned int | 传输字节数 |
bi_io_vec |
struct bio_vec * | 指向 bio_vec 数组,描述每个内存段 |
bi_vcnt |
unsigned short | bio_vec 数组中有效段的数量 |
bi_rw |
unsigned int | 读写标志(READ/WRITE)及优先级 |
bi_end_io |
bio_end_io_t | I/O完成时的回调函数 |
bi_private |
void * | 驱动私有数据指针 |
4.1.2 bio_vec:内存段描述符
每个 bio_vec 描述一个连续的内存区域,包含页指针、页内偏移和长度。多个 bio_vec 构成一个 bio 的完整数据缓冲区。
struct bio_vec {
struct page *bv_page; /* 指向物理页框 */
unsigned int bv_len; /* 段长度(字节) */
unsigned int bv_offset;/* 页内偏移 */
};
设计意图: bio_vec 机制使得块设备驱动可以直接操作物理页,避免额外的数据拷贝。对于 SD 卡等 DMA 设备,驱动可以直接将 bio_vec 中的物理地址填入 DMA 描述符。
4.1.3 bio 与 bio_vec 关系图
4.2 request 结构体
request 是块设备驱动处理的基本单位,它由一个或多个 物理地址连续 的 bio 合并而成。驱动层的 request_fn 或 make_request_fn 直接操作 request 对象。
4.2.1 request 关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
q |
struct request_queue * | 所属请求队列 |
cmd_flags |
unsigned int | 请求标志(REQ_OP_READ/WRITE/FLUSH 等) |
__sector |
sector_t | 请求起始扇区 |
__data_len |
unsigned int | 请求总数据长度 |
bio |
struct bio * | 指向第一个 bio |
biotail |
struct bio * | 指向最后一个 bio |
rq_disk |
struct gendisk * | 目标磁盘 |
start_time_ns |
u64 | 请求入队时间戳(用于统计) |
special |
void * | 驱动私有数据 |
4.2.2 request 与 bio 的聚合关系
多个 bio 可以合并为一个 request,前提是它们访问的扇区连续且方向相同。合并过程由 I/O 调度器自动完成,驱动开发者通常无需手动干预。
4.3 请求队列的创建与配置
请求队列(request_queue)是块设备驱动的核心调度枢纽。驱动通过 blk_init_queue 或 blk_alloc_queue 创建队列,并绑定请求处理函数。
4.3.1 请求队列创建流程
- 分配队列:调用
blk_alloc_queue(GFP_KERNEL)分配队列结构体。 - 设置 make_request_fn:对于非传统块设备(如 MTD、RAM disk),使用
blk_queue_make_request绑定自定义处理函数。 - 初始化传统请求队列:使用
blk_init_queue(request_fn, lock)创建带 I/O 调度器的队列。 - 配置队列参数:设置最大扇区数、段大小、DMA 对齐等硬件约束。
- 绑定到 gendisk:将队列指针赋值给
gendisk->queue。
4.3.2 关键配置函数
| 函数 | 作用 | 典型调用时机 |
|---|---|---|
blk_init_queue |
创建带 I/O 调度器的请求队列 | 驱动初始化时 |
blk_alloc_queue |
分配空队列(需手动绑定 make_request_fn) | 无调度器场景(如 MTD) |
blk_queue_make_request |
绑定自定义请求处理函数 | blk_alloc_queue 之后 |
blk_queue_max_hw_sectors |
设置单次传输最大扇区数 | 硬件能力约束 |
blk_queue_max_segments |
设置最大分散/聚集段数 | DMA 描述符数量限制 |
blk_queue_logical_block_size |
设置逻辑块大小(默认 512) | 非 512 字节扇区设备 |
blk_queue_dma_alignment |
设置 DMA 对齐要求 | 硬件 DMA 约束 |
4.3.3 请求队列初始化代码示例
#include <linux/blkdev.h>
#include <linux/blk-mq.h>
static struct request_queue *my_queue;
static spinlock_t my_queue_lock;
/* 传统请求处理函数 */
static void my_request_fn(struct request_queue *q)
{
struct request *req;
while ((req = blk_fetch_request(q)) != NULL) {
/* 处理请求:获取 bio、遍历 bio_vec、启动 DMA */
struct bio *bio = req->bio;
sector_t sector = blk_rq_pos(req);
unsigned int nr_sectors = blk_rq_sectors(req);
/* 实际数据传输逻辑 */
// ...
__blk_end_request_all(req, 0); /* 0 表示成功 */
}
}
static int __init my_driver_init(void)
{
/* 步骤1:分配并初始化请求队列 */
spin_lock_init(&my_queue_lock);
my_queue = blk_init_queue(my_request_fn, &my_queue_lock);
if (!my_queue)
return -ENOMEM;
/* 步骤2:配置硬件约束 */
blk_queue_max_hw_sectors(my_queue, 256); /* 最大 128KB */
blk_queue_max_segments(my_queue, 32); /* 最多 32 个段 */
blk_queue_logical_block_size(my_queue, 512); /* 512 字节扇区 */
/* 步骤3:绑定到 gendisk */
my_gendisk->queue = my_queue;
return 0;
}
注意: 对于 SD 卡、eMMC 等真实设备,建议使用 blk-mq(多队列)框架以获得更好的多核扩展性。传统
blk_init_queue 适用于简单块设备或教学示例。
4.4 请求队列与 I/O 调度器
请求队列内部集成了 I/O 调度器,负责对 bio 进行合并、排序和分发。常见的 I/O 调度器包括:
- NOOP:简单 FIFO,适合 SSD 或闪存设备。
- Deadline:为每个请求设置截止时间,避免饥饿。
- CFQ:完全公平队列,为进程分配时间片。
- BFQ:基于权重的预算公平队列。
4.4.1 请求队列内部架构
4.5 本章小结
- bio 是 I/O 请求的最小描述单元,通过 bio_vec 支持分散/聚集。
- request 聚合多个连续 bio,是驱动处理的基本单位。
- request_queue 负责 bio 的合并、排序和分发,驱动通过
blk_init_queue或blk_alloc_queue创建。 - I/O 调度器(NOOP/Deadline/CFQ)影响请求的合并策略和响应延迟。
- 配置队列参数(最大扇区、段数、对齐)必须与硬件能力匹配。