IO模型详解及应用

如何阅读这篇文章顺序

1.1:了解同步异步和阻塞非阻塞

    1.11: 同步异步

    1.12:阻塞非阻塞

1.2:了解一次read操作需要的步骤

1.3:五种模型

1.1:I/O模型中的同步异步,阻塞非阻塞:

1.11:同步和异步:synchronousasyncronous

    关注的是消息通知机制

 

同步概念:调用发出之后不会立即返回,但一旦返回,则返回则是最终结果:

异步:调用发出之后,被调用方立即返回消息,但返回的并非最终结果,被调用者通过状态、通知机制等来通知调用者,或通过回调函数来处理结果。

同步异步:关注的是调用者如何等待结果

 

 

1.22:阻塞和非阻塞:blocknonblock

 

阻塞:调用结果返回之前,调用者会被挂起:调用者只有在得到返回结果之后才能继续:

非阻塞:调用者在结果返回之前:不会被挂起,即调用不会阻塞调用者

例子:比如你去饭馆吃面需要等着上面才能吃到。   

阻塞:当你在点面的时候前面排队的人很多,老板和你说一句客官稍等,这时候你没办法做其他事情这就是阻塞。

非阻塞:比如你和老板关系不错,你跟老板说一句给你上面,你就出去买烟估摸时间差不多了就回去了,这就是非阻塞。

  • 异步同步:关注的是调用者等待被调用者返回调用结果时的状态
  • 阻塞和非阻塞:关注的是如何通知被调用者结果已经完成

根据上面的概念划分的话常见的I/O模型分为这五种。

I/O模型:

blocking IO  # 阻塞式IO

nonblocking IO  # 非阻塞式IOIO multiplexing # 复用式IO

signal driven #事件驱动IO,事件驱动式IO有通知机制的

通知:

    水平触发:多次通知

    边缘触发:只通知一次

 asynchronous IO # 异步IO

1.2:为了解释I/O模型,举个read操作的例子。

例如:从磁盘一次read操作大体分为两个步骤

用户空间的进程是无法直接访问硬件的,所以步骤大概如下。

当一个用户进程需要进行io操作的时候向系统内核发送调用请求调用什么数据。这时候就分为两个步骤了。

  • 步骤一.当内核得到用户进程通知要调用数据的时候,内核本身是没有这个数据的,数据是在磁盘上的。所以内核会从磁盘加载到自己的内存中(内核内存) # 这个内存不是用户进程的那个内存,虽然内核能这么做,但这是不推荐的。
  • 步骤二.这个时候用户进程还是没有这个数据的,所以用户进程还需要从内核内存中copy一份数据到自己的这个进程内存中。这个就是第二个步骤

真正属于IO操作的步骤是内核内存中的数据到进程内存数据,这个属于真正执行IO操作步骤的阶段。内核内存从磁盘取数据的阶段是等待事件完成阶段。

 

 

 

五种IO模型

阻塞式I/O模型

当调用者发起调用请求之后,此处调用者一方会被挂起,这个时候进程会转入不可中断式睡眠状态,这个时候调用者在得到结果之前什么事情也不能够去做,将一直处于等待过程当中。 阻塞型I/O举例:当一个用户访问你web服务器的时候,这个时候你服务器会有一个进程去处理用户的请求去取数据,这个时候这个进程是被挂起的状态。这个时候这个进程是不能处理其他用户的请求,因为处于睡眠状态。当然事实情况是不会用阻塞式I/O的因为是需要处理两路请求的,一部分网络I/O请求,一部分磁盘I/O请求。一个进程通常情况下只能处理单路I/O的

非阻塞式I/O模型

当调用者发起调用请求时候,被调用者告诉你我收到你的请求了你等着吧~这个时候会有个问题是你必须每过一段时间去问被调用者:好了没?这个叫做盲等待。那到底阻塞好还是非阻塞好。如果一件事情你没办法知道什么时候好。你一遍遍问好了没?还不如直接告诉进程你进入不可中断式睡眠状态吧。盲等待效率不一定高,非阻塞式不一定就是性能好。对于进程而言,由于他没被阻塞,所以他不得不轮询一遍遍的问。当老板告诉你面OK的时候,这个时候数据是从磁盘到了内核内存中了,还没到进程内存中。这个时候调用者去取数据的时候这个时间段还是阻塞的,因为你在取数据不能做其他事情。

 

 

 

这个时候对web服务器来说非阻塞式并没有带来性能的提升。

复用式I/O模型

一般情况下一个进程只能处理一路I/O,一旦这个进程在做I/O操作的时候,其他请求在做什么的时候这个进程都不知道,但我们都知道一个web服务器是需要处理两路I/O的,一路是用户通过网络请求过来,一路是处理磁盘加载到内存中的操作。一旦进程被阻塞在磁盘上,这个时候网络I/O发生异动了(比如用户不请求了)。这个时候阻塞型I/O进程是不得知的。我们没有办法去结束这个进程的,因为他处于不可中断的睡眠状态,你ctrl+C也没用除非他加载完数据。 复用式IO就是在这种情况下被发明在内核中。这个模式是这样的当你发起一个I/O请求的时候,你的操作会交给内核中的一个代理人。这个代理人会将其翻译为内核能理解的请求。这个时候用户请求被阻塞在代理人上而不是内存中。 举例:当你去银行办业务的时候,你们所有人就在柜台人员那里排队,一个个完成。这个时候当你说你要办银行卡的时候就算出卡需要时间,银行人员是不是不能跟你说在旁边等着? 当然咱们还能有这种模式,当你去办业务的时候有个机器让你选择你要办什么业务并给你发一个号码,当你要办卡然后存钱两个业务的时候,这个时候你想办理什么业务的时候都是跟这个代理请求,这个代理给你结果。这样就叫做复用性I/O。这种内核早期的调用就叫做select(),还有种是poll()这两种区别不大。这两个是两个公司发明的,其中一个公司看select组件挺受别人欢迎的就山寨了一个。 select是有限制的最多只能接受1024个请求并发。你要想更多只能改内核源码,但你如果改内核源码的话可能性能还会下降。当用户请求进来的时候主进程接待分给其他子进程响应最多只能1024个,再多就超出上限拒绝服务了。这种模式还是阻塞的,但它不是阻塞自己本身上,而是阻塞在代理人上(select)。 当你发起一个IO请求的时候状态是阻塞的,这个阻塞不是阻塞在自己的调用上,而是阻塞在select上。因为select是内核中复用型I/O代理。 当你发起I/O调用的时候,数据要从磁盘到内核内存。这个时候我们调用是被阻塞的,这个时候阻塞不是阻塞在内核的I/O调用上,而是阻塞select上。这个时候select还能接受其他请求,这是最大的好处,因为他能接收其他信号上面。

他不是在性能上有提升,因为他还是阻塞的。只是他能处理其他请求。

事件驱动型I/O

当请求者发I/O请求的时候,被调用者会告诉你我知道了,你该干嘛就干嘛去。 例子:当你去叫面的时候,老板告诉你大概需要10分钟你该干嘛干嘛。你干完自己的事情的时候,去端面的时候你就是取数据了。这个时候就是你能并行运行的原因。

当这个用在web服务器上,就比如一个用户向你提交请求了。你向内核要数据,内核告诉你我知道了你该干嘛,干嘛去。这个时候你就能去处理其他用户的请求了。这就是一个进程能处理多个请求的原因。当然这个并不是说性能一定好,当然他比阻塞或者盲等待肯定有优势的。因为当你去从内核取数据当进程内存中的时候还是阻塞的。有了事件驱动型I/O就使一个进程能处理多个请求了,当然事件驱动型I/O第二个阶段还是阻塞的。

在看事件驱动型I/O当你一个请求过来的时候,系统告诉你该干嘛干嘛去。这个时候第二个请求过来了,系统告诉第二个请求我知道了你干嘛干嘛去。这个时候进程1的请求数据好了,告诉你过来拿数据吧,这个时候进程二的数据也好了也要告诉你过来拿数据吧。你需要知道一点当内核通知你拿数据的时候,你一段时间不过来拿,这个信号是会消失的。所以这时候需要讲到事件驱动通知的两种机制。 通知: 水平触发:多次通知,就是通知你一次你没响应,我就再通知你。这就很浪费系统资源了 边缘触发:所谓边缘触发就是我通知你一次你没响应,我就将这个通知事件交给回调函数,让调用者自行来获取,或者你可以这么理解我将资源放在某处。你可以自己过来找我要的。简单理解就是面好了我电话一遍遍通知你是水平触发,如果我电话通知你了,然后你没响应,我将面放到后厨,你自己也知道我电话通知你了。你自己明白你会去后厨拿的。

异步I/O模型

当调用者发送请求的时候,内核告诉你请求收到了,你该干嘛就去干嘛。这个时候内核默默的完成两个步骤后就告诉你OK了。 所以web服务器正确步骤是这样的,当用户过来请求资源的时候,内核自己完成这两个步骤从磁盘到内核空间,在到进程空间,这个时候进程就立即打包响应报文给客户端。我们httpd的event模型就是事件驱动型IO,后来我们httpd2.4以后也支持异步I/O了。这个异步模型大大提升了系统性能,他的好处让一个进程能处理多个请求,我们的进程或者内存通常会把常用的资源放在缓存中,当你有两个请求同样的数据的时候,这个数据静态的并不敏感,我们的系统能够直接响应,这对性能的提升来说可想而知

nginx从刚开始设计的时候就是用的事件驱动型IO,它同时也支持异步I/O。它还有种机制是内存映射