Epoll的使用詳解

作者: 大呀大帝國 </br>
email:drnijq@126.com

1.Epoll簡介

EPOLL 的API用來執(zhí)行類似poll()的任務。能夠用于檢測在多個文件描述符中任何IO可用的情況。Epoll API可以用于邊緣觸發(fā)(edge-triggered)和水平觸發(fā)(level-triggered), 同時epoll可以檢測更多的文件描述符千元。以下的系統調用函數提供了創(chuàng)建和管理epoll實例:

  • epoll_create() 可以創(chuàng)建一個epoll實例并返回相應的文件描述符(epoll_create1() 擴展了epoll_create() 的功能)忧饭。
  • 注冊相關的文件描述符使用epoll_ctl()
  • epoll_wait() 可以用于等待IO事件。如果當前沒有可用的事件元媚,這個函數會阻塞調用線程疼蛾。

邊緣觸發(fā)(edge-triggered 簡稱ET)和水平觸發(fā)(level-triggered 簡稱LT):

epoll的事件派發(fā)接口可以運行在兩種模式下:邊緣觸發(fā)(edge-triggered)和水平觸發(fā)(level-triggered),兩種模式的區(qū)別請看下面,我們先假設下面的情況:

  1. 一個代表管道讀取的文件描述符已經注冊到epoll實例上了愉舔。
  2. 在管道的寫入端寫入了2kb的數據钢猛。
  3. epoll_wait 返回一個可用的rfd文件描述符。
  4. 從管道讀取了1kb的數據轩缤。
  5. 調用epoll_wait 完成命迈。

如果rfd被設置了ET,在調用完第五步的epool_wait 后會被掛起火的,盡管在緩沖區(qū)還有可以讀取的數據壶愤,同時另外一段的管道還在等待發(fā)送完畢的反饋。這是因為ET模式下只有文件描述符發(fā)生改變的時候卫玖,才會派發(fā)事件公你。所以第五步操作,可能會去等待已經存在緩沖區(qū)的數據假瞬。在上面的例子中陕靠,一個事件在第二步被創(chuàng)建,再第三步中被消耗脱茉,由于第四步中沒有讀取完緩沖區(qū)剪芥,第五步中的epoll_wait可能會一直被阻塞下去。

下面情況下推薦使用ET模式:

  1. 使用非阻塞的IO琴许。
  2. epoll_wait() 只需要在read或者write返回的時候税肪。

相比之下,當我們使用LT的時候(默認),epoll會比poll更簡單更快速榜田,而且我們可以使用在任何一個地方益兄。

2.API介紹

先簡單的看下EPOLL的API

2.1 創(chuàng)建epoll


#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flags);

epoll_create() 可以創(chuàng)建一個epoll實例。在linux 內核版本大于2.6.8 后箭券,這個size 參數就被棄用了净捅,但是傳入的值必須大于0。

在 epoll_create () 的最初實現版本時辩块, size參數的作用是創(chuàng)建epoll實例時候告訴內核需要使用多少個文件描述符蛔六。內核會使用 size 的大小去申請對應的內存(如果在使用的時候超過了給定的size荆永, 內核會申請更多的空間)。現在国章,這個size參數不再使用了(內核會動態(tài)的申請需要的內存)具钥。但要注意的是,這個size必須要大于0液兽,為了兼容舊版的linux 內核的代碼骂删。

epoll_create() 會返回新的epoll對象的文件描述符。這個文件描述符用于后續(xù)的epoll操作抵碟。如果不需要使用這個描述符桃漾,請使用close關閉坏匪。

epoll_create1() 如果flags的值是0拟逮,epoll_create1()等同于epoll_create()除了過時的size被遺棄了。當然flasg可以使用 EPOLL_CLOEXEC适滓,請查看 open() 中的O_CLOEXEC來查看 EPOLL_CLOEXEC有什么用敦迄。

返回值: 如果執(zhí)行成功,返回一個非負數(實際為文件描述符), 如果執(zhí)行失敗凭迹,會返回-1罚屋,具體原因請查看error.

2.2 設置epoll事件


#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

這個系統調用能夠控制給定的文件描述符epfd指向的epoll實例,op是添加事件的類型嗅绸,fd是目標文件描述符脾猛。

有效的op值有以下幾種:

  • EPOLL_CTL_ADD 在epfd中注冊指定的fd文件描述符并能把eventfd關聯起來。
  • EPOLL_CTL_MOD 改變*** fdevetn***之間的聯系鱼鸠。
  • EPOLL_CTL_DEL 從指定的epfd中刪除fd文件描述符猛拴。在這種模式中event是被忽略的,并且為可以等于NULL蚀狰。

event這個參數是用于關聯制定的fd文件描述符的愉昆。它的定義如下:

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這個參數是一個字節(jié)的掩碼構成的。下面是可以用的事件:

  • EPOLLIN - 當關聯的文件可以執(zhí)行 read ()操作時麻蹋。
  • EPOLLOUT - 當關聯的文件可以執(zhí)行 write ()操作時跛溉。
  • EPOLLRDHUP - (從 linux 2.6.17 開始)當socket關閉的時候,或者半關閉寫段的(當使用邊緣觸發(fā)的時候扮授,這個標識在寫一些測試代碼去檢測關閉的時候特別好用)
  • EPOLLPRI - 當 read ()能夠讀取緊急數據的時候芳室。
  • EPOLLERR - 當關聯的文件發(fā)生錯誤的時候,epoll_wait() 總是會等待這個事件刹勃,并不是需要必須設置的標識堪侯。
  • EPOLLHUP - 當指定的文件描述符被掛起的時候。epoll_wait() 總是會等待這個事件深夯,并不是需要必須設置的標識抖格。當socket從某一個地方讀取數據的時候(管道或者socket),這個事件只是標識出這個已經讀取到最后了(EOF)诺苹。所有的有效數據已經被讀取完畢了,之后任何的讀取都會返回0(EOF)雹拄。
  • EPOLLET - 設置指定的文件描述符模式為邊緣觸發(fā)收奔,默認的模式是水平觸發(fā)。
  • EPOLLONESHOT - (從 linux 2.6.17 開始)設置指定文件描述符為單次模式滓玖。這意味著坪哄,在設置后只會有一次從epoll_wait() 中捕獲到事件,之后你必須要重新調用 epoll_ctl() 重新設置势篡。

返回值:如果成功翩肌,返回0。如果失敗禁悠,會返回-1念祭, errno將會被設置

有以下幾種錯誤:

  • EBADF - epfd 或者 fd 是無效的文件描述符。
  • EEXIST - op是EPOLL_CTL_ADD碍侦,同時 fd 在之前粱坤,已經被注冊到epoll中了。
  • EINVAL - epfd不是一個epoll描述符瓷产≌拘或者fdepfd相同,或者op參數非法濒旦。
  • ENOENT - op是EPOLL_CTL_MOD或者EPOLL_CTL_DEL株旷,但是fd還沒有被注冊到epoll上。
  • ENOMEM - 內存不足尔邓。
  • EPERM - 目標的fd不支持epoll晾剖。

2.3 等待epoll事件

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
                      
int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);

epoll_wait 這個系統調用是用來等待epfd中的事件。events指向調用者可以使用的事件的內存區(qū)域铃拇。maxevents告知內核有多少個events钞瀑,必須要大于0.

timeout這個參數是用來制定epoll_wait 會阻塞多少毫秒,會一直阻塞到下面幾種情況:

  1. 一個文件描述符觸發(fā)了事件慷荔。
  2. 被一個信號處理函數打斷雕什,或者timeout超時。

timeout等于-1的時候這個函數會無限期的阻塞下去显晶,當timeout等于0的時候贷岸,就算沒有任何事件,也會立刻返回磷雇。

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 */
};

每次epoll_wait() 返回的時候偿警,會包含用戶在epoll_ctl中設置的events。

還有一個系統調用epoll_pwait ()唯笙。epoll_pwait()和epoll_wait ()的關系就像select()和 pselect()的關系螟蒸。和pselect()一樣盒使,epoll_pwait()可以讓應用程序安全的等待知道某一個文件描述符就緒或者捕捉到信號。

下面的 epoll_pwait () 調用:

ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);

在內部等同于:

pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = epoll_wait(epfd, &events, maxevents, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);

如果 sigmask為NULL, epoll_pwait()等同于epoll_wait()七嫌。

返回值:有多少個IO事件已經準備就緒少办。如果返回0說明沒有IO事件就緒,而是timeout超時诵原。遇到錯誤的時候英妓,會返回-1,并設置 errno绍赛。

有以下幾種錯誤:

  • EBADF - epfd是無效的文件描述符
  • EFAULT - 指針events指向的內存沒有訪問權限
  • EINTR - 這個調用被信號打斷蔓纠。
  • EINVAL - epfd不是一個epoll的文件描述符,或者maxevents小于等于0

3. 官方demo

#define MAX_EVENTS 10
struct epoll_event  ev, events[MAX_EVENTS];
int         listen_sock, conn_sock, nfds, epollfd;


/* Code to set up listening socket, 'listen_sock',
 * (socket(), bind(), listen()) omitted */

epollfd = epoll_create1( 0 );
if ( epollfd == -1 )
{
    perror( "epoll_create1" );
    exit( EXIT_FAILURE );
}

ev.events   = EPOLLIN;
ev.data.fd  = listen_sock;
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, listen_sock, &ev ) == -1 )
{
    perror( "epoll_ctl: listen_sock" );
    exit( EXIT_FAILURE );
}

for (;; )
{
    nfds = epoll_wait( epollfd, events, MAX_EVENTS, -1 );
    if ( nfds == -1 )
    {
        perror( "epoll_wait" );
        exit( EXIT_FAILURE );
    }

    for ( n = 0; n < nfds; ++n )
    {
        if ( events[n].data.fd == listen_sock )
        {
            conn_sock = accept( listen_sock,
                        (struct sockaddr *) &local, &addrlen );
            if ( conn_sock == -1 )
            {
                perror( "accept" );
                exit( EXIT_FAILURE );
            }
            setnonblocking( conn_sock );
            ev.events   = EPOLLIN | EPOLLET;
            ev.data.fd  = conn_sock;
            if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, conn_sock,
                    &ev ) == -1 )
            {
                perror( "epoll_ctl: conn_sock" );
                exit( EXIT_FAILURE );
            }
        } else {
            do_use_fd( events[n].data.fd );
        }
    }
}

4.完整可運行的DEMO

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

#define MAX_EVENT 20
#define READ_BUF_LEN 256

/**
 * 設置 file describe 為非阻塞模式
 * @param fd 文件描述
 * @return 返回0成功吗蚌,返回-1失敗
 */
static int make_socket_non_blocking (int fd) {
    int flags, s;
    // 獲取當前flag
    flags = fcntl(fd, F_GETFL, 0);
    if (-1 == flags) {
        perror("Get fd status");
        return -1;
    }

    flags |= O_NONBLOCK;

    // 設置flag
    s = fcntl(fd, F_SETFL, flags);
    if (-1 == s) {
        perror("Set fd status");
        return -1;
    }
    return 0;
}

int main() {
    // epoll 實例 file describe
    int epfd = 0;
    int listenfd = 0;
    int result = 0;
    struct epoll_event ev, event[MAX_EVENT];
    // 綁定的地址
    const char * const local_addr = "192.168.0.45";
    struct sockaddr_in server_addr = { 0 };

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listenfd) {
        perror("Open listen socket");
        return -1;
    }
    /* Enable address reuse */
    int on = 1;
    // 打開 socket 端口復用, 防止測試的時候出現 Address already in use
    result = setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
    if (-1 == result) {
        perror ("Set socket");
        return 0;
    }

    server_addr.sin_family = AF_INET;
    inet_aton (local_addr, &(server_addr.sin_addr));
    server_addr.sin_port = htons(8080);
    result = bind(listenfd, (const struct sockaddr *)&server_addr, sizeof (server_addr));
    if (-1 == result) {
        perror("Bind port");
        return 0;
    }
    result = make_socket_non_blocking(listenfd);
    if (-1 == result) {
        return 0;
    }

    result = listen(listenfd, 200);
    if (-1 == result) {
        perror("Start listen");
        return 0;
    }

    // 創(chuàng)建epoll實例
    epfd = epoll_create1(0);
    if (1 == epfd) {
        perror("Create epoll instance");
        return 0;
    }

    ev.data.fd = listenfd;
    ev.events = EPOLLIN | EPOLLET /* 邊緣觸發(fā)選項腿倚。 */;
    // 設置epoll的事件
    result = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    if(-1 == result) {
        perror("Set epoll_ctl");
        return 0;
    }

    for ( ; ; ) {
        int wait_count;
        // 等待事件
        wait_count = epoll_wait(epfd, event, MAX_EVENT, -1);

        for (int i = 0 ; i < wait_count; i++) {
            uint32_t events = event[i].events;
            // IP地址緩存
            char host_buf[NI_MAXHOST];
            // PORT緩存
            char port_buf[NI_MAXSERV];

            int __result;
            // 判斷epoll是否發(fā)生錯誤
            if ( events & EPOLLERR || events & EPOLLHUP || (! events & EPOLLIN)) {
                printf("Epoll has error\n");
                close (event[i].data.fd);
                continue;
            } else if (listenfd == event[i].data.fd) {
                // listen的 file describe 事件觸發(fā), accpet事件

                for ( ; ; ) { // 由于采用了邊緣觸發(fā)模式褪测,這里需要使用循環(huán)
                    struct sockaddr in_addr = { 0 };
                    socklen_t in_addr_len = sizeof (in_addr);
                    int accp_fd = accept(listenfd, &in_addr, &in_addr_len);
                    if (-1 == accp_fd) {
                        perror("Accept");
                        break;
                    }
                    __result = getnameinfo(&in_addr, sizeof (in_addr),
                                           host_buf, sizeof (host_buf) / sizeof (host_buf[0]),
                                           port_buf, sizeof (port_buf) / sizeof (port_buf[0]),
                                           NI_NUMERICHOST | NI_NUMERICSERV);

                    if (! __result) {
                        printf("New connection: host = %s, port = %s\n", host_buf, port_buf);
                    }

                    __result = make_socket_non_blocking(accp_fd);
                    if (-1 == __result) {
                        return 0;
                    }

                    ev.data.fd = accp_fd;
                    ev.events = EPOLLIN | EPOLLET;
                    // 為新accept的 file describe 設置epoll事件
                    __result = epoll_ctl(epfd, EPOLL_CTL_ADD, accp_fd, &ev);

                    if (-1 == __result) {
                        perror("epoll_ctl");
                        return 0;
                    }
                }
                continue;
            } else {
                // 其余事件為 file describe 可以讀取
                int done = 0;
                // 因為采用邊緣觸發(fā)猴誊,所以這里需要使用循環(huán)。如果不使用循環(huán)侮措,程序并不能完全讀取到緩存區(qū)里面的數據。
                for ( ; ;) {
                    ssize_t result_len = 0;
                    char buf[READ_BUF_LEN] = { 0 };

                    result_len = read(event[i].data.fd, buf, sizeof (buf) / sizeof (buf[0]));

                    if (-1 == result_len) {
                        if (EAGAIN != errno) {
                            perror ("Read data");
                            done = 1;
                        }
                        break;
                    } else if (! result_len) {
                        done = 1;
                        break;
                    }

                    write(STDOUT_FILENO, buf, result_len);
                }
                if (done) {
                    printf("Closed connection\n");
                    close (event[i].data.fd);
                }
            }
        }

    }
    close (epfd);
    return 0;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末乖杠,一起剝皮案震驚了整個濱河市分扎,隨后出現的幾起案子,更是在濱河造成了極大的恐慌胧洒,老刑警劉巖畏吓,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異卫漫,居然都是意外死亡菲饼,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門列赎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宏悦,“玉大人,你說我怎么就攤上這事包吝”罚” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵诗越,是天一觀的道長砖瞧。 經常有香客問我日川,道長胯府,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮葬燎,結果婚禮上,老公的妹妹穿的比我還像新娘脑蠕。我一直安慰自己桑涎,他們只是感情好,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布逃片。 她就那樣靜靜地躺著屡拨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪褥实。 梳的紋絲不亂的頭發(fā)上呀狼,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天,我揣著相機與錄音损离,去河邊找鬼哥艇。 笑死,一個胖子當著我的面吹牛僻澎,可吹牛的內容都是我干的貌踏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼窟勃,長吁一口氣:“原來是場噩夢啊……” “哼祖乳!你這毒婦竟也來了?” 一聲冷哼從身側響起秉氧,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤眷昆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后汁咏,有當地人在樹林里發(fā)現了一具尸體亚斋,經...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年攘滩,在試婚紗的時候發(fā)現自己被綠了帅刊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡漂问,死狀恐怖赖瞒,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情级解,我是刑警寧澤冒黑,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站勤哗,受9級特大地震影響抡爹,放射性物質發(fā)生泄漏。R本人自食惡果不足惜芒划,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一冬竟、第九天 我趴在偏房一處隱蔽的房頂上張望欧穴。 院中可真熱鬧,春花似錦泵殴、人聲如沸涮帘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽调缨。三九已至,卻和暖如春吆你,著一層夾襖步出監(jiān)牢的瞬間弦叶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工妇多, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伤哺,地道東北人。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓者祖,卻偏偏與公主長得像立莉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子七问,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內容