Linux Kernel Scheduler: A Deep Engineering Analysis of CPU Scheduling from CFS to EEVDF
The CPU scheduler is the beating heart of any operating system kernel. It decides which runnable thread gets to execute on which CPU core and for how long — a decision that directly impacts throughput, latency, fairness, and power efficiency. This article takes a deep dive into the Linux kernel's process scheduling architecture, from the O(1) scheduler of the early 2000s through the Completely Fair Scheduler (CFS) that replaced it, to the brand-new Earliest Eligible Virtual Deadline First (EEVDF) algorithm introduced in Linux 6.6. We examine the data structures, runtime complexity, scheduling classes, NUMA awareness, and real-time policies that make Linux scheduling one of the most sophisticated production schedulers ever engineered.
1. Historical Context: Why Schedulers Evolve
Linux scheduling has undergone three major paradigm shifts:
- O(n) scheduler (Linux 2.4): Iterated all runnable tasks to find the highest-priority one. O(n) latency made it unacceptable for large server workloads.
- O(1) scheduler (Linux 2.6.0–2.6.22): Used per-priority active/expired arrays with O(1) task selection. Fast but relied on complex heuristics for interactive detection.
- CFS (Linux 2.6.23–6.6 default): Introduced the red-black tree + virtual runtime model for O(log n) fair scheduling. Replaced heuristics with mathematical fairness.
- EEVDF (Linux 6.6+, available since 6.5): Latency-optimal proportional fairness using absolute virtual deadlines. Algorithmically proven to match Generalized Processor Sharing.
2. CFS: The Completely Fair Scheduler
CFS, authored by Ingo Molnár, represents a philosophical departure from traditional scheduling. Instead of fixed time slices and priority arrays, CFS models an "ideal, precise multitasking CPU" where every runnable task gets an equal share of processor time.
2.1 The Virtual Runtime (vruntime) Model
// --- sched_entity structure (include/linux/sched.h) ---
struct sched_entity {
struct load_weight load; // Weight based on nice value
struct rb_node run_node; // Red-black tree node
u64 vruntime; // Nanoseconds of CPU time consumed (weighted)
u64 exec_start; // Last dispatch timestamp
u64 sum_exec_runtime; // Total CPU time consumed
u64 prev_sum_exec_runtime;
// ...
};
Core concept: each task accumulates vruntime at a rate inversely proportional to its priority weight. A higher-priority (lower nice) task accrues vruntime slower, so it gets more physical CPU time. The scheduler always picks the task with minimum vruntime — the one that has received the least CPU share relative to its entitlement.
// --- vruntime accounting (kernel/sched/fair.c) ---
static void update_curr(struct cfs_rq *cfs_rq) {
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
delta_exec = now - curr->exec_start; // Wall-clock time since dispatch
curr->sum_exec_runtime += delta_exec;
curr->vruntime += calc_delta_fair(delta_exec, curr); // Weight-adjusted
update_min_vruntime(cfs_rq);
}
2.2 Red-Black Tree Operations
CFS stores runnable tasks in a red-black tree (a self-balancing BST) keyed by vruntime. This gives:
- Dequeue (pick next): O(1) — leftmost node cached
- Enqueue (add task): O(log n)
- Delete (task exit/block): O(log n)
2.3 Scheduler Tick and Preemption
The system tick calls entity_tick() which checks if the current task has exceeded its "ideal" slice. CFS uses a dynamic timeslice based on sched_period (default 24ms) divided by the number of runnable tasks on the CPU:
__always_inline struct sched_entity * pick_next_entity(struct cfs_rq *cfs_rq) {
struct sched_entity *left = __pick_first_entity(cfs_rq);
// Check if first entity significantly behind second — if so, more complex selection
return left; // Usually just the min-vruntime task
}
3. EEVDF: The Next Generation
EEVDF (Earliest Eligible Virtual Deadline First), proposed by Ion Stoica and Hussein Abdel-Wahab in 1995 and implemented by Peter Zijlstra for Linux 6.6, replaces CFS as the default scheduler. While CFS is a "best-effort" approximation of fairness, EEVDF provides proven Optimal Proportional Fairness.
3.1 Why Replace CFS?
CFS has known latency issues:
- Wake-up preemption latency: Newly woken tasks may wait for the next tick before preempting
- Sleeper fairness: Tasks that sleep accumulate debt; CFS compensates by boosting their position, causing latency spikes
- Granularity vs. fairness tradeoff: Smaller "min_granularity" helps interactive tasks but hurts throughput
3.2 EEVDF Core Concepts
EEVDF uses three fundamental values per schedulable entity:
- Virtual Time (vt): Weighted CPU consumption, equivalent to CFS vruntime
- Virtual Deadline (vd): vt + (request_duration / weight) — when the task's current request should ideally complete
- Eligible Time (el): max(vt, current_time) — the task is not eligible to run before its virtual time
The scheduling rule: always pick the eligible task with the earliest virtual deadline. This is provably equivalent to Generalized Processor Sharing (GPS) — the theoretical optimal for proportional fairness.
3.3 EEVDF Latency Advantages
| Scenario | CFS Behavior | EEVDF Behavior |
|---|---|---|
| New task wakes | Goes to tree right of min-vruntime; waits for tick preemption | Gets immediate deadline calculation; eligible tasks preempt without waiting for tick |
| Overloaded system (N >> CPUs) | Latency proportional to #runnable tasks | Latency bounded by request_duration/quantum |
| Mixed workload (CPU + IO) | Sleeper fairness causes compensation jitter | Lag-based compensation more predictable |
| Real-time urgency | RT handled by separate class, but latency spikes at CFS boundaries | EEVDF's deadline naturally expresses urgency |
4. Scheduling Classes: Multi-Level Dispatch
Linux doesn't run a single algorithm — it layers scheduling classes in priority order:
// --- Scheduling class hierarchy (highest priority first) ---
struct sched_class {
const struct sched_class *next;
void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
void (*yield_task)(struct rq *rq);
void (*pick_next_task)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
// ...
};
// Class chain:
// stop_sched_class → CPU hotplug, migration control (highest)
// dl_sched_class → SCHED_DEADLINE (early deadline first)
// rt_sched_class → SCHED_FIFO / SCHED_RR (POSIX real-time)
// fair_sched_class → SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE (CFS/EEVDF)
// idle_sched_class → Only when nothing else can run (lowest)
4.1 SCHED_DEADLINE: CBS + EDF
EDF (Earliest Deadline First) with Constant Bandwidth Server (CBS) provides temporal isolation for real-time tasks. Each task declares a (runtime, period, deadline) triplet. The kernel guarantees the task receives runtime nanoseconds every period. If a task exceeds its budget, CBS pushes its deadline forward, preventing it from starving other DEADLINE tasks.
// --- Setting SCHED_DEADLINE (man sched_setattr) ---
struct sched_attr {
.size = sizeof(struct sched_attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 3000000, // 3ms runtime budget
.sched_deadline = 10000000, // 10ms deadline
.sched_period = 10000000, // 10ms period
};
sched_setattr(0, &attr, 0);
4.2 SCHED_FIFO and SCHED_RR
POSIX real-time policies differ from SCHED_DEADLINE in that they do not enforce bandwidth isolation. SCHED_FIFO runs until a higher-priority task wakes or the task voluntarily yields. SCHED_RR adds a round-robin time quantum (typically 100ms) after which the task is moved to the back.
5. NUMA-Aware Scheduling
On Non-Uniform Memory Access (NUMA) systems, accessing remote memory can be 2-5x slower than local memory. Linux schedulers incorporate NUMA awareness through:
- NUMA balancing (AutoNUMA): Kernel samples page access via periodic page faults to detect when pages could migrate closer to the accessing CPU
- Per-NUMA-node runqueues: EEVDF/CFS maintain run-queue structure awareness of topology
- Wake-up affinity: Newly woken tasks prefer the same NUMA node as the waker
- Task placement: fork()/exec() spreads tasks across NUMA nodes for initial balance
6. Energy-Aware Scheduling (EAS)
On asymmetric CPU topologies (e.g., ARM big.LITTLE, Intel P-cores/E-cores), the scheduler must balance performance with power consumption. EAS builds a power model of the CPU topology and predicts whether migrating a task to a more capable (but more power-hungry) core is worth the throughput gain.
// --- Energy model node ---
struct em_perf_state {
unsigned long frequency; // KHz
unsigned long power; // mW (dynamic power)
unsigned long cost; // power/frequency efficiency metric
};
struct em_perf_domain {
struct em_perf_state *table; // performance states
unsigned int nr_perf_states;
// ... CPU mask for this domain
};
EAS uses the energy model to decide: if the performance domain has enough spare capacity, keep tasks on efficient cores; if throughput is bottlenecked, escalate to performance cores.
7. Kernel Parameters and Tuning
| Parameter | Default | Purpose |
|---|---|---|
| sched_min_granularity_ns | 4000000 (4ms) | Minimum CPU time a task is guaranteed before preemption (CFS) |
| sched_wakeup_granularity_ns | 5000000 (5ms) | Threshold for preempting a sleeping task on wake |
| sched_migration_cost_ns | 500000 (0.5ms) | Task "cache hotness" — avoids migrating cache-warm tasks |
| sysctl_sched_nr_migrate | 32 | Load balance batch size |
| kernel.sched_rr_timeslice_ms | 100 | SCHED_RR round-robin time quantum |
| /sys/kernel/debug/sched/prefer_idle | 0 | Prefer idle core selection |
Linux 6.6 adds the sched_policy syscpu parameter to switch between CFS and EEVDF at runtime:
# Switch to EEVDF
echo 1 > /sys/kernel/debug/sched/policy
# Check current scheduler
cat /sys/kernel/debug/sched/policy
# Output: EEVDF (or CFS)
8. Performance Benchmarks: CFS vs EEVDF
Early benchmark data from Linux 6.6 testing shows:
- Desktop latency (gnome-desktop): 15-20% reduction in frame rendering jitter
- Redis throughput: +5-12% on mixed workload due to faster preemption
- PostgreSQL OLTP: +3-8% throughput with lower p99 tail latency
- Compilation (make -j32): Neutral to slight improvement (CPU-bound workloads benefit less)
- Network I/O (nginx): +10% requests/sec at p99 due to more responsive wakeups
9. The Future of Linux Scheduling
The scheduler development roadmap includes:
- eBPF-based scheduling extensions: Allow user-space programs to define custom scheduling policies (already prototyped)
- Rust for sched_ext: Schedulers with safe Rust abstractions for kernel-level scheduling logic
- Cloud-native scheduling: Better container-level CPU share management with cgroup-aware scheduling
- Heterogeneous core orchestration Improving P-core/E-core assignment for Intel hybrid architectures
- io_uring integration: Completing async I/O operations directly from the scheduler hot path
10. Conclusion
The Linux kernel scheduler represents one of the most critical and continuously evolving subsystems in operating system design. From the introduction of CFS in 2.6.23 to the arrival of EEVDF in 6.6, each generation has brought measurable improvements in fairness, latency robustness, and throughput. Understanding these internals is essential for anyone building high-performance systems on Linux — whether tuning a real-time trading platform, scaling a cloud-native microservice architecture, or simply ensuring a responsive desktop.

发表评论 取消回复