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