A periodic timer can expire several times before a busy event loop gets CPU time again. Linux timerfd does not compress that delay into a bare “timer fired” notification. A successful read() returns an unsigned 64-bit count of expirations accumulated since the timer was armed or since the preceding successful read.
That counter changes the semantics of delayed timer handling. Readiness says at least one expiration is pending; the value read from the descriptor says how many periods elapsed.
The timer is also a file descriptor
timerfd_create() creates a timer object represented by a file descriptor. The descriptor can participate in poll(), select(), and epoll() alongside sockets, pipes, and other pollable objects.
int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
if (fd == -1) {
perror("timerfd_create");
}CLOCK_MONOTONIC is useful for elapsed-time scheduling because discontinuous wall-clock changes do not alter that clock. Other supported clock choices have different semantics, including clocks tied to real time or suspend behavior.
The descriptor becomes readable after one or more expirations. With TFD_NONBLOCK, a read() performed when no expiration is pending fails with EAGAIN rather than waiting.
A read returns an expiration count
The read payload has a fixed semantic shape: an unsigned 8-byte integer in host byte order.
uint64_t expirations;
ssize_t n = read(fd, &expirations, sizeof(expirations));
if (n == sizeof(expirations)) {
printf("expired %llu times\n",
(unsigned long long) expirations);
}If three timer periods pass before the program reads the descriptor, the returned value can be 3. The application can therefore distinguish one observed wakeup from the number of timer intervals represented by that wakeup.
A buffer smaller than eight bytes is invalid for this operation and causes read() to fail with EINVAL. After a successful read, the reported accumulated count has been consumed; later reads account for subsequent expirations.
This is not a queue containing one record per expiration. The kernel exposes a count. That distinction avoids requiring the event loop to drain a sequence of identical timer records merely to determine how many periods passed.
Periodic arming separates the first deadline from the interval
timerfd_settime() uses struct itimerspec. it_value sets the initial expiration, while it_interval sets the repeating period after that first expiration.
struct itimerspec spec = {
.it_value = { .tv_sec = 1, .tv_nsec = 0 },
.it_interval = { .tv_sec = 1, .tv_nsec = 0 },
};
if (timerfd_settime(fd, 0, &spec, NULL) == -1) {
perror("timerfd_settime");
}With flags set to zero, it_value is relative to the current value of the selected clock. If both fields of it_interval are zero, the timer is one-shot instead of periodic. Setting both fields of it_value to zero disarms the timer.
TFD_TIMER_ABSTIME changes the initial expiration from a relative duration to an absolute value on the selected clock. The repeating interval still describes the period following that initial expiration.
Readiness and scheduling policy are separate
Suppose a timer has a 100 ms interval and the event loop is occupied for 450 ms. Once the loop returns to epoll_wait(), the timer descriptor can already be readable with several expirations accumulated.
timer periods: |---|---|---|---|---|
loop occupied: [-----------------]
descriptor: expiration count grows
loop resumes: read countThe kernel reports elapsed expirations, but it does not choose application policy for them. A program may process one unit of work per expiration, advance a logical clock by the count, coalesce overdue work into one action, or cap catch-up work to protect latency elsewhere.
Those policies are materially different. Treating every readable event as exactly one period silently loses elapsed-period information. Treating the count as a mandatory number of expensive jobs can instead create a catch-up burst. timerfd supplies the count; the event loop decides what the count means for its workload.
epoll readiness does not replace the read
An epoll notification indicates that the timer descriptor is readable. It does not carry the expiration count itself.
struct epoll_event ev = {
.events = EPOLLIN,
.data.fd = fd,
};
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);After readiness is reported, the application reads the timer descriptor to consume the pending count. This mirrors the broader file-descriptor event model: the multiplexer identifies an object with available state, while the object’s own operation retrieves that state.
For edge-triggered event loops, nonblocking I/O and complete state consumption remain important design concerns. The timer-specific fact is simpler: pending expirations make the descriptor readable, and read() retrieves their accumulated number.
Absolute real-time timers can expose clock discontinuities
Wall-clock scheduling has a boundary that elapsed-time scheduling does not. An absolute timer based on CLOCK_REALTIME or CLOCK_REALTIME_ALARM can be armed with TFD_TIMER_CANCEL_ON_SET.
When that flag is combined with TFD_TIMER_ABSTIME, a discontinuous change to the real-time clock marks the timer as canceled. A current or later read() then fails with ECANCELED. This gives the application an explicit signal that its absolute wall-clock deadline was invalidated by a clock step.
That behavior is specific to the documented real-time-clock configuration. It is not a generic cancellation mechanism for every timerfd clock.
Timer state survives normal event-loop delays
The useful boundary in timerfd is between notification and accounting. Polling APIs expose readiness as a boolean condition, but the timer descriptor retains a numeric record of accumulated expirations until a successful read consumes it.
As a result, scheduler delay does not automatically become lost timer-period information. The application still has to define a sensible response to overdue periods, but it receives enough state to make that choice explicitly rather than inferring elapsed intervals from a single readiness event.