目录
- sendmsg、sendmmsg和recvmmsg
- 相关结构体:mmsghdr、msghdr、iovec
- sendmmsg性能测试
- 关于connect
sendmsg、sendmmsg和recvmmsg
以udp发送为例。
sendmsg 和 sendmmsg :两者都能发送多块数据,区别在于sendmsg会将所有数据整合成一个UDP包发出,sendmmsg是每个 mmsghdr 一个UDP包。sendmmsg 是 sendmsg 的复合加强版。
/*** @brief 系统调用 sendmsg* 在发送时, iovec 数组中的每个元素都表示要发送的一个数据块。sendmsg 会将这些数据块作为一个整体发送。* * @param sockfd * @param msg * @param flags * @return ssize_t */
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
/*** @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);
/*** @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。
struct mmsghdr
{struct msghdr msg_hdr; /* 消息头. */unsigned int msg_len; /* 传输的字节数. */
};
描述sendmsg'和
recvmsg’的消息结构体msghdr。
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; /* 接收消息时设置的标志. */
};
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模式
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.00Average: 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包,不组包
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.00Average: 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源码
#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 32int
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.00Average: 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.00Average: 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.90sendmmsg 相对 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()");elseprintf("%d messages sent\n", retval);exit(0);
}
rcvmmsg.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 1int 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数组的大小。