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 系统调用分析
三个核心系统调用
-
io_uring_setup():
- 返回文件描述符给用户操作
- 注册io_uring的上下文(ctx)
- 创建CQ和SQ队列
-
io_uring_enter():
- 提交新的IO请求
- 可选择等待IO完成请求
-
io_uring_register():
- 注册/注销/更新缓冲区、文件、个性化等
io_uring_setup 系统调用
SYSCALL_DEFINE2(io_uring_setup, u32, entries, struct io_uring_params __user *, params)
工作流程:
- 将用户空间参数拷贝到内核空间
- 检查保留字段是否为0
- 检查flags的合法性
- 调用
io_uring_create()创建io_uring上下文,并返回文件描述符
io_uring_create 函数
static struct file *io_uring_create(unsigned entries, struct io_uring_params *p)
工作流程:
- 检查和调整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 系统调用
SYSCALL_DEFINE6(io_uring_enter, unsigned int, fd, u32, to_submit, u32, min_complete, u32, flags, const sigset_t __user *, sig, size_t, sigsz)
工作流程:
- 检查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_uringio_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 系统调用
SYSCALL_DEFINE4(io_uring_register, unsigned int, fd, unsigned int, opcode, void __user *, arg, unsigned int, nr_args)
工作流程:
- 判断文件描述符是否符合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): 注册事件fdIORING_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 机制,通过共享内存环形队列减少系统调用开销,支持多种操作类型(文件、网络、超时等)。合理使用注册资源和高级功能可以进一步提升性能。理解其内部实现机制有助于开发更高效的应用程序和排查潜在问题。