亚洲精品亚洲人成在线观看麻豆,在线欧美视频一区,亚洲国产精品一区二区动图,色综合久久丁香婷婷

              當(dāng)前位置:首頁 > IT技術(shù) > 其他 > 正文

              深入理解NIO多路復(fù)用
              2022-05-31 17:11:28

              1. 背景

              最近準(zhǔn)備學(xué)習(xí)netty,發(fā)現(xiàn)自己對NIO相關(guān)的理論知之甚少。本文對NIO中多路復(fù)用的來龍去脈經(jīng)過了詳細(xì)的分析,通過實戰(zhàn)抓取系統(tǒng)調(diào)用日志,加深對底層理論的理解。在此基礎(chǔ)上,梳理了BIO/NIO相關(guān)的流程圖,加強(qiáng)對NIO的整體流程理解。

              2. 進(jìn)程和線程

              CPU每經(jīng)過一個時間片,隨機(jī)調(diào)度并執(zhí)行一個線程。這些線程共用了進(jìn)程的堆、常量區(qū)、方法區(qū)。每個線程只占用內(nèi)存中的少量內(nèi)存空間,用于存放個字的棧和程序計數(shù)器。線程和進(jìn)程的資源關(guān)系如下圖:

              進(jìn)程和線程共享區(qū)域.png

              3. 內(nèi)核態(tài)和用戶態(tài)

              機(jī)器啟動時,linux首先加載內(nèi)核代碼,啟動內(nèi)核進(jìn)程。創(chuàng)建全局描述符表,用于記錄內(nèi)核的內(nèi)存區(qū)域和用戶程序的內(nèi)存區(qū)域。內(nèi)核用于控制各種硬件,非常敏感,不允許用戶程序直接調(diào)用內(nèi)核代碼,訪問內(nèi)核的內(nèi)存區(qū)域。用戶程序必須通過linux系統(tǒng)調(diào)用,經(jīng)過linux的驗證后,CPU切換到內(nèi)核線程,代替用戶程序執(zhí)行對應(yīng)功能。
              系統(tǒng)調(diào)用過程如下:

              系統(tǒng)調(diào)用過程.png
              具體步驟為:

              1. 用于進(jìn)程代碼內(nèi)執(zhí)行read()方法。read其實就是要執(zhí)行系統(tǒng)調(diào)用了,在CPU的eax寄存器中保存對應(yīng)的系統(tǒng)調(diào)用號3。
              2. CPU執(zhí)行系統(tǒng)中斷指令0x80,查詢中斷描述符表,該指令表示系統(tǒng)調(diào)用。
              3. CPU查詢系統(tǒng)調(diào)用表,發(fā)現(xiàn)eax寄存器中的3表示讀取指令,切換內(nèi)核線程執(zhí)行讀取操作,最后將結(jié)果返回給用戶程序。

              4. 系統(tǒng)調(diào)用實戰(zhàn)

              4.1 BIO

              通過Java編寫SocketServer服務(wù)端代碼,Socket服務(wù)端以BIO阻塞的方式接受客戶端連接,并阻塞地接受客戶端數(shù)據(jù),打印數(shù)據(jù),打印完,維持線程不退出。

              image.png

              4.1.1 啟動SocketServerBIO服務(wù)端

              啟動SocketServerBIO程序,并通過strace命令跟蹤系統(tǒng)調(diào)用的執(zhí)行。-ff -o socket_file參數(shù)表示將系統(tǒng)調(diào)用的執(zhí)行結(jié)果打印到socket_file文件中,將進(jìn)程/線程的跟蹤結(jié)果輸出到相應(yīng)的socket_file.pid上:

              strace -ff -o socket_file /home/hadoop/jdk8/bin/java SocketServerBIO

              啟動BIO服務(wù)端:
              image.png

              創(chuàng)建了一個5735的進(jìn)程號:
              image.png

              通過/proc/5735/fd可以看到該進(jìn)程創(chuàng)建了兩個Socket,分別是IPv4的Socket和IPv6的Socket(不重要,不討論):

              image.png

              通過nestat -antp命令可以查看socket詳細(xì)信息。即SocketServerBIO進(jìn)程正在監(jiān)聽8888端口:

              image.png

              查看strace輸出文件,它以進(jìn)程號5735開始,socket_file.5735記錄進(jìn)程的系統(tǒng)調(diào)用過程,socket_file.5736為主線程的系統(tǒng)調(diào)用過程,后面的socket_file記錄的是子線程的系統(tǒng)調(diào)用過程:
              image.png

              通過socket_file.5736文件可以看到,主線程創(chuàng)建文件描述符分別是4(不重要,不討論)和5:

              image.png

              服務(wù)端創(chuàng)建socket,對應(yīng)文件描述符為5:
              image.png
              綁定8888端口,并監(jiān)聽該端口:
              image.png

              4.1.2 客戶端發(fā)起連接

              客戶端向8888端口發(fā)起連接請求
              nc localhost 8888

              可以看到服務(wù)端接受到了來自客戶端36204端口的socket連接請求:
              image.png

              此時服務(wù)端進(jìn)程5735新增了一個socket:
              image.png

              通過netstat -antp查看該socket詳細(xì)信息:

              image.png

              新增兩個socket連接,其實是一個意思,分別表示服務(wù)端創(chuàng)建了一個socket用于接受本機(jī)的36204端口的客戶端的請求;客戶端創(chuàng)建socket,使用36204端口向服務(wù)端8888端口發(fā)送請求。

              通過socket_file.5736文件可以看到,服務(wù)端接受了36204端口的客戶端請求,創(chuàng)建socket,并創(chuàng)建6號文件描述符,指向該socket:
              image.png
              接受socket請求后,程序代碼里面創(chuàng)建子線程需要通過系統(tǒng)調(diào)用clone()方法,生成的子線程ID號為7356:
              image.png
              同時,strace命令也創(chuàng)建了7356線程對應(yīng)的系統(tǒng)調(diào)用跟蹤信息文件socket_file.7356:
              image.png

              4.1.3 客戶端發(fā)送數(shù)據(jù)

              image.png

              服務(wù)端接受數(shù)據(jù)并打?。?br/>image.png

              查看socket_file.7356文件,發(fā)現(xiàn)該線程接受了hello world的消息,并等待下一次數(shù)據(jù)傳輸:
              image.png

              4.1.4 BIO總結(jié)

              由于BIO的accept方法是阻塞的,因此單線程阻塞時,如果已經(jīng)建立的連接發(fā)送數(shù)據(jù)到服務(wù)端,這時服務(wù)端由于阻塞不能處理該數(shù)據(jù),因此BIO模式下,服務(wù)器性能非常差。這時只有為每個建立的socket創(chuàng)建處理數(shù)據(jù)的子線程。線程模型如下:

              BIO.png

              它的缺點就是創(chuàng)建子線程浪費資源,可以通過NIO方式避免創(chuàng)建為每個連接創(chuàng)建子線程。

              4.2 非阻塞NIO

              通過Java編寫SocketServer服務(wù)端代碼,Socket服務(wù)端以NIO非阻塞的方式接受客戶端連接,并非阻塞地接受客戶端的數(shù)據(jù),打印數(shù)據(jù)。全程只有一個主線程工作。

              image.png

              4.2.1 啟動服務(wù)端

              依然使用strace -ff -o socket_file /home/hadoop/jdk8/bin/java SocketServerNIO命令啟動NIO服務(wù)端:

              image.png

              創(chuàng)建了ID號為29050的進(jìn)程:
              image.png

              strace系統(tǒng)調(diào)用跟蹤到socket_file文件中,如下所示:
              image.png

              socket_file.29051文件為主線程的系統(tǒng)調(diào)用日志??梢园l(fā)現(xiàn)NIO Socket系統(tǒng)調(diào)用中,對ServerSocket設(shè)置了NONBLOCK,即非阻塞(而BIO中,默認(rèn)就是阻塞的):

              image.png

              此時可以看到accept系統(tǒng)調(diào)用持續(xù)進(jìn)行調(diào)用,-1表示沒有連接:

              image.png

              4.2.2 客戶端連接

              image.png
              服務(wù)端接受客戶端請求,并建立連接
              image.png

              此時并沒有創(chuàng)建新線程:
              image.png

              socket_file.29051主線程系統(tǒng)調(diào)用日志創(chuàng)建了新的socket連接,用于與客戶端通信,文件描述符ID為6,并設(shè)置NONBLOCK非阻塞:

              image.png

              4.2.3 客戶端發(fā)送數(shù)據(jù)

              image.png

              服務(wù)端打印了該數(shù)據(jù):
              image.png

              socket_file.29051主線程系統(tǒng)調(diào)用日志中接受了該數(shù)據(jù)??梢钥吹缴厦娴膔ead返回-1,表示read沒有讀取到數(shù)據(jù),這表明read是是非阻塞的:
              image.png

              4.2.4 NIO總結(jié)

              通過設(shè)置NONBLOCK非阻塞,避免為每個socket創(chuàng)建對應(yīng)的線程。

              4.3 C10K問題

              隨著互聯(lián)網(wǎng)的普及,應(yīng)用的用戶群體幾何倍增長,此時服務(wù)器性能問題就出現(xiàn)。最初的服務(wù)器是基于進(jìn)程/線程模型。

              • 對于BIO,新到來一個TCP連接,就需要分配一個線程。假如有C10K,就需要創(chuàng)建1W個線程,可想而知單機(jī)是無法承受的。因此優(yōu)化BIO為NIO。
              • 對于NIO,只有一個線程。如果有C10K個連接,每次就需要進(jìn)行1w次循環(huán)遍歷,處理每個連接的數(shù)據(jù),每次遍歷都是一次系統(tǒng)調(diào)用。其實這種O(n)次數(shù)據(jù)的處理可以優(yōu)化成O(1),O(1)表示固定次數(shù)的遍歷。

              C10K全稱為10000 clients,即服務(wù)端處理1w個客戶端連接時,如何處理這么多連接,避免服務(wù)器出現(xiàn)性能問題。

              4.4 多路復(fù)用NIO

              在4.3節(jié)非阻塞IO的代碼中可以發(fā)現(xiàn),每次在用戶態(tài)都會遍歷所有socket,事件復(fù)雜度為O(n),可以通過多路復(fù)用NIO代碼,在操作系統(tǒng)內(nèi)核中,讓多路復(fù)用器遍歷所有socket,返回發(fā)生了狀態(tài)變化的m個socket,用戶程序每次只需要執(zhí)行m次遍歷即可。這樣將遍歷次數(shù)從n次優(yōu)化成為固定的m次。即將事件復(fù)雜度從O(n)優(yōu)化成為O(1)。下面看看多路復(fù)用器實現(xiàn)的發(fā)展歷程。

              4.4.1 select

              Select是初期的多路復(fù)用器實現(xiàn)。它們的接口如下:

              int select (int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

              參數(shù)解釋:

              • maxfdp:表示要檢查文件描述符的范圍,它的值為最大的文件描述+1。例如,檢查4和17兩個文件描述符,maxfdp大小就是18。
              • readfds:檢查readfds包含的文件描述符中,哪些文件描述符可讀。
              • writefds:檢查writefds包含的文件描述符中,哪些文件描述符可寫。
              • exceptfds:檢查exceptfds包含的文件描述符中,哪些文件描述符有異常要處理。

              fd_set位圖

              傳入select方法的參數(shù)類型為fd_set,它是位圖類型。32位機(jī)器的位圖類型默認(rèn)占1024位,64位的機(jī)器默認(rèn)占2048位。以32位機(jī)器為例,每一位的下標(biāo)表示一個文件描述符,例如1011就表示0,1,4號文件描述符。可以看到,位圖通過1024位就可以表示一個0~1023的數(shù)組,非常節(jié)省空間。但是1024位的位圖表示數(shù)組的范圍只有0~1023,如果要監(jiān)控文件描述符超過1024,應(yīng)該用Poll實現(xiàn)的多路復(fù)用器。

              select方法中的位圖舉例:

              • 當(dāng)select函數(shù)readfds參數(shù)為1001 0101時,是用戶想告訴內(nèi)核,需要監(jiān)視文件描述符等于0,2,4,7的文件的讀事件的狀態(tài)。
              • 當(dāng)select函數(shù)writefds參數(shù)為1000 0001時,是用戶想告訴內(nèi)核,需要監(jiān)視文件描述符等于0,7的文件的寫事件的狀態(tài)。

              select多路復(fù)用的流程如下:

              select多路復(fù)用NIO 2.png

              4.4.2 poll

              poll將輸入?yún)?shù)從位圖改成數(shù)組,如下所示:

              int poll(struct pollfd fds[], nfds_t nfds, int timeout);

              這意味著雖然數(shù)組占用的空間更大,使用poll能夠監(jiān)控的文件描述符不存在上限。select多路復(fù)用的流程如下:

              poll多路復(fù)用NIO 1.png

              4.4.3 select和poll總結(jié)

              1. 每次調(diào)用select和poll都是一次性傳入所有要監(jiān)控的文件描述符,只發(fā)生一次系統(tǒng)調(diào)用。
              2. 在內(nèi)核態(tài),內(nèi)核進(jìn)程通過O(n)的時間復(fù)雜度遍歷文件描述符的狀態(tài)。比較浪費CPU,不過這比用戶態(tài)O(n)遍歷要好,因為用戶態(tài)每次遍歷還要進(jìn)行系統(tǒng)調(diào)用。
              3. 內(nèi)核將發(fā)生狀態(tài)變化的文件描述符拷貝到內(nèi)核空間。
              4. 用戶遍歷狀態(tài)變化后的socket,為固定大小m,用戶態(tài)以O(shè)(1)時間復(fù)雜度遍歷這些socket。

              4.4.4 epoll

              上述select和poll的缺點是,內(nèi)核要以O(shè)(n)的時間復(fù)雜度遍歷文件描述符,當(dāng)客戶端連接越多,集合越大,消耗的CPU資源比較高,epoll就解決了這個問題。在內(nèi)核版本>=2.6則,具體的SelectorProvider為EPollSelectorProvider,否則為默認(rèn)的PollSelectorProvider。可見select和poll已經(jīng)過時了,epoll才是主流。

              epoll原理

              完成epoll操作一共有三個步驟,即三個函數(shù)互相配合:

              //建立一個epoll對象(在epoll文件系統(tǒng)中給這個句柄分配資源);
              int epoll_create(int size);  
              //向epoll對象中添加連接的套接字;
              int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
              // 等待事件的產(chǎn)生,收集發(fā)生事件的連接,類似于select()調(diào)用。
              int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  
              • 先用epoll_create創(chuàng)建一個epoll對象epfd,再通過epoll_ctl將需要監(jiān)視的socket添加到epfd中,最后調(diào)用epoll_wait等待數(shù)據(jù)。
              • 當(dāng)執(zhí)行 epoll_create 時 ,系統(tǒng)會在內(nèi)核cache創(chuàng)建一個紅黑樹和就緒鏈表。
              • 當(dāng)執(zhí)行epoll_ctl放入socket時 ,epoll會檢測上面的紅黑樹是否存在這個socket,存在的話就立即返回,不存在就添加。然后給內(nèi)核中斷處理程序注冊一個回調(diào)函數(shù),告訴內(nèi)核,如果這個socket句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。如果網(wǎng)卡有數(shù)據(jù)到達(dá),向cpu發(fā)出中斷信號,cpu響應(yīng)中斷,中斷程序就會執(zhí)行前面的回調(diào)函數(shù)。紅黑樹是自平衡的二叉排序樹,適合頻繁插入和刪除的場景。增刪查一般時間復(fù)雜度是 O(logn)。
              • epoll_wait就只檢查就緒鏈表,如果鏈表不為空,就返回就緒鏈表中就緒的socket,否則就等待。只將有事件發(fā)生的 Socket 集合傳遞給應(yīng)用程序,不需要像 select/poll 那樣輪詢掃描整個集合(包含有和無事件的 Socket ),大大提高了檢測的效率。

              上述流程如下所示:

              epoll多路復(fù)用NIO.png

              觸發(fā)事件

              epoll有兩種工作模式:LT(level-triggered,水平觸發(fā))模式和ET(edge-triggered,邊緣觸發(fā))模式。

              水平觸發(fā)(level-trggered):處于某個狀態(tài)時一直觸發(fā)。

              • 只要文件描述符關(guān)聯(lián)的讀內(nèi)核緩沖區(qū)非空,有數(shù)據(jù)可以讀取,就一直從epoll_wait中蘇醒并發(fā)出可讀信號進(jìn)行通知。
              • 只要文件描述符關(guān)聯(lián)的內(nèi)核寫緩沖區(qū)不滿,有空間可以寫入,就一直從epoll_wait中蘇醒發(fā)出可寫信號進(jìn)行通知。

              邊緣觸發(fā)(edge-triggered):在狀態(tài)轉(zhuǎn)換的邊緣觸發(fā)一次。

              • 當(dāng)文件描述符關(guān)聯(lián)的讀內(nèi)核緩沖區(qū)由空轉(zhuǎn)化為非空的時候,則從epoll_wait中蘇醒發(fā)出可讀信號進(jìn)行通知。
              • 當(dāng)文件描述符關(guān)聯(lián)的寫內(nèi)核緩沖區(qū)由滿轉(zhuǎn)化為不滿的時候,則從epoll_wait中蘇醒發(fā)出可寫信號進(jìn)行通知。

              簡單的說,ET模式在可讀和可寫時僅僅通知一次,而LT模式則會在條件滿足可讀和可寫時一直通知。比如,某個socket的內(nèi)核緩沖區(qū)中從沒有數(shù)據(jù)變成了有2k數(shù)據(jù),此時ET模式和LT模式都會進(jìn)行通知,隨后應(yīng)用程序可以讀取其中的數(shù)據(jù),假設(shè)只讀取了1k,緩沖區(qū)中還剩1k,此時緩沖區(qū)還是可讀的,如果再次檢查,那么ET模式則不會通知,而LT模式則會再次通知。

              ET模式的性能比LT模式更好,因為如果系統(tǒng)中有大量你不需要讀寫的就緒文件描述符,使用LT模式之后每次epoll_wait它們都會返回,這樣會大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率!而如果使用ET模式,則在不會進(jìn)行第二次通知,系統(tǒng)不會充斥大量你不關(guān)心的就緒文件描述符。

              所以,使用ET模式時需要一次性的把緩沖區(qū)的數(shù)據(jù)讀完為止,也就是一直讀,直到讀到EGAIN(EGAIN說明緩沖區(qū)已經(jīng)空了)為止,否則可能出現(xiàn)讀取數(shù)據(jù)不完整的問題。

              同理,LT模式可以處理阻塞和非阻塞套接字,而ET模式只支持非阻塞套接字,因為如果是阻塞的,沒有數(shù)據(jù)可讀寫時,進(jìn)程會阻塞在讀寫函數(shù)那里,程序就沒辦法繼續(xù)往下執(zhí)行了。

              默認(rèn)情況下,select、poll都只支持LT模式,epoll采用 LT模式工作,可以設(shè)置為ET模式。

              編寫多路復(fù)用服務(wù)端代碼:

              通過Java編寫SocketServer服務(wù)端代碼,創(chuàng)建多路復(fù)用器,將所有socket注冊到多路復(fù)用器中,多路復(fù)用器負(fù)責(zé)監(jiān)聽所有socket狀態(tài)變化,主線程通過獲取socket狀態(tài),進(jìn)行相應(yīng)處理即可。
              image.png

              運行代碼:

              服務(wù)端運行,并將系統(tǒng)調(diào)用日志記錄到socket_file文件中:

              strace -ff -o socket_file /home/hadoop/jdk8/bin/java SocketServerNIOEpoll

              客戶端連接服務(wù)端:

              nc localhost 8890

              客戶端發(fā)送數(shù)據(jù):

              image.png

              服務(wù)端接受數(shù)據(jù):

              image.png

              系統(tǒng)調(diào)用過程分析:

              創(chuàng)建5號serversocket:
              image.png

              綁定端口并監(jiān)聽:
              image.png

              設(shè)置serversocket為非阻塞:
              image.png

              創(chuàng)建epoll文件描述符,epoll文件描述符用于注冊用戶態(tài)的socket:

              image.png

              將serversocket注冊到epoll文件描述符中,阻塞等待新連接到來:
              image.png

              socketserver接受新連接,創(chuàng)建新9號socket:
              image.png

              設(shè)置新的9號socket連接為非阻塞:
              image.png

              將9號socket注冊到多路復(fù)用器epoll的8號文件描述符中(EPOLLIN就是LT,改成EPOLLIN | EPOLLET就是ET):
              image.png

              監(jiān)控到9號文件描述符有讀取事件并處理:
              image.png

              監(jiān)控epoll文件描述符,監(jiān)控紅黑樹中有沒有socket狀態(tài)變化:
              image.png

              4.4.4 epoll總結(jié)

              內(nèi)核事件通知socket狀態(tài)變化,而不是主動遍歷所有注冊的socket,節(jié)省了cpu資源。

              本文摘自 :https://blog.51cto.com/u

              開通會員,享受整站包年服務(wù)立即開通 >