io_uring 是 2019 年做的,与 kernel 5.1 发布。后续打了很多补丁,比较重要的在 2020年5月之后基本达到了一个很好的可用性(为什么这么说详情见下面的分析)。学习这个东西和学习 epoll 系列的、学习 UNP 和 linux 网络编程其实没有什么区别,都是利用已有的工具解决问题,其实不涉及像学 OS、DBMS、分布式系统等这种系统方向技术上的东西,但是有一些惯常做法还是需要记录一下,如何利用工具,以及在工具的限制下解决问题也是一门形式化技术。本文只是提供一个使用 io_uring 的整体的思路地图,方便有兴趣的人可以快速上手,但是实际新的内核技术投入生产环境使用也不是很靠谱的,io_uring 要上生产环境,可能也只是魔改一些特性移植到旧版稳定内核(可能基于 3.x 或者 4.x,比如 Alibaba Cloud Linux 2 在 linux-4.19-LTS 的基础上支持了 io_uring 特性)中。本文标题是地图,是因为没有办法再一个文章里面把 io_uring 给解释清楚,也没必要做一个系列文章,因为网上已经有一些很好的资料,只不过有一些可以补充和总结的地方,本文基于我自己写 io_uring 程序的时候遇到的一些点记录一下就当是一个地图,目录。内容可能有误,我发现了会及时订正。(其实主要是给我自己按图索骥用的)
本文主要给出的参考的资料来源包括中文网路上的各种搞存储搞网络的大佬写的分析文章;kernel 开发的邮件列表; LWN.net;作者 axboe liburing 的github issue 区;Shuveb Hussain 建的整个 loti 网站分区; 以及英文网路上的各种大神资料,各种开源 issue 区、公开 ppt 、技术文章等。
主要内容分别是:前情提要,基本用法,容易有疑问的点,高性能用法,容易有疑问的点。
本文相关只关心 io_uring 怎么用,但是 io_uring 的一些 user space 的用法,性能问题确实和他内部的实现有关,他的内部实现涉及到很多,包括内核线程等,感兴趣的同学,可以从 mailing list 入手。
io_uring 按时间排序的列表 虽然从 2019 开始以来,已经有很多 patch 了,基本是看不完的了,但是 pdf 读完,然后看懂一开始的,之后配合源代码基本能了解个 77 88 了,然后你也可以参与到 liburing 仓库的讨论(实际是看,很多疑问那里都有回答)去了。。。
具体要看的代码文件如下:
fs/io-wq.c fs/io-wq.h fs/io_uring.c include/linux/io_uring.h include/uapi/linux/io_uring.h tools/io_uring/
wq 是 workqueue 的意思。总之,io_uring 就这些内容,当然还有和其他模块相关联的,但是那些属于是必修。
对于他 kernel 里面的 work queue 这个,这里有一篇文章讲解了这个点:https://blog.cloudflare.com/missing-manuals-io_uring-worker-pool/
IORING_OP_XXX
的系统调用号,因为终端的 man 里面搜索不方便,给个网页的跳转链接(最新的 manual 得看最新的 manual,liburing 最新的 manual 得去看 github 里面的):io_uring_enter(2) 。IO_SQE_XXX
的 flags,比如 LINK, io_uring_enter(2) 。IORING_OP_PROVIDE_BUFFERS
的系统调用号。file->f_op
里面有一个表),中断和 NAPI,NAPI 解决的高速网络中的 receive livelock 问题,主要是作为 polled io 的 preliminary。fd_set
是 long 数组,poll 使用 struct array poll fd
, epoll 用 epoll events array
)。io_uring_smp_load_acquire
来获取队列的头尾就很容易理解了,即 pdf 中的 memory barrier 部分)。用两个队列就能完成 io_uring 需要完成的需求。read->pread->readv->preadv->preadv2...
)。当然也介绍了如何使用 io_uring,但是实际并不会直接使用生接口,因为他需要做的事情以及参数的共用太复杂,作者 axboe 已经提供了一个 liburing 配备 manual 供用户直接使用。这个 pdf 还介绍了 SQPOLL 和 POLLED IO,这两个是高性能的很重要的要点。SQPOLL 做的是让内核开线程轮询 Submission Queue。POLLED IO 是类似 NAPI 的做法,提供直接对 io polling 的用法,我不打算涉及这个 POLLED IO 的点。第二个 pdf 介绍了可用的支持点,比如 CANCEL
、eventfd。首先是情提要,为了方便能快速理解下面说的 tutorial 里面的代码。
readv
来写完全是为了顺便教学 readv
,我感觉是没有什么性能的好处的,然后他的 cat 写的时候投递了一大堆请求。readv
、writev
没关系。。总之这三段要说的就是,下面的链接里面的代码还有改进空间。-errno
),只不过他不会阻塞用户程序而已,实际 fd 应该设置为 blocking 状态,否则会引发频繁的 EAGAIN
的 errno,实际 io_uring 对 non-blocking 做了一定的检测和优化,可以在一系列的 patch 里看到,但是读者总是可以自己试一下 non-blocking fd 的读写情况是否有 short count 和频繁 completion 触发。EOF
了才触发。tutorial :这个 tutorial 完整讲解了 io_uring 的建立和提交 IO 请求,这些都使用 liburing 进行即可,包括如何准备请求(prep_xxx
设置好一个 sqe),如何向内核告知有新的请求(也可以注册了 SQPOLL 让内核自己 polling),阻塞等待完成… 只需要把:里面的 cat 和 cp、webserver 的 tutorial 都看完,就已经熟悉了基本的使用了。当然,就像上面说的,这些 demo 都不是很完美的代码。
然后总结一下一些上面教程提到的和没提到的地方,不过没提到的实际都是 manual 里写了的,这里当备注一下,结束这里之后 io_uring 的基本用法就结束了。
首先是 io_uring 性能好的地方,一个是无锁队列,一个是 mmap 无需复制 fd_set,在一个就是减少系统调用开销,准备 100 个 read,然后一次 submit call 就结束(a.k.a. 批量提交),对于支持 await async 语义的无栈协程来说,每次从系统调用醒来的时候都可能会投递一大堆的 IO 请求,然后只需要一次 submit,属实是赢麻了,不过特定引用实测的性能数据可能不是很好看,比如每一轮只投递一次请求的话实际系统调用开销相同。这个涉及场景是不是真的有一个 amortize 以及一些高级特性没用上的原因。就是还得看实测数据而不是纸面性能。
通过 IORING_FEAT_XXX
这个在 io_uring->feature
中的 bitmask 来进行功能、特性的 probing。
要进行 read 和 write 的 fd 使用 blocking io(你当然可以使用 non-blocking,其行为请自己确定)。而且你需要处理 short count 问题。
io_uring 的 cqe->res 是返回值,他是一个 int。ssize_t
是一个 long long
,这里可能会有一些问题。但是 io_uring 保证 IO 都会clamped at 2G in Linux: 资料。系统调用的行为和直接在 fd 上进行系统调用一致,其返回值总是和他们一样。唯一的不同是错误被扩展了,从 cqe->res
的返回值能够直接知道错误,不用查询 errno
,或者说,不能查询 errno
因为此时已经有很多 completion 被投递了,你无法通过 errno 得知一个 completion 的错误情况,例子采用 strerror(abs(cqe->res))
这种方式获得一个正确的 errno
,即,出错 res
总是返回 -errno
。
CQE 不会保证遵守其 SQE 提交的顺序提交,如果有需要,应当去使用 IO_SQE_IO_LINK flag
提交一系列的连续请求,可以看这个教程:Linking Request, 我这里就备注一下,每个设置了 IO_SQE_IO_LINK flag
的 SQE,他将会把他的下一个请求(不用设置 IO_SQE_IO_LINK
的一个 SQE)连接起来。
通过 SQE 的 user data 字段(void*
)进行注册一个 asynchronous completion token(POSAvol2 的术语,这次 io_uring、boost.asio 在 linux 下用 epoll 模拟、以及 windows 的 IOCompletion Port 都是采用这种模式,但是我觉得设计模式是管理学科),从而能够区分是什么 CQE。用 liburing 的 API 就行了。这里有一个需要注意的坑,见下面 timeout 部分。
有两种方案去进行 CQE 的获取,一个是你可以一直轮询这个 mmap 的并且在 kernel 和 user space 都有 能访问的无锁队列,反正读者确实是不需要任何锁机制的,memory barrier 的花销都没有。这种方式就类似于启用了 SQPOLL 的内核,如果你在开发基于 io_uring 的网络框架,那么你应该学 kernel 的做法,poll 队列 poll 了一会儿如果还是空的,就显式调用一个 liburing 的 wait_cqe 系列的函数阻塞,避免浪费太多 CPU。
多线程用法,回想 nginx 中 master process 只做进程的管理,启动时,master 进程向 IOCP 投递一堆 AcceptEx
请求,然后启动(fork)一堆 worker 进程。之后 IOCP 实际工作在多线程/进程下(操作系统提供多线程支持)。虽然 io_uring 无法做到(spsc lock-free queue 的要求,除非全局加锁),但是我们还是有发挥的空间,可以把 SQE 的投递和 CQE 的处理分开不同的线程。对于数据库应用,这种用法很可用,CQE 接收到之后处理的基本是完成的事件,对于需要进行连续的读写保证原子有序的操作,IO_LINK
也提供了帮助,所以 SQE 的产生和 CQE 的接受分开线程是可以接受的。对于网络应用, 用户的 callback 很有可能有裂变的操作,因此 CQE 的处理中常常会投递新的 SQE,因此这个情况,单个 io_uring 实例要跨线程使用,应当要加锁。(参考 Axboe 的说法)
提供了 io_op_noop
来直接触发一个 completion, 实际这个是没有意义的因为本身队列是无锁无阻塞可见的,所以没有 IOCP 那种投递一个唤醒线程的意义,在 nginx 对 Windows 下的 IOCP 的封装里我们明白,IOCP 的 WaitForCompletion
是可以多线程使用和投递的,io_uring 也可以实现同样的事情,但是问题就在于,IOCP 并不是用无锁队列的,io_uring 使用了无锁队列,引发的一个小问题就是跨线程使用同一个 ring 必须要加锁,所以尽量使用单线程来投递(参考 Axboe 的说法),因为我们已经能够减少 blockin IO 了,因此其他事情不会阻塞太多 CPU,公平服务不会打断(compute bound 的服务应当使用 thread pool 调度,因此可以基于运行 io_uring_wait_cqe
或者 eventfd 编写线程池)。
对于 POLLED IO,实际就是 all the way down 之后驱动的汇编层面轮询寄存器直到收到一个电平说明读、写完成了,也就是到 CPU 和硬件的时候,是存在 polling loop 的,中断驱动可能就是这样,kernel 像 Windows 那样投递一个请求包,注册中断处理,等硬件/DMA 读写完成了之后中断触发 kernel 进 handler 处理。异步本身就意味着是 interrupt-driven 的。对于低延时的需求,如果上了超快的网卡,有足够的 CPU 资源,启用这个是很好的。另外,快速的设备比如 Nvme SSD 等,polling 可能是比 interrupt 快的(因为 interrupt 意味着提交了之后,要做 context switch 回去做别的事情,然后一旦硬件读写完成,马上又 context switch 进 handler,而直接 polling 可能没到一次 context switch 的开销就结束了)。我暂时不打算涉及他,polled io 对于高速硬盘里面很有用。然后是 receive livelock 的网卡问题我之前也写过一篇笔记,这里不放链接了,对 polled io 是什么有兴趣可以看看这个西部数据的精美 PPT:lemoal-vault-2017-final (linuxfound.org),以及 Intel SPDK(Storage Performance Development Kit)的 polling 驱动设计思路(kernel bypass 也可以看,但是和这里无关了属于),就能明白这里对 POLLED IO 的支持是什么意思了。快速跳转:
SQPOLL 应该会很常用,因为对于高并发的时候,意味着队列大量时间都是满的,应用程序轮询队列,以及内核开一个线程轮询消费队列,从而避免大量的互相知会(kernel->user 通过 wait_cqe
系列的阻塞调用,user -> kernel 通过 submit
系统调用)。快速跳转: Submission Queue Polling — Lord of the io_uring documentation (unixism.net)。
通过 register buffer 来锁定一些常用的 fd、buffer 等,从而能够避免重复的引用获取,感觉是因为 __copy_from_user
这种函数需要进行权限的检查,然后还要复制到对应做 async io 的地方,fd 的话要每次根据 fd(int)
查询 opened files 数组?Registering files or user buffers allows the kernel to take long term references to internal data structure s or create long term mappings of application memory, greatly reducing per-I/O overhead. 所以可以用,但是对于短连接来说确实是没办法加速太多的。register buffer 使得应用程序可以定制自己的 buffer pool,对于数据库应用、用内存做服务文件缓存 bypass kernel (O_DIRECT
)都有意义。快速跳转:Fixed buffers — Lord of the io_uring documentation (unixism.net)
LIBURING_UDATA_TIMEOUT
(目前看应该是 -1,而-1代表的 0xffffffffffffffff
理论上应该不会被用到,但是我们还是要处理他,如果你在 user_data
里面存了一个 callback 指针之类的东西,这样可能会引发潜在的问题(say Segmentation Fault),因为你可能通过 cqe->user_data != nullptr
来判断,但是这个对于调用了有事件请求的阻塞的时候,就会出问题) 的 user_data (void*/uint64
) 的 completion token,而且对于多线程(一个线程处理 CQ,一个处理 SQ)的时候,包含 timeout 的函数会出问题,解决办法是 kernel 5.11 以后提供一个IORING_FEAT_EXT_ARG
flag(作为 ring 结构体中的 feature 中的一个 bitmask 存在,这个 feature 字段是用来给应用程序做特性 probing 用的)是否支持的补丁(扩展),然后修改了系统调用的 api,从而是安全的实现了类似 epoll_wait 的行为,5.11 的 manual 也更新了,这个改进第一次出现在 alibaba 邮箱请求的 patch 中,邮件列表这里显示了这个针对不同线程分开 SQ 和 CQ 时的问题:[PATCH v3] io_uring: add timeout support for io_uring_enter() (kernel.org)。所以,最后一开始那个 Feature request 的 issue 中,一开始的 workaround (通过投递一个 timeout)最后还是采用回了原来的方案。由于他没有顺序保证,不建议直接使用 hrtimer 创建一堆定时器,但是没有顺序要求,他这个应该是最快的定时器方案,就是整体会影响定时器的性能,因为 O(lgn) ? 这个得看是不是有成千上万个定时器的需求了。wait_cqe
上),然后要做其他事情的时候开另一个线程了。有了 eventfd,可以阻塞或者非阻塞 read,也可以结合 poll/epoll/select
的 event loop,然后顺便处理一个 io_uring 完成事件。这是正常的因为 io_uring 完成就意味着长时间的 IO 操作完成,此时一般是进行一个收割的行为,对于 Proactor 这种有裂变的,其实也是自然的。不过我只写过直接用 wait_cqe + 轮询,没找到这个的实际用途…POLL_ADD
POLL_REMOVE
EPOLL_CTL
等 OP,因此就可以实现 epoll 和 io_uring 同时工作,而且避免了 poll add poll remove epoll ctl 这些东西的系统调用开销。对于让 kernel 内部直接对 NONBLOCK IO 支持 poll 的行为,在这里有讨论:https://github.com/axboe/liburing/issues/364recvmsg/sendmsg
、设置了 low water mark的 tcp socket 、write
这些系统调用的行为是有最小量的限制的),更加具体的做法我这里不说了,有兴趣可以直接去看源码(…),但是 5.7 之后,axboe 发现其实可以内部也用类似 user space 的 poll/select/epoll 这种来做,从而避免了搞一堆 thread,这个特性标记为 IORING_FEAT_FAST_POLL
。这里开一个小节了讲解这个,因为 Shuveb Hussain 的教程中没有涉及这个。
不过之前对 io_uring is slower than epoll 的 issue 讨论中,用于 bench 的 echo server (最初使用的是 axboe 的示例,因此无法说明是用法不当)后来做了一个用上了 automatic buffer selection 的,得到的结果是终于在某些条件下打赢 epoll 了。
这部分先总结到这里后续等待更新。。。。 有需要的可以暂时先根据 frevib/io_uring-echo-server: io_uring echo server (github.com) 这个里面的用法学习 provide buffer 的实操(注意这个 repository 有 3 个 branch,分别是 master、provide buffer 和 fast poll)。我之后再会回来更新这篇笔记。