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 模型在特定应用使用过程中,各有利弊。本节会对这些模型做简要探讨。
同步阻塞 I/O
同步阻塞 I/O 是最常见的模型。在该模型中,用户空间(user-space)的程序发起系统调用后等待返回结果,导致应用程序阻塞直到系统调用完成(数据传输或错误)。发起调用的程序只是等待响应,不占用 CPU,因此从执行的角度来看是比较高效的。
传统的阻塞 I/O 模型是当今应用中最常用的模型。它的执行流程很简单,当发起 read 系统调用时,程序被阻塞并将上下文切换到内核。然后启动 read,当响应返回时,响应数据将从内核空间复制到用户空间缓冲区。然后程序解除阻塞(read 调用返回)。
从应用程序的角度来看,read 操作的时间很长。但是这个应用实际上是被阻塞的,这个 read 操作与内核中的其他任务其实是交替执行的。
同步非阻塞 I/O
同步阻塞比较低效,改进版是同步非阻塞 I/O。在该模型中,设备以非阻塞方式打开。这意味着(如图3所示),不用立即完成 I/O,read 可能会返回:无法立即执行的错误代码(EAGAIN 或 EWOULDBLOCK)。
非阻塞的含义是 I/O 命令可能不会立即生效,需要应用程序进行多次调用直到 I/O 完成。这可能非常低效,因为在大多数情况下,应用程序必须一直运行(busy 状态),直到数据可用或尝试在内核执行 read 命令的过程中去执行其他任务。如图3所示,同步非阻塞方式会导致 I/O 延迟,因为内核中可用的数据与调用 read 的用户返回之间的任何差距可能会拉低整体吞吐量。
异步阻塞 I/O
另一个阻塞范例是基于阻塞通知的非阻塞 I/O。在此模型中,非阻塞 I/O 被设定,然后使用 select 操作阻塞系统调用,直到 I/O 描述符有变动。select 调用可以为多个 I/O 描述符提供通知。对于每个描述符,你可以调用通知描述符的相关功能,如:写入数据、读取数据的可用性以及是否发生错误。
select 调用效率比较低。虽然它是异步通知的模式,但不建议用于高性能 I/O。
异步非阻塞 I/O (AIO)
异步非阻塞 I/O 模型是 I/O 并行操作的一种。read 请求立即返回,表示read 已成功调用。然后,应用程序可以在后台 read 操作完成前做其他事情。当 read 响应到达时,可以生成信号或基于线程回调来完成 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 的理想选择。这样一来,完成一次传输完成后,你可以马上开始下一个。
基于回调函数的异步通知
系统回调是一种备用的通知机制。该机制不是通过触发通知信号,而是通过调用用户空间中的函数来通知。初始化 aiocb 的 sigevent 结构,作为正在完成的特定请求的唯一标识;见清单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