跳转至

title: 小谈io_uring tags: 189, 215](https://github.com/axboe/liburing/issues/215), 10622](https://github.com/netty/netty/issues/10622)] created: 2023-04-24 04:50:36 updated: 2023-07-03 06:53:03


小谈 io_uring

1 什么是 io_uring?

在 Linux 底下操作 IO 有以下方式:

  • read 系列
  • pread
  • preadv

但他们都是 synchronous 的,所以 POSIX 有实做 aio_read,但其乏善可陈且效能欠佳。

事实上 Linux 也有自己的 native async IO interface,但是包含以下缺点:

  1. async IO 只支援 O_DIRECT(or un-buffered) accesses -> file descriptor 的设定
  2. IO submission 伴随 104 bytes 的 data copy (for IO that's supposedly zero copy),此外一次 IO 必须呼叫两个 system call (submit + wait-for-completion)
  3. 有很多可能会导致 async 最后变成 blocking (如: submission 会 block 等待 meta data; request slots 如果都被占满, submission 亦会 block 住)

Jens Axboe 一开始先尝试改写原有的 native aio,但是失败收场,因此他决定提出一个新的 interface,包含以下目标 (越后面越重要):

  1. 易于理解和直观的使用
  2. Extendable,除了 block oriented IO,networking 和 non-block storage 也要能用
  3. 效率
  4. 可扩展性

在设计 io_uring 时,为了避免过多 data copy,Jens Axboe 选择透过 shared memory 来完成 application 和 kernel 间的沟通。其中不可避免的是同步问题,使用 single producer and single consumer ring buffer 来替代 shared lock 解决 shared data 的同步问题。而这沟通的管道又可分为 submission queue (SQ) 和 completion queue (CQ)。

以 CQ 来说,kernel 就是 producer,user application 就是 consumer。SQ 则是相反。

2 链接请求

CQEs can arrive in any order as they become available。(举例: 先读在 HDD 上的 A.txt,再读 SSD 上的 B.txt,若限制完成顺序的话,将会影响到效能)。事实上,也可以强制 ordering (see example )

liburing 预设会非循序的执行 submit queue 上的 operation,但是有些特别的情况,我们需要这些 operation 被循序的执行,如:write+ close。所以我们可以透过添加 IOSQE_IO_LINK 来达到效果。详细用法可参考 linking request

3 提交队列轮询

liburing 可以透过设定 flag:IORING_SETUP_SQPOLL 切换成 poll 模式,这个模式可以避免使用者一直呼叫 io_uring_enter(system call)。此模式下,kernel thread 会一直去检查 submission queue 上是否有工作要做。详细用法可参考 Submission Queue Polling

值得注意的是 kernel thread 的数量需要被控制,否则大量的 CPU cycle 会被 k-thread 占据。为了避免这个机制,liburing 的 kthread 在一定的时间内没有收到工作要做,kthread 就会 sleep,所以下一次要做 submission queue 上的工作就需要走原本的方式:io_uring_enter()

使用 liburing 时,您永远不会直接调用 io_uring_enter() 系统调用。这通常由 liburing 的 io_uring_submit() 函数处理。它会自动确定您是否使用轮询模式,并处理您的程序何时需要调用 io_uring_enter() 而无需您费心。

4 内存排序

如果要直接使用 liburing 就不用管这个议题,但是如果是要操作 raw interface,那这个就很重要。提供两种操作:

  1. read_barrier():确保在进行后续内存读取之前可以看到之前的写入
  2. write_barrier():在先前的写入之后订购此写入

内核将在读取 SQ 环尾之前包含一个 read_barrier(),以确保来自应用程序的尾部写入可见。从 CQ 环的角度来看,由于消费者/生产者的角色是相反的,应用程序只需要在读取 CQ 环尾之前发出 read_barrier() 以确保它看到内核所做的任何写入。

5 解放图书馆

  • 不再需要样板代码来设置 io_uring 实例
  • 为基本用例提供简化的 API。

5.1 高级用例和功能

5.1.1 固定文件和缓冲区

5.1.2 轮询 IO

I/O 依靠硬件中断来发出完成事件的信号。当 IO 被轮询时,应用程序将反复询问硬件驱动程序提交的 IO 请求的状态。

[注]真实轮询示例 [注] 提交队列轮询仅与固定文件(非固定缓冲区)结合使用

5.1.3 内核端轮询

会有 kernel thread 主动侦测 SQ 上是否有东西,这样可以避免呼叫 syscall: io_uring_enter

6 原始程式码

6.1 io_uring_setup

基本的设定。我们关注的是 setup 时需要设定哪些关于 IORING_SETUP_SQPOLL 的操作,预期找到 kthread 的建立,kthread 的工作内容等等。从 io_sq_offload_create 可知 offload 和 kthread 有关。

往里面看可以找到 create_io_thread,透过 copy_process 达到 fork,搭配 wake_up_new_task 启动 process。该 process 要做的事为 io_sq_thread

6.2 io_uring_enter

io_uring_enter 在 prepare 完 write/read 之类的 operation 后会被呼叫,这里我们只关注在 poll 模式下的行为:

if (ctx->flags & IORING_SETUP_SQPOLL) {
    io_cqring_overflow_flush(ctx, false);

    ret = -EOWNERDEAD;
    if (unlikely(ctx->sq_data->thread == NULL))
        goto out;
    if (flags & IORING_ENTER_SQ_WAKEUP)
        wake_up(&ctx->sq_data->wait);
    if (flags & IORING_ENTER_SQ_WAIT) {
        ret = io_sqpoll_wait_sq(ctx);
        if (ret)
            goto out;
    }
    submitted = to_submit;
} else if ...
  1. kthread 闲置太久,为了避免霸占 CPU,所以会主动 sleep,所以若看到 flag:IORING_ENTER_SQ_WAKEUP 设起,就必须要唤醒 kthread。
  2. PATCH:为 SQPOLL SQ 环等待提供 IORING_ENTER_SQ_WAIT

7 安装 liburing

  1. 下载 source code
  2. 。/配置
  3. 须藤使安装
  4. 编译示例: gcc -Wall -O2 -D_GNU_SOURCE -o io_uring-test io_uring-test.c -luring

8 自由流动

io_uring_queue_init -> alloc iov -> io_uring_get_sqe -> io_uring_prep_readv -> io_uring_sqe_set_data -> io_uring_submit

io_uring_wait_cqe -> io_uring_cqe_get_data -> io_uring_cqe_seen -> io_uring_queue_exit

9 liburing/io_uring API

9.1 i_uring

  1. io_uring_setup(u32 个条目,结构 io_uring_params *p)

  2. 描述:建立一个提交队列(SQ)和完成队列(CQ)至少有条目条目,并返回一个文件描述符,该文件描述符可用于对 io_uring 实例执行后续操作。

  3. 关系:通过 liburing 函数包装: io_uring_queue_init
  4. 标志:成员
struct io_uring_params
  • IORING_SETUP_IOPOLL:
    • 忙于等待 I/O 完成
    • 提供更低的延迟,但可能比中断驱动的 I/O 消耗更多的 CPU 资源
    • 仅可用于使用 O_DIRECT 标志打开的文件描述符
    • 在 io_uring 实例上混合和匹配轮询和非轮询 I/O 是非法的
  • IORING_SETUP_SQPOLL:
    • 设置后创建内核线程进行提交队列轮询
    • IORING_SQ_NEED_WAKEUP 如果内核线程空闲超过 sq_thread_idle 毫秒,将设置标志
    • io_uring_register 在 linux 5.11 之前,应用程序必须通过操作 IORING_REGISTER_FILES 码注册一组用于 IO 的文件
  • IORING_SETUP_SQ_AFF:
    • poll 线程将绑定到 cpu 设置的 sq_thread_cpu 字段 struct io_uring_params
  • 如果未指定标志,则为中断驱动的 I/O 设置 io_uring 实例

  • io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args)

  • 操作码:

  • IORING_REGISTER_BUFFERS:
    • uffers 被映射到内核并有资格进行 I/O
    • 使用它们,应用程序必须在提交队列条目中指定 IORING_OP_READ_FIXEDIORING_OP_WRITE_FIXED 操作码,并将该 buf_index 字段设置为所需的缓冲区索引
  • IORING_REGISTER_FILES:
    • 描述:I/O 的寄存器文件。包含指向文件描述符 arg 数组的指针 nr_args
    • 使用它们,IOSQE_FIXED_FILEflag 必须设置在 的 flags 成员中 struct io_uring_sqe,并且该 fd 成员设置为文件描述符数组中文件的索引
  • IORING_REGISTER_EVENTFD:

    • 可以用于 eventfd() 获取 io_uring 实例上的完成事件的通知。
  • io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig)

  • 说明:单次调用既可以提交新的 I/O,也可以等待本次调用或之前调用的 I/O 完成 io_uring_enter()

  • 标志:
  • IORING_ENTER_GETEVENTS:
    • min_complete 在返回之前等待指定数量的事件
    • 可以与 to_submit 一起设置为在单个系统调用中提交和完成事件
  • IORING_ENTER_SQ_WAKEUP
  • IORING_ENTER_SQ_WAIT
  • 操作码:
  • IORING_OP_READV
  • IORING_OP_WRITEV
  • 一些细节:
  • 如果 io_uring 实例被配置为轮询,通过 IORING_SETUP_IOPOLL 在对 的调用中指定 io_uring_setup(),则 min_complete 含义略有不同。传递值 0 指示内核返回任何已经完成的事件,而不会阻塞。如果 min_complete 是一个非零值,如果有任何完成事件可用,内核仍然会立即返回。如果没有可用的事件完成,则调用将轮询直到一个或多个完成变得可用,或者直到进程超过其调度程序时间片。

9.2 解放

10 io_uring 与 epoll

10.1 io_uring 比 epoll 慢?

11 参考

原文链接:https://hackmd.io/@sysprog/iouring#What-is-io_uring