4、设备号管理:静态分配与动态分配、查看已用设备号、设备号冲突处理

设备号,说白了就是内核给每个驱动发的「身份证」。主设备号告诉系统「我是谁」,次设备号告诉系统「我管的是哪个设备」。这个身份证怎么发、发错了怎么办,就是今天要聊的核心。

我刚开始写驱动时,觉得设备号分配就是个小事。随便选个数字,注册就完事了。直到有一次,我的驱动跟系统里另一个模块撞了号,系统直接崩了。嗯,从那以后我再也不敢小看设备号管理了。

4.1 静态分配:老派但可靠

静态分配,就是你在代码里写死一个设备号。比如:

#define MY_MAJOR 240
#define MY_MINOR 0

static int __init my_driver_init(void)
{
    int ret;
    dev_t dev = MKDEV(MY_MAJOR, MY_MINOR);
    
    ret = register_chrdev_region(dev, 1, "my_device");
    if (ret < 0) {
        pr_err("无法注册设备号 %d:%d\n", MY_MAJOR, MY_MINOR);
        return ret;
    }
    return 0;
}

这种做法有什么好处?简单、可控。你明确知道自己的设备号是多少,用户空间写mknod时也方便。我在一些嵌入式项目里经常这么干,因为硬件平台固定,设备号冲突的概率极低。

⚠️ 注意:静态分配最大的坑就是「你以为没人用这个号」。我曾经在一个项目里用了240,结果发现系统里某个隐藏模块也用了240。查了两天才找到原因。所以,如果你要用静态分配,先去查一下 /proc/devices,确认这个号是空闲的。

4.2 动态分配:现代驱动的首选

动态分配,就是让内核帮你挑一个空闲的设备号。代码更简洁:

static int __init my_driver_init(void)
{
    dev_t dev;
    int ret;
    
    ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
    if (ret < 0) {
        pr_err("动态分配设备号失败\n");
        return ret;
    }
    
    pr_info("获得设备号: major=%d, minor=%d\n", 
            MAJOR(dev), MINOR(dev));
    return 0;
}

我个人习惯用动态分配。为什么?省心。你不用去记哪个号被占了,内核帮你管。而且现在udev会自动处理设备节点,用户根本不需要知道设备号是多少。

你想想看,如果你的驱动要支持多个设备实例,动态分配更是方便。每次insmod时内核给你一个新的主设备号,互不干扰。

💡 小技巧:动态分配后,记得把设备号保存下来。我一般会在 /sys 或 debugfs 里暴露出来,方便调试时查看。

4.3 查看已用设备号:知己知彼

不管你是静态还是动态,总得知道系统里已经用了哪些设备号。常用的方法就两个:

方法一:cat /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
  7 vcs
 10 misc
 13 input
 29 fb
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 ttySC
207 ttyMAX
252 my_device
253 device-mapper
254 rtc

Block devices:
  7 loop
  8 sd
  9 md
 11 sr
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
252 device-mapper
253 virtblk
254 mdp

看到没?252就是我的驱动占的。如果这里没出现,说明注册失败了。

方法二:ls -l /dev

这个命令能看到设备节点的主次设备号:

$ ls -l /dev/my_device
crw------- 1 root root 252, 0 Jan 15 10:30 /dev/my_device

c表示字符设备,252是主设备号,0是次设备号。

🔍 排查经验:我曾经遇到一个诡异的问题——驱动加载成功了,/proc/devices里也有,但用户程序就是打不开设备。后来一查,发现/dev下的设备节点主设备号是251,而我驱动注册的是252。这就是典型的设备节点和设备号不匹配。所以,两个命令配合着看,能快速定位问题。

4.4 设备号冲突处理:别慌,有套路

设备号冲突,说白了就是两个驱动想用同一个主设备号。内核会直接拒绝第二个注册的驱动,返回 -EBUSY。

遇到这种情况怎么办?我总结了几个套路:

套路一:改用动态分配

这是最省事的办法。把 register_chrdev_region 换成 alloc_chrdev_region,让内核帮你选。冲突概率几乎为零。

套路二:检查并调整静态号

如果你必须用固定号,那就先查 /proc/devices,找一个确实没人用的号。我一般会选 240-254 之间的号,这些是留给本地实验用的。

套路三:使用次设备号范围

有时候冲突的不是主设备号,而是次设备号范围。比如两个驱动共享同一个主设备号,但管理不同的次设备号范围。这种情况在misc设备里很常见。

// 注册次设备号 0-15 给 my_device_a
register_chrdev_region(MKDEV(252, 0), 16, "my_device_a");

// 注册次设备号 16-31 给 my_device_b
register_chrdev_region(MKDEV(252, 16), 16, "my_device_b");

但说实话,这种做法容易搞混。我个人不太推荐,除非你很清楚自己在做什么。

⚠️ 避坑指南:我曾经在一个项目里,两个驱动模块用了同一个主设备号的不同次设备号范围。结果有一天,一个模块升级了,次设备号范围变了,直接覆盖了另一个模块的设备号。查了整整一天才找到原因。从那以后,我坚持每个独立驱动用独立的主设备号。

4.5 总结一下

设备号管理,说白了就三件事:

  • 静态分配:适合固定平台,但记得先查 /proc/devices
  • 动态分配:现代驱动的标准做法,省心省力
  • 冲突处理:优先用动态分配,实在不行就换号

我个人建议,除非你有非常特殊的需求(比如要兼容旧版用户空间程序),否则一律用动态分配。内核社区也是这个趋势,很多新加入的API都只支持动态分配了。

嗯,设备号这块就聊到这儿。下节课我们聊聊file_operations结构体,那是驱动和用户空间打交道的核心接口。