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时也方便。我在一些嵌入式项目里经常这么干,因为硬件平台固定,设备号冲突的概率极低。
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时内核给你一个新的主设备号,互不干扰。
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结构体,那是驱动和用户空间打交道的核心接口。