0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

聊聊Netty那些事兒之從內(nèi)核角度看IO模型

小林coding ? 來(lái)源:bin的技術(shù)小屋 ? 2023-05-23 10:27 ? 次閱讀

從今天開(kāi)始我們來(lái)聊聊Netty的那些事兒,我們都知道Netty是一個(gè)高性能異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)框架。

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

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

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

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

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

6b8ca5f8-f886-11ed-90ce-dac502259ad0.png

網(wǎng)絡(luò)包收發(fā)過(guò)程.png

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

RingBuffer是網(wǎng)卡在啟動(dòng)的時(shí)候分配和初始化的環(huán)形緩沖隊(duì)列。當(dāng)RingBuffer滿的時(shí)候,新來(lái)的數(shù)據(jù)包就會(huì)被丟棄。我們可以通過(guò)ifconfig命令查看網(wǎng)卡收發(fā)數(shù)據(jù)包的情況。其中overruns數(shù)據(jù)項(xiàng)表示當(dāng)RingBuffer滿時(shí),被丟棄的數(shù)據(jù)包。如果發(fā)現(xiàn)出現(xiàn)丟包情況,可以通過(guò)ethtool命令來(lái)增大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)中的指針,而無(wú)需進(jìn)行數(shù)據(jù)復(fù)制。

內(nèi)核線程ksoftirqd發(fā)現(xiàn)有軟中斷請(qǐng)求到來(lái),隨后調(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)核線程專門(mén)用來(lá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親和性,來(lái)將硬中斷打散到不通的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ū)中。如果沒(méi)有找到,則發(fā)送一個(gè)目標(biāo)不可達(dá)的icmp包。

內(nèi)核在接收網(wǎng)絡(luò)數(shù)據(jù)包時(shí)所做的工作我們就介紹完了,現(xiàn)在我們把視角放到應(yīng)用層,當(dāng)我們程序通過(guò)系統(tǒng)調(diào)用read讀取Socket接收緩沖區(qū)中的數(shù)據(jù)時(shí),如果接收緩沖區(qū)中沒(méi)有數(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ù)。

性能開(kāi)銷

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

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

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

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

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

CPU響應(yīng)硬中斷的開(kāi)銷。

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

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

6ba41850-f886-11ed-90ce-dac502259ad0.png

網(wǎng)絡(luò)包發(fā)送過(guò)程.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ì)開(kāi)始發(fā)送,因?yàn)門(mén)CP協(xié)議的流量控制和擁塞控制,用戶要發(fā)送的數(shù)據(jù)包并不一定會(huì)立馬被發(fā)送出去,需要符合TCP協(xié)議的發(fā)送條件。如果沒(méi)有達(dá)到發(fā)送條件,那么本次send系統(tǒng)調(diào)用就會(huì)直接返回。

如果符合發(fā)送條件,則開(kāi)始調(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)和拷貝,效率很高。

6bc21094-f886-11ed-90ce-dac502259ad0.png

sk_buffer.png

為什么不直接使用Socket發(fā)送隊(duì)列中的sk_buffer而是需要拷貝一份呢?因?yàn)門(mén)CP協(xié)議是支持丟包重傳的,在沒(méi)有收到對(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é)議棧傳輸層的事情就做完了,下面通過(guò)調(diào)用ip_queue_xmit內(nèi)核函數(shù),正式來(lái)到內(nèi)核協(xié)議棧網(wǎng)絡(luò)層的處理。

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

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

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

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

檢查Socket中是否有緩存路由表,如果沒(méi)有的話,則查找路由項(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)過(guò)鄰居子系統(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)程序來(lái)發(fā)送數(shù)據(jù)。

以上過(guò)程全部是用戶線程的內(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來(lái)發(fā)送數(shù)據(jù)。

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

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

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

現(xiàn)在發(fā)送流程終于到了網(wǎng)卡真實(shí)發(fā)送數(shù)據(jù)的階段,前邊我們講到無(wú)論是用戶線程的內(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來(lái)發(fā)送數(shù)據(jù)。在網(wǎng)卡驅(qū)動(dòng)程序函數(shù)dev_hard_start_xmit中會(huì)將sk_buffer映射到網(wǎng)卡可訪問(wèn)的內(nèi)存 DMA 區(qū)域,最終網(wǎng)卡驅(qū)動(dòng)程序通過(guò)DMA的方式將數(shù)據(jù)幀通過(guò)物理網(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 映射。

無(wú)論硬中斷是因?yàn)橛袛?shù)據(jù)要接收,還是說(shuō)發(fā)送完成通知,從硬中斷觸發(fā)的軟中斷都是 NET_RX_SOFTIRQ。

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

性能開(kāi)銷

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

和接收數(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)的開(kāi)銷。

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

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

內(nèi)存拷貝的開(kāi)銷。我們來(lái)回顧下在數(shù)據(jù)包發(fā)送的過(guò)程中都發(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副本出來(lái),將這個(gè)sk_buffer副本向下傳遞。原始sk_buffer保留在Socket發(fā)送隊(duì)列中,等待網(wǎng)絡(luò)對(duì)端ACK,對(duì)端ACK后刪除Socket發(fā)送隊(duì)列中的sk_buffer。對(duì)端沒(méi)有發(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,并將原來(lái)的sk_buffer拷貝到多個(gè)小的sk_buffer中。

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

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

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

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

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

6bd2c362-f886-11ed-90ce-dac502259ad0.png

數(shù)據(jù)接收階段.png

數(shù)據(jù)準(zhǔn)備階段: 在這個(gè)階段,網(wǎng)絡(luò)數(shù)據(jù)包到達(dá)網(wǎng)卡,通過(guò)DMA的方式將數(shù)據(jù)包拷貝到內(nèi)存中,然后經(jīng)過(guò)硬中斷,軟中斷,接著通過(guò)內(nèi)核線程ksoftirqd經(jīng)過(guò)內(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ū)沒(méi)有數(shù)據(jù),那么線程就會(huì)一直等待,直到Socket接收緩沖區(qū)有數(shù)據(jù)為止。隨后將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,系統(tǒng)調(diào)用read返回。

6be561e8-f886-11ed-90ce-dac502259ad0.png

阻塞IO.png

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

非阻塞

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

在第一階段,當(dāng)Socket的接收緩沖區(qū)中沒(méi)有數(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)用返回。

6c0251c2-f886-11ed-90ce-dac502259ad0.png

非阻塞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)來(lái)執(zhí)行第二階段。所以應(yīng)用程序會(huì)在第二階段發(fā)生阻塞,直到數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,系統(tǒng)調(diào)用才會(huì)返回。

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

6c194238-f886-11ed-90ce-dac502259ad0.png

同步IO.png

異步

異步模式下是由內(nèi)核來(lá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)核來(lái)完成,不會(huì)對(duì)應(yīng)用程序造成任何阻塞。

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

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

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

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

6c2d05c0-f886-11ed-90ce-dac502259ad0.png

異步IO.png

IO模型

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

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

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

阻塞IO(BIO)

6be561e8-f886-11ed-90ce-dac502259ad0.png

阻塞IO.png

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

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

阻塞讀

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

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

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

阻塞寫(xiě)

當(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ù)寫(xiě)入Socket緩沖區(qū),然后執(zhí)行在《網(wǎng)絡(luò)包發(fā)送流程》這小節(jié)介紹的后續(xù)流程,然后返回。

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

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

阻塞IO模型

6c5948e2-f886-11ed-90ce-dac502259ad0.png

阻塞IO模型.png

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

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

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

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

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

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

6c0251c2-f886-11ed-90ce-dac502259ad0.png

非阻塞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ù)到來(lái)。

Socket接收緩沖區(qū)中無(wú)數(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)用返回。

非阻塞寫(xiě)

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

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

非阻塞IO模型

6c8c0cb4-f886-11ed-90ce-dac502259ad0.png

非阻塞IO模型.png

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

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

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

適用場(chǎng)景

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

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

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

IO多路復(fù)用

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

本小節(jié)我們來(lái)談?wù)処O多路復(fù)用模型,那么什么是多路?,什么又是復(fù)用呢?

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

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

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

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

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

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

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

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

正如我們所想,操作系統(tǒng)內(nèi)核也確實(shí)為我們提供了這樣的功能實(shí)現(xiàn),下面我們來(lái)一起看下操作系統(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ū)所帶來(lái)的用戶空間與內(nèi)核空間不斷切換的系統(tǒng)開(kāi)銷。

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

6c9f5918-f886-11ed-90ce-dac502259ad0.png

select.png

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

用戶線程將需要監(jiān)聽(tīng)的Socket對(duì)應(yīng)的文件描述符fd數(shù)組通過(guò)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上有讀寫(xiě)事件,0表示該fd上沒(méi)有讀寫(xiě)事件。

6cb1cde6-f886-11ed-90ce-dac502259ad0.png

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)程打開(kāi)的所有文件列表,文件信息封裝在struct file結(jié)構(gòu)體中,這個(gè)數(shù)組存放的類型就是struct file結(jié)構(gòu)體,數(shù)組的下標(biāo)則是我們常說(shuō)的文件描述符fd。

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

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

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

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

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

由于內(nèi)核在遍歷的過(guò)程中已經(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了。

intselect(intmaxfdp1,fd_set*readset,fd_set*writeset,fd_set*exceptset,conststructtimeval*timeout)

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

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

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

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

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)核如果沒(mé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出來(lái),然后發(fā)起真正的讀寫(xiě)調(diào)用。

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

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

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

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

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

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

性能開(kāi)銷

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

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

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

雖然由原來(lái)在用戶空間發(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)聽(tīng)了大量的客戶端連接,只有少數(shù)的連接活躍,然而使用輪詢的這種方式會(huì)隨著連接數(shù)的增大,效率會(huì)越來(lái)越低。

內(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)聽(tīng)0~1023的文件描述符。

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

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

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

poll

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

intpoll(structpollfd*fds,unsignedintnfds,inttimeout)
structpollfd{
intfd;/*文件描述符*/
shortevents;/*需要監(jiān)聽(tīng)的事件*/
shortrevents;/*實(shí)際發(fā)生的事件由內(nèi)核修改設(shè)置*/
};

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

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

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

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

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

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

epoll

通過(guò)上邊對(duì)select,poll核心原理的介紹,我們看到select,poll的性能瓶頸主要體現(xiàn)在下面三個(gè)地方:

因?yàn)閮?nèi)核不會(huì)保存我們要監(jiān)聽(tīng)的socket集合,所以在每次調(diào)用select,poll的時(shí)候都需要傳入,傳出全量的socket文件描述符集合。這導(dǎo)致了大量的文件描述符在用戶空間和內(nèi)核空間頻繁的來(lái)回復(fù)制。

由于內(nèi)核不會(huì)通知具體IO就緒的socket,只是在這些IO就緒的socket上打好標(biāo)記,所以當(dāng)select系統(tǒng)調(diào)用返回時(shí),在用戶空間還是需要完整遍歷一遍socket文件描述符集合來(lái)獲取具體IO就緒的socket。

在內(nèi)核空間中也是通過(guò)遍歷的方式來(lái)得到IO就緒的socket。

下面我們來(lái)看下epoll是如何解決這些問(wèn)題的。在介紹epoll的核心原理之前,我們需要介紹下理解epoll工作過(guò)程所需要的一些核心基礎(chǔ)知識(shí)。

Socket的創(chuàng)建

服務(wù)端線程調(diào)用accept系統(tǒng)調(diào)用后開(kāi)始阻塞,當(dāng)有客戶端連接上來(lái)并完成TCP三次握手后,內(nèi)核會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的Socket作為服務(wù)端與客戶端通信的內(nèi)核接口。

在Linux內(nèi)核的角度看來(lái),一切皆是文件,Socket也不例外,當(dāng)內(nèi)核創(chuàng)建出Socket之后,會(huì)將這個(gè)Socket放到當(dāng)前進(jìn)程所打開(kāi)的文件列表中管理起來(lái)。

下面我們來(lái)看下進(jìn)程管理這些打開(kāi)的文件列表相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)是什么樣的?在了解完這些數(shù)據(jù)結(jié)構(gòu)后,我們會(huì)更加清晰的理解Socket在內(nèi)核中所發(fā)揮的作用。并且對(duì)后面我們理解epoll的創(chuàng)建過(guò)程有很大的幫助。

進(jìn)程中管理文件列表結(jié)構(gòu)

6cc2b426-f886-11ed-90ce-dac502259ad0.png

進(jìn)程中管理文件列表結(jié)構(gòu).png

struct tast_struct是內(nèi)核中用來(lái)表示進(jìn)程的一個(gè)數(shù)據(jù)結(jié)構(gòu),它包含了進(jìn)程的所有信息。本小節(jié)我們只列出和文件管理相關(guān)的屬性。

其中進(jìn)程內(nèi)打開(kāi)的所有文件是通過(guò)一個(gè)數(shù)組fd_array來(lái)進(jìn)行組織管理,數(shù)組的下標(biāo)即為我們常提到的文件描述符,數(shù)組中存放的是對(duì)應(yīng)的文件數(shù)據(jù)結(jié)構(gòu)struct file。每打開(kāi)一個(gè)文件,內(nèi)核都會(huì)創(chuàng)建一個(gè)struct file與之對(duì)應(yīng),并在fd_array中找到一個(gè)空閑位置分配給它,數(shù)組中對(duì)應(yīng)的下標(biāo),就是我們?cè)谟脩艨臻g用到的文件描述符。

對(duì)于任何一個(gè)進(jìn)程,默認(rèn)情況下,文件描述符 0表示 stdin 標(biāo)準(zhǔn)輸入,文件描述符 1表示stdout 標(biāo)準(zhǔn)輸出,文件描述符2表示stderr 標(biāo)準(zhǔn)錯(cuò)誤輸出。

進(jìn)程中打開(kāi)的文件列表fd_array定義在內(nèi)核數(shù)據(jù)結(jié)構(gòu)struct files_struct中,在struct fdtable結(jié)構(gòu)中有一個(gè)指針struct fd **fd指向fd_array。

由于本小節(jié)討論的是內(nèi)核網(wǎng)絡(luò)系統(tǒng)部分的數(shù)據(jù)結(jié)構(gòu),所以這里拿Socket文件類型來(lái)舉例說(shuō)明:

用于封裝文件元信息的內(nèi)核數(shù)據(jù)結(jié)構(gòu)struct file中的private_data指針指向具體的Socket結(jié)構(gòu)。

struct file中的file_operations屬性定義了文件的操作函數(shù),不同的文件類型,對(duì)應(yīng)的file_operations是不同的,針對(duì)Socket文件類型,這里的file_operations指向socket_file_ops。

我們?cè)谟脩艨臻g對(duì)Socket發(fā)起的讀寫(xiě)等系統(tǒng)調(diào)用,進(jìn)入內(nèi)核首先會(huì)調(diào)用的是Socket對(duì)應(yīng)的struct file中指向的socket_file_ops。比如:對(duì)Socket發(fā)起write寫(xiě)操作,在內(nèi)核中首先被調(diào)用的就是socket_file_ops中定義的sock_write_iter。Socket發(fā)起read讀操作內(nèi)核中對(duì)應(yīng)的則是sock_read_iter。

staticconststructfile_operationssocket_file_ops={
.owner=THIS_MODULE,
.llseek=no_llseek,
.read_iter=sock_read_iter,
.write_iter=sock_write_iter,
.poll=sock_poll,
.unlocked_ioctl=sock_ioctl,
.mmap=sock_mmap,
.release=sock_close,
.fasync=sock_fasync,
.sendpage=sock_sendpage,
.splice_write=generic_splice_sendpage,
.splice_read=sock_splice_read,
};

Socket內(nèi)核結(jié)構(gòu)

6ce07646-f886-11ed-90ce-dac502259ad0.png

Socket內(nèi)核結(jié)構(gòu).png

在我們進(jìn)行網(wǎng)絡(luò)程序的編寫(xiě)時(shí)會(huì)首先創(chuàng)建一個(gè)Socket,然后基于這個(gè)Socket進(jìn)行bind,listen,我們先將這個(gè)Socket稱作為監(jiān)聽(tīng)Socket。

當(dāng)我們調(diào)用accept后,內(nèi)核會(huì)基于監(jiān)聽(tīng)Socket創(chuàng)建出來(lái)一個(gè)新的Socket專門(mén)用于與客戶端之間的網(wǎng)絡(luò)通信。并將監(jiān)聽(tīng)Socket中的Socket操作函數(shù)集合(inet_stream_ops)ops賦值到新的Socket的ops屬性中。

conststructproto_opsinet_stream_ops={
.bind=inet_bind,
.connect=inet_stream_connect,
.accept=inet_accept,
.poll=tcp_poll,
.listen=inet_listen,
.sendmsg=inet_sendmsg,
.recvmsg=inet_recvmsg,
......
}

這里需要注意的是,監(jiān)聽(tīng)的 socket和真正用來(lái)網(wǎng)絡(luò)通信的 Socket,是兩個(gè) Socket,一個(gè)叫作監(jiān)聽(tīng) Socket,一個(gè)叫作已連接的Socket。

接著內(nèi)核會(huì)為已連接的Socket創(chuàng)建struct file并初始化,并把Socket文件操作函數(shù)集合(socket_file_ops)賦值給struct file中的f_ops指針。然后將struct socket中的file指針指向這個(gè)新分配申請(qǐng)的struct file結(jié)構(gòu)體。

內(nèi)核會(huì)維護(hù)兩個(gè)隊(duì)列:

一個(gè)是已經(jīng)完成TCP三次握手,連接狀態(tài)處于established的連接隊(duì)列。內(nèi)核中為icsk_accept_queue。

一個(gè)是還沒(méi)有完成TCP三次握手,連接狀態(tài)處于syn_rcvd的半連接隊(duì)列。

然后調(diào)用socket->ops->accept,從Socket內(nèi)核結(jié)構(gòu)圖中我們可以看到其實(shí)調(diào)用的是inet_accept,該函數(shù)會(huì)在icsk_accept_queue中查找是否有已經(jīng)建立好的連接,如果有的話,直接從icsk_accept_queue中獲取已經(jīng)創(chuàng)建好的struct sock。并將這個(gè)struct sock對(duì)象賦值給struct socket中的sock指針。

struct sock在struct socket中是一個(gè)非常核心的內(nèi)核對(duì)象,正是在這里定義了我們?cè)诮榻B網(wǎng)絡(luò)包的接收發(fā)送流程中提到的接收隊(duì)列,發(fā)送隊(duì)列,等待隊(duì)列,數(shù)據(jù)就緒回調(diào)函數(shù)指針,內(nèi)核協(xié)議棧操作函數(shù)集合

根據(jù)創(chuàng)建Socket時(shí)發(fā)起的系統(tǒng)調(diào)用sock_create中的protocol參數(shù)(對(duì)于TCP協(xié)議這里的參數(shù)值為SOCK_STREAM)查找到對(duì)于 tcp 定義的操作方法實(shí)現(xiàn)集合 inet_stream_ops 和tcp_prot。并把它們分別設(shè)置到socket->ops和sock->sk_prot上。

這里可以回看下本小節(jié)開(kāi)頭的《Socket內(nèi)核結(jié)構(gòu)圖》捋一下他們之間的關(guān)系。

socket相關(guān)的操作接口定義在inet_stream_ops函數(shù)集合中,負(fù)責(zé)對(duì)上給用戶提供接口。而socket與內(nèi)核協(xié)議棧之間的操作接口定義在struct sock中的sk_prot指針上,這里指向tcp_prot協(xié)議操作函數(shù)集合。

structprototcp_prot={
.name="TCP",
.owner=THIS_MODULE,
.close=tcp_close,
.connect=tcp_v4_connect,
.disconnect=tcp_disconnect,
.accept=inet_csk_accept,
.keepalive=tcp_set_keepalive,
.recvmsg=tcp_recvmsg,
.sendmsg=tcp_sendmsg,
.backlog_rcv=tcp_v4_do_rcv,
......
}

之前提到的對(duì)Socket發(fā)起的系統(tǒng)IO調(diào)用,在內(nèi)核中首先會(huì)調(diào)用Socket的文件結(jié)構(gòu)struct file中的file_operations文件操作集合,然后調(diào)用struct socket中的ops指向的inet_stream_opssocket操作函數(shù),最終調(diào)用到struct sock中sk_prot指針指向的tcp_prot內(nèi)核協(xié)議棧操作函數(shù)接口集合。

6cf786e2-f886-11ed-90ce-dac502259ad0.png

系統(tǒng)IO調(diào)用結(jié)構(gòu).png

將struct sock 對(duì)象中的sk_data_ready 函數(shù)指針設(shè)置為 sock_def_readable,在Socket數(shù)據(jù)就緒的時(shí)候內(nèi)核會(huì)回調(diào)該函數(shù)。

struct sock中的等待隊(duì)列中存放的是系統(tǒng)IO調(diào)用發(fā)生阻塞的進(jìn)程fd,以及相應(yīng)的回調(diào)函數(shù)。記住這個(gè)地方,后邊介紹epoll的時(shí)候我們還會(huì)提到!

當(dāng)struct file,struct socket,struct sock這些核心的內(nèi)核對(duì)象創(chuàng)建好之后,最后就是把socket對(duì)象對(duì)應(yīng)的struct file放到進(jìn)程打開(kāi)的文件列表fd_array中。隨后系統(tǒng)調(diào)用accept返回socket的文件描述符fd給用戶程序。

阻塞IO中用戶進(jìn)程阻塞以及喚醒原理

在前邊小節(jié)我們介紹阻塞IO的時(shí)候提到,當(dāng)用戶進(jìn)程發(fā)起系統(tǒng)IO調(diào)用時(shí),這里我們拿read舉例,用戶進(jìn)程會(huì)在內(nèi)核態(tài)查看對(duì)應(yīng)Socket接收緩沖區(qū)是否有數(shù)據(jù)到來(lái)。

Socket接收緩沖區(qū)有數(shù)據(jù),則拷貝數(shù)據(jù)到用戶空間,系統(tǒng)調(diào)用返回。

Socket接收緩沖區(qū)沒(méi)有數(shù)據(jù),則用戶進(jìn)程讓出CPU進(jìn)入阻塞狀態(tài),當(dāng)數(shù)據(jù)到達(dá)接收緩沖區(qū)時(shí),用戶進(jìn)程會(huì)被喚醒,從阻塞狀態(tài)進(jìn)入就緒狀態(tài),等待CPU調(diào)度。

本小節(jié)我們就來(lái)看下用戶進(jìn)程是如何阻塞在Socket上,又是如何在Socket上被喚醒的。理解這個(gè)過(guò)程很重要,對(duì)我們理解epoll的事件通知過(guò)程很有幫助

首先我們?cè)谟脩暨M(jìn)程中對(duì)Socket進(jìn)行read系統(tǒng)調(diào)用時(shí),用戶進(jìn)程會(huì)從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)。

在進(jìn)程的struct task_struct結(jié)構(gòu)找到fd_array,并根據(jù)Socket的文件描述符fd找到對(duì)應(yīng)的struct file,調(diào)用struct file中的文件操作函數(shù)結(jié)合file_operations,read系統(tǒng)調(diào)用對(duì)應(yīng)的是sock_read_iter。

在sock_read_iter函數(shù)中找到struct file指向的struct socket,并調(diào)用socket->ops->recvmsg,這里我們知道調(diào)用的是inet_stream_ops集合中定義的inet_recvmsg。

在inet_recvmsg中會(huì)找到struct sock,并調(diào)用sock->skprot->recvmsg,這里調(diào)用的是tcp_prot集合中定義的tcp_recvmsg函數(shù)。

整個(gè)調(diào)用過(guò)程可以參考上邊的《系統(tǒng)IO調(diào)用結(jié)構(gòu)圖》

熟悉了內(nèi)核函數(shù)調(diào)用棧后,我們來(lái)看下系統(tǒng)IO調(diào)用在tcp_recvmsg內(nèi)核函數(shù)中是如何將用戶進(jìn)程給阻塞掉的

6d0b6f40-f886-11ed-90ce-dac502259ad0.png

系統(tǒng)IO調(diào)用阻塞原理.png

inttcp_recvmsg(structkiocb*iocb,structsock*sk,structmsghdr*msg,
size_tlen,intnonblock,intflags,int*addr_len)
{
.................省略非核心代碼...............
//訪問(wèn)sock對(duì)象中定義的接收隊(duì)列
skb_queue_walk(&sk->sk_receive_queue,skb){

.................省略非核心代碼...............

//沒(méi)有收到足夠數(shù)據(jù),調(diào)用sk_wait_data阻塞當(dāng)前進(jìn)程
sk_wait_data(sk,&timeo);
}
intsk_wait_data(structsock*sk,long*timeo)
{
//創(chuàng)建structsock中等待隊(duì)列上的元素wait_queue_t
//將進(jìn)程描述符和回調(diào)函數(shù)autoremove_wake_function關(guān)聯(lián)到wait_queue_t中
DEFINE_WAIT(wait);

//調(diào)用sk_sleep獲取sock對(duì)象下的等待隊(duì)列的頭指針wait_queue_head_t
//調(diào)用prepare_to_wait將新創(chuàng)建的等待項(xiàng)wait_queue_t插入到等待隊(duì)列中,并將進(jìn)程狀態(tài)設(shè)置為可打斷INTERRUPTIBLE
prepare_to_wait(sk_sleep(sk),&wait,TASK_INTERRUPTIBLE);
set_bit(SOCK_ASYNC_WAITDATA,&sk->sk_socket->flags);

//通過(guò)調(diào)用schedule_timeout讓出CPU,然后進(jìn)行睡眠,導(dǎo)致一次上下文切換
rc=sk_wait_event(sk,timeo,!skb_queue_empty(&sk->sk_receive_queue));
...

首先會(huì)在DEFINE_WAIT中創(chuàng)建struct sock中等待隊(duì)列上的等待類型wait_queue_t。

#defineDEFINE_WAIT(name)DEFINE_WAIT_FUNC(name,autoremove_wake_function)

#defineDEFINE_WAIT_FUNC(name,function)
wait_queue_tname={
.private=current,
.func=function,
.task_list=LIST_HEAD_INIT((name).task_list),
}

等待類型wait_queue_t中的private用來(lái)關(guān)聯(lián)阻塞在當(dāng)前socket上的用戶進(jìn)程fd。func用來(lái)關(guān)聯(lián)等待項(xiàng)上注冊(cè)的回調(diào)函數(shù)。這里注冊(cè)的是autoremove_wake_function。

調(diào)用sk_sleep(sk)獲取struct sock對(duì)象中的等待隊(duì)列頭指針wait_queue_head_t。

調(diào)用prepare_to_wait將新創(chuàng)建的等待項(xiàng)wait_queue_t插入到等待隊(duì)列中,并將進(jìn)程設(shè)置為可打斷 INTERRUPTIBL。

調(diào)用sk_wait_event讓出CPU,進(jìn)程進(jìn)入睡眠狀態(tài)。

用戶進(jìn)程的阻塞過(guò)程我們就介紹完了,關(guān)鍵是要理解記住struct sock中定義的等待隊(duì)列上的等待類型wait_queue_t的結(jié)構(gòu)。后面epoll的介紹中我們還會(huì)用到它。

下面我們接著介紹當(dāng)數(shù)據(jù)就緒后,用戶進(jìn)程是如何被喚醒的

在本文開(kāi)始介紹《網(wǎng)絡(luò)包接收過(guò)程》這一小節(jié)中我們提到:

當(dāng)網(wǎng)絡(luò)數(shù)據(jù)包到達(dá)網(wǎng)卡時(shí),網(wǎng)卡通過(guò)DMA的方式將數(shù)據(jù)放到RingBuffer中。

然后向CPU發(fā)起硬中斷,在硬中斷響應(yīng)程序中創(chuàng)建sk_buffer,并將網(wǎng)絡(luò)數(shù)據(jù)拷貝至sk_buffer中。

隨后發(fā)起軟中斷,內(nèi)核線程ksoftirqd響應(yīng)軟中斷,調(diào)用poll函數(shù)將sk_buffer送往內(nèi)核協(xié)議棧做層層協(xié)議處理。

在傳輸層tcp_rcv 函數(shù)中,去掉TCP頭,根據(jù)四元組(源IP,源端口,目的IP,目的端口)查找對(duì)應(yīng)的Socket。

最后將sk_buffer放到Socket中的接收隊(duì)列里。

上邊這些過(guò)程是內(nèi)核接收網(wǎng)絡(luò)數(shù)據(jù)的完整過(guò)程,下邊我們來(lái)看下,當(dāng)數(shù)據(jù)包接收完畢后,用戶進(jìn)程是如何被喚醒的。

6d1d36da-f886-11ed-90ce-dac502259ad0.png

系統(tǒng)IO調(diào)用喚醒原理.png

當(dāng)軟中斷將sk_buffer放到Socket的接收隊(duì)列上時(shí),接著就會(huì)調(diào)用數(shù)據(jù)就緒函數(shù)回調(diào)指針sk_data_ready,前邊我們提到,這個(gè)函數(shù)指針在初始化的時(shí)候指向了sock_def_readable函數(shù)。

在sock_def_readable函數(shù)中會(huì)去獲取socket->sock->sk_wq等待隊(duì)列。在wake_up_common函數(shù)中從等待隊(duì)列sk_wq中找出一個(gè)等待項(xiàng)wait_queue_t,回調(diào)注冊(cè)在該等待項(xiàng)上的func回調(diào)函數(shù)(wait_queue_t->func),創(chuàng)建等待項(xiàng)wait_queue_t是我們提到,這里注冊(cè)的回調(diào)函數(shù)是autoremove_wake_function。

即使是有多個(gè)進(jìn)程都阻塞在同一個(gè) socket 上,也只喚醒 1 個(gè)進(jìn)程。其作用是為了避免驚群。

在autoremove_wake_function函數(shù)中,根據(jù)等待項(xiàng)wait_queue_t上的private關(guān)聯(lián)的阻塞進(jìn)程fd調(diào)用try_to_wake_up喚醒阻塞在該Socket上的進(jìn)程。

記住wait_queue_t中的func函數(shù)指針,在epoll中這里會(huì)注冊(cè)epoll的回調(diào)函數(shù)。

現(xiàn)在理解epoll所需要的基礎(chǔ)知識(shí)我們就介紹完了,嘮叨了這么多,下面終于正式進(jìn)入本小節(jié)的主題epoll了。

epoll_create創(chuàng)建epoll對(duì)象

epoll_create是內(nèi)核提供給我們創(chuàng)建epoll對(duì)象的一個(gè)系統(tǒng)調(diào)用,當(dāng)我們?cè)谟脩暨M(jìn)程中調(diào)用epoll_create時(shí),內(nèi)核會(huì)為我們創(chuàng)建一個(gè)struct eventpoll對(duì)象,并且也有相應(yīng)的struct file與之關(guān)聯(lián),同樣需要把這個(gè)struct eventpoll對(duì)象所關(guān)聯(lián)的struct file放入進(jìn)程打開(kāi)的文件列表fd_array中管理。

熟悉了Socket的創(chuàng)建邏輯,epoll的創(chuàng)建邏輯也就不難理解了。

struct eventpoll對(duì)象關(guān)聯(lián)的struct file中的file_operations 指針指向的是eventpoll_fops操作函數(shù)集合。

staticconststructfile_operationseventpoll_fops={
.release=ep_eventpoll_release;
.poll=ep_eventpoll_poll,
}
6d306b2e-f886-11ed-90ce-dac502259ad0.png

eopll在進(jìn)程中的整體結(jié)構(gòu).png
structeventpoll{

//等待隊(duì)列,阻塞在epoll上的進(jìn)程會(huì)放在這里
wait_queue_head_twq;

//就緒隊(duì)列,IO就緒的socket連接會(huì)放在這里
structlist_headrdllist;

//紅黑樹(shù)用來(lái)管理所有監(jiān)聽(tīng)的socket連接
structrb_rootrbr;

......
}

wait_queue_head_t wq:epoll中的等待隊(duì)列,隊(duì)列里存放的是阻塞在epoll上的用戶進(jìn)程。在IO就緒的時(shí)候epoll可以通過(guò)這個(gè)隊(duì)列找到這些阻塞的進(jìn)程并喚醒它們,從而執(zhí)行IO調(diào)用讀寫(xiě)Socket上的數(shù)據(jù)。

這里注意與Socket中的等待隊(duì)列區(qū)分?。?!

struct list_head rdllist:epoll中的就緒隊(duì)列,隊(duì)列里存放的是都是IO就緒的Socket,被喚醒的用戶進(jìn)程可以直接讀取這個(gè)隊(duì)列獲取IO活躍的Socket。無(wú)需再次遍歷整個(gè)Socket集合。

這里正是epoll比select ,poll高效之處,select ,poll返回的是全部的socket連接,我們需要在用戶空間再次遍歷找出真正IO活躍的Socket連接。而epoll只是返回IO活躍的Socket連接。用戶進(jìn)程可以直接進(jìn)行IO操作。

struct rb_root rbr : 由于紅黑樹(shù)在查找,插入,刪除等綜合性能方面是最優(yōu)的,所以epoll內(nèi)部使用一顆紅黑樹(shù)來(lái)管理海量的Socket連接。

select用數(shù)組管理連接,poll用鏈表管理連接。

epoll_ctl向epoll對(duì)象中添加監(jiān)聽(tīng)的Socket

當(dāng)我們調(diào)用epoll_create在內(nèi)核中創(chuàng)建出epoll對(duì)象struct eventpoll后,我們就可以利用epoll_ctl向epoll中添加我們需要管理的Socket連接了。

首先要在epoll內(nèi)核中創(chuàng)建一個(gè)表示Socket連接的數(shù)據(jù)結(jié)構(gòu)struct epitem,而在epoll中為了綜合性能的考慮,采用一顆紅黑樹(shù)來(lái)管理這些海量socket連接。所以struct epitem是一個(gè)紅黑樹(shù)節(jié)點(diǎn)。

6d4493ba-f886-11ed-90ce-dac502259ad0.png

struct epitem.png

structepitem
{
//指向所屬epoll對(duì)象
structeventpoll*ep;
//注冊(cè)的感興趣的事件,也就是用戶空間的epoll_event
structepoll_eventevent;
//指向epoll對(duì)象中的就緒隊(duì)列
structlist_headrdllink;
//指向epoll中對(duì)應(yīng)的紅黑樹(shù)節(jié)點(diǎn)
structrb_noderbn;
//指向epitem所表示的socket->file結(jié)構(gòu)以及對(duì)應(yīng)的fd
structepoll_filefdffd;
}

這里重點(diǎn)記住struct epitem結(jié)構(gòu)中的rdllink以及epoll_filefd成員,后面我們會(huì)用到。

在內(nèi)核中創(chuàng)建完表示Socket連接的數(shù)據(jù)結(jié)構(gòu)struct epitem后,我們就需要在Socket中的等待隊(duì)列上創(chuàng)建等待項(xiàng)wait_queue_t并且注冊(cè)epoll的回調(diào)函數(shù)ep_poll_callback。

通過(guò)《阻塞IO中用戶進(jìn)程阻塞以及喚醒原理》小節(jié)的鋪墊,我想大家已經(jīng)猜到這一步的意義所在了吧!當(dāng)時(shí)在等待項(xiàng)wait_queue_t中注冊(cè)的是autoremove_wake_function回調(diào)函數(shù)。還記得嗎?

epoll的回調(diào)函數(shù)ep_poll_callback正是epoll同步IO事件通知機(jī)制的核心所在,也是區(qū)別于select,poll采用內(nèi)核輪詢方式的根本性能差異所在。

6d588be0-f886-11ed-90ce-dac502259ad0.png

epitem創(chuàng)建等待項(xiàng).png

這里又出現(xiàn)了一個(gè)新的數(shù)據(jù)結(jié)構(gòu)struct eppoll_entry,那它的作用是干什么的呢?大家可以結(jié)合上圖先猜測(cè)下它的作用!

我們知道socket->sock->sk_wq等待隊(duì)列中的類型是wait_queue_t,我們需要在struct epitem所表示的socket的等待隊(duì)列上注冊(cè)epoll回調(diào)函數(shù)ep_poll_callback。

這樣當(dāng)數(shù)據(jù)到達(dá)socket中的接收隊(duì)列時(shí),內(nèi)核會(huì)回調(diào)sk_data_ready,在阻塞IO中用戶進(jìn)程阻塞以及喚醒原理這一小節(jié)中,我們知道這個(gè)sk_data_ready函數(shù)指針會(huì)指向sk_def_readable函數(shù),在sk_def_readable中會(huì)回調(diào)注冊(cè)在等待隊(duì)列里的等待項(xiàng)wait_queue_t -> func回調(diào)函數(shù)ep_poll_callback。在ep_poll_callback中需要找到epitem,將IO就緒的epitem放入epoll中的就緒隊(duì)列中。

而socket等待隊(duì)列中類型是wait_queue_t無(wú)法關(guān)聯(lián)到epitem。所以就出現(xiàn)了struct eppoll_entry結(jié)構(gòu)體,它的作用就是關(guān)聯(lián)Socket等待隊(duì)列中的等待項(xiàng)wait_queue_t和epitem。

structeppoll_entry{
//指向關(guān)聯(lián)的epitem
structepitem*base;

//關(guān)聯(lián)監(jiān)聽(tīng)socket中等待隊(duì)列中的等待項(xiàng)(private=nullfunc=ep_poll_callback)
wait_queue_twait;

//監(jiān)聽(tīng)socket中等待隊(duì)列頭指針
wait_queue_head_t*whead;
.........
};

這樣在ep_poll_callback回調(diào)函數(shù)中就可以根據(jù)Socket等待隊(duì)列中的等待項(xiàng)wait,通過(guò)container_of宏找到eppoll_entry,繼而找到epitem了。

container_of在Linux內(nèi)核中是一個(gè)常用的宏,用于從包含在某個(gè)結(jié)構(gòu)中的指針獲得結(jié)構(gòu)本身的指針,通俗地講就是通過(guò)結(jié)構(gòu)體變量中某個(gè)成員的首地址進(jìn)而獲得整個(gè)結(jié)構(gòu)體變量的首地址。

這里需要注意下這次等待項(xiàng)wait_queue_t中的private設(shè)置的是null,因?yàn)檫@里Socket是交給epoll來(lái)管理的,阻塞在Socket上的進(jìn)程是也由epoll來(lái)喚醒。在等待項(xiàng)wait_queue_t注冊(cè)的func是ep_poll_callback而不是autoremove_wake_function,阻塞進(jìn)程并不需要autoremove_wake_function來(lái)喚醒,所以這里設(shè)置private為null

當(dāng)在Socket的等待隊(duì)列中創(chuàng)建好等待項(xiàng)wait_queue_t并且注冊(cè)了epoll的回調(diào)函數(shù)ep_poll_callback,然后又通過(guò)eppoll_entry關(guān)聯(lián)了epitem后。剩下要做的就是將epitem插入到epoll中的紅黑樹(shù)struct rb_root rbr中。

這里可以看到epoll另一個(gè)優(yōu)化的地方,epoll將所有的socket連接通過(guò)內(nèi)核中的紅黑樹(shù)來(lái)集中管理。每次添加或者刪除socket連接都是增量添加刪除,而不是像select,poll那樣每次調(diào)用都是全量socket連接集合傳入內(nèi)核。避免了頻繁大量的內(nèi)存拷貝。

epoll_wait同步阻塞獲取IO就緒的Socket

用戶程序調(diào)用epoll_wait后,內(nèi)核首先會(huì)查找epoll中的就緒隊(duì)列eventpoll->rdllist是否有IO就緒的epitem。epitem里封裝了socket的信息。如果就緒隊(duì)列中有就緒的epitem,就將就緒的socket信息封裝到epoll_event返回。

如果eventpoll->rdllist就緒隊(duì)列中沒(méi)有IO就緒的epitem,則會(huì)創(chuàng)建等待項(xiàng)wait_queue_t,將用戶進(jìn)程的fd關(guān)聯(lián)到wait_queue_t->private上,并在等待項(xiàng)wait_queue_t->func上注冊(cè)回調(diào)函數(shù)default_wake_function。最后將等待項(xiàng)添加到epoll中的等待隊(duì)列中。用戶進(jìn)程讓出CPU,進(jìn)入阻塞狀態(tài)。

6d6b519e-f886-11ed-90ce-dac502259ad0.png

epoll_wait同步獲取數(shù)據(jù).png

這里和阻塞IO模型中的阻塞原理是一樣的,只不過(guò)在阻塞IO模型中注冊(cè)到等待項(xiàng)wait_queue_t->func上的是autoremove_wake_function,并將等待項(xiàng)添加到socket中的等待隊(duì)列中。這里注冊(cè)的是default_wake_function,將等待項(xiàng)添加到epoll中的等待隊(duì)列上。

6d7ea474-f886-11ed-90ce-dac502259ad0.png

數(shù)據(jù)到來(lái)epoll_wait流程.png

前邊做了那么多的知識(shí)鋪墊,下面終于到了epoll的整個(gè)工作流程了:

6d93a1d0-f886-11ed-90ce-dac502259ad0.png

epoll_wait處理過(guò)程.png

當(dāng)網(wǎng)絡(luò)數(shù)據(jù)包在軟中斷中經(jīng)過(guò)內(nèi)核協(xié)議棧的處理到達(dá)socket的接收緩沖區(qū)時(shí),緊接著會(huì)調(diào)用socket的數(shù)據(jù)就緒回調(diào)指針sk_data_ready,回調(diào)函數(shù)為sock_def_readable。在socket的等待隊(duì)列中找出等待項(xiàng),其中等待項(xiàng)中注冊(cè)的回調(diào)函數(shù)為ep_poll_callback。

在回調(diào)函數(shù)ep_poll_callback中,根據(jù)struct eppoll_entry中的struct wait_queue_t wait通過(guò)container_of宏找到eppoll_entry對(duì)象并通過(guò)它的base指針找到封裝socket的數(shù)據(jù)結(jié)構(gòu)struct epitem,并將它加入到epoll中的就緒隊(duì)列rdllist中。

隨后查看epoll中的等待隊(duì)列中是否有等待項(xiàng),也就是說(shuō)查看是否有進(jìn)程阻塞在epoll_wait上等待IO就緒的socket。如果沒(méi)有等待項(xiàng),則軟中斷處理完成。

如果有等待項(xiàng),則回到注冊(cè)在等待項(xiàng)中的回調(diào)函數(shù)default_wake_function,在回調(diào)函數(shù)中喚醒阻塞進(jìn)程,并將就緒隊(duì)列rdllist中的epitem的IO就緒socket信息封裝到struct epoll_event中返回。

用戶進(jìn)程拿到epoll_event獲取IO就緒的socket,發(fā)起系統(tǒng)IO調(diào)用讀取數(shù)據(jù)。

再談水平觸發(fā)和邊緣觸發(fā)

網(wǎng)上有大量的關(guān)于這兩種模式的講解,大部分講的比較模糊,感覺(jué)只是強(qiáng)行從概念上進(jìn)行描述,看完讓人難以理解。所以在這里,筆者想結(jié)合上邊epoll的工作過(guò)程,再次對(duì)這兩種模式做下自己的解讀,力求清晰的解釋出這兩種工作模式的異同。

經(jīng)過(guò)上邊對(duì)epoll工作過(guò)程的詳細(xì)解讀,我們知道,當(dāng)我們監(jiān)聽(tīng)的socket上有數(shù)據(jù)到來(lái)時(shí),軟中斷會(huì)執(zhí)行epoll的回調(diào)函數(shù)ep_poll_callback,在回調(diào)函數(shù)中會(huì)將epoll中描述socket信息的數(shù)據(jù)結(jié)構(gòu)epitem插入到epoll中的就緒隊(duì)列rdllist中。隨后用戶進(jìn)程從epoll的等待隊(duì)列中被喚醒,epoll_wait將IO就緒的socket返回給用戶進(jìn)程,隨即epoll_wait會(huì)清空rdllist。

水平觸發(fā)邊緣觸發(fā)最關(guān)鍵的區(qū)別就在于當(dāng)socket中的接收緩沖區(qū)還有數(shù)據(jù)可讀時(shí)。epoll_wait是否會(huì)清空rdllist。

水平觸發(fā):在這種模式下,用戶線程調(diào)用epoll_wait獲取到IO就緒的socket后,對(duì)Socket進(jìn)行系統(tǒng)IO調(diào)用讀取數(shù)據(jù),假設(shè)socket中的數(shù)據(jù)只讀了一部分沒(méi)有全部讀完,這時(shí)再次調(diào)用epoll_wait,epoll_wait會(huì)檢查這些Socket中的接收緩沖區(qū)是否還有數(shù)據(jù)可讀,如果還有數(shù)據(jù)可讀,就將socket重新放回rdllist。所以當(dāng)socket上的IO沒(méi)有被處理完時(shí),再次調(diào)用epoll_wait依然可以獲得這些socket,用戶進(jìn)程可以接著處理socket上的IO事件。

邊緣觸發(fā): 在這種模式下,epoll_wait就會(huì)直接清空rdllist,不管socket上是否還有數(shù)據(jù)可讀。所以在邊緣觸發(fā)模式下,當(dāng)你沒(méi)有來(lái)得及處理socket接收緩沖區(qū)的剩下可讀數(shù)據(jù)時(shí),再次調(diào)用epoll_wait,因?yàn)檫@時(shí)rdlist已經(jīng)被清空了,socket不會(huì)再次從epoll_wait中返回,所以用戶進(jìn)程就不會(huì)再次獲得這個(gè)socket了,也就無(wú)法在對(duì)它進(jìn)行IO處理了。除非,這個(gè)socket上有新的IO數(shù)據(jù)到達(dá),根據(jù)epoll的工作過(guò)程,該socket會(huì)被再次放入rdllist中。

如果你在邊緣觸發(fā)模式下,處理了部分socket上的數(shù)據(jù),那么想要處理剩下部分的數(shù)據(jù),就只能等到這個(gè)socket上再次有網(wǎng)絡(luò)數(shù)據(jù)到達(dá)。

在Netty中實(shí)現(xiàn)的EpollSocketChannel默認(rèn)的就是邊緣觸發(fā)模式。JDK的NIO默認(rèn)是水平觸發(fā)模式。

epoll對(duì)select,poll的優(yōu)化總結(jié)

epoll在內(nèi)核中通過(guò)紅黑樹(shù)管理海量的連接,所以在調(diào)用epoll_wait獲取IO就緒的socket時(shí),不需要傳入監(jiān)聽(tīng)的socket文件描述符。從而避免了海量的文件描述符集合在用戶空間和內(nèi)核空間中來(lái)回復(fù)制。

select,poll每次調(diào)用時(shí)都需要傳遞全量的文件描述符集合,導(dǎo)致大量頻繁的拷貝操作。

epoll僅會(huì)通知IO就緒的socket。避免了在用戶空間遍歷的開(kāi)銷。

select,poll只會(huì)在IO就緒的socket上打好標(biāo)記,依然是全量返回,所以在用戶空間還需要用戶程序在一次遍歷全量集合找出具體IO就緒的socket。

epoll通過(guò)在socket的等待隊(duì)列上注冊(cè)回調(diào)函數(shù)ep_poll_callback通知用戶程序IO就緒的socket。避免了在內(nèi)核中輪詢的開(kāi)銷。

大部分情況下socket上并不總是IO活躍的,在面對(duì)海量連接的情況下,select,poll采用內(nèi)核輪詢的方式獲取IO活躍的socket,無(wú)疑是性能低下的核心原因。

根據(jù)以上epoll的性能優(yōu)勢(shì),它是目前為止各大主流網(wǎng)絡(luò)框架,以及反向代理中間件使用到的網(wǎng)絡(luò)IO模型。

利用epoll多路復(fù)用IO模型可以輕松的解決C10K問(wèn)題。

C100k的解決方案也還是基于C10K的方案,通過(guò)epoll 配合線程池,再加上 CPU、內(nèi)存和網(wǎng)絡(luò)接口的性能和容量提升。大部分情況下,C100K很自然就可以達(dá)到。

甚至C1000K的解決方法,本質(zhì)上還是構(gòu)建在 epoll 的多路復(fù)用 I/O 模型上。只不過(guò),除了 I/O 模型之外,還需要從應(yīng)用程序到 Linux 內(nèi)核、再到 CPU、內(nèi)存和網(wǎng)絡(luò)等各個(gè)層次的深度優(yōu)化,特別是需要借助硬件,來(lái)卸載那些原來(lái)通過(guò)軟件處理的大量功能(去掉大量的中斷響應(yīng)開(kāi)銷,以及內(nèi)核協(xié)議棧處理的開(kāi)銷)。

信號(hào)驅(qū)動(dòng)IO

大家對(duì)這個(gè)裝備肯定不會(huì)陌生,當(dāng)我們?nèi)ヒ恍┟朗吵浅燥埖臅r(shí)候,點(diǎn)完餐付了錢(qián),老板會(huì)給我們一個(gè)信號(hào)器。然后我們帶著這個(gè)信號(hào)器可以去找餐桌,或者干些其他的事情。當(dāng)信號(hào)器亮了的時(shí)候,這時(shí)代表飯餐已經(jīng)做好,我們可以去窗口取餐了。

這個(gè)典型的生活場(chǎng)景和我們要介紹的信號(hào)驅(qū)動(dòng)IO模型就很像。

在信號(hào)驅(qū)動(dòng)IO模型下,用戶進(jìn)程操作通過(guò)系統(tǒng)調(diào)用 sigaction 函數(shù)發(fā)起一個(gè) IO 請(qǐng)求,在對(duì)應(yīng)的socket注冊(cè)一個(gè)信號(hào)回調(diào),此時(shí)不阻塞用戶進(jìn)程,進(jìn)程會(huì)繼續(xù)工作。當(dāng)內(nèi)核數(shù)據(jù)就緒時(shí),內(nèi)核就為該進(jìn)程生成一個(gè) SIGIO 信號(hào),通過(guò)信號(hào)回調(diào)通知進(jìn)程進(jìn)行相關(guān) IO 操作。

這里需要注意的是:信號(hào)驅(qū)動(dòng)式 IO 模型依然是同步IO,因?yàn)樗m然可以在等待數(shù)據(jù)的時(shí)候不被阻塞,也不會(huì)頻繁的輪詢,但是當(dāng)數(shù)據(jù)就緒,內(nèi)核信號(hào)通知后,用戶進(jìn)程依然要自己去讀取數(shù)據(jù),在數(shù)據(jù)拷貝階段發(fā)生阻塞。

信號(hào)驅(qū)動(dòng) IO模型 相比于前三種 IO 模型,實(shí)現(xiàn)了在等待數(shù)據(jù)就緒時(shí),進(jìn)程不被阻塞,主循環(huán)可以繼續(xù)工作,所以理論上性能更佳。

但是實(shí)際上,使用TCP協(xié)議通信時(shí),信號(hào)驅(qū)動(dòng)IO模型幾乎不會(huì)被采用。原因如下:

信號(hào)IO 在大量 IO 操作時(shí)可能會(huì)因?yàn)樾盘?hào)隊(duì)列溢出導(dǎo)致沒(méi)法通知

SIGIO 信號(hào)是一種 Unix 信號(hào),信號(hào)沒(méi)有附加信息,如果一個(gè)信號(hào)源有多種產(chǎn)生信號(hào)的原因,信號(hào)接收者就無(wú)法確定究竟發(fā)生了什么。而 TCP socket 生產(chǎn)的信號(hào)事件有七種之多,這樣應(yīng)用程序收到 SIGIO,根本無(wú)從區(qū)分處理。

但信號(hào)驅(qū)動(dòng)IO模型可以用在 UDP通信上,因?yàn)閁DP 只有一個(gè)數(shù)據(jù)請(qǐng)求事件,這也就意味著在正常情況下 UDP 進(jìn)程只要捕獲 SIGIO 信號(hào),就調(diào)用 read 系統(tǒng)調(diào)用讀取到達(dá)的數(shù)據(jù)。如果出現(xiàn)異常,就返回一個(gè)異常錯(cuò)誤。

這里插句題外話,大家覺(jué)不覺(jué)得阻塞IO模型在生活中的例子就像是我們?cè)谑程门抨?duì)打飯。你自己需要排隊(duì)去打飯同時(shí)打飯師傅在配菜的過(guò)程中你需要等待。

IO多路復(fù)用模型就像是我們?cè)陲埖觊T(mén)口排隊(duì)等待叫號(hào)。叫號(hào)器就好比select,poll,epoll可以統(tǒng)一管理全部顧客的吃飯就緒事件,客戶好比是socket連接,誰(shuí)可以去吃飯了,叫號(hào)器就通知誰(shuí)。

##異步IO(AIO)

以上介紹的四種IO模型均為同步IO,它們都會(huì)阻塞在第二階段數(shù)據(jù)拷貝階段。

通過(guò)在前邊小節(jié)《同步與異步》中的介紹,相信大家很容易就會(huì)理解異步IO模型,在異步IO模型下,IO操作在數(shù)據(jù)準(zhǔn)備階段和數(shù)據(jù)拷貝階段均是由內(nèi)核來(lái)完成,不會(huì)對(duì)應(yīng)用程序造成任何阻塞。應(yīng)用進(jìn)程只需要在指定的數(shù)組中引用數(shù)據(jù)即可。

異步 IO 與信號(hào)驅(qū)動(dòng) IO 的主要區(qū)別在于:信號(hào)驅(qū)動(dòng) IO 由內(nèi)核通知何時(shí)可以開(kāi)始一個(gè) IO 操作,而異步 IO由內(nèi)核通知 IO 操作何時(shí)已經(jīng)完成。

舉個(gè)生活中的例子:異步IO模型就像我們?nèi)ヒ粋€(gè)高檔飯店里的包間吃飯,我們只需要坐在包間里面,點(diǎn)完餐(類比異步IO調(diào)用)之后,我們就什么也不需要管,該喝酒喝酒,該聊天聊天,飯餐做好后服務(wù)員(類比內(nèi)核)會(huì)自己給我們送到包間(類比用戶空間)來(lái)。整個(gè)過(guò)程沒(méi)有任何阻塞。

異步IO的系統(tǒng)調(diào)用需要操作系統(tǒng)內(nèi)核來(lái)支持,目前只有Window中的IOCP實(shí)現(xiàn)了非常成熟的異步IO機(jī)制。

而Linux系統(tǒng)對(duì)異步IO機(jī)制實(shí)現(xiàn)的不夠成熟,且與NIO的性能相比提升也不明顯。

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

再加上信號(hào)驅(qū)動(dòng)IO模型不適用TCP協(xié)議,所以目前大部分采用的還是IO多路復(fù)用模型。

IO線程模型

在前邊內(nèi)容的介紹中,我們?cè)斒隽司W(wǎng)絡(luò)數(shù)據(jù)包的接收和發(fā)送過(guò)程,并通過(guò)介紹5種IO模型了解了內(nèi)核是如何讀取網(wǎng)絡(luò)數(shù)據(jù)并通知給用戶線程的。

前邊的內(nèi)容都是以內(nèi)核空間的視角來(lái)剖析網(wǎng)絡(luò)數(shù)據(jù)的收發(fā)模型,本小節(jié)我們站在用戶空間的視角來(lái)看下如果對(duì)網(wǎng)絡(luò)數(shù)據(jù)進(jìn)行收發(fā)。

相對(duì)內(nèi)核來(lái)講,用戶空間的IO線程模型相對(duì)就簡(jiǎn)單一些。這些用戶空間的IO線程模型都是在討論當(dāng)多線程一起配合工作時(shí)誰(shuí)負(fù)責(zé)接收連接,誰(shuí)負(fù)責(zé)響應(yīng)IO 讀寫(xiě)、誰(shuí)負(fù)責(zé)計(jì)算、誰(shuí)負(fù)責(zé)發(fā)送和接收,僅僅是用戶IO線程的不同分工模式罷了。

Reactor

Reactor是利用NIO對(duì)IO線程進(jìn)行不同的分工:

使用前邊我們提到的IO多路復(fù)用模型比如select,poll,epoll,kqueue,進(jìn)行IO事件的注冊(cè)和監(jiān)聽(tīng)。

將監(jiān)聽(tīng)到就緒的IO事件分發(fā)dispatch到各個(gè)具體的處理Handler中進(jìn)行相應(yīng)的IO事件處理。

通過(guò)IO多路復(fù)用技術(shù)就可以不斷的監(jiān)聽(tīng)I(yíng)O事件,不斷的分發(fā)dispatch,就像一個(gè)反應(yīng)堆一樣,看起來(lái)像不斷的產(chǎn)生IO事件,因此我們稱這種模式為Reactor模型。

下面我們來(lái)看下Reactor模型的三種分類:

單Reactor單線程

6deff20a-f886-11ed-90ce-dac502259ad0.png

單Reactor單線程

Reactor模型是依賴IO多路復(fù)用技術(shù)實(shí)現(xiàn)監(jiān)聽(tīng)I(yíng)O事件,從而源源不斷的產(chǎn)生IO就緒事件,在Linux系統(tǒng)下我們使用epoll來(lái)進(jìn)行IO多路復(fù)用,我們以Linux系統(tǒng)為例:

單Reactor意味著只有一個(gè)epoll對(duì)象,用來(lái)監(jiān)聽(tīng)所有的事件,比如連接事件,讀寫(xiě)事件。

單線程意味著只有一個(gè)線程來(lái)執(zhí)行epoll_wait獲取IO就緒的Socket,然后對(duì)這些就緒的Socket執(zhí)行讀寫(xiě),以及后邊的業(yè)務(wù)處理也依然是這個(gè)線程。

單Reactor單線程模型就好比我們開(kāi)了一個(gè)很小很小的小飯館,作為老板的我們需要一個(gè)人干所有的事情,包括:迎接顧客(accept事件),為顧客介紹菜單等待顧客點(diǎn)菜(IO請(qǐng)求),做菜(業(yè)務(wù)處理),上菜(IO響應(yīng)),送客(斷開(kāi)連接)。

單Reactor多線程

隨著客人的增多(并發(fā)請(qǐng)求),顯然飯館里的事情只有我們一個(gè)人干(單線程)肯定是忙不過(guò)來(lái)的,這時(shí)候我們就需要多招聘一些員工(多線程)來(lái)幫著一起干上述的事情。

于是就有了單Reactor多線程模型:

6dffbdfc-f886-11ed-90ce-dac502259ad0.png

單Reactor多線程

這種模式下,也是只有一個(gè)epoll對(duì)象來(lái)監(jiān)聽(tīng)所有的IO事件,一個(gè)線程來(lái)調(diào)用epoll_wait獲取IO就緒的Socket。

但是當(dāng)IO就緒事件產(chǎn)生時(shí),這些IO事件對(duì)應(yīng)處理的業(yè)務(wù)Handler,我們是通過(guò)線程池來(lái)執(zhí)行。這樣相比單Reactor單線程模型提高了執(zhí)行效率,充分發(fā)揮了多核CPU的優(yōu)勢(shì)。

主從Reactor多線程

做任何事情都要區(qū)分事情的優(yōu)先級(jí),我們應(yīng)該優(yōu)先高效的去做優(yōu)先級(jí)更高的事情,而不是一股腦不分優(yōu)先級(jí)的全部去做。

當(dāng)我們的小飯館客人越來(lái)越多(并發(fā)量越來(lái)越大),我們就需要擴(kuò)大飯店的規(guī)模,在這個(gè)過(guò)程中我們發(fā)現(xiàn),迎接客人是飯店最重要的工作,我們要先把客人迎接進(jìn)來(lái),不能讓客人一看人多就走掉,只要客人進(jìn)來(lái)了,哪怕菜做的慢一點(diǎn)也沒(méi)關(guān)系。

于是,主從Reactor多線程模型就產(chǎn)生了:

6e10761a-f886-11ed-90ce-dac502259ad0.png

主從Reactor多線程

我們由原來(lái)的單Reactor變?yōu)榱硕郣eactor。主Reactor用來(lái)優(yōu)先專門(mén)做優(yōu)先級(jí)最高的事情,也就是迎接客人(處理連接事件),對(duì)應(yīng)的處理Handler就是圖中的acceptor。

當(dāng)創(chuàng)建好連接,建立好對(duì)應(yīng)的socket后,在acceptor中將要監(jiān)聽(tīng)的read事件注冊(cè)到從Reactor中,由從Reactor來(lái)監(jiān)聽(tīng)socket上的讀寫(xiě)事件。

最終將讀寫(xiě)的業(yè)務(wù)邏輯處理交給線程池處理。

注意:這里向從Reactor注冊(cè)的只是read事件,并沒(méi)有注冊(cè)write事件,因?yàn)閞ead事件是由epoll內(nèi)核觸發(fā)的,而write事件則是由用戶業(yè)務(wù)線程觸發(fā)的(什么時(shí)候發(fā)送數(shù)據(jù)是由具體業(yè)務(wù)線程決定的),所以write事件理應(yīng)是由用戶業(yè)務(wù)線程去注冊(cè)。

用戶線程注冊(cè)write事件的時(shí)機(jī)是只有當(dāng)用戶發(fā)送的數(shù)據(jù)無(wú)法一次性全部寫(xiě)入buffer時(shí),才會(huì)去注冊(cè)write事件,等待buffer重新可寫(xiě)時(shí),繼續(xù)寫(xiě)入剩下的發(fā)送數(shù)據(jù)、如果用戶線程可以一股腦的將發(fā)送數(shù)據(jù)全部寫(xiě)入buffer,那么也就無(wú)需注冊(cè)write事件到從Reactor中。

主從Reactor多線程模型是現(xiàn)在大部分主流網(wǎng)絡(luò)框架中采用的一種IO線程模型。我們本系列的主題Netty就是用的這種模型。

Proactor

Proactor是基于AIO對(duì)IO線程進(jìn)行分工的一種模型。前邊我們介紹了異步IO模型,它是操作系統(tǒng)內(nèi)核支持的一種全異步編程模型,在數(shù)據(jù)準(zhǔn)備階段和數(shù)據(jù)拷貝階段全程無(wú)阻塞。

ProactorIO線程模型將IO事件的監(jiān)聽(tīng),IO操作的執(zhí)行,IO結(jié)果的dispatch統(tǒng)統(tǒng)交給內(nèi)核來(lái)做。

6e1f1dbe-f886-11ed-90ce-dac502259ad0.png

proactor.png

Proactor模型組件介紹:

completion handler 為用戶程序定義的異步IO操作回調(diào)函數(shù),在異步IO操作完成時(shí)會(huì)被內(nèi)核回調(diào)并通知IO結(jié)果。

Completion Event Queue 異步IO操作完成后,會(huì)產(chǎn)生對(duì)應(yīng)的IO完成事件,將IO完成事件放入該隊(duì)列中。

Asynchronous Operation Processor 負(fù)責(zé)異步IO的執(zhí)行。執(zhí)行完成后產(chǎn)生IO完成事件放入Completion Event Queue 隊(duì)列中。

Proactor 是一個(gè)事件循環(huán)派發(fā)器,負(fù)責(zé)從Completion Event Queue中獲取IO完成事件,并回調(diào)與IO完成事件關(guān)聯(lián)的completion handler。

Initiator 初始化異步操作(asynchronous operation)并通過(guò)Asynchronous Operation Processor將completion handler和proactor注冊(cè)到內(nèi)核。

Proactor模型執(zhí)行過(guò)程:

用戶線程發(fā)起aio_read,并告訴內(nèi)核用戶空間中的讀緩沖區(qū)地址,以便內(nèi)核完成IO操作將結(jié)果放入用戶空間的讀緩沖區(qū),用戶線程直接可以讀取結(jié)果(無(wú)任何阻塞)。

Initiator 初始化aio_read異步讀取操作(asynchronous operation),并將completion handler注冊(cè)到內(nèi)核。

在Proactor中我們關(guān)心的IO完成事件:內(nèi)核已經(jīng)幫我們讀好數(shù)據(jù)并放入我們指定的讀緩沖區(qū),用戶線程可以直接讀取。在Reactor中我們關(guān)心的是IO就緒事件:數(shù)據(jù)已經(jīng)到來(lái),但是需要用戶線程自己去讀取。

此時(shí)用戶線程就可以做其他事情了,無(wú)需等待IO結(jié)果。而內(nèi)核與此同時(shí)開(kāi)始異步執(zhí)行IO操作。當(dāng)IO操作完成時(shí)會(huì)產(chǎn)生一個(gè)completion event事件,將這個(gè)IO完成事件放入completion event queue中。

Proactor從completion event queue中取出completion event,并回調(diào)與IO完成事件關(guān)聯(lián)的completion handler。

在completion handler中完成業(yè)務(wù)邏輯處理。

Reactor與Proactor對(duì)比

Reactor是基于NIO實(shí)現(xiàn)的一種IO線程模型,Proactor是基于AIO實(shí)現(xiàn)的IO線程模型。

Reactor關(guān)心的是IO就緒事件,Proactor關(guān)心的是IO完成事件。

在Proactor中,用戶程序需要向內(nèi)核傳遞用戶空間的讀緩沖區(qū)地址。Reactor則不需要。這也就導(dǎo)致了在Proactor中每個(gè)并發(fā)操作都要求有獨(dú)立的緩存區(qū),在內(nèi)存上有一定的開(kāi)銷。

Proactor 的實(shí)現(xiàn)邏輯復(fù)雜,編碼成本較 Reactor要高很多。

Proactor 在處理高耗時(shí) IO時(shí)的性能要高于 Reactor,但對(duì)于低耗時(shí) IO的執(zhí)行效率提升并不明顯。

Netty的IO模型

在我們介紹完網(wǎng)絡(luò)數(shù)據(jù)包在內(nèi)核中的收發(fā)過(guò)程以及五種IO模型和兩種IO線程模型后,現(xiàn)在我們來(lái)看下netty中的IO模型是什么樣的。

在我們介紹Reactor IO線程模型的時(shí)候提到有三種Reactor模型:?jiǎn)蜶eactor單線程,單Reactor多線程,主從Reactor多線程。

這三種Reactor模型在netty中都是支持的,但是我們常用的是主從Reactor多線程模型。

而我們之前介紹的三種Reactor只是一種模型,是一種設(shè)計(jì)思想。實(shí)際上各種網(wǎng)絡(luò)框架在實(shí)現(xiàn)中并不是嚴(yán)格按照模型來(lái)實(shí)現(xiàn)的,會(huì)有一些小的不同,但大體設(shè)計(jì)思想上是一樣的。

下面我們來(lái)看下netty中的主從Reactor多線程模型是什么樣子的?

6e2fc9ac-f886-11ed-90ce-dac502259ad0.png

netty中的reactor.png

Reactor在netty中是以group的形式出現(xiàn)的,netty中將Reactor分為兩組,一組是MainReactorGroup也就是我們?cè)诰幋a中常常看到的EventLoopGroup bossGroup,另一組是SubReactorGroup也就是我們?cè)诰幋a中常??吹降腅ventLoopGroup workerGroup。

MainReactorGroup中通常只有一個(gè)Reactor,專門(mén)負(fù)責(zé)做最重要的事情,也就是監(jiān)聽(tīng)連接accept事件。當(dāng)有連接事件產(chǎn)生時(shí),在對(duì)應(yīng)的處理handler acceptor中創(chuàng)建初始化相應(yīng)的NioSocketChannel(代表一個(gè)Socket連接)。然后以負(fù)載均衡的方式在SubReactorGroup中選取一個(gè)Reactor,注冊(cè)上去,監(jiān)聽(tīng)Read事件。

MainReactorGroup中只有一個(gè)Reactor的原因是,通常我們服務(wù)端程序只會(huì)綁定監(jiān)聽(tīng)一個(gè)端口,如果要綁定監(jiān)聽(tīng)多個(gè)端口,就會(huì)配置多個(gè)Reactor。

SubReactorGroup中有多個(gè)Reactor,具體Reactor的個(gè)數(shù)可以由系統(tǒng)參數(shù) -D io.netty.eventLoopThreads指定。默認(rèn)的Reactor的個(gè)數(shù)為CPU核數(shù) * 2。SubReactorGroup中的Reactor主要負(fù)責(zé)監(jiān)聽(tīng)讀寫(xiě)事件,每一個(gè)Reactor負(fù)責(zé)監(jiān)聽(tīng)一組socket連接。將全量的連接分?jǐn)傇诙鄠€(gè)Reactor中。

一個(gè)Reactor分配一個(gè)IO線程,這個(gè)IO線程負(fù)責(zé)從Reactor中獲取IO就緒事件,執(zhí)行IO調(diào)用獲取IO數(shù)據(jù),執(zhí)行PipeLine。

Socket連接在創(chuàng)建后就被固定的分配給一個(gè)Reactor,所以一個(gè)Socket連接也只會(huì)被一個(gè)固定的IO線程執(zhí)行,每個(gè)Socket連接分配一個(gè)獨(dú)立的PipeLine實(shí)例,用來(lái)編排這個(gè)Socket連接上的IO處理邏輯。這種無(wú)鎖串行化的設(shè)計(jì)的目的是為了防止多線程并發(fā)執(zhí)行同一個(gè)socket連接上的IO邏輯處理,防止出現(xiàn)線程安全問(wèn)題。同時(shí)使系統(tǒng)吞吐量達(dá)到最大化

由于每個(gè)Reactor中只有一個(gè)IO線程,這個(gè)IO線程既要執(zhí)行IO活躍Socket連接對(duì)應(yīng)的PipeLine中的ChannelHandler,又要從Reactor中獲取IO就緒事件,執(zhí)行IO調(diào)用。所以PipeLine中ChannelHandler中執(zhí)行的邏輯不能耗時(shí)太長(zhǎng),盡量將耗時(shí)的業(yè)務(wù)邏輯處理放入單獨(dú)的業(yè)務(wù)線程池中處理,否則會(huì)影響其他連接的IO讀寫(xiě),從而近一步影響整個(gè)服務(wù)程序的IO吞吐。

當(dāng)IO請(qǐng)求在業(yè)務(wù)線程中完成相應(yīng)的業(yè)務(wù)邏輯處理后,在業(yè)務(wù)線程中利用持有的ChannelHandlerContext引用將響應(yīng)數(shù)據(jù)在PipeLine中反向傳播,最終寫(xiě)回給客戶端。

netty中的IO模型我們介紹完了,下面我們來(lái)簡(jiǎn)單介紹下在netty中是如何支持前邊提到的三種Reactor模型的。

配置單Reactor單線程

EventLoopGroupeventGroup=newNioEventLoopGroup(1);
ServerBootstrapserverBootstrap=newServerBootstrap();
serverBootstrap.group(eventGroup);

配置多Reactor線程

EventLoopGroupeventGroup=newNioEventLoopGroup();
ServerBootstrapserverBootstrap=newServerBootstrap();
serverBootstrap.group(eventGroup);

配置主從Reactor多線程

EventLoopGroupbossGroup=newNioEventLoopGroup(1);
EventLoopGroupworkerGroup=newNioEventLoopGroup();
ServerBootstrapserverBootstrap=newServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup);

總結(jié)

本文是一篇信息量比較大的文章,用了25張圖,22336個(gè)字從內(nèi)核如何處理網(wǎng)絡(luò)數(shù)據(jù)包的收發(fā)過(guò)程開(kāi)始展開(kāi),隨后又在內(nèi)核角度介紹了經(jīng)常容易混淆的阻塞與非阻塞,同步與異步的概念。以這個(gè)作為鋪墊,我們通過(guò)一個(gè)C10K的問(wèn)題,引出了五種IO模型,隨后在IO多路復(fù)用中以技術(shù)演進(jìn)的形式介紹了select,poll,epoll的原理和它們綜合的對(duì)比。最后我們介紹了兩種IO線程模型以及netty中的Reactor模型。





審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Linux系統(tǒng)
    +關(guān)注

    關(guān)注

    4

    文章

    587

    瀏覽量

    27181
  • dma
    dma
    +關(guān)注

    關(guān)注

    3

    文章

    552

    瀏覽量

    99928
  • TCP通信
    +關(guān)注

    關(guān)注

    0

    文章

    146

    瀏覽量

    4184

原文標(biāo)題:聊聊Netty那些事兒之從內(nèi)核角度看IO模型

文章出處:【微信號(hào):小林coding,微信公眾號(hào):小林coding】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Linux內(nèi)存的那些事兒

    CPU、IO、磁盤(pán)、內(nèi)存,可以說(shuō)是影響計(jì)算機(jī)性能的幾大關(guān)鍵因素。今天,我們就來(lái)探究一下內(nèi)存的那些事兒
    發(fā)表于 09-08 14:16 ?690次閱讀

    基于多路復(fù)用模型Netty框架

    Netty version: 4.1.55.Final 傳統(tǒng)的IO模型的web容器,比如老版本的Tomcat,為了增加系統(tǒng)的吞吐量,需要不斷增加系統(tǒng)核心線程數(shù)量,或者通過(guò)水平擴(kuò)展服務(wù)器數(shù)量,來(lái)增加
    的頭像 發(fā)表于 09-30 11:30 ?717次閱讀

    可綜合的RTL代碼的角度聊聊interface

    SystemVerilog引入了interface,這里我們可綜合的RTL代碼的角度聊聊interface。
    的頭像 發(fā)表于 10-12 09:06 ?1500次閱讀
    <b class='flag-5'>從</b>可綜合的RTL代碼的<b class='flag-5'>角度</b><b class='flag-5'>聊聊</b>interface

    linux下的IO模型詳解

    應(yīng)用進(jìn)程預(yù)先向內(nèi)核注冊(cè)一個(gè)信號(hào)處理函數(shù),然后用戶進(jìn)程返回,并不阻塞,當(dāng)內(nèi)核數(shù)據(jù)準(zhǔn)備就緒時(shí)會(huì)發(fā)送一個(gè)信號(hào)給進(jìn)程,用戶進(jìn)程便在信號(hào)處理函數(shù)中開(kāi)始把數(shù)據(jù)拷貝到用戶空間中  IO復(fù)用模型:顧名
    發(fā)表于 10-09 16:12

    網(wǎng)絡(luò)編程框架netty io介紹

    深入理解網(wǎng)絡(luò)編程框架netty io歡迎大家下載學(xué)習(xí)
    發(fā)表于 09-28 07:36

    電源選型的那些事兒

    電路教程相關(guān)知識(shí)的資料,關(guān)于電源選型的那些事兒
    發(fā)表于 10-10 14:34 ?0次下載

    Linux的那些事兒我是Sysfs

    Linux的那些事兒我是Sysfs
    發(fā)表于 10-29 09:28 ?5次下載
    Linux的<b class='flag-5'>那些</b><b class='flag-5'>事兒</b><b class='flag-5'>之</b>我是Sysfs

    Linux的那些事兒我是SCSI硬盤(pán)

    Linux的那些事兒我是SCSI硬盤(pán)
    發(fā)表于 10-29 09:32 ?19次下載
    Linux的<b class='flag-5'>那些</b><b class='flag-5'>事兒</b><b class='flag-5'>之</b>我是SCSI硬盤(pán)

    Linux的那些事兒我是PCI

    Linux的那些事兒我是PCI
    發(fā)表于 10-29 09:35 ?10次下載
    Linux的<b class='flag-5'>那些</b><b class='flag-5'>事兒</b><b class='flag-5'>之</b>我是PCI

    Linux的那些事兒我是Hub

    Linux的那些事兒我是Hub
    發(fā)表于 10-29 09:37 ?7次下載
    Linux的<b class='flag-5'>那些</b><b class='flag-5'>事兒</b><b class='flag-5'>之</b>我是Hub

    Linux的那些事兒我是EHCI主機(jī)控制器

    Linux的那些事兒我是EHCI主機(jī)控制器
    發(fā)表于 10-29 09:40 ?3次下載
    Linux的<b class='flag-5'>那些</b><b class='flag-5'>事兒</b><b class='flag-5'>之</b>我是EHCI主機(jī)控制器

    Linux的那些事兒我是Block層

    Linux的那些事兒我是Block層
    發(fā)表于 10-29 09:43 ?9次下載
    Linux的<b class='flag-5'>那些</b><b class='flag-5'>事兒</b><b class='flag-5'>之</b>我是Block層

    MOS管的那些事兒.課件下載

    MOS管的那些事兒.課件下載
    發(fā)表于 12-06 15:14 ?0次下載

    詳解Netty高性能異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)框架

    大家好,今天我們來(lái)聊聊Netty那些事兒,我們都知道Netty是一個(gè)高性能異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)框架。
    的頭像 發(fā)表于 03-16 10:57 ?1715次閱讀

    多路IO復(fù)用模型和異步IO模型介紹

    返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用 read 操作,將數(shù)據(jù)內(nèi)核拷貝到用戶進(jìn)程。 這個(gè)模型和阻塞 IO模型其實(shí)并沒(méi)有
    的頭像 發(fā)表于 10-08 17:21 ?677次閱讀
    多路<b class='flag-5'>IO</b>復(fù)用<b class='flag-5'>模型</b>和異步<b class='flag-5'>IO</b><b class='flag-5'>模型</b>介紹