Skip to content

NIO入门笔记

一些术语:

poll:轮询

epoll:多路复用

阻塞:一直等待任务,有任务到来会被唤醒;

非阻塞忙轮询:一遍又一遍地询问有没有任务

IO阻塞:

(1)read:如果内核态缓冲区没有数据,那么将阻塞,如果内核态缓冲区从没有数据到有数据,就会唤醒阻塞的read线程;

(2)write:如果内核态缓冲区满了,无法在写入,此时将阻塞,如果内核态缓冲区从满状态到非满状态,就会唤醒阻塞的write线程。

IO模型:

(1)阻塞IO模型:这个模型有一个明显的缺点,就是一个线程只能处理一个IO操作,如果需要处理多个IO操作,就需要创建多个线程或者进程,这显然不可取;

(2)非阻塞忙轮询IO模型:轮询所有的流,如果可读取或写入,则执行,否则处理下一个流。这个模型有两个缺点:1.在所有流都不可读取和写入的时候,会造成CPU空转,浪费系统资源;2.不能执行处理可读取或写入的流,需要遍历所有的流并进行判断是否可读取或写入,再进行处理;

(3)select模型:对于上述的非阻塞忙轮询IO模型的CPU空转的缺点进行补充。

在所有流都不可读取和写入的时候,CPU会空转,那么引入一个select代理,用于检测所有流是否可读取和可写入,如果所有流都不可读取和写入,那么这个线程将阻塞,如果至少有一个流可读取或写入,那么就会将阻塞的线程唤醒。其实select模型下,维护一个fd_set数据结构(使用的是Bitmap位图算法),每一个元素与流关联,每次调用select()方法时,需要将这个数据结构拷贝到内核态,然后内核判断哪些流可读或可写,并写入到这个数据结构然后拷贝到用户态。因此 1.如果数据结构太大,在用户态和内核态之间拷贝会很耗性能;2.在内核态中,每次都要遍历这个数据结构的所有元素,比较耗性能;3.为了减少拷贝对性能的损害,内核对这个集合的大小做了限制(1024,大小不可修改);

(4)poll模型:这个模型跟select模型类似,只是没有对fd_set数据结构集合的大小做限制,因此只解决了select模型的第三个缺点,第一、二个缺点依然存在;

(5)epoll模型:该模型基于事件驱动的方式,使用一个文件描述符来管理多个描述符,描述符的数量没有限制。每当fd就绪,就会调用系统注册的回调函数,将fd加入到readyList中。进行IO操作的时候,只需要遍历这个readyList,而不需要遍历所有的fd。因此解决了CPU空转、遍历所有fd、大集合在用户态和内核态之间拷贝的性能消耗的缺点。

select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 红黑树
IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
最大连接数 1024(x86)或2048(x64) 无上限 无上限
fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

epoll 是Linux目前大规模网络并发程序开发的首选模型

1、基本概念

一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器 selector 上,多路复用 器轮询到连接有 IO 请求就进行处理,JDK1.4 开始引入。

应用场景:

适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯

NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)

1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组

2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理

3、NIO 的 Buffer 和 channel 都是既可以读也可以写