轉(zhuǎn)載:https://cloud.tencent.com/developer/article/1005481
提到select故俐、poll、epoll相信大家都耳熟能詳了紊婉,三個都是IO多路復(fù)用的機制药版,可以監(jiān)視多個描述符的讀/寫等事件,一旦某個描述符就緒(一般是讀或者寫事件發(fā)生了)喻犁,就能夠?qū)l(fā)生的事件通知給關(guān)心的應(yīng)用程序去處理該事件槽片。本質(zhì)上,select肢础、poll还栓、epoll本質(zhì)上都是同步I/O,相信大家都讀過Richard Stevens的經(jīng)典書籍UNP(UNIX:registered: Network Programming)传轰,書中給出了5種IO模型:
[1] blocking IO - 阻塞IO
[2] nonblocking IO - 非阻塞IO
[3] IO multiplexing - IO多路復(fù)用
[4] signal driven IO - 信號驅(qū)動IO
[5] asynchronous IO - 異步IO
其中前面4種IO都可以歸類為synchronous IO - 同步IO剩盒,在介紹select、poll慨蛙、epoll之前辽聊,首先介紹一下這幾種IO模型,signal driven IO平時用的比較少期贫,這里就不介紹了跟匆。
1. IO - 同步、異步通砍、阻塞贾铝、非阻塞
下面以network IO中的read讀操作為切入點,來講述同步(synchronous) IO和異步(asynchronous) IO埠帕、阻塞(blocking) IO和非阻塞(non-blocking)IO的異同垢揩。一般情況下,一次網(wǎng)絡(luò)IO讀操作會涉及兩個系統(tǒng)對象:
(1) 用戶進程(線程)Process敛瓷;
(2)內(nèi)核對象kernel叁巨,兩個處理階段:
Waiting for the data to be ready - 等待數(shù)據(jù)準備好
Copying the data from the kernel to the process - 將數(shù)據(jù)從內(nèi)核空間的buffer拷貝到用戶空間進程的buffer
IO模型的異同點就是區(qū)分在這兩個系統(tǒng)對象、兩個處理階段的不同上呐籽。
1.1 同步IO 之 Blocking IO
如上圖所示锋勺,用戶進程process在Blocking IO讀recvfrom操作的兩個階段都是等待的。在數(shù)據(jù)沒準備好的時候狡蝶,process原地等待kernel準備數(shù)據(jù)庶橱。kernel準備好數(shù)據(jù)后,process繼續(xù)等待kernel將數(shù)據(jù)copy到自己的buffer贪惹。在kernel完成數(shù)據(jù)的copy后process才會從recvfrom系統(tǒng)調(diào)用中返回苏章。
1.2 同步IO 之 NonBlocking IO
從圖中可以看出,process在NonBlocking IO讀recvfrom操作的第一個階段是不會block等待的奏瞬,如果kernel數(shù)據(jù)還沒準備好枫绅,那么recvfrom會立刻返回一個EWOULDBLOCK錯誤。當kernel準備好數(shù)據(jù)后硼端,進入處理的第二階段的時候并淋,process會等待kernel將數(shù)據(jù)copy到自己的buffer,在kernel完成數(shù)據(jù)的copy后process才會從recvfrom系統(tǒng)調(diào)用中返回珍昨。
1.3 同步IO 之 IO multiplexing
IO多路復(fù)用县耽,就是我們熟知的select、poll镣典、epoll模型兔毙。從圖上可見,在IO多路復(fù)用的時候骆撇,process在兩個處理階段都是block住等待的瞒御。初看好像IO多路復(fù)用沒什么用,其實select神郊、poll肴裙、epoll的優(yōu)勢在于可以以較少的代價來同時監(jiān)聽處理多個IO。
1.4 異步IO
從上圖看出涌乳,異步IO要求process在recvfrom操作的兩個處理階段上都不能等待蜻懦,也就是process調(diào)用recvfrom后立刻返回,kernel自行去準備好數(shù)據(jù)并將數(shù)據(jù)從kernel的buffer中copy到process的buffer在通知process讀操作完成了夕晓,然后process在去處理宛乃。遺憾的是,linux的網(wǎng)絡(luò)IO中是不存在異步IO的,linux的網(wǎng)絡(luò)IO處理的第二階段總是阻塞等待數(shù)據(jù)copy完成的征炼。真正意義上的網(wǎng)絡(luò)異步IO是Windows下的IOCP(IO完成端口)模型析既。
很多時候,我們比較容易混淆non-blocking IO和asynchronous IO谆奥,認為是一樣的眼坏。但是通過上圖,幾種IO模型的比較酸些,會發(fā)現(xiàn)non-blocking IO和asynchronous IO的區(qū)別還是很明顯的宰译,non-blocking IO僅僅要求處理的第一階段不block即可,而asynchronous IO要求兩個階段都不能block住魄懂。
2 Linux的socket 事件wakeup callback機制
言歸正傳沿侈,在介紹select、poll市栗、epoll前缀拭,有必要說說linux(2.6+)內(nèi)核的事件wakeup callback機制,這是IO多路復(fù)用機制存在的本質(zhì)肃廓。Linux通過socket睡眠隊列來管理所有等待socket的某個事件的process智厌,同時通過wakeup機制來異步喚醒整個睡眠隊列上等待事件的process,通知process相關(guān)事件發(fā)生盲赊。通常情況铣鹏,socket的事件發(fā)生的時候,其會順序遍歷socket睡眠隊列上的每個process節(jié)點哀蘑,調(diào)用每個process節(jié)點掛載的callback函數(shù)诚卸。在遍歷的過程中,如果遇到某個節(jié)點是排他的绘迁,那么就終止遍歷合溺,總體上會涉及兩大邏輯:
(1)睡眠等待邏輯;
(2)喚醒邏輯缀台。
(1)睡眠等待邏輯:涉及select棠赛、poll、epoll_wait的阻塞等待邏輯
[1]select膛腐、poll睛约、epoll_wait陷入內(nèi)核,判斷監(jiān)控的socket是否有關(guān)心的事件發(fā)生了哲身,如果沒辩涝,則為當前process構(gòu)建一個wait_entry節(jié)點,然后插入到監(jiān)控socket的sleep_list
[2]進入循環(huán)的schedule直到關(guān)心的事件發(fā)生了
[3]關(guān)心的事件發(fā)生后勘天,將當前process的wait_entry節(jié)點從socket的sleep_list中刪除怔揩。
(2)喚醒邏輯:
[1]socket的事件發(fā)生了捉邢,然后socket順序遍歷其睡眠隊列,依次調(diào)用每個wait_entry節(jié)點的callback函數(shù)
[2]直到完成隊列的遍歷或遇到某個wait_entry節(jié)點是排他的才停止商膊。
[3]一般情況下callback包含兩個邏輯:1.wait_entry自定義的私有邏輯伏伐;2.喚醒的公共邏輯,主要用于將該wait_entry的process放入CPU的就緒隊列翘狱,讓CPU隨后可以調(diào)度其執(zhí)行秘案。
下面就上面的兩大邏輯,分別闡述select潦匈、poll、epoll的異同赚导,為什么epoll能夠比select茬缩、poll高效。
3 大話Select—1024
在一個高性能的網(wǎng)絡(luò)服務(wù)上吼旧,大多情況下一個服務(wù)進程(線程)process需要同時處理多個socket凰锡,我們需要公平對待所有socket,對于read而言圈暗,那個socket有數(shù)據(jù)可讀掂为,process就去讀取該socket的數(shù)據(jù)來處理。于是對于read员串,一個樸素的需求就是關(guān)心的N個socket是否有數(shù)據(jù)”可讀”勇哗,也就是我們期待”可讀”事件的通知,而不是盲目地對每個socket調(diào)用recv/recvfrom來嘗試接收數(shù)據(jù)寸齐。我們應(yīng)該block在等待事件的發(fā)生上欲诺,這個事件簡單點就是”關(guān)心的N個socket中一個或多個socket有數(shù)據(jù)可讀了”,當block解除的時候渺鹦,就意味著扰法,我們一定可以找到一個或多個socket上有可讀的數(shù)據(jù)。另一方面毅厚,根據(jù)上面的socket wakeup callback機制塞颁,我們不知道什么時候,哪個socket會有讀事件發(fā)生吸耿,于是祠锣,process需要同時插入到這N個socket的sleep_list上等待任意一個socket可讀事件發(fā)生而被喚醒,當時process被喚醒的時候珍语,其callback里面應(yīng)該有個邏輯去檢查具體那些socket可讀了锤岸。
于是,select的多路復(fù)用邏輯就清晰了板乙,select為每個socket引入一個poll邏輯是偷,該poll邏輯用于收集socket發(fā)生的事件拳氢,對于可讀事件來說,簡單偽碼如下:
poll()
{
//其他邏輯
if (recieve queque is not empty)
{
sk_event |= POLL_IN蛋铆;
}
//其他邏輯
}
接下來就到select的邏輯了馋评,下面是select的函數(shù)原型:5個參數(shù),后面4個參數(shù)都是in/out類型(值可能會被修改返回)
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
當用戶process調(diào)用select的時候刺啦,select會將需要監(jiān)控的readfds集合拷貝到內(nèi)核空間(假設(shè)監(jiān)控的僅僅是socket可讀)留特,然后遍歷自己監(jiān)控的socket sk,挨個調(diào)用sk的poll邏輯以便檢查該sk是否有可讀事件玛瘸,遍歷完所有的sk后蜕青,如果沒有任何一個sk可讀,那么select會調(diào)用schedule_timeout進入schedule循環(huán)糊渊,使得process進入睡眠右核。如果在timeout時間內(nèi)某個sk上有數(shù)據(jù)可讀了,或者等待timeout了渺绒,則調(diào)用select的process會被喚醒贺喝,接下來select就是遍歷監(jiān)控的sk集合,挨個收集可讀事件并返回給用戶了宗兼,相應(yīng)的偽碼如下:
for (sk in readfds)
{
sk_event.evt = sk.poll();
sk_event.sk = sk;
ret_event_for_process;
}
通過上面的select邏輯過程分析躏鱼,相信大家都意識到,select存在兩個問題:
[1] 被監(jiān)控的fds需要從用戶空間拷貝到內(nèi)核空間
為了減少數(shù)據(jù)拷貝帶來的性能損壞殷绍,內(nèi)核對被監(jiān)控的fds集合大小做了限制染苛,并且這個是通過宏控制的,大小不可改變(限制為1024)篡帕。
[2] 被監(jiān)控的fds集合中殖侵,只要有一個有數(shù)據(jù)可讀,整個socket集合就會被遍歷一次調(diào)用sk的poll函數(shù)收集可讀事件
由于當初的需求是樸素镰烧,僅僅關(guān)心是否有數(shù)據(jù)可讀這樣一個事件拢军,當事件通知來的時候,由于數(shù)據(jù)的到來是異步的怔鳖,我們不知道事件來的時候茉唉,有多少個被監(jiān)控的socket有數(shù)據(jù)可讀了,于是结执,只能挨個遍歷每個socket來收集可讀事件度陆。
到這里,我們有三個問題需要解決:
(1)被監(jiān)控的fds集合限制為1024献幔,1024太小了懂傀,我們希望能夠有個比較大的可監(jiān)控fds集合
(2)fds集合需要從用戶空間拷貝到內(nèi)核空間的問題,我們希望不需要拷貝
(3)當被監(jiān)控的fds中某些有數(shù)據(jù)可讀的時候蜡感,我們希望通知更加精細一點蹬蚁,就是我們希望能夠從通知中得到有可讀事件的fds列表恃泪,而不是需要遍歷整個fds來收集。
4 大話poll—雞肋
select遺留的三個問題中犀斋,問題(1)是用法限制問題贝乎,問題(2)和(3)則是性能問題。poll和select非常相似叽粹,poll并沒著手解決性能問題览效,poll只是解決了select的問題(1)fds集合大小1024限制問題。下面是poll的函數(shù)原型虫几,poll改變了fds集合的描述方式锤灿,使用了pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu),使得poll支持的fds集合限制遠大于select的1024持钉。poll雖然解決了fds集合大小1024的限制問題衡招,但是,它并沒改變大量描述符數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核態(tài)的地址空間之間每强,以及個別描述符就緒觸發(fā)整體描述符集合的遍歷的低效問題。poll隨著監(jiān)控的socket集合的增加性能線性下降州刽,poll不適合用于大并發(fā)場景空执。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
5 大話epoll—終極武功
select遺留的三個問題,問題(1)是比較好解決穗椅,poll簡單兩三下就解決掉了辨绊,但是poll的解決有點雞肋。要解決問題(2)和(3)似乎比較棘手匹表,要怎么解決呢门坷?我們知道,在計算機行業(yè)中袍镀,有兩種解決問題的思想:
[1] 計算機科學領(lǐng)域的任何問題, 都可以通過添加一個中間層來解決
[2] 變集中(中央)處理為分散(分布式)處理
下面默蚌,我們看看,epoll在解決select的遺留問題(2)和(3)的時候苇羡,怎么運用這兩個思想的绸吸。
5.1 fds集合拷貝問題的解決
對于IO多路復(fù)用,有兩件事是必須要做的(對于監(jiān)控可讀事件而言):
- 準備好需要監(jiān)控的fds集合设江;
- 探測并返回fds集合中哪些fd可讀了锦茁。
細看select或poll的函數(shù)原型,我們會發(fā)現(xiàn)叉存,每次調(diào)用select或poll都在重復(fù)地準備(集中處理)整個需要監(jiān)控的fds集合码俩。然而對于頻繁調(diào)用的select或poll而言,fds集合的變化頻率要低得多歼捏,我們沒必要每次都重新準備(集中處理)整個fds集合稿存。
于是笨篷,epoll引入了epoll_ctl系統(tǒng)調(diào)用,將高頻調(diào)用的epoll_wait和低頻的epoll_ctl隔離開挠铲。同時冕屯,epoll_ctl通過(EPOLL_CTL_ADD、EPOLL_CTL_MOD拂苹、EPOLL_CTL_DEL)三個操作來分散對需要監(jiān)控的fds集合的修改安聘,做到了有變化才變更,將select或poll高頻瓢棒、大塊內(nèi)存拷貝(集中處理)變成epoll_ctl的低頻浴韭、小塊內(nèi)存的拷貝(分散處理),避免了大量的內(nèi)存拷貝脯宿。同時念颈,對于高頻epoll_wait的可讀就緒的fd集合返回的拷貝問題,epoll通過內(nèi)核與用戶空間mmap(內(nèi)存映射)同一塊內(nèi)存來解決连霉。mmap將用戶空間的一塊地址和內(nèi)核空間的一塊地址同時映射到相同的一塊物理內(nèi)存地址(不管是用戶空間還是內(nèi)核空間都是虛擬地址榴芳,最終要通過地址映射映射到物理地址),使得這塊物理內(nèi)存對內(nèi)核和對用戶均可見跺撼,減少用戶態(tài)和內(nèi)核態(tài)之間的數(shù)據(jù)交換窟感。
另外,epoll通過epoll_ctl來對監(jiān)控的fds集合來進行增歉井、刪柿祈、改,那么必須涉及到fd的快速查找問題哩至,于是躏嚎,一個低時間復(fù)雜度的增、刪菩貌、改卢佣、查的數(shù)據(jù)結(jié)構(gòu)來組織被監(jiān)控的fds集合是必不可少的了。在linux 2.6.8之前的內(nèi)核菜谣,epoll使用hash來組織fds集合珠漂,于是在創(chuàng)建epoll fd的時候,epoll需要初始化hash的大小尾膊。于是epoll_create(int size)有一個參數(shù)size媳危,以便內(nèi)核根據(jù)size的大小來分配hash的大小。在linux 2.6.8以后的內(nèi)核中冈敛,epoll使用紅黑樹來組織監(jiān)控的fds集合待笑,于是epoll_create(int size)的參數(shù)size實際上已經(jīng)沒有意義了。
5.2 按需遍歷就緒的fds集合
通過上面的socket的睡眠隊列喚醒邏輯我們知道抓谴,socket喚醒睡眠在其睡眠隊列的wait_entry(process)的時候會調(diào)用wait_entry的回調(diào)函數(shù)callback暮蹂,并且寞缝,我們可以在callback中做任何事情。為了做到只遍歷就緒的fd仰泻,我們需要有個地方來組織那些已經(jīng)就緒的fd荆陆。為此,epoll引入了一個中間層集侯,一個雙向鏈表(ready_list)被啼,一個單獨的睡眠隊列(single_epoll_wait_list),并且棠枉,與select或poll不同的是浓体,epoll的process不需要同時插入到多路復(fù)用的socket集合的所有睡眠隊列中,相反process只是插入到中間層的epoll的單獨睡眠隊列中辈讶,process睡眠在epoll的單獨隊列上命浴,等待事件的發(fā)生。同時贱除,引入一個中間的wait_entry_sk生闲,它與某個socket sk密切相關(guān),wait_entry_sk睡眠在sk的睡眠隊列上月幌,其callback函數(shù)邏輯是將當前sk排入到epoll的ready_list中跪腹,并喚醒epoll的single_epoll_wait_list。而single_epoll_wait_list上睡眠的process的回調(diào)函數(shù)就明朗了:遍歷ready_list上的所有sk飞醉,挨個調(diào)用sk的poll函數(shù)收集事件,然后喚醒process從epoll_wait返回屯阀。
于是缅帘,整個過來可以分為以下幾個邏輯:
(1)epoll_ctl EPOLL_CTL_ADD邏輯
[1] 構(gòu)建睡眠實體wait_entry_sk,將當前socket sk關(guān)聯(lián)給wait_entry_sk难衰,并設(shè)置wait_entry_sk的回調(diào)函數(shù)為epoll_callback_sk
[2] 將wait_entry_sk排入當前socket sk的睡眠隊列上
回調(diào)函數(shù)epoll_callback_sk的邏輯如下:
[1] 將之前關(guān)聯(lián)的sk排入epoll的ready_list
[2] 然后喚醒epoll的單獨睡眠隊列single_epoll_wait_list
(2)epoll_wait邏輯
[1] 構(gòu)建睡眠實體wait_entry_proc钦无,將當前process關(guān)聯(lián)給wait_entry_proc,并設(shè)置回調(diào)函數(shù)為epoll_callback_proc
[2] 判斷epoll的ready_list是否為空盖袭,如果為空失暂,則將wait_entry_proc排入epoll的single_epoll_wait_list中,隨后進入schedule循環(huán)鳄虱,這會導(dǎo)致調(diào)用epoll_wait的process睡眠弟塞。
[3] wait_entry_proc被事件喚醒或超時醒來,wait_entry_proc將被從single_epoll_wait_list移除掉拙已,然后wait_entry_proc執(zhí)行回調(diào)函數(shù)epoll_callback_proc
回調(diào)函數(shù)epoll_callback_proc的邏輯如下:
[1] 遍歷epoll的ready_list决记,挨個調(diào)用每個sk的poll邏輯收集發(fā)生的事件,對于監(jiān)控可讀事件而已倍踪,ready_list上的每個sk都是有數(shù)據(jù)可讀的系宫,這里的遍歷必要的(不同于select/poll的遍歷索昂,它不管有沒數(shù)據(jù)可讀都需要遍歷一些來判斷,這樣就做了很多無用功扩借。)
[2] 將每個sk收集到的事件椒惨,通過epoll_wait傳入的events數(shù)組回傳并喚醒相應(yīng)的process。
(3)epoll喚醒邏輯
整個epoll的協(xié)議棧喚醒邏輯如下(對于可讀事件而言):
[1] 協(xié)議數(shù)據(jù)包到達網(wǎng)卡并被排入socket sk的接收隊列
[2] 睡眠在sk的睡眠隊列wait_entry被喚醒潮罪,wait_entry_sk的回調(diào)函數(shù)epoll_callback_sk被執(zhí)行
[3] epoll_callback_sk將當前sk插入epoll的ready_list中
[4] 喚醒睡眠在epoll的單獨睡眠隊列single_epoll_wait_list的wait_entry康谆,wait_entry_proc被喚醒執(zhí)行回調(diào)函數(shù)epoll_callback_proc
[5] 遍歷epoll的ready_list,挨個調(diào)用每個sk的poll邏輯收集發(fā)生的事件
[6] 將每個sk收集到的事件错洁,通過epoll_wait傳入的events數(shù)組回傳并喚醒相應(yīng)的process秉宿。
epoll巧妙的引入一個中間層解決了大量監(jiān)控socket的無效遍歷問題。細心的同學會發(fā)現(xiàn)屯碴,epoll在中間層上為每個監(jiān)控的socket準備了一個單獨的回調(diào)函數(shù)epoll_callback_sk描睦,而對于select/poll窝剖,所有的socket都公用一個相同的回調(diào)函數(shù)教沾。正是這個單獨的回調(diào)epoll_callback_sk使得每個socket都能單獨處理自身书劝,當自己就緒的時候?qū)⒆陨韘ocket掛入epoll的ready_list啦辐。同時域蜗,epoll引入了一個睡眠隊列single_epoll_wait_list走诞,分割了兩類睡眠等待侯谁。process不再睡眠在所有的socket的睡眠隊列上导帝,而是睡眠在epoll的睡眠隊列上虚缎,在等待”任意一個socket可讀就緒”事件撵彻。而中間wait_entry_sk則代替process睡眠在具體的socket上,當socket就緒的時候实牡,它就可以處理自身了陌僵。
5.3 ET(Edge Triggered 邊沿觸發(fā)) vs LT(Level Triggered 水平觸發(fā))
5.3.1 ET vs LT - 概念
說到Epoll就不能不說說Epoll事件的兩種模式了,下面是兩個模式的基本概念
Edge Triggered (ET) 邊沿觸發(fā)
.socket的接收緩沖區(qū)狀態(tài)變化時觸發(fā)讀事件创坞,即空的接收緩沖區(qū)剛接收到數(shù)據(jù)時觸發(fā)讀事件
.socket的發(fā)送緩沖區(qū)狀態(tài)變化時觸發(fā)寫事件碗短,即滿的緩沖區(qū)剛空出空間時觸發(fā)讀事件
僅在緩沖區(qū)狀態(tài)變化時觸發(fā)事件,比如數(shù)據(jù)緩沖去從無到有的時候(不可讀-可讀)
Level Triggered (LT) 水平觸發(fā)
.socket接收緩沖區(qū)不為空题涨,有數(shù)據(jù)可讀偎谁,則讀事件一直觸發(fā)
.socket發(fā)送緩沖區(qū)不滿可以繼續(xù)寫入數(shù)據(jù),則寫事件一直觸發(fā)
符合思維習慣纲堵,epoll_wait返回的事件就是socket的狀態(tài)
通常情況下巡雨,大家都認為ET模式更為高效,實際上是不是呢婉支?下面我們來說說兩種模式的本質(zhì):
我們來回顧一下鸯隅,5.2節(jié)(3)epoll喚醒邏輯 的第五個步驟
[5] 遍歷epoll的ready_list,挨個調(diào)用每個sk的poll邏輯收集發(fā)生的事件
大家是不是有個疑問呢:掛在ready_list上的sk什么時候會被移除掉呢?其實蝌以,sk從ready_list移除的時機正是區(qū)分兩種事件模式的本質(zhì)炕舵。因為,通過上面的介紹跟畅,我們知道ready_list是否為空是epoll_wait是否返回的條件咽筋。于是,在兩種事件模式下徊件,步驟5如下:
對于Edge Triggered (ET) 邊沿觸發(fā):
[5] 遍歷epoll的ready_list奸攻,將sk從ready_list中移除,然后調(diào)用該sk的poll邏輯收集發(fā)生的事件
對于Level Triggered (LT) 水平觸發(fā):
[5.1] 遍歷epoll的ready_list虱痕,將sk從ready_list中移除睹耐,然后調(diào)用該sk的poll邏輯收集發(fā)生的事件
[5.2] 如果該sk的poll函數(shù)返回了關(guān)心的事件(對于可讀事件來說,就是POLL_IN事件)部翘,那么該sk被重新加入到epoll的ready_list中硝训。
對于可讀事件而言,在ET模式下新思,如果某個socket有新的數(shù)據(jù)到達窖梁,那么該sk就會被排入epoll的ready_list,從而epoll_wait就一定能收到可讀事件的通知(調(diào)用sk的poll邏輯一定能收集到可讀事件)夹囚。于是纵刘,我們通常理解的緩沖區(qū)狀態(tài)變化(從無到有)的理解是不準確的,準確的理解應(yīng)該是是否有新的數(shù)據(jù)達到緩沖區(qū)荸哟。
而在LT模式下假哎,某個sk被探測到有數(shù)據(jù)可讀,那么該sk會被重新加入到read_list鞍历,那么在該sk的數(shù)據(jù)被全部取走前位谋,下次調(diào)用epoll_wait就一定能夠收到該sk的可讀事件(調(diào)用sk的poll邏輯一定能收集到可讀事件),從而epoll_wait就能返回堰燎。
5.3.2 ET vs LT - 性能
通過上面的概念介紹,我們知道對于可讀事件而言笋轨,LT比ET多了兩個操作:(1)對ready_list的遍歷的時候秆剪,對于收集到可讀事件的sk會重新放入ready_list;(2)下次epoll_wait的時候會再次遍歷上次重新放入的sk爵政,如果sk本身沒有數(shù)據(jù)可讀了仅讽,那么這次遍歷就變得多余了。
在服務(wù)端有海量活躍socket的時候钾挟,LT模式下洁灵,epoll_wait返回的時候,會有海量的socket sk重新放入ready_list。如果徽千,用戶在第一次epoll_wait返回的時候苫费,將有數(shù)據(jù)的socket都處理掉了,那么下次epoll_wait的時候双抽,上次epoll_wait重新入ready_list的sk被再次遍歷就有點多余百框,這個時候LT確實會帶來一些性能損失。然而牍汹,實際上會存在很多多余的遍歷么铐维?
先不說第一次epoll_wait返回的時候,用戶進程能否都將有數(shù)據(jù)返回的socket處理掉慎菲。在用戶處理的過程中嫁蛇,如果該socket有新的數(shù)據(jù)上來,那么協(xié)議棧發(fā)現(xiàn)sk已經(jīng)在ready_list中了露该,那么就不需要再次放入ready_list睬棚,也就是在LT模式下,對該sk的再次遍歷不是多余的有决,是有效的闸拿。同時,我們回歸epoll高效的場景在于书幕,服務(wù)器有海量socket新荤,但是活躍socket較少的情況下才會體現(xiàn)出epoll的高效、高性能台汇。因此苛骨,在實際的應(yīng)用場合,絕大多數(shù)情況下苟呐,ET模式在性能上并不會比LT模式具有壓倒性的優(yōu)勢痒芝,至少,目前還沒有實際應(yīng)用場合的測試表面ET比LT性能更好牵素。
5.3.3 ET vs LT - 復(fù)雜度
我們知道严衬,對于可讀事件而言,在阻塞模式下笆呆,是無法識別隊列空的事件的请琳,并且,事件通知機制赠幕,僅僅是通知有數(shù)據(jù)俄精,并不會通知有多少數(shù)據(jù)。于是榕堰,在阻塞模式下竖慧,在epoll_wait返回的時候,我們對某個socket_fd調(diào)用recv或read讀取并返回了一些數(shù)據(jù)的時候,我們不能再次直接調(diào)用recv或read圾旨,因為踱讨,如果socket_fd已經(jīng)無數(shù)據(jù)可讀的時候,進程就會阻塞在該socket_fd的recv或read調(diào)用上碳胳,這樣就影響了IO多路復(fù)用的邏輯(我們希望是阻塞在所有被監(jiān)控socket的epoll_wait調(diào)用上勇蝙,而不是單獨某個socket_fd上),造成其他socket餓死挨约,即使有數(shù)據(jù)來了味混,也無法處理。
接下來诫惭,我們只能再次調(diào)用epoll_wait來探測一些socket_fd翁锡,看是否還有數(shù)據(jù)可讀。在LT模式下夕土,如果socket_fd還有數(shù)據(jù)可讀馆衔,那么epoll_wait就一定能夠返回,接著怨绣,我們就可以對該socket_fd調(diào)用recv或read讀取數(shù)據(jù)角溃。然而,在ET模式下篮撑,盡管socket_fd還是數(shù)據(jù)可讀减细,但是如果沒有新的數(shù)據(jù)上來,那么epoll_wait是不會通知可讀事件的赢笨。這個時候未蝌,epoll_wait阻塞住了,這下子坑爹了茧妒,明明有數(shù)據(jù)你不處理萧吠,非要等新的數(shù)據(jù)來了在處理,那么我們就死扛咯桐筏,看誰先忍不住纸型。
等等,在阻塞模式下梅忌,不是不能用ET的么绊袋?是的,正是因為有這樣的缺點铸鹰,ET強制需要在非阻塞模式下使用。在ET模式下皂岔,epoll_wait返回socket_fd有數(shù)據(jù)可讀蹋笼,我們必須要讀完所有數(shù)據(jù)才能離開。因為,如果不讀完剖毯,epoll不會在通知你了圾笨,雖然有新的數(shù)據(jù)到來的時候,會再次通知逊谋,但是我們并不知道新數(shù)據(jù)會不會來擂达,以及什么時候會來。由于在阻塞模式下胶滋,我們是無法通過recv/read來探測空數(shù)據(jù)事件板鬓,于是,我們必須采用非阻塞模式究恤,一直read直到EAGAIN俭令。因此,ET要求socket_fd非阻塞也就不難理解了部宿。
另外抄腔,epoll_wait原本的語意是:監(jiān)控并探測socket是否有數(shù)據(jù)可讀(對于讀事件而言)。LT模式保留了其原本的語意理张,只要socket還有數(shù)據(jù)可讀赫蛇,它就能不斷反饋,于是雾叭,我們想什么時候讀取處理都可以悟耘,我們永遠有再次poll的機會去探測是否有數(shù)據(jù)可以處理,這樣帶來了編程上的很大方便拷况,不容易死鎖造成某些socket餓死作煌。相反,ET模式修改了epoll_wait原本的語意赚瘦,變成了:監(jiān)控并探測socket是否有新的數(shù)據(jù)可讀粟誓。
于是,在epoll_wait返回socket_fd可讀的時候起意,我們需要小心處理鹰服,要不然會造成死鎖和socket餓死現(xiàn)象。典型如listen_fd返回可讀的時候揽咕,我們需要不斷的accept直到EAGAIN悲酷。假設(shè)同時有三個請求到達,epoll_wait返回listen_fd可讀亲善,這個時候设易,如果僅僅accept一次拿走一個請求去處理,那么就會留下兩個請求蛹头,如果這個時候一直沒有新的請求到達顿肺,那么再次調(diào)用epoll_wait是不會通知listen_fd可讀的戏溺,于是epoll_wait只能睡眠到超時才返回,遺留下來的兩個請求一直得不到處理屠尊,處于餓死狀態(tài)旷祸。
5.3.4 ET vs LT - 總結(jié)
最后總結(jié)一下,ET和LT模式下epoll_wait返回的條件
ET - 對于讀操作
[1] 當接收緩沖buffer內(nèi)待讀數(shù)據(jù)增加的時候時候(由空變?yōu)椴豢盏臅r候讼昆、或者有新的數(shù)據(jù)進入緩沖buffer)
[2] 調(diào)用epoll_ctl(EPOLL_CTL_MOD)來改變socket_fd的監(jiān)控事件托享,也就是重新mod socket_fd的EPOLLIN事件,并且接收緩沖buffer內(nèi)還有數(shù)據(jù)沒讀取浸赫。(這里不能是EPOLL_CTL_ADD的原因是闰围,epoll不允許重復(fù)ADD的,除非先DEL了掺炭,再ADD)
因為epoll_ctl(ADD或MOD)會調(diào)用sk的poll邏輯來檢查是否有關(guān)心的事件辫诅,如果有,就會將該sk加入到epoll的ready_list中涧狮,下次調(diào)用epoll_wait的時候炕矮,就會遍歷到該sk,然后會重新收集到關(guān)心的事件返回者冤。
ET - 對于寫操作
[1] 發(fā)送緩沖buffer內(nèi)待發(fā)送的數(shù)據(jù)減少的時候(由滿狀態(tài)變?yōu)椴粷M狀態(tài)的時候肤视、或者有部分數(shù)據(jù)被發(fā)出去的時候)
[2] 調(diào)用epoll_ctl(EPOLL_CTL_MOD)來改變socket_fd的監(jiān)控事件,也就是重新mod socket_fd的EPOLLOUT事件涉枫,并且發(fā)送緩沖buffer還沒滿的時候邢滑。
LT - 對于讀操作
LT就簡單多了,唯一的條件就是愿汰,接收緩沖buffer內(nèi)有可讀數(shù)據(jù)的時候
LT - 對于寫操作
LT就簡單多了困后,唯一的條件就是,發(fā)送緩沖buffer還沒滿的時候
在絕大多少情況下衬廷,ET模式并不會比LT模式更為高效摇予,同時,ET模式帶來了不好理解的語意吗跋,這樣容易造成編程上面的復(fù)雜邏輯和坑點侧戴。因此,建議還是采用LT模式來編程更為舒爽跌宛。
參考資料
http://blog.chinaunix.net/uid-28541347-id-4238524.html http://blog.csdn.net/historyasamirror/article/details/5778378 http://blog.csdn.net/dog250/article/details/50528373 http://blog.csdn.net/zhangskd/article/details/16986931