读过的最好的epoll讲解
首先我們來(lái)定義流的概念,一個(gè)流可以是文件,socket,pipe等等可以進(jìn)行I/O操作的內(nèi)核對(duì)象。
? ? 不管是文件,還是套接字,還是管道,我們都可以把他們看作流。
? ? 之后我們來(lái)討論I/O的操作,通過(guò)read,我們可以從流中讀入數(shù)據(jù);通過(guò)write,我們可以往流寫(xiě)入數(shù)據(jù)。現(xiàn)在假定一個(gè)情形,我們需要從流中讀數(shù)據(jù),但是流中還沒(méi)有數(shù)據(jù),(典型的例子為,客戶端要從socket讀如數(shù)據(jù),但是服務(wù)器還沒(méi)有把數(shù)據(jù)傳回來(lái)),這時(shí)候該怎么辦?
阻塞:阻塞是個(gè)什么概念呢?比如某個(gè)時(shí)候你在等快遞,但是你不知道快遞什么時(shí)候過(guò)來(lái),而且你沒(méi)有別的事可以干(或者說(shuō)接下來(lái)的事要等快遞來(lái)了才能做);那么你可以去睡覺(jué)了,因?yàn)槟阒揽爝f把貨送來(lái)時(shí)一定會(huì)給你打個(gè)電話(假定一定能叫醒你)。
非阻塞忙輪詢:接著上面等快遞的例子,如果用忙輪詢的方法,那么你需要知道快遞員的手機(jī)號(hào),然后每分鐘給他掛個(gè)電話:“你到了沒(méi)?”
? ? 很明顯一般人不會(huì)用第二種做法,不僅顯很無(wú)腦,浪費(fèi)話費(fèi)不說(shuō),還占用了快遞員大量的時(shí)間。
? ? 大部分程序也不會(huì)用第二種做法,因?yàn)榈谝环N方法經(jīng)濟(jì)而簡(jiǎn)單,經(jīng)濟(jì)是指消耗很少的CPU時(shí)間,如果線程睡眠了,就掉出了系統(tǒng)的調(diào)度隊(duì)列,暫時(shí)不會(huì)去瓜分CPU寶貴的時(shí)間片了。
? ? 為了了解阻塞是如何進(jìn)行的,我們來(lái)討論緩沖區(qū),以及內(nèi)核緩沖區(qū),最終把I/O事件解釋清楚。緩沖區(qū)的引入是為了減少頻繁I/O操作而引起頻繁的系統(tǒng)調(diào)用(你知道它很慢的),當(dāng)你操作一個(gè)流時(shí),更多的是以緩沖區(qū)為單位進(jìn)行操作,這是相對(duì)于用戶空間而言。對(duì)于內(nèi)核來(lái)說(shuō),也需要緩沖區(qū)。
假設(shè)有一個(gè)管道,進(jìn)程A為管道的寫(xiě)入方,B為管道的讀出方。
假設(shè)一開(kāi)始內(nèi)核緩沖區(qū)是空的,B作為讀出方,被阻塞著。然后首先A往管道寫(xiě)入,這時(shí)候內(nèi)核緩沖區(qū)由空的狀態(tài)變到非空狀態(tài),內(nèi)核就會(huì)產(chǎn)生一個(gè)事件告訴B該醒來(lái)了,這個(gè)事件姑且稱(chēng)之為“緩沖區(qū)非空”。
? ? 但是“緩沖區(qū)非空”事件通知B后,B卻還沒(méi)有讀出數(shù)據(jù);且內(nèi)核許諾了不能把寫(xiě)入管道中的數(shù)據(jù)丟掉這個(gè)時(shí)候,A寫(xiě)入的數(shù)據(jù)會(huì)滯留在內(nèi)核緩沖區(qū)中,如果內(nèi)核也緩沖區(qū)滿了,B仍未開(kāi)始讀數(shù)據(jù),最終內(nèi)核緩沖區(qū)會(huì)被填滿,這個(gè)時(shí)候會(huì)產(chǎn)生一個(gè)I/O事件,告訴進(jìn)程A,你該等等(阻塞)了,我們把這個(gè)事件定義為“緩沖區(qū)滿”。
假設(shè)后來(lái)B終于開(kāi)始讀數(shù)據(jù)了,于是內(nèi)核的緩沖區(qū)空了出來(lái),這時(shí)候內(nèi)核會(huì)告訴A,內(nèi)核緩沖區(qū)有空位了,你可以從長(zhǎng)眠中醒來(lái)了,繼續(xù)寫(xiě)數(shù)據(jù)了,我們把這個(gè)事件叫做“緩沖區(qū)非滿”
? ? 也許事件Y1已經(jīng)通知了A,但是A也沒(méi)有數(shù)據(jù)寫(xiě)入了,而B繼續(xù)讀出數(shù)據(jù),知道內(nèi)核緩沖區(qū)空了。這個(gè)時(shí)候內(nèi)核就告訴B,你需要阻塞了!,我們把這個(gè)時(shí)間定為“緩沖區(qū)空”。
這四個(gè)情形涵蓋了四個(gè)I/O事件,緩沖區(qū)滿,緩沖區(qū)空,緩沖區(qū)非空,緩沖區(qū)非滿(注都是說(shuō)的內(nèi)核緩沖區(qū),且這四個(gè)術(shù)語(yǔ)都是我生造的,僅為解釋其原理而造)。這四個(gè)I/O事件是進(jìn)行阻塞同步的根本。(如果不能理解“同步”是什么概念,請(qǐng)學(xué)習(xí)操作系統(tǒng)的鎖,信號(hào)量,條件變量等任務(wù)同步方面的相關(guān)知識(shí))。
? ? 然后我們來(lái)說(shuō)說(shuō)阻塞I/O的缺點(diǎn)。但是阻塞I/O模式下,一個(gè)線程只能處理一個(gè)流的I/O事件。如果想要同時(shí)處理多個(gè)流,要么多進(jìn)程(fork),要么多線程(pthread_create),很不幸這兩種方法效率都不高。
? ? 于是再來(lái)考慮非阻塞忙輪詢的I/O方式,我們發(fā)現(xiàn)我們可以同時(shí)處理多個(gè)流了(把一個(gè)流從阻塞模式切換到非阻塞模式再此不予討論):
while?true?{
????for?i?in?stream[];?{
????????if?i?has?data
????????????read?until?unavailable
????}
}
? ? 我們只要不停的把所有流從頭到尾問(wèn)一遍,又從頭開(kāi)始。這樣就可以處理多個(gè)流了,但這樣的做法顯然不好,因?yàn)槿绻械牧鞫紱](méi)有數(shù)據(jù),那么只會(huì)白白浪費(fèi)CPU。這里要補(bǔ)充一點(diǎn),阻塞模式下,內(nèi)核對(duì)于I/O事件的處理是阻塞或者喚醒,而非阻塞模式下則把I/O事件交給其他對(duì)象(后文介紹的select以及epoll)處理甚至直接忽略。
? ? 為了避免CPU空轉(zhuǎn),可以引進(jìn)了一個(gè)代理(一開(kāi)始有一位叫做select的代理,后來(lái)又有一位叫做poll的代理,不過(guò)兩者的本質(zhì)是一樣的)。這個(gè)代理比較厲害,可以同時(shí)觀察許多流的I/O事件,在空閑的時(shí)候,會(huì)把當(dāng)前線程阻塞掉,當(dāng)有一個(gè)或多個(gè)流有I/O事件時(shí),就從阻塞態(tài)中醒來(lái),于是我們的程序就會(huì)輪詢一遍所有的流(于是我們可以把“忙”字去掉了)。代碼長(zhǎng)這樣:
while?true?{
????select(streams[])
????for?i?in?streams[]?{
????????if?i?has?data
????????????read?until?unavailable
????}
}
? ? 于是,如果沒(méi)有I/O事件產(chǎn)生,我們的程序就會(huì)阻塞在select處。但是依然有個(gè)問(wèn)題,我們從select那里僅僅知道了,有I/O事件發(fā)生了,但卻并不知道是那幾個(gè)流(可能有一個(gè),多個(gè),甚至全部),我們只能無(wú)差別輪詢所有流,找出能讀出數(shù)據(jù),或者寫(xiě)入數(shù)據(jù)的流,對(duì)他們進(jìn)行操作。
? ? 但是使用select,我們有O(n)的無(wú)差別輪詢復(fù)雜度,同時(shí)處理的流越多,沒(méi)一次無(wú)差別輪詢時(shí)間就越長(zhǎng)。再次
說(shuō)了這么多,終于能好好解釋epoll了
? ? epoll可以理解為event poll,不同于忙輪詢和無(wú)差別輪詢,epoll之會(huì)把哪個(gè)流發(fā)生了怎樣的I/O事件通知我們。此時(shí)我們對(duì)這些流的操作都是有意義的。(復(fù)雜度降低到了O(1))
? ? 在討論epoll的實(shí)現(xiàn)細(xì)節(jié)之前,先把epoll的相關(guān)操作列出:
epoll_create 創(chuàng)建一個(gè)epoll對(duì)象,一般epollfd = epoll_create()
epoll_ctl (epoll_add/epoll_del的合體),往epoll對(duì)象中增加/刪除某一個(gè)流的某一個(gè)事件
比如
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注冊(cè)緩沖區(qū)非空事件,即有數(shù)據(jù)流入
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注冊(cè)緩沖區(qū)非滿事件,即流可以被寫(xiě)入
epoll_wait(epollfd,...)等待直到注冊(cè)的事件發(fā)生
(注:當(dāng)對(duì)一個(gè)非阻塞流的讀寫(xiě)發(fā)生緩沖區(qū)滿或緩沖區(qū)空,write/read會(huì)返回-1,并設(shè)置errno=EAGAIN。而epoll只關(guān)心緩沖區(qū)非滿和緩沖區(qū)非空事件)。
一個(gè)epoll模式的代碼大概的樣子是:
while?true?{
????active_stream[]?=?epoll_wait(epollfd)
????for?i?in?active_stream[]?{
????????read?or?write?till
????}
}
? ? 限于篇幅,我只說(shuō)這么多,以揭示原理性的東西,至于epoll的使用細(xì)節(jié),請(qǐng)參考man和google,實(shí)現(xiàn)細(xì)節(jié),請(qǐng)參閱linux kernel source。
?
------
文章來(lái)源:http://blog.csdn.net/xiajun07061225/article/details/9250579
?
什么是epoll
?
epoll是什么?按照man手冊(cè)的說(shuō)法:是為處理大批量句柄而作了改進(jìn)的poll。當(dāng)然,這不是2.6內(nèi)核才有的,它是在2.5.44內(nèi)核中被引進(jìn)的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它幾乎具備了之前所說(shuō)的一切優(yōu)點(diǎn),被公認(rèn)為L(zhǎng)inux2.6下性能最好的多路I/O就緒通知方法。
?
epoll的相關(guān)系統(tǒng)調(diào)用
epoll只有epoll_create,epoll_ctl,epoll_wait 3個(gè)系統(tǒng)調(diào)用。
?
1. int epoll_create(int size);
創(chuàng)建一個(gè)epoll的句柄。自從linux2.6.8之后,size參數(shù)是被忽略的。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后,它就是會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。
?
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊(cè)函數(shù),它不同于select()是在監(jiān)聽(tīng)事件時(shí)告訴內(nèi)核要監(jiān)聽(tīng)什么類(lèi)型的事件,而是在這里先注冊(cè)要監(jiān)聽(tīng)的事件類(lèi)型。
第一個(gè)參數(shù)是epoll_create()的返回值。
第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來(lái)表示:
EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中;
EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件;
EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;
?
第三個(gè)參數(shù)是需要監(jiān)聽(tīng)的fd。
第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽(tīng)什么事,struct epoll_event結(jié)構(gòu)如下:
?
[cpp]?view plain?copy
?
?
?
events可以是以下幾個(gè)宏的集合:
EPOLLIN?:表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě);
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET:?將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的。
EPOLLONESHOT:只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里
?
?
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll監(jiān)控的事件中已經(jīng)發(fā)送的事件。參數(shù)events是分配好的epoll_event結(jié)構(gòu)體數(shù)組,epoll將會(huì)把發(fā)生的事件賦值到events數(shù)組中(events不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個(gè)events數(shù)組中,不會(huì)去幫助我們?cè)谟脩魬B(tài)中分配內(nèi)存)。maxevents告之內(nèi)核這個(gè)events有多大,這個(gè)?maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size,參數(shù)timeout是超時(shí)時(shí)間(毫秒,0會(huì)立即返回,-1將不確定,也有說(shuō)法說(shuō)是永久阻塞)。如果函數(shù)調(diào)用成功,返回對(duì)應(yīng)I/O上已準(zhǔn)備好的文件描述符數(shù)目,如返回0表示已超時(shí)。
?
epoll工作原理
epoll同樣只告知那些就緒的文件描述符,而且當(dāng)我們調(diào)用epoll_wait()獲得就緒文件描述符時(shí),返回的不是實(shí)際的描述符,而是一個(gè)代表就緒描述符數(shù)量的值,你只需要去epoll指定的一個(gè)數(shù)組中依次取得相應(yīng)數(shù)量的文件描述符即可,這里也使用了內(nèi)存映射(mmap)技術(shù),這樣便徹底省掉了這些文件描述符在系統(tǒng)調(diào)用時(shí)復(fù)制的開(kāi)銷(xiāo)。
?
另一個(gè)本質(zhì)的改進(jìn)在于epoll采用基于事件的就緒通知方式。在select/poll中,進(jìn)程只有在調(diào)用一定的方法后,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描,而epoll事先通過(guò)epoll_ctl()來(lái)注冊(cè)一個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類(lèi)似callback的回調(diào)機(jī)制,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用epoll_wait()時(shí)便得到通知。
?
Epoll的2種工作方式-水平觸發(fā)(LT)和邊緣觸發(fā)(ET)
?
假如有這樣一個(gè)例子:
1. 我們已經(jīng)把一個(gè)用來(lái)從管道中讀取數(shù)據(jù)的文件句柄(RFD)添加到epoll描述符
2. 這個(gè)時(shí)候從管道的另一端被寫(xiě)入了2KB的數(shù)據(jù)
3. 調(diào)用epoll_wait(2),并且它會(huì)返回RFD,說(shuō)明它已經(jīng)準(zhǔn)備好讀取操作
4. 然后我們讀取了1KB的數(shù)據(jù)
5. 調(diào)用epoll_wait(2)......
?
Edge Triggered 工作模式:
如果我們?cè)诘?步將RFD添加到epoll描述符的時(shí)候使用了EPOLLET標(biāo)志,那么在第5步調(diào)用epoll_wait(2)之后將有可能會(huì)掛起,因?yàn)槭S嗟臄?shù)據(jù)還存在于文件的輸入緩沖區(qū)內(nèi),而且數(shù)據(jù)發(fā)出端還在等待一個(gè)針對(duì)已經(jīng)發(fā)出數(shù)據(jù)的反饋信息。只有在監(jiān)視的文件句柄上發(fā)生了某個(gè)事件的時(shí)候 ET 工作模式才會(huì)匯報(bào)事件。因此在第5步的時(shí)候,調(diào)用者可能會(huì)放棄等待仍在存在于文件輸入緩沖區(qū)內(nèi)的剩余數(shù)據(jù)。在上面的例子中,會(huì)有一個(gè)事件產(chǎn)生在RFD句柄上,因?yàn)樵诘?步執(zhí)行了一個(gè)寫(xiě)操作,然后,事件將會(huì)在第3步被銷(xiāo)毀。因?yàn)榈?步的讀取操作沒(méi)有讀空文件輸入緩沖區(qū)內(nèi)的數(shù)據(jù),因此我們?cè)诘?步調(diào)用 epoll_wait(2)完成后,是否掛起是不確定的。epoll工作在ET模式的時(shí)候,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫(xiě)操作把處理多個(gè)文件描述符的任務(wù)餓死。最好以下面的方式調(diào)用ET模式的epoll接口,在后面會(huì)介紹避免可能的缺陷。
? ?i ? ?基于非阻塞文件句柄
? ?ii ? 只有當(dāng)read(2)或者write(2)返回EAGAIN時(shí)才需要掛起,等待。但這并不是說(shuō)每次read()時(shí)都需要循環(huán)讀,直到讀到產(chǎn)生一個(gè)EAGAIN才認(rèn)為此次事件處理完成,當(dāng)read()返回的讀到的數(shù)據(jù)長(zhǎng)度小于請(qǐng)求的數(shù)據(jù)長(zhǎng)度時(shí),就可以確定此時(shí)緩沖中已沒(méi)有數(shù)據(jù)了,也就可以認(rèn)為此事讀事件已處理完成。
?
Level Triggered 工作模式
相反的,以LT方式調(diào)用epoll接口的時(shí)候,它就相當(dāng)于一個(gè)速度比較快的poll(2),并且無(wú)論后面的數(shù)據(jù)是否被使用,因此他們具有同樣的職能。因?yàn)榧词故褂肊T模式的epoll,在收到多個(gè)chunk的數(shù)據(jù)的時(shí)候仍然會(huì)產(chǎn)生多個(gè)事件。調(diào)用者可以設(shè)定EPOLLONESHOT標(biāo)志,在 epoll_wait(2)收到事件后epoll會(huì)與事件關(guān)聯(lián)的文件句柄從epoll描述符中禁止掉。因此當(dāng)EPOLLONESHOT設(shè)定后,使用帶有 EPOLL_CTL_MOD標(biāo)志的epoll_ctl(2)處理文件句柄就成為調(diào)用者必須作的事情。
?
?
LT(level triggered)是epoll缺省的工作方式,并且同時(shí)支持block和no-block socket.在這種做法中,內(nèi)核告訴你一個(gè)文件描述符是否就緒了,然后你可以對(duì)這個(gè)就緒的fd進(jìn)行IO操作。如果你不作任何操作,內(nèi)核還是會(huì)繼續(xù)通知你?的,所以,這種模式編程出錯(cuò)誤可能性要小一點(diǎn)。傳統(tǒng)的select/poll都是這種模型的代表.
?
ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET與LT的區(qū)別在于,當(dāng)一個(gè)新的事件到來(lái)時(shí),ET模式下當(dāng)然可以從epoll_wait調(diào)用中獲取到這個(gè)事件,可是如果這次沒(méi)有把這個(gè)事件對(duì)應(yīng)的套接字緩沖區(qū)處理完,在這個(gè)套接字中沒(méi)有新的事件再次到來(lái)時(shí),在ET模式下是無(wú)法再次從epoll_wait調(diào)用中獲取這個(gè)事件的。而LT模式正好相反,只要一個(gè)事件對(duì)應(yīng)的套接字緩沖區(qū)還有數(shù)據(jù),就總能從epoll_wait中獲取這個(gè)事件。
因此,LT模式下開(kāi)發(fā)基于epoll的應(yīng)用要簡(jiǎn)單些,不太容易出錯(cuò)。而在ET模式下事件發(fā)生時(shí),如果沒(méi)有徹底地將緩沖區(qū)數(shù)據(jù)處理完,則會(huì)導(dǎo)致緩沖區(qū)中的用戶請(qǐng)求得不到響應(yīng)。
圖示說(shuō)明:
?
Nginx默認(rèn)采用ET模式來(lái)使用epoll。
?
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)核與用戶空間的消息傳遞
????這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了。無(wú)論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點(diǎn)上,epoll是通過(guò)內(nèi)核于用戶空間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)。
?
linux下epoll如何實(shí)現(xiàn)高效處理百萬(wàn)句柄的
開(kāi)發(fā)高性能網(wǎng)絡(luò)程序時(shí),windows開(kāi)發(fā)者們言必稱(chēng)iocp,linux開(kāi)發(fā)者們則言必稱(chēng)epoll。大家都明白epoll是一種IO多路復(fù)用技術(shù),可以非常高效的處理數(shù)以百萬(wàn)計(jì)的socket句柄,比起以前的select和poll效率高大發(fā)了。我們用起epoll來(lái)都感覺(jué)挺爽,確實(shí)快,那么,它到底為什么可以高速處理這么多并發(fā)連接呢?
?
使用起來(lái)很清晰,首先要調(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í),就返回用戶態(tài)的進(jìn)程。
?
從上面的調(diào)用方式就可以看到epoll比select/poll的優(yōu)越之處:因?yàn)楹笳呙看握{(diào)用時(shí)都要傳遞你所要監(jiān)控的所有socket給select/poll系統(tǒng)調(diào)用,這意味著需要將用戶態(tài)的socket列表copy到內(nèi)核態(tài),如果以萬(wàn)計(jì)的句柄會(huì)導(dǎo)致每次都要copy幾十幾百KB的內(nèi)存到內(nèi)核態(tài),非常低效。而我們調(diào)用epoll_wait時(shí)就相當(dāng)于以往調(diào)用select/poll,但是這時(shí)卻不用傳遞socket句柄給內(nèi)核,因?yàn)閮?nèi)核已經(jīng)在epoll_ctl中拿到了要監(jiān)控的句柄列表。
?
所以,實(shí)際上在你調(diào)用epoll_create后,內(nèi)核就已經(jīng)在內(nèi)核態(tài)開(kāi)始準(zhǔn)備幫你存儲(chǔ)要監(jiān)控的句柄了,每次調(diào)用epoll_ctl只是在往內(nèi)核的數(shù)據(jù)結(jié)構(gòu)里塞入新的socket句柄。
?當(dāng)一個(gè)進(jìn)程調(diào)用epoll_creaqte方法時(shí),Linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān):
[cpp]?view plain?copy
?
?
每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體會(huì)在內(nèi)核空間中創(chuàng)造獨(dú)立的內(nèi)存,用于存儲(chǔ)使用epoll_ctl方法向epoll對(duì)象中添加進(jìn)來(lái)的事件。這樣,重復(fù)的事件就可以通過(guò)紅黑樹(shù)而高效的識(shí)別出來(lái)。
?
在epoll中,對(duì)于每一個(gè)事件都會(huì)建立一個(gè)epitem結(jié)構(gòu)體:
[cpp]?view plain?copy
?
?
此外,epoll還維護(hù)了一個(gè)雙鏈表,用戶存儲(chǔ)發(fā)生的事件。當(dāng)epoll_wait調(diào)用時(shí),僅僅觀察這個(gè)list鏈表里有沒(méi)有數(shù)據(jù)即eptime項(xiàng)即可。有數(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少量的句柄到用戶態(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的使用方法
那么究竟如何來(lái)使用epoll呢?其實(shí)非常簡(jiǎn)單。
?
通過(guò)在包含一個(gè)頭文件#include <sys/epoll.h>?以及幾個(gè)簡(jiǎn)單的API將可以大大的提高你的網(wǎng)絡(luò)服務(wù)器的支持人數(shù)。
?
首先通過(guò)create_epoll(int maxfds)來(lái)創(chuàng)建一個(gè)epoll的句柄。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄,之后的所有操作將通過(guò)這個(gè)句柄來(lái)進(jìn)行操作。在用完之后,記得用close()來(lái)關(guān)閉這個(gè)創(chuàng)建出來(lái)的epoll句柄。
?
之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來(lái)查詢所有的網(wǎng)絡(luò)接口,看哪一個(gè)可以讀,哪一個(gè)可以寫(xiě)了。基本的語(yǔ)法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
?
其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后,epoll_events里面將儲(chǔ)存所有的讀寫(xiě)事件。max_events是當(dāng)前需要監(jiān)聽(tīng)的所有socket句柄數(shù)。最后一個(gè)timeout是?epoll_wait的超時(shí),為0的時(shí)候表示馬上返回,為-1的時(shí)候表示一直等下去,直到有事件返回,為任意正整數(shù)的時(shí)候表示等這么長(zhǎng)的時(shí)間,如果一直沒(méi)有事件,則返回。一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話,可以用-1來(lái)等,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線程的話,則可以用0來(lái)保證主循環(huán)的效率。
?
epoll_wait返回之后應(yīng)該是一個(gè)循環(huán),遍歷所有的事件。
?
?
幾乎所有的epoll程序都使用下面的框架:
[cpp]?view plain?copy
?
?
epoll的程序?qū)嵗?/h2>
?
[cpp]?view plain?copy
?
?
?
運(yùn)行方式:
在一個(gè)終端運(yùn)行此程序:epoll.out PORT
另一個(gè)終端:telnet ?127.0.0.1 PORT
截圖:
?
?
參考資料:
?
http://man7.org/linux/man-pages/man2/epoll_create.2.html
https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
總結(jié)
以上是生活随笔為你收集整理的读过的最好的epoll讲解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 生活秀剧情介绍
- 下一篇: C语言指针转换为intptr_t类型