五種I/O模型和Java NIO源碼分析

 最近在學習Java網絡編程和Netty相關的知識,瞭解到Netty是NIO模式的網絡框架,可是提供了不一樣的Channel來支持不一樣模式的網絡通訊處理,包括同步、異步、阻塞和非阻塞。學習要從基礎開始,因此咱們就要先了解一下相關的基礎概念和Java原生的NIO。這裏,就將最近我學習的知識總結一下,以供你們瞭解。linux

 爲了節約你的時間,本文主要內容以下:編程

  • 異步,阻塞的概念
  • 操做系統I/O的類型
  • Java NIO的底層實現

異步,同步,阻塞,非阻塞

同步和異步關注的是消息通訊機制,所謂同步就是調用者進行調用後,在沒有獲得結果以前,該調用一直不會返回,可是一旦調用返回,就獲得了返回值,同步就是指調用者主動等待調用結果;而異步則相反,執行調用以後直接返回,因此可能沒有返回值,等到有返回值時,由被調用者經過狀態,通知來通知調用者.異步就是指被調用者來通知調用者調用結果就緒*.*因此,兩者在消息通訊機制上有所不一樣,一個是調用者檢查調用結果是否就緒,一個是被調用者通知調用者結果就緒數組

阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.阻塞調用是指在調用結果返回以前,當前線程會被掛起,調用線程只有在獲得結果以後纔會繼續執行.非阻塞調用是指在不能馬上獲得結構以前,調用線程不會被掛起,仍是能夠執行其餘事情.bash

 兩組概念相互組合就有四種狀況,分別是同步阻塞,同步非阻塞,異步阻塞,異步非阻塞.咱們來舉個例子來分別類比上訴四種狀況.網絡

 好比你要從網上下載一個1G的文件,按下下載按鈕以後,若是你一直在電腦旁邊,等待下載結束,這種狀況就是同步阻塞;若是你不須要一直呆在電腦旁邊,你能夠去看一會書,可是你仍是隔一段時間來查看一下下載進度,這種狀況就是同步非阻塞;若是你一直在電腦旁邊,可是下載器在下載結束以後會響起音樂來提醒你,這就是異步阻塞;可是若是你不呆在電腦旁邊,去看書,下載器下載結束後響起音樂來提醒你,那麼這種狀況就是異步非阻塞.app

Unix的I/O類型

 知道上述兩組概念以後,咱們來看一下Unix下可用的5種I/O模型:框架

  • 阻塞I/O(bloking IO)
  • 非阻塞I/O(nonblocking IO)
  • 多路複用I/O(IO multiplexing)
  • 信號驅動I/O(signal driven IO)
  • 異步I/O(asynchronous IO)

 前4種都是同步,只有最後一種是異步I/O.須要注意的是***Java NIO依賴於Unix系統的多路複用I/O,對於I/O操做來講,它是同步I/O,可是對於編程模型來講,它是異步網絡調用***.下面咱們就以系統read的調用來介紹不一樣的I/O類型.異步

 當一個read發生時,它會經歷兩個階段:socket

  • 1 等待數據準備
  • 2 將數據從內核內存空間拷貝到進程內存空間中

 不一樣的I/O類型,在這兩個階段中有不一樣的行爲.可是因爲這塊內容比較多,並且多爲表述性的知識,因此這裏咱們只給出幾張圖片來解釋,感受興趣的同窗能夠去具體瞭解一下。 async

阻塞I/O

非阻塞I/O

多路複用I/O

信號驅動

異步I/O

Java NIO的底層實現

 咱們都知道Netty經過JNI的方式提供了Native Socket Transport,爲何Netty要提供本身的Native版本的NIO呢?明明Java NIO底層也是基於epoll調用(最新的版本)的.這裏,咱們先不明說,你們想想可能的狀況.下列的源碼都來自於OpenJDK-8u40-b25版本.

open方法

 若是咱們順着Selector.open()方法一個類一個類的找下去,很容易就發現Selector的初始化是由DefaultSelectorProvider根據不一樣操做系統平臺生成的不一樣的SelectorProvider,對於Linux系統,它會生成EPollSelectorProvider實例,而這個實例會生成EPollSelectorImpl做爲最終的Selector實現.

class EPollSelectorImpl extends SelectorImpl
{
    .....
    // The poll object
    EPollArrayWrapper pollWrapper;
    .....
    EPollSelectorImpl(SelectorProvider sp) throws IOException {
        .....
        pollWrapper = new EPollArrayWrapper();
        pollWrapper.initInterrupt(fd0, fd1);
        .....
    }
    .....
}
複製代碼

EpollArrayWapper將Linux的epoll相關係統調用封裝成了native方法供EpollSelectorImpl使用.

private native int epollCreate();
    private native void epollCtl(int epfd, int opcode, int fd, int events);
    private native int epollWait(long pollAddress, int numfds, long timeout,
                                 int epfd) throws IOException;
複製代碼

 上述三個native方法就對應Linux下epoll相關的三個系統調用

//建立一個epoll句柄,size是這個監聽的數目的最大值.
int epoll_create(int size);
//事件註冊函數,告訴內核epoll監聽什麼類型的事件,參數是感興趣的事件類型,回調和監聽的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待事件的產生,相似於select調用,events參數用來從內核獲得事件的集合
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
複製代碼

 因此,咱們會發如今EpollArrayWapper的構造函數中調用了epollCreate方法,建立了一個epoll的句柄.這樣,Selector對象就算創造完畢了.

register方法

 與open相似,ServerSocketChannelregister函數底層是調用了SelectorImpl類的register方法,這個SelectorImpl就是EPollSelectorImpl的父類.

protected final SelectionKey register(AbstractSelectableChannel ch,
                                      int ops,
                                      Object attachment)
{
    if (!(ch instanceof SelChImpl))
        throw new IllegalSelectorException();
    //生成SelectorKey來存儲到hashmap中,一共以後獲取
    SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
    //attach用戶想要存儲的對象
    k.attach(attachment);
    //調用子類的implRegister方法
    synchronized (publicKeys) {
        implRegister(k);
    }
    //設置關注的option
    k.interestOps(ops);
    return k;
}
複製代碼

EpollSelectorImpl的相應的方法實現以下,它調用了EPollArrayWrapperadd方法,記錄下Channel所對應的fd值,而後將ski添加到keys變量中.在EPollArrayWrapper中有一個byte數組eventLow記錄全部的channel的fd值.

protected void implRegister(SelectionKeyImpl ski) {
        if (closed)
            throw new ClosedSelectorException();
        SelChImpl ch = ski.channel;
        //獲取Channel所對應的fd,由於在linux下socket會被看成一個文件,也會有fd
        int fd = Integer.valueOf(ch.getFDVal());
        fdToKey.put(fd, ski);
        //調用pollWrapper的add方法,將channel的fd添加到監控列表中
        pollWrapper.add(fd);
        //保存到HashSet中,keys是SelectorImpl的成員變量
        keys.add(ski);
    }
複製代碼

 咱們會發現,調用register方法並無涉及到EpollArrayWrapper中的native方法epollCtl的調用,這是由於他們將這個方法的調用推遲到Select方法中去了.

Select方法

 和register方法相似,SelectorImpl中的select方法最終調用了其子類EpollSelectorImpldoSelect方法

protected int doSelect(long timeout) throws IOException {
    .....
    try {
        ....
        //調用了poll方法,底層調用了native的epollCtl和epollWait方法
        pollWrapper.poll(timeout);
    } finally {
        ....
    }
    ....
    //更新selectedKeys,爲以後的selectedKeys函數作準備
    int numKeysUpdated = updateSelectedKeys();
    ....
    return numKeysUpdated;
}
複製代碼

 由上述的代碼,能夠看到,EPollSelectorImpl先調用EPollArrayWapperpoll方法,而後在更新SelectedKeys.其中poll方法會先調用epollCtl來註冊先前在register方法中保存的Channel的fd和感興趣的事件類型,而後epollWait方法等待感興趣事件的生成,致使線程阻塞.

int poll(long timeout) throws IOException {
    updateRegistrations(); ////先調用epollCtl,更新關注的事件類型
    ////致使阻塞,等待事件產生
    updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
    .....
    return updated;
}
複製代碼

 等待關注的事件產生以後(或在等待時間超過預先設置的最大時間),epollWait函數就會返回.select函數從阻塞狀態恢復.

selectedKeys方法

 咱們先來看SelectorImpl中的selectedKeys方法.

//是經過Util.ungrowableSet生成的,不能添加,只能減小
private Set<SelectionKey> publicSelectedKeys;
public Set<SelectionKey> selectedKeys() {
    ....
    return publicSelectedKeys;
}
複製代碼

 很奇怪啊,怎麼直接就返回publicSelectedKeys了,難道在select函數的執行過程當中有修改過這個變量嗎?

publicSelectedKeys這個對象實際上是selectedKeys變量的一份副本,你能夠在SelectorImpl的構造函數中找到它們倆的關係,咱們再回頭看一下selectupdateSelectedKeys方法.

private int updateSelectedKeys() {
    //更新了的keys的個數,或在說是產生的事件的個數
    int entries = pollWrapper.updated; 
    int numKeysUpdated = 0;
    for (int i=0; i<entries; i++) {
        //對應的channel的fd
        int nextFD = pollWrapper.getDescriptor(i);
        //經過fd找到對應的SelectionKey
        SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
        if (ski != null) {
            int rOps = pollWrapper.getEventOps(i);
            //更新selectedKey變量,並通知響應的channel來作響應的處理
            if (selectedKeys.contains(ski)) {
                if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
                    numKeysUpdated++;
                }
            } else {
                ski.channel.translateAndSetReadyOps(rOps, ski);
                if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
                    selectedKeys.add(ski);
                    numKeysUpdated++;
                }
            }
        }
    }
    return numKeysUpdated;
}
複製代碼

後記

 看到這裏,詳細你們都已經瞭解到了NIO的底層實現了吧.這裏我想在說兩個問題.

 一是爲何Netty本身又重新實現了一邊native相關的NIO底層方法? 聽聽Netty的創始人是怎麼說的吧連接。由於Java的版本使用的epoll的level-triggered模式,而Netty則但願使用edge-triggered模式,並且Java版本沒有將epoll的部分配置項暴露出來,好比說TCP_CORK和SO_REUSEPORT。

 二是看這麼多源碼,花費這麼多時間有什麼做用呢?我感受若是從非功利的角度來看,那麼就是純粹的但願瞭解的更多,有時候看完源碼或在理解了底層原理以後,都會用一種恍然大悟的感受,好比說AQS的原理.若是從目的性的角度來看,那麼就是你知道底層原理以後,你的把握性就更強了,若是出了問題,你能夠更快的找出來,而且解決.除此以外,你還能夠按照具體的現實狀況,以源碼爲模板在本身造輪子,實現一個更加符合你當前需求的版本.

 後續若是有時間,我但願好好了解一下epoll的操做系統級別的實現原理.

相關文章
相關標籤/搜索