C++ io_uring的使用小结

C++ io_uring的使用小结 io_uring是 Linux 内核在 5.1 版本引入的一套全新的、高性能的异步 I/O (Asynchronous I/O) 接口。它的出现是为了解决旧有的epoll和linux-aio在面对现代高速存储设备如 NVMe SSD和高并发网络场景时的性能瓶颈。虽然io_uring是一个 C 语言的内核 API但在 C 高性能网络编程和存储编程中它正逐渐成为主流选择。以下是对 Cio_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开发库12# Ubuntu/Debiansudoaptinstallliburing-dev完整代码示例这个例子展示了如何异步读取一个文件的前 1024 个字节。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293#include iostream#include fcntl.h#include unistd.h#include cstring#include liburing.h#include sys/stat.h// 定义队列深度即环形缓冲区的大小#define QUEUE_DEPTH 8#define BLOCK_SZ 1024intmain() {// 1. 初始化 io_uring 结构structio_uring ring;// io_uring_queue_init(深度, 实例指针, 标志位)// 0 表示默认配置intret io_uring_queue_init(QUEUE_DEPTH, ring, 0);if(ret 0) {std::cerr io_uring_queue_init failed: -ret std::endl;return1;}// 2. 打开文件 (使用 O_DIRECT 通常能发挥 io_uring 最大性能但这里为了简单使用普通模式)// 注意实际项目中请确保文件存在或者创建一个测试文件intfd open(test.txt, O_RDONLY);if(fd 0) {// 如果文件不存在创建一个临时的fd open(test.txt, O_RDWR | O_CREAT, 0644);constchar* msg Hello from io_uring! This is a test file content.;write(fd, msg,strlen(msg));fsync(fd);lseek(fd, 0, SEEK_SET);// 重置文件指针}// 准备缓冲区charbuffer[BLOCK_SZ];memset(buffer, 0, BLOCK_SZ);structiovec iov;iov.iov_base buffer;iov.iov_len BLOCK_SZ;// 3. 获取一个提交队列项 (SQE)structio_uring_sqe *sqe io_uring_get_sqe(ring);if(!sqe) {std::cerr Could not get SQE std::endl;return1;}// 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_enterret io_uring_submit(ring);if(ret 0) {std::cerr io_uring_submit failed: -ret std::endl;return1;}std::cout Request submitted, waiting for completion... std::endl;// 6. 等待完成队列项 (CQE)structio_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;return1;}// 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);return0;}代码编译1g -o uring_test uring_test.cpp -luring4. io_uring 的高级特性对于追求极致性能的 C 开发者io_uring提供了几个杀手级特性A. Submission Queue Polling (SQPOLL)默认情况下io_uring_submit仍然需要一次系统调用 (io_uring_enter) 来通知内核有新任务。如果在初始化时设置IORING_SETUP_SQPOLL标志内核会启动一个专门的内核线程来轮询 SQ。效果用户只需把 SQE 放入环形队列内核线程自动发现并处理。完全消除了系统调用开销。代价消耗更多的 CPU 资源内核线程一直在空转检查。