关于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);

内核态

  1. 读取SQ中的任务
  2. 调度系统调用执行
  3. 完成后将结果写入CQ,更新CQ指针

用户态

  1. 读取io_uring_cqe,包含返回值,用户数据
  2. 可以阻塞也可以非阻塞等待
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;
    ...
};

工作流程

  1. 应用向 SQE 数组中写入要提交的 I/O 请求。
  2. 把这个 SQE 的索引写入到 SQ ring 的 array 数组中。
  3. 更新 tail(表示新写入的位置)。
  4. 系统调用 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;
};

工作流程

  1. 内核在处理完某个请求后,将结果写入 cqe
  2. 写入 tail 指针,表示有新结果可读。
  3. 应用查看 head != tail 即可读取完成结果。
  4. 应用读取后,更新 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                # 说明

posted @ 2025-03-31 19:08  Tohomson  阅读(157)  评论(0)    收藏  举报