Chinaunix首页 | 论坛 | 博客
  • 博客访问: 7775106
  • 博文数量: 701
  • 博客积分: 2150
  • 博客等级: 上尉
  • 技术积分: 13233
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-29 16:28
个人简介

天行健,君子以自强不息!

文章分类

全部博文(701)

文章存档

2019年(2)

2018年(12)

2017年(76)

2016年(120)

2015年(178)

2014年(129)

2013年(123)

2012年(61)

分类: 网络与安全

2013-05-31 18:07:55

紧接上文
下一节将深入介绍这种模型,探索这种模型使用的 API,然后展示几个命令。

一、异步 I/O 的动机
从前面 I/O 模型的分类中,我们可以看出 AIO 的动机。
a. 阻塞模型需要在 I/O 操作开始时阻塞应用程序,这意味着不可能同时重叠进行处理和 I/O 操作。
b. 同步非阻塞模型允许处理和 I/O 操作重叠进行,但是这需要应用程序根据重现的规则来检查 I/O 操作的状态。
c. 异步非阻塞 I/O 了,它允许处理和 I/O 操作重叠(并行)进行,包括 I/O 操作完成的通知。
除了需要阻塞之外,select 函数所提供的功能(异步阻塞 I/O)与 AIO 类似。
不过,它是对通知事件进行阻塞,而不是对 I/O 调用进行阻塞。

二、Linux 上的 AIO 简介
本节将探索 Linux 的异步 I/O 模型,从而帮助我们理解如何在应用程序中使用这种技术。
在传统的 I/O 模型中,有一个使用惟一句柄标识的 I/O 通道。
在 UNIX? 中,这些句柄是文件描述符(这对等同于文件、管道、套接字等等)。
在阻塞 I/O 中,我们发起了一次传输操作,当传输操作完成或发生错误时,系统调用就会返回。


在异步非阻塞 I/O 中,我们可以同时发起多个传输操作。
这需要每个传输操作都有惟一的上下文,这样我们才能在它们完成时区分到底是哪个传输操作完成了。
在 AIO 中,这是一个 aiocb(AIO I/O Control Block)结构。
这个结构包含了有关传输的所有信息,包括为数据准备的用户缓冲区。
在产生 I/O (称为完成)通知时,aiocb 结构就被用来惟一标识所完成的 I/O 操作。
这个 API 的展示显示了如何使用它。

NOTE:
Linux 上的 AIO
AIO 在 2.5 版本的内核中首次出现,现在已经是 2.6 版本的产品内核的一个标准特性了。

三、AIO API
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 操作


每个 API 函数都使用 aiocb 结构开始或检查。
这个结构有很多元素,但是清单 1 仅仅给出了需要(或可以)使用的元素。

清单 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 */
  ...

};

sigevent 结构告诉 AIO 在 I/O 操作完成时应该执行什么操作。我们将在 AIO 的展示中对这个结构进行探索。
现在我们将展示各个 AIO 的 API 函数是如何工作的,以及我们应该如何使用它们。


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 

...

  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 中,
在打开要从中读取数据的文件之后,
我们就清空了 aiocb 结构,
然后分配一个数据缓冲区。并将对这个数据缓冲区的引用放到 aio_buf 中。
然后,我们将 aio_nbytes 初始化成缓冲区的大小。
      并将 aio_offset 设置成 0(该文件中的第一个偏移量)。
      我们将 aio_fildes 设置为从中读取数据的文件描述符。
在设置这些域之后,就调用 aio_read 请求进行读操作。
我们然后可以调用 aio_error 来确定 aio_read 的状态。
只要状态是 EINPROGRESS,就一直忙碌等待,直到状态发生变化为止。
现在,请求可能成功,也可能失败。


注意使用这个 API 与标准的库函数从文件中读取内容是非常相似的。
除了 aio_read 的一些异步特性之外,另外一个区别是读操作偏移量的设置。
在传统的 read 调用中,偏移量是在文件描述符上下文中进行维护的。
对于每个读操作来说,偏移量都需要进行更新,这样后续的读操作才能对下一块数据进行寻址。
对于异步 I/O 操作来说这是不可能的,因为我们可以同时执行很多读请求,因此必须为每个特定的读请求都指定偏移量。

使用 AIO 接口来编译程序
我们可以在 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。
  
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 的第二个参数是 cblist 中元素的个数,而不是 aiocb 引用的个数。
cblist 中任何 NULL 元素都会被 aio_suspend 忽略。

如果为 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 所示。
  

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 );

对于读操作来说,aio_lio_opcode 域的值为 LIO_READ。
对于写操作来说,我们要使用 LIO_WRITE,不过 LIO_NOP 对于不执行操作来说也是有效的。

四、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;
}

在清单 5 中,我们在 aio_completion_handler 函数中设置信号处理程序来捕获 SIGIO 信号。
然后初始化 aio_sigevent 结构产生 SIGIO 信号来进行通知
(这是通过 sigev_notify 中的 SIGEV_SIGNAL 定义来指定的)。
当读操作完成时,信号处理程序就从该信号的 si_value 结构中提取出 aiocb,
并检查错误状态和返回状态来确定 I/O 操作是否完成。

对于性能来说,这个处理程序也是通过请求下一次异步传输而继续进行 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;
}

在清单 6 中,在创建自己的 aiocb 请求之后,我们使用 SIGEV_THREAD 请求了一个线程回调函数来作为通知方法。
然后我们将指定特定的通知处理程序,并将要传输的上下文加载到处理程序中
(在这种情况中,是个对 aiocb 请求自己的引用)。
在这个处理程序中,我们简单地引用到达的 sigval 指针并使用 AIO 函数来验证请求已经完成。


五、对 AIO 进行系统优化
proc 文件系统包含了两个虚拟文件,它们可以用来对异步 I/O 的性能进行优化:
/proc/sys/fs/aio-nr 文件提供了系统范围异步 I/O 请求现在的数目。
/proc/sys/fs/aio-max-nr 文件是所允许的并发请求的最大个数。
最大个数通常是 64KB,这对于大部分应用程序来说都已经足够了。

六、结束语
使用异步 I/O 可以帮助我们构建 I/O 速度更快、效率更高的应用程序。
如果我们的应用程序可以对处理和 I/O 操作重叠进行,
那么 AIO 就可以帮助我们构建可以更高效地使用可用 CPU 资源的应用程序。
尽管这种 I/O 模型与在大部分 Linux 应用程序中使用的传统阻塞模式都不同,
但是异步通知模型在概念上来说却非常简单,可以简化我们的设计。

阅读(3937) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~