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 < SelectionKey > selectedKeys = selector.selectedKeys();
- Iterator < SelectionKey > 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()) {
- // 读数据的处理
- }
- }
- }
分析这段代码之前, 先搞清楚 selectorSelectionKeypollArray 等几个数据结构以及相互持有关系
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 < SelectionKey > selectedKeys;
selectedKeys 是一个集合, 代表 poll 系统调用后返回的所有就绪事件, 里面存放的数据结构是 SelectionKey
- final SelChImpl channel; // package-private
- public final SelectorImpl 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;interestOpsreadyOps 与 pollArray 里面的 eventOpsreventOps 对应
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; // 文件描述符移除
- @Native static final short POLLCONN = 0x0002; // 可连接
我记得 POLLCONN 在之前的版本中直接被赋值成 POLLOUT, 这里改成了 0x0002, 这里我是真不知道为什么希望高手来回复一下
最终这些事件都会传递到内核的 poll 系统调用, 去监控所有传递给 poll 的文件描述符
回到之前的 NIO 代码
1 先看看 servChannel.register(selector, SelectionKey.OP_ACCEPT) 是如何实现注册的
一路调用后, 会到一个关键方法
- protected final SelectionKey register(AbstractSelectableChannel ch,
- int ops,
- Object attachment)
- {
- if (!(ch instanceof SelChImpl))
- throw new IllegalSelectorException();
- SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
- k.attach(attachment);
- synchronized (publicKeys) {
- implRegister(k); // 这一步把 channel 的文件描述符 fd 添加到 pollArray(见上图)
- }
- k.interestOps(ops); // 这一步把感兴趣事件 eventOps 添加到 pollArray(见上图)
- return k;
- }
具体的逻辑肯定比注释要复杂接下来看看 pollArray 的内存操作, 以添加文件描述符 fd 为例
- void putDescriptor(int i, int fd) {
- int offset = SIZE_POLLFD * i + FD_OFFSET;
- pollArray.putInt(offset, fd);
- }
- final void putInt(int offset, int value) {
- unsafe.putInt(offset + address, value);
- }
最终还是用 unsafe 直接修改内存
2 再看看最核心的 selector.select(1000) 次方法最终调用 doSelect 方法, 而 doSelect 方法的实现有多种, 我们就以 poll 版本进行探秘
- // 做了很多删减
- protected int doSelect(long timeout)
- throws IOException
- {
- // 执行最核心的 poll 系统调用
- pollWrapper.poll(totalChannels, 0, timeout);
- // 将到来的就绪事件更新保存
- int numKeysUpdated = updateSelectedKeys();
- return numKeysUpdated;
- }
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 int updateSelectedKeys() {
- int numKeysUpdated = 0;
- // Skip zeroth entry; it is for interrupts only
- for (int i = channelOffset; i < totalChannels; 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++;
- }
- }
- }
- }
- return numKeysUpdated;
- }
把就绪事件的掩码进行翻译, 感觉就像是 Java 做的一层适配, 让我们用户不用去关注事件掩码等细节
看一下实现这一逻辑的一段代码, 在 ServerSocketChannel 类里面:
- /**
- * Translates native poll revent set into a ready operation set
- */
- public boolean translateReadyOps(int ops, int initialOps,
- 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/pollepoll 等系统调用 Java 层面为每个 selector 开辟了一块内存, 用来保存用户注册的所有 channel 所有感兴趣事件, 并最终当作参数传递给底层的系统调用, 最后将内核返回的结果封装成 selectedKeys 等数据结构
来源: https://www.cnblogs.com/cz123/p/8421660.html