Epoll簡介以及例子
第一部分:Epoll簡介
問題 : Select,Poll和Epoll的區別
答案 :
Epoll和Select的區別
1. 遍歷方式的區別。select判斷是否有事件發生是遍歷的,而epoll是事件響應的,一旦控制代碼上有事件來了,就馬上選出來。
2. 數目的區別。select一般由一個核心引數(1024)限制了監聽的控制代碼數,但是epoll通常受限於開啟檔案的數目,通常會打得多。
3. epoll自身,還有兩種觸發方式。水平觸發和邊緣觸發。邊沿觸發的效率更高(高了不少,但是程式設計的時候要小心處理每個時間,防止漏掉處理某些事件)。
Select
select()系統呼叫提供一個機制來實現同步多元I/O:
#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set); |
呼叫select()將阻塞,直到指定的檔案描述符準備好執行I/O,或者可選引數timeout指定的時間已經過去。
select()成功返回時,每組set都被修改以使它只包含準備好I/O的檔案描述符。例如,假設有兩個檔案描述符,值分別是7和9,被放在readfds中。當select()返回時,如果7仍然在set中,則這個檔案描述符已經準備好被讀取而不會阻塞。如果9已經不在set中,則讀取它將可能會阻塞(我說可能是因為資料可能正好在select返回後就可用,這種情況下,下一次呼叫select()將返回檔案描述符準備好讀取)。
第一個引數n,等於所有set中最大的那個檔案描述符的值加1。 當select()返回時,timeout引數的狀態在不同的系統中是未定義的,因此每次呼叫select()之前必須重新初始化timeoutPoll
和select()不一樣,poll()沒有使用低效的三個基於位的檔案描述符set,而是採用了一個單獨的結構體pollfd陣列,由fds指標指向這個組。pollfd結構體定義如下:
#include <sys/poll.h> int poll (struct pollfd *fds, unsigned int nfds, int timeout); struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ }; |
每一個pollfd結構體指定了一個被監視的檔案描述符,可以傳遞多個結構體,指示poll()監視多個檔案描述符。每個結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。revents域是檔案描述符的操作結果事件掩碼。核心在呼叫返回時設定這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN:有資料可讀。POLLRDNORM:有普通資料可讀。POLLRDBAND:有優先資料可讀。POLLPRI:有緊迫資料可讀。POLLOUT:寫資料不會導致阻塞。POLLWRNORM:寫普通資料不會導致阻塞。POLLWRBAND:寫優先資料不會導致阻塞。POLLMSG:SIGPOLL訊息可用。此外,revents域中還可能返回下列事件:POLLER:指定的檔案描述符發生錯誤。POLLHUP:指定的檔案描述符掛起事件。POLLNVAL:指定的檔案描述符非法。這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。POLLIN | POLLPRI等價於select()的讀事件,POLLOUT | POLLWRBAND等價於select()的寫事件。POLLIN等價於POLLRDNORM | POLLRDBAND,而POLLOUT則等價於POLLWRNORM。例如,要同時監視一個檔案描述符是否可讀和可寫,我們可以設定events為POLLIN | POLLOUT。在poll返回時,我們可以檢查revents中的標誌,對應於檔案描述符請求的events結構體。如果POLLIN事件被設定,則檔案描述符可以被讀取而不阻塞。如果POLLOUT被設定,則檔案描述符可以寫入而不導致阻塞。這些標誌並不是互斥的:它們可能被同時設定,表示這個檔案描述符的讀取和寫入操作都會正常返回而不阻塞。timeout引數指定等待的毫秒數,無論I/O是否準備好,poll都會返回。timeout指定為負數值表示無限超時;timeout為0指示poll呼叫立即返回並列出準備好I/O的檔案描述符,但並不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。返回值和錯誤程式碼成功時,poll()返回結構體中revents域不為0的檔案描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,並設定errno為下列值之一:EBADF:一個或多個結構體中指定的檔案描述符無效。EFAULT:fds指標指向的地址超出程序的地址空間。EINTR:請求的事件之前產生一個訊號,呼叫可以重新發起。EINVAL:nfds引數超出PLIMIT_NOFILE值。ENOMEM:可用記憶體不足,無法完成請求。
Epoll
Epoll的優點:1.支援一個程序開啟大數目的socket描述符(FD) select 最不能忍受的是一個程序所開啟的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對於那些需要支援的上萬連線數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個巨集然後重新編譯核心,不過資料也同時指出這樣會帶來網路效率的下降,二是可以選擇多程序的解決方案(傳統的 Apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上執行緒間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於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這一步的。
Epoll簡介:
在linux的網路程式設計中,很長的時間都在使用select來做事件觸發。在linux新的核心中,有了一種替換它的機制,就是epoll。相比於select,epoll最大的好處在於它不會隨著監聽fd數目的增長而降低效率。因為在核心中的select實現中,它是採用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。epoll的介面非常簡單,一共就三個函式:1. int epoll_create(int size);建立一個epoll的控制代碼,size用來告訴核心這個監聽的數目一共有多大。這個引數不同於select()中的第一個引數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll控制代碼後,它就是會佔用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件註冊函式,它不同與select()是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。第一個引數是epoll_create()[上面一個函式]的返回值,第二個引數表示動作,用三個巨集來表示:EPOLL_CTL_ADD:註冊新的fd到epfd中;EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;EPOLL_CTL_DEL:從epfd中刪除一個fd;第三個引數是需要監聽的fd,第四個引數是告訴核心需要監聽什麼事,struct epoll_event結構如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下幾個巨集的集合:EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);EPOLLOUT:表示對應的檔案描述符可以寫;EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);EPOLLERR:表示對應的檔案描述符發生錯誤;EPOLLHUP:表示對應的檔案描述符被結束通話;EPOLLET:將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的產生,類似於select()呼叫。引數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,引數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函式返回需要處理的事件數目,如返回0表示已超時。令人高興的是,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確認。
第二部分: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專用的檔案描述符,其中的引數是指定生成描述符的最大範圍(我覺得這個引數和select函式的第一個引數應該是類似的但是該怎麼設定才好,我也不太清楚)。 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
#include <iostream> #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> #define MAXLINE 10 #define OPEN_MAX 100 #define LISTENQ 20 #define SERV_PORT 5555 #define INFTIM 1000 void setnonblocking(int sock) { int opts; opts = fcntl(sock, F_GETFL); if(opts < 0) { perror("fcntl(sock,GETFL)"); exit(1); } opts = opts | O_NONBLOCK; if(fcntl(sock, F_SETFL, opts) < 0) { perror("fcntl(sock,SETFL,opts)"); exit(1); } } int main() { int i, maxi, listenfd, connfd, sockfd, epfd, nfds; ssize_t n; char line[MAXLINE]; socklen_t clilen; //宣告epoll_event結構體的變數,ev用於註冊事件,陣列用於回傳要處理的事件 struct epoll_event ev, events[20]; //生成用於處理accept的epoll專用的檔案描述符 epfd = epoll_create(256); struct sockaddr_in clientaddr; struct sockaddr_in serveraddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); //把socket設定為非阻塞方式 setnonblocking(listenfd); //設定與要處理的事件相關的檔案描述符 ev.data.fd = listenfd; //設定要處理的事件型別 ev.events = EPOLLIN | EPOLLET; //註冊epoll事件 epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; char *local_addr = "200.200.200.204"; inet_aton(local_addr, &(serveraddr.sin_addr)); //htons(SERV_PORT); serveraddr.sin_port = htons(SERV_PORT); bind(listenfd, (sockaddr *)&serveraddr, sizeof(serveraddr)); listen(listenfd, LISTENQ); maxi = 0; for ( ; ; ) { //等待epoll事件的發生 nfds = epoll_wait(epfd, events, 20, 500); //處理所發生的所有事件 for(i = 0; i < nfds; ++i) { if(events[i].data.fd == listenfd) { connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen); if(connfd < 0) { perror("connfd<0"); exit(1); } setnonblocking(connfd); char *str = inet_ntoa(clientaddr.sin_addr); std::cout << "connect from " < _u115 ? tr << std::endl; //設定用於讀操作的檔案描述符 ev.data.fd = connfd; //設定用於注測的讀操作事件 ev.events = EPOLLIN | EPOLLET; //註冊ev epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); } else if(events[i].events & EPOLLIN) { if ( (sockfd = events[i].data.fd) < 0) continue; if ( (n = read(sockfd, line, MAXLINE)) < 0) { if (errno == ECONNRESET) { close(sockfd); events[i].data.fd = -1; } else std::cout << "readline error" << std::endl; } else if (n == 0) { close(sockfd); events[i].data.fd = -1; } //設定用於寫操作的檔案描述符 ev.data.fd = sockfd; //設定用於注測的寫操作事件 ev.events = EPOLLOUT | EPOLLET; //修改sockfd上要處理的事件為EPOLLOUT epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); } else if(events[i].events & EPOLLOUT) { sockfd = events[i].data.fd; write(sockfd, line, n); //設定用於讀操作的檔案描述符 ev.data.fd = sockfd; //設定用於注測的讀操作事件 ev.events = EPOLLIN | EPOLLET; //修改sockfd上要處理的事件為EPOLIN epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); } } } } |
例子2
/* *\ 伺服器端的原始碼 */ #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <fcntl.h> #include <iostream> #include <signal.h> #include <sys/epoll.h> #define MAXFDS 256 #define EVENTS 100 #define PORT 8888 int epfd; bool setNonBlock(int fd) { int flags = fcntl(fd, F_GETFL, 0); flags |= O_NONBLOCK; if(-1 == fcntl(fd, F_SETFL, flags)) return false; return true; } int main(int argc, char *argv[], char *evp[]) { int fd, nfds, confd; int on = 1; char *buffer[512]; struct sockaddr_in saddr, caddr; struct epoll_event ev, events[EVENTS]; if(-1 == socket(AF_INET, SOCKSTREAM), 0) { std::cout << "建立套接字出錯啦" << std::endl; return -1; } struct sigaction sig; sigemptyset(&sig.sa_mask); sig_handler = SIG_IGN; sigaction(SIGPIPE, &N > sig, NULL); epfd = epoll_create(MAXFDS); setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); memset(&saddr, 0, sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_port = htons((short)(PORT)); saddr.sin_addr.s_addr = INADDR_ANY; if(-1 == bind(fd, (struct sockaddr *)&saddr, sizeof(saddr))) { std::cout << "套接字不能繫結到伺服器上" << std::endl; return -1; } if(-1 == listen(fd, 32)) { std::cout << "監聽套接字的時候出錯了" << std::endl; return -1; } ev.data.fd = fd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); while(true) { nfds = epoll_wait(epfd, &events, MAXFDS, 0); for(int i = 0; i < nfds; ++ i) { if(fd == events[i].data.fd) { memset(&caddr, sizeof(caddr)); cfd = accept(fd, (struct sockaddr *)&caddr, &sizeof(caddr)); if(-1 == cfd) { std::cout << "伺服器接收套接字的時候出問題了" << std::endl; break; } setNonBlock(cfd); ev.data.fd = cfd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); } else if(events[i].data.fd & EPOLLIN) { bzero(&buffer, sizeof(buffer)); std::cout << "伺服器端要讀取客戶端發過來的訊息" << std::endl; ret = recv(events[i].data.fd, buffer, sizeof(buffer), 0); if(ret < 0) { std::cout << "伺服器收到的訊息出錯了" << endl; return -1; } std::cout << "接收到的訊息為:" << (char *) buffer << std::endl; ev.data.fd = events[i].data.fd; ev.events = EPOLLOUT; epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &ev); } else if(events[i].data.fd & EPOLLOUT) { bzero(&buffer, sizeof(buffer)); bcopy("The [email protected]: [email protected]", buffer, sizeof("The [email protected]: [email protected]")); ret = send(events[i].data.fd, buffer, strlen(buffer)); if(ret < 0) { std::cout << "伺服器傳送訊息給客戶端的時候出錯啦" << std::endl; return -1; } ev.data.fd = events[i].data.fd; epoll_ctl(epfd, EPOL |