在前些天的《 Java NIO 类库 Selector 机制解析 》文章中,我们知道了下面的事情:
1) Sun 的 JVM 在实现 Selector 上,在 Linux 和 Windows 平台下的细节。
2) Selector 类的 wakeup() 方法如何唤醒阻塞在 select() 系统调用上的细节。
先给大家做一个简单的回顾,在 Windows 下, Sun 的 Java 虚拟机在 Selector.open() 时会自己和自己建立 loopback 的 TCP 链接;在 Linux 下, Selector 会创建 pipe 。这主要是为了 Selector.wakeup() 可以方便唤醒阻塞在 select() 系统调用上的线程(通过向自己所建立的 TCP 链接和管道上随便写点什么就可以唤醒阻塞线程)
我们知道,无论是建立 TCP 链接还是建立管道都会消耗系统资源,而在 Windows 上,某些 Windows 上的防火墙设置还可能会导致 Java 的 Selector 因为建立不起 loopback 的 TCP 链接而出现异常。
而在我的另一篇文章《 用 GDB 调试 Java 程序 》中介绍了另一个 Java 的解释器—— GNU 的 gij ,以及编译器 gcj ,不但可以比较高效地运行 Java 程序,而且还可以把 Java 程序直接编译成可执行文件。
GNU 的之所以要重做一个 Java 的编译和解释器,其一个重要原因就是想解释 Sun 的 JVM 的效率和资源耗费问题。当然, GNU 的 Java 编译 / 解释器并不需要考虑太多复杂的平台,他们只需要专注于 Linux 和衍生自 Unix System V 的操作系统,对于开发人员来说,离开了 Windows ,一切都会变得简单起来。在这里,让我们看看 GNU 的 gij 是如何解释 Selector.open() 和 Selector.wakeup() 的。
同样,我们需要一个测试程序。在这里,为了清晰,我不会例出所有的代码,我只给出我所使用的这个程序的一些关键代码。
我的这个测试程序中,和所有的 Socket 程序一样,下面是一个比较标准的框架,当然,这个框架应该是在一个线程中,也就是一个需要继承 Runnable 接口,并实现 run() 方法的一个类。(注意:其中的 s 是一个成员变量,是 Selector 类型,以便主线程序使用)
// 生成一个侦听端
ServerSocketChannel ssc = ServerSocketChannel.open();
// 将侦听端设为异步方式
ssc.configureBlocking(false );
// 生成一个信号监视器
s = Selector.open();
// 侦听端绑定到一个端口
ssc.socket().bind(new InetSocketAddress(port));
// 设置侦听端所选的异步信号 OP_ACCEPT
ssc.register (s,SelectionKey.OP_ACCEPT);
System.out.println("echo server has been set up ......" );
while (true ){
int n = s.select();
if (n == 0 ) { // 没有指定的 I/O 事件发生
continue ;
}
Iterator it = s.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if (key.isAcceptable()) { // 侦听端信号触发
…… …… ……
…… …… ……
}
if (key.isReadable()) { // 某 socket 可读信号
…… …… ……
…… …… ……
}
it.remove();
}
}
而在主线程中,我们可以通过 Selector.wakeup() 来唤醒这个阻塞在 select() 上的线程,下面是写在主线程中的唤醒程序:
new Thread(this ).start();
try {
//Sleep 30 seconds
Thread.sleep(30000 );
System.out.println("wakeup the select ");
s.wakeup();
}catch (Exception e){
e.printStackTrace();
}
这个程序在主线程中,先启动一个线程,也就是上面那个 Socket 线程,然后休息 30 秒,为的是让上面的那个线程有阻塞在 select() ,然后打印出一条信息,这是为了我们用 strace 命令查看具体的系统调用时能够快速定位。之后调用的是 Selector 的 wakeup() 方法来唤醒侦听线程。
接下来,我们可以通过两种方式来编译这个程序:
1) 使用 gcj 或是 sun 的 javac 编译成 class 文件,然后使用 gij 解释执行。
2) 使用 gcj 直接编译成可执行文件。
(无论你用那种方法,都是一样的结果,本文使用第二种方法,关于 gcj 的编译方法,请参看我的《 用 GDB 调试 Java 程序 》)
编译成可执行文件后,执行程序时,使用 lsof 命令,我们可以看到没有任何 pipe 的建立。可见 GNU 的解释更为的节省资源。而对于一个 Unix 的 C 程序员来说,这意味着如果要唤醒 select() 只能使用 pthread_kill() 来发送一个信号了。下面就让我们使用 strace 命令来验证这个想法。
下图是使用 strace 命令来跟踪整个程序运行时的系统调用,我们利用我们的输出的“ wakeup the select ”字符串快速的找到了 wakeup 的实际系统调用。
果然,我们可可以看到, tgkill(5829, 5831, SIGHUP) 这个系统调用,第一个参数是“源线程 id ”,第二个参数是“目的线程 id ”,第三个参数是“信号 SIGHUP” 。通过每一行前面的线程号我们可以看到紧接着 tgkill 后面的 5831 线程的“ … select resumed ”字样。
可见, GNU 的确是使用最为传统的 pthread_kill 或 kill 系统调用向阻塞线程发信号的方法来实现 Selector.wakeup() 的,这也证明了 GNU 的 Java 编译 / 解释器是不会消耗系统文件描述符的。而我们也终于看到了回归经典的 Java 实现机制。