4. 块设备驱动注册与注销:alloc_disk、add_disk、del_gendisk、put_disk接口使用与实战
各位同学,今天我们来聊聊块设备驱动里最核心的四个接口——alloc_disk、add_disk、del_gendisk、put_disk。说白了,这就是块设备从“出生”到“入土”的全生命周期管理。我当年刚接触这部分时,总觉得不就是分配、添加、删除、释放嘛,有啥难的?结果第一次写驱动就踩了坑,嗯,今天我把这些经验都掏出来给你们。
4.1 块设备的一生:从分配到销毁
先看一张图,把整个流程串起来。我个人习惯,学任何接口前先看生命周期图,这样代码才不会写乱。
你看,整个流程非常清晰。先分配,再设置参数,然后注册,最后注销释放。每一步都有对应的接口,少一个都不行。
4.2 alloc_disk:给块设备“办出生证”
alloc_disk 这个函数,说白了就是向内核申请一个 gendisk 结构体。你想想看,块设备在内核里用什么表示?就是 struct gendisk。没有它,你的设备就是个黑户。
struct gendisk *alloc_disk(int minors);
参数 minors 表示这个磁盘设备支持多少个分区。比如你传 16,那就支持 16 个分区(0~15)。我建议你传 1 或者 16,别传太奇怪的值。为什么?因为内核会为每个分区预分配资源,传多了浪费,传少了不够用。
我的经验: 我在项目中遇到过传 0 的情况,结果内核直接 panic 了。所以 minors 至少传 1,哪怕你不需要分区。
4.3 设置 gendisk 参数:别让设备“裸奔”
分配完 gendisk 后,你得给它设置几个关键字段。不然内核都不知道你这设备是干啥的。
struct gendisk *disk;
disk = alloc_disk(16);
if (!disk)
return -ENOMEM;
/* 设置主设备号 */
disk->major = MAJOR_NUM;
/* 设置第一个次设备号 */
disk->first_minor = 0;
/* 设置设备名,比如 /dev/myblock */
disk->disk_name = "myblock";
/* 设置操作函数集 */
disk->fops = &myblock_ops;
/* 关联请求队列 */
disk->queue = myblock_queue;
/* 设置私有数据,方便在回调中获取 */
disk->private_data = mydev;
这里有个坑,我当年犯过——忘记设置 queue。结果 add_disk 一调用,系统直接 oops。你想想看,块设备没有请求队列,内核怎么跟你交互数据?所以 queue 必须提前创建好,并且赋值给 disk->queue。
4.4 add_disk:让设备“上线营业”
add_disk 是真正把设备注册到内核的函数。调用之后,/dev 下就会出现你的设备节点,用户空间就能访问了。
void add_disk(struct gendisk *disk);
这个函数一调用,内核会做几件事:
- 创建设备节点(比如
/dev/myblock) - 将
gendisk加入全局链表 - 触发
uevent,通知用户空间 - 调用你的
open回调(如果有)
注意: add_disk 调用之后,你的设备就“活”了。用户随时可能发起读写请求。所以在这之前,一定要确保所有资源都准备好了。我曾经在 add_disk 之后才初始化互斥锁,结果并发访问直接死锁。
4.5 del_gendisk 与 put_disk:优雅地“退休”
设备要卸载时,得先 del_gendisk,再 put_disk。顺序不能乱,就像你关门之前得先让客人出去一样。
void del_gendisk(struct gendisk *disk);
void put_disk(struct gendisk *disk);
del_gendisk 的作用:
- 删除
/dev下的设备节点 - 从全局链表中移除
- 拒绝新的 I/O 请求
- 等待正在进行的 I/O 完成
put_disk 的作用:
- 释放
gendisk结构体占用的内存 - 减少引用计数
避坑指南: 我曾经只调了 del_gendisk 忘了调 put_disk,结果每次卸载模块,内存泄漏 4KB。虽然不大,但次数多了系统就扛不住了。所以记住:del_gendisk 和 put_disk 是成对出现的,缺一不可。
4.6 完整实战代码:一个简单的块设备驱动
下面我把整个流程串起来,写一个最简单的块设备驱动框架。你直接拿去改改就能用。
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
static struct myblock_dev {
spinlock_t lock;
struct request_queue *queue;
struct gendisk *disk;
} mydev;
static struct block_device_operations myblock_ops = {
.owner = THIS_MODULE,
.open = myblock_open,
.release = myblock_release,
};
static int __init myblock_init(void)
{
/* 1. 分配 gendisk */
mydev.disk = alloc_disk(16);
if (!mydev.disk)
return -ENOMEM;
/* 2. 创建请求队列 */
mydev.queue = blk_init_queue(myblock_request, &mydev.lock);
if (!mydev.queue) {
put_disk(mydev.disk);
return -ENOMEM;
}
/* 3. 设置 gendisk 参数 */
mydev.disk->major = register_blkdev(0, "myblock");
mydev.disk->first_minor = 0;
mydev.disk->fops = &myblock_ops;
mydev.disk->queue = mydev.queue;
mydev.disk->private_data = &mydev;
snprintf(mydev.disk->disk_name, 32, "myblock");
/* 4. 注册设备 */
add_disk(mydev.disk);
printk(KERN_INFO "myblock: device registered\n");
return 0;
}
static void __exit myblock_exit(void)
{
/* 5. 注销设备 */
del_gendisk(mydev.disk);
blk_cleanup_queue(mydev.queue);
put_disk(mydev.disk);
unregister_blkdev(mydev.disk->major, "myblock");
printk(KERN_INFO "myblock: device unregistered\n");
}
module_init(myblock_init);
module_exit(myblock_exit);
MODULE_LICENSE("GPL");
4.7 常见错误与调试技巧
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| insmod 后 /dev 下无节点 | 忘记调用 add_disk | 检查 init 函数中是否调用了 add_disk |
| 读写操作导致 oops | queue 未设置或设置错误 | 确认 disk->queue 已正确赋值 |
| rmmod 后内存泄漏 | 忘记调用 put_disk | 检查 exit 函数中 del_gendisk 后是否有 put_disk |
| 设备号冲突 | register_blkdev 传参错误 | 使用动态分配(传0),或检查 /proc/devices |
调试小技巧: 我习惯在 add_disk 前后加 printk,打印出 disk->major 和 disk->first_minor。然后去 /proc/devices 和 /dev 下核对,确保注册成功。另外,dmesg 是你的好朋友,出问题先看它。
好了,这一章的内容就到这里。四个接口,一个生命周期,加上一个完整示例,够你写一个基础的块设备驱动了。记住我踩过的坑,别在同样的地方摔倒。
公众号:蓝海资料掘金营,微信deep3321