上一篇我们讲到了关于TCP/IP协议的一些内容,这些是网络编程的必备知识。在了解NIO之前我们必须要了解一下对应的系统层IO模型,比如java的NIO对应是那种IO模型,阻塞和同步的差异在哪里,又是否相同。了解了这些更方便我们的后续的NIO探解。
一、同步、异步、阻塞、非阻塞
同步、异步,阻塞、非阻塞,这四种状态常有人分不清,主要是这四种状态的定义本身也不是很明确,所以各种解答的方式都有。常见的分类有以下:
同步阻塞IO
同步非阻塞IO
异步非阻塞IO
针对某种IO模型,我们如何分类,可以基于POSIX对同步/异步的定义来判别:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;
那么从上我们可以看出:
阻塞:是否阻塞主要体现在调用的线程是否可以干别的,关注的是程序的等待状态
同步:是否同步体现在消息通信机制上 。
也就是说同步和异步说的是消息的通知机制,阻塞非阻塞说的是线程的状态 。
如果说以上的定义依然无法判别,我们可以从输入操作的两个阶段来看:
一般来说,一个输入操作通常包括两个不同阶段:
(1)等待数据准备好;(2)从内核向进程复制数据。
是否同步的判断依据是:是否针对的是整个过程,也就是2个阶段,是否有阻塞。
是否阻塞的判断依据是:按程序(线程)等待消息通知时的状态角度来说的,也就是主要是针对第一阶段来说。
举例
我们举例来说:
比如说做饭这件事,一般要分为连个步骤。
1、买菜,准备食材
2、炒菜,做出饭菜
方案一:自己动手处理。
1、去超市买菜,准备食材(阻塞,当前时段只能做一件事,且需要持续的等待)
2、回家切菜,炒菜,做出美味饭菜(阻塞,还是自己来处理)
评价: 方案一同步阻塞。首先阶段一是阻塞的,所以认定为阻塞,两个阶段都是阻塞的,认定为同步的。
方案二:盒马配送食材,自己做饭
1、网上下单,盒马配送食材,快递到了会敲门联系你。(非阻塞的,这期间你可以干其他事)
2、拿到菜,切菜、炒菜,做出美味饭菜(阻塞)
评价:方案二为同步非阻塞。阶段一为非阻塞,认定为非阻塞。阶段二为阻塞,两阶段中有一个为阻塞,认定为同步。
方案三:盒马配送,请阿姨做饭
1、网上下单,盒马配送食材,快递到了会敲门联系你。(非阻塞的,这期间你可以干其他事)
2、网上请阿姨小时工,帮忙做这一餐,做好通知我。(非阻塞,期间可以干其他事)
评价:方案三为异步非阻塞。阶段一为非阻塞,认定为非阻塞。阶段二非阻塞,则两阶段中都没有阻塞,认定为异步。
那么是否有异步阻塞IO模型,没有,要记得异步状态是包含二个阶段的,如果有阻塞的过程,为何还叫异步?
网上有很多介绍有异步阻塞模型的,我目前查到的资料还没有这个证明,若有找到相关论文,还请指教。目前我认为没有这个模型的。
二、Unix 5种I/O模型
在《UNIX网络编程:卷一》的第六章书中列出了五种IO模型:
-
阻塞式I/O;
-
非阻塞式I/O;
-
I/O复用(select,poll,epoll...);
-
信号驱动式I/O(SIGIO);
-
异步I/O(POSIX的aio_系列函数);
1.阻塞式I/O
同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在linux中,默认情况下所有的socket都是blocking。它符合人们最常见的思考逻辑。
在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,等待kernel准备好从网络上接收到的数据报 + 等待收到的报文被从kernel复制到buf中,recvfrom方法才会返回,最后进程再处理数据。
这就是阻塞式IO模型
打开UC浏览器 查看更多精彩图片
2.非阻塞式I/O
非阻塞IO时对一个非阻塞描述符循环调用recvfrom,持续的轮询(polling),以查看某个操作是否就绪。与阻塞IO不一样,"非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 '被' CPU光顾"。
非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。如此循环的进行recvform系统调用,检查内核数据,直到数据准备好,再拷贝数据到进程。拷贝数据整个过程,进程仍然是属于阻塞的状态。
这就是非阻塞式IO模型
打开UC浏览器 查看更多精彩图片
3.I/O复用
IO multiplexing就是我们说的select,poll,epoll 。为何叫多路复用,是因为它I/O多路复用可以同时监听多个fd,如此就减少了为每个需要监听的fd开启线程的开销。
select调用是内核级别的,可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,这个过程是阻塞的。
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这几个函数可以同时阻塞多个I/O操作`。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理),才真正调用I/O操作函数。
IO复用有人把其称为同步非阻塞的,也有称为同步阻塞。其实这个是否阻塞还需要看第一个阶段,第一个阶段有的阻塞,有的不阻塞。主要也是阻塞在select阶段,属于用户主动等待阶段,我们且规范为阻塞状态,所以,把IO多路复用归为同步阻塞模式。
这是IO复用的模型:
打开UC浏览器 查看更多精彩图片
select、poll、epoll的不同
打开UC浏览器 查看更多精彩图片
4.信号驱动式I/O
信号驱动式I/O:首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
也就是说第一个阶段,完全是非阻塞的,等数据到达会给一个信号通知,第二个阶段recvfrom还是阻塞过程,和之上无差异。
信号驱动式I/O 过程如下:
打开UC浏览器 查看更多精彩图片
5.异步I/O
异步IO不是顺序执行,用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
打开UC浏览器 查看更多精彩图片
总结
针对这5种IO模型,我采用一张图来总结一下。
打开UC浏览器 查看更多精彩图片
三、java IO
Unix中的五种I/O模型,除信号驱动I/O外,Java对其它四种I/O模型都有所支持。其中Java最早提供的blocking I/O即是同步阻塞I/O,而NIO即是同步非阻塞I/O,同时通过NIO实现的Reactor模式即是I/O复用模型的实现,通过AIO实现的Proactor模式即是异步I/O模型的实现。
所以说严格意义上来说,通过Reactor模式实现的NIO,和unix中的I/O多路复用是相同的概念,但这是一种编程模型,而不是原生支持。这也是我们下面所要进行的netty讲解的主要思想。