io_uring 用户库源码分析
当前内容基于 liburing 2.1 版本
整体流程
之前在 io_uring 简介和使用 有过总结,使用 io_uring 的一般流程如下:
- 使用
open
、fstat
等函数来打开文件以及元数据查看等操作- 因为 io_uring 替换的是读写接口,后续 io_uring 操作的对象是
fd
(由open
函数执行返回的)
- 因为 io_uring 替换的是读写接口,后续 io_uring 操作的对象是
- 使用
io_uring_queue_init
初始化struct io_uring ring
结构体 - 初始化
struct iovec *iovecs
结构体用于存放用户态 buffer 指针和长度 - 通过
io_uring_get_sqe
获取sqe
- 通过
io_uring_prep_#OP
对sqe
填充命令,buffer 以及 offset 信息- 【可选】 通过
io_uring_sqe_set_data
对sqe
附加user_data
信息(该信息会在cqe
中进行返回)
- 【可选】 通过
- 通过
io_uring_submit
对整个ring
的所有sqe
进行下发 - 通过
io_uring_wait_cqe
或者io_uring_peek_cqe
来获取cqe
io_uring_wait_cqe
会阻塞当前线程直到有一个cqe
返回io_uring_peek_cqe
不会阻塞,如果当前没有cqe
,就会返回错误io_uring_cqe_get_data
可以从cqe
中获取user_data
- 通过
io_uring_cqe_seen
对当前cqe
进行清除,避免被二次处理 - 所有 IO 完成后,通过
io_uring_queue_exit
将ring
销毁
io_uring_queue_init
-
函数调用逻辑
graph TD io_uring_queue_init --> io_uring_queue_init_params --> __sys_io_uring_setup --> syscall -->|陷入内核|io_uring_setup io_uring_queue_init_params --> io_uring_queue_mmap --> io_uring_mmap --> mmap
graph TD io_uring_queue_init --> io_uring_queue_init_params --> __sys_io_uring_setup --> syscall -->|陷入内核|io_uring_setup io_uring_queue_init_params --> io_uring_queue_mmap --> io_uring_mmap --> mmap -
函数功能
该函数主要将队列深度以及额外的
flags
参数传递到内核,让内核的io_uring_setup
来初始化io_uring
结构体,同时使用mmap
将在内核中初始化的SQ
、CQ
以及SQEs
映射到用户态初始化时传递的
flags
将影响io_uring
的运行方式:IORING_SETUP_IOPOLL
:开启此选项必须保证后续只用O_DIRECT
打开文件并且文件系统的file_operations
中注册了iopoll
函数,否则 IO 将下发失败。开启后内核将调用注册的iopoll
函数来主动轮询设备驱动确认 IO 是否完成IORING_SETUP_SQPOLL
:将启动一个单独的内核线程io_sq_thread
,内核将主动轮询 SQ,然后将 IO 下发至驱动设备,能大大减少提交 IO 时的系统调用开销(内核线程工作时,提交 IO 将无需系统调用;但是该线程可能会休眠,休眠时需要系统调用来唤醒该线程)IORING_SETUP_SQ_AFF
:当IORING_SETUP_SQPOLL
已经配置后,启用sq_thread_cpu
字段,用于配置内核线程io_sq_thread
的跑在哪个 CPU 上
io_uring_get_sqe
由于 SQ 已经通过 mmap
映射到用户态,该函数只需在读取 sq->khead
时通过 io_uring_smp_load_acquire
保证一致性,而 sq->sqe_tail
只用于用户态,直接读取即可,根据 sq->khead
以及 sq->sqe_tail
判断 SQ 是否已满,未满则给出 sq->sqe_tail
处的 sqe
即可,然后更新 sq->sqe_tail
io_uring_prep_#OP
通过调用 io_uring_prep_rw
对 sqe
填充命令 OP、fd
、buffer 指针以及 offset 信息等
io_uring_sqe_set_data
直接对 sqe->user_data
进行赋值
io_uring_submit
-
函数调用逻辑
graph TD io_uring_submit --> __io_uring_submit_and_wait --> __io_uring_flush_sq __io_uring_submit_and_wait --> __io_uring_submit --> sq_ring_needs_enter __io_uring_submit --> __sys_io_uring_enter --> __sys_io_uring_enter2 --> syscall -->|陷入内核|io_uring_enter
graph TD io_uring_submit --> __io_uring_submit_and_wait --> __io_uring_flush_sq __io_uring_submit_and_wait --> __io_uring_submit --> sq_ring_needs_enter __io_uring_submit --> __sys_io_uring_enter --> __sys_io_uring_enter2 --> syscall -->|陷入内核|io_uring_enter -
函数功能
-
__io_uring_flush_sq
根据
sq->sqe_tail
、sq->sqe_head
差值依次填充sq->array
,然后一次性更新sq->ktail
,并返回内核中仍未处理sqe
数量(sq->ktail - sq->khead
) -
sq_ring_needs_enter
判断内核线程
io_sq_thread
是否启用以及正常工作(没有休眠):- 首先要判断用户态
ring->flags
是否配置了IORING_SETUP_SQPOLL
标志位,判断是否启用了内核线程io_sq_thread
- 然后再判断内核态
ring->sq.kflags
是否配置了IORING_SQ_NEED_WAKEUP
标志位,判断内核线程io_sq_thread
是否需要唤醒
当内核线程
io_sq_thread
启用并且正常工作时,则整个io_uring_submit
到此结束,无需后续的__sys_io_uring_enter
系统调用,减少了 IO 下发的系统调用的开销 - 首先要判断用户态
-
__sys_io_uring_enter
系统调用陷入内核态,将参数传递给内核的
io_uring_enter
函数,主要用于提交 IO 和获取 IO 完成情况,具体功能和初始化时配置的ring->flags
相关
-
io_uring_wait_cqe
在用户态轮询判断是否有一个新的 cqe
,无需系统调用陷入内核,但是会阻塞当前线程直到有一个新的 cqe
或者出错
io_uring_peek_cqe
仅在用户态判断一次是否有新的 cqe
,无需系统调用陷入内核,如果没有新的 cqe
,会返回失败信息 -errno
io_uring_cqe_get_data
cqe->user_data
会在 IO 完成后,从 sqe
复制到对应的 cqe
中,该函数只用直接对 cqe->user_data
进行读取
io_uring_cqe_seen
更新 cq->khead
,避免当前 cqe
被重复获取
io_uring_queue_exit
首先通过 munmap
将初始化时 mmap
的 SQ
、CQ
以及 SQEs
解除映射,然后通过 close
关闭 io_uring
对应的 fd
,close
会调用到该 fd
注册的 io_uring_release
来释放 io_uring
参考资料
- 本文链接: https://ywang-wnlo.github.io/posts/d7259d1d/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 4.0 许可协议。转载请注明出处!