在复杂的服务器环境中,我们经常会遇到一个棘手的问题:设备名称的非确定性。无论是系统启动时对存储设备的枚举,还是运行时热插拔一个 USB 设备,内核都倾向于按照发现顺序分配设备名,如 /dev/sda/dev/sdbeth0eth1。这种动态分配机制在下一次重启后可能会完全改变,导致依赖于设备路径的应用程序或脚本瞬间失效。为了解决这一顽疾,udev 提供了一套强大而灵活的规则引擎,用于实现持久化的设备命名和自动化管理。

什么是 udev?

udev 是 Linux 内核的设备管理器,它可以根据设备的属性(如供应商 ID、产品 ID 或序列号)识别设备,可以为应用软件提供设备事件,可以管理设备节点的权限,并可以在 /dev/ 目录中创建额外的符号链接,或重命名网络接口。

它的名字是 “userspace /dev” 的缩写,其核心组件在用户空间运行,负责响应内核发出的设备事件。

主要组件

  • libudev: 一个标准的库,它允许其他应用程序(如系统监控工具)查询 udev 数据库,访问设备信息和状态。
  • udevd: 核心的守护进程,作为后台服务运行。它监听来自内核 netlink 套接字的 uevent,并负责解析事件、匹配规则和执行操作。
  • udevadm: 一个命令行管理工具,是与 udev 系统交互的主要接口。它功能强大,可以用于触发事件、查询设备属性、测试规则和控制 udevd 守护进程。

udev 是如何工作的

我们可以用一个类比来理解 udev 的工作模式。

想象一个大型自动化物流仓库的包裹分拣中心。每当一个新包裹(设备)通过卡车(硬件总线)抵达时,它首先会经过一个扫描站。

  1. 事件产生 (uevent): 内核就像是仓库的入口扫描仪。当一个新包裹被检测到时(设备插入),扫描仪会立即生成一张包含基础信息的电子标签(uevent),例如“来自 PCI 通道”、“包裹类型为存储设备”、“内核临时编号为 sdb”。这张标签被广播给分拣系统。

  2. 守护进程监听 (udevd): udevd 守护进程就是分拣中心的大脑。它持续不断地接收这些来自入口的 uevent 电子标签。

  3. 规则匹配 (Rules Matching): 大脑会查阅一本厚厚的操作手册(/etc/udev/rules.d/ 等目录下的规则文件)。手册里写满了各种指令,例如:“如果包裹来自‘SanDisk’公司(ATTR{idVendor}),且产品型号为‘Ultra Flair’(ATTR{idProduct}),并且有唯一的序列号(ATTR{serial})……”

  4. 执行操作 (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跳转到下一个名称匹配的 LABELGOTO=“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

可以看到,稳定的符号链接已成功创建,它将始终指向这个特定的物理设备。

参考资料

  1. Udev - Wikipedia
  2. udev - Arch Linux
  3. An introduction to Udev: The Linux subsystem for managing device events