1.介绍
io_uring是linux5.1版本中引入的一种异步IO机制,它是一套全新的系统调用,它的设计目的是为了克服传统 POSIX AIO 接口存在的限制,提供更高的性能和更好的可扩展性。
io_uring通过使用共享内存和环形缓冲区机制,io_uring 减少了应用程序与内核之间交互所需的系统调用次数,从而降低了开销,它会申请一块用户态与内核态共享的内存,并再这块内存中建立一个环形队列,以此来实现用户与内核的通信,避免不必要的拷贝以此提高性能。
它的实现过程为每个io_uring实例映射一个fd,然后调用io_uring_setup系统调用分配一块共享内存,通过mmap映射到用户态,它的流程如下图:

那么io_uring 与epoll的性能究竟谁更好呢?下面我们直接上代码测试。
2.代码实现
对于io_uring在网络编程中使用,会用到liburing的库,它提供了一系列辅助函数和工具,简化了io_uring 的编程模型,使开发者能够更容易地利用 io_uring 的优势,对于liburing库的安装可以去他的官方仓库把源码clone下来,仓库地址:liburing。
git clone https://github.com/axboe/liburing.git
然后进入库目录,执行以下命令:
./configure --cc=gcc --cxx=g++
make -j$(nproc)
sudo make install
liburing库用来网络编程的主要函数如下:
//用于初始化 io_uring 上下文,并允许你设置一些高级参数
//entries用于指定提交实例的数量,第二个参数传入io_uring实例
//第三个参数struct io_uring_params 结构的指针
int io_uring_queue_init_params(unsigned entries, struct io_uring *ring,struct io_uring_params *p);
//用于从 io_uring 的提交队列中获取一个空闲的提交队列条目。
//这个函数是创建和提交I/O请求的关键步骤之一。
//传入的参数为指向struct io_uring结构的指针,该结构表示io_uring上下文
struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);
//用于将提交队列中的所有未提交的I/O请求提交给内核。这个函数是io_uring
//模型中一个非常重要的部分,因为它负责将应用程序设置的 I/O 请求实际发送到内核进行处理。
// 指向struct io_uring结构的指针,该结构表示io_uring上下文。
int io_uring_submit(struct io_uring *ring);
//用于等待一个 I/O 请求的完成事件(Completion Queue Event, CQE)。这个函数会阻塞,
//直到有一个完成事件可用为止
//第一个参数为指向struct io_uring结构的指针,该结构表示 io_uring 上下文
//第二个参数为指向struct io_uring_cqe*类型的指针,用于接收完成事件的指针。
int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);
//用于从完成队列中批量获取多个完成事件。与io_uring_wait_cqe不同,io_uring_peek_batch_cqe不会阻塞,
//而是立即返回当前可用的完成事件数量。这使得它适合在非阻塞场景中使用,例如在事件循环中检查是否有新的完成事件
//第一个参数:指向struct io_uring结构的指针,该结构表示io_uring上下文。
//第二个参数:指向struct io_uring_cqe*类型的数组,用于接收完成事件的指针。
//第三个参数:数组cqes的大小,即最多可以获取的完成事件数量。
unsigned io_uring_peek_batch_cqe(struct io_uring *ring, struct io_uring_cqe **cqes, unsigned count);
//用于准备一个发送数据的I/O请求。这个函数会填充struct io_uring_sqe结构体,以便后续使用io_uring_submit提交请求
//第一个参数:指向struct io_uring_sqe结构的指针,该结构表示一个提交队列条目。
//其他参数与send()函数一样
void io_uring_prep_send(struct io_uring_sqe *sqe, int sockfd, const void *buf, size_t len, int flags);
//用于准备一个发送数据的I/O请求。这个函数会填充struct io_uring_sqe结构体,以便后续使用io_uring_submit提交请求
//其他参数与recv()函数一样
void io_uring_prep_recv(struct io_uring_sqe *sqe, int sockfd, void *buf, size_t len, int flags);
ok,现在来实现echo server代码:
#include <stdio.h>
#include <liburing.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define EVENT_ACCEPT 1
#define EVENT_READ 1 << 1
#define EVENT_WRITE 1 << 2
struct conn {
int fd;
int event;
};
char buffer[1024][1024] = {0};
int init_server(unsigned short port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(port);
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) < 0) {
perror("bind");
return -1;
}
listen(sockfd, 10);
return sockfd;
}
int set_event_recv(struct io_uring *ring, int sockfd,
void *buf, size_t len, int flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
struct conn recv_info = {
.fd = sockfd,
.event = EVENT_READ,
};
io_uring_prep_recv(sqe, sockfd, buf, len, flags);
memcpy(&sqe->user_data, &recv_info, sizeof(struct conn));
}
int set_event_send(struct io_uring *ring, int sockfd,
void *buf, size_t len, int flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
struct conn send_info = {
.fd = sockfd,
.event = EVENT_WRITE,
};
io_uring_prep_send(sqe, sockfd, buf, len, flags);
memcpy(&sqe->user_data, &send_info, sizeof(struct conn));
}
int set_event_accept(struct io_uring *ring, int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
struct conn accept_info = {
.fd = sockfd,
.event = EVENT_ACCEPT,
};
io_uring_prep_accept(sqe, sockfd, (struct sockaddr*)addr, addrlen, flags);
memcpy(&sqe->user_data, &accept_info, sizeof(struct conn));
}
int main() {
unsigned short port = 9999;
int sockfd = init_server(port);
struct io_uring_params params;
memset(¶ms, 0, sizeof(params));
struct io_uring ring;
io_uring_queue_init_params(1024, &ring, ¶ms);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
while (1) {
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
struct io_uring_cqe *cqes[128] = {0};
int nready = io_uring_peek_batch_cqe(&ring, cqes, 128);
int i = 0;
for (i = 0;i < nready;i ++) {
struct io_uring_cqe *entries = cqes[i];
struct conn result;
memcpy(&result, &entries->user_data, sizeof(struct conn));
if (result.event & EVENT_ACCEPT) {
set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
int connfd = entries->res;
set_event_recv(&ring, connfd, buffer[connfd], 1024, 0);
} else if (result.event & EVENT_READ) {
int ret = entries->res;
if (ret == 0) {
close(result.fd);
} else if (ret > 0) {
set_event_send(&ring, result.fd, buffer[result.fd], ret, 0);
}
} else if (result.event & EVENT_WRITE) {
int ret = entries->res;
set_event_recv(&ring, result.fd, buffer[result.fd], 1024, 0);
}
}
//清理队列
io_uring_cq_advance(&ring, nready);
}
for (int i = 0; i < 1024; i++) {
free(buffer[i]);
}
return 0;
}
我的linux内核版本是6.2.0,建议不要低于我的版本

运行看结果 :

ok,可以看到可以正常收发数据,现在我们来测试它的性能,现在来写一个client测试代码。
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <pthread.h>
#include <arpa/inet.h>
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)
#define TEST_MESSAGE "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz\r\n"
#define RBUFFER_LENGTH 1024
#define WBUFFER_LENGTH 1024
typedef struct test_context_s {
char serverip[16];
int port;
int threadnum;
int connection;
int requestion;
int failed;
} test_context_t;
typedef struct test_context_s test_context_t;
int connect_server(const char *ip, unsigned short port) {
int connfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in tcpserver_addr;
memset(&tcpserver_addr, 0, sizeof(struct sockaddr_in));
tcpserver_addr.sin_family = AF_INET;
tcpserver_addr.sin_addr.s_addr = inet_addr(ip);
tcpserver_addr.sin_port = htons(port);
int ret = connect(connfd, (struct sockaddr*)&tcpserver_addr, sizeof(struct sockaddr_in));
if (ret) {
perror("connect");
return -1;
}
return connfd;
}
int send_recv_tcppkt(int fd) {
char wbuffer[WBUFFER_LENGTH] = {0};
int i = 0;
for (i = 0;i < 8;i ++) {
strcpy(wbuffer + i * strlen(TEST_MESSAGE), TEST_MESSAGE);
}
int res = send(fd, wbuffer, strlen(wbuffer), 0);
if (res < 0) {
exit(1);
}
char rbuffer[RBUFFER_LENGTH] = {0};
res = recv(fd, rbuffer, RBUFFER_LENGTH, 0);
if (res <= 0) {
exit(1);
}
if (strcmp(rbuffer, wbuffer) != 0) {
printf("failed: '%s' != '%s'\n", rbuffer, wbuffer);
return -1;
}
return 0;
}
static void *test_qps_entry(void *arg) {
test_context_t *pctx = (test_context_t*)arg;
int connfd = connect_server(pctx->serverip, pctx->port);
if (connfd < 0) {
printf("connect_tcpserver failed\n");
return NULL;
}
int count = pctx->requestion / pctx->threadnum;
int i = 0;
int res;
while (i++ < count) {
res = send_recv_tcppkt(connfd);
if (res != 0) {
printf("send_recv_tcppkt failed\n");
pctx->failed ++; //
continue;
}
}
return NULL;
}
// ./test_qps -s 127.0.0.1 -p 9999 -t 50 -c 100 -n 1000000
int main(int argc, char *argv[]) {
int ret = 0;
test_context_t ctx = {0};
int opt;
while ((opt = getopt(argc, argv, "s:p:t:c:n:?")) != -1) {
switch (opt) {
case 's':
printf("-s: %s\n", optarg);
strcpy(ctx.serverip, optarg);
break;
case 'p':
printf("-p: %s\n", optarg);
ctx.port = atoi(optarg);
break;
case 't':
printf("-t: %s\n", optarg);
ctx.threadnum = atoi(optarg);
break;
case 'c':
printf("-c: %s\n", optarg);
ctx.connection = atoi(optarg);
break;
case 'n':
printf("-n: %s\n", optarg);
ctx.requestion = atoi(optarg);
break;
default:
return -1;
}
}
pthread_t *ptid = malloc(ctx.threadnum * sizeof(pthread_t));
int i = 0;
struct timeval tv_begin;
gettimeofday(&tv_begin, NULL);
for (i = 0;i < ctx.threadnum;i ++) {
pthread_create(&ptid[i], NULL, test_qps_entry, &ctx);
}
for (i = 0;i < ctx.threadnum;i ++) {
pthread_join(ptid[i], NULL);
}
struct timeval tv_end;
gettimeofday(&tv_end, NULL);
int time_used = TIME_SUB_MS(tv_end, tv_begin);
printf("success: %d, failed: %d, time_used: %d, qps: %d\n", ctx.requestion-ctx.failed,
ctx.failed, time_used, ctx.requestion * 1000 / time_used);
clean:
free(ptid);
return ret;
}
现在运行看结果:
现在再来写一个基于epoll的rector的echo server与io_uring对比:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/epoll.h>
#define BUFFER_SIZE 1024
#define CONNECT_SIZE 1024
typedef int (*CALLBACK)(int fd);
typedef struct conn {
int fd;
char rbuffer[BUFFER_SIZE];
int rlen;
char wbuffer[BUFFER_SIZE];
int wlen;
CALLBACK send_callback;
union {
CALLBACK recv_callback;
CALLBACK accept_callback;
} r_actor;
} conn;
int epfd = 0;
static conn conn_list[CONNECT_SIZE] = {0};
void set_event(int fd, int event) {
struct epoll_event ev;
ev.data.fd = fd;
ev.events = event;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
int send_cb(int fd) {
conn* c = &conn_list[fd];
int len = send(c->fd, c->wbuffer, c->wlen, 0);
set_event(c->fd, EPOLLIN | EPOLLET);
return len;
}
int recv_cb(int fd) {
conn* c = &conn_list[fd];
memset(c->rbuffer, 0, BUFFER_SIZE);
c->rlen = recv(c->fd, c->rbuffer, BUFFER_SIZE, 0);
if(c->rlen <= 0) {
close(c->fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, c->fd, NULL);
return -1;
}
memcpy(c->wbuffer, c->rbuffer, c->rlen);
c->wlen = c->rlen;
set_event(c->fd, EPOLLOUT | EPOLLET);
}
int accept_cb(int fd) {
conn* c = &conn_list[fd];
struct sockaddr_in client_addr;
socklen_t len = sizeof(struct sockaddr_in);
int connfd = accept(c->fd, (struct sockaddr*)&client_addr, &len);
if(connfd < 0) {
printf("accept failed\n");
return -1;
}
conn_list[connfd].fd = connfd;
conn_list[connfd].r_actor.recv_callback = recv_cb;
conn_list[connfd].send_callback = send_cb;
struct epoll_event ev;
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
serv_addr.sin_family = INADDR_ANY;
serv_addr.sin_port = htons(9999);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr_in)) < 0) {
printf("bind failed\n");
close(listenfd);
return -1;
}
listen(listenfd, 10);
printf("listen listenfd: %d\n", listenfd);
conn_list[listenfd].fd = listenfd;
conn_list[listenfd].r_actor.recv_callback = accept_cb;
epfd = epoll_create(1);
struct epoll_event ev;
ev.data.fd = listenfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while(1) {
struct epoll_event events[1024];
int nready = epoll_wait(epfd, events, 1024, -1);
for(int i = 0; i < nready; ++i) {
if(events[i].events & EPOLLIN) {
conn_list[events[i].data.fd].r_actor.recv_callback(events[i].data.fd);
}
if(events[i].events & EPOLLOUT) {
conn_list[events[i].data.fd].send_callback(events[i].data.fd);
}
}
}
}
再来看测试结果:

可以看到,在每个包512bytes下io_uring的性能比epoll高10%左右,接下来再来做128bytes与256bytes看看结果。
| 128bytes | 256bytes | 512bytes | |
| epoll | 13122ms | 13484ms | 12915ms |
| io_uring | 11770ms | 12403ms | 11456ms |
| 差值 | 1352ms | 1081ms | 1459ms |
3.总结
根据结果可以看到,基本上io_uring的性能比rector模型下的epoll要高10%左右。
学习参考: 0voice · GitHub

1293

被折叠的 条评论
为什么被折叠?



