3. 还有挂起的例外情况吗
1 2 9 - 1 4 0 对于例外情况,需检查标志 s o _ o o b m a r k 和 S S _ R E C V A T M A R K 。 直 到 进 程 读 完 数 据流中的同步标记后,例外情况才可能存在。
原来,select调用的底层实现里面,把很多个事件都只是归并进了可读和可写这两种状态。比如在我之前看来,server端的socket已经将连接排队,就代表可连接状态,可是在select看来,这就是可读状态。
有了前面的一些基础,现在上一段Java NIO的代码
//创建一个selector
Selector selector =Selector.open();//创建一个ServerSocketChannel
ServerSocketChannel servChannel =ServerSocketChannel.open();
servChannel.configureBlocking(false);//绑定端口号
servChannel.socket().bind(new InetSocketAddress(8080), 1024);//注册感兴趣事件
servChannel.register(selector, SelectionKey.OP_ACCEPT);//select系统调用
selector.select(1000);
Set selectedKeys =selector.selectedKeys();
Iterator it =selectedKeys.iterator();
SelectionKey key= null;while(it.hasNext()) {
key=it.next();
it.remove();if(key.isValid()) {//处理新接入的请求消息
if(key.isAcceptable()) {
ServerSocketChannel ssc=(ServerSocketChannel) key.channel();//接收客户端的连接,并创建一个SocketChannel
SocketChannel sc =ssc.accept();
sc.configureBlocking(false);//将SocketChannel和感兴趣事件注册到selector
sc.register(selector, SelectionKey.OP_READ);
}if(key.isReadable()) {//读数据的处理
}
}
}
分析这段代码之前,先搞清楚selector、SelectionKey、pollArray等几个数据结构以及相互持有关系。
pollArray干的是数组的活,但是并不是一个直接的数组。
selector诞生的时候,随之关联了一块内存(pollArray),然后用unsafe类来小心翼翼的按字节顺序写入数据,最终实现了数组结构的功能。这种看似怪异的实现方式,应该是处于效率的考虑吧。
selector并没有直接持有pollArray,而是持有一个pollArray的封装类PollArrayWrapper的引用。
//The poll fd array
PollArrayWrapper pollWrapper; //在selector的父类里面//The set of keys with data ready for an operation
protected Set selectedKeys;
selectedKeys是一个集合,代表poll系统调用后返回的所有就绪事件,里面存放的数据结构是SelectionKey。
final SelChImpl channel; //package-private
public finalSelectorImpl selector;//Index for a pollfd array in Selector that this key is registered with
private int index; //pollArray里面的索引值,保存在这里是方便实现数组操作
private volatile int interestOps; //注册的感兴趣事件掩码
private int readyOps; //就绪事件掩码
SelectionKey不但持有channel,还持有selector;interestOps、readyOps与pollArray里面的eventOps、reventOps对应。
Java定义了一些针对文件描述符的事件,其实也是对底层操作系统poll定义的事件的一个映射。事件用掩码来表示,非常方便进行位操作。如下:
public static final short POLLIN = 0x0001; //文件描述符可读
public static final short POLLOUT = 0x0004; //文件描述符可写
public static final short POLLERR = 0x0008; //文件描述符出现错误
public static final short POLLHUP = 0x0010; //文件描述符挂断
public static final short POLLNVAL = 0x0020; //文件描述符不对
public static final short POLLREMOVE = 0x0800; //文件描述符移除
@Nativestatic final short POLLCONN = 0x0002; //可连接
我记得POLLCONN在之前的版本中直接被赋值成POLLOUT,这里改成了0x0002,这里我是真不知道为什么。希望高手来回复一下。
最终这些事件都会传递到内核的poll系统调用,去监控所有传递给poll的文件描述符。
回到之前的NIO代码
1、先看看 servChannel.register(selector, SelectionKey.OP_ACCEPT) 是如何实现注册的
一路调用后,会到一个关键方法
protected finalSelectionKey register(AbstractSelectableChannel ch,intops,
Object attachment)
{if (!(ch instanceofSelChImpl))throw newIllegalSelectorException();
SelectionKeyImpl k= new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);synchronized(publicKeys) {
implRegister(k);//这一步把channel的文件描述符fd添加到pollArray(见上图)
}
k.interestOps(ops);//这一步把感兴趣事件eventOps添加到pollArray(见上图)
returnk;
}
具体的逻辑肯定比注释要复杂。接下来看看pollArray的内存操作,以添加文件描述符fd为例
void putDescriptor(int i, intfd) {int offset = SIZE_POLLFD * i +FD_OFFSET;
pollArray.putInt(offset, fd);
}final void putInt(int offset, intvalue) {
unsafe.putInt(offset+address, value);
}
最终还是用unsafe直接修改内存
2、再看看最核心的selector.select(1000)。次方法最终调用doSelect方法,而doSelect方法的实现有多种,我们就以poll版本进行探秘
//做了很多删减
protected int doSelect(longtimeout)throwsIOException
{//执行最核心的poll系统调用
pollWrapper.poll(totalChannels, 0, timeout);//将到来的就绪事件更新保存
int numKeysUpdated =updateSelectedKeys();returnnumKeysUpdated;
}
poll系统调用会把用户空间的线程挂起,也就是阻塞调用,timeout指定多长时间后必须返回。
updateSelectedKeys方法根据poll返回的channel就绪事件,去更新pollArray对应fd的reventOps(见上图),以及selector的selectedKeys。
/*** Copy the information in the pollfd structs into the opss
* of the corresponding Channels. Add the ready keys to the
* ready queue.*/
protected intupdateSelectedKeys() {int numKeysUpdated = 0;//Skip zeroth entry; it is for interrupts only
for (int i=channelOffset; i
int rOps =pollWrapper.getReventOps(i);if (rOps != 0) {
SelectionKeyImpl sk=channelArray[i];
pollWrapper.putReventOps(i,0); //重置为0,即为未就绪
if(selectedKeys.contains(sk)) {//把事件的掩码翻译成SelectionKey中定义的操作(OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT)
if(sk.channel.translateAndSetReadyOps(rOps, sk)) {
numKeysUpdated++;
}
}else{
sk.channel.translateAndSetReadyOps(rOps, sk);if ((sk.nioReadyOps() & sk.nioInterestOps()) != 0) {//更新selectedKeys
selectedKeys.add(sk);
numKeysUpdated++;
}
}
}
}returnnumKeysUpdated;
}
把就绪事件的掩码进行翻译,感觉就像是Java做的一层适配,让我们用户不用去关注事件掩码等细节
看一下实现这一逻辑的一段代码,在ServerSocketChannel类里面:
/*** Translates native poll revent set into a ready operation set*/
public boolean translateReadyOps(int ops, intinitialOps,
SelectionKeyImpl sk) {int intOps = sk.nioInterestOps(); //Do this just once, it synchronizes
int oldOps =sk.nioReadyOps();int newOps =initialOps;if ((ops & PollArrayWrapper.POLLNVAL) != 0) {//This should only happen if this channel is pre-closed while a//selection operation is in progress//## Throw an error if this channel has not been pre-closed
return false;
}if ((ops &(PollArrayWrapper.POLLERR| PollArrayWrapper.POLLHUP)) != 0) {
newOps=intOps;
sk.nioReadyOps(newOps);return (newOps & ~oldOps) != 0;
}//这里将可连接当作可读来看待的
if (((ops & PollArrayWrapper.POLLIN) != 0) &&((intOps& SelectionKey.OP_ACCEPT) != 0))
newOps|=SelectionKey.OP_ACCEPT;
sk.nioReadyOps(newOps);return (newOps & ~oldOps) != 0;
}
通过上面的分析,大概有了一个清晰的思路:
Java NIO主要是基于底层操作系统提供的的IO多路复用功能,比如Linux下的select/poll、epoll等系统调用。Java层面为每个selector开辟了一块内存,用来保存用户注册的所有channel、所有感兴趣事件,并最终当作参数传递给底层的系统调用,最后将内核返回的结果封装成selectedKeys等数据结构。