使用异步 I/O 提高程序性能

AIO 简介

Linux 异步(asynchronous) I/O 是 Linux 2.6 内核的标准功能,你也可以给 2.4 内核打上补丁包。 AIO 背后的基本思想是:允许进程发起多个 I/O 操作,不必阻塞(block)或等待(wait)到操作完成,而是在 I/O 发出完成通知之后,该进程便可获取到 I/O 的结果。

I/O 模型

以下是 Linux 下可用的 I/O 模型,同步、异步、阻塞和非阻塞模型。

使用异步 I/O 提高程序性能_第1张图片
图1.基本 Linux I/O 模型

这些 I/O 模型在特定应用使用过程中,各有利弊。本节会对这些模型做简要探讨。

同步阻塞 I/O

同步阻塞 I/O 是最常见的模型。在该模型中,用户空间(user-space)的程序发起系统调用后等待返回结果,导致应用程序阻塞直到系统调用完成(数据传输或错误)。发起调用的程序只是等待响应,不占用 CPU,因此从执行的角度来看是比较高效的。

传统的阻塞 I/O 模型是当今应用中最常用的模型。它的执行流程很简单,当发起 read 系统调用时,程序被阻塞并将上下文切换到内核。然后启动 read,当响应返回时,响应数据将从内核空间复制到用户空间缓冲区。然后程序解除阻塞(read 调用返回)。

使用异步 I/O 提高程序性能_第2张图片
图2.同步阻塞I / O模型的典型流程

从应用程序的角度来看,read 操作的时间很长。但是这个应用实际上是被阻塞的,这个 read 操作与内核中的其他任务其实是交替执行的。

同步非阻塞 I/O

同步阻塞比较低效,改进版是同步非阻塞 I/O。在该模型中,设备以非阻塞方式打开。这意味着(如图3所示),不用立即完成 I/O,read 可能会返回:无法立即执行的错误代码(EAGAIN 或 EWOULDBLOCK)。

使用异步 I/O 提高程序性能_第3张图片
图3.同步非阻塞 I/O 模型的典型流程

非阻塞的含义是 I/O 命令可能不会立即生效,需要应用程序进行多次调用直到 I/O 完成。这可能非常低效,因为在大多数情况下,应用程序必须一直运行(busy 状态),直到数据可用或尝试在内核执行 read 命令的过程中去执行其他任务。如图3所示,同步非阻塞方式会导致 I/O 延迟,因为内核中可用的数据与调用 read 的用户返回之间的任何差距可能会拉低整体吞吐量

异步阻塞 I/O

另一个阻塞范例是基于阻塞通知的非阻塞 I/O。在此模型中,非阻塞 I/O 被设定,然后使用 select 操作阻塞系统调用,直到 I/O 描述符有变动。select 调用可以为多个 I/O 描述符提供通知。对于每个描述符,你可以调用通知描述符的相关功能,如:写入数据、读取数据的可用性以及是否发生错误。

使用异步 I/O 提高程序性能_第4张图片
图4.异步阻塞 I/O 模型的典型流程(*select*)

select 调用效率比较低。虽然它是异步通知的模式,但不建议用于高性能 I/O。

异步非阻塞 I/O (AIO)

异步非阻塞 I/O 模型是 I/O 并行操作的一种。read 请求立即返回,表示read 已成功调用。然后,应用程序可以在后台 read 操作完成前做其他事情。当 read 响应到达时,可以生成信号或基于线程回调来完成 I/O 事务。

使用异步 I/O 提高程序性能_第5张图片
图5.异步非阻塞 I/O 模型的典型流程

在单个进程中可以对多个 I/O 请求并行计算、处理,是利用了处理速度和 I/O 速度之间的速度差。当一个或多个慢 I/O 请求处于待处理状态时,CPU 可以先执行其他任务,或者在其他 I/O 执行过程中去操作已完成的 I/O。

Linux AIO 介绍

在传统的 I/O 模型中,每个 I/O 通道都有一个唯一的句柄标识。在UNIX® 中,叫做是文件描述符(对于文件,管道,socket 等都是相同的)。阻塞 I/O 时,传输或在系统调用完成或发生错误时返回。

AIO 最早在 Linux kernel 2.5 中出现,现在已经在 2.6 的生产环境发布。

在异步非阻塞 I/O 中,可以同时开启多个传输。因此需要一个描述传输的上下文信息。在 AIO 中,这是一个 aiocb(AIO I/O 控制块)结构。该结构包含有关传输的所有信息,包括用于数据的用户缓冲区。当发生 I/O 通知(称为完成)时,提供 aiocb 结构来唯一地标识完成的 I/O。

AIO API

AIO API 接口非常简单,但它提供了使用几种不同通知模型进行数据传输的必要功能。

表1. AIO 接口 APIs

API 函数 备注
aio_read 请求异步 read 操作
aio_error 检查异步请求状态
aio_return 获取已完成的异步请求返回状态
aio_write 请求异步 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 的各个 API 功能如何工作以及如何使用它们。

AIO 通知

下面将介绍异步通知的方法。我将通过信号量和回调函数来探索异步通知。

基于信号量的异步通知

使用信号量进行进程间通信(IPC)是UNIX 的经典机制,AIO 也支持该方式。在下面的案例中,应用程序定义了当发生指定信号时调用的信号处理程序。然后,应用程序指定异步请求将在请求完成时产生一个信号。作为信号上下文的一部分,提供特定的 aiocb 请求来跟踪多个潜在未完成的请求。

清单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 的理想选择。这样一来,完成一次传输完成后,你可以马上开始下一个。

基于回调函数的异步通知

系统回调是一种备用的通知机制。该机制不是通过触发通知信号,而是通过调用用户空间中的函数来通知。初始化 aiocbsigevent 结构,作为正在完成的特定请求的唯一标识;见清单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 中是允许并发请求的最大数量,一般是65536(即64KB,对大部分程序来说已经足够了)。

总结

使用异步 I/O 可以构建出更快更高效的 I/O 应用。如果你的应用程序可以并行处理和 I/O,则 AIO 可以帮你提高 CPU 资源使用率。虽然异步 I/O 模式与大多数 Linux 应用程序中的传统阻塞模式不同,但异步通知模型在概念上很简单,可以简化设计。

原文地址:https://www.ibm.com/developerworks/library/l-async/index.html

你可能感兴趣的:(使用异步 I/O 提高程序性能)