xref: /linux/kernel/sched/ext/sub.c (revision ad45691d8c73075baa1ada6cf2d55c0245259750)
1 // SPDX-License-Identifier: GPL-2.0
2 /*
3  * BPF extensible scheduler class: Documentation/scheduler/sched-ext.rst
4  *
5  * Sub-scheduler hierarchy support.
6  *
7  * A sub-scheduler is an scx_sched attached to a cgroup subtree under another
8  * scx_sched. This file holds the sub-scheduler implementation: the scheduler
9  * tree walk, capability delegation, per-shard cap state and its sync, and the
10  * sub-scheduler enable/disable paths. The core dispatch/enqueue machinery it
11  * builds on lives in ext.c.
12  *
13  * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
14  * Copyright (c) 2026 Tejun Heo <tj@kernel.org>
15  */
16 #include <linux/rhashtable.h>
17 #include "internal.h"
18 #include "cid.h"
19 #include "arena.h"
20 #include "sub.h"
21 
22 #ifdef CONFIG_EXT_SUB_SCHED
23 
24 /**
25  * scx_next_descendant_pre - find the next descendant for pre-order walk
26  * @pos: the current position (%NULL to initiate traversal)
27  * @root: sched whose descendants to walk
28  *
29  * To be used by scx_for_each_descendant_pre(). Find the next descendant to
30  * visit for pre-order traversal of @root's descendants. @root is included in
31  * the iteration and the first node to be visited.
32  */
33 struct scx_sched *scx_next_descendant_pre(struct scx_sched *pos, struct scx_sched *root)
34 {
35 	struct scx_sched *next;
36 
37 	lockdep_assert(lockdep_is_held(&scx_enable_mutex) ||
38 		       lockdep_is_held(&scx_sched_lock));
39 
40 	/* if first iteration, visit @root */
41 	if (!pos)
42 		return root;
43 
44 	/* visit the first child if exists */
45 	next = list_first_entry_or_null(&pos->children, struct scx_sched, sibling);
46 	if (next)
47 		return next;
48 
49 	/* no child, visit my or the closest ancestor's next sibling */
50 	while (pos != root) {
51 		if (!list_is_last(&pos->sibling, &scx_parent(pos)->children))
52 			return list_next_entry(pos, sibling);
53 		pos = scx_parent(pos);
54 	}
55 
56 	return NULL;
57 }
58 
59 static struct scx_sched *scx_find_sub_sched(u64 cgroup_id)
60 {
61 	return rhashtable_lookup(&scx_sched_hash, &cgroup_id,
62 				 scx_sched_hash_params);
63 }
64 
65 void scx_set_task_sched(struct task_struct *p, struct scx_sched *sch)
66 {
67 	rcu_assign_pointer(p->scx.sched, sch);
68 }
69 
70 struct cgroup *sch_cgroup(struct scx_sched *sch)
71 {
72 	return sch->cgrp;
73 }
74 
75 /* for each descendant of @cgrp including self, set ->scx_sched to @sch */
76 void set_cgroup_sched(struct cgroup *cgrp, struct scx_sched *sch)
77 {
78 	struct cgroup *pos;
79 	struct cgroup_subsys_state *css;
80 
81 	cgroup_for_each_live_descendant_pre(pos, css, cgrp)
82 		rcu_assign_pointer(pos->scx_sched, sch);
83 }
84 
85 static DECLARE_WAIT_QUEUE_HEAD(scx_unlink_waitq);
86 
87 void drain_descendants(struct scx_sched *sch)
88 {
89 	/*
90 	 * Child scheds that finished the critical part of disabling will take
91 	 * themselves off @sch->children. Wait for it to drain. As propagation
92 	 * is recursive, empty @sch->children means that all proper descendant
93 	 * scheds reached unlinking stage.
94 	 */
95 	wait_event(scx_unlink_waitq, list_empty(&sch->children));
96 }
97 
98 static void scx_fail_parent(struct scx_sched *sch,
99 			    struct task_struct *failed, s32 fail_code)
100 {
101 	struct scx_sched *parent = scx_parent(sch);
102 	struct scx_task_iter sti;
103 	struct task_struct *p;
104 
105 	scx_error(parent, "ops.init_task() failed (%d) for %s[%d] while disabling a sub-scheduler",
106 		  fail_code, failed->comm, failed->pid);
107 
108 	/*
109 	 * Once $parent is bypassed, it's safe to put SCX_TASK_NONE tasks into
110 	 * it. This may cause downstream failures on the BPF side but $parent is
111 	 * dying anyway.
112 	 */
113 	scx_bypass(parent, true);
114 
115 	scx_task_iter_start(&sti, sch->cgrp);
116 	while ((p = scx_task_iter_next_locked(&sti))) {
117 		if (scx_task_on_sched(parent, p))
118 			continue;
119 
120 		scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {
121 			scx_disable_and_exit_task(sch, p);
122 			scx_set_task_sched(p, parent);
123 		}
124 	}
125 	scx_task_iter_stop(&sti);
126 }
127 
128 void scx_sub_disable(struct scx_sched *sch)
129 {
130 	struct scx_sched *parent = scx_parent(sch);
131 	struct scx_task_iter sti;
132 	struct task_struct *p;
133 	int ret;
134 
135 	/*
136 	 * Guarantee forward progress and wait for descendants to be disabled.
137 	 * To limit disruptions, $parent is not bypassed. Tasks are fully
138 	 * prepped and then inserted back into $parent.
139 	 */
140 	scx_bypass(sch, true);
141 	drain_descendants(sch);
142 
143 	/*
144 	 * Here, every runnable task is guaranteed to make forward progress and
145 	 * we can safely use blocking synchronization constructs. Actually
146 	 * disable ops.
147 	 */
148 	mutex_lock(&scx_enable_mutex);
149 	percpu_down_write(&scx_fork_rwsem);
150 	scx_cgroup_lock();
151 
152 	set_cgroup_sched(sch_cgroup(sch), parent);
153 
154 	scx_task_iter_start(&sti, sch->cgrp);
155 	while ((p = scx_task_iter_next_locked(&sti))) {
156 		struct rq *rq;
157 		struct rq_flags rf;
158 
159 		/* filter out duplicate visits */
160 		if (scx_task_on_sched(parent, p))
161 			continue;
162 
163 		/*
164 		 * By the time control reaches here, all descendant schedulers
165 		 * should already have been disabled.
166 		 */
167 		WARN_ON_ONCE(!scx_task_on_sched(sch, p));
168 
169 		/*
170 		 * @p is pinned by the iter: css_task_iter_next() takes a
171 		 * reference and holds it until the next iter_next() call, so
172 		 * @p->usage is guaranteed > 0.
173 		 */
174 		get_task_struct(p);
175 
176 		scx_task_iter_unlock(&sti);
177 
178 		/*
179 		 * $p is READY or ENABLED on @sch. Initialize for $parent,
180 		 * disable and exit from @sch, and then switch over to $parent.
181 		 *
182 		 * If a task fails to initialize for $parent, the only available
183 		 * action is disabling $parent too. While this allows disabling
184 		 * of a child sched to cause the parent scheduler to fail, the
185 		 * failure can only originate from ops.init_task() of the
186 		 * parent. A child can't directly affect the parent through its
187 		 * own failures.
188 		 */
189 		ret = __scx_init_task(parent, p, false);
190 		if (ret) {
191 			scx_fail_parent(sch, p, ret);
192 			put_task_struct(p);
193 			break;
194 		}
195 
196 		rq = task_rq_lock(p, &rf);
197 
198 		if (scx_get_task_state(p) == SCX_TASK_DEAD) {
199 			/*
200 			 * sched_ext_dead() raced us between __scx_init_task()
201 			 * and this rq lock and ran exit_task() on @sch (the
202 			 * sched @p was on at that point), not on $parent.
203 			 * $parent's just-completed init is owed an exit_task()
204 			 * and we issue it here.
205 			 */
206 			scx_sub_init_cancel_task(parent, p);
207 			task_rq_unlock(rq, p, &rf);
208 			put_task_struct(p);
209 			continue;
210 		}
211 
212 		scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {
213 			/*
214 			 * $p is initialized for $parent and still attached to
215 			 * @sch. Disable and exit for @sch, switch over to
216 			 * $parent, override the state to READY to account for
217 			 * $p having already been initialized, and then enable.
218 			 */
219 			scx_disable_and_exit_task(sch, p);
220 			scx_set_task_state(p, SCX_TASK_INIT_BEGIN);
221 			scx_set_task_state(p, SCX_TASK_INIT);
222 			scx_set_task_sched(p, parent);
223 			scx_set_task_state(p, SCX_TASK_READY);
224 			scx_enable_task(parent, p);
225 		}
226 
227 		task_rq_unlock(rq, p, &rf);
228 		put_task_struct(p);
229 	}
230 	scx_task_iter_stop(&sti);
231 
232 	scx_disable_dump(sch);
233 
234 	scx_cgroup_unlock();
235 	percpu_up_write(&scx_fork_rwsem);
236 
237 	/*
238 	 * All tasks are moved off of @sch but there may still be on-going
239 	 * operations (e.g. ops.select_cpu()). Drain them by flushing RCU. Use
240 	 * the expedited version as ancestors may be waiting in bypass mode.
241 	 * Also, tell the parent that there is no need to keep running bypass
242 	 * DSQs for us.
243 	 */
244 	synchronize_rcu_expedited();
245 	scx_disable_bypass_dsp(sch);
246 
247 	scx_unlink_sched(sch);
248 
249 	mutex_unlock(&scx_enable_mutex);
250 
251 	/*
252 	 * @sch is now unlinked from the parent's children list. Notify and call
253 	 * ops.sub_detach/exit(). Note that ops.sub_detach/exit() must be called
254 	 * after unlinking and releasing all locks. See scx_claim_exit().
255 	 */
256 	wake_up_all(&scx_unlink_waitq);
257 
258 	if (parent->ops.sub_detach && sch->sub_attached) {
259 		struct scx_sub_detach_args sub_detach_args = {
260 			.ops = &sch->ops,
261 			.cgroup_path = sch->cgrp_path,
262 		};
263 		SCX_CALL_OP(parent, sub_detach, NULL,
264 			    &sub_detach_args);
265 	}
266 
267 	scx_log_sched_disable(sch);
268 
269 	if (sch->ops.exit)
270 		SCX_CALL_OP(sch, exit, NULL, sch->exit_info);
271 	if (sch->sub_kset)
272 		kobject_del(&sch->sub_kset->kobj);
273 	kobject_del(&sch->kobj);
274 }
275 
276 /* verify that a scheduler can be attached to @cgrp and return the parent */
277 static struct scx_sched *find_parent_sched(struct cgroup *cgrp)
278 {
279 	struct scx_sched *parent = cgrp->scx_sched;
280 	struct scx_sched *pos;
281 
282 	lockdep_assert_held(&scx_sched_lock);
283 
284 	/* can't attach twice to the same cgroup */
285 	if (parent->cgrp == cgrp)
286 		return ERR_PTR(-EBUSY);
287 
288 	/* does $parent allow sub-scheds? */
289 	if (!parent->ops.sub_attach)
290 		return ERR_PTR(-EOPNOTSUPP);
291 
292 	/* can't insert between $parent and its exiting children */
293 	list_for_each_entry(pos, &parent->children, sibling)
294 		if (cgroup_is_descendant(pos->cgrp, cgrp))
295 			return ERR_PTR(-EBUSY);
296 
297 	return parent;
298 }
299 
300 static bool assert_task_ready_or_enabled(struct task_struct *p)
301 {
302 	u32 state = scx_get_task_state(p);
303 
304 	switch (state) {
305 	case SCX_TASK_READY:
306 	case SCX_TASK_ENABLED:
307 		return true;
308 	default:
309 		WARN_ONCE(true, "sched_ext: Invalid task state %d for %s[%d] during enabling sub sched",
310 			  state, p->comm, p->pid);
311 		return false;
312 	}
313 }
314 
315 void scx_sub_enable_workfn(struct kthread_work *work)
316 {
317 	struct scx_enable_cmd *cmd = container_of(work, struct scx_enable_cmd, work);
318 	struct sched_ext_ops *ops = cmd->ops;
319 	struct cgroup *cgrp;
320 	struct scx_sched *parent, *sch;
321 	struct scx_task_iter sti;
322 	struct task_struct *p;
323 	s32 i, ret;
324 
325 	mutex_lock(&scx_enable_mutex);
326 
327 	if (!scx_enabled()) {
328 		ret = -ENODEV;
329 		goto out_unlock;
330 	}
331 
332 	/* See scx_root_enable_workfn() for the @ops->priv check. */
333 	if (rcu_access_pointer(ops->priv)) {
334 		ret = -EBUSY;
335 		goto out_unlock;
336 	}
337 
338 	cgrp = cgroup_get_from_id(ops->sub_cgroup_id);
339 	if (IS_ERR(cgrp)) {
340 		ret = PTR_ERR(cgrp);
341 		goto out_unlock;
342 	}
343 
344 	raw_spin_lock_irq(&scx_sched_lock);
345 	parent = find_parent_sched(cgrp);
346 	if (IS_ERR(parent)) {
347 		raw_spin_unlock_irq(&scx_sched_lock);
348 		ret = PTR_ERR(parent);
349 		goto out_put_cgrp;
350 	}
351 	kobject_get(&parent->kobj);
352 	raw_spin_unlock_irq(&scx_sched_lock);
353 
354 	/* scx_alloc_and_add_sched() consumes @cgrp whether it succeeds or not */
355 	sch = scx_alloc_and_add_sched(cmd, cgrp, parent);
356 	kobject_put(&parent->kobj);
357 	if (IS_ERR(sch)) {
358 		ret = PTR_ERR(sch);
359 		goto out_unlock;
360 	}
361 
362 	ret = scx_link_sched(sch);
363 	if (ret)
364 		goto err_disable;
365 
366 	if (sch->level >= SCX_SUB_MAX_DEPTH) {
367 		scx_error(sch, "max nesting depth %d violated",
368 			  SCX_SUB_MAX_DEPTH);
369 		goto err_disable;
370 	}
371 
372 	if (sch->ops.init) {
373 		ret = SCX_CALL_OP_RET(sch, init, NULL);
374 		if (ret) {
375 			ret = scx_ops_sanitize_err(sch, "init", ret);
376 			scx_error(sch, "ops.init() failed (%d)", ret);
377 			goto err_disable;
378 		}
379 		sch->exit_info->flags |= SCX_EFLAG_INITIALIZED;
380 	}
381 
382 	ret = scx_arena_pool_init(sch);
383 	if (ret)
384 		goto err_disable;
385 
386 	ret = scx_set_cmask_scratch_alloc(sch);
387 	if (ret)
388 		goto err_disable;
389 
390 	if (scx_validate_ops(sch, ops))
391 		goto err_disable;
392 
393 	struct scx_sub_attach_args sub_attach_args = {
394 		.ops = &sch->ops,
395 		.cgroup_path = sch->cgrp_path,
396 	};
397 
398 	ret = SCX_CALL_OP_RET(parent, sub_attach, NULL,
399 			      &sub_attach_args);
400 	if (ret) {
401 		ret = scx_ops_sanitize_err(sch, "sub_attach", ret);
402 		scx_error(sch, "parent rejected (%d)", ret);
403 		goto err_disable;
404 	}
405 	sch->sub_attached = true;
406 
407 	scx_bypass(sch, true);
408 
409 	for (i = SCX_OPI_BEGIN; i < SCX_OPI_END; i++)
410 		if (((void (**)(void))ops)[i])
411 			set_bit(i, sch->has_op);
412 
413 	percpu_down_write(&scx_fork_rwsem);
414 	scx_cgroup_lock();
415 
416 	/*
417 	 * Set cgroup->scx_sched's and check CSS_ONLINE. Either we see
418 	 * !CSS_ONLINE or scx_cgroup_lifetime_notify() sees and shoots us down.
419 	 */
420 	set_cgroup_sched(sch_cgroup(sch), sch);
421 	if (!(cgrp->self.flags & CSS_ONLINE)) {
422 		scx_error(sch, "cgroup is not online");
423 		goto err_unlock_and_disable;
424 	}
425 
426 	/*
427 	 * Initialize tasks for the new child $sch without exiting them for
428 	 * $parent so that the tasks can always be reverted back to $parent
429 	 * sched on child init failure.
430 	 */
431 	WARN_ON_ONCE(scx_enabling_sub_sched);
432 	scx_enabling_sub_sched = sch;
433 
434 	scx_task_iter_start(&sti, sch->cgrp);
435 	while ((p = scx_task_iter_next_locked(&sti))) {
436 		struct rq *rq;
437 		struct rq_flags rf;
438 
439 		/*
440 		 * Task iteration may visit the same task twice when racing
441 		 * against exiting. Use %SCX_TASK_SUB_INIT to mark tasks which
442 		 * finished __scx_init_task() and skip if set.
443 		 *
444 		 * A task may exit and get freed between __scx_init_task()
445 		 * completion and scx_enable_task(). In such cases,
446 		 * scx_disable_and_exit_task() must exit the task for both the
447 		 * parent and child scheds.
448 		 */
449 		if (p->scx.flags & SCX_TASK_SUB_INIT)
450 			continue;
451 
452 		/* @p is pinned by the iter; see scx_sub_disable() */
453 		get_task_struct(p);
454 
455 		if (!assert_task_ready_or_enabled(p)) {
456 			ret = -EINVAL;
457 			goto abort;
458 		}
459 
460 		scx_task_iter_unlock(&sti);
461 
462 		/*
463 		 * As $p is still on $parent, it can't be transitioned to INIT.
464 		 * Let's worry about task state later. Use __scx_init_task().
465 		 */
466 		ret = __scx_init_task(sch, p, false);
467 		if (ret)
468 			goto abort;
469 
470 		rq = task_rq_lock(p, &rf);
471 
472 		if (scx_get_task_state(p) == SCX_TASK_DEAD) {
473 			/*
474 			 * sched_ext_dead() raced us between __scx_init_task()
475 			 * and this rq lock and ran exit_task() on $parent (the
476 			 * sched @p was on at that point), not on @sch. @sch's
477 			 * just-completed init is owed an exit_task() and we
478 			 * issue it here.
479 			 */
480 			scx_sub_init_cancel_task(sch, p);
481 			task_rq_unlock(rq, p, &rf);
482 			put_task_struct(p);
483 			continue;
484 		}
485 
486 		p->scx.flags |= SCX_TASK_SUB_INIT;
487 		task_rq_unlock(rq, p, &rf);
488 
489 		put_task_struct(p);
490 	}
491 	scx_task_iter_stop(&sti);
492 
493 	/*
494 	 * All tasks are prepped. Disable/exit tasks for $parent and enable for
495 	 * the new @sch.
496 	 */
497 	scx_task_iter_start(&sti, sch->cgrp);
498 	while ((p = scx_task_iter_next_locked(&sti))) {
499 		/*
500 		 * Use clearing of %SCX_TASK_SUB_INIT to detect and skip
501 		 * duplicate iterations.
502 		 */
503 		if (!(p->scx.flags & SCX_TASK_SUB_INIT))
504 			continue;
505 
506 		scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {
507 			/*
508 			 * $p must be either READY or ENABLED. If ENABLED,
509 			 * __scx_disabled_and_exit_task() first disables and
510 			 * makes it READY. However, after exiting $p, it will
511 			 * leave $p as READY.
512 			 */
513 			assert_task_ready_or_enabled(p);
514 			__scx_disable_and_exit_task(parent, p);
515 
516 			/*
517 			 * $p is now only initialized for @sch and READY, which
518 			 * is what we want. Assign it to @sch and enable.
519 			 */
520 			scx_set_task_sched(p, sch);
521 			scx_enable_task(sch, p);
522 
523 			p->scx.flags &= ~SCX_TASK_SUB_INIT;
524 		}
525 	}
526 	scx_task_iter_stop(&sti);
527 
528 	scx_enabling_sub_sched = NULL;
529 
530 	scx_cgroup_unlock();
531 	percpu_up_write(&scx_fork_rwsem);
532 
533 	scx_bypass(sch, false);
534 
535 	pr_info("sched_ext: BPF sub-scheduler \"%s\" enabled\n", sch->ops.name);
536 	kobject_uevent(&sch->kobj, KOBJ_ADD);
537 	ret = 0;
538 	goto out_unlock;
539 
540 out_put_cgrp:
541 	cgroup_put(cgrp);
542 out_unlock:
543 	mutex_unlock(&scx_enable_mutex);
544 	cmd->ret = ret;
545 	return;
546 
547 abort:
548 	put_task_struct(p);
549 	scx_task_iter_stop(&sti);
550 
551 	/*
552 	 * Undo __scx_init_task() for tasks we marked. scx_enable_task() never
553 	 * ran for @sch on them, so calling scx_disable_task() here would invoke
554 	 * ops.disable() without a matching ops.enable(). scx_enabling_sub_sched
555 	 * must stay set until SUB_INIT is cleared from every marked task -
556 	 * scx_disable_and_exit_task() reads it when a task exits concurrently.
557 	 */
558 	scx_task_iter_start(&sti, sch->cgrp);
559 	while ((p = scx_task_iter_next_locked(&sti))) {
560 		if (p->scx.flags & SCX_TASK_SUB_INIT) {
561 			scx_sub_init_cancel_task(sch, p);
562 			p->scx.flags &= ~SCX_TASK_SUB_INIT;
563 		}
564 	}
565 	scx_task_iter_stop(&sti);
566 	scx_enabling_sub_sched = NULL;
567 err_unlock_and_disable:
568 	/* we'll soon enter disable path, keep bypass on */
569 	scx_cgroup_unlock();
570 	percpu_up_write(&scx_fork_rwsem);
571 err_disable:
572 	mutex_unlock(&scx_enable_mutex);
573 	/*
574 	 * Some enable failures only return an errno (e.g. -ENOMEM from an
575 	 * allocation) without calling scx_error(). Record it so
576 	 * scx_flush_disable_work() runs the disable and ops.exit() fires.
577 	 */
578 	scx_error(sch, "scx_sub_enable() failed (%d)", ret);
579 	scx_flush_disable_work(sch);
580 	cmd->ret = 0;
581 }
582 
583 static s32 scx_cgroup_lifetime_notify(struct notifier_block *nb,
584 				      unsigned long action, void *data)
585 {
586 	struct cgroup *cgrp = data;
587 	struct cgroup *parent = cgroup_parent(cgrp);
588 
589 	if (!cgroup_on_dfl(cgrp))
590 		return NOTIFY_OK;
591 
592 	switch (action) {
593 	case CGROUP_LIFETIME_ONLINE:
594 		/* inherit ->scx_sched from $parent */
595 		if (parent)
596 			rcu_assign_pointer(cgrp->scx_sched, parent->scx_sched);
597 		break;
598 	case CGROUP_LIFETIME_OFFLINE:
599 		/* if there is a sched attached, shoot it down */
600 		if (cgrp->scx_sched && cgrp->scx_sched->cgrp == cgrp)
601 			scx_exit(cgrp->scx_sched, SCX_EXIT_UNREG_KERN,
602 				 SCX_ECODE_RSN_CGROUP_OFFLINE,
603 				 "cgroup %llu going offline", cgroup_id(cgrp));
604 		break;
605 	}
606 
607 	return NOTIFY_OK;
608 }
609 
610 static struct notifier_block scx_cgroup_lifetime_nb = {
611 	.notifier_call = scx_cgroup_lifetime_notify,
612 };
613 
614 static s32 __init scx_cgroup_lifetime_notifier_init(void)
615 {
616 	return blocking_notifier_chain_register(&cgroup_lifetime_notifier,
617 						&scx_cgroup_lifetime_nb);
618 }
619 core_initcall(scx_cgroup_lifetime_notifier_init);
620 
621 void scx_pstack_recursion_on_dispatch(struct bpf_prog *prog)
622 {
623 	struct scx_sched *sch;
624 
625 	guard(rcu)();
626 	sch = scx_prog_sched(prog->aux);
627 	if (unlikely(!sch))
628 		return;
629 
630 	scx_error(sch, "dispatch recursion detected");
631 }
632 
633 __bpf_kfunc_start_defs();
634 
635 /**
636  * scx_bpf_sub_dispatch - Trigger dispatching on a child scheduler
637  * @cgroup_id: cgroup ID of the child scheduler to dispatch
638  * @aux: implicit BPF argument to access bpf_prog_aux hidden from BPF progs
639  *
640  * Allows a parent scheduler to trigger dispatching on one of its direct
641  * child schedulers. The child scheduler runs its dispatch operation to
642  * move tasks from dispatch queues to the local runqueue.
643  *
644  * Returns: true on success, false if cgroup_id is invalid, not a direct
645  * child, or caller lacks dispatch permission.
646  */
647 __bpf_kfunc bool scx_bpf_sub_dispatch(u64 cgroup_id, const struct bpf_prog_aux *aux)
648 {
649 	struct rq *this_rq = this_rq();
650 	struct scx_sched *parent, *child;
651 
652 	guard(rcu)();
653 	parent = scx_prog_sched(aux);
654 	if (unlikely(!parent))
655 		return false;
656 
657 	child = scx_find_sub_sched(cgroup_id);
658 
659 	if (unlikely(!child))
660 		return false;
661 
662 	if (unlikely(scx_parent(child) != parent)) {
663 		scx_error(parent, "trying to dispatch a distant sub-sched on cgroup %llu",
664 			  cgroup_id);
665 		return false;
666 	}
667 
668 	return scx_dispatch_sched(child, this_rq, this_rq->scx.sub_dispatch_prev,
669 				  true);
670 }
671 
672 __bpf_kfunc_end_defs();
673 
674 #endif	/* CONFIG_EXT_SUB_SCHED */
675