UNIX epoll 與 Node.js 事件迴圈多路分解器

counterxing發表於2019-03-04

關於I / O複用,可以先查閱《UNIX網路程式設計》第六章selectpoll相關內容。

selectpollepoll都是I / O複用的機制,在《UNIX網路程式設計》裡重點講了selectpoll的機制,但selectpoll並不是現代高效能伺服器的最佳選擇。包括現在的Node.js中的事件迴圈機制(event loop)也是基於epoll實現的。

select和poll的缺點

按照《UNIX網路程式設計》中所述,pollselect類似,沒有解決以下的問題:

  • 每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
  • 同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
  • select支援的檔案描述符數量太小了,預設是1024

epoll對於上述缺點的改進

epoll既然是對selectpoll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epollselectpoll的呼叫介面上的不同,selectpoll都只提供了一個函式——select或者poll函式。而epoll提供了三個函式,epoll_create,epoll_ctlepoll_waitepoll_create是建立一個epoll控制程式碼;epoll_ctl是註冊要監聽的事件型別;epoll_wait則是等待事件的產生。   對於第一個缺點,epoll的解決方案在epoll_ctl函式中。每次註冊新的事件到epoll控制程式碼中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。   對於第二個缺點,epoll的解決方案不像selectpoll一樣每次都把current輪流加入fd對應的裝置等待佇列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)併為每個fd指定一個回撥函式,當裝置就緒,喚醒等待佇列上的等待者時,就會呼叫這個回撥函式,而這個回撥函式會把就緒的fd加入一個就緒連結串列)。epoll_wait的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。   對於第三個缺點,epoll沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,一般來說這個數目和系統記憶體關係很大。

epoll介面

epoll操作過程需要三個介面,分別如下:

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
複製程式碼

epoll_create方法

#include <sys/epoll.h>
int epoll_create(int size);
複製程式碼

建立一個epoll的控制程式碼,size用來告訴核心這個監聽的數目一共有多大,這個引數不同於select()中的第一個引數,給出最大監聽的fd+1的值,引數size並不是限制了epoll所能監聽的描述符最大個數,只是對核心初始分配內部資料結構的一個建議。 當建立好epoll控制程式碼後,它就會佔用一個fd值,在linux下如果檢視/proc/程式id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。

#include <sys/epoll.h>
#define FDSIZE 1024
// ...
int main(int argc,char *argv[])
{
  int epollfd = epoll_create(FDSIZE); // 這裡並不是指最大檔案描述符數量為1024,而是給核心初始化資料結構的一個建議。
  return 0;
}
複製程式碼

epoll_ctl方法

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
複製程式碼

epoll_ctl方法是epoll的事件註冊函式,它不同與select()是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。

  • epfd:是epoll_create()的返回值。
  • op:表示對對應的fd檔案描述符的操作,一般情況下表示想要監聽事件、刪除事件和修改事件處理函式,用三個巨集來表示:
    • EPOLL_CTL_ADD,表示對於對應的fd檔案描述符新增一組事件監聽;
    • EPOLL_CTL_DEL,表示對於對應的fd檔案描述符刪除該組事件監聽;
    • EPOLL_CTL_MOD,表示對於對應的fd檔案描述符修改該組事件監聽為新的events
  • fd:表示需要監聽的fd(檔案描述符)
  • event:是告訴核心需要監聽的事件集合,傳入一個指標,指向事件集合的第一項,struct epoll_event的結構如下:
struct epoll_event {
  __uint32_t events;  // 表示一類epoll事件
  epoll_data_t data;  // 使用者傳遞的資料
}

複製程式碼

因為events表示一類epoll事件,它可以是以下幾個巨集的集合:

  • EPOLLIN:表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
  • EPOLLOUT:表示對應的檔案描述符可以寫;
  • EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
  • EPOLLERR:表示對應的檔案描述符發生錯誤;
  • EPOLLHUP:表示對應的檔案描述符被結束通話;
  • EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的;
  • EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡;

例如,如果想讓epoll對於對應的檔案描述符fd新增一組事件,監聽對應的檔案描述符可讀的情況:

static void add_event_epoll_in(int epollfd, int fd)
{
  struct epoll_event ev;
  ev.events = EPOLLIN;
  ev.data.fd = fd;
  epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
}
複製程式碼

epoll_wait方法

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
複製程式碼

epoll_wait方法等待事件的產生,類似於select()呼叫,返回需要處理的事件數目。

  • epfd:是epoll_create()的返回值;
  • events:用來從核心得到事件的集合,我們一般把需要處理的事件對應的檔案描述符fd放到events結構體下data引數內,這樣我們可以在事件處理函式中取到對應的檔案描述符fd,執行對應操作(例如對於TCP套接字,我們呼叫readwrite等);
  • maxevents:告之核心這個events的數量,這個maxevents的值不能大於建立epoll_create()時的size,否則會造成溢位的風險;
  • timeout:超時時間,以毫秒為單位,如果設定為-1,表示一直等待,設定為0表示不等待;

舉例:應用程式一般阻塞與epoll_wait呼叫,一旦events中任意一事件觸發,epoll_wait執行,等待指定的timeout超時時間,如果I / O完成則立即返回需要處理的事件數目。

static void do_epoll(int listenfd)
{
    int epollfd;
    struct epoll_event events[EPOLLEVENTS];
    int ret;
    char buf[MAXSIZE];
    memset(buf,0,MAXSIZE);
    //建立一個描述符
    epollfd = epoll_create(FDSIZE);
    //新增監聽描述符事件
    add_event_epoll_in(epollfd, listenfd);
    for ( ; ; )
    {
        // 獲取已經準備好的描述符事件數目
        ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
        handle_events(epollfd, events, ret, listenfd, buf);
    }
    close(epollfd);
}

static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
    int i;
    int fd;
    //進行選好遍歷
    for (i = 0;i < num;i++)
    {
        // 在這裡取到需要處理的檔案描述符
        fd = events[i].data.fd;
        //根據描述符的型別和事件型別進行處理
        if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            handle_accpet(epollfd,listenfd);
        else if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,buf);
        else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,buf);
    }
}
複製程式碼

使用epoll重構伺服器回射程式

服務端

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>

#define IPADDRESS   "127.0.0.1"
#define PORT        8787
#define MAXSIZE     1024
#define LISTENQ     5
#define FDSIZE      1000
#define EPOLLEVENTS 100

//函式宣告
//建立套接字並進行繫結
static int socket_bind(const char* ip,int port);
//IO多路複用epoll
static void do_epoll(int listenfd);
//事件處理函式
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf);
//處理接收到的連線
static void handle_accpet(int epollfd,int listenfd);
//讀處理
static void do_read(int epollfd,int fd,char *buf);
//寫處理
static void do_write(int epollfd,int fd,char *buf);
//新增事件
static void add_event(int epollfd,int fd,int state);
//修改事件
static void modify_event(int epollfd,int fd,int state);
//刪除事件
static void delete_event(int epollfd,int fd,int state);

int main(int argc,char *argv[])
{
    int  listenfd;
    listenfd = socket_bind(IPADDRESS,PORT);
    listen(listenfd,LISTENQ);
    do_epoll(listenfd);
    return 0;
}

static int socket_bind(const char* ip,int port)
{
    int  listenfd;
    struct sockaddr_in servaddr;
    listenfd = socket(AF_INET,SOCK_STREAM,0);
    if (listenfd == -1)
    {
        perror("socket error:");
        exit(1);
    }
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&servaddr.sin_addr);
    servaddr.sin_port = htons(port);
    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
    {
        perror("bind error: ");
        exit(1);
    }
    return listenfd;
}

static void do_epoll(int listenfd)
{
    int epollfd;
    struct epoll_event events[EPOLLEVENTS];
    int ret;
    char buf[MAXSIZE];
    memset(buf,0,MAXSIZE);
    //建立一個描述符
    epollfd = epoll_create(FDSIZE);
    //新增監聽描述符事件
    add_event(epollfd,listenfd,EPOLLIN);
    for ( ; ; )
    {
        //獲取已經準備好的描述符事件
        ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
        handle_events(epollfd,events,ret,listenfd,buf);
    }
    close(epollfd);
}

static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
    int i;
    int fd;
    //進行選好遍歷
    for (i = 0;i < num;i++)
    {
        fd = events[i].data.fd;
        //根據描述符的型別和事件型別進行處理
        if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            handle_accpet(epollfd,listenfd);
        else if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,buf);
        else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,buf);
    }
}
static void handle_accpet(int epollfd,int listenfd)
{
    int clifd;
    struct sockaddr_in cliaddr;
    socklen_t  cliaddrlen;
    clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
    if (clifd == -1)
        perror("accpet error:");
    else
    {
        printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
        //新增一個客戶描述符和事件
        add_event(epollfd,clifd,EPOLLIN);
    }
}

static void do_read(int epollfd,int fd,char *buf)
{
    int nread;
    nread = read(fd,buf,MAXSIZE);
    if (nread == -1)
    {
        perror("read error:");
        close(fd);
        delete_event(epollfd,fd,EPOLLIN);
    }
    else if (nread == 0)
    {
        fprintf(stderr,"client close.\n");
        close(fd);
        delete_event(epollfd,fd,EPOLLIN);
    }
    else
    {
        printf("read message is : %s",buf);
        //修改描述符對應的事件,由讀改為寫
        modify_event(epollfd,fd,EPOLLOUT);
    }
}

static void do_write(int epollfd,int fd,char *buf)
{
    int nwrite;
    nwrite = write(fd,buf,strlen(buf));
    if (nwrite == -1)
    {
        perror("write error:");
        close(fd);
        delete_event(epollfd,fd,EPOLLOUT);
    }
    else
        modify_event(epollfd,fd,EPOLLIN);
    memset(buf,0,MAXSIZE);
}

static void add_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

static void delete_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

static void modify_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
複製程式碼

客戶端

// client.c
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define MAXSIZE     1024
#define IPADDRESS   "127.0.0.1"
#define SERV_PORT   8787
#define FDSIZE        1024
#define EPOLLEVENTS 20

static void handle_connection(int sockfd);
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_write(int epollfd,int fd,int sockfd,char *buf);
static void add_event(int epollfd,int fd,int state);
static void delete_event(int epollfd,int fd,int state);
static void modify_event(int epollfd,int fd,int state);

int main(int argc,char *argv[])
{
    int                 sockfd;
    struct sockaddr_in  servaddr;
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
    connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
    //處理連線
    handle_connection(sockfd);
    close(sockfd);
    return 0;
}


static void handle_connection(int sockfd)
{
    int epollfd;
    struct epoll_event events[EPOLLEVENTS];
    char buf[MAXSIZE];
    int ret;
    epollfd = epoll_create(FDSIZE);
    add_event(epollfd,STDIN_FILENO,EPOLLIN);
    for ( ; ; )
    {
        ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
        handle_events(epollfd,events,ret,sockfd,buf);
    }
    close(epollfd);
}

static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf)
{
    int fd;
    int i;
    for (i = 0;i < num;i++)
    {
        fd = events[i].data.fd;
        if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,sockfd,buf);
        else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,sockfd,buf);
    }
}

static void do_read(int epollfd,int fd,int sockfd,char *buf)
{
    int nread;
    nread = read(fd,buf,MAXSIZE);
        if (nread == -1)
    {
        perror("read error:");
        close(fd);
    }
    else if (nread == 0)
    {
        fprintf(stderr,"server close.\n");
        close(fd);
    }
    else
    {
        if (fd == STDIN_FILENO)
            add_event(epollfd,sockfd,EPOLLOUT);
        else
        {
            delete_event(epollfd,sockfd,EPOLLIN);
            add_event(epollfd,STDOUT_FILENO,EPOLLOUT);
        }
    }
}

static void do_write(int epollfd,int fd,int sockfd,char *buf)
{
    int nwrite;
    nwrite = write(fd,buf,strlen(buf));
    if (nwrite == -1)
    {
        perror("write error:");
        close(fd);
    }
    else
    {
        if (fd == STDOUT_FILENO)
            delete_event(epollfd,fd,EPOLLOUT);
        else
            modify_event(epollfd,fd,EPOLLIN);
    }
    memset(buf,0,MAXSIZE);
}

static void add_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

static void delete_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

static void modify_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
複製程式碼

執行結果:

UNIX epoll 與 Node.js 事件迴圈多路分解器

Node.js的Event Loop

眾所周知,Node.js是單執行緒的,可用作高效能伺服器。顯然,若僅僅限定為1024個描述符,對於高併發的請求顯然是不支援的。Node.js內部的Event Demultiplexer(事件多路分解器)藉助epoll來實現事件迴圈機制。其基本步驟如下:

UNIX epoll 與 Node.js 事件迴圈多路分解器

  1. 應用程式通過向Event Demultiplexer(事件多路分解器)提交請求來生成新的I / O操作。應用程式還指定一個處理程式,當操作完成時將呼叫該處理程式。向Event Demultiplexer(事件多路分解器)提交新請求是一種非阻塞呼叫,它立即將控制權返回給該應用程式。
  2. 當一組I / O操作完成時,事件多路分解器將新的事件推入Event Queue(事件佇列)
  3. 此時Event Loop遍歷Event Queue的專案。
  4. 對於每個事件,呼叫關聯的處理程式。
  5. 處理程式是應用程式程式碼的一部分,當它執行完成時將控制權返回給Event Loop。但是,在處理程式執行過程中可能會請求新的非同步操作,從而導致新的操作被插入Event Demultiplexer(事件多路分解器)
  6. Event Loop中的所有專案被處理完時,迴圈將再次阻塞Event Demultiplexer(事件多路分解器),當有新事件可用時,Event Demultiplexer(事件多路分解器)將觸發另一個週期。

通過epoll解析上述步驟操作:

  • 事件多路分解器即為通過epoll_create()建立的epoll控制程式碼的抽象,在Node.js啟動時,事件多路分解器會阻塞於epoll_wait呼叫。
  • 對於第一步,註冊事件處理程式時,呼叫epoll_ctl()方法,設定op引數為EPOLL_CTL_ADD,向事件多路分解器新增一組事件。
  • 對於第二步,一旦I / O完成,又呼叫epoll_ctl()方法,設定op引數為EPOLL_CTL_DEL,刪除對應事件,此時把控制權返還給應用程式。
  • 事件迴圈遍歷事件佇列,只要沒有事件,就阻塞於epoll_wait()
  • 不斷重複上述步驟,實現Node.js`的事件迴圈機制。

參考資料:

相關文章