跳转至

io_uring 系统性整理

这里有个误解,I/O 模型其实是针对整个系统的所有 I/O 操作的,但是平时很少对文件系统使用异步读写,同步或直接映射的情况比较多。更别提多路复用了,这个机制基本只用在 network 中。

lwn Kernel article index

I/O 模型

  • blocking I/O

blocking

同步阻塞,直到内核收到数据返回给线程。

  • nonblocking I/O

nonblocking

同步不阻塞,但是如果内核没收到数据会返回一个 EWOULDBLOCK

  • I/O multiplexing (select and poll)

multiplex

异步阻塞,使用 selet(using select requires two system calls instead of one)、poll 系统调用循环等待 socket 可读时,使用 recvfrom 收取数据。主要优势在于能够在单线程监控多个文件描述符 fd。

初次之外还有 epoll,使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次

优点有: 没有最大并发连接的限制,能打开的 FD 的上限远大于 1024(1G 的内存上能监听约 10 万个端口)。 效率提升,不是轮询的方式,不会随着 FD 数目的增加效率下降。只有活跃可用的 FD 才会调用 callback 函数;即 Epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll 的效率就会远远高于 select 和 poll。 内存拷贝,利用 mmap() 文件映射内存加速与内核空间的消息传递;即 epoll 使用 mmap 减少复制开销。

这种方法基本等价于 一个进程创建多个线程,每个线程维护一个 blocking I/O

  • signal driven I/O (SIGIO)

multiplex

非阻塞,通过 sigaction 系统调用安装 signal handler,当 datagram 数据报可读时,向 I/O 接收进程发送 SIGIO 信号,可以在 signal handler 里面读这个数据,然后通知 main loop;也可以先通知 main loop,让 main loop 去读这个数据。

  • asynchronous I/O (the POSIX aio_functions)

aio

异步非阻塞,也是调用 aio_read 之后立刻返回,和 SIGIO 的区别是直到接收到数据并将数据传输到用户时,才产生完成信号。

comparison

comparison

参考: io models 彻底理解 IO多路复用 聊聊IO多路复用之select、poll、epoll详解

Asynchronous I/O

首先确定这里的 AIO 是内核态的,由 libaio 封装的系统调用运行库,而不是 glibc 用户态 AIO,使用多线程模拟的。

linux kernel AIO 的主要缺点在于项目泥潭,bug 太多,项目设计和领导更换,而且实现比较复杂,直到现在只能比较稳定支持以 O_DIRECT(直接映射修改,bypass page cache)方式打开文件,需要自己处理 buffer、offset 对其这些问题,不能用 page cache 层以 bio 的方式读写 block 数据。

因为使用 page buffer 层时涉及到 block driver 里面的队列,相比 O_DIRECT 多出很多阻塞点,因此实现起来比较令人恼火。因此这个项目根本就没实现起来。

因此 io_uring 的主要对比对象是多路复用和 DPDK、SPDK,是一个事实上的新异步 IO API

Linux AIO does suffer from a number of ailments. The subsystem is quite complex and requires explicit code in any I/O target for it to be supported.

实现不了的地方基本上都开一个 kernel thread 跑,感觉开销更大了。

参考: Linux Asynchronous I/O

Fixing asynchronous I/O, again Linux kernel AIO这个奇葩 2017Toward non-blocking asynchronous I/O

io_uring

参考:

Kernel Recipes 2019 - Faster IO through io_uring

20190115Ringing in a new asynchronous I/O API

20200715Operations restrictions for io_uring

20200320Automatic buffer selection for io_uring

20200124The rapid growth of io_uring

20200511Hussain: Lord of the io_uring

Linux异步IO新时代:io_uring

20200716io_uring: add restrictions to support untrusted applications and guests

20200225io_uring support for automatic buffers

io_uring(1) – 我们为什么会需要 io_uring

linux “io_uring” 提权漏洞(CVE-2019-19241)分析

io_uring(2)- 从创建必要的文件描述符 fd 开始

uring 这个词没有翻译”something that looks a little less like io_urine”.

这是一个为了高速 I/O 提出的新的一系列系统调用,简单来说就是新的 ring buffer。之前的异步 I/O 策略是 libaio,这个机制饱受诟病,于是 Jens Axboe 直接提出 io_uring,性能远超 aio。

从 5.7 开始超出纯 I/O 的范畴,io_uring 开始为一部接口提供 FAST POLL 机制,用户无需再像使用 select、event poll 等多路复用机制来监听文件句柄,只要把读写请求直接丢到 io_uring 的 submission queue 中提交 ,当文件句柄不可读写时,内核会主动添加 poll handler,当文件句柄可读写时主动调用 poll handler 再次下发读写请求,从而减少系统调用次数提高性能

这是一个线程粒度的异步 I/O 机制,分为 submission queue 和 completion queue,在使用系统调用申请之后,直接返回可以使用 mmap 映射的 file discriptor。

应用程序可以直接使用 mmap 映射的两个 ring buffer 直接与内核进行 I/O 数据传输交换,减少了大量系统调用的开销。

具体流程:

  • setup int io_uring_setup(int entries, struct io_uring_params *params);

其中 entries 表示 submission and completion queues 两个队列的大小

param 中设置两个队列和具体的状态

struct io_uring_params {
    __u32 sq_entries;
    __u32 cq_entries;
    __u32 flags;
    __u16 resv[10];
    struct io_sqring_offsets sq_off;
    struct io_cqring_offsets cq_off;
};

最终实现目的通过 file descriptor 与内核共享 ring buffer

subqueue = mmap(0, params.sq_off.array + params.sq_entries*sizeof(__u32),
    PROT_READ|PROT_WRITE|MAP_SHARED|MAP_POPULATE,
             ring_fd, IORING_OFF_SQ_RING);

sqentries = mmap(0, params.sq_entries*sizeof(struct io_uring_sqe),
    PROT_READ|PROT_WRITE|MAP_SHARED|MAP_POPULATE,
            ring_fd, IORING_OFF_SQES);


cqentries = mmap(0, params.cq_off.cqes + params.cq_entries*sizeof(struct io_uring_cqe),
      PROT_READ|PROT_WRITE|MAP_SHARED|MAP_POPULATE,
            ring_fd, IORING_OFF_CQ_RING);

相关资料: Ringing in a new asynchronous I/O API The rapid growth of io_uring

#include <liburing.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
    struct io_uring ring;
    io_uring_queue_init(32, &ring, 0);

    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    int fd = open("/home/carter/test.txt", O_WRONLY | O_CREAT);
    struct iovec iov = {
        .iov_base = "Hello world",
        .iov_len = strlen("Hello world"),
    };
    io_uring_prep_writev(sqe, fd, &iov, 1, 0);
    io_uring_submit(&ring);

    struct io_uring_cqe *cqe;

    for (;;) {
        io_uring_peek_cqe(&ring, &cqe);
        if (!cqe) {
            puts("Waiting...");
            // accept 新连接,做其他事
        } else {
            puts("Finished.");
            break;
        }
    }
    io_uring_cqe_seen(&ring, cqe);
    io_uring_queue_exit(&ring);
}

总结邮件

今天重新思考了一下 IO 模型,并阅读了 io_uring 和多路复用相关代码,感觉突然想通了。io_uring 取代了 AIO 而不是取代了 usercopy,usercopy 部署的安全策略未必适用擅长传输大量数据的 io_uring,这个工作可以推后再进行。具体如下。

一、将 I/O 模型的设计和实现分离

I/O 在操作系统中含义包括与 I/O 设备通信和输入输出数据,I/O 模型是针对第一种含义提出的解决方案。

linux 在实现 I/O 的过程中参考了这些模型进行实现,但并没有在同一层次进行实现。例如 LKM 开发中定义的 file_operations 实际只包括 read(同步)/read_iter(异步)/mmap/poll 等函数指针,在 I/O 模型中的阻塞同步和非阻塞同步的情况可以通过使用 read/read_iter 附加 O_NONBLOCK 的方式实现。

select epoll 多路复用和 AIO 这些 I/O 模型,则是分别在与之相同或不同的层次对底层函数进行封装。

例如 select 系统调用是在内核层通过 vfs_poll 遍历相关的 file_descriptor,glibc 实现的 AIO 是在用户空间多线程调用这些阻塞/非阻塞的同步/异步系统调用,epoll(更像是一个通知机制)是将 select/poll 中需要每次都传递的 file descriptor 都保存在内核中,减少了 usercopy;通过 event 监听 callback 进行通知,减少了对 fd 的遍历开销。

二、思考 io_uring 的设计和实现

io_uring 的设计借鉴了以上的优点,在内核空间通过 kthread 实现对阻塞读写任务的托管,并加入了 zero copy 特性,开发者可以通过一次系统调用唤醒线程一直向共享 ringbuffer 中写数据,而不是每次写数据都需要系统调用,这在内核和用户通信范畴内很大程度上减少了系统调用的次数,消除了 usercopy 的负担。

但无法否认 io_uring 是对下层 file_operations 的封装,下层函数又是 device driver file_operations 的封装(甚至对 buffer I/O 中间还有一层 page cache、一层 block layer、一层 I/O schedule),因此 io_uring 在许多情况无法获得 SPDK 用户空间直通 driver 的性能优势。

三、对安全问题的思考

我目前理解的安全风险主要来自于 usercopy 造成的 out-of-bound、information/pointer leakage 和 race 情况,尤其是 struct 结构可能存在的函数/数据指针,但是 io_uring 消除掉的 usercopy 主要负责大量 I/O 数据的传输,而非带有指针的控制数据结构(io_uring 中的控制数据也在用 copy_*_user 传输,如图),因此对安全问题的认识比我预期要复杂一些(主要问题可能是 OOB 和 Iago 攻击),需要加深对漏洞形式的理解,但好处是急迫程度下降了。

我只能继续积累漏洞阅读量提升认知水平,思考 copy_*_user 可以部署的安全机制和策略。

usercopy

四、总结

我这阶段应该继续把重点放在 kernel extension 问题的描述上,对这个问题我已经基本有了一定想法,大致是将威胁模型定位为 kernel rootkits,通过修改页表或切换地址空间构建运行时沙箱,使用可信基截获、保护 gateway,使用 hook 方式监控 driver 相关函数和数据 I/O。可以将性能的提升和对 DPDK/SPDK 使用的 UIO 和 VFIO 保护作为贡献点(这可能是这次突发奇想的意外收获),现在面临的问题是不确定相关方案是否有实现、近期 driver 保护方案相关只有四篇。

原文链接:https://www.zi-c.wang/2020/11/15/io-uring-%E7%B3%BB%E7%BB%9F%E6%80%A7%E6%95%B4%E7%90%86/