1. 程式人生 > >Epoll簡介以及例子

Epoll簡介以及例子

第一部分:Epoll簡介

問題 :  Select,Poll和Epoll的區別

答案 

Epoll和Select的區別


1. 遍歷方式的區別。select判斷是否有事件發生是遍歷的,而epoll是事件響應的,一旦控制代碼上有事件來了,就馬上選出來。

2. 數目的區別。select一般由一個核心引數(1024)限制了監聽的控制代碼數,但是epoll通常受限於開啟檔案的數目,通常會打得多。

3. epoll自身,還有兩種觸發方式。水平觸發和邊緣觸發。邊沿觸發的效率更高(高了不少,但是程式設計的時候要小心處理每個時間,防止漏掉處理某些事件)。

Select

select()系統呼叫提供一個機制來實現同步多元I/O:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);

呼叫select()將阻塞,直到指定的檔案描述符準備好執行I/O,或者可選引數timeout指定的時間已經過去。

select()成功返回時,每組set都被修改以使它只包含準備好I/O的檔案描述符。例如,假設有兩個檔案描述符,值分別是7和9,被放在readfds中。當select()返回時,如果7仍然在set中,則這個檔案描述符已經準備好被讀取而不會阻塞。如果9已經不在set中,則讀取它將可能會阻塞(我說可能是因為資料可能正好在select返回後就可用,這種情況下,下一次呼叫select()將返回檔案描述符準備好讀取)。

第一個引數n,等於所有set中最大的那個檔案描述符的值1 select()返回時,timeout引數的狀態在不同的系統中是未定義的,因此每次呼叫select()之前必須重新初始化timeout
和檔案描述符set。實際上,當前版本的Linux會自動修改timeout引數,設定它的值為剩餘時間。因此,如果timeout被設定為5秒,然後在檔案描述符準備好之前經過了3秒,則這一次呼叫select()返回時tv_sec將變為2
因為檔案描述符set是靜態建立的,它們對檔案描述符的最大數目強加了一個限制,能夠放進set中的最大檔案描述符的值由FD_SETSIZE指定。在Linux中,這個值是1024。本章後面我們還將看到這個限制的衍生物。 返回值和錯誤程式碼select() 成功時返回準備好I/O的檔案描述符數目,包括所有三個set。如果提供了timeout,返回值可能是0;錯誤時返回-1,並且設定errno為下面幾個值之一:EBADF: 給某個set提供了無效檔案描述符。EINTR::等待時捕獲到訊號,可以重新發起呼叫。EINVAL::引數n為負數,或者指定的timeout非法。ENOMEM::不夠可用記憶體來完成請求。

Poll

select()不一樣,poll()沒有使用低效的三個基於位的檔案描述符set,而是採用了一個單獨的結構體pollfd陣列,由fds指標指向這個組pollfd結構體定義如下:

#include <sys/poll.h>

int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

每一個pollfd結構體指定了一個被監視的檔案描述符,可以傳遞多個結構體,指示poll()監視多個檔案描述符。每個結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。revents域是檔案描述符的操作結果事件掩碼。核心在呼叫返回時設定這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:

POLLIN:有資料可讀。POLLRDNORM:有普通資料可讀。POLLRDBAND:有優先資料可讀。POLLPRI:有緊迫資料可讀。POLLOUT:寫資料不會導致阻塞。POLLWRNORM:寫普通資料不會導致阻塞。POLLWRBAND:寫優先資料不會導致阻塞。POLLMSG:SIGPOLL訊息可用。此外,revents域中還可能返回下列事件:POLLER:指定的檔案描述符發生錯誤。POLLHUP:指定的檔案描述符掛起事件。POLLNVAL:指定的檔案描述符非法。這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。使用poll()select()不一樣,你不需要顯式地請求異常情況報告。

POLLIN | POLLPRI等價於select()的讀事件,POLLOUT | POLLWRBAND等價於select()的寫事件。POLLIN等價於POLLRDNORM | POLLRDBAND,而POLLOUT則等價於POLLWRNORM例如,要同時監視一個檔案描述符是否可讀和可寫,我們可以設定eventsPOLLIN | POLLOUT。在poll返回時,我們可以檢查revents中的標誌,對應於檔案描述符請求的events結構體。如果POLLIN事件被設定,則檔案描述符可以被讀取而不阻塞。如果POLLOUT被設定,則檔案描述符可以寫入而不導致阻塞。這些標誌並不是互斥的:它們可能被同時設定,表示這個檔案描述符的讀取和寫入操作都會正常返回而不阻塞。timeout引數指定等待的毫秒數,無論I/O是否準備好,poll都會返回。timeout指定為負數值表示無限超時;timeout0指示poll呼叫立即返回並列出準備好I/O的檔案描述符,但並不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。返回值和錯誤程式碼成功時,poll()返回結構體中revents域不為0的檔案描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,並設定errno為下列值之一:EBADF:一個或多個結構體中指定的檔案描述符無效。EFAULT:fds指標指向的地址超出程序的地址空間。EINTR:請求的事件之前產生一個訊號,呼叫可以重新發起。EINVAL:nfds引數超出PLIMIT_NOFILE值。ENOMEM:可用記憶體不足,無法完成請求。

Epoll

Epoll的優點:1.支援一個程序開啟大數目的socket描述符(FD)    select 最不能忍受的是一個程序所開啟的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對於那些需要支援的上萬連線數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個巨集然後重新編譯核心,不過資料也同時指出這樣會帶來網路效率的下降,二是可以選擇多程序的解決方案(傳統的 Apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上執行緒間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。

2.IO效率不隨FD數目增加而線性下降傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由於網路延時,任一時間只有部分的socket"活躍"的,但是select/poll每次呼叫都會線性掃描全部的集合,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對"活躍"socket進行操作---這是因為在核心實現中epoll是根據每個fd上面的callback函式實現的。那麼,只有"活躍"socket才會主動的去呼叫 callback函式,其他idle狀態socket則不會,在這點上,epoll實現了一個""AIO,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。

3.使用mmap加速核心與使用者空間的訊息傳遞。這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要核心把FD訊息通知給使用者空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心於使用者空間mmap同一塊記憶體實現的。而如果你想我一樣從2.5核心就關注epoll的話,一定不會忘記手工 mmap這一步的。

Epoll簡介:

linux的網路程式設計中,很長的時間都在使用select來做事件觸發。在linux新的核心中,有了一種替換它的機制,就是epoll相比於selectepoll最大的好處在於它不會隨著監聽fd數目的增長而降低效率。因為在核心中的select實現中,它是採用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。epoll的介面非常簡單,一共就三個函式:1. int epoll_create(int size);建立一個epoll的控制代碼,size用來告訴核心這個監聽的數目一共有多大。這個引數不同於select()中的第一個引數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll控制代碼後,它就是會佔用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件註冊函式,它不同與select()是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。第一個引數是epoll_create()[上面一個函式]的返回值,第二個引數表示動作,用三個巨集來表示:EPOLL_CTL_ADD:註冊新的fdepfd中;EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;EPOLL_CTL_DEL:從epfd中刪除一個fd第三個引數是需要監聽的fd,第四個引數是告訴核心需要監聽什麼事,struct epoll_event結構如下:struct epoll_event {  __uint32_t events;  /* Epoll events */  epoll_data_t data;  /* User data variable */};events可以是以下幾個巨集的集合:EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);EPOLLOUT:表示對應的檔案描述符可以寫;EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);EPOLLERR:表示對應的檔案描述符發生錯誤;EPOLLHUP:表示對應的檔案描述符被結束通話;EPOLLETEPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的產生,類似於select()呼叫。引數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,引數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函式返回需要處理的事件數目,如返回0表示已超時。
令人高興的是,2.6核心的epoll比其2.5開發版本的/dev/epoll簡潔了許多,所以,大部分情況下,強大的東西往往是簡單的。唯一有點麻煩是epoll有2種工作方式: LT和ET(水平觸發和邊緣觸發)   LT(level triggered)是預設的工作方式,並且同時支援block和no-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。   ET (edge-triggered)是高速工作方式,只支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,直到你做了某些操作導致那個檔案描述符不再為就緒狀態了(比如,你在傳送,接收或者接收請求,或者傳送接收的資料少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),核心不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。

第二部分:Epoll的三個例子

epoll用到的所有函式都是在標頭檔案sys/epoll.h中宣告的,下面簡要說明所用到的資料結構和函式: 所用到的資料結構
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 */
};
* This source code was highlighted by YcdoiT. ( style: Vs )
結構體epoll_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件,其中epoll_data 聯合體用來儲存觸發事件的某個檔案描述符相關的資料,例如一個client連線到伺服器,伺服器通過呼叫accept函式可以得到於這個client對應的socket檔案描述符,可以把這檔案描述符賦給epoll_data的fd欄位以便後面的讀寫操作在這個檔案描述符上進行。epoll_event 結構體的events欄位是表示感興趣的事件和被觸發的事件可能的取值為:EPOLLIN :表示對應的檔案描述符可以讀; EPOLLOUT:表示對應的檔案描述符可以寫; EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來); EPOLLERR:表示對應的檔案描述符發生錯誤; EPOLLHUP:表示對應的檔案描述符被結束通話; EPOLLET:表示對應的檔案描述符有事件發生; 所用到的函式: 1、epoll_create函式      函式宣告:int epoll_create(int size)      該函式生成一個epoll專用的檔案描述符,其中的引數是指定生成描述符的最大範圍(我覺得這個引數和select函式的第一個引數應該是類似的但是該怎麼設定才好,我也不太清楚)。 2、epoll_ctl函式      函式宣告:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)      該函式用於控制某個檔案描述符上的事件,可以註冊事件,修改事件,刪除事件。     引數:epfd:由 epoll_create 生成的epoll專用的檔案描述符;                 op:要進行的操作例如註冊事件,可能的取值:                         EPOLL_CTL_ADD 註冊;                         EPOLL_CTL_MOD 修改;                         EPOLL_CTL_DEL 刪除                 fd:關聯的檔案描述符;                 event:指向epoll_event的指標;     如果呼叫成功返回0,不成功返回-1 3、epoll_wait函式     函式宣告:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
該函式用於輪詢I/O事件的發生; 引數: epfd:由epoll_create 生成的epoll專用的檔案描述符; epoll_event:用於回傳代處理事件的陣列; maxevents:每次能處理的事件數; timeout:等待I/O事件發生的超時值; 返回發生事件數。
例子1
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#define INFTIM 1000

void setnonblocking(int sock)
{
    int opts;
    opts = fcntl(sock, F_GETFL);
    if(opts < 0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts | O_NONBLOCK;
    if(fcntl(sock, F_SETFL, opts) < 0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}

int main()
{
    int i, maxi, listenfd, connfd, sockfd, epfd, nfds;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;
    //宣告epoll_event結構體的變數,ev用於註冊事件,陣列用於回傳要處理的事件
    struct epoll_event ev, events[20];
    //生成用於處理accept的epoll專用的檔案描述符
    epfd = epoll_create(256);

    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    //把socket設定為非阻塞方式
    setnonblocking(listenfd);
    //設定與要處理的事件相關的檔案描述符
    ev.data.fd = listenfd;
    //設定要處理的事件型別
    ev.events = EPOLLIN | EPOLLET;
    //註冊epoll事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;

    char *local_addr = "200.200.200.204";
    inet_aton(local_addr, &(serveraddr.sin_addr)); //htons(SERV_PORT);
    serveraddr.sin_port = htons(SERV_PORT);
    bind(listenfd, (sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);

    maxi = 0;
    for ( ; ; )
    {
        //等待epoll事件的發生
        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);
                if(connfd < 0)
                {
                    perror("connfd<0");
                    exit(1);
                }
                setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);
                std::cout << "connect from " < _u115 ? tr << std::endl;
                //設定用於讀操作的檔案描述符
                ev.data.fd = connfd;
                //設定用於注測的讀操作事件
                ev.events = EPOLLIN | EPOLLET;
                //註冊ev
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }
            else if(events[i].events & EPOLLIN)
            {
                if ( (sockfd = events[i].data.fd) < 0) continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0)
                {
                    if (errno == ECONNRESET)
                    {

                        close(sockfd);
                        events[i].data.fd = -1;
                    }
                    else
                        std::cout << "readline error" << std::endl;
                }
                else if (n == 0)
                {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                //設定用於寫操作的檔案描述符
                ev.data.fd = sockfd;
                //設定用於注測的寫操作事件
                ev.events = EPOLLOUT | EPOLLET;
                //修改sockfd上要處理的事件為EPOLLOUT
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
            }
            else if(events[i].events & EPOLLOUT)
            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //設定用於讀操作的檔案描述符
                ev.data.fd = sockfd;
                //設定用於注測的讀操作事件
                ev.events = EPOLLIN | EPOLLET;
                //修改sockfd上要處理的事件為EPOLIN
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
            }

        }

    }
}
* This source code was highlighted by YcdoiT. ( style: Vs )
例子2

/*

*\ 伺服器端的原始碼

*/

#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <iostream>
#include <signal.h>
#include <sys/epoll.h>

#define MAXFDS 256
#define EVENTS 100
#define PORT 8888

int epfd;
bool setNonBlock(int fd)
{
    int flags = fcntl(fd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    if(-1 == fcntl(fd, F_SETFL, flags))
        return false;
    return true;
}

int main(int argc, char *argv[], char *evp[])
{
    int fd, nfds, confd;
    int on = 1;
    char *buffer[512];
    struct sockaddr_in saddr, caddr;
    struct epoll_event ev, events[EVENTS];

    if(-1 == socket(AF_INET, SOCKSTREAM), 0)
    {
        std::cout << "建立套接字出錯啦" << std::endl;
        return -1;
    }


    struct sigaction sig;
    sigemptyset(&sig.sa_mask);
    sig_handler = SIG_IGN;
    sigaction(SIGPIPE, &N > sig, NULL);

    epfd = epoll_create(MAXFDS);

    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons((short)(PORT));
    saddr.sin_addr.s_addr = INADDR_ANY;
    if(-1 == bind(fd, (struct sockaddr *)&saddr, sizeof(saddr)))
    {
        std::cout << "套接字不能繫結到伺服器上" << std::endl;
        return -1;
    }

    if(-1 == listen(fd, 32))
    {
        std::cout << "監聽套接字的時候出錯了" << std::endl;
        return -1;
    }

    ev.data.fd = fd;
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

    while(true)
    {
        nfds = epoll_wait(epfd, &events, MAXFDS, 0);

        for(int i = 0; i < nfds; ++ i)
        {
            if(fd == events[i].data.fd)
            {
                memset(&caddr, sizeof(caddr));
                cfd = accept(fd, (struct sockaddr *)&caddr, &sizeof(caddr));
                if(-1 == cfd)
                {
                    std::cout << "伺服器接收套接字的時候出問題了" << std::endl;
                    break;
                }

                setNonBlock(cfd);

                ev.data.fd = cfd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
            }
            else if(events[i].data.fd & EPOLLIN)
            {
                bzero(&buffer, sizeof(buffer));
                std::cout << "伺服器端要讀取客戶端發過來的訊息" << std::endl;
                ret = recv(events[i].data.fd, buffer, sizeof(buffer), 0);
                if(ret < 0)
                {
                    std::cout << "伺服器收到的訊息出錯了" << endl;
                    return -1;
                }
                std::cout << "接收到的訊息為:" << (char *) buffer << std::endl;
                ev.data.fd = events[i].data.fd;
                ev.events = EPOLLOUT;
                epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &ev);
            }
            else if(events[i].data.fd & EPOLLOUT)
            {
                bzero(&buffer, sizeof(buffer));
                bcopy("The [email protected][email protected]", buffer, sizeof("The [email protected][email protected]"));
                ret = send(events[i].data.fd, buffer, strlen(buffer));
                if(ret < 0)
                {
                    std::cout << "伺服器傳送訊息給客戶端的時候出錯啦" << std::endl;
                    return -1;
                }
                ev.data.fd = events[i].data.fd;
                epoll_ctl(epfd, EPOL