summaryrefslogtreecommitdiff
path: root/kernel/sched/ext/internal.h
AgeCommit message (Collapse)Author
39 hourssched_ext: Keep proxy donors with slice left on the local DSQAndrea Righi
Commit ee172227d0dc ("sched_ext: Delegate proxy donor admission to BPF schedulers") makes put_prev_task_scx() pass a retained proxy donor to ops.enqueue() with SCX_ENQ_BLOCKED. Some of these puts are only proxy bookkeeping; proxy_resched_idle() drops the rq's donor reference before switching to idle when: - find_proxy_task() cannot run the mutex owner yet and retries through idle (e.g., the owner is on a remote CPU), - proxy_migrate_task() detaches the donor before migration, - proxy_deactivate() blocks a task in the chain whose owner cannot run. In the first case, BPF has already selected a donor with slice left. Returning it to ops.enqueue() forces BPF to dispatch it again before proxy resolution can continue. In the other two cases, the donor is typically deactivated right after ops.enqueue(), undoing any placement BPF makes. As also discussed at the sched_ext microconference at Linux Plumbers 2026, these ops.enqueue/dequeue() invocations do not represent any meaningful scheduling events, so we should avoid triggering them. A more reasonable semantic is to keep a donor with slice left at the head of the local DSQ, so the next pick can resolve its owner, or deactivation can remove it without an unnecessary BPF handoff. An IMMED donor can stay there only for a bookkeeping put during proxy resolution. Mark a blocked pick as awaiting resolution with SCX_RQ_PROXY_PENDING to distinguish that put from a real preemption and re-insert the donor with SCX_ENQ_IMMED, so that it only needs SCX_CAP_ENQ_IMMED rather than SCX_CAP_ENQ. A preempted IMMED donor still returns to BPF. If a higher priority class takes the CPU once proxy resolution completes, schedule a local reenqueue, so that a parked IMMED donor returns to BPF instead of lingering on the local DSQ. Update the SCX_ENQ_IMMED and SCX_ENQ_BLOCKED documentation accordingly. Moreover, a donor that has run out of slice now follows the regular put path, so it can receive SCX_ENQ_LAST when the CPU is about to go idle and the scheduler needs to arrange a follow-up scheduling event. This does not apply to bookkeeping puts, where the switch to idle is only temporary. The blocked-donor specific sanity checks go away together with the special case. When a retained donor is removed ahead of a scheduler ownership or policy change, mark the proxy reset with SCX_RQ_PROXY_BLOCKING and skip reenqueueing the active donor: sched_proxy_block_task() will dequeue it next. Report it as not runnable to ops.stopping(), as a regular dequeue of the current donor does. This avoids a transient ops.enqueue/dequeue() pair and a false ENQ_LAST warning. With this, the sched_ext core absorbs the transient proxy-bookkeeping puts and the BPF scheduler only sees a blocked donor when there is an actual placement decision to make. Fixes: ee172227d0dc ("sched_ext: Delegate proxy donor admission to BPF schedulers") Signed-off-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
3 dayssched_ext: Merge branch 'for-7.3-fixes' into for-7.4Tejun Heo
Pull to receive 2935062be771 ("sched_ext: Wait for SCX_OPSS_DISPATCHING before moving tasks off DSQs") and resolve its conflicts with the proxy execution and reject DSQ work here so that for-next merges clean. Conflicts: - unlink_dsq_and_switch_rq_lock(): efa7bc7d7195 ("sched_ext: Avoid relocking DSQ during remote consumption") clears p->scx.dsq in the success branch and 2935062be771 adds the wait there. Kept both, the wait after the clear, so that the proxy reject in consume_remote_task() is covered too. - scx_reenq_reject(): 18f5c699d1d0 ("sched_ext: Rename scx_reenq_wait_dispatching() to scx_wait_dispatching()") renames the call in sub.c while cea8b60e4aee ("sched_ext: Move reject DSQ draining into core") moved the function into ext.c. Dropped the sub.c side and applied the rename at the new home. Signed-off-by: Tejun Heo <tj@kernel.org>
3 dayssched_ext: Rename scx_reenq_wait_dispatching() to scx_wait_dispatching()Kuba Piecuch
Waiting for SCX_OPSS_DISPATCHING to clear isn't specific to the reenqueue paths. Any path which takes a task off a DSQ can find it while the dispatch is still in flight, and the next patch calls the helper from the DSQ consume and move paths. Rename it to scx_wait_dispatching() and update its comment accordingly. No functional change. Reviewed-by: Andrea Righi <arighi@nvidia.com> Assisted-by: Claude:claude-opus-5.5 Signed-off-by: Kuba Piecuch <jpiecuch@google.com> Signed-off-by: Tejun Heo <tj@kernel.org>
6 dayssched_ext: Merge branch 'for-7.3-fixes' into for-7.4Tejun Heo
Pull to receive 069403dff337 ("sched_ext: Fix use-after-free of a destroyed user DSQ by a deferred reenqueue") and point its new cancel sweep in free_dsq_irq_workfn() at dsq->pcpu_user, which 92344fad5b54 ("sched_ext: Skip per-CPU data allocation for built-in DSQs") renamed here, so that for-next builds. Signed-off-by: Tejun Heo <tj@kernel.org>
6 dayssched_ext: Fix stall when a task enqueued with SCX_ENQ_LAST lands back on ↵Tejun Heo
its CPU's local DSQ With SCX_OPS_ENQ_LAST, the last runnable task on a CPU is passed to ops.enqueue() instead of being kept running. Exiting tasks without SCX_OPS_ENQ_EXITING, migration disabled tasks without SCX_OPS_ENQ_MIGRATION_DISABLED and tasks on an offline rq skip ops.enqueue() and go straight to the local DSQ. This happens in put_prev_task_scx() after the pick has settled on idle, so nothing reschedules the CPU, which idles with the task queued until an unrelated wakeup lands on it or the watchdog fires. scx_mitosis, which sets SCX_OPS_ENQ_LAST without SCX_OPS_ENQ_EXITING, hit this during mass cgroup teardown: exiting tasks stuck in exit_mmap() for over 40 seconds. A task put on the local DSQ runs, as anywhere else. If the SCX_ENQ_LAST enqueue left the task on this CPU's local DSQ, set need_resched on the task being switched in, as proxy_resched_idle() does. resched_curr() would flag the task being put instead, and __schedule() clears that right after the pick. Fixes: f0e1a0643a59 ("sched_ext: Implement BPF extensible scheduler class") Cc: stable@vger.kernel.org # v6.12+ Reported-by: Xiangyu Bu <xbu@meta.com> Link: https://github.com/sched-ext/scx/pull/3870 Signed-off-by: Tejun Heo <tj@kernel.org> Cc: Dan Schatzberg <schatzberg.dan@gmail.com>
7 dayssched_ext: Add ops.sub_child_ecaps_updated() to report a child's effective ↵Tejun Heo
cap changes A grant or revoke only records the target caps. They take effect on a cid at its next dispatch, and nothing tells the parent when. Without that the parent cannot act on a revoke: a child that ran a cid at a low cpuperf target leaves the target there when PERF is revoked, as the kernel resets targets only at root enable. Nor can the parent tell when it may schedule on the cid again. Add ops.sub_child_ecaps_updated(), delivered to the direct parent right after the child's own ops.sub_ecaps_updated() with the child's cgroup id and the same before and after caps, in the same dispatch context. Both deliveries are suppressed while the child is bypassing and replayed together afterwards. A disabled child reports nothing: ops.sub_detach() is where the parent restores what it had delegated. A sub is now bypassed before it is linked, so that the grants queued for it from ops.sub_attach() are consumed while it is bypassed and replayed to it and the parent once it is enabled. Otherwise a sync consumed before the sub's ops are registered would reach the parent but never the sub. v2: Drop the disable report with its sleep lock and nested-dispatch gate, the parent restores from ops.sub_detach() instead. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
10 dayssched_ext: Add ops.sub_cid_sched_updated() to report the sched running on a cidTejun Heo
A parent sched that delegates cpus to sub-scheds has no way of telling how much each delegatee is using them. A parent that sizes its delegations by demand cannot measure it, and one that wants to hand a cid from one sub to another cannot see which sub it would take the cid from. Add ops.sub_cid_sched_updated(), invoked on a sched when what it sees running on a cid changes: NONE when no task of its subtree runs there, SELF for its own task, or a direct child's cgroup id. Changes inside a child's subtree are not reported. The op costs one pointer compare per context switch when the sched does not change, a notification reaches only the scheds that see a change, and the whole mechanism sits behind a static key that is on only while a loaded sched implements the op. After a sched's tasks are gone, its disable closes every rq session still pointing at it, so nothing refers to it once it is freed. The op is cid-form only like the other sub ops and runs with the rq lock held on the context switch path. Signed-off-by: Tejun Heo <tj@kernel.org>
10 dayssched_ext: Clear a sub-scheduler's caps before ops.sub_detach()Tejun Heo
A sub-scheduler can leave standing state behind on the cids delegated to it, cpuperf targets being the obvious case, and the kernel does not track who wrote it. Restoring it is the parent's job and ops.sub_detach() is the place for it. Today the parent can't rely on that: the dying sub-scheduler keeps every cap until it is marked dead after ops.exit(), so its ops.exit() or a late timer can write after the parent has cleaned up. Clear every cap of the sub-scheduler before ops.sub_detach(). The pshard caps go first as a pending sync recomputes ecaps from them, and ecaps are then zeroed directly. A grant racing the clear is refused under the child's pshard lock, so nothing hands the caps back. Fixes: 86094b95efcf ("sched_ext: Add per-shard cap delegation for sub-schedulers") Signed-off-by: Tejun Heo <tj@kernel.org>
13 dayssched_ext: Generate qseq from a per-task counterKuba Piecuch
finish_dispatch() uses the qseq embedded in p->scx.ops_state to tell whether the QUEUED instance of a task it's about to claim is the one scx_bpf_dsq_insert() saw. qseq is generated from rq->scx.ops_qseq, but the counters of different rqs are independent, so if a task is dequeued and re-enqueued on a different rq between scx_bpf_dsq_insert() and finish_dispatch(), the new QUEUED instance can end up with the same qseq as the old one: CPU X CPU Z ----- ----- enqueue p on rq A, qseq = N ops.dispatch() scx_bpf_dsq_insert(p) records qseq N sched_setaffinity(p) dequeue p from rq A enqueue p on rq B, qseq = N finish_dispatch(p, N) qseq matches, p is claimed The claim itself is still atomic so the core stays consistent, but an insert issued for a previous QUEUED instance gets applied to a new one which the BPF scheduler has just received through ops.enqueue(). This breaks the guarantee that dispatches targeting a stale instance are ignored. Generate qseq from a per-task counter, p->scx.ops_qseq, instead so that consecutive QUEUED instances of a task never share a qseq regardless of which rq they're on. The counter is only updated in scx_do_enqueue_task() with the task's rq locked, so no additional synchronization is needed, and it fits in an existing hole in struct sched_ext_entity on 64bit. Remove the now unused rq->scx.ops_qseq. Never generate qseq 0. NONE and DISPATCHING don't carry a qseq, so scx_bpf_dsq_insert() on a task in either state records 0. With a per-task counter, every task's first QUEUED instance would otherwise get qseq 0 and could be claimed by such an insert. Wrap the counter where the QSEQ field wraps so that it can't reach a value that shifts to 0 on 32bit either. Fixes: f0e1a0643a59 ("sched_ext: Implement BPF extensible scheduler class") Cc: stable@vger.kernel.org # v6.12+ Assisted-by: Claude:claude-opus-5.5 Signed-off-by: Kuba Piecuch <jpiecuch@google.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-28sched_ext: Delegate proxy donor admission to BPF schedulersAndrea Righi
Proxy execution keeps a mutex-blocked donor runnable so that its scheduling context can execute the mutex owner. Define SCX_OPS_ENQ_BLOCKED as the admission contract for retained donors. Schedulers without the flag block EXT donors normally. Schedulers with the flag continue to own blocked donors and receive them through ops.enqueue() with SCX_ENQ_BLOCKED, allowing BPF to choose their DSQ, CPU and ordering. After BPF dispatches a donor, proxy execution may move its context to the rq of the mutex owner. This move is distinct from BPF placement: proxy_set_task_cpu() changes task_cpu() while preserving the donor's pre-proxy CPU in wake_cpu as the starting point for normal wakeup placement. A proxy-migrated donor returns through the full wakeup activation path when the mutex is released. Do not carry a retained proxy session across BPF scheduler ownership changes. Before root activation, parent-to-child takeover, rehome or punt, fully deactivate a retained donor unconditionally. The incoming scheduler then starts with clean task state and applies its admission policy the next time the task blocks. A retained donor can wake through ttwu_runnable() without another enqueue_task_scx() call. Use WF_TTWU_RQ to identify this path, where the wakeup skipped activation and enqueueing. wakeup_preempt_scx() requests dispatch reconsideration after is_blocked is cleared. A full wakeup activation has already enqueued the task and does not need another reschedule. Blocked-donor enqueueing takes precedence over the exiting and migration-disabled local-DSQ fallbacks, so that an opted-in scheduler sees every eligible donor request. Co-developed-by: John Stultz <jstultz@google.com> Signed-off-by: John Stultz <jstultz@google.com> Signed-off-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-28sched_ext: Handle proxy-exec races in remote DSQ transfersAndrea Righi
Without proxy execution, the DSQ lock and holding_cpu handshake ensure that a task cannot be dequeued or start running during an rq-lock handoff without clearing holding_cpu. Proxy execution is an exception: a task can start physically executing as a lock owner while its scheduling context remains on a DSQ; its on-CPU or migration-disabled state can therefore change without clearing holding_cpu. Recheck these states after acquiring the source rq lock. If the transfer can no longer proceed, park the task on the source rq's reject DSQ and reenqueue it through its owning scheduler. This preserves the BPF scheduler's placement policy and keeps descendant tasks within their sub-scheduler's cap grants. Queue a deferred drain for every rejection. If a drain finds a proxy-rejected task still running or donating, leave it parked and record a retry request on the rq. scx_proxy_reenqueue_retry() consumes the request and schedules another drain when proxy state changes. Without this change and proxy execution enabled, stress-ng --pipeherd can trigger this race and migrate an active execution context, leading to sleeping-while-atomic warnings and subsequent lockdep corruption. This is a preparatory change to support proxy execution with sched_ext. Suggested-by: Tejun Heo <tj@kernel.org> Signed-off-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-28sched_ext: Fix ops.running/stopping() pairing for proxy-exec donorsAndrea Righi
With proxy execution, pick_next_task() can select a blocked task as the scheduling context before find_proxy_task() resolves the execution context. From the BPF scheduler perspective, that donor is running while its scheduling context drives the lock owner; ops.tick() and other accounting must therefore remain enclosed by a matching ops.running()/ops.stopping() session. In this scenario, the session boundaries do not always match physical task switches. Keep the "running" session open when the same donor continues on the same CPU. When proxy execution migrates a donor's scheduling context to another CPU, end its running session on the source CPU and start a new session on the destination CPU only after proxy resolution succeeds. This prevents a failed resolution from exposing a provisional ops.running() event. Track these sessions with a new SCX_TASK_RUN_TRACKED flag. The explicit running-state tracking is also required by later donor-based accounting: it prevents an EXT owner executing for a non-EXT donor from being treated as the active EXT scheduling context when it is dequeued. This is a preparatory change for enabling proxy execution together with sched_ext. Acked-by: John Stultz <jstultz@google.com> Signed-off-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-26sched_ext: Merge branch 'for-7.3-fixes' into for-7.4Tejun Heo
Pull to receive 94480606a677 ("sched_ext: Add a size argument to scx_bpf_cid_topo() so struct scx_cid_topo can grow") for the upcoming cluster level. Conflicts: kept both declarations where 5cadf7545cc6 ("sched_ext: Add a CID NUMA node lookup kfunc") added one next to the changed scx_bpf_cid_topo() line, and pointed 29871903f3a3 ("sched_ext: Don't deliver duplicate ops.cgroup_set_idle() for same value") at the renamed tg->scx.sched_idle. Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-19sched_ext: Pass the initial cmask to cid-form ops.enable()Tejun Heo
The cid-form API has an obvious hole. A task's cid mask is only visible through ops.set_cmask(), which fires on affinity changes and class switches but not when a task enters a scheduler through fork, sub-sched enable or re-home, and there is no p->cpus_ptr equivalent to fall back on. Schedulers work around it by seeding the mask in ops.init_task() from p->cpus_ptr cid by cid, which is subtly wrong: on sub-sched enable and re-home, an affinity change between init_task() and enable() is delivered to the sched the task is still on, and nothing corrects the new sched's copy afterwards. Fix it by adding struct scx_enable_args to cid-form ops.enable() carrying the task's cmask, built in the per-cpu scratch under the rq lock as the task enters the scheduler, and calling set_cmask() with the same mask right after enable(), ahead of set_weight(). A scheduler can then track affinity in set_cmask() alone, and scx_qmap drops its init_task() seed. set_cmask() no longer fires for a cid-form task before it is enabled, and the class-switch republish in switching_to_scx() is limited to the cpu form. This changes the cid-form ops.enable() signature, which is fine as the cid-form API is still considered unreleased. An args struct rather than a bare cmask argument leaves room for more initial state without another signature change, and the cmask travels as a plain arena address because BTF can't type arena struct members yet. v2: The cmask travels as a u64 arena address, cmask_arena_addr, instead of a kernel-typed pointer, with the typing limitation and the planned typed alias documented (Sashiko review). v3: The initial set_cmask() is delivered before set_weight() so that the mask is in place when weight-dependent state is derived (Andrea Righi). Selftest added. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-09-18sched_ext: Add lazy preemption supportAndrea Righi
The fair scheduling class can request lazy rescheduling, deferring an in-kernel scheduling boundary until returning to user space or until the next scheduler tick. sched_ext only exposes immediate preemption, preventing BPF schedulers from making the same trade-off. Add SCX_ENQ_PREEMPT_LAZY and SCX_KICK_PREEMPT_LAZY. Both expire the current sched_ext task slice but request lazy rescheduling. Immediate preemption, WAIT and plain kicks take precedence when requests are combined, while a lazy enqueue to a non-local DSQ retains the head-insertion semantics of SCX_ENQ_PREEMPT. Add SCX_OPS_LAZY_RESCHED as the default slice-expiry policy for newly enabled tasks and initialize it before ops.enable(). Add scx_bpf_task_set_lazy_resched() so the owning scheduler can override the persistent policy per task from any callback while preserving sub-scheduler task ownership boundaries. Bypass continues to force immediate rescheduling. For an infinite-slice task whose tick is stopped, set the lazy reschedule request before restoring the scheduler tick dependency. This lets the dependency update's IPI serve the request on return to user space and guarantees that the next tick promotes it otherwise. Use pinned rq locking for remote kick delivery so RQCF_UPDATED reflects clock updates since lock acquisition. Accumulate kick requests independently and resolve precedence while holding the target rq lock. Reject invalid SCX_KICK_IDLE combinations and unknown kick flags, which were previously silently ignored. Tested-by: Tao Cui <cuitao@kylinos.cn> Signed-off-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-17sched_ext: Don't run ops.dequeue() with a DSQ lock heldfangqiurong
ops.dequeue() is invoked with the source user DSQ's lock still held on the consume and move paths (scx_consume_dispatch_q(), move_task_between_dsqs()). A BPF scheduler which locks the source user DSQ from ops.dequeue() - e.g. by iterating it with bpf_iter_scx_dsq - self-deadlocks. ops.dequeue() can only call the "any" kfuncs and none of them can lock a builtin DSQ, so the global and bypass paths can't deadlock; however, all DSQ locks share one lockdep class, so iterating any user DSQ from ops.dequeue() on those paths trips the recursion check. Move the invocation after the DSQ unlock on all three paths. SCX_TASK_IN_CUSTODY is cleared under the lock serializing the transfer so that the callback is invoked exactly once. Fixes: ebf1ccff79c4 ("sched_ext: Fix ops.dequeue() semantics") Cc: stable@vger.kernel.org # v7.1+ Acked-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: fangqiurong <fangqiurong@kylinos.cn> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-15sched_ext: Wait for SCX_OPSS_DISPATCHING before reenqueueing a taskTejun Heo
ebf1ccff79c4 ("sched_ext: Fix ops.dequeue() semantics") moved the final ops_state store in scx_dispatch_enqueue() after the DSQ unlock so that the custody update and ops.dequeue() precede it. A task can thus be found on a DSQ while still SCX_OPSS_DISPATCHING. The dequeue and core-sched pick paths wait for the state to clear in ops_dequeue() but the reenqueue paths don't. A reenqueue in that window runs ops.enqueue() and sets SCX_OPSS_QUEUED before the dispatch has completed. The dispatcher's final store then overwrites it with SCX_OPSS_NONE and finish_dispatch() drops every later dispatch of the task. Wait for SCX_OPSS_DISPATCHING to clear before dequeueing a task for reenqueue, the same way ops_dequeue() does. Fixes: ebf1ccff79c4 ("sched_ext: Fix ops.dequeue() semantics") Cc: stable@vger.kernel.org # v7.1+ Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-09-15sched_ext: Maintain an online cid mask in the scheduler arenaTejun Heo
Schedulers on the default cid mapping treat [0, nr_online_cids) as the online set and restart on hotplug. Schedulers that install their own mapping with scx_bpf_cid_override() have no way to learn which cids are online: the count no longer identifies members and the CPU-form cpumask is unusable from cid programs. This is an obvious hole in the cid API. Add scx_bpf_online_cmask(), a kernel-maintained cmask in the scheduler's arena, allocated alongside the per-CPU scratch masks and populated after the cid mapping is finalized and before ops.init(), for child schedulers too. The pointer stays valid through ops.exit() with no reference to take. It is the arena offset as a void pointer, the same form struct_ops arena arguments arrive in. The verifier types the void return as a scalar for the program's arena cast. The mask follows the SCX hotplug notifications: seeded from cpu_active_mask and updated before ops.cid_online/offline() runs, so it lags cpu_online_mask only inside a hotplug transition. Updates walk the scheduler list under the lock that also serializes unlinking. Reads are live, not atomic snapshots. Root initialization excludes hotplug. v2: Reworded the getter kerneldoc (Andrea Righi). Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-09-15sched_ext: scx_qmap: Restore unused idle claims from ops.dispatch()Tejun Heo
scx_qmap tracks idle cids itself. pick_direct_dispatch_cid() claims a cid by clearing its bit and the task is inserted into that cid's local DSQ, which kicks the CPU. When the task does not arrive, for example because the insert fell back to the global DSQ after an affinity change, the CPU wakes, finds nothing and picks idle again. That is not an idle transition, so ops.update_idle() is not called and the cid stays marked busy until an unrelated task runs on it. Restore the claim from ops.dispatch(). The kick guarantees a dispatch on the kicked CPU, and when it finds nothing to run with a NULL @prev, the CPU is going back to idle. Document the pattern in ops.update_idle(), which reports only actual transitions. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com> Cc: Andrea Righi <arighi@nvidia.com>
2026-09-03sched_ext: Make scx_locked_rq() return NULL from NMIWanwu Li
scx_locked_rq() reads the per-CPU scx_locked_rq_state, which tracks the rq locked by the context running on the CPU. Tracing progs can call kfuncs from NMI, and an NMI interrupts - rather than replaces - the context that set scx_locked_rq_state, so a non-NULL read from NMI falsely tells the caller that it holds the interrupted context's rq lock. Three "any"-category kfuncs read scx_locked_rq() on their success path and take an unsafe fast path on a non-NULL return: - scx_bpf_task_set_slice() writes p->scx.slice directly, racing update_curr_scx()'s non-atomic read-modify-write of the same field. - scx_bpf_dsq_nr_queued() resolves %SCX_DSQ_LOCAL to (scx_locked_rq() ?: this_rq()) and can report the interrupted context's local DSQ length instead of the caller's. - scx_bpf_locked_rq() hands the interrupted context's rq to the BPF program, which may then operate on it as if it owned the rq lock. Make scx_locked_rq() return NULL from NMI so that all three take their unlocked paths: scx_bpf_task_set_slice() stashes the request into the atomic p->scx.slice_oob for application under the rq lock, scx_bpf_dsq_nr_queued() falls back to this_rq(), and scx_bpf_locked_rq() reports an error and aborts the scheduler through the NMI-safe exit path. The kfuncs that take scheduler locks reject NMI calls through scx_kf_allowed_ctx() before reaching scx_locked_rq(), and the internal callers only run from struct_ops callbacks, which never run in NMI, so no other caller is affected. Suggested-by: Tejun Heo <tj@kernel.org> Link: https://lore.kernel.org/r/d84b31727f04e1ed0d40042ba1c09e61@kernel.org Signed-off-by: Wanwu Li <liwanwu@kylinos.cn> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-09-02sched_ext: Reject NMI calls to lock-taking kfuncsWanwu Li
commit e06ece82d7b0 ("sched_ext: Report NMI kicks with scx_error()") made scx_bpf_kick_cpu() reject NMI calls, and its cover letter describes the reachability: sched_ext kfuncs in the "any" category "are callable from tracing progs that can attach to functions running in NMI", and an unlucky call from there "could deadlock the machine". The fix in that series made the error/exit path lock-free so scx_error() is safe to call from NMI. That closes the *error* path of every kfunc, but not a kfunc's own business-logic lock acquisition on its success path. The remaining lock-taking kfuncs that scx_kfunc_context_filter() exposes to BPF_PROG_TYPE_TRACING have the same hazard: if an NMI lands on a CPU whose interrupted context already holds the lock, the kfunc's raw spinlock acquisition spins forever and hard-locks the CPU: - scx_bpf_destroy_dsq() -> dsq->lock - scx_bpf_dsq_reenq() -> rq's deferred_reenq_lock - scx_bpf_cpuperf_set() / scx_bpf_cidperf_set() -> rq->lock - scx_bpf_sub_grant() / scx_bpf_sub_revoke() -> pshard lock (via the shared sub_cap_preamble()) - bpf_iter_scx_dsq_next() / bpf_iter_scx_dsq_destroy() -> dsq->lock (the reject is placed in bpf_iter_scx_dsq_new(), which is otherwise lockless, so next() and destroy() are no-ops while it leaves kit->dsq NULL) As things stand, there is no scenario for reenqueueing, iterating a DSQ, setting a performance target or granting sub-caps from NMI. The guards defend against a buggy or malicious BPF program turning an "any"-category kfunc into a machine-wide hard-lockup through the door that scx_kfunc_context_filter() already opens. This matches the intent of scx_bpf_kick_cpu()'s NMI check, which the commit cited above added not to enable an NMI use case but to surface such a bug as a clean abort. Route all of them through a new scx_kf_allowed_ctx() helper and reuse scx_bpf_kick_cpu()'s existing in_nmi() check - now shared with its cid equivalent scx_bpf_kick_cid() through scx_kick_cpu() - so the rule is stated once and the coverage is auditable from one place. scx_error() is already NMI-safe (commit f883dbb64ca5 ("sched_ext: Make exit claiming lock-free")), so the reject-abort cannot deadlock the lock acquisition. Kfuncs with an error return report -EDEADLK, the situation being avoided. Read-only members of the reachable sets (dsq_peek, dsq_nr_queued, cpuperf_cur/cap, sub_caps, the idle cpumask helpers and the cid lookups) take no scheduler lock on the path a tracing program reaches them, and were audited to that effect; they are correctly left unguarded. The select_cpu kfuncs do take pi_lock, but scx_kfunc_context_filter() only exposes the any/idle/cid sets to BPF_PROG_TYPE_TRACING, and struct_ops programs never run in NMI, so no lock-taking path here is reachable from NMI. Link: https://lore.kernel.org/r/20260901095652.1009104-1-liwanwu@kylinos.cn Signed-off-by: Wanwu Li <liwanwu@kylinos.cn> Acked-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-31sched_ext: Pass the initial cpu.idle state in scx_cgroup_init_argsTao Cui
scx_cgroup_init_args carries the initial weight and bandwidth control parameters of a cgroup to ops.cgroup_init(), but not its cpu.idle state. A cgroup that was already configured idle before the scheduler was loaded (or before it was onlined under it) is presented as non-idle, and the BPF scheduler only learns about it if cpu.idle is written again later. Add the sched_idle state to scx_cgroup_init_args and fill it in all four places that build the args: scx_tg_online() for cgroups onlined under the scheduler, scx_cgroup_init() for cgroups that already exist when the scheduler is loaded, and the sub-scheduler handover paths scx_cgroup_claim_subtree() and scx_cgroup_return_subtree(). Verified in a VM with a probe scheduler printing the init args: a cgroup configured cpu.idle=1 before loading shows sched_idle=1 in ops.cgroup_init(), the default shows 0, and later cpu.idle writes still come through ops.cgroup_set_idle(). The sub-scheduler paths are compile-tested only. Fixes: 347ed2d566da ("sched/ext: Implement cgroup_set_idle() callback") Signed-off-by: Tao Cui <cuitao@kylinos.cn> Reviewed-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-31sched_ext: Fix several comment issuesWanwu Li
Fix several comment issues found during review: __setschduler_prio() -> __setscheduler_class() scx_iter_scx_dsq_new() -> bpf_iter_scx_dsq_new() scx_next_task_scx() -> set_next_task_scx() Signed-off-by: Wanwu Li <liwanwu@kylinos.cn> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-18sched_ext: Allow ops.cgroup_set_bandwidth() to be sleepableChangwoo Min
ops.cgroup_set_bandwidth() is delivered from scx_group_set_bandwidth(), which runs from the cpu.max cgroup interface write path (tg_set_bandwidth()) in process context. scx_group_set_bandwidth() holds percpu_down_read(&scx_cgroup_ops_rwsem), whose read side may sleep. The call site is therefore sleepable, like ops.cgroup_init(). bpf_scx_check_member() rejects a sleepable program on any member not on its allow-list, so a BPF scheduler cannot allocate -- which is sleepable -- when a cgroup gains a cpu.max limit at runtime; it must instead pre-reserve memory for a callback that cannot allocate. Add cgroup_set_bandwidth() to the allow-list so the callback can allocate on demand, and document that it may block. A scheduler must decide at load time whether to mark the callback sleepable, but the allow-list entry is a verifier property with no symbol to probe. Add a compatibility marker whose presence in the kernel's BTF lets userspace detect this support: DEFINE_SCX_COMPAT_MARKER() emits an empty, callerless function, here scx_compat_marker_cgroup_set_bandwidth_may_sleep(). It is __used __retain so neither the compiler nor the linker (under CONFIG_LD_DEAD_CODE_DATA_ELIMINATION) drops it. The markers share the scx_compat_marker_ prefix and are collected near the end of ext.c so more can be added as further capabilities appear. Signed-off-by: Changwoo Min <changwoo@igalia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-17sched_ext: Merge branch 'for-7.3-arena-args' into for-7.3Tejun Heo
Pull to receive the __arena argument conversion: 67f1f4a48c24 ("sched_ext: Pass kernel arena pointers to ops_cid callbacks") a8dc810968af ("sched_ext: Convert sub-cap kfuncs to __arena cmask arguments") a05c5b5cb5cf ("sched_ext: Convert scx_bpf_cid_override() to __arena array arguments") along with the bpf-next branch carrying the __arena argument support they depend on. Conflict in kernel/sched/ext/ext.c between: c384ab8a0b13 ("sched_ext: Move the config-off sub-cap kfunc stubs into sub.c") and: a8dc810968af ("sched_ext: Convert sub-cap kfuncs to __arena cmask arguments") which updated the stubs in their old ext.c location. Resolved by keeping ext.c without the stubs and applying the prototype conversion to the relocated stubs in sub.c. Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-15sched_ext: Make core-sched task ordering hierarchy-awareTejun Heo
With sub-schedulers, tasks of different schedulers routinely share rqs and SMT siblings, but scx_prio_less() consults ops.core_sched_before() only when both tasks belong to the same scheduler. Every pair spanning two schedulers falls back to the default ordering, so no scheduler can express ordering across a scheduler boundary, including a root over its sub-schedulers' tasks. Order a pair spanning schedulers by the nearest common ancestor that implements ops.core_sched_before(): both tasks are in its subtree, making this the one op where a scheduler is called on tasks it delegated to its sub-schedulers and may not be scheduling anymore. Same-scheduler pairs keep using the owning scheduler's op so a parent never orders inside a subtree it delegated. The op is skipped when the deciding scheduler is bypassing on either task's CPU. Update scx_qmap to fall back to the kernel's default ordering when handed a delegated task it has no task_ctx for. Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-14sched_ext: fix stale references in doc commentsTao Cui
- inlines.h: scx_bpf_dispatch() doesn't exist; the comment means scx_bpf_sub_dispatch() - internal.h: name %SCX_DEQ_SCHED_CHANGE instead of the never-defined %SCX_DEQ_SAVE - internal.h: @name shows up in the ops file in the scheduler's sysfs directory, not a "kernel.sched_ext_ops" sysctl Signed-off-by: Tao Cui <cuitao@kylinos.cn> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-12sched_ext: Pass kernel arena pointers to ops_cid callbacksTejun Heo
The cid-form set_cmask() and sub_caps_updated() callbacks receive cmasks that the kernel builds in the arena, and the kernel converts the kernel addresses to the BPF arena pointer form by hand before each call. BPF now translates between BPF and kernel arena addresses for __arena arguments. Tag the arguments __arena in the cfi stubs and the ops_cid member declarations and pass the kernel arena addresses directly, dropping the manual scx_kaddr_to_arena() conversions and the now-unused helper. The delivered value is unchanged and existing BPF-side code works as before. The arena argument address translation is currently implemented only on x86-64. cid-form schedulers implementing these callbacks load only there for now. Signed-off-by: Tejun Heo <tj@kernel.org>
2026-08-03sched_ext: Eject the top rescue consumer on overloadTejun Heo
When rescue demand on a cpu persistently exceeds the configured bandwidth, tasks age on that cpu's rescue DSQ until the stall watchdog fires. The watchdog blames the waiting task's owner, but the misbehaving party is whoever floods the queue, not whoever happens to time out. Track each sched's recent rescue consumption per cpu as a decaying average. Once the oldest waiter on a cpu's rescue DSQ has been queued past a threshold derived from the rescue knobs (4s at the defaults), the rescue timer ejects the sub with the highest recent consumption on that cpu with SCX_EXIT_ERROR_RESCUE. With no recent consumer there is no victim and nothing is ejected - the generic stall watchdog eventually blames the waiter's owner instead. Ejections on a cpu are spaced one threshold apart so the freed bandwidth can drain the backlog before another sub is judged. The overload check only wins the race against the stall watchdog when the watchdog timeout clears the threshold, and a single in-budget wait must not cross the trigger on its own. Warn on a scheduler whose timeout doesn't fit and on knobs whose funding period exceeds half the threshold. v2: - Track kill_at in jiffies_64 - on 32-bit, the time_before() grace check wraps 2^31 ticks after the last ejection and suppresses ejections. (sashiko AI) - Track rescue_avg_at in jiffies_64 likewise - the unsigned long decay delta truncates mod 2^32 on 32-bit and can revive a weeks-old usage average in the victim pick. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-08-03sched_ext: Add bandwidth-limited rescue execution for stranded tasksTejun Heo
A local DSQ insert lacking the needed caps is diverted to the reject DSQ and bounced back through ops.enqueue() so the scheduler can re-decide. That recovery assumes the scheduler has somewhere legal to send the task. When it doesn't, e.g. when the task's affinity is restricted to cids delegated away, the task starves until the stall watchdog ejects the scheduler. An exiting task is worse - it skips ops.enqueue() and the rejection becomes a self-requeuing cycle that burns the CPU until the watchdog fires. Add SCX_ENQ_RESCUE, a fallback modifier on local DSQ inserts. When the insert would be rejected for missing caps, the kernel takes over and runs the task on the target CPU without consulting the owning scheduler. The kernel sets the flag itself when enqueueing an exiting task. Rescue is a last-resort forward-progress backstop with a persistent disadvantage, not a way around cap enforcement. A per-CPU token bucket accrues rescue_bandwidth_ppt (default 2%) of CPU time and rescues run one at a time in arrival order. Each is granted a slice of the rescue_quantum_us (default 5ms) quantum divided across the waiters, waits at the tail of the local DSQ claiming no priority, and rejoins its scheduler as a fresh arrival once the slice is served. The schedulers keep their normal control over an admitted rescuee and may preempt or reslice it. Service is measured on CPU time actually received, so neither shortens the rescue. Prolonged denial escalates - the remaining slice turns into protected execution (SCX_TASK_PROTECTED) and the rescuee preempts the current task. Escalation is paced by the same bucket, and delivered service converges on the configured bandwidth no matter how aggressively the schedulers dispatch. Both knobs are root-only and SCX_RESCUE_DISABLE turns rescue off, making SCX_ENQ_RESCUE inserts reject as usual. v2: - Add SCX_OPS_OPEN() fix-ups for the new ops fields so cpu-form schedulers setting them still load on older kernels. (Andrea) Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-08-03sched_ext: Add SCX_TASK_PROTECTEDTejun Heo
A BPF scheduler can displace any of its tasks at will - cut a running one's slice with an SCX_ENQ_PREEMPT dispatch, an SCX_KICK_PREEMPT kick or a direct shortening, and jump a queued one with HEAD insertions. Sometimes the kernel needs a slice and a DSQ position to stick regardless. Add SCX_TASK_PROTECTED, guarding both: - The slice becomes immutable. Every scheduler-reachable write is refused and counted as SCX_EV_SLICE_DENIED. Higher scheduling classes are unaffected. PREEMPT|IMMED can't preempt a running protected task and gets reenqueued. - A protected task that reached the head of its DSQ keeps it - HEAD insertions land behind the leading run of protected tasks and reenqueue sweeps skip them. Only rq-owned DSQs can hold protected tasks, so the walk runs only for them. The bit lives in p->scx.flags so that both the refusal and the head walk read it under the rq lock that protects it. Protection ends when the slice is consumed, when the task leaves the rq except for a save/restore on the running task, on a yield, when the scheduler enters bypass, and when the task leaves scx. The flag is kernel-internal and not used yet. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-08-03sched_ext: Synchronize slice and dsq_vtime writesTejun Heo
p->scx.slice and p->scx.dsq_vtime writes have no synchronization rules. The dsq insert kfuncs write both fields synchronously from whatever context they're called in - a direct dispatch from ops.select_cpu() writes with only pi_lock held - and, as the kfuncs are safe to call spuriously with the invalid dispatch discarded later, a scheduler can modify any task's slice by spuriously calling them. The latter stands in the way of an upcoming patch which adds kernel-granted slices that the schedulers must not be able to modify. Give both fields explicit rules. While the task is running, sleeping or queued on an rq-owned DSQ, the rq lock protects them - these are the states where the kernel consumes the slice. While queued on a user DSQ or on the BPF side, the kernel neither consumes nor decides on the fields and every writer acts for the BPF scheduler - synchronizing the writers is the scheduler's responsibility and whichever write lands last wins. To conform, an insert kfunc no longer writes the fields when called. The values travel with the dispatch and take effect when the task is inserted. A discarded dispatch has no side effects. The rq lock rule is asserted at the slice store. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-08-03sched_ext: Factor out __scx_bpf_now()Tejun Heo
scx_bpf_now() couples the valid-or-fresh rq clock read to the current rq. The read is useful for kernel-internal timing against a specific rq, including a remotely locked one. Factor it out into __scx_bpf_now(). Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-08-03sched_ext: Make several ext.c helpers available outside ext.cTejun Heo
set_task_slice(), task_unlink_from_dsq(), move_local_task_to_local_dsq(), init_dsq() and dump_line() will be used outside ext.c. Add the scx_ prefix and declare them in internal.h. The scx_sched_all list will also be used outside ext.c, drop its static. No functional changes. v2: Declare scx_sched_all outside the CONFIG_EXT_SUB_SCHED block - the definition is unconditional. (sashiko AI) Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-08-02sched_ext: Fix stale @cgroup_id in sched_ext_ops kernel-docLiang Luo
The kernel-doc comment for sched_ext_ops::sub_cgroup_id uses the old @cgroup_id name, which no longer matches the struct member. This produces two kernel-doc warnings: Warning: struct member sub_cgroup_id not described in sched_ext_ops Warning: Excess struct member cgroup_id description in sched_ext_ops Update the @param name to match the actual member. Signed-off-by: Liang Luo <luoliang@kylinos.cn> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-07-27sched_ext: Report NMI kicks with scx_error()Tejun Heo
The per-cpu kick lists are protected by IRQ masking which doesn't stop NMIs, so scx_bpf_kick_cpu() from NMI silently drops the kick after a one-time warning. A dropped kick can leave a CPU idle when the scheduler believes it was woken, which is a correctness problem for the scheduler even if the kernel is fine. Now that scx_error() works from NMI, abort the scheduler instead so that the bug is surfaced deterministically. The warned_nmi_kick tracking is no longer needed. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-27sched_ext: Format bstr exit messages after claiming the exitTejun Heo
The bstr exit kfuncs format the message into a shared static buffer under a raw spinlock before initiating the exit. The lock can't be taken from NMI and needlessly serializes all bstr exits system-wide. Now that exit claiming is lock-free, reverse the order: claim the exit first and format directly into the exit_info message buffer which the claim winner owns exclusively. The new scx_exit_bstr() implements the sequence, replacing scx_bstr_format(), and the shared buffer and lock are deleted; the formatter itself is what bpf_trace_printk() already runs from NMI. scx_prog_sched() callers were relying on the lock for RCU protection, which is now provided explicitly. A malformed format no longer changes or fails the requested operation: scx_bpf_exit_bstr() keeps its graceful exit kind and scx_bpf_sub_kill_bstr() still kills the child, with a fallback message carrying the formatting errno, while the sched that supplied the bad format is aborted for its bug. Before this and the previous patch, an "any" category kfunc called from NMI context could trigger scx_error() and deadlock - e.g. a tracing prog attached to a function running in NMI calling scx_bpf_dsq_peek() on a non-existent DSQ would try to grab scx_sched_lock, which may be held by the interrupted CPU. This and the previous patch fix the deadlock: scx_error() and the bstr exit kfuncs, and thus scx_bpf_error() and scx_bpf_exit(), are now safe to call from any context including NMI. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-27sched_ext: Make exit claiming lock-freeTejun Heo
scx_claim_exit() claims descendants' exits by walking the subtree under scx_sched_lock, making exit claiming, and thus scx_error(), unusable from NMI and from under scx_sched_lock. However, kfuncs raising errors can run from NMI-attached BPF progs, the hardlockup handler runs in NMI, and scx_link_sched() wants to report failures under the lock. The walk does two things with different urgencies: ->aborting must be asserted synchronously to break IRQs-off dispatch-path live-locks, while the descendants' exit_kind claims can happen later. Split them: sweep ->aborting locklessly under RCU to unwedge the system and defer the locked SCX_EXIT_PARENT walk to a new irq_work, both of which are NMI-safe. The sweep stores each node's ->aborting and then reads its children list while scx_link_sched() inserts and then checks the parent's ->aborting, the two sides paired by full barriers - one side always sees the other. A link that sees ->aborting undoes its insert and fails. As the undo's list_del_rcu() leaves ->sibling non-empty, list_empty() can no longer identify a never-linked sched during teardown - add sch->linked instead. trace_sched_ext_exit can now fire from NMI and is called after the ->aborting stores so that its callbacks don't hold up live-lock recovery. The exit backtrace is skipped for NMI exits as stack_trace_save()'s NMI-safety is arch-dependent and undocumented. v2: Move trace_sched_ext_exit() after the ->aborting stores (Andrea). Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-26sched_ext: Bound per-task reenqueues and eject the owning schedulerTejun Heo
Unlike local reenqueues, cap rejections have no repeat limit. A malfunctioning scheduler can keep re-inserting a task to a cid it lacks caps on, cycling the task through reject and reenqueue. This was assumed safe because a task that never runs trips the stall watchdog. However, the reenqueue irq_work re-arms itself and outranks the timer vector, blocking everything else on the CPU including stall detection and recovery, until the NMI hardlockup detector fires. Local reenqueues already have a repeat cap, SCX_REENQ_LOCAL_MAX_REPEAT, which needs generalizing to cover all reenqueues. It also has an attribution problem. Counted per-cpu on root, it tears down the whole hierarchy even when a sub-scheduler caused the repeated reenqueues. Generalize by bounding every reenqueue with one per-task counter. reenq_cnt is bumped in scx_do_enqueue_task() on each SCX_ENQ_REENQ, the single path every reenqueue producer passes through, and cleared in clr_task_runnable() when the task is picked to run and in scx_disable_task() when it leaves the scheduler's control. Past SCX_REENQ_MAX_REPEAT the task's owning scheduler is ejected with a new SCX_EXIT_ERROR_REENQ and the task is left stranded to be picked up during sched exit. The SCX_EV_REENQ_LOCAL_REPEAT event becomes SCX_EV_REENQ_REPEAT, counting repeat reenqueues from all sources. v2: Count SCX_EV_REENQ_REPEAT only when a reenqueue leads to another reenqueue, not on every reenqueue. v3: - Also clear reenq_cnt in scx_disable_task() so that the count doesn't carry over to the next owner across sched class switches, scheduler replacement or sub-scheduler rehoming (Andrea Righi). - Update the stale SCX_EV_REENQ_LOCAL_REPEAT references in sched-ext.rst (Andrea Righi). Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-25sched_ext: Resolve most remaining scx_root accessesTejun Heo
scx_root is __rcu and naked accesses were left as transitional markers for the multi-scheduler transition, to be converted to accesses through the associated scheduler instances. Most accesses have since been converted to resolve the sched from the program or task at hand. The remaining naked sites divide into ones that semantically always want the root sched, which this patch resolves, and one that is left to a later patch. The resolved sites: - The SCX_OPS_TID_TO_TASK validation and the ecaps sync kick already hold a sched whose ancestors[] pins the root as entry 0 with plain pointers stable for the sched's lifetime. Reach the root through the sched at hand. - The dispatch entry, class switch, idle notification and fork init paths only execute while the scheduler is live and scx_root never changes inside the live window, so no update can race them. Add scx_root_protected_live() which documents that invariant and resolves with a plain load. - The hotplug path, including the ecaps reseeds, runs with the hotplug lock held, which excludes the scx_root writers. Add scx_root_protected(), which accepts either the hotplug lock or scx_enable_mutex. - Is-root tests use a zero level instead of comparing against the global. touch_core_sched_dispatch() stays naked, to be resolved by a later patch. Signed-off-by: Tejun Heo <tj@kernel.org>
2026-07-24sched_ext: Gate scx_bpf_cidperf_set() behind a new SCX_CAP_PERFTejun Heo
scx_bpf_cidperf_set() reaches cpufreq with no cap check, so any cid-form sub-sched can steer the frequency of any cid in its view, including ones it holds nothing on. Gate it behind a new SCX_CAP_PERF rather than SCX_CAP_BASE: hardware control is a separate axis from queue access - a parent may well delegate scheduling on a cid without handing over its frequency. PERF neither implies nor is implied by the other caps. The check runs under the target rq's lock, which ecaps updates are also folded under, so it is authoritative - a write can never land after a revoke has taken effect. Denials are counted in SCX_EV_SUB_CIDPERF_DENIED. The operation is synchronous and the outcome is reported to the caller: scx_bpf_cidperf_set() now returns 0 or -errno, -EACCES on denial. The cid-form interface is still under initial development, so the signature is changed in place without versioning. scx_qmap grants PERF alongside its existing cid grants so the cpuperf demo keeps working in sub-scheds. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-24sched_ext: Count kicks denied for lacking baseline cid accessTejun Heo
kick_one_cpu() silently skips a kick when the kicking sub-sched lacks SCX_CAP_BASE on the target cid, as does kick_one_cpu_if_idle() for idle kicks. The skips are sound with the same logic as the reenq gate but are invisible today, unlike the preempt degradation counted in SCX_EV_SUB_PREEMPT_DENIED. Count them in a new SCX_EV_SUB_KICK_DENIED event so every cap denial is observable. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-24sched_ext: Gate local DSQ reenq on baseline cid accessTejun Heo
scx_bpf_dsq_reenq() with an SCX_DSQ_LOCAL_ON target schedules deferred reenq work on the cid's cpu, raising an IPI when the target rq isn't the locked one. Nothing checks caps along the way, so a sub-sched holding no cap at all on a cid can force its cpu to take IPIs and rq lock cycles at will. The analogous scx_bpf_kick_cid() path gates delivery on SCX_CAP_BASE in kick_one_cpu() to prevent exactly this. Apply the same rule at the reenq scheduling point: if the calling sched lacks SCX_CAP_BASE on the target cid, drop the reenq and count it in the new SCX_EV_SUB_REENQ_DENIED event. The check is lockless, which is fine: a reenq slipping through right after a revoke is harmless, and a wrong denial can't happen - if the caller has seen its ownership of the cpu, the check sees it too. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-21sched_ext: Build the cid tables privately and publish them with RCUTejun Heo
The cid tables are visible to the cid kfuncs while being modified: the first enable publishes the global pointers before filling them, ops.init_cids() overrides rewrite them in place, and re-enables rebuild them in place. A racing TRACING or SYSCALL program can read unfilled entries, including uninitialized memory in the kmalloc'd tables, or torn topo updates. Tie the tables' lifetimes to the root sched instead: each root enable builds a fresh set privately and publishes the per-table __rcu globals once the layout is final, and root disable unpublishes and RCU-frees the set. A non-NULL global is now always a fully built table which stays valid for the reader's RCU read section, and lookups stay two loads. Kfuncs treat NULL as no-mapping, also after the scheduler exits instead of reporting the stale last mapping. The cid kfuncs are available whether the root scheduler is cid-form or cpu-form, the latter to allow gradual migration to cids. Every root therefore builds and publishes a default mapping. Every reader must either be gated on scheduler liveness or NULL-check inside an RCU read section. Fix the two kfuncs that were neither: scx_bpf_this_cid() read the table with no RCU or preemption protection and scx_bpf_task_cid() relied on KF_RCU, which doesn't put a sleepable program in an RCU read section. The hotplug callbacks are instead serialized by retiring the tables inside the cpus_read_lock() section that clears scx_root. v2: Document why every root builds the tables (desc + cid.c comment). Reported-by: Andrea Righi <arighi@nvidia.com> Closes: https://lore.kernel.org/r/al3tLtPZZkFjMveK@gpd4 Reviewed-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org>
2026-07-19sched_ext: Rename the cid-form cgroup ops to cpuctl_*Tejun Heo
Two unrelated things go by "cgroup" in the cid form. Sub-schedulers attach to cgroups, and the cgroup_*() ops deliver cpu controller events. While the ops names suggest cgroup2 hierarchy, they actually operate on the cpu controller. Rename them to cpuctl_* in struct sched_ext_ops_cid, which has no users outside scx_qmap yet. The cpu form is deployed ABI and keeps the old names. The layout is unchanged and the kernel keeps calling through the cpu-form union view. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-19sched_ext: Hand over cgroups at sub-scheduler enable/disableTejun Heo
Sub-schedulers don't get cgroups yet: every task_group is inited on the root sched and the routing added by the previous patches always resolves to it. Add the handover: an enabling sub-scheduler takes over the cgroups in its subtree and a disabling one returns them to its parent. scx_cgroup_claim_subtree() runs while the sub enables, after the subtree's cgrp->scx_sched's are set and before any task is claimed. It inits each subtree task_group on the sub, exits it from the parent and updates tg->scx.sched. A failed ops.cgroup_init() unwinds the sub-side inits and aborts the enable with the parent untouched. Disabling reverses it with scx_cgroup_return_subtree(): exit each cgroup from the sub, then re-init it on the parent with the current tg->scx.* values, resyncing weight and bandwidth changes made while the sub had it. When a re-init fails, the parent is failed and the remaining task_groups still transfer uninited and get no cgroup ops - the same punting done for tasks. The dying parent's own disable moves them onward. The handover walks include dying but not yet offlined task_groups, the same as root's bulk walks: a removed cgroup keeps hosting scheduling events until its dying tasks finish their final context switches, and its ops.cgroup_exit() must follow the last of them. tg on/offlining is excluded through cgroup_lock(), so either ordering against an rmdir of a subtree cgroup delivers balanced init/exit pairs. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-19sched_ext: Deliver cgroup ops to each task_group's schedTejun Heo
With sub-schedulers claiming cgroup subtrees, cgroup ops must be delivered to each task_group's sched rather than always to root. Add tg->scx.sched to track which sched initialized the task_group. It is set and cleared together with SCX_TG_INITED. Deliver the ops accordingly: - ops.cgroup_exit() goes to the sched whose ops.cgroup_init() it pairs with. - ops.cgroup_prep_move/move/cancel_move() go to the task's sched, and only for moves that don't re-home the task. A re-homing move is reported through the ops.exit_task/init_task() pair instead. The cgroups passed to the move ops can be outside the sched's inited set as the cpu controller can be coarser than the sub-scheduler topology. - Knobs of a cgroup belong to the parent, so ops.set_weight/idle/bandwidth() go to the parent task_group's sched. All task_groups currently resolve to the root sched, so no behavior changes until sub-schedulers start claiming cgroups. While at it, scx_cgroup_init() is restructured so both paths share the recording. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-19sched_ext: Re-home tasks on cgroup migrationTejun Heo
A task's sched (p->scx.sched) must match its cgroup's owner (cgrp->scx_sched). cgroup migration breaks the invariant: scx_cgroup_move_task() only fires root's ops.cgroup_move() and never re-homes the task, leading to wrong-sched scheduling and, once the stale sched is freed, a use-after-free. Hook into the new cgroup task migration events and re-home each task whose destination cgroup is owned by a different sched. The events map naturally to the transfer: MIGRATING runs the fallible init for the destination sched, letting it reject the migration the same way ops.cgroup_prep_move() can, MIGRATED does the re-home, which can't fail, and CANCELED undoes the init when the migration falls through. Pre-commit, the task's task_group still reflects the source, so __scx_init_task() grows an explicit cgroup argument for the migration path to hand ops.init_task() the destination cgroup. Signed-off-by: Tejun Heo <tj@kernel.org> Closes: https://lore.kernel.org/r/alnxrsexEe_nQwqL@gpd4 Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-19sched_ext: Relocate scx_cgroup_enabledTejun Heo
scx_cgroup_enabled is in the CONFIG_EXT_GROUP_SCHED block. The upcoming cgroup migration re-homing needs the gate outside the block. Move the definition and flag flips outside CONFIG_EXT_GROUP_SCHED. No functional changes. Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>
2026-07-15sched_ext: Add the scx_has_subs static key and gate sub-sched hot pathsTejun Heo
With CONFIG_EXT_SUB_SCHED=y but no sub-scheduler attached - the common case - hot paths still pay for sub-sched bookkeeping. Gate it behind __scx_has_subs, a static key counting live sub-schedulers, so that a root-only system stops paying. Most conversions are simple skip-if-no-sub tests. scx_idle_notify() is special - it's a hierarchy walk, so give it a fast path which notifies the root directly using the same tests as the walk. A pending SCX_RQ_SUB_IDLE_RENOTIFY can be ignored as no sub can be owed one and the caller clears the flag either way. Suggested-by: Andrea Righi <arighi@nvidia.com> Signed-off-by: Tejun Heo <tj@kernel.org> Reviewed-by: Andrea Righi <arighi@nvidia.com>