千里之行始于足下 之(一) I/O模型

269 阅读8分钟

    You have a dream, you got to protect it. People can't do something by themselves; they wanna tell you you can not do it. You want something. Go get it!

     先问是不是,再问为什么!

    在前文章,曾提及I/O相关知识,在本文中将进一步对I/O模型进行解读,便于提高自己对I/O更加深层次的理解。在前面万丈高楼平地起之Java基础四中,对I/O一些基本知识以及阐述过,本文不再赘述。

    在正文开始之前,先了解几个概念同步、异步、阻塞、非阻塞,在之前文章也阐述过或多或少的知识点。通过一个常见的例子来加深理解:一个人去银行柜台办理业务,要取出一百万现金,柜员表示大额现金要请示领导,这个人堵在窗口,等待柜员将领导来到该窗口,询问相关事宜,给他取出一百万现金,一直等钱全部取出这个人才离开;第二次,这个人又来到窗口要取一百万现金,柜员表示,您可以到VIP室有人会帮助您办理业务,这个人离开柜台。这个例子中:就是这个人在窗口等待柜员处理业务,没有处理完不离开,即同步就是发起一个调用后,被调用者未处理网请求之前,调用不返回;阻塞即就是发起一个请求,调用者一直等待请求结果返回,当前线程挂起,其他线程不工作,只有当前线程完成后,其他线程才能工作;后来,这个人直接到VIP办理业务,在柜台等了一会就离开了,等到点好一百万,回来取钱。即异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者没有返回结果,此时被调用者可以处理其他请求,被调用者通过依赖事件,回调等机制通知调用者期返回结果;即非阻塞发起一个请求,调用者不用一直等待着结果返回,可以先处理其他事。

BIO 模型

传统同步阻塞模式图


      每一个客户端都要分配一个线程使用,也就是意味着服务端在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁,这就是典型的 一请求一应答通信模型。这样的话就会有很大的风险:创建线程失败、线程堆溢出、线程阻塞导致系统崩溃等。

改进同步阻塞模式图

      采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task投递到后端的线程池中进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。这只是规避了单一线程创建时带来的一些风险,并不能从根本上解决问题。                 

NIO 模型

   非阻塞I/O模型图


     NIO是面向缓冲区(面向块)编程,数据读取到一个缓冲区内,再通过通道流动,这就增加了处理过程中的灵活性,可以提供非阻塞式的高伸缩性网络。使用一个线程来处理多个操作,减少线程的创建和销毁,提高资源利用率。 

NIO三大核心组件:

1、缓冲区(Buffer)

IO 面向流,而 NIO 面向缓冲区

    缓冲区是一个对象,它包含一些要写入或者要读出的数据。在面向流的I/O中可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。

2、通道(Channel)

NIO 通过通道进行读写缓冲区内的数据

    通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和缓冲区交互。依靠 缓冲区,通道可以异步地读写。

3、选择器(Selector)

NIO有选择器,而IO没有

    选择器用于使用单个线程处理多个通道,利用较少的线程来处理这些通道,减少线程的切换,线程之间的切换消耗对于操作系统来说是昂贵的。

三大组件的关系

    每一个通道都会对应一个缓冲区,一个选择器可以对应多个通道但一个选择器仅对应一个线程,通道都是要注册到选择器中去;由事件驱动决定切换到哪一个通道,选择器会根据不同的事件,在多个通道之间切换;缓冲区实质上是一块内存空间,底层是一个数组,数据的读写都是通过缓冲区实现的,值得注意的是,读写操作要使用Buffer.filp()方法切换。

   还有一个AIO模型并没有进行解析,有关更多的知识将会在日后Netty分析中阐述,如果还有后文的话!

上文讲述的都是Java中相关的IO,接下来说说操作系统中的IO。

阻塞IO模型

    线程或者进程等待某一个条件,如果条件不满足,将会一直等待下去,直到条件满足才会进行下一步操作。应用进程通过系统调用接收数据,由于内核并没有将数据报准备好,此时应用进程将阻塞住,直到内核准备好数据报,系统对数据报进行处理,处理好的数据报给应用进程,应用进程结束阻塞,进行下一步操作。

非阻塞IO模型

    线程或者进程等待某一个条件,如果条件不满足,将不会一直等待下去,直接返回,然后采用轮询的方式来询问条件准备好了?发现条件满足才会进行下一步操作。应用进程通过系统调用接收数据,由于内核并没有将数据报准备好,此时应用进程将不停询问内核,直到内核准备好数据报,如果内核一直没有准备好,内核返回error,应用进程将不再询问,而是在后一次再发送一次请求,内核准备好数据报,将数据从内核中复制到用户空间,系统对数据报进行处理,处理好的数据报给应用进程,进行下一步操作。再两次请求中,应用进程可以其他操作,并不会进入阻塞状态。

IO复用模型

      多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。

    IO多路复用是利用了select方法,多个进程的IO可以注册到同一个select上,当用户进程调用该selectselect会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好时,select调用进程会阻塞。当任意一个IO所需的数据准备好之后,select调用就会返回,然后进程在通过recvfrom来进行数据拷贝。

这里的IO复用模型,并没有向内核注册信号处理函数,所以,他并不是非阻塞的。进程在发出select后,要等到select监听的所有IO操作中至少有一个需要的数据准备好,才会有返回,并且也需要再次发送请求去进行文件的拷贝。

信号驱动IO模型

    应用进程先向内核注册一个信号处理方法,应用进程返回不阻塞,当内核并将数据报准备好时将发送一个信号给应用进程,应用进程载信号处理方法中将内核数据复制到用户空间,系统对数据报进行处理,处理好的数据报给应用进程。

异步IO模型

    应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。应用进程发起请求操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。当内核接收到请求后,会立刻返回,内核便开始等待数据准备,数据准备好以后,直接把数据拷贝到用户控件,然后再通知进程本次IO已经完成。

有关底层IO线程模型知识来自于:《漫话:如何给女朋友解释什么是Linux的五种IO模型?》 


本文要是有遗漏的地方,我将在评论区给出,同时若读者发现本文不当之处也请在评论区指出,共同研究。

特别注意:本文摘抄文字版权属于原作者!!!