热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux原生异步IO原理与实现(NativeAIO)

linux服务器开发相关视频解析:linux服务器性能优化之异步的原理与实现网络底层io的那些事,redis,memcached

linux服务器开发相关视频解析:

linux服务器性能优化之异步的原理与实现
网络底层io的那些事,redis,memcached,nginx

c/c++ linux服务器开发免费学习地址:c/c++ linux后台服务器高级架构师


什么是异步 IO?

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

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

Linux原生异步IO原理与实现(Native AIO)

从上图可知,同步 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 的原理。

如图所示:

Linux原生异步IO原理与实现(Native AIO)

Linux 原生 AIO 处理流程:


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

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


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

【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

Linux原生异步IO原理与实现(Native AIO)


Linux原生AIO实现

一般来说,使用 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 结构表示,定义如下:

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 结构中,比较重要的成员为 active_reqs 和 ring_info。active_reqs 保存了所有正在进行的异步 IO 操作,而 ring_info 成员用于存放异步 IO 操作的结果。

kioctx 结构如图所示:

Linux原生异步IO原理与实现(Native AIO)

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

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

struct kiocb {...struct file *ki_filp; // 异步IO操作的文件对象struct kioctx *ki_ctx; // 指向所属的异步IO上下文...struct list_head ki_list; // 用于连接所有正在进行的异步IO操作对象__u64 ki_user_data; // 用户提供的数据指针(可用于区分异步IO操作)loff_t ki_pos; // 异步IO操作的文件偏移量...
};

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


  • ki_filp:用于保存进行异步 IO 的文件对象。
  • ki_ctx:指向所属的异步 IO 上下文对象。
  • ki_list:用于连接当前异步 IO 上下文中的所有 IO 操作对象。
  • ki_user_data:这个字段主要提供给用户自定义使用,比如区分异步 IO 操作,或者设置一个回调函数等。
  • ki_pos:用于保存异步 IO 操作的文件偏移量。

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

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;// 如果环形缓冲区不大于 8 个内存页时// ring_pages 就指向 internal_pages 字段
#define AIO_RING_PAGES 8struct page *internal_pages[AIO_RING_PAGES];
};

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

Linux原生异步IO原理与实现(Native AIO)

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

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

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

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

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

asmlinkage long sys_io_setup(unsigned nr_events, aio_context_t *ctxp)
{struct kioctx *ioctx = NULL;unsigned long ctx;long ret;...ioctx = ioctx_alloc(nr_events); // 调用 ioctx_alloc 函数创建异步IO上下文ret = PTR_ERR(ioctx);if (!IS_ERR(ioctx)) {ret = put_user(ioctx->user_id, ctxp); // 把异步IO上下文的标识符返回给调用者if (!ret)return 0;io_destroy(ioctx);}
out:return ret;
}

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

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

static struct kioctx *ioctx_alloc(unsigned nr_events)
{struct mm_struct *mm;struct kioctx *ctx;...ctx = kmem_cache_alloc(kioctx_cachep, GFP_KERNEL); // 申请一个 kioctx 对象...INIT_LIST_HEAD(&ctx->active_reqs); // 初始化异步 IO 操作队列...if (aio_setup_ring(ctx) < 0) // 初始化环形缓冲区goto out_freectx;...return ctx;...
}

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


  • 调用 kmem_cache_alloc 函数向内核申请一个异步 IO 上下文对象。
  • 初始化异步 IO 上下文各个成员变量,如初始化异步 IO 操作队列。
  • 调用 aio_setup_ring 函数初始化环形缓冲区。

环形缓冲区初始化函数 aio_setup_ring 的实现有点小复杂,主要涉及内存管理的知识点,所以这里跳过这部分的分析。

提交异步 IO 操作

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

struct iocb {__u64 aio_data; // 用户自定义数据, 可用于标识IO操作或者设置回调函数...__u16 aio_lio_opcode; // IO操作类型, 如读(IOCB_CMD_PREAD)或者写(IOCB_CMD_PWRITE)操作__s16 aio_reqprio;__u32 aio_fildes; // 进行IO操作的文件句柄__u64 aio_buf; // 进行IO操作的缓冲区(如写操作的话就是写到文件的数据)__u64 aio_nbytes; // 缓冲区的大小__s64 aio_offset; // IO操作的文件偏移量...
};

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

asmlinkage long
sys_io_submit(aio_context_t ctx_id, long nr, struct iocb __user **iocbpp)
{struct kioctx *ctx;long ret = 0;int i;...ctx = lookup_ioctx(ctx_id); // 通过异步IO上下文标识符获取异步IO上下文对象...for (i = 0; i < nr; i++) {struct iocb __user *user_iocb;struct iocb tmp;if (unlikely(__get_user(user_iocb, iocbpp+i))) {ret = -EFAULT;break;}// 从用户空间复制异步IO操作到内核空间if (unlikely(copy_from_user(&tmp, user_iocb, sizeof(tmp)))) {ret = -EFAULT;break;}// 调用 io_submit_one 函数提交异步IO操作ret = io_submit_one(ctx, user_iocb, &tmp);if (ret)break;}put_ioctx(ctx);return i ? i : ret;
}

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

int io_submit_one(struct kioctx *ctx, struct iocb __user *user_iocb,struct iocb *iocb)
{struct kiocb *req;struct file *file;ssize_t ret;char *buf;...file = fget(iocb->aio_fildes); // 通过文件句柄获取文件对象...req = aio_get_req(ctx); // 获取一个异步IO操作对象...req->ki_filp = file; // 要进行异步IO的文件对象req->ki_user_obj = user_iocb; // 指向用户空间的iocb对象req->ki_user_data = iocb->aio_data; // 设置用户自定义数据req->ki_pos = iocb->aio_offset; // 设置异步IO操作的文件偏移量buf = (char *)(unsigned long)iocb->aio_buf; // 要进行异步IO操作的数据缓冲区// 根据不同的异步IO操作类型来进行不同的处理switch (iocb->aio_lio_opcode) {case IOCB_CMD_PREAD: // 异步读操作...ret = -EINVAL;// 发起异步IO操作, 会根据不同的文件系统调用不同的函数:// 如ext3文件系统会调用 generic_file_aio_read 函数if (file->f_op->aio_read)ret = file->f_op->aio_read(req, buf, iocb->aio_nbytes, req->ki_pos);break;...}...// 异步IO操作或许会在调用 aio_read 时已经完成, 或者会被添加到IO请求队列中。// 所以, 如果异步IO操作被提交到IO请求队列中, 直接返回if (likely(-EIOCBQUEUED == ret)) return 0;aio_complete(req, ret, 0); // 如果IO操作已经完成, 调用 aio_complete 函数完成收尾工作return 0;
}

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


  • 通过调用 fget 函数获取文件句柄对应的文件对象。
  • 调用 aio_get_req 函数获取一个类型为 kiocb 结构的异步 IO操作对象,这个结构前面已经分析过。另外,aio_get_req 函数还会把异步 IO 操作对象添加到异步 IO 上下文的active_reqs 队列中。
  • 根据不同的异步 IO 操作类型来进行不同的处理,如 异步读操作 会调用文件对象的 aio_read 方法来进行处理。不同的文件系统,其aio_read 方法的实现不一样,如 Ext3 文件系统的 aio_read 方法会指向 generic_file_aio_read函数。
  • 如果异步 IO 操作被添加到内核的 IO 请求队列中,那么就直接返回。否则就代表 IO 操作已经完成,那么就调用 aio_complete
    函数完成收尾工作。

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

Linux原生异步IO原理与实现(Native AIO)

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

异步 IO 操作完成

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

int aio_complete(struct kiocb *iocb, long res, long res2)
{struct kioctx *ctx = iocb->ki_ctx;struct aio_ring_info *info;struct aio_ring *ring;struct io_event *event;unsigned long flags;unsigned long tail;int ret;...info = &ctx->ring_info; // 环形缓冲区对象spin_lock_irqsave(&ctx->ctx_lock, flags); // 对异步IO上下文进行上锁ring = kmap_atomic(info->ring_pages[0], KM_IRQ1); // 对内存页进行虚拟内存地址映射tail = info->tail; // 环形缓冲区下一个空闲的位置event = aio_ring_event(info, tail, KM_IRQ0); // 从环形缓冲区获取空闲的位置保存结果tail = (tail + 1) % info->nr; // 更新下一个空闲的位置// 保存异步IO结果到环形缓冲区中event->obj = (u64)(unsigned long)iocb->ki_user_obj;event->data = iocb->ki_user_data;event->res = res;event->res2 = res2;...info->tail = tail;ring->tail = tail; // 更新环形缓冲区下一个空闲的位置put_aio_ring_event(event, KM_IRQ0); // 解除虚拟内存地址映射kunmap_atomic(ring, KM_IRQ1); // 解除虚拟内存地址映射// 释放异步IO对象ret = __aio_put_req(ctx, iocb);spin_unlock_irqrestore(&ctx->ctx_lock, flags);...return ret;
}

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 函数的实现:

asmlinkage long sys_io_getevents(aio_context_t ctx_id,long min_nr,long nr,struct io_event *events,struct timespec *timeout)
{struct kioctx *ioctx = lookup_ioctx(ctx_id);long ret = -EINVAL;...if (likely(NULL != ioctx)) {// 调用 read_events 函数读取IO操作的结果ret = read_events(ioctx, min_nr, nr, events, timeout);put_ioctx(ioctx);}return ret;
}

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

static int read_events(struct kioctx *ctx,long min_nr, long nr,struct io_event *event,struct timespec *timeout)
{long start_jiffies = jiffies;struct task_struct *tsk = current;DECLARE_WAITQUEUE(wait, tsk);int ret;int i = 0;struct io_event ent;struct timeout to;memset(&ent, 0, sizeof(ent));ret = 0;while (likely(i < nr)) {ret = aio_read_evt(ctx, &ent); // 从环形缓冲区中读取一个IO处理结果if (unlikely(ret <= 0)) // 如果环形缓冲区没有IO处理结果, 退出循环break;ret = -EFAULT;// 把IO处理结果复制到用户空间if (unlikely(copy_to_user(event, &ent, sizeof(ent)))) {break;}ret = 0;event++;i++;}if (min_nr <= i)return i;if (ret)return ret;...
}

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

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

static int aio_read_evt(struct kioctx *ioctx, struct io_event *ent)
{struct aio_ring_info *info = &ioctx->ring_info;struct aio_ring *ring;unsigned long head;int ret = 0;ring = kmap_atomic(info->ring_pages[0], KM_USER0);// 如果环形缓冲区的head指针与tail指针相等, 代表环形缓冲区为空, 所以直接返回if (ring->head == ring->tail) goto out;spin_lock(&info->ring_lock);head = ring->head % info->nr;if (head != ring->tail) {// 根据环形缓冲区的head指针从环形缓冲区中读取结果struct io_event *evp = aio_ring_event(info, head, KM_USER1);*ent = *evp; // 将结果保存到ent参数中head = (head + 1) % info->nr; // 移动环形缓冲区的head指针到下一个位置ring->head = head; // 保存环形缓冲区的head指针ret = 1;put_aio_ring_event(evp, KM_USER1);}spin_unlock(&info->ring_lock);out:kunmap_atomic(ring, KM_USER0);return ret;
}

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


总结

本文主要分析了 Linux 原生 AIO 的原理及实现,但为了不陷入太多的实现细节中,本文并没有涉及到磁盘 IO 相关的知识点。然而磁盘 IO 也是 AIO 实现中不可或缺的一部分,所以有兴趣的朋友可以通过阅读 Linux 的源码来分析其实现原理。


推荐阅读
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 本文介绍了OpenStack的逻辑概念以及其构成简介,包括了软件开源项目、基础设施资源管理平台、三大核心组件等内容。同时还介绍了Horizon(UI模块)等相关信息。 ... [详细]
  • 本文讨论了如何使用Web.Config进行自定义配置节的配置转换。作者提到,他将msbuild设置为详细模式,但转换却忽略了带有替换转换的自定义部分的存在。 ... [详细]
  • Centos下安装memcached+memcached教程
    本文介绍了在Centos下安装memcached和使用memcached的教程,详细解释了memcached的工作原理,包括缓存数据和对象、减少数据库读取次数、提高网站速度等。同时,还对memcached的快速和高效率进行了解释,与传统的文件型数据库相比,memcached作为一个内存型数据库,具有更高的读取速度。 ... [详细]
  • 三、查看Linux版本查看系统版本信息的命令:lsb_release-a[root@localhost~]#lsb_release-aLSBVersion::co ... [详细]
  • 浅析对象 VO、DTO、DO、PO 概念
    作者|CatQi链接|cnblogs.comqixuejiap4390086.html前言由于此订阅号换了个皮肤,导致用户接受文章不及时。读者可以打开订阅号「Web项 ... [详细]
  • Docker安装Rabbitmq(配合宝塔)
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Docker安装Rabbitmq(配合宝塔)相关的知识,希望对你有一定的参考价值。一、事前准备 ... [详细]
  • RabbitMQ的消息持久化处理
    1、RabbitMQ的消息持久化处理,消息的可靠性是RabbitMQ的一大特色,那么RabbitMQ是如何保证消息可靠性的呢——消息持久化。2、auto ... [详细]
author-avatar
丶原色调
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有