返回顶部
分享到

C++ io_uring的使用

C语言 来源:互联网 作者:佚名 发布时间:2026-01-11 17:04:17 人浏览
摘要

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

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 模式:

  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 开发库:

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

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 操作。

C. 链接请求 (IOSQE_IO_LINK)

你可以将多个 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 是非常有价值的。

版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。

您可能感兴趣的文章 :

原文链接 :
    Tag :
相关文章
  • C++ io_uring的使用
    io_uring是 Linux 内核在 5.1 版本引入的一套全新的、高性能的异步 I/O (Asynchronous I/O) 接口。它的出现是为了解决旧有的epoll和linux-aio在面对现代
  • C++特有的bool变量使用
    C++中的bool类型 在C++中,bool是一种基本数据类型,专门用于表示布尔值(真或假)。它是C++语言特有的布尔类型,与C语言中使用整数模拟布
  • C++右值引用(rvalue references)与移动语义(move semant
    一、右值引用(rvalue references)与移动语义(move semantics)设计动机 1.1 为什么需要移动语义 传统 C++ 的对象拷贝(copy)在管理资源(堆内存
  • c++日志库log4cplus快速入门

    c++日志库log4cplus快速入门
    log4j 用于Java,log4cplus从它衍生而来,用于c++。 用于c++的日志库还有很多,如 log4cxx等,可以根据实际需求选择使用。 log4cplus 的地址:http
  • 从入门到精通C++11 <chrono> 库特性

    从入门到精通C++11 <chrono> 库特性
    在 C++11 标准中,引入了许多新的库特性,其中chrono库为时间处理提供了强大而灵活的支持。这个库使得在 C++ 中处理时间变得更加方便和精
  • C++ Sort函数使用场景分析

    C++ Sort函数使用场景分析
    C++ Sort函数详解 前言 :sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某
  • 使用DeepSeek API 结合VSCode提升开发效率

    使用DeepSeek API 结合VSCode提升开发效率
    在当今的软件开发领域,API 的使用已经成为不可或缺的一部分。DeepSeek 是一个强大的 API 平台,提供了丰富的功能和数据,可以帮助开发者
  • 使用Cline+deepseek实现VsCode自动化编程

    使用Cline+deepseek实现VsCode自动化编程
    不知道大家有没有听说过cursor这个工具,类似于AI+VsCode的结合体,只要绑定chatgpt、claude等大模型API,就可以实现对话式自助编程,简单闲聊
  • C++中什么是虚函数
    简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。 首先:强调一个概念定义一个函数为虚函数,不代表函数为不被实现的函数。
  • Qt实现文件的压缩和解压缩操作

    Qt实现文件的压缩和解压缩操作
    一、实现方式 通过Qt自带的库来实现,使用多线程方式,通过信号和槽来触发压缩与解压缩,并将压缩和解压缩结果回传过来。 使用的类:
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计