回顾一下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.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;
}
}
- 如果该channel和selector已经注册过,则直接添加事件和附件
- 否则通过selector实现注册过程。继续调用SelectorImp的register方法
- 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就说的差不多了。