« Previous Lecture | Next Lecture »
Linux CPU Scheduler Internals – Part 3: When Does the Scheduler Run? The Timer Interrupt Part
How the scheduler-tick keeps CPU sharing fair without ever calling schedule() from interrupt context — free Linux kernel development course
Intermediate
6.x Series
100% Free
Continuing our deep dive into Linux CPU scheduler internals: Part 2 established that no dedicated scheduler thread exists and that scheduling can never run in interrupt context. That leaves a puzzle. If threads only reschedule themselves when they happen to enter the kernel, what stops a CPU-bound thread from hogging the processor forever? The answer in this lesson of our free Linux kernel development course is an elegant two-part design: the timer interrupt does light bookkeeping and merely marks that a reschedule is due, while the actual switch happens later, safely, in process context.
What You Will Learn
- Why fairness requires the scheduler to run periodically — and why the timer interrupt cannot call
schedule()directly - What the scheduler tick (
sched_tick(), formerlyscheduler_tick()) actually does - The per-class
task_tick()hook and how the fair class updatesvruntime - Where
CONFIG_HZfits in, and what changed with EEVDF in kernel 6.6+ - The role of the timer softirq in tick processing
Prerequisites
Parts 1 and 2 of this series, plus a basic idea of hardware interrupts. Everything else is built up from scratch, in keeping with the beginner-friendly spirit of this free embedded systems course.
The Fairness Problem in Linux CPU Scheduler Internals
The scheduler’s job is to arbitrate access to the CPU among all competing threads. On a busy system with many runnable threads, fairness is only possible if the “policeman” — the scheduler itself — gets to intervene regularly. But how do you guarantee regular intervention when, as we learned, the scheduler is not a thread and only runs when invoked by the currently executing context?
A first idea seems obvious: the timer interrupt already fires CONFIG_HZ times per second on each CPU (commonly 250, often 1000 on desktop and embedded configs) — so just call schedule() from the timer interrupt handler! But this collides head-on with the golden rule from Part 2: the scheduler must never be invoked from any atomic or interrupt context. The timer interrupt path is disqualified by definition. So what does Linux actually do?
The real design splits the work between two code paths that cooperate:
| 1. Timer interrupt context Runs the scheduler tick every 1/HZ s: • update runqueue statistics • account runtime of current • load-balancing bookkeeping • if current has run long enough → set the need-resched flag (TIF_NEED_RESCHED) Never calls schedule() here! |
→ | 2. Process context At the next safe opportunity (e.g., on return from the interrupt to the task, or on return from a system call), the kernel notices the need-resched flag and calls schedule() in legal, process context → context switch happens here. |
Inside the Scheduler Tick — Where Linux CPU Scheduler Internals Meet the Timer
The periodic bookkeeping lives in kernel/sched/core.c. For most of Linux history the function was named scheduler_tick(); since kernel 6.10 it is simply sched_tick(). It runs with interrupts disabled, invoked from the timer-interrupt machinery (more precisely, tick handling is driven through the timer softirq, TIMER_SOFTIRQ, and related tick infrastructure). Its duties are strictly “meta work” that keeps scheduling running smoothly:
- Update the per-CPU runqueue clock and statistics
- Charge the elapsed tick to the currently running task’s accounting
- Trigger periodic load-balancing across CPUs when due
- Invoke the class-specific tick hook for
current
The crucial point deserves repeating: the actual schedule() function is never called from the tick. At most, the tick calls the scheduling-class hook of the interrupted task — sched_class->task_tick() — which may decide the task has exhausted its allotment and set the need-resched flag. The switch itself is deferred to a safe point.
The task_tick() Hook and vruntime
Each scheduling class supplies its own task_tick() implementation. For a thread in the fair class, the hook is task_tick_fair(), and one of its key jobs is updating the task’s vruntime — the virtual runtime, a priority-weighted measure of how much CPU time the task has consumed. A low-nice (high-priority) task’s vruntime advances slower than wall-clock; a high-nice task’s advances faster. Under EEVDF (kernel 6.6+), vruntime still exists but is now combined with two newer concepts: eligibility (has the task received less than its fair share so far?) and a per-task virtual deadline derived from its time slice. The next fair-class task picked is the eligible one with the earliest virtual deadline — hence the name Earliest Eligible Virtual Deadline First.
| Aspect | CFS (pre-6.6) | EEVDF (6.6 and later) |
|---|---|---|
| Pick criterion | Smallest vruntime | Eligible task with earliest virtual deadline |
| Latency handling | Heuristics and tunables | Deadline math; fewer knobs, better latency guarantees |
| vruntime | Central ordering key | Still tracked; feeds eligibility and deadlines |
| User-visible policies | SCHED_OTHER/BATCH/IDLE | Unchanged |
Watching the Tick’s Effects on Your Machine
You cannot call kernel functions from user space, but you can observe their footprints. First, find your kernel’s tick rate:
$ grep 'CONFIG_HZ=' /boot/config-$(uname -r)
CONFIG_HZ=1000
Next, watch involuntary context switches accumulate for a CPU-hungry process — each one is the deferred outcome of tick-driven need-resched marking:
$ yes > /dev/null &
[1] 7412
$ watch -n1 'grep ctxt_switches /proc/7412/status'
voluntary_ctxt_switches: 2
nonvoluntary_ctxt_switches: 1874 <-- keeps climbing
$ kill %1
You can even see the fair-class internals that task_tick_fair() maintains (requires CONFIG_SCHED_DEBUG):
$ grep -E 'vruntime|deadline' /proc/self/sched
se.vruntime : 215467.310394
se.deadline : 215470.310394
Finally, perf can count the tick and the resulting switches system-wide:
$ sudo perf stat -e context-switches,cpu-migrations -a sleep 5
Tickless Kernels in Modern Linux CPU Scheduler Internals
Modern kernels reduce or eliminate the periodic tick where possible: CONFIG_NO_HZ_IDLE (the common default) stops the tick on idle CPUs to save power, and CONFIG_NO_HZ_FULL can stop it even on busy CPUs running a single task — prized in low-latency, HPC, and telecom deployments. The conceptual model you learned still holds; the tick is simply delivered only when it has real work to do.
Common Mistakes and Troubleshooting
- Believing the tick performs context switches: it only marks need-resched; the switch happens at the next safe point in process context (or on interrupt return path to the task).
- Grepping for scheduler_tick() in new kernels: renamed to
sched_tick()in 6.10 — check your source version. - Tuning legacy CFS knobs on 6.6+: several old tunables are gone under EEVDF; consult current docs before cargo-culting old advice.
- Assuming HZ=250 everywhere: distributions differ (desktop kernels often use 1000); always verify with the config.
Best Practices
- For latency-sensitive embedded work, measure with
perf schedand considerNO_HZ_FULLplus CPU isolation rather than blindly raising priorities. - When reading kernel source, start at
kernel/sched/core.cand followsched_tick()→ classtask_tick()hooks — it is one of the friendliest entry points into Linux CPU scheduler internals. - Keep the two-path model (tick marks, process context switches) in mind whenever you reason about preemption behavior in drivers.
Key Takeaways
- In Linux CPU scheduler internals, fair CPU sharing requires periodic scheduler intervention, but
schedule()is illegal in interrupt context. - Linux splits the job: the tick (interrupt context) does bookkeeping and flags need-resched; the switch executes later in process context.
sched_tick()(ex-scheduler_tick()) updates runqueues, accounting, load balancing, and calls the classtask_tick()hook.- For fair-class tasks,
task_tick_fair()updates vruntime; EEVDF adds eligibility and virtual deadlines on top. - Tick-related processing is carried through the timer softirq machinery; tickless configs suppress unneeded ticks.
Frequently Asked Questions
Q1. So when exactly does schedule() finally run?
At well-defined safe points: on return from interrupt/syscall to a task with need-resched set, when a task blocks (I/O, locks, sleep), when it exits, or via explicit preemption points in a preemptible kernel.
Q2. What is TIF_NEED_RESCHED?
A per-thread flag meaning “a reschedule is pending for you.” The tick (or a wakeup of a higher-priority task) sets it; the kernel checks it at safe points and calls schedule() there.
Q3. Is vruntime gone now that CFS is replaced?
No. EEVDF still maintains vruntime; it additionally computes eligibility and a virtual deadline per task and picks the eligible task with the earliest deadline.
Q4. What does CONFIG_HZ control, practically?
The base periodic-tick frequency per CPU — the granularity of tick-driven accounting and preemption checks. Higher HZ gives finer-grained preemption at slightly higher overhead.
Q5. Do real-time tasks depend on the tick to preempt others?
No — when an RT task wakes, the wakeup path itself sets need-resched on the affected CPU immediately; preemption follows at the next safe point without waiting for a tick.
Q6. Where can I read more from authoritative sources?
The kernel scheduler design docs, the EEVDF documentation, and sched(7).
Conclusion
The Linux scheduler achieves periodic, fair intervention without ever breaking its own golden rule: the timer tick — running in interrupt context — restricts itself to bookkeeping and to raising a flag, while the context switch is executed later by the task itself in process context. Add EEVDF’s deadline-based picking (6.6+) and the tickless options, and you now have an accurate, modern picture of when and how Linux CPU scheduler internals come alive. This wraps up the three-part scheduler internals arc of our free Linux kernel development course; the journey continues with CPU affinity and control groups in upcoming lectures of this free Linux device drivers course track.
Master the Kernel — 100% Free
EmbeddedPathashala’s free Linux kernel development course, free Linux device drivers course, and free embedded systems course are open to everyone, forever.

2 Comments