udev 入门:管理设备事件的 Linux 子系统
udev 是一个为你的计算机提供设备事件的 Linux 子系统。通俗来讲就是,当你的计算机上插入了像网卡、外置硬盘(包括 U 盘)、鼠标、键盘、游戏操纵杆和手柄、DVD-ROM 驱动器等等设备时,代码能够检测到它们。这样就能写出很多可能非常有用的实用程序,而它已经很好了,普通用户就可以写出脚本去做一些事情,比如当某个硬盘驱动器插入时,执行某个任务。
这篇文章教你去如何写一个由一些 udev 事件触发的 udev 脚本,比如插入了一个 U 盘。当你理解了 udev 的工作原理,你就可以用它去做各种事情,比如当一个游戏手柄连接后加载一个指定的驱动程序,或者当你用于备份的驱动器连接后,自动执行备份工作。
一个初级的脚本
使用 udev 的最佳方式是从一个小的代码块开始。不要指望从一开始就写出完整的脚本,而是从最简单的确认 udev 触发了某些指定的事件开始。
对于你的脚本,依据你的目标,并不是在任何情况下都能保证你亲眼看到你的脚本运行结果的,因此需要在你的脚本日志中确认它成功触发了。而日志文件通常放在 /var
目录下,但那个目录通常是 root 用户的领地。对于测试目的,可以使用 /tmp
,它可以被普通用户访问并且在重启动后就被清除了。
打开你喜欢的文本编辑器,然后输入下面的简单脚本:
#!/usr/bin/bash
echo $date > /tmp/udev.log
把这个脚本放在 /usr/local/bin
或缺省可运行路径的位置中。将它命名为 trigger.sh
,并运行 chmod +x
授予可运行权限:
$ sudo mv trigger.sh /usr/local/bin
$ sudo chmod +x /usr/local/bin/trigger.sh
这个脚本没有任何和 udev 有关的事情。当它运行时,这个脚本将在文件 /tmp/udev.log
中放入当前的时间戳。你可以自己测试一下这个脚本:
$ /usr/local/bin/trigger.sh
$ cat /tmp/udev.log
Tue Oct 31 01:05:28 NZDT 2035
接下来让 udev 去触发这个脚本。
唯一设备识别
为了让你的脚本能够被一个设备事件触发,udev 必须要知道在什么情况下调用该脚本。在现实中,你可以通过它的颜色、制造商、以及插入到你的计算机这一事实来识别一个 U 盘。而你的计算机,它需要一系列不同的标准。
udev 通过序列号、制造商、以及提供商 ID 和产品 ID 号来识别设备。由于现在你的 udev 脚本还处于它的生命周期的早期阶段,因此要尽可能地宽泛、非特定和包容。换句话说就是,你希望首先去捕获尽可能多的有效 udev 事件来触发你的脚本。
使用 udevadm monitor
命令你可以实时利用 udev,并且可以看到当你插入不同设备时发生了什么。用 root 权限试一试。
$ su
# udevadm monitor
该监视函数输出接收到的事件:
- UDEV:在规则处理之后发出 udev 事件
- KERNEL:内核发送 uevent 事件
在 udevadm monitor
命令运行时,插入一个 U 盘,你将看到各种信息在你的屏幕上滚动而出。注意那一个 ADD
事件的事件类型。这是你所需要的识别事件类型的一个好方法。
udevadm monitor
命令提供了许多很好的信息,但是你可以使用 udevadm info
命令以更好看的格式来看到它,假如你知道你的 U 盘当前已经位于你的 /dev
树。如果不在这个树下,拔下它并重新插入,然后立即运行这个命令:
$ su -c 'dmesg | tail | fgrep -i sd*'
举例来说,如果那个命令返回 sdb: sdb1
,说明内核已经给你的 U 盘分配了 sdb
卷标。
或者,你可以使用 lsblk
命令去查看所有连接到你的系统上的驱动器,包括它的大小和分区。
现在,你的驱动器已经处于你的文件系统中了,你可以使用下面的命令去查看那个设备的相关 udev 信息:
# udevadm info -a -n /dev/sdb | less
这个命令将返回许多信息。现在我们只关心信息中的第一个块。
你的任务是从 udev 的报告中找出能唯一标识那个设备的部分,然后当计算机检测到这些唯一属性时,告诉 udev 去触发你的脚本。
udevadm info
命令处理一个(由设备路径指定的)设备上的报告,接着“遍历”父级设备链。对于找到的大多数设备,它以一个“键值对”格式输出所有可能的属性。你可以写一个规则,从一个单个的父级设备属性上去匹配插入设备的属性。
looking at device '/devices/000:000/blah/blah//block/sdb':
KERNEL=="sdb"
SUBSYSTEM=="block"
DRIVER==""
ATTR{ro}=="0"
ATTR{size}=="125722368"
ATTR{stat}==" 2765 1537 5393"
ATTR{range}=="16"
ATTR{discard_alignment}=="0"
ATTR{removable}=="1"
ATTR{blah}=="blah"
一个 udev 规则必须包含来自单个父级设备的一个属性。
父级属性是描述一个设备的最基本的东西,比如它是插入到一个物理端口的东西、或是一个容量多大的东西、或这是一个可移除的设备。
由于 KERNEL
卷标 sdb
可能会由于分配给在它之前插入的其它驱动器而发生变化,因此卷标并不是一个 udev 规则的父级属性的好选择。但是,在做概念论证时你可以使用它。一个事件的最佳候选者是 SUBSYSTEM
属性,它表示那个设备是一个 “block” 系统设备(也就是为什么我们要使用 lsblk
命令来列出设备的原因)。
在 /etc/udev/rules.d
目录中打开一个名为 80-local.rules
的文件,然后输入如下代码:
SUBSYSTEM=="block", ACTION=="add", RUN+="/usr/local/bin/trigger.sh"
保存文件,拔下你的测试 U 盘,然后重启动系统。
等等,重启动 Linux 机器?
理论上说,你只需要运行 udevadm control —reload
即可,它将重新加载所有规则,但是在我们实验的现阶段,最好要排除可能影响实验结果的所有因素。udev 是非常复杂的,为了不让你躺在床上整晚都在思考为什么这个规则不能正常工作,是因为语法错误吗?还是应该重启动一下。所以,不管 POSIX 自负地告诉你过什么,你都应该去重启动一下。
当你的系统重启动完毕之后,(使用 Ctl+Alt+F3
或类似快捷键)切换到一个文本控制台,并插入你的 U 盘。如果你运行了一个最新的内核,当你插入 U 盘后你或许可以看到一大堆输出。如果看到一个错误信息,比如 “Could not execute /usr/local/bin/trigger.sh”,或许是因为你忘了授予这个脚本可运行的权限。否则你将看到的是,一个设备插入,它得到内核设备分配的一些东西,等等。
现在,见证奇迹的时刻到了。
$ cat /tmp/udev.log
Tue Oct 31 01:35:28 NZDT 2035
如果你在 /tmp/udev.log
中看到了最新的日期和时间,那么说明 udev 已经成功触发了你的脚本。
改进规则做一些有用的事情
现在的问题是使用的规则太通用了。插入一个鼠标、一个 U 盘、或某个人的 U 盘都将盲目地触发这个脚本。现在,我们开始专注于希望触发你的脚本的是确定的某个 U 盘。
实现上述目标的一种方式是使用提供商 ID 和产品 ID。你可以使用 lsusb
命令去得到这些数字。
$ lsusb
Bus 001 Device 002: ID 8087:0024 Slacker Corp. Hub
Bus 002 Device 002: ID 8087:0024 Slacker Corp. Hub
Bus 003 Device 005: ID 03f0:3307 TyCoon Corp.
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 hub
Bus 001 Device 003: ID 13d3:5165 SBo Networks
在这个例子中,TyCoon Corp
前面的 03f0:3307
就表示了提供商 ID 和产品 ID 的属性。你也可以通过 udevadm info -a -n /dev/sdb | grep vendor
的输出来查看这些数字,但是从 lsusb
的输出中可以很容易地一眼找到这些数字。
现在,可以在你的脚本中包含这些属性了。
SUBSYSTEM=="block", ATTRS{idVendor}=="03f0", ACTION=="add", RUN+="/usr/local/bin/thumb.sh"
测试它(是的,为了确保不会有来自 udev 的影响因素,我们仍然建议先重新启动一下),它应该会像前面一样工作,现在,如果你插入一个不同公司制造的 U 盘(因为它们的提供商 ID 不一样)、或插入一个鼠标、或插入一个打印机,这个脚本将不会被触发。
继续添加新属性来进一步专注于你希望去触发你的脚本的那个唯一的 U 盘。使用 udevadm info -a -n /dev/sdb
命令,你可以找出像提供商名字、序列号、或产品名这样的东西。
为了保证思路清晰,确保每次只添加一个新属性。我们(和在网上看到的其他人)在 udev 规则中所遇到的大多数错误都是因为一次添加了太多的属性,而奇怪为什么不能正常工作了。逐个测试属性是最安全的作法,这样可以确保 udev 能够成功识别到你的设备。
安全
编写 udev 规则当插入一个驱动器后自动去做一些事情,将带来安全方面的担忧。在我的机器上,我甚至都没有打开自动挂载功能,而基于本文的目的,当设备插入时,脚本和规则可以运行一些命令来做一些事情。
在这里需要记住两个事情。
- 聚焦于你的 udev 规则,当你真实地使用它们时,一旦让规则发挥作用将触发脚本。执行一个脚本去盲目地复制数据到你的计算上,或从你的计算机上复制出数据,是一个很糟糕的主意,因为有可能会遇到一个人拿着和你相同品牌的 U 盘插入到你的机器上的情况。
- 不要在写了 udev 规则和脚本后忘记了它们的存在。我知道哪个计算上有我的 udev 规则,这些机器一般是我的个人计算机,而不是那些我带着去开会或办公室工作的计算机。一台计算机的 “社交” 程度越高,它就越不能有 udev 规则存在于它上面,因为它将潜在地导致我的数据最终可能会出现在某个人的设备、或某个人的数据中、或在我的设备上出现恶意程序。
换句话说就是,随着一个 GNU 系统提供了一个这么强大的功能,你的任务是小心地如何使用它们的强大功能。如果你滥用它或不小心谨慎地使用它,最终将让你出问题,它非常可能会导致可怕的问题。
现实中的 Udev
现在,你可以确认你的脚本是由 udev 触发的,那么,可以将你的关注点转到脚本功能上了。到目前为止,这个脚本是没有用的,它除了记录脚本已经运行过了这一事实外,再没有做更多的事情。
我使用 udev 去触发我的 U 盘的 自动备份 。这个创意是,将我正在处理的文档的主副本保存在我的 U 盘上(因为我随身带着它,这样就可以随时处理它),并且在我每次将 U 盘插入到那台机器上时,这些主文档将备份回我的计算机上。换句话说就是,我的计算机是备份驱动器,而产生的数据是移动的。源代码是可用的,你可以随意查看 attachup
的代码,以进一步限制你的 udev 的测试示例。
虽然我使用 udev 最多的情况就是这个例子,但是 udev 能抓取很多的事件,像游戏手柄(当连接游戏手柄时,让系统去加载 xboxdrv 模块)、摄像头、麦克风(当指定的麦克风连接时用于去设置输入),所以应该意识到,它能做的事情远比这个示例要多。
我的备份系统的一个简化版本是由两个命令组成的一个过程:
SUBSYSTEM=="block", ATTRS{idVendor}=="03f0", ACTION=="add", SYMLINK+="safety%n"
SUBSYSTEM=="block", ATTRS{idVendor}=="03f0", ACTION=="add", RUN+="/usr/local/bin/trigger.sh"
第一行使用属性去检测我的 U 盘,这在前面已经讨论过了,接着在设备树中为我的 U 盘分配一个符号链接,给它分配的符号连接是 safety%n
。这个 %n
是一个 udev 宏,它是内核分配给这个设备的任意数字,比如 sdb1、sdb2、sdb3、等等。因此 %n
应该是 1 或 2 或 3。
这将在 dev 树中创建一个符号链接,因此它不会干涉插入一个设备的正常过程。这意味着,如果你在自动挂载设备的桌面环境中使用它,将不会出现问题。
第二行运行这个脚本。
我的备份脚本如下:
#!/usr/bin/bash
mount /dev/safety1 /mnt/hd
sleep 2
rsync -az /mnt/hd/ /home/seth/backups/ && umount /dev/safety1
这个脚本使用符号链接,这将避免出现 udev 命名导致的意外情况(例如,假设一个命名为 DISK 的 U 盘已经插入到我的计算机上,而我插入的其它 U 盘恰好名字也是 DISK,那么第二个 U 盘的卷标将被命名为 DISK_
,这将导致我的脚本不会正常运行),它在我喜欢的挂载点 /mnt/hd
上挂载了 safety1
(驱动器的第一个分区)。
一旦 safely
挂载之后,它将使用 rsync 将驱动器备份到我的备份文件夹(我真实使用的脚本用的是 rdiff-backup
,而你可以使用任何一个你喜欢的自动备份解决方案)。
udev 让你的设备你做主
udev 是一个非常灵活的系统,它可以让你用其它系统很少敢提供给用户的方式去定义规则和功能。学习它,使用它,去享受 POSIX 的强大吧。
本文内容来自 Slackermedia Handbook,它以 GNU Free Documentation License 1.3 许可证授权使用。
via: https://opensource.com/article/18/11/udev
作者:Seth Kenlon 选题:lujun9972 译者:qhwdw 校对:wxy
本文转载来自 Linux 中国: https://github.com/Linux-CN/archive