SourceXR

C/C++ Cross-Reference Tool

Timer Notifications using File Descriptors

The timerfd syscalls can simplify the work of network application programmers, especially when the application is built around a reactive loop and has to handle timing events.

These events are usually not implemented in terms of file descriptors and thus require specific handling to fit in the reactive loop, for example, timing out the event loop to poll them.

These system calls, respectively named timerfd_create, timerfd_settime and timer_gettime, bring to the programmer timer notifications through a file descriptor, which can be put in a reactor (select/poll/epoll) and therefore have a single entry points for all possible events.

In this article we describe a small use of these calls within a reactor loop.

We use basic application code snippets where we create the reactor, setup the timerfd file descriptor and then wait for events around the epoll_wait loop with an infinite timeout.

timerfd System Calls

File Descriptor Creation

The first system call is used to create the file descriptor:

int timerfd_create (int clock, int flags);

The first argument takes a clock as parameter. Two clocks are available: CLOCK_MONOTONIC, not settable, always increasing, with a starting point some time in the past and CLOCK_REALTIME, a system-wide real time clock. More information on these clocks can be found in the clock_gettime(2) man page.

The second parameter takes some flags as parameter. These flags are currently TFD_NONBLOCK and TFD_CLOEXEC. The first one means that if no timer is available when we check for the timer state (through a read(2) call), the call fails with a return value of -1 and errno set to EAGAIN, otherwise the call is blocked, waiting for an event. The second one closes the file descriptor when exec is called.

Timer Setup & Value

A single syscall is used to start and stop the timer:

int timerfd_settime (int fd, const struct itimerspec *new_value,
                     struct itimerspec *old_value, int flags);

As specified, the new_value describes the parameters to be set on the timerfd descriptor and old_value is filled with the previous value. As described in the timerfd man page the itimerspec structures are the following:

struct timespec {
    time_t tv_sec; /* Seconds */
    long tv_nsec;  /* Nanoseconds */
};

struct itimerspec {
    struct timespec it_interval; /* Interval for periodic timer */
    struct timespec it_value;    /* Initial expiration */
};

If it_value fields are both set to 0, the timer is disarmed otherwise it is armed.

The flags can be set to TFD_TIMER_ABSTIME to interpret the times of the previous arguments as absolute times. Otherwise they are interpreted as relative times.

And finally the last syscall:

int timerfd_gettime (int fd, struct itimerspec *curr_value);

As guessed, it returns the current value of the time: values contains relative times to the next timer expiration.

Event Notification

The created timer is used within a reactor and registered as a read event (EPOLLIN). When the timer expires, the reactor returns, and we use the read syscall to get the number of expired timers. This value is returned as an 8-byte integer.

Sample Program

In this small program we setup a periodic timer with 1-second period. When the timer is triggered we print a message.

#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <iostream>
#include <errno.h>
#include <string.h>

int main() {

    int fd = timerfd_create (CLOCK_MONOTONIC, TFD_NONBLOCK);
    if (fd == -1) {
        std::cerr << strerror (errno);
        return 1;
    }

    const int size = 10;
    int epollfd = epoll_create (size);
    if (epollfd == -1) {
        std::cerr << strerror (errno) << "\n";
        return 1;
    }

    epoll_event event;
    epoll_event events[size];

    // add timer to reactor
    event.events = EPOLLIN; // notification is a read event
    event.data.fd = fd; // user data
    int r = epoll_ctl (epollfd, EPOLL_CTL_ADD, fd, &event);
    if (r == -1) {
        std::cerr << strerror (errno) << "\n";
        close (epollfd);
        close (fd);
        return 1;
    }

    // add event to be triggered
    int flags = 0;
    itimerspec new_timer;
    // 1-second periodic timer
    new_timer.it_interval.tv_sec = 1;
    new_timer.it_interval.tv_nsec = 0;
    new_timer.it_value.tv_sec = 1;
    new_timer.it_value.tv_nsec = 0;
    itimerspec old_timer;
    int i = timerfd_settime (fd, flags, &new_timer, &old_timer);
    if (i == -1) {
        std::cerr << strerror (errno) << "\n";
        close (epollfd);
        close (fd);
        return 1;
    }

    // watch possibly other fds
    // ...

    while (true) {

        const int infinity = -1;
        // and wait for events
        int r = epoll_wait (epollfd, events, size, infinity);
        if (r == -1) {
            close (epollfd);
            close (fd);
            return 1;
        }

        // demultiplex events
        int i = 0;
        while (i < r) {
            if (events[i].data.fd == fd) {
                // timer is triggered
                size_t s = 0;
                int i = read (fd, &s, sizeof (s));
                if (i != -1) {
                    std::cout << "Timer triggered (" << s << ")\n";
                    // handle_event();
                }
            }
            ++i;
        }
    }
    close(epollfd);
    close (fd);
    return 0;
}

Conclusion

timerfd provides a very convenient way of using timers within an event loop. We no longer have to worry about timer notifications with a computed time out given to the reactor. Along with signalfd, they form a uniform way to handle these non-file-based events, and as such the application implementation can be simplified.

Comments !