一、io_uring特点
1. 简介:
io_uring是最早Linux 5.1引入的,后面不断迭代, 高性能异步 I/O 框架,旨在彻底优化传统 I/O 模型的性能瓶颈。
2. 其核心特点:
-
共享的环形队列
用户态与内核态通过共享内存,构建的环形队列(SQ/CQ)用来传递 I/O 请求和完成事件,避免数据多次拷贝。实现零拷贝. -
批量处理(Batching)
支持单次系统调用(io_uring_enter)提交/收割请求和完成事件(SQE/CQE),减少了上下文切换。 -
内核轮询(SQ Polling)
可选的内核线程主动轮询提交队列(SQ),进一步消除系统调用延迟。 -
灵活的通知机制
支持事件驱动(如epoll
)或轮询模式,适应不同场景。
二、工作流程
1. 初始化阶段
-
创建 io_uring 实例
调用io_uring_queue_init
初始化环形队列(SQ 和 CQ),并设置参数(如队列大小、标志位)。struct io_uring ring; io_uring_queue_init(ENTRIES, &ring, IORING_SETUP_SQPOLL); // 启用 SQPOLL 模式
- SQ(提交队列):用户态提交 I/O 请求的入口。
- CQ(完成队列):内核返回 I/O 结果的出口。
用户态提交I/O请求(SQE)到SQ,内核消费,内核将完成事件(CQE)写入CQ,用户态消费
-
注册资源(可选)
预注册文件描述符、缓冲区等资源,减少内核检查开销:// 注册文件描述符 int files[] = {fd1, fd2}; io_uring_register_files(&ring, files, 2); // 注册固定缓冲区 struct iovec iov = {buffer, buffer_size}; io_uring_register_buffers(&ring, &iov, 1);
2. 提交 I/O 请求
-
获取 SQE(提交队列条目)
从 SQ 中获取一个空闲的 SQE,描述 I/O 操作细节:struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
-
配置 SQE 参数
设置操作类型(读、写等)、目标文件描述符、缓冲区地址等:io_uring_prep_read(sqe, fd, buf, size, offset); // 准备读操作 sqe->flags |= IOSQE_IO_LINK; // 启用链式请求 sqe->user_data = (void*)request_id; // 关联用户数据
-
批量提交请求
将 SQ 中的 SQE 批量提交到内核:io_uring_submit(&ring); // 触发系统调用(若未启用 SQPOLL)
- SQPOLL 模式:内核线程自动轮询 SQ,无需显式提交。
3. 内核处理阶段
-
消费 SQ 请求
内核从 SQ 中取出 SQE,解析并执行对应的 I/O 操作(如磁盘读取、网络发送)。 -
异步执行 I/O
- 对于文件 I/O,数据可能直接从页缓存复制到用户缓冲区。
- 对于网络 I/O,数据通过套接字缓冲区传输。
-
生成 CQE(完成队列条目)
I/O 完成后,内核将结果写入 CQ,包含状态码和用户数据(user_data
):struct io_uring_cqe { __u64 user_data; // 对应 SQE 的 user_data __s32 res; // 操作结果(成功字节数或错误码) __u32 flags; // 附加标志 };
4. 处理完成事件
-
获取 CQE
用户态从 CQ 中获取完成的 I/O 事件:struct io_uring_cqe *cqe; io_uring_wait_cqe(&ring, &cqe); // 阻塞等待至少一个 CQE // 或 io_uring_peek_cqe(&ring, &cqe); // 非阻塞检查
-
处理结果
根据cqe->res
判断操作是否成功:if (cqe->res >= 0) { // 成功:处理数据(cqe->res 为实际传输字节数) } else { // 失败:处理错误(错误码为 -cqe->res) }
-
标记 CQE 为已消费
移动 CQ 尾指针,释放 CQE 空间:io_uring_cqe_seen(&ring, cqe);
四、示例代码
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(ENTRIES_LEGHT , &ring, ¶ms);
// struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
#if 0
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
#else
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
#endif
char buffer[BUFFER_LENGTH] = {0};
while(1)
{
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
// 拿到环形队列的头指针 阻塞等待至少一个CQE
io_uring_wait_cqe(&ring, &cqe);
struct io_uring_cqe *cqes[120];
int nready = io_uring_peek_batch_cqe(&ring, cqes, 120);// 准备好的cqe数量
int i = 0;
for( i = 0; i < nready; i++)
{
// printf("io_uring_peek_batch_cqe\n");
// cqes[i].resulut
struct io_uring_cqe *entries = cqes[i];
struct conn_info result;
memcpy(&result, &entries->user_data, sizeof(struct conn_info));
if(result.event == EVENT_ACCEPT)
{
set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
// printf("io_uring_accept\n");
int clientfd = entries->res;
set_event_recv(&ring, clientfd, buffer, BUFFER_LENGTH, 0);// set read
//entries
}
else if(result.event == EVENT_READ) // 走到这里数据已经读到了用户的缓冲区
{
int ret = entries->res;
// printf("io_uring_recv ret: %d : %s\n", ret, buffer);
if(ret == 0)
{
close(result.fd);
}else if (ret > 0){
// 前面已经接收完了,这里设置进行回发
set_event_send(&ring, result.fd, buffer, ret, 0); // 接受完就回发
//set_event_recv(&ring, result.fd, buffer, BUFFER_LENGTH, 0);// set read
}
}else if((result.event == EVENT_WRITE)){ // 这里已经执行完了,数据已经写出去了
int ret = entries->res;
// printf("set_event_send ret: %d : %s\n", ret, buffer);
// 这里设置又可以接收了
set_event_recv(&ring, result.fd, buffer, ret, 0);
}
}
// 清空cq
io_uring_cq_advance(&ring, nready);
}
高级功能
链式请求(Chained Requests)
依赖执行:通过 IOSQE_IO_LINK 标志串联多个 SQE,前一个操作完成后再执行下一个。
sqe1->flags |= IOSQE_IO_LINK; // sqe2 在 sqe1 完成后执行
内核轮询(SQPOLL)
零系统调用提交:内核线程主动轮询 SQ,无需调用 io_uring_submit()。
配置:初始化时设置 IORING_SETUP_SQPOLL 标志。
五、性能对比(与传统模型)**
指标 | io_uring | epoll + 非阻塞 I/O | libaio |
---|---|---|---|
系统调用次数 | 极低(批量提交/收割) | 高(每次 I/O 需系统调用) | 中等(批量提交) |
上下文切换 | 极少 | 频繁 | 中等 |
最大吞吐量 | 百万级 IOPS | 十万级 IOPS | 十万级 IOPS |
延迟 | 微秒级 | 毫秒级 | 微秒级(但功能受限) |
六、适用场景
- 高频网络服务:Web 服务器、API 网关、实时通信。
- 存储系统:数据库(MySQL、Redis)、分布式存储引擎。
- 低延迟应用:金融交易系统、实时音视频处理。
- 混合负载:同时处理网络和磁盘 I/O 的高并发场景。
七、总结
io_uring 的工作流程围绕 提交队列(SQ) 和 完成队列(CQ) 展开,通过共享内存和异步处理机制实现高性能 I/O。其核心优势在于:
- 零拷贝:减少数据在用户态与内核态之间的复制。
- 批量处理:单次系统调用处理多个请求。
- 内核协作:通过 SQPOLL 和注册资源进一步降低开销。
掌握其流程后,可结合具体场景(如网络服务、存储引擎)设计高效异步 I/O 模型,充分发挥 Linux 内核的潜力。