Chinaunix首页 | 论坛 | 博客
  • 博客访问: 52678
  • 博文数量: 104
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 10
  • 用 户 组: 普通用户
  • 注册时间: 2016-09-11 15:25
文章分类

全部博文(104)

文章存档

2016年(104)

我的朋友

分类: 嵌入式

2016-09-20 09:48:42

1、异步通知的概念

异步通知的意思是:一旦设备就绪,则主动通知应用程序。异步通知类似于“中断”的机制,而不像之前学的阻塞型I/Opoll。阻塞I/O意味着一直等待设备可访问后再访问,非阻塞I/O使用poll意味着查询设备是否可访问,而异步通知则意味着设备通知自身可访问,实现了异步I/O

 

2、应用层中启用异步通知机制的三个步骤

1调用signal函数,让指定的信号SIGIO与处理函数sig_handler对应。

  1. signal(SIGIO, sig_handler);

2指定一个进程作为文件的属主(filp->owner)”,这样内核才知道信号要发给哪个进程。

  1. fcntl(fd, F_SET_OWN, getpid());

3在设备文件中添加FASYNC标志,即异步通知模式。

  1. f_flags = fcntl(fd, F_GETFL);

  2. fcntl(fd, F_SETFL, f_flags | FASYNC);

3、驱动中需要实现的异步通知

驱动中的3项工作和应用程序中的3项工作是一一对应的。如下图(自己拍的,没来得及处理):

内核已经实现了两步,我们要实现一个简单的传参。由于FASYNC标志改变时,驱动程序中的fasync()函数得以执行,故在驱动中要实现fasync()函数。

1)定义结构体fasync_struct

  1. struct fasync_struct *async_queue;//异步结构体指针

2)实现XXX_fasync,把函数fasync_helperfdfilp和定义的结构体传给内核。

  1. int XXX_fasync (int fd, struct file *filp, int mode)
  2. {
  3. struct XXX_dev *dev = filp->private_data;
  4. return fasync_helper(fd, filp, mode, &dev->async_queue);
  5. }

3)当设备可写时,调用函数kill_fasync发送信号SIGIO给内核。

  1. if (dev->async_queue){
  2. XXX_fasync(&dev->async_queue, SIGIO, POLL_IN);
  3. }

4)当设备关闭时,需要将fasync_struct从异步队列中删除:

  1. XXX_fasync(-1, filp, 0);

4、异步I/O

1)简介

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

 

I/O 模型

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

1. 基本 Linux I/O 模型的简单矩阵


 

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

 同步阻塞 I/O

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

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



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

 同步非阻塞 I/O

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

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


 

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

 异步阻塞 I/O

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

4. 异步阻塞 I/O 模型的典型流程 (select)


 

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

 异步非阻塞 I/OAIO

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

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


 

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

3 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 调用进行阻塞。

4Linux 上的 AIO

在异步非阻塞 I/O 中,我们可以同时发起多个传输操作。这需要每个传输操作都有惟一的上下文,这样我们才能在它们完成时区分到底是哪个传输操作完成了。在 AIO 中,这是一个 aiocbAIO I/O Control Block)结构。这个结构包含了有关传输的所有信息,包括为数据准备的用户缓冲区。在产生 I/O (称为完成)通知时,aiocb 结构就被用来惟一标识所完成的 I/O 操作。这个 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. struct aiocb {
  2.   int aio_fildes; // File Descriptor
  3.   int aio_lio_opcode; // Valid only for lio_listio (r/w/nop)
  4.   volatile void *aio_buf; // Data Buffer
  5.   size_t aio_nbytes; // Number of Bytes in Data Buffer
  6.   struct sigevent aio_sigevent; // Notification Structure

  7.   /* Internal fields */
  8.   ...
  9. };


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

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


  1. int aio_read( struct aiocb *aiocbp );

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

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


点击(此处)折叠或打开

  1. #include <aio.h>
  2. ...
  3.   int fd, ret;
  4.   struct aiocb my_aiocb;

  5.   fd = open( "file.txt", O_RDONLY );
  6.   if (fd < 0) perror("open");

  7.   /* Zero out the aiocb structure (recommended) */
  8.   bzero( (char *)&my_aiocb, sizeof(struct aiocb) );

  9.   /* Allocate a data buffer for the aiocb request */
  10.   my_aiocb.aio_buf = malloc(BUFSIZE+1);
  11.   if (!my_aiocb.aio_buf) perror("malloc");

  12.   /* Initialize the necessary fields in the aiocb */
  13.   my_aiocb.aio_fildes = fd;
  14.   my_aiocb.aio_nbytes = BUFSIZE;
  15.   my_aiocb.aio_offset = 0;

  16.   ret = aio_read( &my_aiocb );
  17.   if (ret < 0) perror("aio_read");

  18.   while ( aio_error( &my_aiocb ) == EINPROGRESS ) ;

  19.   if ((ret = aio_return( &my_iocb )) > 0) {
  20.     /* got ret bytes on the read */
  21.   } else {
  22.     /* read failed, consult errno */
  23.   }


在清单
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_error 函数被用来确定请求的状态。其原型如下:


  1. int aio_error( struct aiocb *aiocbp );

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

·       EINPROGRESS,说明请求尚未完成

·       ECANCELLED,说明请求被应用程序取消了

·       -1,说明发生了错误,具体错误原因可以查阅 errno

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


  1. ssize_t aio_return( struct aiocb *aiocbp );

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

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


  1. int aio_write( struct aiocb *aiocbp );

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

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

我们可以使用 aio_suspend 函数来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个 aiocb 引用列表,其中任何一个完成都会导致 aio_suspend 返回。 aio_suspend 的函数原型如下:

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

aio_suspend 的使用非常简单。我们要提供一个 aiocb 引用列表。如果任何一个完成了,这个调用就会返回 0。否则就会返回 -1,说明发生了错误。请参看清单 3


点击(此处)折叠或打开

  1. struct aioct *cblist[MAX_LIST]

  2. /* Clear the list. */
  3. bzero( (char *)cblist, sizeof(cblist) );

  4. /* Load one or more references into the list */
  5. cblist[0] = &my_aiocb;

  6. ret = aio_read( &my_aiocb );
  7. ret = aio_suspend( cblist, MAX_LIST, NULL );

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

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

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

最后,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 所示。


点击(此处)折叠或打开

  1. struct aiocb aiocb1, aiocb2;
  2. struct aiocb *list[MAX_LIST];
  3. ...
  4. /* Prepare the first aiocb */
  5. aiocb1.aio_fildes = fd;
  6. aiocb1.aio_buf = malloc( BUFSIZE+1 );
  7. aiocb1.aio_nbytes = BUFSIZE;
  8. aiocb1.aio_offset = next_offset;
  9. aiocb1.aio_lio_opcode = LIO_READ;
  10. ...
  11. bzero( (char *)list, sizeof(list) );
  12. list[0] = &aiocb1;
  13. list[1] = &aiocb2;
  14. ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );

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

 

本文后半部分选自文献:https://www.ibm.com/developerworks/cn/linux/l-async/

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