epoll使用詳解

在Linux網(wǎng)絡(luò)編程當(dāng)中,很長(zhǎng)時(shí)間都是使用select來(lái)做事件的觸發(fā),而在新的linux內(nèi)核當(dāng)中,有一種替換他的機(jī)制,就是epoll()//#include <sys/epoll.h>。
相對(duì)于select,epoll的好處就是更加靈活,沒(méi)有描述符的限制,且不會(huì)隨著監(jiān)聽fd數(shù)目的增長(zhǎng)而降低效率。因?yàn)樵趦?nèi)核中的select的實(shí)現(xiàn)使用的是輪詢機(jī)制,輪詢的fd數(shù)目越多,耗時(shí)就越多,并且在linux/posix_types.h頭文件中有這樣的聲明:#define __FD_SETSIZE 1024 當(dāng)然可以通過(guò)修改頭文件,再重新編譯內(nèi)核來(lái)擴(kuò)大這個(gè)數(shù)目,但是這似乎并不治本。epoll使用一個(gè)文件描述符管理多個(gè)文件描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個(gè)事件表當(dāng)中,這樣在用戶空間和內(nèi)核空間的copy只需要一次。

輪詢(polling)

  • 是一種CPU決策如何提供周邊設(shè)備的服務(wù)方式,又稱為程序控制輸入輸出(programmed I/O)。輪詢由CPU定時(shí)發(fā)出詢問(wèn),依序詢問(wèn)每一個(gè)周邊設(shè)備是否需要其服務(wù),有需要即給予服務(wù),服務(wù)結(jié)束后再詢問(wèn)下一個(gè)周邊,接著不斷地周而復(fù)始。輪詢法容易實(shí)現(xiàn),但是效率偏低。

epoll的接口非常簡(jiǎn)單,一共就三個(gè)函數(shù)

  1. int epoll_create(int size); 創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大(即你的epoll所支持的最大句柄數(shù)注意和select()的最大描述符值+1相區(qū)別)。這個(gè)參數(shù)不同于select中的第一個(gè)參數(shù)給出最大監(jiān)聽描述符的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. *int epoll_ctl(int epfd,int op,int fd.struct epoll_event event);epoll的事件注冊(cè)函數(shù),他與select()在監(jiān)聽事件時(shí)告訴內(nèi)核要監(jiān)聽什么類型的事件不同,epoll在這里先注冊(cè)要監(jiān)聽的事件類型(讀、寫、異常)。
  1. 第一個(gè)參數(shù)是epoll_create()的返回值。
  2. 第二個(gè)參數(shù)表示動(dòng)作。使用三個(gè)宏表示:EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中;EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽事件;EPOLL_CTL_DEL:從epfd中刪除某個(gè)fd。
  3. 第三個(gè)參數(shù)是需要監(jiān)聽的fd。
  4. 第四個(gè)參數(shù)是要告訴內(nèi)核需要監(jiān)聽什么事件,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 event; epoll_data_t data; }events可以是以下的幾個(gè)宏的集合:EPOLLIN:表示對(duì)應(yīng)的文件描述符可讀(包括對(duì)端socket正常關(guān)閉)。EPOLLOUT:表示對(duì)應(yīng)的文件描述符可寫;EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的可讀數(shù)據(jù)(此處應(yīng)該表示有帶外數(shù)據(jù)到來(lái));
    EPOLLERR:表示對(duì)應(yīng)的描述符發(fā)生錯(cuò)誤;EPOLLHUP:標(biāo)識(shí)對(duì)應(yīng)的文件描述符被掛斷;EPOLLET:將EPOLL設(shè)置為邊緣觸發(fā)(Edge Triggered)模式,相對(duì)于水平觸發(fā)。EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,若還需要繼續(xù)監(jiān)聽這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列當(dāng)中。
  1. *int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout);等待事件的發(fā)生,類似于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:立即返回,-1:永久阻塞)。函數(shù)返回需要處理的事件的數(shù)目。若返回0則表示已超時(shí)。

那么究竟如何來(lái)使用epoll呢?其實(shí)非常簡(jiǎn)單。
通過(guò)在包含一個(gè)頭文件#include <sys/epoll.h> 以及幾個(gè)簡(jiǎn)單的API將可以大大的提高你的網(wǎng)絡(luò)服務(wù)器的支持人數(shù)。

首先通過(guò)create_epoll(int maxfds)來(lái)創(chuàng)建一個(gè)epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄,之后的所有操作將通過(guò)這個(gè)句柄來(lái)進(jìn)行操作。在用完之后,記得用close()來(lái)關(guān)閉這個(gè)創(chuàng)建出來(lái)的epoll句柄。

之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來(lái)查詢所有的網(wǎng)絡(luò)接口,看哪一個(gè)可以讀,哪一個(gè)可以寫了。基本的語(yǔ)法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后,epoll_events里面將儲(chǔ)存所有的讀寫事件。max_events是當(dāng)前需要監(jiān)聽的所有socket句柄數(shù)。最后一個(gè)timeout是 epoll_wait的超時(shí),為0的時(shí)候表示馬上返回,為-1的時(shí)候表示一直等下去,直到有事件范圍,為任意正整數(shù)的時(shí)候表示等這么長(zhǎng)的時(shí)間,如果一直沒(méi)有事件,則范圍。一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話,可以用-1來(lái)等,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線程的話,則可以用0來(lái)保證主循環(huán)的效率。

epoll_wait范圍之后應(yīng)該是一個(gè)循環(huán),遍利所有的事件。

幾乎所有的epoll程序都使用下面的框架:

    for( ; ; )
    {
        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); //accept這個(gè)連接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽隊(duì)列中
            }
            else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù),讀socket
            {
                n = read(sockfd, line, MAXLINE)) < 0    //讀
                ev.data.ptr = md;     //md為自定義類型,添加數(shù)據(jù)
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù),異步處理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數(shù)據(jù)
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發(fā)送數(shù)據(jù)
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)
            }
            else
            {
                //其他的處理
            }
        }
    }

關(guān)于ET、LT兩種工作模式
epoll對(duì)文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認(rèn)模式,LT模式與ET模式的區(qū)別如下:
  LT模式:當(dāng)epoll_wait檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序可以不立即處理該事件。下次調(diào)用epoll_wait時(shí),會(huì)再次響應(yīng)應(yīng)用程序并通知此事件。
  ET模式:當(dāng)epoll_wait檢測(cè)到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序必須立即處理該事件。如果不處理,下次調(diào)用epoll_wait時(shí),不會(huì)再次響應(yīng)應(yīng)用程序并通知此事件。
  ET模式在很大程度上減少了epoll事件被重復(fù)觸發(fā)的次數(shù),因此效率要比LT模式高。epoll工作在ET模式的時(shí)候,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫操作把處理多個(gè)文件描述符的任務(wù)餓死。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容