linux性能提升之sendmmsg和recvmmsg

目录

sendmsg、sendmmsg和recvmmsg

以udp发送为例。

sendmsg 和 sendmmsg :两者都能发送多块数据,区别在于sendmsg会将所有数据整合成一个UDP包发出,sendmmsg是每个 mmsghdr 一个UDP包。sendmmsg 是 sendmsg 的复合加强版。

cpp 复制代码
/**
 * @brief 系统调用 sendmsg
 * 在发送时, iovec 数组中的每个元素都表示要发送的一个数据块。sendmsg 会将这些数据块作为一个整体发送。
 * 
 * @param sockfd 
 * @param msg 
 * @param flags 
 * @return ssize_t 
 */
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
cpp 复制代码
/**
 * @brief 系统调用 sendmmsg ,可以发送多个mmsghdr数据,每个mmsghdr包含一个msghdr,msghdr中包含多个数据包构成的数组iovec*
 * 
 * @param __fd          fd
 * @param __vmessages   指向 mmsghdr 数组的指针
 * @param __vlen        mmsghdr 数组的成员个数
 * @param __flags 
 * @return int          成功发送 mmsghdr 的个数
 */
extern int sendmmsg (int __fd, struct mmsghdr *__vmessages,unsigned int __vlen, int __flags);
cpp 复制代码
/**
 * @brief recvmmsg消息接收
 * 在接收时, iovec 数组中的每个元素都表示接收的数据块的位置和大小。recvmsg 会将接收到的数据分散到这些缓冲区中。
 * @param __fd          fd
 * @param __vmessages   消息结构体数组mmsghdr *
 * @param __vlen        消息结构体mmsghdr*数组大小
 * @param __flags       标识
 * @param __tmo         超时
 * @return int       Returns the number of bytes read or -1 for errors
 */
extern int recvmmsg (int __fd, struct mmsghdr *__vmessages,
		     unsigned int __vlen, int __flags,
		     const struct timespec *__tmo);

相关结构体:mmsghdr、msghdr、iovec

sendmmsg消息结构体mmsghdr。

cpp 复制代码
struct mmsghdr
{
    struct msghdr msg_hdr;	/* 消息头.  */
    unsigned int msg_len;	/* 传输的字节数.  */
};

描述sendmsg'和recvmsg'的消息结构体msghdr。

cpp 复制代码
struct msghdr
{
    void *msg_name;		/* 发送/接收的网络地址.  */
    socklen_t msg_namelen;	/* 网络地址长度.  */

    struct iovec *msg_iov;	/* 指向数据缓冲区的指针,可以是数组.  */
    size_t msg_iovlen;		/* 数据缓存区数组 msg_iov 的成员数量.  */

    void *msg_control;		/* 指向辅助数据的缓冲区 (用于控制消息) */
    size_t msg_controllen;	/* 辅助数据缓冲区的大小 */

    int msg_flags;		/* 接收消息时设置的标志.  */
};
cpp 复制代码
struct iovec 
{
    void  *iov_base;    // 指向数据缓冲区的指针
    size_t iov_len;     // 数据缓冲区的长度
};

sendmmsg性能测试

使用官方手册的例子,进行改写:
https://man7.org/linux/man-pages/man2/sendmmsg.2.html
https://man7.org/linux/man-pages/man2/recvmmsg.2.html

测试用例1:sendto模式

cpp 复制代码
Average:        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
Average:         eth0      9.72      9.32      1.27      1.56      0.00      0.00      0.00
Average:           lo 568170.44 568170.44  78788.46  78788.46      0.00      0.00      0.00

Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
Average:     all    6.73    0.00   18.75    0.00    0.00   26.53    0.00    0.00    0.00   47.98
Average:       0    8.58    0.00   27.74    0.00    0.00   40.52    0.00    0.00    0.00   23.15
Average:       1    4.98    0.00    9.76    0.00    0.00   12.45    0.00    0.00    0.00   72.81

测试用例2:sendmmsg模式,1个mmsghdr只包含一个udp包,不组包

cpp 复制代码
Average:        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
Average:         eth0      7.67      6.94      0.85      1.74      0.00      0.00      0.00
Average:           lo 665072.24 665072.24  92226.18  92226.18      0.00      0.00      0.00

Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
Average:     all    1.45    0.00   19.90    0.00    0.00   30.60    0.00    0.00    0.00   48.05
Average:       0    0.60    0.00   30.06    0.00    0.00   47.19    0.00    0.00    0.00   22.14
Average:       1    2.20    0.00    9.80    0.00    0.00   14.10    0.00    0.00    0.00   73.90

sendmmsg 相对 sendto 的优势:

1、降低了用户使用CPU占比(%usr),但对内核系统(%sys)和软中断(%soft)没有改善;传输速率提升15%左右。

2、sendmmsg :经测试,如果每个原始udp包都设置一个 mmsghdr 头, __vlen 数量和原始udp包数量一致,观察CPU软中断、性能消耗等和sendto类似,%usr消耗下降,速率有提升但有限。

3、sendmmsg :如果把多个原始udp包都放在一个 mmsghdr 头, __vlen 数量是1,CPU软中断下降,速率有提升,但所有udp包会被合并成一个udp大包,使用同一个ip/udp头部,相当于调用了 sendmsg 。

4、思考: sendmsg/sendmmsg/sendto 等系统调用只是把数据拷贝到内核缓存区,这个拷贝的过程并不会消耗太多CPU性能,真正消耗性能触发软中断的是内核把数据包通过网卡发送出去。

sndmmsg.cpp源码

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

#define BUFF_SIZE    100
#define ARR_SIZE     32

int
main(void)
{
    int                 retval;
    int                 sockfd;
    struct sockaddr_in  addr;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket()");
        exit(EXIT_FAILURE);
    }

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_port = htons(1234);
#if 0 // 是否使用connect
/*
1、如果使用connect,则可以用send代替sendto; sendmmsg 的msg_name和msg_namelen可以不赋值;但如果对端没有启动,sendmmsg 发送的返回值只有1。
2、不使用connect,sendmmsg 的msg_name和msg_namelen需要赋值才能发送成功,且不管对方有没有启动,sendmmsg的正常返回值就是第三个参数大小,即mmsghdr数组的大小。
*/
    if (connect(sockfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
        perror("connect()");
        exit(EXIT_FAILURE);
    }
#endif

#if 0 // sendto模式
/*
Average:        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
Average:         eth0      9.72      9.32      1.27      1.56      0.00      0.00      0.00
Average:           lo 568170.44 568170.44  78788.46  78788.46      0.00      0.00      0.00

Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
Average:     all    6.73    0.00   18.75    0.00    0.00   26.53    0.00    0.00    0.00   47.98
Average:       0    8.58    0.00   27.74    0.00    0.00   40.52    0.00    0.00    0.00   23.15
Average:       1    4.98    0.00    9.76    0.00    0.00   12.45    0.00    0.00    0.00   72.81
*/
    std::string msg = "hello";
    char* buf = new char[BUFF_SIZE];
    while(1)
    {
        sendto(sockfd, buf,BUFF_SIZE,0,(struct sockaddr*)&addr,sizeof(addr));
    }
#endif

#if 0 // sendmmsg模式1,1个mmsghdr只包含一个udp包,不组包
/*
Average:        IFACE   rxpck/s   txpck/s    rxkB/s    txkB/s   rxcmp/s   txcmp/s  rxmcst/s
Average:         eth0      7.67      6.94      0.85      1.74      0.00      0.00      0.00
Average:           lo 665072.24 665072.24  92226.18  92226.18      0.00      0.00      0.00

Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
Average:     all    1.45    0.00   19.90    0.00    0.00   30.60    0.00    0.00    0.00   48.05
Average:       0    0.60    0.00   30.06    0.00    0.00   47.19    0.00    0.00    0.00   22.14
Average:       1    2.20    0.00    9.80    0.00    0.00   14.10    0.00    0.00    0.00   73.90

sendmmsg 相对 send 的优势:降低了用户使用CPU占比(%usr),但对内核系统(%sys)和软中断(%soft)没有改善;传输速率提升15%左右。
*/
    struct iovec    _iovec[ARR_SIZE];
    struct mmsghdr  _hdrvec[ARR_SIZE];
    
    printf("iovec.size=%d\n",sizeof(iovec));
    printf("_iovec.size=%d\n",sizeof(_iovec));
    memset(_iovec, 0 ,sizeof(_iovec));
    memset(_hdrvec, 0 ,sizeof(_hdrvec));
    for(int index=0;index<ARR_SIZE;index++)
    {
        char* buf = new char[BUFF_SIZE];
        _iovec[index].iov_base = buf;
        _iovec[index].iov_len  = BUFF_SIZE;

        _hdrvec[index].msg_hdr.msg_iov = &_iovec[index];
        _hdrvec[index].msg_hdr.msg_iovlen = 1;
        _hdrvec[index].msg_hdr.msg_name = (void *)&addr;    // 指向目标地址结构体的指针 
        _hdrvec[index].msg_hdr.msg_namelen = sizeof(addr);     // 目标地址结构体的大小 
    }

    while(1)
    {
        retval = sendmmsg(sockfd, _hdrvec, ARR_SIZE, 0);
        // if (retval == -1) 
        //     perror("sendmmsg()");
        // else
        //     printf("%d messages sent\n", retval);
    }
#endif

// sendmmsg模式2,1个mmsghdr包含多个udp包,组包
// 如果可以组包发送,建议直接在应用层组包,而不是通过系统调用组包,后者只会增加开发难度。

// 原测试模块,发送文本,msg1是2个包,发送时合并成一个udp包发送,最终发送2个udp包:"onetwo"和"three"
// 每个mmsghdr头代表一个udp包
    struct iovec        msg1[2], msg2;
    struct mmsghdr      msg[2];
    memset(msg1, 0, sizeof(msg1));
    msg1[0].iov_base = (void*)"one";
    msg1[0].iov_len = 3;
    msg1[1].iov_base = (void*)"two";
    msg1[1].iov_len = 3;

    memset(&msg2, 0, sizeof(msg2));
    msg2.iov_base = (void*)"three";
    msg2.iov_len = 5;

    memset(msg, 0, sizeof(msg));
    msg[0].msg_hdr.msg_iov = msg1;
    msg[0].msg_hdr.msg_iovlen = 2;
    msg[0].msg_hdr.msg_name = (void *)&addr;    // 指向目标地址结构体的指针 
    msg[0].msg_hdr.msg_namelen = sizeof(addr);     // 目标地址结构体的大小 

    msg[1].msg_hdr.msg_iov = &msg2;
    msg[1].msg_hdr.msg_iovlen = 1;
    msg[1].msg_hdr.msg_name = (void *)&addr;    // 指向目标地址结构体的指针 
    msg[1].msg_hdr.msg_namelen = sizeof(addr);     // 目标地址结构体的大小 

    retval = sendmmsg(sockfd, msg, 2, 0);
    if (retval == -1)
        perror("sendmmsg()");
    else
        printf("%d messages sent\n", retval);

    exit(0);
}

rcvmmsg.cpp源码

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

int
main(void)
{
#define VLEN 10
#define BUFSIZE 200
#define TIMEOUT 1
    int                 sockfd, retval;
    char                bufs[VLEN][BUFSIZE+1];
    struct iovec        iovecs[VLEN];
    struct mmsghdr      msgs[VLEN];
    struct timespec     timeout;
    struct sockaddr_in  addr;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket()");
        exit(EXIT_FAILURE);
    }

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    addr.sin_port = htons(1234);
    if (bind(sockfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
        perror("bind()");
        exit(EXIT_FAILURE);
    }

    memset(msgs, 0, sizeof(msgs));
    for (size_t i = 0; i < VLEN; i++) {
        iovecs[i].iov_base         = bufs[i];
        iovecs[i].iov_len          = BUFSIZE;
        msgs[i].msg_hdr.msg_iov    = &iovecs[i];
        msgs[i].msg_hdr.msg_iovlen = 1;
    }

    timeout.tv_sec = TIMEOUT;
    timeout.tv_nsec = 0;

    while(1) 
    {
        retval = recvmmsg(sockfd, msgs, VLEN, 0, &timeout);
        if (retval == -1) {
            perror("recvmmsg()");
            exit(EXIT_FAILURE);
        }

        printf("%d messages received\n", retval);
        for (size_t i = 0; i < retval; i++) {
            bufs[i][msgs[i].msg_len] = 0;
            printf("%zu %s\n", i+1, bufs[i]);
        }
    }

    exit(EXIT_SUCCESS);
}

关于connect

1、如果使用connect,则可以用send代替sendto; sendmmsg 的msg_name和msg_namelen可以不赋值;但如果对端没有启动,sendmmsg 发送的返回值只有1。

2、不使用connect,sendmmsg 的msg_name和msg_namelen需要赋值才能发送成功,且不管对方有没有启动,sendmmsg的正常返回值就是第三个参数大小,即mmsghdr数组的大小。

相关推荐
爱跑步的一个人10 分钟前
STL-常用排序算法
开发语言·c++·排序算法
StudyHappiness23 分钟前
MongoDB新版本,单节点安装
linux·运维·mongodb·kylin
星光樱梦37 分钟前
24. 正则表达式
c++
fathing38 分钟前
c# 调用c++ 的dll 出现找不到函数入口点
开发语言·c++·c#
Kasen's experience1 小时前
STM32 GPIO 配置
stm32·单片机·嵌入式硬件
知行电子-1 小时前
Proteus中数码管动态扫描显示不全(已解决)
单片机·proteus·嵌入式
运维佬1 小时前
在 Linux 系统上部署 Apache Solr
linux·apache·solr
编程墨客1 小时前
第03章 文件编程
linux·运维·服务器
命里有定数1 小时前
windows工具 -- 使用rustdesk和云服务器自建远程桌面服务, 手机, PC, Mac, Linux远程桌面 (简洁明了)
linux·运维·服务器·windows·ubuntu·远程工作
cleveryuoyuo1 小时前
进程的程序替换exec*函数和shell实现
linux·服务器