Selector是一个Java nio中select多路复用的实现者。很早对select就有了解,但是最近使用Java nio的时候中出现一些问题,带着问题翻了翻源码。
首先说一下遇到的问题:
- selector是如何实现select多路复用的。
- 使用selector.selectedKeys()获取到可用的SelectionKey集合,在处理完这个事件后,一定要删除掉。为什么?
- Selector的唤醒是如何实现的。
大名鼎鼎的select
Select可以实现多路复用,也就是使用一个线程来监控多个文件描述的读,写,异常状态。虽然之后又有了Epoll(linux),但实现的功能都是一样的,只不过实现过程中,使用的数据结构和查询算法不同。这里不做比较与深究。
在windows上select方法原型:
int select(int nfds, FD_SET* readfds, FD_SET* writefds, FD_SET* exceptfds, const struct timeval* timeout);
第一个参数 :nfds起到兼容的作用,通常设置为0.
第二到第四个参数:Readfds,writefds,exceptfds分别用来保存对读,写,异常感兴趣的文件描述符。可以将FD_SET理解成一个存放fd的集合。同时在select返回时,这三个fd集合中的数据表示对应事件已经就绪的fd。这三个参数即作为入参又作为出参。
第五个参数:Timeout为超时参数,如果在指定时间内没有fd感兴趣的事件发生的话,select也会返回,只不过readfds,writefds和exceptfds中是没有结果数据的。
我们知道select,Epoll这些多路复用技术都是操作系统内核提供的,编程语言只能通过调用操作系统提供的系统调用,来达到使用这些函数的目的。Java作为一种面向对象的语言,为了实现对外提供的接口更通用,更灵活,在实现Selector到select的过程中也是层层封装与抽象。那么Selector背后,是如何一步步,从抽象到实现,一步步拆解封装,直达操作系统的系统调用的呢?
Java中的Selector
Selector实现过程中的主要的类:
根据使用的jdk所属平台不同,Selector的最终实现也就不同,在windows上,最底层的实现就是WindowsSelectorImpl了,实现的细节作为使用来说是不关心的,因为是面向Selector编程,而非WindowsSelectorImpl。所以即使下层实现了变了,对我们也没有太大影响,这个主要归功于SelectorProvider,他帮Selector屏蔽了下层实现,实现了不同平台SelectorImpl和Selector之间的解耦。
Selector的创建过程
通过使用Selector.open()创建一个Selector,Selector最终实现为WindowsSelectorImpl。具体过程如下:
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
调用provider()方法获取具体的provider:DefaultSelectorProvider
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
由DefaultSelectorProvider在获取具体实现WindowsSelectorProvider。需要说明一下,SelectorProvider可以有多种设定方式:1可以通过使用jvm -D参数java.nio.channels.spi.SelectorProvider在jvm启动时指定。2.使用SPI扩展机制设置。3.使用默认的DefaultSelectorProvider.
public static SelectorProvider create() {
return new sun.nio.ch.WindowsSelectorProvider();
}
再由WindowSelectorProvider获取最终的Selector的实现WindowsSelectorImpl。
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
至此获取到了依赖于平台的具体的Selector实现WindowsSelectorImpl.
Selector调用操作系统提供的select
获得了Selector的具体实现,通过使用selector的select()方法,会调用到WindowsSelectorImpl的doSelect方法上,doSelect方法将主要实现select的逻辑委托给subSelector.poll()上,然后继续跟进最终来到了subSelector的poll0上。
poll0的原型,注意poll0是一个本地方法:
private native int poll0(long pollAddress, int numfds,
int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
poll0和windows上的select函数原型有点相似,但又有点不同。pollAddress是什么鬼?Numfds,readFds,writeFds,exceptonfds和timeout是否与select中的参数对应呢?但是readFds,writeFds,exceptonfds和select原型中参数类型也不同啊,此时心中还是有很多疑问。那就接着看代码。首先是谁调用了这个函数,看一下他调用时传递的参数又都是什么。
private final int pollArrayIndex; // starting index in pollArray to poll
// These arrays will hold result of native select().
// The first element of each array is the number of selected sockets.
// Other elements are file descriptors of selected sockets.
private final int[] readFds = new int [MAX_SELECTABLE_FDS + 1];
private final int[] writeFds = new int [MAX_SELECTABLE_FDS + 1];
private final int[] exceptFds = new int [MAX_SELECTABLE_FDS + 1];
private SubSelector() {
this.pollArrayIndex = 0; // main thread
}
private SubSelector(int threadIndex) { // helper threads
this.pollArrayIndex = (threadIndex + 1) * MAX_SELECTABLE_FDS;
}
private int poll() throws IOException{ // poll for the main thread
return poll0(pollWrapper.pollArrayAddress,
Math.min(totalChannels, MAX_SELECTABLE_FDS),
readFds, writeFds, exceptFds, timeout);
}
private int poll(int index) throws IOException {
// poll for helper threads
return poll0(pollWrapper.pollArrayAddress +
(pollArrayIndex * PollArrayWrapper.SIZE_POLLFD),
Math.min(MAX_SELECTABLE_FDS,
totalChannels - (index + 1) * MAX_SELECTABLE_FDS),
readFds, writeFds, exceptFds, timeout);
}
poll0是由poll调用的,而且参数readFds,writeFds,exceptonfds刚开始都是空的。那么由此可见被监听的fd信息不是通过这三个数组传递进去的。那么被监听的fd是谁传进去的呢?pollWrapper又是什么,是否有他传进来的呢?
PollArrayWrapper
PollArrayWrapper类中主要信息:
private AllocatedNativeObject pollArray; // The fd array
long pollArrayAddress; // pollArrayAddress
@Native private static final short FD_OFFSET = 0; // fd offset in pollfd
@Native private static final short EVENT_OFFSET = 4; // events offset in pollfd
static short SIZE_POLLFD = 8; // sizeof pollfd struct
private int size; // Size of the pollArray
pollArray:可以看做是一段连续的内存空间。
PollArrayAddress是pollArray的起始地址。
pollArray中存放的内容是一个个8字节的数据单元。这个8字节数据有两部分组成,每部分占用4个字节,分别是fd信息和这个fd关注的事件events,这两个信息在8字节数据单元中的偏移量分别由FD_OFFSET和EVENT_OFFSET标识,也就是0和4。pollArray中的数据结构如下:
将pollArray的起始地址pollArrayAddress作为参数传递给poll0,根据这地址从前向后进行解析,不就可以得出fd信息和对应的事件信息了吗。如果是这样的话,那么这个数据结构中的fd和fd关心的事件events又是从哪来的呢?回想在编写nio代码时,我们通过channel的register方法输入过每个channel以及这channel关注的事件信息。这就是fd和fd对应事件events的数据源。所以需要确定PollArray中的数据是否从channel的register中过来的呢?通过查看SocketChannel.register()方法的实现,来到SelectorImpl中。
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
synchronized (publicKeys) {
implRegister(k);
}
k.interestOps(ops);
return k;
}
我们最关心的是ch和ops这个变量的去向,首先将channel信息封装到了SelectionKeyImpl然后通过implRegister(k)向底层深处传递,而ops则通过interestOps(ops)进行传递。ImplRegister(k)方法在WindowsSelectorImpl中的实现:
protected void implRegister(SelectionKeyImpl ski) {
synchronized (closeLock) {
if (pollWrapper == null)
throw new ClosedSelectorException();
growIfNeeded();
channelArray[totalChannels] = ski;
ski.setIndex(totalChannels);
fdMap.put(ski);
keys.add(ski);
pollWrapper.addEntry(totalChannels, ski);
totalChannels++;
}
}
ski作为pollWrapper的addEntry()的参数,最终通过putDescriptor放到上图的pollArray的fd域中:
void addEntry(int index, SelectionKeyImpl ski) {
putDescriptor(index, ski.channel.getFDVal());
}
void putDescriptor(int i, int fd) {
pollArray.putInt(SIZE_POLLFD * i + FD_OFFSET, fd);
}
interestOps(ops)代码跟进,最终来到WindowsSelectorImpl的putEventOps中。
public void putEventOps(SelectionKeyImpl sk, int ops) {
synchronized (closeLock) {
if (pollWrapper == null)
throw new ClosedSelectorException();
// make sure this sk has not been removed yet
int index = sk.getIndex();
if (index == -1)
throw new CancelledKeyException();
pollWrapper.putEventOps(index, ops);
}
}
调用pollWrapper的putEventOps将事件信息放到pollArray中由i指定的位置。
void putEventOps(int i, int event) {
pollArray.putShort(SIZE_POLLFD * i + EVENT_OFFSET, (short)event);
}
到这里,可以明确得出:pollArray的确包含了所有fd和fd关心的事件信息,而且这些信息的确从register中传递过来的。现在已经明确了调用subSelector的poll0的所有实参信息。但是这些信息和windows提供的select函数原型还是对应不上的呀。此时别忘了,subSelector的poll0是一个native方法,也就是说,在jdk里有对应的c语言实现,也就是说通过这个c语言实现的中间层,可能起到了poll0和select之间参数的转换。是否是这样的呢,可以查找openjdk的中WindowsSelectorImpl.c中的Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0方法进行查看:
typedef struct {
jint fd;
jshort events;
} pollfd;
JNIEXPORT jint JNICALL
Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(JNIEnv *env, jobject this,
jlong pollAddress, jint numfds,
jintArray returnReadFds, jintArray returnWriteFds,
jintArray returnExceptFds, jlong timeout)
{
DWORD result = 0;
pollfd *fds = (pollfd *) pollAddress; //类型转换
int i;
FD_SET readfds, writefds, exceptfds;
struct timeval timevalue, *tv;
static struct timeval zerotime = {0, 0};
int read_count = 0, write_count = 0, except_count = 0;
首先定义了一个结构体,这个结构体刚好和我们在pollArray中指定的8字节数据单元是对应的。这时将pollArray的地址转换成了pollfd结构体指针,这样就可以通过fds指针来遍历给定地址中的数据,而且也不会出现数据不一致的问题。
/* Set FD_SET structures required for select */
for (i = 0; i < numfds; i++) {
if (fds[i].events & POLLIN) {
readfds.fd_array[read_count] = fds[i].fd;
read_count++;
}
if (fds[i].events & (POLLOUT | POLLCONN))
{
writefds.fd_array[write_count] = fds[i].fd;
write_count++;
}
exceptfds.fd_array[except_count] = fds[i].fd;
except_count++;
}
将fds指定的信息,解析到readfds,writefds和exceptfds中。
if ((result = select(0 , &readfds, &writefds, &exceptfds, tv))//调用select,传进去解析过的数据
== SOCKET_ERROR) {
/* Bad error - this should not happen frequently */
/* Iterate over sockets and call select() on each separately */
FD_SET errreadfds, errwritefds, errexceptfds;
readfds.fd_count = 0;
writefds.fd_count = 0;
exceptfds.fd_count = 0;
此时所有参数信息都有值了,调用select方法。等select返回后,结果信息再存放在readfds,writefds和exceptfds中。
resultbuf[0] = readfds.fd_count;
for (i = 0; i < (int)readfds.fd_count; i++) {
resultbuf[i + 1] = (int)readfds.fd_array[i];
}
(*env)->SetIntArrayRegion(env, returnReadFds, 0,
readfds.fd_count + 1, resultbuf);
resultbuf[0] = writefds.fd_count;
for (i = 0; i < (int)writefds.fd_count; i++) {
resultbuf[i + 1] = (int)writefds.fd_array[i];
}
(*env)->SetIntArrayRegion(env, returnWriteFds, 0,
writefds.fd_count + 1, resultbuf);
resultbuf[0] = exceptfds.fd_count;
for (i = 0; i < (int)exceptfds.fd_count; i++) {
resultbuf[i + 1] = (int)exceptfds.fd_array[i];
}
(*env)->SetIntArrayRegion(env, returnExceptFds, 0,
exceptfds.fd_count + 1, resultbuf);
最后将结果信息从readfds,writefds和exceptfds中转移到出参returnReadFds,returnWriteFds,returnExceptFds,这里需要注意的是,出参数组第一个元素的值为该数组中fd的个数。到此为止,完成了java Selector实现select。接下来的过程就是在java层面,从出参中解析数据封装成SelectionKey,提供对外的接口使用。
封装好的SelectionKey如何通过Selector的selectedKeys()方法,返回给使用者,以及为什么每次都要将selectedKeys中的SelectionKey删除掉,如果不删除会产生什么影响。由于篇幅问题,放在下一篇。