sched_ext: Add coalescing sub_caps_updated() notifier for sub-schedulers

Wire up ops_cid.sub_caps_updated() to notify sub-scheds of cap changes.

Three constraints shape the design:

  1. Static memory. Deliveries use a fixed-size buffer, both for runtime
     efficiency and so notifications can't be lost under memory pressure.

  2. High-frequency updates. Grant/revoke can mutate caps in bursts, and the
     notifier path must absorb that without amplifying it.

  3. Recursive grant/revoke from the callback. A child receiving a
     notification can call grant/revoke on its own children, which can
     cascade recursively down its subtree.

(1) and (2) lead to coalescing into a fixed payload. Each delivery carries a
single (cmask, caps) pair covering every change since the previous one.
Direction (set vs cleared) isn't encoded as it doesn't fit in the fixed-size
summary. The callback queries scx_bpf_sub_caps() for current state. Only one
delivery is in flight per shard. Further changes fold into the same buffer
and ship as the next callback, so a shard's callbacks fire in order.

(3) leads to deferred delivery. Events accumulate during grant/revoke and
are delivered after the shard lock is released.

v2:
- Request a private stack for ops.sub_caps_updated(). (sashiko AI)
- Build cmask_arena_out via scx_cmask_ref, not by re-reading its header.

Signed-off-by: Tejun Heo <tj@kernel.org>
Reviewed-by: Andrea Righi <arighi@nvidia.com>
This commit is contained in:
Tejun Heo 2026-07-13 22:18:43 -10:00
parent 86094b95ef
commit 5f2a9a4c2e
4 changed files with 254 additions and 9 deletions

View File

@ -7320,6 +7320,11 @@ static int bpf_scx_check_member(const struct btf_type *t,
case offsetof(struct sched_ext_ops, dispatch):
prog->aux->priv_stack_requested = true;
prog->aux->recursion_detected = scx_pstack_recursion_on_dispatch;
break;
case offsetof(struct sched_ext_ops, sub_caps_updated):
prog->aux->priv_stack_requested = true;
prog->aux->recursion_detected = scx_pstack_recursion_on_caps_updated;
break;
}
#endif /* CONFIG_EXT_SUB_SCHED */
@ -7521,11 +7526,13 @@ static struct bpf_struct_ops bpf_sched_ext_ops = {
/*
* cid-form cfi stubs. Stubs whose signatures match the cpu-form (param types
* identical, only param names differ across structs) are reused. Only
* set_cmask needs a fresh stub since the second argument type differs.
* identical, only param names differ across structs) are reused. Some need
* fresh stubs, set_cmask due to an argument type difference and the sub-sched
* notifiers because no cpu-form stub exists to reuse.
*/
static void sched_ext_ops_cid__set_cmask(struct task_struct *p,
const struct scx_cmask *cmask) {}
static void sched_ext_ops__sub_caps_updated(const struct scx_cmask *cmask, u64 caps) {}
static struct sched_ext_ops_cid __bpf_ops_sched_ext_ops_cid = {
.select_cid = sched_ext_ops__select_cpu,
@ -7558,6 +7565,7 @@ static struct sched_ext_ops_cid __bpf_ops_sched_ext_ops_cid = {
#endif
.sub_attach = sched_ext_ops__sub_attach,
.sub_detach = sched_ext_ops__sub_detach,
.sub_caps_updated = sched_ext_ops__sub_caps_updated,
.cid_online = sched_ext_ops__cpu_online,
.cid_offline = sched_ext_ops__cpu_offline,
.init_cids = sched_ext_ops__init_cids,
@ -10012,6 +10020,7 @@ static int __init scx_init(void)
CID_OFFSET_MATCH(dump_task, dump_task);
CID_OFFSET_MATCH(sub_attach, sub_attach);
CID_OFFSET_MATCH(sub_detach, sub_detach);
CID_OFFSET_MATCH(sub_caps_updated, sub_caps_updated);
CID_OFFSET_MATCH(init_cids, init_cids);
CID_OFFSET_MATCH(init, init);
CID_OFFSET_MATCH(exit, exit);

View File

@ -757,6 +757,25 @@ struct sched_ext_ops {
*/
void (*sub_detach)(struct scx_sub_detach_args *args);
/**
* @sub_caps_updated: Caps on this sub-sched's shard changed
* @cmask: cids whose caps changed (cmask->base identifies the shard)
* @caps: SCX_CAP_* that changed
*
* Invoked after grant or revoke modifies caps on a shard. There can be
* only one in-flight invocation per shard. @cmask and @caps coalesce
* all changes since the last delivery. Direction (set vs cleared) isn't
* encoded. Query current state with scx_bpf_sub_caps().
*
* Delivered asynchronously after the change is recorded, and may run
* before it takes effect on any given cpu. Use it to track which caps
* the sub-sched holds and propagate to its own children, not to decide
* if a task can run on a cpu now.
*
* May call scx_bpf_sub_grant() / scx_bpf_sub_revoke() on children.
*/
void (*sub_caps_updated)(const struct scx_cmask *cmask, u64 caps);
/*
* All online ops must come before ops.cpu_online().
*/
@ -977,6 +996,7 @@ struct sched_ext_ops_cid {
#endif /* CONFIG_EXT_GROUP_SCHED */
s32 (*sub_attach)(struct scx_sub_attach_args *args);
void (*sub_detach)(struct scx_sub_detach_args *args);
void (*sub_caps_updated)(const struct scx_cmask *cmask, u64 caps);
void (*cid_online)(s32 cid);
void (*cid_offline)(s32 cid);
s32 (*init_cids)(void);
@ -1224,9 +1244,51 @@ enum scx_cap_flags {
__caps && ((cap_bit) = __ffs64(__caps), true); \
__caps &= __caps - 1)
/*
* Sub-cap update notifier.
*
* ops_cid.sub_caps_updated() notifies sub-scheds when their cap state changes
* so they can refresh internal state without polling scx_bpf_sub_caps() per
* enqueue.
*
* Three constraints shape the design:
*
* 1. Static memory. Deliveries use a fixed-size buffer, both for runtime
* efficiency and so notifications can't be lost under memory pressure.
*
* 2. High-frequency updates. Grant/revoke can mutate caps in bursts, and the
* notifier path must absorb that without amplifying it.
*
* 3. Recursive grant/revoke from the callback. A child receiving a
* notification can call grant/revoke on its own children, which can
* cascade recursively down its subtree.
*
* (1) and (2) lead to coalescing into a fixed payload. Each delivery carries a
* single (cmask, caps) pair covering every change since the previous one.
* Direction (set vs cleared) isn't encoded as it doesn't fit in the fixed-size
* summary. The callback queries scx_bpf_sub_caps() for current state. Only one
* delivery is in flight per shard. Further changes fold into the same buffer
* and ship as the next callback, so a shard's callbacks fire in order.
*
* (3) leads to deferred delivery. Events accumulate during grant/revoke and are
* delivered after the shard lock is released.
*/
struct scx_caps_updated {
raw_spinlock_t lock;
u64 caps;
struct scx_cmask *cmask_arena_out;
struct list_head node_in_flight;
/* Kernel-side accumulator. Access as &cu->cmask. */
TRAILING_OVERLAP(struct scx_cmask, cmask, bits,
u64 _bits[SCX_CMASK_NR_WORDS(SCX_CID_SHARD_MAX_CPUS)];
);
};
struct scx_pshard {
raw_spinlock_t lock; /* serializes caps */
struct scx_sched *sch; /* backpointer */
struct scx_caps_updated caps_updated;
/*
* Per-cap cmask, inline via TRAILING_OVERLAP so cmask.bits[] overlaps
* the trailing _bits[] storage. Access as &caps[i].cmask.
@ -1234,6 +1296,15 @@ struct scx_pshard {
TRAILING_OVERLAP(struct scx_cmask, cmask, bits,
u64 _bits[SCX_CMASK_NR_WORDS(SCX_CID_SHARD_MAX_CPUS)];
) caps[__SCX_NR_CAPS];
/*
* Shard geometry captured at alloc. cmask_arena_out's own header is
* bpf-writable and the live shard range can change before the
* rcu-deferred free, so re-init and size cmask_arena_out from these
* trusted copies instead.
*/
u32 base;
u32 nr_cids;
};
#endif

View File

@ -106,6 +106,15 @@ void set_cgroup_sched(struct cgroup *cgrp, struct scx_sched *sch)
static void free_pshard(struct scx_pshard *pshard)
{
struct scx_caps_updated *cu;
if (!pshard)
return;
cu = &pshard->caps_updated;
if (cu->cmask_arena_out)
scx_arena_free(pshard->sch, cu->cmask_arena_out,
struct_size_t(struct scx_cmask, bits,
SCX_CMASK_NR_WORDS(pshard->nr_cids)));
kfree(pshard);
}
@ -123,7 +132,10 @@ void scx_free_pshards(struct scx_sched *sch)
static struct scx_pshard *alloc_pshard(struct scx_sched *sch, s32 shard_idx, s32 node)
{
const struct scx_cid_shard *shard = &scx_cid_shard_ranges[shard_idx];
size_t cmask_size = struct_size_t(struct scx_cmask, bits,
SCX_CMASK_NR_WORDS(shard->nr_cids));
struct scx_pshard *pshard;
struct scx_caps_updated *cu;
s32 i;
pshard = kzalloc_node(sizeof(*pshard), GFP_KERNEL, node);
@ -132,10 +144,25 @@ static struct scx_pshard *alloc_pshard(struct scx_sched *sch, s32 shard_idx, s32
raw_spin_lock_init(&pshard->lock);
pshard->sch = sch;
pshard->base = shard->base_cid;
pshard->nr_cids = shard->nr_cids;
for (i = 0; i < __SCX_NR_CAPS; i++)
scx_cmask_init(&pshard->caps[i].cmask, shard->base_cid, shard->nr_cids);
cu = &pshard->caps_updated;
raw_spin_lock_init(&cu->lock);
INIT_LIST_HEAD(&cu->node_in_flight);
__scx_cmask_init(&cu->cmask, shard->base_cid, shard->nr_cids, SCX_CID_SHARD_MAX_CPUS);
cu->cmask_arena_out = scx_arena_alloc(sch, cmask_size);
if (!cu->cmask_arena_out) {
free_pshard(pshard);
return NULL;
}
scx_cmask_init(cu->cmask_arena_out, shard->base_cid, shard->nr_cids);
return pshard;
}
@ -188,6 +215,88 @@ void scx_init_root_caps(struct scx_sched *sch)
}
}
/* record a caps change, see struct scx_caps_updated */
static void caps_updated_record(struct scx_pshard *ps, const struct scx_cmask *cids, u64 caps,
struct list_head *to_deliver)
{
struct scx_caps_updated *cu = &ps->caps_updated;
guard(raw_spinlock)(&cu->lock);
scx_cmask_or(&cu->cmask, cids);
cu->caps |= caps;
if (list_empty(&cu->node_in_flight))
list_add_tail(&cu->node_in_flight, to_deliver);
}
/* deliver queued caps_updated callbacks, see struct scx_caps_updated */
static void caps_updated_deliver(struct list_head *to_deliver)
{
struct scx_caps_updated *cu, *tmp;
list_for_each_entry_safe(cu, tmp, to_deliver, node_in_flight) {
struct scx_pshard *ps = container_of(cu, struct scx_pshard, caps_updated);
struct scx_sched *sch = ps->sch;
while (true) {
u64 caps = 0;
/*
* During enable, has_op is set after ops.sub_attach(),
* so !has_op means the op is absent or the sched isn't
* live yet - e.g. caps grant from ops.sub_attach().
* Either way don't consume - leave for
* scx_sub_seed_caps() to deliver once live.
*/
scoped_guard (raw_spinlock, &cu->lock) {
if (cu->caps && SCX_HAS_OP(sch, sub_caps_updated) &&
likely(!READ_ONCE(sch->aborting))) {
struct scx_cmask_ref ref;
caps = cu->caps;
scx_cmask_ref_init_kern(sch, cu->cmask_arena_out,
ps->base, ps->nr_cids, &ref);
scx_cmask_ref_copy(&ref, &cu->cmask);
scx_cmask_clear(&cu->cmask);
cu->caps = 0;
} else {
list_del_init(&cu->node_in_flight);
}
}
if (!caps)
break;
/* caps != 0 only when deliverable (has_op, above) */
SCX_CALL_OP(sch, sub_caps_updated, NULL,
scx_kaddr_to_arena(sch, cu->cmask_arena_out),
caps);
}
}
}
/*
* Deliver caps owed to @sch that couldn't be delivered earlier (e.g. a grant
* taken during its sub_attach(), before has_op was set). Called once @sch is
* enabled.
*/
static void scx_sub_seed_caps(struct scx_sched *sch)
{
LIST_HEAD(to_deliver);
s32 si;
guard(irqsave)();
for (si = 0; si < sch->nr_pshards; si++) {
struct scx_pshard *ps = sch->pshard[si];
struct scx_caps_updated *cu = &ps->caps_updated;
scoped_guard (raw_spinlock, &cu->lock) {
if (cu->caps && list_empty(&cu->node_in_flight))
list_add_tail(&cu->node_in_flight, &to_deliver);
}
}
caps_updated_deliver(&to_deliver);
}
static DECLARE_WAIT_QUEUE_HEAD(scx_unlink_waitq);
void drain_descendants(struct scx_sched *sch)
@ -671,6 +780,9 @@ void scx_sub_enable_workfn(struct kthread_work *work)
scx_bypass(sch, false);
/* @sch is enabled; deliver any caps owed since its sub_attach() */
scx_sub_seed_caps(sch);
pr_info("sched_ext: BPF sub-scheduler \"%s\" enabled\n", sch->ops.name);
kobject_uevent(&sch->kobj, KOBJ_ADD);
ret = 0;
@ -757,7 +869,7 @@ static s32 __init scx_cgroup_lifetime_notifier_init(void)
}
core_initcall(scx_cgroup_lifetime_notifier_init);
void scx_pstack_recursion_on_dispatch(struct bpf_prog *prog)
static void scx_pstack_recursion(struct bpf_prog *prog, const char *op)
{
struct scx_sched *sch;
@ -766,7 +878,17 @@ void scx_pstack_recursion_on_dispatch(struct bpf_prog *prog)
if (unlikely(!sch))
return;
scx_error(sch, "dispatch recursion detected");
scx_error(sch, "%s recursion detected", op);
}
void scx_pstack_recursion_on_dispatch(struct bpf_prog *prog)
{
scx_pstack_recursion(prog, "dispatch");
}
void scx_pstack_recursion_on_caps_updated(struct bpf_prog *prog)
{
scx_pstack_recursion(prog, "sub_caps_updated");
}
__bpf_kfunc_start_defs();
@ -869,6 +991,7 @@ __bpf_kfunc s32 scx_bpf_sub_grant(u64 cgroup_id, u64 caps,
struct scx_cmask_ref ref, denied_ref;
struct scx_sched *parent, *child;
bool any_denied = false;
LIST_HEAD(to_deliver);
s32 si, ret;
guard(irqsave)();
@ -896,6 +1019,7 @@ __bpf_kfunc s32 scx_bpf_sub_grant(u64 cgroup_id, u64 caps,
SCX_CMASK_DEFINE_SHARD(slice, 0, SCX_CID_SHARD_MAX_CPUS);
struct scx_pshard *pps = parent->pshard[si];
struct scx_pshard *cps = child->pshard[si];
u64 granted_caps = 0;
u32 cap_bit;
scx_cmask_ref_shard(&ref, si, slice);
@ -903,6 +1027,9 @@ __bpf_kfunc s32 scx_bpf_sub_grant(u64 cgroup_id, u64 caps,
continue;
SCX_CMASK_DEFINE_SHARD(granted_cids, slice->base, slice->nr_cids);
SCX_CMASK_DEFINE_SHARD(changed_cids, slice->base, slice->nr_cids);
SCX_CMASK_DEFINE_SHARD(delta, slice->base, slice->nr_cids);
scx_cmask_copy(granted_cids, slice);
scoped_guard (raw_spinlock, &pps->lock) {
@ -915,9 +1042,26 @@ __bpf_kfunc s32 scx_bpf_sub_grant(u64 cgroup_id, u64 caps,
scx_for_each_cap_bit(cap_bit, caps)
scx_cmask_and(granted_cids, &pps->caps[cap_bit].cmask);
/* fold granted_cids into the child per requested cap */
scx_for_each_cap_bit(cap_bit, caps)
scx_cmask_or(&cps->caps[cap_bit].cmask, granted_cids);
/*
* For each requested cap, fold the newly-set cids into
* the child and accumulate the delta.
*/
scx_for_each_cap_bit(cap_bit, caps) {
struct scx_cmask *ccm = &cps->caps[cap_bit].cmask;
scx_cmask_copy(delta, granted_cids);
scx_cmask_andnot(delta, ccm);
if (scx_cmask_empty(delta))
continue;
scx_cmask_or(ccm, delta);
scx_cmask_or(changed_cids, delta);
granted_caps |= BIT_U64(cap_bit);
}
if (granted_caps)
caps_updated_record(cps, changed_cids, granted_caps,
&to_deliver);
}
/* record cids that didn't make it through into @denied_out */
@ -932,6 +1076,9 @@ __bpf_kfunc s32 scx_bpf_sub_grant(u64 cgroup_id, u64 caps,
}
}
}
caps_updated_deliver(&to_deliver);
return any_denied ? -EPERM : 0;
}
@ -953,6 +1100,7 @@ __bpf_kfunc void scx_bpf_sub_revoke(u64 cgroup_id, u64 caps,
{
struct scx_cmask_ref ref;
struct scx_sched *parent, *child, *pos;
LIST_HEAD(to_deliver);
s32 si, ret;
guard(irqsave)();
@ -983,18 +1131,32 @@ __bpf_kfunc void scx_bpf_sub_revoke(u64 cgroup_id, u64 caps,
pos = scx_next_descendant_pre(NULL, child);
while (pos) {
struct scx_pshard *ps = pos->pshard[si];
SCX_CMASK_DEFINE_SHARD(changed_cids, slice->base, slice->nr_cids);
SCX_CMASK_DEFINE_SHARD(delta, slice->base, slice->nr_cids);
u64 revoked_caps = 0;
u32 cap_bit;
scoped_guard (raw_spinlock_nested, &ps->lock) {
/*
* For each cap, clear lost cids and accumulate
* the per-cap diff for notification.
*/
scx_for_each_cap_bit(cap_bit, caps) {
struct scx_cmask *cm = &ps->caps[cap_bit].cmask;
if (!scx_cmask_intersects(cm, slice))
scx_cmask_copy(delta, cm);
scx_cmask_and(delta, slice);
if (scx_cmask_empty(delta))
continue;
scx_cmask_andnot(cm, slice);
scx_cmask_andnot(cm, delta);
scx_cmask_or(changed_cids, delta);
revoked_caps |= BIT_U64(cap_bit);
}
if (revoked_caps)
caps_updated_record(ps, changed_cids, revoked_caps,
&to_deliver);
}
if (revoked_caps)
@ -1003,6 +1165,8 @@ __bpf_kfunc void scx_bpf_sub_revoke(u64 cgroup_id, u64 caps,
pos = scx_skip_subtree_pre(pos, child);
}
}
caps_updated_deliver(&to_deliver);
}
/**

View File

@ -21,6 +21,7 @@ void scx_set_task_sched(struct task_struct *p, struct scx_sched *sch);
struct cgroup *sch_cgroup(struct scx_sched *sch);
void set_cgroup_sched(struct cgroup *cgrp, struct scx_sched *sch);
void scx_pstack_recursion_on_dispatch(struct bpf_prog *prog);
void scx_pstack_recursion_on_caps_updated(struct bpf_prog *prog);
void drain_descendants(struct scx_sched *sch);
void scx_sub_disable(struct scx_sched *sch);
void scx_sub_enable_workfn(struct kthread_work *work);