Kernel Korner - udev—用户空间的持久设备命名

作者:Greg Kroah-Hartman

从 2.5 内核开始,系统中所有的物理和虚拟设备都以层级方式在用户空间中通过 sysfs 可见。当任何设备被添加到系统或从系统中移除时,/sbin/hotplug 会向用户空间提供通知。利用这两个特性,现在可以实现动态 /dev 的用户空间实现,它可以提供灵活的设备命名策略。

本文讨论了 udev,这是一个取代 devfs 功能的程序。它在任何时刻为系统中的设备提供 /dev 条目。它还提供了以前仅通过 devfs 无法实现的功能,例如当设备在设备树中移动时,为设备提供持久命名、灵活的设备命名方案、设备更改的外部系统通知以及将所有命名策略移出内核。

/dev 目录是 Linux 机器上所有设备文件应该存放的位置。设备文件详细说明了用户程序如何访问特定的硬件设备或功能。例如,设备文件 /dev/hda 传统上用于表示系统中的第一个 IDE 驱动器。名称 hda 对应于主设备号和次设备号,内核使用这些号码来确定与哪个硬件设备通信。目前,已经定义了广泛的名称范围,这些名称范围与不同的主设备号和次设备号相匹配。

所有主设备号和次设备号都分配了一个名称,该名称与设备类型相匹配。此分配由 Linux 分配名称和号码机构 (LANANA) 完成,当前设备列表可以在其网站上找到(请参阅在线资源部分)。

随着 Linux 开始支持新型设备,这些设备需要分配主设备号和次设备号范围,以便用户可以通过 /dev 目录访问它们。另一种选择是通过文件系统提供访问;我在 2002 年 linux.conf.au 会议上的论文更详细地介绍了如何做到这一点(请参阅在线资源部分)。在内核版本 2.4 及更早版本中,有效的主设备号范围为 1–255,有效的次设备号范围为 1–255。由于这个有限的范围,在 2.3 开发周期中,新的主设备号和次设备号的分配被冻结。此冻结已被解除,2.6 内核已将有效的主设备号范围增加到 4,095。每个主设备号可用的次设备号超过一百万个。

哪个 /dev 条目是哪个设备

当内核找到新的硬件时,它通常会为该类型的硬件分配下一个主/次设备号对。因此,在启动时,找到的第一个 USB 打印机将被分配主设备号 180 和次设备号 0,这在 /dev 中被引用为 /dev/usb/lp0。第二个 USB 打印机将被分配主设备号 180 和次设备号 1,这在 /dev 中被引用为 /dev/usb/lp1。如果用户重新排列 USB 拓扑,例如添加一个 USB 集线器以支持系统中更多的 USB 设备,则打印机的 USB 探测顺序可能会在下次计算机启动时发生变化,从而颠倒了不同次设备号到两台打印机的分配。

对于几乎任何可以在计算机通电时移除或添加的设备类型,情况都是如此。随着支持 PCI 热插拔的系统和热插拔总线(例如 IEEE 1394、USB 和 CardBus)的出现,几乎所有设备都存在这个问题。

随着 2.5 内核中 sysfs 文件系统的出现,确定哪个设备次设备号分配给哪个物理设备的问题变得容易得多。对于具有两个不同 USB 打印机的系统,sysfs /sys/class/usb 目录树将如下所示

/sys/class/usb/
|-- lp0
|   |-- dev
|   |-- device -> ../../../devices/pci0/00:09.0/usb1/1-1/1-1:0
|   `-- driver -> ../../../bus/usb/drivers/usblp
`-- lp1
    |-- dev
    |-- device -> ../../../devices/pci0/00:0d.0/usb3/3-1/3-1:0
    `-- driver -> ../../../bus/usb/drivers/usblp

$ cat /sys/class/usb/lp0/device/serial
HXOLL0012202323480
$ cat /sys/class/usb/lp1/device/serial
W09090207101241330

在 lp0/device 和 lp1/device 符号链接指向的各个 USB 设备目录中,可以确定许多 USB 特定的信息,例如设备的制造商和(希望是唯一的)序列号。

从上面的描述中的 serial 文件可以看出,/dev/usb/lp0 设备文件与序列号为 HXOLL0012202323480 的 USB 打印机相关联,而 /dev/usb/lp1 设备文件与序列号为 W09090207101241330 的 USB 打印机相关联。如果这些打印机被移动,例如将它们都放在 USB 集线器后面,则它们可能会被重命名,因为它们在启动时以不同的顺序被探测到

$ tree /sys/class/usb/
/sys/class/usb/
|-- lp0
|   |-- dev
|   |-- device -> ../../../devices/pci0/00:09.0/usb1/1-1/1-1.1/1-1.1:0
|   `-- driver -> ../../../bus/usb/drivers/usblp
`-- lp1
    |-- dev
    |-- device -> ../../../devices/pci0/00:09.0/usb1/1-1/1-1.4/1-1.4:0
    `-- driver -> ../../../bus/usb/drivers/usblp

$ cat /sys/class/usb/lp0/device/serial
W09090207101241330
$ cat /sys/class/usb/lp1/device/serial
HXOLL0012202323480

如本描述所示,由于这种不同的探测顺序,/dev/usb/lp0 设备现在被分配给序列号为 W09090207101241330 的 USB 打印机。

sysfs 使用户能够确定内核已将哪个设备分配给哪个设备文件。这是一个强大的关联,以前不容易获得。但是,用户通常不关心 /dev/usb/lp0 和 /dev/usb/lp1 现在被反转,并且应该在某个配置文件中更改。用户只是希望能够打印到正确的打印机,无论它在 USB 设备树中的哪个位置。

/dev 太大了

大多数发行版的 /dev 目录中的并非所有设备文件都与当前连接到计算机的物理设备相匹配。相反,/dev 目录是在机器上初始化操作系统时创建的,/dev 目录中填充了所有已知的可能名称。在运行 Red Hat 的 Fedora release 1 的机器上,/dev 目录包含超过 18,000 个不同的条目。对于试图准确确定当前存在哪些设备的用户来说,如此多的条目很快变得笨拙。

devfs

由于 /dev 目录中设备文件数量众多,许多操作系统已转向让内核本身管理 /dev 目录,因为内核始终确切地知道系统上存在哪些设备。它通过创建一个名为 devfs 的基于 RAM 的文件系统来实现这一点。Linux 也有这个选项,并且随着时间的推移,它在许多不同的发行版中变得流行,包括 Gentoo。

对于许多人来说,devfs 解决了他们的迫切需求。但是,基于 Linux 的 devfs 实现仍然存在许多未解决的问题。最值得注意的是,它不提供使用持久名称创建设备节点的能力。

udev 的目标

鉴于前面提到的问题,udev 项目启动了。其目标是在用户空间中运行;创建动态 /dev;如果需要,提供一致的设备命名;并提供用户空间 API 以访问有关当前系统设备的信息。有关 udev 与 devfs 的比较的更多信息,请参阅在线资源部分。

第一个项目,在用户空间中运行,是通过利用以下事实来实现的:/sbin/hotplug 为添加到系统或从系统中移除的每个设备生成事件,sysfs 的能力可以显示有关所有设备的所有必要信息。

第二个项目,创建动态 /dev,是通过捕获所有 /sbin/hotplug 事件、在 sysfs 中查找添加设备的主设备号和次设备号,并使用内核为设备分配的名称创建 /dev 文件来处理的。如果设备已从系统中移除,则很容易移除该设备的 /dev 条目。

udev 在 2003 年 4 月实现了前两个目标,使用的编译代码非常小,只有 6Kb,这证明了捕获热插拔事件和使用 sysfs 的方案是可行且非常容易实现的。自 2003 年初的简陋开端以来,udev 已经实现了其所有目标。它使用户能够使用灵活的基于规则的系统以持久的方式命名设备。

udev 的规则包含在 /etc/udev/udev.rules 文件中,并描述了用户希望以不同于默认内核名称的方式命名的任何设备。以下是 udev.rules 文件的示例

# if /sbin/scsi_id returns "OEM 0815" device will
# be called disk1
BUS="scsi", PROGRAM="/sbin/scsi_id", \
RESULT="OEM 0815", NAME="disk1"

# USB printer to be called lp_color
BUS="usb", SYSFS_serial="W09090207101241330", \
NAME="lp_color"

# SCSI disk with a specific vendor and model number
# is to be called boot
BUS="scsi", SYSFS_vendor="IBM", \
SYSFS_model="ST336", NAME="boot"

# sound card with PCI bus id 00:0b.0 to be called dsp
BUS="pci", ID="00:0b.0", NAME="dsp"

# USB mouse at third port of the second hub to
# be called mouse1
BUS="usb", PLACE="2.3", NAME="mouse1"

# ttyUSB1 should always be called pda with two
# additional symlinks
KERNEL="ttyUSB1", NAME="pda", \
SYMLINK="palmtop handheld"

# multiple USB webcams with symlinks to be called
# webcam0, webcam1, ...
BUS="usb", SYSFS_model="XV3", NAME="video%n", \
SYMLINK="webcam%n"

udev 规则定义了设备属性和所需设备文件名之间的映射。为此,可以从设备查询多个键以确定匹配项。如果在 udev.rules 文件中未找到匹配项,则使用默认内核名称。以下是 udev 理解的不同类型的键的列表

  • BUS:匹配设备的总线类型;这方面的示例包括 PCI、USB 或 SCSI。

  • KERNEL:匹配内核为设备提供的名称。

  • ID:匹配总线上的设备号;例如,PCI 总线 ID 或 USB 设备 ID。

  • PLACE:匹配总线上的拓扑位置,例如 USB 设备插入的物理端口。

  • SYSFS_filename, SYSFS{filename}:允许 udev 匹配任何 sysfs 设备属性,例如标签、供应商、USB 序列号或 SCSI UUID。在一个规则中最多可以检查五个不同的 sysfs 文件,并且需要所有值才能匹配该规则。

  • PROGRAM:允许 udev 调用外部程序并检查结果。如果程序成功返回,则此键有效。程序返回的字符串也可以与 RESULT 键匹配。

  • RESULT:匹配上次 PROGRAM 调用的返回字符串。此键可以在 PROGRAM 调用之后的任何规则中使用。

在不同的键之后,指定了 NAME 和可选的 SYMLINK。NAME 是 udev 在规则匹配时用于调用设备的名称,SYMLINK 指定了生成的任何符号链接(如果有)。可以一次指定多个符号链接,多个符号链接之间用空格隔开。NAME 和 SYMLINK 文件都可以包含目录,以简化 /dev。

示例

回到我们的双打印机示例。为了以一致的方式命名这两个设备,可以使用以下两个 udev 规则

BUS="usb", SYSFS_serial="W09090207101241330", \
NAME="lp_color"
BUS="usb", SYSFS_serial="HXOLL0012202323480", \
NAME="lp_plain"

这些规则使 udev 查找两台打印机的 sysfs 文件 serial,并根据文件中的值,将打印机命名为 lp_color 或 lp_plain。这确保了无论哪个设备先插入或先检测到,或者是否向系统添加了另一个 USB 打印机,这两台打印机都具有相同的持久名称。

高级 udev 规则

udev 允许在 udev.rules 文件中的 NAME、SYMLINK 和 PROGRAM 字段中使用多个类似 printf 的字符串替换。这些字段是

  • %n:设备的内核编号;例如,设备 sda3 的内核编号为 3。

  • %k:设备的内核名称。

  • %M:设备内核主设备号。

  • %m:设备内核次设备号。

  • %b:设备的总线 ID。

  • %c:PROGRAM 返回的字符串。可以在此修饰符中添加一个数字,以仅拾取字符串中的特定单词。由于显而易见的原因,此字段在 PROGRAM 字段中不起作用。

  • %%:% 字符本身。

此外,许多不同的键支持简单的 shell 样式模式匹配。这些模式是

  • *:匹配零个、一个或多个字符。

  • ?:匹配任何单个字符,但不匹配零个字符。

  • [ ]:匹配方括号内指定的任何单个字符;例如,模式字符串tty[SR]将匹配ttySttyR。范围也在此匹配中受支持,使用 - 字符。例如,要匹配所有数字的范围,将使用模式 [0-9]。如果 [ 之后的第一个字符是 !,则匹配任何未包含的字符。

由于能够进行这些简单的字符串替换和字符串模式匹配,再加上 udev 运行任何其他程序并使用其结果的能力,udev 已成为命名设备的极其灵活的工具。作为这种强大功能的示例,请查看以下规则

KERNEL="[hs]d[a-z]", PROGRAM="name_cdrom.pl %M %m", \
NAME="%1c", SYMLINK="cdrom"

此规则匹配任何块设备,并使用设备的 Major 和 Minor 号调用 Perl 脚本 name_cdrom.pl。如果此程序成功,udev 将使用程序输出的第一个单词来命名设备,并创建一个名为 cdrom 的符号链接。name_cdrom.pl 脚本可以在 udev 版本中找到。

此脚本的作用是确定设备是否为 CD-ROM 设备。如果是,它会查询 Free CDDB 数据库,以查看设备中存在的 CD-ROM 是否在数据库中已知。如果是,则根据 CD 命名设备。例如,当使用此规则时,我的 /dev 看起来像这样

$ ls -l /dev/S* /dev/cdrom
brw-------  1 root root 22, 64 Feb 15 08:26 /dev/Samiam-Astray
lrwxrwxrwx  1 root root      8 Feb 15 08:26 /dev/cdrom ->
/dev/Samiam-Astray

这显示了 udev 如何跨 Internet 查询数据库以确定如何命名设备。是的,这是一个疯狂的命名方案,试图使用它,但它显示了 udev 可以多么强大和灵活。

致谢

作者要感谢 IBM 的 Daniel Stekloff,他在许多方面帮助塑造了 udev 的设计。没有他的毅力,udev 根本不会出现。此外,Kay Sievers 在实现 udev 中的大多数高级功能方面发挥了重要作用,最值得注意的是具有字符串修饰符和模式匹配的能力,这两者对于在现实世界中使用 udev 至关重要。没有他的帮助,udev 将不会像现在这样强大和有用。

此外,如果没有 Pat Mochel 的 sysfs 和驱动程序模型核心,udev 将不可能实现。作者感谢他承担了大多数人认为不可能完成的任务,并允许其他人轻松地基于他的通用框架进行构建,从而使所有用户都能看到内核跟踪的“蜘蛛在毒品上编织的网络”。

本文基于关于 udev 的 2002 年渥太华 Linux 研讨会论文(请参阅资源)。

本文资源: /article/7496

Greg Kroah-Hartman 目前是各种不同驱动程序子系统的 Linux 内核维护者。他在 IBM 工作,从事与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。

加载 Disqus 评论