并发编程之IO模型

1>引子

    以一个数据读写的例子来说一下,IO发生时涉及的对象和步骤,它会涉及到两个系统对象,
    一个是调用这个IO的process或者thread,另一个就是系统内核kernel。
    当一个read操作发生时,该操作会经历两个阶段:
    1)等待数据准备 (Waiting for the data to be ready)
    2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
    记住这两点很重要,因为这些IO模型的区别就是在两个阶段上各有不同的情况。

2>阻塞IO

    linux中默认情况下所有的socket都是blocking,一个典型的读操作流程大概如下:

    并发编程之IO模型

    用户进程调用了recvfrom之后,kernel就开始了IO的第一个阶段:准备数据,对于网络I/O来说,很多数据在

    一开始还没到达,这是kernel就要等待数据的到来,

    反观用户进程这边,整个程序都被阻塞---等待kernel准备数据,等待kernel拷贝数据到用户应用程序内存,然后kernel

    返回结果,用户进程才解除block状态,继续运行。

    所以,阻塞IO的特点就是IO执行的两个阶段(等待数据和拷贝数据)都被block了

    实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。
    这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,
    在此期间,线程将无法执行任何运算或响应任何的网络请求。

3>非阻塞IO

    Linux下,可以通过设置socket使其变为non-blocking。当对一个非阻塞socket执行读操作时,流程大概如下

    并发编程之IO模型

        如上,当用户进程发出recvfrom操作时,如果kernel中的数据还没有准备好,它会返回一个error。
    从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程
    判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的
    时间间隔内做其他事情。然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用,
    这个过程通常被称之为轮询,一旦kernel中的数据准备好了,并且又再次收到了用户进程的recvfrom操作请求,

    那么它马上就将数据拷贝到了用户内存,然后返回。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

    

4>多路复用IO

    

    并发编程之IO模型

    当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,
    当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从
    kernel拷贝到用户进程。这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这
    里需要使用两个系统调用\(select和recvfrom\),而blocking IO只调用了一个系统调用\(recvfrom\)。

    但是,用select的优势在于它可以同时处理多个connection。

    很显然,多路复用也有弊端,因select带着一堆的socket在那里问:我这些请求有没有数据已经准备好的。

    kernel只能遍历你所有的socket,并且挨个返回你那些已经准备好数据的请求,当数量达到一定规模,这个

    其实也是占用资源并且耗时的,并且,可以设想这样一个场景,select带了10000个socket问kernel,kernel遍历到

    第9999个这个准备好数据了,那么之前的9998次遍历岂不是耗时又无用?

    由此总结:select IO模型若是用于单连接,还不如 阻塞IO;若是用于多连接,当需要探测的句柄值较大时,

    select()接口本身需要消耗大量时间去轮询各个句柄。

5>异步IO

    并发编程之IO模型

    听起来就比较高效的IO模型,事实上也确实可以,从上图看出,程序发起read请求后,kernel会立马返回一个

    数据没准备好的信息,然后程序这边可以继续往下执行(注意,区别,不管数据有没有准备好,程序几乎没有

    受到任何阻塞,因为就经历一次请求和一次回信的过程),然后,当kernel准备好数据了,就直接拷贝至应用

    程序的内存,通知程序,数据已拷贝完成了!(其实可以设想这样一个类似机制:基于多路复用IO吧,多路复用IO

    是有个类似“中介”的select帮你去询问数据准备好了没,但这个就直接把这个‘机制’交给kernel了!而且还

    绑定了回调函数!只要你kernel这边把数据准备好,便回调‘拷贝至应用程序内存’的操作,并且给应用程序

    发送信号,纵观整个过程:应用程序就发了个信号,就继续执行,然后就收到数据已拷贝好的消息,几乎没有

    任何阻塞。)

6>再总结下四中IO的特点吧

    6.1>阻塞IO和非阻塞IO的区别

    如上面所言,阻塞IO会一直block住对应的进程直到操作完成,而非阻塞IO在kernel还准备数据的情况下会立刻返回。

    6.2>同步IO和异步IO的区别

    同步IO做”IO operation”的时候会将process阻塞,按照这个定义,上面的 阻塞IO、非阻塞IO、多路复用IO

    都可以划为同步IO这一类,异步IO属于另一类

    6.3>各IO模型的比较、总结

    并发编程之IO模型

          首先明确一点:所有的block都是基于应用程序自身的立场

         1>左1:阻塞IO,准备数据和拷贝数据均耗时且组成进程(这两个阶段应用程序是block的),

         2>左2:非阻塞IO,第一段数据准备阶段,应用程序一直在询问-----那一堆check状态,此时程序自身

        其实是没有继续往下走的,一直在问数据,当然,你可以在这里写个分支语句,如果check没有准备好,

        就怎样怎样,以此来提升一些效率。相比起阻塞IO的‘被动等待’,这个属于‘主动询问’。

         3>中间:多路复用IO,之前说过,多路复用在多并发时才有些优势,图上单条线路看起来有点吃亏(步骤比阻塞

        IO还多,block却一样长),因多路复用复用类似有个‘中介’帮你问,在‘中介’回复你数据准备好了的时候,

        你可以安安静静的做其他事情,而不用像非阻塞IO一样,一直自己去问个不停,但是对于线程自身来说,还是阻塞

        的,虽然你不是等kernel回复,但是你在等‘中介’回复的呀,好就好在‘中介’select可以一次性帮好多socket询问

        数据是否准备好,并发时就有明显优势了,

         4>右2:事件驱动IO,貌似用的很少,不太清楚原理。

         5>右1:异步IO,看到没,就这这IO模型,数据准备和拷贝数据阶段都是空白的,都没有block!,wow,从一开始

        发了个read数据的信号,然后就是kernel把数据‘送到它手里’。这个IO模型后面会用到,到时再深入解析吧。