io_uring(1) – 我们为什么会需要 io_uring

2019年6月12日 81 次阅读 0 条评论 0 人点赞

IO 到底怎么啦

当前 Linux 对文件的操作有很多种方式,最古老的最基本就是 readwrite 这样的原始接口,这样的接口简洁直观,但是真的是足够原始,效率什么自然不是第一要素,当然为了符合 POSIX 标准,我们需要它。一段时间之后,程序员们发现,人们需要更为简单的 API,于是出现了 preadpwrite 它允许我们在读写时直接传递 offset,显而易见它表现的更为优秀,在减少编码的同时,提高了代码的健壮性1。后来又出现了 preadvpwritev 这种可以一次性发送多个 IO 的高效接口;接着又出现了变种函数 preadv2pwritev2 他们不仅仅可以发送向量型的 IO,offset,还能设置本次 IO 的标志,比如 RWF_DSYNC、RWF_HIPRI、RWF_SYNC 等等(暂时没有其他)。

上面介绍的一系列的接口全部都是同步接口,意思就是在读写 IO 时,caller 一定会阻塞起来等待结果返回,对于普通的传统编程模型,这其实没有什么大不了的,编程简单且结果可以预测;但是在高效情况下呢?同步导致的后果就是 caller 不再能够继续执行其他的操作,只能静静的等待 IO 结果返回,其实他明明可以利用这段时间继续处理下一个操作,好比是一个 ftp 服务器,当接收到客户机上传的文件,然后将文件写入到本机的过程中时,假设 ftp 服务程序忙于等待文件读写结果的返回,那么就会拒绝到其他正在需要连接的客户机请求2。有没有更好的方式?当然有,那就是采用异步 IO 模型,当一个客户机上传文件时,直接将 IO 的 buffer 提交给内核即可,然后 caller 继续接受下一个客户请求,在内核处理完毕 IO 之后,主动调用各种通知机制,告诉 caller 上一个 IO 已经完成,完成状态保存在某某位置,请查看。

尴尬的 AIO

Great,原来我们是如此迫切的需要异步 IO,他能帮助我们做更多的事情而无需增加 caller 更多的复杂度,AIO 应运而生,POSIX 也适时的添加了 aio_readaio_write 这样的标准接口,好像一切都那么顺利,世界来到了一个完美的位面。Unfortunately,aio 满足了我们的要求,但是他也存在很多的缺陷。

第一,最大的缺陷就是不支持 buffer-io,也就是说,在采用 aio 的时候,你只能使用 O_DIRECT 来发送这一个 IO,带来的影响就是你不再能够借助文件系统缓存来缓存当前的 IO 请求,于是你在得到的同时失去了一些东西。

其次,尽管你强迫所有的 IO 都采用异步 IO,但是有时候确做不到,你的 caller 尽管将任务发送给了内核,但是内核还是通过工作队列或者线程完成的提交工作,假设在写元数据区域的时候,submission 会被挂起等待,假设存储设备的所有通道都很忙的时候,submission 需要挂起等待。于是,这些不确定性的存在,导致你的 caller 在处理完成状态的时候也不得不妥协。

最后,API 函数并不是很友好,基本上每一个 IO 的提交都需要要拷贝 64 + 8 个字节,而完成状态需要拷贝 32 个字节,这里就是 104 个字节的拷贝,当然,这个消耗是否可以承受是和你的 IO 大小有关的,如果你发送的大的 IO 的话,这点消耗可以忽略。同时,每一个 IO 至少需要两次系统调用才能完成(submit 和 wait-for-completion),这在有 spectre/meltdown 的机器上是一个严重的灾难。

导致最终的问题就是,在各种测试场景上,aio 欣欣向荣,然后在实际生产环境中,被采纳的很少,这就成为尴尬的 AIO3

那么应该是什么样子的呢?

有了问题就需要有解决方案,设计解决方案就需要确立目标,于是新接口需要什么目标?

足够简单且难以滥用:所有的用户可见接口都需要满足这个要求,接口一定需要让使用者容易理解且不容易被错误使用。

可扩展:尽管这个接口是为了存储设备而建立的,但是他需要有足够的扩展性让将来支持非阻塞设备以及网络数据传输。

功能丰富:这个没什么好说的,什么接口都需要支持足够丰富的功能,而且需要减少和核外应用程序的耦合度,尽可能减少交互复杂度。

高效:高效率本来就是目标。

可伸缩性:高效和低延迟很重要,但是峰值速率对于存储设备来讲也很重要,为了适应新的硬件要求,接口还需要考虑到伸缩性。

于是,我们需要重新设计接口,新的异步接口 io_uring 就在这样的环境下诞生了,可以预见的就是,在不久的将来,他会成为一个 POSIX 标准存在,也会被更多的企业级软件所采用4。在 Jens Axboe 自己的测试环境中,io_uring 在特定情况下,会比 SPDK 拥有更好的表现。


  1. 我在某处的文章上看到过,pread 所做的偏移只针对本次操作而不影响文件整体的文件偏移指针,就是用 lseek 偏移的那个。 ↩︎
  2. 这只是一个假设,一般情况下服务程序会为每一个客户机 fork 一个进程进行服务的,当然这也不是什么最佳的处理方式,在更多的客户机同时链接时,会导致主机资源消耗殆尽的情况。 ↩︎
  3. 我个人这么认为的,实际上,aio 确实使用场景不多。 ↩︎
  4. 目前已知的 qemu 已经开始采用 io_uring 进行适配,很快将可以通过这个接口来加速虚拟机 IO 的流程。 ↩︎

文章评论(0)