2. 块设备核心数据结构:block_device_operations、gendisk 与 request_queue

块设备驱动的核心在于三个相互关联的数据结构:block_device_operations(操作函数表)、gendisk(通用磁盘描述)和 request_queue(请求队列)。它们构成了块设备驱动与内核块设备层之间的契约,理解这三者的关系是编写正确、高效块设备驱动的前提。

核心关系速览: 每个 gendisk 实例代表一个逻辑磁盘或分区,它内部包含一个指向 block_device_operations 的指针(定义该磁盘支持的操作),以及一个指向 request_queue 的指针(管理该磁盘的 I/O 请求排队与调度)。
块设备驱动三大核心数据结构关系 块设备驱动三大核心数据结构关系 struct gendisk major / first_minor / minors disk_name / queue / fops private_data / flags block_device_operations open / release ioctl / compat_ioctl getgeo / check_events request_queue make_request_fn request_fn / elevator queue_flags / queue_limits gendisk 通过 fops 指针关联操作函数表,通过 queue 指针关联请求队列

2.1 block_device_operations 结构体

block_device_operations 是块设备驱动向内核暴露的操作接口,定义在 <linux/blkdev.h> 中。与字符设备的 file_operations 不同,块设备的操作函数数量较少,因为数据读写不通过这里,而是通过请求队列完成。

成员 函数原型 说明
open int (*open)(struct block_device *, fmode_t) 设备打开时调用,可用于计数或初始化
release void (*release)(struct gendisk *, fmode_t) 设备关闭时调用,清理资源
ioctl int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long) 处理设备特定 ioctl 命令
compat_ioctl int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long) 32位兼容模式下的 ioctl
getgeo int (*getgeo)(struct block_device *, struct hd_geometry *) 获取磁盘几何参数(CHS)
check_events unsigned int (*check_events)(struct gendisk *, unsigned int) 检查介质状态变化(如光盘弹出)
revalidate_disk int (*revalidate_disk)(struct gendisk *) 重新验证磁盘容量或介质变化
owner struct module *owner 通常设置为 THIS_MODULE
关键理解: open 和 release 用于管理设备的打开计数和状态;ioctl 用于非标准控制命令;getgeo 用于传统分区工具(如 fdisk)获取 CHS 参数。数据读写操作不在此结构体中定义。

典型初始化示例:

static int my_open(struct block_device *bdev, fmode_t mode) {
    struct my_disk *disk = bdev->bd_disk->private_data;
    if (!disk) return -ENXIO;
    disk->users++;
    return 0;
}

static void my_release(struct gendisk *gd, fmode_t mode) {
    struct my_disk *disk = gd->private_data;
    disk->users--;
}

static int my_getgeo(struct block_device *bdev, struct hd_geometry *geo) {
    geo->heads = 4;
    geo->sectors = 16;
    geo->cylinders = 1024;
    geo->start = 0;
    return 0;
}

static const struct block_device_operations my_bdops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .ioctl   = my_ioctl,
    .getgeo  = my_getgeo,
};

2.2 gendisk 结构体详解

struct gendisk 是内核中表示一个通用磁盘设备的核心结构体。它封装了磁盘的标识、容量、分区信息、请求队列以及驱动私有数据。每个块设备驱动必须为每个逻辑磁盘分配并注册一个 gendisk 实例。

gendisk 结构体内部组成 gendisk 结构体内部组成 struct gendisk 标识信息 major / first_minor 容量信息 capacity / disk_size 操作接口 fops / queue 驱动私有数据 private_data gendisk 关键字段说明 • major/first_minor/minors:设备号范围,决定该磁盘占用几个次设备号 • capacity:以扇区为单位的磁盘容量(sector_t 类型) • private_data:驱动私有数据指针,通常指向自定义设备结构体

gendisk 生命周期管理:

步骤 函数 说明
1. 分配 alloc_disk(int minors) 分配 gendisk 实例,指定次设备号数量
2. 初始化 手动赋值 设置 major、first_minor、disk_name、fops、queue、private_data、capacity
3. 注册 add_disk(struct gendisk *) 将磁盘注册到内核,创建设备节点并激活 I/O
4. 卸载 del_gendisk(struct gendisk *) 删除磁盘,清理分区信息和设备节点
5. 释放 put_disk(struct gendisk *) 减少引用计数,计数归零时释放内存
常见错误: 在调用 add_disk() 之后,驱动必须确保所有 gendisk 字段已经正确初始化,因为 add_disk() 会立即触发内核对该磁盘的访问(如分区扫描)。不要在 add_disk() 之后再修改 capacity 或 fops。

典型初始化代码:

struct my_disk {
    spinlock_t lock;
    struct request_queue *queue;
    struct gendisk *gd;
    int users;
    // 其他驱动私有数据
};

static int __init my_init(void) {
    struct my_disk *disk;
    
    // 1. 分配驱动私有结构体
    disk = kzalloc(sizeof(*disk), GFP_KERNEL);
    if (!disk) return -ENOMEM;
    spin_lock_init(&disk->lock);
    
    // 2. 分配请求队列
    disk->queue = blk_alloc_queue(GFP_KERNEL);
    if (!disk->queue) goto err_queue;
    blk_queue_make_request(disk->queue, my_make_request);
    
    // 3. 分配 gendisk(支持 16 个分区)
    disk->gd = alloc_disk(16);
    if (!disk->gd) goto err_gd;
    
    // 4. 初始化 gendisk 字段
    disk->gd->major       = my_major;
    disk->gd->first_minor = 0;
    disk->gd->fops        = &my_bdops;
    disk->gd->queue       = disk->queue;
    disk->gd->private_data = disk;
    snprintf(disk->gd->disk_name, 32, "mydisk");
    set_capacity(disk->gd, 1024 * 1024);  // 512MB
    
    // 5. 注册磁盘
    add_disk(disk->gd);
    return 0;
    
err_gd:
    blk_cleanup_queue(disk->queue);
err_queue:
    kfree(disk);
    return -ENOMEM;
}

2.3 request_queue 结构体

struct request_queue 是块设备 I/O 请求的调度与管理中心。每个 gendisk 拥有一个独立的请求队列,所有发往该磁盘的 bio 都会先进入这个队列,经过 I/O 调度器(elevator)的合并与排序后,再传递给驱动处理。

request_queue 内部架构与 I/O 路径 request_queue 内部架构与 I/O 路径 bio 提交 make_request_fn I/O 调度器 (elevator) 合并 / 排序 / 分发 request_fn / 驱动处理 硬件设备

request_queue 核心成员:

成员 类型 说明
make_request_fn 函数指针 接收 bio 并决定如何处理(直接处理或入队)
request_fn 函数指针 从队列中取出 request 并执行(传统模式)
elevator struct elevator_queue * 指向 I/O 调度器实例
queue_flags unsigned long 队列状态标志(如 QUEUE_FLAG_STOPPED)
queue_limits struct queue_limits 设备硬件限制(最大扇区数、对齐要求等)
nr_requests unsigned int 队列中最大请求数
queuedata void * 驱动私有数据指针
两种 I/O 处理模式:
1. 传统 request 模式: 驱动实现 request_fn,内核将 bio 合并为 request 后批量提交。适合机械硬盘等需要 I/O 调度的设备。
2. make_request_fn 模式(BIO 直通): 驱动实现 make_request_fn,直接接收 bio,绕过 I/O 调度器。适合 SSD、RAM disk 等不需要调度的设备。

队列创建与绑定示例:

// 方式一:传统 request 模式(需要 I/O 调度器)
struct request_queue *q;
q = blk_init_queue(my_request_fn, &my_lock);
if (!q) goto err;
disk->queue = q;

// 方式二:make_request_fn 模式(BIO 直通,无调度器)
struct request_queue *q;
q = blk_alloc_queue(GFP_KERNEL);
if (!q) goto err;
blk_queue_make_request(q, my_make_request);
disk->queue = q;

// 设置队列限制
blk_queue_logical_block_size(q, 512);
blk_queue_physical_block_size(q, 4096);
blk_queue_max_hw_sectors(q, 256);
性能提示: 对于基于 RAM 的块设备或 NVMe 等高性能设备,推荐使用 make_request_fn 模式,避免 I/O 调度器的额外开销。对于传统机械硬盘,使用 request_fn 模式以利用 I/O 调度器的合并与排序能力。

2.4 三者协作关系总结

在块设备驱动中,这三个结构体的协作关系可以概括为:

  • gendisk 是"身份证":标识设备的存在、容量和设备号范围。
  • block_device_operations 是"控制面板":提供打开、关闭、ioctl 等控制操作。
  • request_queue 是"数据管道":管理所有读写 I/O 请求的排队、调度与分发。

驱动初始化时,先分配 gendisk 和 request_queue,将两者通过指针关联,再将 block_device_operations 赋值给 gendisk->fops。最后调用 add_disk() 使设备生效。卸载时,先调用 del_gendisk() 移除设备,再清理队列和释放内存。

注意事项: 在调用 add_disk() 之前,必须确保 gendisk->queue 和 gendisk->fops 已经正确赋值。内核在 add_disk() 中会立即使用这些指针,如果为 NULL 将导致内核崩溃。