epoll深入解读
?
首先說一下傳統(tǒng)的I/O多路復(fù)用select和poll,對比一下和epoll之間的區(qū)別:
舉個例子:假如有100萬用戶同時與一個進(jìn)程保持TCP連接,而每一時刻只有幾十或者幾百個tcp連接是活躍的(即能接收到TCP包),那么在每一時刻進(jìn)程只需要處理這100萬連接中的有一小部分。
select和poll這樣處理的:在某一時刻,進(jìn)程收集所有的連接,其實(shí)這100萬連接中大部分是沒有事件發(fā)生的。因此,如果每次收集事件時,都把這100萬連接的套接字傳給操作系統(tǒng)(這首先就是用戶態(tài)內(nèi)存到內(nèi)核內(nèi)存的大量復(fù)制),而由操作系統(tǒng)內(nèi)核尋找這些鏈接上沒有處理的事件,將會是巨大的浪費(fèi)。
而epoll是這樣做的:epoll把select和poll分為了兩個部分,
1、調(diào)用epoll_creat建立一個epoll對象。
2、調(diào)用epoll_ctl向epoll對象中添加這100萬個連接的套接字。
3、調(diào)用epoll_wait收集發(fā)生事件的連接。=》重點(diǎn)是在這里,調(diào)用epoll_wait收集所有發(fā)生的事件的連接,并將事件放在一個鏈表中,這樣只需到該鏈表中尋找發(fā)生連接的事件,而不用遍歷100萬連接!這樣在實(shí)際收集事件時,epoll_wait效率會很高。
三個系統(tǒng)調(diào)用函數(shù)都是用C進(jìn)行封裝,在《深入理解Nginx》P310中由函數(shù)詳細(xì)說明,下面簡單介紹一下。
?
**int** epoll_create(**int** size); **int** epoll_ctl(**int** epfd, **int** op, **int** fd, **struct** epoll_event *event); **int** epoll_wait(**int** epfd, **struct** epoll_event *events,**int** maxevents, **int** timeout);首先要調(diào)用epoll_create建立一個epoll對象。參數(shù)size是內(nèi)核保證能夠正確處理的最大句柄數(shù),多于這個最大數(shù)時內(nèi)核可不保證效果。
epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監(jiān)控,或者把 epoll正在監(jiān)控的某個socket句柄移出epoll,不再監(jiān)控它等等。
epoll_wait在調(diào)用時,在給定的timeout時間內(nèi),當(dāng)在監(jiān)控的所有句柄中有事件發(fā)生時,就返回用戶態(tài)的進(jìn)程。
那么epoll是如何實(shí)現(xiàn)以上想法的呢?
當(dāng)某一個進(jìn)程調(diào)用epoll_creat方法時,linux內(nèi)核會創(chuàng)建一個eventpoll結(jié)構(gòu)體,這個結(jié)構(gòu)體中有兩個成員的使用與epoll的使用方式密切相關(guān)。
?
**struct** eventpoll { //紅黑樹的根節(jié)點(diǎn),這棵樹中存儲著所有添加到epoll中的事件,也就是這個epoll監(jiān)控的事件。 **struct** rb_root rbr; //雙向鏈表rdllist保存著將要通過epoll_wait返回給用戶的、滿足條件的事件。 **struct** list_head rdllist; }epoll為何如此高效:
當(dāng)我們調(diào)用epoll_ctl往里塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,并有效的將發(fā)生事件的句柄給我們用戶。這是由于我們在調(diào)用epoll_create時,內(nèi)核除了幫我們在epoll文件系統(tǒng)里建了個file結(jié)點(diǎn),在內(nèi)核cache里建了個紅黑樹用于存儲以后epoll_ctl傳來的socket外,還會再建立一個list鏈表,用于存儲準(zhǔn)備就緒的事件,當(dāng)epoll_wait調(diào)用時,僅僅觀察這個list鏈表里有沒有數(shù)據(jù)即可。有數(shù)據(jù)就返回,沒有數(shù)據(jù)就sleep,等到timeout時間到后即使鏈表沒數(shù)據(jù)也返回。所以,epoll_wait非常高效。
而且,通常情況下即使我們要監(jiān)控百萬計(jì)的句柄,大多一次也只返回很少量的準(zhǔn)備就緒句柄而已,所以,epoll_wait僅需要從內(nèi)核態(tài)copy少量的句柄到用戶態(tài)而已,如何能不高效?!
那么,這個準(zhǔn)備就緒list鏈表是怎么維護(hù)的呢?當(dāng)我們執(zhí)行epoll_ctl時,除了把socket放到epoll文件系統(tǒng)里file對象對應(yīng)的紅黑樹上之外,還會給內(nèi)核中斷處理程序注冊一個回調(diào)函數(shù),告訴內(nèi)核,如果這個句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。所以,當(dāng)一個socket上有數(shù)據(jù)到了,內(nèi)核在把網(wǎng)卡上的數(shù)據(jù)copy到內(nèi)核中后就來把socket插入到準(zhǔn)備就緒鏈表里了。
如此,一顆紅黑樹,一張準(zhǔn)備就緒句柄鏈表,少量的內(nèi)核cache,就幫我們解決了大并發(fā)下的socket處理問題。執(zhí)行epoll_create時,創(chuàng)建了紅黑樹和就緒鏈表,執(zhí)行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內(nèi)核注冊回調(diào)函數(shù),用于當(dāng)中斷事件來臨時向準(zhǔn)備就緒鏈表中插入數(shù)據(jù)。執(zhí)行epoll_wait時立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。
最后看看epoll獨(dú)有的兩種模式LT和ET。無論是LT和ET模式,都適用于以上所說的流程。區(qū)別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以后調(diào)用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。
這件事怎么做到的呢?當(dāng)一個socket句柄上有事件時,內(nèi)核會把該句柄插入上面所說的準(zhǔn)備就緒list鏈表,這時我們調(diào)用epoll_wait,會把準(zhǔn)備就緒的socket拷貝到用戶態(tài)內(nèi)存,然后清空準(zhǔn)備就緒list鏈表,最后,epoll_wait干了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),并且這些socket上確實(shí)有未處理的事件時,又把該句柄放回到剛剛清空的準(zhǔn)備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。
epoll的優(yōu)點(diǎn):
1.支持一個進(jìn)程打開大數(shù)目的socket描述符(FD)
?
select 最不能忍受的是一個進(jìn)程所打開的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是2048。對于那些需要支持的上萬連接數(shù)目的IM服務(wù)器來說顯然太少了。這時候你一是可以選擇修改這個宏然后重新編譯內(nèi)核,不過資料也同時指出這樣會帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案),不過雖然linux上面創(chuàng)建進(jìn)程的代價比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于2048,舉個例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,具體數(shù)目可以cat/proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
2.IO效率不隨FD數(shù)目增加而線性下降
?
傳統(tǒng)的select/poll另一個致命弱點(diǎn)就是當(dāng)你擁有一個很大的socket集合,不過由于網(wǎng)絡(luò)延時,任一時間只有部分的socket是"活躍"的,但是select/poll每次調(diào)用都會線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個問題,它只會對"活躍"的socket進(jìn)行操作---這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實(shí)現(xiàn)的。那么,只有"活躍"的socket才會主動的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會,在這點(diǎn)上,epoll實(shí)現(xiàn)了一個"偽"AIO,因?yàn)檫@時候推動力在os內(nèi)核。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環(huán)境,epoll并不比select/poll有什么效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idleconnections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了。
3.使用mmap加速內(nèi)核與用戶空間的消息傳遞
?
這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點(diǎn)上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。而如果你想我一樣從2.5內(nèi)核就關(guān)注epoll的話,一定不會忘記手工 mmap這一步的。4.內(nèi)核微調(diào)
這一點(diǎn)其實(shí)不算epoll的優(yōu)點(diǎn)了,而是整個linux平臺的優(yōu)點(diǎn)。也許你可以懷疑linux平臺,但是你無法回避linux平臺賦予你微調(diào)內(nèi)核的能力。比如,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu),那么可以在運(yùn)行時期動態(tài)調(diào)整這個內(nèi)存pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個參數(shù)(TCP完成3次握手的數(shù)據(jù)包隊(duì)列長度),也可以根據(jù)你平臺內(nèi)存大小動態(tài)調(diào)整。更甚至在一個數(shù)據(jù)包面數(shù)目巨大但同時每個數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng)卡驅(qū)動架構(gòu)。
?
總結(jié)
- 上一篇: Android: RecyclerVie
- 下一篇: Makefile:自动化变量 $@,$^