Linux异步I/O是Linux内核中提供的一个相当新的增强。它是2.6版本内核的一个标准特性,但是我们在2.4版本内核的补丁中也可以找到它。AIO背后的基本思想是允许进程发起很多I/O操作,而不用阻塞或等待任何操作完成。稍后或在接收到I/O操作完成的通知时,进程就可以检索I/O操作的结果。
1、 I/O 模型
在深入介绍AIO API之前,让我们先来探索一下Linux上可以使用的不同I/O模型。这并不是一个详尽的介绍,但是我们将试图介绍最常用的一些模型来解释它们与异步I/O之间的区别。图 1给出了同步和异步模型,以及阻塞和非阻塞的模型。
图1 基本Linux I/O模型的简单矩阵
每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点。本节将简要对其一一进行介绍。
注意:I/O密集型进程所执行的I/O操作比执行的处理操作更多。CPU密集型的进程所执行的处理操作比I/O操作更多。Linux 2.6的调度器实际上更加偏爱I/O密集型的进程,因为它们通常会发起一个I/O操作,然后进行阻塞,这就意味着其他工作都可以在两者之间有效地交错进行。
(1)同步阻塞 I/O
最常用的一个模型是同步阻塞I/O模型。在这个模型中,用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞。这意味着应用程序会一直阻塞,直到系统调用完成为止(数据传输完成或发生错误)。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。
图 2 给出了传统的阻塞I/O模型,这也是目前应用程序中最为常用的一种模型。其行为非常容易理解,其用法对于典型的应用程序来说都非常有效。在调用read系统调用时,应用程序会阻塞并对内核进行上下文切换。然后会触发读操作,当响应返回时(从我们正在从中读取的设备中返回),数据就被移动到用户空间的缓冲区中。然后应用程序就会解除阻塞(read调用返回)。
图2 同步阻塞I/O模型的典型流程
从应用程序的角度来说,read 调用会延续很长时间。实际上,在内核执行读操作和其他工作时,应用程序的确会被阻塞。
(2)同步非阻塞I/O
同步阻塞I/O的一种效率稍低的变种是同步非阻塞I/O。在这种模型中,设备是以非阻塞的形式打开的。这意味着I/O操作不会立即完成,read操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN或EWOULDBLOCK),如图3所示。
图3 同步非阻塞I/O模型的典型流程
非阻塞的实现是I/O命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。正如图3所示的一样,这个方法可以引入I/O操作的延时,因为数据在内核中变为可用到用户调用read返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。
(3)异步阻塞 I/O
另外一个阻塞解决方案是带有阻塞通知的非阻塞 I/O。在这种模型中,配置的是非阻塞I/O,然后使用阻塞select系统调用来确定一个I/O描述符何时有操作。使select调用非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为一个描述符提供通知。对于每个提示符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知。
图4 异步阻塞I/O模型的典型流程 (select)
select调用的主要问题是它的效率不是非常高。尽管这是异步通知使用的一种方便模型,但是对于高性能的I/O操作来说不建议使用。
(4)异步非阻塞 I/O(AIO)
最后,异步非阻塞I/O模型是一种处理与I/O重叠进行的模型。读请求会立即返回,说明read请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当read的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次I/O处理过程。
图5 异步非阻塞I/O模型的典型流程
在一个进程中为了执行多个I/O请求而对计算操作和I/O处理进行重叠处理的能力利用了处理速度与I/O速度之间的差异。当一个或多个I/O请求挂起时,CPU可以执行其他任务;或者更为常见的是,在发起其他I/O的同时对已经完成的I/O进行操作。
从前面I/O模型的分类中,我们可以看出AIO的动机。这种阻塞模型需要在I/O操作开始时阻塞应用程序。这意味着不可能同时重叠进行处理和I/O操作。同步非阻塞模型允许处理和I/O操作重叠进行,但是这需要应用程序根据重现的规则来检查I/O操作的状态。这样就剩下异步非阻塞I/O了,它允许处理和I/O操作重叠进行,包括I/O操作完成的通知。
除了需要阻塞之外,select函数所提供的功能(异步阻塞I/O)与AIO类似。不过,它是对通知事件进行阻塞,而不是对I/O调用进行阻塞。
2、Linux上的AIO简介
在传统的I/O模型中,有一个使用惟一句柄标识的I/O通道。在 UNIX中,这些句柄是文件描述符(这对等同于文件、管道、套接字等等)。在阻塞I/O中,我们发起了一次传输操作,当传输操作完成或发生错误时,系统调用就会返回。
在异步非阻塞I/O中,我们可以同时发起多个传输操作。这需要每个传输操作都有惟一的上下文,这样我们才能在它们完成时区分到底是哪个传输操作完成了。在AIO中,这是一个aiocb结构(AIO Control Block)。这个结构包含了有关传输的所有信息,包括为数据准备的用户缓冲区。在产生I/O(称为完成)通知时,aiocb结构就被用来惟一标识所完成的I/O操作。这个API的展示显示了如何使用它。
AIO在2.5版本的内核中首次出现,现在已经是2.6版本的产品内核的一个标准特性了。 AIO接口的API非常简单,但是它为数据传输提供了必需的功能,并给出了两个不同的通知模型。表1给出了AIO的接口函数。
表1. AIO接口API
API 函数 | 说明 |
---|---|
aio_read | 请求异步读操作 |
aio_error | 检查异步请求的状态 |
aio_return | 获得完成的异步请求的返回状态 |
aio_write | 请求异步写操作 |
aio_suspend | 挂起调用进程,直到一个或多个异步请求已经完成(或失败) |
aio_cancel | 取消异步I/O请求 |
lio_listio | 发起一系列I/O操作 |
清单1. aiocb结构中相关的域
- struct aiocb {
- int aio_fildes; // File Descriptor
- int aio_lio_opcode; // Valid only for lio_listio (r/w/nop)
- volatile void *aio_buf; // Data Buffer
- size_t aio_nbytes; // Number of Bytes in Data Buffer
- struct sigevent aio_sigevent; // Notification Structure
- /* Internal fields */
- ...
- };
(1)aio_read
aio_read函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字甚至管道。aio_read函数的原型如下:
int aio_read( struct aiocb *aiocbp );
aio_read函数在请求进行排队之后会立即返回。如果执行成功,返回值就为0;如果出现错误,返回值就为-1,并设置errno的值。
要执行读操作,应用程序必须对aiocb结构进行初始化。下面这个简短的例子就展示了如何填充aiocb请求结构,并使用aio_read来执行异步读请求(现在暂时忽略通知)操作。它还展示了aio_error的用法。
清单 2. 使用aio_read进行异步读操作的例子
- #include <aio.h>
- ...
- int fd, ret;
- struct aiocb my_aiocb;
- fd = open( "file.txt", O_RDONLY );
- if (fd < 0) perror("open");
- /* Zero out the aiocb structure (recommended) */
- bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
- /* Allocate a data buffer for the aiocb request */
- my_aiocb.aio_buf = malloc(BUFSIZE+1);
- if (!my_aiocb.aio_buf) perror("malloc");
- /* Initialize the necessary fields in the aiocb */
- my_aiocb.aio_fildes = fd;
- my_aiocb.aio_nbytes = BUFSIZE;
- my_aiocb.aio_offset = 0;
- ret = aio_read( &my_aiocb );
- if (ret < 0) perror("aio_read");
- while ( aio_error( &my_aiocb ) == EINPROGRESS ) ;
- if ((ret = aio_return( &my_iocb )) > 0) {
- /* got ret bytes on the read */
- } else {
- /* read failed, consult errno */
- }
注意使用这个API与标准的库函数从文件中读取内容是非常相似的。除了aio_read的一些异步特性之外,另外一个区别是读操作偏移量的设置。在传统的read调用中,偏移量是在文件描述符上下文中进行维护的。对于每个读操作来说,偏移量都需要进行更新,这样后续的读操作才能对下一块数据进行寻址。对于异步I/O操作来说这是不可能的,因为我们可以同时执行很多读请求,因此必须为每个特定的读请求都指定偏移量。
我们可以在aio.h头文件中找到函数原型和其他需要的符号。在编译使用这种接口的程序时,我们必须使用POSIX实时扩展库(librt)。
(2) aio_error
aio_error函数被用来确定请求的状态。其原型如下:
int aio_error( struct aiocb *aiocbp );
这个函数可以返回以下内容:
* EINPROGRESS,说明请求尚未完成
* ECANCELLED,说明请求被应用程序取消了
* -1,说明发生了错误,具体错误原因可以查阅errno
(3)aio_return
异步I/O和标准块I/O之间的另外一个区别是我们不能立即访问这个函数的返回状态,因为我们并没有阻塞在read调用上。在标准的read调用中,返回状态是在该函数返回时提供的。但是在异步I/O中,我们要使用aio_return函数。这个函数的原型如下:
ssize_t aio_return( struct aiocb *aiocbp );
只有在aio_error调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数。aio_return的返回值就等价于同步情况中read或write系统调用的返回值(所传输的字节数,如果发生错误,返回值就为-1)。
(4)aio_write
aio_write函数用来请求一个异步写操作。其函数原型如下:
int aio_write( struct aiocb *aiocbp );
aio_write函数会立即返回,说明请求已经进行排队(成功时返回值为0,失败时返回值为-1,并相应地设置errno)。
这与read系统调用类似,但是有一点不一样的行为需要注意。回想一下对于read调用来说,要使用的偏移量是非常重要的。然而,对于write来说,这个偏移量只有在没有设置O_APPEND选项的文件上下文中才会非常重要。如果设置了O_APPEND,那么这个偏移量就会被忽略,数据都会被附加到文件的末尾。否则,aio_offset域就确定了数据在要写入的文件中的偏移量。
(5)aio_suspend
我们可以使用aio_suspend函数来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个aiocb引用列表,其中任何一个完成都会导致aio_suspend返回。 aio_suspend的函数原型如下:
int aio_suspend( const struct aiocb *const cblist[], int n, const struct timespec *timeout );
aio_suspend的使用非常简单。我们要提供一个aiocb引用列表。如果任何一个完成了,这个调用就会返回0。否则就会返回-1,说明发生了错误。请参看清单3。
清单 3. 使用aio_suspend函数阻塞异步I/O
- struct aioct *cblist[MAX_LIST]
- /* Clear the list. */
- bzero( (char *)cblist, sizeof(cblist) );
- /* Load one or more references into the list */
- cblist[0] = &my_aiocb;
- ret = aio_read( &my_aiocb );
- ret = aio_suspend( cblist, MAX_LIST, NULL );
如果为aio_suspend提供了超时,而超时情况的确发生了,那么它就会返回 -1,errno中会包含EAGAIN。
(6)aio_cancel
aio_cancel函数允许我们取消对某个文件描述符执行的一个或所有I/O请求。其原型如下:
int aio_cancel( int fd, struct aiocb *aiocbp );
要取消一个请求,我们需要提供文件描述符和aiocb引用。如果这个请求被成功取消了,那么这个函数就会返回AIO_CANCELED。如果请求完成了,这个函数就会返回AIO_NOTCANCELED。
要取消对某个给定文件描述符的所有请求,我们需要提供这个文件的描述符,以及一个对aiocbp的NULL引用。如果所有的请求都取消了,这个函数就会返回AIO_CANCELED;如果至少有一个请求没有被取消,那么这个函数就会返回AIO_NOT_CANCELED;如果没有一个请求可以被取消,那么这个函数就会返回AIO_ALLDONE。我们然后可以使用aio_error来验证每个AIO请求。如果这个请求已经被取消了,那么aio_error就会返回 -1,并且errno会被设置为ECANCELED。
(7)lio_listio
最后,AIO提供了一种方法使用lio_listio API函数同时发起多个传输。 这个函数非常重要,因为这意味着我们可以在一个系统调用(一次内核上下文切换)中启动大量的I/O操作。从性能的角度来看,这非常重要,因此值得我们花点时间探索一下。lio_listio API函数的原型如下:
int lio_listio( int mode, struct aiocb *list[], int nent, struct sigevent *sig );
mode参数可以是LIO_WAIT或LIO_NOWAIT。LIO_WAIT会阻塞这个调用,直到所有的I/O都完成为止。在操作进行排队之后,LIO_NOWAIT就会返回。list 是一个aiocb引用的列表,最大元素的个数是由nent定义的。注意list的元素可以为NULL,lio_listio会将其忽略。sigevent引用定义了在所有I/O操作都完成时产生信号的方法。
对于lio_listio的请求与传统的read或write请求在必须指定的操作方面稍有不同,如清单4所示。
清单 4. 使用lio_listio函数发起一系列请求
- struct aiocb aiocb1, aiocb2;
- struct aiocb *list[MAX_LIST];
- ...
- /* Prepare the first aiocb */
- aiocb1.aio_fildes = fd;
- aiocb1.aio_buf = malloc( BUFSIZE+1 );
- aiocb1.aio_nbytes = BUFSIZE;
- aiocb1.aio_offset = next_offset;
- aiocb1.aio_lio_opcode = LIO_READ;
- ...
- bzero( (char *)list, sizeof(list) );
- list[0] = &aiocb1;
- list[1] = &aiocb2;
- ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );
3、 AIO通知
现在我们已经看过了可用的AIO函数,本节将深入介绍对异步通知可以使用的方法。我们将通过信号和函数回调来探索异步函数的通知机制。
(1)使用信号进行异步通知
使用信号进行进程间通信(IPC)是UNIX中的一种传统机制,AIO也可以支持这种机制。在这种范例中,应用程序需要定义信号处理程序,在产生指定的信号时就会调用这个处理程序。应用程序然后配置一个异步请求将在请求完成时产生一个信号。作为信号上下文的一部分,特定的aiocb请求被提供用来记录多个可能会出现的请求。清单5展示了这种通知方法。
清单 5. 使用信号作为AIO请求的通知
- void setup_io( ... )
- {
- int fd;
- struct sigaction sig_act;
- struct aiocb my_aiocb;
- ...
- /* Set up the signal handler */
- sigemptyset(&sig_act.sa_mask);
- sig_act.sa_flags = SA_SIGINFO;
- sig_act.sa_sigaction = aio_completion_handler;
- /* Set up the AIO request */
- bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
- my_aiocb.aio_fildes = fd;
- my_aiocb.aio_buf = malloc(BUF_SIZE+1);
- my_aiocb.aio_nbytes = BUF_SIZE;
- my_aiocb.aio_offset = next_offset;
- /* Link the AIO request with the Signal Handler */
- my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
- my_aiocb.aio_sigevent.sigev_signo = SIGIO;
- my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
- /* Map the Signal to the Signal Handler */
- ret = sigaction( SIGIO, &sig_act, NULL );
- ...
- ret = aio_read( &my_aiocb );
- }
- void aio_completion_handler( int signo, siginfo_t *info, void *context )
- {
- struct aiocb *req;
- /* Ensure it's our signal */
- if (info->si_signo == SIGIO) {
- req = (struct aiocb *)info->si_value.sival_ptr;
- /* Did the request complete? */
- if (aio_error( req ) == 0) {
- /* Request completed successfully, get the return status */
- ret = aio_return( req );
- }
- }
- return;
- }
对于性能来说,这个处理程序也是通过请求下一次异步传输而继续进行I/O操作的理想地方。采用这种方式,在一次数据传输完成时,我们就可以立即开始下一次数据传输操作。
(2)使用回调函数进行异步通知
另外一种通知方式是系统回调函数。这种机制不会为通知而产生一个信号,而是会调用用户空间的一个函数来实现通知功能。我们在sigevent结构中设置了对aiocb的引用,从而可以惟一标识正在完成的特定请求。请参看清单6。
清单 6. 对AIO请求使用线程回调通知
- void setup_io( ... )
- {
- int fd;
- struct aiocb my_aiocb;
- ...
- /* Set up the AIO request */
- bzero( (char *)&my_aiocb, sizeof(struct aiocb) );
- my_aiocb.aio_fildes = fd;
- my_aiocb.aio_buf = malloc(BUF_SIZE+1);
- my_aiocb.aio_nbytes = BUF_SIZE;
- my_aiocb.aio_offset = next_offset;
- /* Link the AIO request with a thread callback */
- my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
- my_aiocb.aio_sigevent.notify_function = aio_completion_handler;
- my_aiocb.aio_sigevent.notify_attributes = NULL;
- my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;
- ...
- ret = aio_read( &my_aiocb );
- }
- void aio_completion_handler( sigval_t sigval )
- {
- struct aiocb *req;
- req = (struct aiocb *)sigval.sival_ptr;
- /* Did the request complete? */
- if (aio_error( req ) == 0) {
- /* Request completed successfully, get the return status */
- ret = aio_return( req );
- }
- return;
- }
4、对AIO进行系统优化
proc文件系统包含了两个虚拟文件,它们可以用来对异步I/O的性能进行优化:
/proc/sys/fs/aio-nr文件:提供了系统范围异步I/O请求现在的数目。
/proc/sys/fs/aio-max-nr文件:是所允许的并发请求的最大个数。最大个数通常是64KB,这对于大部分应用程序来说都已经足够了。
5、 结束语
使用异步I/O可以帮助我们构建I/O速度更快、效率更高的应用程序。如果我们的应用程序可以对处理和I/O操作重叠进行,那么AIO就可以帮助我们构建可以更高效地使用可用CPU资源的应用程序。尽管这种I/O模型与在大部分Linux应用程序中使用的传统阻塞模式都不同,但是异步通知模型在概念上来说却非常简单,可以简化我们的设计。
本文整理自:http://www.ibm.com/developerworks/linux/library/l-async/
中文翻译参考这里:http://www.ibm.com/developerworks/cn/linux/l-async/