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

为何UDP套接字多种数据发送方法的速度/效率无显著差异?

UDP高性能发送问题分析与优化方案

问题背景

在运行PetaLinux的嵌入式系统中,需要通过UDP套接字发送大量数据以饱和千兆链路,**速度(系统调用/数据拷贝耗时)和效率(CPU占用)**是核心需求。测试了四种发送策略:

  • 拷贝头部与数据到缓冲区后用sendto()发送,预期因拷贝开销最慢
  • 用sendmsg()聚合缓冲区避免用户态拷贝,预期性能提升
  • 用sendmmsg()减少系统调用次数,降低内核开销
  • 结合connect()预连接套接字,预期节省路由查找开销

但基准测试显示多数方法性能差异极小,仅在笔记本上connect()+sendmmsg()有小幅提升,嵌入式系统无此效果。需要解决:

  1. 解释为何未出现预期性能提升
  2. 验证测试代码的问题
  3. 推荐更高效的兼容方案(如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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 19:05:19