Epoll模型詳解

gettogetto發表於2017-03-26

Epoll模型詳解

1. 核心中提高I/O效能的新方法epoll

epoll是什麼?按照man手冊的說法:是為處理大批量控制程式碼而作了改進的poll。要使用epoll只需要這三個系統調 用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。當然,這不是2.6核心才有的,它是在 2.5.44核心中被引進的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
Linux2.6 核心epoll介紹
    先介紹2本書《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,以2.4核心講解Linux TCP/IP實現,相當不錯.作為一個現實世界中的實現,很多時候你必須作很多權衡,這時候參考一個久經考驗的系統更有實際意義。舉個例子linux內 核中sk_buff結構為了追求速度和安全,犧牲了部分記憶體,所以在傳送TCP包的時候,無論應用層資料多大,sk_buff最小也有272的位元組.其實對於socket應用層程式來說,另外一本書《UNIX Network Programming Volume 1》意義更大一點.2003年的時候,這本書出了最新的第3版本,不過主要還是修訂第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevens給出了網路IO的基本模型。在這裡最重要的莫過於select模型和Asynchronous I/O模型.從理論上說,AIO(非同步I/O似乎是最高效的,你的IO操作可以立即返回,然後等待os告訴你IO操作完成。但是一直以來,如何實現就沒有一個完美的方案。最著名的windows完成埠實現的AIO,實際上也是內部用執行緒池實現的罷了,最後的結果是IO有個執行緒池,你應用也需要一個執行緒池...... 很多文件其實已經指出了這帶來的執行緒context-switch帶來的代價。在linux 平臺上,關於網路AIO一直是改動最多的地方,2.4的年代就有很多AIO核心patch,最著名的應該算是SGI那個。但是一直到2.6核心釋出,網路模組的AIO一直沒有進入穩定核心版本(大部分都是使用使用者執行緒模擬方法,在使用了NPTLlinux上面其實和windows的完成埠基本上差不多了)2.6核心所支援的AIO特指磁碟的AIO---支援io_submit(),io_getevents()以及對Direct IO的支援(就是繞過VFS系統buffer直接寫硬碟,對於流伺服器在記憶體平穩性上有相當幫助)。所以,剩下的select模型基本上就是我們在linux上面的唯一選擇,其實,如果加上no-block socket的配置,可以完成一個""AIO的實現,只不過推動力在於你而不是os而已。不過傳統的select/poll函式有著一些無法忍受的缺點,所以改進一直是2.4-2.5開發版本核心的任務,包括/dev/pollrealtime signal等等。最終,Davide Libenzi開發的epoll進入2.6核心成為正式的解決方案

2. selected 缺陷

首先,在Linux核心中,select所用到的FD_SET是有限的,即核心中有個引數__FD_SETSIZE定義了每個FD_SET的控制程式碼個數,在我用的2.6.15-25-386核心中,該值是1024,搜尋核心原始碼得到:
include/linux/posix_types.h:
#define __FD_SETSIZE         1024
也就是說,如果想要同時檢測1025個控制程式碼的可讀狀態是不可能用select實現的。或者同時檢測1025個控制程式碼的可寫狀態也是不可能的。其次,核心中實現 select是用輪詢方法,即每次檢測都會遍歷所有FD_SET中的控制程式碼,顯然,select函式執行時間與FD_SET中的控制程式碼個數有一個比例關係,即 select要檢測的控制程式碼數越多就會越費時。當然,在前文中我並沒有提及poll方法,事實上用select的朋友一定也試過poll,我個人覺得 selectpoll大同小異,個人偏好於用select而已。

3. epoll的優點

(1) 支援一個程式開啟大數目的socket描述符(FD)

    select 最不能忍受的是一個程式所開啟的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對於那些需要支援的上萬連線數目的IM伺服器來說顯 然太少了。這時候你一是可以選擇修改這個巨集然後重新編譯核心,不過資料也同時指出這樣會帶來網路效率的下降,二是可以選擇多程式的解決方案(傳統的 Apache方案),不過雖然linux上面建立程式的代價比較小,但仍舊是不可忽視的,加上程式間資料同步遠比不上執行緒間同步的高效,所以也不是一種完 美的方案。不過 epoll則沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於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網路卡驅動架構。

4. epoll的工作模式

令人高興的是,2.6核心的epoll比其2.5開發版本的/dev/epoll簡潔了許多,所以,大部分情況下,強大的東西往往是簡單的。唯一有點麻煩 是epoll2種工作方式:LTET
LT(level triggered)是預設的工作方式並且同時支援blockno-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表.
ET (edge-triggered)是高速工作方式只支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,直到你做了某些操作導致那個檔案描述符不再為就緒狀態了(比如,你在傳送,接收或者接收請求,或者傳送接收的資料少於一定量時導致 了一個EWOULDBLOCK錯誤)。但是請注意,如果一直不對這個fdIO操作(從而導致它再次變成未就緒),核心不會傳送更多的通知(only once)

epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統呼叫,具體用法請參考http://www.xmailserver.org/linux-patches/nio-improve.html ,在http://www.kegel.com/rn/也有一個完整的例子,大家一看就知道如何使用了Leader/follower模式執行緒 pool實現,以及和epoll的配合。

5. epoll的使用方法

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使用epoll_wait監聽)告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。第一個引數是epoll_create()的返回值,第二個參數列示動作,用三個巨集來表示:

EPOLL_CTL_ADD:註冊新的fdepfd中;

EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;

EPOLL_CTL_DEL:從epfd中刪除一個fd

第三個引數是需要監聽的fd,第四個引數是告訴核心需要監聽什麼事,struct 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 */
};
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表示已超時。


6. 應用例項

服務端

#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>
#include <errno.h>
using namespace std;
#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#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 argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;
    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
        return 1;
    }
    struct epoll_event ev,events[20]; //宣告epoll_event結構體的變數,ev用於註冊事件,陣列用於回傳要處理的事件
    epfd=epoll_create(256); //生成用於處理acceptepoll專用的檔案描述符
    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="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr)); 
    serveraddr.sin_port=htons(portnumber);
    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)//如果新監測到一個SOCKET使用者連線到了繫結的SOCKET埠,建立新的連線。
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                ev.data.fd=connfd; //設定用於讀操作的檔案描述符
                ev.events=EPOLLIN|EPOLLET; //設定用於注測的讀操作事件
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //註冊ev
            }
            else if(events[i].events&EPOLLIN)//如果是已經連線的使用者,並且收到資料,那麼進行讀入。
            {
                cout << "EPOLLIN" << endl;
                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;
                }
                line[n] = '/0';
                cout << "read " << line << endl;
                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
            }
        }
    }
    return 0;
}

相關文章