Linux的5種網絡IO模型詳解


linux的五種IO模型,分別是:阻塞IO、非阻塞IO、多路復用IO、信號驅動IO以及異步IO。其中阻塞IO、非阻塞IO、多路復用IO、信號驅動IO都屬於同步IO。

同步IO和異步IO

同步IO:應用程序主動向內核查詢是否有可用數據,如果有自己負責把數據從內核copy到用戶空間。

異步IO:應用程序向內核發起讀數據請求需要:(1)告訴內核數據存放位置(2)注冊回調函數,當內核完成數據copy后調用回調通知應用程序取數據。

同步IO/異步IO最大區別:同步IO數據從內核空間到用戶空間的copy動作是由應用程序自己完成。而異步IO則是注冊回調函數並告知內核用戶空間緩沖區存放地址,數據copy由內核完成。

 

詳細介紹之前,援引網上《Linux 網絡編程的5種IO模型:阻塞IO與非阻塞IO》中的一段話,便於簡單理解、記憶:

阻塞IO, 給女神發一條短信, 說我來找你了, 然后就默默的一直等着女神下樓, 這個期間除了等待你不會做其他事情, 屬於備胎做法.
非阻塞IO, 給女神發短信, 如果不回, 接着再發, 一直發到女神下樓, 這個期間你除了發短信等待不會做其他事情, 屬於專一做法.
IO多路復用, 是找一個宿管大媽來幫你監視下樓的女生, 這個期間你可以些其他的事情. 例如可以順便看看其他妹子,玩玩王者榮耀, 上個廁所等等. IO復用又包括 select, poll, epoll 模式. 那么它們的區別是什么?

  • 1) select大媽 每一個女生下樓, select大媽都不知道這個是不是你的女神, 她需要一個一個詢問, 並且select大媽能力還有限, 最多一次幫你監視1024個妹子

  • 2) poll大媽不限制盯着女生的數量, 只要是經過宿舍樓門口的女生, 都會幫你去問是不是你女神

  • 3) epoll大媽不限制盯着女生的數量, 並且也不需要一個一個去問. 那么如何做呢? epoll大媽會為每個進宿舍樓的女生臉上貼上一個大字條,上面寫上女生自己的名字, 只要女生下樓了, epoll大媽就知道這個是不是你女神了, 然后大媽再通知你。
    上面這些同步IO有一個共同點就是, 當女神走出宿舍門口的時候, 你已經站在宿舍門口等着女神的, 此時你屬於阻塞狀態

    接下來是異步IO的情況:
    你告訴女神我來了, 然后你就去打游戲了, 一直到女神下樓了, 發現找不見你了, 女神再給你打電話通知你, 說我下樓了, 你在哪呢? 這時候你才來到宿舍門口。 此時屬於逆襲做法

以下每種模型都有三種圖示和描述,各人可以選擇自己容易理解的部分進行記憶。

1、阻塞IO模型

最傳統的一種IO模型,即在讀寫數據過程中會發生阻塞現象。

當用戶線程發出IO請求之后,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處於阻塞狀態,用戶線程交出CPU。當數據就緒之后,內核會將數據拷貝到用戶線程,並返回結果給用戶線程,用戶線程才解除block狀態。

代碼如下:

printf("Calling recv(). \n");
ret =  recv(socket, recv_buf, sizeof(recv_buf), 0); 
printf("Had called recv(). \n")

也許有人會說,可以采用多線程+ 阻塞IO 來解決效率問題,但是由於在多線程 + 阻塞IO 中,每個socket對應一個線程,這樣會造成很大的資源占用,並且尤其是對於長連接來說,線程的資源一直不會釋放,如果后面陸續有很多連接的話,就會造成性能上的瓶頸。

 
        

當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:准備數據(對於網絡IO來說,很多時候數據在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程需要等待,也就是說數據被拷貝到操作系統內核的緩沖區中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。當kernel一直等到數據准備好了,它就會將數據從kernel中拷貝到用戶內存,然后kernel返回結果,用戶進程才解除block的狀態,重新運行起來。

所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

 

 應用程序請求內核讀取數據,內核數據數據緩沖區無數據或者數據未就緒前,阻塞等待。內核系統等待數據准備就緒后,拷貝數據到用戶空間,待拷貝完成后,返回結果,用戶程序接觸阻塞,處理數據。

2、非阻塞IO模型

當用戶線程發起一個IO操作后,並不需要等待,而是馬上就得到了一個結果。如果結果是一個error時,它就知道數據還沒有准備好,於是它可以再次發送IO操作。一旦內核中的數據准備好了,並且又再次收到了用戶線程的請求,那么內核它馬上就將數據拷貝到了用戶線程,然后返回。

在非阻塞IO模型中,用戶線程需要不斷地詢問內核數據是否就緒,也就說非阻塞IO不會交出CPU,而會一直占用CPU

對於非阻塞IO就有一個非常嚴重的問題,在while循環中需要不斷地去詢問內核數據是否就緒,這樣會導致CPU占用率非常高,因此一般情況下很少使用while循環這種方式來讀取數據。

while(1)
{
    printf("Calling recv(). \n");
    ret =  recv(socket, recv_buf, sizeof(recv_buf), 0); 
    if (EAGAIN == ret) {continue;}
    else if(ret > -1) { break;}
    printf("Had called recv(), retry.\n");
}

 Linux下,可以通過設置socket使其變為non-blocking。

當用戶進程發出read操作時,如果kernel中的數據還沒有准備好,那么它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發起一個read操作后,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有准備好,於是它可以再次發送read操作。一旦kernel中的數據准備好了,並且又再次收到了用戶進程的system call,那么內核它馬上就將數據拷貝到了用戶內存,然后返回。

所以,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數據好了沒有

 

  應用程序請求內核讀取數據,內核直接返回結果,如果數據未准備就緒,則返回error,應用程序繼續請求,周而復始,直到內核數據准備就緒后,當內核再次收到應用程序請求后,將數據拷貝到用戶空間,待拷貝完成后返回ok,應用程序處理數據。

 3、IO多路復用模型

I/O多路復用是操作系統級別的,屬於linux操作系統的五種I/O模型中的一種,是操作系統級別同步非阻塞的。操作系統級別的異步I/O才是真正異步非阻塞的(參見:https://www.zhihu.com/question/59975081/answer/837766592

 所謂I/O多路復用機制,就是說通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。這種機制的使用需要額外的功能來配合: select、poll、epoll。

  • select、poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的
  • select時間復雜度O(n),它僅僅知道了,有I/O事件發生了,卻並不知道是哪幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長。
  • poll(翻譯:輪詢)時間復雜度O(n),poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態, 但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的.
  • epoll時間復雜度O(1),epoll可以理解為event poll,不同於忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))。   

在多路復用IO模型中,會有一個內核線程不斷去輪詢多個socket的狀態,只有當真正讀寫事件發生時,才真正調用實際的IO讀寫操作。因為在多路復用IO模型中,只需要使用一個線程就可以管理多個socket,系統不需要建立新的進程或者線程,也不必維護這些線程和進程,並且只有在真正有讀寫事件進行時,才會使用IO資源,所以它大大減少了資源占用。

 

IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。

當用戶進程調用了select,那么整個進程就會被block,而同時,kernel會 “監視”所有select負責的socket,當任何一個socket中的數據准備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。所以,IO多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入就緒狀態,select()函數就可以返回。

這里需要使用兩個system call(select 和 recvfrom),而blocking IO只調用了一個system call(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。

如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用mutil-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll 的優勢並不是對於單個連接能處理得更好,而是在於性能更多的連接。

 

 

應用程序請求內核讀取數據,首先調用了select,內核監控select監控的所有socket,當有任何一個socket數據准備就緒后,就返回給用戶進程可讀,然后用戶進程再次向內核發送讀取指令,內核將數據拷貝到用戶空間,並返回結果,用戶進程獲得數據后進行處理。這里關於select、poll、epoll的區別不在這里描述,參見:《Linux 網絡編程的5種IO模型:多路復用(select/poll/epoll)》、《select、poll、epoll之間的區別(搜狗面試)》

4.信號驅動IO模型

 

 

  

在信號驅動IO模型中,當用戶線程發起一個IO請求操作,會給對應的socket注冊一個信號函數,然后用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號之后,便在信號函數中調用IO讀寫操作來進行實際的IO請求操作。這個一般用於UDP中,對TCP套接口幾乎是沒用的,原因是該信號產生得過於頻繁,並且該信號的出現並沒有告訴我們發生了什么事情。

在UDP上,SIGIO信號會在下面兩個事件的時候產生:

1 數據報到達套接字

2 套接字上發生錯誤

因此我們很容易判斷SIGIO出現的時候,如果不是發生錯誤,那么就是有數據報到達了。

而在TCP上,由於TCP是雙工的,它的信號產生過於頻繁,並且信號的出現幾乎沒有告訴我們發生了什么事情。因此對於TCP套接字,SIGIO信號是沒有什么使用的。

有關函數

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
             struct sigaction *oldact);

關於有關內容的講解,請參考:Linux 系統編程 學習:進程間通信-Unix IPC-信號

 

 5、異步IO模型

前面四種IO模型實際上都屬於同步IO,只有最后一種是真正的異步IO,因為無論是多路復用IO還是信號驅動模型,IO操作的第2個階段都會引起用戶線程阻塞,也就是內核進行數據拷貝的過程都會讓用戶線程阻塞。

導言

兩種高性能IO設計模式

在傳統的網絡服務設計模式中,有兩種比較經典的模式:多線程與線程池。

多線程

對於多線程模式,也就說來了client,服務器就會新建一個線程來處理該client的讀寫事件,如下圖所示:

img

這種模式雖然處理起來簡單方便,但是由於服務器為每個client的連接都采用一個線程去處理,使得資源占用非常大。因此,當連接數量達到上限時,再有用戶請求連接,直接會導致資源瓶頸,嚴重的可能會直接導致服務器崩潰。

線程池

因此,為了解決這種一個線程對應一個客戶端模式帶來的問題,提出了采用線程池的方式,也就說創建一個固定大小的線程池,來一個客戶端,就從線程池取一個空閑線程來處理,當客戶端處理完讀寫操作之后,就交出對線程的占用。因此這樣就避免為每一個客戶端都要創建線程帶來的資源浪費,使得線程可以重用。

但是線程池也有它的弊端,如果連接大多是長連接,因此可能會導致在一段時間內,線程池中的線程都被占用,那么當再有用戶請求連接時,由於沒有可用的空閑線程來處理,就會導致客戶端連接失敗,從而影響用戶體驗。因此,線程池比較適合大量的短連接應用。

高性能IO模型

因此便出現了下面的兩種高性能IO設計模式:Reactor和Proactor。Proactor前攝器模式和Reactor反應器模式。兩個模式不同的地方在於,Proactor用於異步IO,而Reactor用於同步IO。

Reactor

在Reactor模式中,會先對每個client注冊感興趣的事件,然后有一個線程專門去輪詢每個client是否有事件發生,當有事件發生時,便順序處理每個事件,當所有事件處理完之后,便再轉去繼續輪詢,如下圖所示:

img

從這里可以看出,多路復用IO就是采用Reactor模式。

注意,上面的圖中展示的 是順序處理每個事件,當然為了提高事件處理速度,可以通過多線程或者線程池的方式來處理事件。

Proactor

在Proactor模式中:當檢測到有事件發生時,會新起一個異步操作,然后交由內核線程去處理,當內核線程完成IO操作之后,發送一個通知告知操作已完成;可以得知,異步IO模型采用的就是Proactor模式。

 

 異步IO模型是比較理想的IO模型,在異步IO模型中,當用戶線程發起read操作之后,立刻就可以開始去做其它的事。而另一方面,從內核的角度,當它受到一個asynchronous read之后,它會立刻返回,說明read請求已經成功發起了,因此不會對用戶線程產生任何block。然后,內核會等待數據准備完成,然后將數據拷貝到用戶線程,當這一切都完成之后,內核會給用戶線程發送一個信號,告訴它read操作完成了。也就說用戶線程完全不需要關心實際的整個IO操作是如何進行的,只需要先發起一個請求,當接收內核返回的成功信號時表示IO操作已經完成,可以直接去使用數據了。

也就說在異步IO模型中,IO操作的兩個階段都不會阻塞用戶線程,這兩個階段都是由內核自動完成,然后發送一個信號告知用戶線程操作已完成。用戶線程中不需要再次調用IO函數進行具體的讀寫

 

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

 

 

 

 

 應用程序請求內核讀取數據后不等待,執行其它事項,內核待數據就緒后直接拷貝到用戶空間,並發送信號給應用程序,應用程序收到信號后處理數據。

參考:

https://www.cnblogs.com/schips/p/12583129.html  Linux 網絡編程的5種IO模型 總結

https://www.cnblogs.com/schips/p/12543650.html  Linux 網絡編程的5種IO模型:阻塞IO與非阻塞IO

https://www.cnblogs.com/schips/p/12568408.html  Linux 網絡編程的5種IO模型:多路復用(select/poll/epoll)

https://www.cnblogs.com/schips/p/12575493.html  Linux 網絡編程的5種IO模型:信號驅動IO模型

https://www.cnblogs.com/schips/p/12575933.html  Linux 網絡編程的5種IO模型:異步IO模型

https://www.cnblogs.com/natian-ws/p/10785649.html  Linux IO模式及 select、poll、epoll詳解

https://blog.csdn.net/coolgw2015/article/details/79719328  面試之多路復用

https://blog.csdn.net/yfkscu/article/details/38141635?locationNum=7  常見Linux IO模型分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM