2. 块设备核心数据结构

在嵌入式 Linux 块设备驱动中,三个核心数据结构构成了驱动与内核块 I/O 层之间的桥梁:gendisk(通用磁盘描述)、block_device_operations(块设备操作函数表)和 request_queue(请求队列)。理解这三者的角色、成员关系以及初始化流程,是编写任何块设备驱动的第一步。

核心关系速记:每个块设备对应一个 gendisk 实例;gendisk 内部绑定一个 request_queue 用于 I/O 请求排队与调度;block_device_operations 则提供打开、关闭、获取介质信息等控制操作。三者协同完成从 VFS → 块层 → 驱动的完整路径。

2.1 gendisk 结构体详解

gendisk 是内核中代表“一个磁盘”或“一个分区表容器”的核心对象。它位于 <linux/genhd.h>,驱动通过 alloc_disk 系列函数分配,并在注册时与块设备号绑定。

2.1.1 关键成员

成员类型说明
majorint主设备号
first_minorint起始次设备号
minorsint支持的次设备号数量(分区数 + 1)
disk_namechar[32]设备名称,如 mmcblk0
fopsconst struct block_device_operations *指向操作函数表
queuestruct request_queue *指向该磁盘的请求队列
private_datavoid *驱动私有数据指针,通常指向宿主结构体
flagsint标志位,如 GENHD_FL_REMOVABLE
partstruct disk_part_tbl *分区表信息
part0struct hd_struct代表整个磁盘的分区结构
驱动初始化典型流程:
alloc_disk(minors) → 设置 major、fops、queue、private_data → add_disk(disk) 使设备对用户空间可见。

2.1.2 gendisk 生命周期与分区管理

每个 gendisk 通过 minors 字段声明支持的次设备号数量。例如 SD 卡驱动通常设置 minors = 16,其中 part0 代表整卡,part1 ~ part15 对应分区。内核块层在 add_disk 时自动扫描分区表并创建分区块设备。

常见错误:忘记设置 disk->queue 或 disk->fops 就调用 add_disk,会导致内核 oops。务必在注册前完成所有核心字段的赋值。
gendisk 核心成员与关联对象 struct gendisk major: int first_minor: int minors: int disk_name[32] fops → bd_ops queue → req_queue private_data → void* flags: int part0: hd_struct part: disk_part_tbl* block_device_operations open / release / ioctl getgeo / media_changed request_queue make_request_fn / elevator 驱动私有数据 (如 mmc_card 结构体) 💡 关键绑定 gendisk 通过 fops 和 queue 将控制路径与数据路径 分离管理。

2.2 block_device_operations 结构体

block_device_operations(简称 bd_ops)定义在 <linux/blkdev.h>,是块设备驱动向内核暴露的控制接口。与字符设备的 file_operations 不同,块设备的读写操作不通过此结构体,而是通过 request_queue 处理。

2.2.1 核心回调函数

回调原型用途
openint (*open)(struct block_device *, fmode_t)设备打开时调用,可用于电源管理或独占访问控制
releasevoid (*release)(struct gendisk *, fmode_t)最后一个引用关闭时调用
ioctlint (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long)处理自定义控制命令,如 BLKGETSIZE
compat_ioctlint (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long)32位兼容 ioctl
getgeoint (*getgeo)(struct block_device *, struct hd_geometry *)返回 CHS 几何参数(磁头、柱面、扇区)
media_changedint (*media_changed)(struct gendisk *)检查可移动介质是否更换(如 SD 卡)
revalidate_diskint (*revalidate_disk)(struct gendisk *)介质更换后重新验证容量与分区表
SD 卡驱动典型实现:
open 中增加电源计数;media_changed 检测卡检测引脚电平;revalidate_disk 重新读取 CSD 寄存器更新容量;getgeo 返回固定 CHS 参数(如 255 磁头、63 扇区/磁道)。

2.2.2 与 file_operations 的对比

维度block_device_operationsfile_operations
读写路径不包含 read/write,由 request_queue 处理包含 read/write,直接处理
打开语义open 可被多次调用,需引用计数open 每次打开都调用
主要用途控制、几何、介质管理数据读写与控制
注册方式赋值给 gendisk->fops赋值给 cdev->ops

2.3 request_queue 结构体

request_queue 是块设备 I/O 数据路径的核心调度与排队单元。驱动通过 blk_init_queue 或 blk_alloc_queue + blk_queue_make_request 创建队列,并将队列绑定到 gendisk。

2.3.1 关键成员与功能

成员/接口说明
make_request_fn驱动自定义的请求处理函数(非标准队列模式)
request_fn标准请求处理函数,由 blk_init_queue 注册
elevatorI/O 调度器实例(如 CFQ、deadline、noop)
queue_flags队列标志,如 QUEUE_FLAG_NONROT(非旋转设备)
limitsstruct queue_limits,包含最大扇区数、对齐要求等
queuedata驱动私有数据指针,通常指向宿主结构体

2.3.2 两种请求处理模式

  • 标准请求队列(request_fn 模式):调用 blk_init_queue(request_fn, lock),内核将合并排序后的 struct request 传递给驱动的 request_fn。驱动遍历请求并处理每个 bio。
  • 自制请求队列(make_request_fn 模式):调用 blk_alloc_queue(GFP_KERNEL) + blk_queue_make_request(q, make_request_fn),驱动直接接收 struct bio,绕过 I/O 调度器。适用于简单设备(如 ramdisk、MTD 块层)。
选择建议:SD/MMC 卡驱动通常使用标准请求队列,以便利用 I/O 调度器优化顺序访问;而简单的内存模拟块设备或 MTD 块设备常使用 make_request_fn 模式以减少开销。
request_queue 两种请求处理模式 VFS / 通用块层 (submit_bio) 标准请求队列 blk_init_queue(request_fn, lock) I/O 调度器合并排序 → struct request 驱动遍历 request → bio → 硬件 make_request_fn 模式 blk_alloc_queue + blk_queue_make_request 直接接收 struct bio 绕过 I/O 调度器,驱动自行处理 硬件驱动层(SD/MMC/NAND) 💡 标准模式 适合 SD/MMC 等机械/半机械设备 💡 自制模式 适合 RAM disk、MTD 等简单设备

2.4 三者协作:驱动初始化代码骨架

以下代码片段展示了一个简化的块设备驱动初始化过程,突出 gendisk、block_device_operations 和 request_queue 的绑定关系。

#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

static struct my_block_dev {
    spinlock_t lock;
    struct request_queue *queue;
    struct gendisk *disk;
    /* 模拟存储缓冲区 */
    unsigned char *data;
} my_dev;

static int my_open(struct block_device *bdev, fmode_t mode) {
    /* 增加引用计数或电源管理 */
    return 0;
}

static void my_release(struct gendisk *disk, fmode_t mode) {
    /* 减少引用计数 */
}

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

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

static void my_request_fn(struct request_queue *q) {
    struct request *req;
    while ((req = blk_fetch_request(q)) != NULL) {
        /* 处理请求:获取 bio、数据传输 */
        blk_end_request_all(req, 0);
    }
}

static int __init my_init(void) {
    /* 1. 分配 gendisk */
    my_dev.disk = alloc_disk(16);
    if (!my_dev.disk)
        return -ENOMEM;

    /* 2. 初始化请求队列 */
    my_dev.queue = blk_init_queue(my_request_fn, &my_dev.lock);
    if (!my_dev.queue) {
        put_disk(my_dev.disk);
        return -ENOMEM;
    }

    /* 3. 绑定核心字段 */
    my_dev.disk->major       = 240;          /* 自定义主设备号 */
    my_dev.disk->first_minor = 0;
    my_dev.disk->minors      = 16;
    strcpy(my_dev.disk->disk_name, "myblk");
    my_dev.disk->fops        = &my_bd_ops;
    my_dev.disk->queue       = my_dev.queue;
    my_dev.disk->private_data = &my_dev;

    /* 4. 设置容量(以 512 字节扇区为单位) */
    set_capacity(my_dev.disk, 1024 * 1024);  /* 512 MB */

    /* 5. 注册设备 */
    add_disk(my_dev.disk);
    return 0;
}

static void __exit my_exit(void) {
    del_gendisk(my_dev.disk);
    blk_cleanup_queue(my_dev.queue);
    put_disk(my_dev.disk);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
初始化顺序要点:
① 先分配 gendisk 和 request_queue;
② 再设置 gendisk 的 major、fops、queue、private_data;
③ 调用 set_capacity 设置扇区数;
④ 最后调用 add_disk 使设备可见。顺序颠倒可能导致内核崩溃。

2.5 本章小结

  • gendisk 是块设备在内核中的“身份证”,承载设备号、名称、操作表、队列和私有数据。
  • block_device_operations 负责控制路径(打开、关闭、几何、介质检测),不参与数据读写。
  • request_queue 是数据路径的核心,支持标准 request_fn 和自制 make_request_fn 两种模式。
  • 三者在驱动初始化时通过 gendisk 的 fops 和 queue 字段紧密绑定,缺一不可。