Java NIO Selector(1)---Selector如何实现多路复用select

Selector是一个Java nio中select多路复用的实现者。很早对select就有了解,但是最近使用Java nio的时候中出现一些问题,带着问题翻了翻源码。

首先说一下遇到的问题:

  1. selector是如何实现select多路复用的。
  2. 使用selector.selectedKeys()获取到可用的SelectionKey集合,在处理完这个事件后,一定要删除掉。为什么?
  3. 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删除掉,如果不删除会产生什么影响。由于篇幅问题,放在下一篇。

猜你喜欢

转载自blog.csdn.net/weixin_45701550/article/details/102696807