最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

深入講解Netty那些事兒之從內(nèi)核角度看IO模型(上)

2022-12-01 17:08 作者:補(bǔ)給站Linux內(nèi)核  | 我要投稿

我們都知道Netty是一個(gè)高性能異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)框架。

它的設(shè)計(jì)異常優(yōu)雅簡(jiǎn)潔,擴(kuò)展性高,穩(wěn)定性強(qiáng)。擁有非常詳細(xì)完整的用戶文檔。

同時(shí)內(nèi)置了很多非常有用的模塊基本上做到了開箱即用,用戶只需要編寫短短幾行代碼,就可以快速構(gòu)建出一個(gè)具有高吞吐,低延時(shí),更少的資源消耗,高性能(非必要的內(nèi)存拷貝最小化)等特征的高并發(fā)網(wǎng)絡(luò)應(yīng)用程序。

本文我們來探討下支持Netty具有高吞吐,低延時(shí)特征的基石----netty的網(wǎng)絡(luò)IO模型。

由Netty的網(wǎng)絡(luò)IO模型開始,我們來正式揭開本系列Netty源碼解析的序幕:

網(wǎng)絡(luò)包接收流程

圖片
網(wǎng)絡(luò)包收發(fā)過程.png
  • 當(dāng)網(wǎng)絡(luò)數(shù)據(jù)幀通過網(wǎng)絡(luò)傳輸?shù)竭_(dá)網(wǎng)卡時(shí),網(wǎng)卡會(huì)將網(wǎng)絡(luò)數(shù)據(jù)幀通過DMA的方式放到環(huán)形緩沖區(qū)RingBuffer中。

RingBuffer是網(wǎng)卡在啟動(dòng)的時(shí)候分配和初始化環(huán)形緩沖隊(duì)列。當(dāng)RingBuffer滿的時(shí)候,新來的數(shù)據(jù)包就會(huì)被丟棄。我們可以通過ifconfig命令查看網(wǎng)卡收發(fā)數(shù)據(jù)包的情況。其中overruns數(shù)據(jù)項(xiàng)表示當(dāng)RingBuffer滿時(shí),被丟棄的數(shù)據(jù)包。如果發(fā)現(xiàn)出現(xiàn)丟包情況,可以通過ethtool命令來增大RingBuffer長(zhǎng)度。

  • 當(dāng)DMA操作完成時(shí),網(wǎng)卡會(huì)向CPU發(fā)起一個(gè)硬中斷,告訴CPU有網(wǎng)絡(luò)數(shù)據(jù)到達(dá)。CPU調(diào)用網(wǎng)卡驅(qū)動(dòng)注冊(cè)的硬中斷響應(yīng)程序。網(wǎng)卡硬中斷響應(yīng)程序會(huì)為網(wǎng)絡(luò)數(shù)據(jù)幀創(chuàng)建內(nèi)核數(shù)據(jù)結(jié)構(gòu)sk_buffer,并將網(wǎng)絡(luò)數(shù)據(jù)幀拷貝sk_buffer中。然后發(fā)起軟中斷請(qǐng)求,通知內(nèi)核有新的網(wǎng)絡(luò)數(shù)據(jù)幀到達(dá)。

sk_buff緩沖區(qū),是一個(gè)維護(hù)網(wǎng)絡(luò)幀結(jié)構(gòu)的雙向鏈表,鏈表中的每一個(gè)元素都是一個(gè)網(wǎng)絡(luò)幀。雖然 TCP/IP 協(xié)議棧分了好幾層,但上下不同層之間的傳遞,實(shí)際上只需要操作這個(gè)數(shù)據(jù)結(jié)構(gòu)中的指針,而無需進(jìn)行數(shù)據(jù)復(fù)制。

  • 內(nèi)核線程ksoftirqd發(fā)現(xiàn)有軟中斷請(qǐng)求到來,隨后調(diào)用網(wǎng)卡驅(qū)動(dòng)注冊(cè)的poll函數(shù),poll函數(shù)sk_buffer中的網(wǎng)絡(luò)數(shù)據(jù)包送到內(nèi)核協(xié)議棧中注冊(cè)的ip_rcv函數(shù)中。

每個(gè)CPU會(huì)綁定一個(gè)ksoftirqd內(nèi)核線程專門用來處理軟中斷響應(yīng)。2個(gè) CPU 時(shí),就會(huì)有?ksoftirqd/0?和?ksoftirqd/1這兩個(gè)內(nèi)核線程。

這里有個(gè)事情需要注意下:?網(wǎng)卡接收到數(shù)據(jù)后,當(dāng)DMA拷貝完成時(shí),向CPU發(fā)出硬中斷,這時(shí)哪個(gè)CPU上響應(yīng)了這個(gè)硬中斷,那么在網(wǎng)卡硬中斷響應(yīng)程序中發(fā)出的軟中斷請(qǐng)求也會(huì)在這個(gè)CPU綁定的ksoftirqd線程中響應(yīng)。所以如果發(fā)現(xiàn)Linux軟中斷,CPU消耗都集中在一個(gè)核上的話,那么就需要調(diào)整硬中斷的CPU親和性,來將硬中斷打散不通的CPU核上去。

  • ip_rcv函數(shù)中也就是上圖中的網(wǎng)絡(luò)層取出數(shù)據(jù)包的IP頭,判斷該數(shù)據(jù)包下一跳的走向,如果數(shù)據(jù)包是發(fā)送給本機(jī)的,則取出傳輸層的協(xié)議類型(TCP或者UDP),并去掉數(shù)據(jù)包的IP頭,將數(shù)據(jù)包交給上圖中得傳輸層處理。

傳輸層的處理函數(shù):TCP協(xié)議對(duì)應(yīng)內(nèi)核協(xié)議棧中注冊(cè)的tcp_rcv函數(shù),UDP協(xié)議對(duì)應(yīng)內(nèi)核協(xié)議棧中注冊(cè)的udp_rcv函數(shù)。

  • 當(dāng)我們采用的是TCP協(xié)議時(shí),數(shù)據(jù)包到達(dá)傳輸層時(shí),會(huì)在內(nèi)核協(xié)議棧中的tcp_rcv函數(shù)處理,在tcp_rcv函數(shù)中去掉TCP頭,根據(jù)四元組(源IP,源端口,目的IP,目的端口)查找對(duì)應(yīng)的Socket,如果找到對(duì)應(yīng)的Socket則將網(wǎng)絡(luò)數(shù)據(jù)包中的傳輸數(shù)據(jù)拷貝到Socket中的接收緩沖區(qū)中。如果沒有找到,則發(fā)送一個(gè)目標(biāo)不可達(dá)icmp包。

  • 內(nèi)核在接收網(wǎng)絡(luò)數(shù)據(jù)包時(shí)所做的工作我們就介紹完了,現(xiàn)在我們把視角放到應(yīng)用層,當(dāng)我們程序通過系統(tǒng)調(diào)用read讀取Socket接收緩沖區(qū)中的數(shù)據(jù)時(shí),如果接收緩沖區(qū)中沒有數(shù)據(jù),那么應(yīng)用程序就會(huì)在系統(tǒng)調(diào)用上阻塞,直到Socket接收緩沖區(qū)有數(shù)據(jù),然后CPU內(nèi)核空間(Socket接收緩沖區(qū))的數(shù)據(jù)拷貝用戶空間,最后系統(tǒng)調(diào)用read返回,應(yīng)用程序讀取數(shù)據(jù)。


【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??

性能開銷

從內(nèi)核處理網(wǎng)絡(luò)數(shù)據(jù)包接收的整個(gè)過程來看,內(nèi)核幫我們做了非常之多的工作,最終我們的應(yīng)用程序才能讀取到網(wǎng)絡(luò)數(shù)據(jù)。

隨著而來的也帶來了很多的性能開銷,結(jié)合前面介紹的網(wǎng)絡(luò)數(shù)據(jù)包接收過程我們來看下網(wǎng)絡(luò)數(shù)據(jù)包接收的過程中都有哪些性能開銷:

  • 應(yīng)用程序通過系統(tǒng)調(diào)用用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)的開銷以及系統(tǒng)調(diào)用返回時(shí)從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)的開銷。

  • 網(wǎng)絡(luò)數(shù)據(jù)從內(nèi)核空間通過CPU拷貝用戶空間的開銷。

  • 內(nèi)核線程ksoftirqd響應(yīng)軟中斷的開銷。

  • CPU響應(yīng)硬中斷的開銷。

  • DMA拷貝網(wǎng)絡(luò)數(shù)據(jù)包到內(nèi)存中的開銷。

網(wǎng)絡(luò)包發(fā)送流程

圖片
網(wǎng)絡(luò)包發(fā)送過程.png
  • 當(dāng)我們?cè)趹?yīng)用程序中調(diào)用send系統(tǒng)調(diào)用發(fā)送數(shù)據(jù)時(shí),由于是系統(tǒng)調(diào)用所以線程會(huì)發(fā)生一次用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換,在內(nèi)核中首先根據(jù)fd將真正的Socket找出,這個(gè)Socket對(duì)象中記錄著各種協(xié)議棧的函數(shù)地址,然后構(gòu)造struct msghdr對(duì)象,將用戶需要發(fā)送的數(shù)據(jù)全部封裝在這個(gè)struct msghdr結(jié)構(gòu)體中。

  • 調(diào)用內(nèi)核協(xié)議棧函數(shù)inet_sendmsg,發(fā)送流程進(jìn)入內(nèi)核協(xié)議棧處理。在進(jìn)入到內(nèi)核協(xié)議棧之后,內(nèi)核會(huì)找到Socket上的具體協(xié)議的發(fā)送函數(shù)。

比如:我們使用的是TCP協(xié)議,對(duì)應(yīng)的TCP協(xié)議發(fā)送函數(shù)是tcp_sendmsg,如果是UDP協(xié)議的話,對(duì)應(yīng)的發(fā)送函數(shù)為udp_sendmsg。

  • TCP協(xié)議的發(fā)送函數(shù)tcp_sendmsg中,創(chuàng)建內(nèi)核數(shù)據(jù)結(jié)構(gòu)sk_buffer,將struct msghdr結(jié)構(gòu)體中的發(fā)送數(shù)據(jù)拷貝sk_buffer中。調(diào)用tcp_write_queue_tail函數(shù)獲取Socket發(fā)送隊(duì)列中的隊(duì)尾元素,將新創(chuàng)建的sk_buffer添加到Socket發(fā)送隊(duì)列的尾部。

Socket的發(fā)送隊(duì)列是由sk_buffer組成的一個(gè)雙向鏈表。

發(fā)送流程走到這里,用戶要發(fā)送的數(shù)據(jù)總算是從用戶空間拷貝到了內(nèi)核中,這時(shí)雖然發(fā)送數(shù)據(jù)已經(jīng)拷貝到了內(nèi)核Socket中的發(fā)送隊(duì)列中,但并不代表內(nèi)核會(huì)開始發(fā)送,因?yàn)?code>TCP協(xié)議的流量控制擁塞控制,用戶要發(fā)送的數(shù)據(jù)包并不一定會(huì)立馬被發(fā)送出去,需要符合TCP協(xié)議的發(fā)送條件。如果沒有達(dá)到發(fā)送條件,那么本次send系統(tǒng)調(diào)用就會(huì)直接返回。

  • 如果符合發(fā)送條件,則開始調(diào)用tcp_write_xmit內(nèi)核函數(shù)。在這個(gè)函數(shù)中,會(huì)循環(huán)獲取Socket發(fā)送隊(duì)列中待發(fā)送的sk_buffer,然后進(jìn)行擁塞控制以及滑動(dòng)窗口的管理。

  • 將從Socket發(fā)送隊(duì)列中獲取到的sk_buffer重新拷貝一份,設(shè)置sk_buffer副本中的TCP HEADER。

sk_buffer?內(nèi)部其實(shí)包含了網(wǎng)絡(luò)協(xié)議中所有的?header。在設(shè)置?TCP HEADER的時(shí)候,只是把指針指向?sk_buffer的合適位置。后面再設(shè)置?IP HEADER的時(shí)候,在把指針移動(dòng)一下就行,避免頻繁的內(nèi)存申請(qǐng)和拷貝,效率很高。

圖片
sk_buffer.png

為什么不直接使用Socket發(fā)送隊(duì)列中的sk_buffer而是需要拷貝一份呢?因?yàn)?code>TCP協(xié)議是支持丟包重傳的,在沒有收到對(duì)端的ACK之前,這個(gè)sk_buffer是不能刪除的。內(nèi)核每次調(diào)用網(wǎng)卡發(fā)送數(shù)據(jù)的時(shí)候,實(shí)際上傳遞的是sk_buffer拷貝副本,當(dāng)網(wǎng)卡把數(shù)據(jù)發(fā)送出去后,sk_buffer拷貝副本會(huì)被釋放。當(dāng)收到對(duì)端的ACK之后,Socket發(fā)送隊(duì)列中的sk_buffer才會(huì)被真正刪除。

  • 當(dāng)設(shè)置完TCP頭后,內(nèi)核協(xié)議棧傳輸層的事情就做完了,下面通過調(diào)用ip_queue_xmit內(nèi)核函數(shù),正式來到內(nèi)核協(xié)議棧網(wǎng)絡(luò)層的處理。

    通過route命令可以查看本機(jī)路由配置。

    如果你使用?iptables配置了一些規(guī)則,那么這里將檢測(cè)是否命中規(guī)則。如果你設(shè)置了非常復(fù)雜的 netfilter 規(guī)則,在這個(gè)函數(shù)里將會(huì)導(dǎo)致你的線程?CPU 開銷會(huì)極大增加。

    • sk_buffer中的指針移動(dòng)到IP頭位置上,設(shè)置IP頭。

    • 執(zhí)行netfilters過濾。過濾通過之后,如果數(shù)據(jù)大于?MTU的話,則執(zhí)行分片。

    • 檢查Socket中是否有緩存路由表,如果沒有的話,則查找路由項(xiàng),并緩存到Socket中。接著在把路由表設(shè)置到sk_buffer中。

  • 內(nèi)核協(xié)議棧網(wǎng)絡(luò)層的事情處理完后,現(xiàn)在發(fā)送流程進(jìn)入了到了鄰居子系統(tǒng),鄰居子系統(tǒng)位于內(nèi)核協(xié)議棧中的網(wǎng)絡(luò)層網(wǎng)絡(luò)接口層之間,用于發(fā)送ARP請(qǐng)求獲取MAC地址,然后將sk_buffer中的指針移動(dòng)到MAC頭位置,填充MAC頭

  • 經(jīng)過鄰居子系統(tǒng)的處理,現(xiàn)在sk_buffer中已經(jīng)封裝了一個(gè)完整的數(shù)據(jù)幀,隨后內(nèi)核將sk_buffer交給網(wǎng)絡(luò)設(shè)備子系統(tǒng)進(jìn)行處理。網(wǎng)絡(luò)設(shè)備子系統(tǒng)主要做以下幾項(xiàng)事情:

    • 選擇發(fā)送隊(duì)列(RingBuffer)。因?yàn)榫W(wǎng)卡擁有多個(gè)發(fā)送隊(duì)列,所以在發(fā)送前需要選擇一個(gè)發(fā)送隊(duì)列。

    • sk_buffer添加到發(fā)送隊(duì)列中。

    • 循環(huán)從發(fā)送隊(duì)列(RingBuffer)中取出sk_buffer,調(diào)用內(nèi)核函數(shù)sch_direct_xmit發(fā)送數(shù)據(jù),其中會(huì)調(diào)用網(wǎng)卡驅(qū)動(dòng)程序來發(fā)送數(shù)據(jù)。

以上過程全部是用戶線程的內(nèi)核態(tài)在執(zhí)行,占用的CPU時(shí)間是系統(tǒng)態(tài)時(shí)間(sy),當(dāng)分配給用戶線程的CPU quota用完的時(shí)候,會(huì)觸發(fā)NET_TX_SOFTIRQ類型的軟中斷,內(nèi)核線程ksoftirqd會(huì)響應(yīng)這個(gè)軟中斷,并執(zhí)行NET_TX_SOFTIRQ類型的軟中斷注冊(cè)的回調(diào)函數(shù)net_tx_action,在回調(diào)函數(shù)中會(huì)執(zhí)行到驅(qū)動(dòng)程序函數(shù)?dev_hard_start_xmit來發(fā)送數(shù)據(jù)。

注意:當(dāng)觸發(fā)NET_TX_SOFTIRQ軟中斷來發(fā)送數(shù)據(jù)時(shí),后邊消耗的 CPU 就都顯示在?si這里了,不會(huì)消耗用戶進(jìn)程的系統(tǒng)態(tài)時(shí)間(sy)了。

從這里可以看到網(wǎng)絡(luò)包的發(fā)送過程和接受過程是不同的,在介紹網(wǎng)絡(luò)包的接受過程時(shí),我們提到是通過觸發(fā)NET_RX_SOFTIRQ類型的軟中斷在內(nèi)核線程ksoftirqd中執(zhí)行內(nèi)核網(wǎng)絡(luò)協(xié)議棧接受數(shù)據(jù)。而在網(wǎng)絡(luò)數(shù)據(jù)包的發(fā)送過程中是用戶線程的內(nèi)核態(tài)在執(zhí)行內(nèi)核網(wǎng)絡(luò)協(xié)議棧,只有當(dāng)線程的CPU quota用盡時(shí),才觸發(fā)NET_TX_SOFTIRQ軟中斷來發(fā)送數(shù)據(jù)。

在整個(gè)網(wǎng)絡(luò)包的發(fā)送和接受過程中,NET_TX_SOFTIRQ類型的軟中斷只會(huì)在發(fā)送網(wǎng)絡(luò)包時(shí)并且當(dāng)用戶線程的CPU quota用盡時(shí),才會(huì)觸發(fā)。剩下的接受過程中觸發(fā)的軟中斷類型以及發(fā)送完數(shù)據(jù)觸發(fā)的軟中斷類型均為NET_RX_SOFTIRQ。所以這就是你在服務(wù)器上查看?/proc/softirqs,一般?NET_RX都要比?NET_TX大很多的的原因。

  • 現(xiàn)在發(fā)送流程終于到了網(wǎng)卡真實(shí)發(fā)送數(shù)據(jù)的階段,前邊我們講到無論是用戶線程的內(nèi)核態(tài)還是觸發(fā)NET_TX_SOFTIRQ類型的軟中斷在發(fā)送數(shù)據(jù)的時(shí)候最終會(huì)調(diào)用到網(wǎng)卡的驅(qū)動(dòng)程序函數(shù)dev_hard_start_xmit來發(fā)送數(shù)據(jù)。在網(wǎng)卡驅(qū)動(dòng)程序函數(shù)dev_hard_start_xmit中會(huì)將sk_buffer映射到網(wǎng)卡可訪問的內(nèi)存 DMA 區(qū)域,最終網(wǎng)卡驅(qū)動(dòng)程序通過DMA的方式將數(shù)據(jù)幀通過物理網(wǎng)卡發(fā)送出去。

  • 當(dāng)數(shù)據(jù)發(fā)送完畢后,還有最后一項(xiàng)重要的工作,就是清理工作。數(shù)據(jù)發(fā)送完畢后,網(wǎng)卡設(shè)備會(huì)向CPU發(fā)送一個(gè)硬中斷,CPU調(diào)用網(wǎng)卡驅(qū)動(dòng)程序注冊(cè)的硬中斷響應(yīng)程序,在硬中斷響應(yīng)中觸發(fā)NET_RX_SOFTIRQ類型的軟中斷,在軟中斷的回調(diào)函數(shù)igb_poll中清理釋放?sk_buffer,清理網(wǎng)卡發(fā)送隊(duì)列(RingBuffer),解除 DMA 映射。

無論硬中斷是因?yàn)?code>有數(shù)據(jù)要接收,還是說發(fā)送完成通知,從硬中斷觸發(fā)的軟中斷都是?NET_RX_SOFTIRQ。

這里釋放清理的只是sk_buffer的副本,真正的sk_buffer現(xiàn)在還是存放在Socket的發(fā)送隊(duì)列中。前面在傳輸層處理的時(shí)候我們提到過,因?yàn)閭鬏攲有枰?code>保證可靠性,所以?sk_buffer其實(shí)還沒有刪除。它得等收到對(duì)方的 ACK 之后才會(huì)真正刪除。

性能開銷

前邊我們提到了在網(wǎng)絡(luò)包接收過程中涉及到的性能開銷,現(xiàn)在介紹完了網(wǎng)絡(luò)包的發(fā)送過程,我們來看下在數(shù)據(jù)包發(fā)送過程中的性能開銷:

  • 和接收數(shù)據(jù)一樣,應(yīng)用程序在調(diào)用系統(tǒng)調(diào)用send的時(shí)候會(huì)從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)以及發(fā)送完數(shù)據(jù)后,系統(tǒng)調(diào)用返回時(shí)從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)的開銷。

  • 用戶線程內(nèi)核態(tài)CPU quota用盡時(shí)觸發(fā)NET_TX_SOFTIRQ類型軟中斷,內(nèi)核響應(yīng)軟中斷的開銷。

  • 網(wǎng)卡發(fā)送完數(shù)據(jù),向CPU發(fā)送硬中斷,CPU響應(yīng)硬中斷的開銷。以及在硬中斷中發(fā)送NET_RX_SOFTIRQ軟中斷執(zhí)行具體的內(nèi)存清理動(dòng)作。內(nèi)核響應(yīng)軟中斷的開銷。

  • 內(nèi)存拷貝的開銷。我們來回顧下在數(shù)據(jù)包發(fā)送的過程中都發(fā)生了哪些內(nèi)存拷貝:

    • 在內(nèi)核協(xié)議棧的傳輸層中,TCP協(xié)議對(duì)應(yīng)的發(fā)送函數(shù)tcp_sendmsg會(huì)申請(qǐng)sk_buffer,將用戶要發(fā)送的數(shù)據(jù)拷貝sk_buffer中。

    • 在發(fā)送流程從傳輸層到網(wǎng)絡(luò)層的時(shí)候,會(huì)拷貝一個(gè)sk_buffer副本出來,將這個(gè)sk_buffer副本向下傳遞。原始sk_buffer保留在Socket發(fā)送隊(duì)列中,等待網(wǎng)絡(luò)對(duì)端ACK,對(duì)端ACK后刪除Socket發(fā)送隊(duì)列中的sk_buffer。對(duì)端沒有發(fā)送ACK,則重新從Socket發(fā)送隊(duì)列中發(fā)送,實(shí)現(xiàn)TCP協(xié)議的可靠傳輸。

    • 在網(wǎng)絡(luò)層,如果發(fā)現(xiàn)要發(fā)送的數(shù)據(jù)大于MTU,則會(huì)進(jìn)行分片操作,申請(qǐng)額外的sk_buffer,并將原來的sk_buffer拷貝到多個(gè)小的sk_buffer中。

再談(阻塞,非阻塞)與(同步,異步)

在我們聊完網(wǎng)絡(luò)數(shù)據(jù)的接收和發(fā)送過程后,我們來談下IO中特別容易混淆的概念:阻塞與同步,非阻塞與異步。

網(wǎng)上各種博文還有各種書籍中有大量的關(guān)于這兩個(gè)概念的解釋,但是筆者覺得還是不夠形象化,只是對(duì)概念的生硬解釋,如果硬套概念的話,其實(shí)感覺阻塞與同步,非阻塞與異步還是沒啥區(qū)別,時(shí)間長(zhǎng)了,還是比較模糊容易混淆。

所以筆者在這里嘗試換一種更加形象化,更加容易理解記憶的方式來清晰地解釋下什么是阻塞與非阻塞,什么是同步與異步。

經(jīng)過前邊對(duì)網(wǎng)絡(luò)數(shù)據(jù)包接收流程的介紹,在這里我們可以將整個(gè)流程總結(jié)為兩個(gè)階段:

圖片
數(shù)據(jù)接收階段.png
  • 數(shù)據(jù)準(zhǔn)備階段:?在這個(gè)階段,網(wǎng)絡(luò)數(shù)據(jù)包到達(dá)網(wǎng)卡,通過DMA的方式將數(shù)據(jù)包拷貝到內(nèi)存中,然后經(jīng)過硬中斷,軟中斷,接著通過內(nèi)核線程ksoftirqd經(jīng)過內(nèi)核協(xié)議棧的處理,最終將數(shù)據(jù)發(fā)送到內(nèi)核Socket的接收緩沖區(qū)中。

  • 數(shù)據(jù)拷貝階段:?當(dāng)數(shù)據(jù)到達(dá)內(nèi)核Socket的接收緩沖區(qū)中時(shí),此時(shí)數(shù)據(jù)存在于內(nèi)核空間中,需要將數(shù)據(jù)拷貝用戶空間中,才能夠被應(yīng)用程序讀取。

阻塞與非阻塞

阻塞與非阻塞的區(qū)別主要發(fā)生在第一階段:數(shù)據(jù)準(zhǔn)備階段

當(dāng)應(yīng)用程序發(fā)起系統(tǒng)調(diào)用read時(shí),線程從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài),讀取內(nèi)核Socket的接收緩沖區(qū)中的網(wǎng)絡(luò)數(shù)據(jù)。

阻塞

如果這時(shí)內(nèi)核Socket的接收緩沖區(qū)沒有數(shù)據(jù),那么線程就會(huì)一直等待,直到Socket接收緩沖區(qū)有數(shù)據(jù)為止。隨后將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,系統(tǒng)調(diào)用read返回。

圖片
阻塞IO.png

從圖中我們可以看出:阻塞的特點(diǎn)是在第一階段和第二階段都會(huì)等待。

非阻塞

阻塞非阻塞主要的區(qū)分是在第一階段:數(shù)據(jù)準(zhǔn)備階段

  • 在第一階段,當(dāng)Socket的接收緩沖區(qū)中沒有數(shù)據(jù)的時(shí)候,阻塞模式下應(yīng)用線程會(huì)一直等待。非阻塞模式下應(yīng)用線程不會(huì)等待,系統(tǒng)調(diào)用直接返回錯(cuò)誤標(biāo)志EWOULDBLOCK。

  • 當(dāng)Socket的接收緩沖區(qū)中有數(shù)據(jù)的時(shí)候,阻塞非阻塞的表現(xiàn)是一樣的,都會(huì)進(jìn)入第二階段等待數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,然后系統(tǒng)調(diào)用返回。

圖片
非阻塞IO.png

從上圖中,我們可以看出:非阻塞的特點(diǎn)是第一階段不會(huì)等待,但是在第二階段還是會(huì)等待。

同步與異步

同步異步主要的區(qū)別發(fā)生在第二階段:數(shù)據(jù)拷貝階段。

前邊我們提到在數(shù)據(jù)拷貝階段主要是將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間。然后應(yīng)用程序才可以讀取數(shù)據(jù)。

當(dāng)內(nèi)核Socket的接收緩沖區(qū)有數(shù)據(jù)到達(dá)時(shí),進(jìn)入第二階段。

同步

同步模式在數(shù)據(jù)準(zhǔn)備好后,是由用戶線程內(nèi)核態(tài)來執(zhí)行第二階段。所以應(yīng)用程序會(huì)在第二階段發(fā)生阻塞,直到數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,系統(tǒng)調(diào)用才會(huì)返回。

Linux下的?epoll和Mac 下的?kqueue都屬于同步 IO。

圖片
同步IO.png

異步

異步模式下是由內(nèi)核來執(zhí)行第二階段的數(shù)據(jù)拷貝操作,當(dāng)內(nèi)核執(zhí)行完第二階段,會(huì)通知用戶線程IO操作已經(jīng)完成,并將數(shù)據(jù)回調(diào)給用戶線程。所以在異步模式下?數(shù)據(jù)準(zhǔn)備階段數(shù)據(jù)拷貝階段均是由內(nèi)核來完成,不會(huì)對(duì)應(yīng)用程序造成任何阻塞。

基于以上特征,我們可以看到異步模式需要內(nèi)核的支持,比較依賴操作系統(tǒng)底層的支持。

在目前流行的操作系統(tǒng)中,只有Windows 中的?IOCP才真正屬于異步 IO,實(shí)現(xiàn)的也非常成熟。但Windows很少用來作為服務(wù)器使用。

而常用來作為服務(wù)器使用的Linux,異步IO機(jī)制實(shí)現(xiàn)的不夠成熟,與NIO相比性能提升的也不夠明顯。

但Linux kernel 在5.1版本由Facebook的大神Jens Axboe引入了新的異步IO庫(kù)io_uring?改善了原來Linux native AIO的一些性能問題。性能相比Epoll以及之前原生的AIO提高了不少,值得關(guān)注。

圖片
異步IO.png

IO模型

在進(jìn)行網(wǎng)絡(luò)IO操作時(shí),用什么樣的IO模型來讀寫數(shù)據(jù)將在很大程度上決定了網(wǎng)絡(luò)框架的IO性能。所以IO模型的選擇是構(gòu)建一個(gè)高性能網(wǎng)絡(luò)框架的基礎(chǔ)。

在《UNIX 網(wǎng)絡(luò)編程》一書中介紹了五種IO模型:阻塞IO,非阻塞IO,IO多路復(fù)用,信號(hào)驅(qū)動(dòng)IO,異步IO,每一種IO模型的出現(xiàn)都是對(duì)前一種的升級(jí)優(yōu)化。

下面我們就來分別介紹下這五種IO模型各自都解決了什么問題,適用于哪些場(chǎng)景,各自的優(yōu)缺點(diǎn)是什么?

阻塞IO(BIO)

圖片
阻塞IO.png

經(jīng)過前一小節(jié)對(duì)阻塞這個(gè)概念的介紹,相信大家可以很容易理解阻塞IO的概念和過程。

既然這小節(jié)我們談的是IO,那么下邊我們來看下在阻塞IO模型下,網(wǎng)絡(luò)數(shù)據(jù)的讀寫過程。

阻塞讀

當(dāng)用戶線程發(fā)起read系統(tǒng)調(diào)用,用戶線程從用戶態(tài)切換到內(nèi)核態(tài),在內(nèi)核中去查看Socket接收緩沖區(qū)是否有數(shù)據(jù)到來。

  • Socket接收緩沖區(qū)中有數(shù)據(jù),則用戶線程在內(nèi)核態(tài)將內(nèi)核空間中的數(shù)據(jù)拷貝到用戶空間,系統(tǒng)IO調(diào)用返回。

  • Socket接收緩沖區(qū)中無數(shù)據(jù),則用戶線程讓出CPU,進(jìn)入阻塞狀態(tài)。當(dāng)數(shù)據(jù)到達(dá)Socket接收緩沖區(qū)后,內(nèi)核喚醒阻塞狀態(tài)中的用戶線程進(jìn)入就緒狀態(tài),隨后經(jīng)過CPU的調(diào)度獲取到CPU quota進(jìn)入運(yùn)行狀態(tài),將內(nèi)核空間的數(shù)據(jù)拷貝到用戶空間,隨后系統(tǒng)調(diào)用返回。

阻塞寫

當(dāng)用戶線程發(fā)起send系統(tǒng)調(diào)用時(shí),用戶線程從用戶態(tài)切換到內(nèi)核態(tài),將發(fā)送數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間中的Socket發(fā)送緩沖區(qū)中。

  • 當(dāng)Socket發(fā)送緩沖區(qū)能夠容納下發(fā)送數(shù)據(jù)時(shí),用戶線程會(huì)將全部的發(fā)送數(shù)據(jù)寫入Socket緩沖區(qū),然后執(zhí)行在《網(wǎng)絡(luò)包發(fā)送流程》這小節(jié)介紹的后續(xù)流程,然后返回。

  • 當(dāng)Socket發(fā)送緩沖區(qū)空間不夠,無法容納下全部發(fā)送數(shù)據(jù)時(shí),用戶線程讓出CPU,進(jìn)入阻塞狀態(tài),直到Socket發(fā)送緩沖區(qū)能夠容納下全部發(fā)送數(shù)據(jù)時(shí),內(nèi)核喚醒用戶線程,執(zhí)行后續(xù)發(fā)送流程。

阻塞IO模型下的寫操作做事風(fēng)格比較硬剛,非得要把全部的發(fā)送數(shù)據(jù)寫入發(fā)送緩沖區(qū)才肯善罷甘休。

阻塞IO模型

圖片
阻塞IO模型.png

由于阻塞IO的讀寫特點(diǎn),所以導(dǎo)致在阻塞IO模型下,每個(gè)請(qǐng)求都需要被一個(gè)獨(dú)立的線程處理。一個(gè)線程在同一時(shí)刻只能與一個(gè)連接綁定。來一個(gè)請(qǐng)求,服務(wù)端就需要?jiǎng)?chuàng)建一個(gè)線程用來處理請(qǐng)求。

當(dāng)客戶端請(qǐng)求的并發(fā)量突然增大時(shí),服務(wù)端在一瞬間就會(huì)創(chuàng)建出大量的線程,而創(chuàng)建線程是需要系統(tǒng)資源開銷的,這樣一來就會(huì)一瞬間占用大量的系統(tǒng)資源。

如果客戶端創(chuàng)建好連接后,但是一直不發(fā)數(shù)據(jù),通常大部分情況下,網(wǎng)絡(luò)連接也并不總是有數(shù)據(jù)可讀,那么在空閑的這段時(shí)間內(nèi),服務(wù)端線程就會(huì)一直處于阻塞狀態(tài),無法干其他的事情。CPU也無法得到充分的發(fā)揮,同時(shí)還會(huì)導(dǎo)致大量線程切換的開銷。

適用場(chǎng)景

基于以上阻塞IO模型的特點(diǎn),該模型只適用于連接數(shù)少,并發(fā)度低的業(yè)務(wù)場(chǎng)景。

比如公司內(nèi)部的一些管理系統(tǒng),通常請(qǐng)求數(shù)在100個(gè)左右,使用阻塞IO模型還是非常適合的。而且性能還不輸NIO。

該模型在C10K之前,是普遍被采用的一種IO模型。

非阻塞IO(NIO)

阻塞IO模型最大的問題就是一個(gè)線程只能處理一個(gè)連接,如果這個(gè)連接上沒有數(shù)據(jù)的話,那么這個(gè)線程就只能阻塞在系統(tǒng)IO調(diào)用上,不能干其他的事情。這對(duì)系統(tǒng)資源來說,是一種極大的浪費(fèi)。同時(shí)大量的線程上下文切換,也是一個(gè)巨大的系統(tǒng)開銷。

所以為了解決這個(gè)問題,我們就需要用盡可能少的線程去處理更多的連接。網(wǎng)絡(luò)IO模型的演變也是根據(jù)這個(gè)需求來一步一步演進(jìn)的。

基于這個(gè)需求,第一種解決方案非阻塞IO就出現(xiàn)了。我們?cè)谏弦恍」?jié)中介紹了非阻塞的概念,現(xiàn)在我們來看下網(wǎng)絡(luò)讀寫操作在非阻塞IO下的特點(diǎn):

圖片
非阻塞IO.png

非阻塞讀

當(dāng)用戶線程發(fā)起非阻塞read系統(tǒng)調(diào)用時(shí),用戶線程從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài),在內(nèi)核中去查看Socket接收緩沖區(qū)是否有數(shù)據(jù)到來。

  • Socket接收緩沖區(qū)中無數(shù)據(jù),系統(tǒng)調(diào)用立馬返回,并帶有一個(gè)?EWOULDBLOCK?或?EAGAIN錯(cuò)誤,這個(gè)階段用戶線程不會(huì)阻塞,也不會(huì)讓出CPU,而是會(huì)繼續(xù)輪訓(xùn)直到Socket接收緩沖區(qū)中有數(shù)據(jù)為止。

  • Socket接收緩沖區(qū)中有數(shù)據(jù),用戶線程在內(nèi)核態(tài)會(huì)將內(nèi)核空間中的數(shù)據(jù)拷貝到用戶空間,注意這個(gè)數(shù)據(jù)拷貝階段,應(yīng)用程序是阻塞的,當(dāng)數(shù)據(jù)拷貝完成,系統(tǒng)調(diào)用返回。

非阻塞寫

前邊我們?cè)诮榻B阻塞寫的時(shí)候提到阻塞寫的風(fēng)格特別的硬朗,頭比較鐵非要把全部發(fā)送數(shù)據(jù)一次性都寫到Socket的發(fā)送緩沖區(qū)中才返回,如果發(fā)送緩沖區(qū)中沒有足夠的空間容納,那么就一直阻塞死等,特別的剛。

相比較而言非阻塞寫的特點(diǎn)就比較佛系,當(dāng)發(fā)送緩沖區(qū)中沒有足夠的空間容納全部發(fā)送數(shù)據(jù)時(shí),非阻塞寫的特點(diǎn)是能寫多少寫多少,寫不下了,就立即返回。并將寫入到發(fā)送緩沖區(qū)的字節(jié)數(shù)返回給應(yīng)用程序,方便用戶線程不斷的輪訓(xùn)嘗試將剩下的數(shù)據(jù)寫入發(fā)送緩沖區(qū)中。

非阻塞IO模型

圖片
非阻塞IO模型.png

基于以上非阻塞IO的特點(diǎn),我們就不必像阻塞IO那樣為每個(gè)請(qǐng)求分配一個(gè)線程去處理連接上的讀寫了。

我們可以利用一個(gè)線程或者很少的線程,去不斷地輪詢每個(gè)Socket的接收緩沖區(qū)是否有數(shù)據(jù)到達(dá),如果沒有數(shù)據(jù),不必阻塞線程,而是接著去輪詢下一個(gè)Socket接收緩沖區(qū),直到輪詢到數(shù)據(jù)后,處理連接上的讀寫,或者交給業(yè)務(wù)線程池去處理,輪詢線程則繼續(xù)輪詢其他的Socket接收緩沖區(qū)。

這樣一個(gè)非阻塞IO模型就實(shí)現(xiàn)了我們?cè)诒拘」?jié)開始提出的需求:我們需要用盡可能少的線程去處理更多的連接

適用場(chǎng)景

雖然非阻塞IO模型阻塞IO模型相比,減少了很大一部分的資源消耗和系統(tǒng)開銷。

但是它仍然有很大的性能問題,因?yàn)樵?code>非阻塞IO模型下,需要用戶線程去不斷地發(fā)起系統(tǒng)調(diào)用去輪訓(xùn)Socket接收緩沖區(qū),這就需要用戶線程不斷地從用戶態(tài)切換到內(nèi)核態(tài),內(nèi)核態(tài)切換到用戶態(tài)。隨著并發(fā)量的增大,這個(gè)上下文切換的開銷也是巨大的。

所以單純的非阻塞IO模型還是無法適用于高并發(fā)的場(chǎng)景。只能適用于C10K以下的場(chǎng)景。

IO多路復(fù)用

非阻塞IO這一小節(jié)的開頭,我們提到網(wǎng)絡(luò)IO模型的演變都是圍繞著---如何用盡可能少的線程去處理更多的連接這個(gè)核心需求開始展開的。

本小節(jié)我們來談?wù)?code>IO多路復(fù)用模型,那么什么是多路?,什么又是復(fù)用呢?

我們還是以這個(gè)核心需求來對(duì)這兩個(gè)概念展開闡述:

  • 多路:我們的核心需求是要用盡可能少的線程來處理盡可能多的連接,這里的多路指的就是我們需要處理的眾多連接。

  • 復(fù)用:核心需求要求我們使用盡可能少的線程,盡可能少的系統(tǒng)開銷去處理盡可能多的連接(多路),那么這里的復(fù)用指的就是用有限的資源,比如用一個(gè)線程或者固定數(shù)量的線程去處理眾多連接上的讀寫事件。換句話說,在阻塞IO模型中一個(gè)連接就需要分配一個(gè)獨(dú)立的線程去專門處理這個(gè)連接上的讀寫,到了IO多路復(fù)用模型中,多個(gè)連接可以復(fù)用這一個(gè)獨(dú)立的線程去處理這多個(gè)連接上的讀寫。

好了,IO多路復(fù)用模型的概念解釋清楚了,那么問題的關(guān)鍵是我們?nèi)绾稳?shí)現(xiàn)這個(gè)復(fù)用,也就是如何讓一個(gè)獨(dú)立的線程去處理眾多連接上的讀寫事件呢?

這個(gè)問題其實(shí)在非阻塞IO模型中已經(jīng)給出了它的答案,在非阻塞IO模型中,利用非阻塞的系統(tǒng)IO調(diào)用去不斷的輪詢眾多連接的Socket接收緩沖區(qū)看是否有數(shù)據(jù)到來,如果有則處理,如果沒有則繼續(xù)輪詢下一個(gè)Socket。這樣就達(dá)到了用一個(gè)線程去處理眾多連接上的讀寫事件了。

但是非阻塞IO模型最大的問題就是需要不斷的發(fā)起系統(tǒng)調(diào)用去輪詢各個(gè)Socket中的接收緩沖區(qū)是否有數(shù)據(jù)到來,頻繁系統(tǒng)調(diào)用隨之帶來了大量的上下文切換開銷。隨著并發(fā)量的提升,這樣也會(huì)導(dǎo)致非常嚴(yán)重的性能問題。

那么如何避免頻繁的系統(tǒng)調(diào)用同時(shí)又可以實(shí)現(xiàn)我們的核心需求呢?

這就需要操作系統(tǒng)的內(nèi)核來支持這樣的操作,我們可以把頻繁的輪詢操作交給操作系統(tǒng)內(nèi)核來替我們完成,這樣就避免了在用戶空間頻繁的去使用系統(tǒng)調(diào)用來輪詢所帶來的性能開銷。

正如我們所想,操作系統(tǒng)內(nèi)核也確實(shí)為我們提供了這樣的功能實(shí)現(xiàn),下面我們來一起看下操作系統(tǒng)對(duì)IO多路復(fù)用模型的實(shí)現(xiàn)。

select

select是操作系統(tǒng)內(nèi)核提供給我們使用的一個(gè)系統(tǒng)調(diào)用,它解決了在非阻塞IO模型中需要不斷的發(fā)起系統(tǒng)IO調(diào)用去輪詢各個(gè)連接上的Socket接收緩沖區(qū)所帶來的用戶空間內(nèi)核空間不斷切換的系統(tǒng)開銷。

select系統(tǒng)調(diào)用將輪詢的操作交給了內(nèi)核來幫助我們完成,從而避免了在用戶空間不斷的發(fā)起輪詢所帶來的的系統(tǒng)性能開銷。

圖片
select.png
  • 首先用戶線程在發(fā)起select系統(tǒng)調(diào)用的時(shí)候會(huì)阻塞select系統(tǒng)調(diào)用上。此時(shí),用戶線程從用戶態(tài)切換到了內(nèi)核態(tài)完成了一次上下文切換

  • 用戶線程將需要監(jiān)聽的Socket對(duì)應(yīng)的文件描述符fd數(shù)組通過select系統(tǒng)調(diào)用傳遞給內(nèi)核。此時(shí),用戶線程將用戶空間中的文件描述符fd數(shù)組拷貝內(nèi)核空間

這里的文件描述符數(shù)組其實(shí)是一個(gè)BitMap,BitMap下標(biāo)為文件描述符fd,下標(biāo)對(duì)應(yīng)的值為:1表示該fd上有讀寫事件,0表示該fd上沒有讀寫事件。

圖片
fd數(shù)組BitMap.png

文件描述符fd其實(shí)就是一個(gè)整數(shù)值,在Linux中一切皆文件,Socket也是一個(gè)文件。描述進(jìn)程所有信息的數(shù)據(jù)結(jié)構(gòu)task_struct中有一個(gè)屬性struct files_struct *files,它最終指向了一個(gè)數(shù)組,數(shù)組里存放了進(jìn)程打開的所有文件列表,文件信息封裝在struct file結(jié)構(gòu)體中,這個(gè)數(shù)組存放的類型就是struct file結(jié)構(gòu)體,數(shù)組的下標(biāo)則是我們常說的文件描述符fd

  • 當(dāng)用戶線程調(diào)用完select后開始進(jìn)入阻塞狀態(tài),內(nèi)核開始輪詢遍歷fd數(shù)組,查看fd對(duì)應(yīng)的Socket接收緩沖區(qū)中是否有數(shù)據(jù)到來。如果有數(shù)據(jù)到來,則將fd對(duì)應(yīng)BitMap的值設(shè)置為1。如果沒有數(shù)據(jù)到來,則保持值為0。

注意這里內(nèi)核會(huì)修改原始的fd數(shù)組??!

  • 內(nèi)核遍歷一遍fd數(shù)組后,如果發(fā)現(xiàn)有些fd上有IO數(shù)據(jù)到來,則將修改后的fd數(shù)組返回給用戶線程。此時(shí),會(huì)將fd數(shù)組從內(nèi)核空間拷貝到用戶空間。

  • 當(dāng)內(nèi)核將修改后的fd數(shù)組返回給用戶線程后,用戶線程解除阻塞,由用戶線程開始遍歷fd數(shù)組然后找出fd數(shù)組中值為1Socket文件描述符。最后對(duì)這些Socket發(fā)起系統(tǒng)調(diào)用讀取數(shù)據(jù)。

select不會(huì)告訴用戶線程具體哪些fd上有IO數(shù)據(jù)到來,只是在IO活躍fd上打上標(biāo)記,將打好標(biāo)記的完整fd數(shù)組返回給用戶線程,所以用戶線程還需要遍歷fd數(shù)組找出具體哪些fd上有IO數(shù)據(jù)到來。

  • 由于內(nèi)核在遍歷的過程中已經(jīng)修改了fd數(shù)組,所以在用戶線程遍歷完fd數(shù)組后獲取到IO就緒Socket后,就需要重置fd數(shù)組,并重新調(diào)用select傳入重置后的fd數(shù)組,讓內(nèi)核發(fā)起新的一輪遍歷輪詢。

API介紹

當(dāng)我們熟悉了select的原理后,就很容易理解內(nèi)核給我們提供的select API了。

select API中我們可以看到,select系統(tǒng)調(diào)用是在規(guī)定的超時(shí)時(shí)間內(nèi),監(jiān)聽(輪詢)用戶感興趣的文件描述符集合上的可讀,可寫,異常三類事件。

  • maxfdp1 :?select傳遞給內(nèi)核監(jiān)聽的文件描述符集合中數(shù)值最大的文件描述符+1,目的是用于限定內(nèi)核遍歷范圍。比如:select監(jiān)聽的文件描述符集合為{0,1,2,3,4},那么maxfdp1的值為5。

  • fd_set *readset:?對(duì)可讀事件感興趣的文件描述符集合。

  • fd_set *writeset:?對(duì)可寫事件感興趣的文件描述符集合。

  • fd_set *exceptset:對(duì)異常事件感興趣的文件描述符集合。

這里的fd_set就是我們前邊提到的文件描述符數(shù)組,是一個(gè)BitMap結(jié)構(gòu)。

  • const struct timeval *timeout:select系統(tǒng)調(diào)用超時(shí)時(shí)間,在這段時(shí)間內(nèi),內(nèi)核如果沒有發(fā)現(xiàn)有IO就緒的文件描述符,就直接返回。

上小節(jié)提到,在內(nèi)核遍歷完fd數(shù)組后,發(fā)現(xiàn)有IO就緒fd,則會(huì)將該fd對(duì)應(yīng)的BitMap中的值設(shè)置為1,并將修改后的fd數(shù)組,返回給用戶線程。

在用戶線程中需要重新遍歷fd數(shù)組,找出IO就緒fd出來,然后發(fā)起真正的讀寫調(diào)用。

下面介紹下在用戶線程中重新遍歷fd數(shù)組的過程中,我們需要用到的API

  • void FD_ZERO(fd_set *fdset):清空指定的文件描述符集合,即讓fd_set中不在包含任何文件描述符。

  • void FD_SET(int fd, fd_set *fdset):將一個(gè)給定的文件描述符加入集合之中。

每次調(diào)用select之前都要通過FD_ZEROFD_SET重新設(shè)置文件描述符,因?yàn)槲募枋龇蠒?huì)在內(nèi)核被修改。

  • int FD_ISSET(int fd, fd_set *fdset):檢查集合中指定的文件描述符是否可以讀寫。用戶線程遍歷文件描述符集合,調(diào)用該方法檢查相應(yīng)的文件描述符是否IO就緒。

  • void FD_CLR(int fd, fd_set *fdset):將一個(gè)給定的文件描述符從集合中刪除

性能開銷

雖然select解決了非阻塞IO模型中頻繁發(fā)起系統(tǒng)調(diào)用的問題,但是在整個(gè)select工作過程中,我們還是看出了select有些不足的地方。

  • 在發(fā)起select系統(tǒng)調(diào)用以及返回時(shí),用戶線程各發(fā)生了一次用戶態(tài)內(nèi)核態(tài)以及內(nèi)核態(tài)用戶態(tài)的上下文切換開銷。發(fā)生2次上下文切換

  • 在發(fā)起select系統(tǒng)調(diào)用以及返回時(shí),用戶線程在內(nèi)核態(tài)需要將文件描述符集合從用戶空間拷貝到內(nèi)核空間。以及在內(nèi)核修改完文件描述符集合后,又要將它從內(nèi)核空間拷貝到用戶空間。發(fā)生2次文件描述符集合的拷貝

  • 雖然由原來在用戶空間發(fā)起輪詢優(yōu)化成了內(nèi)核空間發(fā)起輪詢但select不會(huì)告訴用戶線程到底是哪些Socket上發(fā)生了IO就緒事件,只是對(duì)IO就緒Socket作了標(biāo)記,用戶線程依然要遍歷文件描述符集合去查找具體IO就緒Socket。時(shí)間復(fù)雜度依然為O(n)。

大部分情況下,網(wǎng)絡(luò)連接并不總是活躍的,如果select監(jiān)聽了大量的客戶端連接,只有少數(shù)的連接活躍,然而使用輪詢的這種方式會(huì)隨著連接數(shù)的增大,效率會(huì)越來越低。

  • 內(nèi)核會(huì)對(duì)原始的文件描述符集合進(jìn)行修改。導(dǎo)致每次在用戶空間重新發(fā)起select調(diào)用時(shí),都需要對(duì)文件描述符集合進(jìn)行重置。

  • BitMap結(jié)構(gòu)的文件描述符集合,長(zhǎng)度為固定的1024,所以只能監(jiān)聽0~1023的文件描述符。

  • select系統(tǒng)調(diào)用 不是線程安全的。

以上select的不足所產(chǎn)生的性能開銷都會(huì)隨著并發(fā)量的增大而線性增長(zhǎng)。

很明顯select也不能解決C10K問題,只適用于1000個(gè)左右的并發(fā)連接場(chǎng)景。

poll

poll相當(dāng)于是改進(jìn)版的select,但是工作原理基本和select沒有本質(zhì)的區(qū)別。

select中使用的文件描述符集合是采用的固定長(zhǎng)度為1024的BitMap結(jié)構(gòu)的fd_set,而poll換成了一個(gè)pollfd結(jié)構(gòu)沒有固定長(zhǎng)度的數(shù)組,這樣就沒有了最大描述符數(shù)量的限制(當(dāng)然還會(huì)受到系統(tǒng)文件描述符限制)

poll只是改進(jìn)了select只能監(jiān)聽1024個(gè)文件描述符的數(shù)量限制,但是并沒有在性能方面做出改進(jìn)。和select上本質(zhì)并沒有多大差別。

  • 同樣需要在內(nèi)核空間用戶空間中對(duì)文件描述符集合進(jìn)行輪詢,查找出IO就緒Socket的時(shí)間復(fù)雜度依然為O(n)

  • 同樣需要將包含大量文件描述符的集合整體在用戶空間內(nèi)核空間之間來回復(fù)制,無論這些文件描述符是否就緒。他們的開銷都會(huì)隨著文件描述符數(shù)量的增加而線性增大。

  • select,poll在每次新增,刪除需要監(jiān)聽的socket時(shí),都需要將整個(gè)新的socket集合全量傳至內(nèi)核。

poll同樣不適用高并發(fā)的場(chǎng)景。依然無法解決C10K問題。


文章篇幅有限,下文繼續(xù)講解

原文作者:bin的技術(shù)小屋

下文:深入講解Netty那些事兒之從內(nèi)核角度看IO模型(下)




深入講解Netty那些事兒之從內(nèi)核角度看IO模型(上)的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
桑植县| 龙山县| 闻喜县| 临城县| 泾阳县| 法库县| 平潭县| 南平市| 遂昌县| 承德市| 江都市| 晋州市| 台安县| 凤台县| 邯郸市| 河北区| 台南县| 江华| 道真| 乌鲁木齐县| 西宁市| 错那县| 罗平县| 云梦县| 中西区| 绍兴市| 易门县| 资源县| 靖州| 银川市| 镇坪县| 县级市| 平顶山市| 房产| 万源市| 蓬莱市| 谢通门县| 永城市| 连州市| 贵定县| 克山县|