四十六、NIO详解

本文你将看到如下内容:

  1. 最好的学习就是教别人。

  2. NIO的用途。

  3. 现代操作系统的5种IO模型。

  4. JavaNIO 的实现及简单的服务器例子。

  5. NodeJs的IO模型。

  6. 计算机IO的硬件基础。

  • 最好的学习就是教别人:

‍首先感谢CDRD给了我这一次分享的机会,为什么要将感谢放在最开头。因为这次我选择java nio 这个主题是因为我自己不理解Java nio的内部实现原理,但是又想学。”最好的学习方式就是教别人”,所以建议大家以后想要学什么东西,可以尝试搞清楚以后,与大家分享。

  • 带着功利心开始我们的学习:谈谈NIO的用途

当今时代是一个知识信息爆炸的时代,知识的量几乎是无限的,如果我们不带着功利心去学习知识,那么提升认知的效率将大幅度降低。那么学习Java NIO能有什么用呢?

答案是:帮助我们提高IO密集型应用的单机并发量。

何为IO密集型应用?何为CPU密集型应用,这个问题留给大家一起思考。在我们所用到的,或者知道的应用中,有哪些是IO密集型的,又有哪些是CPU密集型的?

为什么Java NIO能够提升IO密集型应用的单击并发量呢?这要从操作系统的IO模型说起。

  • 现代操作系统的IO模型:

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

1. 等待数据准备(Waiting for the data to be ready)

2. 将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。

- 阻塞I/O(blocking IO)

- 非阻塞I/O(nonblocking IO)

- I/O 多路复用(IO multiplexing)

- 信号驱动I/O(signal driven IO)

- 异步I/O(asynchronous IO)

1、阻塞I/Oblocking IO

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

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

2、非阻塞I/Ononblocking IO

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的systemcall,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

3I/O 多路复用(IO multiplexing

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个systemcall (select 和recvfrom),而blocking IO只调用了一个systemcall (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading+ blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

4、异步I/Oasynchronous IO

Linux下的asynchronous IO其实用得很少。先看一下它的流程:

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

5、各种IO模型之间的对比

注:由于signaldriven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。

POSIX对于阻塞IO和非阻塞IO的定义:

同步IO操作让IO请求线程阻塞,直到IO完成,对应图中的:Bio、non-blocking IO、multiplexing IO。

异步IO操作直到IO过程完成都不会让IO过程阻塞,对应图中的Aio。

  • Java NIO的底层实现。

让我们来看一看NIO中的类:

FIleChanel: 用于处理文件的读写。

ServerSocketChannel:用于处理服务器端TCP连接的接受或拒绝。

SocketChannel:用于处理服务器端的

DatagramChannel:用于处理UDP报文的收发。

NIO调用操作系统的接口是一个典型的IO多路复用函数select,它的调用过程如下:

由于篇幅有限,这里暂时不讨论select / poll / epoll的差别,大家知道三者的执行效率:

epoll > poll > select 就行了。

  • NodeJs的IO模型。

让我们换一个角度看待这个问题,还有什么底层的IO模型,跟java Nio是类似的呢,答案是java。

Java的执行环境为什么是单线程的呢?

Js最初设计是运行在浏览器中的,而浏览器中的每一个window只会为js开一个线程来运行,浏览器是事件驱动的,所有的操作都被加入事件队列,由一个线程专门轮训处理。在js中,没有类似于java一样的sleep函数。所有的操作都是异步的,包括IO操作在内。

由此诞生了鼎鼎大名的nodeJs,用nodeJs写的服务器,所有的IO请求都由底层的EventLoop代为处理。

用这种方式写的web服务器跟我们平时用得多的tomcat服务器的区别在于,这种服务器的IO请求都交给event loop进行处理,而tomcat是处理用户请求的线程在线程池里面等,这样的话,由于cpu在线程之间来回切换会有开销,所以对于IO密集型的应用来说,以IO复用,模型为基础的服务器的单机并发量要高一些。

废话少说,上一组测试数据:

(数据来源:https://dzone.com/articles/performance-comparison-between)

Node

JavaEE

第一列代表并发数,第二列代表返回时间,第三列代表每秒处理请求的数量,可以看出node服务器的并发处理速度大约比java快了20%。

  • IO的硬件基础。

让我们一起再往底层看一看,现代计算机的一次IO过程低如何完成的。

现代计算机的IO过程如图:

1.操作系统下发一个IO请求。

2.DMA芯片向CPU发送一个接管总线的请求,总线一旦被DMA占据,CPU和其他设备都不能再占有总线去做其他事。

3.CPU将总线控制权交给DMA芯片。

4.DMA拿到总线控制权以后,告诉外部缓存设备可以开始传送数据。

5.IO设备缓存将数据拷贝到内存中。

可以看出,整个IO过程主要是由DMA来做的,这样的目的是为了不占据CPU的时间片,降低CPU的开销。

事实上,我们在select函数中获得的通知,正是在DMA将数据拷贝的操作系统kernel里面后,触发的一个中断。告诉我们说IO传输已经完成,之后再由操作系统来将数据拷贝到用户空间(AIO),或者通知由用户自取(BIO,IO复用)。

总结:

这一次我们从多个角度看到了现代计算机IO发生的过程。回顾一下:

1.计算机的IO是发生的两个过程是什么呢?

2.由这两个过程演化出来的4种IO模型是哪4种呢?

3.IO多路复用的优势在哪儿呢?

4.将单击IO的情况迁移到分布式场景里面,或者数据库IO里面,你能想出有哪些类似的场景呢?

猜你喜欢

转载自blog.csdn.net/u010285974/article/details/84666599