如果說 select 模型和 poll 模型是早期的產(chǎn)物菩咨,在性能上有諸多不盡人意之處吠式,那么自 Linux 2.6 之后新增的 epoll 模型,則徹底解決了性能問題抽米,一舉使得單機(jī)承受百萬并發(fā)的課題變得極為容易特占。
現(xiàn)在可以這么說,只需要一些簡單的設(shè)置更改云茸,然后配合上 epoll 的性能是目,實現(xiàn)單機(jī)百萬并發(fā)輕而易舉。
同時标捺,由于 epoll 整體的優(yōu)化懊纳,使得之前的幾個比較耗費性能的問題不再成為羈絆,所以也成為了 Linux 平臺上進(jìn)行網(wǎng)絡(luò)通訊的首選模型亡容。
講解之前嗤疯,還是 linux man 文檔鎮(zhèn)樓:linux man epoll 4 類文檔 linux man epoll 7 類文檔,倆文檔結(jié)合著讀闺兢,會對 epoll 有個大概的了解茂缚。
和之前提到的 select 和 poll 不同的是,此二者皆屬于系統(tǒng)調(diào)用函數(shù)列敲,但是 epoll 則不然阱佛,他是存在于內(nèi)核中的數(shù)據(jù)結(jié)構(gòu)。
可以通過 epoll_create戴而,epoll_ctl 及 epoll_wait 三個函數(shù)結(jié)合來對此數(shù)據(jù)結(jié)構(gòu)進(jìn)行操控。
說到 epoll_create 函數(shù)翩蘸,其作用是在內(nèi)核中創(chuàng)建一個 epoll 數(shù)據(jù)結(jié)構(gòu)實例所意,然后將返回此實例在系統(tǒng)中的文件描述符。
此 epoll 數(shù)據(jù)結(jié)構(gòu)的組成其實是一個鏈表結(jié)構(gòu),我們稱之為 interest list扶踊,里面會注冊連接上來的 client 的文件描述符泄鹏。
其簡化工作機(jī)制如下:
說道 epoll_ctl 函數(shù),其作用則是對 epoll 實例進(jìn)行增刪改查操作秧耗。有些類似我們常用的 CRUD 操作备籽。
這個函數(shù)操作的對象其實就是 epoll 數(shù)據(jù)結(jié)構(gòu),當(dāng)有新的 client 連接上來的時候分井,他會將此 client 注冊到 epoll 中的 interest list 中车猬,此操作通過附加 EPOLL_CTL_ADD 標(biāo)記來實現(xiàn)。
當(dāng)已有的 client 掉線或者主動下線的時候尺锚,他會將下線的 client從epoll 的 interest list 中移除珠闰,此操作通過附加 EPOLL_CTL_DEL 標(biāo)記來實現(xiàn)。
當(dāng)有 client 的文件描述符有變更的時候瘫辩,他會將 events 中的對應(yīng)的文件描述符進(jìn)行更新伏嗜,此操作通過附加 EPOLL_CTL_MOD 來實現(xiàn)。
當(dāng) interest list 中有 client 已經(jīng)準(zhǔn)備好了伐厌,可以進(jìn)行 IO 操作的時候承绸,他會將這些 clients 拿出來,然后放到一個新的 ready list 里面挣轨。
其簡化工作機(jī)制如下:
說道 epoll_wait 函數(shù)八酒,其作用就是掃描 ready list,處理準(zhǔn)備就緒的 client IO刃唐,其返回結(jié)果即為準(zhǔn)備好進(jìn)行 IO 的 client 的個數(shù)羞迷。通過遍歷這些準(zhǔn)備好的 client,就可以輕松進(jìn)行 IO 處理了画饥。
上面這三個函數(shù)是 epoll 操作的基本函數(shù)衔瓮,但是,想要徹底理解 epoll抖甘,則需要先了解這三塊內(nèi)容热鞍,即:inode,鏈表衔彻,紅黑樹薇宠。
在 Linux 內(nèi)核中,針對當(dāng)前打開的文件艰额,有一個 open file table澄港,里面記錄的是所有打開的文件描述符信息;同時也有一個 inode table柄沮,里面則記錄的是底層的文件描述符信息回梧。
這里假如文件描述符 B fork 了文件描述符 A废岂,雖然在 open file table 中,我們看新增了一個文件描述符 B狱意,但是實際上湖苞,在 inode table 中,A 和 B 的底層是一模一樣的详囤。
這里财骨,將 inode table 中的內(nèi)容理解為 Windows 中的文件屬性,會更加貼切和易懂藏姐。
這樣存儲的好處就是隆箩,無論上層文件描述符怎么變化,由于 epoll 監(jiān)控的數(shù)據(jù)永遠(yuǎn)是 inode table 的底層數(shù)據(jù)包各,那么我就可以一直能夠監(jiān)控到文件的各種變化信息摘仅,這也是 epoll 高效的基礎(chǔ)。
簡化流程如下:
數(shù)據(jù)存儲這塊解決了问畅,那么針對連接上來的客戶端 socket娃属,該用什么數(shù)據(jù)結(jié)構(gòu)保存進(jìn)來呢?
這里用到了紅黑樹护姆,由于客戶端 socket 會有頻繁的新增和刪除操作矾端,而紅黑樹這塊時間復(fù)雜度僅僅為 O(logN),還是挺高效的卵皂。
有人會問為啥不用哈希表呢秩铆?當(dāng)大量的連接頻繁的進(jìn)行接入或者斷開的時候,擴(kuò)容或者其他行為將會產(chǎn)生不少的 rehash 操作灯变,而且還要考慮哈希沖突的情況殴玛。
雖然查詢速度的確可以達(dá)到 o(1),但是 rehash 或者哈希沖突是不可控的添祸,所以基于這些考量滚粟,我認(rèn)為紅黑樹占優(yōu)一些。
客戶端 socket 怎么管理這塊解決了刃泌,接下來凡壤,當(dāng)有 socket 有數(shù)據(jù)需要進(jìn)行讀寫事件處理的時候,系統(tǒng)會將已經(jīng)就緒的 socket 添加到雙向鏈表中耙替,然后通過 epoll_wait 方法檢測的時候亚侠。
其實檢查的就是這個雙向鏈表,由于鏈表中都是就緒的數(shù)據(jù)俗扇,所以避免了針對整個客戶端 socket 列表進(jìn)行遍歷的情況硝烂,使得整體效率大大提升。
整體的操作流程為:
首先狐援,利用 epoll_create 在內(nèi)核中創(chuàng)建一個 epoll 對象钢坦。其實這個 epoll 對象究孕,就是一個可以存儲客戶端連接的數(shù)據(jù)結(jié)構(gòu)啥酱。
然后爹凹,客戶端 socket 連接上來,會通過 epoll_ctl 操作將結(jié)果添加到 epoll 對象的紅黑樹數(shù)據(jù)結(jié)構(gòu)中镶殷。
然后禾酱,一旦有 socket 有事件發(fā)生,則會通過回調(diào)函數(shù)將其添加到 ready list 雙向鏈表中绘趋。
最后颤陶,epoll_wait 會遍歷鏈表來處理已經(jīng)準(zhǔn)備好的 socket,然后通過預(yù)先設(shè)置的水平觸發(fā)或者邊緣觸發(fā)來進(jìn)行數(shù)據(jù)的感知操作陷遮。
從上面的細(xì)節(jié)可以看出滓走,由于 epoll 內(nèi)部監(jiān)控的是底層的文件描述符信息,可以將變更的描述符直接加入到 ready list帽馋,無需用戶將所有的描述符再進(jìn)行傳入搅方。
同時由于 epoll_wait 掃描的是已經(jīng)就緒的文件描述符,避免了很多無效的遍歷查詢绽族,使得 epoll 的整體性能大大提升姨涡,可以說現(xiàn)在只要談?wù)?Linux 平臺的 IO 多路復(fù)用,epoll 已經(jīng)成為了不二之選吧慢。