epoll(2) 使用及源碼分析的引子

epoll(2) 使用及源碼分析的引子

本文代碼取自內核版本 4.17

epoll(2) - I/O 事件通知設施。

epoll 是內核在2.6版本后實現的,是對 select(2)/poll(2) 更高效的改進,同時它自身也是一種文件,不恰當的比方可以看作 eventfd + poll。

多路復用也是一直在改進的,經歷的幾個階段

  1. select(2) - 只能關注 1024 個文件描述符,并且范圍固定在 0 - 1023,每次函數調用都需要把所有關注的數據復制進內核空間,再對所有的描述符集合進行遍歷判斷。
  2. poll(2) - 改進 select(2) 前面兩個缺點,可以自定義關注的描述符,數量也不受限制(不超過系統的限制),每次調用同樣需要復制所有的事件進內核空間,全部遍歷。
  3. epoll(2) - 不需要每次調用時所有關注的文件描述符進行內核-用戶空間的復制,而是直接將所有的文件描述符和事件常駐內核空間,同時也不需要每次遍歷所有文件描述符。

提供的系統調用

#include <sys/epoll.h>

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

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

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() - 用來創建一個 epoll 實例,返回一個新的文件描述符。第一個參數 @size 自 2.6.8 開始無意義,但必須大于 0。

epoll_create1() - 參數 @flags 為 0 則等效于 epoll_create(),flags 可以為 EPOLL_CLOEXEC, 在exec新程序時關閉文件描述符。

epoll_ctl() - epoll 的控制接口,用戶調用該系統調用來控制監聽的文件描述符。參數 @epfd 為 epoll_create() 返回的新文件描述符,參數 @op 為 epoll_ctl() 提供的控制操作:

  • EPOLL_CTL_ADD, 向 epoll 中注冊一個新的文件描述符;
  • EPOLL_CTL_MOD, 修改關聯文件描述符中的事件;
  • EPOLL_CTL_DEL, 移除 epoll 中的描述符,且無視 @event 參數;
    參數 @fd 為需要控制的文件描述符,參數 @event 為相關聯的 struct epoll_event 結構。

epoll_wait() - 等待epoll中監聽文件描述符就緒的 I/O 事件。參數 @epfd 為epoll實例對應的文件描述符,由 epoll_create() 創建,、
參數 @events 為就緒的事件集合的地址,參數 @maxevents 為需要就緒事件集合的大小,必須大于 0,參數 @timeout 為超時時間,單位為 微秒。

水平觸發模式和邊緣觸發模式

epoll 默認使用水平觸發模式,邊緣觸發模式需要設置 events |= EPOLLET

邊緣觸發模式的特點是邊緣觸發模式只在關注的文件描述符發生改變時才產生就緒的事件,考慮高低電平的圖片,邊緣是有一個瞬間的概念,而水平則有一個持續的狀態。
這就導致了,邊緣觸發有可能會丟失需要通知的事件。分析如下

現有 5 個步驟:

  1. 管道讀端的文件描述符 rfd 被注冊到 epoll 實例中。
  2. 管道寫端寫入了 2 kB 數據。
  3. 調用 epoll_wait(2) 返回了 rfd 作為就緒的文件描述符。
  4. 管道讀端讀取了 1 kB 數據。
  5. 調用 epoll_wait(2)。

如果文件描述符 rfd 使用 EPOLLET 邊緣觸發模式注冊到 epoll 中,那么在執行上面的 5 的時候,盡管管道的讀端緩沖區還有數據,epoll_wait(2) 還是可能會掛起,
同時寫端可能會基于其已發送的數據期望響應。產生這個情況的原因是邊緣觸發模式只在關注的文件描述符發生改變時才產生就緒的事件。在上面的步驟中,2 寫入的數據,
因此在 rdf 上生成一個事件,由于在 4 中的讀取操作不會消耗整個緩沖區數據,故在 5 對 epoll_wait(2) 調用可能發生阻塞。

使用邊緣觸發模式的程序應該使用非阻塞文件描述符來避免阻塞讀寫造成處理多個文件描述符時產生的饑餓問題。
所以建議使用的邊緣觸發模式時遵從一下兩點:

  1. 文件描述符為非阻塞方式,并且
  2. 只在 read(2) / write(2) 返回 EAGAIN 后進行等待。

在使用邊緣觸發模式時,在接收到多個數據塊的時候會產生多個事件,因此用戶可以選擇指定 EPOLLONESHOT 標志,在 epoll_wait(2) 收到事件后禁用相關的文件描述符。
而設置 EPOLLONESHOT 標志后,需要用戶手動調用 epoll_ctl(2) 重新設置文件描述符。

在示例代碼中可以看到邊緣觸發和水平觸發的區別

示例代碼

把 eventfd 注冊到 epoll 中,進行兩個線程間的通信。使用 eventfd 的 EFD_SEMAPHORE 的標志來模擬 read(2) 讀取部分數據。

程序初始值設置 1000,在水平模式下,會先用 1000 次 read(2),把計數器的值消耗為 0,之后 write(2) 寫入 cnt,就調用 cnt 次 read(2),總之只要水位(count)不為0就可讀。
而 epoll 設置 EPOLLET 后只有發生了 write(2) 操作 epoll_wait(2) 才能產生一個可讀事件,而計數器則是逐漸增大。

// 代碼取自上一篇文章的 eventfd 示例

#include <unistd.h>
#include <pthread.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <stdio.h>

int efd;

void *run_eventfd_write(void *arg) {
    uint64_t count = 1;
    while (1) {
        printf("write count: %zu\n", count);
        write(efd, &count, sizeof(count));
        count++;
        sleep(2);  // 將睡眠時間調成大于 timeout 時間,
    }
}

int main() {
    unsigned int initval = 1000;
    int flags = 0;
    int timeout = 1000;
    flags |= EFD_SEMAPHORE; // 使計數器器的值每一次減 1 而不清空,保持計數器的值不直接置為 0
    efd = eventfd(initval, flags);

    int epfd = epoll_create(32);
    struct epoll_event epfds;
    struct epoll_event ev;
    ev.data.fd = efd;
    ev.events = EPOLLIN;
    ev.events |= EPOLLET; // 對比注釋這個行代碼的打印輸出
    epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev);

    pthread_t pid;
    pthread_create(&pid, NULL, run_eventfd_write, NULL);

    while (1) {
        int ret = epoll_wait(epfd, &epfds, 1, timeout);
        if (ret > 0) {
            uint64_t count;
            read(efd, &count, sizeof(count));
            printf("read count: %zu\n", count);
        } else if (ret == 0) {
            printf("not avaiable data\n");
        }
    }
}

特殊功能的標志

EPOLLONESHOT

單次命中,內核 2.6.2 引入,當事件就緒被 epoll_wait(2) 返回時,這個事件就不再被關注了。

EPOLLWAKEUP

內核 3.5 引入,如果 EPOLLONESHOTEPOLLET 標志被清除后,并且進程擁有CAP_BLOCK_SUSPEND(阻止系統掛起的特性)權限,這個標志能夠保證事件在掛起或者處理的時候,系統不會掛起或休眠。

EPOLLEXCLUSIVE

排他的喚醒,內核 4.5 引入,解決驚群的問題,下一篇文章會分析到。

雜記

本文不準備把源碼分析放在這里,由于是文件的原因 epoll(2) 對比 poll(2) 和 select(2) 來說要復雜很多,這里拋出幾個點來引出源碼分析的重點:

  1. epoll(2) 得到就緒事件的復雜度為何是 \(O(1)\)
  2. epoll(2) 和普通的文件相比的區別在哪里,比如和 eventfd(2) 比較
  3. epoll(2) 相對 poll(2)/select(2) 多提供了 EPOLLET 的觸發模式,現象在上面可以看到區別,實現是如何做到的。
  4. epoll(2) 相互關注時,有就緒事件到來會產生相互喚醒的問題,為何會出現這樣的問題
  5. 對于問題 4,內核是如何解決這種相互喚醒的問題。

剛開始把源碼分析放了出來,但是發現寫的過于混亂沒有重點,代碼貼的太多,所以想專門寫一文來著分析上面的問題。

posted on 2019-10-26 16:18  小胖西瓜  閱讀(...)  評論(...編輯  收藏

導航

統計

11选5走势图