C++ io_uring的使用小结
io_uring是 Linux 内核在 5.1 版本引入的一套全新的、高性能的异步 I/O (Asynchronous I/O) 接口。它的出现是为了解决旧有的epoll和linux-aio在面对现代高速存储设备(如 NVMe SSD)和高并发网络场景时的性能瓶颈。
虽然io_uring是一个 C 语言的内核 API,但在 C++ 高性能网络编程和存储编程中,它正逐渐成为主流选择。
以下是对 C++io_uring的详细介绍,包括其原理、优势以及如何在 C++ 中使用它。
1. 为什么我们需要 io_uring?
在io_uring出现之前,Linux 下主要有两种 I/O 模式:
- 同步 I/O (read/write) + 多路复用 (epoll):
- 这是最主流的网络编程模式(如 Nginx, Redis, Node.js)。
- 缺点:
read/write是系统调用,每次调用都需要在用户态和内核态之间切换。对于海量小包处理,系统调用的开销非常大。此外,epoll只能通知“可读/可写”状态,实际的数据拷贝还是同步发生的。
- Linux Native AIO (libaio):
- 缺点:仅支持 Direct I/O (O_DIRECT),对 Buffered I/O 支持很差(经常退化为同步阻塞)。API 设计复杂,且存在不必要的内存拷贝。
io_uring的目标:提供统一的、全异步的、零拷贝(或少拷贝)的、无锁的 I/O 接口,既支持文件 I/O 也支持网络 I/O。
2. io_uring 的核心原理:环形缓冲区 (Ring Buffer)
io_uring的名字来源于 “User Ring”。它在用户态和内核态之间共享了两个环形队列(Ring Buffer),从而避免了频繁的系统调用和内存拷贝。
这两个队列分别是:
- 提交队列 (Submission Queue, SQ):
- 用户程序向这个队列中放入 I/O 请求(称为 SQE, Submission Queue Entry)。
- 例如:“请把文件 A 的前 4KB 读取到缓冲区 B”。
- 完成队列 (Completion Queue, CQ):
- 内核处理完请求后,将结果(称为 CQE, Completion Queue Entry)放入这个队列。
- 用户程序从这里读取结果(例如:“读取成功,读取了 4096 字节”)。
工作流程:
- 用户将 SQE 放入 SQ。
- 用户通过一次系统调用 (
io_uring_enter) 通知内核(或者在轮询模式下甚至不需要系统调用)。 - 内核从 SQ 获取请求并执行。
- 内核将结果写入 CQ。
- 用户从 CQ 读取结果。
3. C++ 中使用 io_uring (liburing)
直接操作内核的原始结构体非常繁琐且容易出错。因此,通常使用官方封装的 C 库liburing。在 C++ 中,我们通常直接调用liburing的 C 接口,或者使用对其进行 C++ 封装的库(如asio的 io_uring backend)。
下面是一个使用liburing进行异步文件读取的 C++ 示例。
前置准备
你需要安装liburing开发库:
1 2 |
|
完整代码示例
这个例子展示了如何异步读取一个文件的前 1024 个字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
|
代码编译
1 |
|
4. io_uring 的高级特性
对于追求极致性能的 C++ 开发者,io_uring提供了几个杀手级特性:
A. Submission Queue Polling (SQPOLL)
默认情况下,io_uring_submit仍然需要一次系统调用 (io_uring_enter) 来通知内核有新任务。
如果在初始化时设置IORING_SETUP_SQPOLL标志,内核会启动一个专门的内核线程来轮询 SQ。
- 效果:用户只需把 SQE 放入环形队列,内核线程自动发现并处理。完全消除了系统调用开销。
- 代价:消耗更多的 CPU 资源(内核线程一直在空转检查)。
