Linux 5.1内核AIO 的新归宿:io_uring

640?wx_fmt=jpeg

作者/公司介绍

@panic,SmartX 存储研发工程师。

    SmartX是中国领先的超融合产品与企业云解决方案提供商拥有国内最顶尖的分布式存储和超融合架构研发团队在分布式存储、虚拟化计算、微服务、容器、前端开发、自动化测试等领域都做着行业最前沿的实践。

在正文开始之前,我们先来看一段对话(演绎版本)

Jens Axboe :Linus,我有个好东西,你瞅瞅?
Linus:啥玩意儿,不是已经有 aio 了么,为啥又来一套,你咋不去好好修 aio 的问题。aio 还有 balabala 问题没有修呢。
Jens Axboe :我这个 DIAO 啊,balabala
Linus :看你诚意这么足,那行吧,我先收到我的 tree 下,不 push out 出去,让我测测先。
【不一会儿】
Linus :你这 IO 引用计数写的辣鸡,一看就有问题,去改吧。
……

背景

Linus 和 Jens 在讨论的,就是 Linux Kernel 即将在 5.1 版本加入一个重大 feature:io_uring。

对做存储的来说,这是一个大事情,值得普大喜奔,广而告之。libaio 即将埋入黄土,io_uring 拔地而起。

一句话总结 io_uring 就是:一套全新的 syscall,一套全新的 async API,更高的性能,更好的兼容性,来迎接高 IOPS,高吞吐量的未来。

先看一下性能数据(数据来自 Jens Axboe)。

4k randread,3D Xpoint 盘:

Interface       QD      Polled          Latency         IOPS--------------------------------------------------------------------------io_uring        1       0                9.5usec         77Kio_uring        2       0                8.2usec        183Kio_uring        4       0                8.4usec        383Kio_uring        8       0               13.3usec        449Klibaio          1       0                9.7usec         74Klibaio          2       0                8.5usec        181Klibaio          4       0                8.5usec        373Klibaio          8       0               15.4usec        402Kio_uring        1       1                6.1usec        139Kio_uring        2       1                6.1usec        272Kio_uring        4       1                6.3usec        519Kio_uring        8       1               11.5usec        592Kspdk            1       1                6.1usec        151Kspdk            2       1                6.2usec        293Kspdk            4       1                6.7usec        536Kspdk            8       1               12.6usec        586K

io_uring vs libaio,在非 polling 模式下,io_uring 性能提升不到 10%,好像并没有什么了不起的地方。

然而 io_uring 提供了 polling 模式。在 polling 模式下,io_uring 和 SPDK 的性能非常接近,特别是高 QueueDepth 下,io_uring 有赶超的架势,同时完爆 libaio。

测试 per-core,4k randread 多设备下的最高 IOPS 能力:

Interface       QD      Polled          IOPS--------------------------------------------------------------------------io_uring        128     1               1620Klibaio          128     0                608Kspdk            128     1               1739K

最近几年一直流行 kernel bypass,从网络到存储,各个领域开花,内核在性能方面被各种诟病。io_uring 出现以后,算是扳回一局。

io_uring 有如此出众的性能,主要来源于以下几个方面:

  • 用户态和内核态共享提交队列(submission queue)和完成队列(completion queue)

  • IO 提交和收割可以 offload 给 Kernel,且提交和完成不需要经过系统调用(system call)

  • 支持 Block 层的 Polling 模式

  • 通过提前注册用户态内存地址,减少地址映射的开销

不仅如此,io_uring 还可以完美支持 buffered IO,而 libaio 对于 buffered IO 的支持则一直是被诟病的地方。

io_uring

io_uring 提供了一套新的系统调用,应用程序可以使用两个队列,Submission Queue(SQ) 和 Completion Queue(CQ) 来和 Kernel 进行通信。这种方式类似 RDMA 或者 NVMe 的方式,可以高效处理 IO。

syscall425        io_uring_setup426        io_uring_enter427        io_uring_register

io_uring 准备阶段

io_uring_setup 需要两个参数,entries 和 io_uring_params。

其中 entries,代表 queue depth。

io_uring_params 的定义如下。

struct io_uring_params {	__u32 sq_entries;	__u32 cq_entries;	__u32 flags;	__u32 sq_thread_cpu;	__u32 sq_thread_idle;	__u32 resv[5];	struct io_sqring_offsets sq_off;	struct io_cqring_offsets cq_off;};struct io_sqring_offsets {	__u32 head;	__u32 tail;	__u32 ring_mask;	__u32 ring_entries;	__u32 flags;	__u32 dropped;	__u32 array;	__u32 resv1;	__u64 resv2;};struct io_cqring_offsets {	__u32 head;	__u32 tail;	__u32 ring_mask;	__u32 ring_entries;	__u32 overflow;	__u32 cqes;	__u64 resv[2];};

其中,flags、sq_thread_cpu、sq_thread_idle 属于输入参数,用于定义 io_uring 在内核中的行为。其他参数属于输出参数,由内核负责设置。

在 io_setup 返回的时候,内核已经初始化好了 SQ 和 CQ,此外,还有内核还提供了一个 Submission Queue Entries(SQEs)数组。

640?wx_fmt=png

之所以额外采用了一个数组保存 SQEs,是为了方便通过 RingBuffer 提交内存上不连续的请求。SQ 和 CQ 中每个节点保存的都是 SQEs 数组的偏移量,而不是实际的请求,实际的请求只保存在 SQEs 数组中。这样在提交请求时,就可以批量提交一组 SQEs 上不连续的请求。

但由于 SQ,CQ,SQEs 是在内核中分配的,所以用户态程序并不能直接访问。io_setup 的返回值是一个 fd,应用程序使用这个 fd 进行 mmap,和 kernel 共享一块内存。

这块内存共分为三个区域,分别是 SQ,CQ,SQEs。kernel 返回的 io_sqring_offset 和 io_cqring_offset 分别描述了 SQ 和 CQ 的指针在 mmap 中的 offset。而 SQEs 则直接对应了 mmap 中的 SQEs 区域。

mmap 的时候需要传入 MAP_POPULATE 参数,以防止内存被 page fault。

IO 提交

IO 提交的做法是找到一个空闲的 SQE,根据请求设置 SQE,并将这个 SQE 的索引放到 SQ 中。SQ 是一个典型的 RingBuffer,有 head,tail 两个成员,如果 head == tail,意味着队列为空。SQE 设置完成后,需要修改 SQ 的 tail,以表示向 RingBuffer 中插入一个请求。

当所有请求都加入 SQ 后,就可以使用 :

int io_uring_enter(unsigned int fd, u32 to_submit, u32 min_complete, u32 flags);

来提交 IO 请求。

io_uring_enter 被调用后会陷入到内核,内核将 SQ 中的请求提交给 Block 层。to_submit 表示一次提交多少个 IO。

如果 flags 设置了 IORING_ENTER_GETEVENTS,并且 min_complete > 0,那么这个系统调用会同时处理 IO 收割。这个系统调用会一直 block,直到 min_complete 个 IO 已经完成。

这个流程貌似和 libaio 没有什么区别,IO 提交的过程中依然会产生系统调用。

但 io_uring 的精髓在于,提供了 submission offload 模式,使得提交过程完全不需要进行系统调用。

如果在调用 io_uring_setup 时设置了 IORING_SETUP_SQPOLL 的 flag,内核会额外启动一个内核线程,我们称作 SQ 线程。这个内核线程可以运行在某个指定的 core 上(通过 sq_thread_cpu 配置)。这个内核线程会不停的 Poll SQ,除非在一段时间内没有 Poll 到任何请求(通过 sq_thread_idle 配置),才会被挂起。

640?wx_fmt=png

当程序在用户态设置完 SQE,并通过修改 SQ 的 tail 完成一次插入时,如果此时 SQ 线程处于唤醒状态,那么可以立刻捕获到这次提交,这样就避免了用户程序调用 io_uring_enter 这个系统调用。如果 SQ 线程处于休眠状态,则需要通过调用 io_uring_enter,并使用 IORING_SQ_NEED_WAKEUP 参数,来唤醒 SQ 线程。用户态可以通过 sqring 的 flags 变量获取 SQ 线程的状态。

IO 收割

当 IO 完成时,内核负责将完成 IO 在 SQEs 中的 index 放到 CQ 中。由于 IO 在提交的时候可以顺便返回完成的 IO,所以收割 IO 不需要额外系统调用。这是跟 libaio 比较大的不同,省去了一次系统调用。

如果使用了 IORING_SETUP_SQPOLL 参数,IO 收割也不需要系统调用的参与。由于内核和用户态共享内存,所以收割的时候,用户态遍历 [cring->head, cring->tail) 区间,这是已经完成的 IO 队列,然后找到相应的 CQE 并进行处理,最后移动 head 指针到 tail,IO 收割就到此结束了。

由于提交和收割的时候需要访问共享内存的 head,tail 指针,所以需要使用 rmb/wmb 内存屏障操作确保时序。

所以在最理想的情况下,IO 提交和收割都不需要使用系统调用。

其它高级特性

io_uring 支持还支持以下特性。

IORING_REGISTER_FILES

这个的用途是避免每次 IO 对文件做 fget/fput 操作,当批量 IO 的时候,这组原子操作可以避免掉。

IORING_SETUP_IOPOLL

这个功能让内核采用 Polling 的模式收割 Block 层的请求。当没有使用 SQ 线程时,io_uring_enter 函数会主动的 Poll,以检查提交给 Block 层的请求是否已经完成,而不是挂起,并等待 Block 层完成后再被唤醒。使用 SQ 线程时也是同理。

通过 perf 可以看到,当使用 IOPOLL 时,88% 的 CPU 时间花费在调用 blkdev_iopoll 和 blk_poll 上。

640?wx_fmt=png

IORING_REGISTER_BUFFERS

如果应用提交到内核的虚拟内存地址是固定的,那么可以提前完成虚拟地址到物理 pages 的映射,避免在 IO 路径上进行转换,从而优化性能。用法是,在 setup io_uring 之后,调用 io_uring_register,传递 IORING_REGISTER_BUFFERS 作为 opcode,参数是一个指向 iovec 的数组,表示这些地址需要 map 到内核。在做 IO 的时候,使用带 FIXED 版本的opcode(IORING_OP_READ_FIXED /IORING_OP_WRITE_FIXED)来操作 IO 即可。

内核在处理 IORING_REGISTER_BUFFERS 时,提前使用 get_user_pages 来获得 userspace 虚拟地址对应的物理 pages。在做 IO 的时候,如果提交的虚拟地址曾经被注册过,那么就免去了虚拟地址到 pages 的转换。

下面是两个版本的 perf 数据。

带 fixed buffer:

640?wx_fmt=png

不带 fixed buffer:

640?wx_fmt=png

可以明显看到,提前 map pages,可以减少 iov_iter_get_pages 7% 的 CPU 时间消耗。

关于名字

取名一直是一个老大难的问题,io_uring 这个名字,有点意思,社区有人吐槽,看起来像 io urine(*不是一个很好的词*),太欢乐了。

有人说可以叫做 aio_ring,io_ring,ring_io。

总结

io_uring 的接口虽然简单,但操作起来有些复杂,需要手动 mmap 来映射内存。可以看到,io_uring 是完全为性能而生的新一代 native async IO 模型,比 libaio 高级不少。通过全新的设计,共享内存,IO 过程不需要系统调用,由内核完成 IO 的提交, 以及 IO completion polling 机制,实现了高IOPS,高 Bandwidth。相比 kernel bypass,这种 native 的方式显得友好一些。

当然,不可否认,aio 也在与时俱进。自从 kernel 2.5 进入 upstream 以来,aio 一直都没有实现完整。aio 对 Direct IO 支持的很好,但是其他的 IO 类型支持的不完善。尝试使用其他类型的 IO,例如 buffered IO,可能导致同步的行为。polling 也是一个方向,最近 aio 的 polling 机制已经实现,感兴趣的可以尝试一下。

参考

  1. lore.kernel.org/linux-b

  2. lwn.net/ml/linux-fsdeve

  3. git.kernel.dk/cgit/fio/

  4. lore.kernel.org/linux-b

  5. lwn.net/Articles/743714

  6. io_uring_setup.2\man - liburing - io_uring library


查看我们精华技术文章请移步:

Linux阅码场原创精华文章汇总

扫描下方二维码关注"Linux阅码场"

640?wx_fmt=png

您的耐心阅读,请随手点个“在看”吧~

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值