# NIO 存在的问题
在 NIO 中,若 Selector 的轮询结果为空,也没有 wakeup()
或新消息处理,会发生空轮询导致 CPU 占用 100%。
前置知识:
JDK1.5 引入 epoll
机制。 epoll
是 linux2.6 内核的系统调用,设计目的就是替代 select、poll 线性复杂度的模型, epoll
的时间复杂度是 O (1)。 epoll
在高并发场景表现优秀。
文件描述符 (File Descriptor——fd):linux 内核利用文件描述符来访问文件,打开显存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
文件句柄:Windows 下的概念,句柄是各种对象的标识符,它是一个非负整数,也用于定位文件数据在内存中的位置。
linux 下的文件描述符其实就相当于 windows 下的句柄。文件句柄只是 windows 众多句柄中的一种类型而已。
对于操作系统会有这样一个需求:如何在一个进程(线程)中处理多个文件,或者监听多个文件的 IO 事件。
select
、 poll
和 epoll
都是操作系统中实现 IO 多路复用的方法。
# select
select
方法本质就是维护了一个文件描述符数组(32 位系统 MAX=1024,64 位 MAX=2048),依次实现 IO 多路复用,select 会监视文件描述符的变化。
select()
机制中提供了 fd_set
数据结构,实际上是一个 long 类型的数组,每个数组元素都能与以打开的文件描述符建立联系。
/proc/sys/fs/file-max
中指定了系统范围内所有进程可打开的文件的数量限制
当 select
方法被调用,首先需要将 fd_set
从用户空间拷贝到内核空间,然后内核用 poll
机制(非多路复用的 poll)返回一个 fd 准备就绪个数。方法返回后需要轮询 fd_set
,检查发生 IO 事件的 fd。
缺陷:
- 使用轮询,效率低。
- 会导致用户空间和内核空间频繁拷贝数据。
# poll
poll
和 select
很类似,只不过 poll
维护的是一个链表,单个进程监听的 fd 不再有数量限制,但是和 select
相同的轮询和复制问题依然存在。
# epoll
epoll 全称 EventPoll,是 linux 内核实现 IO 多路复用的模型。
在 Linux 中, selector
和 poll
监听文件描述符 list,进行线性的查找,复杂度 O (n)。
epoll
使用了内核文件级别的回调机制,复杂度 O (1)。
epoll 基于事件驱动,给每个 fd 注册一个回调函数,当对应的 fd 有 IO 事件发生时就调用这个回调函数,将这个 fd 放入一个链表中,这样客户端可以直接从链表中获得发生 IO 事件的 fd,从而达到 O (1) 级别的监听。
epoll 有的三个系统调用:epoll_create, epoll_ctl, epoll_wait
# JDK 中 epoll 的实现
理论上无客户端连接时 Selector.select()
方法会阻塞,但空轮询 bug 导致:即使无客户端连接,NIO 照样不断的从 select 本应该阻塞的 Selector.select()
中 wake up 出来,导致 CPU100% 问题。