关于io_uring
io_uring是什么
io_uring是2019年,Linux内核5.1引入的异步io接口,通过环形buffer将用户态和内核态连接起来,实现低时延、低开销、异步、高吞吐的IO接口。
Linux传统的io机制:
最常用的epoll只能检测IO就绪,读写是阻塞的系统调用(recv、write、send、read等),需要用户手动发起系统调用。
iouring的优势:
1. inuring的优势是真正的异步IO,且全覆盖系统接口(read
, write
, accept
, connect
, fsync
, open
, sendmsg
, recvmsg
…),
2. 零系统调用开销路径(通过 SQPOLL 模式)
3. 缓冲区预注册(避免内核重复拷贝)
4. 用户态与内核共享队列:使用 Submission Queue(SQ)与 Completion Queue(CQ)通信,无需频繁 syscall 切换
5. 更好的线程模型整合:可以无锁批量提交 IO 任务,完美适配多线程/协程调度器
in_uring 的适用场景:
适合用在密集IO、高并发、低时延的场景;
nginx io_uring模块、libhv 框架
二、核心概念与设计原理
io_uring基于两个环形队列:
队列名称 | 作用 | 位置 |
---|---|---|
SQ(Submission Queue) | 用户提交请求(read, write 等) | 用户态 & 内核共享 |
CQ(Completion Queue) | 内核返回完成结果(成功/失败) | 用户态 & 内核共享 |
这两个队列在初始化的时候就mmap到用户空间,不需要系统调用。
使用方式:
1. 将请求写入SQ
2. 提交
3. 从CQ读取结果
工作流程:
+--------------------------+ +--------------------------+
| 用户空间 User Space | | 内核空间 Kernel Space |
| | | |
| 1. 填写 SQE | | |
| 2. 写入 SQ -> tail++ | ---> | |
| 3. io_uring_enter() 调用 | | |
| | | 4. 读取 SQ 并处理 IO 操作 |
| | <--- | 5. 写入 CQ -> tail++ |
| 6. 读取 CQE | | |
+--------------------------+ +--------------------------+
具体流程:
用户态
1. 用户提交:用户准备好一个io_uring_sqe,指定到操作类型,指定文件描述符,缓冲区,偏移地址等;
2. 写入SQ,更新tail指针
3. 调用io_uring_enter()通知内核处理(或者启用SQPLL轮询)
// 初始化 io_uring 环境
io_uring_queue_init(queue_depth, &ring, flags);
// 1. 获取一个 SQE(Submission Queue Entry)
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
// 2. 准备一个 IO 操作(例如 readv)
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
// 可选:附加用户数据,用于标识完成事件
io_uring_sqe_set_data(sqe, custom_ptr);
// 3. 提交请求给内核(执行 io_uring_enter)
io_uring_submit(&ring);
内核态
- 读取SQ中的任务
- 调度系统调用执行
- 完成后将结果写入CQ,更新CQ指针
用户态
- 读取io_uring_cqe,包含返回值,用户数据
- 可以阻塞也可以非阻塞等待
struct io_uring_cqe* cqe;
// 阻塞等待一个完成事件(会 sleep,直到内核写入 CQ)
io_uring_wait_cqe(&ring, &cqe);
// 获取结果值(成功返回字节数,失败返回负 errno)
int res = cqe->res;
// 获取用户数据(之前通过 io_uring_sqe_set_data 设置的)
void* user_data = io_uring_cqe_get_data(cqe);
// 标记 CQE 已消费,准备下一次读取
io_uring_cqe_seen(&ring, cqe);
SQE与CQE数据结构:
struct io_uring_sqe {
__u8 opcode; // 操作类型,如 READ, WRITE, ACCEPT…
__u8 flags;
__u16 ioprio;
__s32 fd;
__u64 offset;
__u64 addr; // 用户缓冲区地址
__u32 len;
__u64 user_data; // 用户自定义数据(回调、标识等)
};
struct io_uring_cqe {
__u64 user_data; // 与 SQE 中设置的一致
__s32 res; // 返回值:成功时为字节数,失败时为 -errno
__u32 flags;
};
阻塞等待与轮询:
如果使用阻塞等待模式,内核会在有结果的时候唤醒在等待的IO,如果使用轮询需要轮询查看CQ,无论是否就绪都会立即返回。
io_uring_wait_cqe(&ring, &cqe); //阻塞等待
int ret = io_uring_peek_cqe(&ring, &cqe); //非阻塞
// 等待至少 min_complete 个请求完成(推荐)
io_uring_submit_and_wait(&ring, min_complete); //批处理
io_uring_peek_batch_cqe() 或循环 peek 读取多个结果
2. 与 epoll 对比的关键优势
- 支持直接注册 fd(减少多次系统调用)
- 完全异步,天然非阻塞
- 多种操作支持(read、write、accept、connect、openat、sendmsg、recvmsg、timeout…)
最简单的io_uring示例
#include <liburing.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct io_uring ring;
char buf[4096];
struct iovec iov = {
.iov_base = buf,
.iov_len = sizeof(buf)
};
// 1. 初始化 io_uring 环
// 参数:unsigned entries:设置 ring buffer 的大小(最多同时挂起 8 个 IO 操作)。注意必须是 2 的幂
//struct io_uring *ring:输出参数,初始化后用于后续提交/读取的上下文结构
//unsigned flags :额外的行为标志位(如 IORING_SETUP_SQPOLL 开启内核轮询),此处设为默认。
io_uring_queue_init(8, &ring, 0);
// 2. 打开文件
int fd = open("test.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
// 3. 获取一个 SQE 并准备 readv 操作
struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
//后两个参数: iovec 数组的元素个数;文件中读取的起始偏移(如 0 表示从头开始)
io_uring_prep_readv(sqe, fd, &iov, 1, 0);
// 4. 提交请求
io_uring_submit(&ring);
// 5. 等待完成并获取 CQE
struct io_uring_cqe* cqe;
io_uring_wait_cqe(&ring, &cqe);
// 6. 检查是否成功
if (cqe->res >= 0) {
printf("读取成功,字节数:%d\n", cqe->res);
write(STDOUT_FILENO, buf, cqe->res);
} else {
printf("读取失败,errno = %d\n", -cqe->res);
}
// 7. 标记 CQE 已处理
io_uring_cqe_seen(&ring, cqe);
// 8. 释放资源
close(fd);
io_uring_queue_exit(&ring);
return 0;
}
四、进阶内容
1. 支持的操作类型(I/O 命令总览)
这些操作会被封装进 io_uring_sqe
(提交项)中,通过 io_uring_prep_*()
来准备
文件类操作(文件读写/打开/同步)
操作类型 | 说明 |
---|---|
IORING_OP_READ |
异步 read() |
IORING_OP_READV |
异步 readv() ,分散读取 |
IORING_OP_WRITE |
异步 write() |
IORING_OP_WRITEV |
异步 writev() ,聚集写入 |
IORING_OP_OPENAT |
异步 openat() 打开文件 |
IORING_OP_CLOSE |
异步关闭文件描述符 |
IORING_OP_FSYNC |
异步刷盘(fsync) |
IORING_OP_STATX |
获取文件元数据(类似 stat ) |
数据传输类操作(zero-copy / pipe)
操作类型 | 说明 |
---|---|
IORING_OP_SPLICE |
在两个 FD 之间零拷贝数据 |
IORING_OP_TEE |
在 pipe 中复制数据,无拷贝 |
IORING_OP_READ_FIXED |
从注册内存读取(更高性能) |
IORING_OP_WRITE_FIXED |
写入注册内存 |
定时与取消操作
操作类型 | 说明 |
---|---|
IORING_OP_TIMEOUT |
定时器,类似 timerfd |
IORING_OP_TIMEOUT_REMOVE |
移除定时器 |
IORING_OP_LINK_TIMEOUT |
用于 link 的超时控制 |
IORING_OP_ASYNC_CANCEL |
取消某个正在进行的请求 |
等待类操作(事件通知)
操作类型 | 说明 |
---|---|
IORING_OP_POLL_ADD |
异步添加事件(替代 epoll) |
IORING_OP_POLL_REMOVE |
移除监听事件 |
其他支持类型
操作类型 | 说明 |
---|---|
IORING_OP_NOP |
空操作(可用于测试) |
IORING_OP_PROVIDE_BUFFERS |
注册缓冲区池(支持缓冲复用) |
IORING_OP_REMOVE_BUFFERS |
io_uring的环形队列是一个单生产者单消费者的无锁队列,是坦然线程安全的;
内核通过mmap的方式共享给用户,不需要系统调用;
队列为空/满的判断方式:tail - head < ring_entries
io_uring
主要包含两个环形队列(Ring):
环形队列 | 作用 | 方向 |
---|---|---|
SQ(Submission Queue) | 应用提交 I/O 请求 | 应用 → 内核 |
CQ(Completion Queue) | 内核通知 I/O 完成结果 | 内核 → 应用 |
Submission Queue(SQ)
SQ 实际上由两个部分组成:
SQ Ring(队列控制区): 包含环形缓冲区的元信息。
SQEs (Submission Queue Entries):真正要提交的 I/O 请求结构数组。
struct io_sq_ring {
__u32 *head; // 内核读取位置(应用更新)
__u32 *tail; // 应用写入位置(应用更新)
__u32 *ring_mask; // 用于下标取模
__u32 *ring_entries; // 队列总长度
__u32 *flags;
__u32 *array; // 指向 sqes 的索引数组
};
struct io_uring_sqe {
__u8 opcode;
__u8 flags;
__u16 ioprio;
__s32 fd;
__u64 off;
__u64 addr;
__u32 len;
__u32 op_flags;
...
};
工作流程
- 应用向 SQE 数组中写入要提交的 I/O 请求。
- 把这个 SQE 的索引写入到 SQ ring 的
array
数组中。 - 更新
tail
(表示新写入的位置)。 - 系统调用
io_uring_enter()
通知内核“可以处理这些请求”。
Completion Queue(CQ)
CQ 同样由两个部分组成:
CQ Ring(控制区):
struct io_cq_ring {
__u32 *head; // 应用读取位置(内核更新)
__u32 *tail; // 内核写入位置(内核更新)
__u32 *ring_mask;
__u32 *ring_entries;
__u32 *overflow;
...
};
CQEs (Completion Queue Entries):
struct io_uring_cqe {
__u64 user_data; // 应用提交时附带的标识
__s32 res; // 返回值(比如read返回字节数或错误码)
__u32 flags;
};
工作流程
- 内核在处理完某个请求后,将结果写入
cqe
。 - 写入
tail
指针,表示有新结果可读。 - 应用查看
head != tail
即可读取完成结果。 - 应用读取后,更新
head
。
DPDK + io_uring + VictoriaMetrics 进行数据采集与展示
[ 网卡收包 DPDK ]
↓
[ 协议识别模块(可选)]
↓
[ 流量标签提取 + 元数据整理 ]
↓
[ 异步写入缓冲队列 ringbuf ]
↓
[ io_uring 线程 → 格式化为时序指标 → POST 到 VictoriaMetrics ]
↓
[ Prometheus / Grafana 查询展示 ]
dpdk_vm_demo/
├── main.c # 主函数
├── dpdk_rx.c # 收包逻辑
├── ringbuf.c/h # 通用缓冲区
├── vm_writer.c/h # io_uring + HTTP 写入 VictoriaMetrics
├── common.h # 数据结构
├── Makefile # 编译脚本
└── README.md # 说明