A periodic Linux timerfd can expire several times before an event loop reads it. The next successful read() does not return one record per wakeup. It returns one host-order uint64_t containing the number of expirations accumulated since the timer was last armed or since the preceding successful read.
That count makes timerfd readiness a notification that timer state is consumable, not a one-to-one mapping between scheduler wakeups and timer periods.
Read consumes an accumulated expiration count
A timerfd created with timerfd_create() represents one kernel timer through a file descriptor. timerfd_settime() supplies an initial expiration in it_value and, for a periodic timer, a nonzero it_interval.
struct itimerspec spec = {
.it_value = { .tv_sec = 0, .tv_nsec = 100000000 },
.it_interval = { .tv_sec = 0, .tv_nsec = 100000000 },
};
int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
timerfd_settime(fd, 0, &spec, NULL);If four 100 ms periods expire before the descriptor is read, a successful read can return the integer 4. The application receives a count, not four separately queued timer objects.
uint64_t expirations;
ssize_t n = read(fd, &expirations, sizeof(expirations));
if (n == sizeof(expirations)) {
/* expirations may be greater than 1 */
}The buffer must provide at least eight bytes. A smaller buffer causes EINVAL. With TFD_NONBLOCK, a read performed before any expiration is available fails with EAGAIN.
Readiness stays tied to unread expiration state
poll(), select(), and epoll report a timerfd as readable after one or more expirations have occurred. The readiness condition persists while an unread expiration count is available.
A successful read consumes the current count. For a periodic timer, later periods begin accumulating a new count. Event-loop dispatch frequency can therefore differ from timer frequency without losing the number of elapsed periods represented by the timerfd counter.
This property matters when a process is descheduled, paused, or busy with other work. A 10 ms periodic timer does not imply that user space receives one callback every 10 ms. If execution resumes after 70 ms, the read can report several expirations at once.
The counter represents periods, not delayed work items
The expiration value records timer periods that elapsed. It does not imply that the application has an independent work item for every count, nor does it force the application to replay an operation that many times.
A state sampler may treat any positive count as a request to refresh current state. A simulation loop may advance by the reported number of ticks. A rate-control component may use the count to detect scheduling delay. Those policies belong to the application; timerfd supplies the elapsed-expiration count.
This boundary prevents a common semantic error: equating event-loop wakeup count with elapsed timer count. One wakeup can expose multiple expirations, and scheduling can coalesce observation without changing the timer’s configured period.
Rearming defines a new timer setting
timerfd_settime() replaces the timer’s current setting. A zero it_value disarms it. A nonzero it_value arms it, and a nonzero it_interval establishes periodic expiration after the initial event.
Relative mode interprets it_value from the clock value at the time of the call. TFD_TIMER_ABSTIME instead interprets it as an absolute value on the selected clock.
timerfd_gettime() reports the remaining time until the next expiration and the current interval. Its returned it_value is relative even when the timer was armed with TFD_TIMER_ABSTIME.
Clock choice changes the time domain
CLOCK_MONOTONIC tracks monotonic elapsed time and is not affected by discontinuous wall-clock changes. CLOCK_REALTIME follows system wall-clock time and can move when that clock is explicitly changed.
For absolute CLOCK_REALTIME or CLOCK_REALTIME_ALARM timers, TFD_TIMER_CANCEL_ON_SET can request cancellation after a discontinuous real-time clock change. A current or later read then fails with ECANCELED. This behavior is separate from ordinary expiration accumulation.
Clock selection is therefore part of the timer’s semantic boundary. Expiration counting says how many configured periods elapsed in the chosen clock domain; it does not make different clocks equivalent.
Descriptor sharing also shares the timer object
After fork(), inherited descriptors refer to the same underlying timer object. Duplicated file descriptors likewise refer to the same open file description and timer state. A successful read through one reference consumes the accumulated expiration count visible through the shared timer.
This makes multiple readers competitors, not subscribers. The expiration count is not copied for every descriptor or process that can access the timerfd.
When all descriptors associated with the timer object are closed, the kernel disarms the timer and releases its resources. TFD_CLOEXEC prevents the newly created descriptor from surviving a successful execve().
The resulting contract is compact: timerfd converts timer expiration state into file-descriptor readiness, while each successful read transfers the accumulated expiration count and clears that consumable state for the next interval of observation.