JavaNIO类库Selector机制解析

上传人:小** 文档编号:111330469 上传时间:2022-06-20 格式:DOC 页数:13 大小:622KB
返回 下载 相关 举报
JavaNIO类库Selector机制解析_第1页
第1页 / 共13页
JavaNIO类库Selector机制解析_第2页
第2页 / 共13页
JavaNIO类库Selector机制解析_第3页
第3页 / 共13页
点击查看更多>>
资源描述
JavaNIO类库Selector机制解析、八、前言自从J2SE1.4版本以来,JDK发布了全新的I/O类库,简称NIO,其不但引入了全新的高效的I/O机制,同时,也引入了多路复用的异步模式。NIO的包中主要包含了这样几种抽象数据类型:Buffer:包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。Charset:它提供Unicode字符串影射到字节序列以及逆映射的操作。Channels:包含socket,file和pipe三种管道,都是全双工的通道。Selector:多个异步I/O操作集中到一个或多个线程中(可以被看成是Unix中select()函数的面向对象版本)。我的大学同学赵锟在使用NIO类库书写相关网络程序的时候,发现了一些Java异常RuntimeException,异常的报错信息让他开始了对NIO的Selector进行了一些调查。当赵锟对我共享了Selector的一些底层机制的猜想和调查时候,我们觉得这是一件很有意思的事情,于是在伙同赵锟进行过一系列的调查后,我俩发现了很多有趣的事情,于是导致了这篇文章的产生。这也是为什么本文的作者署名为我们两人的原因。先要说明的一点是,赵锟和我本质上都是出身于Unix/Linux/C/C+的开发人员,对于Java,这并不是我们的长处,这篇文章本质上出于对Java的Selector的好奇,因为从表面上来看Selector似乎做到了一些让我们这些C/C+出身的人比较惊奇的事情。面让我来为你讲述一下这段故事。、故事开始:让C+程序员写Java程序!没有严重内存问题,大量丰富的SDK类库,超容易的跨平台,除了在性能上有些微辞,C+出身的程序员从来都不会觉得Java是一件很困难的事情。当然,对于长期习惯于使用操作系统API(系统调用SystemCall)的C/C+程序来说,面对Java中的比较“另类”地操作系统资源的方法可能会略感困惑,但万变不离其宗,只需要对面向对象的设计模式有一定的了解,用不了多长时间,Java的SDK类库也能玩得随心所欲。在使用Java进行相关网络程序的的设计时,出身C/C+的人,首先想到的框架就是多路复用,想到多路复用,Unix/Linux下马上就能让从想到select,poll,epoll系统调用。于是,在看到Java的NIO中的Selector类时必然会倍感亲切。稍加查阅一下SDK手册以及相关例程,不一会儿,一个多路复用的框架便呈现出来,随手做个单元测试,没啥问题,一切和C/C+照旧。然后告诉兄弟们,框架搞定,以后咱们就在Windows上开发及单元测试,完成后到运行环境Unix上集成测试。心中并暗自念到,跨平台就好啊,开发活动都可以跨平台了。然而,好景不长,随着代码越来越多,逻辑越来越复杂。好好的框架居然在Windows上单元测试运行开始出现异常,看着Java运行异常出错的函数栈,异常居然由Selector.open()抛出,错误信息居然是Unabletoestablishloopbackconnection。“Selector.open()居然报loopbackconnection错误,凭什么?不应该啊?open的时候又没有什么loopback的socket连接,怎么会报这个错?”长期使用C/C+的程序当然会对操作系统的调用非常熟悉,虽然Java的虚拟机搞的什么系统调用都不见了,但C/C+的程序员必然要比Java程序敏感许多。三、开始调查:怎么Java这么“傻”!于是,C/C+的老鸟从Systemlnternals上下载ProcessExplorer来查看一下究竟是什么个LoopbackConnectiono果然,打开java运行进程,发现有一些自己连接自己的localhost的TCP/IP链接。于是另一个问题又出现了,“凭什么啊?为什么会有自己和自己的连接?我程序里没有自己连接自己啊,怎么可能会有这样的链接啊?而自己连接自己的端口号居然是些奇怪的端口。”问题变得越来越蹊跷了。难道这都是Selector.open()在做怪?难道Selector.open()要创建一个自己连接自己的链接?写个程序看看:importjava.nio.channels.Selector;importjava.lang.RuntimeException;importjava.lang.Thread;publicclassTestSelectorprivatestaticfinalintMAXSIZE=5;publicstaticfinalvoidmain(Stringargc)Selectorsels=newSelectorMAXSIZE;tryfor(inti=0;iMAXSIZE;+i)selsi=Selector.open();/selsi.close();Thread.sleep(30000);catch(Exceptionex)thrownewRuntimeException(ex);这个程序什么也没有,就是做5次Selector.open(),然后休息30秒,以便我使用ProcessExplorer工具来查看进程。程序编译没有问题,运行起来,在ProcessExplorer中看到下面的对话框:(居然有10个连接,从连接端口我们可以知道,互相连接,如:第一个连第二个,第二个又连第一个)不由得赞叹我们的Java啊,先不说这是不是一件愚蠢的事。至少可以肯定的是,Java在消耗宝贵的系统资源方面,已经可以赶的上某些蠕虫病毒了。如果不信,不妨把上面程序中的那个MAXSIZE的值改成65535试试,不一会你就会发现你的程序有这样的错误了:(在我的XP机器上大约运行到2000个Selector.open()左右)Exceptioninthreadmainjava.lang.RuntimeException:java.io.IOException:UnabletoestablishloopbackconnectionatTest.main(Test.java:18)Causedby:java.io.IOException:Unabletoestablishloopbackconnectionatsun.nio.ch.PipeImpl$Initializer.run(UnknownSource)atjava.security.AccessController.doPrivileged(NativeMethod)atsun.nio.ch.PipeImpl.(UnknownSource)atsun.nio.ch.SelectorProviderImpl.openPipe(UnknownSource)atjava.nio.channels.Pipe.open(UnknownSource)atsun.nio.ch.WindowsSelectorImpl.(UnknownSource)atsun.nio.ch.WindowsSelectorProvider.openSelector(UnknownSource)atjava.nio.channels.Selector.open(UnknownSource)atTest.main(Test.java:15)Causedby:.SocketException:Nobufferspaceavailable(maximumconnectionsreached?):connectatsun.nio.ch.Net.connect(NativeMethod)atsun.nio.ch.SocketChannelImpl.connect(UnknownSource)atjava.nio.channels.SocketChannel.open(UnknownSource).9more四、继续调查:如此跨平台当然,没人像我们这么变态写出那么多的Selector.open(),但这正好可以让我们来明白Java背着大家在干什么事。上面的那些“愚蠢连接”是在Windows平台上,如果不出意外,Unix/Linux下应该也差不多吧。于是我们把上面的程序放在Linux下跑了跑。使用netstat命令,并没有看到自己和自己的Socket连接。貌似在Linux上使用了和Windows不一样的机制?!如果在Linux上不建自己和自己的TCP连接的话,那么文件描述符和端口都会被省下来了,是不是也就是说我们调用65535个Selector.open()的话,应该不会出现异常了。可惜,在实现运行过程序当中,还是一样报错:(大约在400个Selector.open()左右,还不如Windows)Exceptioninthreadmainjava.lang.RuntimeException:java.io.IOException:ToomanyopenfilesatTest1.main(Test1.java:19)Causedby:java.io.IOException:Toomanyopenfilesatsun.nio.ch.IOUtil.initPipe(NativeMethod)atsun.nio.ch.EPollSelectorImpl.(EPollSelectorImpl.java:49)atsun.nio.ch.EPollSelectorProvider.openSelector(EPollSelectorProvider.java:18)atjava.nio.channels.Selector.open(Selector.java:209)atTest1.main(Test1.java:15)我们发现,这个异常错误是“Toomanyopenfiles”,于是我想到了使用lsof命令来查看一下打开的文件。看到了有一些pipe文件,一共5对,10个(当然,管道从来都是成对的)。如下图所示。文件駕辑W缪鞘hdi3nCHiitYtu!hdEnfltfeuitU叨盹/Iib/1d-2.S.l.eo/dev/pts/0Zdev/pts/O/dev/pts/O/nsr/lib打糊打凯牆-&s-uft-l.G.0.O(3/jA学tb:mrra摘论ZjaiT-VIJJjaw25578hchenjavg2557Slichejavai2578hchen.jaira25E78LclieiiJava.2&578抡出壯t,$arjava_25573hchenjawsi2578hcheti伽电hhen.Java25578hchenjaira.25E78licheejawa25578hchen.255TShchetljava2&57Sbchenjava.2557Shchenjava2557Shchen.Java2557ShehsnJava.25578hchen.jmvs25578hchesijava25&73hcJ-enjava25刃呂hchen.hchenJubtrntLi;VJ$Lch&nJ&ubuirtii:缶mcnl2u3r010914S997058222S50210oon-ooooooorT-vrr-Tr-OTJTJTJOT-TCTx1rxTx亠TTxTvh-.J.VTx_EETA*FFOFFOFFOFFOFFO6676G766TG67667oooooooonvoooooo000-0032621326217249262292G227249E6Z3S262373492G24926247340 S63724pipeUipe/aiKn_iDad?:jleventpol1Pipepips/anninode-:Intpol11pupeplr巳AtiFi:evenxpol1pip。Pipe/anofLinode:eirtpal1JPiflepipe/anfin_iiKdeiento!j可见,Selector.open()在Linux下不用TCP连接,而是用pipe管道。看来,这个pipe管道也是自己给自己的。所以,我们可以得出下面的结论:1) Windows下,Selector.open()会自己和自己建立两条TCP链接。不但消耗了两个TCP连接和端口,同时也消耗了文件描述符。2) Linux下,Selector.open。会自己和自己建两条管道。同样消耗了两个系统的文件描述符。估计,在Windows下,Sun的JVM之所以选择TCP连接,而不是Pipe,要么是因为性能的问题,要么是因为资源的问题。可能,Windows下的管道的性能要慢于TCP链接,也有可能是Windows下的管道所消耗的资源会比TCP链接多。这些实现的细节还有待于更为深层次的挖掘。但我们至少可以了解,原来Java的Selector在不同平台上的机制。五、迷惑不解:为什么要自己消耗资源?令人不解的是为什么我们的Java的NewI/O要设计成这个样子?如果说老的I/O不能多路复用,如下图所示,要开N多的线程去挨个侦听每一个Channel(文件描述符),如果这样做很费资源,且效率不高的话。那为什么在新的I/O机制依然需要自己连接自己,而且,还是重复连接,消耗双倍的资源?通过WEB搜索引擎没有找到为什么。只看到N多的人在报BUG,但SUN却没有任何解释。下面一个图展示了,老的IO和新IO的在网络编程方面的差别。看起来NIO的确很好很强大。但似乎比起C/C+来说,Java的这种实现会有一些不必要的开销。SockrtChann日Ireist-srregale-ThreadOldIOSmanythreadsLiook白dcmpeadnngEpoinnmiLtijlesocketsSwK1-SacksKJacketSakatInputSfreiLmInputstrea-Ti1npulStBanSotkelChdnnelScckBEChann已setect1IL111BireadThreadThreadThraarirsa.dSockedSocketSockSocket*珂ewIO,singletlueadaockeiloiLselection六、它山之石:从Apache的Mina框架了解Selector上面的调查没过多长时间,正好同学赵锟的一个同事也在开发网络程序,这位仁兄使用了Apache的Mina框架。当我们把Mina框架的源码研读了一下后。发现在Mina中有这么一个机制:1) Mina框架会创建一个Work对象的线程。2) Work对象的线程的run()方法会从一个队列中拿出一堆Channel,然后使用Selector.select()方法来侦听是否有数据可以读/写。3) 最关键的是,在select的时候,如果队列有新的Channel加入,那么,Selectorselect()会被唤醒,然后重新select最新的Channel集合。4) 要唤醒select方法,只需要调用Selector的wakeup()方法。对于熟悉于系统调用的C/C+程序员来说,一个阻塞在select上的线程有以下三种方式可以被唤醒:1) 有数据可读/写,或出现异常。2) 阻塞时间到,即timeout。3) 收到一个non-block的信号。可由kill或pthread_kill发出。所以,Selector.wakeup()要唤醒阻塞的select,那么也只能通过这三种方法,其中:1) 第二种方法可以排除,因为select一旦阻塞,应无法修改其timeout时间。2) 而第三种看来只能在Linux上实现,Windows上没有这种信号通知的机制。所以,看来只有第一种方法了。再回想到为什么每个Selector.open(),在Windows会建立一对自己和自己的loopback的TCP连接;在Linux上会开一对pipe(pipe在Linux下一般都是成对打开),估计我们能够猜得出来那就是如果想要唤醒select,只需要朝着自己的这个loopback连接发点数据过去,于是,就可以唤醒阻塞在select上的线程了。七、真相大白:可爱的Java你太不容易了使用Linux下的strace命令,我们可以方便地证明这一点。参看下图。图中,请注意下面几点:1) 26654是主线程,之前我输出notifytheselect字符串是为了做一个标记,而不至于迷失在大量的stracelog中。2) 26662是侦听线程,也就是select阻塞的线程。3) 图中选中的两行。26654的write正是wakeup()方法的系统调用,而紧接着的就是26662的epoll_wait的返回。文件輪挹查春续端标签蒂励Of/jayahdhanJOLixnytLi;*j萍再2S6S1get.timeofcly(120656788,S631SO,EJULLJ=0:茨拠1gettinieofday(1206567S61,呂63792hNULL)二0.26651cloci.gettimeCCLra.RELIIWE,120656TB64,0643T05S6J-)=0266S1futex25G54,futesre&uoie)-1HfHEDOUT(Connecticntinedout26S54futex0k80S98dO?FUTB_WAKE?1)=0266&4cl(CLOCLMDTONIC,3S732r773994559)=026654writed,notifytheseleet?F,17)土1736654write(5RW1unfinisfiedg6E5EJ、卸oH_wait劭nmedEFDLUtb(谄羽已u6辰侶082T795525413352適h1伍乂-心三12fi654vr讥g1)=1255&4i.writeresumed=1:26E523et-timeo-fday(tmfijiishiel2G654TunapSTdaiOOO,12288.PROT.REtD|PROfT.WRIIT|PROT.EJCEC.W_PRIVATE|JIAP.FIJO|JIAP_AUDM|7MDIEJWNQEESEBVE,-1,02662.gattine&f-dajjresusie(12065GT7ES4,3S757E),HULL)=0266M、-0i1da4000.266S2resiiJ(S.266&4rt_sigpKJcrk(JSG.SEDMSKpL26562飞T爲128)=126654J、.rt_sjgprocmaftrssmiEcI-NULLt=0Iit-12G6G2vrite(l?dselectreturn,ISunfiiii51Kd从上图可见,这和我们之前的猜想正好一样。可见,JDK的Selector自己和自己建的那些TCP连接或是pipe,正是用来实现Selector的notify和wakeup的功能的。这两个方法完全是来模仿Linux中的的kill和pthread_kill给阻塞在select上的线程发信号的。但因为发信号这个东西并不是一个跨平台的标准(pthread_kill这个系统调用也不是所有Unix/Linux都支持的),而pipe是所有的Unix/Linux所支持的,但Windows又不支持,所以,Windows用了TCP连接来实现这个事。关于Windows,我一直在想,Windows的防火墙的设置是不是会让Java的类似的程序执行异常呢?呵呵。如果不知道Java的SDK有这样的机制,谁知道会有多少个程序为此引起的问题度过多少个不眠之夜,尤其是Java程序员。八、后记文章到这里是可以结束了,但关于JavaNIO的Selector引出来的其它话题还有许多,比如关于GNU的Java编译器又是如何,它是否会像Sun的Java解释器如此做傻事?我在这里先卖一个关子,关于GNU的Java编译器,我会在另外一篇文章中讲述,近期发布,敬请期待。关于本文中所使用的实验平台如下:Windows:WindowsXP+SP2,SunJ2SE(build1.7.0-ea-b23)Linux:Ubuntu7.10+LinuxKernel2.6.22-14-generic,J2SE(build1.6.0_03-b05)本文主要的调查工作由我的大学同学赵锟完成,我帮其验证调查成果及猜想。在此也向大家介绍我的大学同学赵锟,他也是一个技术高手,在软件开发方面,特别是Unix/LinuxC/C+方面有着相当的功底,相信自此以后,会有很多文章会由我和他一同发布。JavaNIO类库Selector机制解析(续)在前些天的JavaNIO类库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的gj以及编译器gcj,不但可以比较高效地运行Java程序,而且还可以把Java程序直接编译成可执行文件。GNU的之所以要重做一个Java的编译和解释器,其一个重要原因就是想解释Sun的JVM的效率和资源耗费问题。当然,GNU的Java编译/解释器并不需要考虑太多复杂的平台,他们只需要专注于Linux和衍生自UnixSystemV的操作系统,对于开发人员来说,离开了Windows,一切都会变得简单起来。在这里,让我们看看GNU的gij是如何解释Selector.open()和Selector.wakeup()的。同样,我们需要一个测试程序。在这里,为了清晰,我不会例出所有的代码,我只给出我所使用的这个程序的一些关键代码。我的这个测试程序中,和所有的Socket程序一样,下面是一个比较标准的框架,当然,这个框架应该是在一个线程中,也就是一个需要继承Runnable接口,并实现run()方法的一个类。(注意:其中的s是一个成员变量,是Selector类型,以便主线程序使用)/生成一个侦听端ServerSocketChannelssc=ServerSocketChannel.open();/将侦听端设为异步方式ssc.configureBlocking(false);/生成一个信号监视器s=Selector.open();/侦听端绑定到一个端口ssc.socket().bind(newInetSocketAddress(port);/设置侦听端所选的异步信号OP_ACCEPTssc.register(s,SelectionKey.OP_ACCEPT);System.out.println(echoserverhasbeensetup);while(true)intn=s.select();if(n=0)/没有指定的I/O事件发生continue;Iteratorit=s.selectedKeys().iterator();while(it.hasNext()SelectionKeykey=(SelectionKey)it.next();if(key.isAcceptable()/侦听端信号触发if(key.isReadable()/某socket可读信号it.remove();而在主线程中,我们可以通过Selector.wakeup()来唤醒这个阻塞在select()上的线程,下面是写在主线程中的唤醒程序:newThread(this).start();try/Sleep30secondsThread.sleep(30000);System.out.println(wakeuptheselect);s.wakeup();catch(Exceptione)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命令来跟踪整个程序运行时的系统调用,我们利用我们的输出的“wakeuptheselect字符串快速的找到了wakeup的实际系统调用。果然,我们可可以看到,tgkill(5829,5831,SIGHUP)这个系统调用,第一个参数是“源线程id”,第二个参数是“目的线程id”,第三个参数是“信号SIGHUP”。通过每一行前面的线程号我们可以看到紧接着tgkill后面的5831线程的“selectresumed”字样。可见,GNU的确是使用最为传统的pthread_kill或kill系统调用向阻塞线程发信号的方法来实现Selector.wakeup()的,这也证明了GNU的Java编译/解释器是不会消耗系统文件描述符的。而我们也终于看到了回归经典的Java实现机制。
展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 办公文档 > 解决方案


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!