優點
支持一個進程打開大數目的socket描述符
select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設定,默認值是2048。對於那些需要支持的上萬連線數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯伺服器代碼,不過資料也同時指出這樣會帶來網路效率的下降,二是可以選擇多進程的解決方案(傳統的Apache方案),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上執行緒間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大可以打開檔案的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max查看,一般來說這個數目和系統記憶體關係很大。
IO效率不隨FD數目增加而線性下降
傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由於網路延時,任一時間只有部分的socket是“活躍”的,但是select/poll每次調用都會線性掃描全部的集合,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對“活躍”的socket進行操作---這是因為在核心實現中epoll是根據每個fd上面的callback函式實現的。那么,只有“活躍”的socket才會主動的去調用 callback函式,其他idle狀態socket則不會,在這點上,epoll實現了一個“偽”AIO,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
使用mmap加速核心與用戶空間的訊息傳遞
這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要核心把FD訊息通知給用戶空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心與用戶空間mmap同一塊記憶體實現的。而如果你像我一樣從2.5核心就關注epoll的話,一定不會忘記手工 mmap這一步的。
核心微調
這一點其實不算epoll的優點了,而是整個linux平台的優點。也許你可以懷疑linux平台,但是你無法迴避linux平台賦予你微調核心的能力。比如,核心TCP/IP協定棧使用記憶體池管理sk_buff結構,那么可以在運行時期動態調整這個記憶體pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函式的第2個參數(TCP完成3次握手的數據包佇列長度),也可以根據你平台記憶體大小動態調整。更甚至在一個數據包個數目巨大但同時每個數據包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。
使用
令人高興的是,2.6核心的epoll比其2.5開發版本的/dev/epoll簡潔了許多,所以,大部分情況下,強大的東西往往是簡單的。唯一有點麻煩是epoll有2種工作方式:LT和ET。
LT(level triggered)是預設的工作方式,並且同時支持block和no-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
ET (edge-triggered)是高速工作方式,只支持non-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,直到你做了某些操作導致那個檔案描述符不再為就緒狀態了(比如,你在傳送,接收或者接收請求,或者傳送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),核心不會傳送更多的通知(only once),不過在TCP協定中,ET模式的加速效用仍需要更多的benchmark確認。
ET和LT的區別就在這裡體現,LT事件不會丟棄,而是只要讀buffer裡面有數據可以讓用戶讀,則不斷的通知你。而ET則只在事件發生之時通知。可以簡單理解為LT是水平觸發,而ET則為邊緣觸發。LT模式只要有事件未處理就會觸發,而ET則只在高低電平變換時(即狀態從1到0或者0到1)觸發。
系統調用
epoll相關的系統調用有:epoll_create, epoll_ctl和epoll_wait。Linux-2.6.19又引入了可以禁止指定信號的epoll_wait: epoll_pwait。至此epoll家族已全。其中epoll_create用來創建一個epoll檔案描述符,epoll_ctl用來添加/修改/刪除需要偵聽的檔案描述符及其事件,epoll_wait/epoll_pwait接收發生在被偵聽的描述符上的,用戶感興趣的IO事件。epoll檔案描述符用完後,直接用close關閉即可,非常方便。事實上,任何被偵聽的檔案符只要其被關閉,那么它也會自動從被偵聽的檔案描述符集合中刪除,很是智慧型。
每次添加/修改/刪除被偵聽檔案描述符都需要調用epoll_ctl,所以要儘量少地調用epoll_ctl,防止其所引來的開銷抵消其帶來的好處。有的時候,套用中可能存在大量的短連線(比如說Web伺服器),epoll_ctl將被頻繁地調用,可能成為這個系統的瓶頸。
A:IO效率。
在大家苦苦的為線上人數的增長而導致的系統資源吃緊上的問題正在發愁的時候,Linux 2.6核心中提供的System Epoll為我們提供了一套完美的解決方案。傳統的select以及poll的效率會因為線上人數的線形遞增而導致呈二次乃至三次方的下降,這些直接導致了網路伺服器可以支持的人數有了個比較明顯的限制。
自從Linux提供了/dev/epoll的設備以及後來2.6核心中對/dev/epoll設備的訪問的封裝(System Epoll)之後,這種現象得到了大大的緩解,如果說幾個月前,大家還對epoll不熟悉,那么現在來說的話,epoll的套用已經得到了大範圍的普及。
那么究竟如何來使用epoll呢?其實非常簡單。
通過在包含一個頭檔案#include <sys/epoll.h>以及幾個簡單的API將可以大大的提高你的網路伺服器的支持人數。
首先通過epoll_create(int maxfds)來創建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數。這個函式會返回一個新的epoll句柄,之後的所有操作將通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll句柄。
之後在你的網路主循環裡面,每一幀的調用epoll_wait(int epfd, epoll_event *events, int max events, int timeout)來查詢所有的網路接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:
其中kdpfd為用epoll_create創建之後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函式操作成功之後,epoll_events裡面將儲存所有的讀寫事件。maxevents是最大事件數量。最後一個timeout是epoll_wait的逾時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件發生,為任意正整數的時候表示等這么長的時間,如果一直沒有事件,則返回。一般如果網路主循環是單獨的執行緒的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個執行緒的話,則可以用0來保證主循環的效率。
epoll_wait範圍之後應該是一個循環,遍歷所有的事件:
對,epoll的操作就這么簡單,總共不過4個API:epoll_create, epoll_ctl, epoll_wait和close。
如果您對epoll的效率還不太了解,請參考之前關於網路遊戲的網路編程等相關的文章。
【注1-start】(以上內容非本人提交)
範例原始碼目的是為了循環產生ET,不能反映idle connection的狀態,該代碼結構不可套用於實際。
【注1-end】