linux epoll使用詳解

Linux2.6內核中epoll用法詳解linux

引言編程

epoll是linux2.6內核中才有的機制,其餘版本內核中是沒有的,是Linux2.6內核引入的多路複用IO的一種方式,用於提升網絡IO 性能的方法。在linux網絡編程中,很長一段時間都是採用select來實現多事件觸發處理的。Select存在以下幾個方面的問題:一是每次調用時要 重複地從用戶態讀入參數,二是每次調用時要重複地掃描文件描述符,三是每次在調用開始時,要把當前進程放入各個文件描述符的等待隊列。在調用結束後,又把 進程從各個等待隊列中刪除。Select採用輪詢的方式來處理事件觸發,當隨着監聽socket的文件描述符fd的數量增長時,輪詢的時間也就越長,形成 效率低下。並且linux/posix_types.h中有#define __FD_SETSIZE 1024(也有說2048的)的定義,也就是說linux select能監聽的最大fd數目是1024個,雖然能經過內核修改此參數,但這是治標不治本。數組

    epoll的出現能夠有效的解決select效率低下的問題,epoll把參數拷貝到內核態,在每次輪詢時不會重複拷貝。epoll有ET和LT兩種工 做模式,ET是高速模式只能以非阻塞方式進行,LT至關於快速的select,能夠纔有阻塞和非阻塞兩種方式,epoll經過把操做拆分爲 epoll_create,epoll_ctl,epoll_wait三個步驟避免重複地遍歷要監視的文件描述符。服務器

 

Epoll介紹網絡

epoll機制能夠運轉在兩種模式下:Edge Triggered (ET)和Level Triggered (LT)。首先來看一下man手冊中的一個例子:數據結構

      1. 咱們已經把一個用來從管道中讀取數據的文件句柄(RFD)添加到epoll描述符socket

      2. 這個時候從管道的另外一端被寫入了2KB的數據函數

      3. 調用epoll_wait(2),而且它會返回RFD,說明它已經準備好讀取操做性能

      4. 而後咱們讀取了1KB的數據ui

      5. 調用epoll_wait(2)......

 

      Edge Triggered 工做模式:

      若是咱們在第1步將RFD添加到epoll描述符的時候使用了EPOLLET標誌,那麼在第5步調用epoll_wait(2)以後將有可能會掛起,因 爲剩餘的數據還存在於文件的輸入緩衝區內,並且數據發出端還在等待一個針對已經發出數據的反饋信息。只有在監視的文件句柄上發生了某個事件的時候 ET 工做模式纔會彙報事件。所以在第5步的時候,調用者可能會放棄等待仍在存在於文件輸入緩衝區內的剩餘數據。在上面的例子中,會有一個事件產生在RFD句柄 上,由於在第2步執行了一個寫操做,而後,事件將會在第3步被銷燬。由於第4步的讀取操做沒有讀空文件輸入緩衝區內的數據,所以咱們在第5步調用 epoll_wait(2)完成後,是否掛起是不肯定的。epoll工做在ET模式的時候,必須使用非阻塞套接口,以免因爲一個文件句柄的阻塞讀/阻塞 寫操做把處理多個文件描述符的任務餓死。最好如下面的方式調用ET模式的epoll接口,在後面會介紹避免可能的缺陷。

       i    基於非阻塞文件句柄

       ii   只有當read(2)或者write(2)返回EAGAIN時才須要掛起,等待。但這並非說每次read()時都須要循環讀,直到讀到產生一個 EAGAIN才認爲這次事件處理完成,當read()返回的讀到的數據長度小於請求的數據長度時,就能夠肯定此時緩衝中已沒有數據了,也就能夠認爲此事讀 事件已處理完成。

 

      Level Triggered 工做模式

      相反的,以LT方式調用epoll接口的時候,它就至關於一個速度比較快的poll/select,在poll能用的地方epoll均可以用,由於他們 具備一樣的職能。即便使用ET模式的epoll,在收到多個數據包的時候仍然會產生多個事件。調用者能夠設定EPOLLONESHOT標誌,在 epoll_wait收到事件後epoll會與事件關聯的文件句柄從epoll描述符中禁止掉。所以當EPOLLONESHOT設定後,使用帶有 EPOLL_CTL_MOD標誌的epoll_ctl處理文件句柄就成爲調用者必須做的事情。

 

      以上是man手冊對epoll中兩種模式的簡要介紹,這裏有必要對兩種模式進行詳細的介紹:

 

LT是缺省的工做方式,而且同時支持block和no-block socket;在這種作法中,內核會告訴調用者一個文件描述符是否就緒了,而後調用者能夠對這個就緒的fd進行IO操做。若是你不做任何操做,內核仍是會 繼續通知調用者的,因此,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的表明。LT模式跟select有同樣的語義。就 是若是可讀就觸發。好比某管道原來爲空,若是有一個進程寫入2k數據,就會觸發。若是處理進程讀取1k數據,下次輪詢時繼續觸發。該模式下,默認不可讀, 只有epoll通知可讀纔是可讀,不然不可讀。

 

ET是高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過epoll告訴調用者,而後它會假設調用者知道文件描述符已經就緒,而且不會再爲那個 文件描述 符發送更多的就緒通知,直到調用者作了某些操做致使那個文件描述符再也不爲就緒狀態了。可是請注意,若是一直不對這個fd做IO操做(從而致使它再次變成未 就緒),內核不會發送更多的通知。該模式與select有不一樣的語義,只有當從不可讀變爲可讀時才觸發。上面那種狀況,還有1k可讀,因此不會觸發,當繼 續讀,直到返回EAGAIN時,變爲不可讀,若是再次變爲可讀就觸發。默承認讀,調用者能夠隨便讀,直到發生EAGAIN。可讀時讀和不讀,怎麼讀都由調 用者本身決定,中間epoll無論。EAGAIN後不可讀了,等到再次可讀,epoll會再通知一次。理解ET模式最重要的就是理解狀態的變化,對於監聽 可讀事件時,若是是socket是監聽socket,那麼當有新的主動鏈接到來爲狀態發生變化;對通常的socket而言,協議棧中相應的緩衝區有新的數 據爲狀態發生變化。可是,若是在一個時間同時接收了N個鏈接(N>1),可是監聽socket只accept了一個鏈接,那麼其它未 accept的鏈接將不會在ET模式下給監聽socket發出通知,此時狀態不發生變化;對於通常的socket,若是對應的緩衝區自己已經有了N字節的 數據,而只取出了小於N字節的數據,那麼殘存的數據不會形成狀態發生變化。

 

Epoll的調用很簡單隻涉及到三個函數分別是:

 

1.int epoll_create(int size);

建立一個epoll的句柄,size用來告訴內核這個監聽的數目最大值。這個參數不一樣於select()中的第一個參數,給出最大監聽的fd+1的 值。須要注意的是,當建立好epoll句柄後,它就是會佔用一個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,第四個參數是告訴內核須要監聽什麼事,數據結構以下:

 

01 typedef union epoll_data{
02
03 void *ptr;
04
05 int fd;
06
07 __uint32_t u32;
08
09 __uint64_t u64
10
11 }epoll_data_t;
12 struct epoll_event {
13   __uint32_t events;  /* Epoll events */
14   epoll_data_t data;  /* User data variable */
15 };

 

events能夠是如下幾個宏的集合:

EPOLLIN :表示對應的文件描述符能夠讀(包括對端SOCKET正常關閉);

EPOLLOUT:表示對應的文件描述符能夠寫;

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

EPOLLERR:表示對應的文件描述符發生錯誤;

EPOLLHUP:表示對應的文件描述符被掛斷;

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

EPOLLONESHOT:只監聽一次事件,當監聽完此次事件以後,就會把這個fd從epoll的隊列中刪除。若是還須要繼續監聽這個socket的話,須要再次把這個fd加入到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表示已超時。      

 

 

 

epoll簡單例子

下面給出一個簡單使用epoll的例子以加深理解

 

服務端代碼:

001 #include <strings.n>
002
003 #include<sys/types.h>
004 #include <sys/socket.h>
005 #include <sys/epoll.h>
006 #include <netinet/in.h>
007 #include <arpa/inet.h>
008 #include <fcntl.h>
009 #include <unistd.h>
010 #include <stdio.h>
011 #include <errno.h>
012
013
014
015 #define MAXLINE 5
016 #define OPEN_MAX 100
017 #define LISTENQ 20
018 #define SERV_PORT 5000
019 #define INFTIM 1000
020
021 void setnonblocking(int sock)
022 {
023     int opts;
024     opts=fcntl(sock,F_GETFL);
025     if(opts<0)
026     {
027         perror("fcntl(sock,GETFL)");
028         exit(1);
029     }
030     opts = opts|O_NONBLOCK;
031     if(fcntl(sock,F_SETFL,opts)<0)
032     {
033         perror("fcntl(sock,SETFL,opts)");
034         exit(1);
035     }   
036 }
037
038 int main()
039 {
040     int i, maxi, listenfd, connfd, sockfd,epfd,nfds;
041     ssize_t n;
042     char line[MAXLINE];
043     socklen_t clilen;
044     //聲明epoll_event結構體的變量,ev用於註冊事件,數組用於回傳要處理的事件
045     struct epoll_event ev,events[20];
046     //生成用於處理accept的epoll專用的文件描述符
047     epfd=epoll_create(256);
048     struct sockaddr_in clientaddr;
049     struct sockaddr_in serveraddr;
050
051 clilen=sizeof(clientaddr);
052     listenfd = socket(AF_INET, SOCK_STREAM, 0);
053     //把socket設置爲非阻塞方式
054    setnonblocking(listenfd);
055     //設置與要處理的事件相關的文件描述符
056     ev.data.fd=listenfd;
057     //設置要處理的事件類型
058     ev.events=EPOLLIN|EPOLLET;
059     //ev.events=EPOLLIN;
060     //註冊epoll事件
061     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
062     bzero(&serveraddr, sizeof(serveraddr));
063     serveraddr.sin_family = AF_INET;
064     char *local_addr="127.0.0.1";
065     inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);
066     serveraddr.sin_port=htons(SERV_PORT);
067     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
068     listen(listenfd, LISTENQ);
069     maxi = 0;
070     for ( ; ; ) {
071         //等待epoll事件的發生
072         nfds=epoll_wait(epfd,events,20,500);
073         //處理所發生的全部事件     
074         for(i=0;i<nfds;++i)
075         {
076             if(events[i].data.fd==listenfd)//有客戶鏈接
077             {
078                 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
079                 if(connfd<0){
080                     perror("connfd<0");
081                     exit(1);
082                 }
083                 setnonblocking(connfd);
084                 char *str = inet_ntoa(clientaddr.sin_addr);
085                 printf"accapt a connection from %s /n",str);
086                 //設置用於讀操做的文件描述符
087                 ev.data.fd=connfd;
088                 //設置用於注測的讀操做事件
089                 ev.events=EPOLLIN|EPOLLET;
090                 //ev.events=EPOLLIN;
091                 //註冊ev
092                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
093             }
094             else if(events[i].events&EPOLLIN) //客戶端socket可讀事件
095             {
096                recv(events[i].data.fd, line,MAXLINE,0);
097
098 printf(「recv line %s /n 」, line);
099        }
100     }
101     return 0;
102 }

客戶端的代碼用perl寫的以下:

01 #!/usr/bin/perl
02
03 use IO::Socket;
04
05 my $host "127.0.0.1";
06 my $port = 5000;
07
08 my socket=IO::Socket::INET−>new("host:port")ordie"createsocketerror@";
09 my $msg_out "1234567890";
10 print socketmsg_out;
11 print "now send over, go to sleep/n";
12
13 while (1)
14 {
15     sleep(1);
16 }

  同時運行服務端和客戶端程序,會發現服務端在接收5字節數據以後就不會在觸發EPOLLIN事件了,由於採用的是ET模式,客戶端發送的10字 節數據中,只讀取了5字節的數據,還有5字節數據可讀,也就是狀態未發生改變。因此服務端不會在觸發EPOLLIN事件。而若是把ET模式改爲LT模式, 那麼服務端仍是會觸發EPOLLIN事件,將剩餘的5字節數據讀取。

 

總結

本文主要介紹了linux epoll的使用方法,對其中的epoll的兩種模式進行了詳細的分析,在服務器處理中要等待用戶socket鏈接,因爲epoll的性能較高,能夠有效 的處理用戶請求。對於多用戶鏈接時還要要注意在服務端accept時,有可能同時到達多個鏈接,因爲採用ET模式,此時服務器端可能只會讀取一個鏈接而忽 略其餘鏈接,因此採用ET模式時應該採用while(1)這樣的方式來讀取鏈接。本文還給出了一個簡單的來講明epoll的用法,本例只是演示做用,對於 實際應用中應考慮上述多用戶狀況,以及採用epoll+線程池的方法。對於參考資料2中的例子,並不會按做者說的那樣只輸出5字節,而是在出發 EPOLLOUT以後還會出發EPOLLIN事件,也就是會出來後面的67890五個字節,對做者的這個例子研究了很久,才明白是這樣的,不知道是否是沒 有深入理解man的緣由,得再好好看看man.

相關文章
相關標籤/搜索