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,可以尝试:
- 把
SO_SNDBUF调得极小(比如setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &small_size, sizeof(small_size)),注意内核会将设置值翻倍,所以要设得比预期更小); - 同时保持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
相关产品推荐
相关产品推荐

