netty是一个用于客户端和服务器通信的高性能网络通信框架,涉及到跨机器之间的通信,必然离不开I/O,所以我们就从Java 的I/O演变之路开始讨论吧。

  引出一个话题:什么是I/O,或者这么说,一次I/O究竟发生了哪些事情?我们从三个层面来理解一下I/O:

  • 1、从直观上来看:I/O是计算机和外部设备之间的数据流动过程。计算机即是我们平日里随处可见的一台机器,外部设备大体上又包括输入设备和输出设备。比如,键盘、鼠标就属于输入设备;显示器属于输出设备。

  • 2、从计算机架构的层面来看:任何涉及到计算机核心(CPU和内存)与其他设备之间的数据转移就是I/O。本体就是计算机核心(CPU和内存)。例如从硬盘上读取数据到内存,是一次输入,将内存中的数据写入到硬盘就产生了输出。在计算机的世界里,这就是I/O的本质。

  • 3、从程序员编程的角度来看:此时,I/O的主体是我们编写的应用程序(即进程),需要注意的是,我们编写的应用程序其实并不存在实质的I/O过程,真正的I/O是操作系统的任务(用户态和内核态)。我们将应用程序代码的I/O细分为两个动作:I/O调用和I/O执行。I/O调用由应用程序(进程)发起的,而I/O执行是操作系统完成的(这就是系统调用),我们代码里写一个read()函数,实质上包含了我们程序发起的I/O调用,在看不见的地方,还有一次I/O执行(操作系统所完成的系统调用)。 I/O调用的目的是将进程的内部数据迁移到外部即输出,或将外部数据迁移到进程内部即输入(应用程序是主体)。这里,外部数据指非进程空间数据,在编程时,通常讨论的场景是来自外部存储设备的数据,如硬盘、CD-ROM、以及需要socket通信传输的网络数据。

    我们来看一次典型的I/O输入会发生什么事情,以一个进程的输入类型的IO调用为例,它的工作内容如下:

  1. 进程向操作系统请求外部数据——>I/O调用
  2. 操作系统将外部数据加载到内核缓冲区———>I/O执行
  3. 操作系统将数据从内核缓冲区拷贝到进程缓冲区———>I/O执行
  4. 进程读取数据继续后面的工作
    接下来我们来对比两组常见的概念

阻塞I/O和非阻塞I/O

  阻塞和非阻塞强调的是进程对于操作系统IO是否处于就绪状态的处理方式。  
  上面已经说过,应用程序的IO实际是分为两个步骤,IO调用和IO执行。IO调用是由进程发起,IO执行是操作系统的工作。操作系统的IO情况决定了进程IO调用
  是否能够得到立即响应。如进程发起了读取数据的IO调用,操作系统需要将外部数据拷贝到进程缓冲区,在有数据拷贝到进程缓冲区前,进程缓冲区处于不可读状态,
  我们称之为操作系统IO未就绪。
  进程的IO调用是否能得到立即执行是需要操作系统IO处于就绪状态的,对于读取数据的操作,如果操作系统IO处于未就绪状态,当前进程或线程如果一直等待直到其
  就绪,该种IO方式为阻塞IO。如果进程或线程并不一直等待其就绪,而是可以做其他事情,这种方式为非阻塞IO。所以对于非阻塞IO,我们编程时需要经常去轮询
  就绪状态。  

同步I/O和异步I/O

  我们经常会谈及同步IO和异步IO。同步和异步描述的是针对当前执行线程、或进程而言,发起IO调用后,当前线程或进程是否挂起等待操作系统的IO执行完成。
  我们说一个IO执行是同步执行的,意思是程序发起IO调用,当前线程或进程需要等待操作系统完成IO工作并告知进程已经完成,线程或进程才能继续往下执行其他
  既定指令。
  如果说一个IO执行是异步的,意思是该动作是由当前线程或进程请求发起,且当前线程或进程不必等待操作系统IO的执行完毕,可直接继续往下执行其他既定指令。
  操作系统完成IO后,当前线程或进程会得到操作系统的通知。
  以一个读取数据的IO操作而言,在操作系统将外部数据写入进程缓冲区这个期间,进程或线程挂起等待操作系统IO执行完成的话,这种IO执行策略就为同步,如果进程
  或线程并不挂起而是继续工作,这种IO执行策略便为异步。

总结:在IO调用时,对待操作系统IO就绪状态的不同方式,决定了其是阻塞或非阻塞模式;在IO执行时,线程或进程是否挂起等待IO执行决定了其是否为同步或异步IO。

在JDK引进NIO(Non-Blocking IO)之前,Java的I/O是阻塞式,都是面向流的,大体上可以分为两类:

  • 基于字节流的InputStream和OutputStream
  • 基于字符流的Reader和Writer
    字符和字节之间的转换对应的就是编码和解码。在这个时期Java的IO是同步阻塞的(attention:同步和阻塞不是一个概念,他们是不同维度的),且只有输出流和输入流,没有channe、buffer等,效率很低。 Java NIO中文版这本书剖析了Java NIO,清晰易懂,推荐阅读。
    这里,我们借用权威书《UNIX 网络编程:卷一》中的内容,来讨论Linux下的5种I/O模型。

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用程序缓冲区。 好,下面我们以阻塞套接字的recvfrom的的调用图来说明。

1、阻塞I/O模型

如图所示,这是最常用的I/O模型,缺省情况下所有的文件操作都是阻塞的,事实上,网络I/O也是文件I/O的一种(SocketOutputStream是FileOutputStream的子类)。在进程空间发起recvfrom调用(一次I/O调用);此时转到操作系统内核,操作系统内核将数据加载到内核缓冲区(一次I/O执行),这段时间应用进程阻塞住,等待数据准备好;然后由操作系统将数据从内核缓冲区拷贝至用户进程缓冲区(I/O执行),这段时间应用进程仍然处于阻塞状态,等待数据拷贝完成;一旦拷贝完成,操作系统内核就告知用户进程,调用成功返回。由此图我们可以得知,阻塞I/O模型在两个时间段都是被阻塞的。 AJAlVS.png

2、非阻塞I/O模型

还是截了一张书中的图,可以看到,应用进程在发起recvfrom调用的时候,如果发现操作系统内核还没有把数据准备好(即操作系统内核缓冲区中还没有数据),那么就直接返回一个EWOULDBLOCK错误,这种模式的一个问题就是你需要隔段时间去轮询一次,看操作系统内核缓冲区中是否有数据到来;一旦内核缓冲区准备好数据,那么调用开始阻塞,等待操作系统将内核缓冲区中的数据拷贝到用户空间中去,这段时间进程仍然是被阻塞住的,直到拷贝完成才能返回。
由此我们得出结论:阻塞和非阻塞I/O的区别在于是否需要等待操作系统将外部数据加载到内核缓冲区,即前面提到的第一次I/O执行。 AtJa11.png

3、I/O多路复用模型

I/O多路复用的基本原理是:将多个I/O的阻塞复用到对一个select的阻塞上,实现一个线程处理多个请求,它的核心在于多路复用器Selector,多个socket可以注册到一个Selector上,这个selector去轮询它所负责的socket,发现某一个socket的数据准备好了后就返回,然后去执行系统调用recvfrom。图中我们可以看出来,虽然I/O多路复用的函数也是阻塞的,但是其与以上两种还是有不同的,I/O多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上(所以两个系统调用,花费也更大,IO多路复用的性能是有可能比BIO还差的,但其优势在于处理多个连接,参考C10k问题)。
图中显示的系统调用是select,这就不得不引入到我们经典的几个系统调用(select、poll、epoll),这是需要单独的一篇文章来介绍三种调用的不同之处,当然它们的思想都是一致的。
深入理解select、epoll 当然最好还是看Linux内核书籍
注意:当selector轮询到某一个socket可读时,它返回,然后用户进程执行系统调用recvfrom,这个时候就又回到了我们的前两种I/O模型,此时,往往都采用的是NIO方式,因为尽管select返回了,告知用户进程某个socket可读,但并不告诉我们能读的数据有多少,甚至并不意味着我们接着调用recvfrom就能读到数据,所以I/O多路复用一般得搭配NIO。参考:

为什么 IO 多路复用要搭配非阻塞 IO?

使用epoll时需要将socket设为非阻塞吗?

AttrJH.png

4、信号驱动模型

摘抄自《权威指南书》,首先开启套接口的信号驱动功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,用户进程继续工作);当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用进程调用recvfrom接口来读取数据。 AwYmfU.png

5、异步I/O模型

见识一下什么叫真正的异步I/O模型,直接告知内核开启某个操作,并让内核完成整个流程后(包括将数据准备好——拷贝到内核缓冲区,然后将数据从内和缓冲区复制到用户进程缓冲区)。信号驱动模型由内核通知我们什么时候可以开启一个I/O操作,而异步I/O模型直接告诉我们I/O操作已经完成了。 推荐书:Unix网络编程 AwYa1e.png

这就是传说中的五种Linux I/O模型,下一篇文章我们详细分析一下I/O多路复用模型,重点关注epoll。