NIO之终极Selctor源码分析

回顾一下Selector,多路复用器Selector可以管理多个Channel,可以向Selector注册感兴趣的事件,当事件就绪,通过Selector.select()方法获取注册的事件,进行相应的操作。大致流程如下代码。

Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(1234));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
	Set<SelectionKey> keys = selector.selectedKeys();
	Iterator<SelectionKey> iterator = keys.iterator();
	while (iterator.hasNext()) {
	   SelectionKey key = iterator.next();
	   iterator.remove();
	   if (key.isAcceptable()) {
	   ...
	   } else if (key.isReadable() && key.isValid()) {
	   ...
	   }
	   keys.remove(key);
	}
 }
1. Selector实例化过程

先看看Selector的继承关系,其中SelectorImpl抽象类是Java提供的通用实现,在Linux平台有EPollSelectorImpl实现类,在windows平台有WindowsSelectorImpl。
selector
一般我们获取Selector通过Selector.open()获取,

public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
}

Selector内部是通过SelectorProvider类的Provider方法实现。

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;
                    }
                });
    }
}

这里使用单例模式,保证只有一个SelectorProvider。provider由sun.nio.ch.DefaultSelectorProvider.create()创建。create方法内部通过系统的名称来创建创建SelectorProvider,在这里创建了sun.nio.ch.EPollSelectorProvider

 public static SelectorProvider create() {
	   String osName = (String)AccessController.doPrivileged(new GetPropertyAction("os.name"));
	   if (osName .equals("SunOS")) {
	       return createProvider("sun.nio.ch.DevPollSelectorProvider");
	   } else {
	       return (SelectorProvider)(osName .equals("Linux") ? createProvider("sun.nio.ch.EPollSelectorProvider") : new PollSelectorProvider());
	   }
}

继续回到Selector的open()中,获取到SelecProvider实例之后,继续调用openSelector(),很自然进入EPollSelectorProvider的openSelector()方法:

public AbstractSelector openSelector() throws IOException {
    return new EPollSelectorProvider(this);
  }

EPollSelectorImpl是linux系统的实现,该类中有几个重要的成员变量:

// 用于中断的文件描述符
protected int fd0;
protected int fd1;
// EpollArrayWapper将Linux的epoll相关系统调用封装成了native方法供EpollSelectorImpl使用。
EPollArrayWrapper pollWrapper;
// 保存socket句柄和selectkey,有注册到selector的channel对应的SelectionKey和与之对应的文件描述符都会放入到该映射表中。
private Map<Integer,SelectionKeyImpl> fdToKey;

EPollSelectorImpl的构造函数:

 EPollSelectorImpl(SelectorProvider sp) throws IOException {
       super(sp);
       long pipeFds = IOUtil.makePipe(false);
       this.fd0 = (int)(pipeFds>>> 32);
       this.fd1 = (int)pipeFds;
       this.pollWrapper = new EPollArrayWrapper();
       this.pollWrapper.initInterrupt(this.fd0, this.fd1);
       this.fdToKey = new HashMap();
}

初始化的主要步骤如下:

  • 首先调用父类SelectorImp的构造方法。JDK中对于注册到Selector上的IO事件关系是使用SelectionKey来表示,代表了Channel感兴趣的事件,如Read,Write,Connect,Accept。内部初始化publicKeys和publicSelectedKeys,用到的容器是HashSet,前者用来保存所有的感兴趣的事件,后者准备好的事件。下面是父类SelectorImpl的属性和构造方法:
// The set of keys with data ready for an operation
protected Set<SelectionKey> selectedKeys;
// The set of keys registered with this Selector
protected HashSet<SelectionKey> keys;

// Public views of the key sets
private Set<SelectionKey> publicKeys;             // Immutable
private Set<SelectionKey> publicSelectedKeys;     // Removal allowed, but not addition

protected SelectorImpl(SelectorProvider sp) {
    super(sp);
    keys = new HashSet<SelectionKey>();
    selectedKeys = new HashSet<SelectionKey>();
    if (Util.atBugLevel("1.4")) {
        publicKeys = keys;
        publicSelectedKeys = selectedKeys;
    } else {
        publicKeys = Collections.unmodifiableSet(keys);
        publicSelectedKeys = Util.ungrowableSet(selectedKeys);
    }
}

进入SelectorImpl的源码,类中定义了四个Set类型的,并且命名极其相似,前两个是给Selector内部使用的,后两个是给程序员使用的,keys=publicKeys 表示所有注册的事件。selectedKeys=publicSelectedKeys 表示准备好的事件。keys和selectedKeys直接引用这Hashset,publicKeys 和publicSelectedKeys 引用这包装了一层权限控制,内部还是指向前面的HashSet。实际的堆内存中只有两个HashSet对象。父类AbstractSelector中设置Provider的值,还有一个比较重要的字段是cancelKey,标识取消的key 的集合。因为保存了所有的key的keys我们没有修改的权限,所以只能将取消的key单独放在set集合中。

  • makePipe返回管道的2个文件描述符,编码在一个long类型的变量中。高32位代表读 低32位代表写。当要中断select方法时,往fd1中写入数字1会导致fd0有可读内容,select方法会返回,使用pipe为了实现Selector的wakeup逻辑。
void initInterrupt(int fd0, int fd1) {
        outgoingInterruptFD = fd1;
        incomingInterruptFD = fd0;
        epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
    }
  • 最后定义了一个map类型的变量fdToKey,将channel的文件描述符ID和SelectionKey建立映射关系,SelectionKey中保存了Channel Selector 感兴趣的事件。

EpollArrayWapper is what?
EpollArrayWapper将Linux的epoll相关系统调用封装成了native方法供EpollSelectorImpl使用,EpollArrayWapper类中有几个比较重要的Navtive方法:

private native int epollCreate();
private native void epollCtl(int epfd, int opcode, int fd, int events);
private native int epollWait(long pollAddress, int numfds, long timeout,
                                 int epfd) throws IOException;

看起来似乎比较眼熟,没错这三个native方法正是对上述epoll系列系统调用的包装。回忆一下那三个函数的作用:

  • epoll_create: 创建一个epoll fd,并开辟epoll自己的内核高速cache区,建立红黑树,分配好想要的size的内存对象,建立一个list链表,用于存储准备就绪的事件。
  • epoll_ctl: 对新旧事件进行新增修改或者删除
  • epoll_wait: 等待内核返回IO事件

EPollArrayWrapper完成了对epoll文件描述符的构建,以及对linux系统的epoll指令操纵的封装。维护每次selection操作的结果,即epoll_wait结果的epoll_event数组。
EPollArrayWrapper操纵了一个linux系统下epoll_event结构的本地数组。重要的成员变量,英文注释解释的很明确。

// The fd of the epoll driver
private final int epfd;
// The epoll_event array for results from epoll_wait
private final AllocatedNativeObject pollArray;
// Base address of the epoll_event array
private final long pollArrayAddress;
// file descriptors with registration changes pending
private int[] updateDescriptors = new int[INITIAL_PENDING_UPDATE_SIZE];
// 使用数组保存事件变更, 数组的最大长度是MAX_UPDATE_ARRAY_SIZE, 最大64*1024
private final byte[] eventsLow = new byte[MAX_UPDATE_ARRAY_SIZE];
// 超过数组长度的事件会缓存到这个map中,等待下次处理
private Map<Integer,Byte> eventsHigh;
// number of file descriptors with registration changes pending
private int updateCount;
// descriptor is registered with epoll.
private final BitSet registered = new BitSet();

继续分析它的构造方法:

 EPollArrayWrapper() throws IOException {
        this.eventsLow = new byte[MAX_UPDATE_ARRAY_SIZE];
        this.registered = new BitSet();
        this.interrupted = false;
        // creates the epoll file descriptor
        this.epfd = this.epollCreate();
        int allocationSize= NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
        // the epoll_event array passed to epoll_wait
        this.pollArray = new AllocatedNativeObject(allocationSize, true);
        this.pollArrayAddress = this.pollArray.address();
        if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE) {
            this.eventsHigh = new HashMap();
        }
    }

EPoolArrayWrapper构造函数,创建了epoll文件描述符。构建了一个用于存放epoll_wait返回结果的epoll_event数组。epollCreate navative的实现在EPollArrayWrapper.c中:

JNIEXPORT jint JNICALL
Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this)
{
    /*
     * epoll_create expects a size as a hint to the kernel about how to
     * dimension internal structures. We can't predict the size in advance.
     */
     // 这里的size可以不指定,从Linux2.6.8之后,改用了红黑树结构,指定了大小也没啥用
    int epfd = epoll_create(256);
    if (epfd < 0) {
       JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed");
    }
    return epfd;
}

到这里Selector的创建过程完毕,下面分析注册过过程。

2. Resigter过程

下面分析serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT)的底层逻辑

public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException { 
synchronized (regLock) { 
	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; 
	 } 
 }
  1. 如果该channel和selector已经注册过,则直接添加事件和附件
  2. 否则通过selector实现注册过程。继续调用SelectorImp的register方法
  3. addKey(k)是将注册的key添加到socketchannel的成员变量keys[]中

下面继续分析SelectorImp的register方法:

protected final SelectionKey register(AbstractSelectableChannel ch, int ops, Object attachment) { 
	SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
	 k.attach(attachment); 
	 synchronized (publicKeys) { 
		 implRegister(k); 
	 } 
	 k.interestOps(ops);
	 return k; 
  }
  • 以当前channel和selector为参数,初始化SelectionKeyImpl 对象selectionKeyImpl ,并添加附件attachment。
  • implRegister(k)在EPollSelectorImpl类中,作用是将channel注册到epoll中
  • k.interestOps(int) 完成下面两个操作:
    a) 会将注册的感兴趣的事件和其对应的文件描述存储到EPollArrayWrapper对象的eventsLow或eventsHigh中,这是给底层实现epoll_wait时使用的。
    b) 同时该操作还会将设置SelectionKey的interestOps字段,后面做详细分析

先看EPollSelectorImpl.implRegister:

protected void implRegister(SelectionKeyImpl key) {
     SelChImpl channel= key.channel;
     int fd= Integer.valueOf(channel.getFDVal());
     this.fdToKey.put(fd, channel);
     this.pollWrapper.add(fd);
     this.keys.add(fd);
}
  • 将channel对应的fd(文件描述符)和对应的SelectionKey放到fdToKey映射表中。fdToKey是一个map类型的结构,用来保存fd和key的映射关系。
  • 将channel对应的fd(文件描述符)添加到EPollArrayWrapper中,并强制初始化fd的事件为0 ( 强制初始更新事件为0,因为该事件可能存在于之前被取消过的注册中。)
  • 将selectionKey放入到keys集合中。
// Add a file descriptor
void add(int fd) {
   // force the initial update events to 0 as it may be KILLED by a
   // previous registration.
   synchronized (updateLock) {
       assert !registered.get(fd);
       setUpdateEvents(fd, (byte)0, true);
   }
}
/**
 * Sets the pending update events for the given file descriptor. This
 * method has no effect if the update events is already set to KILLED,
 * unless {@code force} is {@code true}.
 */
private void setUpdateEvents(int fd, byte events, boolean force) {
    if (fd < MAX_UPDATE_ARRAY_SIZE) {
        if ((eventsLow[fd] != KILLED) || force) {
            eventsLow[fd] = events;
        }
    } else {
        Integer key = Integer.valueOf(fd);
        if (!isEventsHighKilled(key) || force) {
            eventsHigh.put(key, Byte.valueOf(events));
        }
    }
}

EpollWrapper的add方法内部调用了setUpdateEvents方法,并且把第二个参数事件类型(events)设置为0。在setUpdateEvents中,把fd作为数组的下表,值为事件类型。如果fd大于64*1024,则把fd和事件类型存入eventsHigh中。

SelectorImp的register方法中, k.interestOps(int) 的内部调用了nioInterestOps方法:

public SelectionKey nioInterestOps(int ops) {
  this.channel.translateAndSetInterestOps(ops, this);
  this.interestOps = ops;
  return this;
}
  • translateAndSetInterestOps方法会将注册的感兴趣的事件和其对应的文件描述存储到EPollArrayWrapper对象的eventsLow或eventsHigh中,这是给底层实现epoll_wait时使用的。同时该操作还会将设置SelectionKey的interestOps字段,这是给我们程序员获取使用的。EpollSelectorImpl的setInterest实现:
/**
* Update the events for a given file descriptor
*/
void setInterest(int fd, int mask) {
   synchronized (updateLock) {
       // record the file descriptor and events
       int oldCapacity = updateDescriptors.length;
       if (updateCount == oldCapacity) {
           int newCapacity = oldCapacity + INITIAL_PENDING_UPDATE_SIZE;
           int[] newDescriptors = new int[newCapacity];
           System.arraycopy(updateDescriptors, 0, newDescriptors, 0, oldCapacity);
           updateDescriptors = newDescriptors;
       }
       updateDescriptors[updateCount++] = fd;

       // events are stored as bytes for efficiency reasons
       byte b = (byte)mask;
       assert (b == mask) && (b != KILLED);
       setUpdateEvents(fd, b, false);
   }
}

首先会判断存放fd的数组updateDescriptors是否已满,如果满了,则进行扩容,在原来的基础上加64,然后将fd保存到数组中。然后调用setUpdateEvents,设置fd的事件,在前面已经分析过一次。

3. select

该方法会一直阻塞直到至少一个channel被选择(即,该channel注册的事件发生了为止,除非当前线程发生中断或者selector的wakeup方法被调用。Selector.select方法最终调用的是EPollSelectorImpl的doSelect方法:

protected int doSelect(long paramLong) throws IOException{
	//处理待取消的SelectionKey(调用SelectionKey.cancel()方法取消)
	processDeregisterQueue();
	try {
		begin();
		//调用EPollArrayWrapper的方法epoll获取已经就绪的pollfd
		this.pollWrapper.poll(paramLong);
	} finally {
		end();
	}
	processDeregisterQueue();
	int i = updateSelectedKeys();
	if (this.pollWrapper.interrupted())	{
		this.pollWrapper.putEventOps(this.pollWrapper.interruptedIndex(), 0);
		synchronized (this.interruptLock) {
			this.pollWrapper.clearInterrupted();
			IOUtil.drain(this.fd0);
			this.interruptTriggered = false;
		}
	}
	return i;
}
public final void cancel() {
       synchronized (this) {
           if (valid) {
               valid = false;
               ((AbstractSelector)selector()).cancel(this);
           }
       }
}
  • 调用processDeregisterQueue方法,将cancel的selectionKey从selector中删除,底层会调用epoll_ctl方法移除被epoll所监听的文件描述符;
  • begin和end方法主要是为了处理线程中断,将线程的中断转化为Selector的wakeup方法,避免线程堵塞在IO操作上;
  • 通过fdToKey查找文件描述符对应的SelectionKey,并更新之,

继续看poll的实现:

int poll(long timeout) throws IOException {
        this.updateRegistrations();
        this.updated = this.epollWait(this.pollArrayAddress, NUM_EPOLLEVENTS, timeout, this.epfd);

        for(int i = 0; i < this.updated; ++i) {
            if (this.getDescriptor(i) == this.incomingInterruptFD) {
                this.interruptedIndex = i;
                this.interrupted = true;
                break;
            }
        }
        return this.updated;
    }

updateRegistrations()方法会将已经注册到该selector的fd和事件(eventsLow或eventsHigh)通过调用epollCtl(epfd, opcode, fd, events),注册到linux系统中。具体的实现如下:

/**
 * Update the pending registrations.
 */
private void updateRegistrations() {
    synchronized (updateLock) {
        int j = 0;
        while (j < updateCount) {
            int fd = updateDescriptors[j];
            // 从保存的eventsLow和eventsHigh里取出事件
            short events = getUpdateEvents(fd);
            boolean isRegistered = registered.get(fd);
            int opcode = 0;

            if (events != KILLED) {
                // 判断操作类型以传给epoll_ctl
                // 没有指定EPOLLET事件类型
                if (isRegistered) {
                    opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
                } else {
                    opcode = (events != 0) ? EPOLL_CTL_ADD : 0;
                }
                if (opcode != 0) {
                    // 熟悉的epoll_ctl
                    epollCtl(epfd, opcode, fd, events);
                    if (opcode == EPOLL_CTL_ADD) {
                        registered.set(fd);
                    } else if (opcode == EPOLL_CTL_DEL) {
                        registered.clear(fd);
                    }
                }
            }
            j++;
        }
        updateCount = 0;
    }
}

epollWait就会调用linux底层的epoll_wait方法,并返回在epoll_wait期间有事件触发的entry的个数,至此Selector就说的差不多了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值