udev加载完成后mellanox网卡对应sysfs文件延迟出现
问题说明
一个挺有意思的问题,我们有一个开机启动的脚本,等待udevadm settle
执行完之后,读取/sys/class/net/目录获取所有的网卡列表。
但是在某个型号的机器上,获取到的网卡列表缺少两个mellanox的网卡。但是在大量的其他部署中从没有出现过。
发现问题后登陆到系统中手动执行一下同样的脚本就可以正常列出所有网卡了。客户环境无法继续测试和调试。
环境信息
OS: CentOS 7
kernel: 3.10.0
systemd: 219-42
网卡型号:mellanox
问题分析
在其他的机器上从来没有报过同样的问题,所以基本确定和硬件有关系。
问题出现场景
- 系统启动,脚本作为systemd服务自动启动。
- 等待systemd-udev-trigger服务完成。
[Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/bin/udevadm trigger --type=subsystems --action=add ; /usr/bin/udevadm trigger --type=devices --action=add
- 执行
udevadm settle
,等待执行完成。根据日志,我们看到这条命令大概执行了7s。 - 读取/sys/class/net/目录,获取网卡列表,发现读取到的列表缺少两个mellanox网卡。
初步分析
系统的驱动加载有几个地方是可配置的 https://wiki.archlinux.org/index.php/Kernel_module :
- 通过udev自动加载
- systemd-modules-load.service,会读取/etc/modules-load.d/等目录下的配置文件
- 通过modprobe.d或者kernel command line 可以配置额外参数
CentOS 7上几乎所有driver都是通过udev加载的,有额外的需求有可能通过 2 或 3 做一些额外的配置。经过检查,出问题的环境并没有相关配置。
所以推测在上述流程下,网卡驱动或者udev的问题会导致在读取sys/class/net
时还没有创建出来。
根据默认的udev rules,80-drivers.rules
负责加载驱动
# cat 80-drivers.rules
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="drivers_end"
ENV{MODALIAS}=="?*", RUN{builtin}+="kmod load $env{MODALIAS}"
...
LABEL="drivers_end"
80-net-name-slot.rules
和80-net-setup-link.rules
会对网卡名进行重命名,从ethX重命名为根据PCI插槽等一致的命名方式。因为我们net.ifnames为1,所以会有重命名的过程。
可能性
有可能上述过程中某些步骤是异步的,导致访问/sys/class/net/时部分文件还没创建。
-
udevadm settle 在 udev rules真正处理完之前就会返回
这个比较明确,udevadm settle 会等待处理完成,/run/udev/queue 文件删掉。例如加个sleep的udev rule,也可以看到命令会等待。在虚机里用e1000e网卡测试,通过udevadm trigger测试添加网卡的udev rule:
rmmod e1000e; udevadm trigger -v -c add /sys/devices/pci0000:00/0000:00:02.6/0000:07:00.0
-
udev builtin kmod 命令在load 驱动时异步
builtin kmod 的源码位置
systemd:src/udev/udev-builtin-kmod.c
,udev-builtin-kmod.c:builtin_kmod module-utils.c:module_load_and_warn kmod_module_probe_insert_module kmod_module_probe_insert_module是会同步等待moudle load完成的。
-
udev Rename interface 异步
udev 给网卡重命名使用了netlink接口。
udev-event.c:rename_netif netlink-util.c:rtnl_set_link_name sd-netlink.c:sd_netlink_call sd-netlink.c:sd_netlink_send netlink-socket.c:socket_write_message syscall:sendto
另一方面,kernel中netlink接口更新网卡名。在
rtnl_register(PF_UNSPEC, RTM_NEWLINK, rtnl_setlink, NULL, 0)
注册netlink处理函数后,收到rename消息是会调用到dev_change_name
.rtnetlink.c:rtnl_setlink rtnetlink.c:do_setlink dev.c:dev_change_name
通过一个简单的systemtap脚本也可以观察函数和系统调用的调用过程。可以使用mdelay添加延迟,观察函数的执行情况。例如在dev_change_name中添加delay,可以明显看到sendto 系统调用会在delay结束才完成。
#!/usr/bin/stap probe syscall.sendto{ printf ("%s(%d) sendto(%s)\n", execname(), pid(), argstr) #mdelay(10000) } probe syscall.sendto.return{ printf ("%s(%d) sendto end\n", execname(), pid()) } probe kernel.function("dev_change_name").call { printf("dev_change_name\n") mdelay(10000) } probe kernel.function("rtnl_newlink").call { printf("rtnl_newlink\n") } probe kernel.function("rtnl_setlink").call { printf("rtnl_setlink\n") } probe kernel.function("rtnl_setlink").return{ printf("rtnl_setlink end\n") }
另外netlink消息可以通过tcpdump或wireshark抓包。wireshark可以识别netlink消息。可以参考 https://jvns.ca/blog/2017/09/03/debugging-netlink-requests/
# create the network interface sudo ip link add nlmon0 type nlmon sudo ip link set dev nlmon0 up sudo tcpdump -i nlmon0 -w netlink.pcap # capture your packets wireshark netlink.pcap # look at the results with wireshark
-
驱动加载完成后sysfs目录不会立即生成,导致访问/sys/class/net目录的时候网卡的符号链接还没生成。 这个也不可能。sysfs只是kobject系统的文件系统接口,不会有sysfs和kobject不一致的情况的。
上面这些可能性都是会影响所有设备的,如果有问题应该更早在其他硬件上就出现问题了。另外理论分析和上面测试基本可以排除这些可能性了。
接近真相
mellanox驱动本身异步加载了,导致udev执行完毕时驱动还没有加载完成。
因为之前dmesg里看到mlx4_en的字样,开始主要看的时mlx4_en驱动。另外mlx4_en 依赖mlx4_core。看mlx4_en的驱动,并没有看到加载过程中到生成sysfs的过程中有可能异步的情况。可以看到如下的调用过程。
en_main:mlx4_en_init
intf.c:mlx4_register_interface
* intf.c:mlx4_add_device
en_main.c:mlx4_en_activate
en_netdev.c:mlx4_en_init_netdev
eth.c:alloc_etherdev_mqs
dev.c:alloc_netdev_mqs
eth.c:ether_setup
dev.c:register_netdevice
net-sysfs.c:netdev_register_kobject
driver/base/core.c:device_initialize
driver/base/core.c:device_add
core.c:device_add_class_symlinks
net-sysfs.c:register_queue_kobjects
INIT_WORK(mlx4_en_linkstate)
Root Cause
tl;dr
简单来说,mellanox的网卡有ib卡和普通以太网卡,对于以太网卡驱动分成了mlx4_core
, mlx4_en
两个module,udev rules里加载mlx4_core,在mlx4_core的module_init里通过request_module_nowait
加载mlx4_en
。结果就是在udev rules跑完了,udevadm settle
正常返回的时间点,mlx_en可能还没有加载进来,网卡还不可见。
具体过程
查看modules.alias
,只有mlx4_core
,没有mlx4_en
,所以udev load driver的时候用的是mlx4_core
。
[root@localhost ~]# grep mlx4 /usr/lib/modules/3.10.0-693.11.1.el7.es.10.x86_64/modules.alias
alias pci:v000015B3d00001010sv*sd*bc*sc*i* mlx4_core
alias pci:v000015B3d0000100Fsv*sd*bc*sc*i* mlx4_core
alias pci:v000015B3d0000100Esv*sd*bc*sc*i* mlx4_core
.....
在mlx4_core
的module_init
,会调用到request_module_nowait("mlx4_en")
。所以udev settle
结束只能保证mlx4_core
已经加载,mlx4_en
不一定加载完成。
// mlx4/main.c
static void mlx4_request_modules(struct mlx4_dev *dev)
{
int port;
int has_ib_port = false;
int has_eth_port = false;
#define EN_DRV_NAME "mlx4_en"
#define IB_DRV_NAME "mlx4_ib"
for (port = 1; port <= dev->caps.num_ports; port++) {
if (dev->caps.port_type[port] == MLX4_PORT_TYPE_IB)
has_ib_port = true;
else if (dev->caps.port_type[port] == MLX4_PORT_TYPE_ETH)
has_eth_port = true;
}
if (has_eth_port)
request_module_nowait(EN_DRV_NAME);
if (has_ib_port || (dev->caps.flags & MLX4_DEV_CAP_FLAG_IBOE))
request_module_nowait(IB_DRV_NAME);
}
mlx4_core init
会调用到mlx4_register_device
,mlx4_en init
会调用到mlx4_register_interface
,两者都会调用mlx4_add_device
。
mlx4_add_device
过程中会走到create kobject
,创建sysfs,之后应该就能被userspace通过/sys/class/net
读取到了。
但是为什么mlx4_core
加载之后没有网卡还看不见,必须要mlx4_en
加载呢。
我们看到mlx网卡驱动需要加载两个module,mlx4_core
和mlx_en
,不管加载哪个,另一个都会自动被加载进来(对于以太网卡来说)。
当先加载mlx4_core
时,mlx4_core
会调用mlx4_register_device
把dev_list
初始化,然后到mlx4_en
调用到mlx4_register_interface
里的list_for_each_entry
才会真正进去,调用到mlx4_add_device
。
另一方面,用户有可能手动modprobe mlx4_en
,此时会先调用到mlx4_register_interface
,初始化intf_list
。再进入mlx4_core
调用mlx4_register_device
,这时候mlx4_add_device
才会被调用到。
也就是说,不管先加载哪个module,都可以正常调用到mlx4_add_device
。
//file: intf.c
static LIST_HEAD(intf_list);
static LIST_HEAD(dev_list);
//mlx4_core calls
int mlx4_register_device(struct mlx4_dev *dev)
{
struct mlx4_priv *priv = mlx4_priv(dev);
struct mlx4_interface *intf;
mutex_lock(&intf_mutex);
dev->persist->interface_state |= MLX4_INTERFACE_STATE_UP;
list_add_tail(&priv->dev_list, &dev_list);
list_for_each_entry(intf, &intf_list, list)
mlx4_add_device(intf, priv);
mutex_unlock(&intf_mutex);
mlx4_start_catas_poll(dev);
return 0;
}
//mlx4_en calls
int mlx4_register_interface(struct mlx4_interface *intf)
{
struct mlx4_priv *priv;
if (!intf->add || !intf->remove)
return -EINVAL;
mutex_lock(&intf_mutex);
list_add_tail(&intf->list, &intf_list);
list_for_each_entry(priv, &dev_list, dev_list) {
if (mlx4_is_mfunc(&priv->dev) && (intf->flags & MLX4_INTFF_BONDING)) {
mlx4_dbg(&priv->dev,
"SRIOV, disabling HA mode for intf proto %d\n", intf->protocol);
intf->flags &= ~MLX4_INTFF_BONDING;
}
mlx4_add_device(intf, priv);
}
mutex_unlock(&intf_mutex);
return 0;
}
附:mlx4_core module相关的调用路径如下:
(pci-driver.c) mlx4_driver->probe = mlx4_init_one
main.c:mlx4_init_one
devlink.c:devlink_alloc
main.c:__mlx4_init_one
main.c:mlx4_load_one
intf.c:mlx4_register_device
* intf.c:mlx4_add_device
main.c:mlx4_request_modules
kmod.h:request_module_nowait
module_init(main.c:mlx4_init)
pci-driver.c:__pci_register_driver
driver.c:driver_register
bus.c:bus_add_driver
dd.c:driver_attach
dd.c:__driver_attach
dd.c:driver_probe_device
dd.c:really_probe
dd.c:driver_sysfs_add