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

         上一篇文章中描述了使用系统调用select返回指定fd的就绪的时间信息,然后在java层面利用SelectionKey等抽象概念来封装这些信息,来达到对上层提供简单灵活的接口,并屏蔽底层细节。

处理Select系统调用返回的信息

        上层抽象Selector通过调用select方法,最终调用到WindowsSelectorImpl的doSelece方法,在doSelect中完成了系统调用select的调用(select系统调用委托给subSelector的poll0)。返回事件就绪的fd信息。这些信息以数组的方式存储,获取到这些信息后,接下来就是将这些信息进行封装。在WindowsSelectorImpl的doSelect方法中,完成信息的获取和处理的整个流程。

protected int doSelect(long timeout) throws IOException {
        if (channelArray == null)
            throw new ClosedSelectorException();
        this.timeout = timeout; // set selector timeout
        processDeregisterQueue();
        if (interruptTriggered) {
            resetWakeupSocket();
            return 0;
        }
        // Calculate number of helper threads needed for poll. If necessary
        // threads are created here and start waiting on startLock
        adjustThreadsCount();
        finishLock.reset(); // reset finishLock
        // Wakeup helper threads, waiting on startLock, so they start polling.
        // Redundant threads will exit here after wakeup.
        startLock.startThreads();
        // do polling in the main thread. Main thread is responsible for
        // first MAX_SELECTABLE_FDS entries in pollArray.
        try {
            begin();
            try {
                subSelector.poll();
            } catch (IOException e) {
                finishLock.setException(e); // Save this exception
            }
            // Main thread is out of poll(). Wakeup others and wait for them
            if (threads.size() > 0)
                finishLock.waitForHelperThreads();
          } finally {
              end();
          }
        // Done with poll(). Set wakeupSocket to nonsignaled  for the next run.
        finishLock.checkForException();
        processDeregisterQueue();
        int updated = updateSelectedKeys();
        // Done with poll(). Set wakeupSocket to nonsignaled  for the next run.
        resetWakeupSocket();
        return updated;
    }

        fd就绪事件信息获取有subSelector.poll()方法实现,对获取信息的处理有方法updateSelectedKeys()实现,调用updateSelectedKeys来处理获取到的fd和fd就绪的事件信息,即将这些信息封装成selectionKey的主要实现逻辑。

        在看updateSelectedKeys方法实现前,先说一下selectionKey是什么?SelectionKey是对SelectableChanenl以及其感兴趣事件的封装,根据抽象类SelectionKey接口也可以看出端倪,而且每个SelectableChanel也会有一个与其一一对应的fd。

public abstract class SelectionKey {

    /**
     * Constructs an instance of this class.
     */
    protected SelectionKey() { }

    public abstract SelectableChannel channel();
   
    public abstract Selector selector();

    public abstract int interestOps();

    public abstract SelectionKey interestOps(int ops);

    public abstract int readyOps();

}

updateSelectedKeys方法具体实现:

private int updateSelectedKeys() {
        updateCount++;
        int numKeysUpdated = 0;
        numKeysUpdated += subSelector.processSelectedKeys(updateCount);
        for (SelectThread t: threads) {
            numKeysUpdated += t.subSelector.processSelectedKeys(updateCount);
        }
        return numKeysUpdated;
    }

        变量updateCount,是windowsSelectorImp的实例属性,用来记录调用updateSelectedKeys方法的次数。主要用来协助统计每次调用updateSelectedKeys方法,返回事件就绪fd的个数的准确性,主要就是为了防止,当一个fd的多个事件就绪时,统计重复。后面代码中会体现。

        接下来使用调用subSelector的processSelectedKeys方法,处理poll0方法(调用系统调用select)返回的fd和fd就绪的时间信息。在下面一个for循环,执行其他线程的获取的就绪fd信息。这里先简短说明一下:我们知道select有一个很致命的限制,就是默认最多支持对1024个fd进行检测,虽说是默认,但是在linux上如果要想调整这个参数,需要重新编译操作系统,基本上和不能修改没啥区别。当然如果这个值过大的话,检测的fd过多,select的效率也就降低了。所以java在selector实现时,当检测的fd过多时,采用多个协助线程来处理,每个协助线程1024个fd。每个协助线程(SelectThread)持有一个Subselector实例变量,用来处理分配给这个线程的所有fd。如何给每个协助线程分配fd呢,或者说每个协助线程处理哪些fd呢?,由这个协助线程在线程集合中的索引决定的(线程集合用来存放所有协助线程用的,当协助线程创建创建出来之后,会放到这个集合,集合很简单就是一个ArrayList,是WindowsSelectorImpl的一个实例属性threads),根据该线程在集合中的索引决定这个线程处理pollWrapper中第几个“1024”fds。这里先不考虑总fd个数,超过1024个fd的情况。准确的说应该是1023个,因为第一个fd是用来作为唤醒select系统调用函数用的,关于唤醒select系统调用在后面会讲述。

        每个subSelector处理自己监测fd的就绪事件的主要逻辑就在subSelector.processSelectedKey(updateCount)方法中。

private int processSelectedKeys(long updateCount) {
            int numKeysUpdated = 0;
            numKeysUpdated += processFDSet(updateCount, readFds,
                                           Net.POLLIN,
                                           false);
            numKeysUpdated += processFDSet(updateCount, writeFds,
                                           Net.POLLCONN |
                                           Net.POLLOUT,
                                           false);
            numKeysUpdated += processFDSet(updateCount, exceptFds,
                                           Net.POLLIN |
                                           Net.POLLCONN |
                                           Net.POLLOUT,
                                           true);
            return numKeysUpdated;
        }

        这里就是处理本地函数poll0,返回的就绪事件集合readFds,writeFds和exceptFds,关于poll0返回readFds,writeFds和exceptFds的详细过程可以参考上一篇文章。其实三种集合的处理过程是一样的,只不过传递参数略有不同而已。

        接下来我们来看processFDSet具体如何处理readFds,writeFds和exceptFds的。

 /**
         * Note, clearedCount is used to determine if the readyOps have
         * been reset in this select operation. updateCount is used to
         * tell if a key has been counted as updated in this select
         * operation.
         *
         * me.updateCount <= me.clearedCount <= updateCount
         */
        private int processFDSet(long updateCount, int[] fds, int rOps,
                                 boolean isExceptFds)
        {
            int numKeysUpdated = 0;
            for (int i = 1; i <= fds[0]; i++) {
                int desc = fds[i];
                if (desc == wakeupSourceFd) {
                    synchronized (interruptLock) {
                        interruptTriggered = true;
                    }
                    continue;
                }
                MapEntry me = fdMap.get(desc);
                // If me is null, the key was deregistered in the previous
                // processDeregisterQueue.
                if (me == null)
                    continue;
                SelectionKeyImpl sk = me.ski;

                // The descriptor may be in the exceptfds set because there is
                // OOB data queued to the socket. If there is OOB data then it
                // is discarded and the key is not added to the selected set.
                if (isExceptFds &&
                    (sk.channel() instanceof SocketChannelImpl) &&
                    discardUrgentData(desc))
                {
                    continue;
                }

                if (selectedKeys.contains(sk)) { // Key in selected set
                    if (me.clearedCount != updateCount) {
                        if (sk.channel.translateAndSetReadyOps(rOps, sk) &&
                            (me.updateCount != updateCount)) {
                            me.updateCount = updateCount;
                            numKeysUpdated++;
                        }
                    } else { // The readyOps have been set; now add
                        if (sk.channel.translateAndUpdateReadyOps(rOps, sk) &&
                            (me.updateCount != updateCount)) {
                            me.updateCount = updateCount;
                            numKeysUpdated++;
                        }
                    }
                    me.clearedCount = updateCount;
                } else { // Key is not in selected set yet
                    if (me.clearedCount != updateCount) {
                        sk.channel.translateAndSetReadyOps(rOps, sk);
                        if ((sk.nioReadyOps() & sk.nioInterestOps()) != 0) {
                            selectedKeys.add(sk);
                            me.updateCount = updateCount;
                            numKeysUpdated++;
                        }
                    } else { // The readyOps have been set; now add
                        sk.channel.translateAndUpdateReadyOps(rOps, sk);
                        if ((sk.nioReadyOps() & sk.nioInterestOps()) != 0) {
                            selectedKeys.add(sk);
                            me.updateCount = updateCount;
                            numKeysUpdated++;
                        }
                    }
                    me.clearedCount = updateCount;
                }
            }
            return numKeysUpdated;
        }
    }

        代码思路很清晰,将fd中就绪的事件信息添加或者重置到selectionKey的readyOps中。如果这个selectionKey不在SelectedKeys中,那么就将这个key添加进去。

        需要注意的点在代码的注释中也详细说明了:updateCount和clearedCount的具体含义,updateCount用来说明一个selectionKey是否已经被统计了,clearedCount主要用来记录一个SelectionKey的readyOps是否已经被设置了,防止一个fd上有多个就绪事件时,这些事件信息存放到selectionKey中时产生覆盖,具体来说就是当me.clearedCount != updateCount时,采用赋值的方式,对应代码就是translateAndSetReadOps。当me.clearedCount == updateCount时,采用更新累加的方式,对应代码就是:translateAndUpdateReadyOps。这两个方法都是调用translateReadyOps方法,只是初始参数不同而已,一个为0,实现每次都从新开始添加ops,另一个初始参数为上一次已经添加的ops,从而实现了累积。

public boolean translateAndUpdateReadyOps(int ops, SelectionKeyImpl sk) {
        return translateReadyOps(ops, sk.nioReadyOps(), sk);
    }

    public boolean translateAndSetReadyOps(int ops, SelectionKeyImpl sk) {
        return translateReadyOps(ops, 0, sk);
    }

        到这里,完成从select系统调用返回的fd以及fd就绪事件信息,封装成selectionKey,并将selectionKey添加到selectedkeys中。当然以上这些操作,在上层selector看来,都封装在了一个select接口方法中了,当这些完成了,selector.select(),也就是返回了。这时候,通过调用selector.selectedKeys()返回包含多个已经就绪状态的selectionKey的集合selectedKeys。

        接下来就是我们编写nio代码的范式了。

       try {
            while (true){
                selector.select();
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()){
                    SelectionKey next = iterator.next();
                    iterator.remove();
                    if(next.isAcceptable()){
                        SocketChannel accept = serverSocketChannel.accept();
                        accept.configureBlocking(false);
                        accept.register(selector, SelectionKey.OP_READ);
                    }
                    if(next.isReadable()){
                        SocketChannel chanel = (SocketChannel) next.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        chanel.read(buffer);
                    }
                }
            }
        } catch (Exception e) {
            // ignore
        }

        不知道你会不会有疑问:为什么在遍历selectedKeys时,要将遍历过的selectionKey给remove掉?如果不remove掉会产生什么影响?

        在上面描述的processFDSet执行流程中:重置或者更新SelectionKey的readyOps,如果selectedKeys中不存在这个selectionKey的话,就将这个selectionKey添加进去,然而却没有从selectedKeys中删除selectionkey的操作。那么也就是导致了,selectedKeys中会保存之前执行processFDSet操作添加到selectedKeys中的selectionKey,即使这selectionKey对应的fd上没有事件就绪。当调用selector.selectedKeys是也会将这个selectionkey返回,而且这个selectionKey中的readyOps没有发生变化,还是上一次,该selectionKey就绪的事件,如果我们获取到这个selectionKey的话,会给我们一个错觉就是这个selectionKey绑定的fd上又有事件就绪了的错觉,这种错觉,使我们的程序有多路复用IO模型,变成了非阻塞IO模型。很多时候,给我们程序带来错误。举个简单例子:如果那个过期的selectionKey绑定是serverSocketChanel的话,那么执行serverSocketChannel.accept()返回的就是null,接下来发生什么你应该就清楚了。

        还有一个需要注意的就是,无论是调用subSelector.poll0本地方法,返回被检测fd的就绪事件,还是调用subSelector.processFDSet,将poll0返回的事件信息封装到selectionKey中,仅仅只是将fd上的就绪事件信息从底层传递到上层应用程序,告诉上层应用程序:有事件发生。如果上层应用程序不对事件进行处理的话,那么下次调用系统调用select时会立即返回,因为这些事件还在一直就绪着,例如:如果某个fd读事件就绪了(正常情况下数据量超过接收缓冲区低水位线,fd的读事件就绪),上层程序不做处理,那么,数据在fd的接收缓冲区中就一直存在。该fd的读事件也就是一直就绪。

        到这里也就是说明了上一篇开头说的第二个问题。在文中提到每个线程处理的fd个数最多为1024,其实真正处理业务fd是1023个,第一个是用来做唤醒使用的,那么这个fd是如果实现唤醒的呢?关于这个问题,放在下一篇文章中描述。

猜你喜欢

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