1. 程式人生 > >IO多路復用模型

IO多路復用模型

linux channel col can proc 到來 大致 select 時也

服務器端編程經常需要構造高性能的IO模型,常見的IO模型有四種:

(1)同步阻塞IO(Blocking IO):即傳統的IO模型。

(2)同步非阻塞IO(Non-blocking IO):默認創建的socket都是阻塞的,非阻塞IO要求socket被設置為NONBLOCK。

(3)IO多路復用(IO Multiplexing):即經典的Reactor設計模式,有時也稱為異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。

(4)異步IO(Asynchronous IO):即經典的Proactor設計模式,也稱為異步非阻塞IO。

此文主要講IO多路復用。

I/O是指網絡I/O

多路指多個TCP連接(即socket或者channel),復用指復用一個或幾個線程。

即:同一個線程內同時處理多個TCP連接。 最大優勢是減少系統開銷小,不必創建/維護過多的線程。

IO多路復用模型是建立在內核提供的多路分離函數select基礎之上的,使用select函數可以避免同步非阻塞IO模型中輪詢等待的問題。

技術分享圖片

                圖1 多路分離函數select

如圖1所示,用戶首先將需要進行IO操作的socket添加到select中,然後阻塞等待select系統調用返回。當數據到達時,socket被激活,select函數返回。用戶線程正式發起read請求,讀取數據並繼續執行。

從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。

但是,使用select以後最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。

用戶可以註冊多個socket,然後不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。

用戶線程使用select函數的偽代碼描述為:

{

select(socket);

while(1) {

    sockets = select();

    
for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } } }

其中while循環前將socket添加到select監視中,然後在while內一直調用select獲取被激活的socket,一旦socket可讀,便調用read函數將socket中的數據讀取出來。

然而,使用select函數的優點並不僅限於此。雖然上述方式允許單線程內處理多個IO請求,但是每個IO請求的過程還是阻塞的(在select函數上阻塞),平均時間甚至比同步阻塞IO模型還要長。

如果用戶線程只註冊自己感興趣的socket或者IO請求,然後去做自己的事情,等到數據到來時再進行處理,則可以提高CPU的利用率。

IO多路復用模型使用了Reactor(反應堆)設計模式實現了這一機制。

技術分享圖片

                        圖2 Reactor設計模式

如圖2所示,EventHandler抽象類表示IO事件處理器,它擁有IO文件句柄Handle(通過get_handle獲取),以及對Handle的操作handle_event(讀/寫等)。

繼承EventHandler的子類可以對事件處理器的行為進行定制。

Reactor類用於管理EventHandler(註冊、刪除等),並使用handle_events實現事件循環,不斷調用同步事件多路分離器(一般是內核)的多路分離函數select,只要某個文件句柄被激活(可讀/寫等),select就返回(阻塞),handle_events就會調用與文件句柄關聯的事件處理器的handle_event進行相關操作。

技術分享圖片

                      圖3 IO多路復用

如圖3所示,通過Reactor的方式,可以將用戶線程輪詢IO操作狀態的工作統一交給handle_events事件循環進行處理。

用戶線程註冊事件處理器之後可以繼續執行做其他的工作(異步),而Reactor線程負責調用內核的select函數檢查socket狀態。

當有socket被激活時,則通知相應的用戶線程(或執行用戶線程的回調函數),執行handle_event進行數據讀取、處理的工作。

由於select函數是阻塞的,因此多路IO復用模型也被稱為異步阻塞IO模型。

註意,這裏的所說的阻塞是指select函數執行時線程被阻塞,而不是指socket。

一般在使用IO多路復用模型時,socket都是設置為NONBLOCK的,不過這並不會產生影響,因為用戶發起IO請求時,數據已經到達了,用戶線程一定不會被阻塞。

用戶線程使用IO多路復用模型的偽代碼描述為:

void UserEventHandler::handle_event() {

    if(can_read(socket)) {

        read(socket, buffer);

        process(buffer);

    }

}

{

Reactor.register(new UserEventHandler(socket));

}

用戶需要重寫EventHandler的handle_event函數進行讀取數據、處理數據的工作,用戶線程只需要將自己的EventHandler註冊到Reactor即可。Reactor中handle_events事件循環的偽代碼大致如下。

Reactor::handle_events() {

    while(1) {

        sockets = select();

        for(socket in sockets) {

            get_event_handler(socket).handle_event();

        }

    }

}                

事件循環不斷地調用select獲取被激活的socket,然後根據獲取socket對應的EventHandler,執行器handle_event函數即可。

IO多路復用是最常使用的IO模型,但是其異步程度還不夠“徹底”,因為它使用了會阻塞線程的select系統調用。

因此IO多路復用只能稱為異步阻塞IO,而非真正的異步IO。

IO多路復用模型