c

epoll 筆記 Part 1

Posted by blueskyson on June 6, 2022

此文章摘錄、翻譯自 epoll() Tutorial – epoll() In 3 Easy Steps!,再搭配自己的一些整理。

epollpoll 的變體,允許單一個執行緒值監看多個 file descriptor 的事件,在使用者監看的事件發生時回傳以通知使用者。事件通知的時機又分為 level trigger 和 edge trigger,在 edge trigger 模式中,只有事件觸發,或是 timeout 時 epoll_wait 才會回傳;在 level trigger 模式,epoll_wait 在事件狀態未變更前都會回傳。

Step 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>     // for fprintf()
#include <unistd.h>    // for close()
#include <sys/epoll.h> // for epoll_create1()

int main()
{
    int epoll_fd = epoll_create1(0);

    if (epoll_fd == -1)
    {
        fprintf(stderr, "Failed to create epoll file descriptor\n");
        return 1;
    }

    if (close(epoll_fd))
    {
        fprintf(stderr, "Failed to close epoll file descriptor\n");
        return 1;
    }

    return 0;
}

首先使用 epoll_create1() 建立一個 epoll 的 file descriptor,然後透過 close() 關閉,如果終端機有輸出錯誤訊息,代表 kernel 版本過舊。

另外注意到 epoll_create1() 有個參數 0,原本的用途是告訴 kernel 預期會加入到 epoll 的 file descriptor 數量,kernel 再根據這個參數分配資源給 epoll。如今這個參數已經沒有實質意義了,kernel 會自己動態分配資源,但這個參數欄位可以確保在舊的 kernel 的程式碼可以直接移植到新的 kernel。

Step 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>     // for fprintf()
#include <unistd.h>    // for close()
#include <sys/epoll.h> // for epoll_create1(), epoll_ctl(), struct epoll_event

int main()
{
    struct epoll_event event;
    int epoll_fd = epoll_create1(0);

    if (epoll_fd == -1)
    {
        fprintf(stderr, "Failed to create epoll file descriptor\n");
        return 1;
    }

    event.events = EPOLLIN;
    event.data.fd = 0;

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event))
    {
        fprintf(stderr, "Failed to add file descriptor to epoll\n");
        close(epoll_fd);
        return 1;
    }

    if (close(epoll_fd))
    {
        fprintf(stderr, "Failed to close epoll file descriptor\n");
        return 1;
    }
    return 0;
}

首先透過 event 變數設定要監聽的事件類型,event.events = EPOLLIN; 表示當監視的 file descriptor 可以被 read 時,就觸發事件。另外 epoll 預設是 level trigger,若要用 edge trigger,就要改寫為 event.events = EPOLLIN | EPOLLET;,此外還有 EPOLLONESHOT 等事件類型,詳情見 epoll_ctl(2) — Linux manual page

event.data.fd = 0; 是給使用者自行設置的欄位,在此範例中我們設定為 stdin,如此一來在 Step 3 每當事件觸發時就透過 read() 讀取 events[i].data.fdevent.data 本身是一個 union,可以用來存一些 state、buffer 的位址、要讀取的 file descriptor 等。

接下來要做的就是透過 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event) 告訴 epoll 要監視哪個 file descriptor 以及要監視哪些類型的事件,在這篇筆記中我使用 0,也就是 stdin 作為 epoll 監視的 file descriptor,詳情見 epoll_ctl(2) — Linux manual page

Step 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#define MAX_EVENTS 5
#define READ_SIZE 10
#define TIMEOUT 10000
#include <stdio.h>     // for fprintf()
#include <unistd.h>    // for close(), read()
#include <sys/epoll.h> // for epoll_create1(), epoll_ctl(), struct epoll_event
#include <string.h>    // for strncmp

int main()
{
    int running = 1, event_count, i;
    size_t bytes_read;
    char read_buffer[READ_SIZE + 1];
    struct epoll_event event, events[MAX_EVENTS];
    int epoll_fd = epoll_create1(0);

    if (epoll_fd == -1)
    {
        fprintf(stderr, "Failed to create epoll file descriptor\n");
        return 1;
    }

    event.events = EPOLLIN;
    event.data.fd = 0;

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event))
    {
        fprintf(stderr, "Failed to add file descriptor to epoll\n");
        close(epoll_fd);
        return 1;
    }

    while (running)
    {
        printf("\nPolling for input...\n");
        event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, TIMEOUT);
        printf("%d ready events\n", event_count);
        for (i = 0; i < event_count; i++)
        {
            printf("Reading file descriptor '%d' -- ", events[i].data.fd);
            bytes_read = read(events[i].data.fd, read_buffer, READ_SIZE);
            printf("%zd bytes read.\n", bytes_read);
            read_buffer[bytes_read] = '\0';
            printf("Read '%s'\n", read_buffer);

            if (!strncmp(read_buffer, "stop\n", 5))
                running = 0;
        }
    }

    if (close(epoll_fd))
    {
        fprintf(stderr, "Failed to close epoll file descriptor\n");
        return 1;
    }

    return 0;
}

使用 epoll_wait() 來等待來自 epoll_fd 的事件發生;events 用來存取已經就緒的事件的資訊,比如稍早寫的 event.data.fd = 0;每次最多只回傳 MAX_EVENTS 個事件到 events 中;如果很長一段時間都沒有事件被觸發,至多會 block 住 TIMEOUT 毫秒,並回傳 0。

執行程式:

$ ./epoll

Polling for input...

hello!

1 ready events
Reading file descriptor '0' -- 7 bytes read.
Read 'hello!
'

a long long text

Polling for input...
a long long text
1 ready events
Reading file descriptor '0' -- 10 bytes read.
Read 'a long lon'

Polling for input...
1 ready events
Reading file descriptor '0' -- 7 bytes read.
Read 'g text
'

stop

Polling for input...
1 ready events
Reading file descriptor '0' -- 5 bytes read.
Read 'stop
'