Scheduler Subsystem Details
Runqueue Locking
- Runqueue locks are
raw_spinlock_t(never sleeps, even on PREEMPT_RT) - Multi-runqueue operations must lock in consistent order to prevent deadlock
double_rq_lock()/double_rq_unlock()handle ordering automatically (swaps to ascending order internally viarq_order_less())lockdep_assert_rq_held()validates rq lock is held in accessorstask_rq(p)expands tocpu_rq(task_cpu(p)). Without pinning or holdingpi_lock/ rq lock, the task can migrate aftertask_cpu(p)is read, so the caller gets CPU A's runqueue while the task is now on CPU B.- Never release rq lock with a task in inconsistent state —
on_rq, the RB-tree, andp->__statemust all agree before the lock is dropped. Other CPUs observe these fields immediately after unlock:- Task in tree but
on_rq == 0:try_to_wake_up()seeson_rq == 0and callsactivate_task()→ double enqueue corrupts the RB-tree on_rq == 1but not in tree:try_to_wake_up()seeson_rq == 1and skips enqueue, butpick_next_tasknever finds the task — permanently lostTASK_RUNNINGwithon_rq == 0:try_to_wake_up()seesTASK_RUNNINGand returns early (no wakeup needed) — task hangs forever with no way to recover
- Task in tree but
Task State Transitions
set_current_state()includes a barrier so the state write is ordered relative to subsequent memory accesses (the condition check)- Voluntary sleep pattern: set state BEFORE checking the condition, otherwise a wakeup between the check and the state change is lost
// CORRECT — state set before condition check
set_current_state(TASK_UNINTERRUPTIBLE);
if (!condition)
schedule();
set_current_state(TASK_RUNNING);
// WRONG — lost wakeup if condition changes between check and set_current_state
if (!condition) {
set_current_state(TASK_UNINTERRUPTIBLE); // wakeup already happened
schedule(); // sleeps forever
}
TASK_RUNNINGis the only state where a task can be on a runqueueTASK_DEADrequires special handling — task cannot be rescheduledwake_up_process()handles state races internally (safe for any task state)
CPU Affinity and Migration
set_cpus_allowed_ptr()changes a task's allowed CPU maskkthread_bind()restricts a kthread to a specific CPU- Migration must respect
cpumask_subset()against allowed CPUs - Check
is_migration_disabled()before migrating a task stop_one_cpu()may be needed for forced migration- CPU hotplug requires special care — tasks must not be migrated to a CPU being taken offline
- Migration-disabled sections cannot block or sleep
CFS (SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE)
EEVDF Algorithm
- Selects the eligible task (positive lag = owed service) with the earliest virtual deadline
- Virtual deadline:
vd = vruntime + vslicewherevslice = calc_delta_fair(slice, se)computesslice * NICE_0_LOAD / weight(source comment:vd_i = ve_i + r_i/w_i) - Base slice:
sysctl_sched_base_slice(default 700μs); deadline updated whenvruntime >= deadline
Key Fields
vruntime: per-entity monotonic counter of weighted CPU time; must be normalized when migrating between CPUsmin_vruntime: per-cfs_rq monotonic clock tracking progress; must never decreasevlag: tracks service deficit/surplus;vlag = V - vruntimewhere V is the weighted average vruntime (avg_vruntime); preserved across enqueue/dequeue byplace_entity()on_rq: must be 1 iff entity is in the RB-tree; double enqueue or mismatch corrupts the runqueue- Load weight: derived from nice value via
prio_to_weight[]; ~10% change per nice level
RB-Tree Structure
The RB-tree is sorted by deadline and augmented with min_vruntime per node,
enabling O(log n) eligibility pruning in __pick_eevdf(). A subtree can be
skipped entirely if its min_vruntime shows no eligible entities.
PELT (Per-Entity Load Tracking)
update_load_avg()must be called BEFORE any entity state change (enqueue/dequeue/migration) to maintain hierarchy consistency- Tracks a decaying average of utilization per entity and per cfs_rq
- On migration:
DO_ATTACHattaches load to new CPU,DO_DETACHdetaches from old CPU
CFS Bandwidth
- Group throttling uses
cfs_rq->runtime_remaining - Must properly dequeue on throttle, enqueue on unthrottle
- Hierarchical: parent throttle affects all children
Real-Time (SCHED_FIFO / SCHED_RR)
- Priority range: 1–99 (userspace); higher number = higher priority.
Kernel internal priority is inverted (
MAX_RT_PRIO - 1 - rt_priority) - RT bandwidth:
sched_rt_runtime_us/sched_rt_period_usprevent CPU monopolization (default 95% limit — 950ms per 1000ms period) - Tasks throttled when bandwidth exhausted; check
sched_rt_runtime()
Deadline (SCHED_DEADLINE)
- Invariant:
runtime ≤ deadline ≤ period - Admission control: tasks can be created while
Σ(runtime_i / period_i) ≤ M × (rt_runtime / rt_period)where M is CPU count in the root domain - CBS (Constant Bandwidth Server) enforces bandwidth isolation
- Global Earliest Deadline First (GEDF): on an M-CPU system, the M tasks with earliest deadlines should be running
- Throttling: task blocked when runtime exhausted, unblocked at next period (replenishment)
- DL tasks tracked per root domain for migration decisions
sched_ext (SCHED_EXT — BPF Extensible Scheduler)
- Scheduler behavior defined by BPF programs via ops callbacks (
select_cpu,enqueue,dispatch, etc.) - Dispatch queues (DSQ): tasks queued in local (per-CPU), global, or custom DSQs; can use FIFO or PRIQ (vtime) ordering but not mixed
ops_statetracking: atomicp->scx.ops_stateprevents concurrent BPF operations on the same task; transitions: NONE → QUEUEING → QUEUED → DISPATCHING- Direct dispatch: optimization allowing enqueue path to dispatch directly to
local DSQ via per-CPU
direct_dispatch_taskmarker - Failsafe: watchdog timeout,
scx_error(), and SysRq-S all trigger automatic revert to CFS
Priority Inheritance
- When a high-priority task blocks on a lock held by a low-priority task, the lock holder temporarily inherits the blocked task's priority
- PI chain must be updated atomically; checked for cycles to detect deadlock
- RT mutex is the primary use case
pi_lockprotects PI state; must be held when traversing the chain
Quick Checks
preempt_disable()/preempt_enable()must always be balancedset_task_cpu()only safe during migration with proper locks held- Never call
schedule()with preemption disabled or non-rq locks held - Never enqueue a task that is already on a runqueue