You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

原始Ethernet套接字recv调用无端阻塞问题排查与根因定位

更新

经过多次配置调整,交换多台额外机器上运行App A与App B的角色后,发现问题根源是损坏的网卡驱动。

原始问题

使用原始以太网套接字时遇到异常:当采用send/send/recv/recv循环模式时,最后一次recv调用可能无限期阻塞。

套接字定义如下:

sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

同时在收发使用的接口上开启了混杂模式(promiscuous mode)。

作为基准,以下send/recv循环场景运行完全正常:
搭建了一个PING/PONG应用,App A运行在机器A上,App B运行在机器B上,两台机器通过单根以太网直连:

App A:

while (1) {
    // 发送PING + 传入带接口信息的sockaddr_ll
    sendto(...);
    // 阻塞直到收到PONG + 过滤无关流量的逻辑
    recv(...);
}

App B:

while (1) {
    // 阻塞直到收到PING + 过滤无关流量的逻辑
    recv(...);
    // 回复PONG + 传入带接口信息的sockaddr_ll
    sendto(...);
}

但修改App A为以下逻辑后,问题出现:

while (1) {
    sendto(...); // PING 1
    sendto(...); // PING 2
    recv(...); // PONG 1
    recv(...); // PONG 2?
}

关键观察结果

  • 第二次recv调用可能无限期阻塞。循环次数越多,出现概率越高,通常在10-20次循环内发生。
  • 起初以为是代码问题,但在机器A的接口上用tcpdump监控时,也无法捕获到丢失的帧。等待几秒后按下CTRL+C终止App A,运行ifconfig <interface>会显示接口新增了一个丢包帧。
  • 确认机器B确实回复了PONG:用网络监控硬件将B→A的回复镜像到另一台机器C,在C上运行tcpdump可以看到该PONG帧。
  • 将App A和App B交换到对方机器上运行,问题仍然存在,因此推测这不仅仅是机器A的问题。
  • 最奇怪的现象:如果App A卡在第二次recv调用上,用另一个程序在同一接口和链路上发送任意非PONG帧,就能“触发”丢失的PONG帧通过,App A的recv调用不再阻塞。无论从机器A还是B发送到目标接口,只需任意流量即可触发。
  • 当ARP流量或其他正常流量通过接口时,偶尔会暂时“恢复”,但最终仍会再次卡住。
  • 如果在卡住后终止两台机器上的App A和B,仅重启App A,发送初始的“PING”帧会“触发”接口,此时它接收到的第一个PONG就是上次运行时丢失的那个。
  • 发送速度可能与此有关:去掉循环,手动反复运行App A,问题不会出现(或尚未遇到),因此才使用循环。但逻辑上同一时间链路上最多只有2个待处理帧。

有人能判断这是什么问题吗?看起来像是内核甚至网卡一直持有帧,直到有其他流量触发才释放?这不太可能是Nagle算法,因为处理的是原始以太网帧,而非TCP。甚至tcpdump都无法捕获到丢失的帧,这太费解了。

是内核版本问题?还是两台机器的网卡都有故障(见第4点)?该如何进一步调试?

编辑补充

可以用以下代码复现问题。需要先运行sudo ip link set dev <interface> promisc on开启混杂模式。

“App A”对应sudo ./app <interface> ping,“App B”对应sudo ./app <interface> pong。

App A(ping端)应该反复输出PING PING PONG PONG,App B(pong端)应该反复输出PING PONG。但实际运行时,App A只输出PING PING PONG后就陷入静默。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <ifaddrs.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <net/if.h>

#define PING "PING"
#define PONG "PONG"
// 发送完整的PXNG + 空终止符
#define DATALEN 5
#define min(x, y) (x > y ? y : x)

// 发送PING或PONG(取决于传入的data)
int32_t tx_pxng(char *data, size_t len, int32_t sock, struct sockaddr_ll *sockaddr)
{
    if (sendto(sock, data, len, 0, (struct sockaddr *)sockaddr, sizeof(*sockaddr)) < 0)
        return -1;

    // 确认发送完成
    if (len > ETH_HLEN)
        printf("%s\n", data + ETH_HLEN);

    return 0;
}

// 等待PING或PONG(取决于传入的data)
int32_t rx_pxng(char *data, size_t len, int32_t sock, char *buf, size_t buf_len)
{
    int32_t read;

rx:
    if ((read = recv(sock, buf, buf_len, 0)) < 0)
        return -1;

    // 过滤无关流量
    if (read < ETH_HLEN || memcmp(data, buf + ETH_HLEN, min((size_t)read - ETH_HLEN, len)) != 0)
        goto rx;

    // 确认接收完成
    printf("%s\n", data);

    return 0;
}

int32_t main(int32_t argc, char *argv[])
{
    char *iface_name = NULL;
    bool ping_side = false;
    int32_t index, sock, send_len = ETH_HLEN + DATALEN;
    struct sockaddr_ll sockaddr = {0};
    char recv_buf[ETH_FRAME_LEN] = {0}, send_payload[ETH_HLEN + DATALEN] = {0};

    if (argc < 3)
    {
        printf("Usage: pingpong -i <interface> [-a|-b]");
        return EXIT_FAILURE;
    }

    iface_name = argv[1];
    ping_side = strncmp(argv[2], "ping", 4) == 0;

    // 初始化代码
    if ((index = if_nametoindex(iface_name)) < 0)
        return EXIT_FAILURE;
    sockaddr.sll_family = AF_PACKET;
    sockaddr.sll_protocol = htons(ETH_P_ALL);
    sockaddr.sll_ifindex = index;

    if ((sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0)
        return EXIT_FAILURE;

    memcpy(send_payload + ETH_HLEN, ping_side ? PING : PONG, DATALEN);

    // 主循环
    if (ping_side)
    {
        // "App A"
        while (1)
        {
            // 发送PING 1
            if (tx_pxng(send_payload, send_len, sock, &sockaddr) < 0)
                return EXIT_FAILURE;

            // 发送PING 2
            if (tx_pxng(send_payload, send_len, sock, &sockaddr) < 0)
                return EXIT_FAILURE;

            // 等待PONG 1
            if (rx_pxng(PONG, DATALEN, sock, recv_buf, ETH_FRAME_LEN) < 0)
                return EXIT_FAILURE;

            // 等待PONG 2 - 可能无限期阻塞???
            // 甚至tcpdump都看不到任何帧...
            if (rx_pxng(PONG, DATALEN, sock, recv_buf, ETH_FRAME_LEN) < 0)
                return EXIT_FAILURE;
        }
    }
    else
    {
        // "App B"
        while (1)
        {
            // 等待PING
            if (rx_pxng(PING, DATALEN, sock, recv_buf, ETH_FRAME_LEN) < 0)
                return EXIT_FAILURE;

            // 回复PONG
            if (tx_pxng(send_payload, send_len, sock, &sockaddr) < 0)
                return EXIT_FAILURE;
        }
    }

    return EXIT_SUCCESS;
}

内容的提问来源于stack exchange,提问作者user14587380

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 05:49:49