Epoll多路I/O複用技術

朱超迪發表於2016-09-10

Epoll多路I/O複用技術

通常學習一個新的linux技術,我們應該看看man手冊對其定義。

NAME
   epoll - I/O event notification facility

SYNOPSIS
   #include <sys/epoll.h>

DESCRIPTION
   The  epoll  API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them.  
   The epoll API can be used either as an edge-triggered or a level-triggered interface and scales well to large  numbers  of  
watched  file descriptors. 

那麼從man手冊這段文字我們可以看出,epoll它是由linux另一套的併發處理方案poll演變過來的,它與poll相類似:能夠監控多個檔案描述符的I/O變化。在Linux中,一切皆檔案(有部分不是)所以,任何一個連線,也有一個檔案描述符(一般為int型別)來存放


重點:epoll比poll的優點:支援水平觸發(level-triggered)邊沿觸發(edge-triggered)兩種方案

Edge Triggered (ET) 邊緣觸發只有資料到來才觸發,不管快取區中是否還有資料。
Level Triggered (LT) 水平觸發只要有資料都會觸發。

epoll所需的API函式:

  • epoll_create(2) creates an epoll instance and returns a file descriptor referring to that instance. (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)

  • Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.

  • epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.


1、建立epoll檔案描述符

建立一個epoll控制程式碼,引數size用來告訴核心監聽的檔案描述符的個數,跟記憶體大小有關。需要注意的是,當建立好epoll控制程式碼後,它就是會佔用一個fd值,在linux下如果檢視/proc/程式id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。

int epoll_create(int size);      //size:監聽數目

2、管理epoll中的檔案描述符集合,增加、修改、刪除

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

第一個引數是epoll_create()的返回值
第二個參數列示動作,用三個巨集來表示

EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;

第三個引數是需要監聽的fd
第四個引數是告訴核心需要監聽什麼事

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 */
} __EPOLL_PACKED;

3、收集在epoll監控的事件中已經傳送的事件(預設阻塞等待)

int epoll_wait(int epfd, struct epoll_event *events,
                int maxevents, int timeout);
/*
    events:用來從核心得到事件的集合,
    maxevents:告之核心這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,
    timeout:是超時時間
    -1:阻塞
    0:立即返回,非阻塞
    >0:指定微秒
    返回值:成功返回有多少檔案描述符就緒,時間到時返回0,出錯返回-1
*/

epoll工作原理

1、epoll同樣只告知那些就緒的檔案描述符,而且當我們呼叫epoll_wait()獲得就緒檔案描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個陣列中依次取得相應數量的檔案描述符即可,這裡也使用了記憶體對映(mmap)技術,這樣便徹底省掉了這些檔案描述符在系統呼叫時複製的開銷。

2、另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,程式只有在呼叫一定的方法後,核心才對所有監視的檔案描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個檔案描述符,一旦基於某個檔案描述符就緒時,核心會採用類似callback的回撥機制,迅速啟用這個檔案描述符,當程式呼叫epoll_wait()時便得到通知。

epoll 服務端例子

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <fcntl.h>

#include <unistd.h>
#include <errno.h>

int main()
{
    int sock_server = socket(AF_INET,SOCK_STREAM,0);

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10099);
    addr.sin_addr.s_addr = 0;

    int ret = bind(sock_server,(struct sockaddr*)&addr,sizeof(addr));

    listen(sock_server,5);

    int epollfd = epoll_create(2);

    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = sock_server;
    //吧sock_server加入eopll集合中
    epoll_ctl(epollfd,EPOLL_CTL_ADD,sock_server,&ev);

    while(1)
    {
        struct epoll_event outev[8];
        int ret = epoll_wait(epollfd,outev,8,1000);
        if (ret < 0)
        {
            if (errno == EINTR)//若被訊號打斷,則重新迴圈
                continue;
            break;
        }
        if (ret > 0)//有被喚醒的檔案描述符
        {
            for(int i = 0 ; i<ret; i++)
            {
                int fd = outev[i].data.fd;
                if (fd == sock_server)
                {
                //若為socket的檔案描述符,則用accept進行三次握手,建立連線
                    int newfd = accept(fd,NULL,NULL);
                //EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉)
                    ev.events = EPOLLIN;
                    ev.data.fd = newfd;
                //把新的fd加到epollfd中,繼續等待下一次喚醒                   
                    epoll_ctl(epollfd,EPOLL_CTL_ADD,newfd,&ev);
                }
                else
                {
                //若不是socketfd,則為已經建立的連線,可以直接讀取資料
                    char buf[1024];
                    int readlen = read(fd,buf,sizeof(buf));

                    if (readlen<=0)
                    {
                    //read<=0,證明已經沒有資料,或者出錯,關閉fd
                        close(fd);
                    }
                    else
                    {
                        printf("read data is :%s\n",buf);
                    }
                }
            }
        }
    }
    return 0;
}

測試epoll的客戶端

#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <fcntl.h>

#include <unistd.h>
#include <errno.h>
int main()
{
    int fd = socket(AF_INET,SOCK_STREAM,0);

    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(10099);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = connect(fd , (struct sockaddr*)&addr, sizeof(addr));

    write(fd,"hello server",sizeof("hello server"));

    char buf[1024];
    read(fd,buf,sizeof(buf));
    printf("server:%s\n",buf);
    close(fd);
    return 0;
}

測試結果我就暫時不貼圖了,希望這次部落格能給大家帶來一點收穫!

相關文章