linux_udev机制实现-3

liaocj 2022-09-20 16:04:03
Categories: > > > Tags:

udev builtin 接口

从内核 uevent 事件机制到 netlink 通信,再到用户空间的 udevd 守护进程对内核信息的规则匹配,以及如何使用官方工具调试 udev,讲到这里,udev 的整个流程基本上已经浮出水面了.

我们可以通过阅读 /lib/udev/rules.d/50-udev-default.rules 规则文件来检验学习成果,这个规则文件为系统提供了大量的默认规则,其中包括各种基础设备的处理,完整地阅读这个文件可以大致了解到 /dev 下的各个设备文件,软链接,以及文件权限是如何设置的.

udev 的 builtin(內建) 接口,大多数时候用于对.link文件的解析,其原理和.rules类似

udev 目前被集成到了 systemd 中,所以它的源代码地址为:TODO.

下载程序时要注意下载对应版本的 systemd 源码,systemd 的版本之间是不兼容的,所以在安装使用 udev 的时候,如果使用的 udevadm 客户端程序和 udevd 守护进程的版本不匹配,并不能保证它能完全正常运行。前后版本互不兼容也是 systemd 快速迭代带来的一个弊端。在 systemd 逐渐步入成熟之后这个问题应该会逐步得到解决。

builtin 接口

首先来看看 /lib/udev/rules.d/50-udev-default.rules 中与 builtin 相关的规则:

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"
SUBSYSTEM=="input", ENV{ID_INPUT}=="", IMPORT{builtin}="input_id"
ENV{MODALIAS}!="", IMPORT{builtin}="hwdb --subsystem=$env{SUBSYSTEM}"

builtin 为内置程序,需要通过 IMPORT 关键字导入,也就是 IMPORT{builtin}=”function-name” 表示执行內建的 function-name 函数,执行內建函数会输出一系列 “key=value” 值,这些键值对将会导入到 udev 的全局变量中,相当于在规则中使用 ENV 关键字创建全局变量。

builtin 源码

builtin 接口的源码实现位于 systemd 源码 src/udev/udev-builtin.c 中,所有 udev 支持的內建接口都定义在指针数组 builtins 中:

static const struct udev_builtin *builtins[] = {
#if HAVE_BLKID
        [UDEV_BUILTIN_BLKID] = &udev_builtin_blkid,
#endif
        [UDEV_BUILTIN_BTRFS] = &udev_builtin_btrfs,
        [UDEV_BUILTIN_HWDB] = &udev_builtin_hwdb,
        [UDEV_BUILTIN_INPUT_ID] = &udev_builtin_input_id,
        [UDEV_BUILTIN_KEYBOARD] = &udev_builtin_keyboard,
#if HAVE_KMOD
        [UDEV_BUILTIN_KMOD] = &udev_builtin_kmod,
#endif
        [UDEV_BUILTIN_NET_ID] = &udev_builtin_net_id,
        [UDEV_BUILTIN_NET_LINK] = &udev_builtin_net_setup_link,
        [UDEV_BUILTIN_PATH_ID] = &udev_builtin_path_id,
        [UDEV_BUILTIN_USB_ID] = &udev_builtin_usb_id,
#if HAVE_ACL
        [UDEV_BUILTIN_UACCESS] = &udev_builtin_uaccess,
#endif
};

通过上述贴出的源码定义可以发现,udev 支持的內建接口有:

內建接口的调用方式和在 udev 规则中调用 RUN 命令执行脚本是一样的,比如:

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"

上述规则演示了当内核发生 usb 子系统相关事件,且 DEVTYPE 为 “usb_device” 时,调用內建函数 usb_id,随后再调用內建函数 hwdb,并传递命令行参数:–subsystem=usb,可以将內建接口看成是一个完整的程序。

上文中列出的內建接口列表中每个成员都是一个结构体指针,指向一个 struct udev_builtin 类型的结构体

struct udev_builtin {
    // 名称
    const char *name;
    // 真正执行的函数
    int (*cmd)(struct udev_device *dev, int argc, char *argv[], bool test);
    ...
};

该结构体中的 name 成员对应內建接口的名称,udev 规则中直接使用名称即可调用內建的接口,比如上述的 usb_id 正是 UDEV_BUILTIN_USB_ID 类型的內建接口。而真正执行的接口为 cmd 函数。

接下来,我们简要地分析几个常用的內建接口所实现的功能,如果你对这部分的具体实现有兴趣,也可以仔细阅读相关源代码。

udev_builtin_hwdb

hwdb 简介

硬件数据库文件(hwdb)位于操作系统发行商维护的 /usr/lib/udev/hwdb.d 目录中,以及系统管理员维护的 /etc/udev/hwdb.d 目录中。所有的 hwdb 文件(无论位于哪个目录中),统一按照文件名的字典顺序处理。对于不同目录下的同名 hwdb 文件,仅以 /etc/udev/hwdb.d 目录中的那一个为准。依据这个特性,系统管理员可以使用 /etc/udev/hwdb.d 目录中的自定义文件替代 /usr/lib/udev/hwdb.d 目录中的同名文件。 如果系统管理员想要屏蔽 /usr/lib/udev/hwdb.d 目录中的某个 hwdb 文件, 那么最佳做法是在 /etc/udev/hwdb.d 目录中创建一个指向 /dev/null 的同名符号链接, 即可彻底屏蔽 /usr/lib/udev/hwdb.d 目录中的同名文件。 注意,硬件数据库文件必须以 .hwdb 作为后缀名,否则将被忽略。

每个硬件数据库文件(hwdb)都包含一系列由”matche”与关联的”key-value”组成的记录。 每条记录由一个或多个用于匹配的”matche”字符串(可包含shell风格的通配符)开头, 多个”matche”字符串之间使用换行符分隔,但必须是依次紧紧相连的行(也就是中间不能出现空行), 每一行都必须是一个完整的”matche”字符串(也就是不能将一个”matche”字符串分为两行), 多行之间是逻辑或(OR)的关系。 每一个”matche”字符串都必须顶行书写(也就是行首不能是空白字符)。

“matche”行之后是一个或多个以空格开头的”key-value”行(必须以空格开头作为区分), “key-value”行必须符合 “key=value” 格式。 一个空白行表示一条记录结束。 以 “#” 开头的行将被视为注释而被忽略。

如果有多条记录的”matche”串都匹配同一个给定的查找字符串,那么所有匹配记录中的”key-value”都将被融合在一起。如果某个”key”出现了多次,那么仅以最高优先级记录中的”value”为准(每个”key”仅允许拥有一个单独的”value”)。 对于不同硬件数据库文件(hwdb)中的记录来说,文件名的字典顺序越靠后,优先级越高; 对于同一个硬件数据库文件(hwdb)中的记录来说, 记录自身的位置越靠后,优先级越高。

所有 hwdb 文件都将被 systemd-hwdb 编译为二进制格式的数据库,并存放在 /etc/udev/hwdb.bin 文件中。 注意,操作系统发行商应该将预装的二进制格式的数据库存放在 /usr/lib/udev/hwdb.bin 文件中。 系统在运行时,仅会读取二进制格式的硬件数据库。

下面是一个 hwdb 文件的片段,以帮助我们对 hwdb 文件建立一个基本概念:

...
usb:v1D6B*
 ID_VENDOR_FROM_DATABASE=Linux Foundation

usb:v1D6Bp0001*
 ID_MODEL_FROM_DATABASE=1.1 root hub

usb:v1D6Bp0002*
 ID_MODEL_FROM_DATABASE=2.0 root hub

usb:v1D6Bp0003*
 ID_MODEL_FROM_DATABASE=3.0 root hub
...

该片段取自 /lib/udev/hwdb.d/20-usb-vendor-model.hwdb,ubuntu 18.04 中使用这个文件保存 USB 的 UID:PID 相关信息.对于一个 USB 设备而言,UID:PID 是跟随硬件的,且对于某一类设备是唯一的,因此内核可以在驱动程序中获取 USB 设备的 UID:PID 值,并将它们通过 /sysfs 导出或者通过 netlink 发送到用户空间,udevd 则通过获取的 UID:PID 到数据库中查询,获取相应的数据库信息.

比如上面贴出的 hwdb 文件片段中,VID 1D6B 是被 linux 占用的,PID 为 0001~0003 都被注册为 linux 的 root hub,当当前触发事件的 UID/PID 与数据库中的字段匹配时,ID_MODEL_FROM_DATABASE 这个变量就被赋值为 1.1/2.0/3.0 root hub,并导入到 udev 的全局变量中,udevd 通过这些信息就可以判断当前 root hub 的 USB 类型,并执行一些相应的程序逻辑.

udev_builtin_hwdb 內建接口被定义在 src/udev/udev-builtin-hwdb.c 中:

const struct udev_builtin udev_builtin_hwdb = {
        .name = "hwdb",
        .cmd = builtin_hwdb,
        .init = builtin_hwdb_init,
        ...
};

builtin_hwdb_init 为 hwdb 初始化函数,在 systemd 初始化 hwdb 时调用一次,它调用 sd_hwdb_new 函数,该函数将会读取以下文件:

也就是说,hwdb 中的数据并不取决于 hwdb.d 中的各个以 .hwdb 结尾的硬件数据库文件,而是取决于这些硬件数据库文件最后编译生成的 bin 文件.

在 hwdb 內建程序被调用时,主要是调用 cmd 回调函数,即 builtin_hwdb 函数:

static int builtin_hwdb(struct udev_device *dev, int argc, char *argv[], bool test) {

    // 命令行参数解析部分,主要是对搜索过程进行控制,支持四种命令行参数:
    //     --filter,-f : 指定搜索过滤规则
    //     --device,-d : 指定其它设备
    //     --subsystem,-s : 指定搜索子系统
    //     --lookup-prefix,-p : 指定搜索前缀

    // 搜索函数
    if (udev_builtin_hwdb_search(dev, srcdev, subsystem, prefix, filter, test) > 0)
            return EXIT_SUCCESS;
    return EXIT_FAILURE;
}

搜索函数是 builtin hwdb 的核心部分,它将设备关联属性转换成 hwdb 数据中的 match 格式,再使用这些属性逐个匹配数据库中的数据.

比如,对于 USB 而言,它先读取设备属性中的 idVendor(vid) 和 idProduct(pid),并对属性进行拼接,代码如下:

v = udev_device_get_sysattr_value(dev, "idVendor");
p = udev_device_get_sysattr_value(dev, "idProduct");
vn = strtol(v, NULL, 16);
pn = strtol(p, NULL, 16);
snprintf(s, size, "usb:v%04Xp%04X*", vn, pn);
return s;

最后将返回结果字符串作为匹配关键字去匹配数据库,如果匹配成功,就将数据库中的数据部分(键值对)导入到 udev 全局变量中.

在平常的使用中,网络算是比较重要的部分了,通常跟它打交道的时间最多,同时,因为使用得多,网络接口问题也是非常频繁的.

同样的,udev 负责网络接口的生成以及配置问题,比如网络接口名,比如:为什么 ubuntu 上默认网络接口名为 ens33,而嵌入式设备上通常是 eth0,它们是如何被生成的,又比如为什么我的两台嵌入式设备的 MAC 地址是相同的,理论上 mac 地址不应该是全球唯一的吗?除了最常见的这两个问题,网络还有很多设置问题也是通过 udev 来解决.

网络相关的两个內建函数,一个是 udev_builtin_net_id,另一个是 udev_builtin_net_setup_link,前者负责硬件接口的变量导出,而后者则是真正负责网络相关参数设置的接口.

udev_builtin_net_id 的定义在 src/udev/udev-builtin-net_id.c 中:

const struct udev_builtin udev_builtin_net_id = {
        .name = "net_id",
        .cmd = builtin_net_id,
};

执行 IMPORT{builtin}=”net_id” 时调用 cmd 回调函数,也就是 builtin_net_id 接口:

static int builtin_net_id(struct udev_device *dev, int argc, char *argv[], bool test) {
    // 读取网卡对应 sysfs 路径下(下文中的文件读取都是基于设备路径)的 type 文件,确定当前链路层是以太网还是 slip 网络
    // 读取 ifindex 和 iflink 文件,确定两者的值一致,以确定目标设备是符合处理条件的
    // 获取设备类型,设备类型可能是 wlan/wwan,以此确定命名前缀,wlan 前缀为 wl,wwan 对应 ww,以太网对应 en,而 slip 网络对应 sl.
    // 获取设备 mac 地址,这个 mac 地址是通过读取 address 文件获取的,但同时要满足一个条件:addr_assign_type 文件中的值不为 0,addr_assign_type 表示 mac 地址的确定方式,这个值后文中详解.
    // 将上述确定的 前缀+x+mac地址 进行字符串连接,并赋值给全局变量 ID_NET_NAME_MAC,比如:ID_NET_NAME_MAC=enx00049f0593e6
    // 处理硬件接口:网络设备的接口可能是多种,比如 PCI,USB,根据硬件接口的不同导出 ID_NET_NAME_PATH 和 ID_NET_NAME_SLOT 两个变量.
}

builtin_net_id 主要工作还是导出全局变量给后续的规则使用.

接下来看看核心的部分:udev_builtin_net_setup_link,通过这个接口可以直接对网络设备接口进行设置,udev_builtin_net_setup_link 接口的源代码在 src/udev/udev-builtin-net_setup_link.c 中:

const struct udev_builtin udev_builtin_net_setup_link = {
    .name = "net_setup_link",
    .cmd = builtin_net_setup_link,
    .init = builtin_net_setup_link_init,
    .validate = builtin_net_setup_link_validate,
};

init 回调函数在初始化的时候调用,它的实现是这样的:

static int builtin_net_setup_link_init(struct udev *udev) {
    // 初始化上下文结构,准备执行环境
    ...
    link_config_load(ctx);
}

link_config_load 是初始化阶段的核心函数,这个函数主要是加载以及解析配置文件,对应的配置文件都是以 .link 结尾,分布在指定的目录中,这些目录有: /etc/system/network,/run/systemd/network,/usr/lib/systemd/network,lib/systemd/network, 这些 .link 配置文件用于控制网络接口的各项参数,link 文件的相关信息可以参考文档:

如果你没有更改过你的系统,大概率在 /lib/systemd/network 下有一个 99-default.link 文件,这是系统对网络接口默认的配置,和 systemd 的配置语法一样,配置文件支持多个小节,每个小节有多个选项,而 link_config_load 对 .link 文件的解析结果将保存在 link_config 的结构体中,以备后续使用.

初始化只有一次,当每次执行 IMPORT{builtin}=”net_setup_link” 时,将会调用到 cmd 回调函数,即 builtin_net_setup_link:

static int builtin_net_setup_link(struct udev_device *dev, int argc, char **argv, bool test) {
    // 获取设备的 driver 属性,即该设备在内核中的 driver 名称.并赋值给全局变量 ID_NET_DRIVER,需要注意的是,内核并不会对所有的网络接口都导出 driver 属性. 需要通过内核到用户空间的 nelink 信息确定.
    // 上文中初始化过程中解析 .link 文件会生成 link_config 结构体.使用当前网络设备的信息与 .link 文件中进行匹配,如果匹配成功,就获取对应的 link_config 结构体,作为配置参数.(具体的匹配需要参考 .link 文件中的 [match] 小节中的配置项).

    // 应用所有配置.这个函数主要操作网络接口有:speed,wol,mac,name 等接口配置,需要注意的是,在此之前当前网络接口的命名和 mac 地址都存在默认的值,mac 的默认值为内核导出的 address 文件值,接口名为内核中导出的网络接口名,比如 eth0 .link 配置文件只是在默认值的基础上进行修改,如果没有匹配成功的 .link 文件,网络接口也是可以以默认参数存在并正常使用.

    link_config_apply(ctx, link, dev, &name);
        /*
        根据 .link 文件的解析结果设置的 speed 设置当前网络接口的 speed.
        设置 wol,wol 为 wake on line,远程唤醒
        获取并验证 ifindex 文件内容并验证其值

        获取 .link 文件中指定的 NamePolicy,翻译过来就是命名策略,命名策略一共有以下多种:
            kernel:由内核设置固定的名称,如果设置了该值,且 name_assign_type 文件中的值为 2,3,4,其它所有的策略设置都会失效.
            database:基于网卡的 ID_NET_NAME_FROM_DATABASE 属性值(来自于udev硬件数据库)设置网卡的名称。
            onboard:基于网卡的 ID_NET_NAME_ONBOARD 属性值(来自于板载网卡固件)设置网卡的名称。
            slot:基于网卡的 ID_NET_NAME_SLOT 属性值(来自于可插拔网卡固件)设置网卡的名称。
            path:基于网卡的 ID_NET_NAME_PATH 属性值(来自于网卡的总线位置)设置网卡的名称。
            mac:基于网卡的 ID_NET_NAME_MAC 属性值(来自于网卡的固定MAC地址)设置网卡的名称。
            keep:如果网卡已经被空户空间命名(创建新设备时命名或对已有设备重命名), 那么就保留它(不进行重命名操作)。
        在 .link 文件中,可以设置多种命名策略的组合,优先级从先到后,如果前面的策略被成功实施,后续的策略就会被省略,默认的 /lib/systemd/network/99-default.link 中就指定了 NamePolicy=kernel database onboard slot path.

        同时,前面的策略并不一定被成功实施,比如,设置为 kernel 时,需要在 .link 配置文件中同时设置 Name= 选项,又比如设置为 database 时,ID_NET_NAME_FROM_DATABASE 不能为空,为空表示策略实施失败,尝试下一个,直到 new_name 不为空.

        对于 mac 的设置,同样依赖于 .link 文件中的 MACAddressPolicy= 选项,mac 策略支持两种:
            persistent:内核确定的固定 mac 地址.
            random:随机生成的 mac 地址.
            none:无条件使用内核提供的 mac 地址
        同时需要结合 addr_assign_type 文件中的值,addr_assign_type 是内核导出的文件,用户空间的设置需要和内核适配.比如配置文件中设置为 random,但是内核导出的  addr_assign_type 文件中值为 0,代表 persistent,则不会设置.
        同时,当没有指定 mac policy 时,直接在 .links 文件中指定 MACAddress= 也可以指定 mac 地址,不够要符合 mac 地址的格式规范.
        */
}

udev_builtin_path_id

UDEV_BUILTIN_PATH_ID 这个內建接口主要作用是找到并导出当前设备在 /sys/devices 中的存在路径,它的源码位于:src/udev/udev-builtin-path_id.c 中:

const struct udev_builtin udev_builtin_path_id = {
        .name = "path_id",
        .cmd = builtin_path_id,
        ...
};

同样的,在执行 IMPORT{builtin}=”path_id” 时会调用到 builtin_path_id 函数,这个函数实现比较简单,从当前节点向上索引,一级一级地找到 subsystem 所属的父级目录,并赋值给 ID_PATH,比如:ID_PATH=platform-4a100000.ethernet,其中 - 表示目录分隔符 / 的替换。

udev_builtin_usb_id

udev_builtin_usb_id 这个內建接口负责导出 usb 设备的相关信息,具体的定义在 src/udev/udev-builtin-usb_id.c 中:

const struct udev_builtin udev_builtin_usb_id = {
        .name = "usb_id",
        .cmd = builtin_usb_id,
        ...
};

当执行 IMPORT{builtin}=”usb_id” 时会调用到 cmd 回调函数,即 builtin_usb_id,builtin_usb_id 这个函数的实现比较繁琐,但是总结起来又比较简单:就是通过读取设备目录下的各个属性文件,然后将这些属性文件中的数据复制给全局变量,然后导入到 udev 中。这些全局变量以及对应的属性文件如下:

小结

內建接口通常完成两项工作:

需要注意的是,这篇文章并没有仔细地去分析每个內建接口的实现细节,而只是将內建接口的概念、源代码地址以及主要的內建接口进行了概括,如果各位有兴趣,可以下载源码进行研究。

/sys 下的文件是否可以通过规则修改.
是不是同时设置 policy=kernel 和 name= 才会指定对应的网卡名称.
如果设置了该值,且 name_assign_type 文件中的值为 2,3,4,其它所有的策略设置都会失效.
kernel:由内核设置固定的名称,如果设置了该值,且 name_assign_type 文件中的值为 2,3,4,其它所有的策略设置都会失效.

参考:http://www.jinbuguo.com/systemd/hwdb.html

udevadm test 使用.