1.寫在前面

    對於linux高性能服務器器,前面提到了select、poll和epoll機制,以及它們與socket配合提升性能的底層原理以及相關示例代碼。可以參考:《關於select、poll和epoll的幾個問題》https://blog.51cto.com/u_17355821/14348927

    signalfd、timerfd、eventfd是linux系統在2.6.xx系列版本中增加的機制並提供相應的系統調用接口,完善了linux的事件驅動編程模型,這三個能配合epoll來實現提高linux服務器的性能。這篇文章來講講其它的三個機制:signalfd、timerfd、eventfd。

2.signalfd

2.1 什麼是signalfd

    signalfd 是 Linux 系統中一種非常巧妙的機制,它本質上是一個系統調用,其核心作用是將信號(Signal)這種傳統的異步通知機制,轉化為文件描述符(File Descriptor)上的 I/O 事件。     簡單來説,它可以把“處理信號”這件事,變成像“讀寫文件”或“處理網絡數據”一樣的流程。     在 Linux 傳統編程中,處理信號(如 SIGINT、SIGTERM)通常使用 sigaction 註冊一個信號處理函數(Signal Handler)。這種方式是異步的:程序正在執行某段代碼時,一旦信號到來,內核會強行中斷當前流程去執行處理函數。<br>     而 signalfd 提供了一種同步的替代方案:     創建文件描述符:調用 signalfd() 系統調用,傳入感興趣的信號集合(mask)。     阻塞信號:需要先使用 sigprocmask() 將這些信號標記為“阻塞”,防止它們觸發默認行為或傳統的處理函數。     讀取信號:當這些被監控的信號到達時,signalfd 返回的文件描述符會變為“可讀”狀態。只需要調用 read() 從這個描述符中讀取數據,就能獲取信號的詳細信息。

特性 傳統信號處理 (sigaction) signalfd 機制
處理模型 異步:隨時可能中斷主流程 同步:在主循環中按需處理
調用函數 只能調用“異步信號安全”的函數 可以調用任意函數,無限制
集成性 獨立於 I/O 流程 可與 select/poll/epoll 結合
信息獲取 信息相對有限 可獲取完整的 signalfd_siginfo 結構體
競態條件 容易產生競態條件 (Race Condition) 避免了異步處理帶來的競態問題

    在高性能服務器或複雜的事件驅動程序(如 Nginx、Redis 或 Android 的 init 進程)中,通常使用 epoll 來管理成千上萬個 socket 連接。通過 signalfd,可以把 SIGTERM(退出信號)或 SIGCHLD(子進程退出)也加入到這個 epoll 循環中。這樣,程序不需要為了處理信號而跳出主循環,代碼邏輯更加清晰、線性。

    當從 signalfd 讀取數據時,會得到一個 struct signalfd_siginfo 結構體。它包含了比傳統方式更詳細的信息,例如:

  • ssi_signo:信號編號。
  • ssi_pid:發送該信號的進程 ID。
  • ssi_uid:發送該信號的進程用户 ID。
  • ssi_status:如果是 SIGCHLD,包含子進程的退出狀態。
  • ssi_addr:如果是 SIGSEGV,包含導致段錯誤的內存地址。

2.2 signalfd結合epoll的示例代碼

#define _GNU_SOURCE // 必須定義此宏才能使用 signalfd

#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    sigset_t mask;
    int sfd; // signalfd 的文件描述符
    int epfd; // epoll 的文件描述符
    struct epoll_event ev, events[1];
    ssize_t s;

    // 1. 定義我們想要通過 signalfd 處理的信號集
    // 這裏我們關注 SIGINT (Ctrl+C) 和 SIGTERM
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);

    // 2. 【關鍵步驟】阻塞這些信號
    // 這告訴內核:"不要調用默認處理函數,把這些信號掛起,直到我通過 signalfd 讀取它們"
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask");
        exit(EXIT_FAILURE);
    }

    // 3. 創建 signalfd
    // 參數1: -1 表示創建一個新的 fd
    // 參數2: 信號掩碼,指定監控哪些信號
    // 參數3: 標誌位,這裏設為 0
    sfd = signalfd(-1, &mask, 0);
    if (sfd == -1) {
        perror("signalfd");
        exit(EXIT_FAILURE);
    }

    // 4. 創建 epoll 實例,用於統一管理事件
    epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 5. 將 signalfd 添加到 epoll 的監控列表中
    ev.events = EPOLLIN; // 監聽可讀事件(即信號到達)
    ev.data.fd = sfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev) == -1) {
        perror("epoll_ctl: add");
        exit(EXIT_FAILURE);
    }

    printf("程序已啓動,PID: %d\n", getpid());
    printf("嘗試在終端按下 Ctrl+C 或者執行 'kill %d' 來發送信號\n", getpid());

    // 6. 主事件循環
    while (1) {
        // 等待事件發生 (這裏會阻塞,直到有信號到達或被中斷)
        int nfds = epoll_wait(epfd, events, 1, -1); // -1 表示無限期等待
        
        if (nfds == -1) {
            if (errno == EINTR) {
                // 被其他信號中斷,繼續循環
                continue;
            } else {
                perror("epoll_wait");
                break;
            }
        }

        // 遍歷發生的事件
        for (int n = 0; n < nfds; ++n) {
            if (events[n].data.fd == sfd) {
                // 7. 從 signalfd 中讀取信號信息
                struct signalfd_siginfo fdsi;
                
                s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
                if (s != sizeof(struct signalfd_siginfo)) {
                    perror("read");
                    exit(EXIT_FAILURE);
                }

                // 8. 根據讀取到的信息判斷是哪個信號
                switch (fdsi.ssi_signo) {
                    case SIGINT:
                        printf("\n捕獲到信號: SIGINT (Ctrl+C) 發送者 PID: %u\n", fdsi.ssi_pid);
                        printf("程序即將退出...\n");
                        goto out; // 跳出循環並退出
                        break;
                    case SIGTERM:
                        printf("捕獲到信號: SIGTERM (kill 命令) 發送者 PID: %u\n", fdsi.ssi_pid);
                        printf("程序即將退出...\n");
                        goto out;
                        break;
                    default:
                        printf("捕獲到未知信號: %d\n", fdsi.ssi_signo);
                        break;
                }
            }
        }
    }

out:
    close(sfd);
    close(epfd);
    printf("程序結束。\n");
    exit(EXIT_SUCCESS);
}

3.timerfd

3.1 什麼是timerfd

    timerfd 是 Linux 系統中一個非常優雅的定時器接口。簡單來説,它的核心思想是將“時間”也變成一種文件描述符(File Descriptor)。     就像可以用 read() 讀取文件,或者用 epoll 監聽網絡套接字(Socket)是否有數據到來一樣,也可以用 timerfd 讓一個定時器在超時時變得“可讀”。這樣,就可以把時間事件和I/O 事件放在同一個地方(比如 epoll 循環)進行統一處理。

    在 timerfd 出現之前,Linux 上常用的定時器機制(如 setitimer 或 alarm)通常是基於信號(Signal)的。這種方式存在一些痛點:     信號處理複雜:信號處理函數(Signal Handler)是異步的,容易引發競態條件(Race Condition),編寫安全的信號處理代碼非常困難。     精度問題:傳統定時器精度通常只到毫秒級。     模型割裂:在基於 epoll 的高性能服務器中,I/O 事件在主循環裏處理,而定時器卻要通過信號打斷主循環,邏輯不統一。     timerfd 解決了這些問題:它把異步的信號通知,變成了同步的文件描述符讀取。讀取到的內容是一個 uint64_t 類型的整數,表示自上次讀取以來,定時器超時的次數。如果不讀取,下次超時發生時,這個計數會累加。

特性 説明
統一事件模型 可以和 Socket 一樣被 epoll/poll 監聽,無需處理複雜的信號(Signal)。
高精度 基於內核高精度定時器(hrtimer),支持納秒級精度。
線程安全 避免了異步信號處理帶來的各種併發問題,代碼邏輯更清晰、線性。
防時間漂移 通過讀取 uint64_t 的計數,可以知道錯過了多少次超時,從而修正邏輯,防止長時間任務導致定時器“漂移”。

3.2 timerfd結合epoll的示例代碼

#define _GNU_SOURCE
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <time.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>

// 輔助函數:創建一個定時器,返回文件描述符
int create_timer(uint64_t initial_seconds, uint64_t interval_seconds) {
    int tfd;
    struct itimerspec timer_spec;

    // 1. 創建 timerfd
    // 使用 CLOCK_MONOTONIC (不受系統時間調整影響) 並設置為非阻塞
    tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (tfd == -1) {
        perror("timerfd_create");
        exit(EXIT_FAILURE);
    }

    // 2. 配置定時器參數
    // 第一次觸發時間
    timer_spec.it_value.tv_sec = initial_seconds;
    timer_spec.it_value.tv_nsec = 0;
    
    // 之後的週期間隔
    timer_spec.it_interval.tv_sec = interval_seconds;
    timer_spec.it_interval.tv_nsec = 0;

    // 3. 啓動定時器
    if (timerfd_settime(tfd, 0, &timer_spec, NULL) == -1) {
        perror("timerfd_settime");
        exit(EXIT_FAILURE);
    }

    printf("定時器已啓動:將在 %lu 秒後首次觸發,之後每隔 %lu 秒觸發一次。\n", 
           initial_seconds, interval_seconds);
    
    return tfd;
}

int main() {
    int timer_fd;
    int epfd;
    struct epoll_event ev, events[1];
    uint64_t exp; // 用於讀取超時次數

    // 1. 創建定時器 (2秒後第一次觸發,之後每2秒觸發一次)
    timer_fd = create_timer(2, 2);

    // 2. 創建 epoll 實例
    epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 3. 將 timerfd 添加到 epoll 監聽列表
    ev.events = EPOLLIN; // 監聽可讀事件
    ev.data.fd = timer_fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, timer_fd, &ev) == -1) {
        perror("epoll_ctl: add timer");
        exit(EXIT_FAILURE);
    }

    printf("程序已運行,等待定時器觸發... (按 Ctrl+C 退出)\n");

    // 4. 主事件循環
    while (1) {
        int nfds = epoll_wait(epfd, events, 1, -1); // -1 表示永久阻塞等待

        if (nfds == -1) {
            if (errno == EINTR) {
                // 如果被信號中斷(比如我們按了Ctrl+C但沒處理),繼續循環
                continue;
            }
            perror("epoll_wait");
            break;
        }

        // 處理髮生的事件
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == timer_fd) {
                // 5. 定時器觸發
                // 必須讀取,否則定時器 fd 會一直保持可讀狀態
                ssize_t s = read(timer_fd, &exp, sizeof(uint64_t));
                if (s != sizeof(uint64_t)) {
                    perror("read timerfd");
                    exit(EXIT_FAILURE);
                }

                // 'exp' 的值表示自上次讀取以來,定時器超時的次數。
                // 如果系統負載過高導致錯過了幾次觸發,exp 會大於 1。
                printf("⏰  定時器觸發! 超時次數累加值: %lu (當前時間: %ld)\n", 
                       exp, time(NULL));
            }
        }
    }

    // 清理資源
    close(timer_fd);
    close(epfd);
    printf("程序退出。\n");
    return 0;
}

4.eventfd

4.1 什麼是eventfd

    eventfd 是 Linux 內核提供的一種輕量級事件通知機制。它完美體現了 Linux 將各種機制“轉化為文件描述符(FD)”以便於統一管理的設計哲學。     簡單來説,eventfd 就是一個由內核維護的 64 位無符號整數計數器。內核把它包裝成一個文件描述符暴露給用户空間,用户空間可以通過 read/write 系統調用來操作這個計數器,從而實現進程間或線程間的通信與同步。

可以把 eventfd 想象成一個只能存放一個數字的“信箱”:     寫入(Write):往信箱裏“投遞”一個數字(uint64_t)。內核會把這個數字累加到計數器上。     讀取(Read):從信箱裏“取出”數字。這通常會把計數器清零(或者減一,取決於模式)。     狀態變化:當計數器的值大於 0 時,這個 FD 就是可讀的;當計數器為 0 時,FD 就是不可讀的。

    在 eventfd 出現之前,Linux 系統中線程間的通知與同步主要依賴於互斥鎖與條件變量、管道或信號等機制。但是它們在高性能編程中顯得有些笨重。     eventfd 本質上是為了解決“如何高效地喚醒阻塞在 epoll 上的線程”這一特定問題而誕生的。

    eventfd 的出現填補了“內核事件計數器”這一空白。相比於傳統方式,它的優勢主要體現在以下幾個方面: (1)完美的 epoll 兼容性(統一事件源) 這是 eventfd 最大的優勢。它返回的是一個文件描述符(fd),可以像 socket 一樣直接添加到 epoll 的監聽列表中。 效果: 線程只需要在一個 epoll_wait 循環中,既能處理網絡 I/O 事件,又能處理“線程喚醒”事件。不需要為同步機制單獨開一個線程或使用複雜的信號處理。 (2)極致的輕量級(無數據拷貝) eventfd 的內核實現僅僅是一個 64 位的計數器。 對比 Pipe: Pipe 涉及內核緩衝區的讀寫和內存拷貝。 對比 eventfd: 寫入 eventfd 只是將用户傳入的 8 字節整數累加到計數器上;讀取則是清零計數器。沒有複雜的數據流管理,也沒有內存拷貝開銷。 (3)豐富的語義(計數器 vs 信號) 可靠性: eventfd 是一個計數器,如果在非阻塞模式下連續寫入多次,計數器會累加。當線程最終去讀取時,能一次性拿到累計的次數。這避免了像信號那樣可能會丟失中間事件的問題。 靈活性: 通過設置 EFD_SEMAPHORE 標誌,它可以表現得像一個信號量(每次讀取只減 1),非常適合處理多個獨立事件。 (4)線程安全與簡潔性 eventfd 的讀寫操作本身就是線程安全的系統調用。相比於使用互斥鎖+條件變量那一套繁瑣的配合(需要手動加鎖、解鎖、等待、喚醒),eventfd 的接口極其簡單:read 和 write。

4.2 eventfd使用約束

    平台不通用:它是 Linux 特有的。如果代碼需要移植到 macOS 或 Windows,eventfd 是不可用的(在其他系統上通常需要用 pipe 或 socketpair 模擬)。     無法傳輸複雜數據:它只能傳輸一個 64 位的整數,如果需要傳輸字符串、結構體或大量數據,eventfd 無能為力,必須使用管道、Socket 或共享內存。     僅限於 POSIX 線程或進程:雖然它可以用於進程間通信,但通常是在有親緣關係的進程(父子進程)或通過其他方式傳遞了 FD 的進程間使用。它不像 Socket 那樣可以通過端口號被任意進程訪問。

4.3 eventfd結合epoll的示例代碼

#define _GNU_SOURCE
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

// 全局變量:eventfd 文件描述符
int efd;

// 工作線程函數
void* worker_thread(void* arg) {
    printf("[工作線程] 正在執行耗時任務...\n");
    
    // 模擬耗時操作
    sleep(3); 

    printf("[工作線程] 任務完成,準備通知主線程。\n");

    // 通知主線程:寫入一個 64 位的整數值 (通常用 1 表示事件發生)
    uint64_t val = 1;
    ssize_t s = write(efd, &val, sizeof(uint64_t));
    if (s != sizeof(uint64_t)) {
        perror("worker write");
        exit(EXIT_FAILURE);
    }

    printf("[工作線程] 通知已發送。\n");
    return NULL;
}

int main() {
    int epfd;
    struct epoll_event ev, events[1];
    pthread_t tid;

    // 1. 創建 eventfd
    // 初始值設為 0
    // EFD_NONBLOCK 表示非阻塞模式
    efd = eventfd(0, EFD_NONBLOCK); 
    if (efd == -1) {
        perror("eventfd");
        exit(EXIT_FAILURE);
    }

    // 2. 創建 epoll 實例
    epfd = epoll_create1(0);
    if (epfd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 3. 將 eventfd 添加到 epoll 監聽列表
    ev.events = EPOLLIN; // 監聽可讀事件
    ev.data.fd = efd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev) == -1) {
        perror("epoll_ctl: add");
        exit(EXIT_FAILURE);
    }

    // 4. 創建工作線程
    if (pthread_create(&tid, NULL, worker_thread, NULL) != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    printf("[主線程] 等待工作線程通知... (當前 eventfd 計數器為 0,阻塞中)\n");

    // 5. 主事件循環
    while (1) {
        int nfds = epoll_wait(epfd, events, 1, -1); // 永久阻塞等待

        if (nfds == -1) {
            if (errno == EINTR) continue; // 被中斷則重試
            perror("epoll_wait");
            break;
        }

        // 處理事件
        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == efd) {
                // 6. eventfd 就緒,説明有事件到來
                uint64_t val;
                ssize_t s = read(efd, &val, sizeof(uint64_t));
                
                if (s != sizeof(uint64_t)) {
                    perror("main read");
                    exit(EXIT_FAILURE);
                }

                // 'val' 包含了自上次讀取以來累加的計數
                printf("[主線程] 收到來自工作線程的通知!計數器值: %lu\n", val);
                printf("[主線程] 開始處理後續邏輯...\n");

                // 既然已經收到通知,可以退出循環(或者繼續監聽)
                goto out;
            }
        }
    }

out:
    pthread_join(tid, NULL); // 等待線程結束
    close(efd);
    close(epfd);
    printf("[主線程] 程序退出。\n");
    return 0;
}

5.寫在後面

    像signalfd、timerfd、eventfd、socket配合epoll來實現事件通知編程的,在linux中還有inotify機制,其實也是類似的方法,先生成一個fd,然後用epoll去監控是否有事件。有興趣也可以自行了解一下。