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,但是包含以下缺点:
- async IO 只支援
O_DIRECT
(or un-buffered) accesses -> file descriptor 的设定 - IO submission 伴随 104 bytes 的 data copy (for IO that's supposedly zero copy),此外一次 IO 必须呼叫两个 system call (submit + wait-for-completion)
- 有很多可能会导致 async 最后变成 blocking (如: submission 会 block 等待 meta data; request slots 如果都被占满, submission 亦会 block 住)
Jens Axboe 一开始先尝试改写原有的 native aio,但是失败收场,因此他决定提出一个新的 interface,包含以下目标 (越后面越重要):
- 易于理解和直观的使用
- Extendable,除了 block oriented IO,networking 和 non-block storage 也要能用
- 效率
- 可扩展性
在设计 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,那这个就很重要。提供两种操作:
read_barrier()
:确保在进行后续内存读取之前可以看到之前的写入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 ...
- 若
kthread
闲置太久,为了避免霸占 CPU,所以会主动 sleep,所以若看到 flag:IORING_ENTER_SQ_WAKEUP
设起,就必须要唤醒 kthread。 - PATCH:为 SQPOLL SQ 环等待提供 IORING_ENTER_SQ_WAIT
7 安装 liburing¶
- 下载 source code
- 。/配置
- 须藤使安装
- 编译示例: 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¶
-
io_uring_setup(u32 个条目,结构 io_uring_params *p)
-
描述:建立一个提交队列(SQ)和完成队列(CQ)至少有条目条目,并返回一个文件描述符,该文件描述符可用于对 io_uring 实例执行后续操作。
- 关系:通过 liburing 函数包装:
io_uring_queue_init
- 标志:成员
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
- poll 线程将绑定到 cpu 设置的
-
如果未指定标志,则为中断驱动的 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_FIXED
或IORING_OP_WRITE_FIXED
操作码,并将该buf_index
字段设置为所需的缓冲区索引
- IORING_REGISTER_FILES:
- 描述:I/O 的寄存器文件。包含指向文件描述符
arg
数组的指针nr_args
- 使用它们,
IOSQE_FIXED_FILE
flag 必须设置在 的 flags 成员中struct io_uring_sqe
,并且该fd
成员设置为文件描述符数组中文件的索引
- 描述:I/O 的寄存器文件。包含指向文件描述符
-
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