C++ - 網絡編程模型 - Linux EPOLL


1.簡介
 
   Linux I/O多路複用技術在比較多的TCP網絡服務器中有使用,即比較多的用到select函數。Linux 2.6內核中有提高網絡I/O性能的新方法,即epoll 。 epoll是什麼?按照man手冊的說法是爲處理大批量句柄而作了改進的poll。要使用epoll只需要以下的三個系統函數調用:  epoll_create(2),epoll_ctl(2),epoll_wait(2)。
 

 

2.select模型的缺陷
 
   (1) 在Linux內核中,select所用到的FD_SET是有限的
 
   內核中有個參數__FD_SETSIZE定義了每個FD_SET的句柄個數:#define __FD_SETSIZE 1024。也就是說,如果想要同時檢測1025個句柄的可讀狀態是不可能用select實現的;或者同時檢測1025個句柄的可寫狀態也是不可能的。
 
   (2) 內核中實現select是使用輪詢方法
 
   每次檢測都會遍歷所有FD_SET中的句柄,顯然select函數的執行時間與FD_SET中句柄的個數有一個比例關係,即select要檢測的句柄數越多就會越費時
 

 

3.Windows IOCP模型的缺陷
 
    windows完成端口實現的AIO,實際上也只是使用內部用線程池實現的,最後的結果是IO有個線程池,你的應用程序也需要一個線程池。很多文檔其實已經指出了這引發的線程context-switch所帶來的代價。
 

 

4.EPOLL模型的優點
 
   (1) 支持一個進程打開大數目的socket描述符(FD)
 
   epoll沒有select模型中的限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於select 所支持的2048
 
   (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加速內核與用戶空間的消息傳遞
 
   無論是select,poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝就顯得很重要。在這點上,epoll是通過內核於用戶空間mmap同一塊內存實現。
 

 

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

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

 

6.EPOLL模型的使用方法
 
   epoll用到的所有函數都是在頭文件sys/epoll.h中聲明的,下面簡要說明所用到的數據結構和函數:
 
   (1) epoll_data、epoll_data_t、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_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件。epoll_event 結構體的events字段是表示感興趣的事件和被觸發的事件,可能的取值爲:
 
   EPOLLIN:      表示對應的文件描述符可以讀;
 
   EPOLLOUT:     表示對應的文件描述符可以寫;
 
   EPOLLPRI:     表示對應的文件描述符有緊急的數據可讀;
 
   EPOLLERR:     表示對應的文件描述符發生錯誤;
 
   EPOLLHUP:     表示對應的文件描述符被掛斷;
 
   EPOLLET:      表示對應的文件描述符有事件發生;
 
 
 
   聯合體epoll_data用來保存觸發事件的某個文件描述符相關的數據。例如一個client連接到服務器,服務器通過調用accept函數可以得到於這個client對應的socket文件描述符,可以把這文件描述符賦給epoll_data的fd字段,以便後面的讀寫操作在這個文件描述符上進行。
 
 
 
   (2)epoll_create
 
   函數聲明:intepoll_create(intsize)
 
   函數說明:該函數生成一個epoll專用的文件描述符,其中的參數是指定生成描述符的最大範圍。
 
 
 
   (3) epoll_ctl函數
 
   函數聲明:intepoll_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。
 
 
 
   (4) epoll_wait函數
 
   函數聲明:int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout)
 
   函數說明:該函數用於輪詢I/O事件的發生。
 
   epfd:由epoll_create 生成的epoll專用的文件描述符;
 
   epoll_event:用於回傳代處理事件的數組;
 
   maxevents:每次能處理的事件數;
 
   timeout:等待I/O事件發生的超時值;
 
   返回發生事件數。
 
 
 
   設計思路:
 
   首先通過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函數操作成功之後,events裏面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket句柄數。最後一個timeout參數指示 epoll_wait的超時條件,爲0時表示馬上返回;爲-1時表示函數會一直等下去直到有事件返回;爲任意正整數時表示等這麼長的時間,如果一直沒有事件,則會返回。一般情況下如果網絡主循環是單線程的話,可以用-1來等待,這樣可以保證一些效率,如果是和主循環在同一個線程的話,則可以用0來保證主循環的效率。epoll_wait返回之後,應該進入一個循環,以便遍歷所有的事件。
 
對epoll 的操作就這麼簡單,總共不過4個API:epoll_create, epoll_ctl,epoll_wait和close。以下是man中的一個例子。
 

 

struct epoll_event ev, *events;
 
for(;;)
 
{
 
   nfds = epoll_wait(kdpfd, events, maxevents, -1);    //等待IO事件
 
   for(n = 0; n < nfds; ++n)
 
   {
 
   //如果是主socket的事件,則表示有新連接進入,需要進行新連接的處理。
 
      if(events[n].data.fd == listener)
 
      {
 
         client = accept(listener, (struct sockaddr *) &local,  &addrlen);
 
if(client < 0)
 
         {
 
            perror("accept error");
 
            continue;
 
         }
 
         // 將新連接置於非阻塞模式
 
         setnonblocking(client);
 
         ev.events = EPOLLIN | EPOLLET;
 
         //注意這裏的參數EPOLLIN | EPOLLET並沒有設置對寫socket的監聽,
 
         //如果有寫操作的話,這個時候epoll是不會返回事件的,
 
         //如果要對寫操作也監聽的話,應該是EPOLLIN | EPOLLOUT | EPOLLET。
 
         // 並且將新連接也加入EPOLL的監聽隊列
 
         ev.data.fd = client;
 
         // 設置好event之後,將這個新的event通過epoll_ctl

         if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)
 
         {
 
            //加入到epoll的監聽隊列裏,這裏用EPOLL_CTL_ADD
 
            //來加一個新的 epoll事件。可以通過EPOLL_CTL_DEL來減少
 
            //一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。
 
            fprintf(stderr, "epoll set insertion error: fd=%d"0, client);
 
            return -1;
 
         }
 
      }
 
      else
 
      // 如果不是主socket的事件的話,則代表這是一個用戶的socket的事件,
 
      // 則用來處理這個用戶的socket的事情是,比如說read(fd,xxx)之類,或者一些其他的處理。
 
         do_use_fd(events[n].data.fd);
 
   }
 
}
 

 


7.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用於註冊事件, events數組用於回傳要處理的事件
 
   struct epoll_event ev,events[20];
 
   //生成用於處理accept的epoll專用的文件描述符, 指定生成描述符的最大範圍爲256
 
   epfd = epoll_create(256);
 
   struct sockaddr_in clientaddr;
 
   struct sockaddr_in serveraddr;
 
   listenfd = socket(AF_INET, SOCK_STREAM, 0);
 
 
 
   setnonblocking(listenfd);       //把用於監聽的socket設置爲非阻塞方式
 
   ev.data.fd = listenfd;          //設置與要處理的事件相關的文件描述符
 
   ev.events = EPOLLIN | EPOLLET;  //設置要處理的事件類型
 
   epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);     //註冊epoll事件
 
   bzero(&serveraddr, sizeof(serveraddr));
 
   serveraddr.sin_family = AF_INET;
 
   char *local_addr = "200.200.200.204";
 
   inet_aton(local_addr, &(serveraddr.sin_addr));
 
   serveraddr.sin_port = htons(SERV_PORT);  //或者htons(SERV_PORT);
 
   bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
 
   listen(listenfd, LISTENQ);
 

 

   maxi = 0;
 
   for( ; ; )
 
   {
 
      nfds = epoll_wait(epfd, events, 20, 500); //等待epoll事件的發生
 
      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);           //把客戶端的socket設置爲非阻塞方式
 
            char *str = inet_ntoa(clientaddr.sin_addr);
 
            std::cout << "connect from " << str  <<std::endl;
 
            ev.data.fd=connfd;                //設置用於讀操作的文件描述符
 
            ev.events=EPOLLIN | EPOLLET;      //設置用於注測的讀操作事件
 
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
 
            //註冊ev事件
 
         }
 
         else if(events[i].events&EPOLLIN)      //讀事件
 
         {
 
            if ( (sockfd = events[i].data.fd) < 0)
 
            {
 
               continue;
 
            }
 
            if ( (n = read(sockfd, line, MAXLINE)) < 0) // 這裏和IOCP不同
 
            {
 
               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);
 
         }
 
      }
 
   }
 
}
 

 


8.EPOLL進階思考
 
8.1. 問題來源
 
   最近學習EPOLL模型,介紹中說將EPOLL與Windows IOCP模型進行比較,說其的優勢在於解決了IOCP模型大量線程上下文切換的開銷,於是可以看出,EPOLL模型不需要多線程,即單線程中可以處理EPOLL邏輯。如果引入多線程反而會引起一些問題。但是EPOLL模型的服務器端到底可以不可以用多線程技術,如果可以,改怎麼取捨,這成了困擾我的問題。上網查了一下,有這樣幾種聲音:
 
   (1) “要麼事件驅動(如epoll),要麼多線程,要麼多進程,把這幾個綜合起來使用,感覺更加麻煩。”;
 
   (2) “單線程使用epoll,但是不能發揮多核;多線程不用epoll。”;
 
   (3) “主通信線程使用epoll所有需要監控的FD,有事件交給多線程去處理”;
 
   (4) “既然用了epoll, 那麼線程就不應該看到fd, 而只看到的是一個一個的業務請求/響應; epoll將網絡數據組裝成業務數據後, 轉交給業務線程進行處理。這就是常說的半同步半異步”。
 
   我比較贊同上述(3)、(4)中的觀點
 
   EPOLLOUT只有在緩衝區已經滿了,不可以發送了,過了一會兒緩衝區中有空間了,就會觸發EPOLLOUT,而且只觸發一次。如果你編寫的程序的網絡IO不大,一次寫入的數據不多的時候,通常都是epoll_wait立刻就會觸發 EPOLLOUT;如果你不調用 epoll,直接寫 socket,那麼情況就取決於這個socket的緩衝區是不是足夠了。如果緩衝區足夠,那麼寫就成功。如果緩衝區不足,那麼取決你的socket是不是阻塞的,要麼阻塞到寫完成,要麼出錯返回。所以EPOLLOUT事件具有較大的隨機性,ET模式一般只用於EPOLLIN, 很少用於EPOLLOUT。
 
8.2. 具體做法
 
   (1) 主通信線程使用epoll所有需要監控的FD,負責監控listenfd和connfd,這裏只監聽EPOLLIN事件,不監聽EPOLLOUT事件;
 
   (2) 一旦從Client收到了數據以後,將其構造成一個消息,放入消息隊列中;
 
   (3) 若干工作線程競爭,從消息隊列中取出消息並進行處理,然後把處理結果發送給客戶端。發送客戶端的操作由工作線程完成。直接進行write。write到EAGAIN或EWOULDBLOCK後,線程循環continue等待緩衝區隊列
 
發送函數代碼如下:
 

 

bool send_data(int connfd, char *pbuffer, unsigned int &len,int flag)
 
{
 
   if ((connfd < 0) || (0  == pbuffer))
 
   {
 
      return false;
 
   }
 
  
 
   int result = 0;
 
   int remain_size = (int) len;
 
   int send_size = 0;
 
   const char *p = pbuffer;
 

 

   time_t start_time = time(NULL);
 
   int time_out = 3;
 

 

   do
 
   {
 
      if (time(NULL) > start + time_out)
 
      {
 
         return false;
 
      }
 

 

      send_size = send(connfd, p, remain_size, flag);
 
      if (nSentSize < 0)
 
      {
 
         if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR))
 
         {
 
            continue;
 
         }
 
         else
 
         {
 
            len -= remain_size;
 
            return false;
 
         }
 
      }
 
 
 
      p += send_size;
 
      remain_size -= send_size;
 
   }while(remain_size > 0);
 

 

   return true;
 
}
 

本篇文章來源於 Linux公社網站(www.linuxidc.com)  原文鏈接:http://www.linuxidc.com/Linux/2012-04/57876.htm

發佈了61 篇原創文章 · 獲贊 30 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章