一、异步io
1.1、什么是I/O?
在Linux中,一切皆文件fd,定时器,文件,网络,驱动等;就是通信、读取文件的通道(fd)
1.2、处理I/O的方式
- 阻塞IO:主线程发起io请求,然后阻塞等待结果返回,效率极低
- 非阻塞IO + 多路复用:典型的
epoll,同时监视多个文件描述符,一旦某个io请求就绪,则通知主线程处理。但这个过程还是有阻塞的,只是阻塞在epoll_wait上 - 真正的异步I/O: 主线程发起io请求,立即返回,完全不用等待;操作系统会在后台完成整个I/O操作,然后通过回调函数通知主线程。
1.3、io_uring
io_uring是2019年加入Linux内核的特性,由Linux内核大师Jens Axboe设计,旨在提供一个统一、高效且真正的异步I/O解决方案。
核心思想 :通过共享内存,原子操作,无锁环形队列实现用户态与内核态之间的零拷贝
核心组件 :
- 提交队列:一个环形队列,应用程序将需要执行的I/O操作放入此队列
- 完成队列:也是一个环形队列,内核 将已完成的I/O操作结果放入此队列.

1.4、核心系统调用
- io_uring_setup:初始化
c
/*
* Desc: 初始化io_uring实例,创建提交队列和完成队列
* @entries: 提交队列的大小,必须是2的幂
* @params: 用于输入输出参数
* @return: 返回io_uring实例的fd
*/
int io_uring_setup(unsigned int entries, struct io_uring_params *params);
- io_uring_register: 注册资源
c
/*
* Desc: 注册资源,减少每次I/O操作的重复验证与映射开销
* @fd: 需注册的资源fd
* @opcode: 注册类型
* @arg: 资源参数
* @nr_args: 资源参数个数
*/
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
常用的opcode:
| 操作码 | 功能描述 |
|---|---|
IORING_REGISTER_BUFFERS |
注册用户缓冲区数组(struct iovec),用于固定缓冲区 I/O。内核会锁定这些内存。 |
IORING_REGISTER_FILES |
注册文件描述符数组。之后可在 SQE 中通过索引和 IOSQE_FIXED_FILE 标志来引用,避免每次查找。 |
IORING_REGISTER_EVENTFD |
注册一个 eventfd 描述符,用于异步接收 I/O 完成事件的通知。 |
IORING_REGISTER_PROBE |
查询内核,检查当前支持哪些 io_uring 操作码(opcodes),用于兼容性检查。 |
IORING_UNREGISTER_BUFFERS IORING_UNREGISTER_FILES |
注销之前注册的缓冲区或文件描述符。 |
- io_uring_enter:行动与等待------关键步骤
c
/*
* Desc: 提交I/O请求并等待完成
* @fd: io_uring实例的fd
* @to_submit: 提交的SQE数量
* @min_complete: 最小完成数量
* @flags: 控制函数行为的标志位
* @sig: 信号集,可用于在等待期间原子性地替换和恢复进程的信号掩码,避免信号竞争
*/
int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);
1.5、liburing库
io_uring_setup、io_uring_register、io_uring_enter 这些系统调用底层是用C语言实现的,不方便直接使用。因此,提供了liburing库,封装了这些系统调用的复杂性,并提供了一个更高级的API。
二、利用io_uring实现tcp server
2.1、环境准备
linux内核版本需要大于5.1,否则不能完美支持io_uring。
-
查看linux内核版本:
uname -r
如果内核版本低于5.1,则需要升级内核。***注意:***CentOs已经停服了,所以不建议在CentOs上使用io_uring。
-
安装
liburing库:
bash
sudo apt install liburing-dev
2.2、服务器初始化
步骤依旧还是原来的那样
c
int init_server(unsigned short port) {
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket error");
return -1;
}
int opt = 1;
// 2. 设置SO_REUSEADDR选项,允许地址重用
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt error");
close(sockfd);
return -1;
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(port);
// 3. 绑定套接字
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0) {
perror("bind error");
close(sockfd);
return -1;
}
// 4. 开始监听连接请求
if (listen(sockfd, 10) < 0) {
perror("listen error");
close(sockfd);
return -1;
}
printf("Server listening on port %d\n", port);
return sockfd;
}
2.3、io_uring初始化
使用liburing库初始化io_uring
c
struct io_uring ring;
if (io_uring_queue_init(1024, &ring, 0) < 0) {
perror("io_uring_queue_init");
close(sockfd);
exit(1);
}
2.4、提交accept请求
c
/**
* @brief 设置接受连接事件到io_uring队列
* @param ring io_uring实例指针
* @param sockfd 监听套接字文件描述符
* @param addr 客户端地址结构体指针,用于存储接受的客户端地址信息
* @param len 客户端地址结构体长度指针
*
* @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
*/
int set_event_accept(struct io_uring* ring, int sockfd, struct sockaddr* addr, socklen_t* len)
{
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
if (!sqe) return -1;
struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
info->fd = sockfd;
info->event_type = EVENT_ACCEPT;
io_uring_prep_accept(sqe, sockfd, addr, len, 0);
io_uring_sqe_set_data(sqe, info);
return 0;
}
2.5、为接受的连接提交读请求
c
/**
* @brief 设置接收数据事件到io_uring队列
* @param ring io_uring实例指针
* @param fd 要接收数据的套接字文件描述符
*
* @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
*/
int set_event_recv(struct io_uring* ring, int fd)
{
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
if (!sqe) return -1;
struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
info->fd = fd;
info->event_type = EVENT_READ;
io_uring_prep_recv(sqe, fd, info->buffer, sizeof(info->buffer), 0);
io_uring_sqe_set_data(sqe, info);
return 0;
}
2.6、回写数据到客户端
c
/**
* @brief 设置io_uring发送事件
*
* @param ring io_uring实例指针
* @param fd 目标套接字文件描述符
* @param data 要发送的数据指针
* @param len 数据长度
* @return int 成功返回0,失败返回-1
*/
int set_event_send(struct io_uring* ring, int fd, const char* data, int len)
{
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
if (!sqe) return -1;
struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
info->fd = fd;
info->event_type = EVENT_WRITE;
memcpy(info->buffer, data, len);
info->buffer[len] = '\0';
io_uring_prep_send(sqe, fd, info->buffer, len, 0);
io_uring_sqe_set_data(sqe, info);
return 0;
}
2.7、事件处理循环
c
while (1) {
// 提交所有准备好的SQE到内核
io_uring_submit(&ring);
// 等待完成事件
struct io_uring_cqe* cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
perror("io_uring_wait_cqe");
break;
}
// 获取用户数据(conn_info结构体)
struct conn_info* info = (struct conn_info*)io_uring_cqe_get_data(cqe);
int res = cqe->res;
if (res < 0) {
printf("Error on fd %d: %s\n", info->fd, strerror(-res));
if (info->fd != sockfd) {
close(info->fd);
}
free(info);
io_uring_cqe_seen(&ring, cqe);
continue;
}
// 根据事件类型进行不同处理
switch (info->event_type) {
case EVENT_ACCEPT: {
printf("New connection accepted, fd: %d\n", res);
// 为新连接提交读请求,开始接收数据
set_event_recv(&ring, res);
// 重新提交accept请求,继续监听新连接
set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len);
break;
}
case EVENT_READ: {
if (res == 0) {
// 客户端关闭连接(收到EOF)
printf("Client fd %d disconnected\n", info->fd);
close(info->fd);
} else {
// 成功读取数据,打印接收内容
printf("Received %d bytes from fd %d: %.*s\n",
res, info->fd, res, info->buffer);
// 回写相同数据给客户端(echo服务器)
set_event_send(&ring, info->fd, info->buffer, res);
}
break;
}
case EVENT_WRITE: {
printf("Sent %d bytes to fd %d\n", res, info->fd);
// 写完成后,重新提交读请求等待下一轮数据
set_event_recv(&ring, info->fd);
break;
}
}
// 释放连接信息内存并标记CQE已处理
free(info);
io_uring_cqe_seen(&ring, cqe);
}
执行结果:


三、拓展
3.1、C++中的async也是异步,那和io_uring有什么区别?
| 特性 | async |
io_uring |
|---|---|---|
| 抽象层次 | C++标准库层面 | Linux内核层面 |
| 主要目标 | 异步计算任务 | 异步IO操作 |
| 实现机制 | 线程池+future/promise |
内核环形队列+共享内存 |
| 阻塞位置 | 阻塞在future.get() |
几乎无阻塞 |
| 适用场景 | CPU密集型任务 | IO密集型应用(如网络服务器) |
3.2、io_uring与epoll的区别
| 特性 | epoll |
io_uring |
|---|---|---|
| 模型 | 同步非阻塞 | 异步非阻塞 |
| 阻塞位置 | epoll_wait() |
几乎无阻塞 |
| 批处理能力 | 弱,需要遍历列表并逐个调用read/write |
强,可以批量提交多个IO请求 |
| 内存开销 | 较低 | 较高(需要维护环形队列和缓冲区) |
| 实现复杂度 | 相对简单 | 比较复杂 |
3.3、io_uring会替代epoll吗?
不会,之间是互补关系。
- 对于纯网络I/O,
io_uring在极限场景下的性能优于epoll,但在实际中,哪有那么多极限场景 - 对于非网络I/O,存储I/O,那么
io_uring是毫不疑问的赢家.
附上代码:
c
#include <cstdio>
#include <liburing.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#define EVENT_ACCEPT 0
#define EVENT_READ 1
#define EVENT_WRITE 2
struct conn_info {
int fd;
int event_type;
char buffer[1024]; // 每个连接独立的缓冲区
};
int init_server(unsigned short port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket error");
return -1;
}
// 设置 SO_REUSEADDR
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt error");
close(sockfd);
return -1;
}
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_in)) == -1)
{
perror("bind error");
close(sockfd);
return -1;
}
if(listen(sockfd, 10) < 0)
{
perror("listen error");
close(sockfd);
return -1;
}
printf("Server listening on port %d\n", port);
return sockfd;
}
/**
* @brief 设置接受连接事件到io_uring队列
* @param ring io_uring实例指针
* @param sockfd 监听套接字文件描述符
* @param addr 客户端地址结构体指针,用于存储接受的客户端地址信息
* @param len 客户端地址结构体长度指针
*
* @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
*/
int set_event_accept(struct io_uring* ring, int sockfd, struct sockaddr* addr, socklen_t* len)
{
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
if (!sqe) return -1;
struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
info->fd = sockfd;
info->event_type = EVENT_ACCEPT;
io_uring_prep_accept(sqe, sockfd, addr, len, 0);
io_uring_sqe_set_data(sqe, info);
return 0;
}
/**
* @brief 设置接收数据事件到io_uring队列
* @param ring io_uring实例指针
* @param fd 要接收数据的套接字文件描述符
*
* @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
*/
int set_event_recv(struct io_uring* ring, int fd)
{
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
if (!sqe) return -1;
struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
info->fd = fd;
info->event_type = EVENT_READ;
io_uring_prep_recv(sqe, fd, info->buffer, sizeof(info->buffer), 0);
io_uring_sqe_set_data(sqe, info);
return 0;
}
/**
* @brief 设置io_uring发送事件
*
* @param ring io_uring实例指针
* @param fd 目标套接字文件描述符
* @param data 要发送的数据指针
* @param len 数据长度
* @return int 成功返回0,失败返回-1
*/
int set_event_send(struct io_uring* ring, int fd, const char* data, int len)
{
struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
if (!sqe) return -1;
struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
info->fd = fd;
info->event_type = EVENT_WRITE;
memcpy(info->buffer, data, len);
info->buffer[len] = '\0';
io_uring_prep_send(sqe, fd, info->buffer, len, 0);
io_uring_sqe_set_data(sqe, info);
return 0;
}
int main()
{
unsigned short port = 8080;
int sockfd = init_server(port);
if (sockfd < 0) {
exit(1);
}
struct io_uring ring;
if (io_uring_queue_init(1024, &ring, 0) < 0) {
perror("io_uring_queue_init");
close(sockfd);
exit(1);
}
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
// 初始提交accept请求
set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len);
printf("Server started, waiting for connections...\n");
while (1) {
// 提交所有准备好的SQE到内核
io_uring_submit(&ring);
// 等待完成事件
struct io_uring_cqe* cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
perror("io_uring_wait_cqe");
break;
}
// 获取用户数据(conn_info结构体)
struct conn_info* info = (struct conn_info*)io_uring_cqe_get_data(cqe);
int res = cqe->res;
if (res < 0) {
printf("Error on fd %d: %s\n", info->fd, strerror(-res));
if (info->fd != sockfd) {
close(info->fd);
}
free(info);
io_uring_cqe_seen(&ring, cqe);
continue;
}
// 根据事件类型进行不同处理
switch (info->event_type) {
case EVENT_ACCEPT: {
printf("New connection accepted, fd: %d\n", res);
// 为新连接提交读请求,开始接收数据
set_event_recv(&ring, res);
// 重新提交accept请求,继续监听新连接
set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len);
break;
}
case EVENT_READ: {
if (res == 0) {
// 客户端关闭连接(收到EOF)
printf("Client fd %d disconnected\n", info->fd);
close(info->fd);
} else {
// 成功读取数据,打印接收内容
printf("Received %d bytes from fd %d: %.*s\n",
res, info->fd, res, info->buffer);
// 回写相同数据给客户端(echo服务器)
set_event_send(&ring, info->fd, info->buffer, res);
}
break;
}
case EVENT_WRITE: {
printf("Sent %d bytes to fd %d\n", res, info->fd);
// 写完成后,重新提交读请求等待下一轮数据
set_event_recv(&ring, info->fd);
break;
}
}
// 释放连接信息内存并标记CQE已处理
free(info);
io_uring_cqe_seen(&ring, cqe);
}
// 清理资源:释放io_uring实例并关闭监听套接字
io_uring_queue_exit(&ring);
close(sockfd);
return 0;
}
//gcc -o exe io_uring_.cpp -luring