目标
使用 dpdk-16.04 版本的数通引擎需要适配 Mellanox 网卡,需要支持 dpdk secondary 进程正常收发包。
现状
- dpdk-16.04 的 mlx5 pmd 驱动支持要导入的麦洛斯网卡
- 缺乏 dpdk-16.04 mlx5 pmd 驱动使用手册等相关信息
- linux 内核版本为 3.X 版本,rootfs 为内部定制化版本
- 初次接触 mlx5 pmd 驱动,对其内部工作原理并不了解
阅读 dpdk 官方手册获得的信息
mlx5 pmd 驱动依赖外部的库和内核驱动来申请资源并进行初始化,如下几个依赖必须被单独安装:
- libibverbs
此库实现 mlx5 pmd 驱动使用的用户态 Verbs 框架。它提供了一个内核与用户态底层驱动如 libmlx5 之间的通用接口。它允许缓慢且需要特权的操作(上下文初始化,硬件资源申请等操作)被内核管理,其它快速的操作完全在用户态执行。 - libmlx5
Mellanox ConnectX-4/ConnectX-5 设备的底层用户态驱动库,它会被 libibverbs 库自动加载。此库实现了基础的硬件队列收发包过程调用。 - Kernel modules
它们提供内核侧的 Verbs API 以及底层管理实际硬件初始化以及用户态进程资源共享的设备驱动。如下驱动必须被加载并且绑定到设备上:- mlx5_core:管理 Mellanox ConnectX-4/ConnectX-5 设备以及相关的以太网内核网络设备的硬件驱动
- mlx5_ib:InifiniBand 设备驱动
- ib_uverbs:Verbs 的用户态驱动
- 固件更新
Mellanox OFED/EN 版本包括 ConnectX-4/ConnectX-5/ConnectX-6/BlueField 适配器的固件更新。因为每个版本都提供新功能,所以必须应用这些更新以匹配它们附带的内核模块和库。
可以使用如下两种不同的方式配置 mlx5 网卡:
- RDMA Core with Linux Kernel
内核版本至少为 v4.14,rdma-core 可以直接安装、通过源码编译 - Mellanox OFED/EN
主流的发行版都有二进制包,也可以通过源码编译
目标平台使用的内核与 rootfs 都是定制化的版本,没有 Mellanox OFED/EN 对应的二进制安装包,尝试源码编译发现许多编译依赖都不满足,只能使用 rdma-core + 内核驱动这种方式。
mlx5 内核驱动的适配
适配过程:
- 在 debian11 发行版虚拟机中加载相关模块,查看依赖配置
- 选取 v4.14 之后的内核版本,基于 5.10 内核开启相关依赖配置适配驱动
适配成功的标准:
- mlx5_core、mlx5_id、ib_uverbs 模块成功加载
- 网卡成功绑定到 mlx5_core 驱动,netdev 设备成功生成, /dev/infiniband/uverbsX 设备文件成功生成
dpdk-16.04 mlx5 pmd 驱动正确使用 libverbs 库
此版本驱动仅仅链接了 libibverbs 库,新版本中会链接 libibverbs 与 mlx5,由于此 pmd 需要链接外部 so,默认编译配置关闭了此 PMD 驱动。
同时 16.04 的驱动中使用了一些 exp 实验接口,从 libibverbs/tree/expose_send_params 找到了相关版本并进行编译生成 libverbs.so,运行 l2fwd 时报了如下错误信息:
Warning: no userspace device-specific driver found for XXXX
网上搜索没有找到相关内容,分析 libibverbs/tree/expose_send_params 项目源码,发现除了 libibverbs.so,还需要 libmlx5.so 这个驱动库。
分析源码发现 libibverbs.so 首先会枚举设备,然后加载驱动并完成驱动 probe,如果设备未匹配到任何驱动则报错退出。
上文提到 libmlx5.so 会被 libibverbs.so 自动加载,实际上有如下三种可能过程:
- 在 /etc/libibverbs.d/ 目录下存放驱动配置文件,libibvers.so 库在加载时会扫描这些配置文件,生成对应版本的驱动 so 名称并尝试使用 dlopen 加载之
例如,要自动加载 mlx5 驱动,其配置文件路径 /etc/libibverbs.d/mlx5.driver ,内容为 driver mlx5 - 设置 RDMAV_DRIVERS、IBV_DRIVERS 环境变量,使用以 :; 分隔的驱动名称设置其中一个环境变量,libibvers.so 会解析这两个环境变量来自动加载驱动
- 链接目标驱动,加入目标驱动的构造函数,在 main 函数执行前自动加载相应驱动。例如要使用 mlx5 设备,只需要链接 libmlx5.so。
老版本 dpdk mlx5 pmd 驱动只链接了 libibverbs.so,要加载驱动,只能使用前两种方式;新版本中则添加了 libmlx5.so 的链接项目,对应上述第三种方式。
这里描述的内容源自对源码的分析,源码来自 libibverbs/tree/expose_send_params 项目,分析内容记录见下文。
libverbs 与 libmlx5 驱动分析
dpdk mlx5 pmd 驱动中调用 ibv_get_device_list 接口来初始化设备,此接口使用 gcc .symver 汇编宏声明,对应源码中的 __ibv_get_device_list 接口。
__ibv_get_device_list
-
使用 pthread_once 调用 init_resources 函数,如果初始化失败则立即返回
-
使用 pthread_once 调用 count_devices 函数,此函数将每个设备填充到 device_list 数组中
count_devices 函数调用 ibverbs_get_device_list 加载驱动并获取设备数目
-
遍历每个设备,调用 get_device 函数增加引用计数
-
使用设备数目创建 ibv_device 指针数组并使用 device_list 数组项目进行初始化,最后返回 ibv_device 指针数组地址
使用 default_symver 宏,声明符号,相关代码如下:
default_symver(__ibv_get_device_list, ibv_get_device_list);
default_symver 宏代码如下:
#define DEFAULT_ABI ""
#ifdef HAVE_SYMVER_SUPPORT
# define symver(name, api, ver) \
asm(".symver " #name "," #api "@" #ver)
# define default_symver(name, api) \
asm(".symver " #name "," #api "@@" DEFAULT_ABI)
#else
# define symver(name, api, ver)
# define default_symver(name, api) \
extern __typeof(name) api __attribute__((alias(#name)))
#endif /* HAVE_SYMVER_SUPPORT */
.symver 是一个 gcc 支持的汇编宏指令,能够使用此指令指定符号的版本,如上代码中将 __ibv_get_device_list 函数设置为 ibv_get_device_list@@IBVERBS_1.1 版本。
init_resources
调用 ibverbs_init 完成环境初始化工作
ibverbs_init
- 根据环境变量判断是否需要执行 fork-safety 请求
- 获取 sysfs 的路径
- 读取 /sys/class/infiniband_verbs/abi_version 文件内容转化为数字版本信息,校验版本,版本号小于 3、大于 6 均表示非法
- 获取 RLIMIT_MEMLOCK 限制,不支持此限制、限制小于等于 32768 时,打印告警信息
- 遍历 /etc/libibverbs.d/ 目录,读取驱动的配置,为每个配置项目创建 ibv_driver_name 结构保存驱动名称,并将此结构链入到 driver_name_list 链表中
ibverbs_get_device_list
- 调用 find_sysfs_devs 扫描 sys 目录,扫描设备并注册到 sysfs_dev_list 链表中
- 遍历 sysfs_dev_list 中的每个设备,调用 try_drivers 遍历驱动链表,尝试绑定驱动,驱动绑定成功 try_drivers 会返回一个 ibv_device 结构,返回成功后将此结构添加到 device_list 数组中,并增加设备数目,然后将 sysfs_dev->have_driver 字段设置为 1;驱动绑定失败则设置 no_driver 变量为 1
- 判断 no_driver 是否为 0,为 0 则表明不需要尝试加载驱动,此函数直接返回
- 判断是否能够 dlopen 自己,能够 dlopen 表明是动态链接的 libverbs.so,否则为静态链接,此时将 statically_linked 变量设置为 1 后返回
- 调用 load_drivers 加载配置的所有驱动
-
判断 uid 是否等于 euid,相等时,尝试加载 RDMAV_DRIVERS 环境变量设置的 so 驱动、IBV_DRIVERS 环境设置的驱动。这两个环境变量的值为以 :; 分隔的驱动名称
-
遍历 driver_name_list,调用 load_driver 以 name 为参数加载驱动 so
load_driver 函数关键流程:
- 当 name 以 / 开头时,拼接成 name + -rdmav2.so 路径,使用 dlopen 并指定 RTLD_NOW 参数打开此驱动。示例名称为 /lib64/xxx/libmlx5-rdmav2.so
- 当 name 为相对路径时,拼接成 lib + name -rdmav2.so,使用 dlopen 并指定 RTLD_NOW 参数打开此驱动。示例名称为 libmlx5-rmdav2.so
- 当 1 与 2 都失败时,尝试从 providers 包编译时配置的 libdir 目录中加载 /lib + name + -rdmav2.so。libdir 目录一般为 /lib64,示例名称为 /lib64/libmlx5-rdmav2.so
-
- 重新遍历设备链表执行 try_drivers 函数绑定驱动
- 遍历设备链表,判断是否绑定驱动,未成功绑定驱动则打印错误信息,并释放所有的设备,最后将 sysfs_dev_list 链表设置为空,返回设备数目
find_sysfs_devs
- 遍历 /sys/class/infiniband_verbs/ 目录,为子目录下的每个非 . 目录创建一个 ibv_sysfs_dev 结构,新创建的结构使用 sysfs_dev 变量引用
- 初始化 sysfs_dev->sysfs_path 字段为 /sys/class/infiniband_verbs/ + 目录名称,判断 sysfs_path 目录是否是目录;否,则跳过
- 读取 sysfs_dev->sysfs_path + ibdev 文件内容,填充到 sysfs_dev->ibdev_name 中
- 将 sysfs_dev->ibdev_path 字段设置为 /sys//class/infiniband/ + sysfs_dev->ibdev_name
- 将此 sysfs_dev 设备链入到 sysfs_dev_list 链表中并将 sysfs_dev->have_driver 字段设置为 0 表示未绑定任何驱动的状态
- 读取 sysfs_dev->sysfs_path + "abi_version” 文件并将读取到的字符串转化为数字,将 abi_version 保存到 sysfs_dev->abi_version 字段中
- 更新 sysfs_dev_list 指向此 sysfs_dev 并将 sysfs_dev 设置为空
rdma-core 项目的一些特点
上文描述的 libibverbs.so 与 libmlx5.so 在高版本中由 rdma-core 项目源码编译生成。在我的 debian 系统中安装 rdma-core 相关的 deb 包,能够发现有 libmlx5-rdmavXX.so 的 so,它实际是个软链接,指向 libmlx5.so。
libibverbs 提供 ibverbs 的通用接口,不同的硬件驱动使用 ibverbs 接口与硬件进行交互,观察到会有许多种命令的下发,大都是通过 write 写入特定格式的内容到打开的设备文件中,此设备文件在 mlx5 网卡驱动中对应 /dev/infiniband/uverbsX,此文件由 ib_uverbs 模块绑定到一个 file_operations 实例中,ib_uverbs 模块作为通用接口,解析命令并向下层调用硬件驱动层实现的命令回调函数来完成硬件控制操作。
同时,初勘其源码发现内部符号版本控制做的很好,能够观测符号信息来获取它支持的外部 api 版本。
dpdk mlx5 pmd 驱动的一些特点
快速浏览代码,发现 mlx5 pmd 驱动有如下特点:
- 驱动实际是对 libibverbs.so 与 libmlx5.so 中 api 调用的封装
- 老版本驱动并不支持 secondary 进程收包,收包函数被设置为一个空函数
- 网卡状态通过 ioctl 向 netdev 设备下发命令获取,类似于 ifconfig 与 ethtool 的过程,libibverbs 核心功能是提供数据收发及相关的硬件配置
- mlx5_core 模块生成的 netdev 设备让 mlx5 网卡设备能够通过标准的 linux 网络管理命令——ifconfig、ethtool 等来控制
- 统计数据是通过软件累加的而非读硬件寄存器
- 收包函数中针对异常过程还有一些恢复的动作,涉及一些硬件操作
- 对 fork 出的进程正常使用网卡功能有支持
实际的模型有点类似于 igb_uio + KNI,不过 igb_uio 主要用于映射中断,硬件控制大部分通过直接读写寄存器完成,而 mlx5 的模型中 mlx5_core 与 KNI 的部分功能类似,但 ib_uverbs 内核模块的功能比 igb_uio + uio 这种模型更为复杂,它涉及大量的硬件共享资源相关的配置。
mlx5_core 与 Auxiliary Bus 总线
debian11 5.15 内核中 mlx5 驱动适配了 Auxiliary Bus 总线,扩展出的设备通过此总线进行管理,我们甚至于可以手动绑定、解绑虚拟的设备到 Auxiliary 总线下挂的驱动上。
更详细的信息可以访问 Managing multifunction devices with the auxiliary bus.