一、为什么要有异步I/O

异步IO相比同步IO不会阻塞当前程序的执行,可以继续向下执行。即当应用程序发起一个IO操作后,调用者不会立刻得到结果,而是在内核完成IO操作后,通过信号或回调来通知调用者。

二、信号驱动I/O

信号驱动IO是异步IO的一种实现,在异步IO中,当文件描述符上可以执行I/O操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其他任务直到文件描述符可以执行I/O操作为止,此时内核会发送信号给进程。

使用信号驱动,程序需要按照如下步骤执行:

  • 通过指定O_NONBLOCK标志使能非阻塞I/O
  • 通过制定O_ASYNC标志使能异步I/O
  • 通过设置异步I/O时间的接收进程。当文件描述符上可执行I/O操作时会发送信号通知该进程。
  • 为内核发送的通知信号注册一个信号处理函数。异步信号I/O缺省是SIGIO,所以内核会给进程发送信号SIGIO。

以上步骤完成后,进程可以去执行其他的任务,当I/O就绪时,内核会向进程发送一个SIGIO信号,当进程接收到信号时,会执行预先注册号的信号处理函数,这样就可以在信号处理函数中进行I/O操作了

1、使能O_ASYNC

调用open时无法通过指定O_ASYNC标志来使能异步I/O,但是可以通过fcntl()函数添加O_ASYNC标志来使能I/O:

int flag;

flag = fcntl(fd,F_GETFL);     // 先从打开的文件描述符中获取原来的flag
flag |= O_ASYNC;              // 将O_ASYNC标志添加到flag
fcntl(fd,F_SETFL,flag);       // 重新设置flag

2、设置异步I/O时间的接收过程

为文件描述符设置异步I/O时间的接收进程,也就是设置异步I/O的所有者:

fcntl(fd,F_SETOWN,getpid());   // 也可以传入其他进程的pid

3、注册SIGIO信号的处理函数

通过signal()或sigaction()函数为SIGIO信号注册一个信号处理函数,当进程接收到内核发送过来的SIGIO信号时,会执行该函数。

代码实例:

#define _GNU_SOURCE // F_SETSIG
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#define MOUSE "/dev/input/mouse0"

static int fd;

static void sigio_handler(int sig)
{
    static int loops = 5;
    char buf[100] = {0};
    int ret;

    if(SIGIO != sig)
    {
        return ;
    }

    ret = read(fd,buf,sizeof(buf));
    if(0 < ret)
        printf("mouse : read %d bytes\n",ret);

    loops--;
    if(0>=loops)
    {
        close(fd);
        exit(0);
    }
}

int main(void)
{
    int flag;

    // 打开设备,使能非阻塞IO
    fd = open(MOUSE,O_RDONLY|O_NONBLOCK);
    if(-1 == fd)
    {
        perror("open mouse error");
        exit(-1);
    }

    // 使能异步IO
    flag = fcntl(fd,F_GETFL);
    flag |= O_ASYNC;
    fcntl(fd,F_SETFL,flag);

    // 设置异步IO的所有者
    fcntl(fd,F_SETOWN,getpid());

    // 注册信号回调函数
    signal(SIGIO,sigio_handler);
    
    for(;;)
    {
        sleep(1);
    }
}

运行结果:

ios异步初始化 异步io实现_ios异步初始化

但是使用默认信号SIGIO会存在一些问题,SIGIO是标准信号,不可靠信号,非实时信号,不支持信号排队机制,不知道文描述符发生了什么事件,未判断文件描述符是否处于可读的就绪态,所以需要进一步优化(实时信号替换)。

1、使用实时信号替换默认信号SIGIO

比如使用SIGRTMIN信号替换SIGIO,比如:

fcntl(fd,F_SETSIG,SIGRTMIN);

2、使用sigaction()函数注册信号处理函数

在应用程序中需要为实时信号注册信号处理函数,使用sigaction函数进行注册,sigaction原型:

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);

使用实例:

#define _GNU_SOURCE // F_SETSIG
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

#define MOUSE "/dev/input/mouse0"

static int fd;

static void io_handler(int sig,siginfo_t *info,void *context)
{
    static int loops = 5;
    char buf[100] = {0};
    int ret;

    if(SIGRTMIN != sig)
    {
        return ;
    }

    // 判断鼠标是否可读
    if(POLL_IN == info->si_code)
    {
        ret = read(fd,buf,sizeof(buf));
        if(0 < ret)
        {
            printf("mouse : read %d bytes\n",ret);
        }

        loops--;
        if(0>=loops)
        {
            close(fd);
            exit(0);
        }
    }
}

int main(void)
{
    struct sigaction act;
    int flag;

    // 打开设备,使能非阻塞IO
    fd = open(MOUSE,O_RDONLY|O_NONBLOCK);
    if(-1 == fd)
    {
        perror("open mouse error");
        exit(-1);
    }

    // 使能异步IO
    flag = fcntl(fd,F_GETFL);
    flag |= O_ASYNC;
    fcntl(fd,F_SETFL,flag);

    // 设置异步IO的所有者
    fcntl(fd,F_SETOWN,getpid());

    // 指定实时信号SIGRTMIN作为异步I/O通知信号
    fcntl(fd,F_SETSIG,SIGRTMIN);

    // 为实时信号SIGRTMIN注册信号处理函数
    act.sa_sigaction = io_handler;
    act.sa_flags = SA_SIGINFO;
    sigemptyset(&act.sa_mask);
    sigaction(SIGRTMIN,&act,NULL);

    for(;;)
    {
        sleep(1);
    }
}

运行结果:

 

ios异步初始化 异步io实现_c++_02

三、Linux异步I/O - Native AIO

Linux Native AIO是Linux支持的原生AIO,很多第三方的异步IO库,比如libeio和glibc AIO。很多三方库异步IO库不是真正的异步IO,而是通过多线程来模拟异步IO,比如libeio。

aio_*系列的调用是有glibc提供的,是glibc用线程+阻塞调用来模拟的,性能较差,为了能更多的控制io行为,可以使用更低级的libaio。

Ubuntu安装livaio:

sudo apt install libaio-dev

Linux AIO执行流程:

ios异步初始化 异步io实现_c++_03

Linux原生AIO处理流程:

  • 当应用程序调用io_submit系统调用发起一个异步IO操作后,回想内核的IO任务队列添加一个IO任务,并且返回成功。
  • 内核会在后台处理IO任务队列中的IO任务,然后把处理结果存储在IO任务中
  • 应用程序可以调用io_getevents

从上面流程可以看出,Linux异步IO操作主要由两个步骤组成:

  • 1)调用io_submit函数发起一个异步IO操作
  • 2)调用io_getevents函数获取异步IO的结果

实例代码: 

#define _GNU_SOURCE
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <fcntl.h>
 #include <libaio.h>#define FILEPATH "./aio.txt"
int main()
 {
     io_context_t context;     // 异步IO的上下文
     struct iocb io[1],*p[1] = {&io[0]};
     struct io_event e[1];
     unsigned nr_events = 10;
     struct timespec timeout;
     char *wbuf;
     int wbuflen = 1024;
     int ret,num=0,i;    posix_memalign((void **)&wbuf,512,wbuflen);
    memset(wbuf,'@',wbuflen);
     memset(&context,0,sizeof(io_context_t));    timeout.tv_sec = 0;
     timeout.tv_nsec = 10000000;    // 1.打开要进行异步IO的文件
     int fd = open(FILEPATH,O_CREAT | O_RDWR | O_DIRECT,0644);
     if (fd < 0) {
         printf("open error: %d\n", errno);
         return 0;
     }    // 2.创建一个异步IO的上下文
     if(0 != io_setup(nr_events,&context))
     {
         printf("io_setup error: %d\n", errno);
         return 0;
     }    // 3.创建一个异步IO任务
     io_prep_pwrite(&io[0],fd,wbuf,wbuflen,0);    // 4.提交异步IO任务
     if((ret = io_submit(context,1,p)) != 1)
     {
         printf("io_submit error: %d\n", ret);
         io_destroy(context);
         return -1;
     }    // 5.获取异步IO的结果
     while(1)
     {
         ret = io_getevents(context,1,1,e,&timeout);
         if (ret < 0) {
             printf("io_getevents error: %d\n", ret);
             break;
         }        if (ret > 0) {
             printf("result, res2: %d, res: %d\n", e[0].res2, e[0].res);
             break;
         }
     }
     return 0;
 }

编译命令:

cc aio_demo.c -laio

运行结果:

目录下会出现一个aio.txt的文件,内容为1024个@字符

程序说明:

  • 通过调用open系统调用打开要进行异步IO的文件,AIO操作必须设置O_DIRECT直接IO标志位
  • 调用io_setup系统调用创建一个异步IO上下文
  • 调用io_prep_pwrite或者io_prep_pread函数创建一个异步写或者异步读任务
  • 调用io_submit系统调用将异步IO提交到内核
  • 调用io_getevents系统调用获取异步IO的结果

以上示例使用while检测,还可以使用epoll结合eventfd,结合事件驱动的方式来获取异步IO操作的结果。