1. 设备节点的生成机制
Linux 中的设备节点(例如 /dev/ttyS0、/dev/sda 等)本质上是特殊的文件,它们代表内核中的设备,用户态程序通过这些节点与设备通信。
1.1 内核初始化:使用 devtmpfs 创建节点
- 内核加载某个驱动后,调用 device_register() 等函数注册设备。
- 同时通过 devtmpfs 文件系统自动创建 /dev/ 下的设备节点。
- devtmpfs 是内核在启动 early stage 时就挂载的临时内存文件系统。
关于devtmpfs
现代主流 Linux 操作系统中 ,/dev 下的设备节点大都是kernel基于 devtmpfs 创建的。devtmpfs 是在内核启动的中早期阶段初始化的,通常在多数驱动加载前完成挂载 ,并在驱动加载过程中动态创建设备节点。在内核早期初始化阶段, 调用 devtmpfs_init()创建一个内存中的虚拟文件系统(tmpfs 类型)。
可以通过以下命令查看操作系统是否支持devtmpfs:
bash
grep DEVTMPFS /boot/config-$(uname -r)
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y # 如果 CONFIG_DEVTMPFS_MOUNT=y,内核会自动挂载 /dev
我们以串口举例,/dev/ttyS0 事实上是由linux 内核在调用device_register() 时 生成的, 和udev 是没有关系的。 通常, 此时udev这种用户级进程还没有启动起来。
使用mount 命令查看
bash
mount | grep devtmpfs
udev on /dev type devtmpfs (rw,nosuid,relatime,size=7916552k,nr_inodes=1979138,mode=755)
这说明 当前系统的 /dev 是挂载在 devtmpfs 上的。systemd-udevd 用户空间服务"管理 "这个挂载点,但挂载本身由内核完成。
2. 关于udev
既然devtmpfs 已经创建 了设备节点,为什么还需要udev 呢? 我们给出以张表
功能 | 是否由 devtmpfs 实现 | 是否由 udev 实现 |
---|---|---|
创建基础设备文件(如 /dev/ttyS0) | 是 | 有时也创建(如特殊权限/别名) |
设置权限、属主、属组(如 dialout) | 不行 | 可以 |
创建符号链接(如 /dev/serial/by-id/)) | 不行 | 负责 |
重命名设备或动态处理热插拔 | 不行 | 负责 |
由此可见,udev 必不可少, 虽然devtmpfs 已经创建 了设备节点,但udev 负责管理这些设备节点。
udev 通过监听 内核通知用户空间"设备事件"uevent(基于netlink) 来实现对设备的节点的管理。
当设备**注册(register)或注销(unregister)**时,内核通过调用:
c
kobject_uevent(&kobj, KOBJ_ADD);
kobject_uevent(&kobj, KOBJ_REMOVE);
来产生用户空间可见的事件。
2.1 uevent 的事件类型(常见)
内核宏 | udev 中看到的 ACTION |
说明 |
---|---|---|
KOBJ_ADD |
add |
设备添加(如插入 USB) |
KOBJ_REMOVE |
remove |
设备移除(如拔掉 USB) |
KOBJ_CHANGE |
change |
设备属性变更(如状态/权限变化) |
KOBJ_MOVE |
move |
更换路径(罕见) |
KOBJ_ONLINE |
online |
设备上线(用于网络等) |
KOBJ_OFFLINE |
offline |
设备离线(同上) |
举个例子,插入 /dev/ttyUSB0
- 设备驱动调用 device_register() 或 register_chrdev_region()
- 内核通过 kobject_uevent(KOBJ_ADD) 生成事件
- 内核借助 netlink 的 NETLINK_KOBJECT_UEVENT 协议将事件广播到用户空间
- udevd(systemd-udevd)通过 netlink socket 监听事件
- 收到后,udevd 读取环境变量并根据 /etc/udev/rules.d/ 匹配规则
- 如果有匹配项:
- 执行 RUN+="脚本",或者
- 设置权限、改名、建立软链等
2.2 udev 工具使用
2.2.1 查看设备信息
bash
# 显示与设备 /dev/ttyUSB0 直接对应的 udev 属性信息,只看"当前设备"这一层。
udevadm info /dev/ttyUSB0
# udevadm info -a -n /dev/ttyUSB0 会 递归显示设备及其所有父设备的属性
#(udev 规则可以使用父设备的属性!) 它列出了所有可能用于写 udev 规则的字段,
# 包括父设备的 ATTRS{}、KERNELS、SUBSYSTEMS 等
udevadm info -a -n /dev/ttyUSB0
编写复杂 udev 规则:你可以通过 ATTRS{serial}、SUBSYSTEMS 等对父设备做更细粒度的过滤。
2.2.2 实时监控内核与 udev 事件
只监控内核事件
bash
udevadm monitor --kernel
只监控 udev 事件
bash
udevadm monitor --udev
显示环境变量(设备属性)
bash
# 显示设备的所有 udev 属性(property),是 udev 设备节点关联的完整属性集,
# 等同于运行 udevadm info --query=property 得到的内容
udevadm monitor --property
udevadm monitor --environment # 内核(kernel)事件和 udev 事件的环境变量。
其它
你可以通过 udevadm monitor --environment --udev 看到这些变量:
bash
UDEV [552.115431] add /devices/pci0000:00/0000:00:14.0/usb1/1-1 (usb)
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-1
SUBSYSTEM=usb
DEVNAME=/dev/bus/usb/001/002
DEVTYPE=usb_device
PRODUCT=1d6b/2/300
TYPE=9/0/1
...
内核在发送 uevent 时,会构造这些环境变量,并通过 netlink 发送出去
2.2.3 udevadm trigger
udevadm trigger 是 Linux 系统中用于触发内核设备事件(uevent)重新发送的命令,常用于手动触发设备的 add、change、remove 等事件,进而让 udev 重新处理设备节点,执行相关规则和动作。
- 重新触发内核发送设备事件(uevent),比如重新"添加"或"修改"设备。
- 让 udev 重新扫描和处理指定设备。
- 常用于调试、重载设备驱动,或修复设备节点状态异常
bash
udevadm trigger --action=add /sys/class/tty/ttyS0
# 让内核重新"触发" /sys/class/tty/ttyS0 设备的 add 事件
# 使 udev 重新处理这个设备(比如重新创建设备节点,运行对应规则和脚本)
2.2.4 模拟设备添加,测试规则逻辑
udevadm test 是一个用于测试和调试 udev 规则的强大工具。它可以模拟 udev 处理设备事件的过程,显示规则匹配和执行的详细信息,但不会实际更改系统状态(不会创建设备节点或运行脚本)。
工作流程
- udevadm test 读取指定设备的 sysfs 信息
- 模拟 udev 收到的设备事件(一般是 add)
- 按照规则文件逐条匹配并打印匹配结果
- 显示将执行的动作(创建设备节点、运行程序等)
- 不实际执行这些动作,只打印模拟信息
bash
udevadm test /sys/class/tty/ttyS0
2.3 udev 规则
udev 规则文件用于定义如何响应内核设备事件(如添加、移除等),以便创建设备节点、设置权限、运行脚本等。其语法基于匹配键(匹配条件)与赋值键(行为指令)的组合。
基本语法格式:
bash
MATCH_KEY=="value", ASSIGN_KEY="value"
MATCH_KEY :匹配条件,只有满足时该规则才会生效
ASSIGN_KEY:指定当规则生效时应执行的操作(如设备权限、运行程序等)
2.3.1 常用匹配键(左侧)
关键字 | 说明 |
---|---|
KERNEL |
匹配设备名,如 sda , ttyS0 |
SUBSYSTEM |
匹配子系统,如 block , tty , net |
ACTION |
匹配事件类型,如 add , remove , change |
DEVPATH |
匹配 /sys 中设备路径 |
ATTR{key} |
匹配设备属性(来自 sysfs) |
ATTRS{key} |
匹配父设备属性(用于多层设备匹配) |
DRIVER |
匹配驱动名 |
ENV{key} |
匹配环境变量(如 ENV{ID_SERIAL} ) |
PROGRAM |
执行命令,匹配其退出值 |
TEST |
测试文件是否存在 |
TAG |
匹配指定标签 |
2.3.2 常用赋值键(右侧)
关键字 | 说明 |
---|---|
NAME |
设定设备节点名称 |
SYMLINK |
创建符号链接,如 /dev/serial/by-id/... |
OWNER |
设定所有者 |
GROUP |
设定所属组 |
MODE |
设定权限(如 0660 ) |
RUN+="" |
添加要执行的命令或脚本(+ 允许多个) |
TAG+="value" |
添加标签 |
ENV{key}="val" |
设置环境变量 |
2.3.3 示例规则解析
bash
SUBSYSTEM=="tty", KERNEL=="ttyS0", ACTION=="add", MODE="0660", GROUP="dialout", SYMLINK+="serial0", RUN+="/usr/local/bin/setup_serial.sh"
含义:
- 当子系统为 tty 且设备名是 ttyS0 且事件是 add 时:
- 设置权限为 0660
- 设置所属组为 dialout
- 创建 /dev/serial0 符号链接指向该设备
- 执行 /usr/local/bin/setup_serial.sh
bash
SUBSYSTEM=="tty", ATTRS{id}=="PNP0501", ACTION=="add", RUN+="/usr/local/bin/fixup_serial.sh"
含义:
- 匹配的是 tty 子系统,也就是串口设备,如 /dev/ttyS0, /dev/ttyUSB0
- 匹配该设备的父设备属性 id 为 PNP0501,这是常见的 PC 标准串口标识符
- 仅当设备 被添加 时触发(比如开机时创建)
- 触发时执行 /usr/local/bin/fixup_serial.sh 脚本
2.3.4 执行脚本
在 udev 的 RUN+= 脚本(如 /usr/local/bin/fixup_serial.sh)中,可以使用 udev 在事件中提供的一系列 环境变量,这些变量由内核 uevent 和 udev 合并生成,反映了设备的状态和属性。
不是所有在 udev 规则文件中出现的变量,在 RUN+="..." 调用的脚本中都可以直接使用。
常见可用变量列表
变量名 | 说明 |
---|---|
ACTION |
事件类型:add 、remove 、change 等 |
DEVPATH |
sysfs 中的设备路径,如 /devices/pnp0/00:02/tty/ttyS0 |
SUBSYSTEM |
子系统名,如 tty 、block 、net |
DEVNAME |
设备节点名称,如 /dev/ttyS0 |
DEVTYPE |
设备类型(如 partition , disk , usb_interface ) |
MAJOR /MINOR |
主设备号、次设备号 |
SEQNUM |
事件序号(udev 使用) |
ID_SERIAL |
串口设备的序列号(某些设备才有) |
ID_PATH |
唯一物理路径标识(如 pci-0000:00... ) |
ID_VENDOR , ID_MODEL |
USB 等设备的信息 |
DRIVER |
驱动名称(如果有) |
如何查看实际的环境变量?
bash
udevadm info -a -n /dev/ttyUSB0
# 或者
udevadm monitor --environment --udev
不能在脚本中直接使用的变量
变量 | 原因说明 |
---|---|
KERNEL |
匹配规则时使用,但不是标准环境变量,默认不传入脚本 |
ATTR{} 、ATTRS{} |
用于匹配设备属性,不自动传入脚本中 |
ENV{custom} |
仅在规则内部设置的环境变量,需显式传给脚本 |
PROGRAM="..." 的返回值 |
在规则中可用,脚本中不可见,需手动传递 |
想在脚本中使用规则中的值怎么办?
bash
SUBSYSTEM=="tty", KERNEL=="ttyS0", ENV{MY_PORT}="S0", RUN+="/usr/local/bin/do_something.sh"
然后在脚本中使用:
bash
echo "My port is: $MY_PORT"
建议调试方法
你可以在脚本最前面加入:
bash
env > /tmp/udev_env.txt