Interval Timers and setitimer

An interval timer is the classic BSD-derived mechanism for asking the kernel to send your process a signal after a span of time, optionally over and over on a fixed period. The interface is two system calls — setitimer(which, new_value, old_value) to arm or disarm a timer and getitimer(which, curr_value) to read its remaining time — plus three timer “channels” selected by which: ITIMER_REAL counts down in real (wall-clock) time and delivers SIGALRM; ITIMER_VIRTUAL counts down only while the process burns user-mode CPU time and delivers SIGVTALRM; ITIMER_PROF counts down during user + system CPU time and delivers SIGPROF, the timer that statistical profilers were built on. The convenience wrapper alarm(seconds) is just a coarse, seconds-granularity arming of ITIMER_REAL. The defining limitation — and the reason POSIX timers (timer_create) eventually superseded these — is that there is exactly one of each timer per process: you cannot have two simultaneous ITIMER_REALs (setitimer(2); Linux 6.12 kernel/time/itimer.c).

This note pins to the Linux 6.12 LTS source tree (released 2024-11-17), cross-checked against the current setitimer(2), alarm(2), and timer_create(2) manual pages.


Mental Model

Think of a process as owning three pre-built, single-slot timer sockets. Each socket measures against a different notion of “elapsed time”, and each is wired to a different doorbell (signal). You hand a socket a duration; when that much of its kind of time has gone by, the doorbell rings. If you also gave it an interval, the socket re-arms itself automatically and keeps ringing forever until you disarm it. There is no fourth socket and no second ITIMER_REAL socket — three sockets, period.

The three “kinds of time” are the crux:

  • ITIMER_REAL measures wall-clock seconds — the same time a clock on the wall measures. It keeps counting whether your process runs, sleeps, blocks on I/O, or is preempted. A 5-second ITIMER_REAL rings 5 seconds later no matter what the process does in between.
  • ITIMER_VIRTUAL measures only the time the CPU spends executing your code in user mode. While the process is asleep, blocked, or running a system call, this clock is frozen. A 5-”second” ITIMER_VIRTUAL on a process that is mostly idle might take real-world minutes to fire.
  • ITIMER_PROF measures user + system CPU time — your user-mode code plus the kernel time spent on your behalf inside system calls — but still excludes time the process is descheduled. It was designed so a profiler could sample where a program spends CPU, including its syscall cost.
flowchart TB
  subgraph proc["One process (signal_struct)"]
    REAL["ITIMER_REAL slot<br/>signal-&gt;real_timer (hrtimer)<br/>+ it_real_incr"]
    VIRT["ITIMER_VIRTUAL slot<br/>signal-&gt;it[CPUCLOCK_VIRT]<br/>{expires, incr}"]
    PROF["ITIMER_PROF slot<br/>signal-&gt;it[CPUCLOCK_PROF]<br/>{expires, incr}"]
  end
  WALL["Wall-clock time<br/>(hrtimer / clockevent)"] -->|fires| REAL
  UCPU["User-mode CPU accounting"] -->|checked at tick| VIRT
  UKCPU["User+system CPU accounting"] -->|checked at tick| PROF
  REAL -->|delivers| SIGALRM["SIGALRM"]
  VIRT -->|delivers| SIGVTALRM["SIGVTALRM"]
  PROF -->|delivers| SIGPROF["SIGPROF"]

The three interval-timer channels of one process and what drives each. What it shows: ITIMER_REAL is implemented as a genuine hrtimer that the clockevent hardware fires in wall-clock time, while ITIMER_VIRTUAL and ITIMER_PROF are pairs of {expires, incr} counters checked against CPU-time accounting whenever the per-process CPU timers are examined (driven by the scheduling-clock tick). The insight to take: “real” timers ride the precise hardware timer path; the two CPU timers ride the much coarser CPU-accounting path, so their resolution is bounded by the tick, not by hrtimer precision.


Mechanical Walk-through

The data structures

Everything lives in the signal_struct — the structure shared by all threads of a process (a thread group), which is exactly why interval timers are per-process, not per-thread. In Linux 6.12’s include/linux/sched/signal.h the relevant fields are:

struct hrtimer real_timer;       /* the ITIMER_REAL backing hrtimer */
ktime_t it_real_incr;            /* ITIMER_REAL reload interval (0 = one-shot) */
struct cpu_itimer it[2];         /* ITIMER_VIRTUAL and ITIMER_PROF */

and the CPU-timer slot is just a pair of nanosecond counters:

struct cpu_itimer {
        u64 expires;             /* absolute CPU-time deadline, 0 = disarmed */
        u64 incr;                /* reload interval, 0 = one-shot */
};

The array index is a clock id: it[CPUCLOCK_VIRT] is ITIMER_VIRTUAL, it[CPUCLOCK_PROF] is ITIMER_PROF. So the three “sockets” are literally one hrtimer plus two two-field structs hanging off the shared signal state.

The userspace structure

Userspace describes a timer with struct itimerval, defined in include/uapi/linux/time.h:

struct itimerval {
        struct timeval it_interval;  /* timer interval (reload) */
        struct timeval it_value;     /* current value (time to next expiry) */
};
struct timeval {
        __kernel_old_time_t tv_sec;  /* seconds */
        __kernel_suseconds_t tv_usec;/* microseconds */
};

The two halves carry the whole semantics:

  • it_value is the time until the first expiry. Setting it to zero disarms the timer.
  • it_interval is the auto-reload period. If non-zero, after the first expiry the timer reloads itself to it_interval and fires periodically forever. If zero, the timer is one-shot: it fires once and disarms (setitimer(2)).

So a one-shot 250ms timer is it_value = {0, 250000}, it_interval = {0, 0}; a periodic 250ms timer is it_value = {0, 250000}, it_interval = {0, 250000}.

Arming an ITIMER_REAL timer

setitimer() enters the kernel at SYSCALL_DEFINE3(setitimer, ...). It copies the itimerval in with get_itimerval(), which runs the validity check:

#define timeval_valid(t) \
        (((t)->tv_sec >= 0) && (((unsigned long) (t)->tv_usec) < USEC_PER_SEC))

Both it_value and it_interval must be canonical — non-negative seconds and microseconds in [0, 999999] — or the call returns -EINVAL (itimer.c L313-330). The microseconds are then scaled to nanoseconds and stored internally as a struct itimerspec64, so the kernel works in nanoseconds even though the ABI speaks microseconds.

do_setitimer() handles the ITIMER_REAL case directly on the hrtimer:

case ITIMER_REAL:
again:
        spin_lock_irq(&tsk->sighand->siglock);
        timer = &tsk->signal->real_timer;
        if (ovalue) {                       /* report previous setting */
                ovalue->it_value = itimer_get_remtime(timer);
                ovalue->it_interval
                        = ktime_to_timespec64(tsk->signal->it_real_incr);
        }
        /* We are sharing ->siglock with it_real_fn() */
        if (hrtimer_try_to_cancel(timer) < 0) {
                spin_unlock_irq(&tsk->sighand->siglock);
                hrtimer_cancel_wait_running(timer);
                goto again;
        }
        expires = timespec64_to_ktime(value->it_value);
        if (expires != 0) {
                tsk->signal->it_real_incr =
                        timespec64_to_ktime(value->it_interval);
                hrtimer_start(timer, expires, HRTIMER_MODE_REL);
        } else
                tsk->signal->it_real_incr = 0;

Reading this top to bottom: it grabs the process’s siglock, optionally fills in the old value for the caller, then cancels any existing timer before re-arming — this is what enforces “one timer per channel”: re-arming overwrites. The hrtimer_try_to_cancel / hrtimer_cancel_wait_running retry loop handles the race where the timer is concurrently firing on another CPU; rather than deadlock against its own callback (which also takes siglock), it drops the lock, waits for the callback to finish, and retries from the again: label. Finally, a non-zero it_value arms the underlying hrtimer in relative mode (HRTIMER_MODE_REL) and remembers the reload interval; a zero it_value just clears the interval, leaving the timer disarmed.

The ITIMER_REAL expiry callback

When the hrtimer fires, the clockevent path invokes the registered callback it_real_fn():

enum hrtimer_restart it_real_fn(struct hrtimer *timer)
{
        struct signal_struct *sig =
                container_of(timer, struct signal_struct, real_timer);
        struct pid *leader_pid = sig->pids[PIDTYPE_TGID];
 
        trace_itimer_expire(ITIMER_REAL, leader_pid, 0);
        kill_pid_info(SIGALRM, SEND_SIG_PRIV, leader_pid);
 
        return HRTIMER_NORESTART;
}

It recovers the signal_struct from the embedded real_timer via container_of, then sends SIGALRM to the thread-group leader (PIDTYPE_TGID) — i.e. to the process, not a specific thread — and returns HRTIMER_NORESTART. The use of SEND_SIG_PRIV marks the signal as kernel-originated so it is not subject to the same permission checks a user-sent signal would be.

The genuinely surprising part is the periodic reload, which does not happen in this callback at all — note it returns HRTIMER_NORESTART and there is no hrtimer_forward anywhere in itimer.c. Instead, a periodic ITIMER_REAL re-arms itself in the signal-dequeue path. When the target process actually dequeues the pending SIGALRM (in dequeue_signal() in kernel/signal.c), the kernel does:

if (unlikely(signr == SIGALRM)) {
        struct hrtimer *tmr = &tsk->signal->real_timer;
        if (!hrtimer_is_queued(tmr) &&
            tsk->signal->it_real_incr != 0) {
                hrtimer_forward(tmr, tmr->base->get_time(),
                                tsk->signal->it_real_incr);
                hrtimer_restart(tmr);
        }
}

That is, the timer is forwarded by it_real_incr and restarted only when the previous SIGALRM is consumed, not when it fires. The in-tree comment explains why: doing the restart in dequeue rather than in the hrtimer callback prevents a denial-of-service in the high-resolution-timer case — “to prevent DoS attacks in the high resolution timer case … as the SIGALRM is a legacy signal and only queued once.” If reload happened in the callback, a process that never handled SIGALRM could be drowned in re-fires of a tight periodic hrtimer; by gating the reload on dequeue, the next expiry cannot be scheduled until the process has caught up with the last one. (real_timer is initialized in copy_signal() in kernel/fork.c with hrtimer_init(&sig->real_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL) and sig->real_timer.function = it_real_fn — confirming ITIMER_REAL is backed by a CLOCK_MONOTONIC hrtimer.)

Arming the CPU timers (ITIMER_VIRTUAL / ITIMER_PROF)

These take a completely different path because they are not measured in wall-clock time at all. do_setitimer() routes them to set_cpu_itimer():

nval = timespec64_to_ns(&value->it_value);
ninterval = timespec64_to_ns(&value->it_interval);
 
spin_lock_irq(&tsk->sighand->siglock);
oval = it->expires;
ointerval = it->incr;
if (oval || nval) {
        if (nval > 0)
                nval += TICK_NSEC;          /* round up by one tick */
        set_process_cpu_timer(tsk, clock_id, &nval, &oval);
}
it->expires = nval;
it->incr = ninterval;

Two things matter here. First, nval += TICK_NSEC: the requested duration is bumped up by one tick worth of nanoseconds. CPU timers are only ever checked at tick granularity (see below), so the kernel deliberately rounds up to guarantee the timer never fires early — a profiler that fired before its budget elapsed would be a correctness bug. Second, set_process_cpu_timer() converts the relative nval into an absolute CPU-time deadline (adding the current CPU-time sample) and, crucially, calls tick_dep_set_signal(tsk, TICK_DEP_BIT_POSIX_TIMER):

void set_process_cpu_timer(struct task_struct *tsk, unsigned int clkid,
                           u64 *newval, u64 *oldval)
{
        ...
        now = cpu_clock_sample_group(clkid, tsk, true);
        ...
        if (*newval)
                *newval += now;             /* relative -> absolute */
        ...
        tick_dep_set_signal(tsk, TICK_DEP_BIT_POSIX_TIMER);
}

That tick_dep_set_signal is the coupling to the scheduling-clock tick: arming a CPU timer forces the periodic tick to keep running, because the kernel needs the tick to keep sampling CPU time and to notice when the deadline is crossed. On a NOHZ_FULL CPU that would otherwise run tickless, arming an ITIMER_PROF/ITIMER_VIRTUAL re-enables the tick.

How CPU timers actually fire

The CPU timers have no hardware timer of their own. Instead, on each scheduling-clock tick the kernel runs run_posix_cpu_timers() (entered with interrupts disabled), which fast-path-checks whether any per-process CPU timer might have expired, and if so calls check_process_timers()check_cpu_itimer():

static void check_cpu_itimer(struct task_struct *tsk, struct cpu_itimer *it,
                             u64 *expires, u64 cur_time, int signo)
{
        if (!it->expires)
                return;
 
        if (cur_time >= it->expires) {
                if (it->incr)
                        it->expires += it->incr;   /* periodic reload */
                else
                        it->expires = 0;           /* one-shot, disarm */
 
                trace_itimer_expire(signo == SIGPROF ?
                                    ITIMER_PROF : ITIMER_VIRTUAL,
                                    task_tgid(tsk), cur_time);
                send_signal_locked(signo, SEND_SIG_PRIV, tsk, PIDTYPE_TGID);
        }
 
        if (it->expires && it->expires < *expires)
                *expires = it->expires;
}

cur_time is the freshly sampled CPU-time total for the process (user time for CPUCLOCK_VIRT, user+system for CPUCLOCK_PROF). When it crosses the stored expires, the timer either reloads by adding incr (periodic) or disarms (one-shot), and SIGPROF or SIGVTALRM is delivered to the thread group. check_process_timers() wires the two channels to their signals explicitly:

check_cpu_itimer(tsk, &sig->it[CPUCLOCK_PROF], ..., samples[CPUCLOCK_PROF], SIGPROF);
check_cpu_itimer(tsk, &sig->it[CPUCLOCK_VIRT], ..., samples[CPUCLOCK_VIRT], SIGVTALRM);

This is the whole reason the two CPU timers are tick-resolution: they are only inspected when run_posix_cpu_timers() runs, which is at most once per tick (and only when the tick is on). If HZ=1000, the practical resolution is ~1ms; with a slower HZ it is correspondingly coarser. By contrast ITIMER_REAL rides an hrtimer and so can have sub-microsecond precision in principle.

Reading a timer with getitimer

do_getitimer() mirrors the set path. For ITIMER_REAL it computes the hrtimer’s remaining time with itimer_get_remtime(), which calls __hrtimer_get_remaining() and has a deliberate quirk: if the timer is still active but the computed remainder is <= 0 (it is about to fire), it reports 1 microsecond (NSEC_PER_USEC) rather than zero, because in this API a zero it_value means “disarmed” — reporting zero for a live-but-imminent timer would be a lie (itimer.c L29-45). For the CPU timers, get_cpu_itimer() samples current CPU time and subtracts it from the absolute deadline; a timer about to fire reports TICK_NSEC for the same reason.

alarm() — the simple ITIMER_REAL wrapper

alarm(seconds) is sugar over setitimer(ITIMER_REAL, ...) with a one-shot, seconds-granularity value:

static unsigned int alarm_setitimer(unsigned int seconds)
{
        struct itimerspec64 it_new, it_old;
        ...
        it_new.it_value.tv_sec = seconds;
        it_new.it_value.tv_nsec = 0;
        it_new.it_interval.tv_sec = it_new.it_interval.tv_nsec = 0;
 
        do_setitimer(ITIMER_REAL, &it_new, &it_old);
        ...
        return it_old.it_value.tv_sec;   /* seconds left on the *previous* alarm */
}

Because it shares the ITIMER_REAL slot, alarm() and setitimer(ITIMER_REAL, ...) clobber each other — there is only one underlying timer (alarm(2)). alarm() returns the number of whole seconds remaining on whatever alarm was previously pending (rounding up, per the comment “we’d better return too much than too little”), or 0 if none was set. alarm(0) disarms. The it_new.it_interval = 0 makes it inherently one-shot: alarm() can never be periodic. On 32-bit machines seconds is clamped to INT_MAX to avoid a time_t overflow producing a negative — and therefore immediately-expiring — timer.


Code: arming, handling, and a profiler sketch

A complete one-shot real-time timeout, with a handler:

#include <signal.h>
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
 
static volatile sig_atomic_t fired = 0;
static void on_alarm(int signo) { fired = 1; }   /* keep handlers async-signal-safe */
 
int main(void) {
    struct sigaction sa = { .sa_handler = on_alarm };
    sigaction(SIGALRM, &sa, NULL);               /* install handler first! */
 
    struct itimerval it = {
        .it_value    = { .tv_sec = 0, .tv_usec = 500000 },  /* fire in 500 ms */
        .it_interval = { .tv_sec = 0, .tv_usec = 0 },       /* one-shot      */
    };
    setitimer(ITIMER_REAL, &it, NULL);
 
    while (!fired) pause();    /* wait for SIGALRM */
    puts("timer fired");
    return 0;
}

Line-by-line: the handler must be installed before arming — if SIGALRM arrives with the default disposition, the process is killed (the default action for SIGALRM, SIGVTALRM, and SIGPROF is to terminate). it_value = 500ms arms a single expiry; it_interval = 0 makes it one-shot. pause() sleeps until any signal arrives. A real handler should do almost nothing but set a flag (sig_atomic_t), because only async-signal-safe functions may be called from a signal handler.

A periodic profiling tick, the historical use of ITIMER_PROF:

struct itimerval prof = {
    .it_value    = { .tv_sec = 0, .tv_usec = 10000 },  /* first sample at 10 ms */
    .it_interval = { .tv_sec = 0, .tv_usec = 10000 },  /* then every 10 ms      */
};
setitimer(ITIMER_PROF, &prof, NULL);   /* SIGPROF every 10 ms of CPU time */

Here it_interval is non-zero, so the timer auto-reloads: SIGPROF arrives roughly every 10ms of consumed CPU time (user + system), not wall-clock. In the SIGPROF handler a profiler records the interrupted program counter; over many samples the distribution of PCs reveals where CPU time is spent. This is exactly the mechanism gprof’s sampling and many statistical profilers used. Note the resolution caveat: because ITIMER_PROF is tick-driven, asking for a 10ms interval on a HZ=100 kernel (10ms tick) gives you essentially the coarsest possible profiling — one sample per tick.

Reading remaining time:

struct itimerval cur;
getitimer(ITIMER_REAL, &cur);
printf("%ld.%06ld s left\n", (long)cur.it_value.tv_sec, (long)cur.it_value.tv_usec);

If cur.it_value is all zeros, the timer is disarmed; otherwise it is the time to the next expiry.


Failure Modes and Common Misunderstandings

Default disposition kills the process. Forgetting to install a handler (or SIG_IGN) for SIGALRM/SIGVTALRM/SIGPROF before arming means the first expiry terminates the process. This is a classic first-encounter surprise.

Only one timer per channel. Library code that arms ITIMER_REAL and application code that calls alarm() (or sleep(3), which on some libc implementations is built on SIGALRM) will silently destroy each other’s timers. The alarm(2) man page is blunt: “alarm() and setitimer(2) share the same timer; calls to one will interfere with the other,” and “mixing calls to alarm() and sleep(3) is a bad idea.” This single-slot constraint is the headline reason to prefer [[POSIX Timers and timer_create|timer_create]], which gives each caller its own independent timer.

Signal coalescing / lost expirations. Standard POSIX signals do not queue: if an ITIMER_PROF timer expires twice before the process handles the first SIGPROF, the second expiry is merged — the handler runs once, not twice. Interval timers provide no overrun count, so a profiler under heavy load silently undercounts. POSIX timers fix this with timer_getoverrun().

ITIMER_VIRTUAL on an idle or I/O-bound process seems “stuck.” Because it only counts user-mode CPU, a program that spends its life blocked in read() may never trigger a virtual timer. This is not a bug; it is the definition. People reach for ITIMER_VIRTUAL expecting wall-clock behavior and are baffled.

Tick-resolution coarseness. Requesting a 100µs ITIMER_PROF does not give you 100µs profiling — the CPU timers are checked at tick granularity and rounded up by TICK_NSEC, so the effective floor is one tick (1ms at HZ=1000). Only ITIMER_REAL (an hrtimer) can be genuinely sub-millisecond.

Fork/exec inheritance. Interval timers are not inherited across fork(2) — a child starts with all three disarmed — but they survive execve(2) (setitimer(2)). Code that execs a helper while a timer is armed will see SIGALRM arrive in the new program image, which usually was not expecting it.

The setitimer(which, NULL, ...) misfeature. Passing a NULL new_value is tolerated by Linux (it zeroes the timer) but logs a one-time kernel warning — “Misfeature support will be removed” — and is non-portable. Always pass a real itimerval.


Alternatives and When to Choose Them

NeedUseWhy
Per-process wall-clock alarm, legacy codeITIMER_REAL / alarm()Simplest; but single-slot
Statistical CPU profilingITIMER_PROF (SIGPROF)Classic; coarse and lossy under load
Many independent timers, overrun accountingPOSIX Timers and timer_createPer-timer ids, more clocks, timer_getoverrun
Timer that composes with epoll/polltimerfdA readable fd is far easier to multiplex than a signal
Precise sub-ms kernel/userspace deadlineHigh-Resolution Timers and hrtimers (kernel) / clock_nanosleep (user)Hardware-precise

POSIX timers (timer_create) are the direct, strictly-more-powerful replacement. A process may create many timers (bounded by RLIMIT_SIGPENDING), choose from many clocks (CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID, CLOCK_BOOTTIME, …), pick the notification mechanism (SIGEV_SIGNAL, SIGEV_THREAD, SIGEV_NONE, or Linux’s SIGEV_THREAD_ID), and read accurate overrun counts (timer_create(2)). Anything setitimer can do, timer_create can do — ITIMER_VIRTUAL/ITIMER_PROF map onto CLOCK_THREAD_CPUTIME_ID/CLOCK_PROCESS_CPUTIME_ID. timerfd is the right choice when you want a timer inside an event loop rather than delivered asynchronously by signal — see timerfd.

Standards status. The interval-timer API is genuinely legacy: it derives from 4.2BSD/SVr4, was specified in POSIX.1-2001, marked obsolete in POSIX.1-2008, and removed in POSIX.1-2024 in favor of timer_create (setitimer(2)). alarm() remains specified (POSIX.1-2024) because it is so widely embedded.


Production Notes

The single durable production niche for interval timers is SIGPROF-based statistical profiling. The Linux setitimer(ITIMER_PROF)SIGPROF loop is the substrate beneath glibc’s profiling support and many language-runtime profilers, because it samples CPU time (not wall time) and so reports where a program computes, weighting out time spent blocked. Its weaknesses — no overrun count, one-per-process, tick-resolution — are precisely why modern profilers increasingly prefer eBPF-based sampling or timer_create with CLOCK_*_CPUTIME_ID. But for a quick, portable, dependency-free CPU profiler, the SIGPROF interval timer is still the path of least resistance, and remains the mechanism a great deal of existing code relies on.

The second living use is legacy alarm() timeouts — the “give this blocking call N seconds then SIGALRM interrupts it” idiom that predates timerfd, epoll timeouts, and SO_RCVTIMEO. It is fragile (signal interrupts a syscall with EINTR, single-slot, races with sleep) but ubiquitous in old C code. New code should reach for an fd-based or timer_create-based timeout instead.

A subtle interaction worth knowing: arming a CPU interval timer (ITIMER_VIRTUAL/ITIMER_PROF) pins the scheduling-clock tick on via TICK_DEP_BIT_POSIX_TIMER. On latency-isolated NOHZ_FULL CPUs, where the entire point is to run tickless, an inadvertent CPU interval timer (or a profiler attached to an isolated thread) re-introduces the periodic tick and the OS jitter it was meant to eliminate. Profiling a nohz_full workload therefore perturbs exactly what it measures.


See Also