Linux: I/O多路转接之epoll(有图有代码有真相!!!)
一、基本概念
epoll是Linux內(nèi)核為處理大批量文件描述符而作了改進(jìn)的poll,是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率。
另一點(diǎn)原因就是獲取事件的時(shí)候,它無(wú)須遍歷整個(gè)被偵聽(tīng)的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊(duì)列的描述符集合就行了。epoll除了提供select/poll那種IO事件的
水平觸發(fā)(Level Triggered)外,還提供了邊緣觸發(fā)(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態(tài),減少epoll_wait/epoll_pwait的調(diào)用,提高應(yīng)用程序效率。
二、epoll函數(shù)解析
epoll過(guò)程分為三個(gè)接口
#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):
創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大。這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù),給出最大監(jiān)聽(tīng)的fd+1的值。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后,它就是會(huì)占用一個(gè)fd值,
在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。
2、epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注冊(cè)函數(shù),它不同與select()是在監(jiān)聽(tīng)事件時(shí)告訴內(nèi)核要監(jiān)聽(tīng)什么類(lèi)型的事件epoll的事件注冊(cè)函數(shù),它不同與select()是在監(jiān)聽(tīng)事件時(shí)告訴內(nèi)核要監(jiān)聽(tīng)什么類(lèi)型的事件,而是在這里先注冊(cè)要監(jiān)聽(tīng)的事件類(lèi)型。
第一個(gè)參數(shù)是epoll_create()的返回值;
第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來(lái)表示:
EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中;
EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件;
EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;
第三個(gè)參數(shù)是需要監(jiān)聽(tīng)的fd;
第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽(tīng)什么事,struct epoll_event結(jié)構(gòu)如下:
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可以是以下幾個(gè)宏的集合:
EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫(xiě);
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的。
EPOLLONESHOT:只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里
3、epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的產(chǎn)生,類(lèi)似于select()調(diào)用。參數(shù)events用來(lái)從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個(gè)events有多大,這個(gè)maxevents的值不
能大于創(chuàng)建epoll_create()時(shí)的size,參數(shù)timeout是超時(shí)時(shí)間(毫秒,0會(huì)立即返回,-1將不確定,也有說(shuō)法說(shuō)是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時(shí)。
三、epoll 的工作原理
epoll同樣只告知那些就緒的?文件描述符,?而且當(dāng)我們調(diào)?用epoll_wait()獲得就緒?文件描述符時(shí),返回的不是實(shí)際的描述符,?而是?一個(gè)代表就緒描述符數(shù)量的值,
你只需要去epoll指定的?個(gè)數(shù)組中依次取得相應(yīng)數(shù)量的?文件描述符即可,這?里也使?用了內(nèi)存映射(mmap)技術(shù),這樣便徹底省掉了這些?文件描述符在系統(tǒng)調(diào)?用時(shí)復(fù)制的開(kāi)銷(xiāo)。
另?一個(gè)本質(zhì)的改進(jìn)在于epoll采?用基于事件的就緒通知?方式。在select/poll中,進(jìn)程只有在調(diào)?用?一定的?方法后,內(nèi)核才對(duì)所有監(jiān)視的?文件描述符進(jìn)?行掃描,
?而epoll事先通過(guò)epoll_ctl()來(lái)注冊(cè)?一個(gè)?文件描述符,?一旦基于某個(gè)?文件描述符就緒時(shí),內(nèi)核會(huì)采?用類(lèi)似callback的回調(diào)機(jī)制,迅速激活這個(gè)?文件描述符,
當(dāng)進(jìn)程調(diào)?用epoll_wait()時(shí)便得到通知。
Epoll的2種?工作?方式-?水平觸發(fā)(LT)和邊緣觸發(fā)(ET):??
假如有這樣一個(gè)例子:
1. 我們已經(jīng)把一個(gè)用來(lái)從管道中讀取數(shù)據(jù)的文件句柄(RFD)添加到epoll描述符
2. 這個(gè)時(shí)候從管道的另一端被寫(xiě)入了2KB的數(shù)據(jù)
3. 調(diào)用epoll_wait(2),并且它會(huì)返回RFD,說(shuō)明它已經(jīng)準(zhǔn)備好讀取操作
4. 然后我們讀取了1KB的數(shù)據(jù)
5. 調(diào)用epoll_wait(2)......
Edge Triggered 工作模式:
如果我們?cè)诘?步將RFD添加到epoll描述符的時(shí)候使用了EPOLLET標(biāo)志,那么在第5步調(diào)用epoll_wait(2)之后將有可能會(huì)掛
起,因?yàn)槭S嗟臄?shù)據(jù)還存在于文件的輸入緩沖區(qū)內(nèi),而且數(shù)據(jù)發(fā)出端還在等待一個(gè)針對(duì)已經(jīng)發(fā)出數(shù)據(jù)的反饋信息。只有在監(jiān)視
的文件句柄上發(fā)生了某個(gè)事件的時(shí)候 ET 工作模式才會(huì)匯報(bào)事件。因此在第5步的時(shí)候,調(diào)用者可能會(huì)放棄等待仍在存在于文
件輸入緩沖區(qū)內(nèi)的剩余數(shù)據(jù)。在上面的例子中,會(huì)有一個(gè)事件產(chǎn)生在RFD句柄上,因?yàn)樵诘?步執(zhí)行了一個(gè)寫(xiě)操作,然后,
事件將會(huì)在第3步被銷(xiāo)毀。因?yàn)榈?步的讀取操作沒(méi)有讀空文件輸入緩沖區(qū)內(nèi)的數(shù)據(jù),因此我們?cè)诘?步調(diào)用 epoll_wait(2)完成
后,是否掛起是不確定的。epoll工作在ET模式的時(shí)候,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫(xiě)操
作把處理多個(gè)文件描述符的任務(wù)餓死。最好以下面的方式調(diào)用ET模式的epoll接口,在后面會(huì)介紹避免可能的缺陷。
? ?i ? ?基于非阻塞文件句柄
? ?ii ? 只有當(dāng)read(2)或者write(2)返回EAGAIN時(shí)才需要掛起,等待。但這并不是說(shuō)每次read()時(shí)都需要循環(huán)讀,直到讀到產(chǎn)生一
個(gè)EAGAIN才認(rèn)為此次事件處理完成,當(dāng)read()返回的讀到的數(shù)據(jù)長(zhǎng)度小于請(qǐng)求的數(shù)據(jù)長(zhǎng)度時(shí),就可以確定此時(shí)緩沖中已沒(méi)有
數(shù)據(jù)了,也就可以認(rèn)為此事讀事件已處理完成。
Level Triggered 工作模式
相反的,以LT方式調(diào)用epoll接口的時(shí)候,它就相當(dāng)于一個(gè)速度比較快的poll(2),并且無(wú)論后面的數(shù)據(jù)是否被使用,因此他們具
有同樣的職能。因?yàn)榧词故褂肊T模式的epoll,在收到多個(gè)chunk的數(shù)據(jù)的時(shí)候仍然會(huì)產(chǎn)生多個(gè)事件。調(diào)用者可以設(shè)定
EPOLLONESHOT標(biāo)志,在 epoll_wait(2)收到事件后epoll會(huì)與事件關(guān)聯(lián)的文件句柄從epoll描述符中禁止掉。因此當(dāng)?
EPOLLONESHOT設(shè)定后,使用帶有 EPOLL_CTL_MOD標(biāo)志的epoll_ctl(2)處理文件句柄就成為調(diào)用者必須作的事情。
注:ET模式在很大程度上減少了epoll事件被重復(fù)觸發(fā)的次數(shù),因此效率要比LT模式高。epoll工作在ET模式的時(shí)候,必須使用
非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫(xiě)操作把處理多個(gè)文件描述符的任務(wù)餓死。
LT(level triggered)是epoll缺省的?工作?方式,并且同時(shí)?支持block和no-block socket.在這種做法中,內(nèi)核告訴你?一個(gè)?
文件描述符是否就緒了,然后你可以對(duì)這個(gè)就緒的fd進(jìn)?行IO操作。如果你不作任何操作,內(nèi)核還是會(huì)繼續(xù)通知你 的,所以,
這種模式編程出錯(cuò)誤可能性要?小一點(diǎn)。傳統(tǒng)的select/poll都是這種模型的代表。
ET (edge-triggered)是?高速?工作?方式,只?支持no-block socket,它效率要?比LT更?高。ET與LT的區(qū)別在于,當(dāng)一個(gè)
新的事件到來(lái)時(shí),ET模式下當(dāng)然可以從epoll_wait調(diào)?用中獲取到這個(gè)事件,可是如果這次沒(méi)有把這個(gè)事件對(duì)應(yīng)的套接字緩沖
區(qū)處理完,在這個(gè)套接字中沒(méi)有新的事件再次到來(lái)時(shí),在ET模式下是?無(wú)法再次從epoll_wait調(diào)?用中獲取這個(gè)事件的。?而
LT模式正好相反,只要?一個(gè)事件對(duì)應(yīng)的套接字緩沖區(qū)還有數(shù)據(jù),就總能從epoll_wait中獲取這個(gè)事件。因此,LT模式下開(kāi)發(fā)
基于epoll的應(yīng)?用要簡(jiǎn)單些,不太容易出錯(cuò)。?而在ET模式下事件發(fā)?生時(shí),如果沒(méi)有徹底地將緩沖區(qū)數(shù)據(jù)處理完,則會(huì)導(dǎo)
致緩沖區(qū)中的?用戶請(qǐng)求得不到響應(yīng)。Nginx默認(rèn)采?用ET模式來(lái)使?用epoll。
四、epoll實(shí)現(xiàn)服務(wù)器
#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;//超時(shí)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{}}//寫(xiě)事件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 支持一個(gè)進(jìn)程所能打開(kāi)的最大連接數(shù)
| ?select ? ? ? ? ? ? ? ?? | ?單個(gè)進(jìn)程所能打開(kāi)的最大連接數(shù)有FD_SETSIZE宏定義,其大小是32個(gè)整數(shù)的大小(在32位的機(jī)器上,大小就是32*32,同理64位機(jī)器上 FD_SETSIZE為32*64),當(dāng)然我們可以對(duì)它進(jìn)行修改,然后重新編譯內(nèi)核,但是性能可能會(huì)受到影響,這需要進(jìn)一步的測(cè)試。 |
| ?poll | ?poll本質(zhì)上和select沒(méi)有區(qū)別,但是它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的 |
| ? epoll | ?雖然連接數(shù)有上限,但是很大,1G內(nèi)存的機(jī)器上可以打開(kāi)10萬(wàn)左右的連接,2G內(nèi)存的機(jī)器可以打開(kāi)20萬(wàn)左右的連接。 |
?
2 FD劇增后帶來(lái)的IO效率問(wèn)題
| ?select ? ? ? ? ? ? ? ? ?? | ?因?yàn)槊看握{(diào)用時(shí)都會(huì)對(duì)連接進(jìn)行線性遍歷,所以隨著FD的增加會(huì)造成遍歷速度慢的“線性下降性能問(wèn)題”。 |
| ?poll | ?同上 |
| ?epoll | ?因?yàn)閑poll內(nèi)核中實(shí)現(xiàn)是根據(jù)每個(gè)fd上的callback函數(shù)來(lái)實(shí)現(xiàn)的,只有活躍的socket才會(huì)主動(dòng)調(diào)用callback,所以在活躍socket較少的情況下,使用epoll沒(méi)有前面兩者的線性下降的性能問(wèn)題,但是所有socket都很活躍的情況下,可能會(huì)有性能問(wèn)題。 |
???
3 消息傳遞方式
| ?select | ?內(nèi)核需要將消息傳遞到用戶空間,都需要內(nèi)核拷貝動(dòng)作 |
| ?poll | ?同上 |
| ?epoll | ?epoll通過(guò)內(nèi)核和用戶空間共享一塊內(nèi)存來(lái)實(shí)現(xiàn)的 |
???
綜上,在選擇select,poll,epoll時(shí)要根據(jù)具體的使用場(chǎng)合以及這三種方式的自身特點(diǎn)。表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十
分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機(jī)制需要很多函數(shù)回調(diào)。
總結(jié)
以上是生活随笔為你收集整理的Linux: I/O多路转接之epoll(有图有代码有真相!!!)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CentOS7和CentOS8 Aste
- 下一篇: Linux内核启动去掉企鹅,修改linu