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

ScenarioCFS BehaviorEEVDF Behavior
New task wakesGoes to tree right of min-vruntime; waits for tick preemptionGets immediate deadline calculation; eligible tasks preempt without waiting for tick
Overloaded system (N >> CPUs)Latency proportional to #runnable tasksLatency bounded by request_duration/quantum
Mixed workload (CPU + IO)Sleeper fairness causes compensation jitterLag-based compensation more predictable
Real-time urgencyRT handled by separate class, but latency spikes at CFS boundariesEEVDF'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

ParameterDefaultPurpose
sched_min_granularity_ns4000000 (4ms)Minimum CPU time a task is guaranteed before preemption (CFS)
sched_wakeup_granularity_ns5000000 (5ms)Threshold for preempting a sleeping task on wake
sched_migration_cost_ns500000 (0.5ms)Task "cache hotness" — avoids migrating cache-warm tasks
sysctl_sched_nr_migrate32Load balance batch size
kernel.sched_rr_timeslice_ms100SCHED_RR round-robin time quantum
/sys/kernel/debug/sched/prefer_idle0Prefer 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.

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.369021s