1. 程式人生 > >網絡IO模型

網絡IO模型

版本 回調 許可 roc ron 用兩個 fcntl nec poll

常見的網絡IO模型5種

阻塞IO(blocking IO), 無阻塞IO(noblocking IO), IO多路復用(IO multiplexing),信號驅動 (signal driven IO),異步IO (asynchronous IO)

阻塞IO(blocking IO)

在linux中,默認情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:

技術分享圖片

幾乎所有的程序員第一次接觸到的網絡編程都是從listen()、send()、recv() 等接口開始的,這些接口都是阻塞型的。使用這些接口可以很方便的構建服務器/客戶機的模型。下面是一個簡單地“一問一答”的服務器。

技術分享圖片

大部分的socket接口都是阻塞型的。所謂阻塞型接口是指系統調用(一般是IO接口)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。

一個簡單的改進方案是在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。具體使用多進程還是多線程,並沒有一個特定的模式。傳統意義上,進程的開銷要遠遠大於線程,所以如果需要同時為較多的客戶機提供服務,則不推薦使用多進程;如果單個服務執行體需要消耗較多的CPU資源,譬如需要進行大規模或長時間的數據運算或文件訪問,則進程較為安全。通常,使用pthread_create ()創建新線程,fork()創建新進程。

我們假設對上述的服務器 / 客戶機模型,提出更高的要求,即讓服務器同時為多個客戶機提供一問一答的服務。於是有了如下的模型。

技術分享圖片

很多程序員可能會考慮使用“線程池”或“連接池”。“線程池”旨在減少創建和銷毀線程的頻率,其維持一定合理數量的線程,並讓空閑的線程重新承擔新的執行任務。“連接池”維持連接的緩存池,盡量重用已有的連接、減少創建和關閉連接的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種數據庫等。但是,“線程池”和“連接池”技術也只是在一定程度上緩解了頻繁調用IO接口帶來的資源占用。而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的響應並不比沒有池的時候效果好多少。所以使用“池”必須考慮其面臨的響應規模,並根據響應規模調整“池”的大小。

對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。總之,多線程模型可以方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,可以用非阻塞接口來嘗試解決這個問題。

無阻塞式IO (nonblocking IO)

Linux下,可以通過設置socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

技術分享圖片

非阻塞的接口相比於阻塞型接口的顯著差異在於,在被調用之後立即返回。使用如下的函數可以將某句柄fd設為非阻塞狀態。

fcntl( fd, F_SETFL, O_NONBLOCK );
下面將給出只用一個線程,但能夠同時從多個連接中檢測數據是否送達,並且接受數據的模型。

技術分享圖片

在非阻塞狀態下,recv() 接口在被調用後立即返回,返回值代表了不同的含義。如在本例中,
* recv() 返回值大於 0,表示接受數據完畢,返回值即是接受到的字節數;
* recv() 返回 0,表示連接已經正常斷開;
* recv() 返回 -1,且 errno 等於 EAGAIN,表示 recv 操作還沒執行完成;
* recv() 返回 -1,且 errno 不等於 EAGAIN,表示 recv 操作遇到系統錯誤 errno。


可以看到服務器線程可以通過循環調用recv()接口,可以在單個線程內實現對所有連接的數據接收工作。但是上述模型絕不被推薦。因為,循環調用recv()將大幅度推高CPU 占用率;此外,在這個方案中recv()更多的是起到檢測“操作是否完成”的作用,實際操作系統提供了更為高效的檢測“操作是否完成“作用的接口,例如select()多路復用模式,可以一次檢測多個連接是否活躍。

IO多路復用 (IO multiplexing)

IO multiplexing這個詞可能有點陌生,但是如果我說select/epoll,大概就都能明白了。有些地方也稱這種IO方式為事件驅動IO(event driven IO)。我們都知道,select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:

技術分享圖片

這個圖和blocking IO的圖其實並沒有太大的不同,事實上還更差一些。因為這裏需要使用兩個系統調用(select和recvfrom),而blocking IO只調用了一個系統調用(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。(多說一句:所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。)
在多路復用模型中,對於每一個socket,一般都設置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。因此select()與非阻塞IO類似。

異步IO (asynchronous IO)

Linux下的asynchronous IO其實用得不多,從內核2.6版本才開始引入。先看一下它的流程:

技術分享圖片

用戶進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。然後,kernel會等待數據準備完成,然後將數據拷貝到用戶內存,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了。

用異步IO實現的服務器這裏就不舉例了,以後有時間另開文章來講述。異步IO是真正非阻塞的,它不會對請求進程產生任何的阻塞,因此對高並發的網絡服務器實現至關重要。
到目前為止,已經將四個IO模型都介紹完了。現在回過頭來回答最初的那幾個問題:blocking和non-blocking的區別在哪,synchronous IO和asynchronous IO的區別在哪。
先回答最簡單的這個:blocking與non-blocking。前面的介紹中其實已經很明確的說明了這兩者的區別。調用blocking IO會一直block住對應的進程直到操作完成,而non-blocking IO在kernel還在準備數據的情況下會立刻返回。


在說明synchronous IO和asynchronous IO的區別之前,需要先給出兩者的定義。Stevens給出的定義(其實是POSIX的定義)是這樣子的:
* A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
* An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區別就在於synchronous IO做”IO operation”的時候會將process阻塞。按照這個定義,之前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。有人可能會說,non-blocking IO並沒有被block啊。這裏有個非常“狡猾”的地方,定義中所指的”IO operation”是指真實的IO操作,就是例子中的recvfrom這個系統調用。non-blocking IO在執行recvfrom這個系統調用的時候,如果kernel的數據沒有準備好,這時候不會block進程。但是當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶內存中,這個時候進程是被block了,在這段時間內進程是被block的。而asynchronous IO則不一樣,當進程發起IO操作之後,就直接返回再也不理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程中,進程完全沒有被block。

還有一種不常用的signal driven IO,即信號驅動IO。總的來說,UNP中總結的IO模型有5種之多:阻塞IO,非阻塞IO,IO復用,信號驅動IO,異步IO。前四種都屬於同步IO。阻塞IO不必說了。非阻塞IO ,IO請求時加上O_NONBLOCK一類的標誌位,立刻返回,IO沒有就緒會返回錯誤,需要請求進程主動輪詢不斷發IO請求直到返回正確。IO復用同非阻塞IO本質一樣,不過利用了新的select系統調用,由內核來負責本來是請求進程該做的輪詢操作。看似比非阻塞IO還多了一個系統調用開銷,不過因為可以支持多路IO,才算提高了效率。信號驅動IO,調用sigaltion系統調用,當內核中IO數據就緒時以SIGIO信號通知請求進程,請求進程再把數據從內核讀入到用戶空間,這一步是阻塞的。
異步IO,如定義所說,不會因為IO操作阻塞,IO操作全部完成才通知請求進程。
各個IO Model的比較如圖所示:

技術分享圖片

圖12 各種IO模型的比較

經過上面的介紹,會發現non-blocking IO和asynchronous IO的區別還是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,但是它仍然要求進程去主動的check,並且當數據準備完成以後,也需要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則完全不同。它就像是用戶進程將整個IO操作交給了他人(kernel)完成,然後他人做完後發信號通知。在此期間,用戶進程不需要去檢查IO操作的狀態,也不需要主動的去拷貝數據。

網絡IO模型