Linux C/C++ 学习日记(47):dpdk(八):UDP的pps测试:内核 VS dpdk

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

PPS 是 Packets Per Second 的缩写,中文意为每秒数据包数 ,是衡量网络设备或程序数据包处理能力的核心指标,具体指单位时间(1 秒)内成功发送或接收的数据包数量。

在本文中:pps:每秒发包的个数

一、发送100万个UDP包的测试结果

dpdk:

sudo ./build/ustack -l 0 -n 2 -- --sip 192.168.248.135 --sport 8080 --dmac 00:50:56:c0:00:08 --dip 192.168.248.1 --dport 8080 --count 1000000 --udp

每秒发包数为178126个左右

posix API:

./normal_udp_send_tool

每秒发包数位72364个左右

二、结果论述

pps对比:

  • dpdk:178126
  • posix AP:72364
  • 前者大约是后者的2倍

三、原因

1、DPDK 发包的 "性能突破点"

DPDK 是专为高性能网络 设计的用户态框架,核心优势在于绕过内核协议栈,直接在用户空间完成数据包的构造、发送,从而消除了一系列内核级开销:

  1. 无内核上下文切换:传统 Socket 发送数据包时,需从用户态切换到内核态(系统调用开销),DPDK 完全在用户态操作,无此开销;
  2. 无内存拷贝:传统 Socket 需将用户空间数据拷贝到内核空间(再由内核发往网卡),DPDK 通过 "大页内存""mbuf 内存池" 等机制,实现用户空间直接向网卡硬件缓冲区写数据,避免多次内存拷贝;
  3. 批量操作优化 :DPDK 支持批量构造、批量发送 (如代码中的 BURST_SIZE),将多个数据包的开销 "摊薄",进一步提升效率;
  4. 轮询式网卡驱动:DPDK 用 "轮询(Polling)" 代替传统网卡的 "中断" 模式,消除了中断处理的延迟和开销,适合高吞吐场景。

2、标准 UDP Socket 的 "性能瓶颈"

标准 Socket 基于内核协议栈实现,每一步操作都存在固有开销:

  1. 系统调用开销 :调用 sendto 时,需从用户态切换到内核态,完成权限校验、参数解析等操作,单次调用虽快,但高频调用时开销显著;
  2. 内核协议栈处理:内核需完成 UDP 封装、IP 封装、校验和计算等一系列协议逻辑,且这些操作是 "逐包串行" 的,无法像 DPDK 那样批量优化;
  3. 内存拷贝开销:用户空间数据需拷贝到内核空间,再由内核拷贝到网卡硬件缓冲区,两次拷贝带来额外耗时。

3、数据对比的直观结论

从测试结果看:

  • DPDK 发包速率约 17.8 万 PPS
  • 标准 UDP Socket 发包速率约 7.2 万 PPS
  • 前者是后者的 2.4 倍以上,且随着发包量增大、数据包变小,这个差距会更明显(小包场景下,DPDK 的 "批量优化" 和 "无拷贝" 优势更突出)。

小包场景下优势更明显的原因:

  • 大包和小包,内核拷贝的速率是差不多的,内核更多的消耗在于的是内核与用户态数据之间的交互(这个损耗的时间远大于拷贝数据大小的影响)。
  • 而dpdk由于是无拷贝的,即没有与内核的数据交互,其将数据写到内存池所需的时间主要受拷贝数据大小的影响。
  • 因此说,小包场景下,dpdk的优势会更明显

简言之,DPDK 通过 "用户态直连网卡、批量操作、无内存拷贝" 等设计,彻底消除了内核协议栈的开销,因此在数据包吞吐能力上远优于依赖内核的标准 Socket 方案。

四、测试代码

nomal_udp_send_tool.c

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <unistd.h>

#define SEND_DATA "Hello, World!"  // 与DPDK版本保持一致的发包数据
#define TARGET_IP "192.168.248.1"  // 目标IP(请替换为实际测试IP)
#define TARGET_PORT 8000           // 目标端口(请替换为实际测试端口)
#define SEND_COUNT 1000000          // 总发包数

// 计算时间差(毫秒)
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

int main() {
    int sockfd;
    struct sockaddr_in dest_addr;
    char send_buf[1024] = "Hello,World!";
    strncpy(send_buf, SEND_DATA, strlen(SEND_DATA));

    // 创建UDP套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置目标地址
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(TARGET_PORT);
    if (inet_pton(AF_INET, TARGET_IP, &dest_addr.sin_addr) <= 0) {
        perror("inet_pton failed");
        exit(EXIT_FAILURE);
    }

    struct timeval tv_begin, tv_end;
    gettimeofday(&tv_begin, NULL);

    // 循环发送数据包
    int sent = 0;
    while (sent < SEND_COUNT) {
        ssize_t send_len = sendto(sockfd, send_buf, strlen(send_buf), 0,
                                  (struct sockaddr*)&dest_addr, sizeof(dest_addr));
        if (send_len < 0) {
            perror("sendto failed");
            break;
        }
        sent++;
    }

    gettimeofday(&tv_end, NULL);
    int time_used = TIME_SUB_MS(tv_end, tv_begin);
    int success = sent;

    // 输出结果
    printf("===== 标准Socket UDP发包详情 =====\n");
    printf("目标IP: %s\n", TARGET_IP);
    printf("目标端口: %d\n", TARGET_PORT);
    printf("总发包数: %d\n", SEND_COUNT);
    printf("成功发包数: %d\n", success);
    printf("发包数据: %s\n", send_buf);
    printf("耗时: %d ms\n", time_used);
    printf("发包速率: %d pps\n", time_used > 0 ? (success * 1000 / time_used) : 0);

    close(sockfd);
    return 0;
}

dpdk_udp_send_tool.c

cpp 复制代码
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <getopt.h>
#include <unistd.h>
#include <sys/time.h>
#include <rte_timer.h>

#define ENABLE_SEND 1
#define ENABLE_ARP 1
#define LOG_ENABLE 0

#define NUM_MBUFS (4096 - 1)
#define BURST_SIZE 1000                          // 每批次发送的数据包数量
static const char *send_data = "Hello, World!"; // 统一发包数据

#if ENABLE_SEND
static uint32_t gSrcIp;
static uint32_t gDstIp;
static uint8_t gSrcMac[RTE_ETHER_ADDR_LEN];
static uint8_t gDstMac[RTE_ETHER_ADDR_LEN];
static uint16_t gSrcPort;
static uint16_t gDstPort;
#endif

static const struct rte_eth_conf port_conf_default = {
    .rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN}};

// 全局端口ID
int gDpdkPortId = 0;

// 计算时间差(毫秒)
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

static void ng_init_port(struct rte_mempool *mbuf_pool)
{
    uint16_t nb_sys_ports = rte_eth_dev_count_avail();
    if (nb_sys_ports == 0)
    {
        rte_exit(EXIT_FAILURE, "No Supported eth found\n");
    }

    struct rte_eth_dev_info dev_info;
    rte_eth_dev_info_get(gDpdkPortId, &dev_info);

    const int num_rx_queues = 1;
    const int num_tx_queues = 1;
    struct rte_eth_conf port_conf = port_conf_default;
    rte_eth_dev_configure(gDpdkPortId, num_rx_queues, num_tx_queues, &port_conf);

    if (rte_eth_rx_queue_setup(gDpdkPortId, 0, 1024,
                               rte_eth_dev_socket_id(gDpdkPortId), NULL, mbuf_pool) < 0)
    {
        rte_exit(EXIT_FAILURE, "Could not setup RX queue\n");
    }

#if ENABLE_SEND
    struct rte_eth_txconf txq_conf = dev_info.default_txconf;
    txq_conf.offloads = port_conf.rxmode.offloads;
    if (rte_eth_tx_queue_setup(gDpdkPortId, 0, 1024,
                               rte_eth_dev_socket_id(gDpdkPortId), &txq_conf) < 0)
    {
        rte_exit(EXIT_FAILURE, "Could not setup TX queue\n");
    }
#endif

    if (rte_eth_dev_start(gDpdkPortId) < 0)
    {
        rte_exit(EXIT_FAILURE, "Could not start\n");
    }
}

static int ng_encode_tcp_pkt(uint8_t *msg, uint16_t total_len)
{
    struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
    rte_memcpy(eth->s_addr.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN);
    rte_memcpy(eth->d_addr.addr_bytes, gDstMac, RTE_ETHER_ADDR_LEN);
    eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);

    struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr *)(msg + sizeof(struct rte_ether_hdr));
    ip->version_ihl = 0x45;
    ip->type_of_service = 0;
    ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr));
    ip->packet_id = 0;
    ip->fragment_offset = 0;
    ip->time_to_live = 64;
    ip->next_proto_id = IPPROTO_TCP;
    ip->src_addr = gSrcIp;
    ip->dst_addr = gDstIp;
    ip->hdr_checksum = rte_ipv4_cksum(ip);

    struct rte_tcp_hdr *tcp = (struct rte_tcp_hdr *)(msg + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr));
    tcp->src_port = htons(gSrcPort);
    tcp->dst_port = htons(gDstPort);
    tcp->tcp_flags = 1 << 1; // SYN标志
    tcp->data_off = 0x50;
    tcp->rx_win = htons(65535);
    tcp->sent_seq = htonl(12345);
    tcp->recv_ack = 0x0;
    tcp->cksum = 0;
    tcp->cksum = rte_ipv4_udptcp_cksum(ip, tcp);

    struct in_addr addr;
    addr.s_addr = gSrcIp;
    printf(" --> tcp src: %s:%d, ", inet_ntoa(addr), gSrcPort);
    addr.s_addr = gDstIp;
    printf("dst: %s:%d, %s\n", inet_ntoa(addr), gDstPort, send_data);

    return 0;
}

static struct rte_mbuf *ng_tcp_send(struct rte_mempool *mbuf_pool)
{
    const unsigned total_len = sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_tcp_hdr);

    struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);
    if (!mbuf)
    {
        rte_exit(EXIT_FAILURE, "rte_pktmbuf_alloc\n");
    }
    mbuf->pkt_len = total_len;
    mbuf->data_len = total_len;

    uint8_t *pktdata = rte_pktmbuf_mtod(mbuf, uint8_t *);
    ng_encode_tcp_pkt(pktdata, total_len);

    return mbuf;
}

static int ng_encode_udp_pkt(uint8_t *msg, uint16_t total_len)
{
    struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;
    rte_memcpy(eth->s_addr.addr_bytes, gSrcMac, RTE_ETHER_ADDR_LEN);
    rte_memcpy(eth->d_addr.addr_bytes, gDstMac, RTE_ETHER_ADDR_LEN);
    eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);

    struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr *)(msg + sizeof(struct rte_ether_hdr));
    ip->version_ihl = 0x45;
    ip->type_of_service = 0;
    ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr));
    ip->packet_id = 0;
    ip->fragment_offset = 0;
    ip->time_to_live = 64;
    ip->next_proto_id = IPPROTO_UDP;
    ip->src_addr = gSrcIp;
    ip->dst_addr = gDstIp;
    ip->hdr_checksum = rte_ipv4_cksum(ip);

    struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(msg + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr));
    udp->src_port = htons(gSrcPort);
    udp->dst_port = htons(gDstPort);
    uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);
    udp->dgram_len = htons(udplen);

    rte_memcpy((uint8_t *)(udp + 1), send_data, strlen(send_data));
    udp->dgram_cksum = 0;
    udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);

#if LOG_ENABLE
    struct in_addr addr;
    addr.s_addr = gSrcIp;
    printf("--> udp  src: %s:%d, ", inet_ntoa(addr), gSrcPort);
    addr.s_addr = gDstIp;
    printf("dst: %s:%d --> %s\n", inet_ntoa(addr), gDstPort, send_data);
#endif
    return 0;
}

static struct rte_mbuf *ng_udp_send(struct rte_mempool *mbuf_pool)
{
    const unsigned total_len = sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_udp_hdr) + strlen(send_data);

    struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);
    if (!mbuf)
    {
        rte_exit(EXIT_FAILURE, "rte_pktmbuf_alloc\n");
    }
    mbuf->pkt_len = total_len;
    mbuf->data_len = total_len;

    uint8_t *pktdata = rte_pktmbuf_mtod(mbuf, uint8_t *);
    ng_encode_udp_pkt(pktdata, total_len);

    return mbuf;
}

static struct option args[] = {
    {"sip", required_argument, 0, 's'},
    {"sport", required_argument, 0, 'p'},
    {"dmac", required_argument, 0, 'm'},
    {"dip", required_argument, 0, 'i'},
    {"dport", required_argument, 0, 'd'},
    {"count", required_argument, 0, 'c'},
    {"udp", no_argument, 0, 'u'},
    {0, 0, 0, 0}};

int main(int argc, char *argv[])
{
    int ret = rte_eal_init(argc, argv);
    if (ret < 0)
    {
        rte_exit(EXIT_FAILURE, "Error with EAL init\n");
    }

    argc -= ret;
    argv += ret;

    char opt;
    int flag = 0; // 0: TCP, 1: UDP
    int count = 1;

    while ((opt = getopt_long(argc, argv, "s:p:m:i:d:c:u:?", args, NULL)) != -1)
    {
        switch (opt)
        {
        case 's':
            inet_pton(AF_INET, optarg, &gSrcIp);
            break;
        case 'p':
            gSrcPort = atoi(optarg);
            break;
        case 'm':
            sscanf(optarg, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &gDstMac[0], &gDstMac[1], &gDstMac[2],
                   &gDstMac[3], &gDstMac[4], &gDstMac[5]);
            break;
        case 'i':
            inet_pton(AF_INET, optarg, &gDstIp);
            break;
        case 'd':
            gDstPort = atoi(optarg);
            break;
        case 'u':
            flag = 1;
            break;
        case 'c':
            count = atoi(optarg);
            count = count > 1 ? count : 1;
            break;
        case '?':
            printf("Invalid option\n");
            return -1;
        default:
            break;
        }
    }

    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS,
                                                            0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
    if (mbuf_pool == NULL)
    {
        rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
    }

    ng_init_port(mbuf_pool);
    rte_eth_macaddr_get(gDpdkPortId, (struct rte_ether_addr *)gSrcMac); // 获取本地MAC

    // 记录两种计时方式的开始时间
    uint64_t start_tsc = rte_get_tsc_cycles();
    struct timeval tv_begin;
    gettimeofday(&tv_begin, NULL);

    // 分段发送逻辑:每次发送BURST_SIZE个,直到完成总数量
    int sent = 0;
    int failed = 0;
    while (sent < count)
    {
        struct rte_mbuf *bursts[BURST_SIZE];
        int batch_count = 0;

        // 批量构造BURST_SIZE个数据包
        for (batch_count = 0; batch_count < BURST_SIZE && sent < count; batch_count++)
        {
            struct rte_mbuf *mbuf = NULL;

            //printf("Batch %d, Packet %d\n", (sent / BURST_SIZE) + 1, batch_count + 1);
            if (flag)
            {
                mbuf = ng_udp_send(mbuf_pool);
            }
            else
            {
                // 循环端口号,避免溢出
                gSrcPort = (gSrcPort + sent % 10000) % 65535;
                mbuf = ng_tcp_send(mbuf_pool);
            }
            bursts[batch_count] = mbuf;
            sent++;
        }

        // 批量发送
        uint16_t txed = rte_eth_tx_burst(gDpdkPortId, 0, bursts, batch_count);
        //printf("Batch %d: Sent %d, Failed %d\n", (sent / BURST_SIZE), txed, batch_count - txed);
        failed += (batch_count - txed);

        // 释放未发送成功的数据包
        for (int j = txed; j < batch_count; j++)
        {
            rte_pktmbuf_free(bursts[j]);
            sent--; // 回退计数,保证总发送量准确
        }
    }

    // 记录两种计时方式的结束时间
    uint64_t end_tsc = rte_get_tsc_cycles();
    struct timeval tv_end;
    gettimeofday(&tv_end, NULL);

    // 计算原有微秒级计时(毫秒显示)
    int time_used_ms = TIME_SUB_MS(tv_end, tv_begin);
    int success = count - failed;

    // 计算TSC纳秒级计时
    uint64_t tsc_cycles = end_tsc - start_tsc;
    double tsc_hz = rte_get_tsc_hz();
    double time_used_ns = (double)tsc_cycles / tsc_hz * 1e9; // 转换为纳秒
    int time_used_us = (int)(time_used_ns / 1000);           // 转换为微秒

    // 格式化目的MAC
    char dst_mac_str[20];
    snprintf(dst_mac_str, sizeof(dst_mac_str), "%02x:%02x:%02x:%02x:%02x:%02x",
             gDstMac[0], gDstMac[1], gDstMac[2],
             gDstMac[3], gDstMac[4], gDstMac[5]);

    // 本地IP转换:网络字节序 -> 主机字节序 -> 点分十进制(修正字节序)
    struct in_addr src_addr;
    src_addr.s_addr = gSrcIp;
    if (inet_ntoa(src_addr) == NULL)
    {
        printf("本地IP格式无效\n");
    }
    else
    {
        printf("本地IP: %s\n", inet_ntoa(src_addr));
    }

    // 目的IP转换:网络字节序 -> 主机字节序 -> 点分十进制(修正字节序)
    struct in_addr dst_addr;
    dst_addr.s_addr = gDstIp;
    if (inet_ntoa(dst_addr) == NULL)
    {
        printf("目的IP格式无效\n");
    }
    else
    {
        printf("目的IP: %s\n", inet_ntoa(dst_addr));
    }

    printf("目的端口: %d\n", gDstPort);
    printf("总发包数: %d\n", count);
    printf("成功发包数: %d\n", success);
    printf("失败发包数: %d\n", failed);
    printf("发包数据: %s\n", send_data);
    printf("每批次推送到tx队列的包数量: %d\n", BURST_SIZE);
    printf("===== 计时对比 =====\n");
    printf("原有微秒级计时(毫秒): %d ms\n", time_used_ms);
    printf("TSC纳秒级计时(微秒): %d us\n", time_used_us);
    printf("TSC纳秒级计时(纳秒): %.2f ns\n", time_used_ns);
    printf("===== 发包速率对比 =====\n");

    // 修正后的发包速率计算
    printf("原有方式测的发包速率: %d pps\n", time_used_ms > 0 ? (success * 1000 / time_used_ms) : 0);
    // 显式转换为uint64_t,避免溢出
    uint64_t tsc_pps = time_used_us > 0 ? ((uint64_t)success * 1000000) / time_used_us : 0;
    printf("TSC方式测的发包速率: %lu pps\n", tsc_pps);
    //printf("原有方式发包速率: %d pps\n", time_used_ms > 0 ? (success * 1000 / time_used_ms) : 0);
    //printf("TSC方式发包速率: %d pps\n", time_used_us > 0 ? (success * 1000000 / time_used_us) : 0);

    return 0;
}
相关推荐
西岸行者6 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意6 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码6 天前
嵌入式学习路线
学习
毛小茛6 天前
计算机系统概论——校验码
学习
babe小鑫6 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms6 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下6 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。6 天前
2026.2.25监控学习
学习
im_AMBER6 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J6 天前
从“Hello World“ 开始 C++
c语言·c++·学习