| /* SPDX-License-Identifier: GPL-2.0 */ |
| /* |
| * scx_qmap: a demonstration and testing scheduler for sched_ext features. |
| * |
| * A simple scheduler that exercises a broad set of sched_ext features. Unlikely |
| * to be useful for real workloads. It demonstrates: |
| * |
| * - BPF-side queueing using TIDs. |
| * - BPF arena for scheduler state. |
| * - Core-sched support. |
| * - Hierarchical sub-scheduling: delegating cpus to child cgroup schedulers. |
| * |
| * Base design: Five FIFOs (arena-backed doubly-linked lists through per-task |
| * context). A task is assigned to a FIFO by its compound weight. Each cpu |
| * round-robins the FIFOs, dispatching more from higher ones. |
| * |
| * Sub-scheduling: Any qmap sched can delegate cpus to its own child cgroup |
| * schedulers and keep the rest for its tasks. Terminology: |
| * |
| * excl - A cpu the delegatee owns wholly (ENQ_IMMED|ENQ|PREEMPT). |
| * shared - A cpu delegated as ENQ_IMMED only. Time-shared. |
| * held_excl / held_shared - What this node was handed by its parent. |
| * held-excl cpus are re-delegatable. A held-shared cpu is a |
| * time-share that stays self-local. |
| * self - The excl cpus the node kept for itself, plus all of held_shared. |
| * owner - Who holds a cid - a child slot, CID_SELF, or CID_NONE. |
| * avail - Cpus whose caps are in effect, per ops.sub_ecaps_updated(). |
| * usable - self AND avail. Placement decisions use this: self is the |
| * delegation split and can run ahead of what the cpus honor. |
| * |
| * The scheduler splits its held-excl cpus among self and the children in |
| * proportion to each node's cpu.weight, handing each the floor of its share as |
| * excl cpus. The leftover from rounding forms a shared pool the round-robin |
| * timer hands around. With no excl cpu to delegate, the node evicts its |
| * children. |
| * |
| * This policy is a demonstration only, not a practical one. The split |
| * considers only direct children and is not work-conserving. It only exists to |
| * drive sub-sched primitives with as simple logic as possible. |
| * |
| * Copyright (c) 2022 Meta Platforms, Inc. and affiliates. |
| * Copyright (c) 2022 Tejun Heo <tj@kernel.org> |
| * Copyright (c) 2022 David Vernet <dvernet@meta.com> |
| */ |
| #include <scx/common.bpf.h> |
| |
| #include "scx_qmap.h" |
| |
| enum consts { |
| ONE_SEC_IN_NS = 1000000000, |
| ONE_MSEC_IN_NS = 1000000, |
| LOWPRI_INTV_NS = 10 * ONE_MSEC_IN_NS, |
| SHARED_DSQ = 0, |
| HIGHPRI_DSQ = 1, |
| LOWPRI_DSQ = 2, |
| HIGHPRI_WEIGHT = 8668, /* this is what -20 maps to */ |
| }; |
| |
| char _license[] SEC("license") = "GPL"; |
| |
| const volatile u64 slice_ns; |
| const volatile u32 stall_user_nth; |
| const volatile u32 stall_kernel_nth; |
| const volatile u32 dsp_inf_loop_after; |
| const volatile u32 dsp_batch; |
| const volatile bool highpri_boosting; |
| const volatile bool print_dsqs_and_events; |
| const volatile bool print_msgs; |
| const volatile u64 sub_cgroup_id; |
| const volatile s32 disallow_tgid; |
| const volatile bool suppress_dump; |
| const volatile u32 immed_stress_nth; |
| const volatile u32 max_tasks; |
| |
| /* sub-sched: period for handing the round-robin cid pool to the next child */ |
| const volatile u64 round_robin_ns; |
| |
| /* |
| * Optional cid-override test harness. When cid_override_mode is non-zero, |
| * qmap_init_cids() calls scx_bpf_cid_override() with the caller-supplied arrays |
| * to exercise the kfunc's acceptance and error paths. See enum |
| * qmap_cid_override for the modes. |
| */ |
| const volatile u32 cid_override_mode; |
| const volatile u32 cid_override_nr_shards; |
| |
| UEI_DEFINE(uei); |
| |
| /* |
| * All scheduler state - per-cpu context, stats counters, core-sched sequence |
| * numbers, sub-sched cgroup ids - lives in this single BPF arena map. Userspace |
| * reaches it via skel->arena->qa. |
| */ |
| struct { |
| __uint(type, BPF_MAP_TYPE_ARENA); |
| __uint(map_flags, BPF_F_MMAPABLE); |
| __uint(max_entries, 1 << 16); /* upper bound in pages */ |
| #if defined(__TARGET_ARCH_arm64) || defined(__aarch64__) |
| __ulong(map_extra, 0x1ull << 32); /* user/BPF mmap base */ |
| #else |
| __ulong(map_extra, 0x1ull << 44); |
| #endif |
| } arena SEC(".maps"); |
| |
| struct qmap_arena __arena_global qa; |
| |
| /* ensure that BPF and userspace are seeing the same size for qmap_cmask */ |
| _Static_assert(QMAP_CMASK_WORDS == CMASK_NR_WORDS(SCX_QMAP_MAX_CPUS), |
| "QMAP_CMASK_WORDS must equal CMASK_NR_WORDS(SCX_QMAP_MAX_CPUS)"); |
| _Static_assert(sizeof(struct qmap_cmask) == |
| struct_size_t(struct scx_cmask, bits, QMAP_CMASK_WORDS), |
| "qmap_cmask must be exactly sized to back a full scx_cmask"); |
| |
| /* Per-queue locks. Each in its own .data section as bpf_res_spin_lock requires. */ |
| __hidden struct bpf_res_spin_lock qa_q_lock0 SEC(".data.qa_q_lock0"); |
| __hidden struct bpf_res_spin_lock qa_q_lock1 SEC(".data.qa_q_lock1"); |
| __hidden struct bpf_res_spin_lock qa_q_lock2 SEC(".data.qa_q_lock2"); |
| __hidden struct bpf_res_spin_lock qa_q_lock3 SEC(".data.qa_q_lock3"); |
| __hidden struct bpf_res_spin_lock qa_q_lock4 SEC(".data.qa_q_lock4"); |
| |
| static struct bpf_res_spin_lock *qa_q_lock(s32 qid) |
| { |
| switch (qid) { |
| case 0: return &qa_q_lock0; |
| case 1: return &qa_q_lock1; |
| case 2: return &qa_q_lock2; |
| case 3: return &qa_q_lock3; |
| case 4: return &qa_q_lock4; |
| default: return NULL; |
| } |
| } |
| |
| /* |
| * If enabled, CPU performance target is set according to the queue index |
| * according to the following table. |
| */ |
| static const u32 qidx_to_cpuperf_target[] = { |
| [0] = SCX_CPUPERF_ONE * 0 / 4, |
| [1] = SCX_CPUPERF_ONE * 1 / 4, |
| [2] = SCX_CPUPERF_ONE * 2 / 4, |
| [3] = SCX_CPUPERF_ONE * 3 / 4, |
| [4] = SCX_CPUPERF_ONE * 4 / 4, |
| }; |
| |
| /* |
| * Per-queue sequence numbers to implement core-sched ordering. |
| * |
| * Tail seq is assigned to each queued task and incremented. Head seq tracks the |
| * sequence number of the latest dispatched task. The distance between the a |
| * task's seq and the associated queue's head seq is called the queue distance |
| * and used when comparing two tasks for ordering. See qmap_core_sched_before(). |
| */ |
| |
| /* |
| * Per-task scheduling context. Allocated from the qa.task_ctxs[] slab in |
| * arena. While the task is alive the entry is referenced from task_ctx_stor; |
| * while it's free the entry sits on the free list singly-linked through |
| * @next_free. |
| * |
| * When the task is queued on one of the five priority FIFOs, @q_idx is the |
| * queue index and @q_next/@q_prev link it in the queue's doubly-linked list. |
| * @q_idx is -1 when the task isn't on any queue. |
| */ |
| struct task_ctx { |
| struct task_ctx __arena *next_free; /* only valid on free list */ |
| struct task_ctx __arena *q_next; /* queue link, NULL if tail */ |
| struct task_ctx __arena *q_prev; /* queue link, NULL if head */ |
| struct qmap_fifo __arena *fifo; /* queue we're on, NULL if not queued */ |
| u64 tid; |
| s32 pid; /* for dump only */ |
| bool force_local; /* Dispatch directly to local_dsq */ |
| bool highpri; |
| u64 core_sched_seq; |
| struct scx_cmask cpus_allowed; /* per-task affinity in cid space */ |
| }; |
| |
| /* |
| * Slab stride for task_ctx. cpus_allowed's flex array bits[] overlaps the |
| * tail bytes appended per entry; struct_size() gives the actual per-entry |
| * footprint. |
| */ |
| #define TASK_CTX_STRIDE \ |
| struct_size_t(struct task_ctx, cpus_allowed.bits, \ |
| CMASK_NR_WORDS(SCX_QMAP_MAX_CPUS)) |
| |
| /* All task_ctx pointers are arena pointers. */ |
| typedef struct task_ctx __arena task_ctx_t; |
| |
| /* Holds an arena pointer to the task's slab entry. */ |
| struct task_ctx_stor_val { |
| task_ctx_t *taskc; |
| }; |
| |
| struct { |
| __uint(type, BPF_MAP_TYPE_TASK_STORAGE); |
| __uint(map_flags, BPF_F_NO_PREALLOC); |
| __type(key, int); |
| __type(value, struct task_ctx_stor_val); |
| } task_ctx_stor SEC(".maps"); |
| |
| /* Protects the task_ctx slab free list. */ |
| __hidden struct bpf_res_spin_lock qa_task_lock SEC(".data.qa_task_lock"); |
| |
| static int qmap_spin_lock(struct bpf_res_spin_lock *lock) |
| { |
| if (bpf_res_spin_lock(lock)) { |
| scx_bpf_error("res_spin_lock failed"); |
| return -EBUSY; |
| } |
| return 0; |
| } |
| |
| /* |
| * Try prev_cid, then scan cpus_allowed AND idle_cids AND usable_cids |
| * round-robin from prev_cid + 1. Atomic claim retries on race; bounded by |
| * IDLE_PICK_RETRIES to keep the verifier's insn budget in check. |
| */ |
| #define IDLE_PICK_RETRIES 16 |
| |
| static s32 pick_direct_dispatch_cid(struct task_struct *p, s32 prev_cid, |
| task_ctx_t *taskc) |
| { |
| u32 nr_cids = scx_bpf_nr_cids(); |
| s32 cid; |
| u32 i; |
| |
| if (cmask_test(prev_cid, &qa.usable_cids.mask) && |
| cmask_test_and_clear(prev_cid, &qa.idle_cids.mask)) |
| return prev_cid; |
| |
| cid = prev_cid; |
| bpf_for(i, 0, IDLE_PICK_RETRIES) { |
| cid = cmask_next_and2_set_wrap(&taskc->cpus_allowed, |
| &qa.idle_cids.mask, |
| &qa.usable_cids.mask, cid + 1); |
| barrier_var(cid); |
| if (cid >= nr_cids) |
| return -1; |
| if (cmask_test_and_clear(cid, &qa.idle_cids.mask)) |
| return cid; |
| } |
| return -1; |
| } |
| |
| /* |
| * Force a reference to the arena map. The verifier associates an arena with |
| * a program by finding an LD_IMM64 instruction that loads the arena's BPF |
| * map; programs that only use arena pointers returned from task-local |
| * storage (like qmap_select_cpu) never reference @arena directly. Without |
| * this, the verifier rejects addr_space_cast with "addr_space_cast insn |
| * can only be used in a program that has an associated arena". |
| */ |
| #define QMAP_TOUCH_ARENA() do { asm volatile("" :: "r"(&arena)); } while (0) |
| |
| static task_ctx_t *lookup_task_ctx(struct task_struct *p) |
| { |
| struct task_ctx_stor_val *v; |
| |
| QMAP_TOUCH_ARENA(); |
| |
| v = bpf_task_storage_get(&task_ctx_stor, p, 0, 0); |
| if (!v || !v->taskc) |
| return NULL; |
| return v->taskc; |
| } |
| |
| /* Append @taskc to the tail of @fifo. Must not already be queued. */ |
| static void qmap_fifo_enqueue(struct qmap_fifo __arena *fifo, task_ctx_t *taskc) |
| { |
| struct bpf_res_spin_lock *lock = qa_q_lock(fifo->idx); |
| |
| if (!lock || qmap_spin_lock(lock)) |
| return; |
| taskc->fifo = fifo; |
| taskc->q_next = NULL; |
| taskc->q_prev = fifo->tail; |
| if (fifo->tail) |
| fifo->tail->q_next = taskc; |
| else |
| fifo->head = taskc; |
| fifo->tail = taskc; |
| bpf_res_spin_unlock(lock); |
| } |
| |
| /* Pop the head of @fifo. Returns NULL if empty. */ |
| static task_ctx_t *qmap_fifo_pop(struct qmap_fifo __arena *fifo) |
| { |
| struct bpf_res_spin_lock *lock = qa_q_lock(fifo->idx); |
| task_ctx_t *taskc; |
| |
| if (!lock || qmap_spin_lock(lock)) |
| return NULL; |
| taskc = fifo->head; |
| if (taskc) { |
| fifo->head = taskc->q_next; |
| if (taskc->q_next) |
| taskc->q_next->q_prev = NULL; |
| else |
| fifo->tail = NULL; |
| taskc->q_next = NULL; |
| taskc->q_prev = NULL; |
| taskc->fifo = NULL; |
| } |
| bpf_res_spin_unlock(lock); |
| return taskc; |
| } |
| |
| /* Remove @taskc from its fifo. No-op if not queued. */ |
| static void qmap_fifo_remove(task_ctx_t *taskc) |
| { |
| struct qmap_fifo __arena *fifo = taskc->fifo; |
| struct bpf_res_spin_lock *lock; |
| |
| if (!fifo) |
| return; |
| |
| lock = qa_q_lock(fifo->idx); |
| if (!lock || qmap_spin_lock(lock)) |
| return; |
| |
| /* Re-check under lock — a concurrent pop may have cleared fifo. */ |
| if (taskc->fifo != fifo) { |
| bpf_res_spin_unlock(lock); |
| return; |
| } |
| |
| if (taskc->q_next) |
| taskc->q_next->q_prev = taskc->q_prev; |
| else |
| fifo->tail = taskc->q_prev; |
| if (taskc->q_prev) |
| taskc->q_prev->q_next = taskc->q_next; |
| else |
| fifo->head = taskc->q_next; |
| taskc->q_next = NULL; |
| taskc->q_prev = NULL; |
| taskc->fifo = NULL; |
| bpf_res_spin_unlock(lock); |
| } |
| |
| s32 BPF_STRUCT_OPS(qmap_select_cid, struct task_struct *p, |
| s32 prev_cid, u64 wake_flags) |
| { |
| task_ctx_t *taskc; |
| s32 cid; |
| |
| if (!(taskc = lookup_task_ctx(p))) |
| return prev_cid; |
| |
| if (p->scx.weight < 2 && !(p->flags & PF_KTHREAD)) |
| return prev_cid; |
| |
| cid = pick_direct_dispatch_cid(p, prev_cid, taskc); |
| |
| if (cid >= 0) { |
| taskc->force_local = true; |
| return cid; |
| } else { |
| return prev_cid; |
| } |
| } |
| |
| /* |
| * A received time-shared cid is held ENQ_IMMED-only, so inserts meant to run |
| * there must set SCX_ENQ_IMMED. |
| */ |
| static u64 needs_immed(s32 cid) |
| { |
| return qa.cid_shared[cid] ? SCX_ENQ_IMMED : 0; |
| } |
| |
| /* first cid this node does NOT hold for fault injection, -1 if none */ |
| static s32 first_unavail_cid(void) |
| { |
| s32 nr_cids = qa.nr_cids, c; |
| |
| if (nr_cids > SCX_QMAP_MAX_CPUS) { |
| scx_bpf_error("-ERANGE"); |
| return -1; |
| } |
| |
| bpf_for(c, 0, nr_cids) { |
| if (!cmask_test(c, &qa.held_excl.mask) && |
| !cmask_test(c, &qa.held_shared.mask)) |
| return c; |
| } |
| return -1; |
| } |
| |
| static int weight_to_idx(u32 weight) |
| { |
| /* Coarsely map the compound weight to a FIFO. */ |
| if (weight <= 25) |
| return 0; |
| else if (weight <= 50) |
| return 1; |
| else if (weight < 200) |
| return 2; |
| else if (weight < 400) |
| return 3; |
| else |
| return 4; |
| } |
| |
| void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags) |
| { |
| static u32 user_cnt, kernel_cnt; |
| task_ctx_t *taskc; |
| int idx = weight_to_idx(p->scx.weight); |
| s32 cid; |
| |
| if (enq_flags & SCX_ENQ_REENQ) { |
| u64 reason = p->scx.flags & SCX_TASK_REENQ_REASON_MASK; |
| |
| __sync_fetch_and_add(&qa.nr_reenqueued, 1); |
| if (scx_bpf_task_cid(p) == 0) |
| __sync_fetch_and_add(&qa.nr_reenqueued_cid0, 1); |
| /* cap-loss and IMMED-handback bounces, relocated below */ |
| if (reason == SCX_TASK_REENQ_CAP) |
| __sync_fetch_and_add(&qa.nr_reenq_cap, 1); |
| else if (reason == SCX_TASK_REENQ_IMMED) |
| __sync_fetch_and_add(&qa.nr_reenq_immed, 1); |
| } |
| |
| if (p->flags & PF_KTHREAD) { |
| if (stall_kernel_nth && !(++kernel_cnt % stall_kernel_nth)) |
| return; |
| } else { |
| if (stall_user_nth && !(++user_cnt % stall_user_nth)) |
| return; |
| } |
| |
| if (qa.test_error_cnt && !--qa.test_error_cnt) |
| scx_bpf_error("test triggering error"); |
| |
| if (!(taskc = lookup_task_ctx(p))) |
| return; |
| |
| /* |
| * All enqueued tasks must have their core_sched_seq updated for correct |
| * core-sched ordering. Also, take a look at the end of qmap_dispatch(). |
| */ |
| taskc->core_sched_seq = qa.core_sched_tail_seqs[idx]++; |
| |
| /* |
| * A task of ours that can run on none of our self cids - the parent |
| * didn't grant them or we delegated them to children - would starve in |
| * SHARED/FIFO since we only pull from those on self cids. |
| * |
| * Force it onto its first allowed cid's local DSQ with SCX_ENQ_RESCUE. |
| * If we hold ENQ on that cid it runs. Otherwise the kernel diverts the |
| * task to its rescue path. IMMED would turn the insert into a legal |
| * placement on a time-shared cid and the kernel would bounce it back |
| * here instead of rescuing it. |
| */ |
| if (!cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) { |
| s32 c = cmask_next_set_wrap(&taskc->cpus_allowed, 0); |
| |
| if (c >= 0 && c < scx_bpf_nr_cids()) { |
| taskc->force_local = false; |
| __sync_fetch_and_add(&qa.nr_rescue_dsp, 1); |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | c, slice_ns, |
| enq_flags | SCX_ENQ_RESCUE); |
| return; |
| } |
| } |
| |
| /* |
| * Fault injection: deliberately dispatch one of our own tasks to a cid |
| * we don't hold. The inserts carry SCX_ENQ_RESCUE and divert to the |
| * kernel rescue path, a deterministic rescue-traffic generator. Under |
| * -B 0 the kernel cap check rejects and re-enqueues them instead, so |
| * nr_inject_attempts tracks nr_reenq_cap 1:1 and proves delivery-time |
| * enforcement. Throttled. |
| */ |
| if (qa.inject_mode == QMAP_INJ_WRONG_CID && p->nr_cpus_allowed > 1 && |
| !(enq_flags & SCX_ENQ_REENQ)) { |
| static u32 inj_cnt; |
| |
| if (!(++inj_cnt % 64)) { |
| s32 bad = first_unavail_cid(); |
| |
| if (bad >= 0 && cmask_test(bad, &taskc->cpus_allowed)) { |
| __sync_fetch_and_add(&qa.nr_inject_attempts, 1); |
| __sync_fetch_and_add(&qa.nr_rescue_dsp, 1); |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | bad, slice_ns, |
| enq_flags | SCX_ENQ_RESCUE); |
| return; |
| } |
| } |
| } |
| |
| /* |
| * IMMED stress testing: Every immed_stress_nth'th enqueue, dispatch |
| * directly to prev_cpu's local DSQ even when busy to force dsq->nr > 1 |
| * and exercise the kernel IMMED reenqueue trigger paths. |
| */ |
| if (immed_stress_nth && !(enq_flags & SCX_ENQ_REENQ)) { |
| static u32 immed_stress_cnt; |
| |
| if (!(++immed_stress_cnt % immed_stress_nth)) { |
| taskc->force_local = false; |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | scx_bpf_task_cid(p), |
| slice_ns, enq_flags); |
| return; |
| } |
| } |
| |
| /* |
| * If qmap_select_cid() is telling us to or this is the last runnable |
| * task on the CPU, enqueue locally. |
| */ |
| if (taskc->force_local) { |
| taskc->force_local = false; |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, slice_ns, |
| enq_flags | needs_immed(scx_bpf_task_cid(p))); |
| return; |
| } |
| |
| /* see lowpri_timerfn() */ |
| if (__COMPAT_has_generic_reenq() && |
| p->scx.weight < 2 && !(p->flags & PF_KTHREAD) && !(enq_flags & SCX_ENQ_REENQ)) { |
| scx_bpf_dsq_insert(p, LOWPRI_DSQ, slice_ns, enq_flags); |
| return; |
| } |
| |
| /* if select_cid() wasn't called, try direct dispatch */ |
| if (!__COMPAT_is_enq_cpu_selected(enq_flags) && |
| (cid = pick_direct_dispatch_cid(p, scx_bpf_task_cid(p), taskc)) >= 0) { |
| __sync_fetch_and_add(&qa.nr_ddsp_from_enq, 1); |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cid, slice_ns, |
| enq_flags | needs_immed(cid)); |
| return; |
| } |
| |
| /* |
| * If the task was re-enqueued due to the CPU being preempted by a |
| * higher priority scheduling class, just re-enqueue the task directly |
| * on the global DSQ. As we want another CPU to pick it up, find and |
| * kick an idle cid. |
| */ |
| if (enq_flags & SCX_ENQ_REENQ) { |
| s32 cid; |
| |
| scx_bpf_dsq_insert(p, SHARED_DSQ, 0, enq_flags); |
| cid = cmask_next_and2_set_wrap(&taskc->cpus_allowed, |
| &qa.idle_cids.mask, |
| &qa.usable_cids.mask, 0); |
| if (cid < scx_bpf_nr_cids()) |
| scx_bpf_kick_cid(cid, SCX_KICK_IDLE); |
| return; |
| } |
| |
| /* Queue on the selected FIFO. */ |
| qmap_fifo_enqueue(&qa.fifos[idx], taskc); |
| |
| if (highpri_boosting && p->scx.weight >= HIGHPRI_WEIGHT) { |
| taskc->highpri = true; |
| __sync_fetch_and_add(&qa.nr_highpri_queued, 1); |
| } |
| __sync_fetch_and_add(&qa.nr_enqueued, 1); |
| } |
| |
| void BPF_STRUCT_OPS(qmap_dequeue, struct task_struct *p, u64 deq_flags) |
| { |
| task_ctx_t *taskc; |
| |
| __sync_fetch_and_add(&qa.nr_dequeued, 1); |
| if (deq_flags & SCX_DEQ_CORE_SCHED_EXEC) |
| __sync_fetch_and_add(&qa.nr_core_sched_execed, 1); |
| |
| taskc = lookup_task_ctx(p); |
| if (taskc && taskc->fifo) { |
| if (taskc->highpri) |
| __sync_fetch_and_sub(&qa.nr_highpri_queued, 1); |
| qmap_fifo_remove(taskc); |
| } |
| } |
| |
| static void update_core_sched_head_seq(struct task_struct *p) |
| { |
| int idx = weight_to_idx(p->scx.weight); |
| task_ctx_t *taskc; |
| |
| if ((taskc = lookup_task_ctx(p))) |
| qa.core_sched_head_seqs[idx] = taskc->core_sched_seq; |
| } |
| |
| /* |
| * One pass over SHARED_DSQ: rescue stranded tasks and boost highpri ones. A |
| * task whose cids were lost while it was queued in the fifos would strand on |
| * SHARED_DSQ, which is consumed only on self cids it can't run on - move it to |
| * the kernel rescue path. One whose cids were lost after the highpri cull is |
| * likewise rescued out of HIGHPRI_DSQ below. |
| * |
| * To demonstrate the use of scx_bpf_dsq_move(), implement silly selective |
| * priority boosting mechanism by moving highpri tasks to HIGHPRI_DSQ and then |
| * consuming them first. This makes minor difference only when dsp_batch is |
| * larger than 1. |
| * |
| * scx_bpf_dsq_move[_vtime]() are allowed both from ops.dispatch() and |
| * non-rq-lock holding BPF programs. As demonstration, this function is called |
| * from qmap_dispatch() and monitor_timerfn(). |
| */ |
| static bool scan_shared_dsq(bool from_timer) |
| { |
| struct task_struct *p; |
| s32 this_cid = scx_bpf_this_cid(); |
| u32 nr_cids = scx_bpf_nr_cids(); |
| |
| /* rescue strands and move highpri tasks to HIGHPRI_DSQ */ |
| bpf_for_each(scx_dsq, p, SHARED_DSQ, 0) { |
| static u64 highpri_seq; |
| task_ctx_t *taskc; |
| s32 c; |
| |
| if (!(taskc = lookup_task_ctx(p))) |
| return false; |
| |
| /* stranded? rescue - it can't be dispatched here either way */ |
| if (!cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) { |
| c = cmask_next_set_wrap(&taskc->cpus_allowed, 0); |
| if (c >= 0 && c < scx_bpf_nr_cids()) { |
| __sync_fetch_and_add(&qa.nr_rescue_dsp, 1); |
| scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c, |
| SCX_ENQ_RESCUE); |
| } |
| continue; |
| } |
| |
| if (taskc->highpri) { |
| /* exercise the set_*() and vtime interface too */ |
| scx_bpf_dsq_move_set_slice(BPF_FOR_EACH_ITER, slice_ns * 2); |
| scx_bpf_dsq_move_set_vtime(BPF_FOR_EACH_ITER, highpri_seq++); |
| scx_bpf_dsq_move_vtime(BPF_FOR_EACH_ITER, p, HIGHPRI_DSQ, 0); |
| } |
| } |
| |
| /* |
| * Scan HIGHPRI_DSQ and dispatch until a task that can run here is |
| * found. Prefer this_cid if the task allows it; otherwise RR-scan the |
| * task's cpus_allowed starting after this_cid. |
| */ |
| bpf_for_each(scx_dsq, p, HIGHPRI_DSQ, 0) { |
| task_ctx_t *taskc; |
| bool dispatched = false; |
| s32 cid; |
| |
| if (!(taskc = lookup_task_ctx(p))) |
| return false; |
| |
| /* only run highpri tasks on cids this node can use right now */ |
| if (cmask_test(this_cid, &taskc->cpus_allowed) && |
| cmask_test(this_cid, &qa.usable_cids.mask)) |
| cid = this_cid; |
| else |
| cid = cmask_next_and_set_wrap(&taskc->cpus_allowed, |
| &qa.usable_cids.mask, |
| this_cid + 1); |
| if (cid >= nr_cids) { |
| s32 c; |
| |
| /* self cids lack caps in effect yet, leave it queued */ |
| if (cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) |
| continue; |
| |
| /* stranded after the cull - rescue it from here */ |
| c = cmask_next_set_wrap(&taskc->cpus_allowed, 0); |
| if (c >= 0 && c < nr_cids) { |
| __sync_fetch_and_add(&qa.nr_rescue_dsp, 1); |
| scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c, |
| SCX_ENQ_RESCUE); |
| } |
| continue; |
| } |
| |
| if (scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | cid, |
| SCX_ENQ_PREEMPT | needs_immed(cid))) { |
| if (cid == this_cid) { |
| dispatched = true; |
| __sync_fetch_and_add(&qa.nr_expedited_local, 1); |
| } else { |
| __sync_fetch_and_add(&qa.nr_expedited_remote, 1); |
| } |
| if (from_timer) |
| __sync_fetch_and_add(&qa.nr_expedited_from_timer, 1); |
| } else { |
| __sync_fetch_and_add(&qa.nr_expedited_lost, 1); |
| } |
| |
| if (dispatched) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev) |
| { |
| struct task_struct *p; |
| struct cpu_ctx __arena *cpuc; |
| task_ctx_t *taskc; |
| u32 batch = dsp_batch ?: 1; |
| s32 owner, i; |
| |
| if (scan_shared_dsq(false)) |
| return; |
| |
| /* |
| * Sub-sched routing: a child-owned cid goes to its owner. Never run |
| * this node's own tasks on a delegated cid. Read without the guard. |
| */ |
| owner = qa.part.cid_owner[cid]; |
| if (owner == CID_SHARED) { |
| /* route to the live rr holder (0 = self, runs below) */ |
| s32 pos = qa.part.rr_pos; |
| u64 holder_cgid = (pos >= 0 && pos < MAX_PARTS) ? |
| qa.part.rr_slots[pos] : 0; |
| |
| if (holder_cgid) { |
| scx_bpf_sub_dispatch(holder_cgid); |
| return; |
| } |
| } else if (owner >= 0 && owner < MAX_SUB_SCHEDS) { |
| u64 cgid = qa.sub_sched_ctxs[owner].cgroup_id; |
| |
| if (cgid) { |
| if (scx_bpf_sub_dispatch(cgid)) |
| __sync_fetch_and_add(&qa.sub_sched_ctxs[owner].nr_dsps, 1); |
| return; |
| } |
| } |
| |
| if (!qa.nr_highpri_queued && scx_bpf_dsq_move_to_local(SHARED_DSQ, needs_immed(cid))) |
| return; |
| |
| if (dsp_inf_loop_after && qa.nr_dispatched > dsp_inf_loop_after) { |
| /* |
| * PID 2 should be kthreadd which should mostly be idle and off |
| * the scheduler. Let's keep dispatching it to force the kernel |
| * to call this function over and over again. |
| */ |
| p = bpf_task_from_pid(2); |
| if (p) { |
| scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, slice_ns, 0); |
| bpf_task_release(p); |
| return; |
| } |
| } |
| |
| cpuc = &qa.cpu_ctxs[scx_bpf_this_cid()]; |
| |
| for (i = 0; i < 5; i++) { |
| /* Advance the dispatch cursor and pick the fifo. */ |
| if (!cpuc->dsp_cnt) { |
| cpuc->dsp_idx = (cpuc->dsp_idx + 1) % 5; |
| cpuc->dsp_cnt = 1 << cpuc->dsp_idx; |
| } |
| |
| /* Dispatch or advance. */ |
| bpf_repeat(BPF_MAX_LOOPS) { |
| task_ctx_t *taskc; |
| |
| taskc = qmap_fifo_pop(&qa.fifos[cpuc->dsp_idx]); |
| if (!taskc) |
| break; |
| |
| p = scx_bpf_tid_to_task(taskc->tid); |
| if (!p) |
| continue; |
| |
| if (taskc->highpri) |
| __sync_fetch_and_sub(&qa.nr_highpri_queued, 1); |
| |
| update_core_sched_head_seq(p); |
| __sync_fetch_and_add(&qa.nr_dispatched, 1); |
| |
| scx_bpf_dsq_insert(p, SHARED_DSQ, slice_ns, 0); |
| |
| /* |
| * scx_qmap uses a global BPF queue that any CPU's |
| * dispatch can pop from. If this CPU popped a task that |
| * can't run here, it gets stranded on SHARED_DSQ after |
| * consume_dispatch_q() skips it. Kick the task's home |
| * CPU so it drains SHARED_DSQ. |
| * |
| * There's a race between the pop and the flush of the |
| * buffered dsq_insert: |
| * |
| * CPU 0 (dispatching) CPU 1 (home, idle) |
| * ~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~ |
| * pop from BPF queue |
| * dsq_insert(buffered) |
| * balance: |
| * SHARED_DSQ empty |
| * BPF queue empty |
| * -> goes idle |
| * flush -> on SHARED |
| * kick CPU 1 |
| * wakes, drains task |
| * |
| * The kick prevents indefinite stalls but a per-CPU |
| * kthread like ksoftirqd can be briefly stranded when |
| * its home CPU enters idle with softirq pending, |
| * triggering: |
| * |
| * "NOHZ tick-stop error: local softirq work is pending, handler #N!!!" |
| * |
| * from report_idle_softirq(). The kick lands shortly |
| * after and the home CPU drains the task. This could be |
| * avoided by e.g. dispatching pinned tasks to local or |
| * global DSQs, but the current code is left as-is to |
| * document this class of issue -- other schedulers |
| * seeing similar warnings can use this as a reference. |
| */ |
| if (!cmask_test(cid, &taskc->cpus_allowed)) |
| scx_bpf_kick_cid(scx_bpf_task_cid(p), 0); |
| batch--; |
| cpuc->dsp_cnt--; |
| if (!batch || !scx_bpf_dispatch_nr_slots()) { |
| if (scan_shared_dsq(false) || |
| scx_bpf_dsq_move_to_local(SHARED_DSQ, needs_immed(cid))) |
| return; |
| goto prev; |
| } |
| if (!cpuc->dsp_cnt) |
| break; |
| } |
| |
| cpuc->dsp_cnt = 0; |
| } |
| |
| if (scan_shared_dsq(false)) |
| return; |
| prev: |
| /* |
| * No other tasks. @prev will keep running. Update its core_sched_seq as |
| * if the task were enqueued and dispatched immediately. |
| * |
| * No @prev to keep running means the CPU goes idle. If its claim was |
| * never used, that is not a transition and ops.update_idle() stays |
| * silent. Restore the claim here. |
| */ |
| if (prev) { |
| taskc = lookup_task_ctx(prev); |
| if (!taskc) |
| return; |
| |
| taskc->core_sched_seq = |
| qa.core_sched_tail_seqs[weight_to_idx(prev->scx.weight)]++; |
| } else { |
| cmask_set(cid, &qa.idle_cids.mask); |
| } |
| } |
| |
| void BPF_STRUCT_OPS(qmap_tick, struct task_struct *p) |
| { |
| struct cpu_ctx __arena *cpuc = &qa.cpu_ctxs[scx_bpf_this_cid()]; |
| int idx; |
| |
| /* |
| * Use the running avg of weights to select the target cpuperf level. |
| * This is a demonstration of the cpuperf feature rather than a |
| * practical strategy to regulate CPU frequency. |
| */ |
| cpuc->avg_weight = cpuc->avg_weight * 3 / 4 + p->scx.weight / 4; |
| idx = weight_to_idx(cpuc->avg_weight); |
| cpuc->cpuperf_target = qidx_to_cpuperf_target[idx]; |
| |
| scx_bpf_cidperf_set(scx_bpf_task_cid(p), cpuc->cpuperf_target); |
| } |
| |
| /* |
| * The distance from the head of the queue scaled by the weight of the queue. |
| * The lower the number, the older the task and the higher the priority. |
| */ |
| static s64 task_qdist(struct task_struct *p, task_ctx_t *taskc) |
| { |
| int idx = weight_to_idx(p->scx.weight); |
| s64 qdist; |
| |
| qdist = taskc->core_sched_seq - qa.core_sched_head_seqs[idx]; |
| |
| /* |
| * As queue index increments, the priority doubles. The queue w/ index 3 |
| * is dispatched twice more frequently than 2. Reflect the difference by |
| * scaling qdists accordingly. Note that the shift amount needs to be |
| * flipped depending on the sign to avoid flipping priority direction. |
| */ |
| if (qdist >= 0) |
| return qdist << (4 - idx); |
| else |
| return qdist << idx; |
| } |
| |
| /* |
| * This is called to determine the task ordering when core-sched is picking |
| * tasks to execute on SMT siblings and should encode about the same ordering as |
| * the regular scheduling path. Use the priority-scaled distances from the head |
| * of the queues to compare the two tasks which should be consistent with the |
| * dispatch path behavior. |
| */ |
| bool BPF_STRUCT_OPS(qmap_core_sched_before, |
| struct task_struct *a, struct task_struct *b) |
| { |
| task_ctx_t *taskc_a = lookup_task_ctx(a); |
| task_ctx_t *taskc_b = lookup_task_ctx(b); |
| |
| /* |
| * A task delegated to a sub-scheduler has no task_ctx here. Order such |
| * pairs by the kernel's default ordering - a running task after every |
| * waiting task, then by runnable_at. |
| */ |
| if (!taskc_a || !taskc_b) { |
| if (a->on_cpu != b->on_cpu) |
| return b->on_cpu; |
| return time_before(a->scx.runnable_at, b->scx.runnable_at); |
| } |
| |
| return task_qdist(a, taskc_a) < task_qdist(b, taskc_b); |
| } |
| |
| /* |
| * sched_switch tracepoint and cpu_release handlers are no longer needed. |
| * With SCX_OPS_ALWAYS_ENQ_IMMED, wakeup_preempt_scx() reenqueues IMMED |
| * tasks when a higher-priority scheduling class takes the CPU. |
| */ |
| |
| s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init_task, struct task_struct *p, |
| struct scx_init_task_args *args) |
| { |
| struct task_ctx_stor_val *v; |
| task_ctx_t *taskc; |
| |
| if (qa.inject_mode == QMAP_INJ_INIT_FAIL && |
| !bpf_strncmp(p->comm, 6, "qmfail")) |
| return -ENOMEM; |
| |
| if (p->tgid == disallow_tgid) |
| p->scx.disallow = true; |
| |
| /* pop a slab entry off the free list */ |
| if (qmap_spin_lock(&qa_task_lock)) |
| return -EBUSY; |
| taskc = qa.task_free_head; |
| if (taskc) |
| qa.task_free_head = taskc->next_free; |
| bpf_res_spin_unlock(&qa_task_lock); |
| if (!taskc) { |
| scx_bpf_error("task_ctx slab exhausted (max_tasks=%u)", max_tasks); |
| return -ENOMEM; |
| } |
| |
| taskc->next_free = NULL; |
| taskc->q_next = NULL; |
| taskc->q_prev = NULL; |
| taskc->fifo = NULL; |
| taskc->tid = p->scx.tid; |
| taskc->pid = p->pid; |
| taskc->force_local = false; |
| taskc->highpri = false; |
| taskc->core_sched_seq = 0; |
| cmask_init(&taskc->cpus_allowed, 0, scx_bpf_nr_cids()); |
| bpf_rcu_read_lock(); |
| cmask_from_cpumask(&taskc->cpus_allowed, p->cpus_ptr); |
| bpf_rcu_read_unlock(); |
| |
| v = bpf_task_storage_get(&task_ctx_stor, p, NULL, |
| BPF_LOCAL_STORAGE_GET_F_CREATE); |
| if (!v) { |
| /* push back to the free list */ |
| if (!qmap_spin_lock(&qa_task_lock)) { |
| taskc->next_free = qa.task_free_head; |
| qa.task_free_head = taskc; |
| bpf_res_spin_unlock(&qa_task_lock); |
| } |
| return -ENOMEM; |
| } |
| v->taskc = taskc; |
| return 0; |
| } |
| |
| void BPF_STRUCT_OPS(qmap_exit_task, struct task_struct *p, |
| struct scx_exit_task_args *args) |
| { |
| struct task_ctx_stor_val *v; |
| task_ctx_t *taskc; |
| |
| v = bpf_task_storage_get(&task_ctx_stor, p, NULL, 0); |
| if (!v || !v->taskc) |
| return; |
| taskc = v->taskc; |
| v->taskc = NULL; |
| |
| if (qmap_spin_lock(&qa_task_lock)) |
| return; |
| taskc->next_free = qa.task_free_head; |
| qa.task_free_head = taskc; |
| bpf_res_spin_unlock(&qa_task_lock); |
| } |
| |
| void BPF_STRUCT_OPS(qmap_dump, struct scx_dump_ctx *dctx) |
| { |
| task_ctx_t *taskc; |
| s32 i; |
| |
| QMAP_TOUCH_ARENA(); |
| |
| if (suppress_dump) |
| return; |
| |
| /* |
| * Walk the queue lists without locking - kfunc calls (scx_bpf_dump) |
| * aren't in the verifier's kfunc_spin_allowed() list so we can't hold |
| * a lock and dump. Best-effort; racing may print stale tids but the |
| * walk is bounded by bpf_repeat() so it always terminates. |
| */ |
| bpf_for(i, 0, 5) { |
| scx_bpf_dump("QMAP FIFO[%d]:", i); |
| taskc = qa.fifos[i].head; |
| bpf_repeat(4096) { |
| if (!taskc) |
| break; |
| scx_bpf_dump(" %d:%llu", taskc->pid, taskc->tid); |
| taskc = taskc->q_next; |
| } |
| scx_bpf_dump("\n"); |
| } |
| } |
| |
| void BPF_STRUCT_OPS(qmap_dump_cid, struct scx_dump_ctx *dctx, s32 cid, bool idle) |
| { |
| struct cpu_ctx __arena *cpuc = &qa.cpu_ctxs[cid]; |
| |
| if (suppress_dump || idle) |
| return; |
| |
| scx_bpf_dump("QMAP: dsp_idx=%llu dsp_cnt=%llu avg_weight=%u cpuperf_target=%u", |
| cpuc->dsp_idx, cpuc->dsp_cnt, cpuc->avg_weight, |
| cpuc->cpuperf_target); |
| } |
| |
| void BPF_STRUCT_OPS(qmap_dump_task, struct scx_dump_ctx *dctx, struct task_struct *p) |
| { |
| struct task_ctx_stor_val *v; |
| task_ctx_t *taskc; |
| |
| QMAP_TOUCH_ARENA(); |
| |
| if (suppress_dump) |
| return; |
| v = bpf_task_storage_get(&task_ctx_stor, p, NULL, 0); |
| if (!v || !v->taskc) |
| return; |
| taskc = v->taskc; |
| |
| scx_bpf_dump("QMAP: force_local=%d core_sched_seq=%llu", |
| taskc->force_local, taskc->core_sched_seq); |
| } |
| |
| s32 BPF_STRUCT_OPS(qmap_cpuctl_init, struct cgroup *cgrp, struct scx_cgroup_init_args *args) |
| { |
| QMAP_TOUCH_ARENA(); |
| |
| if (print_msgs) |
| bpf_printk("CGRP INIT %llu weight=%u period=%lu quota=%ld burst=%lu", |
| cgrp->kn->id, args->weight, args->bw_period_us, |
| args->bw_quota_us, args->bw_burst_us); |
| |
| if (qa.inject_mode == QMAP_INJ_CGRP_INIT_FAIL) { |
| char name[7] = {}; |
| |
| bpf_probe_read_kernel_str(name, sizeof(name), cgrp->kn->name); |
| if (!bpf_strncmp(name, 6, "qmfail")) |
| return -ENOMEM; |
| } |
| |
| return 0; |
| } |
| |
| static void redistribute(void); |
| |
| void BPF_STRUCT_OPS(qmap_cpuctl_set_weight, struct cgroup *cgrp, u32 weight) |
| { |
| u64 cgid = cgrp->kn->id; |
| s32 i; |
| |
| QMAP_TOUCH_ARENA(); |
| |
| if (print_msgs) |
| bpf_printk("CGRP SET %llu weight=%u", cgid, weight); |
| |
| /* |
| * Knobs belong to the parent, so this op carries the child subs' |
| * attach point weights. Adjust the matching sub's share of the cid |
| * partition. Other cgroups don't participate in the split. |
| */ |
| for (i = 0; i < MAX_SUB_SCHEDS; i++) { |
| if (qa.sub_sched_ctxs[i].cgroup_id != cgid) |
| continue; |
| if (qa.sub_sched_ctxs[i].weight != weight) { |
| qa.sub_sched_ctxs[i].weight = weight; |
| redistribute(); |
| } |
| break; |
| } |
| } |
| |
| void BPF_STRUCT_OPS(qmap_cpuctl_set_bandwidth, struct cgroup *cgrp, u64 period_us, |
| u64 quota_us, u64 burst_us) |
| { |
| if (print_msgs) |
| bpf_printk("CGRP SET %llu period=%lu quota=%ld burst=%lu", |
| cgrp->kn->id, period_us, quota_us, burst_us); |
| } |
| |
| void BPF_STRUCT_OPS(qmap_cpuctl_move, struct task_struct *p, struct cgroup *from, |
| struct cgroup *to) |
| { |
| if (print_msgs) |
| bpf_printk("CGRP MOVE %d %llu -> %llu", |
| p->pid, from->kn->id, to->kn->id); |
| } |
| |
| void BPF_STRUCT_OPS(qmap_update_idle, s32 cid, bool idle) |
| { |
| QMAP_TOUCH_ARENA(); |
| |
| /* |
| * The kernel delivers update_idle() for every cid this node holds |
| * SCX_CAP_BASE on. Track every cid's idle state regardless of |
| * delegation: the direct-dispatch pick masks idle_cids with usable_cids |
| * at selection, so a cid already idle when it returns to self needs no |
| * reseed here. |
| */ |
| if (idle) |
| cmask_set(cid, &qa.idle_cids.mask); |
| else |
| cmask_clear(cid, &qa.idle_cids.mask); |
| } |
| |
| void BPF_STRUCT_OPS(qmap_set_cmask, struct task_struct *p, |
| const struct scx_cmask *cmask_in) |
| { |
| struct scx_cmask __arena *cmask = (struct scx_cmask __arena *)(long)cmask_in; |
| task_ctx_t *taskc; |
| |
| taskc = lookup_task_ctx(p); |
| if (!taskc) |
| return; |
| cmask_copy(&taskc->cpus_allowed, cmask); |
| } |
| |
| struct monitor_timer { |
| struct bpf_timer timer; |
| }; |
| |
| struct { |
| __uint(type, BPF_MAP_TYPE_ARRAY); |
| __uint(max_entries, 1); |
| __type(key, u32); |
| __type(value, struct monitor_timer); |
| } monitor_timer SEC(".maps"); |
| |
| /* |
| * Aggregate cidperf across the first nr_online_cids cids. Post-hotplug |
| * the first-N-are-online invariant drifts, so some cap/cur values may |
| * be stale. For this demo monitor that's fine; the scheduler exits on |
| * the enable-time hotplug_seq mismatch and userspace restarts, which |
| * rebuilds the layout. |
| */ |
| static void monitor_cpuperf(void) |
| { |
| u32 nr_online = scx_bpf_nr_online_cids(); |
| u64 cap_sum = 0, cur_sum = 0, cur_min = SCX_CPUPERF_ONE, cur_max = 0; |
| u64 target_sum = 0, target_min = SCX_CPUPERF_ONE, target_max = 0; |
| s32 cid; |
| |
| QMAP_TOUCH_ARENA(); |
| |
| bpf_for(cid, 0, nr_online) { |
| struct cpu_ctx __arena *cpuc = &qa.cpu_ctxs[cid]; |
| u32 cap = scx_bpf_cidperf_cap(cid); |
| u32 cur = scx_bpf_cidperf_cur(cid); |
| u32 target; |
| |
| cur_min = cur < cur_min ? cur : cur_min; |
| cur_max = cur > cur_max ? cur : cur_max; |
| |
| cur_sum += (u64)cur * cap / SCX_CPUPERF_ONE; |
| cap_sum += cap; |
| |
| target = cpuc->cpuperf_target; |
| target_sum += target; |
| target_min = target < target_min ? target : target_min; |
| target_max = target > target_max ? target : target_max; |
| } |
| |
| if (!nr_online || !cap_sum) |
| return; |
| |
| qa.cpuperf_min = cur_min; |
| qa.cpuperf_avg = cur_sum * SCX_CPUPERF_ONE / cap_sum; |
| qa.cpuperf_max = cur_max; |
| |
| qa.cpuperf_target_min = target_min; |
| qa.cpuperf_target_avg = target_sum / nr_online; |
| qa.cpuperf_target_max = target_max; |
| } |
| |
| /* |
| * Dump the currently queued tasks in the shared DSQ to demonstrate the usage of |
| * scx_bpf_dsq_nr_queued() and DSQ iterator. Raise the dispatch batch count to |
| * see meaningful dumps in the trace pipe. |
| */ |
| static void dump_shared_dsq(void) |
| { |
| struct task_struct *p; |
| s32 nr; |
| |
| if (!(nr = scx_bpf_dsq_nr_queued(SHARED_DSQ))) |
| return; |
| |
| bpf_printk("Dumping %d tasks in SHARED_DSQ in reverse order", nr); |
| |
| bpf_rcu_read_lock(); |
| bpf_for_each(scx_dsq, p, SHARED_DSQ, SCX_DSQ_ITER_REV) |
| bpf_printk("%s[%d]", p->comm, p->pid); |
| bpf_rcu_read_unlock(); |
| } |
| |
| static int monitor_timerfn(void *map, int *key, struct bpf_timer *timer) |
| { |
| bpf_rcu_read_lock(); |
| scan_shared_dsq(true); |
| bpf_rcu_read_unlock(); |
| |
| monitor_cpuperf(); |
| |
| if (print_dsqs_and_events) { |
| struct scx_event_stats events; |
| |
| dump_shared_dsq(); |
| |
| __COMPAT_scx_bpf_events(&events, sizeof(events)); |
| |
| bpf_printk("%35s: %lld", "SCX_EV_SELECT_CPU_FALLBACK", |
| scx_read_event(&events, SCX_EV_SELECT_CPU_FALLBACK)); |
| bpf_printk("%35s: %lld", "SCX_EV_DISPATCH_LOCAL_DSQ_OFFLINE", |
| scx_read_event(&events, SCX_EV_DISPATCH_LOCAL_DSQ_OFFLINE)); |
| bpf_printk("%35s: %lld", "SCX_EV_DISPATCH_KEEP_LAST", |
| scx_read_event(&events, SCX_EV_DISPATCH_KEEP_LAST)); |
| bpf_printk("%35s: %lld", "SCX_EV_ENQ_SKIP_EXITING", |
| scx_read_event(&events, SCX_EV_ENQ_SKIP_EXITING)); |
| bpf_printk("%35s: %lld", "SCX_EV_REFILL_SLICE_DFL", |
| scx_read_event(&events, SCX_EV_REFILL_SLICE_DFL)); |
| bpf_printk("%35s: %lld", "SCX_EV_BYPASS_DURATION", |
| scx_read_event(&events, SCX_EV_BYPASS_DURATION)); |
| bpf_printk("%35s: %lld", "SCX_EV_BYPASS_DISPATCH", |
| scx_read_event(&events, SCX_EV_BYPASS_DISPATCH)); |
| bpf_printk("%35s: %lld", "SCX_EV_BYPASS_ACTIVATE", |
| scx_read_event(&events, SCX_EV_BYPASS_ACTIVATE)); |
| } |
| |
| if (bpf_timer_start(timer, ONE_SEC_IN_NS, 0)) |
| scx_bpf_error("failed to re-arm stats timer"); |
| return 0; |
| } |
| |
| struct lowpri_timer { |
| struct bpf_timer timer; |
| }; |
| |
| struct { |
| __uint(type, BPF_MAP_TYPE_ARRAY); |
| __uint(max_entries, 1); |
| __type(key, u32); |
| __type(value, struct lowpri_timer); |
| } lowpri_timer SEC(".maps"); |
| |
| /* |
| * Nice 19 tasks are put into the lowpri DSQ. Every 10ms, reenq is triggered and |
| * the tasks are transferred to SHARED_DSQ. |
| */ |
| static int lowpri_timerfn(void *map, int *key, struct bpf_timer *timer) |
| { |
| scx_bpf_dsq_reenq(LOWPRI_DSQ, 0); |
| if (bpf_timer_start(timer, LOWPRI_INTV_NS, 0)) |
| scx_bpf_error("failed to re-arm lowpri timer"); |
| return 0; |
| } |
| |
| struct round_robin_timer { |
| struct bpf_timer timer; |
| }; |
| |
| struct { |
| __uint(type, BPF_MAP_TYPE_ARRAY); |
| __uint(max_entries, 1); |
| __type(key, u32); |
| __type(value, struct round_robin_timer); |
| } round_robin_timer SEC(".maps"); |
| |
| enum part_pending_flags { |
| PART_REFRESH = BIT_U64(0), |
| PART_REDISTRIBUTE = BIT_U64(1), |
| }; |
| |
| /* |
| * Partition update synchronization. qa.part can be written from concurrent |
| * contexts. This single-runner guard admits one writer at a time without |
| * holding a lock across the grant/revoke kfuncs. part_pending coalesces |
| * refresh and repartition requests that arrive while it is held. |
| * |
| * They live in .bss, not the arena: rr_advance() runs from a bpf_timer |
| * callback, where the verifier rejects atomic ops on arena memory. |
| */ |
| static u64 part_busy; |
| static u64 part_pending; |
| |
| static bool part_try_start(void) |
| { |
| /* set busy, report whether it was previously clear (we acquired it) */ |
| return !__sync_fetch_and_or(&part_busy, 1); |
| } |
| |
| static void part_end(void) |
| { |
| __sync_fetch_and_and(&part_busy, 0); |
| } |
| |
| /* |
| * compute_partition() scratch. |
| * |
| * The excl-held cids are handed out in cid order: position 0..nr_excl-1 over |
| * the held cids is split into contiguous ranges, one per participant that gets |
| * at least one excl cid. Range k is owned by cp_range_owner[k] and ends at the |
| * cumulative position cp_range_end[k]. |
| */ |
| static s32 cp_range_owner[MAX_PARTS]; /* exclusive range k: its owner id ... */ |
| static s32 cp_range_end[MAX_PARTS]; /* ... and the cumulative position it ends at */ |
| |
| /* a participant in the partition: self or an attached child */ |
| struct participant { |
| s32 slot; /* child slot, or CID_SELF */ |
| u32 weight; /* cpu.weight */ |
| }; |
| |
| /** |
| * place_one - assign one excl-held cid to its owner |
| * @cid: the excl-held cid to place |
| * @n: its position among the excl-held cids, in [0, nr_excl) |
| * @total_excl: how many positions are owned exclusively (the rest are shared) |
| * |
| * Position @n below @total_excl is owned exclusively. It falls in the range |
| * whose cumulative end it is under, owned by cp_range_owner[]. A position at or |
| * above @total_excl is the rounding leftover which joins the shared pool. |
| * |
| * A separate __noinline function to help verification. |
| */ |
| __noinline int place_one(s32 cid, s32 n, s32 total_excl) |
| { |
| s32 owner = CID_SELF, i, s; |
| |
| if (cid < 0 || cid >= SCX_QMAP_MAX_CPUS || n < 0 || n >= SCX_QMAP_MAX_CPUS || |
| total_excl < 0) { |
| scx_bpf_error("-ERANGE"); |
| return 0; |
| } |
| |
| if (n < total_excl) { |
| for (i = 0; i < MAX_PARTS; i++) { |
| if (n < cp_range_end[i]) { |
| owner = cp_range_owner[i]; |
| break; |
| } |
| } |
| qa.part.cid_owner[cid] = owner; |
| } else { |
| s = n - total_excl; |
| if (s < 0 || s >= MAX_PARTS) { |
| scx_bpf_error("-ERANGE"); |
| return 0; |
| } |
| qa.part.shared_cids[s] = cid; |
| /* time-shared: dispatch resolves the live holder via rr_pos */ |
| qa.part.cid_owner[cid] = CID_SHARED; |
| } |
| return 0; |
| } |
| |
| /** |
| * compute_partition - build the cid partition from this node's held caps |
| * |
| * Decide each cid's owner, the shared pool and the rr rotation. __noinline to |
| * help verification. See the comment at the top of the file. |
| */ |
| __noinline void compute_partition(void) |
| { |
| s32 nr_cids = qa.nr_cids; |
| s32 nr_excl, total_excl = 0, nr_rr = 0; |
| s32 sum_w, i, cid, n = 0, share, self_w; |
| u64 cgid_snap[MAX_SUB_SCHEDS]; |
| s32 w_snap[MAX_SUB_SCHEDS]; |
| |
| if (nr_cids > SCX_QMAP_MAX_CPUS) { |
| scx_bpf_error("-ERANGE"); |
| return; |
| } |
| |
| /* find out the cids we hold */ |
| scx_bpf_sub_caps(0, SCX_CAP_ENQ, &qa.held_excl.mask); |
| scx_bpf_sub_caps(0, SCX_CAP_ENQ_IMMED, &qa.held_shared.mask); |
| cmask_andnot(&qa.held_shared.mask, &qa.held_excl.mask); /* held only as ENQ_IMMED */ |
| |
| qa.part.nr_shared = 0; |
| qa.part.nr_rr = 0; |
| qa.part.rr_pos = 0; |
| |
| nr_excl = cmask_weight(&qa.held_excl.mask); |
| qa.part.nr_excl = nr_excl; |
| |
| /* no excl cid: held_shared stays self-local, the rest unheld */ |
| if (!nr_excl) { |
| bpf_for(cid, 0, nr_cids) { |
| if (cmask_test(cid, &qa.held_shared.mask)) |
| qa.part.cid_owner[cid] = CID_SELF; |
| else |
| qa.part.cid_owner[cid] = CID_NONE; |
| } |
| return; |
| } |
| |
| /* |
| * Snapshot membership and weights so the sum_w and share loops agree. A |
| * mid-compute change would otherwise wrap nr_shared negative. The self |
| * weight is fixed at the default: a cgroup's weight is its parent's |
| * knob, not the scheduler's own business. |
| */ |
| self_w = 100; |
| bpf_for(i, 0, MAX_SUB_SCHEDS) { |
| cgid_snap[i] = qa.sub_sched_ctxs[i].cgroup_id; |
| w_snap[i] = cgid_snap[i] ? (qa.sub_sched_ctxs[i].weight ?: 100) : 0; |
| } |
| |
| /* |
| * Participants are self plus each child. Give each a fixed range/rr |
| * slot: self at slot 0, child i at slot i+1. |
| * |
| * sum_w totals every participant's weight. |
| */ |
| sum_w = self_w; |
| bpf_for(i, 0, MAX_SUB_SCHEDS) { |
| barrier_var(sum_w); |
| sum_w += w_snap[i]; |
| } |
| |
| /* |
| * Split [0, nr_excl) into one contiguous range per participant, each |
| * the floor of its weight share. cp_range_owner[]/cp_range_end[] record |
| * each range's owner and cumulative end, total_excl counts the |
| * exclusive slots, and the rest (nr_excl - total_excl) are shared. |
| * rr_slots[] lists every participant for the round-robin. |
| */ |
| share = (u64)nr_excl * self_w / sum_w; |
| total_excl += share; |
| cp_range_owner[0] = CID_SELF; |
| cp_range_end[0] = total_excl; |
| qa.part.rr_slots[nr_rr++] = 0; /* self holds slot 0 (cgid 0 = no grant) */ |
| |
| bpf_for(i, 0, MAX_SUB_SCHEDS) { |
| u64 cgid = cgid_snap[i]; |
| s32 w = w_snap[i]; |
| |
| barrier_var(total_excl); |
| share = (u64)nr_excl * w / sum_w; |
| total_excl += share; |
| cp_range_owner[i + 1] = cgid ? i : CID_NONE; |
| cp_range_end[i + 1] = total_excl; |
| |
| if (cgid) { |
| barrier_var(nr_rr); |
| if (nr_rr < 0 || nr_rr >= MAX_PARTS) { |
| scx_bpf_error("-ERANGE"); |
| return; |
| } |
| qa.part.rr_slots[nr_rr++] = cgid; |
| } |
| } |
| |
| /* assign each cid: held-excl by position, the rest self/none */ |
| bpf_for(cid, 0, nr_cids) { |
| if (cmask_test(cid, &qa.held_excl.mask)) { |
| place_one(cid, n, total_excl); |
| n++; |
| barrier_var(n); |
| } else if (cmask_test(cid, &qa.held_shared.mask)) { |
| qa.part.cid_owner[cid] = CID_SELF; /* time-share, self-local */ |
| } else { |
| qa.part.cid_owner[cid] = CID_NONE; /* not held */ |
| } |
| } |
| |
| qa.part.nr_shared = nr_excl - total_excl; |
| qa.part.nr_rr = nr_rr; |
| } |
| |
| /* |
| * Charge elapsed wall time to each cid's current owner. Runs under the |
| * partition guard before every ownership change and from the stats flush, so |
| * alloc_ns[] reflects the layout that was in effect. Shared-pool time is |
| * charged to the live round-robin holder. |
| */ |
| static __noinline void account_alloc(void) |
| { |
| u64 now = bpf_ktime_get_ns(); |
| s32 rr_owner = CID_SELF; |
| s32 nr_cids = qa.nr_cids; |
| u64 delta; |
| s32 cid, i; |
| |
| if (nr_cids < 0 || nr_cids > SCX_QMAP_MAX_CPUS) { |
| scx_bpf_error("-ERANGE"); |
| return; |
| } |
| |
| /* first call starts the clock */ |
| if (!qa.alloc_ts) { |
| qa.alloc_ts = now; |
| return; |
| } |
| delta = now - qa.alloc_ts; |
| qa.alloc_ts = now; |
| qa.alloc_window_ns += delta; |
| |
| /* resolve the live shared-pool holder to an owner id */ |
| if (qa.part.nr_shared && qa.part.nr_rr) { |
| u32 pos = qa.part.rr_pos; |
| u64 cgid = pos < MAX_PARTS ? qa.part.rr_slots[pos] : 0; |
| |
| if (cgid) { |
| rr_owner = CID_NONE; |
| bpf_for(i, 0, MAX_SUB_SCHEDS) |
| if (qa.sub_sched_ctxs[i].cgroup_id == cgid) |
| rr_owner = i; |
| } |
| } |
| |
| bpf_for(cid, 0, nr_cids) { |
| s32 owner = qa.part.cid_owner[cid]; |
| |
| if (owner == CID_SHARED) |
| owner = rr_owner; |
| if (owner >= 0 && owner < MAX_SUB_SCHEDS) |
| qa.alloc_ns[owner] += delta; |
| else if (owner == CID_SELF) |
| qa.self_alloc_ns += delta; |
| } |
| } |
| |
| /* |
| * usable_cids = self_cids & avail_cids. The inputs have separate writers, |
| * apply_partition() and qmap_sub_ecaps_updated(), so the result is rebuilt in |
| * full under the partition guard, in scratch first so that readers never see |
| * self_cids alone. |
| */ |
| static void refresh_usable(void) |
| { |
| cmask_copy(&qa.usable_scratch.mask, &qa.self_cids.mask); |
| cmask_and(&qa.usable_scratch.mask, &qa.avail_cids.mask); |
| cmask_copy(&qa.usable_cids.mask, &qa.usable_scratch.mask); |
| } |
| |
| /* |
| * apply_partition - execute the plan compute_partition() built |
| * |
| * Turn the owner map into the per-child, shared and self cmasks and issue the |
| * grant/revoke kfuncs as a delta against each child's previous grant. If no |
| * excl cid, evict every child. |
| */ |
| __noinline void apply_partition(void) |
| { |
| s32 nr_cids = qa.nr_cids; |
| s32 nr_shared = qa.part.nr_shared; |
| s32 i, cid; |
| |
| if (nr_cids < 0 || nr_cids > SCX_QMAP_MAX_CPUS || |
| nr_shared < 0 || nr_shared > MAX_PARTS) { |
| scx_bpf_error("-ERANGE"); |
| return; |
| } |
| |
| /* no excl cpu: run own tasks on the held shares, evict children */ |
| if (!qa.part.nr_excl) { |
| cmask_copy(&qa.self_cids.mask, &qa.held_shared.mask); |
| refresh_usable(); |
| bpf_for(i, 0, MAX_SUB_SCHEDS) |
| if (qa.sub_sched_ctxs[i].cgroup_id) |
| scx_bpf_sub_kill(qa.sub_sched_ctxs[i].cgroup_id, |
| "parent holds no excl cpu to distribute"); |
| return; |
| } |
| |
| /* |
| * Snapshot the old pool. The per-child revoke below clears ENQ_IMMED on |
| * the previously-granted pool, so a cid that left the pool (now a |
| * sibling's excl) doesn't keep a stale ENQ_IMMED on its last holder. |
| */ |
| cmask_copy(&qa.prev_rr_cids.mask, &qa.rr_cids.mask); |
| |
| /* turn the owner map into the rr pool, per-child excl, and self sets */ |
| cmask_init(&qa.rr_cids.mask, 0, nr_cids); |
| cmask_init(&qa.self_cids.mask, 0, nr_cids); |
| |
| /* snapshot each child's grant, then rebuild the new sets below */ |
| bpf_for(i, 0, MAX_SUB_SCHEDS) { |
| cmask_copy(&qa.sub_sched_ctxs[i].prev_granted.mask, |
| &qa.sub_sched_ctxs[i].granted_cids.mask); |
| cmask_init(&qa.sub_sched_ctxs[i].granted_cids.mask, 0, nr_cids); |
| } |
| |
| bpf_for(i, 0, nr_shared) |
| cmask_set(qa.part.shared_cids[i], &qa.rr_cids.mask); |
| bpf_for(cid, 0, nr_cids) { |
| s32 o = qa.part.cid_owner[cid]; |
| |
| if (cmask_test(cid, &qa.rr_cids.mask)) |
| continue; |
| if (o >= 0 && o < MAX_SUB_SCHEDS) |
| cmask_set(cid, &qa.sub_sched_ctxs[o].granted_cids.mask); |
| else if (o == CID_SELF) |
| cmask_set(cid, &qa.self_cids.mask); |
| } |
| refresh_usable(); |
| |
| /* |
| * Apply each child's exclusive cids as a delta against its previous |
| * grant. Separately clear the previous shared grant (ENQ_IMMED on the |
| * old pool), covering cids still pooled and cids that left for a |
| * sibling's excl. The current holder is granted the new pool below. |
| */ |
| bpf_for(i, 0, MAX_SUB_SCHEDS) { |
| struct sub_sched_ctx __arena *ssc = &qa.sub_sched_ctxs[i]; |
| u64 cgid = ssc->cgroup_id; |
| |
| if (!cgid) |
| continue; |
| |
| cmask_copy(&qa.to_revoke_cids.mask, &ssc->prev_granted.mask); |
| cmask_andnot(&qa.to_revoke_cids.mask, &ssc->granted_cids.mask); |
| cmask_copy(&qa.to_grant_cids.mask, &ssc->granted_cids.mask); |
| cmask_andnot(&qa.to_grant_cids.mask, &ssc->prev_granted.mask); |
| |
| scx_bpf_sub_revoke(cgid, SCX_CAP_ENQ_IMMED | SCX_CAP_PERF, |
| &qa.prev_rr_cids.mask); |
| scx_bpf_sub_revoke(cgid, SCX_CAP_ENQ | SCX_CAP_PREEMPT | |
| SCX_CAP_ENQ_IMMED | SCX_CAP_PERF, |
| &qa.to_revoke_cids.mask); |
| scx_bpf_sub_grant(cgid, SCX_CAP_ENQ | SCX_CAP_PREEMPT | |
| SCX_CAP_ENQ_IMMED | SCX_CAP_PERF, |
| &qa.to_grant_cids.mask, NULL); |
| } |
| |
| /* the current holder of the shared pool gets ENQ_IMMED on all of it */ |
| if (nr_shared) { |
| s32 pos = qa.part.rr_pos; |
| u64 holder_cgid; |
| |
| if (pos < 0 || pos >= MAX_PARTS) { |
| scx_bpf_error("-ERANGE"); |
| return; |
| } |
| |
| holder_cgid = qa.part.rr_slots[pos]; /* 0 = self, nothing to grant */ |
| if (holder_cgid) |
| scx_bpf_sub_grant(holder_cgid, |
| SCX_CAP_ENQ_IMMED | SCX_CAP_PERF, |
| &qa.rr_cids.mask, NULL); |
| } |
| } |
| |
| /** |
| * execute_partition - Run pending partition updates |
| * |
| * The rr timer is the backstop if the loop reaches its iteration limit. |
| */ |
| static void execute_partition(void) |
| { |
| u64 pending; |
| s32 i; |
| |
| bpf_for(i, 0, 1024) { |
| if (!part_try_start()) |
| break; |
| |
| pending = __sync_fetch_and_and(&part_pending, 0); |
| if (pending & PART_REDISTRIBUTE) { |
| /* charge elapsed time before repartitioning */ |
| account_alloc(); |
| compute_partition(); |
| apply_partition(); |
| } else if (pending & PART_REFRESH) { |
| refresh_usable(); |
| } |
| |
| /* |
| * Requests are published before trying the guard. Releasing it |
| * before checking pending work ensures a racing request is |
| * either observed here or handled by a caller that acquires the |
| * guard. |
| */ |
| part_end(); |
| if (!__sync_fetch_and_or(&part_pending, 0)) |
| break; |
| } |
| } |
| |
| static void redistribute(void) |
| { |
| __sync_fetch_and_or(&part_pending, PART_REDISTRIBUTE); |
| execute_partition(); |
| } |
| |
| /* |
| * Userspace pokes this (PROG_RUN) to bring alloc_ns[] current before reading |
| * it for the stats display. Skipping when the partition guard is held is |
| * fine - alloc_ts is untouched, so the elapsed time is charged next time. |
| */ |
| SEC("syscall") |
| int flush_alloc(void *ctx) |
| { |
| if (part_try_start()) { |
| account_alloc(); |
| part_end(); |
| execute_partition(); |
| } |
| return 0; |
| } |
| |
| /* |
| * Hand the shared pool to the next participant in the rotation. Self's turn |
| * just revokes the pool back to this sched. A child's turn grants it ENQ_IMMED |
| * on the entire pool. As only excl-held cids are time-shared, a wall-clock |
| * rotation works. Driven by the round-robin timer. |
| */ |
| static void rr_advance(void) |
| { |
| s32 nr_shared, old_pos, new_pos; |
| u64 old_cgid, new_cgid; |
| u32 nr_rr; /* unsigned for % */ |
| |
| /* a redistribute holds the partition and rebuilds the pool, so skip */ |
| if (!part_try_start()) |
| return; |
| |
| nr_rr = qa.part.nr_rr; |
| nr_shared = qa.part.nr_shared; |
| |
| if (nr_shared < 0 || nr_shared > MAX_PARTS) { |
| scx_bpf_error("-ERANGE"); |
| return; |
| } |
| |
| if (nr_shared && nr_rr >= 2) { |
| /* close out the outgoing holder's pool time */ |
| account_alloc(); |
| |
| old_pos = qa.part.rr_pos; |
| new_pos = (old_pos + 1) % nr_rr; |
| old_cgid = qa.part.rr_slots[old_pos]; |
| new_cgid = qa.part.rr_slots[new_pos]; |
| qa.part.rr_pos = new_pos; |
| |
| /* |
| * Move the ENQ_IMMED cap to the next participant. The shared |
| * cids stay marked CID_SHARED. qmap_dispatch() resolves the |
| * live holder via rr_pos without the guard, so a dispatch |
| * racing this handoff may reenqueue a task once. Harmless for a |
| * time-share. |
| */ |
| if (old_cgid) |
| scx_bpf_sub_revoke(old_cgid, |
| SCX_CAP_ENQ_IMMED | SCX_CAP_PERF, |
| &qa.rr_cids.mask); |
| if (new_cgid) |
| scx_bpf_sub_grant(new_cgid, |
| SCX_CAP_ENQ_IMMED | SCX_CAP_PERF, |
| &qa.rr_cids.mask, NULL); |
| } |
| |
| part_end(); |
| |
| execute_partition(); |
| } |
| |
| /* advance the time-shared cid pool every round_robin_ns */ |
| static int round_robin_timerfn(void *map, int *key, struct bpf_timer *timer) |
| { |
| rr_advance(); |
| if (bpf_timer_start(timer, round_robin_ns, 0)) |
| scx_bpf_error("failed to re-arm round-robin timer"); |
| return 0; |
| } |
| |
| /* |
| * Custom cid layout for the cid-override test. On invalid input the kfunc |
| * scx_error()s and aborts the scheduler. |
| */ |
| s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init_cids) |
| { |
| u32 nr_cpu_ids = scx_bpf_nr_cpu_ids(); |
| |
| if (!cid_override_mode) |
| return 0; |
| |
| /* the arena arrays are sized SCX_QMAP_MAX_CPUS */ |
| if (nr_cpu_ids > SCX_QMAP_MAX_CPUS) { |
| scx_bpf_error("nr_cpu_ids=%u exceeds SCX_QMAP_MAX_CPUS=%d", |
| nr_cpu_ids, SCX_QMAP_MAX_CPUS); |
| return -EINVAL; |
| } |
| |
| scx_bpf_cid_override(qa.cid_override_cpu_to_cid, nr_cpu_ids, |
| qa.cid_override_shard_start, cid_override_nr_shards); |
| return 0; |
| } |
| |
| s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init) |
| { |
| u8 __arena *slab; |
| u32 nr_pages, key = 0, i; |
| u32 nr_cids, nr_cpu_ids; |
| struct bpf_timer *timer; |
| s32 ret; |
| |
| nr_cids = scx_bpf_nr_cids(); |
| nr_cpu_ids = scx_bpf_nr_cpu_ids(); |
| |
| if (nr_cids > SCX_QMAP_MAX_CPUS) { |
| scx_bpf_error("nr_cids=%u exceeds SCX_QMAP_MAX_CPUS=%d", |
| nr_cids, SCX_QMAP_MAX_CPUS); |
| return -EINVAL; |
| } |
| if (nr_cpu_ids > SCX_QMAP_MAX_CPUS) { |
| scx_bpf_error("nr_cpu_ids=%u exceeds SCX_QMAP_MAX_CPUS=%d", |
| nr_cpu_ids, SCX_QMAP_MAX_CPUS); |
| return -EINVAL; |
| } |
| |
| /* |
| * Allocate the task_ctx slab in arena and thread the entire slab onto |
| * the free list. max_tasks is set by userspace before load. Each entry |
| * is TASK_CTX_STRIDE bytes - task_ctx's trailing cpus_allowed flex |
| * array extends into the stride tail. |
| */ |
| if (!max_tasks) { |
| scx_bpf_error("max_tasks must be > 0"); |
| return -EINVAL; |
| } |
| |
| nr_pages = (max_tasks * TASK_CTX_STRIDE + PAGE_SIZE - 1) / PAGE_SIZE; |
| slab = bpf_arena_alloc_pages(&arena, NULL, nr_pages, NUMA_NO_NODE, 0); |
| if (!slab) { |
| scx_bpf_error("failed to allocate task_ctx slab"); |
| return -ENOMEM; |
| } |
| qa.task_ctxs = (task_ctx_t *)slab; |
| |
| bpf_for(i, 0, 5) |
| qa.fifos[i].idx = i; |
| |
| bpf_for(i, 0, max_tasks) { |
| task_ctx_t *cur = (task_ctx_t *)(slab + i * TASK_CTX_STRIDE); |
| task_ctx_t *next = (i + 1 < max_tasks) ? |
| (task_ctx_t *)(slab + (i + 1) * TASK_CTX_STRIDE) : NULL; |
| cur->next_free = next; |
| } |
| qa.task_free_head = (task_ctx_t *)slab; |
| |
| /* cache the cid count, trusted to be <= SCX_QMAP_MAX_CPUS hereafter */ |
| qa.nr_cids = nr_cids; |
| |
| /* cmasks are embedded in qa, so they only need initializing */ |
| cmask_init(&qa.idle_cids.mask, 0, nr_cids); |
| cmask_init(&qa.rr_cids.mask, 0, nr_cids); |
| cmask_init(&qa.prev_rr_cids.mask, 0, nr_cids); |
| cmask_init(&qa.self_cids.mask, 0, nr_cids); |
| cmask_init(&qa.avail_cids.mask, 0, nr_cids); |
| cmask_init(&qa.usable_cids.mask, 0, nr_cids); |
| cmask_init(&qa.to_revoke_cids.mask, 0, nr_cids); |
| cmask_init(&qa.to_grant_cids.mask, 0, nr_cids); |
| cmask_init(&qa.usable_scratch.mask, 0, nr_cids); |
| cmask_init(&qa.held_excl.mask, 0, nr_cids); |
| cmask_init(&qa.held_shared.mask, 0, nr_cids); |
| |
| scx_bpf_sub_caps(0, SCX_CAP_ENQ, &qa.held_excl.mask); |
| scx_bpf_sub_caps(0, SCX_CAP_ENQ_IMMED, &qa.held_shared.mask); |
| cmask_andnot(&qa.held_shared.mask, &qa.held_excl.mask); |
| |
| bpf_for(i, 0, MAX_SUB_SCHEDS) { |
| cmask_init(&qa.sub_sched_ctxs[i].granted_cids.mask, 0, nr_cids); |
| cmask_init(&qa.sub_sched_ctxs[i].prev_granted.mask, 0, nr_cids); |
| } |
| |
| /* |
| * The root starts holding every cid and gets no ecaps notifications, so |
| * its avail set is fixed here. qmap_sub_ecaps_updated() maintains the |
| * per-cid state as effective caps settle, and redistribute() rebuilds |
| * owner and self from held caps. A non-root node starts with nothing. |
| */ |
| bpf_for(i, 0, nr_cids) { |
| if (!sub_cgroup_id) { |
| cmask_set(i, &qa.self_cids.mask); |
| cmask_set(i, &qa.avail_cids.mask); |
| cmask_set(i, &qa.usable_cids.mask); |
| qa.part.cid_owner[i] = CID_SELF; |
| } else { |
| qa.part.cid_owner[i] = CID_NONE; |
| } |
| } |
| qa.part.nr_shared = 0; |
| |
| ret = scx_bpf_create_dsq(SHARED_DSQ, -1); |
| if (ret) { |
| scx_bpf_error("failed to create DSQ %d (%d)", SHARED_DSQ, ret); |
| return ret; |
| } |
| |
| ret = scx_bpf_create_dsq(HIGHPRI_DSQ, -1); |
| if (ret) { |
| scx_bpf_error("failed to create DSQ %d (%d)", HIGHPRI_DSQ, ret); |
| return ret; |
| } |
| |
| ret = scx_bpf_create_dsq(LOWPRI_DSQ, -1); |
| if (ret) |
| return ret; |
| |
| timer = bpf_map_lookup_elem(&monitor_timer, &key); |
| if (!timer) |
| return -ESRCH; |
| bpf_timer_init(timer, &monitor_timer, CLOCK_MONOTONIC); |
| bpf_timer_set_callback(timer, monitor_timerfn); |
| ret = bpf_timer_start(timer, ONE_SEC_IN_NS, 0); |
| if (ret) |
| return ret; |
| |
| if (__COMPAT_has_generic_reenq()) { |
| /* see lowpri_timerfn() */ |
| timer = bpf_map_lookup_elem(&lowpri_timer, &key); |
| if (!timer) |
| return -ESRCH; |
| bpf_timer_init(timer, &lowpri_timer, CLOCK_MONOTONIC); |
| bpf_timer_set_callback(timer, lowpri_timerfn); |
| ret = bpf_timer_start(timer, LOWPRI_INTV_NS, 0); |
| if (ret) |
| return ret; |
| } |
| |
| /* sub-sched: drive the boundary-cid round-robin from a bpf timer */ |
| timer = bpf_map_lookup_elem(&round_robin_timer, &key); |
| if (!timer) |
| return -ESRCH; |
| bpf_timer_init(timer, &round_robin_timer, CLOCK_MONOTONIC); |
| bpf_timer_set_callback(timer, round_robin_timerfn); |
| ret = bpf_timer_start(timer, round_robin_ns, 0); |
| if (ret) |
| return ret; |
| |
| return 0; |
| } |
| |
| void BPF_STRUCT_OPS(qmap_exit, struct scx_exit_info *ei) |
| { |
| UEI_RECORD(uei, ei); |
| } |
| |
| /* |
| * Seed a new sub slot with the cgroup's current weight. The kernel delivers |
| * ops.cpuctl_set_weight() only on value-changing writes, so a weight set |
| * before the sub attached would otherwise go unnoticed. |
| */ |
| static u32 cgrp_cur_weight(u64 cgid) |
| { |
| struct cgroup_subsys_state *css; |
| struct cgroup *cgrp; |
| u32 weight = 100; |
| |
| cgrp = bpf_cgroup_from_id(cgid); |
| if (!cgrp) |
| return weight; |
| |
| css = BPF_CORE_READ(cgrp, subsys[cpu_cgrp_id]); |
| if (css) { |
| struct task_group *tg = container_of(css, struct task_group, css); |
| u32 w = BPF_CORE_READ(tg, scx.weight); |
| |
| if (w) |
| weight = w; |
| } |
| bpf_cgroup_release(cgrp); |
| return weight; |
| } |
| |
| s32 BPF_STRUCT_OPS(qmap_sub_attach, struct scx_sub_attach_args *args) |
| { |
| s32 i; |
| |
| /* as long as there is at least one excl cpu, children can attach */ |
| if (!cmask_weight(&qa.held_excl.mask)) |
| return -ENOSPC; |
| |
| for (i = 0; i < MAX_SUB_SCHEDS; i++) { |
| if (qa.sub_sched_ctxs[i].cgroup_id) |
| continue; |
| |
| qa.sub_sched_ctxs[i].cgroup_id = args->ops->sub_cgroup_id; |
| qa.sub_sched_ctxs[i].weight = cgrp_cur_weight(args->ops->sub_cgroup_id); |
| qa.nr_sub_scheds++; |
| bpf_printk("attaching sub-sched[%d] on %s", i, args->cgroup_path); |
| redistribute(); |
| return 0; |
| } |
| |
| return -ENOSPC; |
| } |
| |
| void BPF_STRUCT_OPS(qmap_sub_detach, struct scx_sub_detach_args *args) |
| { |
| s32 i; |
| |
| for (i = 0; i < MAX_SUB_SCHEDS; i++) { |
| if (qa.sub_sched_ctxs[i].cgroup_id != args->ops->sub_cgroup_id) |
| continue; |
| |
| qa.sub_sched_ctxs[i].cgroup_id = 0; |
| qa.sub_sched_ctxs[i].weight = 100; |
| cmask_init(&qa.sub_sched_ctxs[i].granted_cids.mask, 0, qa.nr_cids); |
| qa.nr_sub_scheds--; |
| bpf_printk("detaching sub-sched[%d] on %s", i, args->cgroup_path); |
| redistribute(); |
| break; |
| } |
| } |
| |
| void BPF_STRUCT_OPS(qmap_sub_caps_updated, const struct scx_cmask *cmask, u64 caps) |
| { |
| /* our held caps changed, redistribute */ |
| redistribute(); |
| } |
| |
| void BPF_STRUCT_OPS(qmap_sub_ecaps_updated, s32 cid, u64 before, u64 after) |
| { |
| /* |
| * Effective caps updated. Track which cids hold shared caps so a self |
| * task placed there enqueues IMMED, and which cids have ENQ_IMMED in |
| * effect at all (avail, see the header comment). |
| */ |
| if (after & SCX_CAP_ENQ_IMMED) { |
| qa.cid_shared[cid] = (after & SCX_CAP_ENQ) ? 0 : 1; |
| cmask_set(cid, &qa.avail_cids.mask); |
| } else { |
| qa.cid_shared[cid] = 0; |
| cmask_clear(cid, &qa.avail_cids.mask); |
| } |
| |
| __sync_fetch_and_or(&part_pending, PART_REFRESH); |
| execute_partition(); |
| } |
| |
| SCX_OPS_CID_DEFINE(qmap_ops, |
| .flags = SCX_OPS_ENQ_EXITING | SCX_OPS_TID_TO_TASK, |
| .select_cid = (void *)qmap_select_cid, |
| .enqueue = (void *)qmap_enqueue, |
| .dequeue = (void *)qmap_dequeue, |
| .dispatch = (void *)qmap_dispatch, |
| .tick = (void *)qmap_tick, |
| .core_sched_before = (void *)qmap_core_sched_before, |
| .set_cmask = (void *)qmap_set_cmask, |
| .update_idle = (void *)qmap_update_idle, |
| .init_task = (void *)qmap_init_task, |
| .exit_task = (void *)qmap_exit_task, |
| .dump = (void *)qmap_dump, |
| .dump_cid = (void *)qmap_dump_cid, |
| .dump_task = (void *)qmap_dump_task, |
| .cpuctl_init = (void *)qmap_cpuctl_init, |
| .cpuctl_set_weight = (void *)qmap_cpuctl_set_weight, |
| .cpuctl_set_bandwidth = (void *)qmap_cpuctl_set_bandwidth, |
| .cpuctl_move = (void *)qmap_cpuctl_move, |
| .sub_attach = (void *)qmap_sub_attach, |
| .sub_detach = (void *)qmap_sub_detach, |
| .sub_caps_updated = (void *)qmap_sub_caps_updated, |
| .sub_ecaps_updated = (void *)qmap_sub_ecaps_updated, |
| .init_cids = (void *)qmap_init_cids, |
| .init = (void *)qmap_init, |
| .exit = (void *)qmap_exit, |
| .timeout_ms = 5000U, |
| .name = "qmap"); |