4. 设备节点的创建
在Linux字符设备驱动开发中,设备节点(Device Node)是用户空间访问内核驱动的“文件接口”。设备节点位于/dev目录下,通过主设备号(Major Number)和次设备号(Minor Number)唯一标识一个驱动程序及其管理的设备实例。本章将深入剖析设备节点的两种创建方式:手动创建(mknod)与自动创建(udev/mdev机制),并对比其适用场景与实现原理。
核心概念:设备节点本身不包含设备数据,它只是一个指向内核驱动程序的“门牌号”。用户空间通过
open()、read()、write()等系统调用操作设备节点时,VFS(虚拟文件系统)根据设备号找到对应的驱动程序,并调用其注册的file_operations结构体中的函数。
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事件通知,实现设备节点的自动创建、删除和权限管理。
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 注册
为了让驱动支持自动创建设备节点,驱动代码中必须完成以下两个关键步骤:
- 创建设备类(class):使用
class_create()在/sys/class/下创建一个类目录。 - 创建设备(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 是首选的自动设备节点管理工具。配置步骤如下:
- 确保内核配置支持:
CONFIG_UEVENT_HELPER=y CONFIG_NET=y CONFIG_SYSFS=y - 在文件系统启动脚本中启动 mdev:
# /etc/init.d/rcS 或 /etc/init.d/mdev.sh echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -smdev -s会扫描/sys目录,为所有已注册的设备创建设备节点。 - 配置 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()避免冲突。
4.5 本章小结
- 手动创建(mknod)是基础但静态的方式,适合驱动开发调试阶段,需要开发者手动管理设备号与节点生命周期。
- 自动创建(udev/mdev)是现代Linux设备管理的标准方式,通过内核 uevent 机制实现设备节点的动态创建、删除和权限管理。
- 驱动开发者需要在代码中调用
class_create()和device_create()来触发自动创建流程。 - 嵌入式系统优先选择 mdev,桌面/服务器系统选择 udev,两者原理相同但实现复杂度不同。
- 掌握两种创建方式,能够根据项目阶段(开发/调试/量产)灵活选择,是嵌入式驱动开发的基本功。