目录
前言
我们在本专栏前面的文章中大概学习了TCP/IP四层模型,因为我们不学网络,所以那些知识也是够用的。我们接下来要回归到我们的应用层的学习——也就是写代码。
接下来我们要学习一下IO
一,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调用为例,它将完成或引起如下工作内容:
- 进程向操作系统请求外部数据
- 操作系统将外部数据加载到内核缓冲区
- 操作系统将数据从内核缓冲区拷贝到进程缓冲区
- 进程读取数据继续后面的工作
从上面的描述来看,我们更容易理解一个IO操作,应用程序和操作系统都干了些什么,也帮助我们更容器理解阻塞和非阻塞,异步和同步的相关IO编程概念。
1.2.IO操作
IO操作会涉及到用户空间和内核空间的转换,先来理解以下规则:
- 内存空间分为用户空间和内核空间,也称为用户缓冲区和内核缓冲区;
- 用户的应用程序不能直接操作内核空间,需要将数据从内核空间拷贝到用户空间才能使用;
- 无论是read操作,还是write操作,都只能在内核空间里执行;
- 磁盘IO和网络IO请求加载到内存的数据都是先放在内核空间的;
再来看看所谓的读(Read)和写(Write)操作:
- 读操作:操作系统检查内核缓冲区有没有需要的数据,如果内核缓冲区已经有需要的数据了,那么就直接把内核空间的数据copy到用户空间,供用户的应用程序使用。如果内核缓冲区没有需要的数据,对于磁盘IO,直接从磁盘中读取到内核缓冲区(这个过程可以不需要cpu参与)。而对于网络IO,应用程序需要等待客户端发送数据,如果客户端还没有发送数据,对应的应用程序将会被阻塞,直到客户端发送了数据,该应用程序才会被唤醒,从Socket协议找中读取客户端发送的数据到内核空间,然后把内核空间的数据copy到用户空间,供应用程序使用。
- 写操作:用户的应用程序将数据从用户空间copy到内核空间的缓冲区中(如果用户空间没有相应的数据,则需要从磁盘—>内核缓冲区—>用户缓冲区依次读取),这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘或通过网络发送出去,由操作系统决定。除非应用程序显示地调用了sync 命令,立即把数据写入磁盘,或执行flush()方法,通过网络把数据发送出去。
- 绝大多数磁盘IO和网络IO的读写操作都是上述过程,除了零拷贝IO。
本质来说,IO的本质就是等待+数据拷贝。
我们以网络IO为例子,看看一个单纯的IO是什么样子的
客户端发起系统调用之后,内核的操作可以被分成两步:
-
等待数据
此阶段网络数据进入网卡,然后网卡将数据放到指定的内存位置,此过程CPU无感知。然后经过网卡发起硬中断,再经过软中断,内核线程将数据发送到socket的内核缓冲区中。
-
数据拷贝
数据从socket的内核缓冲区拷贝到用户空间。
那什么是高效的IO呢?
- 单位时间内,IO过程中,等待的比重越小,IO效率越高。
二,阻塞IO,非阻塞IO,异步IO,同步IO
2.1.阻塞IO和非阻塞IO
阻塞和非阻塞强调的是进程对于操作系统IO是否处于就绪状态的处理方式。
上面已经说过,应用程序的IO实际是分为两个步骤,IO调用和IO执行。IO调用是由进程发起,IO执行是操作系统的工作。操作系统的IO情况决定了进程IO调用是否能够得到立即响应。如进程发起了读取数据的IO调用,操作系统需要将外部数据拷贝到进程缓冲区,在有数据拷贝到进程缓冲区前,进程缓冲区处于不可读状态,我们称之为操作系统IO未就绪。
进程的IO调用是否能得到立即执行是需要操作系统IO处于就绪状态的,对于读取数据的操作,如果操作系统IO处于未就绪状态,当前进程或线程如果一直等待直到其就绪,该种IO方式为阻塞IO。如果进程或线程并不一直等待其就绪,而是可以做其他事情,这种方式为非阻塞IO。所以对于非阻塞IO,我们编程时需要经常去轮询就绪状态。
我们得搞清楚,这个阻塞描述的对象是谁?
- 是一个函数调用!!!函数调用过程会阻塞,一直不返回,从而导致进程/线程阻塞在这里。
- 阻塞
阻塞调用是指函数调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回,它还会抢占cpu去执行其他逻辑,也会主动检测io是否准备好。
- 非阻塞:
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
事实上,在socket编程中,阻塞与非阻塞在API上区别在于socket是否设置了SOCK_NONBLOCK
这个参数,默认情况下是阻塞的,设置了该参数则为非阻塞。
2.2.同步IO和异步IO
我们经常会谈及同步IO和异步IO。同步和异步描述的是针对当前执行线程、或进程而言,发起IO调用后,当前线程或进程是否挂起等待操作系统的IO执行完成。
- 我们说一个IO执行是同步执行的,意思是程序发起IO调用,当前线程或进程需要等待操作系统完成IO工作并告知进程已经完成,线程或进程才能继续往下执行其他既定指令。
- 如果说一个IO执行是异步的,意思是该动作是由当前线程或进程请求发起,且当前线程或进程不必等待操作系统IO的执行完毕,可直接继续往下执行其他既定指令。操作系统完成IO后,当前线程或进程会得到操作系统的通知。
以一个读取数据的IO操作而言,在操作系统将外部数据写入进程缓冲区这个期间,进程或线程挂起等待操作系统IO执行完成的话,这种IO执行策略就为同步,如果进程或线程并不挂起而是继续工作,这种IO执行策略便为异步。
同步IO会阻塞当前的调用线程,而异步IO则允许发起IO请求的调用线程继续执行,等到IO请求被处理后,会通知调用线程。
对于异步的IO请求,其最大的好处是:慢速的IO请求相对于应用程序而言是异步执行,这样可以极大提高应用程序的处理吞吐量。
发起IO请求的应用程序需要关心的是IO执行完成的结果,而不必忙等IO请求执行的过程。它只需要提交一个IO操作,当内核执行这个IO操作时,线程可以去运行其他逻辑,也不需要定期去查看IO是否完成,当内核完成这个IO操作后会以某种方式通知应用。
此时应用的运行和IO执行变成了并行的关系,可以批量的进行IO操作,让设备的能力得到最大发挥
在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。
- 同步的执行效率会比较低,耗费时间,但有利于我们对流程进行控制,避免很多不可掌控的意外情况;
- 异步的执行效率高,节省时间,但是会占用更多的资源,也不利于我们对进程进行控制
同步IO的优点
- 1、同步流程对结果处理通常更为简单,可以就近处理。
- 2、同步流程对结果的处理始终和前文保持在一个上下文内。
- 3、同步流程可以很容易捕获、处理异常。
- 4、同步流程是最天然的控制过程顺序执行的方式。
异步IO的优点
- 1、异步流程可以立即给调用方返回初步的结果。
- 2、异步流程可以延迟给调用方最终的结果数据,在此期间可以做更多额外的工作,例如结果记录等等。
- 3、异步流程在执行的过程中,可以释放占用的线程等资源,避免阻塞,等到结果产生再重新获取线程处理。
- 4、异步流程可以等多次调用的结果出来后,再统一返回一次结果集合,提高响应效率。
异步IO使用场景
- 1、不涉及共享资源,或对共享资源只读,即非互斥操作
- 2、没有时序上的严格关系
- 3、不需要原子操作,或可以通过其他方式控制原子性
- 4、常用于IO操作等耗时操作,因为比较影响客户体验和使用性能
- 5、不影响主线程逻辑
2.3.总结
再简单点理解就是:
- 1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
- 2. 异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)。
- 3. 阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
- 4. 非阻塞,就是调用我(函数),我(函数)立即返回,通过select通知调用者
同步IO和异步IO的区别就在于: 数据拷贝的时候进程是否阻塞
阻塞IO和非阻塞IO的区别就在于: 应用程序的调用是否立即返回
综上可知,同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。
说完了同步异步、阻塞非阻塞,一个很自然的操作就是对他们进行排列组合。
- 同步阻塞
- 同步非阻塞
- 异步非阻塞
- 异步阻塞
我们可以举个例子来理解这四种情况
这个故事以老张煮开水为引子,生动地解释了同步与异步、阻塞与非阻塞的概念,这些概念在编程和网络通信中尤为重要。下面是对这四个场景的详细解释:
同步阻塞
- 老张操作:老张直接把水壶放到火上,然后站在旁边等待水开。
- 解释:在这个过程中,老张无法做其他事情,必须等待水开,这就是同步阻塞。程序执行到这里会暂停,直到水开(即操作完成)后才会继续执行后面的代码。
同步非阻塞
- 老张操作:老张还是把水壶放到火上,但这次他选择去客厅看电视,并时不时回到厨房查看水是否开了。
- 解释:虽然老张没有在厨房一直等,但他还是需要时不时去查看水是否开了,这种“轮询”的方式就是同步非阻塞。程序会定期检查某个操作是否完成,但这仍然需要占用程序的时间来检查,效率并不高。
异步阻塞
- 老张操作:老张买了会响的水壶,并把它放到火上,但他还是选择站在旁边等水开,只不过这次他可以通过水壶的响声来判断水是否开了。
- 解释:这里的“异步”实际上是指老张不需要一直盯着水壶看水是否开了,但他仍然选择等在原地,等待水壶响。这种方式并没有真正提高老张的效率,因为他仍然被“阻塞”在原地等待,所以称为异步阻塞。但这里的核心是“异步”的概念被误解了,因为老张仍然是在等待,并没有去执行其他任务。在编程中,真正的异步是程序可以继续执行其他任务,而不必等待当前任务完成。
- 很显然这个阻塞是多余的。
异步非阻塞
- 老张操作:老张再次使用响水壶,但这次他完全放心地去客厅看电视,直到水壶响了才去厨房拿壶。
- 解释:这是真正的异步非阻塞模式。老张不需要一直等待水壶,他可以继续做其他事情(如看电视),当水壶的水开了(即某个操作完成)时,他会通过水壶的响声(回调或通知)得知,并去处理(拿壶)。这种方式下,老张的效率最高,因为他可以在等待水开的同时做其他事情。在编程中,这种模式允许程序在等待某个操作完成时,继续执行其他任务,从而提高程序的并发性和效率。
看到这里大家应该明白了,这四种组合吧!!!
注意:异步阻塞是什么鬼?
按照上文的解释,该IO模型在第一阶段应该是用户线程阻塞,等待数据;第二阶段应该是内核线程(或专门的IO线程)处理IO操作,然后把数据通过事件或者回调的方式通知用户线程,既然如此,那么第一步的阻塞完全没有必要啊!非阻塞调用,然后继续处理其他任务岂不是更好。
因此,压根不存在异步阻塞这种模型哦~
三,五种IO模型
IO模型分为五种,分别是阻塞式IO,非阻塞IO,信号驱动IO,多路转接IO,异步IO。
下面我们讲一个例子先来浅浅谈一下这5个模型IO的做法。
- 从前有一条小河,河里有许多条鱼,一个叫张三的少年就很喜欢钓鱼,他带着自己的鱼竿就去钓鱼了,但张三这个人很固执,只要鱼没上钩,张三就一直等着,什么都不干,死死的盯着鱼漂,只有鱼漂动了,张三才会动,然后把鱼钓上来,钓上来之后,张三就又会重复之前的动作,一动不动的等待鱼儿上钩。
- 而此时走过来一个李四,李四这名少年也很喜欢钓鱼,但李四和张三不一样,李四左口袋装着《Linux高性能服务器编程》,右口袋装着一本《算法导论》,左手拿手机,右手拿了一根鱼竿,李四拿了钓鱼凳坐下之后,李四就开始钓鱼了,但李四不像张三一样,固执的死盯着鱼漂看,李四一会看会儿左口袋的书,一会玩会手机,一会儿又看算法导论,一会又看鱼漂,所以李四一直循环着前面的动作,直到循环到看鱼漂时,发现鱼漂已经动了好长时间了,此时李四就会把鱼儿钓上来,之后继续重复循环前面的动作。
- 此时又来了一个王五少年,王五就拿着他自己的iphone14pro max和一根鱼竿外加一个铃铛,然后就来钓鱼了,王五把铃铛挂到鱼竿上,等鱼上钩的时候,铃铛就会响,王五根本不看鱼竿,就一直玩自己的iphone,等鱼上钩的时候,铃铛会自动响,王五此时再把鱼儿钓上来就好了,之后王五又继续重复前面的动作,只要铃铛不响,王五就一直玩手机,只有铃铛响了,王五才会把鱼钓上来。
- 此时又来了一个赵六的人,赵六和前面的三个人都不一样,赵六是个小土豪,赵六手里拿了一堆鱼竿,目测有几百根鱼竿,赵六到达河边,首先就把几百根鱼竿每隔几米插上去,总共插了好几百米的鱼竿,然后赵六就依次遍历这些鱼竿,哪个鱼竿上的鱼漂动了,赵六就把这根鱼竿上的鱼钓上来,然后接下来赵六就又继续重复之前遍历鱼竿的动作进行钓鱼了。
- 然后又来了一个钱七,钱七比赵六还有钱,钱七是上市公司的CEO,钱七有自己的司机,钱七不喜欢钓鱼,但钱七喜欢吃鱼,所以钱七就把自己的司机留在了岸边,并且给了司机一个电话和一个桶,告诉司机,等你把鱼钓满一桶的时候,就给我打电话,然后我就从公司开车过来接你,所以钱七就直接开车回公司开什么股东大会去了,而他的司机就被留在这里继续钓鱼了。
在上面的例子中,你认为谁的钓鱼方式更加高效呢?
首先我们认为,如果一个人在不停的钓鱼,时不时的就收鱼竿,把鱼钓上来,等待鱼儿上钩的时间比重却很低,那么这个人在我看来他的钓鱼方式就是高效的。而如果一个人大部分的时间都是在等待,只有那么极少数次在收杆把鱼钓上来,那么这个人的钓鱼方式就是低效的。
而上面的例子中,鱼其实就是数据,鱼竿其实就是文件描述符fd,每个人都算是进程,但除了钱七的司机,这个司机算是操作系统,河流就是内核缓冲区,鱼漂就是就绪的事件,代表钓鱼这件事情已经就绪了,进程可以对数据做拷贝了。
其实赵六的方式是最高效的,也就是多路转接这种IO模型是最高效的,因为赵六的鱼竿多啊,钓上鱼的几率就大啊,其他人只有一根鱼竿,只能关心这一根鱼竿上的数据,自然就没有赵六的效率高,同理为什么渣男的女朋友多啊,因为广撒网嘛,找到女朋友的概率要比普通的老实人高啊,因为人家一次可以关心那么多的微信账号,哪个女孩发消息了人家就和谁聊天,肯定比你只有一个女孩的微信效率要高。
所以本文章主要来介绍多路转接这种IO模型,同时也会讲解阻塞和非阻塞IO,需要注意的是,实际项目中,最常用的就是阻塞IO,同时大部分的fd默认就是阻塞的,因为这种IO太简单了,越简单的东西往往就越可靠,代码编写也越简单,调试和找bug的难度也就越低,这样的代码可维护性很高,所以他就越常用。
阻塞,非阻塞,信号驱动在IO效率上是没有差别的,因为他们三个人都只有一根鱼竿,等待鱼上钩的概率都是一样的,相当于他们等待事件就绪的概率是相同的,所以从IO效率上来看,这三个模型之间是没有差别的。只不过三者等待的方式是不同的,阻塞是一直在进行等待,而非阻塞可能会使用轮询的方式来进行等待,在等待的时间段内,非阻塞可能还会做一些其他的事情,信号驱动和非阻塞一样,在等待的时间段内,信号驱动会做一些其他的事情,比如监管一下其他的连接是否就绪等等事情。所以从IO的效率角度来讲,这三种IO并无差别,因为IO的过程分为等待和数据拷贝,三者在这个工作上的效率都是一样的,只不过非阻塞和信号驱动的等待方式与阻塞IO不同。信号驱动只不过是被动的等待,阻塞和非阻塞都是主动的等待,当信号到来时,信号驱动IO会通过回调的方式来处理就绪的事件。
而多路转接相比前三种IO模型更为高效一些,因为他能够一次等待多个文件描述符,但这四种IO都有一个共同的特征,就是直接参与了IO的过程,这样的通信我们称之为同步通信,而异步IO是典型的异步通信,他将等待数据就绪的事情交给了内核来处理,当数据准备好后,操作系统会以信号或回调函数的方式来通知进程可以处理数据了,因为数据已经准备好了,这就是典型的异步通信。
3.1.复习
要深入的理解各种IO模型,那么必须先了解下产生各种IO的原因是什么,要知道这其中的本质问题那么我们就必须要知道一条消息是如何从一个人发送到另外一个人的;
以两个应用程序通讯为例,我们来了解一下当“A”向"B" 发送一条消息,简单来说会经过如下流程:
- 第一步:应用A把消息发送到 TCP发送缓冲区。
- 第二步: TCP发送缓冲区再把消息发送出去,经过网络传递后,消息会发送到B服务器的TCP接收缓冲区。
- 第三步:B再从TCP接收缓冲区去读取属于自己的数据。
根据上图我们基本上了解消息发送要经过 应用A、应用A对应服务器的TCP发送缓冲区、经过网络传输后消息发送到了应用B对应服务器TCP接收缓冲区、然后最终B应用读取到消息。
如果理解了上面的消息发送流程,那么我们下面开始进入主题;
3.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流程:
进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程,操作成功则进程获取到数据。
假设socket为阻塞模式,则IO调用如下图所示。
当处于运行状态的用户线程发起recv系统调用时,如果socket内核缓冲区内没有数据,则内核会将当前线程投入睡眠,让出CPU的占用。
直到网络数据到达网卡,网卡DMA数据到内存,再经过硬中断、软中断,由内核线程唤醒用户线程。
此时socket的数据已经准备就绪,用户线程由用户态进入到内核态,执行数据拷贝,将数据从内核空间拷贝到用户空间,系统调用结束。此阶段,开发者通常认为用户线程处于等待(称为阻塞也行)状态,因为在用户态的角度上,线程确实啥也没干(虽然在内核态干得累死累活)。
3.3.非阻塞式IO模式
按照上面的思路,所谓非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待。
定义
-
非阻塞IO是在应用调用recvfrom读取数据时,如果该缓冲区没有数据的话,就会直接返回一个
EWOULDBLOCK错误
,不会让应用一直等待中。 -
在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断的调用recvfrom请求,直到读取到它数据要的数据为止。
非阻塞式IO流程:
和上面的阻塞IO模型相比,非阻塞IO模型在内核数据没准备好,需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。
- 进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞。
- 进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
这种工作方式下需要不断轮询查看状态
非阻塞式IO的过程大概如下:
- 1、应用进程向内核发起recvfrom读取数据。
- 2、没有数据报准备好,即刻返回EWOULDBLOCK错误码。
- 3、应用进程向内核发起recvfrom读取数据。
- 4、已有数据包准备好就进行一下 步骤,否则还是返回错误码。
- 5、将数据从内核拷贝到用户空间。
- 6、完成后,返回成功提示。
如果将socket设置为非阻塞模式,调用便换了一副光景。
用户线程发起系统调用,如果socket内核缓冲区中没有数据,则系统调用立即返回,不会挂起线程。而线程会继续轮询,直到socket内核缓冲区内有数据为止。
如果socket内核缓冲区内有数据,则用户线程进入内核态,将数据从内核空间拷贝到用户空间。
3.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请求去读取数据。
Linux中IO复用的实现方式主要有Select,Poll和Epoll:
- Select:注册IO、阻塞扫描,监听的IO最大连接数不能多于FD_ SIZE(1024)。
- Poll:原理和Select相似,没有数量限制,但IO数量大,扫描线性性能下降。
- Epoll :事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Linux2.6后内核支持。
定义
多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select,,select会监听所有注册进来的IO,每个IO都会有一个文件描述符fd。
进程通过将一个或多个fd传递给select(或者其他IO复用API),阻塞在select操作上,select帮我们侦测多个fd是否准备就绪,当有fd准备就绪时,select返回数据可读状态,应用程序再调用recvfrom读取数据。
总结:
复用IO的基本思路就是通过slect或poll、epoll 来监控多fd ,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。
3.5、信号驱动IO(signal driven IO)
复用IO模型解决了一个线程可以监控多个fd的问题,但是select是采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否就可读的数据,而无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。
于是信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,具体如下:
- 1、调用sigaction时候建立一个SIGIO的信号联系,
- 2、当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,
- 3、当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞
- 4、所以这样的方式下,一个应用线程也可以同时监控多个fd。
定义
当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知映应用线程调用recvfrom来读取数据。
总结:
IO复用模型里面的select虽然可以监控多个fd了,但select其实现的本质上还是通过不断的轮询fd来监控数据状态, 因为大部分轮询请求其实都是无效的,所以信号驱动IO意在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。
3.6、异步IO( asynchronous IO)
通过观察我们发现,不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。(这也就是为什么上面四种都是同步IO)
在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步;
如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。
思考一个问题:
也许你一开始就有一个疑问,为什么我们明明是想读取数据,什么非得要先发起一个select询问数据状态的请求,然后再发起真正的读取数据请求,能不能有一种一劳永逸的方式,我只要发送一个请求我告诉内核我要读取数据,然后我就什么都不管了,然后内核去帮我去完成剩下的所有事情?
有人设计了一种方案,应用只需要向内核发送一个read 请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种一劳永逸的模式为异步IO模型。
定义
应用告知内核启动某个操作,并让内核在整个操作完成之后,通知应用,这种模型与信号驱动模型的主要区别在于,信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核自动完成IO操作,并通知我们操作什么时候完成。
当进程发起一个IO操作,进程返回(不阻塞),但也不能返回结果。内核把整个IO处理完后,会通知进程结果,如果IO操作成功则进程直接获取到数据。
此模型和前面模型最大的区别是:前4个从内核空间拷贝数据这一过程是阻塞的,需要自己把准备好的数据,放到用户空间。
而全异步不同,异步IO是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。
用户线程完全不需要关心实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据,它是最理想的模型。
总结:
异步IO的优化思路是解决了应用程序需要先后发送询问请求、发送接收数据请求两个阶段的模式,在异步IO的模式下,只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作。