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 字节扇区,而非字节。
gendisk 结构体核心成员关系 struct gendisk disk_name capacity (sectors) private_data queue fops part_tbl 每个 gendisk 对应一个 /dev/ 节点;驱动通过 alloc_disk() 分配,add_disk() 注册

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 驱动私有数据,通常指向硬件设备结构体
request_queue 与 I/O 路径 文件系统 (VFS) 块层 (submit_bio) request_queue make_request_fn / request_fn I/O 调度器 mq-deadline / kyber / bfq 块设备驱动 物理设备
常见错误:驱动在 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 驱动或文件系统私有数据
bio 结构体与数据页映射 struct bio bi_sector, bi_size, bi_rw bio_vec[0] bv_page, bv_offset, bv_len bio_vec[1] bio_vec[2] 物理页框 (page) 物理页框 (page) 物理页框 (page) bio 通过 bio_vec 数组描述分散的内存页,支持向量 I/O(scatter-gather) 驱动使用 bio_for_each_segment() 遍历每个段,进行 DMA 映射

2.5 四者协作关系

在块设备驱动中,这四个数据结构按照以下流程协作:

  1. 注册阶段:驱动调用 alloc_disk() 分配 gendisk,设置 fops、queue、private_data,最后调用 add_disk() 注册到系统。
  2. I/O 提交阶段:文件系统通过 submit_bio() 提交 bio 到块层。块层将 bio 合并到 request_queue 中的 request。
  3. I/O 处理阶段:驱动从 request_queue 取出 request(或直接接收 bio),解析 bio 中的 bi_sector、bi_io_vec,执行 DMA 传输。
  4. I/O 完成阶段:驱动调用 bio_endio() 通知 bio 完成,块层唤醒等待的进程。
四者协作流程图 gendisk request_queue bio fops 典型驱动初始化流程 1. alloc_disk() → 分配 gendisk 2. blk_alloc_queue() / blk_init_queue() → 创建 request_queue 3. 设置 gendisk->fops、gendisk->queue、gendisk->private_data 4. add_disk() → 注册到系统,生成 /dev/ 节点
调试检查点:当块设备驱动加载后,通过 ls -l /sys/block/ 查看 gendisk 注册情况;通过 cat /sys/block/<dev>/queue/scheduler 查看当前 I/O 调度器;通过 blktrace 跟踪 bio 提交与完成事件。