On this page
Scheduler
Coverage: CFS (vruntime/Red-Black Tree) → EEVDF (6.6+) → Scheduling Class Architecture → Load Balancing → Preemption Kernel Versions: 2.6 ~ 6.x, highlighting the CFS→EEVDF transition
Overview
The scheduler is one of the most frequently called subsystems in the kernel. Every schedule() call must answer one question: Who runs next? This question seems simple, but balancing "interactive responsiveness, high throughput, fairness, energy efficiency, and multi-core scalability" is extremely complex.
Linux has gone through three eras of scheduling:
- 2.4: O(n) scheduler — iterates through all processes to select the highest priority
- 2.6 ~ 6.5: CFS (Completely Fair Scheduler) — Red-Black Tree driven by vruntime
- 6.6+: EEVDF — Earliest Eligible Virtual Deadline First
This article focuses on the design principles of CFS and EEVDF, tracing key code paths.
Scheduling Class Architecture
The Linux scheduler is not a single algorithm, but a stackable scheduling class architecture:
Priority from high to low:
| Scheduling Class | Policy | Typical Scenario |
|---|---|---|
stop_sched_class | Highest priority, preempts everything | CPU hotplug, migration |
dl_sched_class | Deadline (EDF + CBS) | Hard real-time tasks |
rt_sched_class | POSIX RT (FIFO / RR) | Soft real-time, chrt -f |
fair_sched_class | CFS / EEVDF | All normal processes |
idle_sched_class | Runs only when no other tasks exist | Idle task (PID 0 per-CPU) |
// kernel/sched/sched.h
;
The stackable design of scheduling classes is one of the most elegant architectural decisions in the Linux scheduler: real-time and normal tasks share the same schedule() entry point, requiring no special handling. If any scheduling class returns NULL, the framework asks the next class. Finally, idle_sched_class ensures there is always a process to run.
Core Data Structures
runqueue (struct rq)
// kernel/sched/sched.h
;
Each CPU has its own struct rq. The lock granularity is per-rq, meaning scheduling decisions only require locking this CPU's lock—this is key to the scheduler's scalability on systems with many cores. raw_spinlock_t means interrupts must be disabled when acquiring this lock, because the scheduler tick can trigger at any time.
sched_entity — Scheduling Entity
// include/linux/sched.h
;
CFS: Fairness Driven by vruntime
Core Idea
CFS does not allocate CPU by time slices, but sorts by virtual run time (vruntime):
vruntime = Physical Execution Time × (NICE_0_LOAD / weight_of_process)
The lower the nice value (higher priority), the larger the weight → The same physical time accumulates less vruntime → Positioned further left in the Red-Black Tree → Selected earlier.
Essence: CFS pursues vruntime convergence. All processes' vruntime should converge; prioritizing the process with the smallest vruntime compensates for the process that has "owed the most CPU time."
nice → weight Mapping
// kernel/sched/core.c
const int sched_prio_to_weight = ;
Key property: For every one level decrease in nice value, weight increases by approximately 25%. This means a nice -20 process gets about 87 times the CPU time of a nice 0 process (not a simple linear relationship). This exponential decay design comes from the original CFS paper—it ensures consistent fairness gaps between adjacent nice values.
Key Functions
// kernel/sched/fair.c
// Update vruntime (called on every tick / enqueue / dequeue / put_prev)
static void
// Pick next: Leftmost node in Red-Black Tree
static struct sched_entity *
calc_delta_fair() implements delta * (NICE_0_LOAD / weight). It uses 128-bit fixed-point arithmetic to avoid overflow and handles precision issues (preventing shrinkage for very small deltas to avoid vruntime stagnation).
vruntime Boundary Handling
The initial vruntime for a new process cannot start from 0—otherwise, a newly forked process would monopolize the CPU for a long time. CFS's approach:
// kernel/sched/fair.c: place_entity()
static void
The penalty returned by sched_vslice() is approximately equal to the time slice this process could run for the first time—this ensures the new process is not "poorer" than processes already running, maintaining fairness.
min_vruntime Tracking
// cfs_rq->min_vruntime monotonically increases, tracking the minimum vruntime of all dequeued processes
// Updated in update_min_vruntime():
static void
// min_vruntime never goes backward—this is the key invariant ensuring fairness
EEVDF: The Paradigm Shift in 6.6
Why EEVDF is Needed
CFS's vruntime sorting is fair in CPU-bound scenarios, but has flaws in latency-sensitive scenarios:
- A process sleeps for a long time → vruntime becomes extremely low compared to others → Occupies CPU for a long time after being woken up (latency spike)
- CFS has no concept of "when it should finish," only "who should run most"
- Response latency is unpredictable—sleeping processes may take a long time to get on the CPU (vruntime is low but doesn't guarantee immediate response)
EEVDF solves this by introducing deadlines. Each entity calculates an eligible time (when it is eligible to run) and a deadline (expected completion time) upon enqueue. The scheduler sorts by deadline but only selects from eligible processes.
EEVDF Core Algorithm
// kernel/sched/fair.c (6.6+)
// Request time (request): scale the slice by weight
static u64
// Enqueue: Calculate deadline
static void
// pick_next: Select the earliest deadline from eligible entities
static struct sched_entity *
Key Differences:
| CFS | EEVDF | |
|---|---|---|
| Selection Criteria | Smallest vruntime | Earliest deadline (from eligible) |
| Slept Process Behavior | Monopolizes CPU for long time (extremely low vruntime) | Has lag compensation upon wake-up, but must re-queue after slice expires |
| Latency Guarantee | None | Yes (each process has a clear completion deadline) |
| Implementation Complexity | Lower | Medium (adds eligibility checks and vlag) |
Dynamic Adjustment of Slice and Deadline
// sysctl_sched_base_slice: Base time slice (default 3ms for EEVDF)
// Actual slice per process = base × (weight / cfs_rq_weight)
// Processes with higher weights get longer slices → Fewer context switches
// If a process yields CPU before slice expires (e.g., blocking on IO):
// → vruntime stops growing → vlag increases → Easier to become eligible upon wake-up
// • Compensates for interactive "use briefly then wait" patterns
Lag Compensation Mechanism
// Lag: This is the biggest implementation difference between EEVDF and CFS
// Definition: lag = avg_vruntime - se->vruntime
// lag > 0: Process lags behind average → Should be prioritized
// lag < 0: Process leads average → Should wait
// Upon wake-up:
static int
Preemption
Wakeup Preemption
// kernel/sched/fair.c: check_preempt_wakeup()
static void
Tick Preemption
// kernel/sched/fair.c: entity_tick()
// Triggered by every scheduler tick (CONFIG_HZ, typically 250/500/1000)
static void
NEED_RESCHED and Deferred Scheduling
// resched_curr() only sets the TIF_NEED_RESCHED flag
// schedule() is not called here — we are in interrupt context!
// Actual context switch happens in exit_to_user_mode_loop() before returning to user mode:
//
// exit_to_user_mode_loop() {
// if (ti_work & _TIF_NEED_RESCHED)
// schedule();
// }
//
// Or in preempt_schedule_irq() for preemptible kernel (CONFIG_PREEMPT=y)
Load Balancing
Scheduling Domain Hierarchy
// Typical hierarchy for modern 2-socket AMD EPYC:
// SMT (Hyper-threading, L1 shared) < MC (Multi-core, L3 shared) < DIE (Same socket) < NUMA (Cross-socket)
// Each layer has its own sched_domain, forming nested CPU masks
// kernel/sched/topology.c: Initialized by arch code at boot
// x86: arch/x86/kernel/smpboot.c constructs scheduling domain topology
// ARM: drivers/base/arch_topology.c obtains from DT/ACPI
Load Balancing Trigger Times
There are four types of balancing:
// 1. Idle balance: CPU about to idle → newidle_balance() called in schedule()
// → Actively pull tasks from busy CPUs (Fast startup, avoids idle latency)
// 2. Periodic balance: scheduler_tick() → trigger_load_balance()
// → SCHED_SOFTIRQ → run_rebalance_domains()
// → Executed asynchronously by softirq, frequency = 1 / (4 * sched_domain->interval)
// 3. Fork/exec balance: wake_up_new_task() → select_task_rq_fair()
// → find_idlest_cpu() → Find least busy CPU within scheduling domain
// 4. Wake balance: try_to_wake_up() → select_task_rq_fair()
// → find_idlest_cpu() or wake_affine() (Prefer wake-up CPU)
load_balance Implementation
// kernel/sched/fair.c: load_balance()
static int
Group Scheduling (CGroup CPU Controller)
// Enable: CONFIG_CGROUP_SCHED
// Interface: /sys/fs/cgroup/cpu/<group>/
// cpu.shares — Weight allocation (default 1024)
// Groups share CPU proportionally by shares. A=2048, B=1024 → A:B = 2:1
// cpu.cfs_quota_us — Max CPU time within period
// cpu.cfs_period_us — Period length (default 100ms)
// quota=50000, period=100000 → Max 50% CPU usage
Each cgroup corresponds to a task_group, containing a sched_entity embedded in the parent cfs_rq. This se "disguises" itself as a normal process to participate in parent group scheduling. After receiving CPU time, it performs secondary allocation within its own cfs_rq based on weights of processes within the group.
Debugging and Observation
# Process scheduling statistics (Most critical file)
# Content: se.sum_exec_runtime, se.vruntime, se.statistics.wait_sum,
# nr_switches, nr_voluntary_switches, nr_involuntary_switches
# System-level scheduler debug info
# Contains: Per-CPU cfs_rq stats, nr_running, min_vruntime, Red-Black Tree size
# ftrace: Capture scheduling events
|
# bpftrace: Statistics scheduling latency by process
# Check if EEVDF is enabled
# Kernel cmdline: Debug switches
# sched_verbose — More dmesg info
# skew_tick=1 — Offset CPU ticks to reduce lock contention
References and Further Reading
- Kernel Documentation:
Documentation/scheduler/directory, especiallysched-design-CFS.rstandsched-eevdf.rst - LWN:
- "EEVDF scheduler design" series (2023, lwn.net/Articles/925371/)
- "The EEVDF scheduler" (lwn.net/Articles/940176/)
- "CFS group scheduling" (lwn.net/Articles/240474/)
- "The multi-queue block and CPU schedulers" (lwn.net/Articles/552451/)
- Source Files:
kernel/sched/fair.c— CFS/EEVDF (~8000 lines, main scheduler body)kernel/sched/core.c— General interfaces like schedule(), try_to_wake_up()kernel/sched/sched.h— Definitions of struct rq, struct cfs_rq, sched_classkernel/sched/topology.c— Scheduling domain constructionkernel/sched/debug.c— Implementation of /proc/sched_debug
- Classic Papers:
- "EEVDF: Earliest Eligible Virtual Deadline First" (Stoica & Abdel-Wahab, 1995)
- "CFS: Completely Fair Process Scheduling in Linux" (MolNar, 2007 LWN)
Keywords: CFS, EEVDF, vruntime, sched_entity, cfs_rq, wakeup preemption, load_balance, sched_domain, cgroup scheduling, vlag