epoll使用詳解


epoll介紹

epoll的行爲與poll(2)相似,監視多個有IO事件的文件描述符。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。

epoll_create(2) 創建一個新的epoll實例,並返回一個引用該實例的文件描述符
epoll_ctl(2) 創建epoll實例後,註冊對感興趣的文件描述符。當前註冊在epoll實例上的文件描述符集被稱爲epoll集合。
epoll_wait(2) 等待I/O事件,如果當前沒有事件可用,則阻塞調用線程。

水平觸發邊沿觸發
epoll事件分佈接口既可以表現爲邊緣觸發(ET),也可以表現爲水平觸發(LT)。這兩種機制的區別
可以這樣描述。假設有這種情況發生:

  1. 表示管道(rfd)的讀側的文件描述符在epoll實例上註冊。
  2. 管道寫入器在管道的寫入端寫入2 kB的數據。
  3. 調用epoll_wait(2)將返回rfd作爲就緒文件描述符。
  4. 管道讀取器從rfd讀取1kb的數據。
  5. epoll_wait(2)調用完成。

如果使用邊緣觸發標誌將rfd文件描述符註冊到epoll接口,那麼第五步的epoll_wait(2)的調用可能會掛起,儘管文件輸入緩衝區仍然有1kb數據可讀;同時,遠程對等端可能正在期望基於它已發送的數據的應答。這樣做的原因是,只有在被監視文件描述符上發生更改時,邊緣觸發模式才交付事件。因此,在步驟5中,調用者可能會以等待那些仍在輸入緩衝區中的數據的狀態下結束。
在上面的例子中,將生成rfd上的一個事件,因爲在2中完成了寫入,而在3中使用了該事件。由於在4中完成的讀操作不會消耗整個緩衝區數據,所以在步驟5中完成的對epoll_wait(2)的調用可能會無限期阻塞。

使用EPOLLET標誌的應用程序應該使用非阻塞文件描述符,以避免在處理多個文件描述符時出現有阻塞的讀寫飢餓任務。建議使用epoll作爲邊沿觸發(EPOLLET)接口的方式如下:
i、 具有非阻塞文件描述符
ii、只有在read(2)或write(2)返回EAGAIN後纔等待事件。
相反,當EPOLLET作爲水平觸發接口使用時(默認情況下,沒有指定EPOLLET), epoll只是一個更快的poll(2),並且可以在使用後者的任何地方使用,因爲它具有相同的語義。

Epoll的優點:

1、支持一個進程打開大數目的socket描述符(FD)

select能打開的文件描述符有一定的限制,FD_SETSIZE設置,默認值是2048,有兩種解決方法,1、修改它的值,然後重新編譯內核。2、使用多進程加入要併發20w個客戶,那麼就要開100進程;epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是2萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關係很大。

2、IO效率不隨FD數目增加而線性下降

select/poll採用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;內核 / 用戶空間內存拷貝問題,select/poll需要複製大量的句柄數據結構,產生巨大的開銷;select/poll返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對"活躍"的socket進行操作—這是因爲在內核實現中epoll是根據每個fd上面的callback函數實現的。

3、支持邊緣觸發模式

select/poll的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行IO操作,那麼之後每次select/poll調用還是會將這些文件描述符通知進程。

4、使用mmap加速內核與用戶空間的消息傳遞。

select/poll和epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝很重要,在這點上,select/poll需要複製整個FD數組,產生巨大的開銷;而epoll是通過內核於用戶空間mmap同一塊內存實現的。

epoll的系統調用

epoll_create

int epoll_create(int size);
int epoll_create1(int flags);
創建一個epoll的句柄。自從linux2.6.8之後,size參數是被忽略的,更推薦使用epoll_crete1(0)來替代,flags可以設置EPOLL_CLOEXEC標誌

epoll_ctl

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

該系統調用對文件描述符epfd引用的epoll(7)實例執行控制操作。它請求對目標文件描述符fd執行操作op。

epfd : epoll_create創建的文件描述符.

op :參數的有效參數爲:
EPOLL_CTL_ADD
在文件描述符epfd引用的epoll實例上註冊目標文件描述符fd。
EPOLL_CTL_MOD
修改已註冊描述符fd關聯的事件。
EPOLL_CTL_DEL
從epfd引用的epoll實例中刪除(取消註冊)目標文件描述符fd。該事件將被忽略,並且可以是NULL

fd :待監聽的fd

epoll_event : 描述鏈接到文件描述符fd的對象,它的定義如下

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

events成員是由以下可用事件類型的零個或多個組合在一起組成的位掩碼:

EPOLLIN :關聯的文件描述符可以讀(包括對端SOCKET正常關閉);

EPOLLOUT:關聯的文件描述符可以寫;

EPOLLPRI:關聯的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);

EPOLLERR:關聯的文件描述符發生錯誤;

EPOLLHUP:關聯的文件描述符被掛斷;

EPOLLRDHUP:流套接字對等關閉連接,或半關閉寫。(當使用邊緣觸發監視時,此標記對於編寫簡單代碼檢測對等端是否關閉特別有用。2.6.17引入)

EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。

EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個fd的話,需要再次把這個fd加入到EPOLL隊列裏

它們在內核頭文件裏的定義如下:

 33 
 34 enum EPOLL_EVENTS
 35   {
 36     EPOLLIN = 0x001,
 37 #define EPOLLIN EPOLLIN
 38     EPOLLPRI = 0x002,
 39 #define EPOLLPRI EPOLLPRI
 40     EPOLLOUT = 0x004,
 41 #define EPOLLOUT EPOLLOUT
 42     EPOLLRDNORM = 0x040,
 43 #define EPOLLRDNORM EPOLLRDNORM
 44     EPOLLRDBAND = 0x080,
 45 #define EPOLLRDBAND EPOLLRDBAND
 46     EPOLLWRNORM = 0x100,
 47 #define EPOLLWRNORM EPOLLWRNORM
 48     EPOLLWRBAND = 0x200,
 49 #define EPOLLWRBAND EPOLLWRBAND
 50     EPOLLMSG = 0x400,
 51 #define EPOLLMSG EPOLLMSG
 52     EPOLLERR = 0x008,
 53 #define EPOLLERR EPOLLERR
 54     EPOLLHUP = 0x010,
 55 #define EPOLLHUP EPOLLHUP
 56     EPOLLRDHUP = 0x2000,
 57 #define EPOLLRDHUP EPOLLRDHUP
 58     EPOLLEXCLUSIVE = 1u << 28,
 59 #define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
 60     EPOLLWAKEUP = 1u << 29,
 61 #define EPOLLWAKEUP EPOLLWAKEUP
 62     EPOLLONESHOT = 1u << 30,
 63 #define EPOLLONESHOT EPOLLONESHOT
 64     EPOLLET = 1u << 31
 65 #define EPOLLET EPOLLET
 66   };
 67 
 68 
 69 /* Valid opcodes ( "op" parameter ) to issue to epoll_ctl().  */
 70 #define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface.  */
 71 #define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface.  */
 72 #define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure.  */

epoll_wait

       #include <sys/epoll.h>

       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
       int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);

等待在epoll監控的事件中已經發生的事件。
epfd : epoll_create() 的返回值.
events : 分配好的epoll_event結構體數組,epoll將會把發生的事件賦值到events數組中(events不可以是空指針,內核只負責把數據複製到這個events數組中,不會去幫助我們在用戶態中分配內存)
maxevents : maxevents告知內核這個events有多大,這個 maxevents的值大於0(否則Error :Invalid argument)
timeout : 超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。如果函數調用成功,返回對應I/O上已準備好的文件描述符數目,如返回0表示已超時,它會阻塞直到

  • 一個文件描述符有事件發生;
  • 信號處理器中斷;
  • 超時;

epoll示例程序

此程序簡單測試一下三個API,註冊標準輸出的描述符到epoll,監視標準輸出的讀事件,觸發後回顯一遍,quit退出程序.

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/epoll.h>
#include <vector>

typedef std::vector<struct epoll_event> PollFdList;

int main(int argc ,char **argv)
{

  int fd;
  char buf[1024];
  int i,res,real_read, maxfd;

  if((fd=open("/dev/stdin",O_RDONLY|O_NONBLOCK)) < 0)
  {
    fprintf(stderr,"open data1 error:%s",strerror(errno));
    return 1;
  }

  PollFdList m_pollfds;
  int epfd = epoll_create1(EPOLL_CLOEXEC);

  struct epoll_event ev;
  ev.events = EPOLLIN | EPOLLPRI;
  ev.data.fd = fd;

  epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
  
  m_pollfds.resize(1024);

  while(1)
  {
    int ret = epoll_wait(epfd, m_pollfds.data(), m_pollfds.size(), 5000);
    if (ret < 0)
    {
      printf("ePoll error : %s\n",strerror(errno));
      return 1;
    }

    if(ret == 0){
      printf("ePoll timeout\n");
      continue;
    }

    for (i = 0; i< 1; i++)
    {
      if (m_pollfds[i].events & EPOLLIN)
      {
        memset(buf, 0, 1024);
        real_read = read(m_pollfds[i].data.fd, buf, 1024);
        if (real_read < 0)
        {
          if (errno != EAGAIN)
          {
            printf("read eror : %s\n",strerror(errno));
            continue;
          }
        }
        else if (!real_read)
        {
          close(m_pollfds[i].data.fd);
          m_pollfds[i].events = 0;
        }
        else
        {
          if (i == 0)
          {
            buf[real_read] = '\0';
            printf("%s", buf);
            if ((buf[0] == 'q') || (buf[0] == 'Q'))
            {
              printf("quit\n");
              return 1;
            }
          }
          else
          {
            buf[real_read] = '\0';
            printf("%s", buf);
          }
        }
      }
    }
  }

  exit(0);
}
./test
hello
hello
hello epoll
hello epoll
ePoll timeout
quit
quit
quit
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章