3. 设备号管理:主设备号与次设备号
在 Linux 字符设备驱动中,设备号是内核识别和访问设备的唯一标识。每个字符设备在系统中都对应一个 dev_t 类型的设备号,该类型是一个 32 位无符号整数,其中高 12 位表示主设备号,低 20 位表示次设备号。
核心概念:主设备号用于驱动与设备的绑定,次设备号用于区分同一驱动下的不同设备实例。
3.1 主设备号与次设备号
| 属性 | 主设备号 (Major) | 次设备号 (Minor) |
|---|---|---|
| 位数 | 12 位 (0~4095) | 20 位 (0~1048575) |
| 作用 | 标识设备对应的驱动程序 | 标识同一驱动下的具体设备实例 |
| 分配方式 | 静态分配或动态分配 | 通常由驱动开发者规划 |
| 内核查找 | 通过主设备号找到 cdev_map |
传递给驱动 open 方法区分设备 |
内核通过 MAJOR(dev_t) 和 MINOR(dev_t) 宏从设备号中提取主、次设备号,使用 MKDEV(major, minor) 宏将两者合并为 dev_t。
#include <linux/kdev_t.h>
dev_t dev = MKDEV(250, 0); // 主设备号 250,次设备号 0
int major = MAJOR(dev); // 返回 250
int minor = MINOR(dev); // 返回 0
提示:主设备号 0 和 1 通常保留给系统使用,2~255 可用于静态分配,256 以上常用于动态分配。
3.2 设备号分配策略对比
内核提供两种设备号分配方式:静态分配和动态分配。开发者需要根据项目场景选择合适的策略。
| 对比维度 | 静态分配 | 动态分配 |
|---|---|---|
| 函数 | register_chrdev_region() |
alloc_chrdev_region() |
| 主设备号来源 | 开发者手动指定 | 内核自动分配 |
| 优点 | 设备号固定,用户空间可预知 | 避免主设备号冲突 |
| 缺点 | 可能与其他驱动冲突 | 设备号不固定,需动态创建设备节点 |
| 适用场景 | 标准设备、嵌入式固定硬件 | 通用驱动、开源模块 |
3.3 静态分配:register_chrdev_region
静态分配要求开发者预先知道可用的主设备号,适用于嵌入式平台中硬件设备固定的场景。
#include <linux/fs.h>
#include <linux/cdev.h>
#define MY_MAJOR 250
#define MY_MINOR 0
#define MY_COUNT 4 // 分配 4 个次设备号
dev_t devno;
int ret;
devno = MKDEV(MY_MAJOR, MY_MINOR);
ret = register_chrdev_region(devno, MY_COUNT, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to register chrdev region\n");
return ret;
}
注意:静态分配前应检查
/proc/devices 确认主设备号未被占用。若冲突,驱动加载将失败。
3.4 动态分配:alloc_chrdev_region
动态分配由内核自动选择一个未使用的主设备号,避免冲突,是推荐的做法,尤其适用于可加载模块。
dev_t devno;
int ret;
// 动态分配:次设备号从 0 开始,连续 4 个
ret = alloc_chrdev_region(&devno, 0, 4, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to alloc chrdev region\n");
return ret;
}
printk(KERN_INFO "Allocated major=%d minor=%d\n",
MAJOR(devno), MINOR(devno));
动态分配后,驱动应通过 MAJOR(devno) 获取分配的主设备号,并据此在 /dev 下创建设备节点(通常配合 udev 或 mdev 自动完成)。
3.5 设备号释放
无论采用哪种分配方式,在驱动卸载时都必须释放设备号,否则会导致资源泄漏。
void cleanup_module(void)
{
// 释放设备号区域
unregister_chrdev_region(devno, 4);
}
3.6 两种分配方式完整对比表
| 操作 | 静态分配 | 动态分配 |
|---|---|---|
| 分配函数 | register_chrdev_region(devno, count, name) |
alloc_chrdev_region(&devno, minor, count, name) |
| 主设备号 | 手动指定 | 内核自动分配 |
| 返回值 | 0 成功,负值失败 | 0 成功,负值失败 |
| 释放函数 | unregister_chrdev_region(devno, count) |
|
| 设备节点创建 | 可手动 mknod 或 udev 规则 |
依赖 udev 自动创建 |
| 推荐度 | 低(仅特殊场景) | 高(通用驱动首选) |
3.7 实践建议与常见错误
- 优先使用动态分配:除非有明确的硬件平台约束,否则始终使用
alloc_chrdev_region。 - 检查返回值:设备号分配可能因资源耗尽而失败,务必检查返回值。
- 匹配分配与释放:
unregister_chrdev_region的count参数必须与分配时一致。 - 避免主设备号硬编码:动态分配后通过
MAJOR(devno)获取,不要写死。 - 设备节点自动创建:配合
udev规则或class_create+device_create自动生成节点。
最佳实践:在
module_init 中先分配设备号,再注册 cdev;在 module_exit 中先删除 cdev,再释放设备号。顺序不可颠倒。