Linux异步IOio_uring 与 libaio
我们可以先整体看一下 linux 的 IO 模型大体有哪些类型。
linux 的 IO 主要可以分为两个大类,而我们今天要介绍的 io_uring 就属于其中的 kernel IO 模型中的 async IO 模式的一种。
作为存储系统的开发者,高带宽和高 IOPS 是我们不断的性能追求,相比于通过 kernel bypass 的方式和硬件相结合来实现这种目标,kernel native IO 的方式似乎是一种更加友好通用的实现方式。
从 linux 的 IO 接口的发展看,async IO 是对于普通应用程序来说,实现高性能的必然选择,它通过异步方式来和 linux kernel 进行交互,减少了对用户态应用程序的阻塞过程,可以让应用程序有更多的机会去处理其他任务,提高了并发度
io_uring 简介
从上面的分析中看出,io_uring 是 kernel natvie aio 的一种,它是 Linux Kernel 5.1 版本加入一个特性。通过设计 io_uring 这套全新的 aysnc IO 系统调用接口,让应用程序可以获得更高的性能,更好的兼容性。
libaio 的局限
在 io_uring 出现之前,主流的使用 kernel aio 模式的接口是使用 libaio 接口,这种接口存在着如下一些局限:
(1) 仅支持 direct IO。在采用 aysnc IO 的时候,只能使用 O_DIRECT,不能借助文件系统缓存来缓存当前的 IO 请求,还存在 size 对齐(直接操作磁盘,所有写入内存块数量必须是文件系统块大小的倍数,而且要与内存页大小对齐)等限制,这直接影响了 aio 在很多场景的使用。
从图中流程看,例如 read 请求来说,direct IO 的模式会把从盘上读取的数据直接返回给了用户态的内存空间,不会在 kernel 中缓存,当存在多次重复读取的场景,每次都需要读盘,大大增加了 kernel 的负担。
(2) 仍然可能被阻塞。即使应用层主观上,希望系统层采用异步 IO,但是客观上,有时候还是可能会被阻塞。
(3) 拷贝开销大。每个 IO 提交需要拷贝 64+8 字节,每个 IO 完成需要拷贝 32 字节,总共 104 字节的拷贝。这个拷贝开销是否可以承受,和单次 IO 大小有关:如果需要发送的 IO 本身就很大,相较之下,这点消耗可以忽略,而在大量小 IO 的场景下,这样的拷贝影响比较大。
(4) API 不友好。每一个 IO 至少需要两次系统调用才能完成(submit 和 wait-for-completion),需要非常小心地使用完成事件以避免丢事件。
io_uring 的优势
io_uring 围绕高效进行设计,其设计了一对共享的 ring buffer 用于应用和内核之间的通信,通过该设计实现了如下的三个好处:
(1)避免在提交和完成事件中存在内存拷贝;
(2)避免了 libaio 中在提交和完成任务的时候系统调用过程;
(3)该队列采用了无锁的访问模式,通过内存屏障减少了竞争;
在共享的 ring buffer 设计中,针对提交队列(SQ),应用是 IO 提交的生产者(producer),内核是消费者(consumer);反过来,针对完成队列(CQ),内核是完成事件的生产者,应用是消费者。(类似rdma)
另外,io_uring 还存在如下的优势:
(1)提交和完成不需要经过系统调用,而且减少了对用户态线程的阻塞;该部分的支持主要通过共享的 ring buffer 和设置 polling 模式来实现。
(2)支持 Block 层的 polling 模式
(3)支持 buffered IO,充分利用缓存,减少数据碰盘产生的系统延迟;
io_uring 的实现
名称解释:
用户态接口:
io_uring 的实现仅仅使用了三个用户态的系统调用接口:
(1)io_uring_setup:初始化一个新的 io_uring 上下文,内核通过一块和用户共享的内存区域进行消息的传递。
(2)io_uring_enter:提交任务以及收割任务。
(3)io_uring_register:注册用户态和内核态的共享 buffer。
使用前两个系统调用已经足够使用 io_uring 接口了。
io_uring 的接口虽然简单,但操作起来有些复杂,需要手动 mmap 来映射内存。可以看到,io_uring 是完全为性能而生的新一代 native async IO 模型,比 libaio 高级不少。通过全新的设计,共享内存,IO 过程不需要系统调用,由内核完成 IO 的提交, 以及 IO completion polling 机制,实现了高 IOPS,高 Bandwidth。相比 kernel bypass(spdk nvme),这种 native 的方式显得友好一些。
liburing实现异步cat操作
int main(int argc, char *argv[]) {
struct io_uring ring;
if (argc < 2) {
fprintf(stderr, "Usage: %s [file name] <[file name] ...>\n",
argv[0]);
return 1;
}
/* Initialize io_uring */
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
for (int i = 1; i < argc; i++) {
int ret = submit_read_request(argv[i], &ring);
if (ret) {
fprintf(stderr, "Error reading file: %s\n", argv[i]);
return 1;
}
get_completion_and_print(&ring);
}
/* Call the clean-up function. */
io_uring_queue_exit(&ring);
return 0;
}