当前位置: 首页 > article >正文

【网络】高级IO——五种IO模式

目录

前言 

一,IO

1.1.什么是IO?

1.2.IO操作

1.3.阻塞和非阻塞IO

1.4.异步和同步IO

二,五种IO模型 

2.1.简要讲讲五种IO模型

2.1.1.复习

2.1.2.阻塞IO模型

2.1.3.非阻塞式IO模式

2.1.4.多路复用IO模式

2.1.5、信号驱动IO(signal driven IO)

2.1.6、异步IO( asynchronous IO)

2.2.总结


前言 

我们在本专栏前面的文章中大概学习了TCP/IP四层模型,因为我们不学网络,所以那些知识也是够用的。我们接下来要回归到我们的应用层的学习——也就是写代码。

接下来,我们在接下来的学习中将学习以下内容

  1. 理解五种IO模型的基本概念,重点是IO多路转接
  2. 掌握select编程模型,能够实现select版本的TCP服务器
  3. 掌握poll编程模型,能够实现poll版本的TCP服务器
  4. 掌握epoll编程模型,能够实现epoll版本的TCP服务器
  5. 理解epoll的LT模式和ET模式,
  6. 理解select和epoll的优缺点对比

一,IO

1.1.什么是IO?

        在计算机中,输入/输出(即IO)是指信息处理系统(比如计算机)和外部世界(可以是人或其他信息处理系统)的通信。输入是指系统接收的信号或数据,输出是指从系统发出的数据或信号。这个术语可以用作某个动作的一部分:“执行I/O”就是指执行输入输出动作。

        I/O设备是指用户和计算机之间沟通的硬件部分。例如,键盘或鼠标就是计算机的输入设备,显示器和打印机是输出设备。用来在计算机之间通信的设备,例如调制解调器和网卡,通常同时执行输入输出动作。

还记得冯诺依曼体系不?

在我们自己的电脑内部其实就无时不刻的在进行着IO,因为冯诺依曼体系已经决定了计算机是要无时不刻进行IO的,从存储设备中拿取数据到内存,将处理结果再返回至内存 

        在计算机的体系结构中,CPU和主存的组合被认为是计算机的大脑,因为CPU可以直接执行单个指令读或写。任何与CPU和内存的这个组合(大脑)的信息传输,例如从硬盘读数据,都可以被称为I/O。

        也就是说从计算机架构上来讲,任何涉及到计算机核心(CPU和内存)与其他设备间的数据转移的过程就是IO。本体就是计算机核心(CPU和内存)。

        例如从硬盘上读取数据到内存,是一次输入,将内存中的数据写入到硬盘就产生了输出。在计算机的世界里,这就是IO的本质。

我们可以从编程的角度去理解IO。

        此时,IO的主体是其应用程序的运行态,即进程,特别强调的是我们的应用程序其实并不存在实质的IO过程,真正的IO过程是操作系统的事情,这里把应用程序的IO操作分为两种动作:IO调用和IO执行。IO调用是由进程发起,IO执行是操作系统的工作。因此,更准确些来说,此时所说的IO是应用程序对操作系统IO功能的一次触发,即IO调用。

        IO调用的目的是将进程的内部数据迁移到外部即输出,或将外部数据迁移到进程内部即输入。这里,外部数据指非进程空间数据,在编程时,通常讨论的场景是来自外部存储设备的数据,如硬盘、CD-ROM、以及需要socket通信传输的网络数据。

以一个进程的输入类型的IO调用为例,它将完成或引起如下工作内容:

  1. 进程向操作系统请求外部数据
  2. 操作系统将外部数据加载到内核缓冲区
  3. 操作系统将数据从内核缓冲区拷贝到进程缓冲区
  4. 进程读取数据继续后面的工作

从上面的描述来看,我们更容易理解一个IO操作,应用程序和操作系统都干了些什么,也帮助我们更容器理解阻塞和非阻塞,异步和同步的相关IO编程概念。

1.2.IO操作

IO操作会涉及到用户空间和内核空间的转换,先来理解以下规则:

  • 内存空间分为用户空间和内核空间,也称为用户缓冲区和内核缓冲区;
  • 用户的应用程序不能直接操作内核空间,需要将数据从内核空间拷贝到用户空间才能使用;
  • 无论是read操作,还是write操作,都只能在内核空间里执行;
  • 磁盘IO和网络IO请求加载到内存的数据都是先放在内核空间的;

再来看看所谓的读(Read)和写(Write)操作:

  1. 读操作:操作系统检查内核缓冲区有没有需要的数据,如果内核缓冲区已经有需要的数据了,那么就直接把内核空间的数据copy到用户空间,供用户的应用程序使用。如果内核缓冲区没有需要的数据,对于磁盘IO,直接从磁盘中读取到内核缓冲区(这个过程可以不需要cpu参与)。而对于网络IO,应用程序需要等待客户端发送数据,如果客户端还没有发送数据,对应的应用程序将会被阻塞,直到客户端发送了数据,该应用程序才会被唤醒,从Socket协议找中读取客户端发送的数据到内核空间,然后把内核空间的数据copy到用户空间,供应用程序使用。
  2. 写操作:用户的应用程序将数据从用户空间copy到内核空间的缓冲区中(如果用户空间没有相应的数据,则需要从磁盘—>内核缓冲区—>用户缓冲区依次读取),这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘或通过网络发送出去,由操作系统决定。除非应用程序显示地调用了sync 命令,立即把数据写入磁盘,或执行flush()方法,通过网络把数据发送出去。
  3. 绝大多数磁盘IO和网络IO的读写操作都是上述过程,除了零拷贝IO。

1.3.阻塞和非阻塞IO

阻塞和非阻塞强调的是进程对于操作系统IO是否处于就绪状态的处理方式。

        上面已经说过,应用程序的IO实际是分为两个步骤,IO调用和IO执行IO调用是由进程发起,IO执行是操作系统的工作。操作系统的IO情况决定了进程IO调用是否能够得到立即响应。如进程发起了读取数据的IO调用,操作系统需要将外部数据拷贝到进程缓冲区,在有数据拷贝到进程缓冲区前,进程缓冲区处于不可读状态,我们称之为操作系统IO未就绪。

        进程的IO调用是否能得到立即执行是需要操作系统IO处于就绪状态的,对于读取数据的操作,如果操作系统IO处于未就绪状态,当前进程或线程如果一直等待直到其就绪,该种IO方式为阻塞IO。如果进程或线程并不一直等待其就绪,而是可以做其他事情,这种方式为非阻塞IO。所以对于非阻塞IO,我们编程时需要经常去轮询就绪状态。

1.4.异步和同步IO

        我们经常会谈及同步IO和异步IO。同步和异步描述的是针对当前执行线程、或进程而言,发起IO调用后,当前线程或进程是否挂起等待操作系统的IO执行完成。

        我们说一个IO执行是同步执行的,意思是程序发起IO调用,当前线程或进程需要等待操作系统完成IO工作并告知进程已经完成,线程或进程才能继续往下执行其他既定指令。

        如果说一个IO执行是异步的,意思是该动作是由当前线程或进程请求发起,且当前线程或进程不必等待操作系统IO的执行完毕,可直接继续往下执行其他既定指令。操作系统完成IO后,当前线程或进程会得到操作系统的通知。

        以一个读取数据的IO操作而言,在操作系统将外部数据写入进程缓冲区这个期间,进程或线程挂起等待操作系统IO执行完成的话,这种IO执行策略就为同步,如果进程或线程并不挂起而是继续工作,这种IO执行策略便为异步。

二,五种IO模型 

IO模型分为五种,分别是阻塞式IO,非阻塞IO,信号驱动IO,多路转接IO,异步IO。


 

2.1.简要讲讲五种IO模型

2.1.1.复习

要深入的理解各种IO模型,那么必须先了解下产生各种IO的原因是什么,要知道这其中的本质问题那么我们就必须要知道一条消息是如何从一个人发送到另外一个人的;

以两个应用程序通讯为例,我们来了解一下当“A”向"B" 发送一条消息,简单来说会经过如下流程:

  • 第一步:应用A把消息发送到 TCP发送缓冲区。
  • 第二步: TCP发送缓冲区再把消息发送出去,经过网络传递后,消息会发送到B服务器的TCP接收缓冲区。
  • 第三步:B再从TCP接收缓冲区去读取属于自己的数据。

        根据上图我们基本上了解消息发送要经过 应用A、应用A对应服务器的TCP发送缓冲区、经过网络传输后消息发送到了应用B对应服务器TCP接收缓冲区、然后最终B应用读取到消息。

如果理解了上面的消息发送流程,那么我们下面开始进入主题;

2.1.2.阻塞IO模型

我们把视角切换到上面图中的第三步, 也就是应用B从TCP缓冲区中读取数据。

思考一个问题:

        因为应用之间发送消息是间断性的,也就是说在上图中TCP缓冲区还没有接收到属于应用B该读取的消息时,那么此时应用B向TCP缓冲区发起读取申请,TCP接收缓冲区是应该马上告诉应用B 现在没有你的数据,还是说让应用B在这里等着,直到有数据再把数据交给应用B

        把这个问题应用到第一个步骤也是一样,应用A在向TCP发送缓冲区发送数据时,如果TCP发送缓冲区已经满了,那么是告诉应用A现在没空间了,还是让应用A等待着,等TCP发送缓冲区有空间了再把应用A的数据访拷贝到发送缓冲区。

        如果上面的问题你已经思考过了,那么其实你已经明白了什么是阻塞IO了,所谓阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。

阻塞式IO流程:

  • 1、应用进程向内核发起recfrom读取数据。
  • 2、准备数据报(应用进程阻塞)。
  • 3、将数据从内核负责到应用空间。
  • 4、复制完成后,返回成功提示。

2.1.3.非阻塞式IO模式

按照上面的思路,所谓非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待。

定义

  • 非阻塞IO是在应用调用recvfrom读取数据时,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。

  • 在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断的调用recvfrom请求,直到读取到它数据要的数据为止。

非阻塞式IO流程:

  • 1、应用进程向内核发起recvfrom读取数据。
  • 2、没有数据报准备好,即刻返回EWOULDBLOCK错误码。
  • 3、应用进程向内核发起recvfrom读取数据。
  • 4、已有数据包准备好就进行一下 步骤,否则还是返回错误码。
  • 5、将数据从内核拷贝到用户空间。
  • 6、完成后,返回成功提示。

2.1.4.多路复用IO模式

思考一个问题:

        我们还是把视角放到应用B从TCP缓冲区中读取数据这个环节来。如果在并发的环境下,可能会N个人向应用B发送消息,这种情况下我们的应用就必须创建多个线程去读取数据,每个线程都会自己调用recvfrom 去读取数据。那么此时情况可能如下图:

 

        如上图一样,并发情况下服务器很可能一瞬间会收到几十上百万的请求,这种情况下应用B就需要创建几十上百万的线程去读取数据,同时又因为应用线程是不知道什么时候会有数据读取,为了保证消息能及时读取到,那么这些线程自己必须不断的向内核发送recvfrom 请求来读取数据;

        那么问题来了,这么多的线程不断调用recvfrom 请求数据,先不说服务器能不能扛得住这么多线程,就算扛得住那么很明显这种方式是不是太浪费资源了,线程是我们操作系统的宝贵资源,大量的线程用来去读取数据了,那么就意味着能做其它事情的线程就会少。

        所以,有人就提出了一个思路,能不能提供一种方式,可以由一个线程监控多个网络请求(我们后面将称为fd文件描述符,linux系统把所有网络请求以一个fd来标识),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。 

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

定义

        进程通过将一个或多个fd传递给select(或者其他IO复用API),阻塞在select操作上,select帮我们侦测多个fd是否准备就绪,当有fd准备就绪时,select返回数据可读状态,应用程序再调用recvfrom读取数据。

总结:

        复用IO的基本思路就是通过slect或poll、epoll 来监控多fd ,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。 

2.1.5、信号驱动IO(signal driven IO)

        复用IO模型解决了一个线程可以监控多个fd的问题,但是select是采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否就可读的数据,而无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。

于是信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,具体如下:

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

定义

        首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知映应用线程调用recvfrom来读取数据。 

总结:

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

2.1.6、异步IO( asynchronous IO)

        通过观察我们发现,不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。(这也就是为什么上面四种都是同步IO)

        在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步;

        如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。

思考一个问题:

        也许你一开始就有一个疑问,为什么我们明明是想读取数据,什么非得要先发起一个select询问数据状态的请求,然后再发起真正的读取数据请求,能不能有一种一劳永逸的方式,我只要发送一个请求我告诉内核我要读取数据,然后我就什么都不管了,然后内核去帮我去完成剩下的所有事情?

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

定义

        应用告知内核启动某个操作,并让内核在整个操作完成之后,通知应用,这种模型与信号驱动模型的主要区别在于,信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核自动完成IO操作,并通知我们操作什么时候完成。 

总结:

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

2.2.总结

下面我们讲一个例子先来浅浅谈一下这5个模型IO的做法。

  1. 从前有一条小河,河里有许多条鱼,一个叫张三的少年就很喜欢钓鱼,他带着自己的鱼竿就去钓鱼了,但张三这个人很固执,只要鱼没上钩,张三就一直等着,什么都不干,死死的盯着鱼漂,只有鱼漂动了,张三才会动,然后把鱼钓上来,钓上来之后,张三就又会重复之前的动作,一动不动的等待鱼儿上钩。
  2. 而此时走过来一个李四,李四这名少年也很喜欢钓鱼,但李四和张三不一样,李四左口袋装着《Linux高性能服务器编程》,右口袋装着一本《算法导论》,左手拿手机,右手拿了一根鱼竿,李四拿了钓鱼凳坐下之后,李四就开始钓鱼了,但李四不像张三一样,固执的死盯着鱼漂看,李四一会看会儿左口袋的书,一会玩会手机,一会儿又看算法导论,一会又看鱼漂,所以李四一直循环着前面的动作,直到循环到看鱼漂时,发现鱼漂已经动了好长时间了,此时李四就会把鱼儿钓上来,之后继续重复循环前面的动作。
  3. 此时又来了一个王五少年,王五就拿着他自己的iphone14pro max和一根鱼竿外加一个铃铛,然后就来钓鱼了,王五把铃铛挂到鱼竿上,等鱼上钩的时候,铃铛就会响,王五根本不看鱼竿,就一直玩自己的iphone,等鱼上钩的时候,铃铛会自动响,王五此时再把鱼儿钓上来就好了,之后王五又继续重复前面的动作,只要铃铛不响,王五就一直玩手机,只有铃铛响了,王五才会把鱼钓上来。
  4. 此时又来了一个赵六的人,赵六和前面的三个人都不一样,赵六是个小土豪,赵六手里拿了一堆鱼竿,目测有几百根鱼竿,赵六到达河边,首先就把几百根鱼竿每隔几米插上去,总共插了好几百米的鱼竿,然后赵六就依次遍历这些鱼竿,哪个鱼竿上的鱼漂动了,赵六就把这根鱼竿上的鱼钓上来,然后接下来赵六就又继续重复之前遍历鱼竿的动作进行钓鱼了。
  5. 然后又来了一个钱七,钱七比赵六还有钱,钱七是上市公司的CEO,钱七有自己的司机,钱七不喜欢钓鱼,但钱七喜欢吃鱼,所以钱七就把自己的司机留在了岸边,并且给了司机一个电话和一个桶,告诉司机,等你把鱼钓满一桶的时候,就给我打电话,然后我就从公司开车过来接你,所以钱七就直接开车回公司开什么股东大会去了,而他的司机就被留在这里继续钓鱼了。

 在上面的例子中,你认为谁的钓鱼方式更加高效呢?

        首先我们认为,如果一个人在不停的钓鱼,时不时的就收鱼竿,把鱼钓上来,等待鱼儿上钩的时间比重却很低,那么这个人在我看来他的钓鱼方式就是高效的。而如果一个人大部分的时间都是在等待,只有那么极少数次在收杆把鱼钓上来,那么这个人的钓鱼方式就是低效的。

        而上面的例子中,鱼其实就是数据,鱼竿其实就是文件描述符fd,每个人都算是进程,但除了钱七的司机,这个司机算是操作系统,河流就是内核缓冲区,鱼漂就是就绪的事件,代表钓鱼这件事情已经就绪了,进程可以对数据做拷贝了。

        其实赵六的方式是最高效的,也就是多路转接这种IO模型是最高效的,因为赵六的鱼竿多啊,钓上鱼的几率就大啊,其他人只有一根鱼竿,只能关心这一根鱼竿上的数据,自然就没有赵六的效率高,同理为什么渣男的女朋友多啊,因为广撒网嘛,找到女朋友的概率要比普通的老实人高啊,因为人家一次可以关心那么多的微信账号,哪个女孩发消息了人家就和谁聊天,肯定比你只有一个女孩的微信效率要高。

        所以本文章主要来介绍多路转接这种IO模型,同时也会讲解阻塞和非阻塞IO,需要注意的是,实际项目中,最常用的就是阻塞IO,同时大部分的fd默认就是阻塞的,因为这种IO太简单了,越简单的东西往往就越可靠,代码编写也越简单,调试和找bug的难度也就越低,这样的代码可维护性很高,所以他就越常用。

        阻塞,非阻塞,信号驱动在IO效率上是没有差别的,因为他们三个人都只有一根鱼竿,等待鱼上钩的概率都是一样的,相当于他们等待事件就绪的概率是相同的,所以从IO效率上来看,这三个模型之间是没有差别的。只不过三者等待的方式是不同的,阻塞是一直在进行等待,而非阻塞可能会使用轮询的方式来进行等待,在等待的时间段内,非阻塞可能还会做一些其他的事情,信号驱动和非阻塞一样,在等待的时间段内,信号驱动会做一些其他的事情,比如监管一下其他的连接是否就绪等等事情。所以从IO的效率角度来讲,这三种IO并无差别,因为IO的过程分为等待和数据拷贝,三者在这个工作上的效率都是一样的,只不过非阻塞和信号驱动的等待方式与阻塞IO不同。信号驱动只不过是被动的等待,阻塞和非阻塞都是主动的等待,当信号到来时,信号驱动IO会通过回调的方式来处理就绪的事件。

        而多路转接相比前三种IO模型更为高效一些,因为他能够一次等待多个文件描述符,但这四种IO都有一个共同的特征,就是直接参与了IO的过程,这样的通信我们称之为同步通信,而异步IO是典型的异步通信,他将等待数据就绪的事情交给了内核来处理,当数据准备好后,操作系统会以信号或回调函数的方式来通知进程可以处理数据了,因为数据已经准备好了,这就是典型的异步通信。


http://www.kler.cn/a/301703.html

相关文章:

  • 性能高于Transformer模型1.7-2倍,彩云科技发布基于DCFormer架构通用大模型云锦天章
  • 023、ELK 从入门到实践
  • leetcode 扫描线专题 06-leetcode.253 meeting room ii 力扣.253 会议室 II
  • 第二十一周机器学习笔记:动手深度学习之——数据操作、数据预处理
  • 逆向攻防世界CTF系列37-crackme
  • 加速 AI 创新:引入 Elastic AI 生态系统
  • 概念——二连杆与三连杆解算
  • VS2019界面介绍
  • vue3+ant design vue动态实现级联菜单~
  • Gradle和Maven
  • 第四部分:1---文件内核对象,文件描述符,输出重定向
  • Unity基本操作
  • 前端封装组件可视化库
  • 第15-05章:获取运行时类的完整结构
  • 【AcWing】871. 约数之和
  • Spring Security 快速开始
  • centos7安装MySQL5.7.44
  • Docker Swarm管理(Docker技术集群与应用)
  • k8s的配置
  • 【网络安全】漏洞挖掘之CVE-2019-9670+检测工具
  • 如何使用Flask渲染模板
  • 比 GPT-4 便宜 187 倍的Mistral 7B (非广告)
  • 明基相机sd卡格式化还能恢复数据吗?可以这样操作
  • 漫谈设计模式 [16]:中介者模式
  • L3学习打卡笔记
  • QT进行音频录制