未加星标

Linux中的异步I/O模型

字体大小 | |
[系统(linux) 所属分类 系统(linux) | 发布者 店小二04 | 时间 2018 | 作者 红领巾 ] 0人收藏点击收藏
前言

这篇文章是翻译自 Introduction to AIO ,它非常简明的介绍了各种I/O模型的异同,并且详细的介绍了异步I/O在linux的使用方法。以前一直对异步I/O的了解非常少,翻译这篇文章算是对各种网络模型的一个总结,顺便学习了Linux中的异步I/O模型。一举两得,哈哈!

AIO简介

Linux的异步I/O是内核中新增的功能,在2.6版本中被正式引入,但是你可以通过补丁在2.4内核中使用。AIO(asynchronous I/O)背后的基本思想是允许进程启动大量I/O操作,而不必阻塞或等待任何操作完成。 在稍后的某个时间或者收到I/O操作完成的通知,可以获取I/O操作的结果。

I/O模型

在讲解AIO的API之前,我们先介绍一下Linux中目前支持的各种I/O模型。我们不会详细介绍所有I/O模型,只会介绍常见的I/O模型用来与AIO做对比。图1展示了同步和异步,以及阻塞和非阻塞四种模型。


Linux中的异步I/O模型

这些I/O模型中的每一个都有它适合的应用场景。接下来我们会大概介绍一下每种I/O模型。

同步阻塞I/O模型

同步阻塞I/O模型是最常见的模型之一,用户空间的应用程序在执行系统调用之后会被阻塞,直到系统调用完成(数据传输完成或者出现错误)。此时应用程序只是处于简单的等待响应状态不会消耗CPU,所以站在CPU的角度他是高效的。

图2描述了至今在应用程序中依然非常常用的阻塞I/O模型。它的行为很好理解,并且对于典型应用来说使用它是非常有效的。当 read 系统函数被调用时,应用程序被阻塞,并且上下文切换到内核空间。读操作开始执行,当请求返回(从你读取的设备中)时数据被传送到用户空间的buffer中。然后应用程序被唤醒( read 系统调用返回)。


Linux中的异步I/O模型

图2.同步阻塞I/O模型的典型流程

从应用程序的角度来看, read 系统调用持续了很长一段时间,但实际上读操作在和其他内核任务一起执行,此时应用程序处于阻塞状态。

同步非阻塞I/O模型

同步阻塞I/O的一种变体是效率较低的同步非阻塞I/O。在这种模型下,设备以非阻塞方式打开,这意味着如果不能立即完成I/O操作 read 系统调用会返回一个错误码(EAGAIN或EWOULDBLOCK)来指明无法立即完成读操作,如图3。


Linux中的异步I/O模型

图3.同步非阻塞I/O模型的典型流程

非阻塞意味着如果I/O操作不能立即完成,则需要应用程序多次调用直到任务完成。这可能非常低效,因为大多数时候应用程序必须忙等待或者尝试做其他事情直到数据可用。如图3所示这种方式也可能会造成延时,因为数据可用和用户调用 read 系统调用之间的时间间隔会降低整体数据的吞吐量。

异步阻塞I/O模型

另一种阻塞范例是具有阻塞通知的非阻塞I/O。在这种模型下,设备还是以非阻塞方式打开,然后应用程序阻塞在 select 系统调用中,用它来监听可用的I/O操作。 select 系统调用最大的好处是可以监听多个描述符,而且可以指定每个描述符要监听的事件:可读事件、可写事件和发生错误事件。


Linux中的异步I/O模型

图4.异步阻塞I/O模型的典型流程

select 系统调用的主要问题是效率不高。虽然它是一个非常方便的异步通知模型,但不建议将其用于高性能I/O中。(译者注:高性能场景一般使用 epoll 系统调用)

异步非阻塞I/O模型(AIO)

最后是异步非阻塞I/O模型他是可以并行处理I/O的模型之一。异步非阻塞I/O模型的读请求会立即返回,表明读操作成功启动。然后应用程序就可以在读操作完成之前做其他的事情。当读操作完成时,内核可以通过信号或者基于线程的回调函数来通知应用程序读取数据。


Linux中的异步I/O模型

图5.异步非阻塞I/O模型的典型流程

在单个进程可以并行执行多个I/O请求是因为CPU的处理速度要远大于I/O的处理速度。 当一个或多个I/O请求在等待处理时,CPU可以处理其他任务或者处理其他已完成的I/O请求。

异步I/O的有优点

在上面的介绍中可以发现同步阻塞模型在I/O请求时会被阻塞所以不能并行处理I/O请求。同步非阻塞模型可以并行处理但是它要求应用程序定期检查I/O的状态。只剩下异步模型可以并行处理I/O请求并且能够I/O完成通知。 select 系统函数的功能和AIO类似,但是它仍然是阻塞的,只不过它是阻塞在I/O通知上而不是I/O调用时。

Linux下的AIO

传统I/O模型中每个I/O通道都会使用一个唯一的句柄来指定,类UINX系统中这个句柄是文件描述符。阻塞I/O中你初始化一个I/O通道,系统调用会返回一个描述符给你或者出错返回错误码。

在异步I/O中,你可以同时初始化多个I/O通道。这样每个I/O通道都需要保存一个唯一的上下文,以便于当I/O操作完成后你能够识别时哪一个I/O通道。在AIO中这个上下文就是 aiocb (AIO I/O Control Block)结构体。这个结构体保存了每个I/O通道的所有信息包括用于缓存数据的用户空间缓冲区。当I/O操作完成时,内核会提供这个I/O通道特定的 aiocb 结构体。下面的API会展示怎么使用它。

AIO的API

AIO的接口API非常简单,但它为几种不同的通知模型都提供了必要的函数。表1展示了AIO的所有API函数。

API函数 解释 aio_read 异步读请求 aio_error 检查异步请求的状态 aio_return 获取已完成的异步请求的返回状态 aio_write 异步写请求 aio_suspend 挂起调用进程,直到一个或多个异步请求完成(或者失败) aio_cancel 取消一个异步请求 lio_listio 初始化I/O操作列表

上面的每个API函数都是通过 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_read

aio_read 函数用于对一个有效的文件描述符发送异步读请求。这个文件描述符可以是一个文件、套接字或者管道。 aio_read 函数的定义如下:

int aio_read( struct aiocb *aiocbp );

当读请求被插入到队列之后 aio_read 函数会立即返回,成功时返回值为0,失败时返回值为-1,并且会设置 errno 全局变量。要执行读请求应用程序必须初始化 aiocb 结构体。下面的示例程序展示了如何填充 aiocb 结构体并使用 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 */ }

示例程序首先打开你要读取数据的文件、初始化 aiocb 结构体为0,然后分配一块内存空间并将返回值赋值给 aio_buf ,将内存空间长度赋值给 aio_nbytes ,将 aio_offset 设置为0(表示同文件头开始读取数据),将你要读取的文件描述符赋值给 aio_fildes 。设置完这些字段之后调用 aio_read 函数。之后你可以调用 aio_error 函数来检查 aio_read 的状态。如果状态一直是 EINPROGRESS 则忙等直到状态改变,此时你的读请求要么成功要么失败。

请注意这和使用标准库函数执行读操作的区别,除了 aio_read 本身的不同之外,另一个区别是设置读操作时的偏移,在标准库函数中这个偏移在文件描述符的上下文中维护,每次读操作都会自动更新文件偏移,所以接下来的读操作总是读取的是下一个数据块。这在异步I/O中是不可能实现的因为你可以同时执行多个读操作,所以你必须每次执行读操作时自己指定文件偏移。

aio_error

aio_error 函数用于检查请求的状态。它的定义如下:

int aio_error( struct aiocb *aiocbp );

此函数可以返回一下信息:

errno aio_return

异步I/O和标准阻塞I/O的另外一个不同之处在于你不能立即访问函数的返回状态,因为你没有被阻塞在 read 系统调用上。标准的 read 系统调用会将返回状态赋值在函数的返回值上。在异步I/O中你只能使用 aio_return 函数,此函数的定义如下:

ssize_t aio_return( struct aiocb *aiocbp );

这个函数只能在 aio_error 返回请求完成(成功或者出错)之后被调用。它的返回值和同步模型中的 read 和 write 系统调用的返回值相同(成功传输的字节数或者错误返回-1)。

aio_write

aio_write 用于异步I/O中的写请求,此函数的定义如下:

int aio_write( struct aiocb *aiocbp );

aio_write 函数会立即返回,表示这个请求已经被加入到写队列中(成功时返回0,失败返回-1,并设置 errno 全局变量)。它和异步读函数类似但是有一个区别需要特别注意:异步读函数中设置文件偏移是非常重要的,但是在异步写操作中文件偏移只有在 O_APPEND 选项没有设置时才会起作用。如果 O_APPEND 选项被设置,则文件偏移会被忽略,数据总是会写入到文件的末端,否则数据会被写入到文件偏移所指定的地方。

aio_suspend

你可以调用 aio_suspend 函数阻塞进程直到产生一个信号来通知异步I/O请求已经完成,或者超时。调用者传入一组指向 aiocb 结构体的指针,至少其中一个完成操作则 aio_suspend 函数返回,此函数的定义如下:

int aio_suspend( const struct aiocb *const cblist[], int n, const struct timespec *timeout );

aio_suspend 函数的使用非常简单,一组指向 aiocb 结构体的指针。只要其中一个完成操作,此函数就会返回0成功或者-1失败,代码如下:

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 。

aio_cancel

aio_cancel 函数允许你取消一个或者一个给定文件描述符的所有未完成I/O请求。函数定义如下:

int aio_cancel( int fd, struct aiocb *aiocbp );

如果需要取消单个请求,需要提供文件描述符和一个 aiocb 结构体指针。如果I/O请求被成功取消,此函数会返回 AIO_CANCELED ,如果I/O请求已经完成,此函数会返回 AIO_NOTCANCELED 。

如果需要取消给定描述符的所有请求。需要提供此描述符并将 aiocbp 设置为NULL。如果全部被取消则会返回 AIO_CANCELED ,如果至少有一个不能被取消则会返回 AIO_NOT_CANCELED ,如果没有请求可以被取消则会返回 AIO_ALLDONE 。然后你可以使用 aio_error 函数来检查每个AIO请求,如果此I/O请求被取消则 aio_error 会返回-1,并且 errno 会被设置为 ECANCELED 。

lio_listio

最后,AIO提供 lio_listio 函数用于同时初始化多个 aiocb 结构体。这个函数非常重要它意味着你可以在一次用户空间到内核的上下文切换(系统调用)上启动多个I/O操作。从性能的角度来看他是非常棒的,值得研究一番。 lio_listio 函数的定义如下:

int lio_listio( int mode, struct aiocb *list[], int nent, struct sigevent *sig );

其中的 mode 参数可以填写为 LIO_WAIT 或者 LIO_NOWAIT 。 LIO_WAIT 会阻塞调用直到所有I/O请求完成。 LIO_NOWAIT 会在I/O请求被加入到队列之后立即返回。 list 参数用于存放 aiocb 结构体的指针数组,数组的最大长度由参数 nent 指定。注意 list 数组中的NULL元素会被 lio_listio 函数直接忽略。 sigevent 参数用于指定所有I/O请求完成之后的信号通知方法。

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

aio_lio_opcode 成员被赋值为 LIO_READ 表示为读操作。如果是写操作则为 LIO_WRITE 。也可以使用 LIO_NOP 表示无操作。

AIO通知

此时你已经知道了AIO的所有函数,接下来讨论几种异步通知的方法。信号和函数回调都会被介绍。

通过信号通知异步I/O

通过信号来进行进程间的通信时UNIX系统中的传统方法,它也支持AIO。在下面的示例中,应用程序定义了一个信号处理函数当指定信号产生时会调用此函数。然后设置异步请求完成时使用信号通知方式。 提供一个 aiocb 结构体作为信号上下文的一部分用于识别具体是哪一个I/O请求。

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

示例中设置了操作系统监听 SIGIO 信号并调用 aio_completion_handler 函数来处理,然后设置 aio_sigevent 结构体指定异步请求完成时发起 SIGIO 信号通知(通过 aio_sigevent 结构体中的 SIGEV_SIGNAL 指定)。当读请求完成时,信号处理函数通过信号中的 si_value 结构体提取特定的 aiocb 结构体指针,然后检查它的错误状态和返回状态来确定I/O操作是否已经完成。

从性能的角度考虑,在信号处理函数中继续调用下一次异步I/O请求是一个非常好的选择。这样你就可以在完成一次I/O请求之后立即开始下一次I/O请求。

通过回调函数通知异步I/O

另一种通知机制是系统回调。不同于信号通知方式,系统回调是通过调用用户空间的一个函数来完成通知的。通过将 aiocb 结构体的指针赋值给 aio_sigevent 结构体中来识别特定的I/O请求。示例如下:

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

示例中创建一个 aiocb 结构体之后,通过 SIGEV_THREAD 来指定使用基于线程的回调函数进行通知。然后指定一个特定的回调函数来处理通知并加载要传递给回调函数的上下文(在这个例子中是一个 aiocb 结构体的指针)。在回调函数中通过传入的 sigval 指针获取 aiocb 结构体,然后使用AIO函数检查I/O操作是否已经完成。

AIO的系统设置

proc 文件系统中有两个可以针对异步I/O性能进行调整的虚拟文件:

/proc/sys/fs/aio-nr /proc/sys/fs/aio-max-nr 总结

使用异步I/O可以帮助你创建更快更高效的I/O应用。如果你的应用程序可以并行处理I/O请求,AIO可以帮助你创建更加有效利用CPU的应用程序。而且这种I/O模型不同于绝大多数应用中使用的传统阻塞模型,异步通知模型非常简单可以简化你的设计(译者注:不是很赞同这里的说法,特别是信号通知方式,并不是很简单。。。)。

相关资料 PDF版本 POSIX.1b implementation 从GNU库的视角解释了AIO的内部细节。 Realtime Support in Linux 从调度和POSIX I/O到POSIX线程和 high resolution timers(HRT) 的角度讨论AIO和一些实时扩展。 Design Notes Linux内核中的关于AIO的设计和实现。

本文系统(linux)相关术语:linux系统 鸟哥的linux私房菜 linux命令大全 linux操作系统

代码区博客精选文章
分页:12
转载请注明
本文标题:Linux中的异步I/O模型
本站链接:https://www.codesec.net/view/621075.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 系统(linux) | 评论(0) | 阅读(155)