C++ io_uring介绍

io_uring 是 Linux 内核在 5.1 版本引入的一套全新的、高性能的异步 I/O (Asynchronous I/O) 接口。它的出现是为了解决旧有的 epolllinux-aio 在面对现代高速存储设备(如 NVMe SSD)和高并发网络场景时的性能瓶颈。

虽然 io_uring 是一个 C 语言的内核 API,但在 C++ 高性能网络编程和存储编程中,它正逐渐成为主流选择。

以下是对 C++ io_uring 的详细介绍,包括其原理、优势以及如何在 C++ 中使用它。


1. 为什么我们需要 io_uring?

io_uring 出现之前,Linux 下主要有两种 I/O 模式:

  1. 同步 I/O (read/write) + 多路复用 (epoll):
    • 这是最主流的网络编程模式(如 Nginx, Redis, Node.js)。
    • 缺点:read/write 是系统调用,每次调用都需要在用户态和内核态之间切换。对于海量小包处理,系统调用的开销非常大。此外,epoll 只能通知“可读/可写”状态,实际的数据拷贝还是同步发生的。
  2. 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),从而避免了频繁的系统调用和内存拷贝。

这两个队列分别是:

  1. 提交队列 (Submission Queue, SQ):
    • 用户程序向这个队列中放入 I/O 请求(称为 SQE, Submission Queue Entry)。
    • 例如:“请把文件 A 的前 4KB 读取到缓冲区 B”。
  2. 完成队列 (Completion Queue, CQ):
    • 内核处理完请求后,将结果(称为 CQE, Completion Queue Entry)放入这个队列。
    • 用户程序从这里读取结果(例如:“读取成功,读取了 4096 字节”)。

工作流程:

  1. 用户将 SQE 放入 SQ。
  2. 用户通过一次系统调用 (io_uring_enter) 通知内核(或者在轮询模式下甚至不需要系统调用)。
  3. 内核从 SQ 获取请求并执行。
  4. 内核将结果写入 CQ。
  5. 用户从 CQ 读取结果。

3. C++ 中使用 io_uring (liburing)

直接操作内核的原始结构体非常繁琐且容易出错。因此,通常使用官方封装的 C 库 liburing。在 C++ 中,我们通常直接调用 liburing 的 C 接口,或者使用对其进行 C++ 封装的库(如 asio 的 io_uring backend)。

下面是一个使用 liburing 进行异步文件读取的 C++ 示例。

前置准备

你需要安装 liburing 开发库:

# Ubuntu/Debiansudoaptinstall liburing-dev 
完整代码示例

这个例子展示了如何异步读取一个文件的前 1024 个字节。

#include<iostream>#include<fcntl.h>#include<unistd.h>#include<cstring>#include<liburing.h>#include<sys/stat.h>// 定义队列深度,即环形缓冲区的大小#defineQUEUE_DEPTH8#defineBLOCK_SZ1024intmain(){// 1. 初始化 io_uring 结构structio_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;return1;}// 2. 打开文件 (使用 O_DIRECT 通常能发挥 io_uring 最大性能,但这里为了简单使用普通模式)// 注意:实际项目中请确保文件存在,或者创建一个测试文件int fd =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);// 重置文件指针}// 准备缓冲区char buffer[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_enter ret =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;}
代码编译
g++ -o uring_test uring_test.cpp -luring 

4. io_uring 的高级特性

对于追求极致性能的 C++ 开发者,io_uring 提供了几个杀手级特性:

A. Submission Queue Polling (SQPOLL)

默认情况下,io_uring_submit 仍然需要一次系统调用 (io_uring_enter) 来通知内核有新任务。
如果在初始化时设置 IORING_SETUP_SQPOLL 标志,内核会启动一个专门的内核线程来轮询 SQ。

  • 效果: 用户只需把 SQE 放入环形队列,内核线程自动发现并处理。完全消除了系统调用开销
  • 代价: 消耗更多的 CPU 资源(内核线程一直在空转检查)。
B. 注册文件和缓冲区 (Registered Files/Buffers)

在传统的系统调用中,每次操作内核都需要把文件描述符 (fd) 映射到内部的文件结构,并锁定内存页。

  • Registered Files: 预先将 fd 数组注册给 io_uring,后续请求直接使用索引,减少内核查找 fd 的开销。
  • Registered Buffers: 预先将用户态内存锁定并映射,避免每次 I/O 时内核重复进行 get_user_pages 操作。

你可以将多个 SQE 链接起来,强制它们按顺序执行。例如:先 open 文件,成功后再 read,最后 close。这允许在一次系统调用中编排复杂的 I/O 逻辑。

5. C++ 生态中的 io_uring

虽然可以直接使用 liburing,但在现代 C++ 开发中,我们通常使用更高层的封装:

  1. Boost.Asio:
    • Boost.Asio 已经支持 io_uring 作为底层的 Reactor 实现(在 Linux 上)。
    • 通过 BOOST_ASIO_HAS_IO_URING 宏启用。这使得你可以在不改变现有 Asio 代码逻辑的情况下,享受到 io_uring 的性能提升。
  2. Seastar:
    • 一个高性能的 C++ futures 框架,专为现代硬件设计。它是 io_uring 的早期采用者,非常适合构建高吞吐量的网络服务。
  3. Userver:
    • Yandex 开源的 C++ 异步框架,底层也深度集成了 io_uring

6. io_uring vs Epoll 性能对比

  • 小包/高频 I/O:io_uring 优势巨大。因为减少了系统调用次数,上下文切换开销大幅降低。
  • 大包/低频 I/O: 差距较小,瓶颈主要在内存拷贝和硬件带宽。
  • Spectre/Meltdown 补丁影响: 这些 CPU 漏洞补丁增加了系统调用的开销,因此 io_uring(尤其是 SQPOLL 模式)在受补丁影响的机器上优势更明显。

7. 总结

C++ io_uring 是 Linux 高性能编程的未来。

  • 核心优势: 真正的异步 I/O、减少系统调用、减少内存拷贝、统一了网络和磁盘 I/O 接口。
  • 适用场景: 高并发网络服务器(HTTP/RPC)、高性能数据库、分布式存储系统。
  • 建议: 如果你在编写通用的业务代码,建议使用 Boost.Asio 等封装好的库;如果你在开发底层的存储引擎或极致性能的网关,深入学习并直接使用 liburing 是非常有价值的。

Read more

C++ 二叉搜索树(BST)深度解析:从概念原理、核心操作到底层实现

C++ 二叉搜索树(BST)深度解析:从概念原理、核心操作到底层实现

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》 《算法题讲解指南》--优选算法 《算法题讲解指南》--递归、搜索与回溯算法 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 前言 一、二叉搜索树的核心概念:什么是 BST? 二、二叉搜索树的性能分析:理想与最差情况 三. 二叉搜索树的实战实现   1、节点结构定义:BSTNode   2、BST 类核心操作:Insert、Find、Erase     2.1 插入操作(Insert) 代码实现: 测试代码:     2.2 查找操作(Find) 代码实现: 测试代码:

By Ne0inhk
【C++】深入解析AVL树:平衡搜索树的核心概念与实现

【C++】深入解析AVL树:平衡搜索树的核心概念与实现

【C++】深入解析AVL树:平衡搜索树的核心概念与实现 * 摘要 * 目录 * 一、AVL树的概念 * 二、AVL树的模拟实现 * 1. 节点结构体和树的类模板 * 2. 平衡因子的概念和实现 * 3. 插入 * 4. 旋转操作 * 4.1 右单旋 * 4.2 左单旋 * 4.3 左右双旋 * 4.4 右左双旋 * 三、AVL树的平衡检测 * 总结 摘要 本文深入解析了AVL树的核心概念与实现,包括节点结构设计、平衡因子定义及其更新机制、插入操作的自下而上平衡调整策略,以及四种旋转方式(左单旋、右单旋、左右双旋、右左双旋)对保持树平衡的重要作用。同时,提供了AVL树高度计算与平衡检测的实现方法,确保每个节点的平衡因子正确维护,从而保证树在插入操作后的高效性与稳定性。通过本文内容,读者可以系统掌握AVL树的原理、实现与调试技巧,

By Ne0inhk
【C++】类和对象—(下) 收官之战

【C++】类和对象—(下) 收官之战

前言:上一篇文章我们向大家介绍了类和对象的核心六个成员函数中的4个,其余两个以及初始化列表,static成员,内部类,匿名对象等会在本篇文章介绍! ✨ 坚持用清晰易懂的图解+代码语言, 让每个知识点都简单直观! 🚀 个人主页 :MSTcheng · ZEEKLOG 🌱 代码仓库 :MSTcheng · Gitee 📌 专栏系列 :📖 《C语言》🧩 《数据结构》💡 《C++由浅入深》💬 座右铭 :“路虽远行则将至,事虽难做则必成!” 文章目录 * 一,运算符重载 * 1.1什么是运算符重载? * 1.2 为什么要创造运算符重载? * 二,赋值运算符重载 * 2.1赋值运算符重载的构成 * 2.1 >>流插入<<流提取重载 * 3.1const成员函数 * 4.1取地址运算符重载 * 三,初始化列表 * 3.

By Ne0inhk

为什么顶尖团队都在用Concepts简化C++元编程?真相在这里

第一章:为什么顶尖团队都在用Concepts简化C++元编程? C++20 引入的 Concepts 彻底改变了模板元编程的编写方式,让类型约束从“运行时错误”转向“编译时契约”。传统模板依赖 SFINAE 或 requires 表达式进行类型检查,代码冗长且难以维护。而 Concepts 提供了一种清晰、可读性强的语法,使开发者能直接声明模板参数的语义要求。 更直观的类型约束 使用 Concepts 可以定义可重用的约束条件,提升模板接口的可读性与安全性。例如,定义一个适用于所有可加类型的操作: template concept Addable = requires(T a, T b) { { a + b } -> std::same_as; }; template T add(T a,

By Ne0inhk