一、概述
說到Linux下的IO復用,系統提供了三個系統調用,分別是select poll epoll。那么這三者之間有什么不同呢,什么時候使用三個之間的其中一個呢?
下面,我將從系統調用原型來分析其中的不同。
二、系統接口原型
1. select
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
2. poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *timeout_ts, const sigset_t *sigmask);
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
3. epoll
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
三、參數對比
1. select
- select的第一個參數nfds為fdset集合中最大描述符值加1,fdset是一個位數組,其大小限制為__FD_SETSIZE(1024),位數組的每一位代表其對應的描述符是否需要被檢查;
- select的第二三四個參數表示需要關注讀、寫、錯誤事件的文件描述符位數組,這些參數既是輸入參數也是輸出參數,可能會被內核修改用於標示哪些描述符上發生了關注的事件。所以每次調用select前都需要重新初始化fdset。
- timeout參數為超時時間,該結構會被內核修改,其值為超時剩余的時間。
- select對應於內核中的sys_select調用,sys_select首先將第二三四個參數指向的fd_set拷貝到內核,然后對每個被SET的描述符調用進行poll,並記錄在臨時結果中(fdset),如果有事件發生,select會將臨時結果寫到用戶空間並返回;當輪詢一遍后沒有任何事件發生時,如果指定了超時時間,則select會睡眠到超時,睡眠結束后再進行一次輪詢,並將臨時結果寫到用戶空間,然后返回。
- select返回后,需要逐一檢查關注的描述符是否被SET(事件是否發生)。
2. poll
- poll與select不同,通過一個pollfd數組向內核傳遞需要關注的事件,故沒有描述符個數的限制,pollfd中的events字段和revents分別用於標示關注的事件和發生的事件,故pollfd數組只需要被初始化一次。
- poll的實現機制與select類似,其對應內核中的sys_poll,只不過poll向內核傳遞pollfd數組,然后對pollfd中的每個描述符進行poll,相比處理fdset來說,poll效率更高。
- poll返回后,需要對pollfd中的每個元素檢查其revents值,來得指事件是否發生。
poll事件類型
| 事件 | 描述 | 是否可作為輸入 | 是否可作為輸出 |
| POLLIN | 數據(包括普通數據和優先數據) | 是 | 是 |
| POLLRDNORM | 普通數據可讀 | 是 | 是 |
| POLLRDBAND | 優先級帶數據可讀(Linux不支持) | 是 | 是 |
| POLLPRI | 高優先級數據可讀,比如TCP帶外數據 | 是 | 是 |
| POLLOUT | 數據(包括普通數據和優先數據)可寫 | 是 | 是 |
| POLLWRNORM | 普通數據可寫 | 是 | 是 |
| POLLWRBAND | 優先級帶數據可寫 | 是 | 是 |
| POLLRDHUP | TCP連接被對方關閉,或者對方關閉了寫操作。它由GNU引入 | 是 | 是 |
| POLLERR | 錯誤 | 否 | 是 |
| POLLHUP | 掛起。比如管道的寫端被關閉后,讀端描述符上將收到POLLHUP事件 | 否 | 是 |
| POLLNVAL | 文件描述符沒有打開 | 否 | 是 |
3. epoll
- epoll是Linux特有的I/O復用函數。它在實現上與select、poll有很大的差異。首先,epoll使用一組函數來完成任務,而不是單個函數;
- 其次,epoll把用戶關心的文件描述符上的事件放在內核里的一個事件表中,從而無需像select和poll那樣每次調用都要重復傳入文件的事件放在內核里的一個事件表中。但epoll需要使用一個額外的文件描述符,來唯一標識內核中的這個事件表;
- epoll通過epoll_create創建一個用於epoll輪詢的描述符,通過epoll_ctl添加/修改/刪除事件,通過epoll_wait檢查事件,epoll_wait的第二個參數用於存放結果。
- epoll與select、poll不同,首先,其不用每次調用都向內核拷貝事件描述信息,在第一次調用后,事件信息就會與對應的epoll描述符關聯起來。另外epoll不是通過輪詢,而是通過在等待的描述符上注冊回調函數,當事件發生時,回調函數負責把發生的事件存儲在就緒事件鏈表中,最后寫到用戶空間。
- epoll返回后,該參數指向的緩沖區中即為發生的事件,對緩沖區中每個元素進行處理即可,而不需要像poll、select那樣進行輪詢檢查。
四、性能對比
select、poll的內部實現機制相似,性能差別主要在於向內核傳遞參數以及對fdset的位操作上,另外,select存在描述符數的硬限制,不能處理很大的描述符集合。
這里主要考察poll與epoll在不同大小描述符集合的情況下性能的差異。
測試程序會統計在不同的文件描述符集合的情況下,1s 內poll與epoll調用的次數。
統計結果如下,從結果可以看出,對poll而言,每秒鍾內的系統調用數目雖集合增大而很快降低,而epoll基本保持不變,具有很好的擴展性。
| 描述符集合大小 |
poll |
epoll |
| 1 |
331598 |
258604 |
| 10 |
330648 |
297033 |
| 100 |
91199 |
288784 |
| 1000 |
27411 |
296357 |
| 5000 |
5943 |
288671 |
| 10000 |
2893 |
292397 |
| 25000 |
1041 |
285905 |
| 50000 |
536 |
293033 |
| 100000 |
224 |
285825 |
五、連接數
我本人也曾經在項目中用過select和epoll,對於select,感觸最深的是linux下select最大數目限制(windows 下似乎沒有限制),每個進程的select最多能處理FD_SETSIZE個FD(文件句柄),如果要處理超過1024個句柄,只能采用多進程了。
常見的使用select的多進程模型是這樣的:
一個進程專門accept,成功后將fd通過UNIX socket傳遞給子進程處理,父進程可以根據子進程負載分派。
曾經用過1個父進程 + 4個子進程 承載了超過4000個的負載。
這種模型在我們當時的業務運行的非常好。
epoll在連接數方面沒有限制,當然可能需要用戶調用API重現設置進程的資源限制。
六、相同點
- 都能同時監聽多個文件描述符;
- 它們將等待由timeout參數指定的超時時間,直到一個或者多個文件描述符上有事件發生時返回,返回值是就緒的文件描述符的數量;
七、不同點
對於select:
1. 只能通過三個結構體參數處理三種事件,分別是:可讀、可寫和異常事件,而不能處理更多的事件;
2. 這三個參數既是輸入參數,也是輸出參數,因此,在每次調用select之前,都得對fd_set進行重置;
對於poll:
1. 將文件描述符和事件關聯在一起,任何事件都被統一處理,從而使得編程接口簡潔不少;
2. 內核改變的變量是revents,而不是events,因此,調用之前不需要再重置;
由於每次select和poll調用都返回整個用戶注冊的事件集合(其中包括就緒的和未就緒的),所以應用程序索引就緒文件描述符的時間復雜度為O(n)。
而epoll采用與select和poll完全不同的方式來管理用戶注冊的事件。
八、poll和epoll在使用上的差別
/*poll example*/ /*如何索引poll返回的就緒文件描述符*/ int ret = poll(fds, MAX_EVENT_NUMBER, -1); /*必須遍歷所有已注冊文件描述符並找到其中的就緒者(當然,可以利用ret來稍作優化)*/ for(int i = 0; i < MAX_EVENT_NUMBER; ++i) { if(fds[i].revents & POLLIN) { int sockfd = fds[i].fd; //deal with sockfd. } } /*epoll example*/ int epfd = epoll_create(MAXSIZE); struct epoll_event ev,events[5000]; //設置與要處理的事件相關的文件描述符 ev.data.fd=listenfd; //設置要處理的事件類型 ev.events=EPOLLIN|EPOLLET; //注冊epoll事件 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); int nfds = epoll_wait(epfd,events,6000,-1); //處理所發生的所有事件 for(int i = 0; i< nfds; ++i) { //new accept. if(events[i].data.fd == listenfd) { printf("listen=%d\n",events[i].data.fd); 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<<"connec_ from >>"<<str<<" "<<connfd<<std::endl; //設置用於讀操作的文件描述符 ev.data.fd = connfd; //設置用於注測的讀操作事件 //ev.data.ptr = NULL; ev.events = EPOLLIN|EPOLLET; //注冊ev epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); ev.data.fd = listenfd; //設置要處理的事件類型 ev.events=EPOLLIN|EPOLLET; //注冊epoll事件 epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev); continue; } else if(events[i].events & EPOLLIN) { num1++; //fprintf(stderr,"reading! %d\n",num1); if( (sockfd = events[i].data.fd) <= 0) { num1--; continue; } new_task = NULL; while(new_task == NULL) new_task = new task(); new_task->fd = sockfd; new_task->next=NULL; //fprintf(stderr,"sockfd %d",sockfd); //添加新的讀任務 pthread_mutex_lock(&mutex); if(readhead == NULL) { readhead = new_task; readtail = new_task; } else { readtail->next=new_task; readtail=new_task; } //喚醒所有等待cond1條件的線程 pthread_cond_broadcast(&cond1); pthread_mutex_unlock(&mutex); continue; } else if(events.events & EPOLLOUT) { //fprintf(stderr,"EPOLLOUT"); num++; rdata=(struct user_data *)events[i].data.ptr; sockfd =rdata->fd; if(old == sockfd) { fprintf(stderr,"repreted sockfd=%d\n",sockfd); //exit(1); } old=sockfd; //fprintf(stderr,"write %d\n",num); int size=write(sockfd, rdata->line, rdata->n_size); //fprintf(stderr,"write=%d delete rdata\n",size); fprintf(stderr,"addr=%x fdwrite=%d size=%d\n",rdata,rdata->fd,size); if(rdata!=NULL)//主要問題導致delete重復相同對象 events返回對象相同 { delete rdata; rdata=NULL; } //設置用於讀操作的文件描述符 //fprintf(stderr,"after delete rdata\n"); ev.data.fd=sockfd; //設置用於注測的讀操作事件 ev.events=EPOLLIN|EPOLLET; //修改sockfd上要處理的事件為EPOLIN res = epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); while(res==-1) { //fprintf(stderr,"out error"); exit(1); } //fprintf(stderr,"out EPOLLOUT\n"); continue; } else if(events.events&(EPOLLHUP|EPOLLERR)) { //fprintf(stderr,"EPPOLLERR\n"); int fd=events.data.fd; if(fd>6000) { fd=((struct user_data*)(events.data.ptr))->fd; } //設置用於注測的讀操作事件 ev.data.fd=fd; ev.events=EPOLLIN|EPOLLET|EPOLLOUT; //修改sockfd上要處理的事件為EPOLIN epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev); } }
