CPU and the Scheduler
Every other subsystem in this book is scheduled by this one. A driver that sleeps, a filesystem that
waits for I/O, a network stack that wakes a reader — all of them end in a call into
kernel/sched/, and all of them are affected by decisions made here.
It is also the hardest place in the kernel to make a first contribution, and it is worth understanding why: the code is small and readable, but every change affects every Linux machine in the world, the effects are statistical rather than binary, and the bar is not "is this correct" but "does this help enough to justify the risk". Reading it is enormously valuable. Patching it is a later goal.
Orient Yourself First
Run these before reading further. The tables in this chapter are a snapshot; your tree is the truth.
cd ~/kernel/linux
./scripts/get_maintainer.pl --scm --status -f kernel/sched/
git log --oneline --since="6 months ago" -- kernel/sched/ | head -30
git log --since="1 year ago" --format='%cN' -- kernel/sched/ | sort | uniq -c | sort -rn | head
ls kernel/sched/
ls Documentation/scheduler/
wc -l kernel/sched/*.c | sort -n | tail -12
Predict first: how many lines is kernel/sched/fair.c? How does that compare with a single
mid-sized driver? Most people are surprised in both directions.
Why It Matters
| Because | Consequence |
|---|---|
| It decides which task runs on which CPU, right now | Every latency complaint on every system eventually points here |
| It is the arbiter between throughput and latency | The tradeoff cannot be resolved, only placed |
| Its decisions are statistical | You cannot prove a change is good; you can only measure it, honestly |
| It interacts with power management | Which CPU, at which frequency, in which idle state — one coupled problem |
| It is now extensible from user space | sched_ext lets a BPF program be the scheduler |
Where the Code Is
ls kernel/sched/
| File | What lives there |
|---|---|
core.c | __schedule(), try_to_wake_up(), context_switch(), the class-independent core |
fair.c | The fair class — EEVDF since 6.6, CFS before that. The biggest file. |
rt.c | SCHED_FIFO / SCHED_RR |
deadline.c | SCHED_DEADLINE — EDF with admission control |
ext.c | sched_ext: BPF schedulers (6.12 onward) |
idle.c | The idle task and the idle class |
sched.h | Read this first. struct rq, struct cfs_rq, the class definitions. |
pelt.c | Per-Entity Load Tracking — the load and utilization signals |
topology.c | Scheduling domains: the machine's cache and NUMA structure |
cpufreq_schedutil.c | Frequency selection driven by scheduler utilization |
debug.c | /proc/sched_debug, /sys/kernel/debug/sched/ |
Related, outside kernel/sched/:
ls kernel/cpu.c kernel/smp.c # CPU hotplug, IPIs
ls drivers/cpufreq/ drivers/cpuidle/ # frequency and idle-state drivers
ls tools/sched_ext/ # example BPF schedulers, if present
The Structures
Draw these before reading a single function.
struct task_struct one per task
├── sched_class ──────▶ which policy owns this task
├── se (struct sched_entity) ← fair class state
│ ├── vruntime, deadline, slice, vlag ← EEVDF's bookkeeping
│ ├── load ← weight, from nice
│ └── avg (struct sched_avg) ← PELT: load_avg, util_avg
├── rt (struct sched_rt_entity) ← rt class state
├── dl (struct sched_dl_entity) ← deadline class state
├── prio / static_prio / normal_prio
└── cpus_ptr, cpus_mask ← affinity
struct rq ONE PER CPU. The runqueue.
├── cfs (struct cfs_rq) ── an rbtree of runnable sched_entities
├── rt (struct rt_rq) ── priority-indexed lists
├── dl (struct dl_rq) ── an rbtree by deadline
├── curr, idle, stop ← which task is on-CPU
├── clock, clock_task ← this CPU's notion of time
└── lock ← the runqueue lock
struct sched_domain the machine's TOPOLOGY
└── a hierarchy: SMT siblings -> a core's L2 -> a socket's LLC -> NUMA
Load balancing walks this, cheapest level first.
rg -n "struct rq \{" -A 40 kernel/sched/sched.h
rg -n "struct sched_entity \{" -A 30 include/linux/sched.h
rg -n "struct sched_domain \{" -A 30 include/linux/sched/topology.h
The Verbs
The scheduler's polymorphism is one ops table, and finding it is finding the architecture.
rg -n "struct sched_class \{" -A 40 kernel/sched/sched.h
rg -n "DEFINE_SCHED_CLASS" kernel/sched/*.c
| Method | Called when |
|---|---|
enqueue_task / dequeue_task | A task becomes runnable / stops being runnable |
pick_next_task | The core asks this class for a task to run |
put_prev_task / set_next_task | Bookkeeping around a switch |
task_tick | The timer tick, for the running task |
select_task_rq | Which CPU should this task wake on? |
check_preempt_curr (naming varies) | Should the woken task preempt the running one? |
balance / pick_task | Load balancing hooks |
The classes are a priority-ordered chain, and pick_next_task() walks it in order: the first
class with a runnable task wins. The order is established by linker section placement, not a linked
list you can grep for directly:
rg -n "DEFINE_SCHED_CLASS" kernel/sched/ # each class, in section order
rg -n "for_each_class|pick_next_task\b" kernel/sched/core.c | head
Roughly: stop → deadline → rt → fair → (ext) → idle. Verify the order on your tree rather than
trusting that sentence — sched_ext inserted itself into this chain in 6.12 and the arrangement is
exactly the kind of thing that changes.
The Concepts
| Chapter | Answers |
|---|---|
| Scheduling Classes and the Pick Path | What runs next, and who decides? |
| EEVDF and Fairness | What does "fair" mean, and how did it change in 6.6? |
| Placement, Balancing, and Power | Which CPU, at what frequency, and why is that one question? |
And Lab 10 makes you trace a single wakeup from write() to the
target task running on a CPU you can name.
How to Read It
1. Documentation/scheduler/ -- sched-design-CFS.rst, sched-ext.rst,
sched-deadline.rst, sched-domains.rst. An hour, and it saves ten.
2. kernel/sched/sched.h -- struct rq and struct sched_class. Do not read
a .c file until you can draw these.
3. ONE PATH: __schedule() in core.c.
- who calls it (voluntary sleep, preemption, exit-to-user)
- pick_next_task(): the class chain
- context_switch(): the actual switch
Follow it in GDB with a breakpoint and `bt`.
4. THE OTHER PATH: try_to_wake_up() in core.c.
- select_task_rq(): which CPU?
- enqueue, then maybe an IPI to make that CPU reschedule
This is where most of the interesting decisions are.
5. Only then open fair.c, and only the part you need.
Tip:
fair.cis large and it is the wrong place to start.core.c's__schedule()andtry_to_wake_up()are the skeleton; the fair class is one implementation hanging off it. People who start infair.cspend a week onvruntimearithmetic without knowing what calls it.
Observing It
Everything here is instrumented. Use it before reading code, not after.
T=/sys/kernel/tracing; [ -d "$T" ] || T=/sys/kernel/debug/tracing
ls $T/events/sched/ # ~25 tracepoints
# The four that matter most:
# sched_wakeup a task became runnable, and on which CPU
# sched_switch a context switch, with both tasks' states
# sched_migrate_task a task moved between CPUs
# sched_stat_* time spent runnable-but-not-running (needs SCHEDSTATS)
sudo perf sched record -- sleep 5
sudo perf sched latency --sort max | head -20 # who waited longest
sudo perf sched timehist | head -30 # every switch, annotated
sudo bpftrace -e 'tracepoint:sched:sched_switch { @[args.prev_comm, args.next_comm] = count(); }'
sudo bpftrace -e 'tracepoint:sched:sched_migrate_task { @[args.orig_cpu, args.dest_cpu] = count(); }'
cat /proc/sched_debug 2>/dev/null | head -40 # per-runqueue internals
ls /sys/kernel/debug/sched/ 2>/dev/null
cat /proc/pressure/cpu 2>/dev/null # PSI: time lost to CPU contention
What Is Moving
Read the last six months rather than believing a book:
git log --oneline --since="6 months ago" -- kernel/sched/ | head -40
git log --oneline --since="2 years ago" -- kernel/sched/ext.c | tail -20
The large changes of the recent past, each of which you should verify is still true:
| Change | Roughly | Why it matters |
|---|---|---|
| EEVDF replaced CFS in the fair class | 6.6 | The core fairness algorithm changed; older documentation and every blog post about vruntime is now partly wrong |
sched_ext | 6.12 | A BPF program can be the scheduler. Enormous for experimentation. |
Preemption models selectable at boot (PREEMPT_DYNAMIC) | ongoing | "Which preemption model" is now a runtime question |
PREEMPT_RT selectable on x86-64 | 6.12 | Real-time is mainline, not a patch set |
| PELT / util_est / uclamp refinements | ongoing | The signals cpufreq consumes |
grep -rn "EEVDF\|eevdf" kernel/sched/fair.c | head
ls Documentation/scheduler/sched-ext.rst 2>/dev/null && echo "sched_ext docs present"
What a Good First Contribution Looks Like
Realistically, in ascending order of ambition:
| Target | Why it is plausible |
|---|---|
Documentation fixes in Documentation/scheduler/ | The EEVDF transition left real staleness. Verify a claim against fair.c; when it is wrong, that is a patch. |
A sched_ext example scheduler | tools/sched_ext/ is young, the bar is lower, and the code is BPF rather than core kernel |
| Selftests | tools/testing/selftests/sched/ is thin |
| A tracepoint or a debugfs counter | Additive, low risk, and useful to others |
| A bug with a reproducer | A demonstrable wrong behavior does its own arguing |
| A performance change | Only with the full apparatus from Engineering: a hypothesis, a control, variance, and multiple machines. Expect months. |
Warning: The failure mode here is specific and worth naming. A newcomer measures a workload, changes a heuristic, sees a 3% improvement, and sends it. The reply asks about the other twelve workloads, the other four topologies, and the tail latency — because every scheduler heuristic is a tradeoff someone already made deliberately. A scheduler patch is a measurement project with a small diff attached, and the measurement is 95% of the work.
Common Misconceptions
| Misconception | Reality |
|---|---|
| "The scheduler runs periodically and picks a task" | It runs when something calls it: a sleep, a wakeup, a preemption point, the tick. Between calls it is not running at all. |
"nice sets priority" | For the fair class, nice sets a weight — a proportion of CPU, not an order. SCHED_FIFO priorities are an order. |
| "Higher priority always wins" | Across classes, yes. Within the fair class there is no priority order, only weights. |
| "CFS is the Linux scheduler" | The fair class implements EEVDF since 6.6. There are five classes. |
| "The scheduler balances load periodically" | Most placement happens at wakeup (select_task_rq). Periodic balancing is the correction, not the mechanism. |
"taskset makes a task run" | It restricts where it may run. It does not make it run sooner. |
"A busy loop at SCHED_FIFO is fine" | It can starve everything on that CPU, including kernel threads. This is why sched_rt_runtime_us exists. |
| "Scheduler latency is the scheduler's fault" | Usually it is a lock, an interrupt, or a preemption-disabled region belonging to someone else. Measure before blaming. |
Validation / Self-check
- Name the five scheduling classes in priority order, and the command that shows you the real order on your tree.
- What is a runqueue, how many are there, and what protects one?
- What is the difference between
struct task_structandstruct sched_entity, and why are they separate? - Name six methods of
struct sched_classand say when each is called. - Which two functions in
core.care the skeleton of the whole subsystem, and why isfair.cthe wrong place to start? - Where does most placement happen — at wakeup or during periodic balancing? What follows from that?
- What changed in 6.6, and what does it make stale in older documentation?
- What is
sched_extand why does it lower the barrier to contributing here? - Name four scheduler tracepoints and what question each answers.
- Why is a scheduler performance patch mostly a measurement project?
nice -n 5on a fair-class task: what exactly does that change?- Give three plausible first contributions here and say why each is plausible.
Next: Scheduling Classes and the Pick Path — what runs next, and who decides.