五种IO模型

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了五种IO模型相关的知识,希望对你有一定的参考价值。

文章目录

什么是IO

IO,即input/output,IO模型即输入输出模型,而比较常见且听说的便是磁盘IO,网络IO.

按照冯诺依曼结构的来看,假设我们把运算器、控制器、存储器三个设备看做一个整体(假设称为中转),那么输入设备、输出设备,和中转就构成一个中转IO,也就是说IO是以某一个核心为主体的,而涉及计算机核心与其他核心之间数据迁移的过程我们就成为一个IO.


操作系统的IO

如果要将内存中的数据写入到磁盘,那么其IO主体是什么呢?主体很可能是一个应用程序.

但我们电脑上所跑起来的应用程序是无法直接进行一些特殊操作(IO)的,比如内存读写,磁盘读写,因为用户可能会利用此程序直接或者间接的对计算机造成破坏,应用程序想要进行这些特殊操作,只能交给底层软件—操作系统.也就是说应用程序想要将数据从内存写入磁盘或者反过来说从磁盘读取内存,只能通过操作系统对上层开放的API来进行.

而在任何一个应用程序里面,都会有进程地址空间,该空间分为两部分,一部分称为用户空间(允许应用程序进行访问的空间),另一部分称为内核空间,是只能留给操作系统进行访问的空间,也就是说它受到保护.

因此,一个应用程序想要进行一次IO操作将会分为两个阶段:

  • IO调用:应用程序进程向操作系统内核发起调用。
  • IO执行:操作系统内核完成IO操作

而操作系统完成一次IO操作也包括两个过程:

  • 准备数据阶段:内核等待I/O设备准备好数据
  • 拷贝数据阶段:将数据从内核缓冲区拷贝到用户进程缓冲区

因此一个完整的IO过程包括以下几个步骤:

  1. 应用程序进程向操作系统发起IO调用请求
  2. 操作系统准备数据,把IO外部设备的数据,加载到内核缓冲区
  3. 操作系统拷贝数据,即将内核缓冲区的数据,拷贝到用户进程缓冲区

而一次IO的本质其实就是: 等待 + 拷贝

五种IO模型

在了解IO模型之前,我们先回顾一下网络应用程序之间,是怎么进行数据发送和接收的.按照一下两个应用程序为例:

应用A把消息发送到 TCP发送缓冲区,TCP发送缓冲区再把消息发送出去,经过网络传递后,消息会发送到B服务器的TCP接收缓冲区,B再从TCP接收缓冲区去读取属于自己的数据,同理,当B对A发送数据时,路径类似.

现在我们看下什么是IO模型

阻塞IO

参考上图流程,我们思考一个问题,TCP缓冲区还没有完全接收(假设数据量为1,目前接收数据量为0或小于1)到属于应用B该读取的消息时,应用B向TCP缓冲区发起读取申请,TCP接收缓冲区是应该马上告诉应用B 现在没有你的数据,你去做别的事吧,还是说让应用B在这里等着,直到有数据再把数据交给应用B。

同理,应用A在向TCP发送缓冲区发送数据时,如果TCP发送缓冲区已经满了,那么是告诉应用A现在没空间了,还是让应用A等待着,等TCP发送缓冲区有空间了再把应用A的数据访拷贝到发送缓冲区。

而阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。

学术语言就是:在应用调用recvfrom读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的称为阻塞IO;在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式

流程和流程图:

流程描述:

1、应用进程向内核发起recfrom读取数据

2、内核进行准备数据报(此时应用进程阻塞)

3、内核将数据从内核负复制到应用空间。

4、复制完成后,返回成功提示

代码模拟阻塞IO过程

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main()
    char buf[1024];
    printf("等待您的数据准备:");fflush(stdout);
    ssize_t Rea = read(0,buf,sizeof(buf)-1);  //等待键盘键入数据
    if(Rea>0)
        buf[Rea]=0;
        printf("数据准备完毕,开始写进屏幕\\n");
        sleep(1);
        write(1,buf,strlen(buf));
    
    return 0;

非阻塞IO

非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待,如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码;

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用

流程和流程图:

1、应用进程向内核发起recvfrom读取数据。

2、内核数据报没有准备好,即刻返回EWOULDBLOCK错误码。

3、应用进程再次向内核发起recvfrom读取数据。

4、内核倘若已有数据包准备好就进行下一步骤,否则还是返回错误码,执行第三步骤

5、内核将数据拷贝到用户空间。

6、完成后,返回成功提示。


fctnl函数用来对文件描述符进行处理,他有五种功能:

//函数声明
int fcntl(int fd, int cmd, ... /* arg */ );
  • 复制一个现有的描述符(cmd=F_DUPFD).

  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).

  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).

  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).

  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞

void SetNonBlock(int fd) 
    int fl = fcntl(fd, F_GETFL);  
    if (fl < 0) 
    	perror("fcntl");  return;
    
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);  //设置为非阻塞

代码模拟非阻塞:

int main()

    char buf[1024];
    SetNonBlock(0);
    while (true)
    
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        
            buffer[s] = 0;
            write(1,buffer,strlen(buffer));
            printf("读取成功!");
        
        else
        
            if (errno == EAGAIN || errno == EWOULDBLOCK)
            
                sleep(1);
                std::cout << "当前没有出错,仅仅底层数据没有就绪罢了..." << std::endl;
                continue;
            
            if(errno == EINTR)
                std::cout << "读取被信号中断" << std::endl;
                continue;
            
            std::cout << "read error: " << s << std::endl;
            break;
        
    
    return 0;

多路转接IO(复用IO)

假设在并发的环境下,可能会N个人向应用B发送消息,这种情况下我们的应用就必须创建多个线程去读取数据,每个线程都会自己调用recvfrom 去读取数据。那么此时情况可能如下图:

倘若服务器在上百万请求下,应用B就需要创建上百万的线程去读取数据,同时又因为应用线程是不知道数据是否准备好,为了保证消息能及时读取到,那么这些线程自己必须不断的向内核发送recvfrom 请求来读取数据,而如此庞大的线程资源仅仅只是用来等待读取数据,那么就意味着能做其它事情的线程就会少,这将造成极其庞大的浪费.

多路转接模型: 所以有人就提出了一个思路,由一个线程监控多个网络请求(我们后面将称为fd文件描述符,linux系统把所有网络请求以一个fd来标识),来完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这样就可以节省出大量的线程资源出来

上图可以看出多路复用就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,可以通过它们同时监控多个fd,只要有任何一个数据状态准备就绪了,就返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据.

虽然从流程图上看起来和阻塞IO类似.,实际上最核心之处在于IO多路转接能够同时等待多个文件 描述符的就绪状态,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。

流程图:

信号驱动IO

多路转接解决了一个线程可以监控多个fd的问题,但是select采用无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,别让我总去问数据是否准备就绪,而是等你准备就绪后主动通知我,这边是信号驱动IO.

信号驱动IO是在调用sigaction时候建立一个SIGIO的信号联系,当内核准备好数据之后再通过SIGIO信号通知线程,此fd准备就绪,当线程收到可读信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下,应用线程在发出信号监控后即可返回,不会阻塞,所以一个应用线程也可以同时监控多个fd。

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

多路转接IO里面的select虽然可以监控多个fd了,但select其实现的本质上还是通过不断的轮询fd来监控数据状态, 因为大部分轮询请求其实都是无效的,所以信号驱动IO意在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。

异步IO

多路转接IO和信号驱动IO虽然相比于单纯的阻塞IO和非阻塞IO来说,提升了一些效率,但它们都有着两次操作,即先发送select让其等待,然后再recv进行读取,但我们网络需求是直接读取,所以有人在前面基础上提出了一种方法:

应用只需要向内核发送一个读取请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种模式为异步IO模型

异步IO的优化思路是解决应用程序需要先后发送询问请求、接收数据请求两个阶段的模式,在异步IO的模式下,只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作

同步异步

同步和异步关注的是消息通信机制.

所谓同步,就是在发出一个调用时,自己需要参与等待结果的过程,则为同步,前面四个IO都自己参与了,所以也称为同步IO.

异步IO,则指出发出调用以后,到数据准备完成,自己都未参与,则为异步.

另外, 在讲多进程多线程的时候, 也有提到同步和互斥. 这里的同步和今天所讲的同步异步是完全不相干的概 念.

进程/线程同步:是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系.,尤其是在访问临界资源的时候.

以上是关于五种IO模型的主要内容,如果未能解决你的问题,请参考以下文章

linux五种IO模型与事件驱动模型

五种网络IO模型详解

五种IO模型详解

五种IO模型与多路转接

五种IO模型与多路转接

五种IO模型与多路转接