【OS】5種網路IO模型

TypantK發表於2019-03-31

 參考地址:http://www.cnblogs.com/findumars/p/6361627.html

https://time.geekbang.org/column/article/9293

目錄

阻塞IO(Blocking IO)

非阻塞IO(Non-Blocking IO)-輪詢

多路複用IO(IO Multiplexing)- 事件驅動IO

訊號驅動IO

非同步IO(Asynchronous IO)- 真正的非阻塞

總結



什麼是IO

Java中I/O操作主要是指使用Java進行輸入,輸出操作。 Java所有的I/O機制都是基於資料流進行輸入輸出,這些資料流表示了字元或者位元組資料的流動序列

 

IO要經歷的階段

以接收(read)IO流為例,會涉及到兩個物件:呼叫IO的程式(執行緒)、系統核心(kernel)

一個read操作:

    1)資料準備階段(Socket --> 核心)
    2)將資料從核心拷貝到程式中(核心 --> 程式)

 

本文討論的背景是Linux環境下的network IO。本文最重要的參考文獻是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2節“I/O Models ”,Stevens在這節中詳細說明了各種IO的特點和區別,如果英文夠好的話,推薦直接閱讀。Stevens的文風是有名的深入淺出,所以不用擔心看不懂。本文中的流程圖也是擷取自參考文獻。

 

阻塞IO(Blocking IO)


 *特點:IO執行的兩個階段(等待+拷貝)都被阻塞了

 

角色:

  • 客戶端(程式(應用application)+核心kernel+Socket)
  • 服務端(Socket)

 

服務端內的IO過程

datagram ready 就是從Socket獲取資料的結果

當使用者呼叫了recvfrom(),如果此時核心沒有準備好資料(no datagram ready),整個使用者程式就①阻塞

當核心準備好資料(datagram ready),核心就將資料複製到使用者程式的記憶體,這個過程使用者程式②阻塞直到kernel返回結果(copy complete --> return OK)

簡而言之:等待資料和拷貝資料都阻塞了

 

 客戶端與服務端之間的IO過程UDP(recvfrom() + sendto())

剛才分析過,recvfrom()是阻塞的,而Socket的其他方法sendto()也是阻塞的,這種時候就是“一問一答”,也就是伺服器專心只解決你一個客戶端的問題,此時伺服器的執行緒無法執行任何運算或響應任何的網路請求,影響其他程式/執行緒。這時就可以用多執行緒/多程式來讓伺服器分身出來解決其他執行緒/程式的請求。

解決伺服器IO阻塞(伺服器端-多程式/多執行緒)

*開啟多程式/多執行緒不代表 非同步,因為還是自己(自己是程式/執行緒)來處理

要交給不同的物件(比如核心)來處理,然後自己向下執行才算是非同步

多程式/多執行緒的目的就是讓每一個連線都擁有獨立的程式/執行緒,這樣任何一個連線的阻塞都不會影響其他程式/執行緒

UDP的多執行緒伺服器模型

 

TCP的多執行緒伺服器模型

*一個Socket可以accpet()多次原因: accpet()返回的是一個新socket

 

 而通過多執行緒和多程式解決的區別,其實就是程式和執行緒的區別:最主要的是資源問題

  • 多程式就相當於,每有一個專案,就開一個公司來做這個專案(在父程式的基礎上完全拷貝一個子程式fork(),在 Linux 核心中,會複製檔案描述符的列表,也會複製記憶體空間,還會複製一條記錄當前執行到了哪一行程式的程式)
  •  多執行緒就相當於,在同一個公司,開多一個專案組來做這個專案(很多資源,例如檔案描述符列表、程式空間,還是共享的,只不過多了一個引用而已)

多執行緒和多程式解決伺服器IO阻塞的不足之處

  上述多執行緒的伺服器模型似乎完美的解決了為多個客戶機提供問答服務的要求,但其實並不盡然。如果要同時響應成百上千路的連線請求,則無論多執行緒還是多程式都會嚴重佔據系統資源,降低系統對外界響應效率(太多執行緒/程式),而執行緒與程式本身也更容易進入假死狀態
    很多程式設計師可能會考慮使用“執行緒池”或“連線池”。“執行緒池”旨在減少建立和銷燬執行緒的頻率,其維持一定合理數量的執行緒,並讓空閒的執行緒重新承擔新的執行任務。“連線池”維持連線的快取池,儘量重用已有的連線、減少建立和關閉連線的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種資料庫等。但是,“執行緒池”和“連線池”技術也只是在一定程度上緩解了頻繁呼叫IO介面帶來的資源佔用。而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的響應並不比沒有池的時候效果好多少。所以使用“池”必須考慮其面臨的響應規模,並根據響應規模調整“池”的大小。

走到這裡,對於BIO(阻塞IO)的優化已經差不多了,而還是有侷限性,也就是面對大規模的服務請求,會遇到瓶頸(執行緒池/連線池請求超過上限),所以可以考慮用NIO(非阻塞IO)

 

 

非阻塞IO(Non-Blocking IO)-輪詢


*特點:非阻塞的介面相比於阻塞型介面,前者在被呼叫之後立即返回

*缺點:迴圈呼叫recvfrom()大幅度佔用CPU資源

 

這裡的非阻塞指的是,recvfrom()執行時發現核心還沒準備好資料就直接返回

而在核心準備好資料時,複製核心資料到執行緒記憶體還是會阻塞執行緒的。

也就是等待非阻塞(等待Socket --> 核心),而複製阻塞(核心複製 --> 執行緒記憶體)了,而對於非同步就是兩者都非阻塞

 

Linux下,可以通過設定socket來使其變為非阻塞IO

服務端內的IO過程 

 可以看到recvfrom(),如果核心還沒有資料會立刻返回結果(EWOULDBLOCK)

也就是說,對於使用者發起一個recvfrom()操作可以立刻得到結果。如果得到是error,就知道核心還沒準備好資料;反之..

所以,在NIO中,使用者程式需要輪詢核心是否準備好資料

 

客戶端與服務端之間的IO

使用如下的函式可以將某控制程式碼fd設為非阻塞狀態(也就是將Socket設定成NIO?)
    fcntl( fd, F_SETFL, O_NONBLOCK ); 

可以看到recv(fd1)沒有阻塞,因為有返回錯誤了就繼續往下執行

 

 

多路複用IO(IO Multiplexing)- 事件驅動IO


*優勢:能同時處理更多的連線,而不是對於單個連線能夠處理的更快

解決NIO中迴圈呼叫recvfrom()佔用高CPU的問題 --> 呼叫一個select()

 

伺服器內的IO過程(select-1024個socket)

執行緒/程式執行select(阻塞)通過不斷的輪詢所有的socket,如果某個socket有資料到達了(return readable),就通知使用者程式

 

與BIO的區別:應用(application)有了兩個系統呼叫select + recvfrom,所以對於處理的連線數不是很多時,多路複用IO不一定就比BIO+多執行緒

poll

 輪詢的是連結串列(準備就緒的socket)

epoll

輪詢的是連結串列(準備就緒的socket),還有紅黑樹(存放未準備就緒的socket),共享記憶體(加快執行緒間傳輸速度)

將紅黑樹放到連結串列的過程中會執行回撥函式callback,通知執行緒,這樣就不用輪詢了

 

訊號驅動IO


*特點:等待(不阻塞)+ 拷貝(阻塞)服務端內IO

通過傳SIGIO給signal handler來提醒執行緒資料準備好了

類似於NIO,但是不用迴圈執行recvfrom(),但是多了一個系統呼叫establish SIGIO,適用於長時間Socket沒有準備好資料的情況

 

 

非同步IO(Asynchronous IO)- 真正的非阻塞


*特點:IO執行的兩個階段(等待+拷貝)都沒有阻塞

*特點:事情交給核心來處理(非同步)

 

服務端的IO

從使用者(application)的角度:發起read操作後就可以做其他事了(所謂非同步:就好像所有事情交給核心來處理了)

從核心(kernel)的角度:收到一個asynchronous read(aio_read)後,首先會立刻返回(read),然後等資料準備完成,將資料拷貝到使用者記憶體,然後給使用者程式傳送一個訊號(deliver signal)表示read操作完成

 

 

 

總結


這個圖是對於要執行IO操作的程式所執行的方法

☆☆☆☆BIO/NIO/多路複用/訊號驅動IO 都是同步IO(還得自己負責等待和複製)☆☆☆☆

非同步IO就像程式將整個IO操作交給核心處理

對於BIO和NIO,BIO的兩個階段(等待和複製)都會阻塞,而NIO只有在複製的時候才會阻塞

相關文章