io_uring 是 Linux 内核在 5.1 版本引入的一套全新的、高性能的异步 I/O (Asynchronous I/O) 接口。它的出现是为了解决旧有的 epoll 和 linux-aio 在面对现代高速存储设备(如 NVMe SSD)和高并发网络场景时的性能瓶颈。
虽然 io_uring 是一个 C 语言的内核 API,但在 C++ 高性能网络编程和存储编程中,它正逐渐成为主流选择。
以下是对 C++ io_uring 的详细介绍,包括其原理、优势以及如何在 C++ 中使用它。
在 io_uring 出现之前,Linux 下主要有两种 I/O 模式:
io_uring 的目标: 提供统一的、全异步的、零拷贝(或少拷贝)的、无锁的 I/O 接口,既支持文件 I/O 也支持网络 I/O。
io_uring 的名字来源于 “User Ring”。它在用户态和内核态之间共享了两个环形队列(Ring Buffer),从而避免了频繁的系统调用和内存拷贝。
这两个队列分别是:
工作流程:
直接操作内核的原始结构体非常繁琐且容易出错。因此,通常使用官方封装的 C 库 liburing。在 C++ 中,我们通常直接调用 liburing 的 C 接口,或者使用对其进行 C++ 封装的库(如 asio 的 io_uring backend)。
下面是一个使用 liburing 进行异步文件读取的 C++ 示例。
你需要安装 liburing 开发库:
|
1 2 |
# Ubuntu/Debian sudo apt install liburing-dev |
这个例子展示了如何异步读取一个文件的前 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 |
#include <iostream> #include <fcntl.h> #include <unistd.h> #include <cstring> #include <liburing.h> #include <sys/stat.h>
// 定义队列深度,即环形缓冲区的大小 #define QUEUE_DEPTH 8 #define BLOCK_SZ 1024
int main() { // 1. 初始化 io_uring 结构 struct io_uring ring; // io_uring_queue_init(深度, 实例指针, 标志位) // 0 表示默认配置 int ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0); if (ret < 0) { std::cerr << "io_uring_queue_init failed: " << -ret << std::endl; return 1; }
// 2. 打开文件 (使用 O_DIRECT 通常能发挥 io_uring 最大性能,但这里为了简单使用普通模式) // 注意:实际项目中请确保文件存在,或者创建一个测试文件 int fd = open("test.txt", O_RDONLY); if (fd < 0) { // 如果文件不存在,创建一个临时的 fd = open("test.txt", O_RDWR | O_CREAT, 0644); const char* msg = "Hello from io_uring! This is a test file content."; write(fd, msg, strlen(msg)); fsync(fd); lseek(fd, 0, SEEK_SET); // 重置文件指针 }
// 准备缓冲区 char buffer[BLOCK_SZ]; memset(buffer, 0, BLOCK_SZ); struct iovec iov; iov.iov_base = buffer; iov.iov_len = BLOCK_SZ;
// 3. 获取一个提交队列项 (SQE) struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); if (!sqe) { std::cerr << "Could not get SQE" << std::endl; return 1; }
// 4. 填充 SQE 请求 // 这是一个 "Read Vector" 操作 // 参数: sqe, 文件描述符, iovec数组, iovec数量, 偏移量 io_uring_prep_readv(sqe, fd, &iov, 1, 0);
// 设置用户数据 (user_data),这是一个 64 位字段,内核会原样传回 CQE。 // 通常用来存放请求的 ID 或者回调函数的指针。 io_uring_sqe_set_data(sqe, nullptr); // 这里简单设为 null
// 5. 提交请求给内核 // io_uring_submit 会调用系统调用 io_uring_enter ret = io_uring_submit(&ring); if (ret < 0) { std::cerr << "io_uring_submit failed: " << -ret << std::endl; return 1; }
std::cout << "Request submitted, waiting for completion..." << std::endl;
// 6. 等待完成队列项 (CQE) struct io_uring_cqe *cqe; // io_uring_wait_cqe 会阻塞直到至少有一个事件完成 ret = io_uring_wait_cqe(&ring, &cqe); if (ret < 0) { std::cerr << "io_uring_wait_cqe failed: " << -ret << std::endl; return 1; }
// 7. 处理结果 if (cqe->res < 0) { std::cerr << "Async read failed: " << -cqe->res << std::endl; } else { std::cout << "Read " << cqe->res << " bytes." << std::endl; std::cout << "Content: " << buffer << std::endl; }
// 8. 标记 CQE 已处理 (这一步很重要,否则队列会满) io_uring_cqe_seen(&ring, cqe);
// 9. 清理资源 close(fd); io_uring_queue_exit(&ring);
return 0; } |
|
1 |
g++ -o uring_test uring_test.cpp -luring |
对于追求极致性能的 C++ 开发者,io_uring 提供了几个杀手级特性:
默认情况下,io_uring_submit 仍然需要一次系统调用 (io_uring_enter) 来通知内核有新任务。
如果在初始化时设置 IORING_SETUP_SQPOLL 标志,内核会启动一个专门的内核线程来轮询 SQ。
在传统的系统调用中,每次操作内核都需要把文件描述符 (fd) 映射到内部的文件结构,并锁定内存页。
你可以将多个 SQE 链接起来,强制它们按顺序执行。例如:先 open 文件,成功后再 read,最后 close。这允许在一次系统调用中编排复杂的 I/O 逻辑。
虽然可以直接使用 liburing,但在现代 C++ 开发中,我们通常使用更高层的封装:
Boost.Asio:
Seastar:
Userver:
C++ io_uring 是 Linux 高性能编程的未来。