深入講解Netty那些事兒之從內(nèi)核角度看IO模型(上)
我們都知道Netty是一個(gè)高性能異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)框架。
它的設(shè)計(jì)異常優(yōu)雅簡(jiǎn)潔,擴(kuò)展性高,穩(wěn)定性強(qiáng)。擁有非常詳細(xì)完整的用戶文檔。
同時(shí)內(nèi)置了很多非常有用的模塊基本上做到了開箱即用,用戶只需要編寫短短幾行代碼,就可以快速構(gòu)建出一個(gè)具有高吞吐,低延時(shí),更少的資源消耗,高性能(非必要的內(nèi)存拷貝最小化)等特征的高并發(fā)網(wǎng)絡(luò)應(yīng)用程序。
本文我們來探討下支持Netty具有高吞吐,低延時(shí)特征的基石----netty的網(wǎng)絡(luò)IO模型。
由Netty的網(wǎng)絡(luò)IO模型開始,我們來正式揭開本系列Netty源碼解析的序幕:
網(wǎng)絡(luò)包接收流程

當(dāng)
網(wǎng)絡(luò)數(shù)據(jù)幀
通過網(wǎng)絡(luò)傳輸?shù)竭_(dá)網(wǎng)卡時(shí),網(wǎng)卡會(huì)將網(wǎng)絡(luò)數(shù)據(jù)幀通過DMA的方式
放到環(huán)形緩沖區(qū)RingBuffer
中。
RingBuffer
是網(wǎng)卡在啟動(dòng)的時(shí)候分配和初始化
的環(huán)形緩沖隊(duì)列
。當(dāng)RingBuffer滿
的時(shí)候,新來的數(shù)據(jù)包就會(huì)被丟棄
。我們可以通過ifconfig
命令查看網(wǎng)卡收發(fā)數(shù)據(jù)包的情況。其中overruns
數(shù)據(jù)項(xiàng)表示當(dāng)RingBuffer滿
時(shí),被丟棄的數(shù)據(jù)包
。如果發(fā)現(xiàn)出現(xiàn)丟包情況,可以通過ethtool命令
來增大RingBuffer長(zhǎng)度。
當(dāng)
DMA操作完成
時(shí),網(wǎng)卡會(huì)向CPU發(fā)起一個(gè)硬中斷
,告訴CPU
有網(wǎng)絡(luò)數(shù)據(jù)到達(dá)。CPU調(diào)用網(wǎng)卡驅(qū)動(dòng)注冊(cè)的硬中斷響應(yīng)程序
。網(wǎng)卡硬中斷響應(yīng)程序會(huì)為網(wǎng)絡(luò)數(shù)據(jù)幀創(chuàng)建內(nèi)核數(shù)據(jù)結(jié)構(gòu)sk_buffer
,并將網(wǎng)絡(luò)數(shù)據(jù)幀拷貝
到sk_buffer
中。然后發(fā)起軟中斷請(qǐng)求
,通知內(nèi)核
有新的網(wǎng)絡(luò)數(shù)據(jù)幀到達(dá)。
sk_buff
緩沖區(qū),是一個(gè)維護(hù)網(wǎng)絡(luò)幀結(jié)構(gòu)的雙向鏈表
,鏈表中的每一個(gè)元素都是一個(gè)網(wǎng)絡(luò)幀
。雖然 TCP/IP 協(xié)議棧分了好幾層,但上下不同層之間的傳遞,實(shí)際上只需要操作這個(gè)數(shù)據(jù)結(jié)構(gòu)中的指針,而無需進(jìn)行數(shù)據(jù)復(fù)制
。
內(nèi)核線程
ksoftirqd
發(fā)現(xiàn)有軟中斷請(qǐng)求到來,隨后調(diào)用網(wǎng)卡驅(qū)動(dòng)注冊(cè)的poll函數(shù)
,poll函數(shù)
將sk_buffer
中的網(wǎng)絡(luò)數(shù)據(jù)包
送到內(nèi)核協(xié)議棧中注冊(cè)的ip_rcv函數(shù)
中。
每個(gè)CPU
會(huì)綁定一個(gè)ksoftirqd
內(nèi)核線程專門
用來處理軟中斷響應(yīng)
。2個(gè) CPU 時(shí),就會(huì)有?ksoftirqd/0
?和?ksoftirqd/1
這兩個(gè)內(nèi)核線程。
這里有個(gè)事情需要注意下:?網(wǎng)卡接收到數(shù)據(jù)后,當(dāng)
DMA拷貝完成
時(shí),向CPU發(fā)出硬中斷
,這時(shí)哪個(gè)CPU
上響應(yīng)了這個(gè)硬中斷
,那么在網(wǎng)卡硬中斷響應(yīng)程序
中發(fā)出的軟中斷請(qǐng)求
也會(huì)在這個(gè)CPU綁定的ksoftirqd線程
中響應(yīng)。所以如果發(fā)現(xiàn)Linux軟中斷,CPU消耗都集中在一個(gè)核上
的話,那么就需要調(diào)整硬中斷的CPU親和性
,來將硬中斷打散
到不通的CPU核
上去。
在
ip_rcv函數(shù)
中也就是上圖中的網(wǎng)絡(luò)層
,取出
數(shù)據(jù)包的IP頭
,判斷該數(shù)據(jù)包下一跳的走向,如果數(shù)據(jù)包是發(fā)送給本機(jī)的,則取出傳輸層的協(xié)議類型(TCP
或者UDP
),并去掉
數(shù)據(jù)包的IP頭
,將數(shù)據(jù)包交給上圖中得傳輸層
處理。
傳輸層的處理函數(shù):
TCP協(xié)議
對(duì)應(yīng)內(nèi)核協(xié)議棧中注冊(cè)的tcp_rcv函數(shù)
,UDP協(xié)議
對(duì)應(yīng)內(nèi)核協(xié)議棧中注冊(cè)的udp_rcv函數(shù)
。
當(dāng)我們采用的是
TCP協(xié)議
時(shí),數(shù)據(jù)包到達(dá)傳輸層時(shí),會(huì)在內(nèi)核協(xié)議棧中的tcp_rcv函數(shù)
處理,在tcp_rcv函數(shù)中去掉
TCP頭,根據(jù)四元組(源IP,源端口,目的IP,目的端口)
查找對(duì)應(yīng)的Socket
,如果找到對(duì)應(yīng)的Socket則將網(wǎng)絡(luò)數(shù)據(jù)包中的傳輸數(shù)據(jù)拷貝到Socket
中的接收緩沖區(qū)
中。如果沒有找到,則發(fā)送一個(gè)目標(biāo)不可達(dá)
的icmp
包。內(nèi)核在接收網(wǎng)絡(luò)數(shù)據(jù)包時(shí)所做的工作我們就介紹完了,現(xiàn)在我們把視角放到應(yīng)用層,當(dāng)我們程序通過系統(tǒng)調(diào)用
read
讀取Socket接收緩沖區(qū)
中的數(shù)據(jù)時(shí),如果接收緩沖區(qū)中沒有數(shù)據(jù)
,那么應(yīng)用程序就會(huì)在系統(tǒng)調(diào)用上阻塞
,直到Socket接收緩沖區(qū)有數(shù)據(jù)
,然后CPU
將內(nèi)核空間
(Socket接收緩沖區(qū))的數(shù)據(jù)拷貝
到用戶空間
,最后系統(tǒng)調(diào)用read返回
,應(yīng)用程序讀取
數(shù)據(jù)。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


性能開銷
從內(nèi)核處理網(wǎng)絡(luò)數(shù)據(jù)包接收的整個(gè)過程來看,內(nèi)核幫我們做了非常之多的工作,最終我們的應(yīng)用程序才能讀取到網(wǎng)絡(luò)數(shù)據(jù)。
隨著而來的也帶來了很多的性能開銷,結(jié)合前面介紹的網(wǎng)絡(luò)數(shù)據(jù)包接收過程我們來看下網(wǎng)絡(luò)數(shù)據(jù)包接收的過程中都有哪些性能開銷:
應(yīng)用程序通過
系統(tǒng)調(diào)用
從用戶態(tài)
轉(zhuǎn)為內(nèi)核態(tài)
的開銷以及系統(tǒng)調(diào)用返回
時(shí)從內(nèi)核態(tài)
轉(zhuǎn)為用戶態(tài)
的開銷。網(wǎng)絡(luò)數(shù)據(jù)從
內(nèi)核空間
通過CPU拷貝
到用戶空間
的開銷。內(nèi)核線程
ksoftirqd
響應(yīng)軟中斷
的開銷。CPU
響應(yīng)硬中斷
的開銷。DMA拷貝
網(wǎng)絡(luò)數(shù)據(jù)包到內(nèi)存
中的開銷。
網(wǎng)絡(luò)包發(fā)送流程

當(dāng)我們?cè)趹?yīng)用程序中調(diào)用
send
系統(tǒng)調(diào)用發(fā)送數(shù)據(jù)時(shí),由于是系統(tǒng)調(diào)用所以線程會(huì)發(fā)生一次用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換,在內(nèi)核中首先根據(jù)fd
將真正的Socket找出,這個(gè)Socket對(duì)象中記錄著各種協(xié)議棧的函數(shù)地址,然后構(gòu)造struct msghdr
對(duì)象,將用戶需要發(fā)送的數(shù)據(jù)全部封裝在這個(gè)struct msghdr
結(jié)構(gòu)體中。調(diào)用內(nèi)核協(xié)議棧函數(shù)
inet_sendmsg
,發(fā)送流程進(jìn)入內(nèi)核協(xié)議棧處理。在進(jìn)入到內(nèi)核協(xié)議棧之后,內(nèi)核會(huì)找到Socket上的具體協(xié)議的發(fā)送函數(shù)。
比如:我們使用的是
TCP協(xié)議
,對(duì)應(yīng)的TCP協(xié)議
發(fā)送函數(shù)是tcp_sendmsg
,如果是UDP協(xié)議
的話,對(duì)應(yīng)的發(fā)送函數(shù)為udp_sendmsg
。
在
TCP協(xié)議
的發(fā)送函數(shù)tcp_sendmsg
中,創(chuàng)建內(nèi)核數(shù)據(jù)結(jié)構(gòu)sk_buffer
,將struct msghdr
結(jié)構(gòu)體中的發(fā)送數(shù)據(jù)拷貝
到sk_buffer
中。調(diào)用tcp_write_queue_tail
函數(shù)獲取Socket
發(fā)送隊(duì)列中的隊(duì)尾元素,將新創(chuàng)建的sk_buffer
添加到Socket
發(fā)送隊(duì)列的尾部。
Socket
的發(fā)送隊(duì)列是由sk_buffer
組成的一個(gè)雙向鏈表
。
發(fā)送流程走到這里,用戶要發(fā)送的數(shù)據(jù)總算是從
用戶空間
拷貝到了內(nèi)核
中,這時(shí)雖然發(fā)送數(shù)據(jù)已經(jīng)拷貝
到了內(nèi)核Socket
中的發(fā)送隊(duì)列
中,但并不代表內(nèi)核會(huì)開始發(fā)送,因?yàn)?code>TCP協(xié)議的流量控制
和擁塞控制
,用戶要發(fā)送的數(shù)據(jù)包并不一定
會(huì)立馬被發(fā)送出去,需要符合TCP協(xié)議
的發(fā)送條件。如果沒有達(dá)到發(fā)送條件
,那么本次send
系統(tǒng)調(diào)用就會(huì)直接返回。
如果符合發(fā)送條件,則開始調(diào)用
tcp_write_xmit
內(nèi)核函數(shù)。在這個(gè)函數(shù)中,會(huì)循環(huán)獲取Socket
發(fā)送隊(duì)列中待發(fā)送的sk_buffer
,然后進(jìn)行擁塞控制
以及滑動(dòng)窗口的管理
。將從
Socket
發(fā)送隊(duì)列中獲取到的sk_buffer
重新拷貝一份
,設(shè)置sk_buffer副本
中的TCP HEADER
。
sk_buffer
?內(nèi)部其實(shí)包含了網(wǎng)絡(luò)協(xié)議中所有的?header
。在設(shè)置?TCP HEADER
的時(shí)候,只是把指針指向?sk_buffer
的合適位置。后面再設(shè)置?IP HEADER
的時(shí)候,在把指針移動(dòng)一下就行,避免頻繁的內(nèi)存申請(qǐng)和拷貝,效率很高。

為什么不直接使用
Socket
發(fā)送隊(duì)列中的sk_buffer
而是需要拷貝一份呢?因?yàn)?code>TCP協(xié)議是支持丟包重傳
的,在沒有收到對(duì)端的ACK
之前,這個(gè)sk_buffer
是不能刪除的。內(nèi)核每次調(diào)用網(wǎng)卡發(fā)送數(shù)據(jù)的時(shí)候,實(shí)際上傳遞的是sk_buffer
的拷貝副本
,當(dāng)網(wǎng)卡把數(shù)據(jù)發(fā)送出去后,sk_buffer
拷貝副本會(huì)被釋放。當(dāng)收到對(duì)端的ACK
之后,Socket
發(fā)送隊(duì)列中的sk_buffer
才會(huì)被真正刪除。
當(dāng)設(shè)置完
TCP頭
后,內(nèi)核協(xié)議棧傳輸層
的事情就做完了,下面通過調(diào)用ip_queue_xmit
內(nèi)核函數(shù),正式來到內(nèi)核協(xié)議棧網(wǎng)絡(luò)層
的處理。通過
route
命令可以查看本機(jī)路由配置。如果你使用?
iptables
配置了一些規(guī)則,那么這里將檢測(cè)是否命中
規(guī)則。如果你設(shè)置了非常復(fù)雜的 netfilter 規(guī)則
,在這個(gè)函數(shù)里將會(huì)導(dǎo)致你的線程?CPU 開銷
會(huì)極大增加
。將
sk_buffer
中的指針移動(dòng)到IP頭
位置上,設(shè)置IP頭
。執(zhí)行
netfilters
過濾。過濾通過之后,如果數(shù)據(jù)大于?MTU
的話,則執(zhí)行分片。檢查
Socket
中是否有緩存路由表,如果沒有的話,則查找路由項(xiàng),并緩存到Socket
中。接著在把路由表設(shè)置到sk_buffer
中。內(nèi)核協(xié)議棧
網(wǎng)絡(luò)層
的事情處理完后,現(xiàn)在發(fā)送流程進(jìn)入了到了鄰居子系統(tǒng)
,鄰居子系統(tǒng)
位于內(nèi)核協(xié)議棧中的網(wǎng)絡(luò)層
和網(wǎng)絡(luò)接口層
之間,用于發(fā)送ARP請(qǐng)求
獲取MAC地址
,然后將sk_buffer
中的指針移動(dòng)到MAC頭
位置,填充MAC頭
。經(jīng)過
鄰居子系統(tǒng)
的處理,現(xiàn)在sk_buffer
中已經(jīng)封裝了一個(gè)完整的數(shù)據(jù)幀
,隨后內(nèi)核將sk_buffer
交給網(wǎng)絡(luò)設(shè)備子系統(tǒng)
進(jìn)行處理。網(wǎng)絡(luò)設(shè)備子系統(tǒng)
主要做以下幾項(xiàng)事情:選擇發(fā)送隊(duì)列(
RingBuffer
)。因?yàn)榫W(wǎng)卡擁有多個(gè)發(fā)送隊(duì)列,所以在發(fā)送前需要選擇一個(gè)發(fā)送隊(duì)列。將
sk_buffer
添加到發(fā)送隊(duì)列中。循環(huán)從發(fā)送隊(duì)列(
RingBuffer
)中取出sk_buffer
,調(diào)用內(nèi)核函數(shù)sch_direct_xmit
發(fā)送數(shù)據(jù),其中會(huì)調(diào)用網(wǎng)卡驅(qū)動(dòng)程序
來發(fā)送數(shù)據(jù)。
以上過程全部是用戶線程的內(nèi)核態(tài)在執(zhí)行,占用的CPU時(shí)間是系統(tǒng)態(tài)時(shí)間(
sy
),當(dāng)分配給用戶線程的CPU quota
用完的時(shí)候,會(huì)觸發(fā)NET_TX_SOFTIRQ
類型的軟中斷,內(nèi)核線程ksoftirqd
會(huì)響應(yīng)這個(gè)軟中斷,并執(zhí)行NET_TX_SOFTIRQ
類型的軟中斷注冊(cè)的回調(diào)函數(shù)net_tx_action
,在回調(diào)函數(shù)中會(huì)執(zhí)行到驅(qū)動(dòng)程序函數(shù)?dev_hard_start_xmit
來發(fā)送數(shù)據(jù)。
注意:當(dāng)觸發(fā)
NET_TX_SOFTIRQ
軟中斷來發(fā)送數(shù)據(jù)時(shí),后邊消耗的 CPU 就都顯示在?si
這里了,不會(huì)消耗用戶進(jìn)程的系統(tǒng)態(tài)時(shí)間(sy
)了。
從這里可以看到網(wǎng)絡(luò)包的發(fā)送過程和接受過程是不同的,在介紹網(wǎng)絡(luò)包的接受過程時(shí),我們提到是通過觸發(fā)
NET_RX_SOFTIRQ
類型的軟中斷在內(nèi)核線程ksoftirqd
中執(zhí)行內(nèi)核網(wǎng)絡(luò)協(xié)議棧
接受數(shù)據(jù)。而在網(wǎng)絡(luò)數(shù)據(jù)包的發(fā)送過程中是用戶線程的內(nèi)核態(tài)
在執(zhí)行內(nèi)核網(wǎng)絡(luò)協(xié)議棧
,只有當(dāng)線程的CPU quota
用盡時(shí),才觸發(fā)NET_TX_SOFTIRQ
軟中斷來發(fā)送數(shù)據(jù)。
在整個(gè)網(wǎng)絡(luò)包的發(fā)送和接受過程中,
NET_TX_SOFTIRQ
類型的軟中斷只會(huì)在發(fā)送網(wǎng)絡(luò)包時(shí)并且當(dāng)用戶線程的CPU quota
用盡時(shí),才會(huì)觸發(fā)。剩下的接受過程中觸發(fā)的軟中斷類型以及發(fā)送完數(shù)據(jù)觸發(fā)的軟中斷類型均為NET_RX_SOFTIRQ
。所以這就是你在服務(wù)器上查看?/proc/softirqs
,一般?NET_RX
都要比?NET_TX
大很多的的原因。
現(xiàn)在發(fā)送流程終于到了網(wǎng)卡真實(shí)發(fā)送數(shù)據(jù)的階段,前邊我們講到無論是用戶線程的內(nèi)核態(tài)還是觸發(fā)
NET_TX_SOFTIRQ
類型的軟中斷在發(fā)送數(shù)據(jù)的時(shí)候最終會(huì)調(diào)用到網(wǎng)卡的驅(qū)動(dòng)程序函數(shù)dev_hard_start_xmit
來發(fā)送數(shù)據(jù)。在網(wǎng)卡驅(qū)動(dòng)程序函數(shù)dev_hard_start_xmit
中會(huì)將sk_buffer
映射到網(wǎng)卡可訪問的內(nèi)存 DMA 區(qū)域
,最終網(wǎng)卡驅(qū)動(dòng)程序通過DMA
的方式將數(shù)據(jù)幀
通過物理網(wǎng)卡發(fā)送出去。當(dāng)數(shù)據(jù)發(fā)送完畢后,還有最后一項(xiàng)重要的工作,就是清理工作。數(shù)據(jù)發(fā)送完畢后,網(wǎng)卡設(shè)備會(huì)向
CPU
發(fā)送一個(gè)硬中斷,CPU
調(diào)用網(wǎng)卡驅(qū)動(dòng)程序注冊(cè)的硬中斷響應(yīng)程序
,在硬中斷響應(yīng)中觸發(fā)NET_RX_SOFTIRQ
類型的軟中斷,在軟中斷的回調(diào)函數(shù)igb_poll
中清理釋放?sk_buffer
,清理網(wǎng)卡
發(fā)送隊(duì)列(RingBuffer
),解除 DMA 映射。
無論
硬中斷
是因?yàn)?code>有數(shù)據(jù)要接收,還是說發(fā)送完成通知
,從硬中斷觸發(fā)的軟中斷都是?NET_RX_SOFTIRQ
。
這里釋放清理的只是
sk_buffer
的副本,真正的sk_buffer
現(xiàn)在還是存放在Socket
的發(fā)送隊(duì)列中。前面在傳輸層
處理的時(shí)候我們提到過,因?yàn)閭鬏攲有枰?code>保證可靠性,所以?sk_buffer
其實(shí)還沒有刪除。它得等收到對(duì)方的 ACK 之后才會(huì)真正刪除。
性能開銷
前邊我們提到了在網(wǎng)絡(luò)包接收過程中涉及到的性能開銷,現(xiàn)在介紹完了網(wǎng)絡(luò)包的發(fā)送過程,我們來看下在數(shù)據(jù)包發(fā)送過程中的性能開銷:
和接收數(shù)據(jù)一樣,應(yīng)用程序在調(diào)用
系統(tǒng)調(diào)用send
的時(shí)候會(huì)從用戶態(tài)
轉(zhuǎn)為內(nèi)核態(tài)
以及發(fā)送完數(shù)據(jù)后,系統(tǒng)調(diào)用
返回時(shí)從內(nèi)核態(tài)
轉(zhuǎn)為用戶態(tài)
的開銷。用戶線程內(nèi)核態(tài)
CPU quota
用盡時(shí)觸發(fā)NET_TX_SOFTIRQ
類型軟中斷,內(nèi)核響應(yīng)軟中斷的開銷。網(wǎng)卡發(fā)送完數(shù)據(jù),向
CPU
發(fā)送硬中斷,CPU
響應(yīng)硬中斷的開銷。以及在硬中斷中發(fā)送NET_RX_SOFTIRQ
軟中斷執(zhí)行具體的內(nèi)存清理動(dòng)作。內(nèi)核響應(yīng)軟中斷的開銷。內(nèi)存拷貝的開銷。我們來回顧下在數(shù)據(jù)包發(fā)送的過程中都發(fā)生了哪些內(nèi)存拷貝:
在內(nèi)核協(xié)議棧的傳輸層中,
TCP協(xié)議
對(duì)應(yīng)的發(fā)送函數(shù)tcp_sendmsg
會(huì)申請(qǐng)sk_buffer
,將用戶要發(fā)送的數(shù)據(jù)拷貝
到sk_buffer
中。在發(fā)送流程從傳輸層到網(wǎng)絡(luò)層的時(shí)候,會(huì)
拷貝
一個(gè)sk_buffer副本
出來,將這個(gè)sk_buffer副本
向下傳遞。原始sk_buffer
保留在Socket
發(fā)送隊(duì)列中,等待網(wǎng)絡(luò)對(duì)端ACK
,對(duì)端ACK
后刪除Socket
發(fā)送隊(duì)列中的sk_buffer
。對(duì)端沒有發(fā)送ACK
,則重新從Socket
發(fā)送隊(duì)列中發(fā)送,實(shí)現(xiàn)TCP協(xié)議
的可靠傳輸。在網(wǎng)絡(luò)層,如果發(fā)現(xiàn)要發(fā)送的數(shù)據(jù)大于
MTU
,則會(huì)進(jìn)行分片操作,申請(qǐng)額外的sk_buffer
,并將原來的sk_buffer拷貝
到多個(gè)小的sk_buffer中。
再談(阻塞,非阻塞)與(同步,異步)
在我們聊完網(wǎng)絡(luò)數(shù)據(jù)的接收和發(fā)送過程后,我們來談下IO中特別容易混淆的概念:阻塞與同步
,非阻塞與異步
。
網(wǎng)上各種博文還有各種書籍中有大量的關(guān)于這兩個(gè)概念的解釋,但是筆者覺得還是不夠形象化,只是對(duì)概念的生硬解釋,如果硬套概念的話,其實(shí)感覺阻塞與同步
,非阻塞與異步
還是沒啥區(qū)別,時(shí)間長(zhǎng)了,還是比較模糊容易混淆。
所以筆者在這里嘗試換一種更加形象化,更加容易理解記憶的方式來清晰地解釋下什么是阻塞與非阻塞
,什么是同步與異步
。
經(jīng)過前邊對(duì)網(wǎng)絡(luò)數(shù)據(jù)包接收流程的介紹,在這里我們可以將整個(gè)流程總結(jié)為兩個(gè)階段:

數(shù)據(jù)準(zhǔn)備階段:?在這個(gè)階段,網(wǎng)絡(luò)數(shù)據(jù)包到達(dá)網(wǎng)卡,通過
DMA
的方式將數(shù)據(jù)包拷貝到內(nèi)存中,然后經(jīng)過硬中斷,軟中斷,接著通過內(nèi)核線程ksoftirqd
經(jīng)過內(nèi)核協(xié)議棧的處理,最終將數(shù)據(jù)發(fā)送到內(nèi)核Socket
的接收緩沖區(qū)中。數(shù)據(jù)拷貝階段:?當(dāng)數(shù)據(jù)到達(dá)
內(nèi)核Socket
的接收緩沖區(qū)中時(shí),此時(shí)數(shù)據(jù)存在于內(nèi)核空間
中,需要將數(shù)據(jù)拷貝
到用戶空間
中,才能夠被應(yīng)用程序讀取。
阻塞與非阻塞
阻塞與非阻塞的區(qū)別主要發(fā)生在第一階段:數(shù)據(jù)準(zhǔn)備階段
。
當(dāng)應(yīng)用程序發(fā)起系統(tǒng)調(diào)用read
時(shí),線程從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài),讀取內(nèi)核Socket
的接收緩沖區(qū)中的網(wǎng)絡(luò)數(shù)據(jù)。
阻塞
如果這時(shí)內(nèi)核Socket
的接收緩沖區(qū)沒有數(shù)據(jù),那么線程就會(huì)一直等待
,直到Socket
接收緩沖區(qū)有數(shù)據(jù)為止。隨后將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,系統(tǒng)調(diào)用read
返回。

從圖中我們可以看出:阻塞的特點(diǎn)是在第一階段和第二階段都會(huì)等待
。
非阻塞
阻塞
和非阻塞
主要的區(qū)分是在第一階段:數(shù)據(jù)準(zhǔn)備階段
。
在第一階段,當(dāng)
Socket
的接收緩沖區(qū)中沒有數(shù)據(jù)的時(shí)候,阻塞模式下
應(yīng)用線程會(huì)一直等待。非阻塞模式下
應(yīng)用線程不會(huì)等待,系統(tǒng)調(diào)用
直接返回錯(cuò)誤標(biāo)志EWOULDBLOCK
。當(dāng)
Socket
的接收緩沖區(qū)中有數(shù)據(jù)的時(shí)候,阻塞
和非阻塞
的表現(xiàn)是一樣的,都會(huì)進(jìn)入第二階段等待
數(shù)據(jù)從內(nèi)核空間
拷貝到用戶空間
,然后系統(tǒng)調(diào)用返回
。

從上圖中,我們可以看出:非阻塞的特點(diǎn)是第一階段不會(huì)等待
,但是在第二階段還是會(huì)等待
。
同步與異步
同步
與異步
主要的區(qū)別發(fā)生在第二階段:數(shù)據(jù)拷貝階段
。
前邊我們提到在數(shù)據(jù)拷貝階段
主要是將數(shù)據(jù)從內(nèi)核空間
拷貝到用戶空間
。然后應(yīng)用程序才可以讀取數(shù)據(jù)。
當(dāng)內(nèi)核Socket
的接收緩沖區(qū)有數(shù)據(jù)到達(dá)時(shí),進(jìn)入第二階段。
同步
同步模式
在數(shù)據(jù)準(zhǔn)備好后,是由用戶線程
的內(nèi)核態(tài)
來執(zhí)行第二階段
。所以應(yīng)用程序會(huì)在第二階段發(fā)生阻塞
,直到數(shù)據(jù)從內(nèi)核空間
拷貝到用戶空間
,系統(tǒng)調(diào)用才會(huì)返回。
Linux下的?epoll
和Mac 下的?kqueue
都屬于同步 IO
。

異步
異步模式
下是由內(nèi)核
來執(zhí)行第二階段的數(shù)據(jù)拷貝操作,當(dāng)內(nèi)核
執(zhí)行完第二階段,會(huì)通知用戶線程IO操作已經(jīng)完成,并將數(shù)據(jù)回調(diào)給用戶線程。所以在異步模式
下?數(shù)據(jù)準(zhǔn)備階段
和數(shù)據(jù)拷貝階段
均是由內(nèi)核
來完成,不會(huì)對(duì)應(yīng)用程序造成任何阻塞。
基于以上特征,我們可以看到異步模式
需要內(nèi)核的支持,比較依賴操作系統(tǒng)底層的支持。
在目前流行的操作系統(tǒng)中,只有Windows 中的?IOCP
才真正屬于異步 IO,實(shí)現(xiàn)的也非常成熟。但Windows很少用來作為服務(wù)器使用。
而常用來作為服務(wù)器使用的Linux,異步IO機(jī)制
實(shí)現(xiàn)的不夠成熟,與NIO相比性能提升的也不夠明顯。
但Linux kernel 在5.1版本由Facebook的大神Jens Axboe引入了新的異步IO庫(kù)io_uring
?改善了原來Linux native AIO的一些性能問題。性能相比Epoll
以及之前原生的AIO
提高了不少,值得關(guān)注。

IO模型
在進(jìn)行網(wǎng)絡(luò)IO操作時(shí),用什么樣的IO模型來讀寫數(shù)據(jù)將在很大程度上決定了網(wǎng)絡(luò)框架的IO性能。所以IO模型的選擇是構(gòu)建一個(gè)高性能網(wǎng)絡(luò)框架的基礎(chǔ)。
在《UNIX 網(wǎng)絡(luò)編程》一書中介紹了五種IO模型:阻塞IO
,非阻塞IO
,IO多路復(fù)用
,信號(hào)驅(qū)動(dòng)IO
,異步IO
,每一種IO模型的出現(xiàn)都是對(duì)前一種的升級(jí)優(yōu)化。
下面我們就來分別介紹下這五種IO模型各自都解決了什么問題,適用于哪些場(chǎng)景,各自的優(yōu)缺點(diǎn)是什么?
阻塞IO(BIO)

經(jīng)過前一小節(jié)對(duì)阻塞
這個(gè)概念的介紹,相信大家可以很容易理解阻塞IO
的概念和過程。
既然這小節(jié)我們談的是IO
,那么下邊我們來看下在阻塞IO
模型下,網(wǎng)絡(luò)數(shù)據(jù)的讀寫過程。
阻塞讀
當(dāng)用戶線程發(fā)起read
系統(tǒng)調(diào)用,用戶線程從用戶態(tài)切換到內(nèi)核態(tài),在內(nèi)核中去查看Socket
接收緩沖區(qū)是否有數(shù)據(jù)到來。
Socket
接收緩沖區(qū)中有數(shù)據(jù)
,則用戶線程在內(nèi)核態(tài)將內(nèi)核空間中的數(shù)據(jù)拷貝到用戶空間,系統(tǒng)IO調(diào)用返回。Socket
接收緩沖區(qū)中無數(shù)據(jù)
,則用戶線程讓出CPU,進(jìn)入阻塞狀態(tài)
。當(dāng)數(shù)據(jù)到達(dá)Socket
接收緩沖區(qū)后,內(nèi)核喚醒阻塞狀態(tài)
中的用戶線程進(jìn)入就緒狀態(tài)
,隨后經(jīng)過CPU的調(diào)度獲取到CPU quota
進(jìn)入運(yùn)行狀態(tài)
,將內(nèi)核空間的數(shù)據(jù)拷貝到用戶空間,隨后系統(tǒng)調(diào)用返回。
阻塞寫
當(dāng)用戶線程發(fā)起send
系統(tǒng)調(diào)用時(shí),用戶線程從用戶態(tài)切換到內(nèi)核態(tài),將發(fā)送數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間中的Socket
發(fā)送緩沖區(qū)中。
當(dāng)
Socket
發(fā)送緩沖區(qū)能夠容納下發(fā)送數(shù)據(jù)時(shí),用戶線程會(huì)將全部的發(fā)送數(shù)據(jù)寫入Socket
緩沖區(qū),然后執(zhí)行在《網(wǎng)絡(luò)包發(fā)送流程》這小節(jié)介紹的后續(xù)流程,然后返回。當(dāng)
Socket
發(fā)送緩沖區(qū)空間不夠,無法容納下全部發(fā)送數(shù)據(jù)時(shí),用戶線程讓出CPU,進(jìn)入阻塞狀態(tài)
,直到Socket
發(fā)送緩沖區(qū)能夠容納下全部發(fā)送數(shù)據(jù)時(shí),內(nèi)核喚醒用戶線程,執(zhí)行后續(xù)發(fā)送流程。
阻塞IO
模型下的寫操作做事風(fēng)格比較硬剛,非得要把全部的發(fā)送數(shù)據(jù)寫入發(fā)送緩沖區(qū)才肯善罷甘休。
阻塞IO模型

由于阻塞IO
的讀寫特點(diǎn),所以導(dǎo)致在阻塞IO
模型下,每個(gè)請(qǐng)求都需要被一個(gè)獨(dú)立的線程處理。一個(gè)線程在同一時(shí)刻只能與一個(gè)連接綁定。來一個(gè)請(qǐng)求,服務(wù)端就需要?jiǎng)?chuàng)建一個(gè)線程用來處理請(qǐng)求。
當(dāng)客戶端請(qǐng)求的并發(fā)量突然增大時(shí),服務(wù)端在一瞬間就會(huì)創(chuàng)建出大量的線程,而創(chuàng)建線程是需要系統(tǒng)資源開銷的,這樣一來就會(huì)一瞬間占用大量的系統(tǒng)資源。
如果客戶端創(chuàng)建好連接后,但是一直不發(fā)數(shù)據(jù),通常大部分情況下,網(wǎng)絡(luò)連接也并不
總是有數(shù)據(jù)可讀,那么在空閑的這段時(shí)間內(nèi),服務(wù)端線程就會(huì)一直處于阻塞狀態(tài)
,無法干其他的事情。CPU也無法得到充分的發(fā)揮
,同時(shí)還會(huì)導(dǎo)致大量線程切換的開銷
。
適用場(chǎng)景
基于以上阻塞IO模型
的特點(diǎn),該模型只適用于連接數(shù)少
,并發(fā)度低
的業(yè)務(wù)場(chǎng)景。
比如公司內(nèi)部的一些管理系統(tǒng),通常請(qǐng)求數(shù)在100個(gè)左右,使用阻塞IO模型
還是非常適合的。而且性能還不輸NIO。
該模型在C10K之前,是普遍被采用的一種IO模型。
非阻塞IO(NIO)
阻塞IO模型
最大的問題就是一個(gè)線程只能處理一個(gè)連接,如果這個(gè)連接上沒有數(shù)據(jù)的話,那么這個(gè)線程就只能阻塞在系統(tǒng)IO調(diào)用上,不能干其他的事情。這對(duì)系統(tǒng)資源來說,是一種極大的浪費(fèi)。同時(shí)大量的線程上下文切換,也是一個(gè)巨大的系統(tǒng)開銷。
所以為了解決這個(gè)問題,我們就需要用盡可能少的線程去處理更多的連接。,網(wǎng)絡(luò)IO模型的演變
也是根據(jù)這個(gè)需求來一步一步演進(jìn)的。
基于這個(gè)需求,第一種解決方案非阻塞IO
就出現(xiàn)了。我們?cè)谏弦恍」?jié)中介紹了非阻塞
的概念,現(xiàn)在我們來看下網(wǎng)絡(luò)讀寫操作在非阻塞IO
下的特點(diǎn):

非阻塞讀
當(dāng)用戶線程發(fā)起非阻塞read
系統(tǒng)調(diào)用時(shí),用戶線程從用戶態(tài)
轉(zhuǎn)為內(nèi)核態(tài)
,在內(nèi)核中去查看Socket
接收緩沖區(qū)是否有數(shù)據(jù)到來。
Socket
接收緩沖區(qū)中無數(shù)據(jù)
,系統(tǒng)調(diào)用立馬返回,并帶有一個(gè)?EWOULDBLOCK
?或?EAGAIN
錯(cuò)誤,這個(gè)階段用戶線程不會(huì)阻塞
,也不會(huì)讓出CPU
,而是會(huì)繼續(xù)輪訓(xùn)
直到Socket
接收緩沖區(qū)中有數(shù)據(jù)為止。Socket
接收緩沖區(qū)中有數(shù)據(jù)
,用戶線程在內(nèi)核態(tài)
會(huì)將內(nèi)核空間
中的數(shù)據(jù)拷貝到用戶空間
,注意這個(gè)數(shù)據(jù)拷貝階段,應(yīng)用程序是阻塞的
,當(dāng)數(shù)據(jù)拷貝完成,系統(tǒng)調(diào)用返回。
非阻塞寫
前邊我們?cè)诮榻B阻塞寫
的時(shí)候提到阻塞寫
的風(fēng)格特別的硬朗,頭比較鐵非要把全部發(fā)送數(shù)據(jù)一次性都寫到Socket
的發(fā)送緩沖區(qū)中才返回,如果發(fā)送緩沖區(qū)中沒有足夠的空間容納,那么就一直阻塞死等,特別的剛。
相比較而言非阻塞寫
的特點(diǎn)就比較佛系,當(dāng)發(fā)送緩沖區(qū)中沒有足夠的空間容納全部發(fā)送數(shù)據(jù)時(shí),非阻塞寫
的特點(diǎn)是能寫多少寫多少
,寫不下了,就立即返回。并將寫入到發(fā)送緩沖區(qū)的字節(jié)數(shù)返回給應(yīng)用程序,方便用戶線程不斷的輪訓(xùn)
嘗試將剩下的數(shù)據(jù)
寫入發(fā)送緩沖區(qū)中。
非阻塞IO模型

基于以上非阻塞IO
的特點(diǎn),我們就不必像阻塞IO
那樣為每個(gè)請(qǐng)求分配一個(gè)線程去處理連接上的讀寫了。
我們可以利用一個(gè)線程或者很少的線程,去不斷地輪詢
每個(gè)Socket
的接收緩沖區(qū)是否有數(shù)據(jù)到達(dá),如果沒有數(shù)據(jù),不必阻塞
線程,而是接著去輪詢
下一個(gè)Socket
接收緩沖區(qū),直到輪詢到數(shù)據(jù)后,處理連接上的讀寫,或者交給業(yè)務(wù)線程池去處理,輪詢線程則繼續(xù)輪詢
其他的Socket
接收緩沖區(qū)。
這樣一個(gè)非阻塞IO模型
就實(shí)現(xiàn)了我們?cè)诒拘」?jié)開始提出的需求:我們需要用盡可能少的線程去處理更多的連接
適用場(chǎng)景
雖然非阻塞IO模型
與阻塞IO模型
相比,減少了很大一部分的資源消耗和系統(tǒng)開銷。
但是它仍然有很大的性能問題,因?yàn)樵?code>非阻塞IO模型下,需要用戶線程去不斷地
發(fā)起系統(tǒng)調(diào)用
去輪訓(xùn)Socket
接收緩沖區(qū),這就需要用戶線程不斷地從用戶態(tài)
切換到內(nèi)核態(tài)
,內(nèi)核態(tài)
切換到用戶態(tài)
。隨著并發(fā)量的增大,這個(gè)上下文切換的開銷也是巨大的。
所以單純的非阻塞IO
模型還是無法適用于高并發(fā)的場(chǎng)景。只能適用于C10K
以下的場(chǎng)景。
IO多路復(fù)用
在非阻塞IO
這一小節(jié)的開頭,我們提到網(wǎng)絡(luò)IO模型
的演變都是圍繞著---如何用盡可能少的線程去處理更多的連接這個(gè)核心需求開始展開的。
本小節(jié)我們來談?wù)?code>IO多路復(fù)用模型,那么什么是多路
?,什么又是復(fù)用
呢?
我們還是以這個(gè)核心需求來對(duì)這兩個(gè)概念展開闡述:
多路:我們的核心需求是要用盡可能少的線程來處理盡可能多的連接,這里的
多路
指的就是我們需要處理的眾多連接。復(fù)用:核心需求要求我們使用
盡可能少的線程
,盡可能少的系統(tǒng)開銷
去處理盡可能多
的連接(多路
),那么這里的復(fù)用
指的就是用有限的資源
,比如用一個(gè)線程或者固定數(shù)量的線程去處理眾多連接上的讀寫事件。換句話說,在阻塞IO模型
中一個(gè)連接就需要分配一個(gè)獨(dú)立的線程去專門處理這個(gè)連接上的讀寫,到了IO多路復(fù)用模型
中,多個(gè)連接可以復(fù)用
這一個(gè)獨(dú)立的線程去處理這多個(gè)連接上的讀寫。
好了,IO多路復(fù)用模型
的概念解釋清楚了,那么問題的關(guān)鍵是我們?nèi)绾稳?shí)現(xiàn)這個(gè)復(fù)用
,也就是如何讓一個(gè)獨(dú)立的線程去處理眾多連接上的讀寫事件呢?
這個(gè)問題其實(shí)在非阻塞IO模型
中已經(jīng)給出了它的答案,在非阻塞IO模型
中,利用非阻塞
的系統(tǒng)IO調(diào)用去不斷的輪詢眾多連接的Socket
接收緩沖區(qū)看是否有數(shù)據(jù)到來,如果有則處理,如果沒有則繼續(xù)輪詢下一個(gè)Socket
。這樣就達(dá)到了用一個(gè)線程去處理眾多連接上的讀寫事件了。
但是非阻塞IO模型
最大的問題就是需要不斷的發(fā)起系統(tǒng)調(diào)用
去輪詢各個(gè)Socket
中的接收緩沖區(qū)是否有數(shù)據(jù)到來,頻繁
的系統(tǒng)調(diào)用
隨之帶來了大量的上下文切換開銷。隨著并發(fā)量的提升,這樣也會(huì)導(dǎo)致非常嚴(yán)重的性能問題。
那么如何避免頻繁的系統(tǒng)調(diào)用同時(shí)又可以實(shí)現(xiàn)我們的核心需求呢?
這就需要操作系統(tǒng)的內(nèi)核來支持這樣的操作,我們可以把頻繁的輪詢操作交給操作系統(tǒng)內(nèi)核來替我們完成,這樣就避免了在用戶空間
頻繁的去使用系統(tǒng)調(diào)用來輪詢所帶來的性能開銷。
正如我們所想,操作系統(tǒng)內(nèi)核也確實(shí)為我們提供了這樣的功能實(shí)現(xiàn),下面我們來一起看下操作系統(tǒng)對(duì)IO多路復(fù)用模型
的實(shí)現(xiàn)。
select
select
是操作系統(tǒng)內(nèi)核提供給我們使用的一個(gè)系統(tǒng)調(diào)用
,它解決了在非阻塞IO模型
中需要不斷的發(fā)起系統(tǒng)IO調(diào)用
去輪詢各個(gè)連接上的Socket
接收緩沖區(qū)所帶來的用戶空間
與內(nèi)核空間
不斷切換的系統(tǒng)開銷
。
select
系統(tǒng)調(diào)用將輪詢
的操作交給了內(nèi)核
來幫助我們完成,從而避免了在用戶空間
不斷的發(fā)起輪詢所帶來的的系統(tǒng)性能開銷。

首先用戶線程在發(fā)起
select
系統(tǒng)調(diào)用的時(shí)候會(huì)阻塞
在select
系統(tǒng)調(diào)用上。此時(shí),用戶線程從用戶態(tài)
切換到了內(nèi)核態(tài)
完成了一次上下文切換
用戶線程將需要監(jiān)聽的
Socket
對(duì)應(yīng)的文件描述符fd
數(shù)組通過select
系統(tǒng)調(diào)用傳遞給內(nèi)核。此時(shí),用戶線程將用戶空間
中的文件描述符fd
數(shù)組拷貝
到內(nèi)核空間
。
這里的文件描述符數(shù)組其實(shí)是一個(gè)BitMap
,BitMap
下標(biāo)為文件描述符fd
,下標(biāo)對(duì)應(yīng)的值為:1
表示該fd
上有讀寫事件,0
表示該fd
上沒有讀寫事件。

文件描述符fd其實(shí)就是一個(gè)整數(shù)值
,在Linux中一切皆文件,Socket
也是一個(gè)文件。描述進(jìn)程所有信息的數(shù)據(jù)結(jié)構(gòu)task_struct
中有一個(gè)屬性struct files_struct *files
,它最終指向了一個(gè)數(shù)組,數(shù)組里存放了進(jìn)程打開的所有文件列表,文件信息封裝在struct file
結(jié)構(gòu)體中,這個(gè)數(shù)組存放的類型就是struct file
結(jié)構(gòu)體,數(shù)組的下標(biāo)
則是我們常說的文件描述符fd
。
當(dāng)用戶線程調(diào)用完
select
后開始進(jìn)入阻塞狀態(tài)
,內(nèi)核
開始輪詢遍歷fd
數(shù)組,查看fd
對(duì)應(yīng)的Socket
接收緩沖區(qū)中是否有數(shù)據(jù)到來。如果有數(shù)據(jù)到來,則將fd
對(duì)應(yīng)BitMap
的值設(shè)置為1
。如果沒有數(shù)據(jù)到來,則保持值為0
。
注意這里內(nèi)核會(huì)修改原始的
fd
數(shù)組??!
內(nèi)核遍歷一遍
fd
數(shù)組后,如果發(fā)現(xiàn)有些fd
上有IO數(shù)據(jù)到來,則將修改后的fd
數(shù)組返回給用戶線程。此時(shí),會(huì)將fd
數(shù)組從內(nèi)核空間
拷貝到用戶空間
。當(dāng)內(nèi)核將修改后的
fd
數(shù)組返回給用戶線程后,用戶線程解除阻塞
,由用戶線程開始遍歷fd
數(shù)組然后找出fd
數(shù)組中值為1
的Socket
文件描述符。最后對(duì)這些Socket
發(fā)起系統(tǒng)調(diào)用讀取數(shù)據(jù)。
select
不會(huì)告訴用戶線程具體哪些fd
上有IO數(shù)據(jù)到來,只是在IO活躍
的fd
上打上標(biāo)記,將打好標(biāo)記的完整fd
數(shù)組返回給用戶線程,所以用戶線程還需要遍歷fd
數(shù)組找出具體哪些fd
上有IO數(shù)據(jù)
到來。
由于內(nèi)核在遍歷的過程中已經(jīng)修改了
fd
數(shù)組,所以在用戶線程遍歷完fd
數(shù)組后獲取到IO就緒
的Socket
后,就需要重置
fd數(shù)組,并重新調(diào)用select
傳入重置后的fd
數(shù)組,讓內(nèi)核發(fā)起新的一輪遍歷輪詢。
API介紹
當(dāng)我們熟悉了select
的原理后,就很容易理解內(nèi)核給我們提供的select API
了。
從select API
中我們可以看到,select
系統(tǒng)調(diào)用是在規(guī)定的超時(shí)時(shí)間內(nèi)
,監(jiān)聽(輪詢
)用戶感興趣的文件描述符集合上的可讀
,可寫
,異常
三類事件。
maxfdp1 :
?select傳遞給內(nèi)核監(jiān)聽的文件描述符集合中數(shù)值最大的文件描述符+1
,目的是用于限定內(nèi)核遍歷范圍。比如:select
監(jiān)聽的文件描述符集合為{0,1,2,3,4}
,那么maxfdp1
的值為5
。fd_set *readset:
?對(duì)可讀事件
感興趣的文件描述符集合。fd_set *writeset:
?對(duì)可寫事件
感興趣的文件描述符集合。fd_set *exceptset:
對(duì)異常事件
感興趣的文件描述符集合。
這里的
fd_set
就是我們前邊提到的文件描述符數(shù)組
,是一個(gè)BitMap
結(jié)構(gòu)。
const struct timeval *timeout:
select系統(tǒng)調(diào)用超時(shí)時(shí)間,在這段時(shí)間內(nèi),內(nèi)核如果沒有發(fā)現(xiàn)有IO就緒
的文件描述符,就直接返回。
上小節(jié)提到,在內(nèi)核
遍歷完fd
數(shù)組后,發(fā)現(xiàn)有IO就緒
的fd
,則會(huì)將該fd
對(duì)應(yīng)的BitMap
中的值設(shè)置為1
,并將修改后的fd
數(shù)組,返回給用戶線程。
在用戶線程中需要重新遍歷fd
數(shù)組,找出IO就緒
的fd
出來,然后發(fā)起真正的讀寫調(diào)用。
下面介紹下在用戶線程中重新遍歷fd
數(shù)組的過程中,我們需要用到的API
:
void FD_ZERO(fd_set *fdset):
清空指定的文件描述符集合,即讓fd_set
中不在包含任何文件描述符。void FD_SET(int fd, fd_set *fdset):
將一個(gè)給定的文件描述符加入集合之中。
每次調(diào)用
select
之前都要通過FD_ZERO
和FD_SET
重新設(shè)置文件描述符,因?yàn)槲募枋龇蠒?huì)在內(nèi)核
中被修改
。
int FD_ISSET(int fd, fd_set *fdset):
檢查集合中指定的文件描述符是否可以讀寫。用戶線程遍歷
文件描述符集合,調(diào)用該方法檢查相應(yīng)的文件描述符是否IO就緒
。void FD_CLR(int fd, fd_set *fdset):
將一個(gè)給定的文件描述符從集合中刪除
性能開銷
雖然select
解決了非阻塞IO模型
中頻繁發(fā)起系統(tǒng)調(diào)用
的問題,但是在整個(gè)select
工作過程中,我們還是看出了select
有些不足的地方。
在發(fā)起
select
系統(tǒng)調(diào)用以及返回時(shí),用戶線程各發(fā)生了一次用戶態(tài)
到內(nèi)核態(tài)
以及內(nèi)核態(tài)
到用戶態(tài)
的上下文切換開銷。發(fā)生2次上下文切換
在發(fā)起
select
系統(tǒng)調(diào)用以及返回時(shí),用戶線程在內(nèi)核態(tài)
需要將文件描述符集合
從用戶空間拷貝
到內(nèi)核空間。以及在內(nèi)核修改完文件描述符集合
后,又要將它從內(nèi)核空間拷貝
到用戶空間。發(fā)生2次文件描述符集合的拷貝
雖然由原來在
用戶空間
發(fā)起輪詢優(yōu)化成了
在內(nèi)核空間
發(fā)起輪詢但select
不會(huì)告訴用戶線程到底是哪些Socket
上發(fā)生了IO就緒
事件,只是對(duì)IO就緒
的Socket
作了標(biāo)記,用戶線程依然要遍歷
文件描述符集合去查找具體IO就緒
的Socket
。時(shí)間復(fù)雜度依然為O(n)
。
大部分情況下,網(wǎng)絡(luò)連接并不總是活躍的,如果
select
監(jiān)聽了大量的客戶端連接,只有少數(shù)的連接活躍,然而使用輪詢的這種方式會(huì)隨著連接數(shù)的增大,效率會(huì)越來越低。
內(nèi)核
會(huì)對(duì)原始的文件描述符集合
進(jìn)行修改。導(dǎo)致每次在用戶空間重新發(fā)起select
調(diào)用時(shí),都需要對(duì)文件描述符集合
進(jìn)行重置
。BitMap
結(jié)構(gòu)的文件描述符集合,長(zhǎng)度為固定的1024
,所以只能監(jiān)聽0~1023
的文件描述符。select
系統(tǒng)調(diào)用 不是線程安全的。
以上select
的不足所產(chǎn)生的性能開銷
都會(huì)隨著并發(fā)量的增大而線性增長(zhǎng)
。
很明顯select
也不能解決C10K
問題,只適用于1000
個(gè)左右的并發(fā)連接場(chǎng)景。
poll
poll
相當(dāng)于是改進(jìn)版的select
,但是工作原理基本和select
沒有本質(zhì)的區(qū)別。
select
中使用的文件描述符集合是采用的固定長(zhǎng)度為1024的BitMap
結(jié)構(gòu)的fd_set
,而poll
換成了一個(gè)pollfd
結(jié)構(gòu)沒有固定長(zhǎng)度的數(shù)組,這樣就沒有了最大描述符數(shù)量的限制(當(dāng)然還會(huì)受到系統(tǒng)文件描述符限制)
poll
只是改進(jìn)了select
只能監(jiān)聽1024
個(gè)文件描述符的數(shù)量限制,但是并沒有在性能方面做出改進(jìn)。和select
上本質(zhì)并沒有多大差別。
同樣需要在
內(nèi)核空間
和用戶空間
中對(duì)文件描述符集合進(jìn)行輪詢
,查找出IO就緒
的Socket
的時(shí)間復(fù)雜度依然為O(n)
。同樣需要將
包含大量文件描述符的集合
整體在用戶空間
和內(nèi)核空間
之間來回復(fù)制
,無論這些文件描述符是否就緒。他們的開銷都會(huì)隨著文件描述符數(shù)量的增加而線性增大。select,poll
在每次新增,刪除需要監(jiān)聽的socket時(shí),都需要將整個(gè)新的socket
集合全量傳至內(nèi)核
。
poll
同樣不適用高并發(fā)的場(chǎng)景。依然無法解決C10K
問題。
文章篇幅有限,下文繼續(xù)講解
原文作者:bin的技術(shù)小屋
下文:深入講解Netty那些事兒之從內(nèi)核角度看IO模型(下)
