io_uring(1) – 我们为什么会需要 io_uring
当前 Linux 对文件的操作有很多种方式,最古老的最基本就是 read
和 write
这样的原始接口,这样的接口简洁直观,但是真的是足够原始,效率什么自然不是第一要素,当然为了符合 POSIX 标准,我们需要它。一段时间之后,程序员们发现,人们需要更为简单的 API,于是出现了 pread
和 pwrite
它允许我们在读写时直接传递 offset,显而易见它表现的更为优秀,在减少编码的同时,提高了代码的健壮性。后来又出现了 preadv
和 pwritev
这种可以一次性发送多个 IO 的高效接口;接着又出现了变种函数 preadv2
和 pwritev2
他们不仅仅可以发送向量型的 IO,offset,还能设置本次 IO 的标志,比如 RWF_DSYNC、RWF_HIPRI、RWF_SYNC 等等(暂时没有其他)。
上面介绍的一系列的接口全部都是同步接口,意思就是在读写 IO 时,caller 一定会阻塞起来等待结果返回,对于普通的传统编程模型,这其实没有什么大不了的,编程简单且结果可以预测;但是在高效情况下呢?同步导致的后果就是 caller 不再能够继续执行其他的操作,只能静静的等待 IO 结果返回,其实他明明可以利用这段时间继续处理下一个操作,好比是一个 ftp 服务器,当接收到客户机上传的文件,然后将文件写入到本机的过程中时,假设 ftp 服务程序忙于等待文件读写结果的返回,那么就会拒绝到其他正在需要连接的客户机请求。有没有更好的方式?当然有,那就是采用异步 IO 模型,当一个客户机上传文件时,直接将 IO 的 buffer 提交给内核即可,然后 caller 继续接受下一个客户请求,在内核处理完毕 IO 之后,主动调用各种通知机制,告诉 caller 上一个 IO 已经完成,完成状态保存在某某位置,请查看。
Great,原来我们是如此迫切的需要异步 IO,他能帮助我们做更多的事情而无需增加 caller 更多的复杂度,AIO 应运而生,POSIX 也适时的添加了 aio_read
和 aio_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 的机器上是一个严重的灾难。