上一篇文章中描述了使用系统调用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是如果实现唤醒的呢?关于这个问题,放在下一篇文章中描述。