Bootstrap

Linux AIO实现原理

前言

最近在看Rocksdb中关于异步I/O与预读的代码,有点不太理解的是如果linux内核版本不够高的话,Rocksdb异步I/O会完全回退到同步的I/O操作,而不是尝试采用linux的另一种aio的方式来达到异步。同时之前或多或少接触过io_uring的知识,这是正好有机会做个总结,和aio对比一下。内容总共分为两篇,这一篇主要用图解的方式讲明aio的实现原理和流程。


异步I/O

有关同步与异步I/O的对比以及常见的5种I/O模型,会在另外一篇文章中讲述,在这里就简单说明。异步I/O是指当一个进程发起I/O请求之后,不等待结果,而是由内核在I/O请求完成之后通过信号或者回调的方式通知进程。

在经典的计算机存储结构中,设备的存取速度自上而下由快到慢。数据流从最上层的寄存器(L1/2/3 cache)开始,到达内存DRAM,进而到达持久化磁盘。同时我们也知道处理器的运行速度远远快过内存的速度,更远远快于从磁盘上存取的数据的速度(暂时不去考虑近年来不断涌现的新的存储介质,例如SSD/NVMe盘,持久化内存等)。于是,当处理器发出一次I/O请求时,总是会存在一个等待磁盘将数据返回给处理器的时间差。为了不让CPU在这段时间进行忙等,就有了中断的操作,让CPU先去处理其他的事情,在磁盘数据准备完成之后再通过中断的方式通知CPU。有关中断的详细讲解也可以等我后续更新的另一篇文章。

Linux aio

Linux下的aio接口是Linux内核支持的原生异步I/O接口,完全不同于一些利用线程模拟异步的第三方aio库。aio接口最适合的场景是通过O_DIRECT绕过page cache访问块存储设备,例如磁盘或者存储阵列。

调用流程

  • io_setup创建一个异步I/O请求
  • io_submit向内核的I/O任务队列中提交一个I/O请求。内核会在后台进行调度处理任务队列中的I/O任务,然后在执行后将结果存储在I/O任务中。
  • 进程通过io_getevents获取I/O请求状态,如果返回失败则请求还没有完成,否则返回请求结果。

实现原理

异步I/O上下文的表示

在Linux内核中,异步I/O上下文由kioctx结构体表示。

struct kioctx {
    atomic_t                users;    // 引用计数器
    int                     dead;     // 是否已经关闭
    struct mm_struct        *mm;      // 对应的内存管理对象unsigned long           user_id;  // 唯一的ID,用于标识当前上下文, 返回给用户
    struct kioctx           *next;wait_queue_head_t       wait;     // 等待队列
    spinlock_t              ctx_lock; // 锁int                     reqs_active; // 正在进行的异步IO请求数
    struct list_head        active_reqs; // 正在进行的异步IO请求对象
    struct list_head        run_list;unsigned                max_reqs;  // 最大IO请求数struct aio_ring_info    ring_info; // 环形缓冲区struct work_struct      wq;
};

在kioctx的成员变量中,比较重要的是用来表示I/O任务队列环的wait,用来保存I/O操作结果的环形缓冲区ring_info,以及保存所有正在处理的I/O请求的链表active_reqs。整体的结构如下图所示。
在这里插入图片描述
可以看到,active_reqs是由一个一个的kiocb结构体组成。先看它的定义,

struct kiocb {
    ...
    struct file         *ki_filp;     
    struct kioctx       *ki_ctx;
    ...
    struct list_head    ki_list;
    __u64               ki_user_data;
    loff_t              ki_pos;
    ...
};
  • ki_flip:异步I/O操作的文件对象。
  • ki_ctx:异步I/O的上下文
  • ki_list:这个很好理解,维护链表构成的指针
  • ki_user_data:用户data指针
  • ki_pos:异步I/O文件offset

至于aio_ring_info,其相当于环形缓冲区的一个metadata区域。同样先看定义,

struct aio_ring_info {
    unsigned long       mmap_base;     // 环形缓冲区的虚拟内存地址
    unsigned long       mmap_size;     // 环形缓冲区的大小struct page         **ring_pages;  // 环形缓冲区所使用的内存页数组
    spinlock_t          ring_lock;     // 保护环形缓冲区的自旋锁
    long                nr_pages;      // 环形缓冲区所占用的内存页数unsigned            nr, tail;
#define AIO_RING_PAGES  8
    struct page         *internal_pages[AIO_RING_PAGES]; 
};

struct aio_ring {
    unsigned    id;
    unsigned    nr;    // 环形缓冲区可容纳的 io_event 数
    unsigned    head;  // 环形缓冲区的开始位置
    unsigned    tail;  // 环形缓冲区的结束位置
    ...
};

环形缓冲区的作用是存储I/O请求的执行结果,通过head以及tail之前的相互运算来判断是否为空。这里的aio_ring_info相当于环形缓冲区的元数据,真正的缓冲区数据需要对ring_pages调用kmap_atomic()来建立虚拟内存的映射来获取。注意这里有一个优化,如果ring buffer的大小不大于 8 个内存页,那么ring_pages字段就指向internal_pages 字段。这样的好处是避免了后续再去调用kmap_atomic()来建立虚拟内存的映射的开销。


介绍完涉及到的数据结构,下面我们分步来看aio的每一个系统调用都做了些什么。

  • 设置异步I/O上下文
    aio调用链
    • 这一步最关键的调用是ioctx_alloc(),它负责分配一个异步I/O上下文,也就是kioctx,然后初始化其中的active_reqs以及ring buffer。
  • 提交异步I/O请求
    在这里插入图片描述
    • lookup_ioctx():获取异步I/O上下文。
    • copy_from_user(iocb):将用户指定的iocb从内核态拷贝到用户态。
    • io_submit_one():提交异步I/O请求。
    • aio_get_req():创建一个I/O请求对象,也就是kiocb,设置该结构体相应域的值,然后放入active_req中。
    • aio_read()/aio_write():这里就是具体的aio读写的调用了,具体的实现取决于具体接入的文件系统。
    • aio_complete():在I/O请求完成之后,内核调用该函数将结果放入ring buffer中,见下文。在这里插入图片描述
    • kmap_atomic():对ring buffer中的ring_pages建立虚拟内存的映射,构建aio_ring
    • aio_ring_event():在ring buffer中获取一个空闲的io_event保存I/O操作的结果。
    • 设置event各个域的值。
    • put_aio_ring_event():将设置好的io_event放入ring buffer中。
    • kunmap_atomic():解除虚拟内存映射。
  • 获取异步I/O请求结果
    在这里插入图片描述
    • read_events():获取下一个可以拿到的io_event。
    • aio_read_evt():在循环中寻找环形缓冲区中的下一个可以拿到的结果,如果为空就退出。
    • aio_ring_event() :根据环形缓冲区当前head指针指向的io_event,将结果存到要返回的io_event中。
小结

由此我们就已经分析完整个Linux aio的结构框架了,包括相关的数据结构(kioctx, kiocb, io_event, aio_ring_info, aio_ring, iocb等等),以及各个步骤调用的详细原理。再结合下面这个完整的流程图,可以更好的理解。
在这里插入图片描述

io_uring

io_uring是Linux内核进入5.0时代之后带来的异步I/O框架。下一篇就来讲讲io_uring。

;