epoll + io_uring IO 测试方案(send/recv 网络IO)
核心区分:
- epoll :IO 多路复用,仍然依赖
send/recv系统调用,只是等待就绪事件,适合高并发 TCP 连接压测;- io_uring :Linux 新异步IO框架,可以把
send/recv打包成异步SQE,减少syscall次数,是当前高性能网络IO的主流。 下面给两套可直接编译运行的最小压测代码,单连接吞吐版,再讲多连接压测、指标采集、调优和对比。
环境前提:Linux 内核 ≥ 5.10(io_uring推荐5.15+),gcc,-O2编译。
一、epoll 版本:epoll + send/recv 吞吐测试
epoll 只是事件驱动,收发仍然用传统 send/recv。非阻塞socket。 逻辑: server:epoll监听fd,accept拿到新conn,加入epoll;当conn可读,循环recv。 client:连接成功,循环send,非阻塞。
epoll_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <time.h>
#define PORT 8888
#define BUF_SIZE (1<<20)
#define MAX_EVENTS 16
static int set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
return 0;
}
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
set_nonblock(listen_fd);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
int epoll_fd = epoll_create1(0);
struct epoll_event ev;
ev.data.fd = listen_fd;
ev.events = EPOLLIN;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
char *buf = malloc(BUF_SIZE);
struct epoll_event events[MAX_EVENTS];
long long total_recv = 0;
int conn_fd = -1;
clock_t start = 0;
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
if (fd == listen_fd) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int cfd = accept4(listen_fd, (struct sockaddr*)&client_addr, &client_len, SOCK_NONBLOCK);
if (cfd < 0) continue;
printf("client connected %d\n", cfd);
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET; // EPOLLET 边缘触发
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, cfd, &ev);
conn_fd = cfd;
start = clock();
} else if (events[i].events & EPOLLIN) {
int n;
while ((n = recv(fd, buf, BUF_SIZE, 0)) > 0) {
total_recv += n;
}
if (n == 0 || (n < 0 && errno != EAGAIN)) {
// 对端关闭
clock_t end = clock();
double sec = (double)(end - start)/CLOCKS_PER_SEC;
printf("epoll server recv total: %lld bytes, bw: %.2f MB/s\n",
total_recv, total_recv / sec / (1<<20));
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
free(buf);
close(epoll_fd);
close(listen_fd);
return 0;
}
}
}
}
}
epoll_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <time.h>
#define PORT 8888
#define BUF_SIZE (1<<20)
#define SEND_TOTAL (10LL << 30) // 10GB
#define MAX_EVENTS 8
static int set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
return 0;
}
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
set_nonblock(sock);
struct sockaddr_in serv_addr = {0};
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
int epoll_fd = epoll_create1(0);
struct epoll_event ev;
ev.data.fd = sock;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev);
char *buf = malloc(BUF_SIZE);
memset(buf, 'A', BUF_SIZE);
long long remain = SEND_TOTAL;
clock_t start = clock();
struct epoll_event events[MAX_EVENTS];
while (remain > 0) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLOUT) {
while (remain > 0) {
int send_len = remain > BUF_SIZE ? BUF_SIZE : remain;
int n = send(sock, buf, send_len, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EAGAIN) break;
perror("send");
exit(1);
}
remain -= n;
}
if (remain <= 0) {
clock_t end = clock();
double sec = (double)(end - start)/CLOCKS_PER_SEC;
printf("epoll client send total: %lld bytes, bw: %.2f MB/s\n",
SEND_TOTAL, SEND_TOTAL / sec / (1<<20));
close(sock);
free(buf);
close(epoll_fd);
return 0;
}
}
}
}
}
编译&运行:
gcc epoll_server.c -o epoll_server -O2
gcc epoll_client.c -o epoll_client -O2
# 窗口1
./epoll_server
# 窗口2
./epoll_client
epoll特点: ✅ 适合大量连接,只等事件就绪; ❌ 就绪后还是要循环调用 send/recv,每个收发都触发syscall,小包场景syscall开销高。
参考链接 :0voice · GitHub