为何UDP套接字多种数据发送方法的速度/效率无显著差异?
UDP高性能发送问题分析与优化方案
问题背景
在运行PetaLinux的嵌入式系统中,需要通过UDP套接字发送大量数据以饱和千兆链路,**速度(系统调用/数据拷贝耗时)和效率(CPU占用)**是核心需求。测试了四种发送策略:
- 拷贝头部与数据到缓冲区后用
sendto()发送,预期因拷贝开销最慢 - 用
sendmsg()聚合缓冲区避免用户态拷贝,预期性能提升 - 用
sendmmsg()减少系统调用次数,降低内核开销 - 结合
connect()预连接套接字,预期节省路由查找开销
但基准测试显示多数方法性能差异极小,仅在笔记本上connect()+sendmmsg()有小幅提升,嵌入式系统无此效果。需要解决:
- 解释为何未出现预期性能提升
- 验证测试代码的问题
- 推荐更高效的兼容方案(如
MSG_ZEROCOPY)
测试结果
sysCall | connected | Time(μS) sendto() | no | 28119 sendmsg() | no | 28340 sendmmsg() | no | 28367 sendto() | yes | 28109 sendmsg() | yes | 28341 sendmmsg() | yes | 25021
测试代码(MVE)
#include <netinet/in.h> #include <sys/socket.h> #include <iostream> #include <chrono> #include <unistd.h> #include <string.h> #include <random> using namespace std; void randomize_sim_buf(); uint8_t send_buf[9000], sim_buf[10][1920*1200*3/2], sim_header[20] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}; int main(int argc, char const *argv[]) { int socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); struct sockaddr_in socket_bind, socket_destination; int return_stat = 0; uint8_t * p; iovec iov[2]; msghdr message_hdr; iovec iovs[1024][2]; mmsghdr mmsg[1024]; socket_bind.sin_family = socket_destination.sin_family = AF_INET; socket_bind.sin_addr.s_addr = htonl(0x0A42AB01); socket_destination.sin_addr.s_addr = htonl(0x0A42AB15); socket_bind.sin_port = htons(0xF3D4); socket_destination.sin_port = htons(0xF4D4); return_stat = bind(socket_fd, (sockaddr *)&socket_bind, sizeof(sockaddr_in)); bool connected = false; cout << "Connect socket?\n"; cin >> connected; if (connected){ return_stat = connect(socket_fd, (sockaddr *)&socket_destination, sizeof(sockaddr_in)); cout << "Socket connect() returned " << return_stat << endl; } string send_type = ""; int packet_size = 1500; cout << "{sendto|sendmsg|sendmmsg} [packet size]?\n"; cin >> send_type >> packet_size; uint64_t duration_cnt = 0; for (int i = 0; i < 1000; i++){ int msg_idx = 0; auto start = chrono::high_resolution_clock::now(); for (uint j = 0; j < (1920*1200*3/2); j+= packet_size){ uint datalen = (((1920*1200*3/2) - j) >= packet_size) ? packet_size : ((1920*1200*3/2) - j); p = sim_buf[i % 10]; if (send_type == "sendto"){ memcpy(send_buf, &sim_header, 20); memcpy(send_buf + 20, &p[j], datalen); sendto(socket_fd, send_buf, datalen, 0, (sockaddr*)&socket_destination, sizeof(sockaddr_in)); } else if (send_type == "sendmsg"){ iov[0].iov_base = &sim_header; iov[0].iov_len = 20; iov[1].iov_base = &p[j]; iov[1].iov_len = datalen; message_hdr.msg_controllen = 0; message_hdr.msg_flags = 0; message_hdr.msg_iov = iov; message_hdr.msg_iovlen = 2; message_hdr.msg_name = &socket_destination; message_hdr.msg_namelen = sizeof(sockaddr_in); sendmsg(socket_fd, &message_hdr, 0); } else if (send_type == "sendmmsg"){ iovs[msg_idx][0].iov_base = &sim_header; iovs[msg_idx][0].iov_len = 20; iovs[msg_idx][1].iov_base = &p[j]; iovs[msg_idx][1].iov_len = datalen; mmsg[msg_idx].msg_hdr.msg_controllen = 0; mmsg[msg_idx].msg_hdr.msg_flags = 0; mmsg[msg_idx].msg_hdr.msg_name = &socket_destination; mmsg[msg_idx].msg_hdr.msg_namelen = sizeof(sockaddr_in); mmsg[msg_idx].msg_hdr.msg_iov = iovs[msg_idx]; mmsg[msg_idx].msg_hdr.msg_iovlen = 2; msg_idx++; if (msg_idx == 1024){ sendmmsg(socket_fd, mmsg, msg_idx, 0); msg_idx = 0; } } else { cout << "Invalid send type supplied\n"; return -1; } } if (send_type == "sendmmsg") sendmmsg(socket_fd, mmsg, msg_idx, 0); auto stop = chrono::high_resolution_clock::now(); duration_cnt += std::chrono::duration_cast<std::chrono::microseconds>(stop - start).count(); randomize_sim_buf(); usleep(15000); } cout << "Average duration to send a buffer: " << (duration_cnt / 1000) << endl; return 0; } void randomize_sim_buf(){ for (int i = 0; i < 10; i++){ for (int j = 0; j < (1920*1200*3/2); j++){ sim_buf[i][j] = rand() % 255; } } }
问题解答
1. 为何未出现预期性能提升
(1)sendmsg()未体现优势的原因
- 内核拷贝优化抵消用户态拷贝差异:现代Linux内核对UDP发送做了
copy_from_user批量优化,即使sendto()需要用户态拷贝,内核也会用SSE/AVX等高效指令完成,20字节头部的拷贝开销几乎可忽略。 sendmsg()额外开销抵消收益:构造msghdr和iovec结构的用户态操作开销,抵消了避免拷贝带来的收益,在1500字节小包场景下更明显。
(2)sendmmsg()无提升的原因
- 系统调用开销占比低:千兆链路下,1500字节单包的发送速率约833包/ms,系统调用开销在整体耗时中占比极小,减少调用次数的收益不显著。
- 嵌入式内核优化不足:部分嵌入式Linux内核未对
sendmmsg()做充分优化,或内存带宽等硬件瓶颈掩盖了系统调用的开销差异。
(3)connect()无提升的原因
- 路由查找开销可忽略:UDP路由查找是基于目的IP的简单哈希查询,固定目的地址场景下内核会缓存路由结果,即使不
connect(),后续发送的路由查找开销极低。 - 地址传递开销占比小:
connect()仅绑定目的地址避免每次发送传递地址结构,但地址结构的拷贝开销在整体发送流程中占比极小。
2. 测试代码的问题
(1)计时与数据发送逻辑缺陷
- 未处理系统调用返回值:
sendto()/sendmsg()/sendmmsg()可能返回EAGAIN或部分发送,导致实际发送数据量不足,测试结果失真。 - 无意义的重复赋值:循环内
p = sim_buf[i % 10];每次j循环都重复执行,增加不必要开销。
(2)sendmmsg()实现问题
- 未利用
connect()优化:即使connect()绑定了目的地址,仍重复设置msg_name和msg_namelen,浪费优化效果。 - 栈溢出风险:
iovs数组在栈上分配1024*2个iovec(约32KB),超出嵌入式系统默认栈大小(通常8KB-16KB)。
(3)数据准备干扰测试
randomize_sim_buf()在每次循环后随机化整个缓冲区,CPU开销可能掩盖发送逻辑的性能差异,应提前初始化所有缓冲区。
3. 高效兼容方案推荐
(1)使用MSG_ZEROCOPY
- 原理:直接映射用户态缓冲区到内核,避免用户态到内核态的数据拷贝,CPU占用可降低30%-50%。
- 使用方式:在发送调用中添加
MSG_ZEROCOPY标志,同时确保:- 用户态缓冲区连续且对齐(用
posix_memalign分配) - 发送完成前不修改或释放缓冲区(可通过
SO_ZEROCOPY选项获取完成通知)
- 用户态缓冲区连续且对齐(用
- 要求:Linux内核4.14+,PetaLinux可通过配置启用该特性。
(2)优化sendmmsg()使用
- 结合
connect(),去掉mmsghdr中的msg_name和msg_namelen设置,减少结构初始化开销。 - 增大批量发送包数(如4096),进一步降低系统调用占比;改用堆分配
iovs和mmsg数组,避免栈溢出。
(3)调整套接字选项
- 设置
SO_SNDBUF为足够大的值(如1MB),避免内核发送队列满导致阻塞。 - 启用
SO_BUSY_POLL(Linux 3.11+),减少内核态到用户态的上下文切换开销,适合高吞吐量场景。
(4)硬件层面优化
- 使用Jumbo Frame(大帧),减少包数量,降低系统调用和协议栈开销。
- 启用网卡UDP分段卸载(USO),部分网卡支持该特性,可降低CPU开销。
内容的提问来源于stack exchange,提问作者Douglas B
相关产品推荐
相关产品推荐

