目前常見的面向IO操做編程模型有如下幾種:編程
IO多路複用技術(又稱:事件驅動IO),是爲了解決傳統同步阻塞IO模式下,服務端的每一個線程(進程)只能給一個Client提供服務的問題,避免服務器建立大量線程(佔用大量內存、線程切換開銷)的問題而設計的。本文對各類網絡IO編程模型進行簡要的介紹。數組
本文主要討論的場景是Linux下的網絡IO,主要參考了Richard Stevens的《UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking API》和網絡上的相關文章。服務器
先來了解下IO過程相關的同步、異步,阻塞、非阻塞等概念。在IO操做過程當中涉及的對象包括:網絡
IO操做過程當中包括兩個階段(以Read操做爲例),分別是:數據結構
關於同/異步,非阻塞/阻塞,就是圍繞兩個對象和兩個階段展開的。多線程
同步異步
同步、異步主要是指用戶程序和 內核 的交互模式。
同步:用戶程序觸發IO操做後,經過一直等待或輪詢的方式去檢查內核IO操做是否就緒,直到IO就緒。
異步:用戶程序觸發IO操做後,幹其餘事情去了,當內核IO操做就緒後,內核把數據寫入用戶程序指定的Buffer。併發
從代碼實現表現來看,IO操做後,同步模式下,用戶須要本身不斷去輪詢內核獲取結果;而異步模式下內核會回寫數據到用戶程序Buffer並通知用戶程序。同步異步的關鍵:IO設備狀態須要本身輪詢,仍是內核主動通知。框架
阻塞非阻塞
阻塞非阻塞,是程序執行IO操做的系統調用時,根據IO設備操做的就緒狀態採起不一樣處理方式。
阻塞:當程序試圖進行讀寫IO操做時,若是暫時沒有可讀數據或者不可寫,程序就一直處於等待狀態,直到可讀或可寫爲止。
非阻塞:當程序試圖進行讀寫IO操做時,若是無可讀數據或不可寫,程序立刻返回,而不會等待。異步
從代碼實現表現來看,IO操做後,阻塞模式下,方法掛起不返回,直到IO設備就緒操做成功,方法才返回;而非阻塞模式,調用方法不等待IO設備就緒就直接返回。阻塞非阻塞的關鍵:方法調用後是否當即返回。函數
Linux中默認的Socket IO都是Blocking的,讀數據的流程以下:
當用戶程序調用recvfrom系統調用時,Kernel開始IO的第一個階段:數據準備。對於網絡IO,此時數據極可能還在網絡上傳送,所以Kernel須要等待數據傳輸完成。
此時,用戶程序會阻塞,等待Kernel返回。當Kernel接收到數據後,須要把數據拷貝的用戶內存空間。而後Kernel(recvfrom)才返回,用戶程序才能繼續往下執行。
因此,Blocking IO在的數據準備和數據拷貝兩個階段,用戶程序都是Blocking的。Socket編程提供的listen/send/recv等接口,都是Blocking模式的。
Blocking模式的主要優勢時程序結構/流程清晰;缺點時每一個線程同時只能給一個客戶端提供服務,只能採用多線程(多進程)的方式來提供併發服務。採用多線程的方式有如下缺點:
對於多線程的問題,能夠經過線程池來避免線程重複建立回收的開銷,經過維護必定數量的線程,防止服務器資源耗盡等問題,經過請求排隊處理,保持適當的併發服務等。可是對於成千上萬的請求時,線程池模式仍是會存在瓶頸。
經過給Socket設置setblocking(False)把Socket設置爲非阻塞模式,非阻塞模式下,經過recvform系統調用讀取數據的流程以下:
在Non-Blocking模式下,調用recvfrom時,若是Kernel尚未準備好數據,不會Block,而是直接返回Error。用戶程序經過判斷返回值,能夠肯定數據是否Ready。若是Kernel數據未準備好,用戶程序能夠再發起調用,或者趁這段時間作些其餘操做,而後再回來調用,直到方法返回成功。
一旦用戶再次調用recvfrom,而且Kernel數據已經Ready,就會把數據拷貝到用戶內存空間,並返回。因此在Non-Blocking模式,用戶程序須要經過不斷調用Kernel(即輪詢)來查詢數據是否Ready,並且數據拷貝階段仍是Block的。
因爲須要輪詢,雖然用戶程序在這段時間能夠作其餘操做,可是一般仍是只是不斷的執行輪詢操做,輪詢會大量佔用CPU時間,並且期間去執行其餘操做,會讓程序執行邏輯混亂,因此被阻塞模式通常不被推薦使用,而是採用Kernel自帶輪詢的IO多路複用技術。
IO多路複用,又叫事件驅動IO(Event Driver IO),底層使用了Kernel提供的select/poll/epoll(爲何有3種技術,也是從老到新一步步優化的過程)等系統調用。此技術的主要方法是在單個線程中,由select函數負責對多個Socket輪詢,一旦其中有Socket數據Ready了,就會通知用戶程序進行處理。
當調用了select後,用戶程序會Block,同時Kernel監控此select負責的全部Socket,當有一個或多個Socket數據Ready事件發生時(事件驅動IO),select就會返回。用戶程序再逐個調用Ready的Socket的recvfrom函數,把數據從Kernel拷貝到用戶內存。
從流程上看,IO多路複用和Blocking IO區別不大,都會Block用戶程序。IO多路複用的主要優勢是一個線程能夠Handle多個Socket。但若是IO數據接收後的處理任務是計算密集型業務時,由一個線程處理多個任務,性能上會沒法勝任,客戶端出現卡頓和延遲。
select模型的另一個問題是雖然用戶程序不在須要輪詢了,可是Kernel仍是須要去輪詢Socket,消耗大量CPU,系統設置Socket的上限是1024。許多系統提供了更高效的接口,如Linux的epoll,BSD的kqueue等,但這些接口的主要問題是不能跨平臺。
select | poll | epoll | |
---|---|---|---|
實現年代 | 1984 | 1997 | 2002 |
底層數據結構 | 數組 | 鏈表 | 哈希表 |
鏈接數 | 1024 | 無限制 | 無限制,OS支持最大FD數 |
事件檢測方法 | 遍歷FD(File Descriptor) | 遍歷FD | IO就緒自動callback,檢查就緒鏈表便可 |
時間複雜度 | O(n) | O(n) | O(1) |
FD處理 | 每次調用,須要把FD從用戶內存拷貝到內核 | 每次調用,FD拷貝 | epoll_ctl一次拷貝FD,無需重複拷貝 |
函數 | select | poll | epoll_create建立句柄,epoll_ctl註冊IO就緒監聽事件,epoll_wait等待事件就緒 |
IO多路複用模式,對於單個Socket,是非阻塞的,以便在一個線程裏同時處理多個Socket,可是select在處理輪詢IO設備狀態時,是阻塞的。
異步IO是Linux2.6內核引入的新功能,使用的很少,流程以下:
用戶程序發起aio_read調用後,程序當即返回。從Kernel的角度,流程以下:
說實話,異步IO功能我也沒用過,並且Linux底層仍是使用epoll實現的,性能上並無優點。Java著名NIO框架Netty就使用了IO多路複用,而非異步IO。
直接上大神的圖。
從上圖可見,阻塞式IO和IO多路複用,特色還行比較明細的,本質上都是同步IO,就不展開了。Non-Blocking雖然在數據準備階段不阻塞了,可是用戶程序仍是須要輪詢Check數據狀態;而異步IO,發完請求後,用戶程序就返回了,數據準備和拷貝,都由Kernel來完成。