26人参与 • 2026-01-11 • C/C++
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 模式:
read/write 是系统调用,每次调用都需要在用户态和内核态之间切换。对于海量小包处理,系统调用的开销非常大。此外,epoll 只能通知“可读/可写”状态,实际的数据拷贝还是同步发生的。io_uring 的目标: 提供统一的、全异步的、零拷贝(或少拷贝)的、无锁的 i/o 接口,既支持文件 i/o 也支持网络 i/o。
io_uring 的名字来源于 “user ring”。它在用户态和内核态之间共享了两个环形队列(ring buffer),从而避免了频繁的系统调用和内存拷贝。
这两个队列分别是:
工作流程:
io_uring_enter) 通知内核(或者在轮询模式下甚至不需要系统调用)。直接操作内核的原始结构体非常繁琐且容易出错。因此,通常使用官方封装的 c 库 liburing。在 c++ 中,我们通常直接调用 liburing 的 c 接口,或者使用对其进行 c++ 封装的库(如 asio 的 io_uring backend)。
下面是一个使用 liburing 进行异步文件读取的 c++ 示例。
你需要安装 liburing 开发库:
# ubuntu/debian sudo apt install liburing-dev
这个例子展示了如何异步读取一个文件的前 1024 个字节。
#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;
}
g++ -o uring_test uring_test.cpp -luring
对于追求极致性能的 c++ 开发者,io_uring 提供了几个杀手级特性:
默认情况下,io_uring_submit 仍然需要一次系统调用 (io_uring_enter) 来通知内核有新任务。
如果在初始化时设置 ioring_setup_sqpoll 标志,内核会启动一个专门的内核线程来轮询 sq。
在传统的系统调用中,每次操作内核都需要把文件描述符 (fd) 映射到内部的文件结构,并锁定内存页。
get_user_pages 操作。你可以将多个 sqe 链接起来,强制它们按顺序执行。例如:先 open 文件,成功后再 read,最后 close。这允许在一次系统调用中编排复杂的 i/o 逻辑。
虽然可以直接使用 liburing,但在现代 c++ 开发中,我们通常使用更高层的封装:
boost.asio:
io_uring 作为底层的 reactor 实现(在 linux 上)。boost_asio_has_io_uring 宏启用。这使得你可以在不改变现有 asio 代码逻辑的情况下,享受到 io_uring 的性能提升。seastar:
io_uring 的早期采用者,非常适合构建高吞吐量的网络服务。userver:
io_uring。io_uring 优势巨大。因为减少了系统调用次数,上下文切换开销大幅降低。io_uring(尤其是 sqpoll 模式)在受补丁影响的机器上优势更明显。c++ io_uring 是 linux 高性能编程的未来。
liburing 是非常有价值的。到此这篇关于c++ io_uring的使用小结的文章就介绍到这了,更多相关c++ io_uring内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论