nginx學(xué)習(xí)之epoll
首先說(shuō)一下傳統(tǒng)的I/O多路復(fù)用select和poll,對(duì)比一下和epoll之間的區(qū)別:
舉個(gè)例子:假如有100萬(wàn)用戶(hù)同時(shí)與一個(gè)進(jìn)程保持TCP連接,而每一時(shí)刻只有幾十或者幾百個(gè)tcp連接是活躍的(即能接收到TCP包)玛荞,那么在每一時(shí)刻進(jìn)程只需要處理這100萬(wàn)連接中的有一小部分秉沼。
select和poll這樣處理的:在某一時(shí)刻,進(jìn)程收集所有的連接辕狰,其實(shí)這100萬(wàn)連接中大部分是沒(méi)有事件發(fā)生的改备。因此,如果每次收集事件時(shí)蔓倍,都把這100萬(wàn)連接的套接字傳給操作系統(tǒng)(這首先就是用戶(hù)態(tài)內(nèi)存到內(nèi)核內(nèi)存的大量復(fù)制)悬钳,而由操作系統(tǒng)內(nèi)核尋找這些鏈接上沒(méi)有處理的事件盐捷,將會(huì)是巨大的浪費(fèi)。
而epoll是這樣做的:epoll把select和poll分為了兩個(gè)部分默勾,
1碉渡、調(diào)用epoll_creat建立一個(gè)epoll對(duì)象。
2母剥、調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬(wàn)個(gè)連接的套接字滞诺。
3、調(diào)用epoll_wait收集發(fā)生事件的連接环疼。=》重點(diǎn)是在這里习霹,調(diào)用epoll_wait收集所有發(fā)生的事件的連接,并將事件放在一個(gè)鏈表中炫隶,這樣只需到該鏈表中尋找發(fā)生連接的事件淋叶,而不用遍歷100萬(wàn)連接!這樣在實(shí)際收集事件時(shí)伪阶,epoll_wait效率會(huì)很高煞檩。
三個(gè)系統(tǒng)調(diào)用函數(shù)都是用C進(jìn)行封裝,在《深入理解Nginx》P310中由函數(shù)詳細(xì)說(shuō)明栅贴,下面簡(jiǎn)單介紹一下斟湃。
**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建立一個(gè)epoll對(duì)象。參數(shù)size是內(nèi)核保證能夠正確處理的最大句柄數(shù)筹误,多于這個(gè)最大數(shù)時(shí)內(nèi)核可不保證效果桐早。
epoll_ctl可以操作上面建立的epoll,例如厨剪,將剛建立的socket加入到epoll中讓其監(jiān)控哄酝,或者把 epoll正在監(jiān)控的某個(gè)socket句柄移出epoll,不再監(jiān)控它等等祷膳。
epoll_wait在調(diào)用時(shí)陶衅,在給定的timeout時(shí)間內(nèi),當(dāng)在監(jiān)控的所有句柄中有事件發(fā)生時(shí)直晨,就返回用戶(hù)態(tài)的進(jìn)程搀军。
那么epoll是如何實(shí)現(xiàn)以上想法的呢?
當(dāng)某一個(gè)進(jìn)程調(diào)用epoll_creat方法時(shí)勇皇,linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體罩句,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員的使用與epoll的使用方式密切相關(guān)。
**struct** eventpoll {
//紅黑樹(shù)的根節(jié)點(diǎn)敛摘,這棵樹(shù)中存儲(chǔ)著所有添加到epoll中的事件门烂,也就是這個(gè)epoll監(jiān)控的事件。
**struct** rb_root rbr;
//雙向鏈表rdllist保存著將要通過(guò)epoll_wait返回給用戶(hù)的屯远、滿足條件的事件蔓姚。
**struct** list_head rdllist;
}
epoll為何如此高效:
當(dāng)我們調(diào)用epoll_ctl往里塞入百萬(wàn)個(gè)句柄時(shí)慨丐,epoll_wait仍然可以飛快的返回坡脐,并有效的將發(fā)生事件的句柄給我們用戶(hù)。這是由于我們?cè)谡{(diào)用epoll_create時(shí)房揭,內(nèi)核除了幫我們?cè)趀poll文件系統(tǒng)里建了個(gè)file結(jié)點(diǎn)备闲,在內(nèi)核cache里建了個(gè)紅黑樹(shù)用于存儲(chǔ)以后epoll_ctl傳來(lái)的socket外,還會(huì)再建立一個(gè)list鏈表崩溪,用于存儲(chǔ)準(zhǔn)備就緒的事件浅役,當(dāng)epoll_wait調(diào)用時(shí)斩松,僅僅觀察這個(gè)list鏈表里有沒(méi)有數(shù)據(jù)即可伶唯。有數(shù)據(jù)就返回,沒(méi)有數(shù)據(jù)就sleep惧盹,等到timeout時(shí)間到后即使鏈表沒(méi)數(shù)據(jù)也返回乳幸。所以,epoll_wait非常高效钧椰。
而且粹断,通常情況下即使我們要監(jiān)控百萬(wàn)計(jì)的句柄,大多一次也只返回很少量的準(zhǔn)備就緒句柄而已嫡霞,所以瓶埋,epoll_wait僅需要從內(nèi)核態(tài)copy少量的句柄到用戶(hù)態(tài)而已,如何能不高效诊沪?养筒!
那么,這個(gè)準(zhǔn)備就緒list鏈表是怎么維護(hù)的呢端姚?當(dāng)我們執(zhí)行epoll_ctl時(shí)晕粪,除了把socket放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹(shù)上之外,還會(huì)給內(nèi)核中斷處理程序注冊(cè)一個(gè)回調(diào)函數(shù)渐裸,告訴內(nèi)核巫湘,如果這個(gè)句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里昏鹃。所以尚氛,當(dāng)一個(gè)socket上有數(shù)據(jù)到了,內(nèi)核在把網(wǎng)卡上的數(shù)據(jù)copy到內(nèi)核中后就來(lái)把socket插入到準(zhǔn)備就緒鏈表里了洞渤。
如此阅嘶,一顆紅黑樹(shù),一張準(zhǔn)備就緒句柄鏈表您宪,少量的內(nèi)核cache奈懒,就幫我們解決了大并發(fā)下的socket處理問(wèn)題奠涌。執(zhí)行epoll_create時(shí),創(chuàng)建了紅黑樹(shù)和就緒鏈表磷杏,執(zhí)行epoll_ctl時(shí)溜畅,如果增加socket句柄,則檢查在紅黑樹(shù)中是否存在极祸,存在立即返回慈格,不存在則添加到樹(shù)干上,然后向內(nèi)核注冊(cè)回調(diào)函數(shù)遥金,用于當(dāng)中斷事件來(lái)臨時(shí)向準(zhǔn)備就緒鏈表中插入數(shù)據(jù)浴捆。執(zhí)行epoll_wait時(shí)立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。
最后看看epoll獨(dú)有的兩種模式LT和ET稿械。無(wú)論是LT和ET模式选泻,都適用于以上所說(shuō)的流程。區(qū)別是美莫,LT模式下页眯,只要一個(gè)句柄上的事件一次沒(méi)有處理完,會(huì)在以后調(diào)用epoll_wait時(shí)次次返回這個(gè)句柄厢呵,而ET模式僅在第一次返回窝撵。
這件事怎么做到的呢?當(dāng)一個(gè)socket句柄上有事件時(shí)襟铭,內(nèi)核會(huì)把該句柄插入上面所說(shuō)的準(zhǔn)備就緒list鏈表碌奉,這時(shí)我們調(diào)用epoll_wait,會(huì)把準(zhǔn)備就緒的socket拷貝到用戶(hù)態(tài)內(nèi)存寒砖,然后清空準(zhǔn)備就緒list鏈表赐劣,最后,epoll_wait干了件事入撒,就是檢查這些socket隆豹,如果不是ET模式(就是LT模式的句柄了),并且這些socket上確實(shí)有未處理的事件時(shí)茅逮,又把該句柄放回到剛剛清空的準(zhǔn)備就緒鏈表了璃赡。所以,非ET的句柄献雅,只要它上面還有事件碉考,epoll_wait每次都會(huì)返回。而ET模式的句柄挺身,除非有新中斷到侯谁,即使socket上的事件沒(méi)有處理完,也是不會(huì)次次從epoll_wait返回的。
epoll的優(yōu)點(diǎn):
1.支持一個(gè)進(jìn)程打開(kāi)大數(shù)目的socket描述符(FD)
select 最不能忍受的是一個(gè)進(jìn)程所打開(kāi)的FD是有一定限制的墙贱,由FD_SETSIZE設(shè)置热芹,默認(rèn)值是2048。對(duì)于那些需要支持的上萬(wàn)連接數(shù)目的IM服務(wù)器來(lái)說(shuō)顯然太少了惨撇。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核伊脓,不過(guò)資料也同時(shí)指出這樣會(huì)帶來(lái)網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案)魁衙,不過(guò)雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小报腔,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效剖淀,所以也不是一種完美的方案纯蛾。不過(guò) epoll則沒(méi)有這個(gè)限制,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目纵隔,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右翻诉,具體數(shù)目可以cat
/proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
2.IO效率不隨FD數(shù)目增加而線性下降
傳統(tǒng)的select/poll另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的socket集合巨朦,不過(guò)由于網(wǎng)絡(luò)延時(shí)米丘,任一時(shí)間只有部分的socket是"活躍"的,但是select/poll每次調(diào)用都會(huì)線性掃描全部的集合糊啡,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個(gè)問(wèn)題吁津,它只會(huì)對(duì)"活躍"的socket進(jìn)行操作---這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中epoll是根據(jù)每個(gè)fd上面的callback函數(shù)實(shí)現(xiàn)的棚蓄。那么,只有"活躍"的socket才會(huì)主動(dòng)的去調(diào)用 callback函數(shù)碍脏,其他idle狀態(tài)socket則不會(huì)梭依,在這點(diǎn)上,epoll實(shí)現(xiàn)了一個(gè)"偽"AIO典尾,因?yàn)檫@時(shí)候推動(dòng)力在os內(nèi)核役拴。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個(gè)高速LAN環(huán)境钾埂,epoll并不比select/poll有什么效率河闰,相反,如果過(guò)多使用epoll_ctl,效率相比還有稍微的下降褥紫。但是一旦使用idle
connections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了姜性。
3.使用mmap加速內(nèi)核與用戶(hù)空間的消息傳遞
這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了。無(wú)論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶(hù)空間髓考,如何避免不必要的內(nèi)存拷貝就很重要部念,在這點(diǎn)上,epoll是通過(guò)內(nèi)核于用戶(hù)空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。而如果你想我一樣從2.5內(nèi)核就關(guān)注epoll的話儡炼,一定不會(huì)忘記手工 mmap這一步的妓湘。
4.內(nèi)核微調(diào)
這一點(diǎn)其實(shí)不算epoll的優(yōu)點(diǎn)了,而是整個(gè)linux平臺(tái)的優(yōu)點(diǎn)乌询。也許你可以懷疑linux平臺(tái)多柑,但是你無(wú)法回避linux平臺(tái)賦予你微調(diào)內(nèi)核的能力。比如楣责,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu)竣灌,那么可以在運(yùn)行時(shí)期動(dòng)態(tài)調(diào)整這個(gè)內(nèi)存pool(skb_head_pool)的大小--- 通過(guò)echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個(gè)參數(shù)(TCP完成3次握手的數(shù)據(jù)包隊(duì)列長(zhǎng)度)秆麸,也可以根據(jù)你平臺(tái)內(nèi)存大小動(dòng)態(tài)調(diào)整初嘹。更甚至在一個(gè)數(shù)據(jù)包面數(shù)目巨大但同時(shí)每個(gè)數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng)卡驅(qū)動(dòng)架構(gòu)。