io_uring 使用教程| io_uring 完全指南 | io_uring 实践指导 | io_uring 资料参考

io_uring 完全指南地图

背景

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/

manual 速点链接

  • 系统调用:IORING_OP_XXX 的系统调用号,因为终端的 man 里面搜索不方便,给个网页的跳转链接(最新的 manual 得看最新的 manual,liburing 最新的 manual 得去看 github 里面的):io_uring_enter(2) 。
  • sqe 的 flagIO_SQE_XXX 的 flags,比如 LINK, io_uring_enter(2) 。
  • register buffer、fd、eventfd,io_uring_register(2)。
  • polled io 和 sq polling , io_uring_setup(2)。
  • provide buffer 的 automatic buffer selection,看对应的 IORING_OP_PROVIDE_BUFFERS 的系统调用号。

I/O 前情提要

  • buffered regular file io 也有 blocking 路径: 如缓冲区分配、DMA 投递、LRU 的页面选择等开销,其中总有一些是 all the way down 到 interrupt-driven driver 的,也就是实际就是 asynchronous 的(就是 ACT 模式(see: POSAvol2),compltion token 就是中断源到达时根据中断编号知道是哪个驱动完成了读写),但是 blocking IO 的模式 block 了用户进程徒增烦恼(所以 Windows 下的 IOCP 确实先进)。
  • DMA 等块读写需要对齐的内存(与复制的硬件支持有关,以及对齐的内存可以省去低位地址)。
  • I/O Completion Ports , Windows 系统下的驱动机制 IRPs 本身就是异步的。
  • socket、regular file 的 poll 接口(UNIX 哲学 struct file 中有类似于虚函数表的东西 file->f_op 里面有一个表),中断和 NAPI,NAPI 解决的高速网络中的 receive livelock 问题,主要是作为 polled io 的 preliminary。

性能点的前情提要

  • context switch 、syscall 的开销特别是 meltdown + spectre 之后引入 KPTI(包括中断)(intel 后来引入有 pid 的 TLB,因此没有 TLB flushes,AMD 不用打 KPTI 补丁,但是 context switch 本身可能涉及流水线炸掉,L1 cache 失效,具体我真的不清楚了,现代 CPU 已经太精细去猜想了,等一个体系结构懂哥)。传统网络编程里多次 read 多次 write 的开销。
  • select, poll, epoll 的用法,原理复杂度,缺点及改进等(select 使用 fd_set 是 long 数组,poll 使用 struct array poll fd, epoll 用 epoll events array)。
  • 内存复制开销,类似 epoll 系统调用里,如 epoll events 参数的结构体内存 user space 到 kernel 之间的开销(本身 kernel 里面是有 user space 的内存映射的)。
  • 单消费者单生产者 wait-free queue。(写过 SPSC lock-free queue 的话,对为什么要 用 io_uring_smp_load_acquire 来获取队列的头尾就很容易理解了,即 pdf 中的 memory barrier 部分)。用两个队列就能完成 io_uring 需要完成的需求。
  • futex (Fast Userspace Mutexes)的轮询思路:futex.c - kernel/futex.c - Linux source code (v4.6) - Bootlin, 即 spinlock(user space) + sleeplock(kernel),以及 nginx 的 ngx_spinlock 等的轮询思路。我之前有一篇文章(笔记) 从自旋锁、睡眠锁、读写锁到 Linux RCU 机制讲解 里面也讲到这个思路,而这个用在任何需要轮询的地方都是很有用的。

基本认识

  • 原理方面的中文网络上已经有一些可读的文章 ,主要是对无锁队列的分析,以及 SQ队列和 CQ 队列为什么要采用不同的设计思路(SQ 多一层间接引用)这里就不放了。
  • io_uring.pdf, io_uring_whatsnew.pdf 这两个 pdf 是了解 io_uring 必读的。第一个 pdf 介绍了为什么要引入 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。
  • Unixism loti 这个是大佬 Shuveb Hussain 总结的一大堆 io_uring 和 liburing 如何使用的网页,里面的有所有基本的用法,但是有一些比较新的内容(比如 [Automatic buffer selection]([Automatic buffer selection for io_uring LWN.net]))没有讲解,下面会给足参考资料和讲解。

基本使用

  • 首先是情提要,为了方便能快速理解下面说的 tutorial 里面的代码。

    • 下面提供的教程链接里面用了 readv 来写完全是为了顺便教学 readv,我感觉是没有什么性能的好处的,然后他的 cat 写的时候投递了一大堆请求。
    • 实际 cat 采用分开的读写还是有一点用的,实际的 cat 源码(cat 源码,建议看 simple cat 部分即可)里面也是限制每次系统调用读写的大小,因为一次读写太多的话,其实和内核的缓存是有关的(主要是文件),所以下面 tutorial 里面的这个源码里面还有改进空间。 cat 每读一个块就进行一次输出,既能避免内存耗尽,也能避免长时间的阻塞,特别是对于有对于 grep 的大文件 cat 请求的话。
    • 对于 cp 来说,如果两个文件所在对应的硬件不同,一个读完成之后是可以马上投递一个写,然后再投递读,此时是可以读写 simutaneously 的,当然这些都和 readvwritev 没关系。。总之这三段要说的就是,下面的链接里面的代码还有改进空间。
    • io_uring 是 async io,其返回值意味着以正常的系统调用进行后的返回值 (也就是 -errno),只不过他不会阻塞用户程序而已,实际 fd 应该设置为 blocking 状态,否则会引发频繁的 EAGAIN 的 errno,实际 io_uring 对 non-blocking 做了一定的检测和优化,可以在一系列的 patch 里看到,但是读者总是可以自己试一下 non-blocking fd 的读写情况是否有 short count 和频繁 completion 触发。
    • blocking socket io (主要是 TCP)的 short count(short read/write) 问题,注意是仍然存在于 io_uring 中的。对于 为什么要允许 short read 和 short write,之前的博客或者网路上都有很多文章探讨了,具体来说,short read (low water mark 相关的再看一下 manual: setsockopt(2) (freebsd.org))保证了连接出问题的时候仍然能够避免阻塞而消费掉接受缓冲区,short write 只有在超出缓冲区限制或者 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)

一些关键的部分(惯常做法)

  • 上面没有提到的一些要点,然而都是用 io_uring 的时候会比较关心的,由于本文是地图,所以肯定要指出。
  • 首先是 sqe 的安全获取,实际因为 sqe 队列可能满了,所以获取 sqe 的时候肯定要考虑返回的 sqe == NULL 的情况,这个时候可以先尝试进行一次 submit 然后再申请新的 sqe,可以参照 liburing 的做法编写一个获取 sqe 的函数,参照这里:https://github.com/axboe/liburing/blob/master/src/queue.c#L263
  • timeout 的部分,liburing 提供了 wait_cqe_timeout ,但是一来是并不支持类似 epoll_wait 的这种,他的实现其实就是通过 prep 一个 IO_OP_TIMEOUT 来做的,可见这里:Feature request: timed waiting support 。这个 timeout 主要是用来唤醒阻塞在 wait 线程的,提供类似 epoll_wait 的功能。实际是内核做了一个 hrtimer(rbtree 实现),可见这里:io_uring: IORING_OP_TIMEOUT support。但是这样会引入一个问题,就是他投递一个 timeout 事件会导致某个 completion token 会返回一个 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) ? 这个得看是不是有成千上万个定时器的需求了。
  • io_uring 支持向一个 eventfd 投递完成事件,从而应用程序可以做其他事情,这个用法是很有用的,从而就是主要进程也可以避免被挂起等待(睡死在 wait_cqe 上),然后要做其他事情的时候开另一个线程了。有了 eventfd,可以阻塞或者非阻塞 read,也可以结合 poll/epoll/select 的 event loop,然后顺便处理一个 io_uring 完成事件。这是正常的因为 io_uring 完成就意味着长时间的 IO 操作完成,此时一般是进行一个收割的行为,对于 Proactor 这种有裂变的,其实也是自然的。不过我只写过直接用 wait_cqe + 轮询,没找到这个的实际用途…
  • 使用 NON-BLOCKING: 前面提到 io_uring 应当通过 blocking io 注册 fd,实际上 io_uring 也可以支持 non-blocking io。因为 io_uring 支持 poll 和 epoll 。他提供了 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/364
  • 这部分先总结到这里等待更新。。。。

高性能特性

  • 如何获取高性能
    • 首先要说一下为什么五月以后才基本可用,也就是 kernel 5.7 之后(虽然这篇快速笔记我不是很想涉及原理),Windows 下,驱动就是异步。io_uring 一开始的实现是这样的,他通过内部使用一堆 worker thread 去做 non-blocking io 读写,直到 EAGAIN(确实是必须的因为像 recvmsg/sendmsg、设置了 low water mark的 tcp socket 、write 这些系统调用的行为是有最小量的限制的),更加具体的做法我这里不说了,有兴趣可以直接去看源码(…),但是 5.7 之后,axboe 发现其实可以内部也用类似 user space 的 poll/select/epoll 这种来做,从而避免了搞一堆 thread,这个特性标记为 IORING_FEAT_FAST_POLL
    • 前面说了这么多,但是实际去对一个很简单的 io_uring 程序(没有用到 register、automatic buffer selection、SQPOLLING 、POLLED IO 的网络程序或者硬盘读写程序(毕竟网络程序也需要进行大量的硬盘读写,包括高速SSD 硬盘的随机读写以及垃圾硬盘的随机读写、顺序读写))进行测试的时候,可能会发现性能并没有特别明显的提升(网络读写对比 epoll + non-blocking IO 模型,硬盘读写对比 aio)。
  • 开启了 SQ POLLING 和 POLLED IO 的时候,文件读写会相比原来的 aio 提升很多(数据来自 Jens Axboe 邮件列表里面给的,以及阿里云有在他们的内核上做实测),能够接近 SPDK 的性能。要知道 SPDK 可是 kernel bypass 超人(libaio 如果 CPU 所有核都用上了才能达到SPDK/io_uring 一个核的水平,aio 比总量还是可以的)。
    • 总而言之,要获取性能,高级特性是必不可少的(就像 epoll 不开 ET 的时候,只是等于 poll 的红黑树加速版,但是其实 epoll 的这个纸面分析也奇妙,因为 ET 虽然减少一个 O(n) 的 level 检查,但是所有系统调用都有可能要进行多一次非阻塞系统调用直到 EAGAIN,context switch 的开销,所以这些东西必须实测才知道性能差距),前面的基础用法和学了 epoll、aio 之类怎么用的手册活一般没有什么不同。
  • 实际我们可以编写简单的短连接网络应用(不使用 POLLING、POLLED IO、register buffer),say HTTP1.0 测试程序,然后用 webbench 或者其他的测试工具来测试就能够发现,io_uring 对于这种 webserver 来说,提升并不大,有时候还会变弱。这个问题主要在于上面提到的 fd 的频繁映射和取消,缓冲区的频繁复制的问题(say 一个 html 页面文件)?实际存疑。中文网路上已经有一些文章进行了一些不错的定量分析,比如这个直接用 bpftrace 进行了不同开销的测试的文章,但是文中的使能(enable)两个字令人迷糊; 以及这个对 [nginx 进行了适配的文章](面对疾风吧!io_uring 优化nginx 实战演练_)(标转载,找不到原文连接)我自己测试了类似 HTTP 短连接时的程序,用 boost.asio 的框架,分别启用 io_uring 和采用 epoll 模拟,他们基本没有差距。结论是长连接+大量连接数的时候,io_uring 确实赢(主要是此时才有 0 syscall + zero copy(指注册 buffer)的优势),netty 用了 rust_echo_bench 和 tcpkali 测试的结果可以在这里看到:Any io_uring performance tests after all of the work that has been done? 。
  • **轮询:**对于 SQPOLLING、POLLED IO 其实上面已经提到了,要使用他们,只需要按照 manual 来写程序就行了,也就是 SQPOLLING 要注意有新的 SQE 投递的时候唤醒 kernel 就行了,since 所有的 SQE 投递都是可控的,所以是没有难度的,而且启用了 SQPOLLING 的时候,POLLED IO 的 completion reaping 也让 kernel thread 做了,用了高级特性反而让程序变得简单了,就是如此。其实我这里描述的就是 futex 的思路,我确实还记得 futex 里面是怎么避免过多的 context switch 从而实现 fast userspace mutex的,他首先会在 user space spin 一段时间,超过限制了之后退出 spin 然后进入到 kernel 里面睡觉。
  • 总之,短连接上来说这个优化可能很难进行了,特别是基于 async await 的模型来写的话,一次只投递一次 accept 的时候(正如上面的 webserver 的写法,如果 accept completion,再投递一次 accept),boost asio 的示例也是这样,我自己写的也是这样,很快就发现这样的话属实是搞笑的(就算 io_uring submit 之后内部实现是会先检查一下是否可以直接 return if yes then just post a cqe immediately,但是此时 syscall 的 overhead 已经发生了),因为这样等于每次 accept 都要一个 syscall 。可能的做法是,批量投递 accept 请求(就像 nginx 使用 IOCP 那样),从而缓解 kernel 的半连接 backlog 队列引发的 RST 返回到客户端。
  • 然而,对于无论是网络程序还是硬盘读写程序(两个加起来等于一个数据库…RocksDB、TiKV 已经用上 io_uring 了, 参考链接不放了,直接去 github 看 PR 吧,纯网络方面,nginx 和 netty 也有在适配了)而言,异步 IO 的另一个问题是缓冲区导致的内存消耗。这个 boost.asio 对 Proactor 的介绍里面就说到了,这个问题会通过 Automatic buffer selection 来解决, 这个对应的 patch set 描述在这里:Support for automatic buffer selection。

Automatic buffer selection

  • 这里开一个小节了讲解这个,因为 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)。我之后再会回来更新这篇笔记。

你可能感兴趣的:(操作系统/数据库,网络编程,io_uring,linux,aio)