从 systemd-udevd 运行 log 中研究其自动加载内核模块的过程

环境信息

linux 环境:debian10 系统

systemd 版本:systemd 241

运行环境:VMware 虚机环境

systemd-udevd 命令 help 信息

longyu@debian:~$ /lib/systemd/systemd-udevd --help
systemd-udevd [OPTIONS...]

Manages devices.

  -h --help                   Print this message
  -V --version                Print version of the program
  -d --daemon                 Detach and run in the background
  -D --debug                  Enable debug output
  -c --children-max=INT       Set maximum number of workers
  -e --exec-delay=SECONDS     Seconds to wait before executing RUN=
  -t --event-timeout=SECONDS  Seconds to wait before terminating an event
  -N --resolve-names=early|late|never
                              When to resolve users and groups

See the systemd-udevd.service(8) man page for details.

-D 参数可以使能 debug 信息,首先开启这个参数来观察 systemd-udevd 的输出信息。

systemd-udevd -D 参数前台运行

  1. 执行 sudo systemctl stop systemd-udevd 停止正在运行的进程
  2. 手动执行 sudo /lib/systemd/systemd-udevd -D

手动运行时终端的部分输出信息如下:

.........
Load module index
timestamp of '/etc/systemd/network' changed
timestamp of '/usr/lib/systemd/network' changed
Parsed configuration file /usr/lib/systemd/network/99-default.link
Created link configuration context.
timestamp of '/etc/udev/rules.d' changed
timestamp of '/lib/udev/rules.d' changed
Reading rules file: /usr/lib/udev/rules.d/50-firmware.rules
Reading rules file: /usr/lib/udev/rules.d/50-udev-default.rules
Reading rules file: /usr/lib/udev/rules.d/55-dm.rules
Reading rules file: /usr/lib/udev/rules.d/60-block.rules
Reading rules file: /usr/lib/udev/rules.d/60-cdrom_id.rules
Reading rules file: /usr/lib/udev/rules.d/60-drm.rules
.........
Rules contain 24576 bytes tokens (2048 * 12 bytes), 12517 bytes strings
1770 strings (22169 bytes), 1165 de-duplicated (10258 bytes), 606 trie nodes used

上面输出的大部分在读取并解析 udev 的规则,这些规则描述了 systemd-udevd 接收到一个事件时的处理过程。

虚拟机动态添加新的网络设备模拟热插拔

添加一个新的网络设备操作图示如下:
在这里插入图片描述
添加后终端输出信息中与加载模块相关的信息如下:

Successfully forked off 'n/a' as PID 2557.
0000:02:08.0: Worker [2557] is forked for processing SEQNUM=4168.
0000:02:08.0: Processing device (SEQNUM=4168, ACTION=add)
0000:02:08.0: IMPORT builtin 'hwdb' /usr/lib/udev/rules.d/50-udev-default.rules:14
0000:02:08.0: RUN 'kmod load $env{MODALIAS}' /usr/lib/udev/rules.d/80-drivers.rules:5
0000:02:08.0: sd-device: Created db file '/run/udev/data/+pci:0000:02:08.0' for '/devices/pci0000:00/0000:00:11.0/0000:02:08.0'
Loading module: pci:v00008086d0000100Fsv000015ADsd00000750bc02sc00i00
Module 'e1000' is already loaded
0000:02:08.0: Device (SEQNUM=4168, ACTION=add) processed
0000:02:08.0: sd-device-monitor: Passed 547 byte to netlink monitor

从上面的输出看 systemd-udevd 自动加载内核模块的过程可能有如下流程:

  1. 内部创建一个子进程处理从内核收到的 uevent 消息
  2. 根据消息的内容匹配 udev 规则,依次执行匹配到的规则对应的内部命令,这里 pci 设备会创建 MODALIAS 属性,udevd 匹配到 /usr/lib/udev/rules.d/80-drivers.rules 规则,进而执行 kmod load 命令,此命令的参数为 MODALIAS 属性的值
  3. 创建内部的数据库文件
  4. 使用 MODALIAS 匹配关联的内核驱动并判断是否加载,未加载就加载,已经加载则继续向下执行
  5. 事件处理完成后正常返回

在上述假想流程中,对于 udevd 规则与 udevd 从内核收到的 uevent netlink 消息的内容存在盲区,在进一步的分析前先查看下这些信息。

与加载内核模块相关的 udev 规则

50-udev-default.rules 规则信息:

longyu@debian:~$ sed -n '14p' /usr/lib/udev/rules.d/50-udev-default.rules
ENV{MODALIAS}!="", IMPORT{builtin}="hwdb --subsystem=$env{SUBSYSTEM}"

80-drivers.rules 规则信息:

longyu@debian:~$ sed -n '5p' /usr/lib/udev/rules.d/80-drivers.rules
ENV{MODALIAS}=="?*", RUN{builtin}+="kmod load $env{MODALIAS}"

关键的规则为 80-drivers.rules 规则,加载模块实际是通过 kmod load 命令完成的。

2. 模拟 udevd 从内核收到的 uevent netlink 消息的内容

使用如下程序(部分代码摘自 udevd 源码并进行了一些修改):

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <ctype.h>  
#include <sys/un.h>  
#include <sys/ioctl.h>  
#include <sys/socket.h>  
#include <linux/types.h>  
#include <linux/netlink.h>  
#include <errno.h>  
#include <unistd.h>  
#include <arpa/inet.h>  
#include <netinet/in.h>  

#ifndef NEWLINE
#define NEWLINE "\r\n"
#endif

#define UEVENT_BUFFER_SIZE 2048  

static int init_hotplug_sock()  
{  
  const int buffersize = 1024;  
  int ret;  

  struct sockaddr_nl snl;  
  bzero(&snl, sizeof(struct sockaddr_nl));  
  snl.nl_family = AF_NETLINK;  
  snl.nl_pid = getpid();  
  snl.nl_groups = 1;  

  int s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);  
  if (s == -1)   
  {  
	  perror("socket");  
	  return -1;  
  }  
  setsockopt(s, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));  

  ret = bind(s, (struct sockaddr *)&snl, sizeof(struct sockaddr_nl));  
  if (ret < 0)   
  {  
	  perror("bind");  
	  close(s);  
	  return -1;  
  }  

  return s;  
}  

char *truncate_nl(char *s) {

        s[strcspn(s, NEWLINE)] = 0;
        return s;
}

int device_new_from_nulstr(uint8_t *nulstr, size_t len) {
		int i = 0;
        int r;

        while (i < len) {
                char *key;
                const char *end;

                key = (char*)&nulstr[i];
                end = memchr(key, '\0', len - i);
                if (!end)
					return 0;

                i += end - key + 1;
                truncate_nl(key);
				printf("%s\n", key);	
        }
}	

int main(int argc, char* argv[])  
{  
	int hotplug_sock = init_hotplug_sock();  
	int bufpos;

  	while(1)  
  	{  
		int len;
  		/* Netlink message buffer */  
  		char buf[UEVENT_BUFFER_SIZE * 2];

		memset(&buf, 0x00, sizeof(buf));	
  		len = recv(hotplug_sock, &buf, sizeof(buf), 0);
		
		if (len <= 0)
			continue;
		
		printf("\nnew message:\n");
		bufpos = strlen(buf) + 1;
		
		printf("%s\n", buf);

		device_new_from_nulstr((uint8_t*)&buf[bufpos], len - bufpos);
  	}
  	return 0;  
 }

编译并运行此程序,重新添加一个网络设备后,终端输出信息如下:

new message:
add@/devices/pci0000:00/0000:00:03.1/0000:0a:00.0
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:03.1/0000:0a:00.0
SUBSYSTEM=pci
PCI_CLASS=20000
PCI_ID=8086:10D3
PCI_SUBSYS_ID=8086:0000
PCI_SLOT_NAME=0000:0a:00.0
MODALIAS=pci:v00008086d000010D3sv00008086sd00000000bc02sc00i00
SEQNUM=2068

new message:
add@/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/ptp/ptp4
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/ptp/ptp4
SUBSYSTEM=ptp
MAJOR=249
MINOR=4
DEVNAME=ptp4
SEQNUM=2069

new message:
add@/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/net/eth0
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/net/eth0
SUBSYSTEM=net
INTERFACE=eth0
IFINDEX=7
SEQNUM=2070

new message:
add@/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/net/eth0/queues/rx-0
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/net/eth0/queues/rx-0
SUBSYSTEM=queues
SEQNUM=2071

new message:
add@/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/net/eth0/queues/tx-0
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/net/eth0/queues/tx-0
SUBSYSTEM=queues
SEQNUM=2072

new message:
bind@/devices/pci0000:00/0000:00:03.1/0000:0a:00.0
ACTION=bind
DEVPATH=/devices/pci0000:00/0000:00:03.1/0000:0a:00.0
SUBSYSTEM=pci
DRIVER=e1000e
PCI_CLASS=20000
PCI_ID=8086:10D3
PCI_SUBSYS_ID=8086:0000
PCI_SLOT_NAME=0000:0a:00.0
MODALIAS=pci:v00008086d000010D3sv00008086sd00000000bc02sc00i00
SEQNUM=2073

可以看到内核触发了多个不同设备的 uevent 消息,每一个 uevent 消息都有自己的键值对,ACTION 表示 uevent 的动作,add 表示设备添加,DEVPATH 的值添加 /sys 前缀指向设备在 /sys 目录中的具体位置,SEQNUM 表示消息号,依次递增,SUBSYSTEM 表示设备所属的子系统。

当 systemd-udevd 收到一个上述类型的 uevent 消息时就会按照键值对的形式进行解析并设置内部的数据结构。

3. 2 中与 udevd 自动加载驱动相关的 netlink 消息

add@/devices/pci0000:00/0000:00:03.1/0000:0a:00.0
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:03.1/0000:0a:00.0
SUBSYSTEM=pci
PCI_CLASS=20000
PCI_ID=8086:10D3
PCI_SUBSYS_ID=8086:0000
PCI_SLOT_NAME=0000:0a:00.0
MODALIAS=pci:v00008086d000010D3sv00008086sd00000000bc02sc00i00
SEQNUM=2068

上述消息在被 udevd 解析后,pci 设备 0000:0a:00.0 的 MODALIAS 属性被设置,最终匹配到上文提到的 80-drivers.rules 规则完成模块加载过程。

当设备成功 probe 驱动后,设备在 sys 目录中对应的 uevent 文件内容如下:

longyu@virt-debian10:~$ cat /sys/devices/pci0000:00/0000:00:03.1/0000:0a:00.0/uevent
DRIVER=e1000e
PCI_CLASS=20000
PCI_ID=8086:10D3
PCI_SUBSYS_ID=8086:0000
PCI_SLOT_NAME=0000:0a:00.0
MODALIAS=pci:v00008086d000010D3sv00008086sd00000000bc02sc00i00

udevd 如何通过 MODALIAS 获取到需要加载的驱动?

udevd 通过 libkmod 模块来完成内核模块的查找、加载工作。libkmod 实际是通过解析 /lib/modules/$(uname -r)/modules.alias.bin 来获取不同设备使用的模块信息。

在我的虚拟机中使用 pci:v00008086d000010D3sv00008086sd00000000bc02sc00i00 中的前几个字段(表示 vendor id 与 device id 的字段)检索 /lib/modules/$(uname -r)/modules.alias 获取到如下信息:

longyu@virt-debian10:/lib/modules/4.19.0-8-amd64$ grep 'v00008086d000010D3' ./modules.alias
alias pci:v00008086d000010D3sv*sd*bc*sc*i* e1000e

最后一列 e1000e 表示此设备使用的驱动,获取到这个驱动后就可以调用 libkmod 提供的接口加载 e1000e 模块。

总结

本文从 systemd-udevd 的 manual 入手,通过 debug 选项获取 udevd 执行的 log 信息,通过扩大信息来为进一步研究 udevd 的代码做了铺垫。

这些信息定义了 udevd 的关键流程,有了这些信息再来理解它的实现就不那么困难了。

<think>嗯,用户问的是为什么Linux网卡会被命名两次。我需要先回忆一下相关的知识,然后逐步整理出一个清晰的解释。 首先,我记得在早期的Linux系统中,网卡命名是基于驱动加载顺序的,比如eth0、eth1这样的名字。这种方法有时候会导致网卡名称在重启后变化,尤其是当系统中有多个网卡时,驱动加载顺序不同就会导致名称不同,这显然有问题。 然后,后来systemdudev被引入,带来了可预测的命名规则。比如,根据固件信息、总线位置或者MAC地址来生成稳定的名称,比如enp0s3这样的格式。这样可以确保每次启动时同一网卡获得相同的名称,避免混乱。 但是,为什么会发生两次命名呢?可能的情况是系统同时使用了传统的命名方式和新的可预测命名方式。比如,在系统启动过程中,初始阶段内核可能会给网卡一个临时的名称,比如eth0,然后udev再根据规则重命名为新的名称,比如enp0s3。这时候在日志中可能会看到网卡被两次命名的信息,第一次是内核初始化的名称,第二次是udev应用规则后的名称。 另外,可能用户遇到的情况是,在系统启动时,网卡先被内核命名,然后被systemd-udevd服务根据规则再次命名。这种情况通常发生在系统从传统命名方式迁移到可预测命名方式的过程中,或者是由于某些配置冲突导致的。 还需要考虑到用户可能使用的是某些旧版本的系统,或者是在升级过程中出现了问题,导致命名规则被应用了两次。另外,如果用户手动修改了udev规则或者网络配置,也可能导致这种情况发生。 总结一下,可能的原因包括:内核初始命名和udev重命名的过程、传统命名方式与新规则的冲突、系统升级或配置错误。需要向用户解释这两个阶段的不同,以及为什么会出现两次命名的情况,可能带来的影响,比如接口名称变化导致网络配置失效,以及如何解决这类问题,比如检查udev规则、使用一致的命名策略等。</think>### Linux网卡被命名两次的原因及背景 Linux系统中网卡被命名两次的现象通常与 **内核初始化** 和 **用户态规则重命名** 两个阶段有关。以下是具体原因和流程分析: --- #### 1. **内核初始化阶段(临时命名)** 当Linux系统启动时,内核会加载网卡驱动并为其分配**临时名称**,例如: - `eth0`、`eth1`(传统命名) - `enp0s3`(新式可预测命名,取决于内核版本) 此阶段命名的名称可能不稳定,例如: - 多网卡时,驱动加载顺序不同会导致名称变化。 - 硬件插拔(如USB网卡)可能导致名称随机分配。 --- #### 2. **用户态规则重命名(持久化命名)** 内核初始化后,用户态的 `systemd-udevd` 服务会根据预设规则对网卡进行**二次命名**,目的是实现名称的**持久化和可预测性**。常见规则包括: - **基于固件/拓扑的命名**(如 `enp0s3`) - **基于MAC地址的命名**(如 `ens3-00:11:22:33:44:55`) - **自定义规则**(通过 `/etc/udev/rules.d/` 配置) 此阶段可能覆盖内核的临时名称,导致日志中出现类似提示: ```log 网卡重命名:eth0 → enp0s3 ``` --- #### 3. **为什么会“两次命名”?** - **兼容性过渡**: 从传统命名(`eth0`)迁移到可预测命名(`enp0s3`)时,系统可能先分配旧名称,再应用新规则。 - **规则冲突**: 若自定义规则与内核默认行为冲突,可能触发多次重命名。 - **驱动延迟加载**: 部分驱动(如虚拟化网卡)可能在udev规则生效后才加载,导致内核临时名称被覆盖。 --- #### 4. **影响与解决方案** - **问题表现**: 网卡名称变化可能导致网络配置失效(如`eth0`突然变成`enp0s3`)。 - **解决方法**: 1. **固定命名规则**: 编辑 `/etc/default/grub`,添加 `net.ifnames=0` 禁用可预测命名,回退到 `eth0` 风格。 2. **自定义udev规则**: 在 `/etc/udev/rules.d/70-persistent-net.rules` 中绑定MAC地址与名称。 3. **检查驱动加载顺序**: 通过 `modprobe` 强制优先加载关键网卡驱动。 --- #### 5. **验证与调试** - 查看内核初始化日志: ```bash dmesg | grep -i "renamed from eth0" ``` - 检查udev规则执行过程: ```bash udevadm test-builtin net_id /sys/class/net/enp0s3 ``` --- ### 总结 Linux网卡被命名两次的本质是**内核初始化与用户态规则协同工作的结果**。通过理解这一机制,可以更好地控制网卡名称的稳定性,避免因名称变化导致的网络服务异常。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值