← Previous Lecture
|
Next Lecture →
Linux Scheduler Runqueues and pick_next_task(): How a CPU Gets Its Next Thread
CPU Scheduling Internals – Part 2B | Updated for Linux Kernel 6.12+ / 7.x | Free Linux Kernel Development Course
Runqueue per CPU
EEVDF Pick
Free Course
The Linux scheduler runqueue is where every runnable thread waits for its turn on a CPU, and pick_next_task() is the function that decides who goes next. In this lecture of our free Linux kernel development course, we trace the complete path from schedule() down to the moment a scheduler class hands over a task, and we clear up a widespread misconception about how the Linux scheduler runqueue is really organized in modern kernels. Everything here reflects current 6.x LTS and 7.x kernels – including the array-based class iteration that replaced the old linked list, and the EEVDF red-black tree inside the fair class.
This tutorial continues the EmbeddedPathashala free embedded systems course track and complements our free Linux device drivers course – when your driver wakes a sleeping thread, the mechanics described below determine how quickly that thread reaches a CPU.
What You Will Learn
- The exact call chain from
schedule()to a scheduler class picking a task - How modern kernels iterate scheduler classes without a linked list
- How the Linux scheduler runqueue is really structured: one
struct rqper CPU with per-class sub-runqueues - Which data structure each class uses internally (rb-trees, priority arrays)
- A worked example showing which thread wins the CPU when multiple classes have runnable work
Prerequisites
- Part 2A of this series – Linux scheduler classes and the priority ladder
- Comfort reading C structures and macros
- Optional: kernel source of any 6.12+ release to follow along in
kernel/sched/
The Scheduling Call Chain
Whenever the kernel decides the current thread should stop running – it blocked, its time allocation expired, or a higher-priority thread woke up – the core entry point is schedule(). The essential path looks like this:
| schedule() |
| ↓ |
| __schedule() – disables preemption, takes the runqueue lock |
| ↓ |
| pick_next_task() – fast path, or full class walk |
| ↓ |
| class->pick_next_task(rq) – the winning class returns a task |
| ↓ |
| context_switch() – the chosen task starts running |
The core walk is beautifully small. Here is a simplified version of what modern kernels do inside the picker when the fast path does not apply:
/* kernel/sched/core.c (simplified for teaching) */
static struct task_struct *pick_task(struct rq *rq)
{
const struct sched_class *class;
struct task_struct *p;
for_each_class(class) {
p = class->pick_task(rq);
if (p)
return p;
}
BUG(); /* impossible: the idle class always has a task */
}
Each class is asked, in priority order, “do you have a runnable task?”. The first class that answers wins – a deadline task beats an RT task, which beats a fair task, and so on. If literally nothing is runnable, the idle class returns the per-CPU idle thread, which is why the final BUG() can never fire on a healthy kernel.
There is also a valuable fast-path optimization: since the overwhelming majority of tasks on a normal system live in the fair class, the picker first checks whether every runnable task on this CPU belongs to the fair (or idle) class. If so, it skips the walk entirely and calls the fair picker directly.
How for_each_class() Works in Modern Kernels
Older kernels chained the classes on a singly linked list using a next pointer inside struct sched_class. That pointer is gone. Modern kernels place each class object into its own named section, and the linker script lays those sections out contiguously in memory, highest priority first. Iteration then becomes plain pointer arithmetic between two boundary symbols:
/* kernel/sched/sched.h (simplified, modern kernels) */
extern struct sched_class __sched_class_highest[];
extern struct sched_class __sched_class_lowest[];
#define for_class_range(class, _from, _to) \
for (class = (_from); class < (_to); class++)
#define for_each_class(class) \
for_class_range(class, __sched_class_highest, __sched_class_lowest)
| __sched_class_highest | stop | dl | rt | fair | ext | idle | __sched_class_lowest |
Iteration = class++ from left to right – no pointers to chase, cache-friendly, and the order is guaranteed at link time. |
|||||||
Why did the kernel developers bother? Two reasons: walking a small contiguous array is faster and friendlier to CPU caches than chasing pointers, and a link-time-fixed order allows the compiler and the fast path to make safe assumptions about class positions.
The Linux Scheduler Runqueue: One Per CPU, Sub-Runqueues Per Class
Here is the misconception worth correcting: older explanations often make it sound like the kernel keeps a completely separate runqueue object for every (CPU × class) combination. The real structure is cleaner. The kernel maintains exactly one main runqueue – struct rq – per CPU core, implemented as a per-CPU variable so each core mostly touches only its own data (a powerful, largely lock-contention-free technique). Inside that per-CPU runqueue, each scheduler class embeds its own class-specific sub-runqueue:
|
|
||||||||||||
Each CPU owns one struct rq; each class keeps its own data structure inside it. sched_ext tasks flow through BPF-managed dispatch queues instead of a fixed per-CPU structure. |
|||||||||||||
The choice of internal data structure belongs entirely to each class – the core never peeks inside:
| Class | Sub-Runqueue Structure | Pick Cost |
|---|---|---|
| Fair (EEVDF) | Red-black tree ordered by virtual deadline; the eligible task with the earliest virtual deadline wins | O(log n) |
| Real-time | Array of 100 linked lists, one per priority level, plus a bitmap for instant highest-priority lookup | O(1) |
| Deadline | Red-black tree ordered by absolute deadline (earliest deadline first) | O(log n) |
| Ext (BPF) | Dispatch queues (DSQs) created and managed by the loaded BPF scheduler | Policy-defined |
| Stop / Idle | Trivial – at most one dedicated per-CPU task each | O(1) |
Worked Example: Who Wins the CPU?
Imagine a multicore system with 100 threads alive, of which five are runnable on CPU 0 at the moment __schedule() runs:
| Thread | Policy | Class | Scheduling Order |
|---|---|---|---|
| D1 (video pipeline) | SCHED_DEADLINE |
deadline | 1st – picked first |
| R1 (audio thread, prio 80) | SCHED_FIFO |
real-time | 2nd |
| R2 (control loop, prio 40) | SCHED_RR |
real-time | 3rd (lower RT priority than R1) |
| F1 (compiler job) | SCHED_OTHER |
fair (EEVDF) | 4th |
| swapper/0 | (kernel internal) | idle | Only if nothing else is runnable |
The class walk asks the deadline class first, and D1 is returned immediately – the RT and fair classes are never even consulted on this pass. When D1 later exhausts its runtime budget or completes its period’s work, the next __schedule() pass will find the deadline sub-runqueue empty and fall through to the real-time class, where R1 beats R2 on static priority. Only when both the deadline and RT sub-runqueues are empty does F1’s EEVDF tree get a chance, and swapper/0 runs only when everyone else sleeps.
Observing Runqueues on Your Machine
You can watch runqueue lengths and per-class state live – a great exercise in this free Linux device drivers course companion track:
# Runnable-thread counts and load per CPU
cat /proc/loadavg
# Rich per-CPU runqueue detail (requires debugfs)
sudo cat /sys/kernel/debug/sched/debug | less
# Per-task scheduling statistics
cat /proc/self/sched | head -15
# Watch context switches per second, system wide
vmstat 1 5
Inside /sys/kernel/debug/sched/debug you will literally see each CPU’s runqueue, the EEVDF fields for fair tasks, and per-class counters – the exact structures we discussed above, live on your hardware.
Common Mistakes and Troubleshooting
- Believing there is one global runqueue. There has not been a single global runqueue for two decades; each CPU owns its runqueue, and load balancers move tasks between them.
- Multiplying CPUs by classes to count runqueue objects. The accurate mental model is one
struct rqper CPU with per-class sub-structures inside it. - Quoting the old linked-list iteration in interviews. The
nextpointer instruct sched_classis gone; classes now sit in a linker-ordered array walked by pointer arithmetic. - Expecting O(1) picks from the fair class. The EEVDF pick is O(log n) via the rb-tree; only the RT class offers a true O(1) pick through its priority bitmap.
- Ignoring the idle class in reasoning. Because it always has a task, code after the class walk can safely assume a task was found – that invariant simplifies the entire core scheduler.
Best Practices
- When debugging latency, first identify which class your thread is in – the diagnosis path differs completely between fair, RT, and deadline tasks.
- Use
/sys/kernel/debug/sched/debugbefore reaching for heavier tracing; it often answers “why isn’t my thread running?” instantly. - On multicore embedded targets, remember per-CPU runqueues mean per-CPU decisions – pin latency-critical threads with CPU affinity so balancing does not surprise you.
- Prefer reading the current kernel source in
kernel/sched/core.cover old blog posts – the scheduler evolves quickly, as this lecture shows.
Key Takeaways
- The call chain is
schedule() → __schedule() → pick_next_task(), with a fast path for the common all-fair case. - Classes are consulted highest-priority-first via
for_each_class(), now an array walk fixed at link time. - Each CPU has exactly one runqueue (
struct rq), a per-CPU variable that minimizes lock contention; each class embeds its own sub-runqueue in it. - Fair uses an EEVDF rb-tree, RT uses O(1) priority arrays, deadline uses an EDF rb-tree, and
sched_extuses BPF-managed dispatch queues. - The idle class guarantees the picker always succeeds – every CPU always has something to run.
Frequently Asked Questions (FAQ)
It selects the next thread for the current CPU by asking each scheduler class, in strict priority order, whether it has a runnable task. The first non-empty class wins. A fast path skips the walk when all runnable tasks are in the fair class, which is the common case.
One main runqueue (struct rq) per CPU core, implemented as a per-CPU variable. Class-specific structures such as cfs_rq, rt_rq, and dl_rq live inside each per-CPU runqueue rather than existing as independent runqueue objects.
Because the idle class, consulted last, always has its per-CPU idle thread ready. If every other class is empty, the CPU runs swapper/N. That is why the code after the loop contains a BUG() that should be unreachable.
No. Modern kernels removed the next pointer. The class objects are placed in dedicated sections that the linker lays out contiguously from highest to lowest priority, and for_each_class() simply increments a pointer across that array.
A red-black tree ordered by virtual deadline. Among currently eligible tasks (those not ahead of their fair share), the one with the earliest virtual deadline is chosen, giving both fairness and better latency behavior than classic CFS.
Class priority trumps everything inside a lower class. The deadline class sits above the real-time class in the ladder, so any runnable deadline task is picked before even the highest-priority FIFO task.
In dispatch queues (DSQs) created and controlled by the loaded BPF scheduler. This gives policy authors full freedom – global queues, per-CPU queues, or custom topologies – while the kernel retains a safe fallback to the fair class.
Conclusion
You now know exactly how a modern Linux kernel turns “something changed, reschedule!” into a concrete thread on a concrete CPU: a locked per-CPU runqueue, a fixed-order walk across six scheduler classes, and class-owned data structures tuned for their job – from O(1) RT bitmaps to EEVDF rb-trees. This mental model will serve you constantly, whether you are chasing latency in an embedded product, writing drivers that wake threads, or answering kernel-internals interview questions. Continue this free Linux kernel development course with the next lecture, where we look at the fair class and EEVDF in depth.
Course Keywords
pick_next_task
Free Linux Kernel Development Course
Free Linux Device Drivers Course
Free Embedded Systems Course
EEVDF rb-tree
Per-CPU Variables
Kernel Internals
Continue Your Free Linux Kernel Development Course
Master the fair class and EEVDF internals in the next lecture.

2 Comments