4、设备号管理:动态与静态分配设备号、设备节点创建(udev/mdev机制)

设备号管理,说白了就是给驱动一个身份证。没有它,用户空间根本找不到你的设备。我刚开始写驱动时,总觉得这步是走形式,后来踩过坑才明白——设备号分配错了,整个系统都可能乱套。

4.1 主设备号与次设备号

Linux 用 dev_t 类型表示设备号,32 位长度。高 12 位是主设备号,低 20 位是次设备号。主设备号对应驱动,次设备号对应具体设备实例。

概念 位数 范围 作用
主设备号 12 bit 0 ~ 4095 标识驱动类型
次设备号 20 bit 0 ~ 1048575 标识具体设备

内核提供了几个宏来操作 dev_t:

#include <linux/types.h>

dev_t dev = MKDEV(major, minor);   // 合成设备号
int major = MAJOR(dev);            // 提取主设备号
int minor = MINOR(dev);            // 提取次设备号

嗯,这里要注意:MKDEV 只是拼数字,不会注册。真正注册要用 register_chrdev_region 或 alloc_chrdev_region。

4.2 静态分配 vs 动态分配

两种方式,各有各的适用场景。我个人的习惯是:产品原型阶段用静态,量产驱动用动态。为什么?往下看。

4.2.1 静态分配

手动指定主设备号,调用 register_chrdev_region 注册。

#define MY_MAJOR 240
#define MY_MINOR 0
#define MY_COUNT 4

dev_t dev = MKDEV(MY_MAJOR, MY_MINOR);
int ret = register_chrdev_region(dev, MY_COUNT, "my_driver");
if (ret < 0) {
    printk("Failed to register device number\n");
    return ret;
}

优点:设备号固定,用户空间可以写死。适合嵌入式设备,驱动和硬件一一对应。

缺点:容易冲突。我记得有一次,两个驱动都用了 240,结果系统启动时第二个驱动直接注册失败。排查了半天才发现是设备号撞车了。

避坑指南:静态分配前,先查 /proc/devices 看看哪些号已经被用了。我曾经在客户现场遇到过,驱动加载后设备节点死活打不开,最后发现主设备号被系统预留了。

4.2.2 动态分配

让内核自动分配,调用 alloc_chrdev_region。

dev_t dev;
int ret = alloc_chrdev_region(&dev, 0, MY_COUNT, "my_driver");
if (ret < 0) {
    printk("Failed to alloc device number\n");
    return ret;
}
int major = MAJOR(dev);
printk("Allocated major: %d\n", major);

优点:不会冲突,内核帮你挑一个空闲的号。适合模块化驱动,或者需要热插拔的场景。

缺点:设备号不固定。用户空间不能写死,必须通过 /sys 或 udev 动态获取。

对比项 静态分配 动态分配
主设备号 手动指定 内核自动分配
冲突风险 高 低
用户空间适配 写死即可 需动态获取
推荐场景 固定硬件平台 通用/可移植驱动

4.3 设备节点创建

设备号注册了,但用户空间还看不到。你需要创建设备节点——就是 /dev/xxx 那个文件。

4.3.1 手动创建(mknod)

调试阶段最直接的方式:

mknod /dev/mydevice c 240 0

c 表示字符设备,240 是主设备号,0 是次设备号。但这种方式太原始了,产品里没人这么干。

4.3.2 udev 机制

udev 是用户空间的设备管理器。它监听内核的 uevent,自动创建/删除设备节点。

驱动侧只需要做一件事:在 probe 函数里调用 device_create。

struct device *dev;
dev = device_create(my_class, NULL, devno, NULL, "my_device%d", minor);
if (IS_ERR(dev)) {
    printk("Failed to create device\n");
    return PTR_ERR(dev);
}

udev 收到 uevent 后,会根据 /etc/udev/rules.d/ 下的规则创建设备节点。默认规则通常就够了,但你可以自定义:

# /etc/udev/rules.d/99-mydevice.rules
KERNEL=="my_device0", MODE="0666", SYMLINK+="myapp"

这条规则把 my_device0 的权限设为 666,并创建一个符号链接 /dev/myapp。

我的经验:udev 规则写错了不会报错,设备节点就是不出来。调试时可以跑 udevadm monitor 看 uevent 有没有发出来,再用 udevadm test 模拟规则匹配。

4.3.3 mdev 机制

mdev 是 busybox 自带的轻量版 udev,嵌入式系统里用得很多。它比 udev 简单,但功能也弱一些。

使用 mdev 需要两步:

  1. 内核配置时开启 CONFIG_UEVENT_HELPER,并设置 helper 路径为 /sbin/mdev
  2. 系统启动时执行 echo /sbin/mdev > /proc/sys/kernel/hotplug

驱动侧代码和 udev 一样,调用 device_create 就行。mdev 会自动在 /dev 下创建节点。

对比项 udev mdev
依赖 systemd / udev 包 busybox
规则文件 /etc/udev/rules.d/ /etc/mdev.conf
功能 强大,支持复杂规则 简单,适合小系统
资源占用 较高 极低
适用场景 桌面/服务器/大型嵌入式 资源受限的嵌入式设备

4.4 完整流程:从注册到节点创建

把上面这些串起来,一个典型的字符驱动设备号管理流程是这样的:

// 1. 分配设备号
if (use_dynamic) {
    ret = alloc_chrdev_region(&dev, 0, 1, "my_driver");
} else {
    dev = MKDEV(240, 0);
    ret = register_chrdev_region(dev, 1, "my_driver");
}

// 2. 创建设备类
my_class = class_create(THIS_MODULE, "my_class");

// 3. 创建设备节点(触发 uevent)
device_create(my_class, NULL, dev, NULL, "my_device");

// 4. 卸载时清理
device_destroy(my_class, dev);
class_destroy(my_class);
unregister_chrdev_region(dev, 1);

你想想看,如果没有 udev/mdev,每次加载驱动都要手动 mknod,多麻烦。而且设备号一变,还得重新创建。自动化机制省心多了。

核心要点:设备号是驱动的身份证,设备节点是用户空间的门把手。两者缺一不可,但现代开发中,动态分配 + udev/mdev 自动创建是标准做法。

4.5 设备号管理知识图谱

下面这张图把整个流程串起来了,从内核空间到用户空间,一目了然:

设备号管理完整流程 内核空间 静态分配 register_chrdev_region 动态分配 alloc_chrdev_region dev_t class_create 用户空间 device_create → uevent udev /etc/udev/rules.d/ mdev /etc/mdev.conf /dev/my_device 设备节点创建成功

4.6 常见问题与避坑

  • 设备节点没出现:先检查 /sys/class/ 下有没有你的设备目录。没有的话,说明 device_create 没调用成功。
  • 权限不够:默认节点权限是 600,普通用户打不开。要么改 udev 规则,要么在 device_create 时指定 devnode 回调。
  • 设备号泄漏:驱动卸载时忘了 unregister_chrdev_region,下次加载就分配不到号了。我犯过这个错,重启才恢复。
  • mdev 不工作:确认 /proc/sys/kernel/hotplug 指向了 /sbin/mdev,而且 busybox 编译时包含了 mdev。