Linux® 中最常用的输?输出QI/OQ模型是同步 I/O。在q个模型中,当请求发Z后,应用E序׃dQ直到请求满ؓ止。这是很好的一U解x案,因ؓ调用应用E序在等?I/O h完成时不需要用Q何中央处理单元(CPUQ。但是在某些情况中,I/O h可能需要与其他q程产生交叠。可UL操作pȝ接口QPOSIXQ异?I/OQAIOQ应用程序接口(APIQ就提供了这U功能。在本文中,我们对q个 API 概要q行介绍Qƈ来了解一下如何用它?/blockquote>
AIO ?
Linux 异步 I/O ?Linux 内核中提供的一个相当新的增强。它?2.6 版本内核的一个标准特性,但是我们?2.4 版本内核的补丁中也可以找到它。AIO 背后的基本思想是允许进E发起很?I/O 操作Q而不用阻塞或{待M操作完成。稍后或在接收到 I/O 操作完成的通知Ӟq程可以检?I/O 操作的结果?/p>
I/O 模型
在深入介l?AIO API 之前Q让我们先来探烦一?Linux 上可以用的不同 I/O 模型。这q不是一个详的介绍Q但是我们将试图介绍最常用的一些模型来解释它们与异?I/O 之间的区别。图 1 l出了同步和异步模型Q以及阻塞和非阻塞的模型?/p>
?1. 基本 Linux I/O 模型的简单矩?/strong>
每个 I/O 模型都有自己的用模式,它们对于特定的应用程序都有自q优点。本节将要对其一一q行介绍?/p>
同步d I/O
 |
I/O 密集型与 CPU 密集型进E的比较
I/O 密集型进E所执行?I/O 操作比执行的处理操作更多。CPU 密集型的q程所执行的处理操作比 I/O 操作更多。Linux 2.6 的调度器实际上更加偏?I/O 密集型的q程Q因为它们通常会发起一?I/O 操作Q然后进行阻塞,q就意味着其他工作都可以在两者之间有效地交错q行?/p>
|
|
最常用的一个模型是同步d I/O 模型。在q个模型中,用户I间的应用程序执行一个系l调用,q会D应用E序d。这意味着应用E序会一直阻塞,直到pȝ调用完成为止Q数据传输完成或发生错误Q。调用应用程序处于一U不再消?CPU 而只是简单等待响应的状态,因此从处理的角度来看Q这是非常有效的?/p>
?2 l出了传l的d I/O 模型Q这也是目前应用E序中最为常用的一U模型。其行ؓ非常Ҏ理解Q其用法对于典型的应用程序来说都非常有效。在调用 read
pȝ调用Ӟ应用E序会阻塞ƈ对内核进行上下文切换。然后会触发L作,当响应返回时Q从我们正在从中d的设备中q回Q,数据pUd到用L间的~冲Z。然后应用程序就会解除阻塞(read
调用q回Q?/p>
?2. 同步d I/O 模型的典型流E?/strong>
从应用程序的角度来说Q?code>read 调用会gl很长时间。实际上Q在内核执行L作和其他工作Ӟ应用E序的确会被d?/p>
同步非阻?I/O
同步d I/O 的一U效率稍低的变种是同步非d I/O。在q种模型中,讑֤是以非阻塞的形式打开的。这意味着 I/O 操作不会立即完成Q?code>read 操作可能会返回一个错误代码,说明q个命o不能立即满Q?code>EAGAIN ?EWOULDBLOCK
Q,如图 3 所C?/p>
?3. 同步非阻?I/O 模型的典型流E?/strong>
非阻塞的实现?I/O 命o可能q不会立xI需要应用程序调用许多次来等待操作完成。这可能效率不高Q因为在很多情况下,当内核执行这个命令时Q应用程序必要q行忙碌{待Q直到数据可用ؓ止,或者试图执行其他工作。正如图 3 所C的一Pq个Ҏ可以引入 I/O 操作的gӞ因ؓ数据在内怸变ؓ可用到用戯?read
q回数据之间存在一定的间隔Q这会导致整体数据吞吐量的降低?/p>
异步d I/O
另外一个阻塞解x案是带有d通知的非d I/O。在q种模型中,配置的是非阻?I/OQ然后用阻?select
pȝ调用来确定一?I/O 描述W何时有操作。 select
调用非常有趣的是它可以用来ؓ多个描述W提供通知Q而不仅仅Z个描q符提供通知。对于每个提C符来说Q我们可以请求这个描q符可以写数据、有L据可用以及是否发生错误的通知?/p>
?4. 异步d I/O 模型的典型流E?(select)
select
调用的主要问题是它的效率不是非常高。尽这是异步通知使用的一U方便模型,但是对于高性能?I/O 操作来说不徏议用?/p>
异步非阻?I/OQAIOQ?
最后,异步非阻?I/O 模型是一U处理与 I/O 重叠q行的模型。读h会立卌回,说明 read
h已经成功发v了。在后台完成L作时Q应用程序然后会执行其他处理操作。当 read
的响应到达时Q就会生一个信h执行一个基于线E的回调函数来完成这?I/O 处理q程?/p>
?5. 异步非阻?I/O 模型的典型流E?/strong>
在一个进E中Z执行多个 I/O h而对计算操作?I/O 处理q行重叠处理的能力利用了处理速度?I/O 速度之间的差异。当一个或多个 I/O h挂vӞCPU 可以执行其他dQ或者更为常见的是,在发起其?I/O 的同时对已经完成?I/O q行操作?/p>
下一节将深入介绍q种模型Q探索这U模型用的 APIQ然后展C几个命令?/p>
异步 I/O 的动?
从前?I/O 模型的分cMQ我们可以看?AIO 的动机。这U阻塞模型需要在 I/O 操作开始时d应用E序。这意味着不可能同旉叠进行处理和 I/O 操作。同步非d模型允许处理?I/O 操作重叠q行Q但是这需要应用程序根据重现的规则来检?I/O 操作的状态。这样就剩下异步非阻?I/O 了,它允许处理和 I/O 操作重叠q行Q包?I/O 操作完成的通知?/p>
除了需要阻塞之外,select
函数所提供的功能(异步d I/OQ与 AIO cM。不q,它是寚w知事gq行dQ而不是对 I/O 调用q行d?/p>
Linux 上的 AIO ?
本节探?Linux 的异?I/O 模型Q从而帮助我们理解如何在应用E序中用这U技术?/p>
在传l的 I/O 模型中,有一个用惟一句柄标识?I/O 通道。在 UNIX® 中,q些句柄是文件描q符Q这对等同于文g、管道、套接字{等Q。在d I/O 中,我们发v了一ơ传输操作,当传输操作完成或发生错误Ӟpȝ调用׃q回?/p>
 |
Linux 上的 AIO
AIO ?2.5 版本的内怸首次出现Q现在已l是 2.6 版本的品内核的一个标准特性了?/p>
|
|
在异步非d I/O 中,我们可以同时发v多个传输操作。这需要每个传输操作都有惟一的上下文Q这h们才能在它们完成时区分到底是哪个传输操作完成了。在 AIO 中,q是一?aiocb
QAIO I/O Control BlockQ结构。这个结构包含了有关传输的所有信息,包括为数据准备的用户~冲区。在产生 I/O Q称为完成)通知Ӟaiocb
l构p用来惟一标识所完成?I/O 操作。这?API 的展C显CZ如何使用它?/p>
AIO API
AIO 接口?API 非常单,但是它ؓ数据传输提供了必需的功能,q给Z两个不同的通知模型。表 1 l出?AIO 的接口函敎ͼ本节E后会更详细q行介绍?/p>
?1. AIO 接口 API
API 函数 |
说明 |
aio_read |
h异步L?/td>
|
aio_error |
查异步请求的状?/td>
|
aio_return |
获得完成的异步请求的q回状?/td>
|
aio_write |
h异步写操?/td>
|
aio_suspend |
挂v调用q程Q直C个或多个异步h已经完成Q或p|Q?/td>
|
aio_cancel |
取消异步 I/O h |
lio_listio |
发v一pd I/O 操作 |
每个 API 函数都?aiocb
l构开始或查。这个结构有很多元素Q但是清?1 仅仅l出了需要(或可以)使用的元素?/p>
清单 1. aiocb l构中相关的?
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 */
...
};
|
sigevent
l构告诉 AIO ?I/O 操作完成时应该执行什么操作。我们将?AIO 的展CZ对这个结构进行探索。现在我们将展示各个 AIO ?API 函数是如何工作的Q以及我们应该如何用它们?/p>
aio_read
aio_read
函数h对一个有效的文g描述W进行异步读操作。这个文件描q符可以表示一个文件、套接字甚至道?code>aio_read 函数的原型如下:
int aio_read( struct aiocb *aiocbp );
|
aio_read
函数在请求进行排队之后会立即q回。如果执行成功,q回值就?0Q如果出现错误,q回值就?-1Qƈ讄 errno
的倹{?/p>
要执行读操作Q应用程序必d aiocb
l构q行初始化。下面这个简短的例子展CZ如何填充 aiocb
hl构Qƈ使用 aio_read
来执行异步读hQ现在暂时忽略通知Q操作。它q展CZ aio_error
的用法,不过我们稍后再作解释?/p>
清单 2. 使用 aio_read q行异步L作的例子
#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 */
}
|
在清?2 中,在打开要从中读取数据的文g之后Q我们就清空?aiocb
l构Q然后分配一个数据缓冲区。ƈ对q个数据~冲区的引用攑ֈ aio_buf
中。然后,我们?aio_nbytes
初始化成~冲区的大小。ƈ?aio_offset
讄?0Q该文g中的W一个偏U量Q。我们将 aio_fildes
讄Z中读取数据的文g描述W。在讄q些域之后,p?aio_read
hq行L作。我们然后可以调?aio_error
来确?aio_read
的状态。只要状态是 EINPROGRESS
Q就一直忙等待,直到状态发生变化ؓ止。现在,h可能成功Q也可能p|?/p>
注意使用q个 API 与标准的库函C文g中读取内Ҏ非常怼的。除?aio_read
的一些异步特性之外,另外一个区别是L作偏U量的设|。在传统?read
调用中,偏移量是在文件描q符上下文中q行l护的。对于每个读操作来说Q偏U量都需要进行更斎ͼq样后箋的读操作才能对下一块数据进行寻址。对于异?I/O 操作来说q是不可能的Q因为我们可以同时执行很多读hQ因此必Mؓ每个特定的读h都指定偏U量?/p>
aio_error
aio_error
函数被用来确定请求的状态。其原型如下Q?/p>
int aio_error( struct aiocb *aiocbp );
|
q个函数可以q回以下内容Q?/p>
EINPROGRESS
Q说明请求尚未完?
ECANCELLED
Q说明请求被应用E序取消?
-1
Q说明发生了错误Q具体错误原因可以查?errno
aio_return
异步 I/O 和标准块 I/O 之间的另外一个区别是我们不能立即讉Kq个函数的返回状态,因ؓ我们q没有阻塞在 read
调用上。在标准?read
调用中,q回状态是在该函数q回时提供的。但是在异步 I/O 中,我们要?aio_return
函数。这个函数的原型如下Q?/p>
ssize_t aio_return( struct aiocb *aiocbp );
|
只有?aio_error
调用定h已经完成Q可能成功,也可能发生了错误Q之后,才会调用q个函数?code>aio_return 的返回值就{h于同步情况中 read
?write
pȝ调用的返回|所传输的字节数Q如果发生错误,q回值就?-1
Q?/p>
aio_write
aio_write
函数用来h一个异步写操作。其函数原型如下Q?
int aio_write( struct aiocb *aiocbp );
|
aio_write
函数会立卌回,说明h已经q行排队Q成功时q回gؓ 0
Q失败时q回gؓ -1
Qƈ相应地设|?errno
Q?/p>
q与 read
pȝ调用cMQ但是有一点不一L行ؓ需要注意。回想一下对?read
调用来说Q要使用的偏U量是非帔R要的。然而,对于 write
来说Q这个偏U量只有在没有设|?O_APPEND
选项的文件上下文中才会非帔R要。如果设|了 O_APPEND
Q那么这个偏U量׃被忽略,数据都会被附加到文g的末。否则,aio_offset
域就定了数据在要写入的文g中的偏移量?/p>
aio_suspend
我们可以使用 aio_suspend
函数来挂P或阻塞)调用q程Q直到异步请求完成ؓ止,此时会生一个信P或者发生其他超时操作。调用者提供了一?aiocb
引用列表Q其中Q何一个完成都会导?aio_suspend
q回?aio_suspend
的函数原型如下:
int aio_suspend( const struct aiocb *const cblist[],
int n, const struct timespec *timeout );
|
aio_suspend
的用非常简单。我们要提供一?aiocb
引用列表。如果Q何一个完成了Q这个调用就会返?0
。否则就会返?-1
Q说明发生了错误。请参看清单 3?/p>
清单 3. 使用 aio_suspend 函数d异步 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 );
|
注意Q?code>aio_suspend 的第二个参数?cblist
中元素的个数Q而不?aiocb
引用的个数?code>cblist 中Q?NULL
元素都会?aio_suspend
忽略?/p>
如果?aio_suspend
提供了超Ӟ而超时情늚发生了Q那么它׃q回 -1
Q?code>errno 中会包含 EAGAIN
?/p>
aio_cancel
aio_cancel
函数允许我们取消Ҏ个文件描q符执行的一个或所?I/O h。其原型如下Q?/p>
int aio_cancel( int fd, struct aiocb *aiocbp );
|
要取消一个请求,我们需要提供文件描q符?aiocb
引用。如果这个请求被成功取消了,那么q个函数׃q回 AIO_CANCELED
。如果请求完成了Q这个函数就会返?AIO_NOTCANCELED
?/p>
要取消对某个l定文g描述W的所有请求,我们需要提供这个文件的描述W,以及一个对 aiocbp
?NULL
引用。如果所有的h都取消了Q这个函数就会返?AIO_CANCELED
Q如果至有一个请求没有被取消Q那么这个函数就会返?AIO_NOT_CANCELED
Q如果没有一个请求可以被取消Q那么这个函数就会返?AIO_ALLDONE
。我们然后可以?aio_error
来验证每?AIO h。如果这个请求已l被取消了,那么 aio_error
׃q回 -1
Qƈ?errno
会被讄?ECANCELED
?/p>
lio_listio
最后,AIO 提供了一U方法?lio_listio
API 函数同时发v多个传输。这个函数非帔R要,因ؓq意味着我们可以在一个系l调用(一ơ内怸下文切换Q中启动大量?I/O 操作。从性能的角度来看,q非帔R要,因此值得我们q旉探烦一下?code>lio_listio API 函数的原型如下:
int lio_listio( int mode, struct aiocb *list[], int nent,
struct sigevent *sig );
|
mode
参数可以?LIO_WAIT
?LIO_NOWAIT
?code>LIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成ؓ止。在操作q行排队之后Q?code>LIO_NOWAIT ׃q回?code>list 是一?aiocb
引用的列表,最大元素的个数是由 nent
定义的。注?list
的元素可以ؓ NULL
Q?code>lio_listio 会将其忽略?code>sigevent 引用定义了在所?I/O 操作都完成时产生信号的方法?/p>
对于 lio_listio
的请求与传统?read
?write
h在必L定的操作斚wE有不同Q如清单 4 所C?/p>
清单 4. 使用 lio_listio 函数发v一pdh
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 );
|
对于L作来_aio_lio_opcode
域的gؓ LIO_READ
。对于写操作来说Q我们要使用 LIO_WRITE
Q不q?LIO_NOP
对于不执行操作来说也是有效的?/p>
AIO 通知
现在我们已经看过了可用的 AIO 函数Q本节将深入介绍对异步通知可以使用的方法。我们将通过信号和函数回调来探烦异步函数的通知机制?/p>
使用信号q行异步通知
使用信号q行q程间通信QIPCQ是 UNIX 中的一U传l机ӞAIO 也可以支持这U机制。在q种范例中,应用E序需要定义信号处理程序,在生指定的信号时就会调用这个处理程序。应用程序然后配|一个异步请求将在请求完成时产生一个信受作Z号上下文的一部分Q特定的 aiocb
h被提供用来记录多个可能会出现的请求。清?5 展示了这U通知Ҏ?/p>
清单 5. 使用信号作ؓ AIO h的通知
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;
}
|
在清?5 中,我们?aio_completion_handler
函数中设|信号处理程序来捕获 SIGIO
信号。然后初始化 aio_sigevent
l构产生 SIGIO
信号来进行通知Q这是通过 sigev_notify
中的 SIGEV_SIGNAL
定义来指定的Q。当L作完成时Q信号处理程序就从该信号?si_value
l构中提取出 aiocb
Qƈ查错误状态和q回状态来定 I/O 操作是否完成?/p>
对于性能来说Q这个处理程序也是通过h下一ơ异步传输而l进?I/O 操作的理惛_斏V采用这U方式,在一ơ数据传输完成时Q我们就可以立即开始下一ơ数据传输操作?/p>
使用回调函数q行异步通知
另外一U通知方式是系l回调函数。这U机制不会ؓ通知而生一个信P而是会调用用L间的一个函数来实现通知功能。我们在 sigevent
l构中设|了?aiocb
的引用,从而可以惟一标识正在完成的特定请求。请参看清单 6?/p>
清单 6. ?AIO h使用U程回调通知
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;
}
|
在清?6 中,在创q aiocb
h之后Q我们?SIGEV_THREAD
h了一个线E回调函数来作ؓ通知Ҏ。然后我们将指定特定的通知处理E序Qƈ要传输的上下文加蝲到处理程序中Q在q种情况中,是个?aiocb
h自己的引用)。在q个处理E序中,我们单地引用到达?sigval
指针q?AIO 函数来验证请求已l完成?/p>
?AIO q行pȝ优化
proc 文gpȝ包含了两个虚拟文Ӟ它们可以用来对异?I/O 的性能q行优化Q?/p>
- /proc/sys/fs/aio-nr 文g提供了系l范围异?I/O h现在的数目?
- /proc/sys/fs/aio-max-nr 文g是所允许的ƈ发请求的最大个数。最大个数通常?64KBQ这对于大部分应用程序来说都已经_了?
l束?
使用异步 I/O 可以帮助我们构徏 I/O 速度更快、效率更高的应用E序。如果我们的应用E序可以对处理和 I/O 操作重叠q行Q那?AIO 可以帮助我们构建可以更高效C用可?CPU 资源的应用程序。尽这U?I/O 模型与在大部?Linux 应用E序中用的传统d模式都不同,但是异步通知模型在概念上来说却非常简单,可以化我们的设计?
参考资?
学习
获得产品和技?/strong>
- 订购免费?SEK for LinuxQ这有两?DVDQ包括最新的 IBM for Linux 的试用YӞ包括 DB2®、Lotus®、Rational®、Tivoli® ?WebSphere®?
- 在您的下一个开发项目中采用 IBM 试用软gQ这可以?developerWorks 上直接下载?
讨论
关于作?/span>
 |

|
 |
Tim Jones 是一名嵌入式软g工程师,他是 GNU/Linux Application Programming?em>AI Application Programming 以及 BSD Sockets Programming from a Multilanguage Perspective {书的作者。他的工E背景非常广泛,从同步宇宙飞船的内核开发到嵌入式架构设计,再到|络?/p>
|