Per-CPU Run Queues and struct rq
Linux does not keep one global list of runnable tasks. It keeps one run queue per CPU, each an instance of
struct rq, stored in the per-CPU variablerunqueues. A run queue is the local universe of a single core: the set of tasks ready to run there, the bookkeeping the scheduler needs to pick among them, the lock that serializes all of it, and the per-CPU clock that times slices. The fair scheduler’s design doc puts the rationale plainly — “CFS uses a runqueue per CPU” — and this per-CPU structure is why the scheduler scales: most scheduling decisions touch only the localrqand never contend on a global lock (sched-design-CFS). Crucially,struct rqdoes not itself hold a flat list of tasks; it embeds one sub-runqueue per scheduling class (struct cfs_rq cfs,struct rt_rq rt,struct dl_rq dl, andstruct scx_rq scx) plus, since v6.12, one embedded server entity (struct sched_dl_entity fair_server) that the deadline class uses to guarantee fair tasks a slice of bandwidth. Each policy therefore keeps its tasks in its own optimal data structure while sharing the per-CPU envelope, the per-CPU lock, and the per-CPU clock. The definitions below are verified againstkernel/sched/sched.hat 6.12 LTS (struct rqbegins at line 1096) and cross-checked at 6.18 LTS (struct rqat line 1119), where the field set is materially unchanged but theCONFIG_SCHED_DEBUGguards have been removed (v6.12 sched.h, v6.18 sched.h).
This note is about the container. The polymorphic methods that operate on it are Scheduling Classes and the sched_class Interface; the master routine that locks it and drives a context switch is The Core Scheduler and __schedule.
What this note owns, and what it does not. It owns struct rq as a data structure and as a synchronisation object: the field layout and what each group of fields is for, the per-CPU declaration and the cpu_rq/this_rq/task_rq accessors, the rq->__lock versus rq_lockp() distinction, the two- and three-lock ordering rules (task_rq_lock, double_rq_lock, double_lock_balance), the rq->clock/clock_task/clock_pelt triple and the RQCF_* update protocol, and the enqueue/dequeue contract that keeps rq->nr_running truthful. It does not own: the algorithms inside each sub-runqueue — The EEVDF Scheduler for cfs, Real-Time Scheduling SCHED_FIFO and SCHED_RR for rt, SCHED_DEADLINE and Earliest Deadline First for dl, sched_ext and BPF-Defined Schedulers for scx; the balancing policy that decides which tasks to move — Scheduler Load Balancing and Scheduling Domains and CPU Topology; the register-and-page-table mechanics of the switch itself — Context Switch Mechanics switch_to and switch_mm; the spinlock implementation — Kernel Spinlocks; and rq->stop and the stop class — Migration the Stop Class and the Migration Thread.
Mental Model — A Private Workspace Per Core
flowchart TB subgraph CPU0["CPU 0"] RQ0["struct rq (runqueues[0])<br/>__lock, nr_running, clock<br/>curr / idle / stop"] RQ0 --> C0["cfs (cfs_rq) — EEVDF rbtree"] RQ0 --> R0["rt (rt_rq) — prio array"] RQ0 --> D0["dl (dl_rq) — EDF rbtree"] RQ0 --> X0["scx (scx_rq) — sched_ext"] end subgraph CPU1["CPU 1"] RQ1["struct rq (runqueues[1])<br/>__lock, nr_running, clock<br/>curr / idle / stop"] RQ1 --> C1["cfs"] RQ1 --> R1["rt"] RQ1 --> D1["dl"] RQ1 --> X1["scx"] end BAL["load balancer<br/>(sched_domains)"] -.->|"migrate tasks,<br/>take both rq locks"| RQ0 BAL -.-> RQ1
The per-CPU run queue layout. What it shows: each CPU owns one struct rq, accessed as runqueues[cpu]; inside it are the per-class sub-runqueues, each holding that class’s runnable tasks in its own structure (EEVDF’s augmented red-black tree for cfs, a priority-indexed array for rt, an EDF tree for dl). The insight to take: because every core has its own queue and its own lock, a scheduling decision on CPU 0 normally never contends with CPU 1 — contention happens only when the load balancer (dashed) deliberately reaches across to move a task, at which point it must lock both run queues in a defined order to avoid deadlock.
Walking struct rq Field by Field
struct rq spans lines 1096–1302 of kernel/sched/sched.h at v6.12 — roughly 200 lines, more than half of them behind #ifdefs. Reading it linearly is unrewarding; reading it by purpose is not. Every field falls into one of eight groups.
| Group | Fields (v6.12 line numbers) | What the group is for | Always present? |
|---|---|---|---|
| Serialization | __lock (1098) | the one lock protecting everything else in the structure; reached only via rq_lockp() | yes |
| Population counters | nr_running (1100), nr_uninterruptible (1149), nr_switches (1119), nr_iowait (1171), ttwu_pending (1117) | how many tasks are runnable across all classes, how many are in uninterruptible sleep, and how much churn there has been; these drive idle detection, load balancing, and /proc/loadavg | nr_running yes; ttwu_pending needs CONFIG_SMP |
| Per-class sub-runqueues | cfs (1128), rt (1129), dl (1130), scx (1132) | the actual task containers, one per scheduling class, embedded by value not by pointer | scx needs CONFIG_SCHED_CLASS_EXT |
| Servers | fair_server (1135), dl_server (1152) | a deadline-class entity that runs fair tasks under a bandwidth reservation, and the pointer to whichever server supplied the currently running task | yes (since v6.12) |
| Distinguished tasks | curr (1151), idle (1153), stop (1154), prev_mm (1156) | what is running now, and the two per-CPU kthreads the scheduler must always be able to fall back on | yes |
| Clocks | clock_update_flags (1158), clock (1159), clock_task (1161), clock_pelt (1162), clock_idle (1165), lost_idle_time (1163) | this CPU’s private, lock-protected notion of time, in three flavours: raw, IRQ/steal-discounted, and PELT-scaled | yes |
| Topology and balancing | rd (1183), sd (1184), cpu (1201), online (1202), cpu_capacity (1186), cfs_tasks (1204), next_balance (1155), active_balance/push_cpu/active_balance_work (1196–1198), balance_callback (1188), nr_pinned (1271), misfit_task_load (1193), avg_rt/avg_dl/avg_irq/avg_hw (1206–1212), idle_stamp/avg_idle/max_idle_balance_cost (1214–1218), scratch_mask (1296) | where this CPU sits in the topology, how loaded it is, and the state machine for pulling and pushing tasks | CONFIG_SMP |
| Feature-specific | NUMA (nr_numa_running 1102–1104), tickless (nohz_* 1106–1114), uclamp (1121–1126), membarrier (1179), IRQ/steal accounting (1226–1234), hrtick (1240–1246), schedstats (1248–1263), cpuidle (1267), core scheduling (core, core_pick, core_cookie, core_forceidle_* 1276–1293), CFS bandwidth IPI (1298–1301) | optional subsystems that hang their per-CPU state off the run queue because they need it under the same lock | each behind its own CONFIG_ |
Two structural facts are worth stating explicitly before the prose walk. First, the sub-runqueues are embedded by value, not referenced by pointer — struct cfs_rq cfs; not struct cfs_rq *cfs;. That is why container_of(cfs_rq, struct rq, cfs) works as the !CONFIG_FAIR_GROUP_SCHED definition of rq_of() (sched.h:1314–1317), and why one cache line of rq can carry the head of the fair tree. Second, the structure is deliberately not ordered for readability; clock_task carries an explicit ____cacheline_aligned (1161) with the comment “Ensure that all clocks are in the same cache line”, and the whole rq is declared DEFINE_PER_CPU_SHARED_ALIGNED so that a remote CPU touching runqueues[n] never false-shares with runqueues[n±1].
classDiagram class rq { raw_spinlock_t __lock unsigned int nr_running unsigned int nr_uninterruptible u64 nr_switches task_struct* curr task_struct* idle task_struct* stop sched_dl_entity* dl_server u64 clock u64 clock_task u64 clock_pelt unsigned int clock_update_flags int cpu int online root_domain* rd sched_domain* sd list_head cfs_tasks } class cfs_rq { load_weight load unsigned int h_nr_running s64 avg_vruntime u64 min_vruntime rb_root_cached tasks_timeline sched_entity* curr } class rt_rq { rt_prio_array active unsigned int rt_nr_running unsigned int rr_nr_running highest_prio curr_next plist_head pushable_tasks int rt_queued } class dl_rq { rb_root_cached root unsigned int dl_nr_running earliest_dl curr_next u64 running_bw u64 max_bw } class scx_rq { BPF dispatch queues } class fair_server["fair_server : sched_dl_entity"] { u64 dl_runtime = 50ms u64 dl_period = 1s bit dl_server = 1 bit dl_defer = 1 } rq *-- cfs_rq : embedded by value rq *-- rt_rq : embedded by value rq *-- dl_rq : embedded by value rq *-- scx_rq : CONFIG_SCHED_CLASS_EXT rq *-- fair_server : embedded by value
struct rq and the five things embedded inside it, at v6.12. What it shows: the composition (filled-diamond) relationships are all by value — these are not separately allocated objects but bytes inside the same per-CPU allocation, so one rq->__lock acquisition protects the lot. Each sub-runqueue carries a different container shape because each class answers a different question. The insight to take: rq->nr_running is a derived total — it must equal cfs.h_nr_running + rt.rt_nr_running + dl.dl_nr_running (+ scx) at all times, and keeping it so is the entire contract that add_nr_running()/sub_nr_running() exist to enforce. fair_server is the odd one out: it is a sched_dl_entity, not a run queue, and it exists so the deadline class can schedule the fair class as if it were a deadline task.
The five containers, and why each has the shape it has
| Sub-structure | Field in rq | Container | Selection cost | What the shape buys |
|---|---|---|---|---|
struct cfs_rq | cfs (sched.h:646) | rb_root_cached tasks_timeline — an augmented red-black tree keyed by virtual deadline, with avg_vruntime/avg_load maintained incrementally so the “eligible” test is O(1) | O(log n) insert/delete, O(1) leftmost read via the _cached leftmost pointer | ordering by a continuously changing key with fair, weight-proportional service — see The EEVDF Scheduler |
struct rt_rq | rt (sched.h, struct rt_rq) | rt_prio_array active — 100 list_head queues plus a DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1) with a delimiter bit | O(1) — sched_find_first_bit() on the bitmap, then take the list head | strict fixed-priority selection in constant time regardless of task count — see Real-Time Scheduling SCHED_FIFO and SCHED_RR |
struct dl_rq | dl (sched.h:833) | rb_root_cached root keyed by absolute deadline, plus running_bw/this_bw/extra_bw/max_bw utilisation accounting and a separate pushable_dl_tasks_root | O(log n) insert, O(1) leftmost | earliest-deadline-first ordering plus the bandwidth sums that admission control needs — see SCHED_DEADLINE and Earliest Deadline First |
struct scx_rq | scx (sched.h:762) | BPF-managed dispatch queues | whatever the loaded BPF scheduler implements | lets a BPF program own the policy entirely — see sched_ext and BPF-Defined Schedulers |
struct sched_dl_entity | fair_server (sched.h:1135) | not a container — a single reservation entity queued into rq->dl | n/a | lets the deadline class force CPU time to the fair class, replacing RT throttling |
Note the asymmetry in the counters. cfs_rq carries both nr_running (entities at this level) and h_nr_running (tasks in the whole hierarchy below, “h” for hierarchical) because cgroup group scheduling nests cfs_rqs inside one another; rt_rq carries rt_nr_running plus a separate rr_nr_running counting only the SCHED_RR subset, which is what lets sched_can_stop_tick() decide whether a nohz_full CPU may stop its tick (a lone SCHED_FIFO task needs no tick; a pair of SCHED_RR tasks does).
The structure (sched.h line 1096 at v6.12) is large and config-heavy; the load-bearing fields are these.
The lock. The first field is raw_spinlock_t __lock. The leading underscores are deliberate: code must not touch rq->__lock directly but go through wrapper accessors (rq_lockp(rq), raw_spin_rq_lock(rq)), for a reason explained in the locking section below. It is a raw_spinlock_t — a spinlock that does not become a sleeping mutex even under PREEMPT_RT — because the scheduler core runs in atomic, IRQ-disabled context where sleeping is forbidden.
Counters. unsigned int nr_running is the number of runnable tasks across all classes on this CPU — the single number the scheduler checks to decide whether the CPU is idle and whether load balancing is warranted. nr_uninterruptible counts tasks in uninterruptible sleep (TASK_UNINTERRUPTIBLE) — its global sum feeds the classic load-average computation. nr_switches counts context switches for statistics. The comment on nr_uninterruptible is itself instructive: it is “part of a global counter where only the total sum over all CPUs matters” — a task can increment it on one CPU and decrement it on another after migration, “always updated under the runqueue lock.”
The per-class sub-runqueues. The heart of the structure:
struct cfs_rq cfs; // fair class (EEVDF) — runnable tasks in an rbtree keyed by virtual deadline
struct rt_rq rt; // real-time class — array of lists, one per RT priority
struct dl_rq dl; // deadline class — rbtree keyed by absolute deadline (EDF)
#ifdef CONFIG_SCHED_CLASS_EXT
struct scx_rq scx; // sched_ext (BPF) class
#endifEach is the per-CPU portion of its class. cfs holds the EEVDF red-black tree and the per-CPU fair-scheduling state (see Virtual Runtime and the Fair Scheduling Invariant and The EEVDF Scheduler); rt holds the priority-indexed run lists for Real-Time Scheduling SCHED_FIFO and SCHED_RR; dl holds the earliest-deadline-first tree for SCHED_DEADLINE and Earliest Deadline First; scx holds the dispatch queues that sched_ext and BPF-Defined Schedulers manage. The class abstraction means the core scheduler reaches these only through the class methods — enqueue_task knows to add a fair task to rq->cfs, an RT task to rq->rt, and so on.
The distinguished task pointers.
struct task_struct __rcu *curr; // the task currently running on this CPU
struct sched_dl_entity *dl_server;
struct task_struct *idle; // this CPU's idle task (the per-CPU swapper/idle thread)
struct task_struct *stop; // this CPU's stop/migration kthread (stop_sched_class)curr is what is running right now on this CPU; __schedule reads it as prev and writes the chosen task into it as the new current. It is annotated __rcu because other CPUs may read it lock-free under RCU (for example when deciding whether to send a reschedule IPI). idle is the special task that runs when nr_running reaches zero — idle_sched_class always has it ready, which is why __pick_next_task ends with BUG() if even idle has nothing (it never should). stop points at the per-CPU high-priority kthread that stop_sched_class schedules, used to execute migrations and other “stop the CPU and do this now” work — see Migration the Stop Class and the Migration Thread. The dl_server pointer is part of the deadline-server mechanism that lets fair tasks borrow deadline-class bandwidth so they cannot be starved by misconfigured real-time load — and it is worth a paragraph of its own, because it is the newest thing in the structure and it changed the meaning of a widely-quoted tunable.
The fair server (new at v6.12). struct sched_dl_entity fair_server (line 1135) is a deadline-class reservation owned by the run queue rather than by any task. It is initialised with dl_runtime = 50 ms, dl_period = 1 s, dl_server = 1 and dl_defer = 1 (kernel/sched/deadline.c:1636–1645), and its pick_task callback returns fair tasks. Because the deadline class outranks the real-time class in the linker-ordered class array, a run queue can now force CPU time to its fair tasks even while a SCHED_FIFO task at priority 99 is runnable — which is exactly what commit 5f6bd380c7bd (“sched/rt: Remove default bandwidth control”, Peter Zijlstra) relied on when it deleted the old per-rt_rq throttling: “Now that fair_server exists, we no longer need RT bandwidth control unless RT_GROUP_SCHED.” The dl_defer flag makes the reservation lazy — its hrtimer is armed at the zero-laxity instant deadline - runtime, i.e. 950 ms into each second — so a healthy system never pays for it. The companion dl_server pointer records which server, if any, supplied rq->curr; __pick_next_task() clears it (rq->dl_server = NULL) at the top of every pick. The design rationale is Daniel Bristot de Oliveira’s, written up by Corbet in Deadline servers as a realtime throttling replacement; the consequences for real-time workloads are traced in Real-Time Scheduling SCHED_FIFO and SCHED_RR. For this note the structural point is the one that matters: struct rq now contains a scheduling entity, not only containers of them.
The clocks. A run queue carries its own notion of time:
unsigned int clock_update_flags;
u64 clock; // rq's view of wall-ish time (ns)
u64 clock_task ____cacheline_aligned; // clock minus time stolen by IRQ/steal
u64 clock_pelt; // clock used for PELT load trackingrq->clock is the run queue’s timestamp in nanoseconds, refreshed once per scheduler activation by update_rq_clock(). rq->clock_task is clock with time consumed by hard/soft IRQ handling and (under virtualization) hypervisor-stolen time subtracted out, so that a task is only charged for time it actually got the CPU — this is the clock the fair class uses to accrue vruntime. The two are deliberately split across cache lines (____cacheline_aligned) so the hot clock_task reads don’t false-share. The accessors enforce discipline (sched.h lines 1643–1657):
static inline u64 rq_clock(struct rq *rq) {
lockdep_assert_rq_held(rq);
assert_clock_updated(rq);
return rq->clock;
}Both rq_clock and rq_clock_task assert that the rq lock is held and that the clock has been updated this activation, catching a whole class of bugs where code reads a stale timestamp.
SMP fields (under CONFIG_SMP). cpu is this run queue’s CPU id; online whether it is up; rd (root domain) and sd (the RCU-protected scheduling-domain pointer) link this CPU into the topology that Scheduling Domains and CPU Topology describes; cpu_capacity records the CPU’s compute capacity for Capacity-Aware and Energy-Aware Scheduling; nr_pinned, active_balance, push_cpu, and balance_callback drive Scheduler Load Balancing. cfs_tasks is a flat list of this CPU’s fair tasks used to pick migration candidates. There are also NUMA counters (nr_numa_running), tickless-idle fields (nohz_*, used by Housekeeping CPUs and Tickless Isolation), and, under CONFIG_SCHED_CORE, the core_* fields for SMT co-scheduling.
Enqueue and Dequeue — the Contract That Keeps nr_running Honest
Everything a run queue does rests on one invariant: rq->nr_running must equal the number of runnable tasks actually sitting in the sub-runqueues. Nothing enforces that structurally — it is maintained by hand, by two inline functions in core.c that every class routes through.
/* kernel/sched/core.c, v6.12 (abridged) */
inline bool dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
if (sched_core_enabled(rq))
sched_core_dequeue(rq, p, flags);
if (!(flags & DEQUEUE_NOCLOCK))
update_rq_clock(rq); /* charge time before removing */
if (!(flags & DEQUEUE_SAVE)) {
sched_info_dequeue(rq, p);
psi_dequeue(p, !(flags & DEQUEUE_SLEEP)); /* pressure-stall accounting */
}
uclamp_rq_dec(rq, p);
return p->sched_class->dequeue_task(rq, p, flags); /* the class does the work */
}
void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
if (task_on_rq_migrating(p))
flags |= ENQUEUE_MIGRATED;
if (flags & ENQUEUE_MIGRATED)
sched_mm_cid_migrate_to(rq, p);
enqueue_task(rq, p, flags);
WRITE_ONCE(p->on_rq, TASK_ON_RQ_QUEUED); /* publish, after the class enqueued */
ASSERT_EXCLUSIVE_WRITER(p->on_rq);
}Read the ordering carefully, because it encodes two subtle rules. p->on_rq is written after enqueue_task() returns and before dequeue_task() is called (deactivate_task() sets TASK_ON_RQ_MIGRATING first) — the comment in deactivate_task() says so explicitly: “Code explicitly relies on TASK_ON_RQ_MIGRATING being set before dequeue_task() and cleared after enqueue_task().” That window is what task_rq_lock() spins on. And the clock is updated inside enqueue/dequeue unless the caller passes *_NOCLOCK, because a task must be charged for the time it ran before its position changes.
The flag vocabulary is small and each bit has an exact meaning (sched.h:2343–2366). The DEQUEUE_* and ENQUEUE_* values are deliberately numerically matched so a save/restore pair can reuse one variable:
| Bit | DEQUEUE_* | ENQUEUE_* | Meaning |
|---|---|---|---|
0x01 | SLEEP | WAKEUP | the task is going to sleep / waking from sleep — the only case where dequeue_task() may return false |
0x02 | SAVE | RESTORE | a temporary dequeue/enqueue pair around a property change (nice, policy, cgroup); suppresses PSI and schedstat accounting so the blip is invisible |
0x04 | MOVE | MOVE | the task is changing task group |
0x08 | NOCLOCK | NOCLOCK | the caller has already called update_rq_clock() this activation — do not call it again |
0x10 | SPECIAL | HEAD | dequeue: a class-internal special case. enqueue: insert at the head of the queue rather than the tail (used when a SCHED_FIFO task is preempted, so it keeps its place) |
0x20 | — | REPLENISH | deadline-class only: this enqueue comes with a bandwidth replenishment, as when rq->fair_server’s defer timer fires |
0x40 | — | MIGRATED | the task arrived from another CPU (0 when !CONFIG_SMP) |
0x80 | — | INITIAL | first enqueue of a brand-new task from wake_up_new_task() |
0x100 | MIGRATING | MIGRATING | this dequeue/enqueue pair is one half of a migration |
0x200 | DELAYED | DELAYED | EEVDF’s delayed-dequeue: the task slept but is kept on the tree until its lag is repaid |
0x400 | — | RQ_SELECTED | the destination run queue has already been chosen |
The counter update itself is add_nr_running()/sub_nr_running() (sched.h:2743–2766), and it does three things, not one:
static inline void add_nr_running(struct rq *rq, unsigned count)
{
unsigned prev_nr = rq->nr_running;
rq->nr_running = prev_nr + count;
if (trace_sched_update_nr_running_tp_enabled())
call_trace_sched_update_nr_running(rq, count); /* 1. tracepoint */
#ifdef CONFIG_SMP
if (prev_nr < 2 && rq->nr_running >= 2)
set_rd_overloaded(rq->rd, 1); /* 2. root-domain hint */
#endif
sched_update_tick_dependency(rq); /* 3. nohz_full tick */
}Step 2 is the load balancer’s cheapest possible signal: the moment a CPU goes from “at most one runnable task” to “two or more”, the whole root domain is flagged overloaded, and idle CPUs elsewhere know it is worth attempting a pull without inspecting every run queue. Step 3 is the nohz_full interlock — sched_update_tick_dependency() calls sched_can_stop_tick(rq) and either clears or sets TICK_DEP_BIT_SCHED for this CPU, which is precisely how a tickless CPU learns that a second runnable task has appeared and the tick must come back.
sequenceDiagram autonumber participant W as waker (any CPU) participant PI as p->pi_lock participant RQ as target rq->__lock participant CL as p->sched_class W->>PI: try_to_wake_up() takes p->pi_lock (IRQs off) W->>W: select_task_rq() picks dst_cpu W->>RQ: rq_lock() on cpu_rq(dst_cpu) plus rq_pin_lock() W->>RQ: update_rq_clock(rq) - clears RQCF_REQ_SKIP, sets RQCF_UPDATED W->>CL: activate_task() then enqueue_task(rq, p, ENQUEUE_WAKEUP) CL->>CL: uclamp_rq_inc() then class->enqueue_task() Note over CL: fair - enqueue_entity() into cfs_rq->tasks_timeline (rbtree)<br/>rt - list_add_tail(queue[prio]) plus __set_bit(prio, bitmap)<br/>dl - enqueue into dl_rq->root keyed by absolute deadline CL->>RQ: add_nr_running(rq, 1) RQ->>RQ: set_rd_overloaded() if nr_running crossed 1 -> 2 RQ->>RQ: sched_update_tick_dependency() - restart the tick on a nohz_full CPU W->>W: WRITE_ONCE(p->on_rq, TASK_ON_RQ_QUEUED) W->>CL: wakeup_preempt() - class decides whether to resched_curr() W->>RQ: rq_unlock(), then release p->pi_lock
A wakeup, from the waker’s point of view, ending with the task queued on a possibly-remote run queue. What it shows: two locks are involved and they are taken in a fixed order — p->pi_lock first (it protects the task’s scheduling identity: its class, its priority, its CPU), then the run-queue lock (it protects the container). The class-specific work is one call in the middle; everything around it is run-queue bookkeeping. The insight to take: the enqueue is not finished when the class returns. rq->nr_running must be bumped, the root domain may need flagging, a tickless CPU may need its tick restarting, and only then may p->on_rq be published — which is why “enqueue” is a core-scheduler operation with a class-provided step, not a class operation with some bookkeeping attached.
Uncertain
Verify: whether
p->on_rqis written before or afterwakeup_preempt()in the fullttwu_do_activate()path, and whether the remote-wakeup (ttwu_queue_wakelist) variant reorders any of this. Reason:activate_task(),dequeue_task()anddeactivate_task()were read verbatim atkernel/sched/core.c:2018–2090, butttwu_do_activate()and theWF_ON_CPU/wake-list fast paths were not traced line by line, and the diagram above compresses them. The ordering rule stated in the prose is quoted directly from the kernel comment and is not in doubt; the diagram’s step numbering is a simplification. To resolve: readttwu_do_activate()andttwu_queue()at v6.12 in full. uncertain
Accessing a Run Queue — cpu_rq, this_rq, task_rq
The run queues are one per-CPU array, declared and defined as:
DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues); // sched.h:1340
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues); // core.c:120SHARED_ALIGNED places each CPU’s rq on its own cache line(s) so cross-CPU accesses (the balancer touching a remote rq) don’t thrash the owning CPU’s cache. The standard accessors (sched.h lines 1342–1346):
#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu))) // the rq for an arbitrary CPU
#define this_rq() this_cpu_ptr(&runqueues) // the rq for the CPU we're running on
#define task_rq(p) cpu_rq(task_cpu(p)) // the rq a given task is queued on
#define cpu_curr(cpu) (cpu_rq(cpu)->curr) // the task currently running on a CPU
#define raw_rq() raw_cpu_ptr(&runqueues)this_rq() is the workhorse on the local hot path (e.g. rq = cpu_rq(smp_processor_id()) at the top of __schedule). cpu_rq(cpu) is how the balancer reaches a remote CPU’s queue. task_rq(p) follows a task to its current queue via task_cpu(p) — which is why migrating a task is fundamentally “dequeue from task_rq(p), update task_cpu, enqueue on the destination rq.”
Locking — Why It Is __lock, Not lock
Most write-ups — and most of the kernel’s own comments — call this lock rq->lock. At v6.12 the field is actually named rq->__lock, and the rename encodes a real subtlety. Code never locks rq->__lock directly. It goes through raw_spin_rq_lock(rq), which resolves the effective lock via rq_lockp(rq). The reason is core scheduling (CONFIG_SCHED_CORE, the SMT-sibling co-scheduling feature). When core scheduling is enabled, two hyperthreads of the same physical core must coordinate their picks, so their run queues share a single lock (sched.h lines 1367–1381):
static inline raw_spinlock_t *rq_lockp(struct rq *rq) {
if (sched_core_enabled(rq))
return &rq->core->__lock; // siblings share the core leader's lock
return &rq->__lock; // otherwise, the rq's own lock
}When core scheduling is not configured (the common case), the alternate definition (lines 1466–1474) just returns &rq->__lock. So rq->__lock is the storage; rq_lockp() is the policy for which lock actually protects this rq. Calling it “rq->lock” is a useful shorthand but hides that two run queues can map to one lock.
Whatever it resolves to, it is a raw_spinlock_t taken with interrupts disabled (raw_spin_rq_lock_irqsave), because the scheduler runs from atomic context and from the timer interrupt. Every run-queue mutation — enqueue, dequeue, pick, clock update — happens under it, and lockdep_assert_rq_held(rq) peppers the code to enforce that. This dependence on the locking primitives connects directly to Linux Kernel Synchronization MOC and Kernel Spinlocks.
The lock ordering rules, in full
Four distinct acquisition helpers exist, and mixing them up is how out-of-tree scheduler code deadlocks.
| Helper | Locks taken, in order | IRQ state | When ordering can be violated | Notes |
|---|---|---|---|---|
rq_lock(rq, rf) / rq_lock_irq / rq_lock_irqsave (sched.h:1788–1810) | rq_lockp(rq) only | _irq/_irqsave variants disable; plain rq_lock assumes already off | n/a | also calls rq_pin_lock(), which resets clock_update_flags and arms the “did you update the clock?” assertion |
__task_rq_lock(p, rf) (core.c:667–686) | rq_lockp(task_rq(p)) | caller must already hold p->pi_lock and have IRQs off | n/a | retry loop: after taking the lock it re-checks rq == task_rq(p) && !task_on_rq_migrating(p), because the task may have migrated between the read and the acquire |
task_rq_lock(p, rf) (core.c:690–727) | p->pi_lock, then rq_lockp(task_rq(p)) | takes pi_lock with _irqsave | n/a | same retry loop, plus a 15-line memory-barrier comment explaining why observing either the old or the new CPU is safe |
double_rq_lock(rq1, rq2) (core.c:649–661) | the two rq_lockp()s, lower first by rq_order_less() | asserts IRQs already disabled | never — it sorts its arguments | the second acquire is raw_spin_rq_lock_nested(rq2, SINGLE_DEPTH_NESTING) so lockdep does not complain about same-class nesting; skipped entirely if both map to the same lock |
double_lock_balance(this_rq, busiest) (sched.h:3026–3031) | busiest in addition to the already-held this_rq | asserts IRQs disabled | yes, transiently — may drop this_rq and retake both | returns 1 if it dropped the lock, so the caller knows its cached state is stale |
The ordering predicate is not by address, despite that being the usual kernel idiom. rq_order_less() (sched.h:2941–2965) is:
static inline bool rq_order_less(struct rq *rq1, struct rq *rq2)
{
#ifdef CONFIG_SCHED_CORE
/*
* In order to not have {0,2},{1,3} turn into into an AB-BA,
* order by core-id first and cpu-id second.
*/
if (rq1->core->cpu < rq2->core->cpu) return true;
if (rq1->core->cpu > rq2->core->cpu) return false;
#endif
return rq1->cpu < rq2->cpu;
}The CONFIG_SCHED_CORE clause is the interesting half and it is easy to get wrong. Under core scheduling, SMT siblings share one lock (rq_lockp() returns &rq->core->__lock), so with a bare CPU-id ordering double_rq_lock(0, 3) would take core-0’s lock then core-1’s, while double_rq_lock(1, 2) would take core-1’s then core-0’s — a textbook AB-BA. Sorting by core id first collapses both to the same order. The kernel’s own comment spells out exactly that example. On the test box this is not hypothetical: /sys/devices/system/cpu/cpu4/topology/thread_siblings_list reads 4,20, so CPUs 4 and 20 are the two hyperthreads of one physical core and, with CONFIG_SCHED_CORE=y (set in this kernel’s config) and core scheduling active, their two struct rqs resolve to a single raw_spinlock_t.
double_lock_balance() additionally has two implementations chosen by CONFIG_PREEMPTION. The fair variant (preemptible kernels) unconditionally drops this_rq and calls double_rq_lock(), paying extra atomics so that lock acquisition follows the architecture’s normal spinlock fairness policy and latency stays bounded. The unfair variant (non-preemptible kernels) first tries raw_spin_rq_trylock(busiest) and, failing that, takes it nested if the order already happens to be right — cheaper, but as the comment admits it “will grant the double lock to lower CPUs over higher ids under contention, regardless of entry order”. A throughput-versus-latency trade made at build time.
sequenceDiagram autonumber participant D as dst CPU (idle-ish, pulling) participant L1 as lock(lower cpu id) participant L2 as lock(higher cpu id) participant S as src rq (busiest) D->>D: sched_balance_rq() finds busiest group then busiest rq D->>L1: double_lock_balance(this_rq, busiest) Note over L1,L2: rq_order_less() orders by rq->core->cpu first,<br/>then rq->cpu - never by address. Under CONFIG_SCHED_CORE<br/>a bare cpu-id order would turn {0,2},{1,3} into an AB-BA deadlock. D->>L2: raw_spin_rq_lock_nested(second, SINGLE_DEPTH_NESTING) D->>S: detach_tasks() - walk src_rq->cfs_tasks in reverse S->>S: deactivate_task(src_rq, p, DEQUEUE_NOCLOCK) - sub_nr_running() S->>S: set_task_cpu(p, dst_cpu) - p->on_rq = TASK_ON_RQ_MIGRATING D->>L2: double_rq_unlock() then double_rq_clock_clear_update() D->>D: attach_tasks() - rq_lock(dst_rq) plus update_rq_clock(dst_rq) D->>D: activate_task(dst_rq, p, ENQUEUE_NOCLOCK) - add_nr_running() D->>D: wakeup_preempt(dst_rq, p, 0) then rq_unlock()
A load-balance pull, the one operation that routinely touches two run queues. What it shows: the balancer detaches under the double lock and attaches under a single lock, with the task parked in TASK_ON_RQ_MIGRATING in between. detach_task() (fair.c:9456–9462) is exactly deactivate_task(env->src_rq, p, DEQUEUE_NOCLOCK); set_task_cpu(p, env->dst_cpu); — two lines — and attach_task() (fair.c:9636–9644) is activate_task(rq, p, ENQUEUE_NOCLOCK); wakeup_preempt(rq, p, 0);. The insight to take: migration is not a special primitive. It is “dequeue from one rq, rewrite task_cpu(p), enqueue on another” — the same two functions any wakeup uses, with _NOCLOCK because the balancer already updated both clocks, and with the TASK_ON_RQ_MIGRATING window as the only new concept. That window is what every task_rq_lock() retry loop is spinning on.
Finally, a nesting detail the field names hide. Under CONFIG_RT_GROUP_SCHED the rt_rq carries its own raw_spinlock_t rt_runtime_lock, and the comment on it says “Nests inside the rq lock” — so an rq lock may be held while taking an rt_runtime_lock, never the reverse. On a kernel built without CONFIG_RT_GROUP_SCHED (Fedora’s setting, and the default) that field does not exist at all. See Real-Time Scheduling SCHED_FIFO and SCHED_RR.
The Clock Update Path
rq->clock is not free-running; it is refreshed deliberately. update_rq_clock() (core.c line 789) reads the per-CPU sched clock and advances rq->clock and rq->clock_task:
void update_rq_clock(struct rq *rq) {
s64 delta;
lockdep_assert_rq_held(rq);
if (rq->clock_update_flags & RQCF_ACT_SKIP) // caller asked to skip this activation
return;
...
delta = sched_clock_cpu(cpu_of(rq)) - rq->clock;
if (delta < 0) return;
rq->clock += delta;
update_rq_clock_task(rq, delta); // also advances clock_task, subtracting IRQ/steal time
}A run queue in fact carries three clocks, not two, and each answers a different question:
| Field | Advances by | Excludes | Consumed by |
|---|---|---|---|
rq->clock (1159) | raw sched_clock_cpu() delta | nothing | the generic timestamp; rq_clock() |
rq->clock_task (1161) | clock delta minus hard-IRQ, soft-IRQ and (under CONFIG_PARAVIRT_TIME_ACCOUNTING) hypervisor-stolen time | time the task did not actually get | per-task exec runtime, vruntime accrual, deadline runtime accounting |
rq->clock_pelt (1162) | clock_task delta scaled by current CPU frequency and capacity | frequency and big/little differences | Per-Entity Load Tracking (PELT) load and utilisation signals, so a task’s measured utilisation does not change just because the CPU clocked down |
update_rq_clock_task() is what performs the subtraction, and it contains an honest admission worth reading: when an IRQ-time delta is larger than the elapsed delta — possible because irq_time is only updated on {soft,}irq_exit and a previous update_rq_clock() may have landed inside an IRQ region — the kernel stops clock_task and banks the remainder for the next update, “to ensure ->clock_task is monotonic”, at the cost of “some slight miss-attribution of {soft,}IRQ time”.
The clock_update_flags field carries three values — RQCF_REQ_SKIP (0x01), RQCF_ACT_SKIP (0x02), RQCF_UPDATED (0x04) (sched.h:1630–1632). A path that knows it will not consume time calls rq_clock_skip_update() to set RQCF_REQ_SKIP; __schedule() promotes it to RQCF_ACT_SKIP by left-shifting the whole field one position — “a left shift is a cheap operation for the fast path”, as the header comment puts it — which is also why testing for RQCF_UPDATED inside __schedule() must be written if (rq->clock_update_flags >= RQCF_UPDATED) rather than with a bitwise &. rq_pin_lock() resets the field to just the two skip bits at the start of each activation, arming the check afresh.
The assertion built on top of this is more subtle than it looks, and it is easy to state backwards:
static inline void assert_clock_updated(struct rq *rq)
{
/*
* The only reason for not seeing a clock update since the
* last rq_pin_lock() is if we're currently skipping updates.
*/
SCHED_WARN_ON(rq->clock_update_flags < RQCF_ACT_SKIP);
}The test is flags < RQCF_ACT_SKIP, i.e. flags < 2. It therefore fires when the field is 0 (nobody updated the clock and nobody asked to skip) or RQCF_REQ_SKIP alone, and it does not fire when RQCF_ACT_SKIP is set. In other words: reading a deliberately-stale clock is permitted; reading a clock that was simply never updated is the bug. A _skip_ request is an explicit statement that the caller understands the value is old. (The complementary helper rq_clock_cancel_skipupdate() exists for exactly one caller — the comment says “See rt task throttling, which is the only time a skip request is canceled” — where an RT run queue being unthrottled while the CPU was idle would otherwise have the wakeup-to-unthrottle interval wrongly charged as runtime.)
stateDiagram-v2 [*] --> Stale: rq lock taken, rq_pin_lock() resets the field Stale --> Updated: update_rq_clock()<br/>delta = sched_clock_cpu(cpu) - rq->clock<br/>rq->clock += delta, then update_rq_clock_task() Updated --> Updated: rq_clock() / rq_clock_task() read freely<br/>assert_clock_updated() passes (flags >= 2) Stale --> Splat: rq_clock() called with flags == 0<br/>SCHED_WARN_ON fires Stale --> ReqSkip: rq_clock_skip_update() sets RQCF_REQ_SKIP ReqSkip --> ActSkip: __schedule() left-shifts the field<br/>RQCF_REQ_SKIP becomes RQCF_ACT_SKIP ActSkip --> ActSkip: update_rq_clock() returns early<br/>rq_clock() is ALLOWED - the staleness is intentional ActSkip --> Stale: rq_clock_cancel_skipupdate()<br/>(only the RT unthrottle path) Updated --> [*]: rq_unlock() ActSkip --> [*]: rq_unlock()
The clock_update_flags state machine. What it shows: four reachable states and the one that produces a warning. Note that Splat is reached only from Stale — never from ActSkip. The insight to take: the assertion is not “the clock must be fresh”; it is “the clock must be fresh or someone must have said out loud that it need not be”. That distinction is the whole design: it catches the code path that forgot to update the clock, while permitting the code path that deliberately avoided a redundant sched_clock_cpu() read on the __schedule() fast path.
Uncertain
Verify: at v6.12 this entire flag-hygiene apparatus —
rq_pin_lock()’s reset,rq_unpin_lock()’s save, andSCHED_WARN_ONitself — sits behind#ifdef CONFIG_SCHED_DEBUG(sched.h:1722–1743), so on a production kernel built without it the assertion is compiled out entirely and only theRQCF_ACT_SKIPearly-return survives. That changed later:config SCHED_DEBUGis present inlib/Kconfig.debugat v6.12, v6.13 and v6.14 and absent from v6.15 onward (existence-checked), and at v6.18rq_pin_lock()andassert_clock_updated()are unconditional with plainWARN_ON_ONCE. Reason: the intermediate commit that removed the option was not read, so the exact release note wording is unverified; the v6.12/v6.15 boundary itself is established by fetchinglib/Kconfig.debugat successive tags. Consequence for the reader: the Fedora 44 test box runs 7.1.13 and has noCONFIG_SCHED_DEBUGline in its config at all — that is not “debugging disabled”, it is “the option no longer exists”. uncertain
The wall-clock-vs-task-clock split is what makes per-task time accounting fair under interrupt load — detailed alongside the tick in The need_resched Flag and Preemption Points and the time subsystem in Linux Time and Timers MOC.
Failure Modes and Diagnostics
- Stale-clock read. Reading
rq->clockwithout having calledupdate_rq_clock()this activation tripsassert_clock_updated(). Note the precise condition: it fires whenclock_update_flags < RQCF_ACT_SKIP, so an explicitrq_clock_skip_update()request suppresses it while a plain forgotten update does not. Symptom: aSCHED_WARN_ONsplat — but only on a kernel where the check is compiled in, which at v6.12 meansCONFIG_SCHED_DEBUG=yand from v6.15 means always (the option was removed). Diagnosis: a code path took the rq lock but skipped the clock update before charging a task. - Lock-ordering deadlock. Taking two run-queue locks in inconsistent order across CPUs is a classic ABBA deadlock; the kernel guards against it with
double_rq_lockordering by address and with lockdep. Out-of-tree code that locks rqs by hand is the usual culprit. nr_runningdrift. If a class’s enqueue/dequeue does not keeprq->nr_runningconsistent with the sum of its sub-runqueue counts, the CPU can appear busy when idle (no balancing, wasted core) or idle when busy (pick_next_taskhits theBUG()because it expected idle to be the only runnable task and it wasn’t). This is why enqueue/dequeue are scrutinized in Runnable State Enqueue and Dequeue.- Per-CPU access from the wrong CPU.
this_rq()is only valid with preemption disabled; using it then migrating mid-function would read the wrong queue. Hot paths run with IRQs/preemption off precisely to pin the CPU.
Inspecting Run Queues in Production
Do not reach for
/proc/sched_debug— it does not existAlmost every write-up on this subject, including older versions of this note, tells you to
cat /proc/sched_debug. That file was removed in Linux 5.13: existence-checkingproc_create(...sched_debug...)inkernel/sched/debug.cacross tags returns a hit at v5.11 and v5.12 and nothing from v5.13 onward. The dump moved to debugfs, at/sys/kernel/debug/sched/debug. Confirmed on the test box:ls -l /proc/sched_debug→No such file or directory.
The full per-run-queue dump lives at /sys/kernel/debug/sched/debug, alongside features (the SCHED_FEAT toggles), preempt (the CONFIG_PREEMPT_DYNAMIC mode), base_slice_ns, migration_cost_ns, nr_migrate, and the per-CPU fair_server/cpu<N>/{runtime,period} tunables (kernel/sched/debug.c:479–538). It prints, for every CPU, nr_running/nr_switches/nr_uninterruptible, clock and clock_task, the cfs_rq/rt_rq/dl_rq internals via print_cfs_rq()/print_rt_rq()/print_dl_rq(), and the runnable-task list. It is root-only in practice: on the test box /sys/kernel/debug is mode 0700 root:root, so none of it could be read here, and none of its contents are quoted in this note.
What an unprivileged process can read turns out to be more than one might expect, and it is all struct rq state:
| Path | Readable unprivileged? | What it exposes from struct rq |
|---|---|---|
/proc/stat → procs_running | yes | exactly nr_running(), which is for_each_online_cpu(i) sum += cpu_rq(i)->nr_running (core.c:5340–5348) — the live sum of every run queue’s depth |
/proc/stat → ctxt | yes | nr_context_switches() = sum of cpu_rq(i)->nr_switches (core.c:5374–5383) |
/proc/loadavg field 4 (R/T) | yes | nr_running() / nr_threads — same rq->nr_running sum, printed as a fraction (fs/proc/loadavg.c:20–25) |
/proc/schedstat | yes | per-CPU rq_sched_info and per-sched_domain balance counters — but only if sched_schedstats is on |
/proc/<pid>/sched | yes (own tasks) | the task’s sched_entity: vruntime, sum_exec_runtime, nr_migrations, PELT averages, uclamp, policy, internal prio, se.slice |
/proc/<pid>/stat fields 18/19/40/41 | yes | task_prio() (= p->prio - 100), nice, rt_priority, raw policy number |
/sys/kernel/debug/sched/* | no (mode 0700) | everything else |
Measured on the test box (Fedora 44, kernel 7.1.13-200.fc44.x86_64, 32 logical CPUs, 2026-09-12):
$ cat /proc/loadavg
2.62 2.23 1.98 2/4528 1698228
^^^^^^ nr_running() = 2 runnable across all 32 rqs, out of 4,528 threads
$ grep -E '^(ctxt|procs_running|procs_blocked)' /proc/stat
ctxt 5484323578 # sum of every rq->nr_switches since boot
procs_running 1
procs_blocked 0
$ cat /proc/sys/kernel/sched_schedstats
0 # schedstat counters are OFF by default: /proc/schedstat reads as zeros
$ head -4 /proc/schedstat
version 17
timestamp 4385189135
cpu0 0 0 0 0 0 0 17139783003378 27566867114530 100294125
domain0 SMT 00010001 0 0 0 0 0 0 0 0 ...
$ head -6 /proc/self/sched
cat (1698816, #threads: 1)
-------------------------------------------------------------------
se.exec_start : 127845240.998336
se.vruntime : 50614.504297
se.sum_exec_runtime : 0.000000
se.nr_migrations : 0Three things to read off that. procs_running 1 against a 32-CPU box with 4,528 threads is the normal steady state — a run queue is empty far more often than not, which is exactly why the per-CPU design costs so little. The domain0 SMT 00010001 line confirms the topology struct rq is wired into: CPU 0’s innermost scheduling domain is an SMT domain whose mask is 0x00010001, i.e. CPUs 0 and 16 — so this machine pairs CPU n with CPU n+16 as hyperthread siblings (independently confirmed: /sys/devices/system/cpu/cpu4/topology/thread_siblings_list reads 4,20). Any experiment that treats two adjacent CPU numbers as two physical cores is measuring the wrong thing. And sched_schedstats defaults to 0, so /proc/schedstat is a field of zeros until someone writes 1 to the sysctl — a trap for anyone scripting against it.
Uncertain
Verify: the contents of
/sys/kernel/debug/sched/debugon a live v6.12 kernel, and specifically whatprint_rt_rq()omits whenCONFIG_RT_GROUP_SCHEDis unset. Reason:/sys/kernel/debugis mode0700 root:rooton the only available host and there is no passwordlesssudo, so the file’s structure was established solely by readingkernel/sched/debug.cat v6.12 — never by reading the file. The commit that gated those prints showsP(rt_throttled); PN(rt_time); PN(rt_runtime);moving inside#ifdef CONFIG_RT_GROUP_SCHED, so those three lines should be absent on a Fedora-style kernel, but this was not observed. To resolve:cat /sys/kernel/debug/sched/debugas root on a v6.12+ box. uncertain
Tracing gives the other view, and is also unavailable here. bpftrace, perf, bpftool and strace are not installed on this host and /sys/kernel/tracing is unreadable (Permission denied), so nothing in this note rests on a trace. Where they are available, the scheduler tracepoints that fire as rq->curr changes and tasks move between run queues are sched_switch, sched_wakeup, sched_wakeup_new, sched_migrate_task, and sched_update_nr_running_tp — the last of these being the tracepoint called directly from add_nr_running()/sub_nr_running(), i.e. a direct feed of the counter this note is about.
A frequent real-world finding is severe imbalance — one CPU’s cfs.h_nr_running in the dozens while a sibling sits idle — caused by aggressive CPU pinning (CPU Affinity and sched_setaffinity) or isolcpus (CPU Isolation isolcpus and nohz_full) defeating the balancer. The cheap unprivileged check for it is for i in $(seq 0 31); do taskset -c $i true; done to confirm the CPUs are usable at all, then ps -eLo psr,comm | sort | uniq -c | sort -rn | head to see how threads are actually distributed across processors; a distribution that is wildly uneven while procs_running stays high is the signature.
See Also
- Scheduling Classes and the sched_class Interface — the methods that operate on the sub-runqueues inside
rq - The Core Scheduler and __schedule — locks the rq, reads
curr, drives the context switch - Scheduler Entry Points and pick_next_task — selects the next task to install as
rq->curr - Runnable State Enqueue and Dequeue — how tasks enter and leave the sub-runqueues and maintain
nr_running - The EEVDF Scheduler / Virtual Runtime and the Fair Scheduling Invariant — the
cfs_rqsub-runqueue’s contents - Scheduler Load Balancing / Scheduling Domains and CPU Topology — moving tasks between per-CPU run queues
- Migration the Stop Class and the Migration Thread — the
rq->stoptask - Linux Kernel Synchronization MOC — the spinlock and RCU primitives
rq->__lockandcurrrely on - Kernel Spinlocks — what a
raw_spinlock_tis, and why it stays a spinlock underPREEMPT_RT - CPU Affinity and sched_setaffinity / CPU Isolation isolcpus and nohz_full — the two commonest reasons a run queue is empty while another is deep
- Capacity-Aware and Energy-Aware Scheduling — what
rq->cpu_capacityand the PELTavg_*signals thatrq->clock_peltfeeds are used for - The cgroup v2 CPU Controller — why
cfs_rqneedsh_nr_runningas well asnr_running - Linux Process Scheduling MOC — the parent map