liburing 基础 API 详解
在了解了 io_uring
的核心原理后,是时候动手写代码了。虽然我们可以直接使用
io_uring_setup 等系统调用,但官方推荐使用 liburing。它封装了繁琐的内存屏障(Memory
Barrier)和环形缓冲区管理,提供了一套更人性化的 API。
1. 核心工作流
使用 liburing 的标准流程可以概括为以下 6
步:
graph TD
A[1. 初始化 io_uring_queue_init] --> B[2. 获取 SQE io_uring_get_sqe]
B --> C[3. 填充请求 io_uring_prep_read/write]
C --> D[4. 提交请求 io_uring_submit]
D --> E[5. 等待完成 io_uring_wait_cqe]
E --> F[6. 处理结果 & 标记 io_uring_cqe_seen]
F -->|更多任务?| B
F -->|结束| G[7. 销毁 io_uring_queue_exit]
2. 关键 API 解析
2.1 初始化与销毁
#include <liburing.h>
struct io_uring ring;
// 初始化一个队列深度为 32 的 ring
// flags 通常为 0,也可以是 IORING_SETUP_SQPOLL 等
int ret = io_uring_queue_init(32, &ring, 0);
// ... 使用 ring ...
// 程序退出前清理资源
io_uring_queue_exit(&ring);2.2 提交请求 (Submission)
这是生产者的过程。我们需要从 Submission Queue (SQ) 中获取一个空闲的 Entry (SQE),然后填充它。
// 1. 获取空闲 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) {
// 队列满了,需要先 submit 或者处理一些 completion
}
// 2. 准备操作 (以 Read 为例)
// 相当于 pread(fd, buf, len, offset)
io_uring_prep_read(sqe, fd, buf, len, offset);
// 3. (可选) 绑定用户数据
// cqe 返回时会带回这个 data,用于区分是哪个请求完成了
io_uring_sqe_set_data(sqe, user_data_ptr);
// 4. 提交给内核
io_uring_submit(&ring);liburing 提供了大量的
io_uring_prep_* 辅助函数,如
io_uring_prep_write,
io_uring_prep_accept,
io_uring_prep_poll_add 等,覆盖了绝大多数 I/O
操作。
2.3 处理结果 (Completion)
这是消费者的过程。内核处理完请求后,会将结果放入 Completion Queue (CQ)。
struct io_uring_cqe *cqe;
// 等待至少一个事件完成 (阻塞)
int ret = io_uring_wait_cqe(&ring, &cqe);
// 或者:非阻塞尝试获取
// int ret = io_uring_peek_cqe(&ring, &cqe);
if (ret == 0) {
// 检查操作结果 (res 相当于系统调用的返回值)
if (cqe->res < 0) {
fprintf(stderr, "Async operation failed: %s\n", strerror(-cqe->res));
} else {
printf("Read %d bytes\n", cqe->res);
}
// 获取之前绑定的用户数据
void *data = io_uring_cqe_get_data(cqe);
// 重要:标记该 CQE 已处理,内核可以复用该槽位
io_uring_cqe_seen(&ring, cqe);
}3. 实战:实现一个简单的
cat 命令
下面我们编写一个完整的程序
01-cat-file.c,它使用 io_uring
读取文件内容并打印到标准输出。
3.1 代码实现
/* examples/io_uring/01-cat-file.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <liburing.h>
#define BLOCK_SZ 4096
int main(int argc, char *argv[]) {
struct io_uring ring;
if (argc < 2) return 1;
// 1. 初始化
io_uring_queue_init(4, &ring, 0);
int fd = open(argv[1], O_RDONLY);
struct stat st;
fstat(fd, &st);
off_t file_sz = st.st_size;
off_t offset = 0;
char buff[BLOCK_SZ];
while (offset < file_sz) {
// 2. 获取 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
int bytes_to_read = BLOCK_SZ;
if (offset + bytes_to_read > file_sz)
bytes_to_read = file_sz - offset;
// 3. 准备 Read 请求
io_uring_prep_read(sqe, fd, buff, bytes_to_read, offset);
// 4. 提交
io_uring_submit(&ring);
// 5. 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 6. 处理结果
if (cqe->res > 0) {
fwrite(buff, 1, cqe->res, stdout);
offset += cqe->res;
}
// 7. 标记 Seen
io_uring_cqe_seen(&ring, cqe);
}
close(fd);
io_uring_queue_exit(&ring);
return 0;
}完整代码: 01-cat-file.c
3.2 编译与运行
你需要安装 liburing-dev (Debian/Ubuntu) 或
liburing (Arch/Fedora)。
gcc 01-cat-file.c -o 01-cat-file -luring
./01-cat-file /etc/passwd4. 总结
通过这个简单的例子,我们看到了 io_uring
的基本骨架。虽然对于同步读取文件来说,这比直接用
read() 麻烦了不少,但在高并发网络编程中,这种
“提交 -> 异步处理 -> 通知”
的模式能带来巨大的性能收益。
下一篇,我们将挑战更复杂的场景:编写一个基于 io_uring 的 TCP Echo Server。
上一篇: 02-vs-epoll-performance.md - 性能对比 下一篇: 04-echo-server.md - 实战:TCP Echo Server