io_uring简介
io_uring是linux于2019年引入内核的异步IO,支持普通的任务提交模式和轮询模式,用户向其一次性提交多个需要完成的系统调用任务,然后内核会对任务进行收割并返回任务完成的结果,用户只需获取任务完成的结果并进行相应的处理,而无需一直等待系统调用的完成。
io_uring 系统调用API
- io_uring_setup()
- io_uring_register()
- io_uring_enter()
io_uring_setup()
设置上下文
int io_uring_setup(u32 entries, struct io_uring_params *p);
- 创建一个提交队列(submit queue, SQ)和一个完成队列(complete queue, CQ).
- 指定io_uring 的入口数目,即同时处理的 I/O 事件数目
- 参数p 用来配置io_uring,内核返回的SQ/CQ配置信息也通过它带回来
- 返回一个文件描述符,随后用于在这个 io_uring 实例上执行操作
- 使用mmap避免频繁的数据copy
io_uring_register()
注册用于异步 I/O 的文件描述符或内存区域
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
该函数用于将文件描述符或内存区域与io_uring关联起来。该函数将返回注册的文件描述符或内存区域的索引,以便后续的 I/O 操作可以使用。
fd: io_uring文件描述符。
opcode: 指定注册操作的类型,如文件描述符的注册或内存区域的注册。
arg: 指向相关数据结构的指针,用于传递需要注册的文件描述符或内存区域的信息。
nr_args:指定相关参数的数量。
io_uring_enter()
该函数用于提交 I/O 事件并等待其完成,返回已完成的 I/O 事件数量
int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);
单次调用同时执行:
- 提交新的 I/O 请求
- 等待 I/O 完成
fd: io_uring文件描述符,即io_uring_setup() 返回的文件描述符
to_submit: 要提交的 I/O 事件数量
min_complete: 指定在返回之前至少完成的 I/O 事件数量,为0即不阻塞
liburing简介
由于io_uring要实现强大的功能和最优的效率,因此其接口和使用方式会比较复杂。但对于大部分不需要极致IO性能的场景和开发者来说,只使用io_uring的基本功能就能获得大部分的性能收益。当只需要基本功能时,io_uring的复杂接口中很大一部分是不会使用的,同时一部分初始化操作也是基本不变的。因此,io_uring的作者又开发了liburing来简化一般场景下io_uring的使用。使用liburing后,io_uring初始化时的大部分参数都不再需要填写,也不需要自己再做内存映射,内存屏障和队列管理等复杂易错的逻辑也都封装在liburing提供的简单接口中,大幅降低了使用难度。
liburing 常见 API
io_uring_queue_init_params()
执行io_uring_setup()系统调用来初始化io_uring队列,生成SQ和CQ
int io_uring_queue_init_params(unsigned entries, struct io_uring *ring, const struct io_uring_params *p);
- entries:指定 io_uring 的入口数目,即同时处理的 I/O 事件数目
- ring:指向 struct io_uring 结构的指针,用于接收初始化后的 I/O uring 环境
- p:指向 struct io_uring_params 结构的指针,包含了自定义的初始化参数
示例:
unsigned short port = 8096;
int listen_fd = init_server(port);
struct io_uring_params ring_params;
memset(&ring_params, 0, sizeof(ring_params));
struct io_uring ring;
io_uring_queue_init_params(1024, &ring, &ring_params);
io_uring_get_sqe()
用于从 SQ 中获取下一个 Submission Queue Entry (SQE)。通过获取 SQE,你可以添加请求。
struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);
ring: 指向 struct io_uring 结构的指针,表示要操作的io_uring对象
io_uring_prep_accept()
用于添加 执行 accept 操作的 请求到 SQE 中
void io_uring_prep_accept(struct io_uring_sqe *sqe, int fd, struct sockaddr *addr, socklen_t *addrlen, int flags);
注意到除了参数sqe,其它参数和accept()一样
示例程序:
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_accept(sqe, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
io_uring_prep_recv()
用于添加 执行 recv 操作的 SQE
void io_uring_prep_accept(struct io_uring_sqe *sqe, int sockfd, void *buf, size_t len, int flags);
注意到除了参数sqe,其它参数和recv()一样
io_uring_prep_send()
void io_uring_prep_send(struct io_uring_sqe *sqe, int sockfd, void *buf, size_t len, int flags);
注意到除了参数sqe,其它参数和send()一样
io_uring_submit()
把所有的请求提交进去进行处理
int io_uring_submit(struct io_uring *ring);
io_uring_wait_cqe()
阻塞当前线程,直到有已完成的 CQE 可用。一旦有 CQE 可用,函数将填充 cqe_ptr 指向的指针,并返回0
int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);
- ring:指向 struct io_uring 的指针,表示I/O uring环境。
- cqe_ptr:指向 CQE 指针的指针,用于存储返回的已完成CQE。
io_uring_peek_batch_cqe()
批量获取已完成的 CQE, 不阻塞
int io_uring_peek_batch_cqe(struct io_uring *ring, struct io_uring_cqe **cqes, unsigned int count);
- ring:指向 struct io_uring 的指针,表示 io_uring 环境
- cqes:一个指向指针数组的指针,用于存储返回的已完成CQE。
- count:要获取的 CQE 数量。
示例程序
io_uring_submint(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
struct io_uring_cqe *cqes[128];
int nready = io_uring_peak_batch_cqe(&ring, cqes, 128);
int i = 0;
for(;i < nready; i++){
//处理
}
io_uring_cq_advance(&ring, nready);
io_uring_cq_advance()
void io_uring_cq_advance(struct io_uring *ring, unsigned int steps);
推进 CQ 中的指针位置,以告知 io_uring 已经成功处理了一定数量的完成事件,并且可以释放这些事件所占用的资源