3. 设备节点创建:从手动mknod到自动管理

设备节点,说白了就是用户空间访问内核驱动的「门牌号」。没有它,应用程序根本找不到你的驱动程序。这一章我带你彻底搞懂设备节点的三种创建方式——手动、半自动、全自动。

3.1 手动创建:mknod命令

早期Linux开发中,设备节点全靠管理员手动创建。现在虽然用得少了,但调试阶段我偶尔还会用——因为它最直接,没有任何中间环节。

mknod基本语法:

mknod /dev/设备名 类型 主设备号 次设备号

类型:c 表示字符设备,b 表示块设备

举个例子。假设你的驱动主设备号是 240,次设备号是 0:

# mknod /dev/mydev c 240 0
# ls -l /dev/mydev
crw-r--r-- 1 root root 240, 0 Mar 15 10:30 /dev/mydev

看到那个 c 了吗?它告诉内核这是个字符设备。后面跟着的主次设备号,就是驱动和设备实例的「身份证」。

我的经验:调试阶段我习惯用 mknod 快速验证。有一次客户现场设备节点死活不出现,我直接手动 mknod 创建,瞬间定位到是 udev 规则配置问题——省去了大量排查时间。

3.2 手动创建的痛点

手动创建有几个硬伤:

  • 设备号冲突——你得记住哪些主设备号已经被用了
  • 重启后消失——每次重启都得重新创建
  • 无法热插拔——设备拔插后节点不会自动更新

说白了,手动方式只适合临时调试。产品级驱动必须用自动管理。

3.3 udev自动创建设备节点

udev 是 Linux 的设备管理器。它监听内核的 uevent,一旦有设备添加或移除,就自动创建或删除节点。

udev 的工作流程其实很简单:

  1. 内核检测到设备,发送 uevent 到用户空间
  2. udev 收到事件,读取设备信息
  3. udev 匹配规则文件(/etc/udev/rules.d/)
  4. 执行匹配规则,创建设备节点

注意:udev 依赖 sysfs 文件系统。如果你的内核没挂载 sysfs(mount -t sysfs sysfs /sys),udev 根本没法工作。我曾经在嵌入式板子上踩过这个坑——折腾了半天才发现是启动脚本忘了挂 sysfs。

一个典型的 udev 规则文件长这样:

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

这条规则的意思是:当内核创建设备名为 mydev 的设备时,设置权限为 0666(所有用户可读写),并创建一个符号链接 /dev/mydevice。

3.4 驱动中自动创建设备节点

手动写 udev 规则还是麻烦。现代驱动开发中,我们直接在驱动代码里调用内核API,让内核自动触发 uevent,udev 自动创建节点。

这里有两个核心API:

API 作用 所在头文件
class_create() 创建设备类,在 /sys/class/ 下生成目录 <linux/device/class.h>
device_create() 在类下创建设备实例,触发 uevent <linux/device.h>

我习惯在驱动入口函数中这样用:

#include <linux/device.h>
#include <linux/device/class.h>

static struct class *my_class;
static struct device *my_device;

static int __init my_driver_init(void)
{
    int ret;

    // 1. 注册设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "mydev");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }

    // 2. 创建设备类
    my_class = class_create(THIS_MODULE, "myclass");
    if (IS_ERR(my_class)) {
        pr_err("Failed to create class\n");
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_class);
    }

    // 3. 创建设备节点(自动触发udev)
    my_device = device_create(my_class, NULL, dev_num, NULL, "mydev");
    if (IS_ERR(my_device)) {
        pr_err("Failed to create device\n");
        class_destroy(my_class);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(my_device);
    }

    pr_info("mydev driver loaded, major=%d\n", MAJOR(dev_num));
    return 0;
}

关键点:device_create() 的最后一个参数是设备节点名。这里传 "mydev",udev 就会在 /dev/ 下创建 /dev/mydev。设备号由 dev_num 指定,主次设备号自动分配。

对应的出口函数要记得清理:

static void __exit my_driver_exit(void)
{
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    unregister_chrdev_region(dev_num, 1);
    pr_info("mydev driver unloaded\n");
}

顺序很重要——先销毁设备,再销毁类,最后释放设备号。反了的话,内核会报 warning。

3.5 三种方式对比

方式 适用场景 优点 缺点
mknod 手动 调试、临时验证 无依赖,立竿见影 重启丢失,无法热插拔
udev 规则 固定设备、权限定制 灵活,可自定义命名和权限 需要写规则文件,维护成本
class_create + device_create 产品级驱动 全自动,内核原生支持 依赖 udev 和 sysfs

3.6 核心流程:设备节点创建全链路

下面这张图展示了从驱动加载到 /dev/ 下出现节点的完整流程:

设备节点创建全链路流程图 设备节点创建全链路流程图 驱动加载 alloc_chrdev_region() class_create() device_create() 内核发送 uevent udev 创建 /dev/mydev 关键说明 device_create() 触发 uevent udev 监听 netlink 接收事件 匹配规则后创建 /dev/ 节点 避坑提醒 确保 sysfs 已挂载 确保 udev 服务已启动 检查 /sys/class/ 目录

3.7 避坑指南

我曾经踩过的坑:

  • 设备节点权限问题——默认创建的节点只有 root 可读写。非 root 应用打开会返回 -EPERM。解决方案:在 udev 规则中设置 MODE="0666",或者在 device_create() 后手动 chmod。
  • 设备节点名冲突——如果你在 device_create() 中传的名字和已有节点重名,创建会失败。我建议用设备名+实例号的方式,比如 "mydev0"、"mydev1"。
  • 卸载驱动后节点残留——如果驱动异常退出,没调用 device_destroy(),/dev/ 下的节点会变成「僵尸节点」。再次加载驱动时会报错。解决办法:rm -f /dev/mydev 手动删除,或者重启 udev 服务。

我的调试小技巧:在驱动初始化成功后,立刻检查 /sys/class/ 下有没有你的类目录,以及 /dev/ 下有没有设备节点。如果 /sys/ 下有但 /dev/ 下没有,问题出在 udev 侧;如果 /sys/ 下都没有,问题出在驱动侧。这个二分法能帮你快速定位。

3.8 小结

设备节点创建是驱动和用户空间的桥梁。手动 mknod 适合调试,udev 规则适合定制,class_create + device_create 是产品级标准做法。记住一点:驱动代码里做好创建和销毁的对称处理,udev 那边基本不用操心。