【I/O模型】异步IO的原理和使用
为什么要有异步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操作了
使能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
设置异步I/O时间的接收过程
为文件描述符设置异步I/O时间的接收进程,也就是设置异步I/O的所有者:
fcntl(fd,F_SETOWN,getpid()); // 也可以传入其他进程的pid
注册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);
}
}
运行结果:
但是使用默认信号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);
}
}
运行结果:
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执行流程:
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操作的结果。
参考文章:
正点原子:嵌入式Linux C应用编程指南 - 13.3章
https://blog.csdn.net/shreck66/article/details/48765533
https://blog.csdn.net/HEYUTAO007/article/details/7065166
Linux 原生异步 IO 原理与使用(Native AIO) - 腾讯云开发者社区-腾讯云 (tencent.com)