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 图展示了这一完整流程:
实践要点: 在调用
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 观察设备状态变化,确保生命周期管理正确。