linux下epoll架構

 

1、Epoll簡介
由於它不會複用 文件描述符 集合來傳遞結果而迫使開發者每次等待事件以前都必須從新準備要被偵聽的文件描述符集合,另外一點緣由就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就好了。
 
 
2、Epoll優勢
<1> 支持一個進程打開大數目的socket描述符(FD)
    epoll 沒有傳統select/poll的「一個進程所打開的FD是有必定限制」的這個限制,它所支持的FD上限是最大能夠打開文件的數目,這個數字通常遠大於select 所支持的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之上了。
 
<3> 使用mmap加速內核與用戶空間的消息傳遞
    這點實際上涉及到epoll的具體實現。不管是select,poll仍是epoll都須要內核把FD消息通知給用戶空間,如何避免沒必要要的內存拷貝就顯得很重要,在這點上,epoll是經過內核於用戶空間mmap同一塊內存實現的。而若是你像我同樣從2.5內核就開始關注epoll的話,必定不會忘記手工mmap這一步的。
 
<4> 內核微調
    這一點其實不算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網卡驅動架構。
 
LT:水平觸發
是缺省的工做方式,而且同時支持block和no-block socket.在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的fd進行IO操做。若是你不做任何操做,內核仍是會繼續通知你 的,因此,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的表明。
       效率會低於ET觸發,尤爲在大併發,大流量的狀況下。可是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,所以不用擔憂事件丟失的狀況。
ET:邊緣觸發
是高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過epoll告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述 符發送更多的就緒通知,直到你作了某些操做致使那個文件描述符再也不爲就緒狀態了(好比,你在發送,接收或者接收請求,或者發送接收的數據少於必定量時致使 了一個EWOULDBLOCK 錯誤)。可是請注意,若是一直不對這個fd做IO操做(從而致使它再次變成未就緒),內核不會發送更多的通知(only once)。
效率很是高,在併發,大流量的狀況下,會比LT少不少epoll的系統調用,所以效率高。可是對編程要求高,須要細緻的處理每一個請求,不然容易發生丟失事件的狀況。
在許多測試中咱們會看到若是沒有大量的idle -connection或者dead-connection,epoll的效率並不會比select/poll高不少,可是當咱們遇到大量的idle- connection(例如WAN環境中存在大量的慢速鏈接),就會發現epoll的效率大大高於select/poll。
 
4、epoll具體使用方法
(1)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);
epoll的事件註冊函數,它不一樣與select()是在監聽事件時告訴內核要監聽什麼類型的事件,而是在這裏先註冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動做,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是須要監聽的fd,第四個參數是告訴內核須要監聽什麼事,struct epoll_event結構以下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

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


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

 
 (2)具體的實現步驟以下:
(a) 使用epoll_create()函數建立文件描述,設定可管理的最大socket描述符數目。
(b) 建立與epoll關聯的接收線程,應用程序能夠建立多個接收線程來處理epoll上的讀通知事件,線程的數量依賴於程序的具體須要。
(c) 建立一個偵聽socket的描述符ListenSock,並將該描述符設定爲非阻塞模式,調用Listen()函數在該套接字上偵聽有無新的鏈接請求,在epoll_event結構中設置要處理的事件類型EPOLLIN,工做方式爲 epoll_ET,以提升工做效率,同時使用epoll_ctl()來註冊事件,最後啓動網絡監視線程。
(d) 網絡監視線程啓動循環,epoll_wait()等待epoll事件發生。
(e) 若是epoll事件代表有新的鏈接請求,則調用accept()函數,將用戶socket描述符添加到epoll_data聯合體,同時設定該描述符爲非阻塞,並在epoll_event結構中設置要處理的事件類型爲讀和寫,工做方式爲epoll_ET。
(f) 若是epoll事件代表socket描述符上有數據可讀,則將該socket描述符加入可讀隊列,通知接收線程讀入數據,並將接收到的數據放入到接收數據的鏈表中,經邏輯處理後,將反饋的數據包放入到發送數據鏈表中,等待由發送線程發送。
 
例子代碼:
 
#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;
 
 struct epoll_event ev,events[20]; //聲明epoll_event結構體的變量, ev用於註冊事件, events數組用於回傳要處理的事件
 epfd=epoll_create(256); //生成用於處理accept的epoll專用的文件描述符, 指定生成描述符的最大範圍爲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) {
              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; //設置用於注測的寫操做事件
          epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要處理的事件爲EPOLLOUT
       }
      else if(events[i].events&EPOLLOUT)    /**寫事件**/
        {
          sockfd = events[i].data.fd;
          write(sockfd, line, n);
 
          ev.data.fd=sockfd; //設置用於讀操做的文件描述符
          ev.events=EPOLLIN | EPOLLET; //設置用於註冊的讀操做事件
          epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要處理的事件爲EPOLIN
        }
     }
 }
}
附錄:linux下epoll的測試效率(網上下載)
 測試程序分客戶端(client)及服務端(server). 服務端分別以select和epoll兩種I/O模型實現.
1.鏈接創建 速度測試
某個時刻連續向server發起大量鏈接請求,比較兩種I/O模型下Server端的鏈接接收速度。在客戶端記錄下鏈接數徹底創建後所花 費的時間.
操做步驟:
I.啓動服務端程序.
Selectserver命令:(./SelectServer 192.168.0.30 8000 1>/dev/null)
EpollServer命令:   (./EpollServer 192.168.0.30 8000 1>/dev/null)
參數1(192.168.0.30)爲server綁定的IP, 參數2(8000)爲server監聽的端口號;
II.啓動客戶端程序
命令:./deadlink 192.168.0.30 8000 800
參數1(192.168.0.30)是server端的IP, 參數2(8000)是server監聽的端口,參數3(800)是你想要創建鏈接的數量.等鏈接所有創建完畢後程序會自動打印出所花費的時間及成功創建的 鏈接數.每一個鏈接數量記錄5組數據,去除一個最大及最小值後,取餘下的3組數據的平均值做爲最終結果.
2.數據傳輸性能測試
client端建立若干線程,每一個線程與server創建一個鏈接。鏈接創建後向server發送取數據請求,而後讀 取server端返回的數據.如此反覆循環。每一個client請求server返回的數據字節數爲1K(1024bytes)大小.當鏈接所有創建後,系 統穩定下來,記錄此時的服務程序對應的CPU佔用率及內存使用率.每一個鏈接數量記錄下12組數據供分析使用.分析結果中將除去一個最大值及最小值,取餘下 的10組數據的平均值做爲最終結果。
操做步驟:
I啓動服務端
Selectserver命令:(./SelectServer 192.168.0.30 8000 1>/dev/null)
EpollServer命令:   (./EpollServer 192.168.0.30 8000 1>/dev/null)
參數1(192.168.0.30)爲server綁定的IP, 參數2(8000)爲server監聽的端口號;
II.啓動客戶端
命令: ./activelink 192.168.0.30 8000 800
參數1(192.168.0.30)是server端的IP, 參數2(8000)是server監聽的端口,參數3(800)是你想要創建的線程數(鏈接數).由於每一個線程創建一個鏈接,因此此數量亦即創建的鏈接 數。
III. netstat –la | grep 「192.168.0.250」 | wc –l 查看鏈接數量,等待創建完成.此處192.168.0.250爲客戶端機器IP地址。
IV.鏈接所有創建後等待5-6分鐘,待系統穩定下來後 top查看並記錄12 組Server 程序所佔CPU/內存使用率.
1.2 測試平臺說明
Server機器配置
CPU(處理 器) Intel(R) Pentium(R) 4 CPU 2.40GHz, L2 cache size: 512 KB
RAM(內 存) 248384kb, 約爲242M
OS(操做系統) Redhat Linux 9.0, kernel 2.6.16-20
NIC(網 卡) Realtek Semiconductor RTL-8139/8139C/8139C+ (rev 10), work on negotiated 100baseTx-FD
client機器配置
CPU(處理器) Intel(R) Pentium(R) 4 CPU 2.0GHz, L2 cache size: 512 KB
RAM(內存) 222948kb, 約爲218M
OS(操做系統) Redhat Linux 9.0, kernel 2.4.20-8
NIC(網卡) VIA Technologies VT6102 [Rhine-II] (rev 74)



 
2 測試結果
2.1 接收鏈接速度測試結果
表 2 1接收鏈接速度測試結果
鏈接數\IO模 型 SelectServer(單位 秒s) EpollServer(單位 秒s)
100 0s 0s
200 0s 0s
300 6s 0s
400 14s 0s
500 24s 0s
600 36s 0.3s
700 48s 0s
800 59s 0s
900 72s 0s
1000 84s 0s
2.2 數據傳輸性能測試
表 2 2數據傳輸性能測試結果
鏈接數\IO模型 SelectServer [cpu%, mem%] EpollServer [cpu%, mem%]
100 [28.06,    0.3] [21.74, 0.3]
200 [43.66,    0.3] [40.50, 0.3]
300 [47.09,    0.3] [42.73, 0.3]
400 [59.04,    0.3] [44.55, 0.3]
500 [54.44,    0.3] [51.00, 0.3]
600 [63.38,    0.3] [50.76, 0.3]
700 [65.77,    0.3] [51.47, 0.3]
800 [70.52,    0.3] [52.80, 0.3]

>>離線閱讀請點擊下載附件html

相關文章
相關標籤/搜索