這次答應(yīng)我鹅巍,一舉拿下 I/O 多路復(fù)用

這次,我們以最簡(jiǎn)單的方式 socket 網(wǎng)絡(luò)模型料祠,一步一步地過(guò)渡到 I/O 多路復(fù)用骆捧。

但我不會(huì)具體說(shuō)到每個(gè)系統(tǒng)調(diào)用的參數(shù),這方面書(shū)上肯定比我說(shuō)得詳細(xì)髓绽。

最基本的 Socket 模型

要想客戶(hù)端和服務(wù)器能在網(wǎng)絡(luò)中通信敛苇,那必須得使用 Socket 編程,它是進(jìn)程間通信里比較特別的方式顺呕,特別之處在于它是可以跨主機(jī)間通信枫攀。

Socket 的中文名叫作插口,乍一看還挺迷惑的株茶。事實(shí)上来涨,雙方要進(jìn)行網(wǎng)絡(luò)通信前,各自得創(chuàng)建一個(gè) Socket启盛,這相當(dāng)于客戶(hù)端和服務(wù)器都開(kāi)了一個(gè)“口子”蹦掐,雙方讀取和發(fā)送數(shù)據(jù)的時(shí)候,都通過(guò)這個(gè)“口子”僵闯。這樣一看卧抗,是不是覺(jué)得很像弄了一根網(wǎng)線,一頭插在客戶(hù)端鳖粟,一頭插在服務(wù)端社裆,然后進(jìn)行通信。

創(chuàng)建 Socket 的時(shí)候牺弹,可以指定網(wǎng)絡(luò)層使用的是 IPv4 還是 IPv6,傳輸層使用的是 TCP 還是 UDP时呀。

UDP 的 Socket 編程相對(duì)簡(jiǎn)單些张漂,這里我們只介紹基于 TCP 的 Socket 編程。

服務(wù)器的程序要先跑起來(lái)谨娜,然后等待客戶(hù)端的連接和數(shù)據(jù)航攒,我們先來(lái)看看服務(wù)端的 Socket 編程過(guò)程是怎樣的。

服務(wù)端首先調(diào)用?socket()?函數(shù)趴梢,創(chuàng)建網(wǎng)絡(luò)協(xié)議為 IPv4漠畜,以及傳輸協(xié)議為 TCP 的 Socket 币他,接著調(diào)用?bind()?函數(shù),給這個(gè) Socket 綁定一個(gè)?IP 地址和端口憔狞,綁定這兩個(gè)的目的是什么蝴悉?

綁定端口的目的:當(dāng)內(nèi)核收到 TCP 報(bào)文,通過(guò) TCP 頭里面的端口號(hào)瘾敢,來(lái)找到我們的應(yīng)用程序拍冠,然后把數(shù)據(jù)傳遞給我們。

綁定 IP 地址的目的:一臺(tái)機(jī)器是可以有多個(gè)網(wǎng)卡的簇抵,每個(gè)網(wǎng)卡都有對(duì)應(yīng)的 IP 地址庆杜,當(dāng)綁定一個(gè)網(wǎng)卡時(shí),內(nèi)核在收到該網(wǎng)卡上的包碟摆,才會(huì)發(fā)給我們晃财;

綁定完 IP 地址和端口后,就可以調(diào)用?listen()?函數(shù)進(jìn)行監(jiān)聽(tīng)典蜕,此時(shí)對(duì)應(yīng) TCP 狀態(tài)圖中的?listen断盛,如果我們要判定服務(wù)器中一個(gè)網(wǎng)絡(luò)程序有沒(méi)有啟動(dòng),可以通過(guò)?netstate?命令查看對(duì)應(yīng)的端口號(hào)是否有被監(jiān)聽(tīng)嘉裤。

服務(wù)端進(jìn)入了監(jiān)聽(tīng)狀態(tài)后郑临,通過(guò)調(diào)用?accept()?函數(shù),來(lái)從內(nèi)核獲取客戶(hù)端的連接屑宠,如果沒(méi)有客戶(hù)端連接厢洞,則會(huì)阻塞等待客戶(hù)端連接的到來(lái)。

那客戶(hù)端是怎么發(fā)起連接的呢典奉?客戶(hù)端在創(chuàng)建好 Socket 后躺翻,調(diào)用connect()?函數(shù)發(fā)起連接,該函數(shù)的參數(shù)要指明服務(wù)端的 IP 地址和端口號(hào)卫玖,然后萬(wàn)眾期待的 TCP 三次握手就開(kāi)始了公你。

在 TCP 連接的過(guò)程中,服務(wù)器的內(nèi)核實(shí)際上為每個(gè) Socket 維護(hù)了兩個(gè)隊(duì)列:

一個(gè)是還沒(méi)完全建立連接的隊(duì)列假瞬,稱(chēng)為?TCP 半連接隊(duì)列陕靠,這個(gè)隊(duì)列都是沒(méi)有完成三次握手的連接,此時(shí)服務(wù)端處于syn_rcvd?的狀態(tài)脱茉;

一個(gè)是一件建立連接的隊(duì)列剪芥,稱(chēng)為?TCP 全連接隊(duì)列,這個(gè)隊(duì)列都是完成了三次握手的連接琴许,此時(shí)服務(wù)端處于?established狀態(tài)税肪;

當(dāng) TCP 全連接隊(duì)列不為空后,服務(wù)端的?accept()?函數(shù),就會(huì)從內(nèi)核中的 TCP 全連接隊(duì)列里拿出一個(gè)已經(jīng)完成連接的 Socket 返回應(yīng)用程序益兄,后續(xù)數(shù)據(jù)傳輸都用這個(gè) Socket锻梳。

注意,監(jiān)聽(tīng)的 Socket 和真正用來(lái)傳數(shù)據(jù)的 Socket 是兩個(gè):

一個(gè)叫作監(jiān)聽(tīng) Socket净捅;

一個(gè)叫作已連接 Socket疑枯;

連接建立后,客戶(hù)端和服務(wù)端就開(kāi)始相互傳輸數(shù)據(jù)了灸叼,雙方都可以通過(guò)?read()?和?write()?函數(shù)來(lái)讀寫(xiě)數(shù)據(jù)神汹。

至此, TCP 協(xié)議的 Socket 程序的調(diào)用過(guò)程就結(jié)束了古今,整個(gè)過(guò)程如下圖:

看到這屁魏,不知道你有沒(méi)有覺(jué)得讀寫(xiě) Socket 的方式,好像讀寫(xiě)文件一樣捉腥。

是的氓拼,基于 Linux 一切皆文件的理念,在內(nèi)核中 Socket 也是以「文件」的形式存在的抵碟,也是有對(duì)應(yīng)的文件描述符桃漾。

PS : 下面會(huì)說(shuō)到內(nèi)核里的數(shù)據(jù)結(jié)構(gòu),不感興趣的可以跳過(guò)這一部分,不會(huì)對(duì)后續(xù)的內(nèi)容有影響。

文件描述符的作用是什么尚困?每一個(gè)進(jìn)程都有一個(gè)數(shù)據(jù)結(jié)構(gòu)task_struct性含,該結(jié)構(gòu)體里有一個(gè)指向「文件描述符數(shù)組」的成員指針蓖扑。該數(shù)組里列出這個(gè)進(jìn)程打開(kāi)的所有文件的文件描述符。數(shù)組的下標(biāo)是文件描述符,是一個(gè)整數(shù),而數(shù)組的內(nèi)容是一個(gè)指針苦囱,指向內(nèi)核中所有打開(kāi)的文件的列表,也就是說(shuō)內(nèi)核可以通過(guò)文件描述符找到對(duì)應(yīng)打開(kāi)的文件脾猛。

然后每個(gè)文件都有一個(gè) inode撕彤,Socket 文件的 inode 指向了內(nèi)核中的 Socket 結(jié)構(gòu),在這個(gè)結(jié)構(gòu)體里有兩個(gè)隊(duì)列猛拴,分別是發(fā)送隊(duì)列接收隊(duì)列羹铅,這個(gè)兩個(gè)隊(duì)列里面保存的是一個(gè)個(gè)?struct sk_buff,用鏈表的組織形式串起來(lái)愉昆。

sk_buff 可以表示各個(gè)層的數(shù)據(jù)包职员,在應(yīng)用層數(shù)據(jù)包叫 data,在 TCP 層我們稱(chēng)為 segment撼唾,在 IP 層我們叫 packet廉邑,在數(shù)據(jù)鏈路層稱(chēng)為 frame。

你可能會(huì)好奇倒谷,為什么全部數(shù)據(jù)包只用一個(gè)結(jié)構(gòu)體來(lái)描述呢蛛蒙?協(xié)議棧采用的是分層結(jié)構(gòu),上層向下層傳遞數(shù)據(jù)時(shí)需要增加包頭渤愁,下層向上層數(shù)據(jù)時(shí)又需要去掉包頭牵祟,如果每一層都用一個(gè)結(jié)構(gòu)體,那在層之間傳遞數(shù)據(jù)的時(shí)候抖格,就要發(fā)生多次拷貝诺苹,這將大大降低 CPU 效率。

于是雹拄,為了在層級(jí)之間傳遞數(shù)據(jù)時(shí)收奔,不發(fā)生拷貝,只用 sk_buff 一個(gè)結(jié)構(gòu)體來(lái)描述所有的網(wǎng)絡(luò)包滓玖,那它是如何做到的呢坪哄?是通過(guò)調(diào)整 sk_buff 中?data?的指針,比如:

當(dāng)接收?qǐng)?bào)文時(shí)势篡,從網(wǎng)卡驅(qū)動(dòng)開(kāi)始翩肌,通過(guò)協(xié)議棧層層往上傳送數(shù)據(jù)報(bào),通過(guò)增加 skb->data 的值禁悠,來(lái)逐步剝離協(xié)議首部念祭。

當(dāng)要發(fā)送報(bào)文時(shí),創(chuàng)建 sk_buff 結(jié)構(gòu)體碍侦,數(shù)據(jù)緩存區(qū)的頭部預(yù)留足夠的空間粱坤,用來(lái)填充各層首部,在經(jīng)過(guò)各下層協(xié)議時(shí)祝钢,通過(guò)減少 skb->data 的值來(lái)增加協(xié)議首部比规。

你可以從下面這張圖看到,當(dāng)發(fā)送報(bào)文時(shí)拦英,data 指針的移動(dòng)過(guò)程蜒什。

如何服務(wù)更多的用戶(hù)?

前面提到的 TCP Socket 調(diào)用流程是最簡(jiǎn)單疤估、最基本的灾常,它基本只能一對(duì)一通信,因?yàn)槭褂玫氖峭阶枞姆绞搅迥矗?dāng)服務(wù)端在還沒(méi)處理完一個(gè)客戶(hù)端的網(wǎng)絡(luò) I/O 時(shí)钞瀑,或者 讀寫(xiě)操作發(fā)生阻塞時(shí),其他客戶(hù)端是無(wú)法與服務(wù)端連接的慷荔。

可如果我們服務(wù)器只能服務(wù)一個(gè)客戶(hù)雕什,那這樣就太浪費(fèi)資源了,于是我們要改進(jìn)這個(gè)網(wǎng)絡(luò) I/O 模型,以支持更多的客戶(hù)端贷岸。

在改進(jìn)網(wǎng)絡(luò) I/O 模型前壹士,我先來(lái)提一個(gè)問(wèn)題,你知道服務(wù)器單機(jī)理論最大能連接多少個(gè)客戶(hù)端偿警?

相信你知道 TCP 連接是由四元組唯一確認(rèn)的躏救,這個(gè)四元組就是:本機(jī)IP, 本機(jī)端口, 對(duì)端IP, 對(duì)端端口

服務(wù)器作為服務(wù)方螟蒸,通常會(huì)在本地固定監(jiān)聽(tīng)一個(gè)端口盒使,等待客戶(hù)端的連接。因此服務(wù)器的本地 IP 和端口是固定的七嫌,于是對(duì)于服務(wù)端 TCP 連接的四元組只有對(duì)端 IP 和端口是會(huì)變化的少办,所以最大 TCP 連接數(shù) = 客戶(hù)端 IP 數(shù)×客戶(hù)端端口數(shù)

對(duì)于 IPv4诵原,客戶(hù)端的 IP 數(shù)最多為 2 的 32 次方凡泣,客戶(hù)端的端口數(shù)最多為 2 的 16 次方,也就是服務(wù)端單機(jī)最大 TCP 連接數(shù)約為 2 的 48 次方皮假。

這個(gè)理論值相當(dāng)“豐滿”鞋拟,但是服務(wù)器肯定承載不了那么大的連接數(shù),主要會(huì)受兩個(gè)方面的限制:

文件描述符惹资,Socket 實(shí)際上是一個(gè)文件贺纲,也就會(huì)對(duì)應(yīng)一個(gè)文件描述符。在 Linux 下褪测,單個(gè)進(jìn)程打開(kāi)的文件描述符數(shù)是有限制的猴誊,沒(méi)有經(jīng)過(guò)修改的值一般都是 1024,不過(guò)我們可以通過(guò) ulimit 增大文件描述符的數(shù)目侮措;

系統(tǒng)內(nèi)存懈叹,每個(gè) TCP 連接在內(nèi)核中都有對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),意味著每個(gè)連接都是會(huì)占用一定內(nèi)存的分扎;

那如果服務(wù)器的內(nèi)存只有 2 GB澄成,網(wǎng)卡是千兆的,能支持并發(fā) 1 萬(wàn)請(qǐng)求嗎畏吓?

并發(fā) 1 萬(wàn)請(qǐng)求墨状,也就是經(jīng)典的 C10K 問(wèn)題 ,C 是 Client 單詞首字母縮寫(xiě)菲饼,C10K 就是單機(jī)同時(shí)處理 1 萬(wàn)個(gè)請(qǐng)求的問(wèn)題肾砂。

從硬件資源角度看,對(duì)于 2GB 內(nèi)存千兆網(wǎng)卡的服務(wù)器宏悦,如果每個(gè)請(qǐng)求處理占用不到 200KB 的內(nèi)存和 100Kbit 的網(wǎng)絡(luò)帶寬就可以滿足并發(fā) 1 萬(wàn)個(gè)請(qǐng)求镐确。

不過(guò)包吝,要想真正實(shí)現(xiàn) C10K 的服務(wù)器,要考慮的地方在于服務(wù)器的網(wǎng)絡(luò) I/O 模型源葫,效率低的模型漏策,會(huì)加重系統(tǒng)開(kāi)銷(xiāo),從而會(huì)離 C10K 的目標(biāo)越來(lái)越遠(yuǎn)臼氨。

多進(jìn)程模型

基于最原始的阻塞網(wǎng)絡(luò) I/O, 如果服務(wù)器要支持多個(gè)客戶(hù)端芭届,其中比較傳統(tǒng)的方式储矩,就是使用多進(jìn)程模型,也就是為每個(gè)客戶(hù)端分配一個(gè)進(jìn)程來(lái)處理請(qǐng)求褂乍。

服務(wù)器的主進(jìn)程負(fù)責(zé)監(jiān)聽(tīng)客戶(hù)的連接持隧,一旦與客戶(hù)端連接完成,accept() 函數(shù)就會(huì)返回一個(gè)「已連接 Socket」逃片,這時(shí)就通過(guò)fork()?函數(shù)創(chuàng)建一個(gè)子進(jìn)程屡拨,實(shí)際上就把父進(jìn)程所有相關(guān)的東西都復(fù)制一份,包括文件描述符褥实、內(nèi)存地址空間呀狼、程序計(jì)數(shù)器、執(zhí)行的代碼等损离。

這兩個(gè)進(jìn)程剛復(fù)制完的時(shí)候哥艇,幾乎一摸一樣。不過(guò)僻澎,會(huì)根據(jù)返回值來(lái)區(qū)分是父進(jìn)程還是子進(jìn)程貌踏,如果返回值是 0,則是子進(jìn)程窟勃;如果返回值是其他的整數(shù)祖乳,就是父進(jìn)程。

正因?yàn)樽舆M(jìn)程會(huì)復(fù)制父進(jìn)程的文件描述符秉氧,于是就可以直接使用「已連接 Socket 」和客戶(hù)端通信了眷昆,

可以發(fā)現(xiàn),子進(jìn)程不需要關(guān)心「監(jiān)聽(tīng) Socket」汁咏,只需要關(guān)心「已連接 Socket」隙赁;父進(jìn)程則相反,將客戶(hù)服務(wù)交給子進(jìn)程來(lái)處理梆暖,因此父進(jìn)程不需要關(guān)心「已連接 Socket」伞访,只需要關(guān)心「監(jiān)聽(tīng) Socket」。

下面這張圖描述了從連接請(qǐng)求到連接建立轰驳,父進(jìn)程創(chuàng)建生子進(jìn)程為客戶(hù)服務(wù)厚掷。

另外弟灼,當(dāng)「子進(jìn)程」退出時(shí),實(shí)際上內(nèi)核里還會(huì)保留該進(jìn)程的一些信息冒黑,也是會(huì)占用內(nèi)存的田绑,如果不做好“回收”工作,就會(huì)變成僵尸進(jìn)程抡爹,隨著僵尸進(jìn)程越多掩驱,會(huì)慢慢耗盡我們的系統(tǒng)資源。

因此冬竟,父進(jìn)程要“善后”好自己的孩子欧穴,怎么善后呢?那么有兩種方式可以在子進(jìn)程退出后回收資源泵殴,分別是調(diào)用?wait()?和?waitpid()函數(shù)涮帘。

這種用多個(gè)進(jìn)程來(lái)應(yīng)付多個(gè)客戶(hù)端的方式,在應(yīng)對(duì) 100 個(gè)客戶(hù)端還是可行的笑诅,但是當(dāng)客戶(hù)端數(shù)量高達(dá)一萬(wàn)時(shí)调缨,肯定扛不住的,因?yàn)槊慨a(chǎn)生一個(gè)進(jìn)程吆你,必會(huì)占據(jù)一定的系統(tǒng)資源弦叶,而且進(jìn)程間上下文切換的“包袱”是很重的,性能會(huì)大打折扣妇多。

進(jìn)程的上下文切換不僅包含了虛擬內(nèi)存湾蔓、棧、全局變量等用戶(hù)空間的資源砌梆,還包括了內(nèi)核堆棧默责、寄存器等內(nèi)核空間的資源。

多線程模型

既然進(jìn)程間上下文切換的“包袱”很重咸包,那我們就搞個(gè)比較輕量級(jí)的模型來(lái)應(yīng)對(duì)多用戶(hù)的請(qǐng)求 ——?多線程模型桃序。

線程是運(yùn)行在進(jìn)程中的一個(gè)“邏輯流”,單進(jìn)程中可以運(yùn)行多個(gè)線程烂瘫,同進(jìn)程里的線程可以共享進(jìn)程的部分資源的媒熊,比如文件描述符列表、進(jìn)程空間坟比、代碼芦鳍、全局?jǐn)?shù)據(jù)、堆葛账、共享庫(kù)等柠衅,這些共享些資源在上下文切換時(shí)是不需要切換,而只需要切換線程的私有數(shù)據(jù)籍琳、寄存器等不共享的數(shù)據(jù)菲宴,因此同一個(gè)進(jìn)程下的線程上下文切換的開(kāi)銷(xiāo)要比進(jìn)程小得多贷祈。

當(dāng)服務(wù)器與客戶(hù)端 TCP 完成連接后,通過(guò)?pthread_create()函數(shù)創(chuàng)建線程喝峦,然后將「已連接 Socket」的文件描述符傳遞給線程函數(shù)势誊,接著在線程里和客戶(hù)端進(jìn)行通信,從而達(dá)到并發(fā)處理的目的谣蠢。

如果每來(lái)一個(gè)連接就創(chuàng)建一個(gè)線程粟耻,線程運(yùn)行完后,還得操作系統(tǒng)還得銷(xiāo)毀線程眉踱,雖說(shuō)線程切換的上寫(xiě)文開(kāi)銷(xiāo)不大挤忙,但是如果頻繁創(chuàng)建和銷(xiāo)毀線程,系統(tǒng)開(kāi)銷(xiāo)也是不小的勋锤。

那么,我們可以使用線程池的方式來(lái)避免線程的頻繁創(chuàng)建和銷(xiāo)毀侥祭,所謂的線程池叁执,就是提前創(chuàng)建若干個(gè)線程,這樣當(dāng)由新連接建立時(shí)矮冬,將這個(gè)已連接的 Socket 放入到一個(gè)隊(duì)列里谈宛,然后線程池里的線程負(fù)責(zé)從隊(duì)列中取出已連接 Socket 進(jìn)程處理。


需要注意的是胎署,這個(gè)隊(duì)列是全局的吆录,每個(gè)線程都會(huì)操作,為了避免多線程競(jìng)爭(zhēng)琼牧,線程在操作這個(gè)隊(duì)列前要加鎖恢筝。

上面基于進(jìn)程或者線程模型的,其實(shí)還是有問(wèn)題的巨坊。新到來(lái)一個(gè) TCP 連接撬槽,就需要分配一個(gè)進(jìn)程或者線程,那么如果要達(dá)到 C10K趾撵,意味著要一臺(tái)機(jī)器維護(hù) 1 萬(wàn)個(gè)連接侄柔,相當(dāng)于要維護(hù) 1 萬(wàn)個(gè)進(jìn)程/線程,操作系統(tǒng)就算死扛也是扛不住的占调。

I/O 多路復(fù)用

既然為每個(gè)請(qǐng)求分配一個(gè)進(jìn)程/線程的方式不合適暂题,那有沒(méi)有可能只使用一個(gè)進(jìn)程來(lái)維護(hù)多個(gè) Socket 呢?答案是有的究珊,那就是?I/O 多路復(fù)用技術(shù)薪者。


一個(gè)進(jìn)程雖然任一時(shí)刻只能處理一個(gè)請(qǐng)求,但是處理每個(gè)請(qǐng)求的事件時(shí)剿涮,耗時(shí)控制在 1 毫秒以?xún)?nèi)啸胧,這樣 1 秒內(nèi)就可以處理上千個(gè)請(qǐng)求赶站,把時(shí)間拉長(zhǎng)來(lái)看,多個(gè)請(qǐng)求復(fù)用了一個(gè)進(jìn)程纺念,這就是多路復(fù)用贝椿,這種思想很類(lèi)似一個(gè) CPU 并發(fā)多個(gè)進(jìn)程,所以也叫做時(shí)分多路復(fù)用陷谱。

我們熟悉的 select/poll/epoll 內(nèi)核提供給用戶(hù)態(tài)的多路復(fù)用系統(tǒng)調(diào)用烙博,進(jìn)程可以通過(guò)一個(gè)系統(tǒng)調(diào)用函數(shù)從內(nèi)核中獲取多個(gè)事件

select/poll/epoll 是如何獲取網(wǎng)絡(luò)事件的呢烟逊?在獲取事件時(shí)渣窜,先把所有連接(文件描述符)傳給內(nèi)核,再由內(nèi)核返回產(chǎn)生了事件的連接宪躯,然后在用戶(hù)態(tài)中再處理這些連接對(duì)應(yīng)的請(qǐng)求即可乔宿。

select/poll/epoll 這是三個(gè)多路復(fù)用接口,都能實(shí)現(xiàn) C10K 嗎访雪?接下來(lái)详瑞,我們分別說(shuō)說(shuō)它們。

select/poll

select 實(shí)現(xiàn)多路復(fù)用的方式是臣缀,將已連接的 Socket 都放到一個(gè)文件描述符集合坝橡,然后調(diào)用 select 函數(shù)將文件描述符集合拷貝到內(nèi)核里,讓內(nèi)核來(lái)檢查是否有網(wǎng)絡(luò)事件產(chǎn)生精置,檢查的方式很粗暴计寇,就是通過(guò)遍歷文件描述符集合的方式,當(dāng)檢查到有事件產(chǎn)生后脂倦,將此 Socket 標(biāo)記為可讀或可寫(xiě)番宁, 接著再把整個(gè)文件描述符集合拷貝回用戶(hù)態(tài)里,然后用戶(hù)態(tài)還需要再通過(guò)遍歷的方法找到可讀或可寫(xiě)的 Socket赖阻,然后再對(duì)其處理贝淤。

所以,對(duì)于 select 這種方式政供,需要進(jìn)行?2 次「遍歷」文件描述符集合播聪,一次是在內(nèi)核態(tài)里,一個(gè)次是在用戶(hù)態(tài)里 布隔,而且還會(huì)發(fā)生?2 次「拷貝」文件描述符集合离陶,先從用戶(hù)空間傳入內(nèi)核空間,由內(nèi)核修改后衅檀,再傳出到用戶(hù)空間中招刨。

select 使用固定長(zhǎng)度的 BitsMap,表示文件描述符集合哀军,而且所支持的文件描述符的個(gè)數(shù)是有限制的沉眶,在 Linux 系統(tǒng)中打却,由內(nèi)核中的 FD_SETSIZE 限制, 默認(rèn)最大值為?1024谎倔,只能監(jiān)聽(tīng) 0~1023 的文件描述符柳击。

poll 不再用 BitsMap 來(lái)存儲(chǔ)所關(guān)注的文件描述符,取而代之用動(dòng)態(tài)數(shù)組片习,以鏈表形式來(lái)組織捌肴,突破了 select 的文件描述符個(gè)數(shù)限制,當(dāng)然還會(huì)受到系統(tǒng)文件描述符限制藕咏。

但是 poll 和 select 并沒(méi)有太大的本質(zhì)區(qū)別状知,都是使用「線性結(jié)構(gòu)」存儲(chǔ)進(jìn)程關(guān)注的 Socket 集合,因此都需要遍歷文件描述符集合來(lái)找到可讀或可寫(xiě)的 Socket孽查,時(shí)間復(fù)雜度為 O(n)饥悴,而且也需要在用戶(hù)態(tài)與內(nèi)核態(tài)之間拷貝文件描述符集合,這種方式隨著并發(fā)數(shù)上來(lái)盲再,性能的損耗會(huì)呈指數(shù)級(jí)增長(zhǎng)西设。

epoll

epoll 通過(guò)兩個(gè)方面,很好解決了 select/poll 的問(wèn)題洲胖。

第一點(diǎn)济榨,epoll 在內(nèi)核里使用紅黑樹(shù)來(lái)跟蹤進(jìn)程所有待檢測(cè)的文件描述字坯沪,把需要監(jiān)控的 socket 通過(guò)?epoll_ctl()?函數(shù)加入內(nèi)核中的紅黑樹(shù)里绿映,紅黑樹(shù)是個(gè)高效的數(shù)據(jù)結(jié)構(gòu),增刪查一般時(shí)間復(fù)雜度是O(logn)腐晾,通過(guò)對(duì)這棵黑紅樹(shù)進(jìn)行操作叉弦,這樣就不需要像 select/poll 每次操作時(shí)都傳入整個(gè) socket 集合,只需要傳入一個(gè)待檢測(cè)的 socket藻糖,減少了內(nèi)核和用戶(hù)空間大量的數(shù)據(jù)拷貝和內(nèi)存分配淹冰。

第二點(diǎn), epoll 使用事件驅(qū)動(dòng)的機(jī)制巨柒,內(nèi)核里維護(hù)了一個(gè)鏈表來(lái)記錄就緒事件樱拴,當(dāng)某個(gè) socket 有事件發(fā)生時(shí),通過(guò)回調(diào)函數(shù)內(nèi)核會(huì)將其加入到這個(gè)就緒事件列表中洋满,當(dāng)用戶(hù)調(diào)用?epoll_wait()?函數(shù)時(shí)晶乔,只會(huì)返回有事件發(fā)生的文件描述符的個(gè)數(shù),不需要像 select/poll 那樣輪詢(xún)掃描整個(gè) socket 集合牺勾,大大提高了檢測(cè)的效率正罢。

從下圖你可以看到 epoll 相關(guān)的接口作用:


epoll 的方式即使監(jiān)聽(tīng)的 Socket 數(shù)量越多的時(shí)候,效率不會(huì)大幅度降低驻民,能夠同時(shí)監(jiān)聽(tīng)的 Socket 的數(shù)目也非常的多了翻具,上限就為系統(tǒng)定義的進(jìn)程打開(kāi)的最大文件描述符個(gè)數(shù)履怯。因而,epoll 被稱(chēng)為解決 C10K 問(wèn)題的利器裆泳。

插個(gè)題外話叹洲,網(wǎng)上文章不少說(shuō),epoll_wait?返回時(shí)晾虑,對(duì)于就緒的事件疹味,epoll使用的是共享內(nèi)存的方式,即用戶(hù)態(tài)和內(nèi)核態(tài)都指向了就緒鏈表帜篇,所以就避免了內(nèi)存拷貝消耗糙捺。

這是錯(cuò)的!看過(guò) epoll 內(nèi)核源碼的都知道笙隙,壓根就沒(méi)有使用共享內(nèi)存這個(gè)玩意洪灯。你可以從下面這份代碼看到, epoll_wait 實(shí)現(xiàn)的內(nèi)核代碼中調(diào)用了?__put_user?函數(shù)竟痰,這個(gè)函數(shù)就是將數(shù)據(jù)從內(nèi)核拷貝到用戶(hù)空間签钩。


好了,這個(gè)題外話就說(shuō)到這了坏快,我們繼續(xù)铅檩!

epoll 支持兩種事件觸發(fā)模式,分別是邊緣觸發(fā)(edge-triggered莽鸿,ET水平觸發(fā)(level-triggered昧旨,LT

這兩個(gè)術(shù)語(yǔ)還挺抽象的祥得,其實(shí)它們的區(qū)別還是很好理解的兔沃。

使用邊緣觸發(fā)模式時(shí),當(dāng)被監(jiān)控的 Socket 描述符上有可讀事件發(fā)生時(shí)级及,服務(wù)器端只會(huì)從 epoll_wait 中蘇醒一次乒疏,即使進(jìn)程沒(méi)有調(diào)用 read 函數(shù)從內(nèi)核讀取數(shù)據(jù),也依然只蘇醒一次饮焦,因此我們程序要保證一次性將內(nèi)核緩沖區(qū)的數(shù)據(jù)讀取完怕吴;

使用水平觸發(fā)模式時(shí),當(dāng)被監(jiān)控的 Socket 上有可讀事件發(fā)生時(shí)县踢,服務(wù)器端不斷地從 epoll_wait 中蘇醒转绷,直到內(nèi)核緩沖區(qū)數(shù)據(jù)被 read 函數(shù)讀完才結(jié)束,目的是告訴我們有數(shù)據(jù)需要讀鹊钛暇咆;

舉個(gè)例子,你的快遞被放到了一個(gè)快遞箱里,如果快遞箱只會(huì)通過(guò)短信通知你一次爸业,即使你一直沒(méi)有去取其骄,它也不會(huì)再發(fā)送第二條短信提醒你,這個(gè)方式就是邊緣觸發(fā)扯旷;如果快遞箱發(fā)現(xiàn)你的快遞沒(méi)有被取出拯爽,它就會(huì)不停地發(fā)短信通知你,直到你取出了快遞钧忽,它才消停毯炮,這個(gè)就是水平觸發(fā)的方式。

這就是兩者的區(qū)別耸黑,水平觸發(fā)的意思是只要滿足事件的條件桃煎,比如內(nèi)核中有數(shù)據(jù)需要讀,就一直不斷地把這個(gè)事件傳遞給用戶(hù)大刊;而邊緣觸發(fā)的意思是只有第一次滿足條件的時(shí)候才觸發(fā)为迈,之后就不會(huì)再傳遞同樣的事件了。

如果使用水平觸發(fā)模式缺菌,當(dāng)內(nèi)核通知文件描述符可讀寫(xiě)時(shí)葫辐,接下來(lái)還可以繼續(xù)去檢測(cè)它的狀態(tài),看它是否依然可讀或可寫(xiě)伴郁。所以在收到通知后耿战,沒(méi)必要一次執(zhí)行盡可能多的讀寫(xiě)操作。

如果使用邊緣觸發(fā)模式焊傅,I/O 事件發(fā)生時(shí)只會(huì)通知一次剂陡,而且我們不知道到底能讀寫(xiě)多少數(shù)據(jù),所以在收到通知后應(yīng)盡可能地讀寫(xiě)數(shù)據(jù)租冠,以免錯(cuò)失讀寫(xiě)的機(jī)會(huì)鹏倘。因此薯嗤,我們會(huì)循環(huán)從文件描述符讀寫(xiě)數(shù)據(jù)顽爹,那么如果文件描述符是阻塞的,沒(méi)有數(shù)據(jù)可讀寫(xiě)時(shí)骆姐,進(jìn)程會(huì)阻塞在讀寫(xiě)函數(shù)那里镜粤,程序就沒(méi)辦法繼續(xù)往下執(zhí)行。所以玻褪,邊緣觸發(fā)模式一般和非阻塞 I/O 搭配使用肉渴,程序會(huì)一直執(zhí)行 I/O 操作,直到系統(tǒng)調(diào)用(如?read?和?write)返回錯(cuò)誤带射,錯(cuò)誤類(lèi)型為?EAGAIN?或?EWOULDBLOCK同规。

一般來(lái)說(shuō),邊緣觸發(fā)的效率比水平觸發(fā)的效率要高,因?yàn)檫吘売|發(fā)可以減少 epoll_wait 的系統(tǒng)調(diào)用次數(shù)券勺,系統(tǒng)調(diào)用也是有一定的開(kāi)銷(xiāo)的的绪钥,畢竟也存在上下文的切換。

select/poll 只有水平觸發(fā)模式关炼,epoll 默認(rèn)的觸發(fā)模式是水平觸發(fā)程腹,但是可以根據(jù)應(yīng)用場(chǎng)景設(shè)置為邊緣觸發(fā)模式。

另外儒拂,使用 I/O 多路復(fù)用時(shí)寸潦,最好搭配非阻塞 I/O 一起使用,Linux 手冊(cè)關(guān)于 select 的內(nèi)容中有如下說(shuō)明:

Under Linux, select() may report a socket file descriptor as "ready for reading", while nevertheless a subsequent read blocks. This could for example happen when data has arrived but upon examination has wrong checksum and is discarded. There may be other circumstances in which a file descriptor is spuriously reported as ready. Thus it may be safer to use O_NONBLOCK on sockets that should not block.

我谷歌翻譯的結(jié)果:

在Linux下社痛,select() 可能會(huì)將一個(gè) socket 文件描述符報(bào)告為 "準(zhǔn)備讀取"见转,而后續(xù)的讀取塊卻沒(méi)有。例如蒜哀,當(dāng)數(shù)據(jù)已經(jīng)到達(dá)池户,但經(jīng)檢查后發(fā)現(xiàn)有錯(cuò)誤的校驗(yàn)和而被丟棄時(shí),就會(huì)發(fā)生這種情況凡怎。也有可能在其他情況下校焦,文件描述符被錯(cuò)誤地報(bào)告為就緒。因此统倒,在不應(yīng)該阻塞的 socket 上使用 O_NONBLOCK 可能更安全寨典。

簡(jiǎn)單點(diǎn)理解,就是多路復(fù)用 API 返回的事件并不一定可讀寫(xiě)的房匆,如果使用阻塞 I/O耸成, 那么在調(diào)用 read/write 時(shí)則會(huì)發(fā)生程序阻塞,因此最好搭配非阻塞 I/O浴鸿,以便應(yīng)對(duì)極少數(shù)的特殊情況井氢。

總結(jié)

最基礎(chǔ)的 TCP 的 Socket 編程,它是阻塞 I/O 模型岳链,基本上只能一對(duì)一通信花竞,那為了服務(wù)更多的客戶(hù)端,我們需要改進(jìn)網(wǎng)絡(luò) I/O 模型掸哑。

比較傳統(tǒng)的方式是使用多進(jìn)程/線程模型约急,每來(lái)一個(gè)客戶(hù)端連接,就分配一個(gè)進(jìn)程/線程苗分,然后后續(xù)的讀寫(xiě)都在對(duì)應(yīng)的進(jìn)程/線程厌蔽,這種方式處理 100 個(gè)客戶(hù)端沒(méi)問(wèn)題,但是當(dāng)客戶(hù)端增大到 10000 個(gè)時(shí)摔癣,10000 個(gè)進(jìn)程/線程的調(diào)度奴饮、上下文切換以及它們占用的內(nèi)存纬向,都會(huì)成為瓶頸。

為了解決上面這個(gè)問(wèn)題戴卜,就出現(xiàn)了 I/O 的多路復(fù)用罢猪,可以只在一個(gè)進(jìn)程里處理多個(gè)文件的 I/O,Linux 下有三種提供 I/O 多路復(fù)用的 API叉瘩,分別是:select膳帕、poll、epoll薇缅。

select 和 poll 并沒(méi)有本質(zhì)區(qū)別危彩,它們內(nèi)部都是使用「線性結(jié)構(gòu)」來(lái)存儲(chǔ)進(jìn)程關(guān)注的 Socket 集合。

在使用的時(shí)候泳桦,首先需要把關(guān)注的 Socket 集合通過(guò) select/poll 系統(tǒng)調(diào)用從用戶(hù)態(tài)拷貝到內(nèi)核態(tài)汤徽,然后由內(nèi)核檢測(cè)事件,當(dāng)有網(wǎng)絡(luò)事件產(chǎn)生時(shí)灸撰,內(nèi)核需要遍歷進(jìn)程關(guān)注 Socket 集合谒府,找到對(duì)應(yīng)的 Socket,并設(shè)置其狀態(tài)為可讀/可寫(xiě)浮毯,然后把整個(gè) Socket 集合從內(nèi)核態(tài)拷貝到用戶(hù)態(tài)完疫,用戶(hù)態(tài)還要繼續(xù)遍歷整個(gè) Socket 集合找到可讀/可寫(xiě)的 Socket,然后對(duì)其處理债蓝。

很明顯發(fā)現(xiàn)壳鹤,select 和 poll 的缺陷在于,當(dāng)客戶(hù)端越多饰迹,也就是 Socket 集合越大芳誓,Socket 集合的遍歷和拷貝會(huì)帶來(lái)很大的開(kāi)銷(xiāo),因此也很難應(yīng)對(duì) C10K啊鸭。

epoll 是解決 C10K 問(wèn)題的利器锹淌,通過(guò)兩個(gè)方面解決了 select/poll 的問(wèn)題。

epoll 在內(nèi)核里使用「紅黑樹(shù)」來(lái)關(guān)注進(jìn)程所有待檢測(cè)的 Socket赠制,紅黑樹(shù)是個(gè)高效的數(shù)據(jù)結(jié)構(gòu)赂摆,增刪查一般時(shí)間復(fù)雜度是 O(logn),通過(guò)對(duì)這棵黑紅樹(shù)的管理憎妙,不需要像 select/poll 在每次操作時(shí)都傳入整個(gè) Socket 集合库正,減少了內(nèi)核和用戶(hù)空間大量的數(shù)據(jù)拷貝和內(nèi)存分配曲楚。

epoll 使用事件驅(qū)動(dòng)的機(jī)制厘唾,內(nèi)核里維護(hù)了一個(gè)「鏈表」來(lái)記錄就緒事件,只將有事件發(fā)生的 Socket 集合傳遞給應(yīng)用程序龙誊,不需要像 select/poll 那樣輪詢(xún)掃描整個(gè)集合(包含有和無(wú)事件的 Socket )抚垃,大大提高了檢測(cè)的效率。

而且,epoll 支持邊緣觸發(fā)和水平觸發(fā)的方式鹤树,而 select/poll 只支持水平觸發(fā)铣焊,一般而言,邊緣觸發(fā)的方式會(huì)比水平觸發(fā)的效率高罕伯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末曲伊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子追他,更是在濱河造成了極大的恐慌坟募,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邑狸,死亡現(xiàn)場(chǎng)離奇詭異懈糯,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)单雾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)赚哗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人硅堆,你說(shuō)我怎么就攤上這事屿储。” “怎么了渐逃?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵扩所,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我朴乖,道長(zhǎng)祖屏,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任买羞,我火速辦了婚禮袁勺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘畜普。我一直安慰自己期丰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布吃挑。 她就那樣靜靜地躺著钝荡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舶衬。 梳的紋絲不亂的頭發(fā)上埠通,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音逛犹,去河邊找鬼端辱。 笑死梁剔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的舞蔽。 我是一名探鬼主播荣病,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼渗柿!你這毒婦竟也來(lái)了个盆?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤朵栖,失蹤者是張志新(化名)和其女友劉穎砾省,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體混槐,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡编兄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了声登。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狠鸳。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖悯嗓,靈堂內(nèi)的尸體忽然破棺而出件舵,到底是詐尸還是另有隱情,我是刑警寧澤脯厨,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布铅祸,位于F島的核電站,受9級(jí)特大地震影響合武,放射性物質(zhì)發(fā)生泄漏临梗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一稼跳、第九天 我趴在偏房一處隱蔽的房頂上張望盟庞。 院中可真熱鬧,春花似錦汤善、人聲如沸什猖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)不狮。三九已至,卻和暖如春在旱,著一層夾襖步出監(jiān)牢的瞬間摇零,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工颈渊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遂黍,地道東北人终佛。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓俊嗽,卻偏偏與公主長(zhǎng)得像雾家,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绍豁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容