同步非同步 阻塞非阻塞 Linux網路io模型
阿新 • • 發佈:2019-02-18
一,概念描述同步與非同步#首先來解釋同步和非同步的概念,這兩個概念與訊息的通知機制有關。也就是同步與非同步主要是從訊息通知機制角度來說的。概念描述:所謂同步就是一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成後,依賴的任務才能算完成,這是一種可靠的任務序列。要麼成功都成功,失敗都失敗,兩個任務的狀態可以保持一致。所謂非同步是不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什麼工作,依賴的任務也立即執行,只要自己完成了整個任務就算完成了。至於被依賴的任務最終是否真正完成,依賴它的任務無法確定,所以它是不可靠的任務序列。非同步io模型:非同步的概念相同,在網路socket讀寫的時候(read函式,receive函式等,產生系統呼叫,把資料拷貝到當核心當前的緩衝區裡面去),例如用read讀資料,發起系統呼叫時,只需把系統呼叫資訊通知給核心,執行緒此時就可以返回了,由核心來處理後續的事宜,核心有佇列來接收訊號並儲存起來,當要讀的socket資料到達就緒時,核心回來判斷是否丟資料包,是否丟位元組,是否有序等等,資料就緒後核心會把資料拷貝到傳送者給他傳遞的某個描述符檔案中或者buffer中去,核心的拷貝完成後 ,通知傳送者,即程序或執行緒。非阻塞io:當產生了系統呼叫,核心資料沒有準備好,但是核心不會把呼叫者的CPU剝奪掉,這時間隔某個時間段再次問系統資源是否準備好,直到所需的資源準備就緒,準備就緒後,等待核心呼叫API把資料拷貝到使用者態裡面去,等待的過程是阻塞的,或執行的時間到了,才會剝奪(此時為主動剝奪)非阻塞的瓶頸:一次讀取資料可能多次切入切出核心,切入切出過程需要上下文保護,需要把當前堆疊中的資訊進行儲存。阻塞模式讀取socket資料時:當資料沒有準備好,即核心的buffer沒有準備好,會發生阻塞,即當前程序或執行緒的CPU被剝奪,則其無法執行其他程式碼,所以對使用者來說,這個程序或執行緒就停止了,使用者量很大的時候,非阻塞就不適合了,因為讀取每一個socket描述符時,都會導致CPU無法被其他程序或執行緒使用,直到資料就緒,伺服器程式設計用這種模型時,因為網路環境不可預知,可能阻塞,而且使用者網路可能比較差,因此會導致整個CPU消耗很大,但是利用率很低。問題:那麼非同步的時候剝奪CPU了嗎?答:CPU也同樣沒有被剝奪,可以去做其他的事情,當核心把資料拷貝執行完成之後,才去通知呼叫者。io複用函式:select poll epoll 當他們返回已經就緒的描述符時,需要執行其他API來消費這個就緒的事件,消費的過程實際上是阻塞的,即讀和寫。因此io複用的作用是能夠一次性批量的監聽和收集描述符,但是描述符就緒後需要讀寫進行消費,消費過程仍然是阻塞的。過程:獲取到就緒的描述符後,加入到描述符表中(加入的方式不一樣),加入後設置非阻塞API。Linux的io模型網路IO的本質是socket的讀取,socket在linux系統被抽象為流,IO可以理解為對流的操作 對於一次IO訪問(以read舉例),資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間將上述過程細化:資料傳送到當前網絡卡,網絡卡驅動程式把資料取出來,拷貝到對應的核心中事先建立好的某一個程序中,拷到這個程序內部維護的某一個檔案描述符對應的緩衝區。對應到程式設計中,先定義xbuffer,然後read的API引數,第一個為fd,從哪個fd中讀,傳給核心,核心知道fd是由哪個程序維護的。當一個read操作發生時,它會經歷兩個階段:第一階段:等待資料準備 (Waiting for the data to be ready)。(對方資料傳送,在路由器中傳送,驅動程式讀取資料,讀取後找到程序、檔案描述符和buffer來拷貝,拷貝後資料已經交付給核心,核心通過一系列規則來判斷是否丟包,有丟包則重發,直到資料沒問題;沒問題後喚醒程序或執行緒,喚醒後在程序執行緒持有CPU執行讀取資料之前也會產生等待,因為排程延遲,當系統負載很高時,核心有可能無法持有CPU去喚醒) 第二階段:將資料從核心拷貝到程序中 (Copying the data from the kernel to the process)。拷貝過程是一個阻塞的過程。對於socket流而言第一步:通常涉及等待網路上的資料分組到達,然後被複制到核心的某個緩衝區(某個socket所分配的讀寫緩衝區)。第二步:把資料從核心緩衝區複製到應用程序緩衝區。(程序緩衝區,task_struct、mm_struct)網路應用需要處理的無非就是兩大類問題,網路IO,資料計算。相對於後者,網路IO的延遲,給應用帶來的效能瓶頸大於後者.因此程式設計的優化就是如何給CPU準備好資料,是CPU的利用率提高。網路IO的模型大致有如下幾種:(劃分的方式有很多,這裡是按照同步和非同步來區分)同步模型(synchronous IO) 阻塞IO(bloking IO) 非阻塞IO(non-blocking IO) 調整一個API就可以設定成非阻塞 多路複用IO(multiplexing IO) 訊號驅動式IO(signal-driven IO) 暫時沒講,不需要學,實際中已經不怎麼用了非同步IO(asynchronous IO)同步和非同步區別在於讀和寫資料的過程中與核心互動的過程,其他的地方相同,如tcp過程都需要建立socket,bind,listen,accept和close。同步非阻塞方式相比同步阻塞方式:優點:能夠在等待任務完成的時間裡幹其他活了(包括提交其他任務,也就是 “後臺” 可以有多個任務在同時執行)。缺點:任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成。這會導致整體資料吞吐量的降低。io多路複用目的:在同一個socket(複用)上能夠同時處理多種型別(多路)的描述符。如udp和tcp,還能同時處理多種事件監聽描述符事件,讀寫事件,異常事件。場景描述餐廳安裝了電子螢幕用來顯示點餐的狀態,這樣我和女友逛街一會,回來就不用去詢問服務員了,直接看電子螢幕就可以了。這樣每個人的餐是否好了,都直接看電子螢幕就可以了,這就是典型的IO多路複用。電子螢幕:io複用中返回的就緒描述符,遍歷描述符詢問型別(是讀是寫還是listen),然後呼叫對應的API去處理這個描述符。網路模型由於同步非阻塞方式需要不斷主動輪詢,輪詢佔據了很大一部分過程,輪詢會消耗大量的CPU時間,而 “後臺” 可能有多個任務在同時進行,人們就想到了迴圈查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它。如果輪詢不是程序的使用者態,而是有人幫忙就好了(即尋找一個代理,減少切入切出的狀態,用代理來問核心,在核心裡判斷)。那麼這就是所謂的 “IO 多路複用”。UNIX/Linux 下的 select、poll、epoll 就是幹這個的(epoll 比 poll、select 效率高,做的事情是一樣的)。IO多路複用有兩個特別的系統呼叫select、poll、epoll函式。select呼叫是核心級別的,select輪詢相對非阻塞的輪詢的區別在於---前者可以等待多個socket,能實現同時對多個IO埠進行監聽,當其中任何一個socket的資料準好了,就能返回進行可讀(具體多少數目就緒好再返回,阻塞非阻塞等是由select最後一個引數,超時機制來控制的)。然後程序再進行recvform系統呼叫,將資料由核心拷貝到使用者程序,當然這個過程是阻塞的。過程描述:使用者將關心的描述符通過呼叫select函式註冊給核心,核心在超時時間內判斷多個描述符就緒,時間到達時,若沒有描述符就緒,會超時返回;如果有描述符就緒,就提前返回。select或poll呼叫之後,會阻塞程序,與blocking IO阻塞(即常規阻塞)不同在於,此時的select不是等到socket資料全部到達再處理, 而是有了一部分資料就會呼叫使用者程序來處理。過程描述:如超時時間為1s,在1s時間到達之前,有一部分資料就緒了,就可以返回。而之前的常規阻塞,讀資料描述符沒有就緒時,只能等待,等待就緒並處理完成之後,才能去處理下一個描述符。下一個事件可能是accept或者繼續讀資料,但每一次呼叫必須只能處理一個,處理完成才能進入下一件處理事件。常規阻塞返回條件:讀到資料或者對方把連線關閉掉才能返回,而且它必須要返回,後面所有客戶端連線,讀資料,寫資料才能被處理。程式碼:在accept之上有一個while或for死迴圈,指的是不斷accept新的連線,如果新的連線有,那麼accept返回,返回後讀資料,如果讀資料時對方資料一直無法就緒,那麼迴圈無法繼續往下行走,於是產生阻塞,當事件就緒處理完成後,才能進入下一次迴圈,處理新的連線。 因此阻塞模型,每時每刻只能處理一個客戶端。阻塞模型+fork的作用:是讓使用者1與使用者2之間不會產生相互等待的關係。如何知道有一部分資料是否到達,監視的事情交給了核心,核心負責資料到達的處理。也可以理解為"非阻塞"吧。過程描述:資料到網絡卡,由驅動程式讀,放在對應程序的某個buffer中去,放的時候會把描述符的狀態進行修改,把描述符的集合返回去,使用者拿到由select和poll返回的描述符集合列表,去處理對應的描述符裡面的資料。此時描述符的集合有沒有就緒,什麼時候就緒,這些處理是由核心直接完成的,核心不需要每一次針對每一個描述符進行多次使用者態與核心態之間的切換。I/O複用模型會用到select、poll、epoll函式,這幾個函式也會使程序阻塞,但是和阻塞I/O所不同的的,這兩個函式可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函式進行檢測,直到有資料可讀或可寫時(注意不是全部資料可讀或可寫),才真正呼叫I/O操作函式。對於多路複用,也就是輪詢多個socket。注:這裡的socket不是建立描述符的socket,是accept函式返回的socket檔案描述符,這個檔案描述符不會佔用新的埠。多路複用既然可以處理多個IO,也就帶來了新的問題,多個IO之間的順序變得不確定了,當然也可以針對不同的編號。比如批量註冊100個描述符,但是先註冊的描述符可能後被處理圖中描述的只是一個描述符返回就緒流程描述IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網路連線的IO。(這也是本身的目標,因為以前的阻塞模型,拿一個程序程式設計的時候,一旦某一個io阻塞,整個while或for迴圈就不能向下進行了;現在雖然也是一個程序,但是能夠處理多個網路連線了)它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有資料到達了,就通知使用者程序。當用戶程序呼叫了select,那麼整個程序會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的資料準備好了,select就會返回。這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。多路複用的特點是通過一種機制一個程序能同時等待多個IO檔案描述符,核心監視這些檔案描述符(套接字描述符),判斷有沒有就緒,就緒了該什麼時候返回,其中的任意一個進入讀就緒狀態,select, poll,epoll函式就可以返回。對於監視的方式,又可以分為 select, poll, epoll三種方式。如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server效能更好,可能延遲還更大。(select/epoll的優勢並不是對於單個連線能處理得更快(因為可能會有延遲),而是在於能處理更多的連線。)原因:因為多執行緒+阻塞io操作,例如可以開40個執行緒,阻塞在每一個io上,如果io準備就緒了,就可以第一時間返回,返回後執行緒可以擁有CPU來處理後續的動作;如果用select或poll,先註冊的描述符可能在後面處理,即順序會變掉。會造成延遲增大。但正常情況下 大多數情況還是使用io複用,根據使用者量確定,這裡只是表示他不是萬能的。在IO multiplexing Model中,實際中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。io複用中呼叫io複用函式也會產生阻塞把當前程序或執行緒阻塞掉,阻塞的原因並不是某個io不就緒,而且因為阻塞後讓核心有時間執行某一些描述符上的檢測以及資料的拷貝動作。所以IO多路複用是阻塞在select,epoll這樣的系統呼叫之上,而沒有阻塞在真正的I/O系統呼叫如recvfrom之上。這個真正的io系統呼叫阻塞是無法避免的。io多路複用劃分到同步阻塞模型中去,因為幾個函式本身就是阻塞的,返回資料讀寫時也是阻塞的。在I/O程式設計過程中,當需要同時處理多個客戶端接入請求時,可以利用多執行緒或者I/O多路複用技術進行處理。I/O多路複用技術通過把多個I/O的阻塞複用到同一個select的阻塞上,從而使得系統在單執行緒的情況下可以同時處理多個客戶端請求。與傳統的多執行緒/多程序模型比,I/O多路複用的最大優勢是系統開銷小,系統不需要建立新的額外程序或者執行緒,也不需要維護這些程序和執行緒的執行,降底了系統的維護工作量,節省了系統資源,io複用的主要場景:伺服器需要同時處理多個處於監聽狀態或者多個連線狀態的套接字。伺服器需要同時處理多種網路協議的套接字。前面三種IO模式,在使用者程序進行系統呼叫的時候,他們在等待資料到來的時候,處理的方式不一樣,直接等待,輪詢,select或poll輪詢,兩個階段過程:第一個階段有的阻塞(阻塞模型),有的不阻塞(非阻塞模型),有的可以阻塞又可以不阻塞(select或poll)。第二個階段都是阻塞的。無論通過上面哪一種方式,當判斷描述符就緒了,消費事件時,就是阻塞的。從整個IO過程來看,他們都是順序執行的,因此可以歸為同步模型(synchronous)。都是程序主動等待且向核心檢查狀態。非阻塞模型--多次自己向系統呼叫檢查,io複用--核心幫助檢查,阻塞--核心幫助檢查,由核心負責喚醒高併發的程式一般使用同步非阻塞方式而非多執行緒 + 同步阻塞方式。要理解這一點,首先要扯到併發和並行的區別。比如去某部門辦事需要依次去幾個視窗,辦事大廳裡的人數就是併發數,而視窗個數就是並行度。也就是說併發數是指同時進行的任務數(如同時服務的 HTTP 請求),而並行數是可以同時工作的物理資源數量(如 CPU 核數)。通過合理排程任務的不同階段,併發數可以遠遠大於並行度,這就是區區幾個 CPU 可以支援上萬個使用者併發請求的奧祕。在這種高併發的情況下,為每個任務(使用者請求)建立一個程序或執行緒的開銷非常大(佔有很多系統資源)。而同步非阻塞方式可以把多個 IO 請求丟到後臺去,這就可以在一個程序裡服務大量的併發 IO 請求。並行:物理上,CPU只有一個,在物理上某一刻只能執行一個實體併發:邏輯上,在某一時間段內,執行多個程序非同步非阻塞場景描述女友不想逛街,又餐廳太吵了,回家好好休息一下。於是我們叫外賣,打個電話點餐,然後我和女友可以在家好好休息一下,飯好了送貨員送到家裡來。這就是典型的非同步,只需要打個電話說一下,然後可以做自己的事情,飯好了就送來了。非同步:只管傳送訊號,相信對方能夠執行任務。網路模型相對於同步IO,非同步IO不是順序執行。使用者程序進行aio_read系統呼叫之後,無論核心資料是否準備好,都會直接返回給使用者程序(即把CPU的使用控制權交還給程序),然後使用者態程序可以去做別的事情。等到socket資料準備好了,核心直接複製資料給程序,然後從核心向程序傳送通知。IO兩個階段,程序都是非阻塞的。(使用者程序發起讀寫事件,以及核心內部的拷貝過程,兩個過程,程序都是無感知的)同步必須要,先建立描述符,監聽描述符,等待就緒,就緒後才能讀寫,讀寫完成後才能進行其他事件。因此必須順序執行。流程描述使用者程序發起aio_read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read(非阻塞請求)之後,首先它會立刻返回,所以不會對使用者程序產生任何block。然後,kernel會等待資料準備完成(資料傳送,資料檢查,拷貝),然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程序傳送一個signal或執行一個基於執行緒的回撥函式來完成這次 IO 處理過程,告訴它read操作完成了。在Linux系統中,通知的方式就是“訊號”五種io模型總結阻塞與非阻塞的區別呼叫blocking IO會一直block住對應的程序直到操作完成,而non-blocking IO在kernel還準備資料的情況下會立刻返回。同步io與非同步io的區別看文章裡epoll優點
- 沒有最大併發連線的限制(這個poll也沒有,但是poll需要把每次關心的描述符拷貝到核心裡去),能開啟的FD的上限遠大於1024(1G的記憶體上能監聽約10萬個埠)。
- 效率提升,不是輪詢的方式,不會隨著FD數目的增加效率下降。只有活躍可用的FD才會呼叫callback函式(回撥函式);即Epoll最大的優點就在於它只管你“活躍”的連線,而跟連線總數無關,因此在實際的網路環境中,Epoll的效率就會遠遠高於select和poll。
- 記憶體拷貝,利用mmap()檔案對映記憶體加速與核心空間的訊息傳遞;即epoll使用mmap減少複製開銷。