在复杂的服务器环境中,我们经常会遇到一个棘手的问题:设备名称的非确定性。无论是系统启动时对存储设备的枚举,还是运行时热插拔一个 USB 设备,内核都倾向于按照发现顺序分配设备名,如 /dev/sda
、/dev/sdb
或 eth0
、eth1
。这种动态分配机制在下一次重启后可能会完全改变,导致依赖于设备路径的应用程序或脚本瞬间失效。为了解决这一顽疾,udev 提供了一套强大而灵活的规则引擎,用于实现持久化的设备命名和自动化管理。
什么是 udev?
udev 是 Linux 内核的设备管理器,它可以根据设备的属性(如供应商 ID、产品 ID 或序列号)识别设备,可以为应用软件提供设备事件,可以管理设备节点的权限,并可以在 /dev/
目录中创建额外的符号链接,或重命名网络接口。
它的名字是 “userspace /dev” 的缩写,其核心组件在用户空间运行,负责响应内核发出的设备事件。
主要组件
- libudev: 一个标准的库,它允许其他应用程序(如系统监控工具)查询 udev 数据库,访问设备信息和状态。
- udevd: 核心的守护进程,作为后台服务运行。它监听来自内核 netlink 套接字的 uevent,并负责解析事件、匹配规则和执行操作。
- udevadm: 一个命令行管理工具,是与 udev 系统交互的主要接口。它功能强大,可以用于触发事件、查询设备属性、测试规则和控制 udevd 守护进程。
udev 是如何工作的
我们可以用一个类比来理解 udev 的工作模式。
想象一个大型自动化物流仓库的包裹分拣中心。每当一个新包裹(设备)通过卡车(硬件总线)抵达时,它首先会经过一个扫描站。
事件产生 (uevent): 内核就像是仓库的入口扫描仪。当一个新包裹被检测到时(设备插入),扫描仪会立即生成一张包含基础信息的电子标签(uevent),例如“来自 PCI 通道”、“包裹类型为存储设备”、“内核临时编号为 sdb”。这张标签被广播给分拣系统。
守护进程监听 (udevd): udevd 守护进程就是分拣中心的大脑。它持续不断地接收这些来自入口的 uevent 电子标签。
规则匹配 (Rules Matching): 大脑会查阅一本厚厚的操作手册(
/etc/udev/rules.d/
等目录下的规则文件)。手册里写满了各种指令,例如:“如果包裹来自‘SanDisk’公司(ATTR{idVendor}
),且产品型号为‘Ultra Flair’(ATTR{idProduct}
),并且有唯一的序列号(ATTR{serial}
)……”执行操作 (Action Execution): 一旦匹配到规则,大脑就会立刻发出指令:
命名/贴标签 (SYMLINK+=): “给这个包裹贴上一个永久、易于识别的内部标签,叫 sandisk。” 这就是在 /dev/ 目录下创建了一个固定的符号链接。
权限分配 (OWNER, GROUP, MODE): “这个包裹只能由‘数据库管理员’(OWNER=mysql)打开。” 这就是设定设备文件的权限。
触发动作 (RUN+=): “通知相关的管理员,一个重要的备份设备已上线。” 这就是执行一个外部脚本。
通过这个流程,无论包裹(设备)何时、以何种顺序到达,只要其自身属性不变,总能被准确识别、命名并进行自动化处理。udev 正是 Linux 系统中这个不知疲倦、高度自动化的设备“分拣中心”。
udev 规则
udev 的强大之处在于其灵活的规则系统。规则文件被存放在三个关键目录,并按以下优先级顺序加载和处理:
/etc/udev/rules.d/
:最高优先级。用于系统管理员自定义规则,会覆盖下面两个目录中的同名规则。/run/udev/rules.d/
:中间优先级。用于运行时动态生成的规则,重启后会丢失。/usr/lib/udev/rules.d/
:最低优先级。由操作系统或软件包提供商预置的默认规则。
规则文件必须以 .rules 结尾,通常以数字前缀命名(如 10-local.rules, 99-custom.rules)以控制解析顺序。数字越小,越早被执行。
规则的构成
一条 udev 规则由一系列逗号分隔的“键-操作符-值”对组成,可分为两类:
匹配键 (Match Keys): 定义规则生效的条件,用于和 uevent 提供的设备属性进行比较。一些常见的匹配键包括 KERNEL、SUBSYSTEM、DRIVER 和 ATTR。
赋值键 (Assignment Keys): 当所有匹配键都满足时,要执行的操作或赋予的属性。一些常见的赋值键包括 NAME、SYMLINK、OWNER、GROUP、MODE 和 RUN。
规则中常用的操作符包括:
- ==:比较是否相等。
- !=:比较是否不相等。
- =:分配一个值。
- +=:向一个列表中添加一个值。
- -=:从一个列表中移除一个值。
- :=:分配一个最终值;之后不允许再有任何更改。
匹配键
匹配键 | 说明 | 示例 |
---|---|---|
ACTION | 动作的名称。 | add、bind、unbind、remove |
DEVPATH | 设备的路径。 | /devices/pci0000:00/0000:00:01.0/usb1/1-1/1-1.1。 |
KERNEL | 设备的内核名称。 | sda、sdb、ttyUSB0、eth0 |
KERNELS | 当前设备或其任何父设备的内核名称。 | |
NAME | 匹配网络接口的名称。一旦在前面的某个规则中设置了 NAME 键,就可以使用它。 | |
SYMLINK | 匹配指向节点的符号链接的名称。一旦前面的某个规则中设置了 SYMLINK 键,就可以使用它。可能存在多个符号链接;只需其中一个匹配即可。 | |
SUBSYSTEM | 设备的子系统。 | block、usb、net、tty |
SUBSYSTEMS | 当前设备或其任何父设备的子系统。 | block、usb、net、tty |
DRIVER | 设备的驱动程序名称。仅为在事件生成时已绑定到驱动程序的设备设置此键。 | usb-storage, e1000e |
DRIVERS | 当前设备或其任何父设备的驱动程序名称。 | usb-storage, e1000e |
ATTR{filename} | 设备的 sysfs 属性值。除非指定的匹配值本身包含尾随空格,否则属性值中的尾随空格将被忽略。 | ATTR{idVendor}==“0781” |
ATTRS{filename} | 当前设备或其任何父设备的 sysfs 属性值的设备。如果指定了多个 ATTRS 匹配项,则所有这些匹配项都必须在同一个设备上匹配。除非指定的匹配值本身包含尾随空格,否则属性值中的尾随空格将被忽略。 | ATTRS{serial}==“12345” |
SYSCTL{kernel parameter} | 内核参数值。 | |
ENV{key} | 设备属性值。 | ENV{ID_FS_USAGE}==“filesystem” |
CONST{key} | 系统范围的常量。支持的键有:“arch”(系统架构)和 “virt”(系统的虚拟化环境)。未知的键将永远不会匹配。 | |
TAG | 设备标签。 | |
TAGS | 当前设备或其任何父设备的标签。 | |
TEST{octal mode mask} | 测试文件是否存在。如果需要,可以指定一个八进制模式掩码。 | |
PROGRAM | 执行一个程序以确定是否存在匹配;如果程序成功返回,则该键为真。设备属性在环境中可供执行的程序使用。程序的标准输出在 RESULT 键中可用。这只能用于非常短时间运行的前台任务。 | PROGRAM=="/bin/true" |
RESULT | 匹配上一次 PROGRAM 调用的返回字符串。此键可以在同一次 PROGRAM 调用之后或任何后续规则中使用。 |
赋值键
赋值键 | 说明 | 示例 |
---|---|---|
NAME | 仅用于网络接口的名称。udev 不能更改设备节点的名称,只能创建额外的符号链接。 | NAME=“lan0” |
SYMLINK | 指向设备节点的符号链接名称。每个匹配的规则都会将此值添加到待创建的符号链接列表中。可以指定多个符号链接(用空格分隔)。 | SYMLINK+=“my_usb” |
OWNER, GROUP, MODE | 设置设备节点的权限(所有者、用户组、模式)。每个指定的值都会覆盖编译时的默认值。 | OWNER=“mysql”, GROUP=“disk”, MODE=“0660” |
SECLABEL{module} | 将指定的 Linux 安全模块(LSM)标签应用于设备节点。 | |
ATTR{key} | 应写入设备 sysfs 属性的值。 | |
SYSCTL{kernel parameter} | 应写入内核参数的值。 | |
ENV{key} | 设置一个设备属性值。以 . 开头的属性名不会存储在数据库中,也不会导出到外部。 | ENV{MY_CUSTOM_FLAG}=“1” |
TAG | 为设备附加一个标签。用于过滤事件或枚举一组带标签的设备。不应作为通用标志滥用,否则可能影响性能。 | TAG+=“systemd” |
RUN{type} | 在处理完所有规则后执行一个程序。type 可为 "program" (外部程序)或 "builtin" (内置程序)。注意:只能用于极短时间运行的前台任务,且禁止网络访问、文件系统挂载/卸载或启动守护进程。 | RUN+="/usr/local/bin/device_added.sh" |
LABEL | 定义一个命名的标签,GOTO 语句可以跳转到此。 | LABEL=“my_custom_end” |
GOTO | 跳转到下一个名称匹配的 LABEL 。 | GOTO=“my_custom_end” |
IMPORT{type} | 导入一组变量作为设备属性。type 包括:“program” / “builtin” / “file” / “db” / “cmdline” / “parent”。 | |
WAIT_FOR | 等待一个文件(相对于 sysfs 设备路径)变为可用,超时时间为 10 秒。 | |
OPTIONS | 设置规则和设备选项。 |
编写 udev 规则
以下是一个完整的端到端示例,演示如何为一个特定的 SanDisk USB 驱动器创建永久性的符号链接 /dev/sandisk
第一步:获取设备信息
首先,插入设备,并使用 lsusb
定位其 Vendor ID 和 Product ID。
$ lsusb
Bus 001 Device 014: ID 0781:5591 SanDisk Corp. Ultra Flair
我们得到了 idVendor=0781 和 idProduct=5591。为了确保唯一性,我们还需要序列号。使用 udevadm 工具查询设备的详细信息。首先找到设备的 sysfs 路径:
# 根据 lsusb 的结果,我们可以推定 USB 设备在 /dev/bus/usb/001/014
$ udevadm info -q path -n /dev/bus/usb/001/014
/devices/platform/fc800000.usb/usb1/1-1/1-1.1
现在,使用这个路径来获取所有属性,并重点关注 ATTRS。
$ udevadm info -a -p /devices/platform/fc800000.usb/usb1/1-1/1-1.1
在大量的输出中,向上追溯,找到包含 idVendor, idProduct, 和 serial 的那个设备层级:
looking at device '/devices/platform/fc800000.usb/usb1/1-1/1-1.1':
KERNEL=="1-1.1"
SUBSYSTEM=="usb"
DRIVER=="usb"
ATTR{serial}=="0501...omitted...7e5b"
ATTR{version}==" 2.10"
ATTR{idProduct}=="5591"
ATTR{devnum}=="14"
ATTR{product}==" SanDisk 3.2Gen1"
ATTR{speed}=="480"
ATTR{urbnum}=="5731"
ATTR{busnum}=="1"
ATTR{manufacturer}==" USB"
ATTR{bMaxPower}=="224mA"
ATTR{idVendor}=="0781"
ATTR{bcdDevice}=="0100"
我们找到了三个最理想的唯一标识符:idVendor, idProduct, 和 serial。
第二步:编写规则文件
在 /etc/udev/rules.d/
目录下创建规则文件。文件名 80-persistent-usb.rules
中的 80 确保它在大多数系统默认规则之后执行。
$ sudo vi /etc/udev/rules.d/80-persistent-usb.rules
写入以下规则:
# Rule for SanDisk Ultra Flair USB Drive
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="0781", ATTRS{idProduct}=="5591", ATTRS{serial}=="0501abf9a2791d7e677d8a4c47621ca966aadef8751b385df31d4a986bdde9addf9100000000000000000000b85ed6a8ff85081091558107142b7e5b", SYMLINK+="sandisk"
规则解析:
SUBSYSTEM=="usb"
: 此规则仅适用于 USB 设备。ACTION=="add"
: 仅在设备添加时触发。ATTRS{...}
: 使用我们从 udevadm 获取的三个唯一属性进行精确匹配。SYMLINK
+=“sandisk”: 创建一个符号链接。
第三步:应用并测试规则
让 udevd
重新加载所有规则文件:
$ sudo udevadm control --reload-rules
你可以通过 udevadm test
命令来模拟一次设备事件,验证规则是否会如期触发,而无需物理上重新插拔设备。
$ sudo udevadm test $(udevadm info -q path -n /dev/bus/usb/001/014)
... (大量调试输出) ...
Reading rules file /etc/udev/rules.d/80-persistent-usb.rules
...
在输出中看到 SYMLINK
被正确创建,证明规则编写无误。
第四步:最终验证
重新插拔 USB 设备,然后检查 /dev/
目录:
$ ls -l /dev/sandisk
lrwxrwxrwx 1 root root 3 Jul 3 12:34 /dev/sandisk -> /dev/bus/usb/001/014
可以看到,稳定的符号链接已成功创建,它将始终指向这个特定的物理设备。