NIO(一)——阻塞/非阻塞/同步/异步/NIO/select/epoll基本概念

下面是我对NIO的一些想法,难免会与有理解上的偏差,因此还望各位大神能指点一二,感激不尽!

IO一般指的是内核与外部进行数据交互的所发生的事情。典型分为网络IO(socket),磁盘IO(文件),管道IO(pipe)等几类。我们将主要介绍网络IO这一部分。
一, 阻塞/非阻塞/同步/异步
谈及网络IO,都会谈到阻塞,非阻塞,同步,异步这几个概念。我们先从IO的过程讲起。
IO主要分两步:1)数据准备阶段;2)数据操作阶段(或称为IO操作阶段)。这里面涉及两个系统对象,一个是调用该IO的进程(或线程),另一个是系统内核。举例讲read()函数分两步,第一步等待数据准备好,第二步将数据从内核拷贝到用户进程。这两个步骤非常重要,因为所谓的阻塞,非阻塞,同步,异步都是针对这两个步骤来说的。
先上一张图,这张图是讲的read这个调用所可能的情况。
NIO(一)——阻塞/非阻塞/同步/异步/NIO/select/epoll基本概念

详细分析见
http://blog.****.net/historyasamirror/article/details/5778378
下面直接粗暴地给出结论:
1)阻塞、非阻塞的概念只是针对第一步数据准备阶段而言的。如果等待到数据准备好再返回,就是阻塞;如果不管数据有没有准备好都立即返回,那就是非阻塞。(第二步的数据拷贝过程一定是阻塞的,但是我们不管他)
2)同步、异步的概念是指在I/O整个阶段,有没有另起一个线程/进程。
因此按照这两个概念。只有同步阻塞、同步非阻塞、异步非阻塞。没有异步阻塞的概念,因为异步的话,一定是另外的一个线程/进程来完成这两个阶段的,那个原本的调用线程是在I/O调用后就立刻返回的,另起的那个线程在第1步可能会阻塞,在第2步一定会阻塞,因此总体看,这个另起的线程一定会有阻塞的情况。也就是说原来的线程不存在阻塞,另起的那个线程一定会阻塞。
3)第一步等待就绪的阻塞是不消耗CPU的;而真正的读写操作的阻塞是消耗CPU的,但是这个过程非常快,性能超高,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

二,BIO,NIO,AIO
以socket.read()为例
BIO。核心是是一个socket对应一个线程,如果TCP RecvBuffer里没有数据,会一直阻塞(第一阶段的阻塞),直到收到数据,返回读到的数据。
NIO。如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞(第一阶段不会阻塞)。
AIO(Async I/O)。不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。

三, NIO
NIO是同步非阻塞。同步是说没有把等待数据就绪和拷贝数据这两个过程都完全交给另外的线程,非阻塞是说等待数据就绪这个过程是非阻塞的。
NIO对于数据是否准备好是非阻塞的,即无论数据有没有就绪,都会第一时间返回。
NIO主要有以下几个事件:read(读就绪),write(写就绪),connect(失败重连就绪),accept(新连接就绪)。首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。
在实际操作上,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。
注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。

四, NIO的主要的两种形式:select和epoll
先说二者的主要过程
NIO(一)——阻塞/非阻塞/同步/异步/NIO/select/epoll基本概念

两者总结:
1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数(回调函数的意思就是调用内核系统函数,目的是通知内核发生了什么事件,在这里是告诉内核,“我”这个fd已经就绪了。因此epoll是fd主动通知的内核,而select是内核亲自去检测fd集合),把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。(不很理解)

select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024

epoll优点:
1) epoll没有句柄数目的限制。
2) epoll不会随着fd的增长而效率低下。前面说了,select检查的是fd_set的全量,因此fd越多,则轮询一遍检查的时间越长,效率越低;epoll是就绪的fd主动通知内核,内核将其记下来就行了,它是记的增量。
3) 使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。(不很理解)

如果不理解fd,请各位自行查阅资料。

参考:
http://blog.****.net/historyasamirror/article/details/4270633
https://tech.meituan.com/nio.html
http://blog.****.net/ysu108/article/details/7570571