找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2541

积分

0

好友

365

主题
发表于 5 天前 | 查看: 22| 回复: 0

对于 Mellanox 网卡,在使用 DPDK 驱动时,如果我们在多线程场景下,让不同的线程分别去获取链路状态、读取统计计数或设置 MTU 等,就可能出现线程被堵塞(hang 住)的问题。下面我们通过修改 DPDK 自带的示例程序 l2fwd 来复现这个问题,并深入分析其根本原因。

复现问题

首先,进入 DPDK 源码目录并进行编译配置。

# cd dpdk-stable-18.11.2
# export RTE_TARGET=build
# export RTE_SDK=`pwd`
# make config T=x86_64-native-linuxapp-gcc
# make -j32
# cd examples/l2fwd

接着,我们需要稍微修改一下 l2fwdmain.c 文件。我们在主循环中让第一个线程(lcore 0)不断地获取链路状态,同时让第二个线程(lcore 1)不断地设置 MTU,以此来模拟多线程并发操作网卡配置的场景。

static void
l2fwd_main_loop(void)
{
        unsigned lcore_id;
        prev_tsc = 0;
        timer_tsc = 0;
        lcore_id = rte_lcore_id();
        qconf = &lcore_queue_conf[lcore_id];
        struct rte_eth_link eth_link;
        while (!force_quit) {
            if (lcore_id == 0) {
                rte_eth_link_get(qconf->rx_port_list[0], ð_link);
                RTE_LOG(INFO, L2FWD, "link is %d on core %d\n", eth_link.link_status, lcore_id);
            }
            else if (lcore_id == 1) {
                rte_eth_dev_set_mtu(qconf->rx_port_list[0], 1500);
                RTE_LOG(INFO, L2FWD, "set mtu on core %d\n", lcore_id);
            }
            usleep(300);
        }
}

编译并运行修改后的 l2fwd 程序。通过 -c 3 参数指定使用两个 CPU 核心(掩码 0x3 对应 core 0 和 core 1)。运行后,我们可以通过按下 Ctrl+C 来中断程序,这表明线程已经无法正常响应,处于挂起状态。

#make
# ./build/l2fwd -c3 -n4 -w 82:00.1 -- -p1
EAL: Detected 40 lcore(s)
EAL: Detected 2 NUMA nodes
EAL: Multi-process socket /var/run/dpdk/rte/mp_socket
EAL: Probing VFIO support...
EAL: VFIO support initialized
EAL: PCI device 0000:82:00.1 on NUMA socket 1
EAL:   probe driver: 15b3:1015 net_mlx5
MAC updating enabled
Notice: odd number of ports in portmask.
Lcore 0: RX port 0
Initializing port 0... done:
Port 0, MAC address: 50:6B:4B:C0:9B:C5
Checking link statusdone
Port0 Link Up. Speed 25000 Mbps - full-duplex
^C

分析原因

为了搞清楚线程究竟卡在了哪里,我们使用 gdb 来查看线程状态和调用栈。

首先,找到 l2fwd 进程的 PID,然后使用 gdb 附加到该进程。

# ps -ef | grep l2fwd
root       8344   7232  0 05:45 pts/3    00:00:00 ./build/l2fwd -c3 -n4 -w 82:00.1 -- -p1
root       8353   7790  0 05:47 pts/0    00:00:00 grep --color=auto l2fwd
# gdb -p 8344
...

gdb 中查看所有线程的信息。我们可以看到一共有四个线程,其中线程 1 和线程 4 分别对应我们执行获取链路状态和设置 MTU 操作的线程,它们都已经堵塞在了 recvmsg 这个系统调用上。

(gdb) info thread
  Id   Target Id         Frame
  1    Thread 0x7f68e4981c00 (LWP 8344) “l2fwd” 0x00007f68e35ce94d in recvmsg () at ../sysdeps/unix/syscall-template.S:84
  2    Thread 0x7f68e2d71700 (LWP 8345) “eal-intr-thread” 0x00007f68e32fba13 in epoll_wait () at ../sysdeps/unix/syscall-template.S:84
  3    Thread 0x7f68e2570700 (LWP 8346) “rte_mp_handle” 0x00007f68e35ce94d in recvmsg () at ../sysdeps/unix/syscall-template.S:84
* 4    Thread 0x7f68e1d6f700 (LWP 8347) “lcore-slave-1” 0x00007f68e35ce94d in recvmsg () at ../sysdeps/unix/syscall-template.S:84

分析线程 1 (获取链路状态):

切换到线程 1 并查看其调用栈(backtrace)。

(gdb) thread 1
[Switching to thread 1 (Thread 0x7f68e4981c00 (LWP 8344))]
#0  0x00007f68e35ce94d in recvmsg () at ../sysdeps/unix/syscall-template.S:84
84      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) bt
#0  0x00007f68e35ce94d in recvmsg () at ../sysdeps/unix/syscall-template.S:84
#1  0x00000000007f2ad6 in mlx5_nl_recv (nlsk_fd=18, sn=2089018456, cb=0x7f2c70 <mlx5_nl_ifindex_cb>, arg=0x7fff4edcd440)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_nl.c:266
#2  0x00000000007f41de in mlx5_nl_ifindex (nl=18, name=name@entry=0x43003e75f8 “mlx5_1”)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_nl.c:782
#3  0x00000000007d6015 in mlx5_get_ifname (dev=0xf7cf40 <rte_eth_devices>, ifname=0x7fff4edcd780)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_ethdev.c:225
#4  0x00000000007d6869 in mlx5_ifreq (ifr=0x7fff4edcd780, req=35091, dev=0xf7cf40 <rte_eth_devices>)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_ethdev.c:285
#5  mlx5_link_update_unlocked_gs (dev=dev@entry=0xf7cf40 <rte_eth_devices>, link=link@entry=0x7fff4edcd830)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_ethdev.c:695
#6  0x00000000007d8833 in mlx5_link_update (dev=0xf7cf40 <rte_eth_devices>, wait_to_complete=1)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_ethdev.c:804
#7  0x000000000051b1cf in rte_eth_link_get (port_id=<optimized out>, eth_link=0x7fff4edcd8a0)
    at /root/dpdk-stable-18.11.2/lib/librte_ethdev/rte_ethdev.c:1913
#8  0x000000000047be2e in l2fwd_main_loop ()
    at /root/dpdk-stable-18.11.2/examples/l2fwd/main.c:210
#9  0x000000000047c1dc in l2fwd_launch_one_lcore (dummy=0x0) at /root/dpdk-stable-18.11.2/examples/l2fwd/main.c:296
#10 0x0000000000562b7b in rte_eal_mp_remote_launch (f=0x47c1cb <l2fwd_launch_one_lcore>, arg=0x0, call_master=CALL_MASTER)
    at /root/dpdk-stable-18.11.2/lib/librte_eal/common/eal_common_launch.c:62
#11 0x000000000047d234 in main (argc=2, argv=0x7fff4edce890) at /root/dpdk-stable-18.11.2/examples/l2fwd/main.c:739

调用栈清晰地显示,线程 1 在执行 rte_eth_link_get 时,最终堵塞在了 mlx5_nl_recv 函数内的 recvmsg 调用处(第 266 行)。

我们查看 mlx5_nl_recv 函数第 266 行附近的上下文,并打印局部变量。可以看到,接收到的数据包序列号(seq)是 2089018456,这与发送时的序列号相同,说明我们接收到了期望的数据。但是,数据包的标志 nlmsg_flags 为 2(NLM_F_MULTI),表示这是一个多部分消息,而 nlmsg_type 不为 0x3(NLMSG_DONE),说明内核还有后续的数据需要发送,因此函数会继续调用 recvmsg 来接收。

(gdb) f 1
#1  0x00000000007f2ad6 in mlx5_nl_recv (nlsk_fd=18, sn=2089018456, cb=0x7f2c70 <mlx5_nl_ifindex_cb>, arg=0x7fff4edcd440)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_nl.c:266
266                         recv_bytes = recvmsg(nlsk_fd, &msg, 0);
...
(gdb) p *(struct nlmsghdr *)buf
$7 = {nlmsg_len = 124, nlmsg_type = 5121, nlmsg_flags = 2, nlmsg_seq = 2089018456, nlmsg_pid = 8344}

分析线程 4 (设置 MTU):

再切换到线程 4 查看其调用栈。

(gdb) thread 4
[Switching to thread 4 (Thread 0x7f68e1d6f700 (LWP 8347))]
#0  0x00007f68e35ce94d in recvmsg () at ../sysdeps/unix/syscall-template.S:84
84      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) bt
#0  0x00007f68e35ce94d in recvmsg () at ../sysdeps/unix/syscall-template.S:84
#1  0x00000000007f2ad6 in mlx5_nl_recv (nlsk_fd=18, sn=628175011, cb=0x7f2c70 <mlx5_nl_ifindex_cb>, arg=0x7f68e1d6d020)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_nl.c:266
#2  0x00000000007f41de in mlx5_nl_ifindex (nl=18, name=name@entry=0x43003e75f8 “mlx5_1”)
    at /root/dpdk-stable-18.11.2/drivers/net/mlx5/mlx5_nl.c:782
...

线程 4 同样堵塞在了 mlx5_nl_recvrecvmsg 中。我们查看它接收到的数据包头部信息,发现一个关键问题:接收到的数据包序列号是 2089018456,但这并不是线程 4 自己发送请求时使用的序列号(628175011)。2089018456 这个序列号恰恰是线程 1 所使用的。

(gdb) f 1
...
(gdb) p *(struct nlmsghdr *)buf
$5 = {nlmsg_len = 20, nlmsg_type = 3, nlmsg_flags = 2, nlmsg_seq = 2089018456, nlmsg_pid = 8344}

这意味着,线程 4 错误地接收到了本应属于线程 1 的内核响应数据。而线程 1 仍在苦苦等待属于它的那部分数据(NLM_F_MULTI 消息的剩余部分),因此一直阻塞在 recvmsg 调用上。

通过分析调用栈可知,无论是获取链路状态 (rte_eth_link_get) 还是设置 MTU (rte_eth_dev_set_mtu),在 Mellanox 驱动的底层,最终都会调用 mlx5_ifreq -> mlx5_get_ifname -> mlx5_nl_ifindex -> mlx5_nl_recv -> recvmsg 这条路径,并可能堵塞在最后的 recvmsg 上。

根源探究

下面我们深入分析 mlx5_nl_ifindex 函数,为什么多个线程同时调用它会出问题。

// 驱动初始化时,创建 NETLINK_RDMA 类型的 socket,将文件描述符 (fd) 保存到 nl_socket_rdma。
// 这个 fd 被所有线程共享使用。
mlx5_pci_probe -> mlx5_dev_spawn
    priv->nl_socket_rdma = mlx5_nl_init(NETLINK_RDMA);

// 通过共享的 nl_socket_rdma 向内核查询信息
int
mlx5_get_ifname(const struct rte_eth_dev *dev, char (*ifname)[IF_NAMESIZE])
{
    struct mlx5_priv *priv = dev->data->dev_private;
    unsigned int ifindex =
        priv->nl_socket_rdma >= 0 ?
        mlx5_nl_ifindex(priv->nl_socket_rdma, priv->ibdev_name) : 0;
    ...
}

unsigned int
mlx5_nl_ifindex(int nl, const char *name)
{
    static const uint32_t pindex = 1;
    // 随机生成一个序列号 (seq),用于匹配一次请求与响应
    uint32_t seq = random();
    struct mlx5_nl_ifindex_data data = {
        .name = name,
        .ibindex = 0, /* Determined during first pass. */
        .ifindex = 0, /* Determined during second pass. */
    };
    union {
        struct nlmsghdr nh;
        uint8_t buf[NLMSG_HDRLEN +
                NLA_HDRLEN + NLA_ALIGN(sizeof(data.ibindex)) +
                NLA_HDRLEN + NLA_ALIGN(sizeof(pindex))];
    } req = {
        .nh = {
            .nlmsg_len = NLMSG_LENGTH(0),
            .nlmsg_type = RDMA_NL_GET_TYPE(RDMA_NL_NLDEV,
                               RDMA_NLDEV_CMD_GET),
            .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP,
        },
    };
    struct nlattr *na;
    int ret;
    // 1. 先发送 RDMA_NLDEV_CMD_GET 消息给内核,请求获取信息
    ret = mlx5_nl_send(nl, &req.nh, seq);
    if (ret < 0)
        return 0;
    // 2. 然后循环调用 recvmsg 接收内核的响应数据
    ret = mlx5_nl_recv(nl, seq, mlx5_nl_ifindex_cb, &data);
    if (ret < 0)
        return 0;
    ...
}

static int
mlx5_nl_recv(int nlsk_fd, uint32_t sn, int (*cb)(struct nlmsghdr *, void *arg),
         void *arg)
        do {
            recv_bytes = recvmsg(nlsk_fd, &msg, 0);
            if (recv_bytes == -1) {
                rte_errno = errno;
                return -rte_errno;
            }
            nh = (struct nlmsghdr *)buf;
        // 关键:只有接收到的消息序列号与发送时指定的序列号 (sn) 一致,才进行后续处理。
        } while (nh->nlmsg_seq != sn);
}

从代码逻辑可以清楚地看到问题所在:

  1. 每个线程在调用 mlx5_nl_ifindex 时,都会通过同一个网络套接字文件描述符(nl_socket_rdma)发送请求。
  2. 每个线程使用自己随机生成的序列号(seq)来标识这次请求-响应对。
  3. mlx5_nl_recv 函数会循环调用 recvmsg,但它只认序列号,它会持续接收数据,直到收到序列号与发送序列号 sn 匹配的消息才会返回。

结合之前的 gdb 分析结果,我们可以还原出问题发生的场景:

  • 线程 1 发送序列号为 2089018456 的请求,内核返回一个多部分消息。
  • 线程 1 接收了第一部分数据(NLM_F_MULTI),因为类型不是 NLMSG_DONE,它需要继续调用 recvmsg 接收第二部分。
  • 在线程 1 即将接收第二部分数据之前,线程 4 发送了序列号为 628175011 的请求。
  • 内核可能将线程 1 请求的第二部分数据,放入了线程 4 正在等待读取的Socket缓冲区中。
  • 线程 4 调用 recvmsg,读到了序列号为 2089018456 的数据(本应属于线程 1),由于序列号不匹配,线程 4 继续阻塞等待。
  • 线程 1 也在等待第二部分数据,同样阻塞。

这本质上是多线程并发读写同一个网络套接字描述符导致的消息乱序问题。因此,只要在多线程环境下,同时调用 mlx5_ifreq 这一系列 API(例如获取统计、设置 MTU、获取链路状态等),都有可能导致线程挂起。

解决办法

针对这个问题,有以下几种解决思路:

  1. 修改业务代码加锁:在应用层,对调用 rte_eth_link_getrte_eth_stats_getrte_eth_dev_set_mtu 等可能触发此问题的操作进行加锁,确保同一时刻只有一个线程执行这些操作。这种方法简单,但会影响并发性能。

  2. 修改 DPDK 驱动加锁:在驱动层 mlx5_nl_send/mlx5_nl_recv 函数内部或 mlx5_nl_ifindex 函数上加锁,保证对共享 nl_socket_rdma 的访问是串行的。这需要修改 DPDK 源码并重新编译。

  3. 升级 DPDK 版本:实际上,DPDK 社区在 2019 年已经修复了这个问题。修复的 patch 认为,DPDK 应用启动后,网卡的接口索引(ifindex)应该是固定不变的。因此,解决方案是在驱动初始化时(mlx5_dev_spawn)就获取一次 ifindex 并保存下来,后续所有操作直接使用这个缓存值,不再每次都通过 netlink 向内核发起请求。这从根本上避免了多线程竞争 netlink socket 的问题。

推荐做法:对于生产环境,最稳妥的方法是升级到已修复该问题的 DPDK 稳定版本(如 19.11 LTS 或更新版本)。如果因某些原因必须使用旧版本,则需要在应用层或驱动层实施加锁策略来规避此风险。

深入了解此类底层网络驱动与多线程交互的问题,对于构建稳定的高性能网络应用至关重要。欢迎在云栈社区交流讨论更多网络性能优化的话题。




上一篇:2025智能手机市场份额分析:华为领跑国内,苹果全球居首
下一篇:Python编程入门:掌握元组、字典与集合在数据处理和关系映射中的核心应用
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:49 , Processed in 0.398022 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表