作者:CppExplore 網(wǎng)址:http://m.shnenglu.com/CppExplore/
多路復(fù)用的方式是真正實(shí)用的服務(wù)器程序,非多路復(fù)用的網(wǎng)絡(luò)程序只能作為學(xué)習(xí)或著陪測(cè)的角色。本文說(shuō)下個(gè)人接觸過(guò)的多路復(fù)用函數(shù):select/poll/epoll/port。kqueue的*nix系統(tǒng)沒(méi)接觸過(guò),估計(jì)熟悉了上面四種,kqueue也只是需要熟悉一下而已。
一、select模型
select原型:

其中參數(shù)n表示監(jiān)控的所有fd中最大值+1。
和select模型緊密結(jié)合的四個(gè)宏,含義不解釋了:




理解select模型的關(guān)鍵在于理解fd_set,為說(shuō)明方便,取fd_set長(zhǎng)度為1字節(jié),fd_set中的每一bit可以對(duì)應(yīng)一個(gè)文件描述符fd。則1字節(jié)長(zhǎng)的fd_set最大可以對(duì)應(yīng)8個(gè)fd。
(1)執(zhí)行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。
(2)若fd=5,執(zhí)行FD_SET(fd,&set);后set變?yōu)?001,0000(第5位置為1)
(3)若再加入fd=2,fd=1,則set變?yōu)?001,0011
(4)執(zhí)行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發(fā)生可讀事件,則select返回,此時(shí)set變?yōu)?000,0011。注意:沒(méi)有事件發(fā)生的fd=5被清空。
基于上面的討論,可以輕松得出select模型的特點(diǎn):
(1)可監(jiān)控的文件描述符個(gè)數(shù)取決與sizeof(fd_set)的值。我這邊服務(wù)器上sizeof(fd_set)=512,每bit表示一個(gè)文件描述符,則我服務(wù)器上支持的最大文件描述符是512*8=4096。據(jù)說(shuō)可調(diào),另有說(shuō)雖然可調(diào),但調(diào)整上限受于編譯內(nèi)核時(shí)的變量值。本人對(duì)調(diào)整fd_set的大小不太感興趣,參考http://m.shnenglu.com/CppExplore/archive/2008/03/21/45061.html中的模型2(1)可以有效突破select可監(jiān)控的文件描述符上限。
(2)將fd加入select監(jiān)控集的同時(shí),還要再使用一個(gè)數(shù)據(jù)結(jié)構(gòu)array保存放到select監(jiān)控集中的fd,一是用于再select返回后,array作為源數(shù)據(jù)和fd_set進(jìn)行FD_ISSET判斷。二是select返回后會(huì)把以前加入的但并無(wú)事件發(fā)生的fd清空,則每次開(kāi)始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時(shí)取得fd最大值maxfd,用于select的第一個(gè)參數(shù)。
(3)可見(jiàn)select模型必須在select前循環(huán)array(加fd,取maxfd),select返回后循環(huán)array(FD_ISSET判斷是否有時(shí)間發(fā)生)。
下面給一個(gè)偽碼說(shuō)明基本select模型的服務(wù)器模型:






















if(--res<=0) continue








if(--res<=0) continue


二、poll模型
poll原型:











和select相比,兩大改進(jìn):
(1)不再有fd個(gè)數(shù)的上限限制,可以將參數(shù)ufds想象成棧低指針,nfds是棧中元素個(gè)數(shù),該棧可以無(wú)限制增長(zhǎng)
(2)引入pollfd結(jié)構(gòu),將fd信息、需要監(jiān)控的事件、返回的事件分開(kāi)保存,則poll返回后不會(huì)丟失fd信息和需要監(jiān)控的事件信息,也就省略了select模型中前面的循環(huán)操作,返回后的循環(huán)仍然不可避免。另每次poll阻塞操作都會(huì)自動(dòng)把上次的revents清空。
poll的服務(wù)器模型偽碼:
















三、epoll模型
epoll阻塞操作的原型:



















與以上模型的優(yōu)點(diǎn):
(1)它保留了poll的兩個(gè)相對(duì)與select的優(yōu)點(diǎn)
(2)epoll_wait的參數(shù)events作為出參,直接返回了有事件發(fā)生的fd,epoll_wait的返回值既是發(fā)生事件的個(gè)數(shù),省略了poll中返回之后的循環(huán)操作。
(3)不再象select、poll一樣將標(biāo)識(shí)符局限于fd,epoll中可以將標(biāo)識(shí)符擴(kuò)大為指針,大大增加了epoll模型下的靈活性。
epoll的服務(wù)器模型偽碼:










epoll使用中的問(wèn)題:
(1)epoll_ctl的EPOLL_CTL_DEL操作中,最后一個(gè)參數(shù)是無(wú)意義的,但是在小版本號(hào)過(guò)低的2.6內(nèi)核下要求最后一個(gè)參數(shù)一定非NULL,否則返回失敗,并且返回的errno在man epoll_ctl中不存在,因此安全期間,保證epoll_ctl的最后一個(gè)參數(shù)總非NULLL。
(2)如果一個(gè)fd(比如管道)的事件導(dǎo)致了另一個(gè)fd2的刪除,則必須掃描返回結(jié)果集中是否有fd2,有則在結(jié)果集中刪除,避免沖突。
(3)有文章說(shuō)epoll在G網(wǎng)環(huán)境下性能會(huì)低于poll/select,看有些測(cè)試,給出的拐點(diǎn)在2w/s并發(fā)之后,我本人的工作范圍不可能達(dá)到這么高的并發(fā),個(gè)人在測(cè)試性能的時(shí)候最大也是取的1w/s的并發(fā),一個(gè)是因?yàn)橄到y(tǒng)單進(jìn)程允許打開(kāi)的文件描述符最大值,4w的數(shù)字太高了,另一個(gè)就是我這邊服務(wù)器的性能達(dá)不到那么高的性能,極限1.7w/s的響應(yīng),那測(cè)試的數(shù)據(jù)竟然在2w并發(fā)的時(shí)候還有2w的響應(yīng),不知道是什么硬件配置。或許等有了G網(wǎng)的環(huán)境,會(huì)關(guān)注epoll高并發(fā)下的性能下降
。
(4)epoll的LT和ET性能的差異,我測(cè)試的數(shù)據(jù)表明兩者性能相當(dāng),“使用epoll就是為了高性能,就是要使用ET模式”這個(gè)說(shuō)法是站不住腳的。個(gè)人傾向于使用LT模式,編程簡(jiǎn)單、安全。
四、port模型
port則和epoll非常接近,不需要前后的兩次掃描,直接返回有事件的結(jié)果,可以象epoll一樣綁定指針,不同點(diǎn)是
(1)epoll可以返回多個(gè)事件,而port一次只返回一個(gè)(port_getn可以返回多個(gè),但是在不到指定的n值時(shí),等待直到達(dá)到n個(gè))
(2)port返回的結(jié)果會(huì)自動(dòng)port_dissociate,如果要再次監(jiān)控,需要重新port_associate
這個(gè)就不多說(shuō)了。
可以看出select-->poll-->epoll/port的演化路線:
(1)從readset、writeset等分離到 將讀寫(xiě)事件集中到統(tǒng)一的結(jié)構(gòu)
(2)從阻塞操作前后的兩次循環(huán) 到 之后的一次循環(huán) 到精確返回有事件發(fā)生的fd
(3)從只能綁定fd信息,到可以綁定指針結(jié)構(gòu)信息
五、抽象接口
綜合以上多路復(fù)用函數(shù)的特點(diǎn),可以進(jìn)行統(tǒng)一的封裝,這里給出我封裝的接口,也算是給一個(gè)思路:










使用的時(shí)候就是先init,再wait,再循環(huán)執(zhí)行next_result直到空,每個(gè)result,使用get_data和get_event挨個(gè)處理,如果某個(gè)fd引起另一個(gè)fd關(guān)閉,調(diào)delete_from_results(除epoll,其它都直接return),處理完reset_data(select和port用,poll/epoll直接return)。