异步I/O学习笔记2

本文深入讲解了异步I/O(AIO)的基本原理及其在Linux系统中的实现方式,包括AIO的关键数据结构aiocb,以及如何使用aio_read、aio_write等函数发起异步读写操作。此外还介绍了如何通过aio_suspend、lio_listio等函数控制异步操作的执行流程,并探讨了AIO在设备驱动中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

在《异步I/O学习笔记》中讲到的异步通知,实际上只能算是一种能够实现异步I/O的机制。在《linux设备开发详解》中,对异步通知和异步I/O 的概念区分并不清楚。我的理解是AIO表面直译就是异步I/O,实际上它又是实现异步I/O的另外一种机制。这两种机制都有这样一共通点:都可以在驱动层使用信号来通知应用层的程序做相应的I/O操作。而AIO可以使用回调函数作为通知。

AIO基本思想:允许进程发起很多I/O操作,而不用阻塞或等待任何操作完成。稍后或在接收到I/O操作完成后,进程就可以检索I/O操作的结果。

与异步通知相比较,异步通知强调的是设备就绪后,驱动程序主动通知应用程序。强调的是应用程序处于被动的一面。AIO强调的多次操作后查询结果。概念本身有交叠的地方。

 

 基本的数据结构与函数调用

异步传输中,用aiocb结构体表征一次传输。它的结构体如下

struct aiocb {

int aio_fildes; //aio文件描述符

off_t aio_offset;

int aio_lio_opcode;//进行批量IO操作时有效,读写类型

volatile void *aio_buf;//数据缓冲

size_t aio_nbytes; //数据缓冲长度

int aio_reqprio;//信号优先级

struct sigevent aio_sigevent; //aio结束后该执行的操作

/* Internal fields */

};

以下引用的是网络博客内容

int aio_fildes

        需要操作的文件描述符,所关联的文件必须能支持seek操作。像pipe、socket等设备不能使用aio。

off_t aio_offset

        偏移量,aio将在文件的这个位置进行操作(读/写)。

volatile void* aio_buf

        指向缓冲区的指针,对于读操作该缓冲区用于存储读出来的数据;对于写操作则把需要写到文件的数据放在此缓冲区里面。

size_t aio_nbytes

        aio_buf所指向的缓冲区的长度。

int aio_reqprio

        AIO操作的优先级。

struct sigevent aio_sigevent

        当AIO操作结束(完成或者出错)时以什么样的方式通知调用aio的进程。aio_sigevent.sigev_notify为SIGEV_NONE表示不通知。SIGEV_SIGNAL表示以信号方式通知,aio_sigevent.sigev_signo指定用于通知的信号(一般用SIGIO)。除此以外,aio_sigevent.sigev_notify只能为SIGEV_THREAD,表示AIO操作结束时以线程的方式执行aio_sigevent.sigev_notify_function指定的函数。

int aio_lio_opcode

        这个参数仅当使用lio_listio时会用到,因为lio_listio允许同时发起一系列AIO操作,每个操作都可以是读或写,因此这个参数用来指定需要执行的操作,可以是:LIO_READ表示读,LIO_WRITE表示写,LIO_NOP表示无操作。

int aio_read(struct aiocb *aiocbp)

        发起一个异步读操作,当请求开始排队或者排队之前发现错误时函数立即返回,不会像read一样阻塞。

        函数会从文件(aiocbp->aio_fildes)的指定位置(aiocbp->aio_offset)最多读取aiocbp->aio_nbytes字节数据到aiocbp->aio_buf中。如果系统支持把IO区别优先次序的话,进入排队之前会根据aiocbp->aio_reqprio调整优先级。

        读操作完成时会根据aiocbp->aio_sigevent指定的方式通知调用进程。

        aio_read返回0表示没有错误发生,否则返回-1,errno指出以下几种出错情况:

        EAGAIN

                资源(临时)不足,请求无法进入排队。

        ENOSYS

                aio_read没有实现。

        EBADF

                aiocbp->aio_fildes指定的文件描述符无效。

        EINVAL

                aiocbp->aio_offset 或 aiocbp->aio_reqprio 的值无效。

        当aio_read返回成功后可以用aio_error和aio_return查询请求状态。如果aio_error返回EINPROGRESS说明操作还没有完成,如果aio_error返回0说明请求已经成功完成,否则返回值代表错误码。如果请求已经完成,可以用aio_return取得操作结果。返回值跟read函数相同。aio_error可能返回的错误码有:

        EBADF

                aiocbp->aio_fildes指定的文件描述符无效。

        ECANCELED

                请求在完成前被取消。

        EINVAL

                aiocbp->aio_offset的值无效。

 

int aio_write(struct aiocb *aiocbp)

        发起一个异步写操作,当请求开始排队或者排队之前发现错误时函数立即返回。函数将aiocbp->aio_buf中的前aiocbp->aio_nbytes字节数据写到aiocbp->aio_fildes指定文件的aiocbp->aio_offset位置。

        出错返回等信息基本同aio_read.

 

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

   用户可以通过这个函数来来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个aiocb引用列表,其中任何一个完成都会导致给函数返回。

 

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

        同时发起多个IO操作,每个操作都可以是读或者写,也可以同上面描述的LIO_NOP表示没有操作。这些操作可以针对同一个fd也可以针对不同的fd。

        参数mode可以为LIO_WAIT或者LIO_NOWAIT。前者表示lio_listio函数阻塞,直到list中的所有操作完成lio_listio返回,0表示全部成功,-1表示出错,如果要知道那个操作出错则必须使用aio_error遍历list;后者则不阻塞,当list中所有操作排队成功后立即返回0,返回-1表示出错。LIO_NOWAIT模式下,如果全部操作都完成(或出错)则会依照最后一个参数sig的设置通知调用进程,如果sig为NULL则表示不通知。

 

int aio_error(const struct aiocb *aiocbp)

        查询aio操作的状态,返回0表示已经完成,返回EINPROGRESS表示未完成,返回其他值表示出错。

int aio_return(const struct aiocb *aiocbp)

        获得aio操作的返回状态,对一个aio_error返回的状态为EINPROGRESS的aiocb调用aio_return,其结果是未定义的,所以调用aio_return之前一定要先用aio_error查询其状态。

        如果aio操作已经完成(aio_error返回0),则可以调用aio_return获得其操作结果,返回值同read,write,即返回成功读写的数据长度。

        注意每个已完成io请求aio_return只能调用一次,第二次调用导致的结果是未定义的。

 

信号知识补充:

struct sigaction {

 

          union {

 

                  __sighandler_t _sa_handler;

 

                  void (*_sa_sigaction)(int, struct siginfo *, void *);

 

          } _u;

 

          sigset_t sa_mask;

 

          unsigned long sa_flags;

 

          void (*sa_restorer)(void);

 

};

sa_handler的原型是一个参数为int,返回类型为void的函数指针。参数即为信号值,所以信号不能传递除信号值之外的任何信息;

  sa_sigaction的原型是一个带三个参数,类型分别为int,struct siginfo *,void *,返回类型为void的函数指针。第一个参数为信号值;第二个参数是一个指向struct siginfo结构的指针,此结构中包含信号携带的数据值;第三个参数没有使用。

sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。默认当前信号本身被阻塞。

sa_flags包含了许多标志位,比较重要的一个是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以传递到信号处理函数中。即使sa_sigaction指定信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误。

sa_flags值:

SA_NOCLDSTOP //此标志为on时,假如signum的值是SIGCHLD,则在子进程停止或恢复执行时不会传信号给调用本系统调用的进程。

  SA_NOCLDWAIT //此标志为on时,当调用此系统调用的进程之子进程终止时,系统不会建立zombie进程。

  SA_RESETHAND //此标志为on时,信号处理函数接收到信号后,会先将对信号处理的方式设为预设方式,而且当函数处理该信号时,后来发生的信号将不会被阻塞。

  SA_ONSTACK //如果利用sigaltstack()建立信号专用堆栈,则此标志会把所有信号送往该堆栈。

  SA_RESTART //此标志为on时,核心会自动重启信号中断的系统调用,否则返回EINTR错误值。

  SA_NODEFER //当此标志为on时,在信号处理函数处置信号的时段中,核心程序不会把这个间隙中产生的信号阻塞。

  SA_SIGINFO //此标志为on时,指定信号处理函数需要三个参数,所以应使用sa_sigaction替sa_handler。

 

应用程序实例:

#include <aio.h>

..

int fd, set;

struct aiocb my_aiocb;

 

fd = open("file.txt", O_RDONLY);

if( fd <0 )

{

    perror("open");

}

//清零aiocb结构体

bzero((char *) &my_aiocb, sizeof(struct aiocb));

//为aiocb请求分配数据缓冲区

my_aiocb.aio_buf = malloc(BUFSIZE + 1);

if(!my_aiocb.aio_buf)

    perror("malloc");

//初始化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)))

{

    // 获得异步读的返回值

}

else

{

  读失败,分析errror

}

 

b)用户空间异步IO aio_suspend()函数使用例程

 

struct aioct *cblist(MAX_LIST)

//清零aioct结构链表

bzero((char *)cblist, sizeof(cblist));

//将一个或更多的aiocb放入aioct结构体链表

cblist[0] = &my_aiocb;

ret = aio_read( &my_aiocb);

ret = aio_suspend( cblist, MAX_LIST, NULL);

c)用户空间异步IO lio_list()函数使用例程

 

struct aiocb aiocb1,aiocb2;

struct  aiocb *list[MAX_LIST];

...

//准备第一个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;//异步读操作

...//准备多个aiocb

bzero((char *)list, sizeof(list));

//将aiocb填入链表

list[0] = &aiocb1;

list[1]   = &aiocb2;

...

ret = lio_listio(LIO_WAIT, list, MAX_LIST, NULL); //发起大量IO操作

d) 使用信号作为AIO异步IO通知机制

void setup_io(..)

{

    int fd;

    struct sigaction sig_act;

    struct aiocb my_aiocb;

    ...

    //设置信号处理函数

     sigemptyset(&sig_act.sa_mask);

sig_act.sa_flags = SA_SIGINFO;

//指定信号处理函数需要三个参数,所以应使用sa_sigaction替sa_handler。

    sig_act.sa_sigaction = aio_completion_handler;

    //设置AIO请求

     bzero((char *)&my_aiocb, sizeof(struct aiocb));

    my_aiocb.aio_flags = fd;

    my_aiocb.aio_buf = malloc(BUF_SIZE + 1);

    my_aiocb.aio_nbytes = BUF_SIZE;

    my_aiocb.offset = next_offset;

    //连接AIO请求和信号处理函数

     my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNVAL;

//SIGEV_SIGNAL表示以信号方式通知调用aio的进程

my_aiocb.aio_sigevent.sigev_signo = SIGIO;

//aio_sigevent.sigev_signo指定用于通知的信号(一般用SIGIO)

    my_aiocb.aic_sigevent.sigev_value.sival_ptr = &my_aiocb;

    //将信号和处理函数绑定

    ret = sigaction(SIGION, &sig_act, NULL);//NULL,没有信号被阻塞

   ...

    ret = aio_read(&my_aiocb);

//信号处理函数

void aio_completion_handler(int signo, siginfo_t *info, void *context)

{

    struct aiocb *req;

    //确定是我们需要的信号

    if(info->si_signo == SIGIO)

    {

     req = (struct aiocb *)info->si_value.sival_ptr; //获得与信号相对应的aiocb;

     //请求的操作是否完成

     if(aio_error(req) ==0 )

     {

         ret = aio_return(req);

     }

    }

    return ;

}

 

e) 使用回调函数最为AIO的通知

oid setup_io(..)

{

    ...//同上

    //连接AIO请求和线程回调函数

    my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;

    my_aiocb.aio_sigevent.notify_function = aio_completion_handler;

// io_sigevent.sigev_notify只能为SIGEV_THREAD,表示AIO操作结束时以线程的方式执行aio_sigevent.sigev_notify_function指定的函数。

    //设置回调函数

    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(int signo, siginfo_t *info, void *context)

{

    struct aiocb *req;

    req = (struct aiocb *)sigval.sival_ptr; //获得aiocb;

     //请求的操作是否完成

    if(aio_error(req) ==0 )

    {

         ret = aio_return(req);

    }

    return ;

}

上述程序在创建aiocb请求之后,使用SIGEV_THREAD请求了一个线程回调函数作为通知方法。在回调函数中。通过(struct aiocb *)info->si_value.sival_ptr可以获得对应的aiocb指针,使用AIO函数可验证请求是否已经完成。

 

 

f)AIO与设备驱动

在Linux内核中,每个IO请求都对应一个kiocb结构体,其ki_filp成员指向对应的file指针,通过is_sync_kiocb可以判断某Kiocb时候为同步IO请求,如果非真,表示是异步IO请求。

 

  块设备和网络设备本身就是异步的。只有字符设备驱动必须明确指出应支持AIO.需要说明的是AIO对于大多数字符设备而言都不是必须的。只有少数才需要。

 

  在字符设备驱动程序中,file_operations包含了3个和AIO相关的函数。如下:

 

 ssize_t (*aio_read) (struct kiocb *iocb, char *buffer, size_t count ,loff_t offset);

 ssize_t (*aio_write) (struct kiocb *iocb, const char *buffer, size_t count ,loff_t offset);

 int (*aio_fsync) (struct kiocb *iocb, int datasync);

aio_read()和aio_write()与file_operation中的read()和write()中的offset参数不同,它直接传递值,而后者传递的是指针。这两个函数本身也不一定完成读写操作,它只是发起,初始化读写操作。

下面来看看实际的代码部分:

//异步读

static ssize_t xxx_aio_read(struct kiocb *iocb, char *buffer, size_t count ,loff_t offset)

{

    return xxx_defer_op(0, iocb, buf, count, pos);

}

 

//异步写

static ssize_t xxx_aio_write(struct kiocb *iocb, const char *buffer, size_t count ,loff_t offset)

{

    return xxx_defer_op(1, iocb, (char *)buf, count, pos);

}

 

//初始化异步IO

static int xxx_defer_op(int write, struct kiocb *iocb, char *buf, size_t count, loff_t pos)

{

  struct async_work *async_wk;

    int result;

    //当可以访问buffer时进行复制

  if(write)

  {

     result = xxx_write (iocb->ki_filp, buf, count, &pos );

//此处iocb->ki_filp指针对应file指针,那么此函数就可以看成是同步时通常写的write驱动函数

  }

  else

     {

           result = xxx_read (iocb->ki_filp, buf, count, &pos );

     }

     //如果是同步IOCB, 立即返回状态

  if(is_sync_kiocb(iocb))

      return resutl;

     //否则,推后几us执行

   async_wk = kmalloc(sizeof(*async_wk), GFP_KERNEL ));

   if(async_wk==NULL)

   return result;

   async_wk->iocb = iocb;

   async_ wk->result = result;

   INIT_WORK(&async_wk->work, xxx_do_deferred_op, async_wk);

   schedule_delayed_work(&async_wk->work, HZ/100);

   return -EIOCBOUEUED;//控制权限返回给用户空间

}

//延迟后执行

static void xxx_do_deferred_op(void *p)

{

      struct async_work *async_wk = (struct async_work*)p;

      aio_complete(async_wk_iocb, async_wk->result, 0);

      kfree(async_wk);

}

在上述代码中有一个async_work的结构体定义如下:

struct async_work

{

  struct kiocb *iocb;//kiocb结构体指针

  int result;//执行结果

  struct work_struct work; //工作结构体

};

在上边代码中最核心的是使用aync_work结构体将操作延迟,通过schedule_delayed_work可以调度其运行,而aio_complete的调用用于通知内核驱动程序已经完成了操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值