写在前面
在了解了 Java BIO (blocking I/O) , UNIX I/O 模型后再对 Java NIO 进行学习,个人感觉这种徐徐渐进的学习方式更适合自己。在深入细节之前要尽可能的从大局角度进行了解,在这时候多花些时间是值得的,我觉得这样会很大程度上提升之后的学习效率。在对 Java I/O 有一个初步系统的理解(理解 Java I/O)前提下, 再延伸到 Java NIO , 对于 NIO 首先有这几点疑惑:
- Java NIO 采用哪种 I/O 模型?
- Java NIO 是如何工作的?
之后的内容也专注于对这几点疑问答案的寻找。
NIO 采用的 I/O 模型
UNIX I/O 模型有 : 阻塞 I/O (blocking I/O) , 非阻塞 I/O (non-blocking I/O) , 多路复用 I/O (multiplexing I/O) , 信号驱动 I/O (signal-driven I/O) , 异步 I/O (asynchronous I/O) 这5种,Java NIO 究竟是用的是那种模型?我在网上查了一下也没有得到确切的答案。在 JDK 源码中找到了答案。确定 Java NIO 使用的是 多路复用 I/O 模型。
NIO 的核心类之一是 java.nio.channels.Selector ,通过静态方法 open() 可以获取到 Selector 的实例。JDK 文档中的描述是 "可以通过调用此类的open()方法来创建选择器,该方法将使用操作系统默认值selector provider 创建一个新的选择器。还可以通过调用自定义选择器提供程序(也就是自定义的 selector provider )的 openSelector()方法来创建选择器。"
Selector open 函数源码 :
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
SelectorProvider 源码 :
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
// 通过系统属性实例化 java.nio.channels.spi.SelectorProvider 对象
if (loadProviderFromProperty())
return provider;
// 通过 SPI 机制实例化 java.nio.channels.spi.SelectorProvider 对象
if (loadProviderAsService())
return provider;
// 前两种方式没有提供使用系统默认的 java.nio.channels.spi.SelectorProvider 实例
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
// 通过系统属性实例化 java.nio.channels.spi.SelectorProvider 对象
private static boolean loadProviderFromProperty() {
String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
if (cn == null)
return false;
try {
Class<?> c = Class.forName(cn, true,
ClassLoader.getSystemClassLoader());
provider = (SelectorProvider)c.newInstance();
return true;
} catch (ClassNotFoundException x) {
throw new ServiceConfigurationError(null, x);
} catch (IllegalAccessException x) {
throw new ServiceConfigurationError(null, x);
} catch (InstantiationException x) {
throw new ServiceConfigurationError(null, x);
} catch (SecurityException x) {
throw new ServiceConfigurationError(null, x);
}
}
// 通过 SPI 机制实例化 java.nio.channels.spi.SelectorProvider 对象
private static boolean loadProviderAsService() {
ServiceLoader<SelectorProvider> sl =
ServiceLoader.load(SelectorProvider.class,
ClassLoader.getSystemClassLoader());
Iterator<SelectorProvider> i = sl.iterator();
for (;;) {
try {
if (!i.hasNext())
return false;
provider = i.next();
return true;
} catch (ServiceConfigurationError sce) {
if (sce.getCause() instanceof SecurityException) {
// Ignore the security exception, try the next provider
continue;
}
throw sce;
}
}
}
DefaultSelectorProvider (这里使用的是针对Windows系统的 JDK) 反编译得到的源码 :
package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
public class DefaultSelectorProvider {
private DefaultSelectorProvider() {
}
public static SelectorProvider create() {
return new WindowsSelectorProvider();
}
}
WindowsSelectorProvider (这里使用的是针对Windows系统的 JDK) 反编译得到的源码 :
package sun.nio.ch;
import java.io.IOException;
import java.nio.channels.spi.AbstractSelector;
public class WindowsSelectorProvider extends SelectorProviderImpl {
public WindowsSelectorProvider() {
}
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
}
WindowsSelectorImpl (这里使用的是针对Windows系统的 JDK) 反编译得到的源码 :
protected int doSelect(long var1) throws IOException {
if (this.channelArray == null) {
throw new ClosedSelectorException();
} else {
this.timeout = var1;
this.processDeregisterQueue();
if (this.interruptTriggered) {
this.resetWakeupSocket();
return 0;
} else {
this.adjustThreadsCount();
this.finishLock.reset();
this.startLock.startThreads();
try {
this.begin();
try {
this.subSelector.poll();
} catch (IOException var7) {
this.finishLock.setException(var7);
}
if (this.threads.size() > 0) {
this.finishLock.waitForHelperThreads();
}
} finally {
this.end();
}
this.finishLock.checkForException();
this.processDeregisterQueue();
int var3 = this.updateSelectedKeys();
this.resetWakeupSocket();
return var3;
}
}
}
private final class SubSelector {
// ...
private int poll() throws IOException {
return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress, Math.min(WindowsSelectorImpl.this.totalChannels, 1024), this.readFds, this.writeFds, this.exceptFds, WindowsSelectorImpl.this.timeout);
}
private int poll(int var1) throws IOException {
return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress + (long)
(this.pollArrayIndex * PollArrayWrapper.SIZE_POLLFD), Math.min(1024,
WindowsSelectorImpl.this.totalChannels - (var1 + 1) * 1024), this.readFds, this.writeFds,
this.exceptFds, WindowsSelectorImpl.this.timeout);
}
// 调用了操作系统
private native int poll0(long var1, int var3, int[] var4, int[] var5, int[] var6, long
var7);
}
DefaultSelectorProvider (这里使用的是针对 Linux 系统的 JDK) 反编译得到的源码 :
package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
import java.security.AccessController;
import sun.security.action.GetPropertyAction;
public class DefaultSelectorProvider {
private DefaultSelectorProvider() {
}
private static SelectorProvider createProvider(String var0) {
Class var1;
try {
var1 = Class.forName(var0);
} catch (ClassNotFoundException var4) {
throw new AssertionError(var4);
}
try {
return (SelectorProvider)var1.newInstance();
} catch (InstantiationException | IllegalAccessException var3) {
throw new AssertionError(var3);
}
}
// 根据不同的操作系统获取不同的 java.nio.channels.spi.SelectorProvider 实例对象
public static SelectorProvider create() {
String var0 = (String)AccessController.doPrivileged(new GetPropertyAction("os.name"));
if (var0.equals("SunOS")) {
return createProvider("sun.nio.ch.DevPollSelectorProvider");
} else {
return (SelectorProvider)(var0.equals("Linux") ? createProvider("sun.nio.ch.EPollSelectorProvider") : new PollSelectorProvider());
}
}
}
EPollSelectorProvider (这里使用的是针对 Linux 系统的 JDK) 反编译得到的源码 :
package sun.nio.ch;
import java.io.IOException;
import java.nio.channels.Channel;
import java.nio.channels.spi.AbstractSelector;
public class EPollSelectorProvider extends SelectorProviderImpl {
public EPollSelectorProvider() {
}
public AbstractSelector openSelector() throws IOException {
return new EPollSelectorImpl(this);
}
public Channel inheritedChannel() throws IOException {
return InheritedChannel.getChannel();
}
}
EPollSelectorImpl (这里使用的是针对 Linux 系统的 JDK) 反编译得到的源码 :
protected int doSelect(long var1) throws IOException {
if (this.closed) {
throw new ClosedSelectorException();
} else {
this.processDeregisterQueue();
try {
this.begin();
this.pollWrapper.poll(var1);
} finally {
this.end();
}
this.processDeregisterQueue();
int var3 = this.updateSelectedKeys();
if (this.pollWrapper.interrupted()) {
this.pollWrapper.putEventOps(this.pollWrapper.interruptedIndex(), 0);
Object var4 = this.interruptLock;
synchronized(this.interruptLock) {
this.pollWrapper.clearInterrupted();
IOUtil.drain(this.fd0);
this.interruptTriggered = false;
}
}
return var3;
}
}
EPollArrayWrapper (这里使用的是针对 Linux 系统的 JDK) 反编译得到的源码 :
int poll(long var1) throws IOException {
this.updateRegistrations();
this.updated = this.epollWait(this.pollArrayAddress, NUM_EPOLLEVENTS, var1, this.epfd);
for(int var3 = 0; var3 < this.updated; ++var3) {
if (this.getDescriptor(var3) == this.incomingInterruptFD) {
this.interruptedIndex = var3;
this.interrupted = true;
break;
}
}
return this.updated;
}
private native int epollCreate();
private native void epollCtl(int var1, int var2, int var3, int var4);
private native int epollWait(long var1, int var3, long var4, int var6) throws IOException;
private static native int sizeofEPollEvent();
private static native int offsetofData();
private static native void interrupt(int var0);
private static native void init();
static {
FD_OFFSET = DATA_OFFSET;
OPEN_MAX = IOUtil.fdLimit();
NUM_EPOLLEVENTS = Math.min(OPEN_MAX, 8192);
MAX_UPDATE_ARRAY_SIZE = ((Integer)AccessController.doPrivileged(new GetIntegerAction("sun.nio.ch.maxUpdateArraySize", Math.min(OPEN_MAX, 65536)))).intValue();
IOUtil.load();
init();
}
在对比了 Windows 平台下和 Linux 平台下 JDK 的代码后, 现在就可以确定 Java NIO 使用的是 多路复用 I/O (multiplexing I/O) 模型了 。
poll 函数
poll函数用于监测多个等待事件,若事件未发生,进程睡眠,放弃CPU控制权,若监测的任何一个事件发生,poll将唤醒睡眠的进程,并判断是什么等待事件发生,执行相应的操作。 poll 函数没有最大文件描述符数量的限制 , poll 函数的缺点是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
#include <poll.h>
// *fdarray : 指向结构数组第一个元素的指针,每个数组都是一个 pollfd 结构,用于指定测试某个给定描述符 fd 的条件。
// nfds : 数组中元素个数。
// timeout : poll 函数返回前等待多长时间 , -1 - 永远等待 ; 0 - 立即返回,不阻塞; > 0 等待指定的毫秒数
int poll(struct pollfd * fdarray, unsigned long nfds, int timeout)
struct pollfd
{
int fd; // 每一个 pollfd 结构体指定了一个被监视的文件描述符
short events; // 指定监测 fd 上发生的事件
short revents; // 文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回.
}
每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件 。成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;失败时,poll() 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
epoll 函数
epoll是一种IO多路转接技术,在LINUX网络编程中,经常用来做事件触发,即当有特定事件到来时,能够检测到,而不必阻塞进行监听。epoll有两种工作方式,ET-水平触发 和 LT-边缘触发(默认工作方式),主要的区别是:LT,内核通知你fd是否就绪,如果没有处理,则会持续通知。而ET,内核只通知一次。
epoll 的优点 :
1. 支持进程打开大量数目的socket描述符,select支持的进程描述符由FD_SETSIZE设置,默认值为1024,而epoll不受这个限制。
2. epoll的效率,不随监听的socket数目增加而线性下降。select采用轮询的方式,对socket集合的描述符表进行扫描,如果socket数量过大,并且大多数socket属于idle状态,select的扫描就做了很多无用功。epoll只会对活跃的socket进行操作,所以,在socket数量比较大,而绝大多数socket属于idle状态时,epoll的效率会远胜于select。如果绝大多数socket是活跃的,由于epoll_ctl的影响,epoll的效率会稍微比select差。
3. 使用mmap加速内核与用户空间的传递。
epoll 主要有三个接口 :
1. int epoll_create( int size ) : 创建一个epoll的句柄,size表示监听的数目一共有多大。
2. int epoll_ctl( int epfd, int op, int fd, struct epoll_event* event ) : 事件注册函数,epfd是epoll_create返回的句柄,op是表示做什么动作,用三个宏表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd; fd表示要监听的描述符,event表示内核要监听什么事,由以下几个宏表示 : EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);EPOLLOUT:表示对应的文件描述符可以写;EPOLLPRI:表示对应的文件描述符有紧急的数据可读;EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 。
3. int epoll_wait( int epfd, struct epoll_event* events, int maxevents, int time_out ) : 等待事件的发生。events,存储epoll_wait操作完成后,存储的事件。maxevents表示当前要监听的所有socket句柄数。time_out为超时时间。返回值表示需要处理的事件数目,0表示超时。
epoll 实现原理 :
1. epoll_create : 在epoll文件系统建立了个file节点,并开辟epoll自己的内核高速cache区,建立红黑树,分配好想要的size的内存对象,建立一个list链表,用于存储准备就绪的事件。
2. epoll_ctl : 把要监听的socket放到对应的红黑树上,给内核中断处理程序注册一个回调函数,通知内核,如果这个句柄的数据到了,就把它放到就绪列表。
3. epoll_wait : 观察就绪列表里面有没有数据,并进行提取和清空就绪列表,非常高效。
Java NIO 核心组件
- Buffer (缓冲区) : 数据存储的缓冲区,对数据进行存、取的操作。
- Channel (信道) : 表示与 I/O 设备或能够执行一个或多个不同 I/O 操作的程序组件实体的开放连接,作用是进行数据的传输。
- Selector (多路复用器) : Selector 是 NIO 得以实现的核心 ,Selector 可以知道当前哪些 Channel 处于就绪状态 , 作用是获取处于就绪状态 Channel 。
- SelectionKey (选择键) : 选择键封装了 Channel 与 Selector 的关系,当 Channel 注册到 Selector 上时会创建一个 SelectionKey 。SelectionKey 封装了 Channel 感兴趣的事件 , 比如 Accept, Connect , Read , Write 。事件驱动就是根据 Channel 当前触发的事件进行相应的处理。SelectionKey 的作用是 , 记录 Channel 在 Selector 上注册了那种事件。
Java NIO 工作过程
根据我个人对 NIO 工作过程的理解整理。
基于操作系统 多路复用 I/O 模型 , Java 也实现了无阻塞的多路复用 I/O ,大幅度的提升了 流 I/O 的性能 , 比起 NIO 出现之前的 BIO 有了很大的进步。只有在调用 select 的时候会阻塞直到函数返回,这是不可避免的。现在对 NIO 有了一个宏观上的理解和认识 , 之后会深入到 NIO 的使用,原理细节。结合实际情况讨论 NIO 的优点、缺点、优化改进等内容。