简介
io_uring 是 Linux 最新的异步 I/O 接口,采纳两个用户和内核共享的 ring buffer 进行交互,性能优于之前的接口且限度更少。尽管 io_uring 依然处于开发迭代中,然而根本的 I/O 接口曾经根本定型,作为高效零碎语言的 Rust 则成为应用该接口的不二之选。当初曾经有许多针对 io_uring 的 Rust 封装,然而有的存在 soundness 问题,有的存在性能问题,都不是平安高效 I/O 的好选项。咱们团队(DatenLord)也进行了本人的尝试,本文就是介绍咱们的 io_uring 异步库实现办法。
Rust 现有异步模式
Rust 的异步库都有本人的异步 I/O 实现办法,然而外部原理大同小异,都是 Reactor 模式,如下图所示:
Worker 线程将关注的 fd 注册到 Epoll 期待队列中,Reactor 线程通过 Epoll wait 期待能够进行操作的 fd,当有 fd 能够操作时,Reactor 线程告诉 Worker 线程进行真正的 I/O 操作。在此过程中,Reactor 线程仅仅起到期待和告诉的作用,并不真正进行 I/O 操作,并且这里的 I/O 接口依然是同步 I/O 接口。这种模式就好比请人帮忙烧开水,然而泡茶的过程还是本人亲自来。
Reactor 模式中,内存 buffer 始终在用户的管制下,在进行真正的 I/O 操作产生前,随时能够 cancel 正在期待的申请,因而 Reactor 模式中不存在内存 data race 的状况,接口也就趁势应用了 reference,及借用机制。接口示例如下:
fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> ImplFuture<'a, Result<usize>>
io_uring Rust 底层封装
io_uring 的官网库只有 C 语言版本及 liburing,因而 Rust 异步封装之前必须有一套可用的 Rust 底层封装。这一层封装大家有不同的抉择:有的抉择本人从头实现,如 tokio 的 io-uring;咱们的抉择则是复用 liburing,先进行一层 binding,而后在进行一层面向对象的封装,形象出 SQ,CQ 和 Register 等,这一层形象借鉴的 tokio 的 io-uring。前一种办法对实现具备更强的控制力,后一种办法则升高了保护老本。无论哪一种办法,最终的目标和成果是一样的——搭建脚手架,为异步封装扫平阻碍。
io_uring 异步模式
io_uring 和 Rust 现有异步模型不同,该异步操作是由操作系统实现的,并不需要用户线程参加,该工作形式非常适合 Proactor 模式。下图为 Proactor 模式示意图:
依据图中所示,异步操作由 Proactor 线程实现,更精确说是由 Proactor 线程提交 I/O 工作给内核,等内核实现了 I/O 操作再讲后果告诉给 Worker 线程。和 Reactor 模式相比,Proactor 为用户实现了更多的工作,这就好比请人帮忙把烧水和泡茶的活一起干了,间接喝茶就行。
io_uring Proactor 设计
在决定了采纳 Proactor 模式来实现 io_uring 之后,咱们还须要思考 io_uring 本人的个性。io_uring 在设计的时候只思考了一个线程一个 io_uring 实例,因而无论是内核接口还是 libfuse 的封装接口都不易实现多线程并发拜访。基于这个思考,有两个办法解决,第一个办法为 io_uring 操作上锁,也就是间接的将多线程并发操作串行化;第二个办法为只用单线程进行 io_uring 操作,其余工作给该线程提交工作。ringbahn 采纳了第一种办法,咱们采取了第二种办法。第二种办法的益处在于,能够将间断的多个 I/O 操作一次提交,在忙碌的零碎中可能进步性能。
下图为咱们的架构设计:
在咱们的设计中,所有的 Worker Task 通过全局的 channel 向 Submitter Task 提交 I/O 工作,当没有 I/O 工作时 Submitter Task 会在期待在该 channel 上,而当申请忙碌时 Submitter Task 会打包多个工作一次性提交。Completer Thread 会收取 ring 上实现的工作,并且唤醒期待这些工作的 Worker Task。
单个 io_uring 实例同时解决的 I/O 申请数目是有下限的,如果实现的工作不及时接管则会呈现失落的状况,因而咱们保护了一个全局计数器来统计正在被解决的 I/O 申请数目,当数目达到下限时则会挂起 Worker Task 让其期待。
内存平安
Rust 语言的内存平安要求不能呈现 data race 和 use after free 的状况,而 io_uring 的应用模型则存在当初的危险。操作系统会异步地操作内存 buffer,这块 buffer 如果被用户同步操作测会呈现 data race 的状况。因而被 Proactor 线程占用的内存必须独占,否则任何被勾销的 I/O 操作都会导致内存被用户态同时应用。
为了达到上述目标,Reactor 的基于 reference 的接口不能被应用,须要采纳新的接口,如下所示:
pub async fn read(
fd: RawFd,
buffer: Vec<u8>,
count: usize,
offset: isize,
) -> (io::Result<usize>, (RawFd, Vec<u8>))
在该接口中用户会在 I/O 操作过程中交出 buffer 所有权,在工作实现时返还 buffer 所有权。
总结
现有的 Rust 异步 I/O 模型(Reactor)其实分为两步,第一步由操作系统告诉用户哪些 fd 能够应用,第二步再由用户实现 I/O 操作,操作系统仅仅负责告诉,真正干活的还是用户本人。区别于该 I/O 模型,io_uring 的 I/O 模型为 Proactor,所有的异步 I/O 申请都是操作系统来实现,用户仅仅须要发送申请和接管后果。
咱们 DatenLord 团队在充分考虑了 io_uring 特点和 Rust 语言的需要后实现了一个 io_uring 的异步库。这个库同时关照到性能和易用性,不便用户迁徙现有代码,同时该库不依赖于任何一部框架,能够和绝大多数已知的异步框架一起应用。此链接为该库的代码地址,欢送大家提交 PR 和 issue,帮忙进一步欠缺性能。
作者 | 施继成
前期编辑 | 张汉东
转自《Rust Magazine 中文精选》