本文参考了:
- 《Understanding the Linux Kernel, Third Edition》16.4章
简述
POSIX 异步 IO interface(AIO)定义了允许进程创建一个或多个异步的 IO 操作的接口。进程可以在 IO 操作完成之后得到操作系统的通知,手段包括:不通知、信号、实例化thread。
POSIX 标准
注意:这只是 POSIX(The Portable Operating System Interface)定义的接口,并不是实现。Linux 内核对异步 IO 的具体实现在后面。(This is an API, not implementation!)
POSIX 1003.1标准定义了异步访问文件的 library function:
系统调用 | 描述 |
---|---|
aio_read() | 异步读取文件 |
aio_write() | 异步写入文件 |
aio_fsync | 发出对当前所有 outstanding 的异步IO操作进行 flush 的请求 (不会阻塞) |
aio_error() | 获取一个处于 outstanding 状态异步 IO 操作的 error code |
aio_return() | 获取一个已经完成的异步 IO 操作的返回码 |
aio_cancel() | 取消一个outstanding的异步 IO 操作 |
aio_suspend() | 暂停进程,直到至少有一个outstanding的异步 IO 操作完成 |
使用异步 IO 其实是很简单的。大致来说分为三步:
- 程序先是使用普通的
open()
系统调用。 - 然后创建一个叫做
aiocb
的 control block填满,比较重要的一些字段如下:
aio_fildes
: 对应文件的 fd(open()
系统调用返回的)aio_buf
: 为此文件准备的User mode bufferaio_nbytes
: 有多少 bytes 应该被传输aio_offset
: read、write 操作应该从哪个 offset 开始(注意这个和同步的 IO 操作是独立)aio_sigevent
: 调用者想要什么方式获取 IO 成功的回调通知,包括SIGEV_NONE
、SIGEV_SIGNAL
、SIGEV_THREAD
,分别对应上文提到的三个通知方式。aio_lio_opcode
: IO 操作类型:read write sync
具体的定义如下:
#includestruct aiocb { /* The order of these fields is implementation-dependent */ int aio_fildes; /* File descriptor */ off_t aio_offset; /* File offset */ volatile void *aio_buf; /* Location of buffer */ size_t aio_nbytes; /* Length of transfer */ int aio_reqprio; /* Request priority */ struct sigevent aio_sigevent; /* Notification method */ int aio_lio_opcode; /* Operation to be performed; lio_listio() only */ /* Various implementation-internal fields not shown */};/* Operation codes for 'aio_lio_opcode': */enum { LIO_READ, LIO_WRITE, LIO_NOP };复制代码
- 最后把
aiocb
这个 control block 的地址传给aio_read()
或者aio_write()
。
这两个函数都会在kernel或library将对应 IO 的数据传输加入传输队列之后立即退出。
之后进程可以用aio_error()
检查异步 IO 的进行状态:
aio_error()返回值 | 描述 |
---|---|
EINPROGRESS | 还在传输中 |
0 | 成功完成 |
其他错误码 | 操作失败 |
可以用aio_return()
获取成功read或write了多少个 bytes,或者-1表示失败。
Linux 内核支持
在操作系统内核不支持异步 IO 的情况下,异步 IO 也能够实现,实现思路如下:aio_read()
和aio_write()
先克隆当前进程,让子进程执行同步的read()
和write()
系统调用,然后父进程结束aio_read()
或aio_write()
,从而实现非阻塞,主进程可以开始做其他事情。显然,这种没有得到内核支持的异步 IO 操作是比较低效的。
Linux 内核从2.6版本起开始支持下面这些系统调用:
系统调用 | 描述 |
---|---|
io_setup() | 为当前进程创建一个异步IO上下文(asynchronous i/o contex) |
io_submit() | 提交一个或多个异步 IO 操作 |
io_getevents() | 获取一些 outstanding 异步 IO 的运行状态 |
io_cancel() | 取消一个异步 IO |
io_destroy() | 摧毁当前进程的异步 IO 上下文 |
异步 IO上下文(AIO context)
用户态的进程要调用io_submit()
之前,先得调用io_setup()
创建异步 IO 上下文。
基本上来说,一个 AIO context 就是一个用于追踪所有进行中的异步IO操作的数据结构,这个 struct 叫做kioctx
。一个应用可能会创建多个 AIO context,一个进程的所有kioctx
用一个链表相连:ioctx_list
。
这个ioctx_list
保存在该进程的memory descriptor上:
来源于《Understanding the Linux Kernel, Third Edition》 P355
kioctx
中有一个重要的数据结构:AIO ring
。AIO ring
的作用是:kernel 把 outstanding 的 异步 IO 操作完成情况写到这里面,由于AIO ring
是位于进程的地址空间的,所以进程可以直接从这个数据接口里面读取异步 IO 的状态,而不用执行相对较慢的系统调用。
提交异步 IO 操作
aio_submit()
系统调用包含三个参数:
ctx_id
:io_setup()
返回的 idiocbpp
: 一个iocb
指针的列表,iocb
包含描述一个异步 IO 操作的信息。nr
:iocbpp
的长度
其中iocb
和 POSIX 标准中的aiocb
是一样的,同样包含aio_fildes, aio_buf, aio_nbytes, aio_offset, aio_lio_opcode
这些字段。
Linux kernel 有一个 service routine : sys_io_submit()
,执行下列操作:
- 检查
iocbpp
列表包含的iocb
descriptors是不是合法的。 - 通过
xtx_id
在ioctx_list
中检索出对应的kioctx
。 - 对每一个
iocb
descriptor,执行下列操作:
- 通过
aio_fildes
获取文件 fd。 - 为该异步 IO 操作新建一个
kiocb
descriptor。 - 检查
AIO ring
中是否有足够的空间来存储完成结果。 - 根据IO 类型(
aio_lio_opcode
字段)设置ki_retry
方法。 - 执行
aio_run_iocb()
函数,这个函数本质上就是调用上一步的ki_retry
方法,开始执行 IO 操作。如果ki_retry
方法返回EIOCBRETRY
,就表示该异步 IO 操作已经被提交了,但是还没有完成。一段时间后aio_lio_opcode
之后会被再次执行,当提示 IO 完成时,就调用aio_complete()
将完成状态写入到AIO ring
。