4、设备号管理:主设备号与次设备号、静态申请与动态分配、查看已注册设备号
设备号,说白了就是内核给每个字符设备发的「身份证」。没有这个号,应用程序根本找不到你的设备。我刚开始写驱动时,觉得设备号就是个数字嘛,随便填一个就行。结果呢?跟别人冲突了,设备死活打不开。嗯,从那以后我再也不敢小看设备号管理了。
4.1 主设备号与次设备号:一对搭档
Linux 用 dev_t 类型来表示设备号。这个类型其实是个 32 位的整数,高 12 位是主设备号,低 20 位是次设备号。
- 主设备号:标识设备对应的驱动程序。说白了,就是告诉内核「这个设备归哪个驱动管」。
- 次设备号:标识同一个驱动下的不同设备实例。比如你插了两个 USB 转串口,主设备号相同,次设备号不同。
我见过不少新手,以为次设备号没用,随便填 0。其实不然。我记得有个项目,需要在同一个驱动里管理 8 个传感器,每个传感器对应一个次设备号。这样应用程序打开 /dev/sensor0 到 /dev/sensor7,驱动根据次设备号就知道操作哪个硬件。
核心数据结构:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
// 主设备号:高12位
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
// 次设备号:低20位
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
// 合成设备号
#define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi))
你想想看,主设备号一共就 12 位,也就是 0~4095。其中 0 和 4095 是保留的,实际可用的主设备号范围是 1~4094。内核社区有个文档叫 Documentation/admin-guide/devices.txt,里面记录了哪些主设备号已经被分配了。
4.2 静态申请:老派但可控
静态申请,就是你自己指定一个主设备号,然后告诉内核:「我要用这个号,你别给别人。」
函数原型很简单:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
- from:起始设备号,用 MKDEV(主, 次) 生成
- count:要申请的连续设备号个数
- name:设备名称,会在 /proc/devices 里显示
举个例子:
dev_t dev_num;
int ret;
dev_num = MKDEV(240, 0); // 主设备号240,起始次设备号0
ret = register_chrdev_region(dev_num, 4, "my_driver");
if (ret < 0) {
printk("设备号申请失败!\n");
return ret;
}
// 成功申请了 240,0 ~ 240,3 四个设备号
注意:静态申请最大的坑就是「撞号」。我曾经在一个项目里用了 240 这个主设备号,结果客户的内核里有个第三方驱动也用了 240。加载模块时直接报错,查了半天才发现是设备号冲突。所以,除非你确定这个号没人用,否则别用静态申请。
什么时候适合静态申请?我个人习惯是在以下场景用:
- 设备号已经在内核社区注册了(比如你写的是标准驱动)
- 嵌入式设备,你知道整个系统里没有其他驱动用这个号
- 教学或实验环境,图省事
4.3 动态分配:现代驱动的首选
动态分配,就是让内核帮你挑一个空闲的主设备号。你只管说「我要几个设备号」,内核给你分配好了告诉你。
函数原型:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
- dev:输出参数,分配到的起始设备号
- baseminor:起始次设备号,一般填 0
- count:要申请的个数
- name:设备名称
代码示例:
dev_t dev_num;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 4, "my_driver");
if (ret < 0) {
printk("动态分配设备号失败\n");
return ret;
}
printk("分配到的设备号:主=%d, 次=%d\n",
MAJOR(dev_num), MINOR(dev_num));
我的建议:现在写新驱动,99% 的情况都用动态分配。内核会从 234 开始往上分配主设备号,基本不会冲突。而且用户空间可以通过 /sys/class/... 或者 udev 规则自动创建设备节点,根本不需要关心主设备号是多少。
动态分配的好处很明显:
- 避免冲突,内核帮你管理
- 代码可移植性好,换个内核也不用改
- 配合 udev/mdev 自动创建设备节点,用户体验好
4.4 查看已注册设备号
怎么知道当前系统里哪些设备号已经被占用了?两个地方可以看:
1. /proc/devices
这个文件列出了所有已注册的字符设备和块设备的主设备号:
$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
10 misc
13 input
29 fb
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
...
Block devices:
8 sd
9 md
11 sr
65 sd
66 sd
...
2. /sys/class/... 下的设备信息
如果你知道设备属于哪个 class,可以看 sysfs:
$ ls -l /sys/class/misc/device_name/dev
crw-rw-rw- 1 root root 10, 50 12月 12 10:00 /sys/class/misc/device_name/dev
这里 10 是主设备号,50 是次设备号。
调试小技巧:我经常在驱动初始化时加一行打印:
printk("设备号注册成功:%d:%d\n", MAJOR(dev_num), MINOR(dev_num));
然后用 dmesg | tail 查看。这样不用去翻 /proc/devices,直接就知道分配到了哪个号。
4.5 释放设备号
有申请就有释放。模块卸载时,一定要把设备号还回去:
void unregister_chrdev_region(dev_t from, unsigned count);
示例:
static int __init my_init(void)
{
// 动态分配
alloc_chrdev_region(&dev_num, 0, 1, "my_dev");
// ... 其他初始化
return 0;
}
static void __exit my_exit(void)
{
// 释放设备号
unregister_chrdev_region(dev_num, 1);
// ... 其他清理
}
module_init(my_init);
module_exit(my_exit);
我曾经踩过的坑:有一次写驱动,忘了释放设备号。模块卸载后重新加载,发现设备号被占用了,alloc_chrdev_region 返回 -EBUSY。查了半天才发现是上次卸载时没调用 unregister_chrdev_region。所以,一定要在 exit 函数里释放设备号,这是基本功。
4.6 知识体系图
下面这张图总结了设备号管理的核心流程:
4.7 总结一下
设备号管理其实就三件事:
- 理解主次设备号:主设备号找驱动,次设备号找设备实例
- 选择申请方式:新驱动用动态分配,特殊场景用静态
- 记得释放:有借有还,再借不难
我个人习惯是:所有新写的驱动都用 alloc_chrdev_region 动态分配。只有在给 legacy 硬件写驱动,或者需要固定设备号做兼容时,才用静态申请。你想想看,内核社区那么多驱动,动态分配能避免 99% 的冲突问题,何乐而不为呢?
最后一个小建议:写驱动时,把设备号相关的操作封装成函数。比如 mydev_alloc_chrdev() 和 mydev_free_chrdev()。这样代码清晰,也方便复用。我早期写的驱动都是散落在 init 和 exit 函数里,后来维护起来特别痛苦。封装一下,清爽多了。
可可精品资料库