问题说明

一个挺有意思的问题,我们有一个开机启动的脚本,等待udevadm settle执行完之后,读取/sys/class/net/目录获取所有的网卡列表。 但是在某个型号的机器上,获取到的网卡列表缺少两个mellanox的网卡。但是在大量的其他部署中从没有出现过。 发现问题后登陆到系统中手动执行一下同样的脚本就可以正常列出所有网卡了。客户环境无法继续测试和调试。

环境信息

OS: CentOS 7
kernel: 3.10.0
systemd: 219-42
网卡型号:mellanox

问题分析

在其他的机器上从来没有报过同样的问题,所以基本确定和硬件有关系。

问题出现场景

  1. 系统启动,脚本作为systemd服务自动启动。
  2. 等待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
    
  3. 执行udevadm settle,等待执行完成。根据日志,我们看到这条命令大概执行了7s。
  4. 读取/sys/class/net/目录,获取网卡列表,发现读取到的列表缺少两个mellanox网卡。

初步分析

系统的驱动加载有几个地方是可配置的 https://wiki.archlinux.org/index.php/Kernel_module :

  1. 通过udev自动加载
  2. systemd-modules-load.service,会读取/etc/modules-load.d/等目录下的配置文件
  3. 通过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.rules80-net-setup-link.rules会对网卡名进行重命名,从ethX重命名为根据PCI插槽等一致的命名方式。因为我们net.ifnames为1,所以会有重命名的过程。

可能性

有可能上述过程中某些步骤是异步的,导致访问/sys/class/net/时部分文件还没创建。

  1. 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

  2. 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完成的。
    
  3. 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
    
  4. 驱动加载完成后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_coremodule_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_devicemlx4_en init会调用到mlx4_register_interface,两者都会调用mlx4_add_devicemlx4_add_device过程中会走到create kobject,创建sysfs,之后应该就能被userspace通过/sys/class/net读取到了。

但是为什么mlx4_core 加载之后没有网卡还看不见,必须要mlx4_en加载呢。 我们看到mlx网卡驱动需要加载两个module,mlx4_coremlx_en,不管加载哪个,另一个都会自动被加载进来(对于以太网卡来说)。

当先加载mlx4_core时,mlx4_core会调用mlx4_register_devicedev_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