2. Linux 块设备层核心数据结构:gendisk、request_queue、bio、request 结构体详解

Linux 块设备层是内核 I/O 路径的核心枢纽,它通过四个关键数据结构将上层文件系统与底层物理设备解耦。理解 gendisk、request_queue、bio 和 request 的职责与协作关系,是编写高效块设备驱动的基础。

学习提示: 这四个结构体构成了块设备 I/O 的“四层模型”。从 bio(I/O 请求描述)到 request(排队与调度单元),再到 request_queue(调度队列),最后通过 gendisk(设备抽象)呈现给 VFS。驱动开发的核心工作就是围绕这些结构体进行初始化、回调注册和数据处理。
块设备层核心数据结构关系图 VFS / 文件系统层 gendisk 设备抽象 / 分区表 / Fops request_queue I/O 调度器 / 队列函数 / 限流 request 合并后的 I/O 单元 / 扇区 / 方向 bio 链表(核心 I/O 单元)

2.1 gendisk:块设备的通用磁盘抽象

struct gendisk 是内核向 VFS 层暴露块设备的标准接口。每个块设备(如 /dev/sda)在内核中对应一个 gendisk 实例。它封装了设备号、分区表、请求队列和操作函数集。

成员 类型 说明
major / first_minor int 主设备号和起始次设备号
disk_name char[32] 设备名称,如 "sda"、"nvme0n1"
queue struct request_queue * 指向该磁盘的请求队列
part_tbl struct disk_part_tbl * 分区表信息
fops const struct block_device_operations * 块设备操作函数集(open/release/ioctl/getgeo)
private_data void * 驱动私有数据指针,常用于指向设备结构体
flags unsigned long 设备标志(如 GENHD_FL_NO_PART)
驱动初始化流程: 分配 gendisk → 设置 major/fops/queue → 添加分区 → 调用 add_disk() 注册到系统。注意:add_disk() 必须在所有成员初始化完成后调用,且调用后设备立即可被用户空间访问。

2.2 request_queue:I/O 调度与排队引擎

struct request_queue 是块设备 I/O 的核心调度器。它负责接收上层的 bio,进行合并与排序,然后以 request 的形式分发给驱动。每个 gendisk 有且仅有一个 request_queue。

request_queue 的关键组件包括:

  • I/O 调度器:如 CFQ、deadline、mq-deadline、none(多队列默认)。决定 request 的排队与分发顺序。
  • 队列函数(make_request_fn):单队列(blk_queue_make_request)或多队列(blk_mq_ops)的入口。
  • 限流与节流:通过 blk_queue_max_hw_sectors、blk_queue_max_segments 等设置硬件限制。
  • 软/硬中断亲和性:多队列模式下,每个队列可绑定到特定 CPU,减少锁竞争。
request_queue 内部结构 bio 输入 I/O 调度器 合并 / 排序 / 限流 (deadline/mq-deadline) 请求队列 request 链表 plug/unplug 管理 驱动分发 多队列(blk-mq)优化 每个硬件队列对应一个软件队列,减少锁竞争 支持 CPU 亲和性与中断合并

2.3 bio:块 I/O 的基本操作单元

struct bio 是文件系统层与块设备层之间传递 I/O 请求的最小载体。它描述了一段连续的内存缓冲区与磁盘扇区之间的数据传输。每个 bio 代表一个“逻辑上连续的 I/O 操作”。

成员 说明
bi_sector 起始扇区号(512 字节为单位)
bi_size 传输字节数
bi_io_vec 指向 bio_vec 数组,描述内存页与偏移
bi_vcnt bio_vec 的数量(段数)
bi_rw 读写标志(REQ_OP_READ / REQ_OP_WRITE)
bi_end_io I/O 完成回调函数
bi_private 驱动或上层私有数据
bi_next 链表指针,用于将多个 bio 链接到同一个 request
关键设计: bio 使用 bio_vec 结构体描述内存段,支持“分散-聚集”DMA。驱动在完成传输后必须调用 bio_endio() 通知上层,否则会导致 I/O 永久挂起。

2.4 request:合并后的 I/O 请求

struct request 是 I/O 调度器经过合并与排序后,提交给驱动的最小执行单元。一个 request 可以包含多个连续的 bio(通过 bio 链表链接),这些 bio 在扇区上连续且方向相同。

request 的核心成员:

  • q:所属的 request_queue
  • cmd_flags:操作类型(读/写/刷/丢弃)
  • __sector:起始扇区
  • __data_len:总数据长度
  • bio:指向第一个 bio 的指针
  • rq_disk:指向对应的 gendisk
  • special:驱动私有数据
bio 合并为 request 示意图 bio 1 (sector 100) bio 2 (sector 108) bio 3 (sector 116) request sector=100, nr_sectors=24 bio 链表: bio1 → bio2 → bio3 方向: READ / 总长度: 12KB 驱动处理 DMA 传输 / 完成回调

2.5 四者协作流程总结

当文件系统发起读写时,数据流经以下路径:

  1. 文件系统层:构造 bio,填充扇区、内存页、回调函数。
  2. 通用块层:将 bio 提交到 gendisk 对应的 request_queue。
  3. I/O 调度器:尝试将 bio 合并到已有的 request 中;若无法合并,则创建新的 request。
  4. 驱动分发:从 request_queue 中获取 request,解析 bio 链表,执行 DMA 传输。
  5. 完成回调:每个 bio 的 bi_end_io 被调用,最终通知文件系统 I/O 完成。
性能关键点: bio 合并的粒度直接影响 I/O 效率。连续扇区的 bio 合并为一个 request 后,驱动可以发起一次大的 DMA 传输,减少中断次数和总线开销。多队列(blk-mq)架构下,每个硬件队列独立调度,进一步降低锁竞争。

2.6 驱动开发中的常用 API

API 作用
alloc_disk(int minors) 分配 gendisk 结构体
blk_alloc_queue(GFP_KERNEL) 分配 request_queue(单队列)
blk_mq_alloc_disk() 分配多队列 gendisk + request_queue(推荐)
blk_queue_max_hw_sectors() 设置硬件最大扇区数
blk_queue_max_segments() 设置最大分散-聚集段数
bio_endio(bio) 完成 bio 传输,唤醒等待者
blk_mq_start_request() 标记 request 开始执行(多队列)
最佳实践: 新驱动开发应优先使用 blk-mq 多队列框架。它天然支持 NUMA 亲和性、CPU 负载均衡和更高的 IOPS。单队列(blk_queue_make_request)仅适用于极简单的块设备或遗留代码。