4. 第一个 io_uring 程序:初始化、提交、等待、处理与清理
本章将带领你编写第一个完整的 io_uring 程序。通过一个具体的文件读取示例,你将掌握 io_uring 编程的五个核心步骤:初始化、提交请求、等待完成、处理结果和清理资源。这是所有 io_uring 应用的基础骨架。
学习目标:完成本章后,你将能够独立编写一个基于 io_uring 的同步文件读取程序,并理解其内部状态流转。
4.1 程序整体流程概览
一个典型的 io_uring 程序遵循“提交-等待-收割”的循环模式。下面的 SVG 图展示了本次示例程序的完整生命周期。
4.2 核心数据结构与 API
在编写代码之前,需要先了解 io_uring 库提供的几个核心数据结构和 API 函数。它们都定义在 <liburing.h> 头文件中。
| 类别 | 名称 | 描述 |
|---|---|---|
| 数据结构 | struct io_uring |
io_uring 实例的主结构体,包含 SQ 和 CQ 的指针及参数 |
| 数据结构 | struct io_uring_sqe |
Submission Queue Entry,描述一个具体的 I/O 操作 |
| 数据结构 | struct io_uring_cqe |
Completion Queue Entry,包含 I/O 操作的结果 |
| 初始化 | io_uring_queue_init() |
初始化 io_uring 实例,分配 SQ 和 CQ 内存 |
| 获取 SQE | io_uring_get_sqe() |
从 SQ 中获取一个空闲的 SQE 条目 |
| 准备读操作 | io_uring_prep_read() |
填充 SQE 字段,准备一个 read 操作 |
| 提交请求 | io_uring_submit() |
将准备好的 SQE 提交给内核 |
| 等待完成 | io_uring_wait_cqe() |
阻塞等待一个 CQE 完成 |
| 标记处理 | io_uring_cqe_seen() |
通知内核该 CQE 已被消费 |
| 清理 | io_uring_queue_exit() |
释放 io_uring 实例占用的资源 |
注意:
io_uring_get_sqe() 返回的 SQE 指针是 SQ 环上的直接引用,不需要手动分配内存。但如果 SQ 已满,该函数会返回 NULL,必须检查返回值。
4.3 完整代码实现
下面的程序打开当前目录下的 testfile.txt,读取前 4096 字节到缓冲区,然后打印读取的内容。这是 io_uring 编程的“Hello World”。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>
#define BUF_SIZE 4096
#define FILE_NAME "testfile.txt"
int main(void) {
struct io_uring ring;
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
char buf[BUF_SIZE];
int fd, ret;
/* 1. 打开文件 */
fd = open(FILE_NAME, O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
/* 2. 初始化 io_uring,队列深度为 32 */
ret = io_uring_queue_init(32, &ring, 0);
if (ret < 0) {
fprintf(stderr, "io_uring_queue_init failed: %d\n", ret);
close(fd);
return 1;
}
/* 3. 获取一个空闲的 SQE */
sqe = io_uring_get_sqe(&ring);
if (!sqe) {
fprintf(stderr, "io_uring_get_sqe failed\n");
io_uring_queue_exit(&ring);
close(fd);
return 1;
}
/* 4. 准备 read 操作:从 fd 读取 BUF_SIZE 字节到 buf */
io_uring_prep_read(sqe, fd, buf, BUF_SIZE, 0);
/* 5. 提交 SQE 到内核 */
ret = io_uring_submit(&ring);
if (ret < 0) {
fprintf(stderr, "io_uring_submit failed: %d\n", ret);
io_uring_queue_exit(&ring);
close(fd);
return 1;
}
/* 6. 等待完成事件 */
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
fprintf(stderr, "io_uring_wait_cqe failed: %d\n", ret);
io_uring_queue_exit(&ring);
close(fd);
return 1;
}
/* 7. 处理结果:cqe->res 包含实际读取的字节数或负错误码 */
if (cqe->res < 0) {
fprintf(stderr, "read failed: %d\n", cqe->res);
} else {
printf("Read %d bytes:\n%.*s\n", cqe->res, cqe->res, buf);
}
/* 8. 标记 CQE 已处理 */
io_uring_cqe_seen(&ring, cqe);
/* 9. 清理资源 */
io_uring_queue_exit(&ring);
close(fd);
return 0;
}
4.4 关键步骤详解
4.4.1 初始化:io_uring_queue_init()
io_uring_queue_init(entries, ring, flags) 是 io_uring 的入口函数。它完成以下工作:
- 通过
io_uring_setup()系统调用与内核协商创建两个环形缓冲区(SQ 和 CQ)。 - 将 SQ 和 CQ 的内存映射到用户空间,实现零拷贝提交和收割。
entries参数指定 SQ 的深度(即最多同时提交多少个未完成的请求)。flags参数用于设置高级特性(如IORING_SETUP_IOPOLL),本例中传 0 表示使用默认中断驱动模式。
性能提示:队列深度
entries 的选择影响吞吐量。对于简单的同步程序,32 或 64 已经足够;对于高并发服务器,通常设置为 1024 或更高。
4.4.2 准备 SQE:io_uring_get_sqe() 与 io_uring_prep_read()
这两个函数配合使用:
io_uring_get_sqe()从 SQ 环中取出下一个可用的 SQE 指针。如果 SQ 已满,返回NULL。io_uring_prep_read(sqe, fd, buf, nbytes, offset)是一个内联辅助函数,它填充 SQE 的字段:
sqe->opcode = IORING_OP_READ;
sqe->fd = fd;
sqe->addr = (unsigned long)buf;
sqe->len = nbytes;
sqe->off = offset。
4.4.3 提交:io_uring_submit()
io_uring_submit() 将 SQ 环中所有新添加的 SQE(通过 SQ 尾指针标识)一次性提交给内核。返回值是实际提交的 SQE 数量。内核收到后,会异步地执行这些 I/O 操作。
4.4.4 等待与处理:io_uring_wait_cqe() 与 cqe->res
io_uring_wait_cqe(ring, &cqe_ptr) 是一个阻塞调用,直到至少有一个 CQE 到达。CQE 中的 res 字段是关键:
- 如果
res >= 0,表示操作成功,值为实际传输的字节数(与read()系统调用的返回值语义一致)。 - 如果
res < 0,表示操作失败,值为负的错误码(例如-EIO、-ENOMEM)。
处理完 CQE 后,必须调用 io_uring_cqe_seen() 来更新 CQ 头指针,释放该 CQE 占用的槽位,以便内核可以写入新的完成事件。
4.5 编译与运行
编译时需要链接 liburing 库:
gcc -o io_uring_read io_uring_read.c -luring
创建一个测试文件并运行:
echo "Hello from io_uring!" > testfile.txt
./io_uring_read
预期输出:
Read 21 bytes:
Hello from io_uring!
4.6 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
io_uring_queue_init 返回负值 |
内核版本过低(< 5.1)或 CONFIG_IO_URING 未开启 |
升级内核或检查内核配置 |
io_uring_get_sqe 返回 NULL |
SQ 队列已满,未及时提交或收割 | 增加队列深度或确保及时调用 io_uring_submit |
cqe->res 为负值(如 -9) |
文件描述符无效(-EBADF)或权限不足 | 检查 open() 是否成功 |
程序卡在 io_uring_wait_cqe |
提交的请求未正确触发,或内核未处理 | 检查 io_uring_submit 返回值是否为 1 |
4.7 本章小结
通过本章的示例,你已经掌握了 io_uring 编程的完整骨架:
- 初始化:
io_uring_queue_init()建立用户态与内核态的通信通道。 - 准备:
io_uring_get_sqe()+io_uring_prep_read()描述 I/O 操作。 - 提交:
io_uring_submit()将请求推入内核。 - 等待:
io_uring_wait_cqe()阻塞直到操作完成。 - 处理:检查
cqe->res获取结果。 - 清理:
io_uring_cqe_seen()和io_uring_queue_exit()释放资源。