4. 设备节点的创建

在Linux字符设备驱动开发中,设备节点(Device Node)是用户空间访问内核驱动的“文件接口”。设备节点位于/dev目录下,通过主设备号(Major Number)和次设备号(Minor Number)唯一标识一个驱动程序及其管理的设备实例。本章将深入剖析设备节点的两种创建方式:手动创建(mknod)与自动创建(udev/mdev机制),并对比其适用场景与实现原理。

核心概念:设备节点本身不包含设备数据,它只是一个指向内核驱动程序的“门牌号”。用户空间通过open()、read()、write()等系统调用操作设备节点时,VFS(虚拟文件系统)根据设备号找到对应的驱动程序,并调用其注册的file_operations结构体中的函数。
设备节点与驱动关系架构图 用户空间 (User Space) 应用程序 (App) → open("/dev/mydev", O_RDWR) VFS 虚拟文件系统层 根据设备节点中的 主设备号 + 次设备号 查找驱动 内核空间 (Kernel Space) — 字符设备驱动 file_operations: .open, .read, .write, .ioctl, .release 硬件设备 (Hardware)

4.1 手动创建设备节点:mknod 命令

mknod是Linux系统中最原始、最直接的设备节点创建工具。它允许超级用户(root)在/dev目录下手工创建设备文件,并指定设备类型、主设备号和次设备号。

4.1.1 mknod 命令语法

mknod /dev/设备名 设备类型 主设备号 次设备号
参数 说明 示例值
/dev/设备名 设备节点在/dev下的路径和名称 /dev/my_char_dev
设备类型 c 表示字符设备,b 表示块设备 c
主设备号 标识设备驱动程序,必须与驱动注册时一致 240
次设备号 标识同一驱动下的不同设备实例 0

4.1.2 实战示例:手动创建与验证

假设我们已加载一个主设备号为 240 的字符设备驱动,创建其设备节点并验证:

# 1. 查看驱动是否已注册(检查 /proc/devices)
cat /proc/devices | grep 240
# 输出示例: 240 my_driver

# 2. 手动创建设备节点
sudo mknod /dev/my_driver c 240 0

# 3. 设置权限(可选,默认只有root可访问)
sudo chmod 666 /dev/my_driver

# 4. 验证设备节点
ls -l /dev/my_driver
# 输出示例: crw-rw-rw- 1 root root 240, 0 Mar 15 10:30 /dev/my_driver

# 5. 测试访问
echo "hello" > /dev/my_driver
cat /dev/my_driver
手动创建的局限性:
  • 静态绑定:设备节点与主/次设备号硬绑定,驱动卸载后节点依然存在,再次加载时若设备号变化则节点失效。
  • 无法热插拔:每次插入新设备或加载驱动后,都需要手动执行mknod,不适合动态变化的嵌入式系统。
  • 权限管理繁琐:需要额外使用chmod和chown调整访问权限。
  • 不适用于大规模设备:当设备数量多(如USB多串口卡)时,手动创建效率极低且易出错。

4.2 自动创建设备节点:udev / mdev 机制

为了解决手动创建的痛点,Linux引入了设备文件系统(devfs),随后被更完善的udev(用户空间设备管理)和mdev(嵌入式简化版udev)取代。这些机制基于内核的uevent事件通知,实现设备节点的自动创建、删除和权限管理。

udev/mdev 自动创建设备节点流程图 ① 驱动加载 / 设备插入 ② 内核注册设备 (alloc_chrdev_region / register_chrdev) 生成主/次设备号,创建 kobject ③ 内核发送 uevent 事件 通过 netlink 发送到用户空间 ④ udev/mdev 守护进程接收 uevent 解析事件类型 (ADD/REMOVE) 和设备信息 ⑤ 在 /dev 下创建/删除设备节点 设置权限、属主、符号链接等

4.2.1 udev 与 mdev 对比

特性 udev(桌面/服务器) mdev(嵌入式/BusyBox)
依赖 需要 libudev、systemd 或独立 udev 服务 集成在 BusyBox 中,依赖极小
事件处理 通过 netlink 接收 uevent,支持复杂规则匹配 通过 /sys 文件系统轮询或接收 uevent
规则配置 /etc/udev/rules.d/ 下的 .rules 文件 /etc/mdev.conf 配置文件
功能丰富度 支持持久化命名、权限控制、符号链接、脚本触发 基础创建/删除,支持简单权限和脚本
适用场景 PC、服务器、功能完整的嵌入式系统 资源受限的嵌入式系统(如路由器、IoT设备)

4.2.2 驱动中支持自动创建:class 与 device 注册

为了让驱动支持自动创建设备节点,驱动代码中必须完成以下两个关键步骤:

  1. 创建设备类(class):使用 class_create() 在 /sys/class/ 下创建一个类目录。
  2. 创建设备(device):使用 device_create() 在类下注册设备实例,内核会自动生成 uevent。

以下是一个典型的驱动初始化代码片段:

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

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

static int __init my_driver_init(void)
{
    int ret;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_driver");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }
    pr_info("Allocated dev_t: major=%d minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));

    // 2. 注册字符设备(cdev_init + cdev_add 略)

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

    // 4. 创建设备节点(自动触发 uevent → udev/mdev 创建 /dev/my_driver)
    my_device = device_create(my_class, NULL, dev_num,
                              NULL, "my_driver");
    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("my_driver loaded successfully\n");
    return 0;
}

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("my_driver unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
关键点解析:
  • class_create() 在 /sys/class/my_driver_class/ 下创建目录。
  • device_create() 在类目录下创建设备子目录,并写入 dev 文件(包含主/次设备号)。
  • 内核检测到 /sys 下的变化后,通过 netlink 发送 ADD 类型的 uevent。
  • 用户空间的 udev/mdev 接收到 uevent 后,读取 dev 文件获取设备号,然后调用 mknod 创建节点。

4.2.3 嵌入式系统中 mdev 的配置与使用

在基于 BusyBox 的嵌入式系统中,mdev 是首选的自动设备节点管理工具。配置步骤如下:

  1. 确保内核配置支持:
    CONFIG_UEVENT_HELPER=y
    CONFIG_NET=y
    CONFIG_SYSFS=y
  2. 在文件系统启动脚本中启动 mdev:
    # /etc/init.d/rcS 或 /etc/init.d/mdev.sh
    echo /sbin/mdev > /proc/sys/kernel/hotplug
    mdev -s

    mdev -s 会扫描 /sys 目录,为所有已注册的设备创建设备节点。

  3. 配置 mdev 规则(可选):
    # /etc/mdev.conf
    # 格式: <设备正则> <用户:组> <权限> [<命令>]
    my_driver  root:root 0666
    ttyUSB0    root:dialout 0660  @/usr/bin/my_usb_script.sh

4.3 手动创建 vs 自动创建:对比总结

对比维度 手动创建 (mknod) 自动创建 (udev/mdev)
操作方式 命令行手动执行 内核事件驱动,自动完成
设备号管理 需手动指定主/次设备号 从 /sys 自动读取
热插拔支持 不支持,需手动干预 原生支持,即插即用
权限控制 需额外 chmod/chown 通过规则文件自动设置
驱动卸载后 节点残留,需手动删除 节点自动删除
适用场景 调试阶段、静态设备、最小系统 产品化系统、动态设备、嵌入式量产
开发调试建议:在驱动开发初期,建议先使用手动 mknod 快速验证驱动功能。待驱动稳定后,再在代码中添加 class_create 和 device_create 支持自动创建,并配置 udev/mdev 规则,使驱动具备产品级可用性。

4.4 常见问题与排查

  • 设备节点创建失败:检查 /proc/devices 确认驱动是否已注册;检查 dmesg 查看内核日志中是否有错误信息。
  • udev/mdev 未自动创建节点:确认 /sys/class/ 下是否存在对应的类目录;检查 uevent_helper 是否指向正确的 mdev 路径;尝试手动执行 mdev -s 重新扫描。
  • 权限问题:使用 ls -l /dev/设备名 查看当前权限;检查 udev 规则文件或 mdev.conf 中的权限设置。
  • 设备号冲突:使用 cat /proc/devices 查看已占用的主设备号;优先使用动态分配 alloc_chrdev_region() 避免冲突。
设备节点创建方式决策流程图 开始:需要设备节点 是否支持自动创建? 手动 mknod 自动 udev/mdev 调试/静态设备 需手动管理节点 产品/动态设备 自动创建/删除

4.5 本章小结

  • 手动创建(mknod)是基础但静态的方式,适合驱动开发调试阶段,需要开发者手动管理设备号与节点生命周期。
  • 自动创建(udev/mdev)是现代Linux设备管理的标准方式,通过内核 uevent 机制实现设备节点的动态创建、删除和权限管理。
  • 驱动开发者需要在代码中调用 class_create() 和 device_create() 来触发自动创建流程。
  • 嵌入式系统优先选择 mdev,桌面/服务器系统选择 udev,两者原理相同但实现复杂度不同。
  • 掌握两种创建方式,能够根据项目阶段(开发/调试/量产)灵活选择,是嵌入式驱动开发的基本功。