Time-sharing control room

OPERATING SYSTEMS · 14 SEPTEMBER 2026 · ABOUT 20 MINUTES WITH THE TIMER LAB

How does 100 ms become a preemption?

A user-level thread is running ordinary instructions. The kernel does not even know that this thread exists. Yet after a quantum, a signal handler can seize control and run another one. The path from “100 ms” to that context switch crosses three different mechanisms: a CPU-time clock, kernel signal delivery, and a scheduler written entirely in user space.

Find your place in this lesson

Begin with the two fields that look almost identical

A threading library might arm a timer like this:

struct itimerval slice = {
    .it_value    = { .tv_sec = 0, .tv_usec = 100000 },
    .it_interval = { .tv_sec = 0, .tv_usec = 100000 },
};

setitimer(ITIMER_VIRTUAL, &slice, NULL);

There are two nested distinctions here. Inside one timeval, seconds and microseconds are two parts of the same duration:

tv_secwhole seconds
+
tv_usec / 1,000,000fractional seconds
duration = tv_sec + tv_usec / 1,000,000

{ 1 second, 250,000 microseconds } = 1.25 seconds

Linux requires tv_usec to be between 0 and 999,999. It is not a second independent timer. By contrast, it_value and it_interval play different roles:

it_value

The initial countdown: time remaining until the next expiration. Zero disarms the timer.

it_interval

The reload value after an expiration. Zero makes the timer one-shot.

With both set to 100 ms, expirations are due after 100, 200, 300… ms measured by the chosen clock. If the value were 100 ms and the interval 200 ms, the sequence would be 100, 300, 500… ms—not 100 and then 200 as absolute timestamps.

But which clock is doing the measuring?

The clock stops when the process stops doing user work

ITIMER_VIRTUAL counts user-mode Central Processing Unit (CPU) time consumed by the process. Wall time can pass while this clock stands still. Time asleep does not count. Time spent executing inside the kernel does not count. If any thread in the process executes user code, that time does count.

That last sentence matters: this is a process-wide virtual timer, and its clock includes the user CPU time of all kernel-visible threads in the process.

Here is a small experiment. A SIGVTALRM handler increments a volatile sig_atomic_t counter. The timer starts at 100 ms and reloads every 100 ms. The program first sleeps for 300 ms, then spins until three signals have arrived.

$ uname -srmo
Linux 6.18.44 x86_64 GNU/Linux
$ gcc --version | head -1
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$ gcc -Wall -Wextra -O0 vtimer-lab.c -o vtimer-lab
$ ./vtimer-lab
after sleep: wall=300 ms  user=  0 ms  ticks=0  remaining≈104 ms
after busy:  wall=324 ms  user=324 ms  ticks=3  remaining≈ 98 ms

This is observed output, not a simulated trace. During the sleep, 300 ms passed on the monotonic wall clock, but measured user time rounded to 0 ms and the virtual timer did not fire. The busy loop then consumed roughly three quanta of user CPU time and produced three signals.

Why did the first remaining value read about 104 ms rather than exactly 100? Timer expirations are not promised early, and Linux’s implementation adds accounting slack related to the scheduler tick when arming these legacy CPU timers. Resolution and system load can delay observation slightly. A 100 ms request is a threshold, not a promise to deliver at exactly 100.000 ms.

Three interval timers answer three different questions

TimerClock decreases duringExpiration signalQuestion it answers
ITIMER_REALwall-clock time, including sleepSIGALRMHow much real time has passed?
ITIMER_VIRTUALuser-mode CPU time, across all process threadsSIGVTALRMHow much user computation has this process consumed?
ITIMER_PROFuser + kernel CPU time, across all process threadsSIGPROFHow much total CPU service has this process consumed?

The distinction is operational. Suppose a user-level thread calls read() and blocks for a second. A real timer can expire during that second. A virtual timer does not charge the wait or the kernel’s system-call work. A profiling timer charges CPU time spent in the kernel, but still not time when the process is asleep and another process owns the CPU.

A virtual timer is therefore attractive for a teaching scheduler: a runnable user thread earns a quantum when it actually executes user code, rather than losing its quantum while the whole process is descheduled. It is not automatically the right production design; first we need to see what an expiration actually does.

The kernel keeps a deadline, not a little stopwatch loop

setitimer is a system call. It crosses into the kernel, which stores the virtual interval timer in process-level signal state. In current Linux source, the ITIMER_VIRTUAL case routes to the virtual CPU clock and records an expiration threshold plus an increment for reloading.

Meanwhile, Linux accounts execution to tasks: user time when the CPU was executing the task outside the kernel, system time when it was executing kernel code on the task’s behalf, and scheduler runtime more generally. For a process clock, Linux aggregates the relevant values across its thread group. The virtual timer compares accumulated user time against its next deadline.

There is no user-space thread repeatedly subtracting microseconds. Nor should you imagine that the Advanced Programmable Interrupt Controller (APIC) sends SIGVTALRM straight to your process. Hardware timer interrupts and scheduler ticks can give the kernel opportunities to update accounting and notice deadlines, but the virtual timer is a kernel software abstraction built on CPU-time accounting. Exact accounting paths vary with architecture and kernel configuration.

CPU execution
user or kernel mode is observable
kernel accounting
user time accumulates for the thread group
deadline check
threshold reached → signal generated

“Generated” still does not mean “the handler has run.” Signal generation marks a notification pending for a process or thread. Delivery is the later act of arranging for a handler or default action to execute.

Follow one quantum from user thread A to user thread B

The stepper below follows the 100 ms virtual timer through the layers. The wall and user counters are explanatory state for one single-kernel-thread process; the signal-frame behavior follows Linux’s documented delivery path.

The surprising handoff is steps 5–7. The kernel does not call the handler like an ordinary C caller. Before returning to user mode, it creates a signal frame on the selected kernel thread’s user stack. That frame preserves the interrupted program counter—the address of the next user instruction—plus architecture-specific register state and the signal mask. The kernel then changes the user-mode program counter to the handler’s address.

Control returns from the kernel, but it returns to the first instruction of the handler. On a normal handler return, a user-space signal trampoline invokes sigreturn; the kernel restores the saved frame, and execution resumes where it was interrupted.

A preemptive user-thread library inserts its own scheduler into that path. The handler records or preserves user thread A’s interrupted context, moves A to a runnable queue, chooses B, and restores B’s context. The underlying kernel thread may never change. Linux schedules that kernel thread; the library schedules A and B inside it.

A signal is not an Interrupt Request

Hardware interrupt

A device or local timer causes a CPU to enter a kernel interrupt vector. The Interrupt Request (IRQ), interrupt vector, and APIC machinery are kernel/hardware routing concepts.

Unix signal

The kernel records a process- or thread-directed notification. Delivery changes the user-mode return context so a handler runs before the interrupted code resumes.

The mechanisms can be causally related: a hardware timer interrupt may cause kernel code to run, update accounting, and discover that a CPU timer is due. But they are different interfaces on different sides of the privilege boundary. The process does not receive an IRQ vector. It observes SIGVTALRM because the kernel prepared its user context that way.

This also answers “which core handles it?” at the useful level. The kernel detects and delivers the pending signal while returning an eligible kernel thread to user mode on some CPU. The handler executes on that same kernel thread and CPU until it is descheduled or migrates later. No separate user-space core is summoned just for the signal.

Preemption means the handler can land in the worst possible place

The timer’s power is also its danger. SIGVTALRM can arrive between two instructions that maintain the run queue. If the handler immediately invokes the scheduler and manipulates the same queue, it can observe a half-finished invariant.

/* conceptually */
block(SIGVTALRM);          // enter scheduler-critical region
runq_remove(current);
runq_insert(next);
unblock(SIGVTALRM);        // invariant is whole again

Each kernel-visible thread has its own signal mask, so a multithreaded library must reason about masks per kernel thread. A new Portable Operating System Interface (POSIX) thread inherits its creator’s mask. The signal disposition—the address and configuration of the handler—is process-wide.

The handler also executes asynchronously with respect to normal library code. Only async-signal-safe operations belong there. printf, allocation, and most general library calls can deadlock or corrupt state if interrupted while holding their own internal locks. A teaching runtime often uses tightly controlled assembly or context primitives, avoids general-purpose library work in the handler, and blocks the timer signal around scheduler state.

Change one assumption: add several kernel threads

Our worked path had one kernel thread. Now suppose an M:N runtime has four kernel threads, each running a different user thread on a different core. The simple model breaks in two places.

  1. The clock is shared. A process has only one ITIMER_VIRTUAL. User CPU time from all four kernel threads contributes. If four cores execute in parallel, the process can consume roughly 100 ms of aggregate user CPU in about 25 ms of wall time.

  2. The notification is process-directed. A process-directed signal may be delivered to any thread that has it unblocked. If several are eligible, Linux chooses one. The timer does not promise “interrupt the kernel thread whose current user thread used 100 ms.”

That may be acceptable for a one-kernel-thread exercise. It is not a per-worker quantum mechanism merely because each worker calls the same initialization function: later setitimer calls replace the same process timer.

Linux exposes a more precise building block through POSIX timers: CLOCK_THREAD_CPUTIME_ID measures CPU time consumed by the calling kernel-visible thread, and Linux’s SIGEV_THREAD_ID can target a particular thread ID. That clock includes user and system CPU time, so it is not an exact per-thread clone of ITIMER_VIRTUAL. The manual explicitly describes the targeted notification mode as intended for threading libraries. A runtime could also use other timer and safe-point designs; the right choice depends on whether it needs hard asynchronous preemption, per-worker fairness, portability, and low overhead.

Return to the 100 ms line

We can now expand one innocent call into the machinery it requests:

setitimer(ITIMER_VIRTUAL, 100 ms initial, 100 ms interval)

→ arm one process-wide user-CPU deadline
→ aggregate user execution until the deadline is reached
→ generate process-directed SIGVTALRM and reload the interval
→ choose an eligible kernel thread for delivery
→ save its user registers in a signal frame
→ resume user mode at the handler
→ let the runtime preserve A, choose B, and switch contexts

The kernel preempts a kernel-visible execution context by arranging signal delivery. The user-thread library turns that interruption into a scheduling decision among objects the kernel cannot see. “100 ms” names only the budget. The timer clock decides what spends it; signal delivery creates the control transfer; the library decides what runs next.

Retrieval check: predict the changed timer

If it_value is 100 ms and it_interval is 200 ms, and the process alternates 50 ms of user computation with 500 ms sleeps, the first signal is due after two compute bursts. Later signals are due every four compute bursts. Sleep stretches wall time but spends none of the virtual timer’s budget.