linux udev 机制实现-1-规则文件
在上一章中对 udev 的执行机制以及 udev 的规则文件做了一个简要的介绍,在这一章节,则着重将焦点放在如何编写规则文件上,毕竟,这才是系统管理员最常用到的知识点。
udev 规则
正如前文中所说,规则文件通常以 .rules 结尾,实际上,规则文件并本身没有太多的意义,最重要的是规则文件中的规则,而 udevd 守护进程也是逐条地去匹配规则,而规则文件主要作用就是对规则进行组织,方便管理。
udev 的规则遵循以下的原则:
- 每条规则必须只占一行,可以使用 \ 来连接编辑器中的多行,这和宏定义是相似的
- 每条规则分为两部分:match(匹配) 和 action(执行动作),这其实很好理解,match 的作用在于确定当前规则是否适用,匹配成功也就是当前规则适用于当前设备时,再执行相应的动作,即 action。每一部分存在一个或多个条目,以逗号分隔。
- 规则中的条目主要分为三个部分:key、operator、value,key 是键,也可以理解为关键字,operator 是操作符,而 value 是值。比如 SUBSYSTEM==”foo”。
对于每一条规则来说,match 和 action 没有特定的区分标志,而是通过 operator(操作符) 进行区分,在大部分的编程语言中,”==” 表示逻辑判断,而 “=” 或 “+=” 之类的属于赋值,所以使用 “==” 或者 “!=” 这种逻辑判断操作符的就是 match 类的条目,而使用赋值类操作符的就是 action 部分。
规则中的每个条目中的键用于匹配或者作为关键字被赋值,一个非常重要的问题就是:它是怎么来的,我们怎么知道它的值是多少?只有知道它的由来,才能针对性地编写对应的规则。
实际上,我们可以把所有的键看成是 udev 的关键字。
一方面,从内核发送到用户空间的信息通常是这样的(以 RTC 为例):
change@/devices/platform/ocp/44e3e000.rtc/rtc/rtc0/omap_rtc_scratch0 ACTION=change DEVPATH=/devices/platform/ocp/44e3e000.rtc/rtc/rtc0 omap_rtc_scratch0 SUBSYSTEM=nvmemSYNTH_UUID=0SEQNUM=4266
这是 beaglebone 平台上触发 rtc 时从内核发送到用户空间的设备信息,在内核信息中,DEVPATH、SUBSYSTEM、SYNTH_UUID 会被自动添加为关键字。
另一方面,udevd 默认提供了许多关键字,比如 SYMLINK 表示软链接,RUN 表示需要执行的程序,同时用户也可以自定义关键字,直接使用 KEY=”foo” 就可以定义一个 KEY 关键字。
看看下面这一条规则示例:
SUBSYSTEM=="rtc", KERNEL=="rtc0", SYMLINK+="rtc", OPTIONS+="link_priority=-100"
这是 50-udev-default.rules 中一条针对 rtc 设备的规则,其中:
SUBSYSTEM=="rtc", KERNEL=="rtc0"
这是规则中的 match 部分,判断当前内核设备消息的 SUBSYSTEM 是否为 “rtc”,同时判断 KERNEL(设备内核名) 是否为 “rtc0”,如果两者都满足条件,则执行后面的 action 部分:
SYMLINK+="rtc", OPTIONS+="link_priority=-100"
第一条 action 条目为 SYMLINK+=”rtc”,表示为设备 rtc0 创建一个软链接,名为 rtc,同时设置 OPTION 属性,这个属性将会在其它地方被使用到。
在你的 linux 系统上,你可以通过 ls -l /dev/rtc* 来查看系统中的 rtc 相关信息,通常是这样的:
lrwxrwxrwx ... /dev/rtc -> rtc0
crw------- ... /dev/rtc0
如果你没有手动地更改系统 rtc 设置,可以看到,/dev/rtc 是指向 /dev/rtc0 的软链接,这个软链接的创建正是由上述的规则所创建,而 /dev/rtc0 则是由内核默认创建的。
udev 规则文件的编写
udev 操作符
在 udev 中,支持的操作符有:
- “==” : 用于 match(匹配),用于判断相等。
- “!=” : 用于 match,用于判断不等于
- “=” : 用于 action ,表示赋值
- “-=” : 用于 action ,表示从值列表中删除指定的值
- “+=” :用于 action,表示将新值附加到值列表中
- “:=” :用于 action,表示绝对赋值,即该操作指定的值就是最终值,禁止后续再进行修改。
udev 关键字
udev 定义了多个关键字,这些关键字用于匹配或者赋值,在实际的 udev 匹配过程中,还支持针对父级设备进行匹配,而不仅仅是生成设备事件的设备自身,linux 中大量的设备由各类总线进行连接,形成树状的设备拓扑结构,所以在新内核中使用设备树在软件上对所有设备进行描述,而总线本身也被视为设备的一种,父子设备之间通常具有强相关性,因此针对父级(或者祖父级)进行模糊地匹配是合理的。
udev 匹配关键字
ACTION
设备事件的操作类型,有以下几种:
- KOBJ_ADD:添加设备
- KOBJ_REMOVE:移除设备
- KOBJ_CHANGE:设备变动
- KOBJ_MOVE:移动设备
- KOBJ_ONLINE:设备上线
- KOBJ_OFFLINE:设备离线
- KOBJ_BIND:设备绑定
- KOBJ_UNBIND:设备解绑
在内核中使用一个枚举结构来定义设备操作类型:
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_BIND,
KOBJ_UNBIND,
KOBJ_MAX
};
其中,KOBJ_MAX 表示设备支持操作的最大值,用于判断指定的设备操作是否超出限制。
DEVPATH
设备对应的路径,该关键字以及对应的值通常包含在内核传递给用户空间的 netlink 消息中。
KERNEL
设备在内核中的名称,内核名称是指设备在sysfs里的名称,也就是默认的设备文件名称,例如”sda”,这个值通常不包含在 netlink 消息中。
NAME
匹配网络接口的名称。仅在先前的规则中已将 NAME 键赋值的前提下,才可将此键用于匹配,也就是默认情况下,NAME 不会被自动赋值。
SYMLINK
匹配指向此设备节点的软连接的名称。 仅在先前的规则中已将 SYMLINK 键赋值的前提下,才可将此键用于匹配。 可能有多个软连接指向同一个设备节点,但只需其中的一个匹配成功即可。
SUBSYSTEM
匹配设备所属的子系统,该关键字以及对应的值通常包含在内核传递给用户空间的 netlink 消息中。
DRIVER
匹配设备的驱动程序名称。仅在设备事件发生时,此设备确实正好绑定着一个驱动程序情况下,此键才会被设置。因此 netlink 消息中只是可能存在该关键字。
ATTR{file}
匹配设备在sysfs中的属性值。属性值中的尾部空白会被忽略,除非指定的值自身就包含尾部空白。大括号中的”文件”是指设备路径(devpath)下的文件。 例如,对于 /dev/sda1 来说,ATTR{size} 的含义其实是指 /sys/block/sda/sda1/size 文件的内容。
SYSCTL{kern_param}
匹配”内核参数”的值。所谓”内核参数”其实是指 /proc/sys/ 中的”内核参数”。例如,可以用 SYSCTL{kernel/hostname} 匹配 /proc/sys/kernel/hostname 的值。
ENV{attr}
设备属性,这些属性是由规则文件或者 udevd 程序中定义的,比如 “DEVTYPE”, “ID_PATH”, “SYSTEMD_WANTS” 等等,相当于环境变量。
PROGRAM
执行指定的程序并检查返回值, 如果返回值为零,则匹配成功,否则匹配失败。 设备的属性会转化为该程序的环境变量供其使用。 同时该程序的标准输出会被 自动保存在 RESULT 键中。
注意,仅可用于执行时间很短的前台程序。
RESULT
匹配最近一次 PROGRAM 程序的输出字符串, 必须位于 PROGRAM 之后(但可出现在同一条规则中)。
TAG
匹配设备的标签,udev 规则中可以为设备指定标签。
上述的关键字中,同时存在多种带后缀 “S” 的版本,即 “KERNELS”,”SUBSYSTEMS”,”DRIVERS”,”ATTRS”,”TAGS”等(具体参考官方文档),这些带后缀 S 的关键字和不带 S 的实现同样的功能,唯一的区别在于:带后缀 “S” 的关键字可以针对父级进行匹配,比如 KERNELS=”foo”,只要当前设备的任一级父级设备满足该条件,即匹配成功。
统配符
udev 的匹配规则中支持统配符的使用:
“*”
匹配任意数量的字符(包括零个)
“?”
匹配单独一个字符
“[]”
匹配中括号内的任意一个字符。 例如 “tty[SR]” 可以匹配 “ttyS” 或 “ttyR” 。 还可以使用 “-“ 符号表示一个区间。 例如 “[0-9]” 可以匹配任意数字。 如果在左括号 “[“ 后紧接着一个 “!” 则表示匹配非括号内的字符。
“|”
用于分隔两个可相互替代的匹配模式(也就是”或”的意思)。 例如 “abc|x*” 的意思是匹配 “abc” 或 “x*”
udev 赋值关键字
NAME
设置网络接口的名称。实际上,udev 并不能直接修改设备节点的名称, 它只能为设备节点创建额外的符号链接(相当于添加了别名)。
SYMLINK
设置指向此设备节点的 软连接名称。
只需在多个名称之间使用空格分隔,即可一次指定多个软连接名称。 如果为多个不同的设备指定了相同的软连接, 那么实际的软连接将指向 link_priority 值最高的设备。 如果 link_priority 值最高的设备被移除, 那么该软连接将重新指向下一个 link_priority 值最高的设备,以此类推。 对于未指定 link_priority 值或者 link_priority 值相等的设备, 它们之间的顺序是不确定的。
符号连接的名称必须不能与内核的默认名称相同, 否则会得到无法预知的结果。
OWNER, GROUP, MODE
设置设备节点的属主、属组、权限。会覆盖内置的默认值。
ATTR{filename}
设置在sysfs中的设备属性。大括号中的”文件”是指设备路径(devpath)下的文件。 例如,对于 /dev/sda1 来说,ATTR{size} 的含义其实是指 /sys/block/sda/sda1/size 文件的内容。
ENV{attr}
设置设备的属性。例如 “DEVTYPE”, “ID_PATH”, “SYSTEMD_WANTS” 等等。可以通过 udevadm info –query=property /dev/sda 命令查看 /dev/sda 的所有属性。 如果属性名以 “.” 开头,那么此属性将不会被记录到udev数据库中,也不会被导出为环境变量(例如 PROGRAM)。
TAG
设置设备的标签。 用于为libudev监视(monitor)功能的用户过滤事件或者枚举已标记的设备。 标签仅在与特殊的设备过滤器一起使用时才有意义,千万不要用于常规目的。 滥用标签将会导致设备事件处理效率显著下降, 所以应该尽量避免为设备设置标签。
RUN{type}
对于每一个设备事件来说,在处理完所有udev规则之后, 都可以再接着执行一个由此键设置的程序列表(默认为空)。 不同的”类型”含义如下:
“program”
一个外部程序, 如果是相对路径, 那么视为相对于 /usr/lib/udev 目录。 否则必须使用绝对路径。如果未明确指定”类型”, 那么这是默认值。
“builtin”
与 program 类似, 但是仅用于表示内置的程序。程序名与其参数之间用空格分隔。 如果参数中含有空格,那么必须使用单引号(‘)界定。
仅可使用运行时间非常短的前台程序, 切勿设置任何 后台守护进程或者长时间运行的程序。
设备事件处理完成之后, 所有派生的进程(无论是否已经分离), 都将会被无条件的杀死。
注意,禁止在 udev 规则中运行访问网络、或挂载/卸载文件系统的程序, 因为在 systemd-udevd.service 中强制使用了默认沙盒机制。
LABEL
设置一个可用作 GOTO 跳转目标的标签。
GOTO
跳转到下一个匹配的 LABEL 标签所在的规则。
IMPORT{type}
将一组变量导入为设备的属性。不同的”类型”含义如下:
“program”
执行一个外部程序, 并且当其返回值为零时导入其输出内容。 注意,输出内容的每一行都必须符合”key=value”格式。 关于程序路径、命令与参数分隔符、引号的使用规则、程序执行时间,等等, 都与 RUN 相同。“builtin”
与 “program” 类似, 但是仅用于执行内置的程序。“file”
导入一个文本文件的内容。该文本文件的每一行都必须符合”key=value”格式。 以”#”开头的行将被视为注释而忽略。“db”
从当前已有的udev数据库中导入一个单独的属性。 仅可用于udev数据库确实 已经被早先的设备事件所填充的情形。“cmdline”
从内核引导选项导入一个单独的属性。对于那些仅有单独的标记而没有值的属性, 其值将被指定为 “1” 。“parent”
从父设备导入已有的属性(包括对应的值)。 可以将 IMPORT{parent} 赋值为shell风格的匹配模式, 以导入 多个属性名称与匹配模式相符的属性。仅可使用运行时间非常短的前台程序,切勿设置任何后台守护进程或者长时间运行的程序。
OPTIONS
规则与设备的选项:
link_priority=value
指定创建符号链接时的优先级。 数值越大优先级越高。默认值是”0”。string_escape=none|replace
在对设备进行命名时,如何处理设备名字中的非常规字符(比如控制字符与不安全的字符)。 none 表示不做处理,保持原样; replace 表示将这些非常规字符替换为”_”(下划线)。static_node=
将本条规则设定的权限 应用到此选项指定的静态设备节点上。 同时,如果在本规则中指定了标签(tag), 那么还会在 /run/udev/static_node-tags/tag 目录中创建一个指向该静态设备节点的软连接。 注意,在 systemd-udevd 启动之前, 静态设备节点就已经由 systemd-tmpfiles 创建完成了。 创建静态设备节点时,并不要求存在对应的内核设备, 因为当这些设备节点被访问时,会触发内核模块的自动加载功能。watch
使用文件系统的 inotify 功能监视设备节点。 当节点被打开并写入之后又被关闭, 将会触发一个”设备状态已变化”的事件。nowatch
禁用针对设备节点的 inotify 监视功能。db_persist
在事件设备的 udev 数据库项上设置 粘滞位(sticky bit)。 这样,即使调用了 udevadm info –cleanup-db 命令, 设备的属性也依然会保存在数据库中。 在某些情况下(例如 Device Mapper 设备), 此选项可用于从 initramfs 切换至真实的根文件系统时,依然保持设备的状态。
字符替换
NAME, SYMLINK, PROGRAM, OWNER, GROUP, MODE, SECLABEL, RUN 都支持简单的字符串替换。 RUN 的替换发生在 所有规则全部处理完成之后、程序将要执行之前, 因此可以使用由匹配成功的规则所设置的设备属性。 而其他键的替换发生在该键所在规则被处理完成的当时。 可用的替换标记如下:
$kernel, %k
设备的内核名称
$number, %n
设备在内核中的序号。例如,对于 “sda3” 来说,此值为 “3”
$devpath, %p
设备路径(devpath)。也就是该设备在sysfs文件系统下的相对路径。例如,/dev/sda1 对应的设备路径是 /block/sda/sda1 (一般对应着 /sys/block/sda/sda1 目录)。
$id, %b
被 SUBSYSTEMS, KERNELS, DRIVERS, ATTRS 成功匹配到的设备的设备名称
$driver
被 SUBSYSTEMS, KERNELS, DRIVERS, ATTRS 成功匹配到的设备的驱动名称
$attr{文件}, %s{文件}
在规则匹配成功时, 设备路径(devpath)下”文件”的内容(用于表示设备的属性)。 如果该设备路径下没有此文件,则从先前 KERNELS, SUBSYSTEMS, DRIVERS, ATTRS 匹配的父设备中提取。
如果”文件”是一个软连接, 则一直追踪软连接到最终的实际文件。
$env{属性}, %E{属性}
设备的属性值。例如 “DEVTYPE”, “ID_PATH”, “SYSTEMD_WANTS” 等等。[提示]可以通过 udevadm info –query=property /dev/sda 命令查看 /dev/sda 的所有属性。
$major, %M
设备的主设备号
$minor, %m
设备的次设备号
$result, %c
外部程序 PROGRAM 的输出字符串。 可以使用 “%c{N}” 提取第N个子字符串(以空格为分隔符,从”1”开始计数)。 也可以通过 “%c{N+}”(也就是在数字后附加一个 “+”)提取 从第N个子字符串开始一直到结尾的部分。
$parent, %P
父设备的节点名称
$name
设备的当前名称。如果没有被任何udev规则修改, 那么等于该设备的内核名称。
$links
一个空格分隔的软链接名称列表,这些软链接都指向该设备的节点。 该值仅在两种情况下存在:(1)发生”remove”事件;(2)先前的规则已对 SYMLINK 赋值。
$root, %r
udev_root 的值
$sys, %S
sysfs 文件系统的挂载点
$devnode, %N
设备节点的名称(也就是设备文件的名称)
%%
百分号 “%” 自身
$$
美元符号 “$” 自身
尽管规则只能被写作一行,并不代表设备的匹配只能使用一行,udev 也可以通过条规则来处理一个设备事件,比如第二条依赖于第一条的执行结果,以实现更灵活的配置,自然地,也就出现了很多赋值关键字和匹配关键字相同的情况。
比如,当系统中增加了一个 rtc 设备时,匹配到的第一条规则读取相应的信息,比如 /sys 目录下的文件、内核传递的信息等,然后把某个有用的信息赋值给某个关键字,在后续的规则中,通过判断该关键字的值再执行其它的逻辑,udev 中这种做法很常见。