设备号管理:主设备号与次设备号
设备号,说白了就是内核用来识别字符设备的“身份证”。每个字符设备在内核里都有一个唯一的编号,这个编号由两部分组成:主设备号和次设备号。
主设备号用来标识设备对应的驱动程序。比如,系统中所有的串口设备可能共享同一个主设备号,因为它们都由同一个驱动来管理。次设备号则用来区分同一个驱动下的不同设备实例。举个例子,你插了两个USB转串口设备,它们的主设备号相同,但次设备号不同——一个可能是0,另一个是1。
我个人习惯把主设备号想象成“部门编号”,次设备号就是“员工工号”。部门决定了谁来处理你的请求,工号则决定了具体是哪个设备在响应。
设备号的内部表示
在内核中,设备号用dev_t类型来表示。这是一个32位的无符号整数,其中高12位是主设备号,低20位是次设备号。嗯,这里要注意:主设备号的范围是0到4095,次设备号的范围是0到1048575。
内核提供了几个宏来操作设备号:
#include <linux/types.h>
MAJOR(dev_t dev); // 从dev_t中提取主设备号
MINOR(dev_t dev); // 从dev_t中提取次设备号
MKDEV(int major, int minor); // 将主次设备号组合成dev_t
我在项目中遇到过不少新手直接拿dev_t当整数用,结果移植到不同架构时出了问题。记住,永远用这些宏来操作设备号,别自己搞位运算。
静态分配设备号
静态分配,就是你自己指定一个主设备号,然后告诉内核:“我就用这个号了,你给我注册上。” 内核会检查这个号是否已经被占用,如果空闲就分配给你。
使用的函数是register_chrdev_region:
#include <linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
first:你要分配的第一个设备号(主设备号和起始次设备号)count:连续设备号的数量name:设备名称(会在/proc/devices中显示)
举个例子:
dev_t dev = MKDEV(240, 0);
int ret = register_chrdev_region(dev, 4, "my_device");
if (ret < 0) {
printk(KERN_ERR "设备号注册失败\n");
return ret;
}
这段代码会尝试注册主设备号240,次设备号从0到3的4个设备。
我曾经在项目里直接用了240这个主设备号,结果发现跟系统里某个硬件驱动冲突了。调试了半天才发现问题。所以,静态分配最大的问题就是——你根本不知道哪个号是空闲的。除非你查了所有已注册的设备号,否则很容易撞车。
静态分配适合什么场景?我个人觉得,只有在你确定某个主设备号永远不会被其他驱动使用时,才考虑它。比如一些 legacy 设备或者硬件厂商指定的专用设备号。
动态分配设备号
动态分配就省心多了。你告诉内核:“给我几个设备号,具体用哪个号你看着办。” 内核会从空闲的设备号中挑一个给你。
使用的函数是alloc_chrdev_region:
#include <linux/fs.h>
int alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name);
dev:输出参数,分配到的设备号会写到这里baseminor:起始次设备号count:连续设备号的数量name:设备名称
看个例子:
dev_t dev;
int ret = alloc_chrdev_region(&dev, 0, 2, "my_device");
if (ret < 0) {
printk(KERN_ERR "动态分配设备号失败\n");
return ret;
}
printk(KERN_INFO "分配到的设备号: major=%d, minor=%d\n",
MAJOR(dev), MINOR(dev));
这段代码让内核分配2个连续的设备号,起始次设备号为0。分配成功后,dev里就存着第一个设备号。
我个人强烈推荐使用动态分配。你想想看,静态分配你得去查表、猜号码,搞不好还要跟其他驱动打架。动态分配多省心,内核帮你搞定一切。而且现在的主流做法都是动态分配,除非你有特殊理由。
设备号的释放
不管你是静态分配还是动态分配,在驱动卸载时都必须释放设备号。用的函数是unregister_chrdev_region:
void unregister_chrdev_region(dev_t first, unsigned int count);
参数跟注册时一样:起始设备号和数量。记得在模块退出函数里调用它:
static void __exit my_driver_exit(void)
{
unregister_chrdev_region(dev, 2);
// 其他清理工作...
}
我曾经见过一个驱动,卸载时忘了释放设备号。结果下次加载时,内核说“这个号已经被占用了”,驱动死活加载不上。最后只能重启机器。嗯,这种低级错误,犯一次就记住了。
查看已分配的设备号
想知道系统里哪些设备号已经被占用了?两个方法:
- 查看/proc/devices:这个文件列出了所有已注册的字符设备和块设备的主设备号及名称。
- 查看/proc/device-tree:如果你用的是设备树,这里也能看到设备信息。
我个人习惯在调试时先cat /proc/devices看一眼,确认自己的设备号有没有冲突。
知识体系图
下面这张图帮你理清设备号管理的整体脉络:
总结
设备号管理其实就三件事:分配、使用、释放。分配有两种方式,我建议你无脑选动态分配。释放千万别忘,否则下次加载驱动时会哭的。
嗯,这里再啰嗦一句:设备号只是字符设备驱动的第一步。你拿到了设备号,还得把它跟具体的文件操作函数绑定起来,才能让用户空间的程序真正用上你的设备。这部分内容,我们后面会详细讲。
- 设备号 = 主设备号(12位) + 次设备号(20位)
- 静态分配:register_chrdev_region() —— 自己指定主设备号
- 动态分配:alloc_chrdev_region() —— 内核帮你选主设备号
- 释放:unregister_chrdev_region() —— 必须成对使用
- 查看已分配设备号:cat /proc/devices
公众号:蓝海资料掘金营,微信deep3321