题目看上去真的很乱,写之前先贴一张写之前理出来的思路图:

java nio 是使用 epoll_用户态

题目里那些乱七八糟的名词都能在图里找到自己的位置了。 下面就是解释一下这张图了。

从IO开始讲吧,先简单解释一下IO。其实IO这个概念真的很杂容易混淆,我理解的IO是分两个大概念的,即网络IO和磁盘IO。网络IO就是socket网络数据传输,磁盘IO就是磁盘文件读写这些。上图中的IO理论思想在网络IO和磁盘IO中都可以适用的,因为这两个有很多共同之处。

为什么有共同之处呢?可以这么理解。磁盘IO就是文件读写嘛,文件是在磁盘上的要读到内存中,必然会涉及用户态和内核态,这里就不解释用户态和内核态了,理解起来就是用户态问内核态要数据,那用户态就是调用方,内核态就是被调用方。而网络IO也很相似啊,网络IO最简单的理解就是一个接口被调用了,总之会有一个socket连接过来,让被调用的服务去干嘛干嘛,就可以理解为这个socket连接是调用方,被调用的服务当然就是被调用方了。画了一张图总结一下:

java nio 是使用 epoll_用户态_02

我们所说的IO通常意义上是指磁盘IO的输入/输出流,即I/O流。其实在磁盘IO体系中不止有流式部分,还有非流式部分,贴两张网上的图:

java nio 是使用 epoll_java nio 是使用 epoll_03

java nio 是使用 epoll_数据_04

 

所以,这么一顿分析下来,一般我们打交道比较多的就是磁盘IO流式部分了,而这么长的名字平时我们只说IO,有点搞混的。当然网络IO和磁盘IO的区分界限也没有那么清楚,比如字节流字符流用于文件读写没有问题,但是网络IO中也避免不了使用字节流啊,这就看个人理解了。

上面是理了一下IO,接下来解释一下同步/异步和阻塞/非阻塞。

还是看上面调用方和被调用方那张图,记住关键的一点,同步/异步是描述调用方是否等待调用结果返回的,阻塞/非阻塞是描述被调用方线程状态的。

拿磁盘IO举例:

同步阻塞IO就是用户态的线程发起read()/write()调用后,会一直等待调用结果返回,而在内核态,要去读一个磁盘文件,数据不一定立马就能准备好啊,同步阻塞IO就是内核态的线程会一直能数据准备好再去读。盗张图贴一下:

java nio 是使用 epoll_java nio 是使用 epoll_05

同步非阻塞IO同理用户态发起read()/write()调用后,也会一直等待调用结果返回,但是在内核态,如果数据没有准备好,内核态的线程可不会等着,就直接干别的去了,那就需要用户态的线程一直去询问数据有没有处理好啊,一直问一直问这个就叫轮询。再贴一张图:

java nio 是使用 epoll_java nio 是使用 epoll_06

 那什么是IO多路复用呢?可以看出同步非阻塞IO是需要用户态要有一个线程不停地去轮询的,这就很消耗CPU资源啊,用户态就很不乐意了,就在想能不能这个轮询的让内核态自己去做就好了,这就是IO多路复用了。所以IO多路复用是在同步非阻塞IO基础上的一次演进,即IO多路复用也必然是同步非阻塞IO。这个轮询的活是就是select、poll和或者epoll这三个机制来完成的,也就是select、poll和或者epoll这三个是内核态的系统调用,三种不同的方式去找到准备好的数据。需要注意的是select和poll是轮询的方式去找到准备好的数据,epoll已经不用轮询了,这个后面会说。既然都是在内核态找到准备好的数据了,更接近OS底层的调用性能也必然是比在用户态的时候好,既然这样,那何必只去找用户态某一个线程需要的数据呢,那就替用户态所有干等着的线程找准备好的数据吧,这就是多路复用这个词的来源。

上面是在磁盘IO理解了同步阻塞IO、同步非阻塞IO和多路复用IO,那么在网络IO怎么理解呢?

对于网络IO,同步阻塞IO不用说了,一个socket连接过来了,里面的数据并不一定准备好了,如果被调用服务的线程在干等着socket中的数据准备好,那就是同步阻塞IO。如果这个服务线程不等呢?而是去询问其他socket连接中的数据有没有准备好,那这就是同步非阻塞IO。

网络IO和磁盘IO的同步非阻塞IO的理解还是有点区别的,磁盘IO的同步非阻塞的轮询操作是由调用方去做的,而且轮询的是被调用方,但是网络IO的同步非阻塞总不能让调用方去轮询吧,难不成还一遍一遍的socket连接过来?所以只能被调用方去轮询,而且轮询的是调用方即一组socket连接,所以网络IO的同步非阻塞IO也可以成为是IO多路复用,即是被调用方在轮询又有多路复用这个概念在里面啊。

JAVA NIO是NIO思想在JAVA领域的实现,所以很多人说JAVA NIO是多路复用IO也没什么问题。既然是NIO思想在JAVA领域的实现,必然在网络IO和磁盘IO都是可用的。在网络IO应用的关键词就是Selector,即一组socket连接注册到上面。在磁盘IO的应用关键字就是FileChannel文件通道和Buffer缓冲区,也是用了select、poll或者epoll这一套。

理清了上面这些概念,然后就可以了解一下select、poll和epoll。

1.select

select函数就是上面说的,做了遍历轮询的活,不止替一个用户态线程找准备好的数据,而是把很多个文件描述符fd放进一个set集合中遍历,就是替多个用户态线程找准备好的数据。当然,既然是set集合那就有数量上限,32位机器上默认是1024个,64位机器上默认是2048个。贴一张图:

java nio 是使用 epoll_用户态_07

时间复杂度:O(n)

select的缺点:

(1)单进程可以打开fd有限制;

(2)对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低;

(3)用户空间和内核空间的复制非常消耗资源;

2.poll

其实poll调用过程和select一样,只不过采用链表的方式替换select的set集合去存储fd,这样连接数就没有限制了。时间复杂度同样为O(n)。

3.epoll

应用示例:Nginx。

epoll就不像前面两个把fd放进一个集合里去遍历了,而是采用注册回调函数,在文件描述就绪的时候网卡驱动会去触发这个回调函数,通知说这个fd的数据已经准备好了,这就很nice了。贴一下图:

java nio 是使用 epoll_数据_08

这样就既没有连接数限制,又不用去遍历轮询消耗CPU。而且时间复杂度为O(1)。 

epoll有两种工作方式:1.水平触发(LT)2.边缘触发(ET) 
LT模式:若就绪的事件一次没有处理完要做的事件,就会一直去处理。即就会将没有处理完的事件继续放回到就绪队列之中(即那个内核中的链表),一直进行处理。 
ET模式:就绪的事件只能处理一次,若没有处理完会在下次的其它事件就绪时再进行处理。而若以后再也没有就绪的事件,那么剩余的那部分数据也会随之而丢失。 
由此可见:ET模式的效率比LT模式的效率要高很多。只是如果使用ET模式,就要保证每次进行数据处理时,要将其处理完,不能造成数据丢失,这样对编写代码的人要求就比较高。 
需要注意的是,ET模式只支持非阻塞的读写:为了保证数据的完整性。

到这里IO的同步模型就梳理得差不多了,还有一个异步模型。

按刚刚的理解,异步模型是需要调用方发起调用动作后就不等了,去干别的事。而我们平常的编程,代码是一行一行写下来,运行的时候也是一个线程一行一行的执行下来,这行的结果没出来呢线程也不会去干别的事,所以我们日常的编程都是同步编程,是做不到真正的异步IO的。顾名思义异步IO需要特殊的异步编程语法,现在有的就是协程,这已经涉及我的知识盲区了,就不继续写了。