新王登基?解密与epoll媲美的io_uring

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(&params, 0, sizeof(params));

	struct io_uring ring;
	io_uring_queue_init_params(1024, &ring, &params);

    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看看结果。

128bytes256bytes512bytes
epoll13122ms13484ms12915ms
io_uring11770ms12403ms11456ms
差值1352ms1081ms1459ms

3.总结

        根据结果可以看到,基本上io_uring的性能比rector模型下的epoll要高10%左右。 

        学习参考: 0voice · GitHub

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值