本文将记录io_uring相关笔记。

Motivation

The native Linux AIO framework suffers from various limitations, which io_uring aims to overcome:

  • It does not support buffered I/O, only direct I/O is supported.
  • It has non-deterministic behavior which may block under various circumstances.
  • It has a sub-optimal API, which requires at least two system calls per I/O, one to submit a request, and one to wait for its completion.
    • Each submission needs to copy 64 + 8 bytes of data, and each completion needs to copy 32 bytes.

Communication channel

An io_uring instance has two rings, a submission queue (SQ) and a completion queue (CQ), shared between the kernel and the application. The queues are single producer, single consumer, and power of two in size.

The queues provide a lock-less access interface, coordinated with memory barriers.

The application creates one or more SQ entries (SQE), and then updates the SQ tail. The kernel consumes the SQEs , and updates the SQ head.

The kernel creates CQ entries (CQE) for one or more completed requests, and updates the CQ tail. The application consumes the CQEs and updates the CQ head.

Completion events may arrive in any order but they are always associated with specific SQEs.

System call

默认流程

默认情形下,提交任务的流程,以及获取结果的方式:

  1. 把sqe放入sqring
  2. 调用io_uring_enter通知内核
  3. 可以轮询cqring等待结果或者通过带IORING_ENTER_GETEVENTSmin_complete参数的io_uring_enter阻塞等待指定数目的任务完成,再去cqring中检查结果

Submission Queue Polling

如果在调用io_uring_setup 时设置了 IORING_SETUP_SQPOLL 的 flag,内核会额外启动一个内核线程,我们称作 SQ 线程。这个内核线程可以运行在某个指定的 core 上(通过 sq_thread_cpu 配置)。这个内核线程会不停的 Poll SQ,除非在一段时间内没有 Poll 到任何请求(通过 sq_thread_idle 配置),才会被挂起。

当程序在用户态设置完 SQE,并通过修改 SQ 的 tail 完成一次插入时,如果此时 SQ 线程处于唤醒状态,那么可以立刻捕获到这次提交,这样就避免了用户程序调用io_uring_enter这个系统调用。如果 SQ 线程处于休眠状态,则需要通过调用io_uring_enter,并使用IORING_SQ_NEED_WAKEUP 参数,来唤醒 SQ 线程。用户态可以通过 sqring 的 flags 变量获取 SQ 线程的状态。

io polling

在默认情况下,当设备处理完IO请求后,设备会发送中断通知内核往cqring添加cqe,并更新cqring的tail指针。用户态程序会轮询cqring获取新的cqe。

但是对于IO low latency或者high IOPS的场景,使用中断并不合适,应该使用polling(refers to performing IO without relying on hardware interrupts to signal a completion event)。此时因为没有中断通知,内核就不会往 cqring中填充cqe,因此用户态程序就不能去轮询cqring了。此时,用户态程序必须调用io_uring_enter with IORING_ENTER_GETEVENTS set and min_complete set to 0来下发轮询任务给内核,内核会轮询检查是否有结果产生,如果有,则将结果放入cqring。

Tips:搞清楚内核什么时候更新cqring,分为如下两种case:

  1. 硬件中断通知内核

  2. 用户态程序调用io_uring_enter来下发polling任务给内核

liburing

为了简化使用io_uring, liburing 库应用而生。用户无需了解诸多 io_uring 细节便可以使用起来,如无需关心 memory barrier,以及 ring buffer 的管理等。

总结

io_uring 主要通过用户态与内核态共享内存的途径,来摒弃使用系统调用来提交 I/O 操作和获取 I/O 操作的结果,从而避免了上下文切换的情况。另外,由于用户态进程与内核态线程通过共享内存的方式通信,从而避免了内存拷贝的过程,提升了 I/O 操作的性能。

所以,io_uring 主要通过两个优化点来提升 I/O 操作的性能:

  • 摒弃使用系统调用来提交 I/O 操作和获取 I/O 操作结果
  • 减少用户态与内核态之间的内存拷贝

参考资料:

  1. An Introduction to the io_uring Asynchronous I/O Framework
  2. What is io_uring?
  3. Efficient IO with io_uring
  4. AIO 的新归宿:io_uring
  5. What’s new with io_uring
  6. io_uring 新异步 IO 机制,性能提升超 150%,堪比 SPDK
  7. 浅析开源项目之io_uring
  8. 下一代异步 IO io_uring 技术解密
  9. Submission Queue Polling)
  10. io_uring
  11. Linux I/O 神器之 io_uring