作者:CppExplore 網址:http://www.shnenglu.com/CppExplore/
多路復用的方式是真正實用的服務器程序,非多路復用的網絡程序只能作為學習或著陪測的角色。本文說下個人接觸過的多路復用函數:select/poll/epoll/port。kqueue的*nix系統沒接觸過,估計熟悉了上面四種,kqueue也只是需要熟悉一下而已。
一、select模型
select原型:

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




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






















if(--res<=0) continue








if(--res<=0) continue


二、poll模型
poll原型:











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
















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



















與以上模型的優點:
(1)它保留了poll的兩個相對與select的優點
(2)epoll_wait的參數events作為出參,直接返回了有事件發生的fd,epoll_wait的返回值既是發生事件的個數,省略了poll中返回之后的循環操作。
(3)不再象select、poll一樣將標識符局限于fd,epoll中可以將標識符擴大為指針,大大增加了epoll模型下的靈活性。
epoll的服務器模型偽碼:










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










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