2. 块设备驱动核心数据结构详解
块设备驱动的核心在于四个关键数据结构:gendisk(通用磁盘描述)、request_queue(请求队列)、block_device_operations(块设备操作集)和 bio(块 I/O 请求)。它们构成了从文件系统到物理设备之间的完整数据通路。理解这些结构的职责、生命周期和交互关系,是编写和调试块设备驱动的基础。
核心要点:gendisk 代表一个逻辑磁盘;request_queue 管理 I/O 请求的排队与调度;block_device_operations 定义驱动回调函数;bio 是 I/O 请求的最小传输单元。四者通过内核块层协作,完成数据读写。
2.1 gendisk:通用磁盘描述符
struct gendisk 是内核中表示一个逻辑磁盘的核心结构体。每个块设备(如 /dev/sda、/dev/nvme0n1)都对应一个 gendisk 实例。它包含了磁盘的几何信息、分区表、请求队列指针以及驱动私有数据。
| 成员 | 类型 | 说明 |
|---|---|---|
major |
int | 主设备号 |
first_minor |
int | 起始次设备号 |
minors |
int | 支持的次设备号数量(通常为分区数+1) |
disk_name |
char[32] | 磁盘名称,如 "vdab"、"nvme0n1" |
queue |
struct request_queue * | 指向该磁盘的请求队列 |
fops |
const struct block_device_operations * | 块设备操作函数表 |
private_data |
void * | 驱动私有数据指针 |
capacity |
sector_t | 磁盘容量,以 512 字节扇区为单位 |
part_tbl |
struct disk_part_tbl * | 分区表信息 |
调试技巧:在驱动中打印 gendisk 信息时,使用
printk("disk=%s, capacity=%llu sectors\n", gendisk->disk_name, (unsigned long long)gendisk->capacity);。注意 capacity 的单位是 512 字节扇区,而非字节。
2.2 request_queue:请求队列与 I/O 调度
struct request_queue 是块设备 I/O 请求的排队和调度中心。文件系统提交的 bio 请求经过块层合并、排序后,形成 request 链表挂入队列。驱动通过注册 make_request_fn 或 request_fn 从队列中取出请求并处理。
- make_request_fn:直接接收 bio,适用于简单块设备(如 ramdisk、virtio-blk)。驱动自行决定如何处理 bio。
- request_fn:接收经过 I/O 调度器合并排序后的 request,适用于传统磁盘(如 SCSI、SATA)。
- I/O 调度器:内核支持多种调度算法(mq-deadline、kyber、bfq),通过 elevator 机制挂载。
| 成员/函数 | 说明 |
|---|---|
make_request_fn |
驱动自定义的 bio 处理函数,绕过 I/O 调度器 |
request_fn |
传统请求处理函数,由 I/O 调度器触发 |
elevator |
指向 I/O 调度器实例 |
queue_flags |
队列标志位,如 QUEUE_FLAG_DISCARD、QUEUE_FLAG_NONROT |
nr_requests |
队列中最大请求数 |
queuedata |
驱动私有数据,通常指向硬件设备结构体 |
常见错误:驱动在
make_request_fn 中未调用 bio_endio 结束 bio,会导致 I/O 永远挂起,系统 D 状态进程堆积。务必在每次 bio 处理完成后调用 bio_endio(bio, errno)。
2.3 block_device_operations:块设备操作函数表
struct block_device_operations 定义了驱动向内核暴露的回调函数。与字符设备的 file_operations 类似,但块设备操作集更精简,主要关注打开、关闭、获取几何信息、介质状态检查等。
| 函数指针 | 说明 | 驱动实现要点 |
|---|---|---|
open |
设备打开时调用 | 检查设备状态,增加引用计数 |
release |
设备关闭时调用 | 减少引用计数,清理资源 |
ioctl |
设备控制命令 | 处理 BLKGETSIZE64、HDIO_GETGEO 等 |
getgeo |
获取磁盘几何信息 | 填充 struct hd_geometry(柱面/磁头/扇区) |
check_events |
检查介质状态变化 | 用于可移动介质设备(如 USB、光驱) |
revalidate_disk |
重新验证磁盘容量 | 容量变化时调用,更新 gendisk->capacity |
// 典型 block_device_operations 实现示例
static int myblock_open(struct block_device *bdev, fmode_t mode) {
struct myblock_dev *dev = bdev->bd_disk->private_data;
printk(KERN_INFO "myblock: open device, capacity=%llu\n",
(unsigned long long)dev->capacity);
return 0;
}
static int myblock_getgeo(struct block_device *bdev, struct hd_geometry *geo) {
struct myblock_dev *dev = bdev->bd_disk->private_data;
geo->heads = 1;
geo->sectors = 1;
geo->cylinders = dev->capacity;
return 0;
}
static const struct block_device_operations myblock_fops = {
.owner = THIS_MODULE,
.open = myblock_open,
.release = myblock_release,
.getgeo = myblock_getgeo,
};
2.4 bio:块 I/O 请求的最小单元
struct bio 是内核块层与文件系统、驱动之间传递 I/O 请求的核心结构体。每个 bio 描述一个或多个连续内存页的传输请求,包含起始扇区、数据方向(读/写)、数据页列表等信息。
| 成员 | 说明 |
|---|---|
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 |
驱动或文件系统私有数据 |
2.5 四者协作关系
在块设备驱动中,这四个数据结构按照以下流程协作:
- 注册阶段:驱动调用
alloc_disk()分配 gendisk,设置fops、queue、private_data,最后调用add_disk()注册到系统。 - I/O 提交阶段:文件系统通过
submit_bio()提交 bio 到块层。块层将 bio 合并到 request_queue 中的 request。 - I/O 处理阶段:驱动从 request_queue 取出 request(或直接接收 bio),解析 bio 中的
bi_sector、bi_io_vec,执行 DMA 传输。 - I/O 完成阶段:驱动调用
bio_endio()通知 bio 完成,块层唤醒等待的进程。
调试检查点:当块设备驱动加载后,通过
ls -l /sys/block/ 查看 gendisk 注册情况;通过 cat /sys/block/<dev>/queue/scheduler 查看当前 I/O 调度器;通过 blktrace 跟踪 bio 提交与完成事件。