epoll簡介 與 UDP server的實現

Abstract
epoll是Linux內核爲處理大批量句柄而作了改進的poll,是Linux下多路複用IO接口select/poll的增強版本,
它能顯著減少程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率。


簡介:
epoll是Linux下多路複用IO接口select/poll的增強版本,
它能顯著提高程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率,
因爲:
 它會複用文件描述符集合來傳遞結果,
 而不用迫使開發者每次等待事件之前都必須重新準備要被偵聽的文件描述符集合,
另一點原因:
 就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,
 只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。


epoll除了提供select/poll那種IO事件的
電平觸發(Level Triggered, LT)外,
還提供了邊沿觸發(Edge Triggered, ET),
這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。


優點:
支持一個進程打開大數目的socket描述符
select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是1024。
對於那些需要支持的上萬連接數目的IM服務器來說顯然太少了。
這時候你一是可以選擇修改這個宏然後重新編譯內核,不過資料也同時指出這樣會帶來網絡效率的下降,
二是可以選擇多進程的解決方案(傳統的Apache方案),不過雖然linux上面創建進程的代價比較小,
但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。


epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,
舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,
一般來說這個數目和系統內存關係很大。


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之上了。


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


內核微調:
這一點其實不算epoll的優點了,而是整個linux平臺的優點。也許你可以懷疑linux平臺,
但是你無法迴避linux平臺賦予你微調內核的能力。
比如,內核TCP/IP協議棧使用內存池管理sk_buff結構,那麼可以在運行時期動態調整這個內存pool
(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。
再比如listen函數的第2個參數(TCP完成3次握手的數據包隊列長度),也可以根據你平臺內存大小動態調整。
更甚至在一個數據包面數目巨大但同時每個數據包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。


使用
令人高興的是,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確認。


ET和LT的區別:
    LT事件不會丟棄,而是隻要讀buffer裏面有數據可以讓用戶讀,則不斷的通知你。
    ET則只在事件發生之時通知。
    可以簡單理解爲LT是水平觸發,而ET則爲邊緣觸發。
    LT模式只要有事件未處理就會觸發,而ET則只在高低電平變換時(即狀態從1到0或者0到1)觸發.


系統調用
epoll相關的系統調用有:
  epoll_create, 
  epoll_ctl,
  epoll_wait。
  Linux-2.6.19又引入了可以屏蔽指定信號的epoll_wait: epoll_pwait。


至此epoll家族已全。其中
  epoll_create用來創建一個epoll文件描述符,
  epoll_ctl用來添加/修改/刪除需要偵聽的文件描述符及其事件,
  epoll_wait/epoll_pwait接收發生在被偵聽的描述符上的,用戶感興趣的IO事件。
  epoll文件描述符用完後,直接用close關閉即可,非常方便。


事實上,任何被偵聽的文件符只要其被關閉,那麼它也會自動從被偵聽的文件描述符集合中刪除,很是智能。
每次添加/修改/刪除被偵聽文件描述符都需要調用epoll_ctl,所以要儘量少地調用epoll_ctl,
防止其所引來的開銷抵消其帶來的好處。有的時候,應用中可能存在大量的短連接(比如說Web服務器),
epoll_ctl將被頻繁地調用,可能成爲這個系統的瓶頸。


A:IO效率。
在大家苦苦的爲在線人數的增長而導致的系統資源吃緊上的問題正在發愁的時候,
Linux 2.6內核中提供的System Epoll爲我們提供了一套完美的解決方案。
傳統的select以及poll的效率會因爲在線人數的線形遞增而導致呈二次乃至三次方的下降,
這些直接導致了網絡服務器可以支持的人數有了個比較明顯的限制。
自從Linux提供了/dev/epoll的設備以及後來2.6內核中對/dev/epoll設備的訪問的封裝(System Epoll)之後,
這種現象得到了大大的緩解,如果說幾個月前,大家還對epoll不熟悉,那麼現在來說的話,
epoll的應用已經得到了大範圍的普及。


那麼究竟如何來使用epoll呢?其實非常簡單。
通過在包含一個頭文件#include <sys/epoll.h>以及幾個簡單的API將可以大大的提高你的網絡服務器的支持人數。
  首先, 通過epoll_create(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 if (event[n].events & EPOLLIN) {  // 如果是已經連接的用戶,並且收到數據,
                                             // 那麼進行讀入
        int sockfd_r;
        if ((sockfd_r = event[n].data.fd) < 0)
            continue;
        read(sockfd_r, buffer, MAXSIZE);


        // 修改sockfd_r上要處理的事件爲EPOLLOUT
        ev.data.fd = sockfd_r;
        ev.events = EPOLLOUT | EPOLLET;
        epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
    } else if (event[n].events & EPOLLOUT) { // 如果有數據發送
        int sockfd_w = events[n].data.fd;
        write(sockfd_w, buffer, sizeof(buffer));


        // 修改sockfd_w上要處理的事件爲EPOLLIN
        ev.data.fd = sockfd_w;
        ev.events = EPOLLIN | EPOLLET;
        epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
    }
    do_use_fd(events[n].data.fd);
  }



對,epoll的操作就這麼簡單,總共不過4個API:
   epoll_create, epoll_ctl, epoll_wait和close。




示例程序:
用epoll實現的多線程UDP server

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
//#include <openssl/ssl.h>
//#include <openssl/err.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <pthread.h>
#include <assert.h>


//#include "oci_api.h"


#define MAXBUF 1024
#define MAXEPOLLSIZE 100


/*
 setnonblocking – 設置句柄爲非阻塞方式
 */
int setnonblocking(int sockfd)
{
  if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1)
  {
    return -1;
  }
  return 0;
}




/*
 pthread_handle_message – 線程處理 socket 上的消息收發
 */
void* pthread_handle_message(int* sock_fd)
{
  char recvbuf[MAXBUF + 1];
  char sendbuf[MAXBUF+1];
  int  ret;
  int  new_fd;
  struct sockaddr_in client_addr;
  socklen_t cli_len=sizeof(client_addr);


  new_fd=*sock_fd; 


  /* 開始處理每個新連接上的數據收發 */
  bzero(recvbuf, MAXBUF + 1);
  bzero(sendbuf, MAXBUF + 1);


  /* 接收客戶端的消息 */
  ret = recvfrom(new_fd, recvbuf, MAXBUF, 0, (struct sockaddr *)&client_addr, &cli_len);
  if (ret > 0){
    printf("socket %d recv from : %s : %d message: %s ,%d bytes/n",
           new_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), recvbuf, ret);
  /* 
  char *s1="insert";
  char *s2="select";
  char *s3="delete";


  if(!strncmp(s1,recvbuf,6))
    oci_insert(recvbuf,sendbuf);
  else if(!strncmp(s2,recvbuf,6))
    oci_select(recvbuf,sendbuf);
  else if(!strncmp(s3,recvbuf,6))
    oci_delete(recvbuf,sendbuf);
  else
    sprintf(sendbuf,"input sql is error!/n");


   ret = sendto(new_fd, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&client_addr, cli_len);
   if(ret<0)
     printf("消息發送失敗!錯誤代碼是%d,錯誤信息是'%s'/n", errno, strerror(errno));
   */


  }
  else
  {
    printf("received failed! error code %d,message : %s /n",
      errno, strerror(errno));    
  }
  /* 處理每個新連接上的數據收發結束 */ 
  //printf("pthread exit!");
  fflush(stdout); 
  pthread_exit(NULL);
}


 


int main(int argc, char **argv)
{
  int listener, kdpfd, nfds, n, curfds;
  socklen_t len;
  struct sockaddr_in my_addr, their_addr;
  unsigned int myport;
  struct epoll_event ev;
  struct epoll_event events[MAXEPOLLSIZE];
  struct rlimit rt;


  myport = 1234; 


  pthread_t thread;
  pthread_attr_t attr;


  /* 設置每個進程允許打開的最大文件數 */
  rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
  if (setrlimit(RLIMIT_NOFILE, &rt) == -1) 
  {
    perror("setrlimit");
    exit(1);
  }
  else 
  {
    printf("setting success /n");
  }


  /* 開啓 socket 監聽 */
  if ((listener = socket(PF_INET, SOCK_DGRAM, 0)) == -1)
  {
    perror("socket create failed !");
    exit(1);
  }
  else
  {
    printf("socket create  success /n");
  }


  /*設置socket屬性,端口可以重用*/
  int opt=SO_REUSEADDR;
  setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));


  setnonblocking(listener);
  bzero(&my_addr, sizeof(my_addr));
  my_addr.sin_family = PF_INET;
  my_addr.sin_port = htons(myport);
  my_addr.sin_addr.s_addr = INADDR_ANY;
  if (bind(listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) 
  {
    perror("bind");
    exit(1);
  } 
  else
  {
    printf("IP and port bind success /n");
  }
 
  /* 創建 epoll 句柄,把監聽 socket 加入到 epoll 集合裏 */
  kdpfd = epoll_create(MAXEPOLLSIZE);
  len = sizeof(struct sockaddr_in);
  ev.events = EPOLLIN | EPOLLET;
  ev.data.fd = listener;
  if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0) 
  {
    fprintf(stderr, "epoll set insertion error: fd=%d/n", listener);
    return -1;
  }
  else
  {
    printf("listen socket added in  epoll success /n");
  }


  while (1) 
  {
    /* 等待有事件發生 */
    nfds = epoll_wait(kdpfd, events, 10000, -1);
    if (nfds == -1)
    {
      perror("epoll_wait");
      break;
    }


    /* 處理所有事件 */
    for (n = 0; n < nfds; ++n)
    {
      if (events[n].data.fd == listener) 
      {
        /*初始化屬性值,均設爲默認值*/
        pthread_attr_init(&attr);
        pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);


        /*  設置線程爲分離屬性*/ 
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);


        if(pthread_create(&thread,&attr,(void*)pthread_handle_message,(void*)&(events[n].data.fd)))
        {
           perror("pthread_creat error!");
           exit(-1);
        } 
       } 
     }
  }
  close(listener);
  return 0;
}

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