原始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

