http://blog.csdn.net/woxiaohahaa/article/details/51498951
文章參考自:http://blog.csdn.net/tennysonsky/article/details/45745887(秋葉原 — Mike VS 麥克《Linux系統編程——I/O多路復用select、poll、epoll的區別使用》)
此外,還有一篇好文:epoll機制:epoll_create、epoll_ctl、epoll_wait、close(魚思故淵的專欄)
在上一篇中,我簡單學習了 IO多路復用的基本概念,這里我將初學其三種實現手段:select,poll,epoll。
I/O 多路復用是為了解決進程或線程阻塞到某個 I/O 系統調用而出現的技術,使進程或線程不阻塞于某個特定的 I/O 系統調用。
select(),poll(),epoll()都是I/O多路復用的機制。I/O多路復用通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒,就是這個文件描述符進行讀寫操作之前),能夠通知程序進行相應的讀寫操作。但select(),poll(),epoll()本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
與多線程(TPC(Thread Per Connection)模型)和多進程(典型的Apache模型(Process Per Connection,簡稱PPC))相比,I/O 多路復用的最大優勢是系統開銷小,系統不需要建立新的進程或者線程,也不必維護這些線程和進程。
select()的使用
所需頭文件:
- #include?<sys/select.h>??
函數原型:
- int?select(int?nfds,?fd_set?*readfds,?fd_set?*writefds,?fd_set?*exceptfds,?struct?timeval?*timeout);??
函數描述:
監視并等待多個文件描述符的屬性變化(可讀、可寫或錯誤異常)。select()函數監視的文件描述符分 3 類,分別是writefds、readfds、和 exceptfds。調用后 select() 函數會阻塞,直到有描述符就緒(有數據可讀、可寫、或者有錯誤異常),或者超時( timeout 指定等待時間),函數才返回。當 select()函數返回后,可以通過遍歷 fdset,來找到就緒的描述符。
參數描述:
nfds:?要監視的文件描述符的范圍,一般取監視的描述符數的最大值+1,如這里寫 10, 這樣的話,描述符 0,1, 2 …… 9 都會被監視,在 Linux 上最大值一般為1024。
readfd:?監視的可讀描述符集合,只要有文件描述符讀操作準備就緒,這個文件描述符就存儲到這。
writefds:?監視的可寫描述符集合。
exceptfds:?監視的錯誤異常描述符集合。
三個參數 readfds、writefds 和 exceptfds 指定我們要讓內核監測讀、寫和異常條件的描述字。如果不需要使用某一個的條件,就可以把它設為NULL 。
幾個較為重要的宏:
- ??
- void?FD_ZERO(fd_set?*fdset);???
- ??
- ??
- void?FD_SET(int?fd,?fd_set?*fdset);??
- ??
- ??
- void?FD_CLR(int?fd,?fd_set?*fdset);??
- ??
- ??
- int?FD_ISSET(int?fd,?fd_set?*fdset);???
- ??
- timeout:?超時時間,它告知內核等待所指定描述字中的任何一個就緒可花多少時間。其?timeval?結構用于指定這段時間的秒數和微秒數。??
- struct?timeval??
- {??
- time_t?tv_sec;?????????
- suseconds_t?tv_usec;???
- };??
三種可能的函數返回情況:
1)永遠等待下去:timeout 設置為空指針 NULL,且沒有一個描述符準備好。
2)等待固定時間:timeout 設置為某個固定時間,在有一個描述符準備好時返回,如果時間到了,就算沒有文件描述符準備就緒,這個函數也會返回 0。
3)不等待(不阻塞):檢查描述字后立即返回,這稱為輪詢。為此,struct timeval變量的時間值指定為 0 秒 0 微秒,文件描述符屬性無變化返回 0,有變化返回準備好的描述符數量。
函數返回值:
成功:就緒描述符的數目(同時修改readfds、writefds 和 exceptfds 三個參數),超時返回 0;
出錯:-1。
下面用 Socket 舉例,兩個客戶端,其中一個每隔 5s 發一個固定的字符串到服務器,另一個采集終端的鍵盤輸入,將其發給服務器,一個服務器,使用 IO 多路復用處理這兩個客戶端。代碼如下:
服務器:
- #include?<cstdio>??
- #include?<sys/select.h>??
- #include?<unistd.h>??
- #include?<stdlib.h>??
- #include?<cstring>??
- #include?<cassert>??
- #include?<sys/types.h>??
- #include?<sys/socket.h>??
- #include?<netinet/in.h>??
- #include?<arpa/inet.h>??
- ??
- const?int?BUFFER_SIZE?=?4096;??
- const?int?SERVER_PORT?=?2222;??
- ??
- inline?int?max(int?a,?int?b){?return?(a?>?b???:?a,?b);}??
- ??
- int?main()??
- {??
- ????int?server_socket;??
- ????char?buff1[BUFFER_SIZE];??
- ????char?buff2[BUFFER_SIZE];??
- ????fd_set?rfds;??
- ????struct?timeval?tv;??
- ????int?ret;??
- ????int?n;??
- ??
- ????server_socket?=?socket(AF_INET,?SOCK_STREAM,?0);??
- ????assert(server_socket?!=?-1);??
- ??
- ????struct?sockaddr_in?server_addr;??
- ????memset(&server_addr,?0,?sizeof(server_addr));??
- ????server_addr.sin_family?=?AF_INET;??
- ????server_addr.sin_port?=?htons(SERVER_PORT);??
- ????server_addr.sin_addr.s_addr?=?htonl(INADDR_ANY);??
- ??
- ????assert(bind(server_socket,?(struct?sockaddr?*)&server_addr,?sizeof(server_addr))?!=?-1);??
- ????assert(listen(server_socket,?5)?!=?-1);??
- ??????
- ????struct?sockaddr_in?client_addr1,?client_addr2;??
- ????socklen_t?client_addr_len?=?sizeof(struct?sockaddr_in);??
- ??????
- ????printf("waiting...\n");??
- ??
- ??????
- ????int?connfd1?=?accept(server_socket,?(struct?sockaddr*)&client_addr1,?&client_addr_len);??
- ????assert(connfd1?!=?-1);??
- ????printf("connect?from?%s:%d\n",?inet_ntoa(client_addr1.sin_addr),?ntohs(client_addr1.sin_port));??
- ????int?connfd2?=?accept(server_socket,?(struct?sockaddr*)&client_addr2,?&client_addr_len);??
- ????assert(connfd2?!=?-1);??
- ????printf("connect?from?%s:%d\n",?inet_ntoa(client_addr2.sin_addr),?ntohs(client_addr2.sin_port));??
- ??
- ????while(1)??
- ????{??
- ????????FD_ZERO(&rfds);??
- ????????FD_SET(connfd1,?&rfds);??
- ????????FD_SET(connfd2,?&rfds);??
- ??
- ????????tv.tv_sec?=?10;??
- ????????tv.tv_usec?=?0;??
- ??????????
- ????????printf("select...\n");??
- ????????ret?=?select(max(connfd1,?connfd2)?+?1,?&rfds,?NULL,?NULL,?NULL);??
- ??????????
- ??????????
- ????????if(ret?==?-1)??
- ????????????perror("select()");??
- ????????else?if(ret?>?0)??
- ????????{??
- ????????????if(FD_ISSET(connfd1,?&rfds))??
- ????????????{?????
- ????????????????n?=?recv(connfd1,?buff1,?BUFFER_SIZE,?0);??
- ????????????????buff1[n]?=?'\0';??????????????????????
- ????????????????printf("connfd1:?%s\n",?buff1);??
- ????????????}??
- ????????????if(FD_ISSET(connfd2,?&rfds))??
- ????????????{??
- ????????????????n?=?recv(connfd2,?buff2,?BUFFER_SIZE,?0);??
- ????????????????buff2[n]?=?'\0';??????????????????????
- ????????????????printf("connfd2:?%s\n",?buff2);??
- ????????????}?????????
- ????????}??
- ????????else??
- ????????????printf("time?out\n");??
- ????}??
- ??
- ????return?0;??
- }??
客戶端1:
- #include?<cstdio>??
- #include?<unistd.h>??
- #include?<stdlib.h>??
- #include?<cstring>??
- #include?<cassert>??
- #include?<sys/types.h>??
- #include?<sys/socket.h>??
- #include?<netinet/in.h>??
- #include?<arpa/inet.h>??
- ??
- const?int?BUFFER_SIZE?=?4096;??
- const?int?SERVER_PORT?=?2222;??
- ??
- int?main()??
- {??
- ????int?client_socket;??
- ????const?char?*server_ip?=?"127.0.0.1";??
- ????char?buffSend[BUFFER_SIZE]?=?"I'm?from?d.cpp";??
- ??
- ????client_socket?=?socket(AF_INET,?SOCK_STREAM,?0);??
- ????assert(client_socket?!=?-1);??
- ??
- ????struct?sockaddr_in?server_addr;??
- ????memset(&server_addr,?0,?sizeof(server_addr));??
- ????server_addr.sin_family?=?AF_INET;??
- ????server_addr.sin_port?=?htons(SERVER_PORT);??
- ????server_addr.sin_addr.s_addr?=?inet_addr(server_ip);??
- ??
- ????assert(connect(client_socket,?(struct?sockaddr?*)&server_addr,?sizeof(server_addr))?!=?-1);??
- ??????
- ????while(1)??
- ????{??
- ????????assert(send(client_socket,?buffSend,?strlen(buffSend),?0)?!=?-1);??
- ????????sleep(5);??
- ????}??
- ????close(client_socket);??
- ??
- ????return?0;??
- }??
客戶端2:
- #include?<cstdio>??
- #include?<unistd.h>??
- #include?<stdlib.h>??
- #include?<cstring>??
- #include?<cassert>??
- #include?<sys/types.h>??
- #include?<sys/socket.h>??
- #include?<netinet/in.h>??
- #include?<arpa/inet.h>??
- ??
- const?int?BUFFER_SIZE?=?4096;??
- const?int?SERVER_PORT?=?2222;??
- ??
- int?main()??
- {??
- ????int?client_socket;??
- ????const?char?*server_ip?=?"127.0.0.1";??
- ????char?buffSend[BUFFER_SIZE];??
- ??
- ????client_socket?=?socket(AF_INET,?SOCK_STREAM,?0);??
- ????assert(client_socket?!=?-1);??
- ??
- ????struct?sockaddr_in?server_addr;??
- ????memset(&server_addr,?0,?sizeof(server_addr));??
- ????server_addr.sin_family?=?AF_INET;??
- ????server_addr.sin_port?=?htons(SERVER_PORT);??
- ????server_addr.sin_addr.s_addr?=?inet_addr(server_ip);??
- ??
- ????assert(connect(client_socket,?(struct?sockaddr?*)&server_addr,?sizeof(server_addr))?!=?-1);??
- ??????
- ????while(1)??
- ????{??
- ????????fgets(buffSend,?BUFFER_SIZE,?stdin);??
- ????????assert(send(client_socket,?buffSend,?strlen(buffSend),?0)?!=?-1);??
- ????}??
- ????close(client_socket);??
- ??
- ????return?0;??
- }??
以上三份代碼有缺陷,代碼沒有很好的結束方式,都是 while(1) 死循環,運行的結束需要用 Ctrl + c ⊙﹏⊙
poll()的使用
select() 和 poll() 系統調用的本質一樣,前者在 BSD UNIX 中引入的,后者在 System V 中引入的。poll() 的機制與 select() 類似,與 select() 在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是 poll() 沒有最大文件描述符數量的限制(但是數量過大后性能也是會下降)。poll() 和 select() 同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制于用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數量的增加而線性增大。
所需頭文件:
函數原型:
- int?poll(struct?pollfd?*fds,?nfds_t?nfds,?int?timeout);??
函數描述:
監視并等待多個文件描述符的屬性變化。
函數參數:
1)fds:與 select() 使用三個 fd_set 的方式不同,poll() 使用一個 pollfd 的指針實現。一個 pollfd 結構體數組,其中包括了你想監視的文件描述符和事件, 事件由結構中事件域 events 來確定,調用后實際發生的事件將被填寫在結構體的 revents 域。
- struct?pollfd{??
- int?fd;???????????
- short?events;?????
- short?revents;????
- };???
_1、fd:每一個 pollfd 結構體指定了一個被監視的文件描述符,可以傳遞多個結構體,指示 poll() 監視多個文件描述符。
_2、events:每個結構體的 events 域是監視該文件描述符的事件掩碼,由用戶來設置這個域。
_3、revents:revents 域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域。events 域中請求的任何事件都可能在 revents 域中返回。
事件的掩碼取值如下:
POLLIN | POLLPRI 等價于 select() 的讀事件,POLLOUT | POLLWRBAND 等價于 select() 的寫事件。POLLIN 等價于 POLLRDNORM | POLLRDBAND,而 POLLOUT 則等價于 POLLWRNORM 。例如,要同時監視一個文件描述符是否可讀和可寫,我們可以設置 events 為 POLLIN | POLLOUT。
每個結構體的 events 域是由用戶來設置,告訴內核我們關注的是什么,而 revents 域是返回時內核設置的,以說明對該描述符發生了什么事件。
2)nfds:用來指定第一個參數數組元素個數。
3)timeout:?指定等待的毫秒數,無論 I/O 是否準備好,poll() 都會返回。

函數返回值:
成功時,poll() 返回結構體中 revents 域不為 0 的文件描述符個數,如果在超時前沒有任何事件發生,poll()返回 0;
失敗時,poll() 返回 -1。
此處我們將上面的例子用 poll() 重新實現如下,只用修改服務器端代碼:
- #include?<cstdio>??
- #include?<poll.h>??
- #include?<unistd.h>??
- #include?<stdlib.h>??
- #include?<cstring>??
- #include?<cassert>??
- #include?<sys/types.h>??
- #include?<sys/socket.h>??
- #include?<netinet/in.h>??
- #include?<arpa/inet.h>??
- ??
- const?int?BUFFER_SIZE?=?4096;??
- const?int?SERVER_PORT?=?2222;??
- ??
- int?main()??
- {??
- ????int?server_socket;??
- ????char?buff1[BUFFER_SIZE];??
- ????char?buff2[BUFFER_SIZE];??
- ????struct?timeval?tv;??
- ????int?ret;??
- ????int?n;??
- ??
- ????server_socket?=?socket(AF_INET,?SOCK_STREAM,?0);??
- ????assert(server_socket?!=?-1);??
- ??
- ????struct?sockaddr_in?server_addr;??
- ????memset(&server_addr,?0,?sizeof(server_addr));??
- ????server_addr.sin_family?=?AF_INET;??
- ????server_addr.sin_port?=?htons(SERVER_PORT);??
- ????server_addr.sin_addr.s_addr?=?htonl(INADDR_ANY);??
- ??
- ????assert(bind(server_socket,?(struct?sockaddr?*)&server_addr,?sizeof(server_addr))?!=?-1);??
- ????assert(listen(server_socket,?5)?!=?-1);??
- ??????
- ????struct?sockaddr_in?client_addr1,?client_addr2;??
- ????socklen_t?client_addr_len?=?sizeof(struct?sockaddr_in);??
- ??????
- ????printf("waiting...\n");??
- ??
- ????int?connfd1?=?accept(server_socket,?(struct?sockaddr*)&client_addr1,?&client_addr_len);??
- ????assert(connfd1?!=?-1);??
- ????printf("connect?from?%s:%d\n",?inet_ntoa(client_addr1.sin_addr),?ntohs(client_addr1.sin_port));??
- ????int?connfd2?=?accept(server_socket,?(struct?sockaddr*)&client_addr2,?&client_addr_len);??
- ????assert(connfd2?!=?-1);??
- ????printf("connect?from?%s:%d\n",?inet_ntoa(client_addr2.sin_addr),?ntohs(client_addr2.sin_port));??
- ??
- ????struct?pollfd?rfds[2];??
- ????rfds[0].fd?=?connfd1;??
- ????rfds[0].events?=?POLLIN;??
- ????rfds[1].fd?=?connfd2;??
- ????rfds[1].events?=?POLLIN;??
- ????tv.tv_sec?=?10;??
- ????tv.tv_usec?=?0;??
- ??????
- ????while(1)??
- ????{??
- ????????printf("poll...\n");??
- ????????ret?=?poll(rfds,?2,?-1);??
- ??????????
- ????????if(ret?==?-1)??
- ????????????perror("poll()");??
- ????????else?if(ret?>?0)??
- ????????{?????
- ????????????if((rfds[0].revents?&?POLLIN)?==?POLLIN)??
- ????????????{?????
- ????????????????n?=?recv(connfd1,?buff1,?BUFFER_SIZE,?0);??
- ????????????????buff1[n]?=?'\0';??
- ????????????????printf("connfd1:?%s\n",?buff1);??
- ????????????}??
- ????????????if((rfds[1].revents?&?POLLIN)?==?POLLIN)??
- ????????????{??
- ????????????????n?=?recv(connfd2,?buff2,?BUFFER_SIZE,?0);??
- ????????????????buff2[n]?=?'\0';??
- ????????????????printf("connfd2:?%s\n",?buff2);??
- ????????????}?????
- ????????}??
- ????????else??
- ????????????printf("time?out\n");??
- ????}??
- ??
- ????return?0;??
- }??
epoll()的使用
epoll 是在內核 2.6 中提出的,是之前的 select() 和 poll() 的增強版本。相對于 select() 和 poll() 來說,epoll 更加靈活,沒有描述符限制。epoll 使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的 copy 只需一次。
epoll 操作過程需要三個接口,分別如下:
- #include?<sys/epoll.h>????
- int?epoll_create(int?size);????
- int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event);????
- int?epoll_wait(int?epfd,?struct?epoll_event?*?events,?int?maxevents,?int?timeout);????
int epoll_create(int size);
功能:
該函數生成一個 epoll 專用的文件描述符。
參數:
size: 用來告訴內核這個監聽的數目一共有多大,參數 size 并不是限制了 epoll 所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。自從 linux 2.6.8 之后,size 參數是被忽略的,也就是說可以填只有大于 0 的任意值。
返回值:
成功:epoll 專用的文件描述符
失敗:-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:
epoll 的事件注冊函數,它不同于 select() 是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。
參數:
epfd:?epoll 專用的文件描述符,epoll_create()的返回值
op:?表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從 epfd 中刪除一個 fd;
fd:?需要監聽的文件描述符
event:?告訴內核要監聽什么事件,struct epoll_event 結構如:
- ??
- typedef?union?epoll_data?{????
- ????void?*ptr;????
- ????int?fd;????
- ????__uint32_t?u32;????
- ????__uint64_t?u64;????
- }?epoll_data_t;????
- ????
- ??
- struct?epoll_event?{????
- ????__uint32_t?events;?????
- ????epoll_data_t?data;?????
- };????
events 可以是以下幾個宏的集合:
EPOLLIN?:表示對應的文件描述符可以讀(包括對端 SOCKET 正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET?:將 EPOLL 設為邊緣觸發(Edge Trigger)模式,這是相對于水平觸發(Level Trigger)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個 socket 的話,需要再次把這個 socket 加入到 EPOLL 隊列里
返回值:
成功:0
失敗:-1
int epoll_wait( int epfd,?struct epoll_event * events,?int maxevents,?int timeout );
功能:
等待事件的產生,收集在 epoll 監控的事件中已經發送的事件,類似于 select() 調用。
參數:
epfd:?epoll 專用的文件描述符,epoll_create()的返回值
events:?分配好的 epoll_event 結構體數組,epoll 將會把發生的事件賦值到events 數組中(events 不可以是空指針,內核只負責把數據復制到這個 events 數組中,不會去幫助我們在用戶態中分配內存)。
maxevents:?maxevents 告之內核這個 events 有多少個 。
timeout:?超時時間,單位為毫秒,為 -1 時,函數為阻塞。
返回值:
成功:返回需要處理的事件數目,如返回 0 表示已超時
失敗:-1
epoll 對文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默認模式,LT 模式與 ET 模式的區別如下:
LT 模式:支持block和no-block socket。當 epoll_wait 檢測到描述符事件發生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用 epoll_wait 時,會再次響應應用程序并通知此事件。效率會低于ET觸發,尤其在大并發,大流量的情況下。但是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,因此不用擔心事件丟失的情況。
ET 模式:只支持no-block socket。當 epoll_wait 檢測到描述符事件發生并將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait 時,不會再次響應應用程序并通知此事件。該模式效率非常高,尤其在高并發,大流量的情況下,會比LT少很多epoll的系統調用。但是對編程要求高,需要細致的處理每個請求,否則容易發生丟失事件的情況。
接下來,我們將上面的例子,改為用 epoll 實現:
- #include?<cstdio>??
- #include?<sys/epoll.h>??
- #include?<unistd.h>??
- #include?<stdlib.h>??
- #include?<cstring>??
- #include?<cassert>??
- #include?<sys/types.h>??
- #include?<sys/socket.h>??
- #include?<netinet/in.h>??
- #include?<arpa/inet.h>??
- ??
- const?int?BUFFER_SIZE?=?4096;??
- const?int?SERVER_PORT?=?2222;??
- ??
- int?main()??
- {??
- ????int?server_socket;??
- ????char?buff1[BUFFER_SIZE];??
- ????char?buff2[BUFFER_SIZE];??
- ????struct?timeval?tv;??
- ????int?ret;??
- ????int?n,?i;??
- ??
- ????server_socket?=?socket(AF_INET,?SOCK_STREAM,?0);??
- ????assert(server_socket?!=?-1);??
- ??
- ????struct?sockaddr_in?server_addr;??
- ????memset(&server_addr,?0,?sizeof(server_addr));??
- ????server_addr.sin_family?=?AF_INET;??
- ????server_addr.sin_port?=?htons(SERVER_PORT);??
- ????server_addr.sin_addr.s_addr?=?htonl(INADDR_ANY);??
- ??
- ????assert(bind(server_socket,?(struct?sockaddr?*)&server_addr,?sizeof(server_addr))?!=?-1);??
- ????assert(listen(server_socket,?5)?!=?-1);??
- ??????
- ????struct?sockaddr_in?client_addr1,?client_addr2;??
- ????socklen_t?client_addr_len?=?sizeof(struct?sockaddr_in);??
- ??????
- ????printf("waiting...\n");??
- ??
- ????int?connfd1?=?accept(server_socket,?(struct?sockaddr*)&client_addr1,?&client_addr_len);??
- ????assert(connfd1?!=?-1);??
- ????printf("connect?from?%s:%d\n",?inet_ntoa(client_addr1.sin_addr),?ntohs(client_addr1.sin_port));??
- ????int?connfd2?=?accept(server_socket,?(struct?sockaddr*)&client_addr2,?&client_addr_len);??
- ????assert(connfd2?!=?-1);??
- ????printf("connect?from?%s:%d\n",?inet_ntoa(client_addr2.sin_addr),?ntohs(client_addr2.sin_port));??
- ??
- ????tv.tv_sec?=?10;??
- ????tv.tv_usec?=?0;??
- ??????
- ????struct?epoll_event?event;??
- ????struct?epoll_event?wait_event[2];??
- ??????
- ????int?epfd?=?epoll_create(10);??
- ????assert(epfd?!=?-1);??
- ??????
- ????event.data.fd?=?connfd1;??
- ????event.events?=?EPOLLIN;??
- ????assert(epoll_ctl(epfd,?EPOLL_CTL_ADD,?connfd1,?&event)?!=?-1);??
- ????event.data.fd?=?connfd2;??
- ????event.events?=?EPOLLIN;??
- ????assert(epoll_ctl(epfd,?EPOLL_CTL_ADD,?connfd2,?&event)?!=?-1);??
- ??
- ??????
- ????while(1)??
- ????{??
- ????????printf("epoll...\n");??
- ????????ret?=?epoll_wait(epfd,?wait_event,?2,?-1);??
- ??????????
- ????????if(ret?==?-1)??
- ????????????perror("epoll()");??
- ????????else?if(ret?>?0)??
- ????????{?????
- ????????????for(i?=?0;?i?<?ret;?++i)??
- ????????????{??
- ????????????????if(wait_event[i].data.fd?==?connfd1?&&?(wait_event[i].events?&?EPOLLIN)?==?EPOLLIN)??
- ????????????????{?????
- ????????????????????n?=?recv(connfd1,?buff1,?BUFFER_SIZE,?0);??
- ????????????????????buff1[n]?=?'\0';??
- ????????????????????printf("connfd1:?%s\n",?buff1);??
- ????????????????}??
- ????????????????else?if(wait_event[i].data.fd?==?connfd2?&&?(wait_event[i].events?&?EPOLLIN)?==?EPOLLIN)??
- ????????????????{??
- ????????????????????n?=?recv(connfd2,?buff2,?BUFFER_SIZE,?0);??
- ????????????????????buff2[n]?=?'\0';??
- ????????????????????printf("connfd2:?%s\n",?buff2);??
- ????????????????}?????
- ????????????}??
- ????????}??
- ????????else??
- ????????????printf("time?out\n");??
- ????}??
- ??
- ????return?0;??
- }??
在 select/poll中,進程只有在調用一定的方法后,內核才對所有監視的文件描述符進行掃描,而 epoll() 事先通過 epoll_ctl() 來注冊一個文件描述符,一旦某個文件描述符就緒時,內核會采用類似 callback 的回調機制(軟件中斷 ),迅速激活這個文件描述符,當進程調用 epoll_wait() 時便得到通知。
下面分析 select、poll、epoll之間的優缺點:
select:
缺點:
1)每次調用select,都存在 fd 集合在用戶態與內核態之間的拷貝,I/O 的效率會隨著監視 fd 的數量的增長而線性下降。
2)select()調用的內部,需要用輪詢的方式去完整遍歷每一個 fd,如果遍歷完所有 fd 后沒有發現就緒 fd,則掛起當前進程,直到有 fd 就緒或者主動超時(使用 schedule_timeout 實現睡一會兒,判斷一次(被定時器喚醒,注意與 select() 函數里面的 timeout 參數區分作用)的效果),被喚醒后它又要再次遍歷 fd (直到有 fd 就緒或 select() 函數超時)。這個過程經歷了多次無謂的遍歷。CPU的消耗會隨著監視 fd 的數量的增長而線性增加。
- 步驟總結如下:??
- 1)先把全部fd掃一遍;??
- 2)如果發現有可用的fd,跳到5;??
- 3)如果沒有,當前進程去睡覺xx秒(schedule_timeout機制);??
- 4)xx秒后自己醒了,或者狀態變化的fd喚醒了自己,跳到1;??
- 5)結束循環體,返回。(注意函數的返回也可能是?timeout?的超時)??
3)select支持的文件描述符數量太小了,默認是1024。
4)由于 select 參數輸入和輸出使用同樣的 fd_set ,導致每次 select() ?之前都要重新初始化要監視的 fd_set,開銷也會比較大。
poll:
poll 的實現和 select 非常相似,它同樣存在 fd 集合在用戶態和內核態間的拷貝,且在函數內部需要輪詢整個 fd 集合。區別于select 的只是描述fd集合的方式不同,poll使用pollfd數組而不是select的fd_set結構,所以poll克服了select文件描述符數量的限制,此外,poll 的 polldf 結構體中分別用 events 和 revents 來存儲輸入和輸出,較之 select() 不用每次調用 poll() 之前都要重新初始化需要監視的事件。
epoll:
epoll是一種 Reactor 模式,提供了三個函數,epoll_create(),epoll_ctl() 和 epoll_wait()。
優點:
1)對于上面的第一個缺點,epoll 的解決方案在 epoll_ctl() 函數中。每次注冊新的事件到 epoll 描述符中時,會把該 fd 拷貝進內核,而不是在epoll_wait的時候重復拷貝。epoll 保證了每個fd在整個過程中只會拷貝一次。
2)對于第二個缺點,epoll 的解決方案不像 select 或 poll 一樣輪詢 fd,而只在 epoll_ctl 時把要監控的 fd 掛一遍,并為每個 fd 指定一個回調函數,當設備就緒,這個回調函數把就緒的 fd 加入一個就緒鏈表。epoll_wait 的工作實際上就是在這個就緒鏈表中查看有沒有就緒的 fd,也即 epoll_wait 只關心“活躍”的描述符,而不用像 select() 和 poll() 需要遍歷所有 fd,它需要不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒(類似與 select, poll),但終究它的輪詢只用判斷就續表是否為空即可,其CPU的消耗不會隨著監視 fd 的數量的增長而線性增加,這就是回調機制的優勢,也正是 epoll 的魅力所在。
同理,select() 和 poll() 函數返回后, 處理就緒 fd 的方法還是輪詢,如下:
- int?res?=?select(maxfd+1,?&readfds,?NULL,?NULL,?120);????
- if?(res?>?0)????
- {????
- ????for?(int?i?=?0;?i?<?MAX_CONNECTION;?i++)????
- ????{????
- ????????if?(FD_ISSET(allConnection[i],?&readfds))????
- ????????{????
- ????????????handleEvent(allConnection[i]);????
- ????????}????
- ????}????
- }????
- ??
而 epoll() 只需要從就緒鏈表中處理就緒的 fd:
- int?res?=?epoll_wait(epfd,?events,?20,?-1);????
- for?(int?i?=?0;?i?<?res;i++)????
- {????
- ????handleEvent(events[n]);????
- }????
此處的效率對比也是高下立判。
3)對于第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個和系統限制有關,linux里面可以用ulimit查看文件打開數限制。
缺點:epoll是 linux 特有的,而 select 和 poll 是在 POSIX 中規定的,跨平臺支持更好。
綜上:
select 、poll、epoll 的使用要根據具體的使用場合,并不是 epoll 的性能就一定好,因為回調函數也是有消耗的,當 socket 連接較少時或者是即使 socket 連接很多,但是連接基本都是活躍的情況下,select / poll 的性能與 epoll 是差不多的。即如果沒有大量的 idle-connection 或者 dead-connection,epoll 的效率并不會比 select/poll 高很多,但是當遇到大量的 idle-connection,就會發現epoll 的效率大大高于 select/poll。