网络 IO 演变发展过程和模型介绍
作者:jaydenwen,騰訊 pcg 后臺開發工程師
在互聯網中提起網絡,我們都會避免不了討論高并發、百萬連接。而此處的百萬連接的實現,脫離不了網絡 IO 的選擇,因此本文作為一篇個人學習的筆記,特此進行記錄一下整個網絡 IO 的發展演變過程。以及目前廣泛使用的網絡模型。
1.網絡 IO 的發展
在本節內容中,我們將一步一步介紹網絡 IO 的演變發展過程。介紹完發展過程后,再對網絡 IO 中幾組容易混淆的概念進行對比、分析。
1.1 網絡 IO 的各個發展階段
通常,我們在此討論的網絡 IO 一般都是針對 linux 操作系統而言。網絡 IO 的發展過程是隨著 linux 的內核演變而變化,因此網絡 IO 大致可以分為如下幾個階段:
1. 阻塞 IO(BIO)
2. 非阻塞 IO(NIO)
3. IO 多路復用第一版(select/poll)
4. IO 多路復用第二版(epoll)
5. 異步 IO(AIO)
而每一個階段,都是因為當前的網絡有一些缺陷,因此又在不斷改進該缺陷。這是網絡 IO 一直演變過程中的本質。下面將對上述幾個階段進行介紹,并對每個階段的網絡 IO 解決了哪些問題、優點、缺點進行剖析。
1.2 網絡的兩個階段
在網絡中,我們通常可以將其廣義上劃分為以下兩個階段:
第一階段:硬件接口到內核態
第二階段:內核態到用戶態
本人理解:我們通常上網,大部分數據都是通過網線傳遞的。因此對于兩臺計算機而言,要進行網絡通信,其數據都是先從應用程序傳遞到傳輸層(TCP/UDP)到達內核態,然后再到網絡層、數據鏈路層、物理層,接著數據傳遞到硬件網卡,最后通過網絡傳輸介質傳遞到對端機器的網卡,然后再一步一步數據從網卡傳遞到內核態,最后再拷貝到用戶態。
1.3 阻塞 IO 和非阻塞 IO 的區別
根據 1.2 節的內容,我們可以知道,網絡中的數據傳輸從網絡傳輸介質到達目的機器,需要如上兩個階段。此處我們把從硬件到內核態這一階段,是否發生阻塞等待,可以將網絡分為阻塞 IO和非阻塞 IO。如果用戶發起了讀寫請求,但內核態數據還未準備就緒,該階段不會阻塞用戶操作,內核立馬返回,則稱為非阻塞 IO。如果該階段一直阻塞用戶操作。直到內核態數據準備就緒,才返回。這種方式稱為阻塞 IO。
因此,區分阻塞 IO 和非阻塞 IO 主要看第一階段是否阻塞用戶操作。
1.4 同步 IO 和異步 IO 的區別
從前面我們知道了,數據的傳遞需要兩個階段,在此處只要任何一個階段會阻塞用戶請求,都將其稱為同步 IO,兩個階段都不阻塞,則稱為異步 IO。
在目前所有的操作系統中,linux 中的 epoll、mac 的 kqueue 都屬于同步 IO,因為其在第二階段(數據從內核態到用戶態)都會發生拷貝阻塞。而只有 windows 中的 IOCP 才真正屬于異步 IO,即 AIO。
2.阻塞 IO
在本節,我們將介紹最初的阻塞 IO,阻塞 IO 英文為 blocking IO,又稱為 BIO。根據前面的介紹,阻塞 IO 主要指的是第一階段(硬件網卡到內核態)。
2.1 阻塞 IO 的概念
阻塞 IO,顧名思義當用戶發生了系統調用后,如果數據未從網卡到達內核態,內核態數據未準備好,此時會一直阻塞。直到數據就緒,然后從內核態拷貝到用戶態再返回。具體過程可以參考 2.2 的圖示。
2.2 阻塞 IO 的過程
2.3 阻塞 IO 的缺點
在一般使用阻塞 IO 時,都需要配置多線程來使用,最常見的模型是阻塞 IO+多線程,每個連接一個單獨的線程進行處理。
我們知道,一般一個程序可以開辟的線程是有限的,而且開辟線程的開銷也是比較大的。也正是這種方式,會導致一個應用程序可以處理的客戶端請求受限。面對百萬連接的情況,是無法處理。
既然發現了問題,分析了問題,那就得解決問題。既然阻塞 IO 有問題,本質是由于其阻塞導致的,因此自然而然引出了下面即將介紹的主角:非阻塞 IO
3.非阻塞 IO
非阻塞 IO 是為了解決前面提到的阻塞 IO 的缺陷而引出的,下面我們將介紹非阻塞 IO 的過程。
3.1 非阻塞 IO 的概念
非阻塞 IO:見名知意,就是在第一階段(網卡-內核態)數據未到達時不等待,然后直接返回。因此非阻塞 IO 需要不斷的用戶發起請求,詢問內核數據好了沒,好了沒。
3.2 非阻塞 IO 的過程
非阻塞 IO 是需要系統內核支持的,在創建了連接后,可以調用 setsockop 設置 noblocking
3.3 非阻塞 IO 的優點
正如前面提到的,非阻塞 IO 解決了阻塞 IO每個連接一個線程處理的問題,所以其最大的優點就是?一個線程可以處理多個連接,這也是其非阻塞決定的。
3.4 非阻塞 IO 的缺點
但這種模式,也有一個問題,就是需要用戶多次發起系統調用。頻繁的系統調用是比較消耗系統資源的。
因此,既然存在這樣的問題,那么自然而然我們就需要解決該問題:保留非阻塞 IO 的優點的前提下,減少系統調用
4.IO 多路復用第一版
為了解決非阻塞 IO 存在的頻繁的系統調用這個問題,隨著內核的發展,出現了 IO 多路復用模型。那么我們就需要搞懂幾個問題:
IO 多路復用到底復用什么?
IO 多路復用如何復用?
IO 多路復用:?很多人都說,IO 多路復用是用一個線程來管理多個網絡連接,但本人不太認可,因為在非阻塞 IO 時,就已經可以實現一個線程處理多個網絡連接了,這個是由于其非阻塞而決定的。
在此處,個人觀點,多路復用主要復用的是通過有限次的系統調用來實現管理多個網絡連接。最簡單來說,我目前有 10 個連接,我可以通過一次系統調用將這 10 個連接都丟給內核,讓內核告訴我,哪些連接上面數據準備好了,然后我再去讀取每個就緒的連接上的數據。因此,IO 多路復用,復用的是系統調用。通過有限次系統調用判斷海量連接是否數據準備好了
無論下面的 select、poll、epoll,其都是這種思想實現的,不過在實現上,select/poll 可以看做是第一版,而 epoll 是第二版
4.1IO 多路復用第一版的概念
IO 多路復用第一版,這個概念是本人想出來的,主要是方便將 select/poll 和 epoll 進行區分
所以此處 IO 多路復用第一版,主要特指 select 和 poll 這兩個。
select 的 api
// readfds:關心讀的fd集合;writefds:關心寫的fd集合;excepttfds:異常的fd集合 int?select?(int?n,?fd_set?*readfds,?fd_set?*writefds,?fd_set?*exceptfds,?struct?timeval?*timeout);select 函數監視的文件描述符分 3 類,分別是 writefds、readfds、和 exceptfds。調用后 select 函數會阻塞,直到有描述副就緒(有數據 可讀、可寫、或者有 except),或者超時(timeout 指定等待時間,如果立即返回設為 null 即可),函數返回。當 select 函數返回后,可以 通過遍歷 fdset,來找到就緒的描述符。
select 目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點。select 的一 個缺點在于單個進程能夠監視的文件描述符的數量存在最大限制,在 Linux 上一般為 1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但 是這樣也會造成效率的降低。
poll 的 api
int?poll?(struct?pollfd?*fds,?unsigned?int?nfds,?int?timeout);struct?pollfd?{int?fd;?/*?file?descriptor?*/short?events;?/*?requested?events?to?watch?*/short?revents;?/*?returned?events?witnessed?*/ };pollfd 結構包含了要監視的 event 和發生的 event,不再使用 select“參數-值”傳遞的方式。同時,pollfd 并沒有最大數量限制(但是數量過大后性能也是會下降)。和 select 函數一樣,poll 返回后,需要輪詢 pollfd 來獲取就緒的描述符。
從上面看,select 和 poll 都需要在返回后,通過遍歷文件描述符來獲取已經就緒的 socket。事實上,同時連接的大量客戶端在一時刻可能只有很少的處于就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。
從本質來說:IO 多路復用中,select()/poll()/epoll_wait()這幾個函數對應第一階段;read()/recvfrom()對應第二階段
4.2IO 多路復用第一版的過程
4.3IO 多路復用第一版的優點
IO 多路復用,主要在于復用,通過 select()或者 poll()將多個 socket fds 批量通過系統調用傳遞給內核,由內核進行循環遍歷判斷哪些 fd 上數據就緒了,然后將就緒的 readyfds 返回給用戶。再由用戶進行挨個遍歷就緒好的 fd,讀取或者寫入數據。
所以通過 IO 多路復用+非阻塞 IO,一方面降低了系統調用次數,另一方面可以用極少的線程來處理多個網絡連接。
4.4IO 多路復用第一版的缺點
雖然第一版 IO 多路復用解決了之前提到的頻繁的系統調用次數,但同時引入了新的問題:用戶需要每次將海量的 socket fds 集合從用戶態傳遞到內核態,讓內核態去檢測哪些網絡連接數據就緒了
但這個地方會出現頻繁的將海量 fd 集合從用戶態傳遞到內核態,再從內核態拷貝到用戶態。所以,這個地方開銷也挺大。
既然還有這個問題,那我們繼續開始解決這個問題,因此就引出了第二版的 IO 多路復用。
其實思路也挺簡單,既然需要拷貝,那就想辦法,不拷貝。既然不拷貝,那就在內核開辟一段區域咯
4.5IO 多路復用第一版的區別
select 和 poll 的區別
select 能處理的最大連接,默認是 1024 個,可以通過修改配置來改變,但終究是有限個;而 poll 理論上可以支持無限個
select 和 poll 在管理海量的連接時,會頻繁的從用戶態拷貝到內核態,比較消耗資源。
5.IO 多路復用第二版
IO 多路復用第二版主要指 epoll,epoll 的出現也是隨著內核版本迭代才誕生的,在網上到處看到,epoll 是內核 2.6 以后開始支持的
epoll 的出現是為了解決前面提到的 IO 多路復用第一版的問題
5.1IO 多路復用第二版的概念
epoll 提供的 api
//創建epollFd,底層是在內核態分配一段區域,底層數據結構紅黑樹+雙向鏈表 int?epoll_create(int?size);//創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大//往紅黑樹中增加、刪除、更新管理的socket?fd int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event);//這個api是用來在第一階段阻塞,等待就緒的fd。 int?epoll_wait(int?epfd,?struct?epoll_event?*?events,?int?maxevents,?int?timeout); 1.?int?epoll_create(int?size); 創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大,這個參數不同于select()中的第一個參數,給出最大監聽的fd+1的值,參數size并不是限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。 當創建好epoll句柄后,它就會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 函數是對指定描述符fd執行op操作。 - epfd:是epoll_create()的返回值。 - op:表示op操作,用三個宏來表示:添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別添加、刪除和修改對fd的監聽事件。 - fd:是需要監聽的fd(文件描述符) - epoll_event:是告訴內核需要監聽什么事,struct epoll_event結構如下:struct?epoll_event?{__uint32_t?events;??/*?Epoll?events?*/epoll_data_t?data;??/*?User?data?variable?*/ };//events可以是以下幾個宏的集合: EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉); EPOLLOUT:表示對應的文件描述符可以寫; EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來); EPOLLERR:表示對應的文件描述符發生錯誤; EPOLLHUP:表示對應的文件描述符被掛斷; EPOLLET:?將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的。 EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里3.?int?epoll_wait(int?epfd,?struct?epoll_event?*?events,?int?maxevents,?int?timeout); 等待epfd上的io事件,最多返回maxevents個事件。 參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個maxevents的值不能大于創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。二 工作模式
epoll 對文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默認模式,LT 模式與 ET 模式的區別如下: LT 模式:當 epoll_wait 檢測到描述符事件發生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用 epoll_wait 時,會再次響應應用程序并通知此事件。 ET 模式:當 epoll_wait 檢測到描述符事件發生并將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait 時,不會再次響應應用程序并通知此事件。
LT 模式
LT(level triggered)是缺省的工作方式,并且同時支持 block 和 no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的 fd 進行 IO 操作。如果你不作任何操作,內核還是會繼續通知你的。
ET 模式
ET(edge-triggered)是高速工作方式,只支持 no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過 epoll 告訴你。然后它會假設你知道文件描述符已經就緒,并且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少于一定量時導致了一個 EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個 fd 作 IO 操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)
ET 模式在很大程度上減少了 epoll 事件被重復觸發的次數,因此效率要比 LT 模式高。epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
5.2IO 多路復用第二版的過程
當 epoll_wait()調用后會阻塞,然后完了當返回時,會返回了哪些 fd 的數據就緒了,用戶只需要遍歷就緒的 fd 進行讀寫即可。
5.3IO 多路復用第二版的優點
IO 多路復用第二版 epoll 的優點在于:
一開始就在內核態分配了一段空間,來存放管理的 fd,所以在每次連接建立后,交給 epoll 管理時,需要將其添加到原先分配的空間中,后面再管理時就不需要頻繁的從用戶態拷貝管理的 fd 集合。通通過這種方式大大的提升了性能。
所以現在的 IO 多路復用主要指 epoll
5.4IO 多路復用第二版的缺點
個人猜想:?如何降低占用的空間
6.異步 IO
6.1 異步 IO 的過程
前面介紹的所有網絡 IO 都是同步 IO,因為當數據在內核態就緒時,在內核態拷貝用用戶態的過程中,仍然會有短暫時間的阻塞等待。而異步 IO 指:內核態拷貝數據到用戶態這種方式也是交給系統線程來實現,不由用戶線程完成,目前只有 windows 系統的 IOCP 是屬于異步 IO。
7.網絡 IO 各種模型
7.1 reactor 模型
目前 reactor 模型有以下幾種實現方案:
1. 單 reactor 單線程模型
2. 單 reactor 多線程模型
3. multi-reactor 多線程模型
4. multi-reactor 多進程模型
下文網絡模型的圖,均摘自這篇文章
7.1.1 單 reactor 單線程模型
此種模型,通常是只有一個 epoll 對象,所有的接收客戶端連接、客戶端讀取、客戶端寫入操作都包含在一個線程內。該種模型也有一些中間件在用,比如 redis
但在目前的單線程 Reactor 模式中,不僅 I/O 操作在該 Reactor 線程上,連非 I/O 的業務操作也在該線程上進行處理了,這可能會大大延遲 I/O 請求的響應。所以我們應該將非 I/O 的業務邏輯操作從 Reactor 線程上卸載,以此來加速 Reactor 線程對 I/O 請求的響應。
7.1.2 單 reactor 多線程模型
該模型主要是通過將,前面的模型進行改造,將讀寫的業務邏輯交給具體的線程池來實現,這樣可以顯示 reactor 線程對 IO 的響應,以此提升系統性能
在工作者線程池模式中,雖然非 I/O 操作交給了線程池來處理,但是所有的 I/O 操作依然由 Reactor 單線程執行,在高負載、高并發或大數據量的應用場景,依然較容易成為瓶頸。所以,對于 Reactor 的優化,又產生出下面的多線程模式。
7.1.3 multi-reactor 多線程模型
在這種模型中,主要分為兩個部分:mainReactor、subReactors。mainReactor 主要負責接收客戶端的連接,然后將建立的客戶端連接通過負載均衡的方式分發給 subReactors,
subReactors 來負責具體的每個連接的讀寫
對于非 IO 的操作,依然交給工作線程池去做,對邏輯進行解耦
mainReactor 對應 Netty 中配置的 BossGroup 線程組,主要負責接受客戶端連接的建立。一般只暴露一個服務端口,BossGroup 線程組一般一個線程工作即可 subReactor 對應 Netty 中配置的 WorkerGroup 線程組,BossGroup 線程組接受并建立完客戶端的連接后,將網絡 socket 轉交給 WorkerGroup 線程組,然后在 WorkerGroup 線程組內選擇一個線程,進行 I/O 的處理。WorkerGroup 線程組主要處理 I/O,一般設置 2*CPU 核數個線程
7.2 proactor 模型
proactor 主要是通過對異步 IO 的封裝的一種模型,它需要底層操作系統的支持,目前只有 windows 的 IOCP 支持的比較好。詳細的介紹可以參考這篇文章
7.3 主流的中間件所采用的網絡模型
7.4 主流網絡框架
netty
gnet
libevent
evio(golang)
ACE(c++)
boost::asio(c++)
mudou (linux only)
關于c++和c的上述幾個庫對比,感興趣的話大家可以自行搜索資料或者閱讀這篇文章。
8.參考資料
IO 模式和 IO 多路復用
Linux IO 模式及 select、poll、epoll 詳解
Chapter 6. I/O Multiplexing: The select and poll Functions
高性能 IO 模型分析-Reactor 模式和 Proactor 模式(二)
歡迎關注騰訊程序員視頻號
總結
以上是生活随笔為你收集整理的网络 IO 演变发展过程和模型介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 海量小文件场景下训练加速优化之路
- 下一篇: 10个程序员才懂的灯谜,你能猜对几个?