暗色模式

Linux udev 设备管理:从 /dev 节点生成到自定义规则

技术教程
2026-09-22
3
0
本文要点
  • /dev 下的节点不是 udev 建出来的,是内核建的/dev 是内核挂的 devtmpfs,设备一注册节点就出现,此时的权限是内核的默认值(brw------- root root,0600)
  • udev(systemd-udevd)随后接到内核发来的 uevent,按 /usr/lib/udev/rules.d + /etc/udev/rules.d 里的规则改属组、改权限、建符号链接、跑脚本。同一个节点,处理前后实测从 brw------- root root 变成 brw-rw---- root disk
  • 想看清这两步,可以在 modprobe 之前先 udevadm control --stop-exec-queue 暂停 udev 的事件队列,这样就能确定地看到内核刚建出来的原始节点,再 --start-exec-queue + udevadm settle 放行,对比 udev 处理之后的结果
  • udevadm monitor --property 能把整条通知链打出来:每个事件都有 KERNEL[...](内核 uevent)和 UDEV[...](udevd 处理完规则后发出)两层,同一个 SEQNUM 对应同一个事件
  • 写规则前先用 udevadm info --query=property 看设备属性、用 --attribute-walk 看能用来匹配的键(KERNEL==SUBSYSTEM==ATTR{}),规则里的匹配键就照抄这些
  • 一条规则可以同时干三件事:SYMLINK+="demoram" 建固定软链、MODE="0640" 改权限、RUN+= 留痕。实测 /dev/demoram -> ram0 出现、权限从 0660 变 0640、日志里多出一行 rule-hit
  • 改完规则必须 udevadm control --reload-rules,光把文件存进去不会生效;要让已经在的设备重新走一遍规则,还得 udevadm trigger(重建节点)再 udevadm settle(等队列排空)
  • /dev/disk/by-uuidby-path 这些「持久命名」全是 udev 规则的产物,不是磁盘上的东西,也不是内核给的——理解了这一点,就明白它们为什么会在设备插拔后重新生成
  • 全部命令在 Ubuntu 22.04 LTS(systemd 249 / udevadm 249、1 核 1.9G 云主机)上真实执行,截图即真实输出;演示用内存盘 brd 模块(rd_size=1024 即 1 MiB),机器重启后不留痕迹

Linux udev 设备管理:从 /dev 节点生成到自定义规则

插上一个 U 盘,/dev/sdb 就出现了;ls /dev/disk/by-uuid/,里面立刻多一个用 UUID 命名的软链接;再插一次,软链接还是那个名字——哪怕这次它被分配成了 sdc

这套「设备来了自动建节点、还能给个稳定名字」的机制,是 Linux 上最容易被当成理所当然、又最容易在出问题时抓瞎的一环。它由两半组成:内核负责把节点建出来,udev 负责把节点改成人能用的样子。本文用一个内存盘当小白鼠,把这两半分开看清楚。

内核先建节点,udev 后改属性

先请出实验器材:brd 是内核自带的内存块设备(RAM disk)模块,加载时用 rd_nr 指定造几个、rd_size 指定每个多大(单位 KiB)。rd_nr=1 rd_size=1024 就是「造一个 1 MiB 的内存盘」,不占磁盘、重启就没了,非常适合做设备相关的演示。

mount | grep ' /dev '
udevadm control --stop-exec-queue
modprobe brd rd_nr=1 rd_size=1024
echo "--- 内核 devtmpfs 刚建出来的节点 ---"; ls -l /dev/ram0
udevadm control --start-exec-queue
udevadm settle
echo "--- udev 处理完 uevent 之后 ---"; ls -l /dev/ram0
echo "sysfs 里登记的设备号: $(cat /sys/class/block/ram0/dev)"
udev on /dev type devtmpfs (rw,nosuid,relatime,size=993144k,nr_inodes=248286,mode=755,inode64)
--- 内核 devtmpfs 刚建出来的节点 ---
brw------- 1 root root 1, 0 Sep 21 21:03 /dev/ram0
--- udev 处理完 uevent 之后 ---
brw-rw---- 1 root disk 1, 0 Sep 21 21:03 /dev/ram0
sysfs 里登记的设备号: 1:0

内核 devtmpfs 建出的节点是 brw------- root root(0600),udev 处理完 uevent 之后变成 brw-rw---- root disk(0660)

这张截图是全文的题眼,三个细节值得逐行看:

① 第一行告诉你 /dev 是什么。 udev on /dev type devtmpfs——/dev 是一个内核挂载的 devtmpfs,不是磁盘上的普通目录。所以设备一在 sysfs 里注册,节点就自动出现了:内核根本不需要 udev 帮忙建节点。这也解释了为什么 /dev 下的东西重启就重建、为什么往里面手工 mknod 的节点重启后会消失。

--stop-exec-queue 是为了把两件事分开看。 正常情况下 modprobe 一返回,udev 可能已经把事件处理完了,你只能看到「结果」,看不到「过程」。先把 udev 的事件执行队列暂停,modprobe 之后节点照样出现(那是内核干的),但没有任何规则被应用——于是我们拿到了内核的原始产物:brw------- 1 root root,也就是 0600,属主属组都是 root。

③ 放行队列后,同一个节点换了一副面孔。 --start-exec-queue 恢复处理、udevadm settle 等队列排空,再 ls 一次:变成 brw-rw---- 1 root disk设备号还是 1:0,节点还是同一个节点,变的只有权限和属组——这就是 udev 存在的意义:内核只管「有这么个设备」,udev 按发行版的策略决定「谁能用它」。

属组这条来自发行版自带的默认规则,写在 /usr/lib/udev/rules.d/50-udev-default.rules 第 73 行:

grep -n 'SUBSYSTEM=="block"' /usr/lib/udev/rules.d/50-udev-default.rules
73:SUBSYSTEM=="block", GROUP="disk"
74:SUBSYSTEM=="block", KERNEL=="sr[0-9]*", GROUP="cdrom"

第 73 行是「所有块设备归 disk 组」。权限位跟着从 0600 变成 0660,则是 udev 的默认口径:规则里设了属组就用 0660,没设就保持 0600。所以「插上硬盘,普通用户加进 disk 组就能读写」这件事,源头就是这一行。

uevent:内核到 udevd 的那条通知线

udev 怎么知道设备来了?靠内核发的 uevent。用一个窗口把这条线抓下来看——先起一个监听器写进文件,再去加载/卸载模块,然后只看关键字段:

rm -f /tmp/uevent.log
udevadm monitor --property --subsystem-match=block > /tmp/uevent.log 2>&1 &
MON=$!; sleep 1
modprobe -r brd 2>/dev/null; sleep 1
modprobe brd rd_nr=1 rd_size=1024 2>/dev/null; sleep 1
kill $MON 2>/dev/null
grep -E "^(KERNEL|UDEV|ACTION|DEVNAME|DEVTYPE|SEQNUM)" /tmp/uevent.log
UDEV - the event which udev sends out after rule processing
KERNEL - the kernel uevent
KERNEL[1062359.722840] remove   /devices/virtual/block/ram0 (block)
ACTION=remove
DEVNAME=/dev/ram0
DEVTYPE=disk
SEQNUM=3355
UDEV  [1062359.725987] remove   /devices/virtual/block/ram0 (block)
ACTION=remove
DEVNAME=/dev/ram0
DEVTYPE=disk
SEQNUM=3355
KERNEL[1062360.734619] add      /devices/virtual/block/ram0 (block)
ACTION=add
DEVNAME=/dev/ram0
DEVTYPE=disk
SEQNUM=3358
UDEV  [1062360.740491] add      /devices/virtual/block/ram0 (block)
ACTION=add
DEVNAME=/dev/ram0
DEVTYPE=disk
SEQNUM=3358

udevadm monitor 抓到的 uevent:每个事件都有 KERNEL 和 UDEV 两层,同一个 SEQNUM 成对出现,间隔只有几毫秒

输出可以按成对来读,一次设备变动会产生两条记录:

  • KERNEL[...] add /devices/virtual/block/ram0 (block)——内核说:有个块设备来了,它在 sysfs 里的路径是 /devices/virtual/block/ram0。这条记录是内核直接广播的。
  • UDEV [...] add /devices/virtual/block/ram0 (block)——udevd 说:我收下了,规则跑完了。这条是 udevd 处理完之后才发出的。

所以排查「设备权限不对、软链接没生成」这类问题时,看 UDEV 那一层有没有出现是第一道分诊:只有 KERNEL 没有 UDEV,说明 udevd 没处理(队列卡住、服务没起来);两层都有但结果不对,那是规则写错了。

两层的 SEQNUM 是同一个(3355、3358),这是内核给事件的编号,可以拿来对账。前面 remove 是我们 modprobe -r brd 卸载模块触发的,后面 add 是重新加载触发的——模块加载/卸载和设备热插拔,走的完全是同一条通知线,这也是为什么本文敢用内存盘代替真 U 盘。

--property 还能把事件的完整属性打出来(上面被 grep 裁掉了大半),包括 DEVPATHSUBSYSTEMMAJORMINORDISKSEQ 等——这些字段和规则里的匹配键是同一套词汇,后面写规则时会直接用到。

写规则前,先问设备「你能用什么键匹配」

udev 规则的匹配条件不能凭空编,得从设备自己身上问出来。两个命令分工明确:

udevadm info --query=property --name=/dev/ram0
udevadm info --attribute-walk --name=/dev/ram0 | grep -E 'KERNEL==|SUBSYSTEM==|ATTR\{' | head -12
DEVPATH=/devices/virtual/block/ram0
DEVNAME=/dev/ram0
DEVTYPE=disk
DISKSEQ=28
MAJOR=1
MINOR=0
SUBSYSTEM=block
USEC_INITIALIZED=1062572002513
UDISKS_IGNORE=1
TAGS=:systemd:
CURRENT_TAGS=:systemd:
    KERNEL=="ram0"
    SUBSYSTEM=="block"
    ATTR{alignment_offset}=="0"
    ATTR{capability}=="40"
    ATTR{discard_alignment}=="0"
    ATTR{diskseq}=="28"
    ATTR{events}==""
    ATTR{events_async}==""
    ATTR{events_poll_msecs}=="-1"
    ATTR{ext_range}=="256"
    ATTR{hidden}=="0"
    ATTR{inflight}=="       0        0"

两个命令的差别很关键:

  • --query=property 给的是「udev 数据库里关于这个设备的一切」,也就是 KEY=VALUE 形式的环境变量。其中 DEVNAME(节点路径)、DEVTYPE(disk 还是 partition)、MAJOR/MINOR(设备号)、SUBSYSTEM(属于哪个子系统)都是规则的常用原料。
  • --attribute-walk 给的是「匹配键写法」,而且会顺着父设备一路往上走(这正是名字里 walk 的意思)。它输出的 KERNEL=="ram0"SUBSYSTEM=="block"ATTR{...}=="..." 都是可以直接抄进规则文件的语法——ATTR{} 对应 sysfs 里的属性文件,比如 ATTR{size} 就是 /sys/class/block/ram0/size 的内容。

写规则时的经验法则:优先用 SUBSYSTEM + KERNEL(或 ATTR{serial}ATTRS{idVendor} 这类硬件特征)匹配,不要用 DEVNAME(它就是你要生成的东西,拿结果当条件会绕进死循环),也不要用会变的东西(比如 sdb 这种按探测顺序分配的盘符——那正是持久命名要解决的问题)。

顺便看一眼 udev 自己的小数据库,它的命名规则是 b<主设备号>:<次设备号>b = block,字符设备则是 c):

cat /run/udev/data/b1:0
ls /dev/disk/
I:1062572002513
E:ID_FS_TYPE=
E:UDISKS_IGNORE=1
G:systemd
Q:systemd
V:1
total 0
drwxr-xr-x 2 root root  60 Sep 10 07:07 by-id
drwxr-xr-x 2 root root 100 Sep 18 21:03 by-label
drwxr-xr-x 2 root root 100 Sep 18 21:08 by-partuuid
drwxr-xr-x 2 root root 220 Sep 10 07:07 by-path
drwxr-xr-x 2 root root 100 Sep 18 21:03 by-uuid

/run/udev/data/ 是 udev 的运行时数据库I: 是设备初始化时刻(微秒),E: 是环境变量,G:/Q: 是标签。注意它在 /run 下——tmpfs,重启就没了,因为设备状态本来就该在每次启动时重新探测。

/dev/disk/ 下面那五个 by-* 目录,里面全是 udev 规则的产物by-uuidby-labelby-partuuid 认的是文件系统/分区表里的元数据,by-pathby-id 认的是硬件拓扑和序列号。它们不是磁盘上的东西,也不是内核给的,所以没插设备的时候目录是空的,插上才会冒出来

写一条自己的规则

前面都是观察,现在动手改。需求:给这个内存盘加一个固定软链 /dev/demoram,把权限收紧到 0640,并在规则命中时留一行痕迹——一条规则同时演示三类动作:

rm -f /tmp/udev-demo.log
cat > /etc/udev/rules.d/99-demoram.rules <<'RULE'
SUBSYSTEM=="block", KERNEL=="ram0", SYMLINK+="demoram", MODE="0640", RUN+="/bin/sh -c 'echo rule-hit >> /tmp/udev-demo.log'"
RULE
udevadm control --reload-rules
udevadm trigger --action=add --sysname-match=ram0
udevadm settle
ls -l /dev/ram0 /dev/demoram
cat /tmp/udev-demo.log
lrwxrwxrwx 1 root root    4 Sep 21 21:02 /dev/demoram -> ram0
brw-r----- 1 root disk 1, 0 Sep 21 21:02 /dev/ram0
rule-hit

自定义规则实测:符号链接 /dev/demoram 被创建、权限变成 0640、RUN 里的日志多了一行 rule-hit

一条规则的语法可以拆成三段,看这条就全了:

匹配键(==)                       赋值(=)        追加(+=)                          执行
SUBSYSTEM=="block", KERNEL=="ram0", SYMLINK+="demoram", MODE="0640", RUN+="/bin/sh -c '...'"
  • SUBSYSTEM=="block", KERNEL=="ram0"——匹配条件,必须同时满足才会执行后半段。== 是比较,= 是赋值,+= 是追加,这三个符号用混是新手最常见的错。比如这里如果写成 SYMLINK="demoram"(用 = 而不是 +=),会把系统默认规则建的软链接全部覆盖掉/dev/disk/by-uuid 下面的东西就跟这个设备说再见了。
  • SYMLINK+="demoram"——在 /dev 下建一个指向本设备的软链。截图里 ls -l /dev/demoram 显示 -> ram0它是个符号链接,不是第二个设备节点,所以文件大小那一列是 4(ram0 四个字符)。
  • MODE="0640"——覆盖默认的 0660。截图里 /dev/ram0brw-rw---- 变成 brw-r-----同一个节点,权限被规则改了
  • RUN+="/bin/sh -c '...'"——规则命中后执行的命令。注意这里必须显式写 /bin/sh -c:udev 规则不是 shell 脚本,>> 这种重定向语法它不认识,得交给 shell 去解释。

三条命令缺一不可,这也是「改了规则却不生效」的头号原因:

  • udevadm control --reload-rules——让 udevd 重新读规则文件。规则文件是 udevd 启动时载入内存的,光把文件存进去,它不会自己发现
  • udevadm trigger --action=add --sysname-match=ram0——让已经存在的设备重新走一遍规则(模拟一次「设备刚插上」)。不加 --sysname-match 会触发全系统所有设备,慢且没必要。
  • udevadm settle——等事件队列处理完。udev 是异步的,trigger 返回只代表事件排进队列了,不等它,下一条 ls 可能什么都看不到。

一个细节:ls -l /dev/ram0 /dev/demoram 的输出里 demoram 排在前面,是 ls 把参数按字典序排了序,不是它先被创建——别拿输出顺序推断执行顺序

规则文件放哪、叫什么名字

udev 读两个目录,文件名相同的,/etc 覆盖 /usr/lib

  • /usr/lib/udev/rules.d/——软件包安装的规则(50-udev-default.rules60-persistent-storage.rules 等都在这里)。不要直接改这里,包升级会覆盖你的修改。
  • /etc/udev/rules.d/——本机管理员的地盘。自己写的规则放这里,本文的 99-demoram.rules 就在这儿。

目录里的文件按文件名的字典序依次读取,这就是为什么规则文件都带数字前缀:50- 是默认值、60- 是持久命名、99- 留给本地自定义——数字越大越晚执行,越晚的赋值越有可能覆盖前面的。想要自己的规则赢,用大数字;想在默认规则之前插手,用小数字。

清理

演示用的内存盘和自定义规则都撤掉,恢复原状:

rm -f /etc/udev/rules.d/99-demoram.rules /tmp/udev-demo.log /tmp/uevent.log
udevadm control --reload-rules
modprobe -r brd
ls /dev/ram0 /dev/demoram
ls: cannot access '/dev/ram0': No such file or directory
ls: cannot access '/dev/demoram': No such file or directory

删规则之后记得再 reload-rules 一次(否则 udevd 内存里还留着旧规则,下次同名设备出现时仍会按老规矩处理),卸载 brd 后节点和软链接一起消失——软链接是 udev 建的,设备没了它自然也不会留着

坑位清单

  1. 节点是内核建的,属性是 udev 给的。modprobe 完立刻 ls,可能看到 brw------- root root——那不是出错了,是 udev 还没处理完,udevadm settle 一下就好。
  2. 改完规则必须 udevadm control --reload-rules,光存文件不生效;已有的设备还要 udevadm trigger 才会重新走规则。
  3. == 是匹配、= 是赋值、+= 是追加。 SYMLINK= 会把系统默认软链接覆盖掉,写自定义软链请一律用 SYMLINK+=
  4. RUN+= 不是 shell,重定向、管道、&& 都不认,要 shell 就显式写 /bin/sh -c '...';也别在 RUN+= 里跑阻塞或长任务,udevd 会把它们杀掉。
  5. 规则文件按文件名排序执行,数字前缀决定先后;自定义规则放 /etc/udev/rules.d/,别改 /usr/lib/udev/rules.d/
  6. 别用 DEVNAME 当匹配条件(它是规则的产物),也别用 sdb 这类会变的盘符,要稳定就用 by-uuid/ATTR{serial}
  7. 网卡改名别写 NAME=,systemd 系统用 .link 文件处理网卡命名;照抄十年前的教程容易翻车。
  8. 给系统盘写规则之前先在无关设备上验证——一条写错的规则可能让根文件系统在开机时挂不上,那就是进不了系统的级别了。

小结

udev 的整条链路,压成一句话就是:内核发现设备 → devtmpfs 建出原始节点 → 发 uevent → udevd 按规则改属性、建软链、跑动作。

把这句话拆开,就得到了排查顺序:

  1. 节点有没有——没有就看内核认没认到这个设备(ls /sys/class/block/);
  2. udevadm monitor 里 KERNEL 和 UDEV 两层齐不齐——只有 KERNEL 说明 udevd 没处理;
  3. 属性对不对——udevadm info --query=property 看 udev 眼中的设备;
  4. 规则写没写对——udevadm info --attribute-walk 拿到能匹配的键,再 --reload-rules + trigger 重放。

这套「内核提供事实、用户态施加策略」的分工,在 Linux 里反复出现:设备是 udev,日志是 journald 与 rsyslog 的接力,网络是内核转发加 nftables 策略。看懂一个,其余的都能照此推演。

想把本文用到的基础补齐,可以先看 Linux 内核模块管理:从 lsmod 到 modprobe 与黑名单配置brd 就是这么加载的)和 Linux 硬件信息采集:从 lscpu 到 dmidecode 识别物理机与虚拟机(sysfs 那条线的延伸);设备要挂起来用,接着看 Linux 磁盘挂载:从 fdisk 分区到 fstab 开机自动挂载;权限位那一层的变化,Linux 文件权限:从 chmod 到 ACL 的完整指南 讲得更细。

发表评论

暂无评论,快来抢沙发吧!