典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)

Posted _BitterSweet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)相关的知识,希望对你有一定的参考价值。

1.典型IO模型

1.1 什么是IO模型

  • 我们在执行一个程序时,这个程序要求外设进行输入或者输出,内核在处理这个数据时,都在做什么?

  • 第一步:等待IO就绪,已经准备好需要的资源了,可以开始操作
    在这里插入图片描述

  • 第二步:将数据拷贝到缓冲区当中(接受缓冲区&发送缓冲区)

1.2 阻塞IO模型

  • 资源不可用的情况下,IO请求一直被阻塞,直到资源可以用
    在这里插入图片描述

  • 以钓鱼举例子:钓鱼的时候,将鱼钩抛入水中(发起IO调用),一直盯着鱼漂(等待IO就绪),鱼儿咬钩(拷贝数据到缓冲区),将鱼儿钓上来(IO调用返回)

  • 阻塞IO的特点:1.发起IO调用之后,等待的时间取决于内核 2.在等待的过程中,执行流是被挂起的(对CPU的利用率低) 3.在IO就绪到拷贝数据之间,实时性很高(响应快)

1.3 非阻塞IO模型

  • 我们在使用程序是轮询的方式,一直询问内核数据有没有准备好
  • 资源不可用的时候,IO请求不会被阻塞,而是直接返回,返回当前资源不可用(EBUSY)

在这里插入图片描述


  • 以钓鱼举例子:钓鱼的时候,将鱼钩抛入水中,看一眼鱼漂,鱼漂如果没动,则看一会手机,再看一眼鱼漂,鱼漂没动,再看一会手机,如此往复,直到鱼儿咬钩,将鱼钓上来
  • 非阻塞IO的特点:1.非阻塞IO对CPU的利用率比阻塞IO高 ``2.代码结构相对复杂 3.需要搭配循环使用,直到IO请求完成 4.IO准备就绪到数据拷贝之间,实时性不高(因为可能在你看手机的过程中,鱼儿咬钩了)

区别:在资源不可用的情况下,就看系统的调用是否立即返回

  • 立即返回:非阻塞IO
  • 没有立即返回:阻塞IO

1.4 信号驱动IO模型

  • 信号驱动IO:内核态中把数据准备完成之后,使用之前定义的SIGIO信号通知应用程序进行IO操作

流程:
1.自定义一个IO信号(SIGIO)的处理函数,再处理函数当中发起IO调用
2.程序收到一个IO信号(SIGIO),内核就会调用自定义的处理函数,在自定义的处理函数中发起IO调用
在这里插入图片描述

  • 以钓鱼举例子:先在鱼杆上绑一个铃铛,将鱼钩抛入水中(接下来玩手机),鱼儿咬钩之后,铃铛就会响,将鱼儿钓上来
  • 信号驱动IO的特点:1.IO准备就绪到拷贝数据之间,实时性更强 2.代码更加复杂,流程控制更加困难(引入了信号) 3.不需要重复发起IO调用,但是需要在代码当中增加自定义信号的逻辑

1.5 异步IO模型

  • 异步IO:当内核中把程序拷贝完成后,再通知程序(信号驱动就是告诉应用程序何时开始拷贝数据)

流程:
1.自定义信号(SIGIO)处理函数,用来通知数据拷贝完成
2.发起一个异步IO调用,并且异步IO调用直接返回
3.异步IO调用返回之后,执行流可以执行用户代码由操作系统内核来等待IO就绪和数据拷贝)
4.当数据拷贝完成之后,内核通过信号来告知调用者
在这里插入图片描述

  • 异步IO调用:异步IO函数接口当中一般会设置一个函数指针,来保存IO调用完成之后,通知调用者的回调函数

小结:
1.同步IO:当程序发出一个调用的时候,在没有得到结果之前,这个调用就不会返回。当调用返回的时候,就一定是得到了一个结果
2.异步IO:当程序发出一个调用,这个调用就直接返回了,所以没有返回结果。等到内核中完成了拷贝的操作,再通过一些方式来通知调用者(信号),或者通过回调函数来处理这个调用
3.异步IO最大的特点:用户不需要拷贝数据了,拷贝数据由操作系统内核来完成,完成拷贝之后,通知调用

1.6 多路转接IO模型

  • IO多路转接:可以完成大量文件描述符的监控,监控的事件:可读事件可写事件异常事件
  • 监控文件描述符:哪个文件描述符准备就绪,就处理哪一个文件描述符
  • 好处:避免了其他进程对没有就绪的文件描述符进行操作,从而陷入阻塞状态

1.6.1 select

  • 作用:用程序来对多个文件描述符的状态进行监控,如果有描述符准备就绪,就返回该描述符,让用户对这个描述符进行操作
  • 流程:.将用于关心的文件描述符拷贝到内核当中,内核来进行监控 .如何内核监控到某个文件描述符就绪,则返回该描述符 .用户对返回的描述符进行操作

  • 函数接口
    在这里插入图片描述

1.nfds:取值为监控最大的文件描述符数值+1(最大的数值为1024),轮询的范围由nfds来决定


2.fd_set:本质是一个结构体,结构体内部是一个fds_bits数组,可以理解为一个1024位的位图,对应1024个文件描述符
在这里插入图片描述

_FD_SIZE:#define _FD_SETSIZE 1024
_NFDBITS:#define _NFDBITS (8*(int)sizeof(_fd_mask))
	数组的元素个数:1024 / 8*(int)sizeof(_fd_mask)
_fd_mask:typedef long int_fd mask
	数组当中比特位的个数:
	(1024/8 * (int)sizeof(_fd_mask)) * 8 *(int)sizeof(_fd_mask)) = 1024
总结:select的事件集合当中总共有1024个比特位,取决于宏 _FD_SETSIZE 这个宏的大小

3.举个例子
在这里插入图片描述


4.fd_set集合中提供了4个函数
在这里插入图片描述

//从事件集合当中删除文件描述符fd,描述符对应的比特位置0
void FD_CLR(int fd, fd_set *set);

//判断fd描述符是否在set集合当中;
//返回值:0表示没有在集合当中,非0表示在集合当中
int  FD_ISSET(int fd, fd_set *set);

//将文件描述符fd设置到set集合当中,描述符对应的比特位置1
void FD_SET(int fd, fd_set *set);

//清空事件集合,将所有的比特位置0
void FD_ZERO(fd_set *set);

5.readfds:可读文件描述符的结合 ;writefds:可写文件描述符的集合;exceptfds:异常文件描述符的集合


6.timeout:超时时间; tv_sec:秒 tv_usec:微秒
在这里插入图片描述

  • timeout == NULL 阻塞监控
  • timeout == 0 非阻塞监控
  • timeout > 0 等待超时时间监控

7.函数返回值
在这里插入图片描述

1.6.1.1 select优缺点总结

  • 优点

1.select遵循的是POSIX标准,可以跨平台移植
2.select的超时时间可以精确到微秒

  • 缺点

1.select采用的是轮询遍历,监控的效率会随着文件描述符的增多而下降
2.select所能监控的文件描述符是有上限的(1024),取决于内核FD_SETSIZE宏的值
3.select监控文件描述符的时候,需要将集合拷贝到内核当中,select发现有事件就绪之后,同时需要将事件集合从内核拷贝到用户空间,效率也会受影响
4.select在返回的时候,会将未就绪的文件描述符从集合当中去除掉,导致下一次监控的时候,如果还需要监控去除掉的文件描述符,就得重新添加
5.select 无法直接查看那个文件描述符已经就绪,需要手动通过返回事件的集合去判断
6.select的超时机制,如果在循环判断的情况下,每次调用之前都需要更新一下时间。因为在计时的时候,这个结构体中的时间是会变的

1.6.2 poll

poll和select相比,跨平台移植性不如select,poll函数只能在Linux环境下使用,也采用轮询遍历

与select相比,改进的点:
1.不限制监控的文件描述符的个数
2.select使用的是事件集合方式,poll采用的是事件结构
(文件描述符对应一个事件结构,这个结构中有两个事件,一个是要监控的文件描述符,另一个是这个文件描述符所对应的事件

1.6.2.1 函数接口


在这里插入图片描述

fds:事件结构数组
nfds:事件结构数组中有效的元素个数
timeout:超时时间


在这里插入图片描述

fd:关心的文件描述符是什么
events:关心的文件描述符产生的事件是什么,如果关心多个事件,可以将多个事件按照按位或的方式连接起来

在这里插入图片描述

revents:当关心的文件描述符产生对应的关心的事件时,返回给调用者发生的事件(每次监控的时候,就会被初始化为空)

1.6.2.2 编程实例

#include<stdio.h>
#include<unistd.h>
#include<poll.h>
#include<iostream>

int main()
{
	//创建一个struct pollfd结构体,关心0号文件描述符,标准输入
	//监视可读事件
    struct pollfd fd_arr[10];
    fd_arr[0].fd = 0;
    fd_arr[0].events = POLLIN;

	//轮询遍历
    while(1)
    {
        int ret = poll(fd_arr, 1 , 1000);
        if(ret < 0) //poll出错
        {
            perror("poll error");
            return -1;
        }
        else if(ret == 0)//监控超时
        {
            printf("poll timeout\\n");
            continue;
        }

        for(int i = 0; i < ret; i++)
        {
            if(fd_arr[i].revents == POLLIN)//当发生的事件可读
            {
				//读取数据
                char buf[1024] = {0};
                read(fd_arr[i].fd, buf, sizeof(buf) - 1);
                printf("buf: %s\\n",buf);
            }
        }
    }
    return 0;

}

运行结果:
在这里插入图片描述

1.6.2.3 poll的优缺点

优点:

  • poll采用了事件结构的方式,简化了代码的编写
  • poll不限制文件描述符的个数
  • 不需要再二次监控的时候重新添加文件描述符

缺点:

  • poll采用轮询遍历事件结构数组的方式,随着文件描述符增多,性能下降
  • poll不支持平台
  • poll也没有告诉用户哪一个具体的文件描述符就绪了,需要自己判断
  • poll也需要将事件结构拷贝到内核,从内核再拷贝到用户空间

1.6.3 epoll(目前公认的在Linux系统下监控性能最高)

1.6.3.1 epoll(创建句柄)接口

在这里插入图片描述


  • 创建epoll的操作句柄
#include <sys/epoll.h>

int epoll_create(int size);

size:本来的含义是定义epoll最大能够监控文件描述符的个数,内核在2.6.8之后就启弃用了,现在采用动态内存开辟的方式,来进行扩容,size的值大于0,在使用完之后,必须使用close进行关闭


返回值:返回epoll的操作句柄
内核观点:在内核当中就是创建一个struct eventpoll结构体,在这个结构体当中有两个重要的变量,一个是红黑树,一个是双向链表
epoll的操作句柄其实就是用来找到struct eventpoll结构体,从而对结构体当中的变量进行操作

1.6.3.2 epoll(添加/删除/修改事件结构)接口

在这里插入图片描述
向内核维护的红黑树当中添加/删除/修改事件结构


#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, 
		struct epoll_event *event);

epfd:epoll_create的返回值,epoll的操作句柄
op:想让epoll_ctl做的事

在这里插入图片描述
fd:告诉epoll函数,我们关心的文件描述符
event:epoll的事件结构,类型是 struct epoll_event结构体

typedef union epoll_data    
{    
  *ptr和fd两者只能选一个*
  
  void *ptr;  --->如果使用ptr,需要包含fd的内容
  int fd; --->用户关心的文件描述符,可以当做文件描述符中的事件就绪之后,返回给我们时查看;其取值为文件描述符的数值

  uint32_t u32;    
  uint64_t u64;    
} epoll_data_t;    
    
struct epoll_event    
{    
  想让文件描述符关心的事件集合:EPOLLIN可读事件 EPOLLOUT可写事件
  uint32_t events;  /* Epoll events */ 
  
  epoll_data类型的联合结构体
  epoll_data_t data;    /* User data variable */    
} __EPOLL_PACKED;  

在这里插入图片描述

1.6.3.3 epoll监控接口

在这里插入图片描述

 #include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
         int maxevents, int timeout);

epfd:epoll操作句柄
events:事件结构数组,作为出参,返回就绪的事件结构,一个文件描述符对应一个事件结构
maxevents:表示最大能接受多少个事件结构
timeout:超时时间

在这里插入图片描述

1.6.3.4 epoll的工作原理

在这里插入图片描述
在这里插入图片描述

1.6.3.5 epoll模拟代码

#include<iostream>
#include<stdio.h>
#include<unistd.h>
#include<sys/epoll.h>
#include<string.h>
#include<stdlib.h>

/*
 * 1.创建epoll操作句柄
 * 2.添加事件结构
 *   2.1准备事件结构
 *   2.2关心的事件及文件描述符
 * 3.监控
 *   3.1阻塞
 *   3.2非阻塞
 *   3.3带有超时事件
 * 4.判断epoll_wait的返回值
 * 5.执行相应的操作 
 * */

int main()
{
    int epfd = epoll_create(5);
    if(epfd < 0)
    {
        perror("create error");
        return 0;
    }
    struct epoll_event ee;
    ee.events = EPOLLIN;
    ee.data.fd = 0;
    epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);


    while(1)
    {

    struct epoll_event arr[10];
    memset(arr, '\\0', sizeof(struct epoll_event) * 10);
    
    int ret = epoll_wait(epfd, arr, 10, 0);
    if(ret < 0)
    {
        perror("epoll_wait error");
        return 0;
    }
    else if(ret == 0)
    {
        sleep(1);
        printf("timeout..\\n");
        continue;
    }
    for(int i = 0; i < 10; i++)
    {
        if(arr[i].events == EPOLLIN)
        {
            char buf[1024] = {0};
            read(arr[i].data.fd, buf,sizeof(buf) - 1);
            printf("buf:%s", buf);
        }
    }
    }
    return 0;
}

在这里插入图片描述

1.6.3.6 工作方式:水平触发 LT模式(EPOLLLT)

  • epoll的默认工作方式,select和poll都是水平触发方式
  • 在LT模式当中,当epoll中检测到了等待触发事件就绪后,可以不立即进行处理,而是只处理一部分。等到第二次调用epoll_wait函数的时候,可以接着刚才没有处理完的数据进行操作(支持阻塞读写和非阻塞读写)
  • 可读事件:只要发送缓冲区当中数据大于低水位标记(1字节),就会一直触发可读事件就绪,直到接收缓冲区当中没有数据可读(接收缓冲区当中的数据低于低水位标记)
  • 可写事件:只要发送缓冲区当中的空间大小大于低水位标记(1字节),就会一直触发可写事件就绪,直到缓冲区当中没有空间可写

1.6.3.7 工作方式:边缘触发 ET模式(EPOLLET)

  • EPOLLET只有epoll才有
  • 可读事件:只有新的数据到来的时候,才会触发可读,否则通知一次就不通知了(每次到来一个新的数据,只会通知一次,如果应用程序没有将接收缓冲区当中的数据读走或者读完,也不会通知,直到新的数据到来,才会触发可读事件。如果出发可读事件,尽量将数据读完
  • 对于ET模式而言,如果就绪事件产生,一定要把握好机会,对于可读事件,将数据读完,对于可写事件,将数据写完
如何在代码中体现ET
设置文件描述符对应的事件结构的时候,只需要在事件结构当中关心的事件变量按位或上 EPOLLET 就可以了
struct epoll_enent ev;
ev.events = EPOLLIN | EPOLLET;

以上是关于典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)的主要内容,如果未能解决你的问题,请参考以下文章

Linux设备驱动中的IO模型---阻塞和非阻塞IO

五种IO模型

Linux下5种IO模型以及阻塞/非阻塞/同步/异步区别

带你整理面试过程中关于IO 模型的相关知识

大白话五种IO模型

WEB服务-Nginx之1-网络IO模型和常用事件驱动模型