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 图展示了两种分配方式的决策流程与内核交互路径。
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 图展示了从驱动加载到卸载的完整设备号生命周期,包含错误处理路径。
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:注销设备号,参数必须与注册时完全一致。
- 设备号生命周期:未注册 → 已注册 → 设备激活 → 设备停用 → 已注销。
- 错误处理:每次分配后检查返回值,失败时回滚所有已获取的资源。