3. 块设备驱动框架搭建

本章是块设备驱动开发的骨架搭建阶段。我们将从零开始,构建一个最小化的块设备驱动框架,重点掌握 模块生命周期管理、gendisk 结构体的分配与注册,以及 块设备操作接口中的 open/release 回调。理解这些基础组件,是后续实现真实硬件(如 SD 卡)驱动的必要前提。

核心目标: 完成一个可加载/卸载的内核模块,该模块在加载时创建一个虚拟块设备(如 /dev/vdX),支持打开和关闭操作,但不涉及实际数据传输。

3.1 块设备驱动核心数据结构

在 Linux 块设备层,驱动与内核交互的核心是 gendisk 和 block_device_operations 两个结构体。下表列出了它们的核心字段与职责:

结构体 关键字段 说明
struct gendisk major, first_minor, minors 设备号范围:主设备号、起始次设备号、次设备号数量
disk_name 设备名称,如 "vdblk"
fops 指向 block_device_operations 的指针
queue 请求队列,用于 I/O 调度
private_data 驱动私有数据指针
struct block_device_operations open 设备打开回调,可做权限检查或状态初始化
release 设备关闭回调,可做资源清理
owner 通常设为 THIS_MODULE,防止模块被卸载
设计原则: 在框架阶段,open 和 release 函数可以只返回 0(成功),但必须存在。内核在访问块设备时会默认调用这两个回调,缺失会导致内核崩溃。

3.2 模块初始化与退出流程

一个完整的块设备驱动模块,其生命周期包含以下步骤。我们使用流程图来直观展示:

块设备驱动模块生命周期流程图 module_init(blk_init) 1. 分配主设备号 (register_blkdev) 2. 分配 gendisk (alloc_disk) 3. 设置 fops / queue / 私有数据 4. 注册 gendisk (add_disk) module_exit(blk_exit) 1. 删除 gendisk (del_gendisk) 2. 释放 gendisk (put_disk) 3. 释放请求队列 (blk_cleanup_queue) 4. 注销主设备号 (unregister_blkdev) 注意:add_disk 之后,设备立即可被用户空间访问,卸载时必须先 del_gendisk

3.3 分配与注册 gendisk

gendisk 是块设备在内核中的核心抽象。分配和注册过程必须遵循严格的顺序:

  1. 分配主设备号: 使用 register_blkdev(major, name),若 major 传 0 则内核动态分配。
  2. 分配 gendisk: alloc_disk(minors),minors 表示支持的次设备号数量(通常为 1 或 16)。
  3. 初始化 gendisk 字段: 设置 major、first_minor、disk_name、fops、queue 等。
  4. 注册 gendisk: add_disk(disk),此调用会使设备节点在 /dev 下出现。
常见错误: 在 add_disk 之后,内核可能立即调用 open 回调。因此,所有必要的初始化(如队列、私有数据)必须在 add_disk 之前完成,否则会导致空指针访问。

3.4 实现简单的 open / release 函数

在框架阶段,open 和 release 函数的主要作用是验证设备状态和引用计数管理。以下是一个最小实现:

static int vd_open(struct block_device *bdev, fmode_t mode)
{
    struct gendisk *disk = bdev->bd_disk;
    struct vd_dev *dev = disk->private_data;

    pr_info("vd_open: device %s opened, mode=0x%x\n", disk->disk_name, mode);

    // 检查设备是否已卸载(可选)
    if (!dev) {
        pr_err("vd_open: device not ready\n");
        return -ENXIO;
    }

    // 增加模块引用计数,防止模块被卸载
    try_module_get(THIS_MODULE);

    return 0;
}

static void vd_release(struct gendisk *disk, fmode_t mode)
{
    pr_info("vd_release: device %s released\n", disk->disk_name);

    // 减少模块引用计数
    module_put(THIS_MODULE);
}

关键点说明:

  • 参数差异: open 接收 struct block_device *,而 release 接收 struct gendisk *,这是内核版本演进的结果,需注意适配。
  • 引用计数: 使用 try_module_get / module_put 确保模块在设备打开期间不会被卸载。
  • 错误返回: 如果设备处于异常状态,应返回负的错误码(如 -ENXIO)。

3.5 完整框架代码示例

将上述组件整合,得到一个最小化的块设备驱动框架:

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

#define VD_MAJOR       0   // 动态分配
#define VD_MINORS      1
#define VD_NAME        "vdblk"

static int vd_major;
static struct gendisk *vd_disk;
static struct request_queue *vd_queue;

static int vd_open(struct block_device *bdev, fmode_t mode)
{
    pr_info("vd: open called\n");
    try_module_get(THIS_MODULE);
    return 0;
}

static void vd_release(struct gendisk *disk, fmode_t mode)
{
    pr_info("vd: release called\n");
    module_put(THIS_MODULE);
}

static struct block_device_operations vd_fops = {
    .owner   = THIS_MODULE,
    .open    = vd_open,
    .release = vd_release,
};

static int __init vd_init(void)
{
    int ret;

    // 1. 注册主设备号
    vd_major = register_blkdev(VD_MAJOR, VD_NAME);
    if (vd_major < 0) {
        pr_err("vd: failed to register blkdev\n");
        return vd_major;
    }

    // 2. 分配请求队列(无真实硬件,使用虚拟队列)
    vd_queue = blk_alloc_queue(GFP_KERNEL);
    if (!vd_queue) {
        pr_err("vd: failed to alloc queue\n");
        unregister_blkdev(vd_major, VD_NAME);
        return -ENOMEM;
    }

    // 3. 分配 gendisk
    vd_disk = alloc_disk(VD_MINORS);
    if (!vd_disk) {
        pr_err("vd: failed to alloc disk\n");
        blk_cleanup_queue(vd_queue);
        unregister_blkdev(vd_major, VD_NAME);
        return -ENOMEM;
    }

    // 4. 初始化 gendisk
    vd_disk->major       = vd_major;
    vd_disk->first_minor = 0;
    vd_disk->fops        = &vd_fops;
    vd_disk->queue       = vd_queue;
    vd_disk->private_data = NULL;  // 后续可指向设备结构体
    snprintf(vd_disk->disk_name, 32, VD_NAME);

    // 5. 注册 gendisk
    add_disk(vd_disk);

    pr_info("vd: initialized, major=%d\n", vd_major);
    return 0;
}

static void __exit vd_exit(void)
{
    del_gendisk(vd_disk);
    put_disk(vd_disk);
    blk_cleanup_queue(vd_queue);
    unregister_blkdev(vd_major, VD_NAME);
    pr_info("vd: exited\n");
}

module_init(vd_init);
module_exit(vd_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Course");
MODULE_DESCRIPTION("Minimal block device driver framework");

3.6 验证与调试

编译并加载模块后,可以通过以下步骤验证框架是否正常工作:

  • 检查设备节点: ls -l /dev/vdblk 应显示块设备文件。
  • 查看内核日志: dmesg | tail 应看到 "vd: initialized" 和 open/release 的打印。
  • 测试打开/关闭: sudo fdisk -l /dev/vdblk 会触发 open 和 release 回调。
  • 卸载模块: sudo rmmod vdblk,确认设备节点消失且日志显示 "vd: exited"。
调试技巧: 如果 add_disk 后设备节点未出现,检查 /proc/devices 中是否注册了主设备号。如果模块卸载时系统崩溃,通常是因为 del_gendisk 调用顺序错误或引用计数未正确管理。

3.7 本章小结

通过本章的学习,我们完成了块设备驱动框架的搭建:

  • 掌握了 gendisk 和 block_device_operations 的核心字段与作用。
  • 理解了模块初始化与退出的标准流程,以及各步骤的依赖关系。
  • 实现了最小化的 open 和 release 函数,并理解了引用计数管理的重要性。
  • 提供了一个可直接编译运行的完整框架代码,为后续章节添加 I/O 能力奠定了基础。

下一阶段,我们将为这个框架添加请求处理函数,使其能够真正处理读写请求,并实现数据传输。