设备号管理:动态与静态分配的艺术

说到设备号管理,很多初学者会觉得这不过是个数字游戏。但我在车规项目里吃过亏,才明白这里面的门道有多深。

设备号,说白了就是内核给每个字符设备发的一个「身份证」。主设备号标识驱动类型,次设备号标识具体设备。你想想看,如果两个驱动抢同一个主设备号,系统会怎样?嗯,轻则设备注册失败,重则系统崩溃。

静态分配:register_chrdev_region

静态分配,就是你自己指定主设备号。内核提供了这个函数:

int register_chrdev_region(dev_t from, unsigned count, const char *name);

参数说明:

  • from:起始设备号(主设备号 + 次设备号)
  • count:连续设备号的数量
  • name:设备名称(会在 /proc/devices 中显示)

举个例子:

#define MY_MAJOR 200
#define MY_MINOR 0
#define DEVICE_COUNT 4

dev_t dev = MKDEV(MY_MAJOR, MY_MINOR);
ret = register_chrdev_region(dev, DEVICE_COUNT, "my_device");
if (ret < 0) {
    printk("设备号注册失败\n");
    return ret;
}
⚠️ 我曾经踩过的坑:静态分配的主设备号不能与系统中已有的冲突。我在一个项目里用了 200 号,结果客户那边有个老驱动也用了 200 号,两台设备打架,排查了整整两天。所以,除非你有特殊需求,否则别用静态分配。

动态分配:alloc_chrdev_region

动态分配就聪明多了。内核自动给你分配一个没被占用的主设备号:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

参数说明:

  • dev:输出参数,返回分配到的设备号
  • baseminor:起始次设备号
  • count:连续设备号的数量
  • name:设备名称

实际用法:

dev_t dev_num;
int ret;

ret = alloc_chrdev_region(&dev_num, 0, 4, "my_device");
if (ret < 0) {
    printk("动态分配设备号失败\n");
    return ret;
}

printk("分配的主设备号: %d\n", MAJOR(dev_num));
printk("起始次设备号: %d\n", MINOR(dev_num));
💡 我个人习惯:在车规项目中,我几乎都用动态分配。原因很简单——你不知道客户的系统里已经装了多少驱动。动态分配能避免冲突,这是最稳妥的做法。

设备号的查看方法

设备号分配好了,怎么查看?我常用的方法有这几个:

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
200 my_device    <-- 这就是我们注册的设备

2. 使用 ls -l 查看设备文件

$ ls -l /dev/my_device*
crw------- 1 root root 200, 0 Jan 15 10:00 /dev/my_device0
crw------- 1 root root 200, 1 Jan 15 10:00 /dev/my_device1
crw------- 1 root root 200, 2 Jan 15 10:00 /dev/my_device2
crw------- 1 root root 200, 3 Jan 15 10:00 /dev/my_device3

看到没?第一列是 c 表示字符设备,后面跟着主设备号和次设备号。

3. 内核日志查看

如果你在驱动里加了打印,dmesg 也能看到:

$ dmesg | grep my_device
[ 1234.5678] 分配的主设备号: 200
[ 1234.5679] 设备注册成功

动态 vs 静态:怎么选?

对比项 静态分配 动态分配
主设备号 手动指定 内核自动分配
冲突风险 高(需人工协调) 低(内核保证唯一)
可移植性 差(依赖特定编号) 好(自动适配)
调试便利性 好(设备号固定) 稍差(每次可能不同)
推荐场景 嵌入式设备、固定硬件 通用驱动、车规项目

核心建议:在车规项目中,我强烈推荐动态分配。原因有三:

  1. 车规系统经常需要 OTA 升级,新驱动可能和旧驱动冲突
  2. 不同车型的硬件配置不同,静态分配需要维护一张「设备号分配表」
  3. 动态分配让驱动更「干净」,不需要关心底层编号

设备号的释放

有分配就得有释放,这是基本素养:

void unregister_chrdev_region(dev_t from, unsigned count);

通常在模块卸载时调用:

static void __exit my_device_exit(void)
{
    unregister_chrdev_region(dev_num, 4);
    printk("设备号已释放\n");
}
⚠️ 注意:释放设备号前,要确保所有设备文件都已关闭,所有引用计数都已归零。否则会出现「设备忙」的错误。我曾经在项目里忘了检查引用计数,结果卸载模块时系统直接 panic 了。

实战中的小技巧

最后分享几个我常用的调试技巧:

  • 查看已分配的设备号:除了 /proc/devices,还可以用 cat /proc/devices | grep -E "^[0-9]" 过滤出数字开头的行
  • 快速定位冲突:如果注册失败,先 cat /proc/devices 看看哪个设备号被占了
  • 调试时用静态分配:开发阶段为了方便,我会先用静态分配固定设备号,等调试完了再改成动态分配
  • 设备号范围:Linux 内核支持的主设备号范围是 1-511,其中 1-255 是传统范围,256-511 是扩展范围

嗯,设备号管理就讲到这里。说白了,这就是个「身份证」管理的问题。动态分配就像去派出所办身份证,系统给你分配一个唯一的号码;静态分配就像自己给自己编号,搞不好就重号了。在车规项目里,我建议你老老实实用动态分配,省心又安全。