转自 https://www.cnblogs.com/cobbliu/articles/8487161.html
POSIX AIO 是在用户控件模拟异步 IO 的功能,不需要内核支持,而 linux AIO 则是 linux 内核原声支持的异步 IO 调用,行为更加低级
关于 linux IO 模型及 AIO、POSIX AIO 的简介,请参看:
libaio 实现的异步 IO 主要包含以下接口:
函数 | 功能 | 原型 |
io_setup | 创建一个异步IO上下文(io_context_t是一个句柄) | int io_setup(int maxevents, io_context_t *ctxp); |
io_destroy | 销毁一个异步IO上下文(如果有正在进行的异步IO,取消并等待它们完成) | int io_destroy(io_context_t ctx); |
io_submit | 提交异步IO请求 | long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp); |
io_cancel | 取消一个异步IO请求 | long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result); |
io_getevents | 等待并获取异步IO请求的事件(也就是异步请求的处理结果) | long io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout); |
iocb 结构
struct iocb主要包含以下字段:
1 struct iocb 2 { 3 /* 4 * 请求类型 5 * 如:IOCB_CMD_PREAD=读、IOCB_CMD_PWRITE=写、等 6 */ 7 __u16 aio_lio_opcode; 8 /* 9 * 要被操作的fd 10 */ 11 __u32 aio_fildes; 12 /* 13 * 读写操作对应的内存buffer 14 */ 15 __u64 aio_buf; 16 /* 17 * 需要读写的字节长度 18 */ 19 __u64 aio_nbytes; 20 /* 21 * 读写操作对应的文件偏移 22 */ 23 __s64 aio_offset; 24 /* 25 * 请求可携带的私有数据 26 * 在io_getevents时能够从io_event结果中取得) 27 */ 28 __u64 aio_data; 29 /* 30 * 可选IOCB_FLAG_RESFD标记 31 * 表示异步请求处理完成时使用eventfd进行通知 32 */ 33 __u32 aio_flags; 34 /* 35 * 有IOCB_FLAG_RESFD标记时,接收通知的eventfd 36 */ 37 __u32 aio_resfd; 38 }
1 struct io_event 2 { 3 /* 4 * 对应iocb的aio_data的值 5 */ 6 __u64 data; 7 /* 8 * 指向对应iocb的指针 9 */ 10 __u64 obj; 11 /* 12 * 对应IO请求的结果 13 * >=0: 相当于对应的同步调用的返回值;<0: -errno 14 */ 15 __s64 res; 16 }
异步 IO 上下文
aio_context_t 即 AIO 上下文句柄,该结构体对应内核中的一个 struct kioctx 结构,用来给一组异步 IO 请求提供一个上下文环境,每个进程可以有多个 aio_context_t,io_setup 的第一个参数声明了同时驻留在内核中的异步 IO 上下文数量
kioctx 结构主要包含以下字段:
1 struct kioctx 2 { 3 /* 4 * 调用者进程对应的内存管理结构 5 * 代表了调用者的虚拟地址空间 6 */ 7 struct mm_struct* mm; 8 /* 9 * 上下文ID,也就是io_context_t句柄的值 10 * 等于ring_info.mmap_base 11 */ 12 unsigned long user_id; 13 /* 14 * 属于同一地址空间的所有kioctx结构通过这个list串连起来 15 * 链表头是mm->ioctx_list 16 */ 17 struct hlist_node list; 18 /* 19 * 等待队列 20 * io_getevents系统调用可能需要等待 21 * 调用者就在该等待队列上睡眠 22 */ 23 wait_queue_head_t wait; 24 /* 25 * 进行中的请求数目 26 */ 27 int reqs_active; 28 /* 29 * 进行中的请求队列 30 */ 31 struct list_head active_reqs; 32 /* 33 * 最大请求数 34 * 对应io_setup调用的int maxevents参数 35 */ 36 unsigned max_reqs; 37 /* 38 * 需要aio线程处理的请求列表 39 * 某些情况下,IO请求可能交给aio线程来提交 40 */ 41 struct list_head run_list; 42 /* 43 * 延迟任务队列 44 * 当需要aio线程处理请求时,将wq挂入aio线程对应的请求队列 45 */ 46 struct delayed_work wq; 47 /* 48 * 存放请求结果io_event结构的ring buffer 49 */ 50 struct aio_ring_info ring_info; 51 }
其中,aio_ring_info 结构用于存放请求结果 io_event 结构的 ring buffer,主要包含以下字段:
1 struct aio_ring_info 2 { 3 unsigned long mmap_base; // ring buffer 的首地址 4 unsigned long mmap_size; // ring buffer 空间大小 5 struct page** ring_pages; // ring buffer 对应的 page 数组 6 long nr_pages; // 分配空间对应的页面数目 7 unsigned nr; // io_event 的数目 8 unsigned tail; // io_event 的存取游标 9 }
aio_ring_info 结构中,nr_page * PAGE_SIZE = mmap_size
以上数据结构都是在内核地址空间上分配的,是内核专有的,用户程序无法访问和使用
但是 io_event 结构是内核在用户地址空间上分配的 buffer,用户可以修改,但是首地址、大小等信息都是由内核维护的,用户程序通过 io_getevents 函数修改
实现原理
io_setup 函数创建了一个 AIO 上下文,并通过值-结果参数 aio_context_t 类型指针返回其句柄
io_setup 调用后,内核会通过 mmap 在对应的用户地址空间分配一段内存,由 aio_ring_info 结构中的 mmap_base、mmap_size 描述这个映射对应的位置和大小,由 ring_pages、nr_pages 描述实际分配的物理内存页面信息,异步 IO 完成后,内核会将异步 IO 的结果写入其中
在 mmap_base 指向的用户地址空间上,会存放着一个 struct aio_ring 结构,用来管理 ring buffer,主要包含以下字段:
1 unsigned id; // 等于 aio_ring_info 中的 user_id 2 unsigned nr; // 等于 aio_ring_info 中的 nr 3 unsigned head; // io_events 数组队首 4 unsigned tail; // io_events 数组游标 5 unsigned magic; // 用于确定数据结构有没有异常篡改 6 unsigned compat_features; 7 unsigned incompat_features; 8 unsigned header_length; // aio_ring 结构大小 9 struct io_event *io_events; // io_event buffer 首地址
这个数据结构存在于用户地址空间中,内核作为生产者,在 buffer 中放入数据,并修改 tail 字段,用户程序作为消费者从 buffer 中取出数据,并修改 head 字段
每一个请求用户都会创建一个 iocb 结构用于描述这个请求,而对应于用户传递的每一个 iocb 结构,内核都会生成一个与之对应的 kiocb 结构,并只该结构中的 ring_info 中预留一个 io_events 空间,用于保存处理的结果
1 struct kiocb 2 { 3 struct kioctx* ki_ctx; /* 请求对应的kioctx(上下文结构)*/ 4 struct list_head ki_run_list; /* 需要aio线程处理的请求,通过该字段链入ki_ctx->run_list */ 5 struct list_head ki_list; /* 链入ki_ctx->active_reqs */ 6 struct file* ki_filp; /* 对应的文件指针*/ 7 void __user* ki_obj.user; /* 指向用户态的iocb结构*/ 8 __u64 ki_user_data; /* 等于iocb->aio_data */ 9 loff_t ki_pos; /* 等于iocb->aio_offset */ 10 unsigned short ki_opcode; /* 等于iocb->aio_lio_opcode */ 11 size_t ki_nbytes; /* 等于iocb->aio_nbytes */ 12 char __user * ki_buf; /* 等于iocb->aio_buf */ 13 size_t ki_left; /* 该请求剩余字节数(初值等于iocb->aio_nbytes)*/ 14 struct eventfd_ctx* ki_eventfd; /* 由iocb->aio_resfd对应的eventfd对象*/ 15 ssize_t (*ki_retry)(struct kiocb *); /*由ki_opcode选择的请求提交函数*/ 16 }
对于虚拟文件系统返回 EIOCBRETRY 需要重试的情况,内核会在当前 CPU 的 aio 线程中添加一个任务,让 aio 完成该任务的重新提交
与 POSIX AIO 区别
从上图中的流程就可以看出,linux 版本的 AIO 与 POSIX 版本的 AIO 最大的不同在于 linux 版本的 AIO 实际上利用了 CPU 和 IO 设备异步工作的特性,与同步 IO 相比,很大程度上节约了 CPU 资源的浪费
而 POSIX AIO 利用了线程与线程之间的异步工作特性,在用户线程中实现 IO 的异步操作
POSIX AIO 支持非 direct-io,而且实现非常灵活,可配置性很高,可以利用内核提供的page cache来提高效率,而 linux 内核实现的 AIO 就只支持 direct-io,cache 的工作就需要用户进程考虑了