These docs are under active development and cover the v0.20 Kobicha security model.
On this page
Concept 4 min read

timerfd and Interval Timers

This page covers two related-but-distinct timer interfaces: timerfd (the modern fd-shaped variant) and the legacy interval-timer family (setitimer, alarm).

For the canonical modern timer interface — timer_create and friends — see POSIX timers. For the underlying clocks all of these read from, see Clocks.

timerfd: timers as file descriptors

timerfd exposes timer expirations as readable events on a file descriptor, designed for integration with epoll and io_uring event loops. Where a POSIX timer fires a signal that interrupts whatever the process is doing, a timerfd posts a readability edge — the event loop notices it on its next iteration, reads the expiration count, and dispatches handler logic in normal thread context.

This is the right primitive for any code that already runs an event loop. No async-signal-safety constraints, no signal-mask juggling, no thread-targeting complexity.

The interface

Call Purpose
timerfd_create(clockid, flags) Create a timerfd associated with a clock. Returns an fd.
timerfd_settime(fd, flags, &new_spec, &old_spec) Arm or disarm. Same itimerspec shape as POSIX timers.
timerfd_gettime(fd, &spec) Query time remaining and current period.

Creation flags:

Flag Effect
TFD_CLOEXEC Set O_CLOEXEC on the fd — almost always wanted.
TFD_NONBLOCK Make the fd non-blocking. read() returns EAGAIN if no expirations are pending.

Settime flags:

Flag Effect
TFD_TIMER_ABSTIME The it_value field is an absolute time on the chosen clock, not relative.
TFD_TIMER_CANCEL_ON_SET If the underlying CLOCK_REALTIME is stepped, cancel the timer and have the next read() return ECANCELED. Useful for code that wants to know when wall-clock time jumped.

Reading expirations

read() on a timerfd returns an 8-byte unsigned 64-bit count: the number of expirations that have occurred since the last successful read.

uint64_t expirations;
read(timerfd, &expirations, sizeof(expirations));
// expirations == number of times the timer fired since last read

If multiple expirations accumulated (the periodic interval was shorter than the time between reads), the count reports them all in one read — same conceptual model as POSIX timer overrun counts.

Choice of clock

timerfd_create accepts the same clocks as timer_create: CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_BOOTTIME, CLOCK_REALTIME_ALARM, CLOCK_BOOTTIME_ALARM. The wake-from-suspend variants (*_ALARM) require SeWakeFromSleepPrivilege at create time, same gate as POSIX timers.

CPU-time clocks (CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID) are not valid for timerfd. Use POSIX timers if you need a CPU-time-based timer.

Integration with event loops

A timerfd lives natively in an epoll set:

int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
struct itimerspec spec = { .it_value = {1, 0}, .it_interval = {1, 0} };
timerfd_settime(tfd, 0, &spec, NULL);

epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &(struct epoll_event){
    .events = EPOLLIN, .data.fd = tfd
});
// ... in the event loop:
//   when tfd is readable, read 8 bytes for expiration count

The same fd is registered with io_uring via IORING_OP_READ or IORING_REGISTER_EVENTFD-style integration. See Event fds and pidfds for the full ecosystem of fd-shaped notification primitives.

Cross-references and security

A timerfd is just a file descriptor. It can be passed across fork, transferred via SCM_RIGHTS (a service that creates timers and hands the fds to client processes), or duplicated with dup2. The receiver of a passed timerfd can read() and timerfd_gettime() it; whether they can timerfd_settime() (re-arm or disarm) requires the same fd, no separate check applies.

There are no Peios-specific access checks beyond what the file-descriptor model provides for any other fd.

Interval timers (legacy)

The interval-timer interface predates POSIX timers and timerfd by decades. It exists in the ABI for compatibility with old software but offers nothing the modern interfaces don't, plus several drawbacks.

Call Purpose
setitimer(which, &new_spec, &old_spec) Arm an interval timer of type which.
getitimer(which, &spec) Query current state.
alarm(seconds) One-shot real-time alarm. Equivalent to setitimer(ITIMER_REAL, ...) with a single second-precision expiry.

Three timer types, each tied to a fixed signal:

which Clock Signal on expiry
ITIMER_REAL Wall time (decremented in real time) SIGALRM
ITIMER_VIRTUAL User CPU time SIGVTALRM
ITIMER_PROF User + system CPU time SIGPROF

The drawbacks vs. modern interfaces:

  • One timer per type per process. Calling setitimer(ITIMER_REAL, ...) twice replaces the first with the second.
  • Fixed signal numbers. Multiple subsystems within one process that all want a periodic timer end up fighting over the same signal.
  • No payload. The handler receives just the signal — no si_value, no way to tell "which logical timer fired" beyond the signal number.
  • Microsecond resolution at best. The interface uses struct timeval, which doesn't expose nanosecond precision.

alarm() is even more constrained — single-second precision, single signal (SIGALRM), one outstanding alarm per process. Its primary use today is in test harnesses ("kill this test if it doesn't finish in N seconds") and in legacy single-purpose programs.

When you'd still see them

Only in old code or in code that's been ported from old Unix substrates. New Peios code should use:

  • timerfd for event-loop integration.
  • timer_create (POSIX timers) for signal-driven code that needs distinct per-timer payloads.
  • epoll_wait with a timeout for "wait at most N ms" patterns.

See also

  • POSIX timers — the modern signal-driven timer interface.
  • Clocks — the underlying clock sources timers read from.
  • Event fds and pidfds — other fd-shaped notification primitives that compose with timerfd in event loops.
  • epoll and polling — the event-loop interface most modern code uses to wait for timerfd events.