from:http://haoel.blog.51cto.com/313033/124582
一、 前言
自從J2SE 1.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(系統調用System Call)的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()拋出,錯誤資訊居然是Unable to establish loopback connection。
“Selector.open()居然報loopback connection錯誤,憑什嗎?不應該啊?open的時候又沒有什麼loopback的socket串連,怎麼會報這個錯?”
長期使用C/C++的程式當然會對作業系統的調用非常熟悉,雖然Java的虛擬機器搞的什麼系統調用都不見了,但C/C++的程式員必然要比Java程式敏感許多。
三、 開始調查 : 怎麼Java這麼“傻”!
於是,C/C++的老鳥從SystemInternals上下載Process Explorer來查看一下究竟是什麼個Loopback Connection。 果然,開啟java運行進程,發現有一些自己串連自己的localhost的TCP/IP連結。於是另一個問題又出現了,
“憑什麼啊?為什麼會有自己和自己的串連?我程式裡沒有自己串連自己啊,怎麼可能會有這樣的連結啊?而自己串連自己的連接埠號碼居然是些奇怪的連接埠。”
問題變得越來越蹊蹺了。難道這都是Selector.open()在做怪?難道Selector.open()要建立一個自己串連自己的連結?寫個程式看看:
import java.nio.channels.Selector;
import java.lang.RuntimeException;
import java.lang.Thread;
public class TestSelector {
private static final int MAXSIZE=5;
public static final void main( String argc[] ) {
Selector [] sels = new Selector[ MAXSIZE];
try{
for( int i = 0 ;i< MAXSIZE ;++i ) {
sels[i] = Selector.open();
//sels[i].close();
}
Thread.sleep(30000);
}catch( Exception ex ){
throw new RuntimeException( ex );
}
}
}
這個程式什麼也沒有,就是做5次Selector.open(),然後休息30秒,以便我使用Process Explorer工具來查看進程。程式編譯沒有問題,運行起來,在Process Explorer中看到下面的對話方塊:(居然有10個串連,從串連連接埠我們可以知道,互相串連, 如:第一個連第二個,第二個又連第一個)
不由得讚歎我們的Java啊,先不說這是不是一件愚蠢的事。至少可以肯定的是,Java在消耗寶貴的系統資源方面,已經可以趕的上某些蠕蟲了。
如果不信,不妨把上面程式中的那個MAXSIZE的值改成65535試試,不一會你就會發現你的程式有這樣的錯誤了:(在我的XP機器上大約運行到2000個Selector.open() 左右)
Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Unable to establish loopback connection
at Test.main(Test.java:18)
Caused by: java.io.IOException: Unable to establish loopback connection
at sun.nio.ch.PipeImpl$Initializer.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at sun.nio.ch.PipeImpl.<init>(Unknown Source)
at sun.nio.ch.SelectorProviderImpl.openPipe(Unknown Source)
at java.nio.channels.Pipe.open(Unknown Source)
at sun.nio.ch.WindowsSelectorImpl.<init>(Unknown Source)
at sun.nio.ch.WindowsSelectorProvider.openSelector(Unknown Source)
at java.nio.channels.Selector.open(Unknown Source)
at Test.main(Test.java:15)
Caused by: java.net.SocketException: No buffer space available (maximum connections reached?): connect
at sun.nio.ch.Net.connect(Native Method)
at sun.nio.ch.SocketChannelImpl.connect(Unknown Source)
at java.nio.channels.SocketChannel.open(Unknown Source)
... 9 more
四、 繼續調查 : 如此跨平台
當然,沒人像我們這麼變態寫出那麼多的Selector.open(),但這正好可以讓我們來明白Java背著大家在幹什麼事。上面的那些“愚蠢串連”是在Windows平台上,如果不出意外,Unix/Linux下應該也差不多吧。
於是我們把上面的程式放在Linux下跑了跑。使用netstat 命令,並沒有看到自己和自己的Socket串連。貌似在Linux上使用了和Windows不一樣的機制?!
如果在Linux上不建自己和自己的TCP串連的話,那麼檔案描述符和連接埠都會被省下來了,是不是也就是說我們調用65535個Selector.open()的話,應該不會出現異常了。
可惜,在實現運行過程式當中,還是一樣報錯:(大約在400個Selector.open()左右,還不如Windows)
Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Too many open files
at Test1.main(Test1.java:19)
Caused by: java.io.IOException: Too many open files
at sun.nio.ch.IOUtil.initPipe(Native Method)
at sun.nio.ch.EPollSelectorImpl.<init>(EPollSelectorImpl.java:49)
at sun.nio.ch.EPollSelectorProvider.openSelector(EPollSelectorProvider.java:18)
at java.nio.channels.Selector.open(Selector.java:209)
at Test1.main(Test1.java:15)
我們發現,這個異常錯誤是“Too many open files”,於是我想到了使用lsof命令來查看一下開啟的檔案。
看到了有一些pipe檔案,一共5對,10個(當然,管道從來都是成對的)。如所示。
可見,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的New I/O要設計成這個樣子?如果說老的I/O不能多工,如所示,要開N多的線程去挨個偵聽每一個Channel (檔案描述符) ,如果這樣做很費資源,且效率不高的話。那為什麼在新的I/O機制依然需要自己串連自己,而且,還是重複串連,消耗雙倍的資源?
通過WEB搜尋引擎沒有找到為什麼。只看到N多的人在報BUG,但SUN卻沒有任何解釋。
下面一個圖展示了,老的IO和新IO的在網路編程方面的差別。看起來NIO的確很好很強大。但似乎比起C/C++來說,Java的這種實現會有一些不必要的開銷。
六、 它山之石 : 從Apache的Mina架構瞭解Selector
上面的調查沒過多長時間,正好同學趙錕的一個同事也在開發網路程式,這位仁兄使用了Apache的Mina架構。當我們把Mina架構的源碼研讀了一下後。發現在Mina中有這麼一個機制:
1)Mina架構會建立一個Work對象的線程。
2)Work對象的線程的run()方法會從一個隊列中拿出一堆Channel,然後使用Selector.select()方法來偵聽是否有資料可以讀/寫。
3)最關鍵的是,在select的時候,如果隊列有新的Channel加入,那麼,Selector.select()會被喚醒,然後重新select最新的Channel集合。
4)要喚醒select方法,只需要調用Selector的wakeup()方法。
對於熟悉於系統調用的C/C++程式員來說,一個阻塞在select上的線程有以下三種方式可以被喚醒:
1) 有資料可讀/寫,或出現異常。
2) 阻塞時間到,即time out。
3) 收到一個non-block的訊號。可由kill或pthread_kill發出。
所以,Selector.wakeup()要喚醒阻塞的select,那麼也只能通過這三種方法,其中:
1)第二種方法可以排除,因為select一旦阻塞,應無法修改其time out時間。
2)而第三種看來只能在Linux上實現,Windows上沒有這種訊號通知的機制。
所以,看來只有第一種方法了。再回想到為什麼每個Selector.open(),在Windows會建立一對自己和自己的loopback的TCP串連;在Linux上會開一對pipe(pipe在Linux下一般都是成對開啟),估計我們能夠猜得出來——那就是如果想要喚醒select,只需要朝著自己的這個loopback串連發點資料過去,於是,就可以喚醒阻塞在select上的線程了。
七、 真相大白 : 可愛的Java你太不容易了
使用Linux下的strace命令,我們可以方便地證明這一點。參看。圖中,請注意下面幾點:
1) 26654是主線程,之前我輸出notify the select字串是為了做一個標記,而不至於迷失在大量的strace log中。
2) 26662是偵聽線程,也就是select阻塞的線程。
3) 圖中選中的兩行。26654的write正是wakeup()方法的系統調用,而緊接著的就是26662的epoll_wait的返回。
從可見,這和我們之前的猜想正好一樣。可見,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程式員。
八、 後記
文章到這裡是可以結束了,但關於Java NIO的Selector引出來的其它話題還有許多,比如關於GNU 的Java編譯器又是如何,它是否會像Sun的Java解譯器如此做傻事?我在這裡先賣一個關子,關於GNU的Java編譯器,我會在另外一篇文章中講述,近期發布,敬請期待。
關於本文中所使用的實驗平台如下:
· Windows:Windows XP + SP2, Sun J2SE (build 1.7.0-ea-b23)
· Linux:Ubuntu 7.10 + Linux Kernel 2.6.22-14-generic, J2SE (build 1.6.0_03-b05)
本文主要的調查工作由我的大學同學趙錕完成,我幫其驗證調查成果及猜想。在此也向大家介紹我的大學同學趙錕,他也是一個技術高手,在軟體開發方面,特別是Unix/Linux C/C++方面有著相當的功底,相信自此以後,會有很多文章會由我和他一同發布。
本篇文章由我成文。但其全部著作權和著作權歸趙錕和我共同所有。我們歡迎大家轉載,但希望保持整篇文章的完整性,並請勿用於任何商業用途。謝謝。
相關文章:Java NIO 類庫Selector機制解析(續)http://haoel.blog.51cto.com/313033/124570
Technorati 標籤: Java,NIO,MINA,Selector