文章目錄
- PPC
- TPC
- select
- poll
- epoll
Epoll引入
Epoll在linux 2.6核心中引入,替代了以前的select/poll模型,能夠充分支援linux下的大規模並髮網絡程式。
Epoll和其他linux下並髮網絡程式對比PPC
典型的Apache模型,Process Per Connection,為每一個新的串連建立一個進程進行相關的處理。
TPC
Thread Per Connection,為每一個新的串連建立一個線程進行相關的處理。顯然PPC和TPC的開銷是較大的,尤其是在大量串連的情況下。
select
1. 最大並發數限制,因為一個進程所開啟的 FD (檔案描述符)是有限制的,由 FD_SETSIZE 設定,預設值是 1024/2048 ,因此 Select 模型的最大並發數就被相應限制了。自己改改這個 FD_SETSIZE ?想法雖好,可是先看看下面吧 …
2. 效率問題, select 每次調用都會線性掃描全部的 FD 集合,這樣效率就會呈現線性下降,把 FD_SETSIZE 改大的後果就是,大家都慢慢來,什嗎?都逾時了??!!
3. 核心 / 使用者空間 記憶體拷貝問題,如何讓核心把 FD 訊息通知給使用者空間呢?在這個問題上 select 採取了記憶體拷貝方法。
poll
基本上效率和 select <1> 支援一個進程開啟大數目的socket描述符(FD)
epoll
1. select 最不能忍受的是一個進程所開啟的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對於那些需要支援上萬串連數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯核心,不過資料也同時指出這樣會帶來網路效率的下降;二是可以選擇多進程的解決方案(傳統的Apache方案),不過雖然linux上面建立進程的代價比較小,但仍舊是不可忽視的,加上進程間資料同步遠比不上線程間同步高效,所以這也不是一種完美的方案。不過epoll
沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於select 所支援的2048。舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。
2. 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之上了。
3.使用mmap加速核心與使用者空間的訊息傳遞
這點實際上涉及到epoll的具體實現。無論是select,poll還是epoll都需要核心把FD訊息通知給使用者空間,如何避免不必要的記憶體拷貝就顯得很重要,在這點上,epoll是通過核心於使用者空間mmap同一塊記憶體實現的。而如果你像我一樣從2.5核心就開始關注epoll的話,一定不會忘記手工mmap這一步的。
4. 核心微調
這一點其實不算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網卡驅動架構。是相同的,
select 缺點的 2 和 3 它都沒有改掉。
Epoll詳細介紹資料結構
epoll用到的所有函數都是在標頭檔sys/epoll.h中聲明的,下面簡要說明所用到的資料結構和函數:
所用到的資料結構:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
結構體epoll_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件,而epoll_data 聯合體用來儲存觸發事件的某個檔案描述符相關的資料。例如一個client串連到伺服器,伺服器通過調用accept函數可以得到於這個client對應的socket檔案描述符,可以把這檔案描述符賦給epoll_data的fd欄位,以便後面的讀寫操作在這個檔案描述符上進行。epoll_event 結構體的events欄位是表示感興趣的事件和被觸發的事件,可能的取值為:
EPOLLIN:表示對應的檔案描述符可以讀;
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀;
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被掛斷;
EPOLLET:表示對應的檔案描述符有事件發生;
函數
1.epoll_create函數
函式宣告:int epoll_create(int size)
該函數產生一個epoll專用的檔案描述符,其中的參數是指定產生描述符的最大範圍。
2.epoll_ctl函數
函式宣告:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
該函數用於控制某個檔案描述符上的事件,可以註冊事件,修改事件,刪除事件。
參數:
epfd:由 epoll_create 產生的epoll專用的檔案描述符;
op:要進行的操作,可能的取值EPOLL_CTL_ADD 註冊、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 刪除;
fd:關聯的檔案描述符;
event:指向epoll_event的指標;
如果調用成功則返回0,不成功則返回-1。
3.epoll_wait函數
函式宣告:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
該函數用於輪詢I/O事件的發生。
參數:
epfd:由epoll_create 產生的epoll專用的檔案描述符;
epoll_event:用於回傳代處理事件的數組;
maxevents:每次能處理的事件數目;
timeout:等待I/O事件發生的逾時值;
返回傳生事件數目。
操作流程
1.使用epoll_create()函數建立檔案描述,設定可管理的最大socket描述符數目。
2. 建立與epoll關聯的接收線程,應用程式可以建立多個接收線程來處理epoll上的讀通知事件,線程的數量依賴於程式的具體需要。
3.建立一個偵聽socket的描述符ListenSock,並將該描述符設定為非阻塞模式,調用Listen()函數在該通訊端上偵聽有無新的串連請求,在epoll_event結構中設定要處理的事件類型EPOLLIN,同時使用epoll_ctl()來註冊事件,最後啟動網路監視線程。
4. 網路監視線程啟動迴圈,epoll_wait()等待epoll事件發生。
5. 如果epoll事件表明有新的串連請求,則調用accept()函數,將使用者socket描述符添加到epoll_data聯合體,同時設定該描述符為非阻塞,並在epoll_event結構中設定要處理的事件類型為讀和寫。
6. 如果epoll事件表明socket描述符上有資料可讀,則將該socket描述符加入可讀隊列,通知接收線程讀入資料,並將接收到的資料放入到接收資料的鏈表中,經邏輯處理後,將反饋的資料包放入到發送資料鏈表中,等待由發送線程發送。
使用例子(轉載)
// // a simple echo server using epoll in linux// // 2009-11-05// by sparkling// #include <sys/socket.h>#include <sys/epoll.h>#include <netinet/in.h>#include <arpa/inet.h>#include <fcntl.h>#include <unistd.h>#include <stdio.h>#include <errno.h>#include <iostream>using namespace std;#define MAX_EVENTS 500struct myevent_s{ int fd; void (*call_back)(int fd, int events, void *arg); int events; void *arg; int status; // 1: in epoll wait list, 0 not in char buff[128]; // recv data buffer int len; long last_active; // last active time};// set eventvoid EventSet(myevent_s *ev, int fd, void (*call_back)(int, int, void*), void *arg){ ev->fd = fd; ev->call_back = call_back; ev->events = 0; ev->arg = arg; ev->status = 0; ev->last_active = time(NULL);}// add/mod an event to epollvoid EventAdd(int epollFd, int events, myevent_s *ev){ struct epoll_event epv = {0, {0}}; int op; epv.data.ptr = ev; epv.events = ev->events = events; if(ev->status == 1){ op = EPOLL_CTL_MOD; } else{ op = EPOLL_CTL_ADD; ev->status = 1; } if(epoll_ctl(epollFd, op, ev->fd, &epv) < 0) printf("Event Add failed[fd=%d]/n", ev->fd); else printf("Event Add OK[fd=%d]/n", ev->fd);}// delete an event from epollvoid EventDel(int epollFd, myevent_s *ev){ struct epoll_event epv = {0, {0}}; if(ev->status != 1) return; epv.data.ptr = ev; ev->status = 0; epoll_ctl(epollFd, EPOLL_CTL_DEL, ev->fd, &epv);}int g_epollFd;myevent_s g_Events[MAX_EVENTS+1]; // g_Events[MAX_EVENTS] is used by listen fdvoid RecvData(int fd, int events, void *arg);void SendData(int fd, int events, void *arg);// accept new connections from clientsvoid AcceptConn(int fd, int events, void *arg){ struct sockaddr_in sin; socklen_t len = sizeof(struct sockaddr_in); int nfd, i; // accept if((nfd = accept(fd, (struct sockaddr*)&sin, &len)) == -1) { if(errno != EAGAIN && errno != EINTR) { printf("%s: bad accept", __func__); } return; } do { for(i = 0; i < MAX_EVENTS; i++) { if(g_Events[i].status == 0) { break; } } if(i == MAX_EVENTS) { printf("%s:max connection limit[%d].", __func__, MAX_EVENTS); break; } // set nonblocking if(fcntl(nfd, F_SETFL, O_NONBLOCK) < 0) break; // add a read event for receive data EventSet(&g_Events[i], nfd, RecvData, &g_Events[i]); EventAdd(g_epollFd, EPOLLIN|EPOLLET, &g_Events[i]); printf("new conn[%s:%d][time:%d]/n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), g_Events[i].last_active); }while(0);}// receive datavoid RecvData(int fd, int events, void *arg){ struct myevent_s *ev = (struct myevent_s*)arg; int len; // receive data len = recv(fd, ev->buff, sizeof(ev->buff)-1, 0); EventDel(g_epollFd, ev); if(len > 0) { ev->len = len; ev->buff[len] = '/0'; printf("C[%d]:%s/n", fd, ev->buff); // change to send event EventSet(ev, fd, SendData, ev); EventAdd(g_epollFd, EPOLLOUT|EPOLLET, ev); } else if(len == 0) { close(ev->fd); printf("[fd=%d] closed gracefully./n", fd); } else { close(ev->fd); printf("recv[fd=%d] error[%d]:%s/n", fd, errno, strerror(errno)); }}// send datavoid SendData(int fd, int events, void *arg){ struct myevent_s *ev = (struct myevent_s*)arg; int len; // send data len = send(fd, ev->buff, ev->len, 0); ev->len = 0; EventDel(g_epollFd, ev); if(len > 0) { // change to receive event EventSet(ev, fd, RecvData, ev); EventAdd(g_epollFd, EPOLLIN|EPOLLET, ev); } else { close(ev->fd); printf("recv[fd=%d] error[%d]/n", fd, errno); }}void InitListenSocket(int epollFd, short port){ int listenFd = socket(AF_INET, SOCK_STREAM, 0); fcntl(listenFd, F_SETFL, O_NONBLOCK); // set non-blocking printf("server listen fd=%d/n", listenFd); EventSet(&g_Events[MAX_EVENTS], listenFd, AcceptConn, &g_Events[MAX_EVENTS]); // add listen socket EventAdd(epollFd, EPOLLIN|EPOLLET, &g_Events[MAX_EVENTS]); // bind & listen sockaddr_in sin; bzero(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(port); bind(listenFd, (const sockaddr*)&sin, sizeof(sin)); listen(listenFd, 5);}int main(int argc, char **argv){ short port = 12345; // default port if(argc == 2){ port = atoi(argv[1]); } // create epoll g_epollFd = epoll_create(MAX_EVENTS); if(g_epollFd <= 0) printf("create epoll failed.%d/n", g_epollFd); // create & bind listen socket, and add to epoll, set non-blocking InitListenSocket(g_epollFd, port); // event loop struct epoll_event events[MAX_EVENTS]; printf("server running:port[%d]/n", port); int checkPos = 0; while(1){ // a simple timeout check here, every time 100, better to use a mini-heap, and add timer event long now = time(NULL); for(int i = 0; i < 100; i++, checkPos++) // doesn't check listen fd { if(checkPos == MAX_EVENTS) checkPos = 0; // recycle if(g_Events[checkPos].status != 1) continue; long duration = now - g_Events[checkPos].last_active; if(duration >= 60) // 60s timeout { close(g_Events[checkPos].fd); printf("[fd=%d] timeout[%d--%d]./n", g_Events[checkPos].fd, g_Events[checkPos].last_active, now); EventDel(g_epollFd, &g_Events[checkPos]); } } // wait for events to happen int fds = epoll_wait(g_epollFd, events, MAX_EVENTS, 1000); if(fds < 0){ printf("epoll_wait error, exit/n"); break; } for(int i = 0; i < fds; i++){ myevent_s *ev = (struct myevent_s*)events[i].data.ptr; if((events[i].events&EPOLLIN)&&(ev->events&EPOLLIN)) // read event { ev->call_back(ev->fd, events[i].events, ev->arg); } if((events[i].events&EPOLLOUT)&&(ev->events&EPOLLOUT)) // write event { ev->call_back(ev->fd, events[i].events, ev->arg); } } } // free resource return 0;}