百科名片
epoll是Linux核心為處理大批量句柄而作了改進的poll,是Linux下多路複用IO接口select/poll的增強版本,它能顯著減少程式在大量并發連接配接中隻有少量活躍的情況下的系統CPU使用率。
編輯本段簡介

使用epoll進行高性能網絡程式設計
epoll是Linux下多路複用IO接口select/poll的增強版本,它能顯著提高程式在大量并發連接配接中隻有少量活躍的情況下的系統CPU使用率,因為它會複用檔案描述符集合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新準備要被偵聽的檔案描述符集合,另一點原因就是擷取事件的時候,它無須周遊整個被偵聽的描述符集,隻要周遊那些被核心IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select/poll那種IO事件的電平觸發(Level Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得使用者空間程式有可能緩存IO狀态,減少epoll_wait/epoll_pwait的調用,提高應用程式效率。
編輯本段優點
支援一個程序打開大數目的socket描述符
select 最不能忍受的是一個程序所打開的FD是有一定限制的,由FD_SETSIZE設定,預設值是1024。對于那些需要支援的上萬連接配接數目的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)是高速工作方式,隻支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,并且不會再為那個檔案描述符發送更多的就緒通知,直到你做了某些操作導緻那個檔案描述符不再為就緒狀态了(比如,你在發送,接收或者接收請求,或者發送接收的資料少于一定量時導緻了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(進而導緻它再次變成未就緒),核心不會發送更多的通知(only once),不過在TCP協定中,ET模式的加速效用仍需要更多的benchmark确認。
ET和LT的差別就在這裡展現,LT事件不會丢棄,而是隻要讀buffer裡面有資料可以讓使用者讀,則不斷的通知你。而ET則隻在事件發生之時通知。可以簡單了解為LT是水準觸發,而ET則為邊緣觸發。LT模式隻要有事件未處理就會觸發,而ET則隻在高低電平變換時(即狀态從1到0或者0到1)觸發。[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)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的文法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create建立之後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裡面将儲存所有的讀寫事件。max_events是目前需要監聽的所有socket句柄數。最後一個timeout是epoll_wait的逾時,為0的時候表示馬上傳回,為-1的時候表示一直等下去,直到有事件範圍,為任意正整數的時候表示等這麼長的時間,如果一直沒有事件,則傳回。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。
epoll_wait範圍之後應該是一個循環,周遊所有的事件:
for(n = 0; n < nfds; ++n) {
if(events[n].data.fd == listener) { //如果是主socket的事件的話,則表示有新連接配接進入了,進行新連接配接的處理。
client = accept(listener, (struct sockaddr *) &local,
&addrlen);
if(client < 0){
perror("accept");
continue;
}
setnonblocking(client); // 将新連接配接置于非阻塞模式
ev.events = EPOLLIN | EPOLLET; // 并且将新連接配接也加入EPOLL的監聽隊列。
注意,這裡的參數EPOLLIN | EPOLLET并沒有設定對寫socket的監聽,如果有寫操作的話,這個時候epoll是不會傳回事件的,如果要對寫操作也監聽的話,應該是EPOLLIN | EPOLLOUT | EPOLLET
ev.data.fd = client;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
// 設定好event之後,将這個新的event通過epoll_ctl加入到epoll的監聽隊列裡面,這裡用EPOLL_CTL_ADD來加一個新的epoll事件,通過EPOLL_CTL_DEL來減少一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。
fprintf(stderr, "epoll set insertion error: fd=%d0,
client);
return -1;
else if(event[n].events & EPOLLIN)//如果是已經連接配接的使用者,并且收到資料,那麼進行讀入
{
int sockfd_r;
if((sockfd_r = event[n].data.fd) < 0)
read(sockfd_r, buffer, MAXSIZE);
//修改sockfd_r上要處理的事件為EPOLLOUT
ev.data.fd = sockfd_r;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
else if(event[n].events & EPOLLOUT)//如果有資料發送
int sockfd_w = events[n].data.fd;
write(sockfd_w, buffer, sizeof(buffer));
//修改sockfd_w上要處理的事件為EPOLLIN
ev.data.fd = sockfd_w;
ev.events = EPOLLIN | EPOLLET;
do_use_fd(events[n].data.fd);
對,epoll的操作就這麼簡單,總共不過4個API:epoll_create, epoll_ctl, epoll_wait和close。