Try   HackMD

透過 timerfd 處理週期性任務

timerfd

timerfd 是 Linux 系統呼叫,允許用 file descriptor 的方式來操作 timer,並可搭配原本的 I/O Multiplexor 機制。一旦 timerfd_create() 將時間轉為 fd,後者在 timeout 時即可 read,這樣我們透過 epoll 一類的系統呼叫,就能處理週期性任務。

Linux 核心設計: 檔案系統概念及實作手法

timerfdselect 的timeout 區別:

  • timerfd_create 將計時器轉換為檔案,一遇到超時,該特別的檔案即可讀取,於是就可搭配 I/O multiplexor,用一致的方式來處理 I/O 事件和超時事件,參見高效 Web 伺服器開發
  • select 系統呼叫的 timeout 的精度較低

範例程式碼:

#include <stdint.h> /* Definition of uint64_t */
#include <stdio.h>
#include <stdlib.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>

#define handle_error(msg)   \
    do {                    \
        perror(msg);        \
        exit(EXIT_FAILURE); \
    } while (0)

static void print_elapsed_time(void)
{
    static struct timespec start;
    struct timespec curr;
    static int first_call = 1;
    int secs, nsecs;

    if (first_call) {
        first_call = 0;
        if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
            handle_error("clock_gettime");
    }

    if (clock_gettime(CLOCK_MONOTONIC, &curr) == -1)
        handle_error("clock_gettime");

    secs = curr.tv_sec - start.tv_sec;
    nsecs = curr.tv_nsec - start.tv_nsec;
    if (nsecs < 0) {
        secs--;
        nsecs += 1000000000;
    }
    printf("%d.%03d: ", secs, (nsecs + 500000) / 1000000);
}

int main(int argc, char *argv[])
{
    struct itimerspec new_value;
    int max_exp, fd;
    struct timespec now;
    uint64_t exp, tot_exp;
    ssize_t s;

    if ((argc != 2) && (argc != 4)) {
        fprintf(stderr, "%s init-secs [interval-secs max-exp]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    if (clock_gettime(CLOCK_REALTIME, &now) == -1)
        handle_error("clock_gettime");

    /* Create a CLOCK_REALTIME absolute timer with initial
       expiration and interval as specified in command line */

    new_value.it_value.tv_sec = now.tv_sec + atoi(argv[1]);
    new_value.it_value.tv_nsec = now.tv_nsec;
    if (argc == 2) {
        new_value.it_interval.tv_sec = 0;
        max_exp = 1;
    } else {
        new_value.it_interval.tv_sec = atoi(argv[2]);
        max_exp = atoi(argv[3]);
    }
    new_value.it_interval.tv_nsec = 0;

    fd = timerfd_create(CLOCK_REALTIME, 0);
    if (fd == -1) handle_error("timerfd_create");

    if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1)
        handle_error("timerfd_settime");

    print_elapsed_time();
    printf("timer started\n");

    for (tot_exp = 0; tot_exp < max_exp;) {
        s = read(fd, &exp, sizeof(uint64_t));
        if (s != sizeof(uint64_t)) handle_error("read");

        tot_exp += exp;
        print_elapsed_time();
        printf("read: %llu; total=%llu\n", (unsigned long long) exp,
               (unsigned long long) tot_exp);
    }

    exit(EXIT_SUCCESS);
}

預期輸出: (參數: 1 1 10)

0.000: timer started
1.000: read: 1; total=1
2.000: read: 1; total=2
3.000: read: 1; total=3
4.000: read: 1; total=4
5.000: read: 1; total=5
6.000: read: 1; total=6
7.000: read: 1; total=7
8.000: read: 1; total=8
9.000: read: 1; total=9
10.000: read: 1; total=10

搭配 epoll 的示範:

#include <pthread.h>
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>

static void add_event(int epoll_fd, int fd, int state)
{
    struct epoll_event ev = {
        .events = state,
        .data.fd = fd,
    };
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);
    return;
}

static int calc_proc_time(int *start)
{
    struct timespec end;
    clock_gettime(CLOCK_REALTIME, &end);
    if (start)
        return end.tv_sec * 1000 + end.tv_nsec / 1000000 - *start;
    return end.tv_sec * 1000 + end.tv_nsec / 1000000;
}

static void *worker(void *user)
{
    int epoll_fd = *(int *) user;
    struct epoll_event events[1] = {0};
    while (1) {
        int ms = calc_proc_time(NULL);
        int fire_events = epoll_wait(epoll_fd, events, 1, -1);
        ms = calc_proc_time(&ms);
        if (fire_events > 0) {
            printf("time out: %d ms\n", ms);
            uint64_t exp;
            ssize_t size = read(events[0].data.fd, &exp, sizeof(uint64_t));
            if (size != sizeof(uint64_t))
                perror("read error");
        }
    }
    return NULL;
}

int timer_update(int timer_fd, int ms)
{
    struct itimerspec its = {
        .it_interval = {.tv_sec = 2, .tv_nsec = 0},
        .it_value.tv_sec = ms / 1000,
        .it_value.tv_nsec = (ms % 1000) * 1000 * 1000,
    };
    if (timerfd_settime(timer_fd, 0, &its, NULL) < 0)
        return -1;
    printf("timer update: %d\n", ms);
    return 0;
}

int main()
{
    /* create epoll */
    int epoll_fd = epoll_create(1);

    /* create timer */
    int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);

    /* add timer fd to epoll monitor event */
    add_event(epoll_fd, timer_fd, EPOLLIN);

    /* create thread to monitor */
    pthread_t tid;
    pthread_create(&tid, NULL, &worker, (void *) &epoll_fd);

    timer_update(timer_fd, 1000);
    while (1)
        usleep(1000000);

    return 0;
}

練習題: