io_uring源码分析
字数 4087 2025-08-29 08:30:31

io_uring 源码分析与使用指南

1. io_uring 概述

io_uring 是 Linux 5.1 引入的系统调用接口,用于异步执行系统调用。相比原有的 Native AIO,io_uring 提供了更高效的异步 I/O 机制。

核心概念

  • SQ (Submission Queue): 提交任务的环形队列
  • SQE (Submission Queue Entry): 提交队列中的单个任务项
  • CQ (Completion Queue): 获取结果的环形队列
  • CQE (Completion Queue Entry): 完成队列中的单个结果项

2. liburing 库使用

安装方法

# 自动安装
sudo apt-get install liburing-dev

# 手动安装
git clone https://git.kernel.dk/liburing
cd liburing
./configure
make
sudo make install

核心结构体

struct io_uring {
    struct io_uring_sq sq;  // 提交队列
    struct io_uring_cq cq;  // 完成队列
    unsigned flags;         // 配置标志
    int ring_fd;            // 文件描述符
};

初始化与销毁

// 初始化
int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);

// 销毁
void io_uring_queue_exit(struct io_uring *ring);

参数说明:

  • entries: 队列大小(必须是2的幂,如1024)
  • flags: 配置标志(如IORING_SETUP_SQPOLL启用内核轮询模式)

提交队列操作

// 获取一个空闲的SQE
struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);

// 提交请求到内核
int io_uring_submit(struct io_uring *ring);

完成队列操作

// 阻塞等待至少一个完成事件
int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);

// 非阻塞检查完成事件
int io_uring_peek_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);

// 标记CQE已处理
void io_uring_cqe_seen(struct io_uring *ring, struct io_uring_cqe *cqe);

准备I/O请求的辅助函数

// 文件操作
void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd, const struct iovec *iov, unsigned nr_vecs, off_t offset);
void io_uring_prep_writev(struct io_uring_sqe *sqe, int fd, const struct iovec *iov, unsigned nr_vecs, off_t offset);
void io_uring_prep_openat(struct io_uring_sqe *sqe, int dfd, const char *path, int flags, mode_t mode);
void io_uring_prep_close(struct io_uring_sqe *sqe, int fd);

// 网络操作
void io_uring_prep_accept(struct io_uring_sqe *sqe, int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
void io_uring_prep_send(struct io_uring_sqe *sqe, int sockfd, const void *buf, size_t len, int flags);
void io_uring_prep_recv(struct io_uring_sqe *sqe, int sockfd, void *buf, size_t len, int flags);

// 超时与事件
void io_uring_prep_timeout(struct io_uring_sqe *sqe, struct __kernel_timespec *ts, unsigned count, unsigned flags);
void io_uring_prep_poll_add(struct io_uring_sqe *sqe, int fd, short poll_mask);

完整示例:异步读取文件

#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

#define BUF_SIZE 4096

int main() {
    struct io_uring ring;
    char buf[BUF_SIZE];
    
    // 初始化io_uring
    if (io_uring_queue_init(32, &ring, 0) < 0) {
        perror("io_uring_queue_init");
        return 1;
    }
    
    // 打开文件
    int fd = open("test.txt", O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    
    // 获取SQE
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    if (!sqe) {
        fprintf(stderr, "Failed to get SQE\n");
        return 1;
    }
    
    // 准备读请求
    struct iovec iov = { .iov_base = buf, .iov_len = BUF_SIZE };
    io_uring_prep_readv(sqe, fd, &iov, 1, 0);
    
    // 提交请求
    io_uring_submit(&ring);
    
    // 等待完成
    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);
    
    // 处理结果
    if (cqe->res < 0) {
        fprintf(stderr, "Async read failed: %s\n", strerror(-cqe->res));
    } else {
        printf("Read %d bytes\n", cqe->res);
    }
    
    // 标记CQE已处理
    io_uring_cqe_seen(&ring, cqe);
    
    // 清理
    close(fd);
    io_uring_queue_exit(&ring);
    
    return 0;
}

3. io_uring 系统调用分析

三个核心系统调用

  1. io_uring_setup():

    • 返回文件描述符给用户操作
    • 注册io_uring的上下文(ctx)
    • 创建CQ和SQ队列
  2. io_uring_enter():

    • 提交新的IO请求
    • 可选择等待IO完成请求
  3. io_uring_register():

    • 注册/注销/更新缓冲区、文件、个性化等

io_uring_setup 系统调用

SYSCALL_DEFINE2(io_uring_setup, u32, entries, struct io_uring_params __user *, params)

工作流程:

  1. 将用户空间参数拷贝到内核空间
  2. 检查保留字段是否为0
  3. 检查flags的合法性
  4. 调用io_uring_create()创建io_uring上下文,并返回文件描述符

io_uring_create 函数

static struct file *io_uring_create(unsigned entries, struct io_uring_params *p)

工作流程:

  1. 检查和调整SQ/CQ环的大小
    • 默认CQsize为SQ的两倍
    • 如果设置IORING_SETUP_CQSIZE,可自定义CQsize(需大于SQsize)
  2. 分配并初始化上下文(ctx)空间(结构体io_ring_ctx
  3. 分配和初始化SQ/CQ环
  4. 获取文件描述符并安装

关键函数:

  • io_ring_ctx_alloc(): 分配上下文空间,初始化列表头、旋转锁等
  • io_allocate_scq_urings(): 分配与用户空间共享的rings本体
  • io_sq_offload_create(): 初始化工作队列
  • io_sq_offload_start(): 唤醒工作线程
  • io_uring_get_file(): 获取文件描述符

io_ring_ctx_alloc 分配上下文空间

使用GFP_KERNEL标志进行常规分配。

io_allocate_scq_urings 分配SQ/CQ环

  1. 获取rings大小
  2. 调用io_mem_alloc(),最终调用__get_free_pages()直接分配页
  3. io_rings结构体通过mmap()与用户进程共享

io_sq_offload_create 初始化工作队列

  1. 检查ctx->flags是否开启SQ轮询模式
  2. 检查权限(需要SYS_ADMINSYS_NICE
  3. 将上下文的SQ数据链添加到sqd的新列表
  4. 设置线程超时(默认1秒)
  5. 设置线程是否需要指定CPU
  6. 分配task_struct内存并初始化
  7. 调用io_init_wq_offload()初始化工作队列

io_uring_get_file 获取文件结构体

  1. 获取文件结构体
  2. 调用io_uring_install_fd()安装文件描述符
  3. 使用sock_create_kern()创建内核UNIX套接字
  4. 通过anon_inode_getfile()创建匿名inode并返回ctx的文件结构体

io_uring_enter 系统调用

SYSCALL_DEFINE6(io_uring_enter, unsigned int, fd, u32, to_submit, u32, min_complete, u32, flags, const sigset_t __user *, sig, size_t, sigsz)

工作流程:

  1. 检查flags、文件描述符
  2. 根据ctx判断是否需要SQ轮询,否则采用to_submit数量提交
  3. 设置IORING_ENTER_GETEVENTS则处理完成事件
  4. 如果启用了IOPOLL但未启用SQPOLL模式,调用io_iopoll_check进行轮询检查
  5. 否则调用io_cqring_wait等待完成事件
  6. 释放资源后返回SQE提交数量

关键函数:

  • io_sqpoll_wait_sq(): 将当前进程添加到ctx->sqo_sq_wait队列并挂起
  • io_submit_sqes(): 提交SQES到io_uring
  • io_iopoll_check(): 轮询检查是否有事件完成
  • io_cqring_wait(): 等待完成队列中有足够的事件

io_sqpoll_wait_sq 等待SQ有空间

  1. 通过io_sqring_full()判断SQring是否为满状态
  2. 如果满,将当前进程添加到ctx->sqo_sq_wait等待队列
  3. 进行进程调度,直到SQring有空闲、收到信号或SQ线程死亡

io_submit_sqes 提交SQES

  1. 检查CQ溢出风险
  2. 确定提交事件数量(用户提交数量、SQ事件数量、SQ队列长度取最小)
  3. 增加当前任务的inflight计数和usage计数
  4. 循环处理每个SQE:
    • 通过上下文获取SQE
    • 初始化io请求(req)
    • 调用io_submit_sqe()提交单个SQE请求

io_iopoll_check 轮询事件完成

  1. 循环调用io_iopoll_getevents()
  2. nr_events进行累加
  3. 每8次启动一次任务,尝试完成I/O请求
  4. 有事件完成或需要进程调度时退出

io_cqring_wait 等待CQ满足任务数

  1. 调用io_run_task_work()处理已有事件
  2. 设置信号掩码、超时时间、超时计数
  3. 挂起当前进程加入等待队列
  4. 调用io_run_task_work_sig()执行待处理任务并检查信号
  5. 根据CQ队列状态返回(不为空返回0,否则返回错误码)

io_uring_register 系统调用

SYSCALL_DEFINE4(io_uring_register, unsigned int, fd, unsigned int, opcode, void __user *, arg, unsigned int, nr_args)

工作流程:

  1. 判断文件描述符是否符合io_uring
  2. 检查上下文是否存在(通过引用计数)
  3. 检查opcode合法性
  4. 根据opcode执行相应操作

常见opcode:

  • IORING_REGISTER_BUFFERS: 注册缓冲区
  • IORING_UNREGISTER_BUFFERS: 注销缓冲区
  • IORING_REGISTER_FILES: 注册文件
  • IORING_UNREGISTER_FILES: 注销文件
  • IORING_REGISTER_FILES_UPDATE: 更新已注册文件
  • IORING_REGISTER_EVENTFD(_ASYNC): 注册事件fd
  • IORING_UNREGISTER_EVENTFD: 注销事件fd

io_sqe_buffer_register 注册用户缓冲区

  1. 检查参数合法性
  2. 判断用户缓冲区是否存在
  3. 检查大小合法性
  4. ctx->user_bufs分配缓冲区数组(GFP_KERNEL
  5. 遍历数组为每个元素分配空间
  6. 从用户空间复制IO向量信息到内核态
  7. 检查基地址、长度(最大1GB)
  8. 分配vmaspages空间
  9. 使用kvmalloc_array()为每个缓冲区分配空间
  10. 调用pin_user_pages()固定用户页面到内存
  11. 检查每个vma(不支持非huge page的文件内存)
  12. 初始化bio_vec和当前缓冲区

io_sqe_buffer_unregister 注销用户缓冲区

  1. 检查用户缓冲区是否存在
  2. 解除用户映射的bio_vec向量
  3. 释放imu->bvec
  4. 释放ctx->user_bufs并置零

io_sqe_files_register 注册文件

  1. 遍历用户传入的所有文件
  2. 使用fget()获取file结构体(增加文件引用计数)
  3. 检查不支持注册io_uring本身
  4. 调用io_sqe_files_scm()设置SCM_RIGHTS
  5. 分配并设置引用节点后返回

4. 高级功能

注册资源(减少开销)

  1. 文件描述符注册:

    • 通过IORING_REGISTER_FILES注册
    • 使用IOSQE_FIXED_FILE标志避免内核检查开销
  2. 缓冲区注册:

    • 通过IORING_REGISTER_BUFFERS注册
    • 内核预注册缓冲区,提升零拷贝性能

内核线程与轮询模式

  • IORING_SETUP_SQPOLL: 启用内核轮询模式
  • 默认线程超时为1秒
  • 可设置线程绑定特定CPU

飞行计数 (inflight)

  • io_submit_sqes()中增加计数
  • 用于跟踪正在处理中的I/O请求
  • SCM_RIGHTS机制相关,影响资源管理

5. 性能优化建议

  1. 合理设置SQ和CQ的大小(通常为2的幂)
  2. 对于高并发场景,考虑启用IORING_SETUP_SQPOLL
  3. 预注册常用文件和缓冲区减少开销
  4. 批量提交请求减少系统调用次数
  5. 使用io_uring_peek_cqe()非阻塞检查减少等待时间
  6. 及时调用io_uring_cqe_seen()释放资源

6. 安全注意事项

  1. 确保正确处理错误返回码
  2. 避免缓冲区溢出(最大1GB限制)
  3. 注意资源泄漏(文件描述符、内存等)
  4. 合理设置权限(SQPOLL需要SYS_ADMINSYS_NICE
  5. 考虑使用IORING_REGISTER_RESTRICTIONS限制操作类型

7. 总结

io_uring 提供了高效的异步 I/O 机制,通过共享内存环形队列减少系统调用开销,支持多种操作类型(文件、网络、超时等)。合理使用注册资源和高级功能可以进一步提升性能。理解其内部实现机制有助于开发更高效的应用程序和排查潜在问题。

io_ uring 源码分析与使用指南 1. io_ uring 概述 io_ uring 是 Linux 5.1 引入的系统调用接口,用于异步执行系统调用。相比原有的 Native AIO,io_ uring 提供了更高效的异步 I/O 机制。 核心概念 SQ (Submission Queue) : 提交任务的环形队列 SQE (Submission Queue Entry) : 提交队列中的单个任务项 CQ (Completion Queue) : 获取结果的环形队列 CQE (Completion Queue Entry) : 完成队列中的单个结果项 2. liburing 库使用 安装方法 核心结构体 初始化与销毁 参数说明: entries : 队列大小(必须是2的幂,如1024) flags : 配置标志(如 IORING_SETUP_SQPOLL 启用内核轮询模式) 提交队列操作 完成队列操作 准备I/O请求的辅助函数 完整示例:异步读取文件 3. io_ uring 系统调用分析 三个核心系统调用 io_ uring_ setup() : 返回文件描述符给用户操作 注册io_ uring的上下文(ctx) 创建CQ和SQ队列 io_ uring_ enter() : 提交新的IO请求 可选择等待IO完成请求 io_ uring_ register() : 注册/注销/更新缓冲区、文件、个性化等 io_ uring_ setup 系统调用 工作流程: 将用户空间参数拷贝到内核空间 检查保留字段是否为0 检查flags的合法性 调用 io_uring_create() 创建io_ uring上下文,并返回文件描述符 io_ uring_ create 函数 工作流程: 检查和调整SQ/CQ环的大小 默认CQsize为SQ的两倍 如果设置 IORING_SETUP_CQSIZE ,可自定义CQsize(需大于SQsize) 分配并初始化上下文(ctx)空间(结构体 io_ring_ctx ) 分配和初始化SQ/CQ环 获取文件描述符并安装 关键函数: io_ring_ctx_alloc() : 分配上下文空间,初始化列表头、旋转锁等 io_allocate_scq_urings() : 分配与用户空间共享的rings本体 io_sq_offload_create() : 初始化工作队列 io_sq_offload_start() : 唤醒工作线程 io_uring_get_file() : 获取文件描述符 io_ ring_ ctx_ alloc 分配上下文空间 使用 GFP_KERNEL 标志进行常规分配。 io_ allocate_ scq_ urings 分配SQ/CQ环 获取rings大小 调用 io_mem_alloc() ,最终调用 __get_free_pages() 直接分配页 io_rings 结构体通过mmap()与用户进程共享 io_ sq_ offload_ create 初始化工作队列 检查 ctx->flags 是否开启SQ轮询模式 检查权限(需要 SYS_ADMIN 或 SYS_NICE ) 将上下文的SQ数据链添加到sqd的新列表 设置线程超时(默认1秒) 设置线程是否需要指定CPU 分配 task_struct 内存并初始化 调用 io_init_wq_offload() 初始化工作队列 io_ uring_ get_ file 获取文件结构体 获取文件结构体 调用 io_uring_install_fd() 安装文件描述符 使用 sock_create_kern() 创建内核UNIX套接字 通过 anon_inode_getfile() 创建匿名inode并返回ctx的文件结构体 io_ uring_ enter 系统调用 工作流程: 检查flags、文件描述符 根据ctx判断是否需要SQ轮询,否则采用 to_submit 数量提交 设置 IORING_ENTER_GETEVENTS 则处理完成事件 如果启用了 IOPOLL 但未启用 SQPOLL 模式,调用 io_iopoll_check 进行轮询检查 否则调用 io_cqring_wait 等待完成事件 释放资源后返回SQE提交数量 关键函数: io_sqpoll_wait_sq() : 将当前进程添加到 ctx->sqo_sq_wait 队列并挂起 io_submit_sqes() : 提交SQES到io_ uring io_iopoll_check() : 轮询检查是否有事件完成 io_cqring_wait() : 等待完成队列中有足够的事件 io_ sqpoll_ wait_ sq 等待SQ有空间 通过 io_sqring_full() 判断SQring是否为满状态 如果满,将当前进程添加到 ctx->sqo_sq_wait 等待队列 进行进程调度,直到SQring有空闲、收到信号或SQ线程死亡 io_ submit_ sqes 提交SQES 检查CQ溢出风险 确定提交事件数量(用户提交数量、SQ事件数量、SQ队列长度取最小) 增加当前任务的inflight计数和usage计数 循环处理每个SQE: 通过上下文获取SQE 初始化io请求(req) 调用 io_submit_sqe() 提交单个SQE请求 io_ iopoll_ check 轮询事件完成 循环调用 io_iopoll_getevents() 对 nr_events 进行累加 每8次启动一次任务,尝试完成I/O请求 有事件完成或需要进程调度时退出 io_ cqring_ wait 等待CQ满足任务数 调用 io_run_task_work() 处理已有事件 设置信号掩码、超时时间、超时计数 挂起当前进程加入等待队列 调用 io_run_task_work_sig() 执行待处理任务并检查信号 根据CQ队列状态返回(不为空返回0,否则返回错误码) io_ uring_ register 系统调用 工作流程: 判断文件描述符是否符合io_ uring 检查上下文是否存在(通过引用计数) 检查opcode合法性 根据opcode执行相应操作 常见opcode: IORING_REGISTER_BUFFERS : 注册缓冲区 IORING_UNREGISTER_BUFFERS : 注销缓冲区 IORING_REGISTER_FILES : 注册文件 IORING_UNREGISTER_FILES : 注销文件 IORING_REGISTER_FILES_UPDATE : 更新已注册文件 IORING_REGISTER_EVENTFD(_ASYNC) : 注册事件fd IORING_UNREGISTER_EVENTFD : 注销事件fd io_ sqe_ buffer_ register 注册用户缓冲区 检查参数合法性 判断用户缓冲区是否存在 检查大小合法性 为 ctx->user_bufs 分配缓冲区数组( GFP_KERNEL ) 遍历数组为每个元素分配空间 从用户空间复制IO向量信息到内核态 检查基地址、长度(最大1GB) 分配 vmas 和 pages 空间 使用 kvmalloc_array() 为每个缓冲区分配空间 调用 pin_user_pages() 固定用户页面到内存 检查每个 vma (不支持非huge page的文件内存) 初始化 bio_vec 和当前缓冲区 io_ sqe_ buffer_ unregister 注销用户缓冲区 检查用户缓冲区是否存在 解除用户映射的 bio_vec 向量 释放 imu->bvec 释放 ctx->user_bufs 并置零 io_ sqe_ files_ register 注册文件 遍历用户传入的所有文件 使用 fget() 获取 file 结构体(增加文件引用计数) 检查不支持注册io_ uring本身 调用 io_sqe_files_scm() 设置 SCM_RIGHTS 分配并设置引用节点后返回 4. 高级功能 注册资源(减少开销) 文件描述符注册 : 通过 IORING_REGISTER_FILES 注册 使用 IOSQE_FIXED_FILE 标志避免内核检查开销 缓冲区注册 : 通过 IORING_REGISTER_BUFFERS 注册 内核预注册缓冲区,提升零拷贝性能 内核线程与轮询模式 IORING_SETUP_SQPOLL : 启用内核轮询模式 默认线程超时为1秒 可设置线程绑定特定CPU 飞行计数 (inflight) 在 io_submit_sqes() 中增加计数 用于跟踪正在处理中的I/O请求 与 SCM_RIGHTS 机制相关,影响资源管理 5. 性能优化建议 合理设置SQ和CQ的大小(通常为2的幂) 对于高并发场景,考虑启用 IORING_SETUP_SQPOLL 预注册常用文件和缓冲区减少开销 批量提交请求减少系统调用次数 使用 io_uring_peek_cqe() 非阻塞检查减少等待时间 及时调用 io_uring_cqe_seen() 释放资源 6. 安全注意事项 确保正确处理错误返回码 避免缓冲区溢出(最大1GB限制) 注意资源泄漏(文件描述符、内存等) 合理设置权限(SQPOLL需要 SYS_ADMIN 或 SYS_NICE ) 考虑使用 IORING_REGISTER_RESTRICTIONS 限制操作类型 7. 总结 io_ uring 提供了高效的异步 I/O 机制,通过共享内存环形队列减少系统调用开销,支持多种操作类型(文件、网络、超时等)。合理使用注册资源和高级功能可以进一步提升性能。理解其内部实现机制有助于开发更高效的应用程序和排查潜在问题。