AIO的实现

思考一个问题:aio 怎么通知到最初的线程的?

Linux AIO 的实现

Linux Native AIO 是原生 AIO,Linux 存在很多第三方异步 IO 库,如 libeioglibc AIO。很多第三方的异步 IO 库不是真正的异步 IO,而是用多线程来模拟异步 IO,如 libeio 就是使用多线程来模拟异步 IO 的。

image.png

  • 程序调用 io_submit 发起异步 IO 操作后,会向内核的 IO 任务队列中添加一个 IO 任务,并返回成功。
  • 内核会在后台处理队列中的任务,把处理结果存储在 IO 任务中。
  • 应用程序可调用 io_getevents 系统调用来获取异步 IO 的处理结果,如果 IO 操作还没完成,那么返回失败信息,否则会返回 IO 处理结果。

主要由两个步骤组成:

  • 调用 io_submit 函数发起一个异步 IO 操作。
  • 调用 io_getevents 函数获取异步 IO 的结果。

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

#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <libaio.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

#define FILEPATH "./aio.txt"

int main()
{
    io_context_t context;
    struct iocb io[1], *p[1] = {&io[0]};
    struct io_event e[1];
    unsigned nr_events = 10;
    struct timespec timeout;
    char *wbuf;
    int wbuflen = 1024;
    int ret, num = 0, i;

    posix_memalign((void **)&wbuf, 512, wbuflen);

    memset(wbuf, '@', wbuflen);
    memset(&context, 0, sizeof(io_context_t));

    timeout.tv_sec = 0;
    timeout.tv_nsec = 10000000;

    int fd = open(FILEPATH, O_CREAT|O_RDWR|O_DIRECT, 0644); // 1. 打开要进行异步IO的文件
    if (fd < 0) {
        printf("open error: %d\n", errno);
        return 0;
    }

    if (0 != io_setup(nr_events, &context)) {               // 2. 创建一个异步IO上下文
        printf("io_setup error: %d\n", errno);
        return 0;
    }

    io_prep_pwrite(&io[0], fd, wbuf, wbuflen, 0);           // 3. 创建一个异步IO任务

    if ((ret = io_submit(context, 1, p)) != 1) {            // 4. 提交异步IO任务
        printf("io_submit error: %d\n", ret);
        io_destroy(context);
        return -1;
    }

    while (1) {
        ret = io_getevents(context, 1, 1, e, &timeout);     // 5. 获取异步IO的结果
        if (ret < 0) {
            printf("io_getevents error: %d\n", ret);
            break;
        }

        if (ret > 0) {
            printf("result, res2: %d, res: %d\n", e[0].res2, e[0].res);
            break;
        }
    }
    return 0;
}

主要有以下步骤:

  • 调用 open 系统调用打开要进行异步 IO 的文件,要注意的是 AIO 操作必须设置 O_DIRECT 直接 IO 标志位。
  • 调用 io_setup 系统调用创建一个异步 IO 上下文。
  • 调用 io_prep_pwriteio_prep_pread 函数创建一个异步写或读任务。
  • 调用 io_submit 系统调用把异步 IO 任务提交到内核。
  • 任务完成后会放到环形缓冲区 ring_info
  • 调用 io_getevents 系统调用获取异步 IO 的结果。

在上面的例子中,获取异步 IO 操作的结果是在一个无限循环中进行的, Linux 还支持一种基于 eventfd 事件通知的机制,可以通过 eventfdepoll 结合来实现事件驱动的方式来获取异步 IO 操作的结果。

glibc 的 AIO

  1. 异步请求被提交到 request_queue 中;request_queue 是个表结构,"行"是 fd、"列"是具体的请求。同一个 fd 的请求会被组织在一起;
  2. 请求有优先级概念,同一个 fd 的请求会按优先级排序执行。
  3. 随着异步请求提交,一些异步处理线程被动态创建。其从 request_queue 中取出请求处理;为避免异步处理线程间的竞争,同一 fd 所对应请求只由一个线程来处理;
  4. 异步处理线程同步处理每个请求,完成后在对应的 aiocb 中填充结果,然后触发可能的信号通知或回调(回调函数是需要创建新线程来调用的);
  5. 异步处理线程在完成某个 fd 所有请求后,进入闲置状态;如果 queue 中有新 fd 加入,则重新工作。闲置一段后,自动退出。有新请求时,再动态创建;

Libaio 内核级别

image.png

  1. 主线程调用 eio_init 函数,初始化 req_queue、res_queue(响应队列)及对应 mutex(互斥锁)和 cond(pthread,Linux 多线程部分);
  2. 所有 IO 操作其实都是对 eio_sumbit 的调用,而 eio_sumbit 的职能是将 IO 操作封装为 request 并插入到 req_queue;并调用 cond_signal 向 worker 线程发出 reqwait 已 OK 的信号;
  3. worker 线程被创建后执行的函数为 etp_proc,其启动后会一直等待 reqwait 条件出现;当 reqwait 条件满足时,etp_proc 从 req_queue 中取得一个 request;并调用 eio_execute 来同步执行该 IO 操作;
  4. eio_execute 完成后,将 response 插入到 res_queue ;并调用 want_poll 来通知主线程 request 已经处理完毕;(worker 线程通知主线程机制是通过向 pipe[1]写一个 byte 数据);
  5. 主线程发现 pipe[0]可读时,调用 eio_poll;从 res_queue 里取 response,并调用该 IO 操作在 init 时设置的 callback 函数;
  6. res_queue 中没有待处理 response 时,调用 done_poll;done_poll 从 pipe[0]读出一个 byte 数据,该 IO 操作完成。
  7. o_getevents 返回结果

内核级 AIO 与用户线程级别的 AIO(glibc 和 libaio)的比较

  1. 原理
    1. linux 版本的 AIO 实际上是利用了 CPU 和 IO 设备可异步工作的特性(IO 请求的提交主要还是在调用者线程上同步完成的)。相比同步 IO,不会占用额外 CPU 资源。
    2. glibc 版本的异步 IO 则利用了线程与线程之间可以异步工作的特性,使用新的线程来完成 IO 请求,会额外占用 CPU 资源(且调用线程和异步处理线程之间还存在线程间通信开销)。不过,IO 请求提交过程都由异步处理线程完成了,调用者线程可以更快地响应其他事情。如果 CPU 资源富足,还不错。
  2. 当调用者连续调用异步 IO 接口,提交多个异步 IO 请求时。
    1. 在 glibc 版本的异步 IO 中,同一个 fd 的读写请求由同一个异步处理线程来同步完成。所以,对底层的 IO 调度器来说,它一次只能看到一个请求。
    2. 内核实现的异步 IO,则是直接将所有请求都提交给了 IO 调度器,IO 调度器能看到所有的请求。请求多了,IO 调度器使用的类电梯算法就能发挥更大功效。请求少了,极端情况下(如系统中的 IO 请求都集中在同一个 fd 上,且不使用预读),IO 调度器总是只能看到一个请求,那么电梯算法将退化成先来先服务算法,可能会极大的增加碰头移动的开销。
  3. direct-io
    1. glibc 版本的异步 IO 支持非 direct-io,可利用内核提供的 page cache 来提高效率。
    2. 而 linux 版本只支持 direct-io,cache 的工作就只能靠用户程序来实现了。

参考

aio 实现

操作系统层面聊聊 BIO,NIO 和 AIO (epoll)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇