欢迎光临散文网 会员登陆 & 注册

带你全面了解Linux原生异步 IO 原理与使用和 AIO 实现(Native AIO)(超级详细)

2022-04-18 18:45 作者:补给站Linux内核  | 我要投稿

什么是异步 IO?

异步 IO:当应用程序发起一个 IO 操作后,调用者不能立刻得到结果,而是在内核完成 IO 操作后,通过信号或回调来通知调用者。

异步 IO 与同步 IO 的区别如 图1 所示:


  • 从上图可知,同步 IO 必须等待内核把 IO 操作处理完成后才返回。而异步 IO 不必等待 IO 操作完成,而是向内核发起一个 IO 操作就立刻返回,当内核完成 IO 操作后,会通过信号的方式通知应用程序。

Linux 原生 AIO 原理

  • Linux Native AIO 是 Linux 支持的原生 AIO,为什么要加原生这个词呢?因为Linux存在很多第三方的异步 IO 库,如 libeio 和 glibc AIO。所以为了加以区别,Linux 的内核提供的异步 IO 就称为原生异步 IO。

  • 很多第三方的异步 IO 库都不是真正的异步 IO,而是使用多线程来模拟异步 IO,如 libeio 就是使用多线程来模拟异步 IO 的。


  • 本文主要介绍 Linux 原生 AIO 的原理和使用,所以不会对其他第三方的异步 IO 库进行分析,下面我们先来介绍 Linux 原生 AIO 的原理。

  • 如 图2 所示:

  • Linux 原生 AIO 处理流程:

  1. 当应用程序调用 io_submit 系统调用发起一个异步 IO 操作后,会向内核的 IO 任务队列中添加一个 IO 任务,并且返回成功。

  2. 内核会在后台处理 IO 任务队列中的 IO 任务,然后把处理结果存储在 IO 任务中。

  3. 应用程序可以调用 io_getevents 系统调用来获取异步 IO 的处理结果,如果 IO 操作还没完成,那么返回失败信息,否则会返回 IO 处理结果。

  • 从上面的流程可以看出,Linux 的异步 IO 操作主要由两个步骤组成:

  1. 调用 io_submit 函数发起一个异步 IO 操作。

  2. 调用 io_getevents 函数获取异步 IO 的结果。

  • 下面我们主要分析,Linux 内核是怎么实现异步 IO 的。


【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码) 


  • 在介绍 Linux 原生 AIO 的实现之前,先通过一个简单的例子来介绍其使用过程:

  • 上面通过一个简单的例子来展示了 Linux 原生 AIO 的使用过程,主要有以下步骤:

  1. 通过调用 open 系统调用打开要进行异步 IO 的文件,要注意的是 AIO 操作必须设置 O_DIRECT 直接 IO 标志位。

  2. 调用 io_setup 系统调用创建一个异步 IO 上下文。

  3. 调用 io_prep_pwrite 或者 io_prep_pread 函数创建一个异步写或者异步读任务。

  4. 调用 io_submit 系统调用把异步 IO 任务提交到内核。

  5. 调用 io_getevents 系统调用获取异步 IO 的结果。

  • 在上面的例子中,我们获取异步 IO 操作的结果是在一个无限循环中进行的,其实 Linux 还支持一种基于 eventfd 事件通知的机制,可以通过 eventfd 和 epoll 结合来实现事件驱动的方式来获取异步 IO 操作的结果,有兴趣可以查阅相关的内容。


  • 上面主要介绍了 Linux 原生 AIO 的原理和使用,Linux 原生 AIO 的使用比较简单,但其内部实现比较复杂,下面介绍 Linux 原生 AIO 的实现过程.

本文基于 Linux-2.6.0 版本内核源码

  • 一般来说,使用 Linux 原生 AIO 需要 3 个步骤:

  1. 调用 io_setup 函数创建一个一般 IO 上下文。

  2. 调用 io_submit 函数向内核提交一个异步 IO 操作。

  3. 调用 io_getevents 函数获取异步 IO 操作结果。

  • 所以,我们可以通过分析这三个函数的实现来理解 Linux 原生 AIO 的实现。

Linux 原生 AIO 实现在源码文件 /fs/aio.c 中。

创建异步 IO 上下文

  • 要使用 Linux 原生 AIO,首先需要创建一个异步 IO 上下文,在内核中,异步 IO 上下文使用 kioctx 结构表示,定义如下:

  • 在 kioctx 结构中,比较重要的成员为 active_reqs 和 ring_info。active_reqs 保存了所有正在进行的异步 IO 操作,而 ring_info 成员用于存放异步 IO 操作的结果。


  • kioctx 结构如 图1 所示:

  • 如 图1 所示,active_reqs 成员保存的异步 IO 操作队列是以 kiocb 结构为单元的,而 ring_info 成员指向一个类型为 aio_ring_info 结构的环形缓冲区(Ring Buffer)。

  • 所以我们先来看看 kiocb 结构和 aio_ring_info 结构的定义:

  • kiocb 结构比较简单,主要用于保存异步 IO 操作的一些信息,如:

  • ki_filp:用于保存进行异步 IO 的文件对象。

  • ki_ctx:指向所属的异步 IO 上下文对象。

  • ki_list:用于连接当前异步 IO 上下文中的所有 IO 操作对象。

  • ki_user_data:这个字段主要提供给用户自定义使用,比如区分异步 IO 操作,或者设置一个回调函数等。

  • ki_pos:用于保存异步 IO 操作的文件偏移量。

  • 而 aio_ring_info 结构是一个环形缓冲区的实现,其定义如下:

  • 这个环形缓冲区主要用于保存已经完成的异步 IO 操作的结果,异步 IO 操作的结果使用 io_event 结构表示。如 图2 所示:

  • 图2 中的 head 代表环形缓冲区的开始位置,而 tail 代表环形缓冲区的结束位置,如果 tail 大于 head,则表示有完成的异步 IO 操作结果可以获取。如果 head 等于 tail,则表示没有完成的异步 IO 操作。

  • 环形缓冲区的 head 和 tail 位置保存在 aio_ring 的结构中,其定义如下:

  • 上面介绍了那么多数据结构,只是为了接下来的源码分析更加容易明白。


  • 现在,我们开始分析异步 IO 上下文的创建过程,异步 IO 上下文的创建通过调用 io_setup 函数完成,而 io_setup 函数会调用内核函数 sys_io_setup,其实现如下:

  • sys_io_setup 函数的实现比较简单,首先调用 ioctx_alloc 申请一个异步 IO 上下文对象,然后把异步 IO 上下文对象的标识符返回给调用者。


  • 所以,sys_io_setup 函数的核心过程是调用 ioctx_alloc  函数,我们继续分析 ioctx_alloc 函数的实现:

  • ioctx_alloc  函数主要完成以下工作:

  • 调用 kmem_cache_alloc 函数向内核申请一个异步 IO 上下文对象。

  • 初始化异步 IO 上下文各个成员变量,如初始化异步 IO 操作队列。

  • 调用 aio_setup_ring 函数初始化环形缓冲区。

  • 环形缓冲区初始化函数 aio_setup_ring 的实现有点小复杂,主要涉及内存管理的知识点,所以这里跳过这部分的分析,有兴趣的可以私聊我一起讨论。

  • 提交异步 IO 操作

  • 提交异步 IO 操作是通过 io_submit 函数完成的,io_submit 需要提供一个类型为 iocb 结构的数组,表示要进行的异步 IO 操作相关的信息,我们先来看看 iocb 结构的定义:

io_submit 函数最终会调用内核函数 sys_io_submit 来实现提供异步 IO 操作,我们来分析 sys_io_submit 函数的实现:

sys_io_submit 函数的实现比较简单,主要从用户空间复制异步 IO 操作信息到内核空间,然后调用 io_submit_one 函数提交异步 IO 操作。我们重点分析 io_submit_one 函数的实现:

  • 上面代码已经对 io_submit_one 函数进行了详细的注释,这里总结一下 io_submit_one 函数主要完成的工作:

  1. 通过调用 fget 函数获取文件句柄对应的文件对象。

  2. 调用 aio_get_req 函数获取一个类型为 kiocb 结构的异步 IO 操作对象,这个结构前面已经分析过。另外,aio_get_req 函数还会把异步 IO 操作对象添加到异步 IO 上下文的 active_reqs 队列中。

  3. 根据不同的异步 IO 操作类型来进行不同的处理,如 异步读操作 会调用文件对象的 aio_read 方法来进行处理。不同的文件系统,其 aio_read 方法的实现不一样,如 Ext3 文件系统的 aio_read 方法会指向 generic_file_aio_read 函数。

  4. 如果异步 IO 操作被添加到内核的 IO 请求队列中,那么就直接返回。否则就代表 IO 操作已经完成,那么就调用 aio_complete 函数完成收尾工作。

  • io_submit_one 函数的操作过程如 图3 所示:

  • 所以,io_submit_one 函数的主要任务就是向内核提交 IO 请求。

异步 IO 操作完成

  • 当异步 IO 操作完成后,内核会调用 aio_complete 函数来把处理结果放进异步 IO 上下文的环形缓冲区 ring_info 中,我们来分析一下 aio_complete 函数的实现:

  • aio_complete 函数的 iocb 参数是我们通过调用 io_submit_once 函数提交的异步 IO 对象,而参数 res 和 res2 是用内核进行 IO 操作完成后返回的结果。

  • aio_complete 函数的主要工作如下:

  • 根据环形缓冲区的 tail 指针获取一个空闲的 io_event 对象来保存 IO 操作的结果。

  • 对环形缓冲区的 tail 指针进行加一操作,指向下一个空闲的位置。

  • 当把异步 IO 操作的结果保存到环形缓冲区后,用户层就可以通过调用 io_getevents 函数来读取 IO 操作的结果,io_getevents 函数最终会调用 sys_io_getevents 函数。


  • 我们来分析 sys_io_getevents 函数的实现:

  • 从上面的代码可以看出,sys_io_getevents 函数主要调用 read_events 函数来读取异步 IO 操作的结果,我们接着分析 read_events 函数:

  • read_events 函数主要还是调用 aio_read_evt 函数来从环形缓冲区中读取异步 IO 操作的结果,如果读取成功,就把结果复制到用户空间中。

  • aio_read_evt 函数是从环形缓冲区中读取异步 IO 操作的结果,其实现如下:

  • aio_read_evt 函数的主要工作就是判断环形缓冲区是否为空,如果不为空就从环形缓冲区中读取异步 IO 操作的结果,并且保存到参数 ent 中,并且移动环形缓冲区的 head 指针到下一个位置。


带你全面了解Linux原生异步 IO 原理与使用和 AIO 实现(Native AIO)(超级详细)的评论 (共 条)

分享到微博请遵守国家法律