4. 块设备驱动注册与注销:alloc_disk、add_disk、del_gendisk、put_disk接口使用与实战

各位同学,今天我们来聊聊块设备驱动里最核心的四个接口——alloc_diskadd_diskdel_gendiskput_disk。说白了,这就是块设备从“出生”到“入土”的全生命周期管理。我当年刚接触这部分时,总觉得不就是分配、添加、删除、释放嘛,有啥难的?结果第一次写驱动就踩了坑,嗯,今天我把这些经验都掏出来给你们。

4.1 块设备的一生:从分配到销毁

先看一张图,把整个流程串起来。我个人习惯,学任何接口前先看生命周期图,这样代码才不会写乱。

块设备生命周期流程图 alloc_disk 设置 disk->major, fops, queue 等 add_disk del_gendisk → put_disk ① 分配 ② 初始化 ③ 注册 ④ 注销

你看,整个流程非常清晰。先分配,再设置参数,然后注册,最后注销释放。每一步都有对应的接口,少一个都不行。

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_gendiskput_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->majordisk->first_minor。然后去 /proc/devices/dev 下核对,确保注册成功。另外,dmesg 是你的好朋友,出问题先看它。

好了,这一章的内容就到这里。四个接口,一个生命周期,加上一个完整示例,够你写一个基础的块设备驱动了。记住我踩过的坑,别在同样的地方摔倒。


公众号:蓝海资料掘金营,微信deep3321