1、概述
io_uring是linux内核中高效的异步I/O框架,Linux内核5.1引入,用于提高I/O的性能。liburing是基于io_uring封装的库,提供更简洁的api。
2、工作机制
1、应用程序将I/O请求提交到提交队列中,每个请求可以理解为提交队列中的一个节点(SQE)。
2、内核处理。
3、当I/O操作完成,内核将结果放到完成队列中,每个结果为完成队列中的一个节点(CQE)。
异步操作:
io_uring允许用户将I/O操作提交给内核,内核在后台异步处理这些操作。用户不需要等待操作完成,而是可以在稍后查询完成队列以获取操作结果。
举例:
把recv任务抛到到提交到队列中,由内核worker从请求队列中取任务执行任务,把recv结果放到完成队列中,应用程序从完成队列中取结果。
应用程序要做的就是往请求队列中抛任务,从完成队列中取结果。
原理图:
3、思考问题
3.1、io_uring如何避免频繁的拷贝?
问题描述:每个任务频繁添加到请求队列,这里会有频繁的拷贝过程?
用户空间和内核之间共享一块内存区域,用于传递提交的I/O请求和完成的I/O请求事件,使用了mmap映射内存来避免频繁拷贝,所以不需要拷贝。
3.2、提交队列和完成队列如何做到线程安全?
对于io_uring的队列来说,多线程操作不需要加锁,io_uring使用了无锁环形队列,支持多个线程可以安全且高效的并发处理I/O请求。
4、核心函数
io_uring提供了3个系统函数,io_uring_setup,io_uring_enter,io_uring_register。liburing对其封装了一层。liburing主要函数介绍:
io_uring_queue_init_params
初始化io_uring环境,包括初始化提交队列(SQ)和完成队列(CQ),内部会调用io_uring_setup
io_uring_prep_*系列函数
io_uring_prep_read
、io_uring_prep_write
、io_uring_prep_accept
、io_uring_prep_send
等,将I/O操作放到提交队列中
io_uring_submit
将提交队列中的操作提交给内核,触发内核执行操作,内部依赖io_uring_enter系统调用
io_uring_wait_cqe
阻塞等待至少一个操作完成,并返回完成的CQE,这一步是阻塞的
io_uring_peek_batch_cqe
批量获取完成队列中的操作结果, 返回值表示已经完成的操作数量
io_uring_cq_advance
这个函数通知io_uring,应用程序已经处理完这些事件,可以从完成队列中释放了
5、io_uring和epoll区别
io_uring只是个异步I/O框架,可以处理网络通信(socket),也可以处理文件操作(读写文件)等 epoll专门用于处理网络通信(socket)
io_uring从完成队列中获取结果,内核已经把操作完成了。比如read操作,从完成队列获取结果时,数据已经读取到buf中了。
epoll发生事件时,具体的I/O操作仍需用户代码完成,比如当有EPOLLIN事件时,程序需要调用read函数,把数据读到buf中。
io_uring可以批量提交多个I/O操作,然后一次性等待他们的完成,大大的减少了系统调用的数量。
io_uring利用共享内存在用户空间和内核空间传递数据,减少了频繁的拷贝。
epoll使用时需要多次系统调用,例如:epoll_ctrl注册或修改文件描述符事件
epoll每次等待事件,都需要从用户空间切换到内核空间。
6、示例代码,使用io_uring实现tcpserver(代码详细注释)
1、首先安装liburing
sudo apt-get install liburing-dev
2、编译代码
gcc -o io_uring_server io_uring.c
#include <stdio.h>
#include <liburing.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define QUEUE_LENGTH 1024
#define BUFFER_LENGTH 1024
#define LISTEN_BACKLOG 50
#define EVENT_ACCEPT 0
#define EVENT_RECV 1
#define EVENT_SEND 2
struct conn_info
{
int fd;
int event;
};
// 初始化服务器
int init_server(unsigned short port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(port); // 0-1023 系统默认端口
// 绑定
if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
{
printf("bind error\n");
return -1;
}
printf("bind success\n");
// 监听
if(-1 == listen(sockfd, LISTEN_BACKLOG))
{
printf("listen error\n");
return -1;
}
printf("listen success sockfd:%d\n", sockfd);
return sockfd;
}
int set_event_accept(struct io_uring* ring, int sockfd, struct sockaddr* addr, socklen_t* addrlen, int flags)
{
// 将accept操作细节填写到提交队列条目(SQE)中
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
io_uring_prep_accept(sqe, sockfd, addr, addrlen, flags);
struct conn_info accept_info = {
.fd = sockfd,
.event = EVENT_ACCEPT,
};
memcpy(&sqe->user_data, &accept_info, sizeof(accept_info));
}
int set_event_recv(struct io_uring* ring, int fd, void* buf, size_t count, int flags)
{
// 将read操作细节填写到提交队列条目(SQE)中
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe, fd, buf, count, flags);
struct conn_info recv_info = {
.fd = fd,
.event = EVENT_RECV,
};
memcpy(&sqe->user_data, &recv_info, sizeof(recv_info));
}
int set_event_send(struct io_uring* ring, int fd, void* buf, size_t count, int flags)
{
// 将send操作细节填写到提交队列条目(SQE)中
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
io_uring_prep_send(sqe, fd, buf, count, flags);
struct conn_info send_info = {
.fd = fd,
.event = EVENT_SEND,
};
memcpy(&sqe->user_data, &send_info, sizeof(send_info));
}
int main()
{
// 初始化服务器
unsigned short port = 2000;
int listen_fd = init_server(port);
if (listen_fd < 0)
{
printf("init server error\n");
return -1;
}
struct io_uring ring;
struct io_uring_params ring_params;
io_uring_queue_init_params(QUEUE_LENGTH, &ring, &ring_params);
#if 0
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
accept(listen_fd, (struct sockaddr*)&clientaddr, &clientlen);
#else
// 设置事件
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
set_event_accept(&ring, listen_fd, (struct sockaddr*)&clientaddr, &clientlen, 0);
#endif
char buf[BUFFER_LENGTH] = {0};
while(1)
{
// 将准备好的SQE提交给内核,触发内核执行操作,内部依赖io_uring_enter系统调用
io_uring_submit(&ring);
// 阻塞等待至少一个操作完成,并返回完成的CQE,这一步是阻塞的
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
struct io_uring_cqe* cqes[128];
// 批量获取已经完成的操作结果, nready表示完成的操作数量
int nready = io_uring_peek_batch_cqe(&ring, cqes, 128);
for (int i = 0; i < nready; i++)
{
struct io_uring_cqe *cqe = cqes[i];
struct conn_info coninfo;
memcpy(&coninfo, &cqe->user_data, sizeof(struct conn_info));
if (coninfo.event == EVENT_ACCEPT)
{
// 走到这里时,说明有新的连接到来,需要处理
set_event_accept(&ring, listen_fd, (struct sockaddr*)&clientaddr, &clientlen, 0);
int clientfd = cqe->res;
set_event_recv(&ring, clientfd, buf, BUFFER_LENGTH, 0);
}
else if (coninfo.event == EVENT_RECV)
{
// 走到这里时,说明有数据可读,需要处理
int clientfd = coninfo.fd;
int nread = cqe->res;
if (nread <= 0)
{
close(clientfd);
}
else if (nread > 0)
{
set_event_send(&ring, clientfd, buf, BUFFER_LENGTH, 0);
printf("recv data: %s\n", buf);
}
}
else if (coninfo.event == EVENT_SEND)
{
// 走到这里时,说明有数据可写,需要处理
int clientfd = coninfo.fd;
set_event_recv(&ring, clientfd, buf, BUFFER_LENGTH, 0);
}
}
// 这个函数通知io_uring,应用程序已经处理完这些事件,可以释放这些事件了
io_uring_cq_advance(&ring, nready);
}
return 0;
}
学习链接:https://github.com/0voice