9.11. 热插拔: 热插拔

9.11.1. 介绍

hotplug 次系统核心,以动态方式加载适当的驱动程且 (在 udevd 的协助下)添加对应的设备文件,处理加入与移除设备的作业。当代的硬件与虚拟化,几乎每个对象都是热插拔:从常见的 USB/PCMCIA/IEEE 1394 周边到 SATA 硬盘,以及 CPU 与内存。

核心内的数据库有每个设备的 ID 及其驱动程序。在启动阶段加载此数据库,侦测各接口的周边设备,并在运行中侦测热插入的设备。接收到插入的设备后,送出消息给 udevd,让其添加对应的条目于 /dev/ 内。

9.11.2. 命名问题

在热插拔连接出现之前,很容易给设备赋予一个固定的名字。可以通过设备在总线上的位置简单命名。但是,如果设备能够在总线上来去自如,这就行不通了。典型的例子是数码相机和U 盘,两者都表现为磁盘驱动器。前者可能链接为 /dev/sdb ,后者可能是 /dev/sdc (假设/dev/sda 代表计算机自身的硬盘)。设备名是不固定的;它取决于设备连接的顺序。

另外,越来越多的驱动使用动态值作为设备的主、次设备号,这样就更不可能给设备赋予静态入口,而重启之后这些基本的特性也随之变化。

udev 正是为了解决该问题而创立的。

实践 网络管理

当代的电脑有多张网络卡 (两张网络卡加一个 wifi 接口),以及在多个接口支持 热插拔,Linux 核心不保证给这些网络接口固定的名称。不过,用户却需要在配置 /etc/network/interfaces 时,需用到固定的名称!

很难要求每个用户建立自己的 udev 规则来解决此问题。因此 udev 使用一种很特殊的方式来进行配置;首次启动(更通俗的说,每次新网卡出现的时候),它会使用网络接口的名字和网卡的MAC 地址创建新规则,并在随后的启动中赋予同样的名字。这些规则储存在 /etc/udev/rules.d/70-persistent-net.rules

此机制有个副作用值得注意。只有一个 PCI 网卡的电脑。自然把网络接口命名为 eth0。然后,网卡坏了,换个新的;新网卡的 MAC 地址当然不同。旧网卡已有名称,eth0,新网卡被命名为 eth1,实际上,eth0 网卡已经不会再回来了 (且网络不会运作如常,因为 /etc/network/interfaces 配置一个 eth0 接口)。在此情况下,重新开机前先删除 /etc/udev/rules.d/70-persistent-net.rules 文件。新的网卡就会取得 eth0 名称。

9.11.3. udev 如何工作

udev 被核心告知有个新的设备,它参考 /sys/ 里对应的条目,搜集该设备的信息,尤其是那些足辨别的独特信息 (网卡的 MAC 地址、某些 USB 设备的序号)。

有了这些信息之后,然后 udev 会查阅/etc/udev/rules.d//lib/udev/rules.d/中包含的所有规则。在这个过程中,它会决定如何命名设备,创建什么样的符号连接(赋予设备另外的名字),执行什么命令。查询所有的文件,顺序(除非文件中使用“GOTO”指令)检查所有的规则。这样,有可能一个事件(event)对应多个规则。

规则文件的语法很简单:每行包含选择规则和变量赋值。前者用于需要对那些事件作出相应,后者定义采取何种行动。它们通过逗号分隔,用运算符区分选择规则(使用比较运算符,例如== or !=)或赋值指令(使用 =, +=:=运算符)。

比较运算符用于如下变量:

  • 内核:内核赋予设备的名字;

  • 行动:与事件相对应的行动(“add”当设备被添加,“remove”当设备被移除);

  • DEVPATH:设备在 /sys/ 记录中的路径;

  • SUBSYSTEM:产生请求的内核子系统(有很多这样的子系统,少数的例子是“usb”,“usb”,“net”,“firmware”,等);

  • ATTR{*属性*}属性 文件的内容在设备的 /sys/*$devpath*/ 文件夹内。可在此找到 MAC 地址及其他辨识用的总线;

  • KERNELSSUBSYSTEMSATTRS{*attributes*} 是用来匹配当前设备父设备的选项变量;

  • PROGRAM:指明要运行的测试程序(真则返回0)。程序的输出内容会被储存以便 RESULT 测试重用;

  • RESULT:对最后一次调用 PROGRAM产生的结果进行检查。

右操作数可以使用模式表达式来同时匹配几个值。比如, * 匹配任何字符串(甚至是空字符串); ? 匹配任何一个字符, [] 匹配方括号中间的字符集(如果首字符是惊叹号标示求反集,连续的字符集可表示为如 a-z)。

关于赋值操作符, =用来赋值(并取代当前值);如果用在列表上,列表被清空并只包含赋予的新值。 := 做同样工作,但是它会阻止随后对该变量的更改。至于 +=,则是给列表添加新项目。如下的变量可以被更改:

  • NAME:将在 /dev/中创建的设备文件名。只有第一次赋值起作用;其它的会被忽略;

  • SYMLINK:指向同一个设备的符号列表清单;

  • OWNER, GROUPMODE 指示拥有该设备的用户和组,还有相关的访问许可;

  • RUN:响应事件时执行的程序清单。

赋予这些变量的值可以使用一系列的替代表示:

  • $kernel 或者 %k:等价于 KERNEL

  • $number 或者 %n:设备的顺序号码,例如,对 sda3,它就是“3”;

  • $devpath%p:等价于 DEVPATH

  • $attr{*attribute*}%s{*attribute*}: 等价于 ATTRS{*attribute*}

  • $major%M:设备的内核主设备号;

  • $minor%m:内核次设备号码;

  • $result%cPROGRAM设定的最后一个程序输出的字符串;

  • 最后, %%$$ 相应代表百分号和美元符号。

以上的清单仍不完备 (只包括最重要的参数),详细的数据在 udev(7) 手册页面。

9.11.4. 一个具体例子

我们来考虑一个U盘并给它指派固定名字的例子。首先,必须要找到能唯一识别它的元素。可以插入并运行 udevadm info -a -n /dev/sdc ( 用指派给U盘的名字代替/dev/sdc )。

  1. #

可以通过检测设备变量,还有父设备变量创建新的规则。上面的例子运行我们创建两个这样的规则:

  1. KERNEL=="sd?", SUBSYSTEM=="block", ATTRS{serial}=="M004021000001", SYMLINK+="usb_key/disk"
  2. KERNEL=="sd?[0-9]", SUBSYSTEM=="block", ATTRS{serial}=="M004021000001", SYMLINK+="usb_key/part%n"

这些规则在文件中设定,例如该文件名为 /etc/udev/rules.d/010_local.rules,就可以移除和重新插入U盘了。可以看到文件/dev/usb_key/disk 代表和U盘相关联的磁盘,/dev/usb_key/part1 是它的第一个扇区。

进阶 调试udev配置

如同其他的后台进程,udevd 把日志保存在 /var/log/daemon.log。但是缺省是不啰唆的,且不足以了解发生的事。udevadm control --log-priority=info 命令增加其详细的内容并可解决此问题。udevadm control --log-priority=err 则回到缺省的层次。