目录
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数组的大小。