Java NIO之Selector源碼深度剖析(基於linux Epoll)

前言

Java NIO之Selector源碼深度剖析(基於linux Epoll)

Java NIO對於Channel fd的處理在不同的操作系統,同一操作系統的不同版本上各不相同。不過傳統的IO就緒方法無非就是select/poll、epoll等。Java NIO底層的IO就緒通知在windows上使用select系統調用,在linux kernel版本小於2.6使用poll,kernel >=2.6則使用的是epoll。而epoll可以說是linux2.6公認性能最好的IO就緒通知方法。

下面來一探究竟Java NIO之Selector在linux上的神秘面紗。

源碼分析

Java NIO的核心是Selector,Selector在設計上是一個單例,如果沒有指SelectorProvider,則使用操作系統默認的SelectorProvider。

Selector實例的操作如下:

Selector selector = Selector.open();

Selector.open()如下:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

SelectorProvider.provider()如下:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

DefaultSelectorProvider.create()如下:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

從上面可知,當Linux 內核版本大小或等於2.6時,會使用EpollSelectorPrivider,如果低於2.6使用使用PollSelectorProvider,如果是SunOS,則使用DevPollSelectorProvider,windows則使用WindowsSelectorProvider。

下面繼續看EpollSelectorProvider的openSelector()方法:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

EpollSelectorImpl構造方法如下:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

從EpollSlectorImpl的構造方法可以看出,Selector的所有初始化都在此進行,下面一個一個地剖析在此構造方法裡面都做了些什麼。

1、首先調用的是super(sp),EpollSlectorImpl的父類為:SelectorImpl。super(sp)如下:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

相關的實例變量為:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

SelectedKeys:存儲IO就緒的fd對應的SelectionKey

keys:存儲所以Channel對應的SelectionKey

publickeys:一個不可變的集合(後續會講到)

publicSelectedKeys:可以進行移除,但不能進行添加的集合(後續會講到)

可以看出SelectorImpl的構造方法主要是實例化selectedKeys和keys等成員變量SelectorImpl的構造方法super(sp)的實現只是簡單地設置下SelectorPovider成員變量。

2、創建管道,看IOUtil的makePile是一個Native方法,對應的實現在openjdk/jdk/src/solaris/native/sun/nio/ch/IOUtil.c

Java NIO之Selector源碼深度剖析(基於linux Epoll)

可以看出在makePipe中通過調用操作pipe系統調用而創建一個管道,當返回0時證明創建成功,管道的讀取端為fd[0],管道的寫入端為fd[1],然後通過將fd[0]寫入到long的高32位,將fd[1]寫入到long的低32位,然後返回此long值到java層。(關於管道的相關知識,請查詢操作系統相關資料)

3、重點看下pollWrapper的實例化(pollWrapper就是一個封裝了操作fd集合的一些方法)

Java NIO之Selector源碼深度剖析(基於linux Epoll)

3.1:調用epollCreate()創建一個epoll句柄,epollCreate是一個native方法,此處就不給出它的native實現了,這個方法有一個參數size,用來說明epoll fd上能關注的最大socket fd數不過在kernel大於2.6.8是無效的,epll_create會返回一個代表些epoll的一個fd值,所以有使用完epoll後,需要關閉epoll。

3.2:計算需要申請的空間大小,該空間主要用於後續通過epoll_wait調用告知內核將就緒的epoll_event回存到該內存空間。

計算方式為:

NUM_EPOLLEVENTS + SIZE_EPOLLEVENT

NUM_EPOLLEVENTS:epoll_event的數量

NUM_EPOLLEVENTS = Math.min(fdLimit(), 8192); 取fdLimit()與8192的最小值

SIZE_EPOLLEVENT:每個epoll_event佔用內存空間的大小,通過sizeof計算得到

SIZE_EPOLLEVENT = sizeofEPollEvent()

sizeofEPollEvent()是一個native方法:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

其實就是計算epoll_event結構體的sizeof運算值。

總的來說,就是計算所有的epoll_event所需要的內存空間大小。

3.3:申請內存。

Java NIO之Selector源碼深度剖析(基於linux Epoll)

pageAligned設置為true,證明申請到的內存返回的地址要在操作系統pageSize自然邊界上對齊。pageSize()是一個native方法,主要是獲取操作系統的物理頁大小,linux默認為4k(4096字節)。

下面來分析一下如何將返回地址在pageSize上對齊。

首先申請的內存大小為size + ps,為什麼不是size?因為如果返回的地址為不是pageSize對齊的,(所謂pageSize對齊就是說address % pageSize == 0)那麼就需要調整address到距離該地址的下一個4k對齊的地址。

那麼如果不加上ps,那麼到時否操作該塊內存時就會變小,少了的內存大小為 下一個4k地址 減去 address。

而如果加上了ps頁大小,則ps肯定是會大於(下一個4k地址 減去 address)的,所以可使用的空間只會變大不會變小。

a & (ps -1) 為a的地址值與前一個4k地址的偏移量(相當於a地址%前一個4k地址的值,餘數),a + ps 的值為(a的下一個4k地址 + (a地址%前一個4k地址的值))

所以a + ps - (a & (ps -1))剛好為為a的下一個4k地址,賦值給address,所以address為epoll_event數組的基地址。

回到EpollArrayWrapper構造方法中,也即是pollArrayAddress即為epoll_event數組的基址。

3.4:初始化剛才申請到的epoll_event數組,將epoll_event中的events、和epll_data都初始化為0。

Java NIO之Selector源碼深度剖析(基於linux Epoll)

EVENT_OFFSET:結構體成員events的偏移量,默認為0。

DATA_OFFSET:結構體成員epoll_data的偏移量

 計算方式為:static final int \tDATA_OFFSET = offsetofData();

offsetofData為native方法:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

可以看出底層是通過offsetof函數來計算出epll_data的偏移量的。offsetof:用於求結構體中一個成員在該結構體中的偏移量pollArray就是代表的就是底層的epoll_event數組了。

底層都是調用unsafe的相對應方法:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

可以看到此處的address作為基地址用到了。

3.5:初始化空閒的fd集合

回到EpollSlectorImpl的構造方法中。

通過調用pollWrapper.initInterrupt(fd0, fd1);初始化管道兩端。

Java NIO之Selector源碼深度剖析(基於linux Epoll)

可以看到此方法裡面對fd0也就是管道讀取端進行了epoll_ctl註冊,為什麼是要註冊fd0呢?因為fd0作為讀取端的fd,可讀取到write端的數據。(後續解釋)

epollCtl方法是一個native方法,具體實現在:EpollArrayWrapper.c中

Java NIO之Selector源碼深度剖析(基於linux Epoll)

epll_ctl就是將Channel對應的fd,及相應的感興趣事件註冊到epoll中,epoll_ctl_func是一個函數指針。

Java NIO之Selector源碼深度剖析(基於linux Epoll)

原來 build jdk是原來是基於官方的2.4版本,但是2.6版本才有epoll的通知,所以只能通過獲取epoll函數的入口地址才能調用。

入口地址的獲取:

Java NIO之Selector源碼深度剖析(基於linux Epoll)

可以看到是通過dlsym函數,動態鏈接RTLD_DEFAULT庫,然後獲取epoll對應的三個函數的入口地址的。

最後就是初始化fdtoKey。也就是fd到SelectionKey的映射,一個HashMap結果,key為fd,value為SelectionKeyImpl。

至此,整個Selector已經實例化好了。


分享到:


相關文章: