Linux 异步 I/O 框架 io_uring:根本原理、步伐示例与性能压测

[复制链接]
发表于 2025-11-13 11:58:24 | 显示全部楼层 |阅读模式
各人以为故意义和资助记得关注和点赞!!!

io_uring 是 2019 年 Linux 5.1 内核初次引入的高性能 异步 I/O 框架,能明显加快 I/O 麋集型应用的性能。 但如果你的应用已经在利用 传统 Linux AIO 了,而且利用方式适当, 那 io_uring 并不会带来太大的性能提升 —— 根据原文测试(以及我们 本身的复现),即便打开高级特性,也只有 5%。除非你真的必要这 5% 的额外性能,否则 切换成 io_uring 代价大概也挺大,由于要 重写应用来适配 io_uring(大概让依赖的平台或框架去适配,总之必要改代码)。
既然性能跟传统 AIO 差不多,那为什么还称 io_uring 为革命性技能呢?

  • 它起首和最大的贡献在于:同一了 Linux 异步 I/O 框架

    • Linux AIO 只支持 direct I/O 模式的存储文件 (storage file),而且告急用在数据库这一细分范畴
    • io_uring 支持存储文件和网络文件(network sockets),也支持更多的异步体系调用 (accept/openat/stat/...),而非仅限于 read/write 体系调用。

  • 计划上是真正的异步 I/O,作为对比,Linux AIO 固然也 是异步的,但仍然大概会壅闭,某些环境下的举动也无法推测;
    好像之前 Windows 在这块反而是领先的,更多参考:

    • 浅析开源项目之 io_uring,“分步试存储”专栏,知乎
    • Is there really no asynchronous block I/O on Linux?,stackoverflow

  • 机动性和可扩展性非常好,以致能基于 io_uring 重写全部体系调用,而 Linux AIO 计划时就没思量扩展性。
eBPF 也算是异步框架(变乱驱动),但与 io_uring 没有本质接洽,二者属于差别子体系, 而且在模子上有一个本质区别:

  • eBPF 对用户是透明的,只需升级内核(到符合的版本),应用步伐无需任何改造
  • io_uring 提供了新的体系调用和用户空间 API,因此必要应用步伐做改造
eBPF 作为动态跟踪工具,可以大概更方便地排查和观测 io_uring 等模块在实验层面的详细题目。
本文先容 Linux 异步 I/O 的发展汗青,io_uring 的原理和功能, 并给出了一些步伐示例性能压测效果(我们在 5.10 内核做了类似测试,结论与原文差不多)。
   Ceph 代码上已经支持了 io_uring,但发行版在编译时没有打开这个设置,判断是否支持 io_uring 直接返回的 false, 因此想测试得本身重新编译。测试时的参考设置:
  1. $ cat /etc/ceph/ceph.conf
  2. [osd]
  3. bluestore_ioring = true
  4. ...
复制代码
确认设置见效(这是只是任意挑一个 OSD):
  1. $ ceph config show osd.16 | grep ioring
  2. bluestore_ioring                       true                                            file
复制代码
还要去看下日志日志,是否由于检测 io_uring 失败而 fallback 回了 libaio。
  
 
目次
1 Linux I/O 体系调用演进
1.1 基于 fd 的壅闭式 I/O:read()/write()
1.2 非壅闭式 I/O:select()/poll()/epoll()
1.3 线程池方式
1.4 Direct I/O(数据库软件):绕过 page cache
1.5 异步 IO(AIO)
1.6 小结
2 io_uring
2.1 与 Linux AIO 的差别
2.2 原理及核心数据布局:SQ/CQ/SQE/CQE
2.3 带来的长处
2.4 三种工作模式
2.5 io_uring 体系调用 API
2.5.1 io_uring_setup()
2.5.2 io_uring_register()
注册的缓冲区(buffer)性子
通过 eventfd() 订阅 completion 变乱
2.5.3 io_uring_enter()
2.6 高级特性
2.7 用户空间库 liburing
3 基于 liburing 的示例应用
3.1 io_uring-test
源码及解释
其他阐明
3.2 link-cp
I/O chain
源码及解释
其他阐明
4 io_uring 性能压测(基于 fio)
4.1 测试环境
4.2 场景一:direct I/O 1KB 随机读(绕过 page cache)
4.2 场景二:buffered I/O 1KB 随机读(数据提前加载到内存,100% hot cache)
4.3 性能测试小结
4.4 ScyllaDB 与 io_uring
5 eBPF
6 竣事语
参考资料

 

很多人大概还没意识到,Linux 内核在已往几年已经发生了一场革命。这场革命源于 两个冲动民气的新接口的引入:eBPF 和 io_uring。 我们以为,二者将会完全改变应用与内核交互的方式,以及 应用开发者思索和对待内核的方式
本文先容 io_uring(我们在 ScyllaDB 中有 io_uring 的深入利用履历),并略微提及一下 eBPF。
1 Linux I/O 体系调用演进

1.1 基于 fd 的壅闭式 I/O:read()/write()

作为各人最认识的读写方式,Linux 内核提供了基于文件形貌符的体系调用, 这些形貌符指向的大概是存储文件(storage file),也大概是 network sockets
  1. ssize_t read(int fd, void *buf, size_t count);
  2. ssize_t write(int fd, const void *buf, size_t count);
复制代码
二者称为壅闭式体系调用(blocking system calls),由于步伐调用 这些函数时会进入 sleep 状态,然后被调理出去(让出处理惩罚器),直到 I/O 利用完成:


  • 如果数据在文件中,而且文件内容已经缓存在 page cache 中,调用会立即返回
  • 如果数据在另一台呆板上,就必要通过网络(比方 TCP)获取,会壅闭一段时间;
  • 如果数据在硬盘上,也会壅闭一段时间。
但很容易想到,随着存储装备越来越快,步伐越来越复杂, 壅闭式(blocking)已经这种最简朴的方式已经不实用了。
1.2 非壅闭式 I/O:select()/poll()/epoll()

壅闭式之后,出现了一些新的、非壅闭的体系调用,比方 select()、poll() 以及更新的 epoll()。 应用步伐在调用这些函数读写时不会壅闭,而是立即返回,返回的是一个 已经 ready 的文件形貌符列表
 

但这种方式存在一个致命缺点:只支持 network sockets 和 pipes —— epoll() 以致连 storage files 都不支持。
1.3 线程池方式

对于 storage I/O,经典的办理思绪是 thread pool: 主线程将 I/O 分发给 worker 线程,后者取代主线程举行壅闭式读写,主线程不会壅闭。
 

这种方式的题目是线程上下文切换开销大概非常大,背面性能压测会看到。
1.4 Direct I/O(数据库软件):绕过 page cache

随后出现了更加机动和强盛的方式:数据库软件(database software) 偶尔 并不想利用利用体系的 page cache, 而是渴望打开一个文件后,直接从装备读写这个文件(direct access to the device)。 这种方式称为直接访问(direct access)或直接 I/O(direct I/O),


  • 必要指定 O_DIRECT flag;
  • 必要应用本身管理本身的缓存 —— 这正是数据库软件所渴望的;
  • 是 zero-copy I/O,由于应用的缓冲数据直接发送到装备,大概直接从装备读取。
1.5 异步 IO(AIO)

前面提到,随着存储装备越来越快,主线程和 worker 线性之间的上下文切换开销占比越来越高。 如今市场上的一些装备,比方 Intel Optane ,耽误已经低到和上下文切换一个量级(微秒 us)。换个方式形貌, 更能让我们感受到这种开销: 上下文每切换一次,我们就少一次 dispatch I/O 的机遇
因此,Linux 2.6 内核引入了异步 I/O(asynchronous I/O)接口, 方便起见,本文简写为 linux-aio。AIO 原理是很简朴的:


  • 用户通过 io_submit() 提交 I/O 哀求,
  • 过一会再调用 io_getevents() 来查抄哪些 events 已经 ready 了。
  • 使步伐员能编写完全异步的代码
近期,Linux AIO 以致支持了 epoll():也就是说 不光能提交 storage I/O 哀求,还能提交网络 I/O 哀求。照如许发展下去,linux-aio 好像能成为一个王者。但由于它糟糕的演进之路,这个愿望险些不大概实现了。 我们从 Linus 标记性的猛烈言辞中就能略窥一斑
   Reply to: to support opening files asynchronously
  So I think this is ridiculously ugly.
  AIO is a horrible ad-hoc design, with the main excuse being “other, less gifted people, made that design, and we are implementing it for compatibility because database people — who seldom have any shred of taste — actually use it”.
  — Linus Torvalds (on lwn.net)
  起首,作为数据库从业职员,我们想借此机遇为我们的没品(lack of taste)向 Linus 致歉。 但更告急的是,我们要进一步表明一下为什么 Linus 是对的:Linux AIO 确实题目缠身,

  • 只支持 O_DIRECT 文件,因此对通例的非数据库应用 (normal, non-database applications)险些是无用的
  • 接口在计划时并未思量扩展性。固然可以扩展 —— 我们也确实这么做了 —— 但每加一个东西都相称复杂;
  • 固然从技能上说接口黑白壅闭的,但现实上有 很多大概的缘故起因都会导致它壅闭,而且引发的方式难以预料。
1.6 小结

以上可以清楚地看出 Linux I/O 的演进:


  • 最开始是同步(壅闭式)体系调用;
  • 然后随着现实需求和详细场景,不绝到场新的异步接口,还要保持与老接口的兼容和协同工作。
别的也看到,在非壅闭式读写的题目上并没有形成同一方案

  • Network socket 范畴:添加一个异步接口,然后去轮询(poll)哀求是否完成(readiness);
  • Storage I/O 范畴:只针对某一细分范畴(数据库)在某一特定时期的需求,添加了一个定制版的异步接口。
这就是 Linux I/O 的演进汗青 —— 只着眼当前,出现一个题目就引入一种计划,而并没有多少前瞻性 —— 直到 io_uring 的出现。
2 io_uring

io_uring 来自资深内核开发者 Jens Axboe 的想法,他在 Linux I/O stack 范畴颇有研究。 从最早的 patch aio: support for IO polling 可以看出,这项工作始于一个很简朴的观察:随着装备越来越快, 停止驱动(interrupt-driven)模式服从已经低于轮询模式 (polling for completions) —— 这也是高性能范畴最常见的主题之一。


  • io_uring 的根本逻辑与 linux-aio 是类似的:提供两个接口,一个将 I/O 哀求提交到内核,一个从内核汲取完成变乱。
  • 但随着开发深入,它渐渐变成了一个完全差别的接口:计划者开始从源头思索 怎样支持完全异步的利用
2.1 与 Linux AIO 的差别

io_uring 与 linux-aio 有着本质的差别:

  • 在计划上是真正异步的(truly asynchronous)。只要 设置了符合的 flag,它在体系调用上下文中就只是将哀求放入队列, 不会做其他任何额外的事变,包管了应用永久不会壅闭
  • 支持任何范例的 I/O:cached files、direct-access files 以致 blocking sockets。
    由于计划上就是异步的(async-by-design nature),因此无需 poll+read/write 来处理惩罚 sockets。 只需提交一个壅闭式读(blocking read),哀求完成之后,就会出如今 completion ring。
  • 机动、可扩展:基于 io_uring 以致能重写(re-implement)Linux 的每个体系调用。
2.2 原理及核心数据布局:SQ/CQ/SQE/CQE

每个 io_uring 实例都有两个环形队列(ring),在内核和应用步伐之间共享:


  • 提交队列:submission queue (SQ)
  • 完成队列:completion queue (CQ)
 

这两个队列:


  • 都是单生产者、单斲丧者,size 是 2 的幂次;
  • 提供无锁接口(lock-less access interface),内部利用 内存屏蔽做同步(coordinated with memory barriers)。
利用方式


  • 哀求

    • 应用创建 SQ entries (SQE),更新 SQ tail;
    • 内核斲丧 SQE,更新 SQ head。

  • 完成

    • 内核为完成的一个或多个哀求创建 CQ entries (CQE),更新 CQ tail;
    • 应用斲丧 CQE,更新 CQ head。
    • 完成变乱(completion events)大概以恣意序次到达,到总是与特定的 SQE 相干联的。
    • 斲丧 CQE 过程无需切换到内核态。

2.3 带来的长处

io_uring 这种哀求方式尚有一个长处是:原来必要多次体系调用(读或写),如今变成批处理惩罚一次提交。
还记得 Meltdown 毛病吗?当时我还写了一篇文章 表明为什么我们的 Scylla NoSQL 数据库受影响很小:aio 已经将我们的 I/O 体系调用批处理惩罚化了。
io_uring 将这种批处理惩罚本事带给了 storage I/O 体系调用之外的 其他一些体系调用,包罗:


  • read
  • write
  • send
  • recv
  • accept
  • openat
  • stat
  • 专用的一些体系调用,比方 fallocate
别的,io_uring 使异步 I/O 的利用场景也不再仅限于数据库应用,平凡的 非数据库应用也能用。这一点值得重复一遍:
   固然 io_uring 与 aio 有一些相似之处,但它的扩展性和架构是革命性的: 它将异步利用的强盛本事带给了全部应用(及其开发者),而 不再仅限于是数据库应用这一细分范畴
  我们的 CTO Avi Kivity 在 the Core C++ 2019 event 上 有一次关于 async 的分享。 核心点包罗:从耽误上来说

  • 今世多核、多 CPU 装备,其内部本身就是一个根本网络;
  • CPU 之间是另一个网络;
  • CPU 和磁盘 I/O 之间又是一个网络。
因此网络编程采取异步是明智的,而如今开发本身的应用也应该思量异步。 这从根本上改变了 Linux 应用的计划方式


  • 之前都是一段序次代码流,必要体系调用时才实验体系调用,
  • 如今必要思索一个文件是否 ready,因而自然地引入 event-loop,不绝通过共享 buffer 提交哀求和汲取效果。
2.4 三种工作模式

io_uring 实例可工作在三种模式:

  • 停止驱动模式(interrupt driven)
    默认模式。可通过 io_uring_enter() 提交 I/O 哀求,然后直接查抄 CQ 状态判断是否完成。
  • 轮询模式(polled)
    Busy-waiting for an I/O completion,而不是通过异步 IRQ(Interrupt Request)汲取关照。
    这种模式必要文件体系(如果有)和块装备(block device)支持轮询功能。 相比停止驱动方式,这种方式耽误更低(结合统调用都省了), 但大概会斲丧更多 CPU 资源。
    现在,只有指定了 O_DIRECT flag 打开的文件形貌符,才气利用这种模式。当一个读 或写哀求提交给轮询上下文(polled context)之后,应用(application)必须调用 io_uring_enter() 来轮询 CQ 队列,判断哀求是否已经完成。
    对一个 io_uring 实例来说,不支持肴杂利用轮询和非轮询模式
  • 内核轮询模式(kernel polled)
    这种模式中,会 创建一个内核线程(kernel thread)来实验 SQ 的轮询工作。
    利用这种模式的 io_uring 实例, 应用无需切到到内核态 就能触发(issue)I/O 利用。 通过 SQ 来提交 SQE,以及监控监控 CQ 的完成状态,应用无需任何体系调用,就能提交和收割 I/O(submit and reap I/Os)。
    如果内核线程的空闲时间高出了用户的设置值,它会关照应用,然后进入 idle 状态。 这种环境下,应用必须调用 io_uring_enter() 来唤醒内核线程。如果 I/O 不停很繁忙,内核线性是不会 sleep 的。
2.5 io_uring 体系调用 API

有三个:


  • io_uring_setup(2)
  • io_uring_register(2)
  • io_uring_enter(2)
下面睁开先容。完备文档见 manpage。
2.5.1 io_uring_setup()

实验异步 I/O 必要先设置上下文
  1. int io_uring_setup(u32 entries, struct io_uring_params *p);
复制代码
这个体系调用


  • 创建一个 SQ 和一个 CQ
  • queue size 至少 entries 个元素,
  • 返回一个文件形貌符,随后用于在这个 io_uring 实例上实验利用。
SQ 和 CQ 在应用和内核之间共享,克制了在初始化和完成 I/O 时(initiating and completing I/O)拷贝数据。
参数 p:


  • 应用用来设置 io_uring,
  • 内核返回的 SQ/CQ 设置信息也通过它带返来。
io_uring_setup() 乐成时返回一个文件形貌符(fd)。应用随后可以将这个 fd 传给 mmap(2) 体系调用,来 map the submission and completion queues 大概传给 to the io_uring_register() or io_uring_enter() system calls.
2.5.2 io_uring_register()

注册用于异步 I/O 的文件或用户缓冲区(files or user buffers):
  1. int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
复制代码
注册文件或用户缓冲区,使内核能长时间持有对该文件在内核内部的数据布局引用(internal kernel data structures associated with the files), 或创建应用内存的长期映射(long term mappings of application memory associated with the buffers), 这个利用只会在注册时实验一次,而不是每个 I/O 哀求都会处理惩罚,因此淘汰了 per-I/O overhead。
注册的缓冲区(buffer)性子


  • Registered buffers 将会被锁定在内存中(be locked in memory),并计入用户的 RLIMIT_MEMLOCK 资源限定。
  • 别的,每个 buffer 有 1GB 的巨细限定
  • 当前,buffers 必须是匿名、非文件后端的内存(anonymous, non-file-backed memory),比方 malloc(3) or mmap(2) with the MAP_ANONYMOUS flag set 返回的内存。
  • Huge pages 也是支持的。整个 huge page 都会被 pin 到内核,纵然只用到了此中一部门。
  • 已经注册的 buffer 无法调解巨细,想调解只能先 unregister,再重新 register 一个新的。
通过 eventfd() 订阅 completion 变乱
可以用 eventfd(2) 订阅 io_uring 实例的 completion events。 将 eventfd 形貌符通过这个体系调用注册就行了。
   The credentials of the running application can be registered with io_uring which returns an id associated with those credentials. Applications wishing to share a ring between separate users/processes can pass in this credential id in the SQE personality field. If set, that particular SQE will be issued with these credentials.
  2.5.3 io_uring_enter()

  1. int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);
复制代码
这个体系调用用于初始化和完成(initiate and complete)I/O,利用共享的 SQ 和 CQ。 单次调用同时实验:

  • 提交新的 I/O 哀求
  • 等候 I/O 完成
参数:

  • fd 是 io_uring_setup() 返回的文件形貌符;
  • to_submit 指定了 SQ 中提交的 I/O 数量;
  • 依据差别模式:

    • 默认模式,如果指定了 min_complete,会等候这个数量标 I/O 变乱完成再返回;
    • 如果 io_uring 是 polling 模式,这个参数体现:

      • 0:要求内核返回当前以及完成的全部 events,无壅闭;
      • 非零:如果有变乱完成,内核仍然立即返回;如果没有完成变乱,内核会 poll,等候指定的次数完成,大概这个进程的时间片用完。


留意:对于 interrupt driven I/O,应用无需进入内核就能查抄 CQ 的 event completions
io_uring_enter() 支持很多利用,包罗:


  • Open, close, and stat files
  • Read and write into multiple buffers or pre-mapped buffers
  • Socket I/O operations
  • Synchronize file state
  • Asynchronously monitor a set of file descriptors
  • Create a timeout linked to a specific operation in the ring
  • Attempt to cancel an operation that is currently in flight
  • Create I/O chains
  • Ordered execution within a chain
  • Parallel execution of multiple chains
当这个体系调用返回时,体现肯定命量标 SEQ 已经被斲丧和提交了,此时可以安全的重用队列中的 SEQ。 此时 IO 提交有大概还停顿在异步上下文中,即现实上 SQE 大概还没有被提交 —— 不外 用户不消关心这些细节 —— 当随后内核必要利用某个特定的 SQE 时,它已经复制了一份。
2.6 高级特性

io_uring 提供了一些用于特殊场景的高级特性:

  • File registration(文件注册):每次发起一个指定文件形貌的操 作,内核都必要耗费一些时钟周期(cycles)将文件形貌符映射到内部体现。 对于那些针对同一文件举行重复利用的场景,io_uring 支持提前注册这些文件,背面直接查找就行了。
  • Buffer registration(缓冲区注册):与 file registration 类 似,direct I/O 场景中,内核必要 map/unmap memory areas。io_uring 支持提前 注册这些缓冲区(buffers)。
  • Poll ring(轮询环形缓冲区):对于非常快是装备,处理惩罚停止的开 销是比力大的。io_uring 答应用户关闭停止,利用轮询模式。前面“三种工作模式”末节 也先容到了这一点。
  • Linked operations(链接利用):答应用户发送串联的哀求。这两 个哀求同时提交,但背面的会等前面的处理惩罚完才开始实验。
2.7 用户空间库 liburing

liburing 提供了一个简朴的高层 API, 可用于一些根本场景,应用步伐克制了直接利用更底层的体系调用。 别的,这个 API 还克制了一些重复利用的代码,如设置 io_uring 实例。
举个例子,在 io_uring_setup() 的 manpage 形貌中,调用这个体系调用得到一个 ring 文 件形貌符之后,应用必须调用 mmap() 来如许的逻辑必要一段略长的代码,而用 liburing 的话,下面的函数已经将上述流程封装好了:
  1. int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);
复制代码
下一节来看两个例子基于 liburing 的例子。
3 基于 liburing 的示例应用

编译:
  1. $ git clone https://github.com/axboe/liburing.git
  2. $ git co -b liburing-2.0 tags/liburing-2.0
  3. $ cd liburing
  4. $ ls examples/
  5. io_uring-cp  io_uring-cp.c  io_uring-test  io_uring-test.c  link-cp  link-cp.c  Makefile  ucontext-cp  ucontext-cp.c
  6. $ make -j4
  7. $ ./examples/io_uring-test <file>
  8. Submitted=4, completed=4, bytes=16384
  9. $ ./examples/link-cp <in-file> <out-file>
复制代码
3.1 io_uring-test

这个步伐利用 4 个 SQE,从输入文件中读取最多 16KB 数据
源码及解释

为方便看清告急逻辑,忽略了一些错误处理惩罚代码,完备代码见 io_uring-test.c。
  1. /* SPDX-License-Identifier: MIT */
  2. /*
  3. * Simple app that demonstrates how to setup an io_uring interface,
  4. * submit and complete IO against it, and then tear it down.
  5. *
  6. * gcc -Wall -O2 -D_GNU_SOURCE -o io_uring-test io_uring-test.c -luring
  7. */
  8. #include "liburing.h"
  9. #define QD    4 // io_uring 队列长度
  10. int main(int argc, char *argv[]) {
  11.     int i, fd, pending, done;
  12.     void *buf;
  13.     // 1. 初始化一个 io_uring 实例
  14.     struct io_uring ring;
  15.     ret = io_uring_queue_init(QD,    // 队列长度
  16.                               &ring, // io_uring 实例
  17.                               0);    // flags,0 表示默认配置,例如使用中断驱动模式
  18.     // 2. 打开输入文件,注意这里指定了 O_DIRECT flag,内核轮询模式需要这个 flag,见前面介绍
  19.     fd = open(argv[1], O_RDONLY | O_DIRECT);
  20.     struct stat sb;
  21.     fstat(fd, &sb); // 获取文件信息,例如文件长度,后面会用到
  22.     // 3. 初始化 4 个读缓冲区
  23.     ssize_t fsize = 0;             // 程序的最大读取长度
  24.     struct iovec *iovecs = calloc(QD, sizeof(struct iovec));
  25.     for (i = 0; i < QD; i++) {
  26.         if (posix_memalign(&buf, 4096, 4096))
  27.             return 1;
  28.         iovecs[i].iov_base = buf;  // 起始地址
  29.         iovecs[i].iov_len = 4096;  // 缓冲区大小
  30.         fsize += 4096;
  31.     }
  32.     // 4. 依次准备 4 个 SQE 读请求,指定将随后读入的数据写入 iovecs
  33.     struct io_uring_sqe *sqe;
  34.     offset = 0;
  35.     i = 0;
  36.     do {
  37.         sqe = io_uring_get_sqe(&ring);  // 获取可用 SQE
  38.         io_uring_prep_readv(sqe,        // 用这个 SQE 准备一个待提交的 read 操作
  39.                             fd,         // 从 fd 打开的文件中读取数据
  40.                             &iovecs[i], // iovec 地址,读到的数据写入 iovec 缓冲区
  41.                             1,          // iovec 数量
  42.                             offset);    // 读取操作的起始地址偏移量
  43.         offset += iovecs[i].iov_len;    // 更新偏移量,下次使用
  44.         i++;
  45.         if (offset > sb.st_size)        // 如果超出了文件大小,停止准备后面的 SQE
  46.             break;
  47.     } while (1);
  48.     // 5. 提交 SQE 读请求
  49.     ret = io_uring_submit(&ring);       // 4 个 SQE 一次提交,返回提交成功的 SQE 数量
  50.     if (ret < 0) {
  51.         fprintf(stderr, "io_uring_submit: %s\n", strerror(-ret));
  52.         return 1;
  53.     } else if (ret != i) {
  54.         fprintf(stderr, "io_uring_submit submitted less %d\n", ret);
  55.         return 1;
  56.     }
  57.     // 6. 等待读请求完成(CQE)
  58.     struct io_uring_cqe *cqe;
  59.     done = 0;
  60.     pending = ret;
  61.     fsize = 0;
  62.     for (i = 0; i < pending; i++) {
  63.         io_uring_wait_cqe(&ring, &cqe);  // 等待系统返回一个读完成事件
  64.         done++;
  65.         if (cqe->res != 4096 && cqe->res + fsize != sb.st_size) {
  66.             fprintf(stderr, "ret=%d, wanted 4096\n", cqe->res);
  67.         }
  68.         fsize += cqe->res;
  69.         io_uring_cqe_seen(&ring, cqe);   // 更新 io_uring 实例的完成队列
  70.     }
  71.     // 7. 打印统计信息
  72.     printf("Submitted=%d, completed=%d, bytes=%lu\n", pending, done, (unsigned long) fsize);
  73.     // 8. 清理工作
  74.     close(fd);
  75.     io_uring_queue_exit(&ring);
  76.     return 0;
  77. }
复制代码
其他阐明

代码中已经添加了解释,这里再表明几点:


  • 每个 SQE 都实验一个 allocated buffer,后者是用 iovec 布局形貌的;
  • 第 3 & 4 步:初始化全部 SQE,用于接下来的 IORING_OP_READV 利用,后者 提供了 readv(2) 体系调用的异步接口
  • 利用完成之后,这个 SQE iovec buffer 中存放的是相干 readv 利用的效果;
  • 接下来调用 io_uring_wait_cqe() 来 reap CQE,并通过 cqe->res 字段验证读取的字节数;
  • io_uring_cqe_seen() 关照内核这个 CQE 已经被斲丧了。
3.2 link-cp

link-cp 利用 io_uring 高级特性 SQE chaining 特性来复制文件。
I/O chain

io_uring 支持创建 I/O chain。一个 chain 内的 I/O 是序次实验的,多个 I/O chain 可以并行实验。
io_uring_enter() manpage 中对 IOSQE_IO_LINK 有 详细表明:
   When this flag is specified, it forms a link with the next SQE in the submission ring. That next SQE will not be started before this one completes. This, in effect, forms a chain of SQEs, which can be arbitrarily long. The tail of the chain is denoted by the first SQE that does not have this flag set. This flag has no effect on previous SQE submissions, nor does it impact SQEs that are outside of the chain tail. This means that multiple chains can be executing in parallel, or chains and individual SQEs. Only members inside the chain are serialized. A chain of SQEs will be broken, if any request in that chain ends in error. io_uring considers any unexpected result an error. This means that, eg, a short read will also terminate the remainder of the chain. If a chain of SQE links is broken, the remaining unstarted part of the chain will be terminated and completed with -ECANCELED as the error code. Available since 5.3.
  为实现复制文件功能,link-cp 创建一个长度为 2 的 SQE chain。


  • 第一个 SQE 是一个读哀求,将数据从输入文件读到 buffer;
  • 第二个哀求,与第一个哀求是 linked,是一个写哀求,将数据从 buffer 写入输出文件。
源码及解释

  1. /* SPDX-License-Identifier: MIT */
  2. /*
  3. * Very basic proof-of-concept for doing a copy with linked SQEs. Needs a
  4. * bit of error handling and short read love.
  5. */
  6. #include "liburing.h"
  7. #define QD    64         // io_uring 队列长度
  8. #define BS    (32*1024)
  9. struct io_data {
  10.     size_t offset;
  11.     int index;
  12.     struct iovec iov;
  13. };
  14. static int infd, outfd;
  15. static unsigned inflight;
  16. // 创建一个 read->write SQE chain
  17. static void queue_rw_pair(struct io_uring *ring, off_t size, off_t offset) {
  18.     struct io_uring_sqe *sqe;
  19.     struct io_data *data;
  20.     void *ptr;
  21.     ptr = malloc(size + sizeof(*data));
  22.     data = ptr + size;
  23.     data->index = 0;
  24.     data->offset = offset;
  25.     data->iov.iov_base = ptr;
  26.     data->iov.iov_len = size;
  27.     sqe = io_uring_get_sqe(ring);                            // 获取可用 SQE
  28.     io_uring_prep_readv(sqe, infd, &data->iov, 1, offset);   // 准备 read 请求
  29.     sqe->flags |= IOSQE_IO_LINK;                             // 设置为 LINK 模式
  30.     io_uring_sqe_set_data(sqe, data);                        // 设置 data
  31.     sqe = io_uring_get_sqe(ring);                            // 获取另一个可用 SQE
  32.     io_uring_prep_writev(sqe, outfd, &data->iov, 1, offset); // 准备 write 请求
  33.     io_uring_sqe_set_data(sqe, data);                        // 设置 data
  34. }
  35. // 处理完成(completion)事件:释放 SQE 的内存缓冲区,通知内核已经消费了 CQE。
  36. static int handle_cqe(struct io_uring *ring, struct io_uring_cqe *cqe) {
  37.     struct io_data *data = io_uring_cqe_get_data(cqe);       // 获取 CQE
  38.     data->index++;
  39.     if (cqe->res < 0) {
  40.         if (cqe->res == -ECANCELED) {
  41.             queue_rw_pair(ring, BS, data->offset);
  42.             inflight += 2;
  43.         } else {
  44.             printf("cqe error: %s\n", strerror(cqe->res));
  45.             ret = 1;
  46.         }
  47.     }
  48.     if (data->index == 2) {        // read->write chain 完成,释放缓冲区内存
  49.         void *ptr = (void *) data - data->iov.iov_len;
  50.         free(ptr);
  51.     }
  52.     io_uring_cqe_seen(ring, cqe);  // 通知内核已经消费了 CQE 事件
  53.     return ret;
  54. }
  55. static int copy_file(struct io_uring *ring, off_t insize) {
  56.     struct io_uring_cqe *cqe;
  57.     size_t this_size;
  58.     off_t offset;
  59.     offset = 0;
  60.     while (insize) {                      // 数据还没处理完
  61.         int has_inflight = inflight;      // 当前正在进行中的 SQE 数量
  62.         int depth;  // SQE 阈值,当前进行中的 SQE 数量(inflight)超过这个值之后,需要阻塞等待 CQE 完成
  63.         while (insize && inflight < QD) { // 数据还没处理完,io_uring 队列也还没用完
  64.             this_size = BS;
  65.             if (this_size > insize)       // 最后一段数据不足 BS 大小
  66.                 this_size = insize;
  67.             queue_rw_pair(ring, this_size, offset); // 创建一个 read->write chain,占用两个 SQE
  68.             offset += this_size;
  69.             insize -= this_size;
  70.             inflight += 2;                // 正在进行中的 SQE 数量 +2
  71.         }
  72.         if (has_inflight != inflight)     // 如果有新创建的 SQE,
  73.             io_uring_submit(ring);        // 就提交给内核
  74.         if (insize)                       // 如果还有 data 等待处理,
  75.             depth = QD;                   // 阈值设置 SQ 的队列长度,即 SQ 队列用完才开始阻塞等待 CQE;
  76.         else                              // data 处理已经全部提交,
  77.             depth = 1;                    // 阈值设置为 1,即只要还有 SQE 未完成,就阻塞等待 CQE
  78.         // 下面这个 while 只有 SQ 队列用完或 data 全部提交之后才会执行到
  79.         while (inflight >= depth) {       // 如果所有 SQE 都已经用完,或者所有 data read->write 请求都已经提交
  80.             io_uring_wait_cqe(ring, &cqe);// 等待内核 completion 事件
  81.             handle_cqe(ring, cqe);        // 处理 completion 事件:释放 SQE 内存缓冲区,通知内核 CQE 已消费
  82.             inflight--;                   // 正在进行中的 SQE 数量 -1
  83.         }
  84.     }
  85.     return 0;
  86. }
  87. static int setup_context(unsigned entries, struct io_uring *ring) {
  88.     io_uring_queue_init(entries, ring, 0);
  89.     return 0;
  90. }
  91. static int get_file_size(int fd, off_t *size) {
  92.     struct stat st;
  93.     if (fstat(fd, &st) < 0)
  94.         return -1;
  95.     if (S_ISREG(st.st_mode)) {
  96.         *size = st.st_size;
  97.         return 0;
  98.     } else if (S_ISBLK(st.st_mode)) {
  99.         unsigned long long bytes;
  100.         if (ioctl(fd, BLKGETSIZE64, &bytes) != 0)
  101.             return -1;
  102.         *size = bytes;
  103.         return 0;
  104.     }
  105.     return -1;
  106. }
  107. int main(int argc, char *argv[]) {
  108.     struct io_uring ring;
  109.     off_t insize;
  110.     int ret;
  111.     infd = open(argv[1], O_RDONLY);
  112.     outfd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
  113.     if (setup_context(QD, &ring))
  114.         return 1;
  115.     if (get_file_size(infd, &insize))
  116.         return 1;
  117.     ret = copy_file(&ring, insize);
  118.     close(infd);
  119.     close(outfd);
  120.     io_uring_queue_exit(&ring);
  121.     return ret;
  122. }
复制代码
其他阐明

代码中实现了三个函数:

  • copy_file():高层复制循环逻辑;它会调用 queue_rw_pair(ring, this_size, offset) 来构造 SQE pair; 并通过一次 io_uring_submit() 调用将全部构建的 SQE pair 提交。
    这个函数维护了一个最大 DQ 数量标 inflight SQE,只要数据 copy 还在举行中;否则,即数据已经全部读取完成,就开始等候和收割全部的 CQE。
  • queue_rw_pair() 构造一个 read-write SQE pair.
    read SQE 的 IOSQE_IO_LINK flag 体现开始一个 chain,write SQE 不消设置这个 flag,标记取这个 chain 的竣事。 用户 data 字段设置为同一个 data 形貌符,而且在随后的 completion 处理惩罚中会用到。
  • handle_cqe() 从 CQE 中提取之前由 queue_rw_pair() 生存的 data 形貌符,并在形貌符中记录处理惩罚希望(index)。
    如果之前哀求被取消,它还会重新提交 read-write pair。
    一个 CQE pair 的两个 member 都处理惩罚完成之后(index==2),开释共享的 data descriptor。 末了关照内核这个 CQE 已经被斲丧。
4 io_uring 性能压测(基于 fio)

对于已经在利用 linux-aio 的应用,比方 ScyllaDB, 不要渴望换成 io_uring 之后能得到大幅的性能提升,这是由于: io_uring 性能相干的底层机制与 linux-aio 并无本质差别(都是异步提交,轮询效果)。
在此,本文也渴望使读者明白:io_uring 起首和最告急的贡献在于: 将 linux-aio 的全部良好特性带给了普罗大众(而非范围于数据库如许的细分范畴)。
4.1 测试环境

本节利用 fio 测试 4 种模式:

  • synchronous reads
  • posix-aio (implemented as a thread pool)
  • linux-aio
  • io_uring
硬件:


  • NVMe 存储装备,物理极限能打到 3.5M IOPS
  • 8 核处理惩罚器
4.2 场景一:direct I/O 1KB 随机读(绕过 page cache)

第一组测试中,我们渴望全部的读哀求都能掷中存储装备(all reads to hit the storage),完全绕开利用体系的页缓存(page cache)。
测试设置:


  • 8 个 CPU 实验 72 fio job,
  • 每个 job 随机读取 4 个文件,
  • iodepth=8(number of I/O units to keep in flight against the file.)。
这种设置包管了 CPU 处于饱和状态,便于观察 I/O 性能。 如果 CPU 数量充足多,那每组测试都大概会打满装备带宽,效果对 I/O 压测就没意义了。
表 1. Direct I/O(绕过体系页缓存):1KB 随机读,CPU 100% 下的 I/O 性能
backendIOPScontext switchesIOPS ±% vs io_uring
sync814,00027,625,004-42.6%
posix-aio (thread pool)433,00064,112,335-69.4%
linux-aio1,322,00010,114,149-6.7%
io_uring (basic)1,417,00011,309,574
io_uring (enhanced)1,486,00011,483,4684.9%
 

几点分析:

  • io_uring 相比 linux-aio 确实有肯定提升,但并非革命性的。
  • 开启高级特性,比方 buffer & file registration 之后性能有进一步提升 —— 但也还 没有到为了这些性能而重写整个应用的田地,除非你是搞数据库研发,想压迫硬件的末了一分性能。
  • io_uring and linux-aio 都比同步 read 接口快 2 倍,而后者又比 posix-aio 快 2 倍 —— 初看有点差别。但看看上下文切换次数,就不难明白为什么 posix-aio 这么慢了。

    • 同步 read 性能差是由于:在这种没有 page cache 的环境下, 每次 read 体系调用都会壅闭,因此就会涉及一次上下文切换
    • posix-aio 性能更差是由于:不光内核和应用步伐之间要频仍上下文切换,线程池的多个线程之间也在频仍切换

4.2 场景二:buffered I/O 1KB 随机读(数据提前加载到内存,100% hot cache)

第二组测试 buffered I/O:

  • 将文件数据提前加载到内存,然后再测随机读。

    • 由于数据全部在 page cache,因此同步 read 永久不会壅闭
    • 这种场景下,我们预期同步读和 io_uring 的性能差距不大(都是最好的)

  • 其他测试条件稳定。
表 2. Buffered I/O(数据全部来自 page cache,100% hot cache):1KB 随机读,100% CPU 下的 I/O 性能
BackendIOPScontext switchesIOPS ±% vs io_uring
sync4,906,000 105,797-2.3%
posix-aio (thread pool)1,070,000114,791,187-78.7%
linux-aio4,127,000105,052-17.9%
io_uring5,024,000106,683
 

效果分析:

  • 同步读和 io_uring 性能差距确实很小,二者都是最好的。
    但留意,现实的应用不大概不停 100% 时间实验 IO 利用,因此 基于同步读的真实应用性能还是要比基于 io_uring 要差的,由于 io_uring 会将多个体系调用批处理惩罚化。
  • posix-aio 性能最差,直接缘故起因是上下文切换次数太多,这也和场景相干: 在这种 CPU 饱和的环境下,它的线程池反而是累赘,会完全拖慢性能。
  • linux-aio 并不是针对 buffered I/O 计划的,在这种 page cache 直接返回的场景, 它的异步接口反而会造成性能丧失 —— 将利用分 为 dispatch 和 consume 两步不光没有性能收益,反而有额外开销。
4.3 性能测试小结

末了再次提示,本节是极度应用/场景(100% CPU + 100% cache miss/hit)测试, 真实应用的举动通常处于同步读和异步读之间:时而一些壅闭利用,时而一些非壅闭利用。 但不管怎么说,用了 io_uring 之后,用户就无需担心同步和异步各占多少比例了,由于它在任何场景下都体现良好

  • 如果利用黑白壅闭的,io_uring 不会有额外开销;
  • 如果利用是壅闭式的,也没关系,io_uring 是完全异步的,而且不依赖线程池或昂贵的上下文切换来实现这种异步本事;
本文测试的都是随机读,但对其他范例的利用,io_uring 体现也黑白常良好的。比方:

  • 打开/关闭文件
  • 设置定时器
  • 通过 network sockets 传输数据
而且利用的是同一套 io_uring 接口
4.4 ScyllaDB 与 io_uring

Scylla 重度依赖 direct I/O,从一开始就利用 linux-aio。 在我们转向 io_uring 的过程中,最开始测试体现对某些 workloads,能取得 50% 以上的性能提升。 但深入研究之后发现,这是由于我们之前的 linux-aio 用的不敷好。 这也展现了一个常常被忽视的究竟:得到高性能没有那么难(条件是你得弄对了)。 在对比 io_uring 和 linux-aio 应用之后,我们很快改进了一版,二者的性能差距就消散了。 但坦白地说,办理这个题目必要一些工作量,由于要改动一个已经利用 了很多年的基于 linux-aio 的接口。而对 io_uring 应用来说,做类似的改动是轻而 易举的。
以上只是一个场景,io_uring 相比 linux-aio 的上风是能应用于 file I/O 之外的场景。 别的,它还自带了特殊的 高性能 接口,比方 buffer registration、file registration、轮询模式等等。
启用 io_uring 高级特性之后,我们看到性能确实有提升:Intel Optane 装备,单个 CPU 读取 512 字节,观察到 5% 的性能提升。与 表 1 & 2 对得上。固然 5% 的提升 看上去不是太大,但对于渴望压榨出硬件全部性能的数据库来说,还是非常宝贵的。
linux-aio: Throughput : 330 MB/s
Lat average : 1549 usec
Lat quantile= 0.5 : 1547 usec
Lat quantile= 0.95 : 1694 usec
Lat quantile= 0.99 : 1703 usec
Lat quantile=0.999 : 1950 usec
Lat max : 2177 usec
io_uring, with buffer and file registration and poll: Throughput : 346 MB/s
Lat average : 1470 usec
Lat quantile= 0.5 : 1468 usec
Lat quantile= 0.95 : 1558 usec
Lat quantile= 0.99 : 1613 usec
Lat quantile=0.999 : 1674 usec
Lat max : 1829 usec
利用 1 个 CPU 从 Intel Optane 装备读取 512 字节。1000 并发哀求。linux-aio 和 io_uring basic interface 性能差别很小。 但启用 io_uring 高级特性后,有 5% 的性能差距。
5 eBPF

eBPF 也是一个变乱驱动框架(因此也是异步的),答应用户空间步伐动态向内核注入字节码,告急有两个利用场景:

  • Networking:本站 已经有相称多的文章
  • Tracing & Observability:比方 bcc 等工具
eBPF 在内核 4.9 初次引入,4.19 以后功能已经很强盛。更多关于 eBPF 的演进信息,可参考: (译)大规模微服务利器:eBPF + Kubernetes(KubeCon, 2020)。
谈到与 io_uring 的团结,就是用 bcc 之类的工具跟踪一些 I/O 相干的内核函数,比方:

  • Trace how much time an application spends sleeping, and what led to those sleeps. (wakeuptime)
  • Find all programs in the system that reached a particular place in the code (trace)
  • Analyze network TCP throughput aggregated by subnet (tcpsubnet)
  • Measure how much time the kernel spent processing softirqs (softirqs)
  • Capture information about all short-lived files, where they come from, and for how long they were opened (filelife)
6 竣事语

io_uring 和 eBPF 这两大特性将给 Linux 编程带来革命性的厘革。 有了这两个特性的加持,开发者就能更充实地利用 Amazon i3en meganode systems 之类的多核/多处理惩罚器体系,以及 Intel Optane 长期存储 之类的 us 级耽误存储装备。
参考资料



  • Efficient IO with io_uring, pdf
  • Ringing in a new asynchronous I/O API, lwn.net
  • The rapid growth of io_uring, lwn.net
  • System call API, manpage
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表