本文要点
/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-uuid、by-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
这张截图是全文的题眼,三个细节值得逐行看:
① 第一行告诉你 /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.rules73: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.logUDEV - 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
输出可以按成对来读,一次设备变动会产生两条记录:
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 裁掉了大半),包括 DEVPATH、SUBSYSTEM、MAJOR、MINOR、DISKSEQ 等——这些字段和规则里的匹配键是同一套词汇,后面写规则时会直接用到。
写规则前,先问设备「你能用什么键匹配」
udev 规则的匹配条件不能凭空编,得从设备自己身上问出来。两个命令分工明确:
udevadm info --query=property --name=/dev/ram0
udevadm info --attribute-walk --name=/dev/ram0 | grep -E 'KERNEL==|SUBSYSTEM==|ATTR\{' | head -12DEVPATH=/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-uuid、by-label、by-partuuid 认的是文件系统/分区表里的元数据,by-path、by-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.loglrwxrwxrwx 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
一条规则的语法可以拆成三段,看这条就全了:
匹配键(==) 赋值(=) 追加(+=) 执行
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/ram0从brw-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.rules、60-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/demoramls: cannot access '/dev/ram0': No such file or directory
ls: cannot access '/dev/demoram': No such file or directory删规则之后记得再 reload-rules 一次(否则 udevd 内存里还留着旧规则,下次同名设备出现时仍会按老规矩处理),卸载 brd 后节点和软链接一起消失——软链接是 udev 建的,设备没了它自然也不会留着。
坑位清单
- 节点是内核建的,属性是 udev 给的。 刚
modprobe完立刻ls,可能看到brw------- root root——那不是出错了,是 udev 还没处理完,udevadm settle一下就好。 - 改完规则必须
udevadm control --reload-rules,光存文件不生效;已有的设备还要udevadm trigger才会重新走规则。 ==是匹配、=是赋值、+=是追加。SYMLINK=会把系统默认软链接覆盖掉,写自定义软链请一律用SYMLINK+=。RUN+=不是 shell,重定向、管道、&&都不认,要 shell 就显式写/bin/sh -c '...';也别在RUN+=里跑阻塞或长任务,udevd 会把它们杀掉。- 规则文件按文件名排序执行,数字前缀决定先后;自定义规则放
/etc/udev/rules.d/,别改/usr/lib/udev/rules.d/。 - 别用
DEVNAME当匹配条件(它是规则的产物),也别用sdb这类会变的盘符,要稳定就用by-uuid/ATTR{serial}。 - 网卡改名别写
NAME=,systemd 系统用.link文件处理网卡命名;照抄十年前的教程容易翻车。 - 给系统盘写规则之前先在无关设备上验证——一条写错的规则可能让根文件系统在开机时挂不上,那就是进不了系统的级别了。
小结
udev 的整条链路,压成一句话就是:内核发现设备 → devtmpfs 建出原始节点 → 发 uevent → udevd 按规则改属性、建软链、跑动作。
把这句话拆开,就得到了排查顺序:
- 节点有没有——没有就看内核认没认到这个设备(
ls /sys/class/block/); udevadm monitor里 KERNEL 和 UDEV 两层齐不齐——只有 KERNEL 说明 udevd 没处理;- 属性对不对——
udevadm info --query=property看 udev 眼中的设备; - 规则写没写对——
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 的完整指南 讲得更细。
评论 (0)
暂无评论,快来抢沙发吧!