徹底搞懂 Select / Poll / Epoll,就這篇了!
之前已經(jīng)把網(wǎng)絡(luò) I/O 相關(guān)要點(diǎn)都盤了,還剩 select/poll/epoll 這幾個(gè)區(qū)別沒(méi)說(shuō),這篇就來(lái)搞搞它們,并且是從完全理解原理的角度來(lái)區(qū)分它們。
本來(lái)是要上源碼的,但是感覺(jué)沒(méi)啥必要,身為應(yīng)用開發(fā)我覺(jué)得理解原理就行了,源碼反正看了就忘了,理解才是最重要!所以我就盡量避免代碼且用大白話來(lái)盤一盤這三個(gè)玩意。
話不多說(shuō),發(fā)車。
小思考
首先,我們知道 select/poll/epoll 是用來(lái)實(shí)現(xiàn)多路復(fù)用的,即一個(gè)線程利用它們即可 hold 住多個(gè) socket。
按照這個(gè)思路,線程不可被任何一個(gè)被管理的 Socket 阻塞,且任一個(gè) Socket 來(lái)數(shù)據(jù)之后都得告知 select/poll/epoll 線程。
想想看,這應(yīng)該如何實(shí)現(xiàn)呢?
我們拿 select 的邏輯來(lái)分析下
按照我們的理解,select 管理多個(gè) Socket 的模型如下圖所示:
這里要注意一下內(nèi)核態(tài)和用戶態(tài)的交互,用戶程序訪問(wèn)不了內(nèi)核空間。
所以,我們調(diào)用 select 會(huì)把所有要管理的 socket 的 fd (文件描述符,Linux下皆為文件,簡(jiǎn)單理解就是通過(guò) fd 能找到這個(gè) socket)傳到內(nèi)核中。
此時(shí),要遍歷所有 socket,看看是否有感興趣的事件發(fā)生。如果沒(méi)有一個(gè) socket 有事件發(fā)生,那么 select 的線程就需要讓出 cpu 阻塞等待,這個(gè)等待可以是不設(shè)置超時(shí)時(shí)間的死等,也可以是設(shè)置 timeout 的有超時(shí)時(shí)間的等待。
假設(shè)此時(shí)客戶端發(fā)送了數(shù)據(jù),網(wǎng)卡接收到的數(shù)據(jù)塞到對(duì)應(yīng)的 socket 的接收隊(duì)列中,此時(shí) socket 知道來(lái)數(shù)據(jù)了,那如何喚醒 select 呢?
其實(shí)每個(gè) socket 有個(gè)屬于自己的睡眠隊(duì)列,select 會(huì)安排一個(gè)內(nèi)應(yīng),即在被管理的 socket 的睡眠隊(duì)列里面塞入一個(gè) entry。
當(dāng) socket 接收到網(wǎng)卡的數(shù)據(jù)后,就會(huì)去它的睡眠隊(duì)列里遍歷 entry,調(diào)用 entry 設(shè)置的 callback 方法,這個(gè) callback 方法里就能喚醒 select !
所以 select 在每個(gè)被它管理的 socket 的睡眠隊(duì)列里都塞入一個(gè)與它相關(guān)的 entry,這樣不論哪個(gè) socket 來(lái)數(shù)據(jù)了,它立馬就能被喚醒然后干活!
但是,select 的實(shí)現(xiàn)不太好,因?yàn)閱拘训?select 此時(shí)只知道來(lái)活了,并不知道具體是哪個(gè) socket 來(lái)數(shù)據(jù)了,所以只能傻傻地遍歷所有 socket ,看看到底是哪個(gè) scoket 來(lái)活了,然后把所有來(lái)活的 socket 封裝成事件返回。
這樣用戶程序就能獲得發(fā)生的事件,然后進(jìn)行 I/O 和業(yè)務(wù)處理了。
這就是 select 的實(shí)現(xiàn)邏輯,理解起來(lái)應(yīng)該不難。
這里再提一嘴 select 的限制,因?yàn)楸还芾淼?socket fd 需要從用戶空間拷貝到內(nèi)核空間,為了控制拷貝的大小而做了限制,即每個(gè) select 能拷貝的 fds 集合大小只有1024。
然后要改的話只能修改宏..再重新編譯內(nèi)核。網(wǎng)上很多文章都是這樣說(shuō)的,但是(沒(méi)錯(cuò)有個(gè)但是)。
我看了一篇文章,確實(shí)有這個(gè)宏,值也是 1024,但內(nèi)核根本沒(méi)有限制 fds 集合的大小。然后托人問(wèn)了個(gè)內(nèi)核大佬,大佬說(shuō)內(nèi)核確實(shí)沒(méi)做限制,glibc那層做了。
所以..重新編譯內(nèi)核?那篇文章放文末。
poll
poll 這玩意相比于 select 主要就是優(yōu)化了 fds 的結(jié)構(gòu),不再是 bit 數(shù)組了,而是一個(gè)叫 pollfd 的玩意,反正就是不用管啥 1024 的限制了。
不過(guò)現(xiàn)在也沒(méi)人用 poll,我就不多說(shuō)了。
epoll
這個(gè)就是重點(diǎn)了。
相信看了 select 的實(shí)現(xiàn),我們稍微思考下,就能想出幾個(gè)可以優(yōu)化的點(diǎn)。
比如,為什么每次 select 需要把監(jiān)控的 fds 傳輸?shù)絻?nèi)核里?不能在內(nèi)核里維護(hù)個(gè)?
為什么 socket 只喚醒 select,不能告訴它是哪個(gè) socket 來(lái)數(shù)據(jù)了?
epoll 主要就是基于上面兩點(diǎn)做了優(yōu)化。
首先,搞了個(gè)叫 epoll_ctl 的方法,這方法就是用來(lái)管理維護(hù) epoll 所監(jiān)控的哪些 socket。
如果你的 epoll 要新加一個(gè) socket 來(lái)管理,那就調(diào)用 epoll_ctl,要?jiǎng)h除一個(gè) socket 也調(diào)用 epoll_ctl,通過(guò)不同的入?yún)?lái)控制增刪改。
這樣,在內(nèi)核里面就維護(hù)了此 epoll 管理的 socket 集合,這樣就不用每次調(diào)用的時(shí)候都得把所有管理的 fds 拷貝到內(nèi)核了。
對(duì)了,這個(gè) socket 集合是用紅黑樹實(shí)現(xiàn)的。
然后和 select 類似,每個(gè) socket 的睡眠隊(duì)列里都會(huì)加個(gè) entry,當(dāng)每個(gè) socket 來(lái)數(shù)據(jù)之后,同樣也會(huì)調(diào)用 entry 對(duì)應(yīng)的 callback。
與 select 不同的是,引入了一個(gè) ready_list 雙向鏈表,callback 里面會(huì)把當(dāng)前的 socket 加入到 ready_list 然后喚醒 epoll。
這樣被喚醒的 epoll 只需要遍歷 ready_list 即可,這個(gè)鏈表里一定是有數(shù)據(jù)可讀的 socket,相比于 select 就不會(huì)做無(wú)用的遍歷了。
同時(shí)收集到的可讀的 fd 按理是要拷貝到用戶空間的,這里又做了個(gè)優(yōu)化,利用了 mmp,讓用戶空間和內(nèi)核空間映射到同一塊內(nèi)存中,這樣就避免了拷貝。
完美啊~
這就是 epoll 基于 select 所作的優(yōu)化,還有一些差別沒(méi)細(xì)說(shuō),比如 epoll 是阻塞睡眠在一個(gè) single_epoll_wait_list 而不是 socket 的睡眠隊(duì)列等等,我就不提了,理解上面的這些已經(jīng)夠了。
ET<
都談到 epoll 了,避免不了要扯扯 ET 和 LT 兩個(gè)模式。
ET,邊沿觸發(fā)。
按照上面的邏輯就是 epoll 遍歷 ready_list 的時(shí)候,會(huì)把 socket 從 ready_list 里面移除,然后讀取這個(gè) scoket 的事件。
而 LT,水平觸發(fā),有點(diǎn)不一樣。
在這個(gè)模式下 epoll 遍歷 ready_list 的時(shí)候,會(huì)把 socket 從 ready_list 里面移除,然后讀取這個(gè) scoket 的事件,如果這個(gè) socket 返回了感興趣的事件,那么當(dāng)前這個(gè) socket 會(huì)再被加入到 ready_list 中,這樣下次調(diào)用 epoll_wait 的時(shí)候,還能拿到這個(gè) socket。
這就是這兩者最本質(zhì)的區(qū)別了。
看到這有人會(huì)問(wèn),這兩種模式的使用會(huì)造成哪種不一樣的結(jié)果?
如果此時(shí)一個(gè)客戶端同時(shí)發(fā)來(lái)了 5 個(gè)數(shù)據(jù)包,按正常的邏輯,只需要喚醒一次 epoll ,把當(dāng)前 socket 加一次到 ready_list 就行了,不需要加 5 次。然后用戶程序可以把 socket 接收隊(duì)列的所有數(shù)據(jù)包都讀完。
但假設(shè)用戶程序就讀了一個(gè)包,然后處理報(bào)錯(cuò)了,后面不讀了,那后面的 4 個(gè)包咋辦?
如果是 ET 模式,就讀不了了,因?yàn)闆](méi)有把 socket 加入到 ready_list 的觸發(fā)條件了。除非這個(gè)客戶端發(fā)了新的數(shù)據(jù)包過(guò)來(lái),這樣才會(huì)再把當(dāng)前 socket 加入到 ready_list,在新包過(guò)來(lái)之前,這 4 個(gè)數(shù)據(jù)包都不會(huì)被讀到。
而 LT 模式不一樣,因?yàn)槊看巫x完有感興趣的事件發(fā)生之后,會(huì)把當(dāng)前 socket 再加入到 ready_list,所以下次肯定能讀到這個(gè) socket,所以后面的 4 個(gè)數(shù)據(jù)包會(huì)被訪問(wèn)到,不論客戶端是否發(fā)送新包。
至此,我想你應(yīng)該理解什么是 ET ,什么是 LT 了,而不用對(duì)著一些什么狀態(tài)變更觸發(fā)這些不易理解的名詞而發(fā)暈。
最后
好了,今天的分析到此完畢,我個(gè)人覺(jué)得對(duì) select/poll/epoll 的理解到這個(gè)程度就差不多了,當(dāng)然還有很多細(xì)節(jié),需要自行去看源碼探究,問(wèn)我我也不懂,這些都是閱讀網(wǎng)上的源碼分析文章得出的結(jié)論。
我也不建議讀的那么深,畢竟人的精力有限對(duì)吧,有涉及到相關(guān)底層優(yōu)化的時(shí)候,再去研究也不遲。
我是yes,從一點(diǎn)點(diǎn)到億點(diǎn)點(diǎn),我們下篇見。
參考:
https://blog.csdn.net/dog250/article/details/105896693(select真的受1024限制嗎?)
https://blog.csdn.net/dog250/article/details/50528373