| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * A scheduler that validates ops.dequeue() is called correctly: | |
| * - Tasks dispatched to terminal DSQs (local, global) bypass the BPF | |
| * scheduler entirely: no ops.dequeue() should be called | |
| * - Tasks dispatched to user DSQs from ops.enqueue() enter BPF custody: | |
| * ops.dequeue() must be called when they leave custody | |
| * - Every ops.enqueue() dispatch to non-terminal DSQs is followed by | |
| * exactly one ops.dequeue() (validate 1:1 pairing and state machine) | |
| * | |
| * Copyright (c) 2026 NVIDIA Corporation. | |
| */ | |
| #include <scx/common.bpf.h> | |
| #define SHARED_DSQ 0 | |
| /* | |
| * BPF internal queue. | |
| * | |
| * Tasks are stored here and consumed from ops.dispatch(), validating that | |
| * tasks on BPF internal structures still get ops.dequeue() when they | |
| * leave. | |
| */ | |
| struct { | |
| __uint(type, BPF_MAP_TYPE_QUEUE); | |
| __uint(max_entries, 32768); | |
| __type(value, s32); | |
| } global_queue SEC(".maps"); | |
| char _license[] SEC("license") = "GPL"; | |
| UEI_DEFINE(uei); | |
| /* | |
| * Counters to track the lifecycle of tasks: | |
| * - enqueue_cnt: Number of times ops.enqueue() was called | |
| * - dequeue_cnt: Number of times ops.dequeue() was called (any type) | |
| * - dispatch_dequeue_cnt: Number of regular dispatch dequeues (no flag) | |
| * - change_dequeue_cnt: Number of property change dequeues | |
| * - bpf_queue_full: Number of times the BPF internal queue was full | |
| */ | |
| u64 enqueue_cnt, dequeue_cnt, dispatch_dequeue_cnt, change_dequeue_cnt, bpf_queue_full; | |
| /* | |
| * Test scenarios: | |
| * 0) Dispatch to local DSQ from ops.select_cpu() (terminal DSQ, bypasses BPF | |
| * scheduler, no dequeue callbacks) | |
| * 1) Dispatch to global DSQ from ops.select_cpu() (terminal DSQ, bypasses BPF | |
| * scheduler, no dequeue callbacks) | |
| * 2) Dispatch to shared user DSQ from ops.select_cpu() (enters BPF scheduler, | |
| * dequeue callbacks expected) | |
| * 3) Dispatch to local DSQ from ops.enqueue() (terminal DSQ, bypasses BPF | |
| * scheduler, no dequeue callbacks) | |
| * 4) Dispatch to global DSQ from ops.enqueue() (terminal DSQ, bypasses BPF | |
| * scheduler, no dequeue callbacks) | |
| * 5) Dispatch to shared user DSQ from ops.enqueue() (enters BPF scheduler, | |
| * dequeue callbacks expected) | |
| * 6) BPF internal queue from ops.enqueue(): store task PIDs in ops.enqueue(), | |
| * consume in ops.dispatch() and dispatch to local DSQ (validates dequeue | |
| * for tasks stored in internal BPF data structures) | |
| */ | |
| u32 test_scenario; | |
| /* | |
| * Per-task state to track lifecycle and validate workflow semantics. | |
| * State transitions: | |
| * NONE -> ENQUEUED (on enqueue) | |
| * NONE -> DISPATCHED (on direct dispatch to terminal DSQ) | |
| * ENQUEUED -> DISPATCHED (on dispatch dequeue) | |
| * DISPATCHED -> NONE (on property change dequeue or re-enqueue) | |
| * ENQUEUED -> NONE (on property change dequeue before dispatch) | |
| */ | |
| enum task_state { | |
| TASK_NONE = 0, | |
| TASK_ENQUEUED, | |
| TASK_DISPATCHED, | |
| }; | |
| struct task_ctx { | |
| enum task_state state; /* Current state in the workflow */ | |
| u64 enqueue_seq; /* Sequence number for debugging */ | |
| }; | |
| struct { | |
| __uint(type, BPF_MAP_TYPE_TASK_STORAGE); | |
| __uint(map_flags, BPF_F_NO_PREALLOC); | |
| __type(key, int); | |
| __type(value, struct task_ctx); | |
| } task_ctx_stor SEC(".maps"); | |
| static struct task_ctx *try_lookup_task_ctx(struct task_struct *p) | |
| { | |
| return bpf_task_storage_get(&task_ctx_stor, p, 0, 0); | |
| } | |
| s32 BPF_STRUCT_OPS(dequeue_select_cpu, struct task_struct *p, | |
| s32 prev_cpu, u64 wake_flags) | |
| { | |
| struct task_ctx *tctx; | |
| tctx = try_lookup_task_ctx(p); | |
| if (!tctx) | |
| return prev_cpu; | |
| switch (test_scenario) { | |
| case 0: | |
| /* | |
| * Direct dispatch to the local DSQ. | |
| * | |
| * Task bypasses BPF scheduler entirely: no enqueue | |
| * tracking, no ops.dequeue() callbacks. | |
| */ | |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0); | |
| tctx->state = TASK_DISPATCHED; | |
| break; | |
| case 1: | |
| /* | |
| * Direct dispatch to the global DSQ. | |
| * | |
| * Task bypasses BPF scheduler entirely: no enqueue | |
| * tracking, no ops.dequeue() callbacks. | |
| */ | |
| scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0); | |
| tctx->state = TASK_DISPATCHED; | |
| break; | |
| case 2: | |
| /* | |
| * Dispatch to a shared user DSQ. | |
| * | |
| * Task enters BPF scheduler management: track | |
| * enqueue/dequeue lifecycle and validate state | |
| * transitions. | |
| */ | |
| if (tctx->state == TASK_ENQUEUED) | |
| scx_bpf_error("%d (%s): enqueue while in ENQUEUED state seq=%llu", | |
| p->pid, p->comm, tctx->enqueue_seq); | |
| scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, 0); | |
| __sync_fetch_and_add(&enqueue_cnt, 1); | |
| tctx->state = TASK_ENQUEUED; | |
| tctx->enqueue_seq++; | |
| break; | |
| } | |
| return prev_cpu; | |
| } | |
| void BPF_STRUCT_OPS(dequeue_enqueue, struct task_struct *p, u64 enq_flags) | |
| { | |
| struct task_ctx *tctx; | |
| s32 pid = p->pid; | |
| tctx = try_lookup_task_ctx(p); | |
| if (!tctx) | |
| return; | |
| switch (test_scenario) { | |
| case 3: | |
| /* | |
| * Direct dispatch to the local DSQ. | |
| * | |
| * Task bypasses BPF scheduler entirely: no enqueue | |
| * tracking, no ops.dequeue() callbacks. | |
| */ | |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, enq_flags); | |
| tctx->state = TASK_DISPATCHED; | |
| break; | |
| case 4: | |
| /* | |
| * Direct dispatch to the global DSQ. | |
| * | |
| * Task bypasses BPF scheduler entirely: no enqueue | |
| * tracking, no ops.dequeue() callbacks. | |
| */ | |
| scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags); | |
| tctx->state = TASK_DISPATCHED; | |
| break; | |
| case 5: | |
| /* | |
| * Dispatch to shared user DSQ. | |
| * | |
| * Task enters BPF scheduler management: track | |
| * enqueue/dequeue lifecycle and validate state | |
| * transitions. | |
| */ | |
| if (tctx->state == TASK_ENQUEUED) | |
| scx_bpf_error("%d (%s): enqueue while in ENQUEUED state seq=%llu", | |
| p->pid, p->comm, tctx->enqueue_seq); | |
| scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, enq_flags); | |
| __sync_fetch_and_add(&enqueue_cnt, 1); | |
| tctx->state = TASK_ENQUEUED; | |
| tctx->enqueue_seq++; | |
| break; | |
| case 6: | |
| /* | |
| * Store task in BPF internal queue. | |
| * | |
| * Task enters BPF scheduler management: track | |
| * enqueue/dequeue lifecycle and validate state | |
| * transitions. | |
| */ | |
| if (tctx->state == TASK_ENQUEUED) | |
| scx_bpf_error("%d (%s): enqueue while in ENQUEUED state seq=%llu", | |
| p->pid, p->comm, tctx->enqueue_seq); | |
| if (bpf_map_push_elem(&global_queue, &pid, 0)) { | |
| scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags); | |
| __sync_fetch_and_add(&bpf_queue_full, 1); | |
| tctx->state = TASK_DISPATCHED; | |
| } else { | |
| __sync_fetch_and_add(&enqueue_cnt, 1); | |
| tctx->state = TASK_ENQUEUED; | |
| tctx->enqueue_seq++; | |
| } | |
| break; | |
| default: | |
| /* For all other scenarios, dispatch to the global DSQ */ | |
| scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags); | |
| tctx->state = TASK_DISPATCHED; | |
| break; | |
| } | |
| scx_bpf_kick_cpu(scx_bpf_task_cpu(p), SCX_KICK_IDLE); | |
| } | |
| void BPF_STRUCT_OPS(dequeue_dequeue, struct task_struct *p, u64 deq_flags) | |
| { | |
| struct task_ctx *tctx; | |
| __sync_fetch_and_add(&dequeue_cnt, 1); | |
| tctx = try_lookup_task_ctx(p); | |
| if (!tctx) | |
| return; | |
| /* | |
| * For scenarios 0, 1, 3, and 4 (terminal DSQs: local and global), | |
| * ops.dequeue() should never be called because tasks bypass the | |
| * BPF scheduler entirely. If we get here, it's a kernel bug. | |
| */ | |
| if (test_scenario == 0 || test_scenario == 3) { | |
| scx_bpf_error("%d (%s): dequeue called for local DSQ scenario", | |
| p->pid, p->comm); | |
| return; | |
| } | |
| if (test_scenario == 1 || test_scenario == 4) { | |
| scx_bpf_error("%d (%s): dequeue called for global DSQ scenario", | |
| p->pid, p->comm); | |
| return; | |
| } | |
| if (deq_flags & SCX_DEQ_SCHED_CHANGE) { | |
| /* | |
| * Property change interrupting the workflow. Valid from | |
| * both ENQUEUED and DISPATCHED states. Transitions task | |
| * back to NONE state. | |
| */ | |
| __sync_fetch_and_add(&change_dequeue_cnt, 1); | |
| /* Validate state transition */ | |
| if (tctx->state != TASK_ENQUEUED && tctx->state != TASK_DISPATCHED) | |
| scx_bpf_error("%d (%s): invalid property change dequeue state=%d seq=%llu", | |
| p->pid, p->comm, tctx->state, tctx->enqueue_seq); | |
| /* | |
| * Transition back to NONE: task outside scheduler control. | |
| * | |
| * Scenario 6: dispatch() checks tctx->state after popping a | |
| * PID, if the task is in state NONE, it was dequeued by | |
| * property change and must not be dispatched (this | |
| * prevents "target CPU not allowed"). | |
| */ | |
| tctx->state = TASK_NONE; | |
| } else { | |
| /* | |
| * Regular dispatch dequeue: kernel is moving the task from | |
| * BPF custody to a terminal DSQ. Normally we come from | |
| * ENQUEUED state. We can also see TASK_NONE if the task | |
| * was dequeued by property change (SCX_DEQ_SCHED_CHANGE) | |
| * while it was already on a DSQ (dispatched but not yet | |
| * consumed); in that case we just leave state as NONE. | |
| */ | |
| __sync_fetch_and_add(&dispatch_dequeue_cnt, 1); | |
| /* | |
| * Must be ENQUEUED (normal path) or NONE (already dequeued | |
| * by property change while on a DSQ). | |
| */ | |
| if (tctx->state != TASK_ENQUEUED && tctx->state != TASK_NONE) | |
| scx_bpf_error("%d (%s): dispatch dequeue from state %d seq=%llu", | |
| p->pid, p->comm, tctx->state, tctx->enqueue_seq); | |
| if (tctx->state == TASK_ENQUEUED) | |
| tctx->state = TASK_DISPATCHED; | |
| /* NONE: leave as-is, task was already property-change dequeued */ | |
| } | |
| } | |
| void BPF_STRUCT_OPS(dequeue_dispatch, s32 cpu, struct task_struct *prev) | |
| { | |
| if (test_scenario == 6) { | |
| struct task_ctx *tctx; | |
| struct task_struct *p; | |
| s32 pid; | |
| if (bpf_map_pop_elem(&global_queue, &pid)) | |
| return; | |
| p = bpf_task_from_pid(pid); | |
| if (!p) | |
| return; | |
| /* | |
| * If the task was dequeued by property change | |
| * (ops.dequeue() set tctx->state = TASK_NONE), skip | |
| * dispatch. | |
| */ | |
| tctx = try_lookup_task_ctx(p); | |
| if (!tctx || tctx->state == TASK_NONE) { | |
| bpf_task_release(p); | |
| return; | |
| } | |
| /* | |
| * Dispatch to this CPU's local DSQ if allowed, otherwise | |
| * fallback to the global DSQ. | |
| */ | |
| if (bpf_cpumask_test_cpu(cpu, p->cpus_ptr)) | |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cpu, SCX_SLICE_DFL, 0); | |
| else | |
| scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0); | |
| bpf_task_release(p); | |
| } else { | |
| scx_bpf_dsq_move_to_local(SHARED_DSQ, 0); | |
| } | |
| } | |
| s32 BPF_STRUCT_OPS(dequeue_init_task, struct task_struct *p, | |
| struct scx_init_task_args *args) | |
| { | |
| struct task_ctx *tctx; | |
| tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, | |
| BPF_LOCAL_STORAGE_GET_F_CREATE); | |
| if (!tctx) | |
| return -ENOMEM; | |
| return 0; | |
| } | |
| s32 BPF_STRUCT_OPS_SLEEPABLE(dequeue_init) | |
| { | |
| s32 ret; | |
| ret = scx_bpf_create_dsq(SHARED_DSQ, -1); | |
| if (ret) | |
| return ret; | |
| return 0; | |
| } | |
| void BPF_STRUCT_OPS(dequeue_exit, struct scx_exit_info *ei) | |
| { | |
| UEI_RECORD(uei, ei); | |
| } | |
| SEC(".struct_ops.link") | |
| struct sched_ext_ops dequeue_ops = { | |
| .select_cpu = (void *)dequeue_select_cpu, | |
| .enqueue = (void *)dequeue_enqueue, | |
| .dequeue = (void *)dequeue_dequeue, | |
| .dispatch = (void *)dequeue_dispatch, | |
| .init_task = (void *)dequeue_init_task, | |
| .init = (void *)dequeue_init, | |
| .exit = (void *)dequeue_exit, | |
| .flags = SCX_OPS_ENQ_LAST, | |
| .name = "dequeue_test", | |
| }; |