16.Selector 选择器

多个 Channel 注册到同一个 Selector 上, Selector 能检测通道上是否有事件发生, 如果有事件发生, 便获取事件然后针对每个事件进行相应的处理.

这样就可以只用一个单线程去管理多个通道, 也就是管理多个连接和请求.

只有在连接真正有读写事件发生时, 才会进行读写, 就大大地减少了系统的开销, 而且不必为每个连接都创建一个线程, 不用去维护多个线程, 避免了多线程之间的上下文切换导致的开销.

下面是 java.nio.channels.Selector 的继承关系.

16.Selector 选择器_第1张图片
该类就是具体选择器的一个超类, 主要功能是通过 SelectorProvider 根据操作系统创建不同的 Selector 实例. 例如 Linux 下会创建 EPollSelectorImpl 实例.

而且还规定了一些基础通用方法:

  • keys: 获取所有注册的 SelectedKey.
  • select: 监听事件.
  • selectedKeys: 获取发生事件的 SelectedKey.

AbstractSelector 类是 Selector 类的一个基本实现, 提供了:

  • cancelledKeys: 获取已经取消注册的所有 SelectedKey.
  • register: 该方法是一个抽象方法, 将 Channel 注册到指定的 Selector 上.
  • deregister: 取消 SelectedKey 注册.

SelectorImpl 类是一个比较全面的 Selector 实现类, 其它的 Selector 的具体实现都是继承此类.

源码分析

我的代码分析不会一步一步看代码, 而是直接看重点.

大家最好能自己看一下代码.

创建 Selector

创建 Selector 是通过 Selector.open() 静态方法进行创建, 在 Linux 下最终会创建 EPollSelectorImpl 对象:

    EPollSelectorImpl(SelectorProvider sp) throws IOException {
        super(sp);
        // makePipe(调用系统函数 pipe) 返回管道的 2 个文件描述符, 编码在一个 long 类型的变量中
        // 高 32 位代表读 低 32 位代表写
        // 使用 pipe 为了实现 Selector 的 wakeup 逻辑.
        long pipeFds = IOUtil.makePipe(false);
        fd0 = (int) (pipeFds >>> 32);
        fd1 = (int) pipeFds;
        try {
            // EPollArrayWrapper 是针对 EPoll 的操作.
            pollWrapper = new EPollArrayWrapper();
            // 绑定管道的读事件
            pollWrapper.initInterrupt(fd0, fd1);
            fdToKey = new HashMap<>();
        } catch (Throwable t) {
            try {
                FileDispatcherImpl.closeIntFD(fd0);
            } catch (IOException ioe0) {
                t.addSuppressed(ioe0);
            }
            try {
                FileDispatcherImpl.closeIntFD(fd1);
            } catch (IOException ioe1) {
                t.addSuppressed(ioe1);
            }
            throw t;
        }
    }

EPollSelectorImpl 可以理解为 EPollArrayWrapper 的又一层包装.

EPollArrayWrapper 构造函数 和 initInterrupt 函数.

    EPollArrayWrapper() throws IOException {
        // creates the epoll file descriptor
        // 通过系统函数 epoll_create 创建 epoll, 与 select 函数类似, 用来监听 socket 上是否发生以及发生了什么事件.
        epfd = epollCreate();

        // the epoll_event array passed to epoll_wait
        // 创建这块内存是为了存放事件结果.
        int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
        pollArray = new AllocatedNativeObject(allocationSize, true);
        pollArrayAddress = pollArray.address();

        // 如果 文件描述符 > 64k, 会使用 eventHigh.
        if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE)
            eventsHigh = new HashMap<>();
    }

    void initInterrupt(int fd0, int fd1) {
        outgoingInterruptFD = fd1;
        incomingInterruptFD = fd0;
        // 调用系统函数 epoll_ctl, 给之前创建的管道的读 fd, 关联一个读事件, 并绑定到 epfd 中.
        // 表示关联的 fd 可以进行读操作了.
        epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
    }

注册 Channel

虽然在 15.Channel 通道中的 SelectableChannel 有说过该类提供了一个 register 方法, 但是最终还是调用的是 SelectorImpl#register 方法.

下面是 AbstractSelectableChannel 中的方法.

    public final SelectionKey register(Selector sel, int ops,
                                       Object att)
        throws ClosedChannelException
    {
        synchronized (regLock) {
            if (!isOpen())
                throw new ClosedChannelException();
            if ((ops & ~validOps()) != 0)
                throw new IllegalArgumentException();
            if (isBlocking())
                throw new IllegalBlockingModeException();
            // 该方法会遍历 SelectionKey 数组, 并获取每一个元素的对应的 Selector,
            // 然后与该方法的参数比较是否为同一个对象, 详细流程查看下面对应方法.
            SelectionKey k = findKey(sel);
            if (k != null) {
                k.interestOps(ops);
                k.attach(att);
            }
            if (k == null) {
                // New registration
                synchronized (keyLock) {
                    if (!isOpen())
                        throw new ClosedChannelException();
                    // 下面有详细解释
                    k = ((AbstractSelector)sel).register(this, ops, att);
                    addKey(k);
                }
            }
            return k;
        }
    }

    private SelectionKey findKey(Selector sel) {
        synchronized (keyLock) {
            if (keys == null)
                return null;
            for (int i = 0; i < keys.length; i++)
                if ((keys[i] != null) && (keys[i].selector() == sel))
                    return keys[i];
            return null;
        }
    }

注意 keys 变量是在 AbstractSelectableChannel 中声明, 可以认为该变量中保存的都是与当前 Channel 有关的 SelectionKey.

下面这是 SelectorImpl 中注册的具体实现, 也就是说 ((AbstractSelector)sel).register(this, ops, att); 调用后执行下面代码.

    protected final SelectionKey register(AbstractSelectableChannel ch,
                                          int ops,
                                          Object attachment)
    {
        if (!(ch instanceof SelChImpl))
            throw new IllegalSelectorException();
        // SelectionKey 表示, Channel 注册到了一个 Selector 上.
        SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
        k.attach(attachment); // 这个参数怎么使用我也没搞明白
        synchronized (publicKeys) {
            implRegister(k);
        }
        k.interestOps(ops);
        return k;
    }

    // EPollSelectorImpl 中的 implRegister 实现.
    protected void implRegister(SelectionKeyImpl ski) {
        if (closed)
            throw new ClosedSelectorException();
        SelChImpl ch = ski.channel;
        // 得到对应连接的文件描述符
        int fd = Integer.valueOf(ch.getFDVal());
        // 文件描述符与 SelectionKey 关联.
        fdToKey.put(fd, ski);
        // 该方法只是将对应描述符的事件先强制设置为 0.
        // 注意: 在该方法中只是将 eventsLow 数组中, 下标为 fd 的值改为 0. 这个时候还没有调用系统函数进行修改.
        pollWrapper.add(fd);
        // keys 是一个 HashSet 集合, 保存了所有的 SelectionKey.
        keys.add(ski);
    }

最后是 k.interestOps(ops); 调用, 在该方法中会根据不同的 Channel 调用不同的 translateAndSetInterestOps 方法.

该方法作用是将程序内部事件, 例如 OP_ACCEPT 转换为 POLLIN 这种被 epoll_ctl 系统函数所支持的事件. 然后会重新设置 eventsLow 对应下标的值.

也就是说这里才确定了你的事件. 但是并没有调用系统函数, 来做绑定.

值得注意的是:

Net.POLLIN 的值并不是固定的, 会根据操作系统设置不同的值, 在 Linux 下该值为 0x0001.

至于为什么要进行转换, 可以查看 参考文章 中的 epoll触发事件的分析.

获取已就绪的 SelectionKey

通过调用 selector.select(1000) 方法, 该方法有一个超时时间, 当然你可以调用 selectselectNow 方法.

该方法最终会调用 EPollSelectorImpl 对象的 doSelect 方法.

    protected int doSelect(long timeout) throws IOException {
        if (closed)
            throw new ClosedSelectorException();
        // 删除已经取消绑定的 SelectionKey
        processDeregisterQueue();
        try {
            // 标记 可能无限期阻塞的 I/O操作的开始
            begin();
            /*
             * 在该方法中会先注册/更新 socket 绑定的事件, 或删除 socket 的事件.
             * 然后调用系统的 epoll_wait(没有超时时间) 或 在 for (;;) 中调用 epoll_wait 方法,
             * 到了超时时间后, 还没有数据则返回 0. >0 表示就绪的 socket 个数.
             */
            pollWrapper.poll(timeout);
        } finally {
            // 标记 可能无限期阻塞的 I/O操作的结束
            end();
        }
        processDeregisterQueue();
        // 这里会通过 fd 找到对应的 SelectionKey, 然后添加到 selectedKeys 集合中, 
        // 并返回就绪的个数.
        int numKeysUpdated = updateSelectedKeys();
        if (pollWrapper.interrupted()) {
            // Clear the wakeup pipe
            // 为什么这里要做这些操作, 我就不清楚了.
            pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
            synchronized (interruptLock) {
                pollWrapper.clearInterrupted();
                IOUtil.drain(fd0);
                interruptTriggered = false;
            }
        }
        return numKeysUpdated;
    }

updateSelectedKeys() 方法主要就是通过不同 Channel 的 translateAndSetReadyOps 方法判断事件类型.

    private int updateSelectedKeys() {
        //  已就绪(产生事件)的文件句柄 数量
        int entries = pollWrapper.updated;
        int numKeysUpdated = 0;
        for (int i=0; i<entries; i++) {
            // 获取事件是哪个文件描述符产生的
            int nextFD = pollWrapper.getDescriptor(i);
            // 通过文件描述符获取对应的 SelectionKey
            // 关于 fdToKey 的添加后面再说.
            SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
            // ski is null in the case of an interrupt
            // 至于为啥为 null 就是中断, 我也没明白.
            if (ski != null) {
                // 获取事件类型.
                int rOps = pollWrapper.getEventOps(i);
                if (selectedKeys.contains(ski)) {
                    if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
                        numKeysUpdated++;
                    }
                } else {
                    // 在该方法中会设置 readyOps 属性, 该属性表示事件类型.
                    ski.channel.translateAndSetReadyOps(rOps, ski);
                    if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
                        selectedKeys.add(ski);
                        numKeysUpdated++;
                    }
                }
            }
        }
        return numKeysUpdated;
    }

pollWrapper 变量的类型为 EPollArrayWrapper, 主要用来操作关于 epoll 底层函数, 例如注册/更新 epoll 或删除 epoll 的事件以及获取发生的事件, 下面是 EPollArrayWrapper#poll 方法.

    int poll(long timeout) throws IOException {
        // 注册/更新 epoll 或删除 epoll 的事件, 实际调用 `epollCtl` 加入到 epollfd 中.
        updateRegistrations();
        // 获取已就绪(产生事件)的文件句柄
        updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
        for (int i=0; i<updated; i++) {
            if (getDescriptor(i) == incomingInterruptFD) {
                interruptedIndex = i;
                interrupted = true;
                break;
            }
        }
        return updated;
    }

通过 getDescriptor(i) 获得事件是哪个文件描述符产生的, 变量 incomingInterruptFD 值保存的是管道的读文件描述符.

也就是说当指定的管道有数据时, 也会产生事件. 并将执行 interrupted = true;. 至于为什么要这个样子后续我就不知道了.

讨论

1. 相同 Channel 能否注册到不同 Selector 上

通过 注册 Channel 可以发现, 并没有做任何限制处理.

但我个人认为一般不会出现, 相同 Channel 注册到不同 Selector 上.

参考文章

同步/异步,阻塞/非阻塞概念深度解析

pipe()函数Unix / Linux

C++ socket, fork & pipes

What’s the difference between pipe and socket?

浅析epoll - epoll函数深入讲解

【Linux】进程间通信(IPC)之管道详解与测试用例

【Linux】进程间通信(IPC)之消息队列详解及测试用例

epoll_ctl()函数 Unix/Linux

epoll触发事件的分析

你可能感兴趣的:(Java,IO)