4. 设备号注册与注销:动态分配与静态指定
字符设备驱动与内核交互的第一道门槛就是设备号。设备号是内核识别设备的唯一整数标识,由主设备号(Major)和次设备号(Minor)组成。主设备号对应驱动类型,次设备号对应同类型下的不同设备实例。内核提供了三种操作设备号的接口:动态分配、静态指定和注销。
4.1 设备号数据结构与宏
设备号在内核中使用 dev_t 类型(32位无符号整数)表示,其中高12位为主设备号,低20位为次设备号。内核提供了便捷的宏进行转换:
| 宏 / 函数 | 作用 | 示例 |
|---|---|---|
MKDEV(major, minor) |
将主、次设备号组合为 dev_t |
MKDEV(240, 0) |
MAJOR(dev) |
从 dev_t 中提取主设备号 |
MAJOR(dev) |
MINOR(dev) |
从 dev_t 中提取次设备号 |
MINOR(dev) |
提示:主设备号范围 0~4095(12位),其中 0~255 为静态预留,256~4095 可供动态分配。次设备号范围 0~1048575(20位)。
4.2 动态分配设备号:alloc_chrdev_region
动态分配是驱动开发中最推荐的方式。驱动无需关心主设备号是否冲突,内核会自动从空闲区域分配一个未使用的主设备号。函数原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned int baseminor,
unsigned int count, const char *name);
| 参数 | 说明 |
|---|---|
dev |
输出参数,返回分配到的设备号起始值 |
baseminor |
起始次设备号,通常为 0 |
count |
需要连续分配的次设备号数量 |
name |
设备名称,会出现在 /proc/devices 中 |
典型用法:
dev_t dev_num;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_dev");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Allocated major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
最佳实践:动态分配是内核社区推荐的做法。它避免了主设备号冲突,也方便驱动以模块形式加载/卸载。只有极少数需要固定主设备号的场景(如历史兼容、嵌入式平台固定映射)才使用静态指定。
4.3 静态指定设备号:register_chrdev_region
当驱动需要固定主设备号时(例如与硬件手册或用户空间约定一致),使用静态指定。函数原型:
int register_chrdev_region(dev_t from, unsigned int count,
const char *name);
| 参数 | 说明 |
|---|---|
from |
指定的起始设备号(需用 MKDEV 构造) |
count |
连续设备号数量 |
name |
设备名称 |
典型用法:
dev_t dev_num = MKDEV(240, 0); // 指定主设备号240,起始次设备号0
int ret;
ret = register_chrdev_region(dev_num, 2, "my_fixed_dev");
if (ret < 0) {
printk(KERN_ERR "Failed to register device number %d\n", ret);
return ret;
}
// 成功注册了 240,0 和 240,1 两个设备号
注意:静态指定前必须确认该主设备号未被占用。可以通过
cat /proc/devices 查看当前已注册的设备号。如果指定的主设备号已被使用,register_chrdev_region 会返回 -EBUSY。
4.4 注销设备号:unregister_chrdev_region
在驱动卸载时,必须释放之前申请的设备号,否则下次加载时可能分配失败。函数原型:
void unregister_chrdev_region(dev_t from, unsigned int count);
典型用法:
// 在模块退出函数中
unregister_chrdev_region(dev_num, 1); // 释放1个设备号
关键原则:注销时使用的
from 和 count 必须与注册时完全一致。如果注册时分配了3个连续设备号,注销时也必须指定 count=3,否则会导致资源泄漏或内核警告。
4.5 设备号生命周期流程
下图展示了从申请到注销的完整设备号生命周期:
4.6 三种接口对比总结
| 接口 | 适用场景 | 主设备号来源 | 冲突风险 | 推荐度 |
|---|---|---|---|---|
alloc_chrdev_region |
通用驱动、模块化驱动 | 内核自动分配 | 无 | ★★★★★ |
register_chrdev_region |
固定设备号、硬件平台约定 | 开发者指定 | 需手动检查 | ★★★☆☆ |
unregister_chrdev_region |
驱动卸载时必调 | — | 参数不匹配会泄漏 | 强制使用 |
4.7 完整示例:动态分配 + 注销
以下是一个完整的字符设备驱动模板,展示设备号管理的标准流程:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
static dev_t my_dev_num;
static struct class *my_class;
static struct device *my_device;
static int __init my_driver_init(void)
{
int ret;
// 1. 动态分配设备号:起始次设备号0,申请1个
ret = alloc_chrdev_region(&my_dev_num, 0, 1, "my_driver");
if (ret < 0) {
pr_err("alloc_chrdev_region failed: %d\n", ret);
return ret;
}
pr_info("Allocated major=%d minor=%d\n",
MAJOR(my_dev_num), MINOR(my_dev_num));
// 2. 创建设备类(用于自动生成设备节点)
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
unregister_chrdev_region(my_dev_num, 1);
return PTR_ERR(my_class);
}
// 3. 创建设备节点 /dev/my_device
my_device = device_create(my_class, NULL, my_dev_num,
NULL, "my_device");
if (IS_ERR(my_device)) {
class_destroy(my_class);
unregister_chrdev_region(my_dev_num, 1);
return PTR_ERR(my_device);
}
return 0;
}
static void __exit my_driver_exit(void)
{
// 逆序释放:先销毁设备节点,再销毁类,最后注销设备号
device_destroy(my_class, my_dev_num);
class_destroy(my_class);
unregister_chrdev_region(my_dev_num, 1);
pr_info("Driver unloaded, device number released\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Character device driver with dynamic dev number");
调试技巧:加载模块后执行
cat /proc/devices | grep my_driver 查看分配的主设备号;执行 ls -l /dev/my_device 查看设备节点的主次设备号是否匹配。
4.8 常见错误与避坑指南
- 忘记注销:驱动卸载时不调用
unregister_chrdev_region,下次加载时动态分配可能返回相同主设备号,但内核会报device number already registered警告。 - 注销参数不匹配:注册时 count=3,注销时 count=1,会导致剩余2个设备号泄漏,内核无法再分配给其他驱动。
- 静态指定冲突:使用
register_chrdev_region时未检查/proc/devices,导致-EBUSY错误。建议在模块参数中允许用户指定主设备号,并回退到动态分配。 - 主设备号溢出:手动指定主设备号大于 4095 时,
MKDEV会截断高20位,导致实际注册的设备号与预期不符。
内核版本差异:在 Linux 5.x 及以上内核中,
class_create 已不再接收 THIS_MODULE 参数(仅需设备名)。请根据实际内核版本调整代码。本示例基于 4.x/5.x 通用写法。