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

转自:https://www.ibm.com/developerworks/cn/linux/l-async/

AIO简介

Linux中最常见的输入输出(I/O)模型是同步I/O。在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止。这是很好的一种解决方案,因为调用应用程序在等待I/O请求完成时不需要使用任何中央处理单元(CPU)。但是在某些情况中,I/O请求可能需要与其他进程产生交叠。可移植操作系统接口(POSIX)异步I/O(AIO)应用程序接口(API)就提供了这种功能。在本文中,我们将对这个API概要进行介绍。并来了解如何使用它。

LINUX异步I/O是linux内核中提供的一个相当新的增强。它是2.6版本内核的一个标准特性,但是我们在2.4版本内核的补丁中也可以找到它。AIO背后的基本思想是允许进程发起多个I/O操作,而不用阻塞或等待任何操作完成。稍微或在接收到I/O操作完成的通知时,进程就可以检索I/O操作的结果。

I/O模型

在深入介绍AIO API之前,让我们先来探索一下linux上可以使用的不同I/O模型。这并不是一个详尽的介绍,但是我们将试图介绍最常见的一些模型来解释它们与异步I/O之间的区别。图一给出了同步和异步模型,以及阻塞和非阻塞的模型。

每个I/O模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点。本节将简要对其意一一进行介绍。

同步阻塞I/O

最常用的一个模型是同步组赛I/O模型。在这个模型中,用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞。这意味着应用程序会一直阻塞,指导系统调用完成为止(数据传输完成或发生错误)。调用应用程序处于一种不再消费CPU而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。

图2给出了传统的阻塞I/O模型,这也是目前应用程序中最为常用的一种模型。其行为非常容易理解,其用法对于典型的应用程序来说都非常有效。在调用read系统调用时,应用程序会阻塞并对内核进行上下文切换。然后会触发读操作,当响应返回时(从我们正在从中读取的设备中返回),数据就被移动到用户空间的缓冲区中,然后应用程序就会解除阻塞(read调用返回)。

从应用程序的角度来说,read调用会延续很长时间。实际上,在内核执行读操作和其其他工作时,应用程序的确会被阻塞。

注:I/O密集型与CPU密集型进程的比较

I/O密集型进程所执行的I/O操作比执行的处理操作更多,

CPU密集型的进程所执行的处理操作比I/O操作更多。

LINUX2.6的调度器实际上更加偏爱I/O密集型的进程,因为它们通常会发起一个I/O操作,然后进行阻塞,这就意味着其他工作都可以在两者之间有效地交错进行。

同步非阻塞I/O

同步阻塞I/O的一种效率稍低的变种是同步非阻塞I/O。在这个模型中,设备是以非阻塞的方式打开。这意味着I/O操作不会立即完成,read操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN或EWOULDELOCK),如图3所示。

非阻塞的实现是I/O命令可能并不会立即满足,需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图试行其他工作。正如图3所示的一样,这个方法可以引入I/O操作的延时,因为数据在内核中变为可用到用户调用read返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。

异步阻塞I/O

另外一个阻塞解决方案是带有阻塞通知的非阻塞I/O。在这种模型中,配置是非阻塞I/O,然后使用select系统调用来确定一个I/O描述符何时有操作,使select调用非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为了一个描述符提供通知。对于每个描述符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知。

select调用的主要问题是它的效率不是非常高。尽管这是异步通知使用的一种方便模型,但是对于高性能的I/O操作来说不建议使用。

异步非阻塞I/O(AIO)

最后,异步非阻塞I/O模型是一种处理与I/O重叠进行的模型。读请求会立即返回,说明read请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当read的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次I/O处理过程。

在一个进程张总为了执行多个I/O请求而对计算操作和I/O处理进行重叠处理的能力利用了处理速度之间的差异。当一个或多个I/O请求挂起时,CPU可以执行其他任务;或者更为常见是,在发起其他I/O的同时对已经完成的I/O进行操作。

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

异步I/O的动机

从前面的I/O模型的分类中,我们可以看出AIO的动机。这种阻塞模型需要在I/O操作开始时阻塞应用程序。这意味着不可能同时重叠进行处理和I/O操作。同步非阻塞模型允许处理和I/O操作重叠进行,但是这需要应用程序根据重现的规则来检查I/O操作的状态。这样剩下异步非阻塞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的展示显示了如何使用它。

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

AIO API

AIO 接口的 API 非常简单,但是它为数据传输提供了必需的功能,并给出了两个不同的通知模型。表 1 给出了 AIO 的接口函数,本节稍后会更详细进行介绍。

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

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

aio_read函数请求对一个有效的文件描述进行异步读操作。这个文件描述可以表示一个文件、管道、套接字。aio_read函数原型如下:

int aio_read(struct aiocb *aiocbp);

aio_read函数在请求进行排队之后会立即返回。如果执行成功,返回值就为0;如果出现错误,返回值就为-1,并设置errno的值。

要执行读操作,应用程序必须对aiocb结构初始化。下面这个简短的例子就展现了如何填充aiocb请求结构,并使用aio_read来执行异步读请求(现在暂时忽略通知)操作。它还展示了aio_error的用法,不过我们将稍后再作解释。

在清单 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)。

aio_error

aio_error 函数被用来确定请求的状态。其原型如下:

int aio_error(struct aiocb *aiocbp);

这个函数可以返回以下内容:

  • EINPROGRESS,说明请求尚未完成
  • ECANCELLED,说明请求被应用程序取消了
  • -1,说明发生了错误,具体错误原因可以查阅 errn

aio_return

异步 I/O 和标准块 I/O 之间的另外一个区别是我们不能立即访问这个函数的返回状态,因为我们并没有阻塞在 read 调用上。在标准的 read 调用中,返回状态是在该函数返回时提供的。但是在异步 I/O 中,我们要使用 aio_return 函数。这个函数的原型如下:

ssize_t aio_return(struct aiocb *aiocbp);

只有在aio_error调用确定请求已经完成(可能成功,也可能发送了错误)之后,才会调用这个函数。aio_return的返回值就等价于同步情况中read或wirte系统调用的返回值(所传输的字节数,如果发生错误,返回值就为-1)。

aio_write

aio_write函数用来请求一个异步写操作。其函数原型如下:

int aio_write(struct aiocb *aiocbp);

aio_write 函数会立即返回,说明请求已经进行排队(成功时返回值为 0,失败时返回值为 -1,并相应地设置 errno)。

这与 read 系统调用类似,但是有一点不一样的行为需要注意。回想一下对于 read 调用来说,要使用的偏移量是非常重要的。然而,对于 write 来说,这个偏移量只有在没有设置 O_APPEND 选项的文件上下文中才会非常重要。如果设置了 O_APPEND,那么这个偏移量就会被忽略,数据都会被附加到文件的末尾。否则,aio_offset 域就确定了数据在要写入的文件中的偏移量。

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。

注意,aio_suspend 的第二个参数是 cblist 中元素的个数,而不是 aiocb 引用的个数。cblist 中任何 NULL 元素都会被 aio_suspend 忽略。

如果为 aio_suspend 提供了超时,而超时情况的确发生了,那么它就会返回 -1errno 中会包含 EAGAIN

aio_cancel

aio_cancel 函数允许我们取消对某个文件描述符执行的一个或所有 I/O 请求。其原型如下:

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

要取消一个请求,我们需要提供文件描述符和 aiocb 引用。如果这个请求被成功取消了,那么这个函数就会返回 AIO_CANCELED。如果请求完成了,这个函数就会返回 AIO_NOTCANCELED

要取消对某个给定文件描述符的所有请求,我们需要提供这个文件的描述符,以及一个对 aiocbp 的 NULL 引用。如果所有的请求都取消了,这个函数就会返回 AIO_CANCELED;如果至少有一个请求没有被取消,那么这个函数就会返回 AIO_NOT_CANCELED;如果没有一个请求可以被取消,那么这个函数就会返回 AIO_ALLDONE。我们然后可以使用 aio_error 来验证每个 AIO 请求。如果这个请求已经被取消了,那么 aio_error 就会返回 -1,并且 errno 会被设置为 ECANCELED

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_NOWAITLIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成为止。在操作进行排队之后,LIO_NOWAIT就会返回。list 是一个 aiocb 引用的列表,最大元素的个数是由 nent 定义的。注意 list 的元素可以为 NULLlio_listio 会将其忽略。sigevent 引用定义了在所有 I/O 操作都完成时产生信号的方法。

对于 lio_listio 的请求与传统的 read 或 write 请求在必须指定的操作方面稍有不同,如清单 4 所示

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

AIO 通知

现在我们已经看过了可用的 AIO 函数,本节将深入介绍对异步通知可以使用的方法。我们将通过信号和函数回调来探索异步函数的通知机制。

使用信号进行异步通知

使用信号进行进程间通信(IPC)是 UNIX 中的一种传统机制,AIO 也可以支持这种机制。在这种范例中,应用程序需要定义信号处理程序,在产生指定的信号时就会调用这个处理程序。应用程序然后配置一个异步请求将在请求完成时产生一个信号。作为信号上下文的一部分,特定的 aiocb 请求被提供用来记录多个可能会出现的请求。清单 5 展示了这种通知方法。

清单 5. 使用信号作为 AIO 请求的通知

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

对于性能来说,这个处理程序也是通过请求下一次异步传输而继续进行 I/O 操作的理想地方。采用这种方式,在一次数据传输完成时,我们就可以立即开始下一次数据传输操作。

使用回调函数进行异步通知

另外一种通知方式是系统回调函数。这种机制不会为通知而产生一个信号,而是会调用用户空间的一个函数来实现通知功能。我们在 sigevent结构中设置了对 aiocb 的引用,从而可以惟一标识正在完成的特定请求。请参看清单 6。

在清单 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 应用程序中使用的传统阻塞模式都不同,但是异步通知模型在概念上来说却非常简单,可以简化我们的设计。

时间: 2024-11-07 13:44:04

使用异步I/O大大提高应用程序的性能的相关文章

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

Linux® 中最常用的输入/输出(I/O)模型是同步 I/O.在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止.这是很好的一种解决方案,因为调用应用程序在等待 I/O 请求完成时不需要使用任何中央处理单元(CPU).但是在某些情况中,I/O 请求可能需要与其他进程产生交叠.可移植操作系统接口(POSIX)异步 I/O(AIO)应用程序接口(API)就提供了这种功能.在本文中,我们将对这个 API 概要进行介绍,并来了解一下如何使用它. AIO 简介 Linux 异步 I/O 是

[转载] 使用异步 I/O 大大提高应用程序的性能

原文: http://www.ibm.com/developerworks/cn/linux/l-async/ Linux® 中最常用的输入/输出(I/O)模型是同步 I/O.在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止.这是很好的一种解决方案,因为调用应用程序在等待 I/O 请求完成时不需要使用任何中央处理单元(CPU).但是在某些情况中,I/O 请求可能需要与其他进程产生交叠.可移植操作系统接口(POSIX)异步 I/O(AIO)应用程序接口(API)就提供了这种功能.在

使用Memcached提高.NET应用程序的性能

在应用程序运行的过程中总会有一些经常需要访问并且变化不频繁的数据,如果每次获取这些数据都需要从数据库或者外部文件系统中去读取,性能肯定会受到影响,所以通常的做法就是将这部分数据缓存起来,只要数据没有发生变化每次获取这些数据的时候直接从内存中区获取性能肯定会大大地提高.在.NET中提供了一个Cache类可以实现这些功能.在ASP.NET中可以通过HttpContext 对象的 Cache 属性或 Page 对象的 Cache 属性来获取这个类的实例. 在大部分情况下我们都可以使用Cache类来提高

DBUtils开源JDBC类库,对JDBC简单封装(作用是:简化编码工作量,同时不会影响程序的性能)

DBUtils:提高了程序的性能,编程更加简便 三个架包 mysql-connector-java-jar commons-dbcp-1.4jar commons-pool-1.5.5jar 导进去 ------------------------------------------------------------------------------- //数据库连接池:三个开源的架包导进去 数据库连接池连接代码(其方法在TestJDBC()类中): public Connection te

提高WPF程序性能的几条建议

这篇博客将介绍一些提高WPF程序的建议(水平有限,如果建议有误,请指正.) 1. 加快WPF程序的启动速度: (1).减少需要显示的元素数量,去除不需要或者冗余的XAML元素代码. (2).使用UI虚拟化,只显示当前需要显示的元素. (3).不要把不要显示的自定义控件隐藏在主界面中,虽然它们不会显示出来,但是程序启动时还是会去计算自定义控件所需的空间和位置. 2. 耗时操作放在放在非UI线程上处理,保持UI的顺畅.处理完成后如果需要在UI上展示,调用Dispatcher.BeginInoke()

提高应用程序的总体性能七方法

在本文中,我们将详细地描述怎样通过调整servlet和JSP页面,来提高应用程序的总体性能.本文的目的是通过对servlet和JSP的一些调优技术来极大地提高你的应用程序的性能,并因此提升整个J2EE应用的性能.通过这些调优技术,你可以发现其实并不是某种技术平台(比如J2EE和.NET之争)决定了你的应用程序的性能,重要是你要对这种平台有一个较为深入的了解,这样你才能从根本上对自己的应用程序做一个优化. 想学习更多关于java的知识,可以点击<Java EE软件工程师>进行学习. 方法一:在s

(转)对《30个提高Web程序执行效率的好经验》的理解

阅读了博客园发布的IT文章<30个提高Web程序执行效率的好经验>,这30条准则对我们web开发是非常有用的,不过大家可能对其中的一些准则是知其然而不知其所以然. 下面是我对这些准则的理解和分析,有些有关JS性能的准则,我也测试了它们的差异,大家可以下载DEMO页面,如有理解不正确的地方,请大家指正.也非常欢迎大家补充. 测试环境: OS:Vista; Processor:3.40GHz; Memory: 2.00GB; System type: 32-bit Operating System

每天一个JavaScript实例-使用缓存计算(memoization)来提高应用程序性能

<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>每天一个JavaScript实例-使用缓存计算(memoization)来提高应用程序性能</title> <script> window.onload = functio

使用Chrome DevTools的Timeline和Profiles提高Web应用程序的性能

来源: http://www.oschina.net/translate/performance-optimisation-with-timeline-profiles 我们都希望创建高性能的Web应用程序.由于我们的应用程序变得越来越复杂,我们可能想要支持丰富的画面以及理想的60帧/秒,这能保证我们的应用程序响应灵敏且生动流畅. 知道如何衡量和提高性能,是一个有用的技能,在这短短的文章中,我会带您简单回顾关于如何通过 Chrome DevTools的 Timeline和Profiles做到这一