EPOLL爲我們帶來了什麼

Q:網絡服務器的瓶頸在哪?
A:IO效率。

在大家苦苦的爲在線人數的增長而導致的系統資源吃緊上的問題正在發愁的時候,Linux 2.6內核中提供的System Epoll爲我們提供了一套完美的解決方案。傳統的select以及poll的效率會因爲在線人數的線形遞增而導致呈二次乃至三次方的下降,這些直接導致了網絡服務器可以支持的人數有了個比較明顯的限制。

自從Linux提供了/dev/epoll的設備以及後來2.6內核中對/dev/epoll設備的訪問的封裝(System Epoll)之後,這種現象得到了大大的緩解,如果說幾個月前,大家還對epoll不熟悉,那麼現在來說的話,epoll的應用已經得到了大範圍的普及。

那麼究竟如何來使用epoll呢?其實非常簡單。
通過在包含一個頭文件#include 以及幾個簡單的API將可以大大的提高你的網絡服務器的支持人數。

首先通過create_epoll(int maxfds)來創建一個epoll的句柄,其中maxfds爲你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之後的所有操作將通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll句柄。

之後在你的網絡主循環裏面,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的語法爲:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd爲用epoll_create創建之後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裏面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket句柄數。最後一個timeout是epoll_wait的超時,爲0的時候表示馬上返回,爲-1的時候表示一直等下去,直到有事件範圍,爲任意正整數的時候表示等這麼長的時間,如果一直沒有事件,則範圍。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。

epoll_wait範圍之後應該是一個循環,遍利所有的事件:
for(n = 0; n < nfds; ++n) {
               if(events[n].data.fd == listener) { //如果是主socket的事件的話,則表示有新連接進入了,進行新連接的處理。
                   client = accept(listener, (struct sockaddr *) &local,
                                   &addrlen);
                   if(client < 0){
                       perror("accept");
                       continue;
                   }
                   setnonblocking(client); // 將新連接置於非阻塞模式
                   ev.events = EPOLLIN | EPOLLET; // 並且將新連接也加入EPOLL的監聽隊列。
注意,這裏的參數EPOLLIN | EPOLLET並沒有設置對寫socket的監聽,如果有寫操作的話,這個時候epoll是不會返回事件的,如果要對寫操作也監聽的話,應該是EPOLLIN | EPOLLOUT | EPOLLET
                   ev.data.fd = client;
                   if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
// 設置好event之後,將這個新的event通過epoll_ctl加入到epoll的監聽隊列裏面,這裏用EPOLL_CTL_ADD來加一個新的epoll事件,通過EPOLL_CTL_DEL來減少一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。
                       fprintf(stderr, "epoll set insertion error: fd=%d0,
                               client);
                       return -1;
                   }
               }
               else // 如果不是主socket的事件的話,則代表是一個用戶socket的事件,則來處理這個用戶socket的事情,比如說read(fd,xxx)之類的,或者一些其他的處理。
                   do_use_fd(events[n].data.fd);
}

對,epoll的操作就這麼簡單,總共不過4個API:epoll_create, epoll_ctl, epoll_wait和close。
如果您對epoll的效率還不太瞭解,請參考我之前關於網絡遊戲的網絡編程等相關的文章。

世界變了,原來擔心的問題,現在已經不是問題了。

epoll與iocp的異同之處

目前國內的網遊研發,在服務器使用的開發平臺方面,win和linux的比例各佔多少,我一時半會也沒有準確數據,但從我瞭解的這麼多公司情況來看,用win系統的還是比較多一點,這些企業一般都是比較單純的網遊公司,而用linux的則多數是一些傳統的互聯網公司,比如網易和騰訊。

網遊服務器用win還是linux,向來都是大家關注的話題。我想,原因可能很多,但此處不想過多論述這個問題,爲避免多費口舌,我還是明確表明一下自己的觀點:我是推薦用linux作開發的,雖然我也是剛轉來作linux平臺下的開發。

那麼,說具體一點。但凡作過比較深入的網絡編程的人,都會知道,在win平臺下,高效的IO模型是IOCP,而在linux底下則是epoll。那麼,epoll與iocp之間到底有哪些異同之處呢?

首先,我們看一下它們相同的地方。

兩者都是處理異步IO的高效模型,這種高效,除了“異步處理”這個共同的特徵之外,二者都可以通過指針攜帶應用層數據:在IOCP裏,應用層數據可以通過單句柄數據和單IO數據來與IOCP底層通信;而在epoll裏,可以通過epoll_data裏的"void *ptr"來傳遞。這是一種很重要的思想,也是它們高效的原因所在:當事件的通知到來時,它不僅告訴你發生了什麼樣的事件,還同時告訴這次事件所操作的數據是哪些。

那麼,epoll和iocp到底又有什麼不同呢?

以我目前粗淺的使用經驗來看,至少可以得到以下結論:

1.iocp是在IO操作完成之後,才通過get函數返回這個完成通知的;而epoll則不是在IO操作完成之後才通知你,它的工作原理是,你如果想進行IO操作時,先向epoll查詢是否可讀或可寫,如果處於可讀或可寫狀態後,epoll會通過epoll_wait函數通知你,此時你再進行進一步的recv或send操作。

2.在1的基礎上,我們其實可以看到,epoll僅僅是一個異步事件的通知機制,其本身並不作任何的IO讀寫操作,它只負責告訴你是不是可以讀或可以寫了,而具體的讀寫操作,還要應用層自己來作;但iocp的封裝就要多一些,它不僅會有完成之後的事件通知,更重要的是,它同時封裝了一部分的IO控制邏輯。從這一點上來看,iocp的封裝似乎更全面一點,但是,換個角度看,epoll僅提供這種機制也是非常好的,它保持了事件通知與IO操作之間彼此的獨立性,使得epoll的使用更加靈活。

這只是我初步使用epoll開發過程中的體會,以後有更深的體會時還會發上來跟大家分享。

linux 2.6內核epoll用法舉例說明

linux 2.6內核epoll用法舉例說明(zz from www.csdn.net)
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 */
        };
結構體epoll_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件,其中epoll_data 聯合體用來保存觸發事件的某個文件描述符相關的數據,例如一個client連接到服務器,服務器通過調用accept函數可以得到於這個client對應的socket文件描述符,可以把這文件描述符賦給epoll_data的fd字段以便後面的讀寫操作在這個文件描述符上進行。epoll_event 結構體的events字段是表示感興趣的事件和被觸發的事件可能的取值爲:EPOLLIN :表示對應的文件描述符可以讀;
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(我不太明白是什麼意思,可能是類似client關閉  socket連接這樣的事件);
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事件發生的超時值;
返回發生事件數。
例子:



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

     //生成用於處理acceptepoll專用的文件描述符

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

               }

 

          }

 

     }

}

 

 

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