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。驱动开发的核心工作就是围绕这些结构体进行初始化、回调注册和数据处理。
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,减少锁竞争。
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:驱动私有数据
2.5 四者协作流程总结
当文件系统发起读写时,数据流经以下路径:
- 文件系统层:构造 bio,填充扇区、内存页、回调函数。
- 通用块层:将 bio 提交到 gendisk 对应的 request_queue。
- I/O 调度器:尝试将 bio 合并到已有的 request 中;若无法合并,则创建新的 request。
- 驱动分发:从 request_queue 中获取 request,解析 bio 链表,执行 DMA 传输。
- 完成回调:每个 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)仅适用于极简单的块设备或遗留代码。