Linux: I/O多路转接之epoll(有图有代码有真相!!!)
一、基本概念
epoll是Linux內核為處理大批量文件描述符而作了改進的poll,是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量并發連接中只有少量活躍的情況下的系統CPU利用率。
另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select/poll那種IO事件的
水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。
二、epoll函數解析
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);
 
1、epoll_create(int size):
創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。這個參數不同于select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當創建好epoll句柄后,它就是會占用一個fd值,
在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。
 
2、epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注冊函數,它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件epoll的事件注冊函數,它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。
第一個參數是epoll_create()的返回值;
第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd;
第四個參數是告訴內核需要監聽什么事,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 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、epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的產生,類似于select()調用。參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個maxevents的值不
能大于創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。
 
 
三、epoll 的工作原理
epoll同樣只告知那些就緒的?文件描述符,?而且當我們調?用epoll_wait()獲得就緒?文件描述符時,返回的不是實際的描述符,?而是?一個代表就緒描述符數量的值,
你只需要去epoll指定的?個數組中依次取得相應數量的?文件描述符即可,這?里也使?用了內存映射(mmap)技術,這樣便徹底省掉了這些?文件描述符在系統調?用時復制的開銷。
另?一個本質的改進在于epoll采?用基于事件的就緒通知?方式。在select/poll中,進程只有在調?用?一定的?方法后,內核才對所有監視的?文件描述符進?行掃描,
?而epoll事先通過epoll_ctl()來注冊?一個?文件描述符,?一旦基于某個?文件描述符就緒時,內核會采?用類似callback的回調機制,迅速激活這個?文件描述符,
當進程調?用epoll_wait()時便得到通知。
 
Epoll的2種?工作?方式-?水平觸發(LT)和邊緣觸發(ET):??
假如有這樣一個例子:
1. 我們已經把一個用來從管道中讀取數據的文件句柄(RFD)添加到epoll描述符
2. 這個時候從管道的另一端被寫入了2KB的數據
3. 調用epoll_wait(2),并且它會返回RFD,說明它已經準備好讀取操作
4. 然后我們讀取了1KB的數據
5. 調用epoll_wait(2)......
 
Edge Triggered 工作模式:
如果我們在第1步將RFD添加到epoll描述符的時候使用了EPOLLET標志,那么在第5步調用epoll_wait(2)之后將有可能會掛
起,因為剩余的數據還存在于文件的輸入緩沖區內,而且數據發出端還在等待一個針對已經發出數據的反饋信息。只有在監視
的文件句柄上發生了某個事件的時候 ET 工作模式才會匯報事件。因此在第5步的時候,調用者可能會放棄等待仍在存在于文
件輸入緩沖區內的剩余數據。在上面的例子中,會有一個事件產生在RFD句柄上,因為在第2步執行了一個寫操作,然后,
事件將會在第3步被銷毀。因為第4步的讀取操作沒有讀空文件輸入緩沖區內的數據,因此我們在第5步調用 epoll_wait(2)完成
后,是否掛起是不確定的。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操
作把處理多個文件描述符的任務餓死。最好以下面的方式調用ET模式的epoll接口,在后面會介紹避免可能的缺陷。
? ?i ? ?基于非阻塞文件句柄
? ?ii ? 只有當read(2)或者write(2)返回EAGAIN時才需要掛起,等待。但這并不是說每次read()時都需要循環讀,直到讀到產生一
個EAGAIN才認為此次事件處理完成,當read()返回的讀到的數據長度小于請求的數據長度時,就可以確定此時緩沖中已沒有
數據了,也就可以認為此事讀事件已處理完成。
 
Level Triggered 工作模式
相反的,以LT方式調用epoll接口的時候,它就相當于一個速度比較快的poll(2),并且無論后面的數據是否被使用,因此他們具
有同樣的職能。因為即使使用ET模式的epoll,在收到多個chunk的數據的時候仍然會產生多個事件。調用者可以設定
EPOLLONESHOT標志,在 epoll_wait(2)收到事件后epoll會與事件關聯的文件句柄從epoll描述符中禁止掉。因此當?
EPOLLONESHOT設定后,使用帶有 EPOLL_CTL_MOD標志的epoll_ctl(2)處理文件句柄就成為調用者必須作的事情。
注:ET模式在很大程度上減少了epoll事件被重復觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用
非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
 
LT(level triggered)是epoll缺省的?工作?方式,并且同時?支持block和no-block socket.在這種做法中,內核告訴你?一個?
文件描述符是否就緒了,然后你可以對這個就緒的fd進?行IO操作。如果你不作任何操作,內核還是會繼續通知你 的,所以,
這種模式編程出錯誤可能性要?小一點。傳統的select/poll都是這種模型的代表。
 
 
ET (edge-triggered)是?高速?工作?方式,只?支持no-block socket,它效率要?比LT更?高。ET與LT的區別在于,當一個
新的事件到來時,ET模式下當然可以從epoll_wait調?用中獲取到這個事件,可是如果這次沒有把這個事件對應的套接字緩沖
區處理完,在這個套接字中沒有新的事件再次到來時,在ET模式下是?無法再次從epoll_wait調?用中獲取這個事件的。?而
LT模式正好相反,只要?一個事件對應的套接字緩沖區還有數據,就總能從epoll_wait中獲取這個事件。因此,LT模式下開發
基于epoll的應?用要簡單些,不太容易出錯。?而在ET模式下事件發?生時,如果沒有徹底地將緩沖區數據處理完,則會導
致緩沖區中的?用戶請求得不到響應。Nginx默認采?用ET模式來使?用epoll。
 
 
 
四、epoll實現服務器
#include<stdio.h> #include<sys/epoll.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<string.h> static void usage(const char *proc) {printf("usage:%s [local_ip] [local_port]",proc); }typedef struct fd_buf {int fd;char buf[10240]; }fd_buf_t,*fd_buf_p;static void *alloc_fd_buf(int fd) {fd_buf_p tmp=(fd_buf_p)malloc(sizeof(fd_buf_t));if(!tmp){perror("malloc");return NULL;}tmp->fd=fd;return tmp; }int startup(const char *_ip,const int _port) {int sock=socket(AF_INET,SOCK_STREAM,0);if(sock<0){perror("socket");return 2;}struct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local .sin_addr.s_addr=inet_addr(_ip);if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){perror("bind");return 3;}if(listen(sock,10)<0){perror("listen");return 4;}return sock; } int main(int argc,char *argv[]) {if(argc!=3){usage(argv[0]);return 1;}int listen_sock=startup(argv[1],atoi(argv[2]));int epfd=epoll_create(256);if(epfd<0){printf("epoll_create");close(listen_sock);return 5;}struct epoll_event ev;ev.events=EPOLLIN;ev.data.ptr=alloc_fd_buf(listen_sock);epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);int num=0;struct epoll_event evs[64];int timeout=-1;while(1){switch((num=epoll_wait(epfd,evs,64,timeout))){//等待失敗 case -1:perror("epoll_wait");break;//超時case 0:perror("timeout...");break;//等待成功default:{int i=0;for(;i<num;i++){fd_buf_p fp=(fd_buf_p)evs[i].data.ptr;if(fp->fd==listen_sock && \(evs[i].events & EPOLLIN)){struct sockaddr_in client;socklen_t len=sizeof(client);int new_sock=accept(listen_sock,\(struct sockaddr*)&client,&len);if(new_sock<0){perror("accept");continue;}printf("get a new client\n");ev.events=EPOLLIN;ev.data.ptr=alloc_fd_buf(new_sock);epoll_ctl(epfd,EPOLL_CTL_ADD,\new_sock,&ev);}else if(fp->fd!=listen_sock){//讀事件if(evs[i].events & EPOLLIN){ssize_t s=read(fp->fd,fp->buf,sizeof(fp->buf));if(s>0){fp->buf[s]=0;printf("client say:%s\n",fp->buf);ev.events=EPOLLOUT;ev.data.ptr=fp;epoll_ctl(epfd,EPOLL_CTL_MOD,fp->fd,&ev);}else if(s<=0){close(fp->fd);epoll_ctl(epfd,EPOLL_CTL_DEL,fp->fd,NULL);free(fp);}else{}}//寫事件else if(evs[i].events & EPOLLOUT){const char *msg="HTTP/1.0 200 OK\r\n\r\n<html><h1>hello epoll</h1></html>";write(fp->fd,msg,strlen(msg));close(fp->fd);epoll_ctl(epfd,EPOLL_CTL_DEL,fp->fd,NULL);free(fp);}else{}}else{}}}break;}}return 0; }
五、epoll與select、poll比較
 
1 支持一個進程所能打開的最大連接數
| ?select ? ? ? ? ? ? ? ?? | ?單個進程所能打開的最大連接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是32*32,同理64位機器上 FD_SETSIZE為32*64),當然我們可以對它進行修改,然后重新編譯內核,但是性能可能會受到影響,這需要進一步的測試。 | 
| ?poll | ?poll本質上和select沒有區別,但是它沒有最大連接數的限制,原因是它是基于鏈表來存儲的 | 
| ? epoll | ?雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接,2G內存的機器可以打開20萬左右的連接。 | 
?
2 FD劇增后帶來的IO效率問題
| ?select ? ? ? ? ? ? ? ? ?? | ?因為每次調用時都會對連接進行線性遍歷,所以隨著FD的增加會造成遍歷速度慢的“線性下降性能問題”。 | 
| ?poll | ?同上 | 
| ?epoll | ?因為epoll內核中實現是根據每個fd上的callback函數來實現的,只有活躍的socket才會主動調用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。 | 
???
3 消息傳遞方式
| ?select | ?內核需要將消息傳遞到用戶空間,都需要內核拷貝動作 | 
| ?poll | ?同上 | 
| ?epoll | ?epoll通過內核和用戶空間共享一塊內存來實現的 | 
???
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。表面上看epoll的性能最好,但是在連接數少并且連接都十
分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。
 
 
總結
以上是生活随笔為你收集整理的Linux: I/O多路转接之epoll(有图有代码有真相!!!)的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: CentOS7和CentOS8 Aste
- 下一篇: wowbl最优势的服务器,CWOW中BL
