首頁>技術>

1 前言

我們都知道,為了實現高效能的通訊伺服器,BIO在高併發的情況下會出現效能急劇下降的問題,甚至會由於建立過多執行緒而導致系統OOM。因此在Java業界,BIO的效能問題一直被開發者所詬病,所幸的是,JDK1.4推出了NIO,NIO基本解決了BIO的效能問題,是目前實現Java高效能伺服器的基礎框架。NIO官方的叫法叫做New IO,而對應於作業系統層面來說其實也是Non-Blocking IO。

大名鼎鼎的Netty就是NIO框架,而目前很多開源框架比如Dubbo,RocketMQ,Seata,Spark,Flink都是採用Netty作為基礎通訊元件。因此,學好Netty很重要,但是NIO作為Netty的基礎,這裡想說的是學好NIO也一樣重要!

學好NIO,那麼必須先理解作業系統層面的5種網路IO模型。

2 5種IO模型2.1 阻塞IO模型

阻塞IO模型如下圖:

從上圖可以看到,不管有無資料報到來,程序(執行緒)是阻塞於 recvfrom 系統呼叫的。這是什麼意思呢?說白了就是假如我們要用套接字讀取資料,此時我們必然會呼叫 read 方法,此時這個 read 方法就會觸發作業系統核心的一次 recvfrom 系統呼叫,此時有兩種情況:

核心還未接收到遠端資料,此時資料報沒有準備好,那麼讀取資料的執行緒就會一直阻塞,直到遠端發來資料報,這一阻塞的過程對應上圖序號1的過程;然後在資料報被從核心複製到使用者空間這一過程中,該執行緒會再次阻塞,直到複製完成,這一過程對應上圖的序號2的過程;核心已經接收到遠端資料,此時資料報已經準備好,那麼資料報就會被從核心複製到使用者空間,這一過程是阻塞的,對應上圖序號2的過程。

可見,阻塞IO模型的話,讀一次資料會發生一次 recvfrom 系統呼叫,整個過程都是阻塞的,即在核心的資料報還未準備好的時候,此時使用者程序( 執行緒)阻塞;當核心的資料報準備好的時候,此時資料報要從核心複製到使用者空間,此時使用者程序(執行緒)也一直阻塞;直到資料報複製到使用者空間後,此時使用者程序(執行緒)才會醒過來,然後處理這些資料報即執行一些使用者的業務邏輯。當然,如果使用者程序(執行緒)在阻塞過程中,如果 recvfrom 系統呼叫被訊號中斷,此時阻塞也是會被喚醒的。

思考:這裡的 recvfrom 系統呼叫被訊號中斷什麼情況下會發生?這個訊號中斷指的是執行緒中斷( Thread.interrupt() )麼?自行思考。

2.2 非阻塞IO模型

非阻塞IO模型如下圖:

如上圖,根據核心中的資料報有無準備好,有以下兩種情形:

當核心中的資料報還沒準備好,此時 recvfrom 系統呼叫立即返回一個 EWOULDBLOCK 錯誤,即不會將使用者程序(執行緒)置於阻塞狀態。我們拿Java的NIO來說,當我們配置 ServerSocketChannel.configureBlocking(false); 或 SocketChannel..configureBlocking(false); 時,我們呼叫 ServerSocketChannel.accept() 的 null 或 SocketChannel.read(buffer) 不會阻塞的,若沒有新連線接入或核心中沒有資料報準備好,此時會理解返回 null 或 0 的返回結果,說白了這個返回結果就是對應 EWOULDBLOCK 錯誤;當核心中的資料報已經準備好時,此時 recvfrom 系統呼叫,使用者程序(執行緒)還是會阻塞,直到核心中的資料報已經複製到了使用者空間,此時使用者程序(執行緒)才會被喚醒來處理接收的資料報。

非阻塞IO在使用者資料報還沒準備好的時候, recvfrom 系統呼叫不會阻塞,接著會繼續進行下一輪的 recvfrom 系統呼叫看資料報有無準備好,週而復始,程序(執行緒)不斷輪訓,因此這是非常耗費CPU的。這種模型不是很常用,適合用在某臺CPU專為某些功能準備的場合。

2.3 IO複用模型

IO複用模型如下圖:

初步從以上IO複用模型來看,這不是跟IO阻塞模型差不多麼?當核心無資料報準備好時, select系統呼叫會阻塞;當核心資料複製到使用者空間時,此時 recvfrom 系統呼叫依然會阻塞,實在是看不到跟IO阻塞模型有啥區別?區別就是IO複用模型還比阻塞IO模型還多一次 recvfrom 系統呼叫,這不是明擺著多浪費一次CPU資源麼?

如果我們這麼想,那為什麼IO複用模型得到大規模廣泛應用呢?其實IO複用模型真正佔優勢的地方在於 select 操作,這個 select 操作可以選擇多個檔案描述符,分別對應Java NIO中的 OP_CONNECT , OP_ACCEPT , OP_READ 和 OP_WRITE 就緒事件。正是基於 一次recvfrom系統呼叫中一個執行緒的select操作可以選擇多個檔案描述符 這個功能,我們現在用一個使用者執行緒就能監聽不同 channel 的 OP_CONNECT , OP_ACCEPT , OP_READ 和 OP_WRITE 這些就緒事件,然後根據某個就緒事件拿到相應的 channel 來做對應的操作。而不用像 阻塞IO模型或非阻塞IO模型 那樣, 一次recvfrom系統呼叫中一個執行緒就只能選擇一個檔案描述符 ,這樣就嚴重限制了伸縮性。這麼說很抽象,就比如拿阻塞IO模型來說,由於使用者程序(執行緒)每一次 recvfrom 系統呼叫都是阻塞且只對應一個檔案描述符,此時如果服務端執行緒阻塞於客戶端A的讀操作時,如果有另外的客戶端B需要接入服務端,此時服務端執行緒由於阻塞於客戶端A的讀操作,因此無法處理客戶端B的連線操作。此時,必然要一個執行緒一個檔案描述符即服務端執行緒每 accept 了一個客戶端連線,此時就需要新建一個執行緒去處理這個客戶端連線的讀寫操作。我們都知道,執行緒是一種很昂貴的CPU資源,當開啟成千上萬的執行緒後,執行緒切換的成本很高,CPU效能肯定下降,說不定高併發下還會OOM。說到這裡,也許有同學會說,對於阻塞IO模型,我們不一個執行緒一個socket,用執行緒池替代,當然,這是一個最佳化的點,但沒解決阻塞IO模型的根本。怎麼說呢?當執行緒池的所有執行緒都阻塞於客戶端的讀或寫操作時,此時其他新接入的執行緒將會積壓線上程池的佇列中阻塞等待。

2.4 訊號驅動IO模型

訊號驅動IO模型如下圖:

可見,訊號驅動IO模型在等待資料報期間是不會阻塞的,即使用者程序(執行緒)傳送一個 sigaction 系統呼叫後,此時立刻返回,並不會阻塞,然後使用者程序(執行緒)繼續執行;當資料報準備好時,此時核心就為該程序(執行緒)產生一個 SIGIO 訊號,此時該程序(執行緒)就發生一次 recvfrom 系統呼叫將資料報從核心複製到使用者空間,注意,這個階段是阻塞的。

PS:網上找了下訊號驅動IO模型的java程式碼,沒找到,會碼訊號驅動IO模型程式碼的小夥伴們可以教教我。

2.5 非同步IO模型

非同步IO模型如下圖:

非同步IO模型也很好理解,即使用者程序(執行緒)在等待資料報和資料報從核心複製到使用者空間這兩階段都是非阻塞的,即使用者程序(執行緒)發生一次系統呼叫後,立即返回,然後該使用者程序(執行緒)繼續往下執行。當核心把接收到資料報並把資料報複製到了使用者空間後,此時再通知使用者程序(執行緒)來處理使用者空間的資料報。也就是說,這一些列IO操作都交給了核心去處理了,使用者程序無須同步阻塞,因此是非同步非阻塞的。

擴充套件:非同步IO模型跟訊號驅動IO模型的區別在於當核心準備好資料報後,對於訊號驅動IO模型,此時核心會通知使用者程序說資料報準備好啦,你需要發起系統呼叫來將資料報從核心複製到使用者空間,此過程是同步阻塞的;而對於非同步IO模型,當資料報準備好時,核心不會再通知使用者程序,而是自己默默將資料報從核心複製到使用者空間後然後再通知使用者程序說,資料已經複製到使用者空間啦,你直接進行業務邏輯處理就行。

3 各種IO模型區別

透過5種IO模型的比對,可以發現,前4 種 IO模型都是同步阻塞IO模型,因為其第二階段資料報從核心複製到使用者空間都是同步阻塞的,只是第一階段等待資料報的處理不同; 最後一種IO模型(非同步IO模型)才是真正的非同步非阻塞IO模型,核心將一切事情都幹完(核心: 我真的好累)。

4 總結

好了,五種IO模型基本就已經總結完了,基本是自己基於《UNIX網路程式設計_卷1_套接字》的讀書總結

原文連結:https://mp.weixin.qq.com/s?__biz=MzAwMDczMjMwOQ==&mid=2247484159&idx=1&sn=62a1626a748547ad837f9c6ccc10b11a

11
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • asp.net core監控—引入Prometheus(二)