利用 io_uring 实现高性能异步 Linux 应用
Source: Dev.to

作者: Sospeter Kinyanjui
简介
很长一段时间里,Linux 只提供了 epoll——一种 I/O 通知机制,允许应用程序向内核发起 read/write 系统调用。
epoll 首次出现在 Linux 2.5.44(2002 年),并在 2.6(2003 年)成为主流。它通过三个系统调用 epoll_create、epoll_ctl 和 epoll_wait 使用 就绪模型。内核在资源就绪时通知应用程序,应用程序随后提交工作。
由于内核仅在有东西就绪时才通知,这种模型的复杂度是 O(1)——无论监视 10 条连接还是 10 000 条,成本相同。然而,每一次通知仍然需要一次系统调用,这就产生了昂贵的 系统调用税:每个事件都要从用户态切换到内核态。
直到 2019 年,io_uring 才出现,提供了一个真正的异步 I/O 接口,所需的系统调用大幅减少。
什么是异步执行?
应用程序能够启动一个长时间运行的任务并继续执行其他工作,而无需等待该任务完成的能力。
异步执行可以更好地利用 CPU 和 I/O 资源。虽然 epoll 是事件驱动的(因此只是一种“异步”幻象),但 io_uring 实际上会批量提交多个 I/O 请求,只需一次系统调用,就能让读写操作独立进行。
定义与实现
io_uring 提供了三个系统调用:
| 调用 | 目的 |
|---|---|
io_uring_setup(2) | 创建 提交队列(SQ)和 完成队列(CQ),并返回文件描述符。它会配置环形缓冲区(head、tail、ring_mask、ring_entries)。 |
io_uring_enter(2) | 告诉内核 “我已经在环中放置了 SQE,请去处理它们”。 |
io_uring_register(2) | 预先向内核注册资源(例如缓冲区、文件),以避免每次请求的查找。 |
io_uring_setup
- 分配一个共享内存区域,用于保存 SQ 和 CQ 结构。
- 用户空间对 SQ 具有 写 权限(内核读取)。
- 内核对 CQ 具有 写 权限(用户读取)。
- 该设计遵循 单生产者 / 单消费者 模型,以获得最高性能。
io_uring_enter
整个操作的 “引擎启动器”。其原型:
#include <sys/syscall.h>
int io_uring_enter(unsigned int fd,
unsigned int to_submit,
unsigned int min_complete,
unsigned int flags,
sigset_t *sig);
调用 io_uring_enter 会通知内核有 to_submit 个 SQE 已准备好进行处理。
io_uring_register
为你的数据提供 “VIP 通行证”。通过预先注册缓冲区或文件,内核可以直接使用它们,无需额外的查找或映射,从而消除大量开销。
生态系统和语言绑定
C 开发者可以使用官方的 liburing 库,它封装了三个系统调用并提供辅助函数。
Rust 也对 io_uring 提供了强大的支持,提供内存安全保证,防止内核和应用同时访问同一缓冲区的经典“危险区”。Rust 编译器确保在内核通过 CQE 返回之前,用户代码不能触碰该缓冲区。
流行的 Rust crate 包括:
tokio-uring– 将io_uring与 Tokio 异步运行时集成。glommio– 基于io_uring构建的每核一线程框架。- 其他:
io-uring、uring-sys等。
还有更多
基于完成的 I/O 并非 Linux 独有:
| 操作系统 | 机制 | 特点 |
|---|---|---|
| Windows | I/O 完成端口 (IOCP) | 异步,但仍需对每个请求进行一次系统调用,导致的系统调用开销高于 io_uring。 |
| macOS | kqueue | 基于就绪;必须调用 kevent 来发现就绪状态,然后再为实际 I/O 发起单独的系统调用,产生了 io_uring 本想消除的同样系统调用开销。 |
因此,io_uring 代表了 Linux 上真正的异步编程模型,最大限度地减少了高性能 I/O 所需的系统调用次数和上下文切换。
“做最好的齿轮,但要记住你并不是唯一的齿轮。” ——提醒我们并非所有问题都必须自己解决;有时合适的工具(如
io_uring)就足以产生巨大影响。
真正的问题:跨平台、基于完成的异步运行时
从我的个人视角来看,我认为真正的问题在于创建一个 跨平台 且 基于完成 的异步运行时。这类技术已经存在:我们有 compio,一个用于异步 I/O 操作的 Rust 框架。
缺失的是什么?
- 零成本抽象 – 可以说 compio 并未提供真正的零成本抽象。
- 固定缓冲区 – 它使用固定缓冲区(
io_uring的设计选择),这些缓冲区是不可变引用。
大多数 Rust 生态系统都是基于 std::io::Read 和 std::io::Write trait 构建的,这些 trait 期望 可变 引用。而 compio 则强调 缓冲区的所有权 而不是借用。这与 io_uring 的基于完成模型非常契合,但也导致了它与生态系统其他部分的真实集成问题。
“但正如我所说,我们只能在这里,一次实现一个方案。即使看似不可能,也要相信自己。下次再见,和平、专注、渴望。”
保持联系
您可以查看我博客上的其他文章。
- GitHub:
- LinkedIn: