前言
在上一篇文章中,我们了解了操作系统中内核程序和用户程序之间的区别和联系,还提到了内核空间和用户空间,当我们需要读取一条数据的时候,首先需要发请求告诉内核,我需要什么数据,等内核准备好数据之后 , 再从内核空间拷贝到用户空间 注意加粗的部分,这两个阶段至关重要
对以上的两个过程以及操作系统的IO流程不了解的,请务必左转去看上一篇文章,上篇文章中是学习IO的基础知识,只有把上一篇文章的内容看懂了,对于后续的IO几种模型的学习和理解才会更为深刻,上一篇文章可以说是整个IO中的基石级别的知识。
文章链接 【网络IO系列】 预备知识 操作系统之内核程序和用户程序
IO的五种模型
我们回到正题,从上篇文章我们知道,当我们进行一次IO的时候,是要经过这两个阶段的,分别是
第一阶段 :等待内核准备数据
第二阶段:数据从内核空间拷贝到用户空间
这两个阶段则决定着IO的各种模型的类型,通过这两个阶段,可以将IO模型分成五种,分别是
- 阻塞式IO(BIO)
- 非阻塞式IO(NIO)
- IO多路复用
- 信号驱动IO
- 全异步IO(AIO)
阻塞
说到阻塞,在这里想先明确一下,什么是阻塞。从线程或者进程的角度来看,阻塞就是因为当前执行的这个线程,暂时的失去了CPU的执行权,被挂起等待下一次线程的调度或者线程被唤醒。如果是具体到我们的IO的话,则可以理解为,阻塞的时候你需要等待,等到数据准备好或者执行结果返回,才能继续下一步的操作,不然只能一直等待下去。而非阻塞,则是,即便数据没有准备好,或者执行没有完成,你也可以去做其他的事情。
举个例子,比如说你出门排队买东西,假如你没有带手机,那你只能老老实实排队等轮到你,在这之前,你除了排队这件事之外,其他什么事情都干不了也不允许干,这个时候你就是阻塞的,因为你只能做排队这一件事。那么同样是排队,这一次你带了手机,那你排队的时候,可以边玩手机边排队,这个时候你就是非阻塞的。
接下来让我们对应到上面的两个阶段,如果等待内核准备数据的时候,执行线程可以去做其他的事,那么在第一阶段就是非阻塞的,否则就是阻塞的。如果在数据从内核空间拷贝到用户空间阶段,执行线程可以去干其他的事,那么第二阶段就是非阻塞的,否则就是阻塞的。
阻塞式IO(BIO)
阻塞式IO,是在两个阶段都阻塞的一种IO模型,用户发起IO请求,在等待数据和数据拷贝阶段,都会被阻塞,只有这两个阶段都完成了,才能去做下一阶段的事情。
就像是你没带手机去吃饭,你跟老板说要吃鱼香肉丝,然后要等老板做好菜(准备数据),然后从厨房把菜端到你面前(数据拷贝),这两个阶段你都只能等着,什么事都干不了。
由于BIO阻塞时间长,因此相对性能就会较低,所以现在用的相对也比较少了。
非阻塞IO(NIO)
非阻塞IO,可以看作是半阻塞IO,因为他在第一阶段数据准备阶段不阻塞,第二阶段数据拷贝阶段阻塞,当用户发出IO请求的时候,会有一个线程去询问内核数据准备好了吗,一直问一直问,在这期间,用户主进程可以去干其他的事,等数据准备好了,到了第二阶段,这个时候,用户线程就要执行拷贝数据,这个时候是阻塞的。这种方式的缺点就是反复的轮训去询问内核数据好了没,是很消耗CPU资源的。
就像是你带手机去吃饭,你点好菜之后,你可以一直问老板,我的菜好了没,老板说没有,问完之后就可以继续玩手机继续等,继续问。等到有一次你问,老板我的菜好了吗,老板说好了,你自己过来端一下。(注意,问菜好没有的,得是你自己问,这家NIO店的老板比较高冷,菜好了你不问他是不会主动告诉你的,这就是NIO的特点,数据准备就绪是用户线程主动发出的询问),这个时候菜好了,你要自己去端(数据拷贝),这个端菜的阶段,你期间啥都干不了,也不能玩手机,所以NIO的第二个阶段是阻塞的。
说到这里我们可以看出BIO和NIO之间的区别了,一个是傻等老板做菜给你,期间你什么都干不了,一个是自己主动询问老板,菜好了没,期间你可以玩手机,或者干其他的,相比BIO,NIO的效率就高了很多。当然,你可能会问了,为啥菜好没好,还得我自己主动去问,这也太不人性化了,确实,这个问题我们想得到,计算机的科学界大师们自然也想得到,于是为了解决这个问题,于是出现了信号驱动IO和IO多路复用。
IO多路复用
通过我们上面对NIO的了解,我们可以知道,NIO多少存在着一些不够好的地方,因为反复的轮训也是很消耗cpu资源的。如果饭店的人少还好说,但是如果饭店人多起来了,比如说来了几百个人,那每个人时不时就要发起一次询问请求,那老板管不过来啊,cpu占用率也会非常高。于是,IO多路复用就出现了,IO多路复用可以说是目前用的最多的一个IO模型,在不同的操作系统内核,也有不同的实现方式,在这篇文章中,我们IO多路复用的大概思想,至于详细介绍,后面会用一篇文章来详细的介绍IO多路复用
IO多路复用,实际上,是通过IO请求都通过一个selector来管理,用户进程的IO请求就不直接发给内核处理程序了,而是注册到这个selector上面,由selector来告诉内核需要哪些数据,然后定时的去查询内核程序,我这个selector上需要的数据,有哪些准备好了,然后再由selector告诉那些准备好了的用户线程,让该用户线程去拷贝数据。在非阻塞IO中,不断地询问状态时通过用户线程去进行的,而在IO多路复用中,询问每个状态是内核在进行的,在IO请求非常多的时候,这个效率要比用户线程轮询要高的多。
就像是你带手机去饭店吃饭,现在这家饭店的老板由于生意越来越好,人越来越多,他有点管理不过来了,于是他请了几个服务员(selector)协助管理,然后现在饭店客户的点餐都是告诉服务员,我需要什么菜,然后服务员把xx桌客户的菜,记在自己的单子上。然后服务员告诉厨房他这个单子上需要哪些菜,让厨房去做。。服务员定时问厨房看看有哪些菜已经准备好了,然后告知15号桌和89号桌客人你们的菜已经好了,请来前台端一下,然后你就去前台端菜,端菜的阶段是阻塞的。
来比较一下IO多路复用和NIO,我们可以发现,当IO请求多的时候,IO多路复用效率无疑是更高的。因为对于用户线程来说,你点完菜就可以一直玩手机了,不用因为一直问老板而分心分神,耽误你打王者,因为菜好了,服务员会通知你
信号驱动IO
通过我们上面两种IO模型的了解,我们可以知道,不管是NIO还是IO多路复用,本质上还是轮询,只不过NIO是用户线程轮询,IO多路复用是委托给selector让他来轮询,那有没有什么办法能让内核主动通知数据好了没。所以,信号驱动IO出现了。信号信号,顾名思义,就是会有一个信号通知你数据已经准备好了,不用你一直去问。信号驱动IO,用户线程发出一个请求告诉内核我需要什么数据,数据准备好了你告诉我一声,然后内核就会记录下这个请求,内核准备好了之后会主动通知用户线程去执行拷贝数据,数据拷贝阶段是阻塞的,需要等数据拷贝完才能做其他的事。
就像是你带手机去吃饭,你点好菜之后,你就只管玩手机了,啥也不用管,就等老板通知你,期间你想干啥就干啥,等到菜准备好了,老板会大声说(内核主动通知用户进程),xxx你的鱼香肉丝已经准备好了,请过来前台端一下,这个时候你要自己去端(数据拷贝),这个端菜的阶段,你期间啥都干不了,也不能玩手机,所以信号驱动IO的第二个阶段也是阻塞的。
我们对比信号驱动IO和NIO,可以发现最重要的区别就是NIO是用户主动询问内核数据好了吗,而信号驱动IO是内核主动通知用户数据已经好了,这就改善了上面说的NIO的问题。
全异步IO
全异步IO是最理想的一种IO模型,所谓全异步IO就是,用户进程发起了一个IO请求,接下来可以干其他的事了,不需要等内核准备好,也不需要执行数据拷贝,数据异步拷贝到用户空间之后,用户进程直接拿来用就行了,这两个阶段都是由内核自动完成。完全不用用户线程操心这些事。
前面四种IO模型实际上都属于同步IO,只有最后一种才是是真正的异步IO,因为不管是是IO多路复用还是信号驱动,IO操作的第2个阶段都会让用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
举个例子就像是,你去饭店吃饭,点好餐之后,你就可以玩手机了,饭菜做好之后,服务员会把饭菜端到你的面前,你也不需要自己去端,你需要点餐和吃饭就行了,其他的你都不用管。简单来说,就是发出请求之后,只需要等待数据完成直接使用,等待期间,你可以做其他的事。整个过程完全的异步,体验最好。
全异步IO虽然非常牛逼,但是现在还不是很成熟,支持全异步IO的操作系统和框架也还不是很多,所以用的也不是很多。我们只需要了解一下就行了
总结
我们这篇文章讲了五种IO模型的思想,并且每种模型我们都通过一个通俗易懂的例子,来描绘其过程。相信你看完之后一定有收获。其中比较重要的两种是NIO和IO多路复用,这是目前来说用的最多的两种,后面的篇幅,会专门的讲这两种模型,尤其是IO多路复用,在不同的OS上,又有select,poll,和epoll方式。等下一篇文章,我们将会细讲。