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

Linux下UDP sendto是否返回ENOBUFS及IP_RECVERR使用疑问

解答你的UDP发送与IP_RECVERR相关问题

先回应你的源码追踪:你提到sendto()的返回值由rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;决定,这个理解没错,但内核后续对UDP的处理逻辑会影响最终用户态看到的返回值,咱们逐个拆解你的疑问:

疑问1:qdisc满时为何UDP发送缓冲区会被占满?

首先得明确UDP发送的完整路径:

  • 应用层调用sendto()后,内核会先把数据拷贝到UDP套接字发送缓冲区(由SO_SNDBUF控制大小)。
  • 内核的UDP发送逻辑会尝试将缓冲区中的数据包取出,封装成skb后送入qdisc队列。
  • 当PFC暂停帧导致NIC txring和qdisc都被占满时,qdisc的enqueue操作会返回NET_XMIT_DELAY或NET_XMIT_CN(表示暂时无法入队,需要稍后重试),而非直接返回NET_XMIT_DROP。
  • 此时内核不会丢弃这个skb,而是将它重新放回UDP套接字发送缓冲区,等待后续的发送重试。
  • 随着你持续发送,套接字发送缓冲区会被这些待重试的数据包填满,最终导致:
    • 阻塞模式下sendto()会阻塞,等待缓冲区有空闲空间;
    • 非阻塞模式下返回EAGAIN,提示套接字缓冲区已满。

只有当qdisc明确返回NET_XMIT_DROP(比如qdisc配置了严格丢弃策略,且内核无法将skb缓存到套接字缓冲区),或者内核完全无法分配skb(内存耗尽)时,才会返回ENOBUFS——这是比较极端的场景,不是qdisc满时的常规行为。

疑问2:调小qdisc长度后为何仍未返回ENOBUFS?

调小txqueuelen只是让qdisc更快达到满队列状态,但内核的处理逻辑没变:

  • 当qdisc满时,内核还是会把无法入队的skb放回UDP套接字发送缓冲区,而不是直接丢弃。
  • 只有当套接字发送缓冲区也满了,且内核无法再分配新的内存来缓存数据包时,才会触发ENOBUFS。但通常SO_SNDBUF的默认值远大于txqueuelen对应的数据包数量,所以你会先遇到缓冲区满导致的EAGAIN,而非ENOBUFS。

如果想触发ENOBUFS,可以尝试:

  1. 把SO_SNDBUF调得极小(比如setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &small_size, sizeof(small_size)),注意内核会将设置值翻倍,所以要设得比预期更小);
  2. 同时保持qdisc和txring被占满,让内核既无法入队到qdisc,也无法缓存到套接字缓冲区。

疑问3:IP_RECVERR的使用问题分析

你的代码存在几个关键问题,导致无法收到预期的错误:

1. 内核不会为qdisc丢包生成错误队列消息

IP_RECVERR主要用于接收ICMP错误(比如目的地不可达、端口未开放)或内核发送路径中的严重错误(比如路由失败),但qdisc因队列满导致的丢包属于网络拥塞行为,内核默认不会将这类丢包事件上报到UDP的错误队列——因为UDP是无连接、不可靠协议,内核不会为这类“软错误”生成错误通知。

2. 代码中的具体问题

  • cnt变量未定义:sprintf(buffer,"%d",cnt);会导致编译失败,需要先声明int cnt = 0;并在循环中递增。
  • 错误处理逻辑的小瑕疵:在ip_control_msg中,你处理完IP_RECVERR后设置ret = -1,然后control_msg返回这个值,最后判断if(control_msg(&msg)>=0)——这个逻辑是对的,但如果真的收到错误,会打印failed!,可以根据需求调整提示信息,让输出更直观。
  • 未处理截断标记:当从错误队列接收消息时,msg.msg_flags可能会包含MSG_CTRUNC,表示控制数据被截断,你可以添加检查来排查这类问题。

修正后的关键代码片段(示例)

int main(int argC, char* arg[]) {
    // ... 其他初始化代码 ...
    int cnt = 0; // 新增cnt定义
    int rtn = 0;
    while(1) {
        bzero(buffer, sizeof(buffer));
        sprintf(buffer,"%d", cnt++); // 递增cnt,避免重复内容
        rtn = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&addr, addr_len);
        if(rtn < 0){
            printf("sendto failed, errno: %d\n", errno);
        }
        char buf[1024];
        char control_buf[1024];
        struct msghdr msg;
        struct iovec iov = { buf, 1024 };
        memset( &msg, 0, sizeof(msg) );
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        msg.msg_control = control_buf;
        msg.msg_controllen = sizeof(control_buf);
        int bread = recvmsg( sockfd, &msg, MSG_ERRQUEUE );
        if( bread == -1 ){
            // 非阻塞下无错误时返回EAGAIN,属于正常情况
            if(errno != EAGAIN && errno != EWOULDBLOCK){
                perror("recvmsg MSG_ERRQUEUE");
            }
            continue;
        }
        // 检查控制数据是否被截断
        if(msg.msg_flags & MSG_CTRUNC){
            printf("Warning: Control data truncated!\n");
        }
        if( control_msg( &msg ) >= 0 )
            printf("No error received from error queue\n");
        else
            printf("Got error from error queue!\n");
        usleep(1000); // 避免循环过于占用CPU
    }
    return 0;
}

如果想验证IP_RECVERR是否正常工作,可以尝试让UDP服务器端关闭端口,然后发送数据包,此时内核会收到ICMP端口不可达错误,应该能在错误队列中接收到对应的消息。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 21:29:09