4. 设备号注册与注销:核心函数详解

在 Linux 字符设备驱动中,设备号是内核识别设备的关键标识。驱动加载时必须向内核申请一个或多个设备号,卸载时必须归还。本章深入剖析设备号注册与注销的三个核心函数:alloc_chrdev_region、register_chrdev_region 和 unregister_chrdev_region,涵盖其原型、参数、返回值、使用场景及典型错误处理。

4.1 设备号基础回顾

设备号由主设备号(major)和次设备号(minor)组成,类型为 dev_t(32 位无符号整数)。主设备号标识设备驱动,次设备号标识同一驱动下的不同设备实例。

概念 说明 示例
主设备号 对应驱动程序,由内核分配或静态指定 8 (sd 块设备)
次设备号 区分同一驱动下的不同设备 0, 1, 2, ...
dev_t 内核中设备号的统一类型 MKDEV(250, 0)
MKDEV(major, minor) 将主次号组合为 dev_t MKDEV(250, 3)
MAJOR(dev) 从 dev_t 提取主设备号 MAJOR(dev) = 250
MINOR(dev) 从 dev_t 提取次设备号 MINOR(dev) = 3
提示: 在 2.6 内核之后,dev_t 扩展为 32 位,其中 12 位用于主设备号(0~4095),20 位用于次设备号(0~1048575)。驱动开发中应始终使用 MAJOR/MINOR 宏操作设备号,避免直接位运算。

4.2 动态分配:alloc_chrdev_region

alloc_chrdev_region 是推荐使用的动态设备号分配函数,由内核自动选择一个未使用的主设备号,避免与现有驱动冲突。

4.2.1 函数原型

#include <linux/fs.h>
#include <linux/kdev_t.h>

int alloc_chrdev_region(dev_t *dev, unsigned int baseminor,
                        unsigned int count, const char *name);

4.2.2 参数详解

参数 类型 说明
dev dev_t * 输出参数,返回分配到的第一个设备号(主设备号 + 起始次设备号)
baseminor unsigned int 起始次设备号,通常为 0
count unsigned int 需要连续分配的次设备号数量
name const char * 设备名称,会在 /proc/devices 中显示

4.2.3 返回值

  • 0:分配成功,dev 指向有效的设备号。
  • 负数:错误码,常见为 -ENOMEM(内存不足)或 -EINVAL(参数无效)。

4.2.4 典型用法

dev_t dev_num;
int ret;

ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_dev");
if (ret < 0) {
    pr_err("Failed to allocate chrdev region: %d\n", ret);
    return ret;
}

pr_info("Allocated device number: major=%d, minor=%d\n",
        MAJOR(dev_num), MINOR(dev_num));
注意: 动态分配的主设备号每次加载驱动时可能不同,因此设备节点(/dev/xxx)需要在驱动加载后动态创建(使用 udev/mdev)或通过 mknod 手动创建。生产环境中强烈建议配合设备模型自动创建设备节点。

4.3 静态指定:register_chrdev_region

当驱动需要固定主设备号(如 legacy 设备或硬件规范要求)时,使用 register_chrdev_region 注册已知的设备号范围。

4.3.1 函数原型

int register_chrdev_region(dev_t from, unsigned int count, const char *name);

4.3.2 参数详解

参数 类型 说明
from dev_t 要注册的起始设备号(包含主设备号和起始次设备号)
count unsigned int 连续设备号的数量
name const char * 设备名称

4.3.3 返回值

  • 0:注册成功。
  • 负数:错误码,常见为 -EBUSY(设备号已被占用)。

4.3.4 典型用法

#define MY_MAJOR 250
#define MY_MINOR 0
#define DEVICE_COUNT 4

dev_t dev_num = MKDEV(MY_MAJOR, MY_MINOR);
int ret;

ret = register_chrdev_region(dev_num, DEVICE_COUNT, "my_static_dev");
if (ret < 0) {
    pr_err("Failed to register chrdev region at major %d: %d\n",
           MY_MAJOR, ret);
    return ret;
}

pr_info("Registered device numbers: %d:%d - %d:%d\n",
        MY_MAJOR, MY_MINOR,
        MY_MAJOR, MY_MINOR + DEVICE_COUNT - 1);
最佳实践: 除非有明确的硬件兼容性要求,否则优先使用 alloc_chrdev_region 动态分配。静态主设备号容易与社区驱动冲突,且不利于设备树和自动设备节点管理。

4.4 设备号分配流程对比

以下 SVG 图展示了两种分配方式的决策流程与内核交互路径。

设备号分配流程对比图 固定主设备号? 是 register_chrdev_region 否 alloc_chrdev_region 检查 -EBUSY 检查 -ENOMEM 设备号分配成功 返回错误码 动态分配:主设备号由内核自动选择,避免冲突;静态分配:主设备号固定,需确保未被占用

4.5 设备号注销:unregister_chrdev_region

驱动卸载时必须释放已注册的设备号,否则后续驱动无法复用该设备号范围。

4.5.1 函数原型

void unregister_chrdev_region(dev_t from, unsigned int count);

4.5.2 参数说明

参数 说明
from 要注销的起始设备号,必须与注册时一致
count 要注销的设备号数量,必须与注册时一致

4.5.3 典型用法

static dev_t my_dev_num;

static int __init my_init(void)
{
    int ret;
    ret = alloc_chrdev_region(&my_dev_num, 0, 1, "mydev");
    if (ret < 0)
        return ret;
    /* ... 其他初始化 ... */
    return 0;
}

static void __exit my_exit(void)
{
    unregister_chrdev_region(my_dev_num, 1);
    /* ... 其他清理 ... */
}

module_init(my_init);
module_exit(my_exit);
关键规则: unregister_chrdev_region 必须在所有设备文件被关闭、所有内核引用被释放之后调用。如果在设备仍在使用时注销设备号,可能导致内核 panic 或文件系统操作崩溃。建议在驱动退出函数的最后一步执行注销操作。

4.6 完整设备号生命周期

以下 SVG 图展示了从驱动加载到卸载的完整设备号生命周期,包含错误处理路径。

设备号生命周期状态机 未注册 alloc/register 已注册 cdev_add 设备激活 cdev_del 设备停用 unregister 已注销 失败 错误处理:释放资源

4.7 常见错误与避坑指南

错误场景 后果 解决方案
忘记注销设备号 驱动卸载后设备号仍被占用,其他驱动无法使用 在 __exit 函数中确保调用 unregister_chrdev_region
注销时 count 不匹配 部分设备号残留,内核资源泄漏 使用全局变量保存注册时的 dev_t 和 count
设备文件打开时卸载驱动 操作设备文件导致内核崩溃 在 cdev_del 前确保所有文件已关闭,或使用引用计数
静态注册时主设备号冲突 register_chrdev_region 返回 -EBUSY 改用 alloc_chrdev_region 动态分配,或检查 /proc/devices
未检查返回值直接使用 设备号无效,后续 cdev_init/cdev_add 失败 始终检查返回值,失败时回滚已分配的资源
核心原则: 设备号注册与注销必须严格配对,且遵循“先注册后使用、先停用后注销”的顺序。在复杂的驱动中,建议将设备号管理封装为独立的初始化/反初始化函数,提高代码可维护性。

4.8 本章小结

  • alloc_chrdev_region:动态分配,主设备号由内核自动选择,推荐用于新驱动。
  • register_chrdev_region:静态注册,适用于需要固定主设备号的场景。
  • unregister_chrdev_region:注销设备号,参数必须与注册时完全一致。
  • 设备号生命周期:未注册 → 已注册 → 设备激活 → 设备停用 → 已注销。
  • 错误处理:每次分配后检查返回值,失败时回滚所有已获取的资源。