從今天開始我們來聊聊Netty的那些事兒祝蝠,我們都知道Netty是一個高性能異步事件驅(qū)動的網(wǎng)絡(luò)框架摔桦。
它的設(shè)計異常優(yōu)雅簡潔,擴展性高富弦,穩(wěn)定性強。擁有非常詳細完整的用戶文檔竞惋。
同時內(nèi)置了很多非常有用的模塊基本上做到了開箱即用椿肩,用戶只需要編寫短短幾行代碼,就可以快速構(gòu)建出一個具有高吞吐
郑象,低延時
,更少的資源消耗
厂榛,高性能(非必要的內(nèi)存拷貝最小化)
等特征的高并發(fā)網(wǎng)絡(luò)應(yīng)用程序盖矫。
本文我們來探討下支持Netty具有高吞吐
,低延時
特征的基石----netty的網(wǎng)絡(luò)IO模型
击奶。
由Netty的網(wǎng)絡(luò)IO模型
開始辈双,我們來正式揭開本系列Netty源碼解析的序幕:
網(wǎng)絡(luò)包接收流程
- 當(dāng)
網(wǎng)絡(luò)數(shù)據(jù)幀
通過網(wǎng)絡(luò)傳輸?shù)竭_網(wǎng)卡時,網(wǎng)卡會將網(wǎng)絡(luò)數(shù)據(jù)幀通過DMA的方式
放到環(huán)形緩沖區(qū)RingBuffer
中柜砾。
RingBuffer
是網(wǎng)卡在啟動的時候分配和初始化
的環(huán)形緩沖隊列
湃望。當(dāng)RingBuffer滿
的時候,新來的數(shù)據(jù)包就會被丟棄
痰驱。我們可以通過ifconfig
命令查看網(wǎng)卡收發(fā)數(shù)據(jù)包的情況证芭。其中overruns
數(shù)據(jù)項表示當(dāng)RingBuffer滿
時,被丟棄的數(shù)據(jù)包
担映。如果發(fā)現(xiàn)出現(xiàn)丟包情況废士,可以通過ethtool命令
來增大RingBuffer長度。
- 當(dāng)
DMA操作完成
時蝇完,網(wǎng)卡會向CPU發(fā)起一個硬中斷
湃密,告訴CPU
有網(wǎng)絡(luò)數(shù)據(jù)到達诅挑。CPU調(diào)用網(wǎng)卡驅(qū)動注冊的硬中斷響應(yīng)程序
四敞。網(wǎng)卡硬中斷響應(yīng)程序會為網(wǎng)絡(luò)數(shù)據(jù)幀創(chuàng)建內(nèi)核數(shù)據(jù)結(jié)構(gòu)sk_buffer
泛源,并將網(wǎng)絡(luò)數(shù)據(jù)幀拷貝
到sk_buffer
中。然后發(fā)起軟中斷請求
忿危,通知內(nèi)核
有新的網(wǎng)絡(luò)數(shù)據(jù)幀到達达箍。
sk_buff
緩沖區(qū),是一個維護網(wǎng)絡(luò)幀結(jié)構(gòu)的雙向鏈表
铺厨,鏈表中的每一個元素都是一個網(wǎng)絡(luò)幀
缎玫。雖然 TCP/IP 協(xié)議棧分了好幾層,但上下不同層之間的傳遞解滓,實際上只需要操作這個數(shù)據(jù)結(jié)構(gòu)中的指針赃磨,而無需進行數(shù)據(jù)復(fù)制
。
- 內(nèi)核線程
ksoftirqd
發(fā)現(xiàn)有軟中斷請求到來洼裤,隨后調(diào)用網(wǎng)卡驅(qū)動注冊的poll函數(shù)
邻辉,poll函數(shù)
將sk_buffer
中的網(wǎng)絡(luò)數(shù)據(jù)包
送到內(nèi)核協(xié)議棧中注冊的ip_rcv函數(shù)
中。
每個CPU
會綁定一個ksoftirqd
內(nèi)核線程專門
用來處理軟中斷響應(yīng)
腮鞍。2個 CPU 時值骇,就會有ksoftirqd/0
和ksoftirqd/1
這兩個內(nèi)核線程。
這里有個事情需要注意下: 網(wǎng)卡接收到數(shù)據(jù)后移国,當(dāng)
DMA拷貝完成
時吱瘩,向CPU發(fā)出硬中斷
,這時哪個CPU
上響應(yīng)了這個硬中斷
迹缀,那么在網(wǎng)卡硬中斷響應(yīng)程序
中發(fā)出的軟中斷請求
也會在這個CPU綁定的ksoftirqd線程
中響應(yīng)使碾。所以如果發(fā)現(xiàn)Linux軟中斷,CPU消耗都集中在一個核上
的話祝懂,那么就需要調(diào)整硬中斷的CPU親和性
票摇,來將硬中斷打散
到不通的CPU核
上去。
- 在
ip_rcv函數(shù)
中也就是上圖中的網(wǎng)絡(luò)層
嫂易,取出
數(shù)據(jù)包的IP頭
兄朋,判斷該數(shù)據(jù)包下一跳的走向,如果數(shù)據(jù)包是發(fā)送給本機的怜械,則取出傳輸層的協(xié)議類型(TCP
或者UDP
)颅和,并去掉
數(shù)據(jù)包的IP頭
,將數(shù)據(jù)包交給上圖中得傳輸層
處理缕允。
傳輸層的處理函數(shù):
TCP協(xié)議
對應(yīng)內(nèi)核協(xié)議棧中注冊的tcp_rcv函數(shù)
峡扩,UDP協(xié)議
對應(yīng)內(nèi)核協(xié)議棧中注冊的udp_rcv函數(shù)
。
當(dāng)我們采用的是
TCP協(xié)議
時障本,數(shù)據(jù)包到達傳輸層時教届,會在內(nèi)核協(xié)議棧中的tcp_rcv函數(shù)
處理响鹃,在tcp_rcv函數(shù)中去掉
TCP頭,根據(jù)四元組(源IP案训,源端口买置,目的IP,目的端口)
查找對應(yīng)的Socket
强霎,如果找到對應(yīng)的Socket則將網(wǎng)絡(luò)數(shù)據(jù)包中的傳輸數(shù)據(jù)拷貝到Socket
中的接收緩沖區(qū)
中忿项。如果沒有找到,則發(fā)送一個目標(biāo)不可達
的icmp
包城舞。內(nèi)核在接收網(wǎng)絡(luò)數(shù)據(jù)包時所做的工作我們就介紹完了轩触,現(xiàn)在我們把視角放到應(yīng)用層,當(dāng)我們程序通過系統(tǒng)調(diào)用
read
讀取Socket接收緩沖區(qū)
中的數(shù)據(jù)時家夺,如果接收緩沖區(qū)中沒有數(shù)據(jù)
脱柱,那么應(yīng)用程序就會在系統(tǒng)調(diào)用上阻塞
,直到Socket接收緩沖區(qū)有數(shù)據(jù)
拉馋,然后CPU
將內(nèi)核空間
(Socket接收緩沖區(qū))的數(shù)據(jù)拷貝
到用戶空間
榨为,最后系統(tǒng)調(diào)用read返回
,應(yīng)用程序讀取
數(shù)據(jù)椅邓。
性能開銷
從內(nèi)核處理網(wǎng)絡(luò)數(shù)據(jù)包接收的整個過程來看柠逞,內(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)用返回
時從內(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ā)送流程
當(dāng)我們在應(yīng)用程序中調(diào)用
send
系統(tǒng)調(diào)用發(fā)送數(shù)據(jù)時,由于是系統(tǒng)調(diào)用所以線程會發(fā)生一次用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換,在內(nèi)核中首先根據(jù)fd
將真正的Socket找出浩蓉,這個Socket對象中記錄著各種協(xié)議棧的函數(shù)地址庆猫,然后構(gòu)造struct msghdr
對象昨稼,將用戶需要發(fā)送的數(shù)據(jù)全部封裝在這個struct msghdr
結(jié)構(gòu)體中霍掺。調(diào)用內(nèi)核協(xié)議棧函數(shù)
inet_sendmsg
,發(fā)送流程進入內(nèi)核協(xié)議棧處理。在進入到內(nèi)核協(xié)議棧之后,內(nèi)核會找到Socket上的具體協(xié)議的發(fā)送函數(shù)。
比如:我們使用的是
TCP協(xié)議
页滚,對應(yīng)的TCP協(xié)議
發(fā)送函數(shù)是tcp_sendmsg
,如果是UDP協(xié)議
的話宴卖,對應(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ā)送隊列中的隊尾元素成翩,將新創(chuàng)建的sk_buffer
添加到Socket
發(fā)送隊列的尾部。
Socket
的發(fā)送隊列是由sk_buffer
組成的一個雙向鏈表
。
發(fā)送流程走到這里,用戶要發(fā)送的數(shù)據(jù)總算是從
用戶空間
拷貝到了內(nèi)核
中扛禽,這時雖然發(fā)送數(shù)據(jù)已經(jīng)拷貝
到了內(nèi)核Socket
中的發(fā)送隊列
中,但并不代表內(nèi)核會開始發(fā)送,因為TCP協(xié)議
的流量控制
和擁塞控制
,用戶要發(fā)送的數(shù)據(jù)包并不一定
會立馬被發(fā)送出去,需要符合TCP協(xié)議
的發(fā)送條件拟赊。如果沒有達到發(fā)送條件
桃移,那么本次send
系統(tǒng)調(diào)用就會直接返回蔗衡。
如果符合發(fā)送條件,則開始調(diào)用
tcp_write_xmit
內(nèi)核函數(shù)王滤。在這個函數(shù)中,會循環(huán)獲取Socket
發(fā)送隊列中待發(fā)送的sk_buffer
,然后進行擁塞控制
以及滑動窗口的管理
。將從
Socket
發(fā)送隊列中獲取到的sk_buffer
重新拷貝一份
,設(shè)置sk_buffer副本
中的TCP HEADER
区端。
sk_buffer
內(nèi)部其實包含了網(wǎng)絡(luò)協(xié)議中所有的header
沥邻。在設(shè)置TCP HEADER
的時候邮利,只是把指針指向sk_buffer
的合適位置祷愉。后面再設(shè)置IP HEADER
的時候订讼,在把指針移動一下就行鳖敷,避免頻繁的內(nèi)存申請和拷貝,效率很高恤浪。
為什么不直接使用
Socket
發(fā)送隊列中的sk_buffer
而是需要拷貝一份呢砂客?
因為TCP協(xié)議
是支持丟包重傳
的齿诉,在沒有收到對端的ACK
之前挥唠,這個sk_buffer
是不能刪除的。內(nèi)核每次調(diào)用網(wǎng)卡發(fā)送數(shù)據(jù)的時候,實際上傳遞的是sk_buffer
的拷貝副本
,當(dāng)網(wǎng)卡把數(shù)據(jù)發(fā)送出去后贝奇,sk_buffer
拷貝副本會被釋放菠赚。當(dāng)收到對端的ACK
之后拌牲,Socket
發(fā)送隊列中的sk_buffer
才會被真正刪除土居。
-
當(dāng)設(shè)置完
TCP頭
后眷蜓,內(nèi)核協(xié)議棧傳輸層
的事情就做完了汽纤,下面通過調(diào)用ip_queue_xmit
內(nèi)核函數(shù),正式來到內(nèi)核協(xié)議棧網(wǎng)絡(luò)層
的處理。- 檢查
Socket
中是否有緩存路由表明郭,如果沒有的話亏推,則查找路由項芽狗,并緩存到Socket
中柔昼。接著在把路由表設(shè)置到sk_buffer
中。
通過
route
命令可以查看本機路由配置盟榴。將
sk_buffer
中的指針移動到IP頭
位置上宅静,設(shè)置IP頭
峭沦。執(zhí)行
netfilters
過濾蛉抓。過濾通過之后,如果數(shù)據(jù)大于MTU
的話翻屈,則執(zhí)行分片厘贼。
如果你使用
iptables
配置了一些規(guī)則凭疮,那么這里將檢測是否命中
規(guī)則逝淹。 如果你設(shè)置了非常復(fù)雜的 netfilter 規(guī)則
欣簇,在這個函數(shù)里將會導(dǎo)致你的線程CPU 開銷
會極大增加
。 - 檢查
內(nèi)核協(xié)議棧
網(wǎng)絡(luò)層
的事情處理完后堕花,現(xiàn)在發(fā)送流程進入了到了鄰居子系統(tǒng)
杠袱,鄰居子系統(tǒng)
位于內(nèi)核協(xié)議棧中的網(wǎng)絡(luò)層
和網(wǎng)絡(luò)接口層
之間,用于發(fā)送ARP請求
獲取MAC地址
援奢,然后將sk_buffer
中的指針移動到MAC頭
位置兼犯,填充MAC頭
。-
經(jīng)過
鄰居子系統(tǒng)
的處理切黔,現(xiàn)在sk_buffer
中已經(jīng)封裝了一個完整的數(shù)據(jù)幀
,隨后內(nèi)核將sk_buffer
交給網(wǎng)絡(luò)設(shè)備子系統(tǒng)
進行處理具篇。網(wǎng)絡(luò)設(shè)備子系統(tǒng)
主要做以下幾項事情:- 選擇發(fā)送隊列(
RingBuffer
)纬霞。因為網(wǎng)卡擁有多個發(fā)送隊列,所以在發(fā)送前需要選擇一個發(fā)送隊列驱显。 - 將
sk_buffer
添加到發(fā)送隊列中诗芜。 - 循環(huán)從發(fā)送隊列(
RingBuffer
)中取出sk_buffer
,調(diào)用內(nèi)核函數(shù)sch_direct_xmit
發(fā)送數(shù)據(jù)埃疫,其中會調(diào)用網(wǎng)卡驅(qū)動程序
來發(fā)送數(shù)據(jù)伏恐。
- 選擇發(fā)送隊列(
以上過程全部是用戶線程的內(nèi)核態(tài)在執(zhí)行,占用的CPU時間是系統(tǒng)態(tài)時間(
sy
)栓霜,當(dāng)分配給用戶線程的CPU quota
用完的時候翠桦,會觸發(fā)NET_TX_SOFTIRQ
類型的軟中斷,內(nèi)核線程ksoftirqd
會響應(yīng)這個軟中斷叙淌,并執(zhí)行NET_TX_SOFTIRQ
類型的軟中斷注冊的回調(diào)函數(shù)net_tx_action
秤掌,在回調(diào)函數(shù)中會執(zhí)行到驅(qū)動程序函數(shù)dev_hard_start_xmit
來發(fā)送數(shù)據(jù)。
注意:當(dāng)觸發(fā)
NET_TX_SOFTIRQ
軟中斷來發(fā)送數(shù)據(jù)時鹰霍,后邊消耗的 CPU 就都顯示在si
這里了闻鉴,不會消耗用戶進程的系統(tǒng)態(tài)時間(sy
)了。
從這里可以看到網(wǎng)絡(luò)包的發(fā)送過程和接受過程是不同的茂洒,在介紹網(wǎng)絡(luò)包的接受過程時孟岛,我們提到是通過觸發(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
用盡時渠羞,才觸發(fā)NET_TX_SOFTIRQ
軟中斷來發(fā)送數(shù)據(jù)。
在整個網(wǎng)絡(luò)包的發(fā)送和接受過程中智哀,
NET_TX_SOFTIRQ
類型的軟中斷只會在發(fā)送網(wǎng)絡(luò)包時并且當(dāng)用戶線程的CPU quota
用盡時次询,才會觸發(fā)。剩下的接受過程中觸發(fā)的軟中斷類型以及發(fā)送完數(shù)據(jù)觸發(fā)的軟中斷類型均為NET_RX_SOFTIRQ
瓷叫。
所以這就是你在服務(wù)器上查看/proc/softirqs
屯吊,一般NET_RX
都要比NET_TX
大很多的的原因送巡。
現(xiàn)在發(fā)送流程終于到了網(wǎng)卡真實發(fā)送數(shù)據(jù)的階段,前邊我們講到無論是用戶線程的內(nèi)核態(tài)還是觸發(fā)
NET_TX_SOFTIRQ
類型的軟中斷在發(fā)送數(shù)據(jù)的時候最終會調(diào)用到網(wǎng)卡的驅(qū)動程序函數(shù)dev_hard_start_xmit
來發(fā)送數(shù)據(jù)盒卸。在網(wǎng)卡驅(qū)動程序函數(shù)dev_hard_start_xmit
中會將sk_buffer
映射到網(wǎng)卡可訪問的內(nèi)存 DMA 區(qū)域
骗爆,最終網(wǎng)卡驅(qū)動程序通過DMA
的方式將數(shù)據(jù)幀
通過物理網(wǎng)卡發(fā)送出去。當(dāng)數(shù)據(jù)發(fā)送完畢后蔽介,還有最后一項重要的工作摘投,就是清理工作。數(shù)據(jù)發(fā)送完畢后虹蓄,網(wǎng)卡設(shè)備會向
CPU
發(fā)送一個硬中斷犀呼,CPU
調(diào)用網(wǎng)卡驅(qū)動程序注冊的硬中斷響應(yīng)程序
,在硬中斷響應(yīng)中觸發(fā)NET_RX_SOFTIRQ
類型的軟中斷武花,在軟中斷的回調(diào)函數(shù)igb_poll
中清理釋放sk_buffer
圆凰,清理網(wǎng)卡
發(fā)送隊列(RingBuffer
),解除 DMA 映射体箕。
無論
硬中斷
是因為有數(shù)據(jù)要接收
专钉,還是說發(fā)送完成通知
,從硬中斷觸發(fā)的軟中斷都是NET_RX_SOFTIRQ
累铅。
這里釋放清理的只是
sk_buffer
的副本跃须,真正的sk_buffer
現(xiàn)在還是存放在Socket
的發(fā)送隊列中。前面在傳輸層
處理的時候我們提到過娃兽,因為傳輸層需要保證可靠性
菇民,所以sk_buffer
其實還沒有刪除。它得等收到對方的 ACK 之后才會真正刪除投储。
性能開銷
前邊我們提到了在網(wǎng)絡(luò)包接收過程中涉及到的性能開銷第练,現(xiàn)在介紹完了網(wǎng)絡(luò)包的發(fā)送過程,我們來看下在數(shù)據(jù)包發(fā)送過程中的性能開銷:
和接收數(shù)據(jù)一樣玛荞,應(yīng)用程序在調(diào)用
系統(tǒng)調(diào)用send
的時候會從用戶態(tài)
轉(zhuǎn)為內(nèi)核態(tài)
以及發(fā)送完數(shù)據(jù)后娇掏,系統(tǒng)調(diào)用
返回時從內(nèi)核態(tài)
轉(zhuǎn)為用戶態(tài)
的開銷。用戶線程內(nèi)核態(tài)
CPU quota
用盡時觸發(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)存清理動作。內(nèi)核響應(yīng)軟中斷的開銷讶坯。-
內(nèi)存拷貝的開銷番电。我們來回顧下在數(shù)據(jù)包發(fā)送的過程中都發(fā)生了哪些內(nèi)存拷貝:
- 在內(nèi)核協(xié)議棧的傳輸層中,
TCP協(xié)議
對應(yīng)的發(fā)送函數(shù)tcp_sendmsg
會申請sk_buffer
辆琅,將用戶要發(fā)送的數(shù)據(jù)拷貝
到sk_buffer
中。 - 在發(fā)送流程從傳輸層到網(wǎng)絡(luò)層的時候俭识,會
拷貝
一個sk_buffer副本
出來,將這個sk_buffer副本
向下傳遞。原始sk_buffer
保留在Socket
發(fā)送隊列中卒暂,等待網(wǎng)絡(luò)對端ACK
,對端ACK
后刪除Socket
發(fā)送隊列中的sk_buffer
寄症。對端沒有發(fā)送ACK
绣硝,則重新從Socket
發(fā)送隊列中發(fā)送,實現(xiàn)TCP協(xié)議
的可靠傳輸叔营。 - 在網(wǎng)絡(luò)層屋彪,如果發(fā)現(xiàn)要發(fā)送的數(shù)據(jù)大于
MTU
,則會進行分片操作绒尊,申請額外的sk_buffer
畜挥,并將原來的sk_buffer拷貝
到多個小的sk_buffer中。
- 在內(nèi)核協(xié)議棧的傳輸層中,
再談(阻塞婴谱,非阻塞)與(同步蟹但,異步)
在我們聊完網(wǎng)絡(luò)數(shù)據(jù)的接收和發(fā)送過程后,我們來談下IO中特別容易混淆的概念:阻塞與同步
谭羔,非阻塞與異步
华糖。
網(wǎng)上各種博文還有各種書籍中有大量的關(guān)于這兩個概念的解釋,但是筆者覺得還是不夠形象化瘟裸,只是對概念的生硬解釋客叉,如果硬套概念的話,其實感覺阻塞與同步
话告,非阻塞與異步
還是沒啥區(qū)別兼搏,時間長了,還是比較模糊容易混淆沙郭。
所以筆者在這里嘗試換一種更加形象化佛呻,更加容易理解記憶的方式來清晰地解釋下什么是阻塞與非阻塞
,什么是同步與異步
棠绘。
經(jīng)過前邊對網(wǎng)絡(luò)數(shù)據(jù)包接收流程的介紹件相,在這里我們可以將整個流程總結(jié)為兩個階段:
數(shù)據(jù)準備階段: 在這個階段,網(wǎng)絡(luò)數(shù)據(jù)包到達網(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ù)到達
內(nèi)核Socket
的接收緩沖區(qū)中時赡突,此時數(shù)據(jù)存在于內(nèi)核空間
中对扶,需要將數(shù)據(jù)拷貝
到用戶空間
中区赵,才能夠被應(yīng)用程序讀取。
阻塞與非阻塞
阻塞與非阻塞的區(qū)別主要發(fā)生在第一階段:數(shù)據(jù)準備階段
浪南。
當(dāng)應(yīng)用程序發(fā)起系統(tǒng)調(diào)用read
時笼才,線程從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài),讀取內(nèi)核Socket
的接收緩沖區(qū)中的網(wǎng)絡(luò)數(shù)據(jù)络凿。
阻塞
如果這時內(nèi)核Socket
的接收緩沖區(qū)沒有數(shù)據(jù)骡送,那么線程就會一直等待
,直到Socket
接收緩沖區(qū)有數(shù)據(jù)為止絮记。隨后將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間摔踱,系統(tǒng)調(diào)用read
返回。
從圖中我們可以看出:阻塞的特點是在第一階段和第二階段都會等待
怨愤。
非阻塞
阻塞
和非阻塞
主要的區(qū)分是在第一階段:數(shù)據(jù)準備階段
派敷。
在第一階段,當(dāng)
Socket
的接收緩沖區(qū)中沒有數(shù)據(jù)的時候撰洗,阻塞模式下
應(yīng)用線程會一直等待篮愉。非阻塞模式下
應(yīng)用線程不會等待,系統(tǒng)調(diào)用
直接返回錯誤標(biāo)志EWOULDBLOCK
了赵。當(dāng)
Socket
的接收緩沖區(qū)中有數(shù)據(jù)的時候潜支,阻塞
和非阻塞
的表現(xiàn)是一樣的,都會進入第二階段等待
數(shù)據(jù)從內(nèi)核空間
拷貝到用戶空間
柿汛,然后系統(tǒng)調(diào)用返回
冗酿。
從上圖中,我們可以看出:非阻塞的特點是第一階段不會等待
络断,但是在第二階段還是會等待
裁替。
同步與異步
同步
與異步
主要的區(qū)別發(fā)生在第二階段:數(shù)據(jù)拷貝階段
。
前邊我們提到在數(shù)據(jù)拷貝階段
主要是將數(shù)據(jù)從內(nèi)核空間
拷貝到用戶空間
貌笨。然后應(yīng)用程序才可以讀取數(shù)據(jù)弱判。
當(dāng)內(nèi)核Socket
的接收緩沖區(qū)有數(shù)據(jù)到達時,進入第二階段锥惋。
同步
同步模式
在數(shù)據(jù)準備好后昌腰,是由用戶線程
的內(nèi)核態(tài)
來執(zhí)行第二階段
。所以應(yīng)用程序會在第二階段發(fā)生阻塞
膀跌,直到數(shù)據(jù)從內(nèi)核空間
拷貝到用戶空間
遭商,系統(tǒng)調(diào)用才會返回。
Linux下的 epoll
和Mac 下的 kqueue
都屬于同步 IO
捅伤。
異步
異步模式
下是由內(nèi)核
來執(zhí)行第二階段的數(shù)據(jù)拷貝操作劫流,當(dāng)內(nèi)核
執(zhí)行完第二階段,會通知用戶線程IO操作已經(jīng)完成,并將數(shù)據(jù)回調(diào)給用戶線程祠汇。所以在異步模式
下 數(shù)據(jù)準備階段
和數(shù)據(jù)拷貝階段
均是由內(nèi)核
來完成仍秤,不會對應(yīng)用程序造成任何阻塞。
基于以上特征可很,我們可以看到異步模式
需要內(nèi)核的支持诗力,比較依賴操作系統(tǒng)底層的支持。
在目前流行的操作系統(tǒng)中我抠,只有Windows 中的 IOCP
才真正屬于異步 IO姜骡,實現(xiàn)的也非常成熟。但Windows很少用來作為服務(wù)器使用屿良。
而常用來作為服務(wù)器使用的Linux,異步IO機制
實現(xiàn)的不夠成熟惫周,與NIO相比性能提升的也不夠明顯尘惧。
但Linux kernel 在5.1版本由Facebook的大神Jens Axboe引入了新的異步IO庫io_uring
改善了原來Linux native AIO的一些性能問題。性能相比Epoll
以及之前原生的AIO
提高了不少递递,值得關(guān)注喷橙。
IO模型
在進行網(wǎng)絡(luò)IO操作時,用什么樣的IO模型來讀寫數(shù)據(jù)將在很大程度上決定了網(wǎng)絡(luò)框架的IO性能登舞。所以IO模型的選擇是構(gòu)建一個高性能網(wǎng)絡(luò)框架的基礎(chǔ)贰逾。
在《UNIX 網(wǎng)絡(luò)編程》一書中介紹了五種IO模型:阻塞IO
,非阻塞IO
,IO多路復(fù)用
,信號驅(qū)動IO
,異步IO
,每一種IO模型的出現(xiàn)都是對前一種的升級優(yōu)化菠秒。
下面我們就來分別介紹下這五種IO模型各自都解決了什么問題疙剑,適用于哪些場景,各自的優(yōu)缺點是什么践叠?
阻塞IO(BIO)
經(jīng)過前一小節(jié)對阻塞
這個概念的介紹言缤,相信大家可以很容易理解阻塞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悴务,進入阻塞狀態(tài)
。當(dāng)數(shù)據(jù)到達Socket
接收緩沖區(qū)后,內(nèi)核喚醒阻塞狀態(tài)
中的用戶線程進入就緒狀態(tài)
讯檐,隨后經(jīng)過CPU的調(diào)度獲取到CPU quota
進入運行狀態(tài)
羡疗,將內(nèi)核空間的數(shù)據(jù)拷貝到用戶空間,隨后系統(tǒng)調(diào)用返回别洪。
阻塞寫
當(dāng)用戶線程發(fā)起send
系統(tǒng)調(diào)用時叨恨,用戶線程從用戶態(tài)切換到內(nèi)核態(tài),將發(fā)送數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間中的Socket
發(fā)送緩沖區(qū)中挖垛。
當(dāng)
Socket
發(fā)送緩沖區(qū)能夠容納下發(fā)送數(shù)據(jù)時痒钝,用戶線程會將全部的發(fā)送數(shù)據(jù)寫入Socket
緩沖區(qū),然后執(zhí)行在《網(wǎng)絡(luò)包發(fā)送流程》這小節(jié)介紹的后續(xù)流程痢毒,然后返回送矩。當(dāng)
Socket
發(fā)送緩沖區(qū)空間不夠,無法容納下全部發(fā)送數(shù)據(jù)時哪替,用戶線程讓出CPU,進入阻塞狀態(tài)
栋荸,直到Socket
發(fā)送緩沖區(qū)能夠容納下全部發(fā)送數(shù)據(jù)時,內(nèi)核喚醒用戶線程凭舶,執(zhí)行后續(xù)發(fā)送流程晌块。
阻塞IO
模型下的寫操作做事風(fēng)格比較硬剛,非得要把全部的發(fā)送數(shù)據(jù)寫入發(fā)送緩沖區(qū)才肯善罷甘休帅霜。
阻塞IO模型
由于阻塞IO
的讀寫特點匆背,所以導(dǎo)致在阻塞IO
模型下,每個請求都需要被一個獨立的線程處理身冀。一個線程在同一時刻只能與一個連接綁定钝尸。來一個請求,服務(wù)端就需要創(chuàng)建一個線程用來處理請求搂根。
當(dāng)客戶端請求的并發(fā)量突然增大時蝶怔,服務(wù)端在一瞬間就會創(chuàng)建出大量的線程,而創(chuàng)建線程是需要系統(tǒng)資源開銷的兄墅,這樣一來就會一瞬間占用大量的系統(tǒng)資源踢星。
如果客戶端創(chuàng)建好連接后,但是一直不發(fā)數(shù)據(jù)隙咸,通常大部分情況下沐悦,網(wǎng)絡(luò)連接也并不
總是有數(shù)據(jù)可讀包归,那么在空閑的這段時間內(nèi)哄褒,服務(wù)端線程就會一直處于阻塞狀態(tài)
,無法干其他的事情敲茄。CPU也無法得到充分的發(fā)揮
充包,同時還會導(dǎo)致大量線程切換的開銷
副签。
適用場景
基于以上阻塞IO模型
的特點遥椿,該模型只適用于連接數(shù)少
,并發(fā)度低
的業(yè)務(wù)場景淆储。
比如公司內(nèi)部的一些管理系統(tǒng)冠场,通常請求數(shù)在100個左右,使用阻塞IO模型
還是非常適合的本砰。而且性能還不輸NIO碴裙。
該模型在C10K之前,是普遍被采用的一種IO模型点额。
非阻塞IO(NIO)
阻塞IO模型
最大的問題就是一個線程只能處理一個連接舔株,如果這個連接上沒有數(shù)據(jù)的話,那么這個線程就只能阻塞在系統(tǒng)IO調(diào)用上还棱,不能干其他的事情载慈。這對系統(tǒng)資源來說,是一種極大的浪費珍手。同時大量的線程上下文切換娃肿,也是一個巨大的系統(tǒng)開銷。
所以為了解決這個問題珠十,我們就需要用盡可能少的線程去處理更多的連接。凭豪,網(wǎng)絡(luò)IO模型的演變
也是根據(jù)這個需求來一步一步演進的焙蹭。
基于這個需求,第一種解決方案非阻塞IO
就出現(xiàn)了嫂伞。我們在上一小節(jié)中介紹了非阻塞
的概念孔厉,現(xiàn)在我們來看下網(wǎng)絡(luò)讀寫操作在非阻塞IO
下的特點:
非阻塞讀
當(dāng)用戶線程發(fā)起非阻塞read
系統(tǒng)調(diào)用時,用戶線程從用戶態(tài)
轉(zhuǎn)為內(nèi)核態(tài)
帖努,在內(nèi)核中去查看Socket
接收緩沖區(qū)是否有數(shù)據(jù)到來撰豺。
Socket
接收緩沖區(qū)中無數(shù)據(jù)
,系統(tǒng)調(diào)用立馬返回拼余,并帶有一個EWOULDBLOCK
或EAGAIN
錯誤污桦,這個階段用戶線程不會阻塞
,也不會讓出CPU
匙监,而是會繼續(xù)輪訓(xùn)
直到Socket
接收緩沖區(qū)中有數(shù)據(jù)為止凡橱。Socket
接收緩沖區(qū)中有數(shù)據(jù)
,用戶線程在內(nèi)核態(tài)
會將內(nèi)核空間
中的數(shù)據(jù)拷貝到用戶空間
亭姥,注意這個數(shù)據(jù)拷貝階段稼钩,應(yīng)用程序是阻塞的
,當(dāng)數(shù)據(jù)拷貝完成达罗,系統(tǒng)調(diào)用返回坝撑。
非阻塞寫
前邊我們在介紹阻塞寫
的時候提到阻塞寫
的風(fēng)格特別的硬朗,頭比較鐵非要把全部發(fā)送數(shù)據(jù)一次性都寫到Socket
的發(fā)送緩沖區(qū)中才返回,如果發(fā)送緩沖區(qū)中沒有足夠的空間容納巡李,那么就一直阻塞死等抚笔,特別的剛。
相比較而言非阻塞寫
的特點就比較佛系击儡,當(dāng)發(fā)送緩沖區(qū)中沒有足夠的空間容納全部發(fā)送數(shù)據(jù)時塔沃,非阻塞寫
的特點是能寫多少寫多少
,寫不下了阳谍,就立即返回蛀柴。并將寫入到發(fā)送緩沖區(qū)的字節(jié)數(shù)返回給應(yīng)用程序,方便用戶線程不斷的輪訓(xùn)
嘗試將剩下的數(shù)據(jù)
寫入發(fā)送緩沖區(qū)中矫夯。
非阻塞IO模型
基于以上非阻塞IO
的特點鸽疾,我們就不必像阻塞IO
那樣為每個請求分配一個線程去處理連接上的讀寫了。
我們可以利用一個線程或者很少的線程训貌,去不斷地輪詢
每個Socket
的接收緩沖區(qū)是否有數(shù)據(jù)到達制肮,如果沒有數(shù)據(jù),不必阻塞
線程递沪,而是接著去輪詢
下一個Socket
接收緩沖區(qū)豺鼻,直到輪詢到數(shù)據(jù)后,處理連接上的讀寫款慨,或者交給業(yè)務(wù)線程池去處理儒飒,輪詢線程則繼續(xù)輪詢
其他的Socket
接收緩沖區(qū)。
這樣一個非阻塞IO模型
就實現(xiàn)了我們在本小節(jié)開始提出的需求:我們需要用盡可能少的線程去處理更多的連接
適用場景
雖然非阻塞IO模型
與阻塞IO模型
相比檩奠,減少了很大一部分的資源消耗和系統(tǒng)開銷桩了。
但是它仍然有很大的性能問題,因為在非阻塞IO模型
下埠戳,需要用戶線程去不斷地
發(fā)起系統(tǒng)調(diào)用
去輪訓(xùn)Socket
接收緩沖區(qū)井誉,這就需要用戶線程不斷地從用戶態(tài)
切換到內(nèi)核態(tài)
,內(nèi)核態(tài)
切換到用戶態(tài)
整胃。隨著并發(fā)量的增大颗圣,這個上下文切換的開銷也是巨大的。
所以單純的非阻塞IO
模型還是無法適用于高并發(fā)的場景屁使。只能適用于C10K
以下的場景欠啤。
IO多路復(fù)用
在非阻塞IO
這一小節(jié)的開頭,我們提到網(wǎng)絡(luò)IO模型
的演變都是圍繞著---如何用盡可能少的線程去處理更多的連接這個核心需求開始展開的屋灌。
本小節(jié)我們來談?wù)?code>IO多路復(fù)用模型洁段,那么什么是多路
?共郭,什么又是復(fù)用
呢祠丝?
我們還是以這個核心需求來對這兩個概念展開闡述:
多路:我們的核心需求是要用盡可能少的線程來處理盡可能多的連接疾呻,這里的
多路
指的就是我們需要處理的眾多連接。復(fù)用:核心需求要求我們使用
盡可能少的線程
写半,盡可能少的系統(tǒng)開銷
去處理盡可能多
的連接(多路
)岸蜗,那么這里的復(fù)用
指的就是用有限的資源
,比如用一個線程或者固定數(shù)量的線程去處理眾多連接上的讀寫事件叠蝇。換句話說璃岳,在阻塞IO模型
中一個連接就需要分配一個獨立的線程去專門處理這個連接上的讀寫,到了IO多路復(fù)用模型
中悔捶,多個連接可以復(fù)用
這一個獨立的線程去處理這多個連接上的讀寫铃慷。
好了,IO多路復(fù)用模型
的概念解釋清楚了蜕该,那么問題的關(guān)鍵是我們?nèi)绾稳崿F(xiàn)這個復(fù)用
犁柜,也就是如何讓一個獨立的線程去處理眾多連接上的讀寫事件呢?
這個問題其實在非阻塞IO模型
中已經(jīng)給出了它的答案堂淡,在非阻塞IO模型
中馋缅,利用非阻塞
的系統(tǒng)IO調(diào)用去不斷的輪詢眾多連接的Socket
接收緩沖區(qū)看是否有數(shù)據(jù)到來,如果有則處理绢淀,如果沒有則繼續(xù)輪詢下一個Socket
萤悴。這樣就達到了用一個線程去處理眾多連接上的讀寫事件了。
但是非阻塞IO模型
最大的問題就是需要不斷的發(fā)起系統(tǒng)調(diào)用
去輪詢各個Socket
中的接收緩沖區(qū)是否有數(shù)據(jù)到來皆的,頻繁
的系統(tǒng)調(diào)用
隨之帶來了大量的上下文切換開銷覆履。隨著并發(fā)量的提升,這樣也會導(dǎo)致非常嚴重的性能問題祭务。
那么如何避免頻繁的系統(tǒng)調(diào)用同時又可以實現(xiàn)我們的核心需求呢?
這就需要操作系統(tǒng)的內(nèi)核來支持這樣的操作怪嫌,我們可以把頻繁的輪詢操作交給操作系統(tǒng)內(nèi)核來替我們完成义锥,這樣就避免了在用戶空間
頻繁的去使用系統(tǒng)調(diào)用來輪詢所帶來的性能開銷。
正如我們所想岩灭,操作系統(tǒng)內(nèi)核也確實為我們提供了這樣的功能實現(xiàn)拌倍,下面我們來一起看下操作系統(tǒng)對IO多路復(fù)用模型
的實現(xiàn)。
select
select
是操作系統(tǒng)內(nèi)核提供給我們使用的一個系統(tǒng)調(diào)用
噪径,它解決了在非阻塞IO模型
中需要不斷的發(fā)起系統(tǒng)IO調(diào)用
去輪詢各個連接上的Socket
接收緩沖區(qū)所帶來的用戶空間
與內(nèi)核空間
不斷切換的系統(tǒng)開銷
柱恤。
select
系統(tǒng)調(diào)用將輪詢
的操作交給了內(nèi)核
來幫助我們完成,從而避免了在用戶空間
不斷的發(fā)起輪詢所帶來的的系統(tǒng)性能開銷找爱。
首先用戶線程在發(fā)起
select
系統(tǒng)調(diào)用的時候會阻塞
在select
系統(tǒng)調(diào)用上梗顺。此時,用戶線程從用戶態(tài)
切換到了內(nèi)核態(tài)
完成了一次上下文切換
用戶線程將需要監(jiān)聽的
Socket
對應(yīng)的文件描述符fd
數(shù)組通過select
系統(tǒng)調(diào)用傳遞給內(nèi)核车摄。此時寺谤,用戶線程將用戶空間
中的文件描述符fd
數(shù)組拷貝
到內(nèi)核空間
仑鸥。
這里的文件描述符數(shù)組其實是一個BitMap
,BitMap
下標(biāo)為文件描述符fd
变屁,下標(biāo)對應(yīng)的值為:1
表示該fd
上有讀寫事件眼俊,0
表示該fd
上沒有讀寫事件。
文件描述符fd其實就是一個整數(shù)值
粟关,在Linux中一切皆文件疮胖,Socket
也是一個文件。描述進程所有信息的數(shù)據(jù)結(jié)構(gòu)task_struct
中有一個屬性struct files_struct *files
闷板,它最終指向了一個數(shù)組澎灸,數(shù)組里存放了進程打開的所有文件列表,文件信息封裝在struct file
結(jié)構(gòu)體中蛔垢,這個數(shù)組存放的類型就是
struct file
結(jié)構(gòu)體击孩,數(shù)組的下標(biāo)
則是我們常說的文件描述符fd
。
- 當(dāng)用戶線程調(diào)用完
select
后開始進入阻塞狀態(tài)
鹏漆,內(nèi)核
開始輪詢遍歷fd
數(shù)組巩梢,查看fd
對應(yīng)的Socket
接收緩沖區(qū)中是否有數(shù)據(jù)到來。如果有數(shù)據(jù)到來艺玲,則將fd
對應(yīng)BitMap
的值設(shè)置為1
括蝠。如果沒有數(shù)據(jù)到來,則保持值為0
饭聚。
注意這里內(nèi)核會修改原始的
fd
數(shù)組<删!
內(nèi)核遍歷一遍
fd
數(shù)組后秒梳,如果發(fā)現(xiàn)有些fd
上有IO數(shù)據(jù)到來法绵,則將修改后的fd
數(shù)組返回給用戶線程。此時酪碘,會將fd
數(shù)組從內(nèi)核空間
拷貝到用戶空間
朋譬。當(dāng)內(nèi)核將修改后的
fd
數(shù)組返回給用戶線程后,用戶線程解除阻塞
兴垦,由用戶線程開始遍歷fd
數(shù)組然后找出fd
數(shù)組中值為1
的Socket
文件描述符徙赢。最后對這些Socket
發(fā)起系統(tǒng)調(diào)用讀取數(shù)據(jù)。
select
不會告訴用戶線程具體哪些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
了哥倔。
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
從select API
中我們可以看到肛捍,select
系統(tǒng)調(diào)用是在規(guī)定的超時時間內(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:
對可讀事件
感興趣的文件描述符集合。fd_set *writeset:
對可寫事件
感興趣的文件描述符集合蒂破。fd_set *exceptset:
對可寫事件
感興趣的文件描述符集合馏谨。
這里的
fd_set
就是我們前邊提到的文件描述符數(shù)組
,是一個BitMap
結(jié)構(gòu)附迷。
-
const struct timeval *timeout:
select系統(tǒng)調(diào)用超時時間惧互,在這段時間內(nèi),內(nèi)核如果沒有發(fā)現(xiàn)有IO就緒
的文件描述符喇伯,就直接返回喊儡。
上小節(jié)提到,在內(nèi)核
遍歷完fd
數(shù)組后稻据,發(fā)現(xiàn)有IO就緒
的fd
艾猜,則會將該fd
對應(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):
將一個給定的文件描述符加入集合之中查牌。
每次調(diào)用
select
之前都要通過FD_ZERO
和FD_SET
重新設(shè)置文件描述符事期,因為文件描述符集合會在內(nèi)核
中被修改
。
int FD_ISSET(int fd, fd_set *fdset):
檢查集合中指定的文件描述符是否可以讀寫纸颜。用戶線程遍歷
文件描述符集合,調(diào)用該方法檢查相應(yīng)的文件描述符是否IO就緒
兽泣。void FD_CLR(int fd, fd_set *fdset):
將一個給定的文件描述符從集合中刪除
性能開銷
雖然select
解決了非阻塞IO模型
中頻繁發(fā)起系統(tǒng)調(diào)用
的問題,但是在整個select
工作過程中胁孙,我們還是看出了select
有些不足的地方唠倦。
在發(fā)起
select
系統(tǒng)調(diào)用以及返回時称鳞,用戶線程各發(fā)生了一次用戶態(tài)
到內(nèi)核態(tài)
以及內(nèi)核態(tài)
到用戶態(tài)
的上下文切換開銷。發(fā)生2次上下文切換
在發(fā)起
select
系統(tǒng)調(diào)用以及返回時稠鼻,用戶線程在內(nèi)核態(tài)
需要將文件描述符集合
從用戶空間拷貝
到內(nèi)核空間冈止。以及在內(nèi)核修改完文件描述符集合
后,又要將它從內(nèi)核空間拷貝
到用戶空間候齿。發(fā)生2次文件描述符集合的拷貝
雖然由原來在
用戶空間
發(fā)起輪詢優(yōu)化成了
在內(nèi)核空間
發(fā)起輪詢但select
不會告訴用戶線程到底是哪些Socket
上發(fā)生了IO就緒
事件熙暴,只是對IO就緒
的Socket
作了標(biāo)記,用戶線程依然要遍歷
文件描述符集合去查找具體IO就緒
的Socket
慌盯。時間復(fù)雜度依然為O(n)
周霉。
大部分情況下,網(wǎng)絡(luò)連接并不總是活躍的亚皂,如果
select
監(jiān)聽了大量的客戶端連接俱箱,只有少數(shù)的連接活躍,然而使用輪詢的這種方式會隨著連接數(shù)的增大灭必,效率會越來越低狞谱。
內(nèi)核
會對原始的文件描述符集合
進行修改。導(dǎo)致每次在用戶空間重新發(fā)起select
調(diào)用時厂财,都需要對文件描述符集合
進行重置
芋簿。BitMap
結(jié)構(gòu)的文件描述符集合,長度為固定的1024
,所以只能監(jiān)聽0~1023
的文件描述符璃饱。select
系統(tǒng)調(diào)用 不是線程安全的与斤。
以上select
的不足所產(chǎn)生的性能開銷
都會隨著并發(fā)量的增大而線性增長
。
很明顯select
也不能解決C10K
問題荚恶,只適用于1000
個左右的并發(fā)連接場景撩穿。
poll
poll
相當(dāng)于是改進版的select
,但是工作原理基本和select
沒有本質(zhì)的區(qū)別谒撼。
int poll(struct pollfd *fds, unsigned int nfds, int timeout)
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 需要監(jiān)聽的事件 */
short revents; /* 實際發(fā)生的事件 由內(nèi)核修改設(shè)置 */
};
select
中使用的文件描述符集合是采用的固定長度為1024的BitMap
結(jié)構(gòu)的fd_set
食寡,而poll
換成了一個pollfd
結(jié)構(gòu)沒有固定長度的數(shù)組,這樣就沒有了最大描述符數(shù)量的限制(當(dāng)然還會受到系統(tǒng)文件描述符限制)
poll
只是改進了select
只能監(jiān)聽1024
個文件描述符的數(shù)量限制廓潜,但是并沒有在性能方面做出改進抵皱。和select
上本質(zhì)并沒有多大差別。
同樣需要在
內(nèi)核空間
和用戶空間
中對文件描述符集合進行輪詢
辩蛋,查找出IO就緒
的Socket
的時間復(fù)雜度依然為O(n)
呻畸。同樣需要將
包含大量文件描述符的集合
整體在用戶空間
和內(nèi)核空間
之間來回復(fù)制
,無論這些文件描述符是否就緒悼院。他們的開銷都會隨著文件描述符數(shù)量的增加而線性增大伤为。select,poll
在每次新增据途,刪除需要監(jiān)聽的socket時绞愚,都需要將整個新的socket
集合全量傳至內(nèi)核
叙甸。
poll
同樣不適用高并發(fā)的場景。依然無法解決C10K
問題位衩。
epoll
通過上邊對select,poll
核心原理的介紹裆蒸,我們看到select,poll
的性能瓶頸主要體現(xiàn)在下面三個地方:
因為內(nèi)核不會保存我們要監(jiān)聽的
socket
集合,所以在每次調(diào)用select,poll
的時候都需要傳入糖驴,傳出全量的socket
文件描述符集合光戈。這導(dǎo)致了大量的文件描述符在用戶空間
和內(nèi)核空間
頻繁的來回復(fù)制。由于內(nèi)核不會通知具體
IO就緒
的socket
遂赠,只是在這些IO就緒
的socket上打好標(biāo)記久妆,所以當(dāng)select
系統(tǒng)調(diào)用返回時,在用戶空間
還是需要完整遍歷
一遍socket
文件描述符集合來獲取具體IO就緒
的socket
跷睦。在
內(nèi)核空間
中也是通過遍歷的方式來得到IO就緒
的socket
筷弦。
下面我們來看下epoll
是如何解決這些問題的。在介紹epoll
的核心原理之前抑诸,我們需要介紹下理解epoll
工作過程所需要的一些核心基礎(chǔ)知識烂琴。
Socket的創(chuàng)建
服務(wù)端線程調(diào)用accept
系統(tǒng)調(diào)用后開始阻塞
,當(dāng)有客戶端連接上來并完成TCP三次握手
后蜕乡,內(nèi)核
會創(chuàng)建一個對應(yīng)的Socket
作為服務(wù)端與客戶端通信的內(nèi)核
接口奸绷。
在Linux內(nèi)核的角度看來,一切皆是文件层玲,Socket
也不例外号醉,當(dāng)內(nèi)核創(chuàng)建出Socket
之后,會將這個Socket
放到當(dāng)前進程所打開的文件列表中管理起來辛块。
下面我們來看下進程管理這些打開的文件列表相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)是什么樣的畔派?在了解完這些數(shù)據(jù)結(jié)構(gòu)后,我們會更加清晰的理解Socket
在內(nèi)核中所發(fā)揮的作用润绵。并且對后面我們理解epoll
的創(chuàng)建過程有很大的幫助线椰。
進程中管理文件列表結(jié)構(gòu)
struct tast_struct
是內(nèi)核中用來表示進程的一個數(shù)據(jù)結(jié)構(gòu),它包含了進程的所有信息尘盼。本小節(jié)我們只列出和文件管理相關(guān)的屬性憨愉。
其中進程內(nèi)打開的所有文件是通過一個數(shù)組fd_array
來進行組織管理,數(shù)組的下標(biāo)即為我們常提到的文件描述符
卿捎,數(shù)組中存放的是對應(yīng)的文件數(shù)據(jù)結(jié)構(gòu)struct file
配紫。每打開一個文件,內(nèi)核都會創(chuàng)建一個struct file
與之對應(yīng)娇澎,并在fd_array
中找到一個空閑位置分配給它笨蚁,數(shù)組中對應(yīng)的下標(biāo)睹晒,就是我們在用戶空間
用到的文件描述符
趟庄。
對于任何一個進程括细,默認情況下,文件描述符
0
表示stdin 標(biāo)準輸入
戚啥,文件描述符1
表示stdout 標(biāo)準輸出
奋单,文件描述符2
表示stderr 標(biāo)準錯誤輸出
。
進程中打開的文件列表fd_array
定義在內(nèi)核數(shù)據(jù)結(jié)構(gòu)struct files_struct
中猫十,在struct fdtable
結(jié)構(gòu)中有一個指針struct fd **fd
指向fd_array
览濒。
由于本小節(jié)討論的是內(nèi)核網(wǎng)絡(luò)系統(tǒng)部分的數(shù)據(jù)結(jié)構(gòu),所以這里拿Socket
文件類型來舉例說明:
用于封裝文件元信息的內(nèi)核數(shù)據(jù)結(jié)構(gòu)struct file
中的private_data
指針指向具體的Socket
結(jié)構(gòu)拖云。
struct file
中的file_operations
屬性定義了文件的操作函數(shù)贷笛,不同的文件類型,對應(yīng)的file_operations
是不同的宙项,針對Socket
文件類型乏苦,這里的file_operations
指向socket_file_ops
。
我們在
用戶空間
對Socket
發(fā)起的讀寫等系統(tǒng)調(diào)用尤筐,進入內(nèi)核首先會調(diào)用的是Socket
對應(yīng)的struct file
中指向的socket_file_ops
汇荐。
比如:對Socket
發(fā)起write
寫操作,在內(nèi)核中首先被調(diào)用的就是socket_file_ops
中定義的sock_write_iter
盆繁。Socket
發(fā)起read
讀操作內(nèi)核中對應(yīng)的則是sock_read_iter
掀淘。
static const struct file_operations socket_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)
在我們進行網(wǎng)絡(luò)程序的編寫時會首先創(chuàng)建一個Socket
,然后基于這個Socket
進行bind
油昂,listen
革娄,我們先將這個Socket
稱作為監(jiān)聽Socket
。
- 當(dāng)我們調(diào)用
accept
后冕碟,內(nèi)核會基于監(jiān)聽Socket
創(chuàng)建出來一個新的Socket
專門用于與客戶端之間的網(wǎng)絡(luò)通信稠腊。并將監(jiān)聽Socket
中的Socket操作函數(shù)集合
(inet_stream_ops
)ops
賦值到新的Socket
的ops
屬性中。
const struct proto_ops inet_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)聽的 socket
和真正用來網(wǎng)絡(luò)通信的Socket
架忌,是兩個 Socket,一個叫作監(jiān)聽 Socket
我衬,一個叫作已連接的Socket
叹放。
- 接著內(nèi)核會為
已連接的Socket
創(chuàng)建struct file
并初始化,并把Socket文件操作函數(shù)集合(socket_file_ops
)賦值給struct file
中的f_ops
指針挠羔。然后將struct socket
中的file
指針指向這個新分配申請的struct file
結(jié)構(gòu)體井仰。
內(nèi)核會維護兩個隊列:
- 一個是已經(jīng)完成
TCP三次握手
,連接狀態(tài)處于established
的連接隊列破加。內(nèi)核中為icsk_accept_queue
俱恶。- 一個是還沒有完成
TCP三次握手
,連接狀態(tài)處于syn_rcvd
的半連接隊列。
- 然后調(diào)用
socket->ops->accept
合是,從Socket內(nèi)核結(jié)構(gòu)圖
中我們可以看到其實調(diào)用的是inet_accept
了罪,該函數(shù)會在icsk_accept_queue
中查找是否有已經(jīng)建立好的連接,如果有的話聪全,直接從icsk_accept_queue
中獲取已經(jīng)創(chuàng)建好的struct sock
泊藕。并將這個struct sock
對象賦值給struct socket
中的sock
指針。
struct sock
在struct socket
中是一個非常核心的內(nèi)核對象难礼,正是在這里定義了我們在介紹網(wǎng)絡(luò)包的接收發(fā)送流程
中提到的接收隊列
娃圆,發(fā)送隊列
,等待隊列
蛾茉,數(shù)據(jù)就緒回調(diào)函數(shù)指針
讼呢,內(nèi)核協(xié)議棧操作函數(shù)集合
- 根據(jù)創(chuàng)建
Socket
時發(fā)起的系統(tǒng)調(diào)用sock_create
中的protocol
參數(shù)(對于TCP協(xié)議
這里的參數(shù)值為SOCK_STREAM
)查找到對于 tcp 定義的操作方法實現(xiàn)集合inet_stream_ops
和tcp_prot
。并把它們分別設(shè)置到socket->ops
和sock->sk_prot
上谦炬。
這里可以回看下本小節(jié)開頭的《Socket內(nèi)核結(jié)構(gòu)圖》捋一下他們之間的關(guān)系吝岭。
socket
相關(guān)的操作接口定義在inet_stream_ops
函數(shù)集合中,負責(zé)對上給用戶提供接口吧寺。而socket
與內(nèi)核協(xié)議棧之間的操作接口定義在struct sock
中的sk_prot
指針上窜管,這里指向tcp_prot
協(xié)議操作函數(shù)集合。
struct proto tcp_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,
......
}
之前提到的對
Socket
發(fā)起的系統(tǒng)IO調(diào)用稚机,在內(nèi)核中首先會調(diào)用Socket
的文件結(jié)構(gòu)struct file
中的file_operations
文件操作集合逗扒,然后調(diào)用struct socket
中的ops
指向的inet_stream_ops
socket操作函數(shù)朝刊,最終調(diào)用到struct sock
中sk_prot
指針指向的tcp_prot
內(nèi)核協(xié)議棧操作函數(shù)接口集合。
將
struct sock
對象中的sk_data_ready
函數(shù)指針設(shè)置為sock_def_readable
,在Socket
數(shù)據(jù)就緒的時候內(nèi)核會回調(diào)該函數(shù)踊兜。struct sock
中的等待隊列
中存放的是系統(tǒng)IO調(diào)用發(fā)生阻塞的進程fd
犹撒,以及相應(yīng)的回調(diào)函數(shù)
跋选。記住這個地方锁孟,后邊介紹epoll的時候我們還會提到!
- 當(dāng)
struct file
仿贬,struct socket
纽竣,struct sock
這些核心的內(nèi)核對象創(chuàng)建好之后,最后就是把socket
對象對應(yīng)的struct file
放到進程打開的文件列表fd_array
中茧泪。隨后系統(tǒng)調(diào)用accept
返回socket
的文件描述符fd
給用戶程序蜓氨。
阻塞IO中用戶進程阻塞以及喚醒原理
在前邊小節(jié)我們介紹阻塞IO
的時候提到,當(dāng)用戶進程發(fā)起系統(tǒng)IO調(diào)用時队伟,這里我們拿read
舉例穴吹,用戶進程會在內(nèi)核態(tài)
查看對應(yīng)Socket
接收緩沖區(qū)是否有數(shù)據(jù)到來。
-
Socket
接收緩沖區(qū)有數(shù)據(jù)嗜侮,則拷貝數(shù)據(jù)到用戶空間
港令,系統(tǒng)調(diào)用返回啥容。 -
Socket
接收緩沖區(qū)沒有數(shù)據(jù),則用戶進程讓出CPU
進入阻塞狀態(tài)
顷霹,當(dāng)數(shù)據(jù)到達接收緩沖區(qū)時咪惠,用戶進程會被喚醒,從阻塞狀態(tài)
進入就緒狀態(tài)
泼返,等待CPU調(diào)度。
本小節(jié)我們就來看下用戶進程是如何阻塞
在Socket
上姨拥,又是如何在Socket
上被喚醒的绅喉。理解這個過程很重要,對我們理解epoll的事件通知過程很有幫助
- 首先我們在用戶進程中對
Socket
進行read
系統(tǒng)調(diào)用時叫乌,用戶進程會從用戶態(tài)
轉(zhuǎn)為內(nèi)核態(tài)
柴罐。 - 在進程的
struct task_struct
結(jié)構(gòu)找到fd_array
,并根據(jù)Socket
的文件描述符fd
找到對應(yīng)的struct file
憨奸,調(diào)用struct file
中的文件操作函數(shù)結(jié)合file_operations
革屠,read
系統(tǒng)調(diào)用對應(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
中會找到struct sock
板甘,并調(diào)用sock->skprot->recvmsg
,這里調(diào)用的是tcp_prot
集合中定義的tcp_recvmsg
函數(shù)党瓮。
整個調(diào)用過程可以參考上邊的《系統(tǒng)IO調(diào)用結(jié)構(gòu)圖》
熟悉了內(nèi)核函數(shù)調(diào)用棧后,我們來看下系統(tǒng)IO調(diào)用在tcp_recvmsg
內(nèi)核函數(shù)中是如何將用戶進程給阻塞掉的
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int nonblock, int flags, int *addr_len)
{
.................省略非核心代碼...............
//訪問sock對象中定義的接收隊列
skb_queue_walk(&sk->sk_receive_queue, skb) {
.................省略非核心代碼...............
//沒有收到足夠數(shù)據(jù)盐类,調(diào)用sk_wait_data 阻塞當(dāng)前進程
sk_wait_data(sk, &timeo);
}
int sk_wait_data(struct sock *sk, long *timeo)
{
//創(chuàng)建struct sock中等待隊列上的元素wait_queue_t
//將進程描述符和回調(diào)函數(shù)autoremove_wake_function關(guān)聯(lián)到wait_queue_t中
DEFINE_WAIT(wait);
// 調(diào)用 sk_sleep 獲取 sock 對象下的等待隊列的頭指針wait_queue_head_t
// 調(diào)用prepare_to_wait將新創(chuàng)建的等待項wait_queue_t插入到等待隊列中寞奸,并將進程狀態(tài)設(shè)置為可打斷 INTERRUPTIBLE
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);
// 通過調(diào)用schedule_timeout讓出CPU,然后進行睡眠在跳,導(dǎo)致一次上下文切換
rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
...
- 首先會在
DEFINE_WAIT
中創(chuàng)建struct sock
中等待隊列上的等待類型wait_queue_t
枪萄。
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
#define DEFINE_WAIT_FUNC(name, function) \
wait_queue_t name = { \
.private = current, \
.func = function, \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
等待類型wait_queue_t
中的private
用來關(guān)聯(lián)阻塞
在當(dāng)前socket
上的用戶進程fd
。func
用來關(guān)聯(lián)等待項上注冊的回調(diào)函數(shù)猫妙。這里注冊的是autoremove_wake_function
瓷翻。
調(diào)用
sk_sleep(sk)
獲取struct sock
對象中的等待隊列頭指針wait_queue_head_t
。調(diào)用
prepare_to_wait
將新創(chuàng)建的等待項wait_queue_t
插入到等待隊列中割坠,并將進程設(shè)置為可打斷INTERRUPTIBL
逻悠。調(diào)用
sk_wait_event
讓出CPU,進程進入睡眠狀態(tài)韭脊。
用戶進程的阻塞過程
我們就介紹完了童谒,關(guān)鍵是要理解記住struct sock
中定義的等待隊列上的等待類型wait_queue_t
的結(jié)構(gòu)。后面epoll
的介紹中我們還會用到它沪羔。
下面我們接著介紹當(dāng)數(shù)據(jù)就緒后饥伊,用戶進程是如何被喚醒的
在本文開始介紹《網(wǎng)絡(luò)包接收過程》這一小節(jié)中我們提到:
- 當(dāng)網(wǎng)絡(luò)數(shù)據(jù)包到達網(wǎng)卡時象浑,網(wǎng)卡通過
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括袒,目的端口)
查找對應(yīng)的Socket
。 - 最后將
sk_buffer
放到Socket
中的接收隊列里稿茉。
上邊這些過程是內(nèi)核接收網(wǎng)絡(luò)數(shù)據(jù)的完整過程锹锰,下邊我們來看下,當(dāng)數(shù)據(jù)包接收完畢后漓库,用戶進程是如何被喚醒的恃慧。
當(dāng)軟中斷將
sk_buffer
放到Socket
的接收隊列上時,接著就會調(diào)用數(shù)據(jù)就緒函數(shù)回調(diào)指針sk_data_ready
渺蒿,前邊我們提到糕伐,這個函數(shù)指針在初始化的時候指向了sock_def_readable
函數(shù)。在
sock_def_readable
函數(shù)中會去獲取socket->sock->sk_wq
等待隊列蘸嘶。在wake_up_common
函數(shù)中從等待隊列sk_wq
中找出一個
等待項wait_queue_t
良瞧,回調(diào)注冊在該等待項上的func
回調(diào)函數(shù)(wait_queue_t->func
),創(chuàng)建等待項wait_queue_t
是我們提到,這里注冊的回調(diào)函數(shù)是autoremove_wake_function
训唱。
即使是有多個進程都阻塞在同一個 socket 上褥蚯,也只喚醒 1 個進程。其作用是為了避免驚群况增。
- 在
autoremove_wake_function
函數(shù)中赞庶,根據(jù)等待項wait_queue_t
上的private
關(guān)聯(lián)的阻塞進程fd
調(diào)用try_to_wake_up
喚醒阻塞在該Socket
上的進程。
記住
wait_queue_t
中的func
函數(shù)指針澳骤,在epoll
中這里會注冊epoll
的回調(diào)函數(shù)歧强。
現(xiàn)在理解epoll
所需要的基礎(chǔ)知識我們就介紹完了为肮,嘮叨了這么多摊册,下面終于正式進入本小節(jié)的主題epoll
了。
epoll_create創(chuàng)建epoll對象
epoll_create
是內(nèi)核提供給我們創(chuàng)建epoll
對象的一個系統(tǒng)調(diào)用颊艳,當(dāng)我們在用戶進程中調(diào)用epoll_create
時茅特,內(nèi)核會為我們創(chuàng)建一個struct eventpoll
對象忘分,并且也有相應(yīng)的struct file
與之關(guān)聯(lián),同樣需要把這個struct eventpoll
對象所關(guān)聯(lián)的struct file
放入進程打開的文件列表fd_array
中管理白修。
熟悉了
Socket
的創(chuàng)建邏輯妒峦,epoll
的創(chuàng)建邏輯也就不難理解了。
struct eventpoll
對象關(guān)聯(lián)的struct file
中的file_operations 指針
指向的是eventpoll_fops
操作函數(shù)集合兵睛。
static const struct file_operations eventpoll_fops = {
.release = ep_eventpoll_release;
.poll = ep_eventpoll_poll,
}
struct eventpoll {
//等待隊列肯骇,阻塞在epoll上的進程會放在這里
wait_queue_head_t wq;
//就緒隊列,IO就緒的socket連接會放在這里
struct list_head rdllist;
//紅黑樹用來管理所有監(jiān)聽的socket連接
struct rb_root rbr;
......
}
-
wait_queue_head_t wq:
epoll中的等待隊列祖很,隊列里存放的是阻塞
在epoll
上的用戶進程笛丙。在IO就緒
的時候epoll
可以通過這個隊列找到這些阻塞
的進程并喚醒它們,從而執(zhí)行IO調(diào)用
讀寫Socket
上的數(shù)據(jù)突琳。
這里注意與
Socket
中的等待隊列區(qū)分H粽7唷拆融!
-
struct list_head rdllist:
epoll中的就緒隊列,隊列里存放的是都是IO就緒
的Socket
啊终,被喚醒的用戶進程可以直接讀取這個隊列獲取IO活躍
的Socket
镜豹。無需再次遍歷整個Socket
集合。
這里正是
epoll
比select 蓝牲,poll
高效之處趟脂,select ,poll
返回的是全部的socket
連接例衍,我們需要在用戶空間
再次遍歷找出真正IO活躍
的Socket
連接昔期。
而epoll
只是返回IO活躍
的Socket
連接。用戶進程可以直接進行IO操作佛玄。
-
struct rb_root rbr :
由于紅黑樹在查找
硼一,插入
,刪除
等綜合性能方面是最優(yōu)的梦抢,所以epoll內(nèi)部使用一顆紅黑樹來管理海量的Socket
連接般贼。
select
用數(shù)組
管理連接,poll
用鏈表
管理連接奥吩。
epoll_ctl向epoll對象中添加監(jiān)聽的Socket
當(dāng)我們調(diào)用epoll_create
在內(nèi)核中創(chuàng)建出epoll
對象struct eventpoll
后哼蛆,我們就可以利用epoll_ctl
向epoll
中添加我們需要管理的Socket
連接了。
- 首先要在epoll內(nèi)核中創(chuàng)建一個表示
Socket連接
的數(shù)據(jù)結(jié)構(gòu)struct epitem
霞赫,而在epoll
中為了綜合性能的考慮腮介,采用一顆紅黑樹來管理這些海量socket連接
。所以struct epitem
是一個紅黑樹節(jié)點端衰。
struct epitem
{
//指向所屬epoll對象
struct eventpoll *ep;
//注冊的感興趣的事件,也就是用戶空間的epoll_event
struct epoll_event event;
//指向epoll對象中的就緒隊列
struct list_head rdllink;
//指向epoll中對應(yīng)的紅黑樹節(jié)點
struct rb_node rbn;
//指向epitem所表示的socket->file結(jié)構(gòu)以及對應(yīng)的fd
struct epoll_filefd ffd;
}
這里重點記住
struct epitem
結(jié)構(gòu)中的rdllink
以及epoll_filefd
成員萤厅,后面我們會用到橄抹。
- 在內(nèi)核中創(chuàng)建完表示
Socket連接
的數(shù)據(jù)結(jié)構(gòu)struct epitem
后,我們就需要在Socket
中的等待隊列上創(chuàng)建等待項wait_queue_t
并且注冊epoll的回調(diào)函數(shù)ep_poll_callback
惕味。
通過《阻塞IO中用戶進程阻塞以及喚醒原理》
小節(jié)的鋪墊楼誓,我想大家已經(jīng)猜到這一步的意義所在了吧!當(dāng)時在等待項wait_queue_t
中注冊的是autoremove_wake_function
回調(diào)函數(shù)名挥。還記得嗎疟羹?
epoll的回調(diào)函數(shù)
ep_poll_callback
正是epoll
同步IO事件通知機制的核心所在,也是區(qū)別于select禀倔,poll
采用內(nèi)核輪詢方式的根本性能差異所在榄融。
這里又出現(xiàn)了一個新的數(shù)據(jù)結(jié)構(gòu)struct eppoll_entry
,那它的作用是干什么的呢救湖?大家可以結(jié)合上圖先猜測下它的作用!
我們知道socket->sock->sk_wq
等待隊列中的類型是wait_queue_t
愧杯,我們需要在struct epitem
所表示的socket
的等待隊列上注冊epoll
回調(diào)函數(shù)ep_poll_callback
。
這樣當(dāng)數(shù)據(jù)到達socket
中的接收隊列時鞋既,內(nèi)核會回調(diào)sk_data_ready
力九,在阻塞IO中用戶進程阻塞以及喚醒原理
這一小節(jié)中,我們知道這個sk_data_ready
函數(shù)指針會指向sk_def_readable
函數(shù)邑闺,在sk_def_readable
中會回調(diào)注冊在等待隊列里的等待項wait_queue_t -> func
回調(diào)函數(shù)ep_poll_callback
跌前。在ep_poll_callback
中需要找到epitem
,將IO就緒
的epitem
放入epoll
中的就緒隊列中陡舅。
而socket
等待隊列中類型是wait_queue_t
無法關(guān)聯(lián)到epitem
抵乓。所以就出現(xiàn)了struct eppoll_entry
結(jié)構(gòu)體,它的作用就是關(guān)聯(lián)Socket
等待隊列中的等待項wait_queue_t
和epitem
靶衍。
struct eppoll_entry {
//指向關(guān)聯(lián)的epitem
struct epitem *base;
// 關(guān)聯(lián)監(jiān)聽socket中等待隊列中的等待項 (private = null func = ep_poll_callback)
wait_queue_t wait;
// 監(jiān)聽socket中等待隊列頭指針
wait_queue_head_t *whead;
.........
};
這樣在ep_poll_callback
回調(diào)函數(shù)中就可以根據(jù)Socket
等待隊列中的等待項wait
灾炭,通過container_of宏
找到eppoll_entry
,繼而找到epitem
了颅眶。
container_of
在Linux內(nèi)核中是一個常用的宏蜈出,用于從包含在某個結(jié)構(gòu)中的指針獲得結(jié)構(gòu)本身的指針,通俗地講就是通過結(jié)構(gòu)體變量中某個成員的首地址進而獲得整個結(jié)構(gòu)體變量的首地址帚呼。
這里需要注意下這次等待項
wait_queue_t
中的private
設(shè)置的是null
掏缎,因為這里Socket
是交給epoll
來管理的,阻塞在Socket
上的進程是也由epoll
來喚醒煤杀。在等待項wait_queue_t
注冊的func
是ep_poll_callback
而不是autoremove_wake_function
眷蜈,阻塞進程
并不需要autoremove_wake_function
來喚醒,所以這里設(shè)置private
為null
- 當(dāng)在
Socket
的等待隊列中創(chuàng)建好等待項wait_queue_t
并且注冊了epoll
的回調(diào)函數(shù)ep_poll_callback
沈自,然后又通過eppoll_entry
關(guān)聯(lián)了epitem
后酌儒。
剩下要做的就是將epitem
插入到epoll
中的紅黑樹struct rb_root rbr
中。
這里可以看到
epoll
另一個優(yōu)化的地方枯途,epoll
將所有的socket
連接通過內(nèi)核中的紅黑樹來集中管理忌怎。每次添加或者刪除socket連接
都是增量添加刪除籍滴,而不是像select,poll
那樣每次調(diào)用都是全量socket連接
集合傳入內(nèi)核榴啸。避免了頻繁大量
的內(nèi)存拷貝
孽惰。
epoll_wait同步阻塞獲取IO就緒的Socket
用戶程序調(diào)用
epoll_wait
后,內(nèi)核首先會查找epoll中的就緒隊列eventpoll->rdllist
是否有IO就緒
的epitem
鸥印。epitem
里封裝了socket
的信息勋功。如果就緒隊列中有就緒的epitem
,就將就緒的socket
信息封裝到epoll_event
返回库说。如果
eventpoll->rdllist
就緒隊列中沒有IO就緒
的epitem
狂鞋,則會創(chuàng)建等待項wait_queue_t
,將用戶進程的fd
關(guān)聯(lián)到wait_queue_t->private
上潜的,并在等待項wait_queue_t->func
上注冊回調(diào)函數(shù)default_wake_function
芳来。最后將等待項添加到epoll
中的等待隊列中甲脏。用戶進程讓出CPU亥至,進入阻塞狀態(tài)
苔埋。
這里和
阻塞IO模型
中的阻塞原理是一樣的,只不過在阻塞IO模型
中注冊到等待項wait_queue_t->func
上的是autoremove_wake_function
脐供,并將等待項添加到socket
中的等待隊列中浑塞。這里注冊的是default_wake_function
借跪,將等待項添加到epoll
中的等待隊列上政己。
- 前邊做了那么多的知識鋪墊,下面終于到了
epoll
的整個工作流程了:
當(dāng)網(wǎng)絡(luò)數(shù)據(jù)包在軟中斷中經(jīng)過內(nèi)核協(xié)議棧的處理到達
socket
的接收緩沖區(qū)時掏愁,緊接著會調(diào)用socket的數(shù)據(jù)就緒回調(diào)指針sk_data_ready
歇由,回調(diào)函數(shù)為sock_def_readable
。在socket
的等待隊列中找出等待項果港,其中等待項中注冊的回調(diào)函數(shù)為ep_poll_callback
沦泌。在回調(diào)函數(shù)
ep_poll_callback
中,根據(jù)struct eppoll_entry
中的struct wait_queue_t wait
通過container_of宏
找到eppoll_entry
對象并通過它的base
指針找到封裝socket
的數(shù)據(jù)結(jié)構(gòu)struct epitem
辛掠,并將它加入到epoll
中的就緒隊列rdllist
中谢谦。隨后查看
epoll
中的等待隊列中是否有等待項,也就是說查看是否有進程阻塞在epoll_wait
上等待IO就緒
的socket
萝衩。如果沒有等待項回挽,則軟中斷處理完成。如果有等待項猩谊,則回到注冊在等待項中的回調(diào)函數(shù)
default_wake_function
,在回調(diào)函數(shù)中喚醒阻塞進程
千劈,并將就緒隊列rdllist
中的epitem
的IO就緒
socket信息封裝到struct epoll_event
中返回。用戶進程拿到
epoll_event
獲取IO就緒
的socket牌捷,發(fā)起系統(tǒng)IO調(diào)用讀取數(shù)據(jù)墙牌。
再談水平觸發(fā)和邊緣觸發(fā)
網(wǎng)上有大量的關(guān)于這兩種模式的講解涡驮,大部分講的比較模糊,感覺只是強行從概念上進行描述喜滨,看完讓人難以理解捉捅。所以在這里,筆者想結(jié)合上邊epoll
的工作過程虽风,再次對這兩種模式做下自己的解讀锯梁,力求清晰的解釋出這兩種工作模式的異同。
經(jīng)過上邊對epoll
工作過程的詳細解讀焰情,我們知道陌凳,當(dāng)我們監(jiān)聽的socket
上有數(shù)據(jù)到來時,軟中斷會執(zhí)行epoll
的回調(diào)函數(shù)ep_poll_callback
,在回調(diào)函數(shù)中會將epoll
中描述socket信息
的數(shù)據(jù)結(jié)構(gòu)epitem
插入到epoll
中的就緒隊列rdllist
中内舟。隨后用戶進程從epoll
的等待隊列中被喚醒合敦,epoll_wait
將IO就緒
的socket
返回給用戶進程,隨即epoll_wait
會清空rdllist
验游。
水平觸發(fā)和邊緣觸發(fā)最關(guān)鍵的區(qū)別就在于當(dāng)socket
中的接收緩沖區(qū)還有數(shù)據(jù)可讀時充岛。epoll_wait
是否會清空rdllist
。
水平觸發(fā):在這種模式下耕蝉,用戶線程調(diào)用
epoll_wait
獲取到IO就緒
的socket后崔梗,對Socket
進行系統(tǒng)IO調(diào)用讀取數(shù)據(jù),假設(shè)socket
中的數(shù)據(jù)只讀了一部分沒有全部讀完垒在,這時再次調(diào)用epoll_wait
蒜魄,epoll_wait
會檢查這些Socket
中的接收緩沖區(qū)是否還有數(shù)據(jù)可讀,如果還有數(shù)據(jù)可讀场躯,就將socket
重新放回rdllist
谈为。所以當(dāng)socket
上的IO沒有被處理完時,再次調(diào)用epoll_wait
依然可以獲得這些socket
踢关,用戶進程可以接著處理socket
上的IO事件伞鲫。邊緣觸發(fā): 在這種模式下,
epoll_wait
就會直接清空rdllist
签舞,不管socket
上是否還有數(shù)據(jù)可讀秕脓。所以在邊緣觸發(fā)模式下,當(dāng)你沒有來得及處理socket
接收緩沖區(qū)的剩下可讀數(shù)據(jù)時儒搭,再次調(diào)用epoll_wait
吠架,因為這時rdlist
已經(jīng)被清空了,socket
不會再次從epoll_wait
中返回师妙,所以用戶進程就不會再次獲得這個socket
了诵肛,也就無法在對它進行IO處理了。除非,這個socket
上有新的IO數(shù)據(jù)到達怔檩,根據(jù)epoll
的工作過程薛训,該socket
會被再次放入rdllist
中媒吗。
如果你在
邊緣觸發(fā)模式
下,處理了部分socket
上的數(shù)據(jù)乙埃,那么想要處理剩下部分的數(shù)據(jù)闸英,就只能等到這個socket
上再次有網(wǎng)絡(luò)數(shù)據(jù)到達。
在Netty
中實現(xiàn)的EpollSocketChannel
默認的就是邊緣觸發(fā)
模式介袜。JDK
的NIO
默認是水平觸發(fā)
模式甫何。
epoll對select,poll的優(yōu)化總結(jié)
-
epoll
在內(nèi)核中通過紅黑樹
管理海量的連接遇伞,所以在調(diào)用epoll_wait
獲取IO就緒
的socket時辙喂,不需要傳入監(jiān)聽的socket文件描述符。從而避免了海量的文件描述符集合在用戶空間
和內(nèi)核空間
中來回復(fù)制鸠珠。
select炬太,poll
每次調(diào)用時都需要傳遞全量的文件描述符集合孽水,導(dǎo)致大量頻繁的拷貝操作测柠。
-
epoll
僅會通知IO就緒
的socket赃阀。避免了在用戶空間遍歷的開銷懂缕。
select工碾,poll
只會在IO就緒
的socket上打好標(biāo)記端圈,依然是全量返回宴倍,所以在用戶空間還需要用戶程序在一次遍歷全量集合找出具體IO就緒
的socket阔逼。
-
epoll
通過在socket
的等待隊列上注冊回調(diào)函數(shù)ep_poll_callback
通知用戶程序IO就緒
的socket危融。避免了在內(nèi)核中輪詢的開銷速侈。
大部分情況下
socket
上并不總是IO活躍
的每界,在面對海量連接的情況下趴樱,select,poll
采用內(nèi)核輪詢的方式獲取IO活躍
的socket呢袱,無疑是性能低下的核心原因焊唬。
根據(jù)以上epoll
的性能優(yōu)勢,它是目前為止各大主流網(wǎng)絡(luò)框架,以及反向代理中間件使用到的網(wǎng)絡(luò)IO模型。
利用epoll
多路復(fù)用IO模型可以輕松的解決C10K
問題嗡官。
C100k
的解決方案也還是基于C10K
的方案芜辕,通過epoll
配合線程池乃沙,再加上 CPU、內(nèi)存和網(wǎng)絡(luò)接口的性能和容量提升眶根。大部分情況下蜀铲,C100K
很自然就可以達到。
甚至C1000K
的解決方法属百,本質(zhì)上還是構(gòu)建在 epoll
的多路復(fù)用 I/O 模型
上。只不過冲秽,除了 I/O 模型之外,還需要從應(yīng)用程序到 Linux 內(nèi)核寻咒、再到 CPU、內(nèi)存和網(wǎng)絡(luò)等各個層次的深度優(yōu)化,特別是需要借助硬件,來卸載那些原來通過軟件處理的大量功能(去掉大量的中斷響應(yīng)開銷
,以及內(nèi)核協(xié)議棧處理的開銷
)。
信號驅(qū)動IO
大家對這個裝備肯定不會陌生,當(dāng)我們?nèi)ヒ恍┟朗吵浅燥埖臅r候膳算,點完餐付了錢,老板會給我們一個信號器违寿。然后我們帶著這個信號器可以去找餐桌湃交,或者干些其他的事情。當(dāng)信號器亮了的時候藤巢,這時代表飯餐已經(jīng)做好搞莺,我們可以去窗口取餐了。
這個典型的生活場景和我們要介紹的信號驅(qū)動IO模型
就很像掂咒。
在信號驅(qū)動IO模型
下才沧,用戶進程操作通過系統(tǒng)調(diào)用 sigaction 函數(shù)
發(fā)起一個 IO 請求,在對應(yīng)的socket
注冊一個信號回調(diào)
绍刮,此時不阻塞
用戶進程温圆,進程會繼續(xù)工作。當(dāng)內(nèi)核數(shù)據(jù)就緒時录淡,內(nèi)核就為該進程生成一個 SIGIO 信號
捌木,通過信號回調(diào)通知進程進行相關(guān) IO 操作。
這里需要注意的是:
信號驅(qū)動式 IO 模型
依然是同步IO
嫉戚,因為它雖然可以在等待數(shù)據(jù)的時候不被阻塞刨裆,也不會頻繁的輪詢澈圈,但是當(dāng)數(shù)據(jù)就緒,內(nèi)核信號通知后帆啃,用戶進程依然要自己去讀取數(shù)據(jù)瞬女,在數(shù)據(jù)拷貝階段
發(fā)生阻塞。
信號驅(qū)動 IO模型 相比于前三種 IO 模型努潘,實現(xiàn)了在等待數(shù)據(jù)就緒時诽偷,進程不被阻塞,主循環(huán)可以繼續(xù)工作疯坤,所以
理論上
性能更佳报慕。
但是實際上,使用TCP協(xié)議
通信時压怠,信號驅(qū)動IO模型
幾乎不會被采用
眠冈。原因如下:
- 信號IO 在大量 IO 操作時可能會因為信號隊列溢出導(dǎo)致沒法通知
-
SIGIO 信號
是一種 Unix 信號,信號沒有附加信息菌瘫,如果一個信號源有多種產(chǎn)生信號的原因蜗顽,信號接收者就無法確定究竟發(fā)生了什么。而 TCP socket 生產(chǎn)的信號事件有七種之多雨让,這樣應(yīng)用程序收到 SIGIO雇盖,根本無從區(qū)分處理。
但信號驅(qū)動IO模型
可以用在 UDP
通信上栖忠,因為UDP 只有一個數(shù)據(jù)請求事件
崔挖,這也就意味著在正常情況下 UDP 進程只要捕獲 SIGIO 信號,就調(diào)用 read 系統(tǒng)調(diào)用
讀取到達的數(shù)據(jù)娃闲。如果出現(xiàn)異常虚汛,就返回一個異常錯誤。
這里插句題外話皇帮,大家覺不覺得阻塞IO模型
在生活中的例子就像是我們在食堂排隊打飯卷哩。你自己需要排隊去打飯同時打飯師傅在配菜的過程中你需要等待。
IO多路復(fù)用模型
就像是我們在飯店門口排隊等待叫號属拾。叫號器就好比select,poll,epoll
可以統(tǒng)一管理全部顧客的吃飯就緒
事件将谊,客戶好比是socket
連接,誰可以去吃飯了渐白,叫號器就通知誰尊浓。
異步IO(AIO)
以上介紹的四種IO模型
均為同步IO
,它們都會阻塞在第二階段數(shù)據(jù)拷貝階段
纯衍。
通過在前邊小節(jié)《同步與異步》中的介紹栋齿,相信大家很容易就會理解異步IO模型
,在異步IO模型
下,IO操作在數(shù)據(jù)準備階段
和數(shù)據(jù)拷貝階段
均是由內(nèi)核來完成瓦堵,不會對應(yīng)用程序造成任何阻塞基协。應(yīng)用進程只需要在指定的數(shù)組
中引用數(shù)據(jù)即可。
異步 IO
與信號驅(qū)動 IO
的主要區(qū)別在于:信號驅(qū)動 IO
由內(nèi)核通知何時可以開始一個 IO 操作
菇用,而異步 IO
由內(nèi)核通知 IO 操作何時已經(jīng)完成
澜驮。
舉個生活中的例子:異步IO模型
就像我們?nèi)ヒ粋€高檔飯店里的包間吃飯,我們只需要坐在包間里面惋鸥,點完餐(類比異步IO調(diào)用
)之后杂穷,我們就什么也不需要管,該喝酒喝酒卦绣,該聊天聊天耐量,飯餐做好后服務(wù)員(類比內(nèi)核
)會自己給我們送到包間(類比用戶空間
)來。整個過程沒有任何阻塞迎卤。
異步IO
的系統(tǒng)調(diào)用需要操作系統(tǒng)內(nèi)核來支持拴鸵,目前只有Window
中的IOCP
實現(xiàn)了非常成熟的異步IO機制
。
而Linux
系統(tǒng)對異步IO機制
實現(xiàn)的不夠成熟蜗搔,且與NIO
的性能相比提升也不明顯。
但Linux kernel 在5.1版本由Facebook的大神Jens Axboe引入了新的異步IO庫
io_uring
改善了原來Linux native AIO的一些性能問題八堡。性能相比Epoll
以及之前原生的AIO
提高了不少樟凄,值得關(guān)注。
再加上信號驅(qū)動IO模型
不適用TCP協(xié)議
兄渺,所以目前大部分采用的還是IO多路復(fù)用模型
缝龄。
IO線程模型
在前邊內(nèi)容的介紹中,我們詳述了網(wǎng)絡(luò)數(shù)據(jù)包的接收和發(fā)送過程挂谍,并通過介紹5種IO模型
了解了內(nèi)核是如何讀取網(wǎng)絡(luò)數(shù)據(jù)并通知給用戶線程的叔壤。
前邊的內(nèi)容都是以內(nèi)核空間
的視角來剖析網(wǎng)絡(luò)數(shù)據(jù)的收發(fā)模型,本小節(jié)我們站在用戶空間
的視角來看下如果對網(wǎng)絡(luò)數(shù)據(jù)進行收發(fā)口叙。
相對內(nèi)核
來講炼绘,用戶空間的IO線程模型
相對就簡單一些。這些用戶空間
的IO線程模型
都是在討論當(dāng)多線程一起配合工作時誰負責(zé)接收連接妄田,誰負責(zé)響應(yīng)IO 讀寫俺亮、誰負責(zé)計算、誰負責(zé)發(fā)送和接收褐荷,僅僅是用戶IO線程的不同分工模式罷了蚤霞。
Reactor
Reactor
是利用NIO
對IO線程
進行不同的分工:
- 使用前邊我們提到的
IO多路復(fù)用模型
比如select,poll,epoll,kqueue
,進行IO事件的注冊和監(jiān)聽闽瓢。 - 將監(jiān)聽到
就緒的IO事件
分發(fā)dispatch
到各個具體的處理Handler
中進行相應(yīng)的IO事件處理
。
通過IO多路復(fù)用技術(shù)
就可以不斷的監(jiān)聽IO事件
本讥,不斷的分發(fā)dispatch
,就像一個反應(yīng)堆
一樣,看起來像不斷的產(chǎn)生IO事件
拷沸,因此我們稱這種模式為Reactor
模型旨椒。
下面我們來看下Reactor模型
的三種分類:
單Reactor單線程
Reactor模型
是依賴IO多路復(fù)用技術(shù)
實現(xiàn)監(jiān)聽IO事件
,從而源源不斷的產(chǎn)生IO就緒事件
堵漱,在Linux系統(tǒng)下我們使用epoll
來進行IO多路復(fù)用
综慎,我們以Linux系統(tǒng)為例:
- 單
Reactor
意味著只有一個epoll
對象,用來監(jiān)聽所有的事件勤庐,比如連接事件
示惊,讀寫事件
。 -
單線程
意味著只有一個線程來執(zhí)行epoll_wait
獲取IO就緒
的Socket
愉镰,然后對這些就緒的Socket
執(zhí)行讀寫米罚,以及后邊的業(yè)務(wù)處理也依然是這個線程。
單Reactor單線程
模型就好比我們開了一個很小很小的小飯館丈探,作為老板的我們需要一個人干所有的事情录择,包括:迎接顧客(accept事件
),為顧客介紹菜單等待顧客點菜(IO請求
)碗降,做菜(業(yè)務(wù)處理
)隘竭,上菜(IO響應(yīng)
),送客(斷開連接
)讼渊。
單Reactor多線程
隨著客人的增多(并發(fā)請求
)动看,顯然飯館里的事情只有我們一個人干(單線程
)肯定是忙不過來的,這時候我們就需要多招聘一些員工(多線程
)來幫著一起干上述的事情爪幻。
于是就有了單Reactor多線程
模型:
- 這種模式下菱皆,也是只有一個
epoll
對象來監(jiān)聽所有的IO事件
,一個線程來調(diào)用epoll_wait
獲取IO就緒
的Socket
挨稿。 - 但是當(dāng)
IO就緒事件
產(chǎn)生時仇轻,這些IO事件
對應(yīng)處理的業(yè)務(wù)Handler
,我們是通過線程池來執(zhí)行奶甘。這樣相比單Reactor單線程
模型提高了執(zhí)行效率篷店,充分發(fā)揮了多核CPU的優(yōu)勢。
主從Reactor多線程
做任何事情都要區(qū)分事情的優(yōu)先級
甩十,我們應(yīng)該優(yōu)先高效
的去做優(yōu)先級更高
的事情船庇,而不是一股腦不分優(yōu)先級的全部去做。
當(dāng)我們的小飯館客人越來越多(并發(fā)量越來越大
)侣监,我們就需要擴大飯店的規(guī)模鸭轮,在這個過程中我們發(fā)現(xiàn),迎接客人
是飯店最重要的工作橄霉,我們要先把客人迎接進來窃爷,不能讓客人一看人多就走掉邑蒋,只要客人進來了,哪怕菜做的慢一點也沒關(guān)系按厘。
于是医吊,主從Reactor多線程
模型就產(chǎn)生了:
我們由原來的
單Reactor
變?yōu)榱?code>多Reactor。主Reactor
用來優(yōu)先專門
做優(yōu)先級最高的事情逮京,也就是迎接客人(處理連接事件
)卿堂,對應(yīng)的處理Handler
就是圖中的acceptor
。當(dāng)創(chuàng)建好連接懒棉,建立好對應(yīng)的
socket
后草描,在acceptor
中將要監(jiān)聽的read事件
注冊到從Reactor
中,由從Reactor
來監(jiān)聽socket
上的讀寫
事件策严。最終將讀寫的業(yè)務(wù)邏輯處理交給線程池處理穗慕。
注意:這里向
從Reactor
注冊的只是read事件
,并沒有注冊write事件
妻导,因為read事件
是由epoll內(nèi)核
觸發(fā)的逛绵,而write事件
則是由用戶業(yè)務(wù)線程觸發(fā)的(什么時候發(fā)送數(shù)據(jù)是由具體業(yè)務(wù)線程決定的
),所以write事件
理應(yīng)是由用戶業(yè)務(wù)線程
去注冊倔韭。
用戶線程注冊
write事件
的時機是只有當(dāng)用戶發(fā)送的數(shù)據(jù)無法一次性
全部寫入buffer
時术浪,才會去注冊write事件
,等待buffer重新可寫
時狐肢,繼續(xù)寫入剩下的發(fā)送數(shù)據(jù)添吗、如果用戶線程可以一股腦的將發(fā)送數(shù)據(jù)全部寫入buffer
,那么也就無需注冊write事件
到從Reactor
中份名。
主從Reactor多線程
模型是現(xiàn)在大部分主流網(wǎng)絡(luò)框架中采用的一種IO線程模型
。我們本系列的主題Netty
就是用的這種模型僵腺。
Proactor
Proactor
是基于AIO
對IO線程
進行分工的一種模型。前邊我們介紹了異步IO模型
桑滩,它是操作系統(tǒng)內(nèi)核支持的一種全異步編程模型,在數(shù)據(jù)準備階段
和數(shù)據(jù)拷貝階段
全程無阻塞蔓搞。
ProactorIO線程模型
將IO事件的監(jiān)聽
,IO操作的執(zhí)行
酿愧,IO結(jié)果的dispatch
統(tǒng)統(tǒng)交給內(nèi)核
來做。
Proactor模型
組件介紹:
completion handler
為用戶程序定義的異步IO操作回調(diào)函數(shù)河爹,在異步IO操作完成時會被內(nèi)核回調(diào)并通知IO結(jié)果裁厅。Completion Event Queue
異步IO操作完成后,會產(chǎn)生對應(yīng)的IO完成事件
蚁鳖,將IO完成事件
放入該隊列中。Asynchronous Operation Processor
負責(zé)異步IO
的執(zhí)行厢破。執(zhí)行完成后產(chǎn)生IO完成事件
放入Completion Event Queue
隊列中同波。Proactor
是一個事件循環(huán)派發(fā)器悲雳,負責(zé)從Completion Event Queue
中獲取IO完成事件
,并回調(diào)與IO完成事件
關(guān)聯(lián)的completion handler
幔翰。Initiator
初始化異步操作(asynchronous operation
)并通過Asynchronous Operation Processor
將completion handler
和proactor
注冊到內(nèi)核。
Proactor模型
執(zhí)行過程:
用戶線程發(fā)起
aio_read
瘫镇,并告訴內(nèi)核
用戶空間中的讀緩沖區(qū)地址秉继,以便內(nèi)核
完成IO操作
將結(jié)果放入用戶空間
的讀緩沖區(qū),用戶線程直接可以讀取結(jié)果(無任何阻塞
)链烈。Initiator
初始化aio_read
異步讀取操作(asynchronous operation
),并將completion handler
注冊到內(nèi)核。
在
Proactor
中我們關(guān)心的IO完成事件
:內(nèi)核已經(jīng)幫我們讀好數(shù)據(jù)并放入我們指定的讀緩沖區(qū)袱蜡,用戶線程可以直接讀取男摧。
在Reactor
中我們關(guān)心的是IO就緒事件
:數(shù)據(jù)已經(jīng)到來,但是需要用戶線程自己去讀取译打。
此時用戶線程就可以做其他事情了耗拓,無需等待IO結(jié)果。而內(nèi)核與此同時開始異步執(zhí)行IO操作奏司。當(dāng)
IO操作
完成時會產(chǎn)生一個completion event
事件乔询,將這個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對比
Reactor
是基于NIO
實現(xiàn)的一種IO線程模型
搪缨,Proactor
是基于AIO
實現(xiàn)的IO線程模型
食拜。Reactor
關(guān)心的是IO就緒事件
,Proactor
關(guān)心的是IO完成事件
副编。在
Proactor
中负甸,用戶程序需要向內(nèi)核傳遞用戶空間的讀緩沖區(qū)地址
。Reactor
則不需要齿桃。這也就導(dǎo)致了在Proactor
中每個并發(fā)操作都要求有獨立的緩存區(qū)惑惶,在內(nèi)存上有一定的開銷。Proactor
的實現(xiàn)邏輯復(fù)雜短纵,編碼成本較Reactor
要高很多带污。Proactor
在處理高耗時 IO
時的性能要高于Reactor
,但對于低耗時 IO
的執(zhí)行效率提升并不明顯
香到。
Netty的IO模型
在我們介紹完網(wǎng)絡(luò)數(shù)據(jù)包在內(nèi)核中的收發(fā)過程
以及五種IO模型
和兩種IO線程模型
后鱼冀,現(xiàn)在我們來看下netty
中的IO模型是什么樣的。
在我們介紹Reactor IO線程模型
的時候提到有三種Reactor模型
:單Reactor單線程
悠就,單Reactor多線程
千绪,主從Reactor多線程
。
這三種Reactor模型
在netty
中都是支持的梗脾,但是我們常用的是主從Reactor多線程模型
荸型。
而我們之前介紹的三種Reactor
只是一種模型,是一種設(shè)計思想炸茧。實際上各種網(wǎng)絡(luò)框架在實現(xiàn)中并不是嚴格按照模型來實現(xiàn)的瑞妇,會有一些小的不同稿静,但大體設(shè)計思想上是一樣的。
下面我們來看下netty
中的主從Reactor多線程模型
是什么樣子的辕狰?
Reactor
在netty
中是以group
的形式出現(xiàn)的改备,netty
中將Reactor
分為兩組,一組是MainReactorGroup
也就是我們在編碼中常陈叮看到的EventLoopGroup bossGroup
,另一組是SubReactorGroup
也就是我們在編碼中常承看到的EventLoopGroup workerGroup
。MainReactorGroup
中通常只有一個Reactor
偶翅,專門負責(zé)做最重要的事情默勾,也就是監(jiān)聽連接accept
事件。當(dāng)有連接事件產(chǎn)生時倒堕,在對應(yīng)的處理handler acceptor
中創(chuàng)建初始化相應(yīng)的NioSocketChannel
(代表一個Socket連接
)灾测。然后以負載均衡
的方式在SubReactorGroup
中選取一個Reactor
,注冊上去垦巴,監(jiān)聽Read事件
媳搪。
MainReactorGroup
中只有一個Reactor
的原因是,通常我們服務(wù)端程序只會綁定監(jiān)聽
一個端口骤宣,如果要綁定監(jiān)聽
多個端口秦爆,就會配置多個Reactor
。
SubReactorGroup
中有多個Reactor
憔披,具體Reactor
的個數(shù)可以由系統(tǒng)參數(shù)-D io.netty.eventLoopThreads
指定等限。默認的Reactor
的個數(shù)為CPU核數(shù) * 2
。SubReactorGroup
中的Reactor
主要負責(zé)監(jiān)聽讀寫事件
芬膝,每一個Reactor
負責(zé)監(jiān)聽一組socket連接
望门。將全量的連接分攤
在多個Reactor
中。一個
Reactor
分配一個IO線程
锰霜,這個IO線程
負責(zé)從Reactor
中獲取IO就緒事件
筹误,執(zhí)行IO調(diào)用獲取IO數(shù)據(jù)
,執(zhí)行PipeLine
癣缅。
Socket連接
在創(chuàng)建后就被固定的分配
給一個Reactor
厨剪,所以一個Socket連接
也只會被一個固定的IO線程
執(zhí)行,每個Socket連接
分配一個獨立的PipeLine
實例友存,用來編排這個Socket連接
上的IO處理邏輯
祷膳。這種無鎖串行化
的設(shè)計的目的是為了防止多線程并發(fā)執(zhí)行同一個socket連接上的IO邏輯處理
,防止出現(xiàn)線程安全問題
屡立。同時使系統(tǒng)吞吐量達到最大化
由于每個
Reactor
中只有一個IO線程
直晨,這個IO線程
既要執(zhí)行IO活躍Socket連接
對應(yīng)的PipeLine
中的ChannelHandler
,又要從Reactor
中獲取IO就緒事件
,執(zhí)行IO調(diào)用
抡秆。所以PipeLine
中ChannelHandler
中執(zhí)行的邏輯不能耗時太長奕巍,盡量將耗時的業(yè)務(wù)邏輯處理放入單獨的業(yè)務(wù)線程池中處理,否則會影響其他連接的IO讀寫
儒士,從而近一步影響整個服務(wù)程序的IO吞吐
。
- 當(dāng)
IO請求
在業(yè)務(wù)線程中完成相應(yīng)的業(yè)務(wù)邏輯處理后檩坚,在業(yè)務(wù)線程中利用持有的ChannelHandlerContext
引用將響應(yīng)數(shù)據(jù)在PipeLine
中反向傳播着撩,最終寫回給客戶端。
netty
中的IO模型
我們介紹完了匾委,下面我們來簡單介紹下在netty
中是如何支持前邊提到的三種Reactor模型
的拖叙。
配置單Reactor單線程
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
配置多Reactor線程
EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
配置主從Reactor多線程
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
總結(jié)
本文是一篇信息量比較大的文章,用了25
張圖赂乐,22336
個字從內(nèi)核如何處理網(wǎng)絡(luò)數(shù)據(jù)包的收發(fā)過程開始展開薯鳍,隨后又在內(nèi)核角度
介紹了經(jīng)常容易混淆的阻塞與非阻塞
,同步與異步
的概念挨措。以這個作為鋪墊挖滤,我們通過一個C10K
的問題,引出了五種IO模型
浅役,隨后在IO多路復(fù)用
中以技術(shù)演進的形式介紹了select,poll,epoll
的原理和它們綜合的對比斩松。最后我們介紹了兩種IO線程模型
以及netty
中的Reactor模型
。
感謝大家聽我嘮叨到這里觉既,哈哈惧盹,現(xiàn)在大家可以揉揉眼,伸個懶腰瞪讼,好好休息一下了钧椰。