epoll

I/O多路複用技術在比較多的TCP網絡服務器中有使用,即比較多的用到select函數。在linux2.6內核中,有了一種替換它的機制,就是epoll。

一、epoll相關的數據結構和函數

epoll用到的所有函數都是在頭文件sys/epoll.h中聲明的,下面簡要說明所用到的數據結構和函數。

1、數據結構

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 */
};

結構體epoll_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件,其中epoll_data 聯合體用來保存觸發事件的某個文件描述符相關的數據,例如一個client連接到服務器,服務器通過調用accept函數可以得到於這個client對應的socket文件描述符,可以把這文件描述符賦給epoll_data的fd字段以便後面的讀寫操作在這個文件描述符上進行。epoll_event 結構體的events字段是表示感興趣的事件和被觸發的事件可能的取值。events可以是以下幾個宏的集合:

EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的;
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏。

2、函數

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);

該函數用於控制某個文件描述符上的事件,可以註冊事件,修改事件,刪除事件。

參數:epfd:由 epoll_create 生成的epoll專用的文件描述符;

         op:要進行的操作例如註冊事件,可能的取值EPOLL_CTL_ADD 註冊、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 刪除;

         fd:關聯的文件描述符;

         event:指向epoll_event的指針。

返回值:如果調用成功返回0,不成功返回-1

(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表示已超時。

 二、epoll使用方法

這是epoll的man手冊提供的一個例子,這段代碼假設一個非阻塞的socket監聽listener被建立並且一個epoll句柄kdpfd已經提前用epoll_create建立了:

 

[CODE]
       struct epoll_event ev, *events;

       for(;;) {

          nfds = epoll_wait(kdpfd, events, maxevents, -1);/*wait for an I/O event.           

             for(n = 0; n < nfds; ++n) {

                if(events[n].data.fd == listener) {/*if listen socket has an I/O, accept the new connect*/

                   client = accept(listener, (struct sockaddr *) &local,&addrlen);

                   if(client < 0){

                       perror("accept");

                       continue;

                   }

                   setnonblocking(client);

                   ev.events = EPOLLIN | EPOLLET;/*EPOLLIN-available for read*/

                   ev.data.fd = client;

                   if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {/*add the new socket into the epoll file descriptors*/

                       fprintf(stderr, "epoll set insertion error: fd=%d/n",client);

                       return -1;

                   }

               }

               else

                   do_use_fd(events[n].data.fd);/*read from a socket which has data come*/

           }

       }

[/CODE]

下面是一個epoll的完整例子。

#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);

                    printf("connection from %s/n",str);

                    //設置用於讀操作的文件描述符

                    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

                              printf("readline error/n");

                    } 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);

               }


          }


     }

}

三、epoll比select的優點

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之上了。下面用一個生活中的例子解釋這點。

 

假設你在大學中讀書,要等待一個朋友來訪,而這個朋友只知道你在A號樓,但是不知道你具體住在哪裏,於是你們約好了在A號樓門口見面。如果你使用的阻塞IO模型來處理這個問題,那麼你就只能一直守候在A號樓門口等待朋友的到來,在這段時間裏你不能做別的事情,不難知道,這種方式的效率是低下的。現在時代變化了,開始使用多路複用IO模型來處理這個問題.你告訴你的朋友來了A號樓找樓管大媽,讓她告訴你該怎麼走.這裏的樓管大媽扮演的就是多路複用IO的角色。

進一步解釋select和epoll模型的差異。select版大媽做的是如下的事情:比如同學甲的朋友來了,select版大媽比較笨,她帶着朋友挨個房間進行查詢誰是同學甲,你等的朋友來了,於是在實際的代碼中,select版大媽做的是以下的事情:

int n = select(&readset,NULL,NULL,100);

for (int i = 0; n > 0; ++i)
{
   if (FD_ISSET(fdarray[i], &readset))
   {
      do_something(fdarray[i]);
      --n;
   }
}

epoll版大媽就比較先進了,她記下了同學甲的信息,比如說他的房間號,那麼等同學甲的朋友到來時,只需要告訴該朋友同學甲在哪個房間即可,不用自己親自帶着人滿大樓的找人了.於是epoll版大媽做的事情可以用如下的代碼表示:

n=epoll_wait(epfd,events,20,500);
for(i=0;i<n;++i)
{
    do_something(events[n]);
}

在epoll中,關鍵的數據結構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 */ 
};

可以看到,epoll_data是一個union結構體,它就是epoll版大媽用於保存同學信息的結構體,它可以保存很多類型的信息:fd,指針,等等.有了這個結構體,epoll大媽可以不用吹灰之力就可以定位到同學甲。別小看了這些效率的提高,在一個大規模併發的服務器中,輪詢IO是最耗時間的操作之一.再回到那個例子中,如果每到來一個朋友樓管大媽都要全樓的查詢同學,那麼處理的效率必然就低下了,過不久樓底就有不少的人了。

對比最早給出的阻塞IO的處理模型, 可以看到採用了多路複用IO之後, 程序可以自由的進行自己除了IO操作之外的工作, 只有到IO狀態發生變化的時候由多路複用IO進行通知, 然後再採取相應的操作, 而不用一直阻塞等待IO狀態發生變化了。從上面的分析也可以看出,epoll比select的提高實際上是一個用空間換時間思想的具體應用。

四、LT和ET工作方式

1、LT工作模式

LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。

2、ET工作模式

(1)ET (edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。在許多測試中我們會看到如果沒有大量的idle -connection或者dead-connection,epoll的效率並不會比select/poll高很多,但是當我們遇到大量的idle- connection(例如WAN環境中存在大量的慢速連接),就會發現epoll的效率大大高於select/poll。

(2)這裏一直強調的"狀態變化"是什麼:
1)對於監聽可讀事件時,如果是socket是監聽socket,那麼當有新的主動連接到來爲狀態發生變化;對一般的socket而言,協議棧中相應的緩衝區有新的數據爲狀態發生變化。但是,如果在一個時間同時接收了N個連接(N>1),但是監聽socket只accept了一個連接,那麼其它未accept的連接將不會在ET模式下給監聽socket發出通知,此時狀態不發生變化;對於一般的socket,如果對應的緩衝區本身已經有了N字節的數據,而只取出了小於N字節的數據,那麼殘存的數據不會造成狀態發生變化.
2)對於監聽可寫事件時,同理可推,不再詳述.
3)而不論是監聽可讀還是可寫,對方關閉socket連接都將造成狀態發生變化。例如客戶主動中斷了socket連接,那麼都將造成server端發生狀態的變化,從而server得到通知,將已經在本方緩衝區中的數據讀出。

總結如下:僅當對方的動作(發出數據,關閉連接等)造成的事件才能導致狀態發生變化,而本方協議棧中已經處理的事件(包括接收了對方的數 據,接收了對方的主動連接請求)並不是造成狀態發生變化的必要條件,狀態變化一定是對方造成的.所以在ET模式下的,必須一直處理到出錯或者完全處理完畢,才能進行下一個動作,否則可能會發生錯誤。

(3)另外,當使用epoll的ET模型來工作時,當產生了一個EPOLLIN事件後,讀數據的時候需要考慮的是當recv()返回的大小如果等於請求的大小,那麼很有可能是緩衝區還有數據未讀完,也意味着該次事件還沒有處理完,所以還需要再次讀取:
while(rs)
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
    // 由於是非阻塞的模式,所以當errno爲EAGAIN時,表示當前緩衝區已無數據可讀
    // 在這裏就當作是該次事件已處理處.
    if(errno == EAGAIN)
     break;
    else
     return;
   }
   else if(buflen == 0)
   {
     // 這裏表示對端的socket已正常關閉.
   }
   if(buflen == sizeof(buf)
     rs = 1;   // 需要再次讀取
   else
     rs = 0;
}

3、用實驗說明LT和ET工作方式的區別

首先給出server的代碼,需要說明的是每次accept的連接,加入可讀集的時候採用的都是ET模式,而且接收緩衝區是5字節的,也就是每次只接收5字節的數據:

#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>
#include <errno.h>

using namespace std;

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#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;
    //ev.events=EPOLLIN;
    //註冊epoll事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));
    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);

                printf("Accepct connection from %s/n",str);
                //設置用於讀操作的文件描述符
                ev.data.fd=connfd;
                //設置用於注測的讀操作事件
                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN;
                //註冊ev
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN)
            {
                printf("EPOLLIN/n");
                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
                        printf("read error/n");

                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                line[n] = '/0';
                printf("read :%s/n" ,line);
             }
         }
    }
    return 0;
}
下面給出測試所用的Perl寫的client端,在client中發送10字節的數據,同時讓client在發送完數據之後進入死循環, 也就是在發送完之後連接的狀態不發生改變--既不再發送數據, 也不關閉連接,這樣才能觀察出server的狀態:
#!/usr/bin/perl

use IO::Socket;

my $host = "127.0.0.1";
my $port = 5000;

my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
my $msg_out = "1234567890";
print $socket $msg_out;
print "now send over, go to sleep/n";

while (1)
{
    sleep(1);
}
運行server和client發現,server僅僅讀取了5字節的數據,而client其實發送了10字節的數據,也就是說,server僅當第一次監聽到了EPOLLIN事件,由於沒有讀取完數據,而且採用的是ET模式,狀態在此之後不發生變化,因此server再也接收不到EPOLLIN事件了.

如果我們把client改爲這樣:
#!/usr/bin/perl

use IO::Socket;

my $host = "127.0.0.1";
my $port = 5000;

my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
my $msg_out = "1234567890";
print $socket $msg_out;
print "now send over, go to sleep/n";
sleep(5);
print "5 second gonesend another line/n";
print $socket $msg_out;

while (1)
{
    sleep(1);
}

可以發現,在server接收完5字節的數據之後一直監聽不到client的事件,而當client休眠5秒之後重新發送數據,server再次監聽到了變化,只不過因爲只是讀取了5個字節,仍然有10個字節的數據(client第二次發送的數據)沒有接收完.

如果上面的實驗中,對accept的socket都採用的是LT模式,那麼只要還有數據留在buffer中,server就會繼續得到通知,讀者可以自行改動代碼進行實驗.

基於這兩個實驗,可以得出這樣的結論:ET模式僅當狀態發生變化的時候才獲得通知,這裏所謂的狀態的變化並不包括緩衝區中還有未處理的數據,也就是說,如果要採用ET模式,需要一直read/write直到出錯爲止,很多人反映爲什麼採用ET模式只接收了一部分數據就再也得不到通知了,大多因爲這樣;而LT模式是只要有數據沒有處理就會一直通知下去的.

 

 

 

參考資料:

1.http://hi.baidu.com/scrich99/blog/item/4c6b013f4a47d40bbba16731.html

2.http://hi.baidu.com/scrich99/blog/item/43379d391d2ccce114cecb37.html

3.http://hi.baidu.com/scrich99/blog/item/87b3a42baa6fe5f598250a30.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章