udev规则不生效?先看这六个原因

udev 这条链路上的坑,几乎全部集中在「它什么时候跑」、「它看到的根文件系统是什么样」、「我的规则为什么没生效」这三件事上。大部分教程只讲怎么写一条 SYMLINK 规则,等你真要在生产环境里做持久化网卡命名、或者给一块盘绑定固定设备号的时候,就会发现改了规则重启一次设备名变了、再重启又变回去,或者规则在容器里根本不执行。这篇按实际故障的顺序讲。

一、先理清启动链路:内核、initramfs、systemd-udevd

设备在 Linux 上出现的时候,内核先建好 /sys 里的设备对象,然后发一个 uevent,systemd-udevd 收到之后按规则处理:改权限、建软链、加载模块、跑程序。问题在于整个开机过程中 udev 会被启动两次,处理的环境完全不同。

第一次在 initramfs 里,此时真正的根文件系统还没挂载,/etc/udev/rules.d 里的规则大概率看不见,只有被打进 initramfs 的那部分规则可用。第二次是真根切换完成之后,由 systemd 启动正式的 udevd,这一轮才会读到你在 /etc/udev/rules.d 里写的规则。

这条链路的直接后果是:如果你的规则依赖于某个不在 initramfs 里的程序(脚本、二进制工具),或者依赖真根上的某个路径,那么第一轮必然失败。失败本身通常无害,第二轮会补上;但如果你的规则要决定根盘或者交换分区的设备名,第一轮失败就是致命的——机器起不来。

lsinitramfs /boot/initrd.img-$(uname -r) | grep udev
systemctl status systemd-udevd
udevadm monitor --udev --property

第二条命令是排查一切 udev 问题的起点。如果服务没起来,后面所有规则都不会执行,而现象往往表现为「网卡改名失败」、「磁盘权限不对」这类看起来毫不相关的问题。第三条 udevadm monitor 是最有用的工具:插拔设备、或者热触发一次,就能看到内核发出的属性,这些属性就是写规则时匹配的依据。

二、写规则的正确姿势:一条规则只做一件事

udev 规则文件放在 /etc/udev/rules.d/,文件名建议以 99- 开头,保证最后执行、不被发行版的规则覆盖。同一文件名在 /usr/lib/udev/rules.d 和 /etc/udev/rules.d 同时存在时,后者优先,这是覆盖发行版规则的标准做法。

规则的本质是「匹配键 + 赋值键」。匹配键有 KERNEL、SUBSYSTEM、ATTRS、ENV,赋值键有 SYMLINK、OWNER、GROUP、MODE、RUN、ENV。最常见的错误是拿 ATTR 去匹配一个有 ATTRS 语义的东西,或者反过来。

SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:11:22:33:44:55", NAME="lan0"
SUBSYSTEM=="block", KERNEL=="sd*", ATTRS{serial}=="WD-WX1234567890", SYMLINK+="disk/backup"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="ttyUSB-device"

三条规则对应三种典型场景。第一条只对特定 MAC 的网络接口改名,注意网卡的匹配必须用 ATTR{address} 而不是 ATTRS——很多人从别的教程里抄了 ATTRS 导致规则永远不命中。第二条用磁盘序列号绑定,序列号是最稳定的标识,比 /dev/sdb 这种按探测顺序分配的设备名可靠得多。第三条用 USB 设备的厂商和产品 ID 建软链,这是串口设备做持久命名的经典写法。

一个规则文件里写多条规则是允许的,但一条规则如果同时改了名字又建了软链,往往会互相干扰。比如 NAME 和 SYMLINK 放在同一条规则里,某些版本里只会执行其中一个。拆成两条规则,一条改名一条建软链,问题就消失了。

三、规则不生效的六种原因,按出现频率排

原因一:没有触发重载。改了规则文件以后必须 udevadm control --reload-rules 加 udevadm trigger,后者相当于把当前所有设备重新走一遍规则。只做前者,已存在的设备不会重新处理。验证方法是用 udevadm test /sys/class/net/eth0 单条测试,它会把规则匹配过程完整打出来,包括哪条规则命中、哪个程序被调用、结果是什么。

原因二:匹配键写错。把 ATTR 写成 ATTRS,或者把 KERNEL=="sda" 写成了 SUBSYSTEM=="sda"。udevadm test 的输出里会看到规则被读取但没有匹配,这是最省事的排查入口。

原因三:环境变量不同。规则里 RUN+= 调用的脚本,运行时的环境非常干净:PATH 可能只有 /usr/bin:/usr/sbin 甚至更少,没有你的 shell profile,HOME 也不一定存在。所以脚本里必须用绝对路径调用每个命令,不能依赖 PATH。这一条导致的失败看起来像「脚本写了但什么都没发生」,其实脚本跑起来了、只是第一行就报 command not found。

原因四:RUN 是异步阻塞的。udev 会等 RUN 里指定的程序执行完才继续处理事件,脚本里若有 sleep 或者网络请求,会把整条设备链路卡住,直接表现为开机变慢甚至超时。需要长时间运行的东西应该交给 systemd service,由规则只负责 TAG+="systemd" 和 ENV{SYSTEMD_WANTS} 触发。

原因五:文件系统只读或者权限不对。规则文件必须是 root:root、权限 644,且不能有执行位。某些系统里有 udevadm verify 可以静态检查语法,但老版本没有,只能靠 udevadm test 一行行看。

原因六:写在 udev 规则里的程序不存在或路径不对。尤其常见于从别处拷贝的路由脚本,路径写的是 /usr/local/bin/xxx,而这台机器上装在 /opt/xxx。用绝对路径加 test -x 检查一次,比在日志里翻半天快。

四、容器里没有 udev,这是设计不是 bug

Docker 容器和大多数 LXC 容器里不跑 systemd-udevd,因为容器共享宿主机内核,/dev 通常是宿主机 udev 的产物或者由 runtime 挂载的设备节点。所以在容器里写 udev 规则一定不会生效,装 udevd 也没用——除非你挂载了 /sys 并给了足够权限,但那属于把容器当虚拟机用,不推荐。

容器里的正确做法是:设备由 docker 的 --device 参数或者 compose 的 devices: 字段直接指定,权限用 --group-add 或者把设备节点的 GID 匹配到容器内的组。需要固定设备名时,在宿主机上配 udev 规则建好软链(比如 /dev/serial/by-id/ 下的稳定路径),再把那个稳定路径挂进容器。这是一条清晰的分界线:设备命名在宿主机解决,容器只消费结果。

LXC 有一点不同,特权容器里能看到部分 /sys,但 udev 事件从宿主机发出,容器里收到的事件往往不完整。如果非要在 LXC 里做设备持久化,最稳的办法仍然是宿主机建软链、容器挂载。

五、网卡持久命名的取舍

传统做法是用 udev 规则按 MAC 给网卡改名,比如所有网卡改成 lan0、wan0。现代发行版已经内置了 systemd 的 net.ifnames=0 或 net.ifnames=1 策略,把命名规则交给 systemd-predictable-names 或者 netplan 处理,自己再叠一层 udev 改名容易打架。

这里有个具体的判断标准:如果机器只有一两块网卡,用发行版默认的可预测命名(enp3s0、ens192)就够了,改名的收益是负的——每次换主板或者换网卡,名字会变,你还得改配置文件。真正需要改名的是「网卡插槽不固定、但角色固定」的场景,比如多网卡的软路由,用 MAC 绑定 wan0/lan1 才有意义。

改名前必须做的一件事:把这个 MAC 对应的物理网卡位置记进资产文档。曾经见过把两块同型号网卡装反、规则按 MAC 绑错、结果软路由把 LAN 当 WAN 的现场,排查了两小时。写一条 ethtool -P eth0 的输出进文档,或者干脆在机箱上贴标签,比什么规则都可靠。

六、把 udev 用在运维里:三个真正有用的场景

第一个是外接备份盘的自动挂载。用 udev 规则在特定序列号的磁盘插入时,触发一个 systemd 服务去做挂载和备份,比在 cron 里轮询 lsblk 可靠得多。注意规则里只做触发,具体逻辑全部放在服务单元的 ExecStart 里,这样脚本可以随意改而不需要 reload udev。

第二个是设备权限收口。/dev/video*、/dev/ttyUSB* 这类设备的默认组经常是 root 或者 dialout,给应用分配最小权限的正确做法是用 udev 规则指定 GROUP 和 MODE,比如 MODE="0660", GROUP="app",而不是把应用加进 root 或者干脆 0666。这条实践比大多数人以为的重要:一个 0666 的串口设备等于给了本机所有用户控制串口的权限。

第三个是排查设备反复掉线。用 udevadm monitor 加时间戳观察,能看到设备是不是在反复 add/remove。如果每秒好几次,基本是 USB 供电不足或者线材问题,不是驱动 bug。先换线、换口、加供电,再怀疑内核。

七、诊断工具箱与验证顺序

把下面几条命令按顺序记下来,能覆盖绝大多数 udev 故障:

udevadm info -a -p /sys/class/net/eth0     # 列出可用于匹配的属性层级
udevadm test /sys/class/net/eth0           # 单条设备走一遍规则,看匹配细节
udevadm control --reload-rules             # 重载规则文件
udevadm trigger --subsystem-match=net      # 重新触发某类设备
journalctl -u systemd-udevd -b --no-pager  # 看 udevd 本启动日志

第一条 udevadm info -a 是最被低估的工具:它会把设备自身的 KERNEL、以及向上每一层父设备的属性都列出来,写规则时从这里挑要匹配的字段,比网上抄可靠一百倍。注意区分输出里的「looking at device」和「looking at parent device」,前者对应 ATTR,后者对应 ATTRS,这就是前面那个坑的根源。

验证顺序建议固定成:先 udevadm test 确认规则命中,再看 journalctl 确认程序被调用,最后看目标结果(软链是否存在、权限是否对)。跳过第一步直接看结果,往往会在「规则根本没匹配」和「规则匹配了但脚本失败」之间来回猜。

八、小结

udev 的复杂度几乎全部来自时机和环境:initramfs 与真根两轮执行、规则里极简的环境变量、RUN 的同步阻塞、容器里根本没有 udevd。写规则时守住三条就够了——用 udevadm info -a 挑匹配键、一条规则只做一件事、需要复杂逻辑时交给 systemd 服务而不是塞进 RUN。再配上一套固定的验证顺序(test → journal → 结果),绝大多数「规则不生效」的问题十分钟内能定位。

Last modification:October 28th, 2026 at 10:24 pm

Leave a Comment