1. 程式人生 > 實用技巧 >解讀I/O多路複用技術

解讀I/O多路複用技術

前言

當我們要編寫一個echo伺服器程式的時候,需要對使用者從標準輸入鍵入的互動命令做出響應。在這種情況下,伺服器必須響應兩個相互獨立的I/O事件:1)網路客戶端發起網路連線請求,2)使用者在鍵盤上鍵入命令列。我們先等待哪個事件呢?沒有哪個選擇是理想的。如果在acceptor中等待一個連線請求,我們就不能響應輸入的命令。類似地,如果在read中等待一個輸入命令,我們就不能響應任何連線請求。針對這種困境的一個解決辦法就是I/O多路複用技術。基本思路就是使用select函式,要求核心掛起程序,只有在一個或多個I/O事件發生後,才將控制返回給應用程式。--《UNIX網路程式設計》
我們以書中的這段描述來引出我們要講述的I/O多路複用技術。

I/O多路複用概述

圖1
I/O多路複用,I/O就是指的我們網路I/O,多路指多個TCP連線(或多個Channel),複用指複用一個或少量執行緒。串起來理解就是很多個網路I/O複用一個或少量的執行緒來處理這些連線。現在大部分講述I/O多路複用的文章用到的上面這張圖是《UNIX網路程式設計》一書的。那麼這也是當前我們理解I/O多路複用技術的基礎知識。從這張圖裡面我們GET到哪些點呢?
個人理解有:
1、怎麼區分的應用程序與核心
2、有兩次系統呼叫分別是select和recvfrom
3、兩次系統呼叫程序都阻塞
4、等待哪些資料準備好
下面我們逐一闡述。

二、使用者程序和核心

圖2

根據網路OSI七層模型和網際網協議族的同比,我們可以知道這裡說的使用者程序和核心是以傳輸層為分割線,傳輸層以上(不包括)是指使用者程序,傳輸層以下(包括)是指核心。上三層,web客戶端比如瀏覽器、web伺服器這些都屬於應用層,裡面跑的程式則是應用程序。下四層處理所有的通訊細節,傳送資料,等待確認,給無序到達的資料排序等等。這四層也是通常作為作業系統核心的一部分提供。由此可見圖1中說的系統呼叫的地方正是第四層和第五層之間的位置。

為了理解使用者程序和核心,再來看一張圖,網路資料流向圖。也清晰的標明瞭使用者程序和核心的位置。值得注意的一點是客戶與伺服器之間的資訊流在其中一端是向下通過協議棧的,跨越網路後,在另一端是向上通過協議棧的。這張圖描述的是區域網內,如果是在廣域網那麼就是通過很多個路由器承載實際資料流。


圖3

三、select和recvfrom

3.1、select

理解了select就抓住了I/O多路複用的精髓,對應的作業系統中呼叫的則是系統的select函式,該函式會等待多個I/O事件(比如讀就緒,寫)的任何一個發生,並且只要有一個網路事件發生,select執行緒就會執行。如果沒有任何一個事件發生則阻塞。我們在下面小節中會重點講述。函式如下:

#include<sys/select.h>
#include<sys/time.h>
int select(int maxfdpl,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

從這個函式的定義中的引數,我們能夠看出它描述的是,當呼叫select的時候告知核心對那些事件(讀就緒,寫)感興趣以及等待多長時間。

為了方便我們理解select呼叫,可以參照下面這張圖,是jdk的基於I/O多路複用技術的NIO實現。重點在於理解Selector複用器。


圖4

大致程式碼如下:

ServerSocketChannel serverChannel = ServerSocketChannel.open();// 開啟一個未繫結的serversocketchannel   
Selector selector = Selector.open();// 建立一個Selector
serverChannel .configureBlocking(false);//設定非阻塞模式
serverChannel .register(selector, SelectionKey.OP_READ);//將ServerSocketChannel註冊到Selector


while(true) {
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;
  Set selectedKeys = selector.selectedKeys();
  Iterator keyIterator = selectedKeys.iterator();
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {//連線就緒
        // a connection was established with a remote server.
    } else if (key.isReadable()) {//讀就緒
        // a channel is ready for reading
    } else if (key.isWritable()) {//寫就緒
        // a channel is ready for writing
    }
    keyIterator.remove();
  }
}

3.2、recvfrom

recvfrom一般用於UDP協議中,但是如果在TCP中connect函式呼叫後也可以用。用於從(已連線)套介面上接收資料,並捕獲資料傳送源的地址。也就是我們本文中以及書中說的真正的I/O操作。

四、阻塞、非阻塞

圖5

這張圖可以看出阻塞式I/O、非阻塞式I/O、I/O複用、訊號驅動式I/O他們的第二階段都相同,也就是都會阻塞到recvfrom呼叫上面就是圖中“發起”的動作。非同步式I/O兩個階段都要處理。這裡我們重點對比阻塞式I/O(也就是我們常說的傳統的BIO)和I/O複用之間的區別。

阻塞式I/O和I/O複用,兩個階段都阻塞,那區別在哪裡呢?就在於第三節講述的Selector,雖然第一階段都是阻塞,但是阻塞式I/O如果要接收更多的連線,就必須建立更多的執行緒。I/O複用模式下在第一個階段大量的連線統統都可以過來直接註冊到Selector複用器上面,同時只要單個或者少量的執行緒來迴圈處理這些連線事件就可以了,一旦達到“就緒”的條件,就可以立即執行真正的I/O操作。這就是I/O複用與傳統的阻塞式I/O最大的不同。也正是I/O複用的精髓所在。

從應用程序的角度去理解始終是阻塞的,等待資料和將資料複製到使用者程序這兩個階段都是阻塞的。這一點我們從應用程式是可以清楚的得知,比如我們呼叫一個以I/O複用為基礎的NIO應用服務。呼叫端是一直阻塞等待返回結果的。
從核心的角度等待Selector上面的網路事件就緒,是阻塞的,如果沒有任何一個網路事件就緒則一直等待直到有一個或者多個網路事件就緒。但是從核心的角度考慮,有一點是不阻塞的,就是複製資料,因為核心不用等待,當有就緒條件滿足的時候,它直接複製,其餘時間在處理別的就緒的條件。這也是大家一直說的非阻塞I/O。實際上是就是指的這個地方的非阻塞。

當我們閱讀《UNIX網路程式設計》(第三版)一書的時候。P124,6.2.3小節中“而不是阻塞在真正的I/O系統呼叫上”這裡的阻塞是相對核心來說的。P127,6.2.7小節“因為其中真正的I/O操作(recvfrom)將阻塞程序”這裡的阻塞是相對使用者程序來說的。明白了這兩點,理解起來就不矛盾了,而且一通到底!

五、適用場景

當服務程式需要承載大量TCP連結的時候,比如我們的訊息推送系統,IM通訊,web聊天等等,在我們已經理解Selector原理的情況下,知道使用I/O複用可以用少量的執行緒處理大量的連結。I/O多路複用技術以事件驅動程式設計為基礎。它執行在單一程序上下文中,因此每個邏輯流都能訪問該程序的全部地址空間,這樣在流之間共享資料變得很容易。

六、總結

我們通常說的NIO大多數場景下都是基於I/O複用技術的NIO,比如jdk中的NIO,當然Tomcat8以後的NIO也是指的基於I/O複用的NIO。注意,使用NIO != 高效能,當連線數<1000,併發程度不高或者區域網環境下NIO並沒有顯著的效能優勢。如果放到線上環境,網路情況在有時候並不穩定的情況下,這種基於I/O複用技術的NIO的優勢就是傳統BIO不可同比的了。那麼使用select的優勢在於我們可以等到網路事件就緒,那麼用少量的執行緒去輪詢Selector上面註冊的事件,不就緒的不處理,就緒的拿出來立即執行真正的I/O操作。這個使得我們就可以用極少量的執行緒去HOLD住大量的連線。

轉載請註明作者及出處,並附上鍊接http://www.jianshu.com/p/db5da880154a



作者:新棟BOOK
連結:https://www.jianshu.com/p/db5da880154a
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。