土法炼钢兴趣小组的算法知识备份

【io_uring 系列】liburing 基础 API 详解:从 Hello World 到文件 I/O

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

liburing 基础 API 详解

在了解了 io_uring 的核心原理后,是时候动手写代码了。虽然我们可以直接使用 io_uring_setup 等系统调用,但官方推荐使用 liburing。它封装了繁琐的内存屏障(Memory Barrier)和环形缓冲区管理,提供了一套更人性化的 API。

1. 核心工作流

使用 liburing 的标准流程可以概括为以下 6 步:

liburing workflow

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 QUEUE_DEPTH 1
#define BLOCK_SZ  4096

int main(int argc, char *argv[]) {
  struct io_uring ring;
  int ret;
  
  if (argc < 2) {
  fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
  return 1;
  }

  // 1. 初始化
  ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
  if (ret < 0) {
  fprintf(stderr, "queue_init: %s\n", strerror(-ret));
  return 1;
  }

  int fd = open(argv[1], O_RDONLY);
  if (fd < 0) {
  perror("open");
  return 1;
  }

  struct stat st;
  if (fstat(fd, &st) < 0) {
  perror("fstat");
  return 1;
  }
  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);
  if (!sqe) {
  fprintf(stderr, "Could not get SQE.\n");
  break;
  }
  
  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. 提交
  ret = io_uring_submit(&ring);
  if (ret < 0) {
  fprintf(stderr, "io_uring_submit: %s\n", strerror(-ret));
  break;
  }

  // 5. 等待完成
  struct io_uring_cqe *cqe;
  ret = io_uring_wait_cqe(&ring, &cqe);
  if (ret < 0) {
  fprintf(stderr, "io_uring_wait_cqe: %s\n", strerror(-ret));
  break;
  }

  // 6. 处理结果
  if (cqe->res < 0) {
  fprintf(stderr, "Async read failed: %s\n", strerror(-cqe->res));
  io_uring_cqe_seen(&ring, cqe);
  break;
  }
  
  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/passwd

4. 常见陷阱与最佳实践

4.1 忘记调用 io_uring_cqe_seen

这是新手最常犯的错误。如果不调用 io_uring_cqe_seen,CQ 会逐渐填满,最终导致 io_uring_wait_cqe 无法返回新结果。

4.2 SQE 队列满时的处理

当 SQ 满了,io_uring_get_sqe 会返回 NULL。此时需要先 io_uring_submit 提交已有请求,或等待一些 CQE 完成后再重试。

struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) {
  // 队列满了,先提交当前请求
  io_uring_submit(&ring);
  // 处理一些完成事件以释放 SQ 槽位
  // ...
  sqe = io_uring_get_sqe(&ring);
}

4.3 Buffer 生命周期管理

io_uring_prep_read/write 传入的 buffer 地址必须在操作完成前保持有效。内核会在后台异步访问这块内存,如果 buffer 被提前释放或栈空间被覆盖,将导致数据损坏或崩溃。

4.4 批量提交 (Batching)

尽量累积多个 SQE 后一次性调用 io_uring_submit,而不是每填一个就提交一次。这能显著减少系统调用开销。

// 好的做法:批量提交
for (int i = 0; i < 10; i++) {
  struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
  io_uring_prep_read(sqe, fds[i], bufs[i], len, 0);
}
io_uring_submit(&ring);  // 一次提交 10 个请求

5. 总结

通过这个简单的例子,我们看到了 io_uring 的基本骨架。虽然对于同步读取文件来说,这比直接用 read() 麻烦了不少,但在高并发网络编程中,这种 “提交 -> 异步处理 -> 通知” 的模式能带来巨大的性能收益。

下一篇,我们将挑战更复杂的场景:编写一个基于 io_uring 的 TCP Echo Server


上一篇: 02-vs-epoll-performance.md - 性能对比 下一篇: 04-echo-server.md - 实战:TCP Echo Server

返回 io_uring 系列索引


By .