Udev 简介:用于管理设备事件的 Linux 子系统

创建一个脚本,当插入特定设备时,触发您的计算机执行特定操作。
335 位读者喜欢这篇文章。
A new presciption for open source health care

Opensource.com

Udev 是 Linux 子系统,为您的计算机提供设备事件。简单来说,它就是检测您何时将设备插入计算机的代码,例如网卡、外部硬盘驱动器(包括 USB 闪存驱动器)、鼠标、键盘、操纵杆和游戏手柄、DVD-ROM 驱动器等等。 这使其成为一个潜在有用的实用程序,并且它充分暴露,以至于标准用户可以手动编写脚本来执行某些操作,例如在插入特定硬盘驱动器时执行某些任务。

本文教您如何创建一个由某些 udev 事件触发的 udev 脚本,例如插入特定的闪存驱动器。 一旦您了解了使用 udev 的过程,您就可以使用它来完成各种事情,例如在连接游戏手柄时加载特定的驱动程序,或者在连接备份驱动器时执行自动备份。

基本脚本

使用 udev 的最佳方法是分小块进行。不要预先编写整个脚本,而是从一些简单地确认 udev 触发某些自定义事件的内容开始。

根据您脚本的目标,您无法保证您会亲眼看到脚本的结果,因此请确保您的脚本记录它已成功触发。 日志文件的常用位置在 /var 目录中,但这主要是 root 用户的领域。 对于测试,请使用 /tmp,普通用户可以访问它,并且通常会在重启时清除。

打开您最喜欢的文本编辑器并输入这个简单的脚本

#!/usr/bin/bash

/usr/bin/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 必须知道在什么条件下它应该调用脚本。 在现实生活中,您可以通过颜色、制造商以及您刚刚将其插入计算机这一事实来识别闪存驱动器。 但是,您的计算机需要一组不同的标准。

Udev 通过序列号、制造商甚至供应商 ID 和产品 ID 号来识别设备。 由于这处于您的 udev 脚本生命周期的早期阶段,因此尽可能广泛、非特定且包容一切。 换句话说,您首先希望捕获几乎任何有效的 udev 事件来触发您的脚本。

使用 udevadm monitor 命令,您可以实时访问 udev,并查看当您插入不同的设备时它看到了什么。 成为 root 用户并尝试一下。

$ su
# udevadm monitor

monitor 功能打印接收到的事件,用于:

  • UDEV:udev 在规则处理后发出的事件
  • KERNEL:内核 uevent

udevadm monitor 运行时,插入一个闪存驱动器,并观察各种信息喷射到您的屏幕上。 请注意,事件类型是 ADD 事件。 这是识别您想要的事件类型的好方法。

udevadm monitor 命令提供了很多有用的信息,但是如果您知道您的闪存驱动器当前位于 /dev 树中的哪个位置,您可以使用命令 udevadm info 以更漂亮的格式查看它。 如果不知道,请拔下并重新插入您的闪存驱动器,然后立即发出此命令

$ su -c 'dmesg | tail | fgrep -i sd*'

例如,如果该命令返回 sdb: sdb1,您就知道内核已为您的闪存驱动器分配了 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 规则必须包含来自单个父设备的一个属性。

父属性是描述设备最基本级别的事物,例如它是已插入物理端口的东西它是具有大小的东西这是一个可移动设备

由于 sdb 的 KERNEL 标签可能会根据您插入该闪存驱动器之前插入了多少其他驱动器而变化,因此这不是 udev 规则的最佳父属性。 但是,它适用于概念验证,因此您可以使用它。 更好的候选者是 SUBSYSTEM 属性,它标识这是一个“block”系统设备(这就是 lsblk 命令列出设备的原因)。

/etc/udev/rules.d 中打开一个名为 80-local.rules 的文件,并输入以下代码

SUBSYSTEM=="block", ACTION=="add", RUN+="/usr/local/bin/trigger.sh"

保存文件,拔下您的测试闪存驱动器,然后重启。

等等,在 Linux 机器上重启

理论上,您只需发出 udevadm control --reload,它应该加载所有规则,但在游戏的这个阶段,最好消除所有变量。 Udev 足够复杂,您不希望整夜躺在床上想知道该规则不起作用是因为语法错误,还是您应该重启。 因此,无论您的 POSIX 自尊心告诉您什么,都要重启。

当您的系统重新上线时,切换到文本控制台(使用 Ctl+Alt+F3 或类似操作)并插入您的闪存驱动器。 如果您运行的是最新的内核,当您插入驱动器时,您可能会在控制台中看到大量输出。 如果您看到类似Could not execute /usr/local/bin/trigger.sh 的错误消息,您可能忘记使脚本可执行。 否则,希望您看到的只是插入了一个设备,它获得了一些内核设备分配,等等。

现在,真相大白了

$ cat /tmp/udev.log
Tue Oct 31 01:35:28 NZDT 2035

如果您从 /tmp/udev.log 中看到最近的日期和时间返回,则 udev 已成功触发您的脚本。

将规则细化为有用的东西

此规则的问题在于它非常通用。 插入鼠标、闪存驱动器或其他人的闪存驱动器都会不加区分地触发您的脚本。 现在是开始关注您要触发脚本的确切闪存驱动器的时候了。

一种方法是使用供应商 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 表示 idVendor 和 idProduct 属性。 您也可以在 udevadm info -a -n /dev/sdb | grep vendor 的输出中看到这些数字,但我发现 lsusb 的输出更容易查看。

您现在可以将这些属性包含在您的规则中。

SUBSYSTEM=="block", ATTRS{idVendor}=="03f0", ACTION=="add", RUN+="/usr/local/bin/thumb.sh"

测试这个(是的,您仍然应该重启,只是为了确保您从 udev 获得新鲜的反应),它应该像以前一样工作,只是现在如果您插入例如由不同公司制造的闪存驱动器(因此具有不同的 idVendor)或鼠标或打印机,则不会触发脚本。

继续添加新属性以进一步关注您要触发脚本的唯一闪存驱动器。 使用 udevadm info -a -n /dev/sdb,您可以找到供应商名称、有时是序列号或产品名称等信息。

为了您自己的理智,请务必一次只添加一个新属性。 我犯过的大多数错误(并且在线看到其他人犯过)是将一堆属性扔到他们的 udev 规则中,并想知道为什么该规则不再起作用。 逐个测试属性是确保 udev 可以成功识别您的设备的最安全方法。

安全

这引发了编写 udev 规则以在插入驱动器时自动执行某些操作的安全问题。 在我的机器上,我什至没有打开自动挂载,但本文提出了脚本和规则,这些脚本和规则仅通过插入某些东西来执行命令。

这里有两件事要牢记。

  1. 一旦您的 udev 规则开始工作,就集中精力,以便它们在您真正想要它们触发脚本时才触发。 如果任何碰巧携带相同品牌闪存驱动器的人将其插入您的计算机,则执行盲目地将数据复制到您的计算机或从您的计算机复制数据的脚本是一个坏主意。
  2. 不要编写您的 udev 规则和脚本然后忘记它们。 我知道哪些计算机上有我的 udev 规则,这些计算机通常是我的个人计算机,而不是我带到会议或在办公室工作的计算机。 计算机越“社交化”,它就越不可能在其上获得 udev 规则,这可能会导致我的数据最终出现在别人的设备上,或者别人的数据或恶意软件出现在我的设备上。

换句话说,与 GNU 系统提供的许多功能一样,您有责任注意您如何运用这种力量。 如果您滥用它或未能尊重它,它很可能会变得非常糟糕。

真实世界中的 Udev

既然您已经确认您的脚本是由 udev 触发的,您就可以将注意力转向脚本的功能。 现在,它毫无用处,除了记录它已被执行的事实之外什么也不做。

我使用 udev 来触发闪存驱动器的 自动备份。 想法是我的活动文档的主副本在我的闪存驱动器上(因为它随处可见,并且可能随时进行处理),并且每次我将驱动器插入该计算机时,这些主文档都会备份到我的计算机。 换句话说,我的计算机是备份驱动器,我的生产数据是移动的。 源代码是可用的,所以请随意查看 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"

第一行使用已讨论的属性检测我的闪存驱动器,然后在设备树中为闪存驱动器分配一个符号链接。 它分配的符号链接是 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 的闪存驱动器插入了我的计算机,并且我插入了我的另一个也称为 DISK 的闪存驱动器,则第二个闪存驱动器将被标记为 DISK_,这将破坏我的脚本)。 它将 safety1(驱动器的第一个分区)挂载在我首选的挂载点 /mnt/hd

安全挂载后,它使用 rsync 将驱动器备份到我的备份文件夹(我的实际脚本使用 rdiff-backup,您可以根据自己的喜好使用任何自动备份解决方案)。

Udev 是您的开发工具

Udev 是一个非常灵活的系统,使您能够以其他系统很少敢于向用户提供的方式定义规则和功能。 学习并使用它,享受 POSIX 的力量。

本文基于 Slackermedia Handbook 中的内容构建,该手册根据 GNU 自由文档许可证 1.3获得许可。

标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。 他曾在电影和计算机行业工作,而且经常同时从事这两个行业。

6 条评论

echo $date > /tmp/udev.log

在此处不起作用 ((Xubuntu 18.04 with bash),最好使用

/bin/date > /tmp/udev.log

干杯!

感谢您的提醒,Lars。 您说得很对,我本意是写 $(date),但括号不知何故丢失了。

在编写过程中,该测试脚本从一个不仅仅返回日期的脚本中精简而来。 既然它所做的只是返回日期,那么 `echo` 是不必要的。

我已经更新了文章。

很棒的文章。
不过,我想知道的是 Udev 和 Systemd 的 .device 单元之间的当前关系。 它们是 Udev 规则的替代品吗?
谢谢!

好问题,Osqui。 Systemd .device 单元可以执行相同的任务,这就是我的下一篇文章的主题(旨在作为本文的配套文章)。

但是,udev 仍然很重要,原因有两个:首先,并非所有 Linux 发行版都附带 systemd,其次,有些东西对 Systemd 不可见,但对 udev 可见,因此您必须使用 udev 标签来提示 Systemd 采取行动。

考虑到这两个因素,我不清楚 Systemd 是否打算最终取代 udev,或者只是提供一种实现许多相同结果的额外手段。 我并不总是喜欢 udev(我仍然不相信它实际上能够在不重启的情况下重新加载其规则),但我很讨厌完全失去它,因为我的 Slackware 系统不运行 Systemd。

回复 作者:Osqui (未验证)

很棒的文章!

也许您可以阐明这个与 udev 相关的问题

我经常摆弄 arduino 和其他 usb 编程板。 它们中的一些需要拔下并重新插入才能进入“刷写模式”。

但是当我重新插入它们时,udev 为符号链接分配了一个新名称,从 /dev/ttyUSB0 切换到 /dev/ttyUSB1(例如)。 然后我的 IDE 找不到我需要重新配置的板。

我认为这与超时和重用旧链接有关,但我不确定如何修复它。

有什么想法吗?

我不确定我是如何错过这条评论的,抱歉回复晚了。

我想尝试的方法(不确定是否有效,因为我实际上没有测试过)是创建一个 udev 规则,该规则通过一些相当通用的属性来识别 arduino。 创建一个规则,以便在插入 arduino 时,将其分配到您选择的某个标准位置。 类似于...

ACTION=="add", YOUR-RULES-TO-DETECT-THE-ARDUINO RUN+="/usr/bin/mkdir /dev/arduino"
ACTION=="remove", YOUR-RULES-TO-DETECT-THE-ARDUINO RUN+="/usr/bin/rmdir /dev/arduino"

不确定它是否会起作用,但值得一试。

回复 作者:Alfonso E.M. (未验证)

© . All rights reserved.