1. 概述
io_uring 是一种高效的 Linux 输入/输出(I/O)子系统,用于提高异步I/O的性能和灵活性。
它提供了一种比传统的 POSIX 异步 I/O 接口更高效和可扩展的方法来执行文件操作、网络通信等任务。
2. io_uing 原理图
io_uring 组成:
- 用户空间库(liburing):用户空间程序与内核通信的接口。
- 环形提交队列:用于向内核提交 I/O 请求的环形队列。应用程序将待执行的 I/O 请求描述符填充到提交队列中,然后通知内核开始执行相应的 I/O 操作。
- 环形完成队列: 用于存放已完成的 I/O 请求事件的环形队列。当内核完成一个 I/O 请求时,会将相关的完成事件描述符填充到完成队列中,并通知应用程序。应用程序可以从完成队列中读取完成事件,以获取有关已完成的 I/O 请求的信息。
- 共享内存:用户空间的应用程序和内核之间需要共享一块内存区域,用于传递提交的 I/O 请求和完成的 I/O 请求事件。
处理流程:
- 初始化 io_uring: 初始化共享内存的大小,环形队列的大小,以及是否启用提交和完成事件的批量处理。
- 应用程序提交请求:应用程序通过 mmap 填充一个或多个 I/O 操作 到 环形提交队列。
- 内核处理请求:内核检查提交队列中的请求,并开始执行相应的 I/O 操作。
- I/O 操作完成:当内核完成一个 I/O 操作时,它将相关的完成事件描述符填充到完成队列中。
- 应用程序处理完成事件:应用程序可以通过轮询或者异步通知等方式,从完成队列中读取完成事件描述符。
性能优势:
- 零拷贝:用户事件提交和内核完成事件返回都使用 mmap 。
- 异步 I/O:应用程序可以将I/O请求提交到io_uring的提交队列中,而无需等待这些请求的完成。
- 批量操作:一次性提交多个I/O请求和一次性处理多个完成事件,从而减少系统调用和上下文切换的开销,提高了系统的效率。
- 事件通知机制:io_uring通过epoll机制来通知应用程序提交的I/O请求是否已完成。
3. io_uing API
long io_uring_setup(u32 entries, struct io_uring_params __user *params);
作用:
用于创建一个新的 io_uring 实例。
它初始化了提交队列(SQ)和完成队列(CQ)并返回一个文件描述符。
参数:
entries:提交队列的条目数,确定了可以排队的 I/O 操作数量。
params: 用户空间指针,指向 io_uring_params 结构,用于配置和获取 io_uring 实例的信息。
返回值:
成功时返回一个文件描述符。
失败时返回一个负值的错误码。
----------------------------------------------------------------------------------------------------------------------
long io_uring_enter(unsigned fd, unsigned to_submit, unsigned min_complete, unsigned flags, sigset_t *sig);
作用:
触发内核来处理提交队列中的 I/O 请求
参数:
fd:由 io_uring_setup 返回的 io_uring 实例的文件描述符。
to_submit:指定要从 SQ 提交到内核的 I/O 请求的数量。
min_complete:告诉内核在返回之前至少需要完成多少个 I/O 操作。
flags:用于修改 io_uring_enter 的行为,例如可以设置以阻塞的方式等待。
sig:指向 sigset_t 结构的指针,用于指定在等待时要屏蔽的信号集。
返回值:
成功:返回已完成的请求数量。
如果指定了 IORING_ENTER_GETEVENTS,则返回在 min_complete 参数中指定的事件数或更多。
失败:返回一个负值的错误码。
----------------------------------------------------------------------------------------------------------------------
long io_uring_register(unsigned fd, unsigned opcode, void *arg, unsigned nr_args);
作用:
可以注册文件描述符、缓冲区、I/O 缓冲区、文件更新通知等
参数:
fd:由 io_uring_setup 返回的 io_uring 实例的文件描述符。
opcode:指定要执行的注册操作的类型,例如注册文件描述符或缓冲区。
arg:指向要注册资源的指针。
nr_args:指定 arg 指向的资源数量。
返回值:
成功时通常返回 0。
失败时返回一个负值的错误码
----------------------------------------------------------------------------------------------------------------------
4. liburing
liburing 是一个为了简化 io_uring 接口的使用而创建的库。它提供了一组更高级别的 API,使得开发者能够更容易地利用 Linux 的异步 I/O 功能。
io_uring_queue_init(); //初始化 io_uring 实例。
io_uring_queue_exit(); //清理和释放 io_uring 实例。
io_uring_get_sqe(); //从提交队列中获取一个提交队列条目(SQE)。
io_uring_sqe_set_data(); //给 SQE 设置用户数据。
io_uring_sqe_set_flags(); //设置 SQE 的标志。
io_uring_prep_readv(); //io_uring_prep_writev(): 准备读写向量操作的 SQE。
io_uring_prep_read_fixed(), io_uring_prep_write_fixed(); //准备带有固定缓冲区的读写操作的 SQE。
io_uring_submit(); //提交请求到内核。
io_uring_wait_cqe() //等待至少一个完成事件。
io_uring_peek_cqe(); //非阻塞地检查是否有完成事件。
io_uring_cqe_seen(); //标记一个完成事件已被用户处理。
5. 示例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <liburing.h>
#define ENTRIES_LENGTH 4096
#define MAX_CONNECTIONS 1024
#define BUFFER_LENGTH 1024
char buf_table[MAX_CONNECTIONS][BUFFER_LENGTH] = {0};
enum {
READ,
WRITE,
ACCEPT,
};
struct conninfo {
int connfd;
int type;
};
// 准备并设置一个读事件到 io_uring 队列中
void set_read_event(struct io_uring *ring, int fd, void *buf, size_t len, int flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring); // 获取提交队列条目
io_uring_prep_recv(sqe, fd, buf, len, flags); // 准备接收请求
struct conninfo ci = {
.connfd = fd,
.type = READ
};
memcpy(&sqe->user_data, &ci, sizeof(struct conninfo)); // 关联用户数据
return ;
}
// 准备并设置一个写事件到 io_uring 队列中
void set_write_event(struct io_uring *ring, int fd, const void *buf, size_t len, int flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring); // 获取提交队列条目
io_uring_prep_send(sqe, fd, buf, len, flags); // 准备发送请求
struct conninfo ci = {
.connfd = fd,
.type = WRITE
};
memcpy(&sqe->user_data, &ci, sizeof(struct conninfo)); // 关联用户数据
return ;
}
// 准备并设置一个接受连接事件到 io_uring 队列中
void set_accept_event(struct io_uring *ring, int fd,
struct sockaddr *cliaddr, socklen_t *clilen, unsigned flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring); // 获取提交队列条目
io_uring_prep_accept(sqe, fd, cliaddr, clilen, flags); // 准备接受连接请求
struct conninfo ci = {
.connfd = fd,
.type = ACCEPT
};
memcpy(&sqe->user_data, &ci, sizeof(struct conninfo)); // 关联用户数据
return ;
}
int main() {
// 创建监听套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) return -1;
struct sockaddr_in servaddr, clientaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
// 绑定套接字到指定端口
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
// 开始监听
listen(listenfd, 10);
struct io_uring_params params;
memset(¶ms, 0, sizeof(params));
struct io_uring ring;
memset(&ring, 0, sizeof(ring));
// 初始化 io_uring 实例
io_uring_queue_init_params(ENTRIES_LENGTH, &ring, ¶ms);
socklen_t clilen = sizeof(clientaddr);
// 设置接受连接的事件
set_accept_event(&ring, listenfd, (struct sockaddr*)&clientaddr, &clilen, 0);
while (1) {
struct io_uring_cqe *cqe;
// 提交所有准备好的事件到内核
io_uring_submit(&ring);
// 等待至少一个完成事件
int ret = io_uring_wait_cqe(&ring, &cqe);
struct io_uring_cqe *cqes[10];
// 一次性检查多个完成事件
int cqecount = io_uring_peek_batch_cqe(&ring, cqes, 10);
unsigned count = 0;
for (int i = 0; i < cqecount; i++) {
cqe = cqes[i];
count++;
struct conninfo ci;
// 提取用户数据
memcpy(&ci, &cqe->user_data, sizeof(ci));
if (ci.type == ACCEPT) {
// 处理接受连接的事件
int connfd = cqe->res;
char *buffer = buf_table[connfd];
// 设置读事件
set_read_event(&ring, connfd, buffer, BUFFER_LENGTH, 0);
// 重新设置接受连接的事件
set_accept_event(&ring, listenfd, (struct sockaddr*)&clientaddr, &clilen, 0);
} else if (ci.type == READ) {
// 处理读取数据的事件
int bytes_read = cqe->res;
if (bytes_read <= 0) {
// 客户端关闭连接或读取错误
close(ci.connfd);
} else {
// 回显读取到的数据
char *buffer = buf_table[ci.connfd];
// 设置写事件
set_write_event(&ring, ci.connfd, buffer, bytes_read, 0);
}
} else if (ci.type == WRITE) {
// 处理写数据的事件
char *buffer = buf_table[ci.connfd];
// 设置读事件等待更多数据
set_read_event(&ring, ci.connfd, buffer, BUFFER_LENGTH, 0);
}
}
// 标记所有处理过的完成事件
io_uring_cq_advance(&ring, count);
}
return 0;
}