设备号管理:主设备号与次设备号

设备号,说白了就是内核用来识别字符设备的“身份证”。每个字符设备在内核里都有一个唯一的编号,这个编号由两部分组成:主设备号次设备号

主设备号用来标识设备对应的驱动程序。比如,系统中所有的串口设备可能共享同一个主设备号,因为它们都由同一个驱动来管理。次设备号则用来区分同一个驱动下的不同设备实例。举个例子,你插了两个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);
    // 其他清理工作...
}
⚠️ 别忘了释放
我曾经见过一个驱动,卸载时忘了释放设备号。结果下次加载时,内核说“这个号已经被占用了”,驱动死活加载不上。最后只能重启机器。嗯,这种低级错误,犯一次就记住了。

查看已分配的设备号

想知道系统里哪些设备号已经被占用了?两个方法:

  1. 查看/proc/devices:这个文件列出了所有已注册的字符设备和块设备的主设备号及名称。
  2. 查看/proc/device-tree:如果你用的是设备树,这里也能看到设备信息。

我个人习惯在调试时先cat /proc/devices看一眼,确认自己的设备号有没有冲突。

知识体系图

下面这张图帮你理清设备号管理的整体脉络:

设备号管理知识体系 设备号 (dev_t) 主设备号(12位) + 次设备号(20位) 设备号分配方式 静态分配 register_chrdev_region() 动态分配 alloc_chrdev_region() ⚠ 需手动指定主设备号,可能冲突 ✅ 内核自动分配,推荐使用 释放设备号:unregister_chrdev_region()

总结

设备号管理其实就三件事:分配、使用、释放。分配有两种方式,我建议你无脑选动态分配。释放千万别忘,否则下次加载驱动时会哭的。

嗯,这里再啰嗦一句:设备号只是字符设备驱动的第一步。你拿到了设备号,还得把它跟具体的文件操作函数绑定起来,才能让用户空间的程序真正用上你的设备。这部分内容,我们后面会详细讲。

📌 核心要点
  • 设备号 = 主设备号(12位) + 次设备号(20位)
  • 静态分配:register_chrdev_region() —— 自己指定主设备号
  • 动态分配:alloc_chrdev_region() —— 内核帮你选主设备号
  • 释放:unregister_chrdev_region() —— 必须成对使用
  • 查看已分配设备号:cat /proc/devices

公众号:蓝海资料掘金营,微信deep3321