4. 请求队列与bio

在块设备驱动中,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 关系图

bio 与 bio_vec 结构关系 struct bio bi_sector: 0x1000 bi_size: 8192 bi_vcnt: 3 bi_io_vec → bi_next → NULL bi_end_io: fn bi_private: ... bio_vec[0] bv_page: 0x9e000 bv_offset: 0 bv_len: 4096 bio_vec[1] bv_page: 0xa1000 bv_offset: 256 bv_len: 2048 bio_vec[2] bv_page: 0xb0000 bv_offset: 0 bv_len: 2048 bi_io_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 调度器自动完成,驱动开发者通常无需手动干预。

request 与 bio 链表结构 struct request __sector: 0x1000 __data_len: 12288 cmd_flags: READ bio → biotail → bio #1 bi_sector: 0x1000 bi_size: 4096 bio #2 bi_sector: 0x1008 bi_size: 4096 bio #3 bi_sector: 0x1010 bi_size: 4096 bio bi_next bi_next biotail

4.3 请求队列的创建与配置

请求队列(request_queue)是块设备驱动的核心调度枢纽。驱动通过 blk_init_queue 或 blk_alloc_queue 创建队列,并绑定请求处理函数。

4.3.1 请求队列创建流程

  1. 分配队列:调用 blk_alloc_queue(GFP_KERNEL) 分配队列结构体。
  2. 设置 make_request_fn:对于非传统块设备(如 MTD、RAM disk),使用 blk_queue_make_request 绑定自定义处理函数。
  3. 初始化传统请求队列:使用 blk_init_queue(request_fn, lock) 创建带 I/O 调度器的队列。
  4. 配置队列参数:设置最大扇区数、段大小、DMA 对齐等硬件约束。
  5. 绑定到 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 请求队列内部架构

请求队列内部架构与 I/O 调度流程 bio 输入 合并 & 排序 I/O 调度器 request 队列 FIFO / 红黑树 分发 request_fn 调度流程说明 1. bio 到达请求队列,I/O 调度器尝试与现有 request 合并(扇区连续、方向相同) 2. 无法合并的 bio 被包装为新 request,按扇区顺序插入红黑树 3. request_fn 从队列头部取出 request,交给驱动处理

4.5 本章小结

  • bio 是 I/O 请求的最小描述单元,通过 bio_vec 支持分散/聚集。
  • request 聚合多个连续 bio,是驱动处理的基本单位。
  • request_queue 负责 bio 的合并、排序和分发,驱动通过 blk_init_queue 或 blk_alloc_queue 创建。
  • I/O 调度器(NOOP/Deadline/CFQ)影响请求的合并策略和响应延迟。
  • 配置队列参数(最大扇区、段数、对齐)必须与硬件能力匹配。