3. 块设备驱动注册与注销:核心 API 详解

块设备驱动的生命周期管理是驱动开发的基础。本章聚焦于四个核心 API:alloc_disk、add_disk、del_gendisk 和 put_disk。理解它们的职责、调用顺序以及资源管理规则,是构建稳定、高性能块设备驱动的第一步。

3.1 核心 API 功能概览

这四个 API 分别负责块设备对象的创建、注册上线、注销下线以及最后的资源释放。下表总结了它们的关键特征:

API 函数 核心功能 调用时机 关键参数 / 返回值
alloc_disk 分配并初始化 struct gendisk 对象 驱动初始化早期,设置完请求队列之后 int minors:分区数量;返回 struct gendisk *
add_disk 将 gendisk 注册到内核块设备层,使其可被用户空间访问 设备硬件就绪、所有属性设置完成后 struct gendisk *disk;无返回值(void)
del_gendisk 从系统中移除 gendisk,停止 I/O 请求分发 设备卸载或热插拔移除时 struct gendisk *disk;无返回值(void)
put_disk 释放 gendisk 对象的引用计数,最终销毁对象 del_gendisk 之后,确保无引用时 struct gendisk *disk;无返回值(void)
关键原则: alloc_disk 与 put_disk 必须成对出现,否则会导致内存泄漏或内核崩溃。add_disk 与 del_gendisk 也必须配对使用。

3.2 生命周期与状态转换

一个 gendisk 对象从创建到销毁,经历四个明确的状态。下面的 SVG 图展示了这一完整流程:

块设备 gendisk 生命周期状态机 alloc_disk 已分配 设置属性 队列/容量/ops add_disk 已注册 del_gendisk 已注销 put_disk 已释放 状态转换必须严格遵循:分配 → 配置 → 注册 → 注销 → 释放
实践要点: 在调用 add_disk 之后,设备立即可被用户空间发现并打开。因此,所有与硬件相关的初始化(如中断注册、DMA 缓冲池准备)必须在 add_disk 之前完成。

3.3 典型驱动代码模板

以下代码片段展示了在驱动初始化与卸载函数中,如何正确使用这四个 API:

// 驱动初始化函数
static int __init my_block_driver_init(void)
{
    struct gendisk *disk;
    struct request_queue *q;

    // 1. 分配请求队列
    q = blk_init_queue(my_request_fn, &my_lock);
    if (!q)
        return -ENOMEM;

    // 2. 分配 gendisk,支持 16 个分区
    disk = alloc_disk(16);
    if (!disk) {
        blk_cleanup_queue(q);
        return -ENOMEM;
    }

    // 3. 设置 gendisk 属性
    disk->major = MY_MAJOR;
    disk->first_minor = 0;
    disk->fops = &my_block_ops;
    disk->queue = q;
    sprintf(disk->disk_name, "myblkdev");

    // 4. 设置容量(以扇区为单位)
    set_capacity(disk, MY_SECTOR_COUNT);

    // 5. 注册块设备
    add_disk(disk);

    return 0;
}

// 驱动卸载函数
static void __exit my_block_driver_exit(void)
{
    struct gendisk *disk = my_gendisk; // 假设全局保存

    // 1. 注销块设备,停止 I/O
    del_gendisk(disk);

    // 2. 清理请求队列
    blk_cleanup_queue(disk->queue);

    // 3. 释放 gendisk 引用
    put_disk(disk);
}
常见错误: 在 add_disk 之后,内核可能立即调用你的 open 回调。如果此时硬件尚未就绪,会导致系统崩溃。务必在 add_disk 之前完成所有硬件初始化。

3.4 API 使用避坑指南

以下是开发过程中最容易出错的几个场景,以列表形式呈现:

  • 分区数量(minors)设置过大: alloc_disk 的 minors 参数决定了分区表的大小。设置过大会浪费内存,设置过小则无法创建足够的分区。通常设置为 16 或 64。
  • 忘记设置 disk->queue: 在调用 add_disk 之前,必须将分配好的请求队列指针赋值给 disk->queue,否则内核会访问空指针。
  • 重复调用 add_disk: 对同一个 gendisk 对象调用两次 add_disk 会导致内核 panic。每个 gendisk 只能注册一次。
  • 在 del_gendisk 之前调用 put_disk: 这会导致 gendisk 对象在注销前被释放,内核访问已释放的内存。正确的顺序是先 del_gendisk,再 put_disk。
  • 忽略 del_gendisk 后的同步: del_gendisk 返回后,可能仍有未完成的 I/O 请求。驱动需要确保所有请求都已处理完毕,才能安全释放相关资源。

3.5 资源管理流程对比

为了更清晰地展示正确与错误的资源管理路径,下表对比了两种流程:

步骤 正确流程 错误流程(常见问题)
1 alloc_disk → 分配对象 alloc_disk → 分配对象
2 设置属性(队列、容量、ops) 设置属性(队列、容量、ops)
3 add_disk → 注册设备 add_disk → 注册设备
4 设备正常工作 设备正常工作
5 del_gendisk → 注销设备 put_disk → 提前释放对象
6 put_disk → 释放对象 del_gendisk → 访问已释放内存
结果 ✔ 资源安全释放 ✘ 内核崩溃或内存泄漏
调试建议: 在开发和测试阶段,可以使用 printk 在 alloc_disk、add_disk、del_gendisk 和 put_disk 调用处打印日志,配合 /proc/diskstats 观察设备状态变化,确保生命周期管理正确。