|  | /* SPDX-License-Identifier: GPL-2.0 */ | 
|  | /* | 
|  | * A demo sched_ext flattened cgroup hierarchy scheduler. It implements | 
|  | * hierarchical weight-based cgroup CPU control by flattening the cgroup | 
|  | * hierarchy into a single layer by compounding the active weight share at each | 
|  | * level. Consider the following hierarchy with weights in parentheses: | 
|  | * | 
|  | * R + A (100) + B (100) | 
|  | *   |         \ C (100) | 
|  | *   \ D (200) | 
|  | * | 
|  | * Ignoring the root and threaded cgroups, only B, C and D can contain tasks. | 
|  | * Let's say all three have runnable tasks. The total share that each of these | 
|  | * three cgroups is entitled to can be calculated by compounding its share at | 
|  | * each level. | 
|  | * | 
|  | * For example, B is competing against C and in that competition its share is | 
|  | * 100/(100+100) == 1/2. At its parent level, A is competing against D and A's | 
|  | * share in that competition is 100/(200+100) == 1/3. B's eventual share in the | 
|  | * system can be calculated by multiplying the two shares, 1/2 * 1/3 == 1/6. C's | 
|  | * eventual shaer is the same at 1/6. D is only competing at the top level and | 
|  | * its share is 200/(100+200) == 2/3. | 
|  | * | 
|  | * So, instead of hierarchically scheduling level-by-level, we can consider it | 
|  | * as B, C and D competing each other with respective share of 1/6, 1/6 and 2/3 | 
|  | * and keep updating the eventual shares as the cgroups' runnable states change. | 
|  | * | 
|  | * This flattening of hierarchy can bring a substantial performance gain when | 
|  | * the cgroup hierarchy is nested multiple levels. in a simple benchmark using | 
|  | * wrk[8] on apache serving a CGI script calculating sha1sum of a small file, it | 
|  | * outperforms CFS by ~3% with CPU controller disabled and by ~10% with two | 
|  | * apache instances competing with 2:1 weight ratio nested four level deep. | 
|  | * | 
|  | * However, the gain comes at the cost of not being able to properly handle | 
|  | * thundering herd of cgroups. For example, if many cgroups which are nested | 
|  | * behind a low priority parent cgroup wake up around the same time, they may be | 
|  | * able to consume more CPU cycles than they are entitled to. In many use cases, | 
|  | * this isn't a real concern especially given the performance gain. Also, there | 
|  | * are ways to mitigate the problem further by e.g. introducing an extra | 
|  | * scheduling layer on cgroup delegation boundaries. | 
|  | * | 
|  | * The scheduler first picks the cgroup to run and then schedule the tasks | 
|  | * within by using nested weighted vtime scheduling by default. The | 
|  | * cgroup-internal scheduling can be switched to FIFO with the -f option. | 
|  | */ | 
|  | #include <scx/common.bpf.h> | 
|  | #include "scx_flatcg.h" | 
|  |  | 
|  | /* | 
|  | * Maximum amount of retries to find a valid cgroup. | 
|  | */ | 
|  | #define CGROUP_MAX_RETRIES 1024 | 
|  |  | 
|  | char _license[] SEC("license") = "GPL"; | 
|  |  | 
|  | const volatile u32 nr_cpus = 32;	/* !0 for veristat, set during init */ | 
|  | const volatile u64 cgrp_slice_ns = SCX_SLICE_DFL; | 
|  | const volatile bool fifo_sched; | 
|  |  | 
|  | u64 cvtime_now; | 
|  | UEI_DEFINE(uei); | 
|  |  | 
|  | struct { | 
|  | __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); | 
|  | __type(key, u32); | 
|  | __type(value, u64); | 
|  | __uint(max_entries, FCG_NR_STATS); | 
|  | } stats SEC(".maps"); | 
|  |  | 
|  | static void stat_inc(enum fcg_stat_idx idx) | 
|  | { | 
|  | u32 idx_v = idx; | 
|  |  | 
|  | u64 *cnt_p = bpf_map_lookup_elem(&stats, &idx_v); | 
|  | if (cnt_p) | 
|  | (*cnt_p)++; | 
|  | } | 
|  |  | 
|  | struct fcg_cpu_ctx { | 
|  | u64			cur_cgid; | 
|  | u64			cur_at; | 
|  | }; | 
|  |  | 
|  | struct { | 
|  | __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); | 
|  | __type(key, u32); | 
|  | __type(value, struct fcg_cpu_ctx); | 
|  | __uint(max_entries, 1); | 
|  | } cpu_ctx SEC(".maps"); | 
|  |  | 
|  | struct { | 
|  | __uint(type, BPF_MAP_TYPE_CGRP_STORAGE); | 
|  | __uint(map_flags, BPF_F_NO_PREALLOC); | 
|  | __type(key, int); | 
|  | __type(value, struct fcg_cgrp_ctx); | 
|  | } cgrp_ctx SEC(".maps"); | 
|  |  | 
|  | struct cgv_node { | 
|  | struct bpf_rb_node	rb_node; | 
|  | __u64			cvtime; | 
|  | __u64			cgid; | 
|  | }; | 
|  |  | 
|  | private(CGV_TREE) struct bpf_spin_lock cgv_tree_lock; | 
|  | private(CGV_TREE) struct bpf_rb_root cgv_tree __contains(cgv_node, rb_node); | 
|  |  | 
|  | struct cgv_node_stash { | 
|  | struct cgv_node __kptr *node; | 
|  | }; | 
|  |  | 
|  | struct { | 
|  | __uint(type, BPF_MAP_TYPE_HASH); | 
|  | __uint(max_entries, 16384); | 
|  | __type(key, __u64); | 
|  | __type(value, struct cgv_node_stash); | 
|  | } cgv_node_stash SEC(".maps"); | 
|  |  | 
|  | struct fcg_task_ctx { | 
|  | u64		bypassed_at; | 
|  | }; | 
|  |  | 
|  | struct { | 
|  | __uint(type, BPF_MAP_TYPE_TASK_STORAGE); | 
|  | __uint(map_flags, BPF_F_NO_PREALLOC); | 
|  | __type(key, int); | 
|  | __type(value, struct fcg_task_ctx); | 
|  | } task_ctx SEC(".maps"); | 
|  |  | 
|  | /* gets inc'd on weight tree changes to expire the cached hweights */ | 
|  | u64 hweight_gen = 1; | 
|  |  | 
|  | static u64 div_round_up(u64 dividend, u64 divisor) | 
|  | { | 
|  | return (dividend + divisor - 1) / divisor; | 
|  | } | 
|  |  | 
|  | static bool vtime_before(u64 a, u64 b) | 
|  | { | 
|  | return (s64)(a - b) < 0; | 
|  | } | 
|  |  | 
|  | static bool cgv_node_less(struct bpf_rb_node *a, const struct bpf_rb_node *b) | 
|  | { | 
|  | struct cgv_node *cgc_a, *cgc_b; | 
|  |  | 
|  | cgc_a = container_of(a, struct cgv_node, rb_node); | 
|  | cgc_b = container_of(b, struct cgv_node, rb_node); | 
|  |  | 
|  | return cgc_a->cvtime < cgc_b->cvtime; | 
|  | } | 
|  |  | 
|  | static struct fcg_cpu_ctx *find_cpu_ctx(void) | 
|  | { | 
|  | struct fcg_cpu_ctx *cpuc; | 
|  | u32 idx = 0; | 
|  |  | 
|  | cpuc = bpf_map_lookup_elem(&cpu_ctx, &idx); | 
|  | if (!cpuc) { | 
|  | scx_bpf_error("cpu_ctx lookup failed"); | 
|  | return NULL; | 
|  | } | 
|  | return cpuc; | 
|  | } | 
|  |  | 
|  | static struct fcg_cgrp_ctx *find_cgrp_ctx(struct cgroup *cgrp) | 
|  | { | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  |  | 
|  | cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0, 0); | 
|  | if (!cgc) { | 
|  | scx_bpf_error("cgrp_ctx lookup failed for cgid %llu", cgrp->kn->id); | 
|  | return NULL; | 
|  | } | 
|  | return cgc; | 
|  | } | 
|  |  | 
|  | static struct fcg_cgrp_ctx *find_ancestor_cgrp_ctx(struct cgroup *cgrp, int level) | 
|  | { | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  |  | 
|  | cgrp = bpf_cgroup_ancestor(cgrp, level); | 
|  | if (!cgrp) { | 
|  | scx_bpf_error("ancestor cgroup lookup failed"); | 
|  | return NULL; | 
|  | } | 
|  |  | 
|  | cgc = find_cgrp_ctx(cgrp); | 
|  | if (!cgc) | 
|  | scx_bpf_error("ancestor cgrp_ctx lookup failed"); | 
|  | bpf_cgroup_release(cgrp); | 
|  | return cgc; | 
|  | } | 
|  |  | 
|  | static void cgrp_refresh_hweight(struct cgroup *cgrp, struct fcg_cgrp_ctx *cgc) | 
|  | { | 
|  | int level; | 
|  |  | 
|  | if (!cgc->nr_active) { | 
|  | stat_inc(FCG_STAT_HWT_SKIP); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (cgc->hweight_gen == hweight_gen) { | 
|  | stat_inc(FCG_STAT_HWT_CACHE); | 
|  | return; | 
|  | } | 
|  |  | 
|  | stat_inc(FCG_STAT_HWT_UPDATES); | 
|  | bpf_for(level, 0, cgrp->level + 1) { | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  | bool is_active; | 
|  |  | 
|  | cgc = find_ancestor_cgrp_ctx(cgrp, level); | 
|  | if (!cgc) | 
|  | break; | 
|  |  | 
|  | if (!level) { | 
|  | cgc->hweight = FCG_HWEIGHT_ONE; | 
|  | cgc->hweight_gen = hweight_gen; | 
|  | } else { | 
|  | struct fcg_cgrp_ctx *pcgc; | 
|  |  | 
|  | pcgc = find_ancestor_cgrp_ctx(cgrp, level - 1); | 
|  | if (!pcgc) | 
|  | break; | 
|  |  | 
|  | /* | 
|  | * We can be oppotunistic here and not grab the | 
|  | * cgv_tree_lock and deal with the occasional races. | 
|  | * However, hweight updates are already cached and | 
|  | * relatively low-frequency. Let's just do the | 
|  | * straightforward thing. | 
|  | */ | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  | is_active = cgc->nr_active; | 
|  | if (is_active) { | 
|  | cgc->hweight_gen = pcgc->hweight_gen; | 
|  | cgc->hweight = | 
|  | div_round_up(pcgc->hweight * cgc->weight, | 
|  | pcgc->child_weight_sum); | 
|  | } | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  |  | 
|  | if (!is_active) { | 
|  | stat_inc(FCG_STAT_HWT_RACE); | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | static void cgrp_cap_budget(struct cgv_node *cgv_node, struct fcg_cgrp_ctx *cgc) | 
|  | { | 
|  | u64 delta, cvtime, max_budget; | 
|  |  | 
|  | /* | 
|  | * A node which is on the rbtree can't be pointed to from elsewhere yet | 
|  | * and thus can't be updated and repositioned. Instead, we collect the | 
|  | * vtime deltas separately and apply it asynchronously here. | 
|  | */ | 
|  | delta = cgc->cvtime_delta; | 
|  | __sync_fetch_and_sub(&cgc->cvtime_delta, delta); | 
|  | cvtime = cgv_node->cvtime + delta; | 
|  |  | 
|  | /* | 
|  | * Allow a cgroup to carry the maximum budget proportional to its | 
|  | * hweight such that a full-hweight cgroup can immediately take up half | 
|  | * of the CPUs at the most while staying at the front of the rbtree. | 
|  | */ | 
|  | max_budget = (cgrp_slice_ns * nr_cpus * cgc->hweight) / | 
|  | (2 * FCG_HWEIGHT_ONE); | 
|  | if (vtime_before(cvtime, cvtime_now - max_budget)) | 
|  | cvtime = cvtime_now - max_budget; | 
|  |  | 
|  | cgv_node->cvtime = cvtime; | 
|  | } | 
|  |  | 
|  | static void cgrp_enqueued(struct cgroup *cgrp, struct fcg_cgrp_ctx *cgc) | 
|  | { | 
|  | struct cgv_node_stash *stash; | 
|  | struct cgv_node *cgv_node; | 
|  | u64 cgid = cgrp->kn->id; | 
|  |  | 
|  | /* paired with cmpxchg in try_pick_next_cgroup() */ | 
|  | if (__sync_val_compare_and_swap(&cgc->queued, 0, 1)) { | 
|  | stat_inc(FCG_STAT_ENQ_SKIP); | 
|  | return; | 
|  | } | 
|  |  | 
|  | stash = bpf_map_lookup_elem(&cgv_node_stash, &cgid); | 
|  | if (!stash) { | 
|  | scx_bpf_error("cgv_node lookup failed for cgid %llu", cgid); | 
|  | return; | 
|  | } | 
|  |  | 
|  | /* NULL if the node is already on the rbtree */ | 
|  | cgv_node = bpf_kptr_xchg(&stash->node, NULL); | 
|  | if (!cgv_node) { | 
|  | stat_inc(FCG_STAT_ENQ_RACE); | 
|  | return; | 
|  | } | 
|  |  | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  | cgrp_cap_budget(cgv_node, cgc); | 
|  | bpf_rbtree_add(&cgv_tree, &cgv_node->rb_node, cgv_node_less); | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  | } | 
|  |  | 
|  | static void set_bypassed_at(struct task_struct *p, struct fcg_task_ctx *taskc) | 
|  | { | 
|  | /* | 
|  | * Tell fcg_stopping() that this bypassed the regular scheduling path | 
|  | * and should be force charged to the cgroup. 0 is used to indicate that | 
|  | * the task isn't bypassing, so if the current runtime is 0, go back by | 
|  | * one nanosecond. | 
|  | */ | 
|  | taskc->bypassed_at = p->se.sum_exec_runtime ?: (u64)-1; | 
|  | } | 
|  |  | 
|  | s32 BPF_STRUCT_OPS(fcg_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags) | 
|  | { | 
|  | struct fcg_task_ctx *taskc; | 
|  | bool is_idle = false; | 
|  | s32 cpu; | 
|  |  | 
|  | cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle); | 
|  |  | 
|  | taskc = bpf_task_storage_get(&task_ctx, p, 0, 0); | 
|  | if (!taskc) { | 
|  | scx_bpf_error("task_ctx lookup failed"); | 
|  | return cpu; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * If select_cpu_dfl() is recommending local enqueue, the target CPU is | 
|  | * idle. Follow it and charge the cgroup later in fcg_stopping() after | 
|  | * the fact. | 
|  | */ | 
|  | if (is_idle) { | 
|  | set_bypassed_at(p, taskc); | 
|  | stat_inc(FCG_STAT_LOCAL); | 
|  | scx_bpf_dispatch(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0); | 
|  | } | 
|  |  | 
|  | return cpu; | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_enqueue, struct task_struct *p, u64 enq_flags) | 
|  | { | 
|  | struct fcg_task_ctx *taskc; | 
|  | struct cgroup *cgrp; | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  |  | 
|  | taskc = bpf_task_storage_get(&task_ctx, p, 0, 0); | 
|  | if (!taskc) { | 
|  | scx_bpf_error("task_ctx lookup failed"); | 
|  | return; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Use the direct dispatching and force charging to deal with tasks with | 
|  | * custom affinities so that we don't have to worry about per-cgroup | 
|  | * dq's containing tasks that can't be executed from some CPUs. | 
|  | */ | 
|  | if (p->nr_cpus_allowed != nr_cpus) { | 
|  | set_bypassed_at(p, taskc); | 
|  |  | 
|  | /* | 
|  | * The global dq is deprioritized as we don't want to let tasks | 
|  | * to boost themselves by constraining its cpumask. The | 
|  | * deprioritization is rather severe, so let's not apply that to | 
|  | * per-cpu kernel threads. This is ham-fisted. We probably wanna | 
|  | * implement per-cgroup fallback dq's instead so that we have | 
|  | * more control over when tasks with custom cpumask get issued. | 
|  | */ | 
|  | if (p->nr_cpus_allowed == 1 && (p->flags & PF_KTHREAD)) { | 
|  | stat_inc(FCG_STAT_LOCAL); | 
|  | scx_bpf_dispatch(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, enq_flags); | 
|  | } else { | 
|  | stat_inc(FCG_STAT_GLOBAL); | 
|  | scx_bpf_dispatch(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags); | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | cgrp = scx_bpf_task_cgroup(p); | 
|  | cgc = find_cgrp_ctx(cgrp); | 
|  | if (!cgc) | 
|  | goto out_release; | 
|  |  | 
|  | if (fifo_sched) { | 
|  | scx_bpf_dispatch(p, cgrp->kn->id, SCX_SLICE_DFL, enq_flags); | 
|  | } else { | 
|  | u64 tvtime = p->scx.dsq_vtime; | 
|  |  | 
|  | /* | 
|  | * Limit the amount of budget that an idling task can accumulate | 
|  | * to one slice. | 
|  | */ | 
|  | if (vtime_before(tvtime, cgc->tvtime_now - SCX_SLICE_DFL)) | 
|  | tvtime = cgc->tvtime_now - SCX_SLICE_DFL; | 
|  |  | 
|  | scx_bpf_dispatch_vtime(p, cgrp->kn->id, SCX_SLICE_DFL, | 
|  | tvtime, enq_flags); | 
|  | } | 
|  |  | 
|  | cgrp_enqueued(cgrp, cgc); | 
|  | out_release: | 
|  | bpf_cgroup_release(cgrp); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Walk the cgroup tree to update the active weight sums as tasks wake up and | 
|  | * sleep. The weight sums are used as the base when calculating the proportion a | 
|  | * given cgroup or task is entitled to at each level. | 
|  | */ | 
|  | static void update_active_weight_sums(struct cgroup *cgrp, bool runnable) | 
|  | { | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  | bool updated = false; | 
|  | int idx; | 
|  |  | 
|  | cgc = find_cgrp_ctx(cgrp); | 
|  | if (!cgc) | 
|  | return; | 
|  |  | 
|  | /* | 
|  | * In most cases, a hot cgroup would have multiple threads going to | 
|  | * sleep and waking up while the whole cgroup stays active. In leaf | 
|  | * cgroups, ->nr_runnable which is updated with __sync operations gates | 
|  | * ->nr_active updates, so that we don't have to grab the cgv_tree_lock | 
|  | * repeatedly for a busy cgroup which is staying active. | 
|  | */ | 
|  | if (runnable) { | 
|  | if (__sync_fetch_and_add(&cgc->nr_runnable, 1)) | 
|  | return; | 
|  | stat_inc(FCG_STAT_ACT); | 
|  | } else { | 
|  | if (__sync_sub_and_fetch(&cgc->nr_runnable, 1)) | 
|  | return; | 
|  | stat_inc(FCG_STAT_DEACT); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * If @cgrp is becoming runnable, its hweight should be refreshed after | 
|  | * it's added to the weight tree so that enqueue has the up-to-date | 
|  | * value. If @cgrp is becoming quiescent, the hweight should be | 
|  | * refreshed before it's removed from the weight tree so that the usage | 
|  | * charging which happens afterwards has access to the latest value. | 
|  | */ | 
|  | if (!runnable) | 
|  | cgrp_refresh_hweight(cgrp, cgc); | 
|  |  | 
|  | /* propagate upwards */ | 
|  | bpf_for(idx, 0, cgrp->level) { | 
|  | int level = cgrp->level - idx; | 
|  | struct fcg_cgrp_ctx *cgc, *pcgc = NULL; | 
|  | bool propagate = false; | 
|  |  | 
|  | cgc = find_ancestor_cgrp_ctx(cgrp, level); | 
|  | if (!cgc) | 
|  | break; | 
|  | if (level) { | 
|  | pcgc = find_ancestor_cgrp_ctx(cgrp, level - 1); | 
|  | if (!pcgc) | 
|  | break; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * We need the propagation protected by a lock to synchronize | 
|  | * against weight changes. There's no reason to drop the lock at | 
|  | * each level but bpf_spin_lock() doesn't want any function | 
|  | * calls while locked. | 
|  | */ | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  |  | 
|  | if (runnable) { | 
|  | if (!cgc->nr_active++) { | 
|  | updated = true; | 
|  | if (pcgc) { | 
|  | propagate = true; | 
|  | pcgc->child_weight_sum += cgc->weight; | 
|  | } | 
|  | } | 
|  | } else { | 
|  | if (!--cgc->nr_active) { | 
|  | updated = true; | 
|  | if (pcgc) { | 
|  | propagate = true; | 
|  | pcgc->child_weight_sum -= cgc->weight; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  |  | 
|  | if (!propagate) | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (updated) | 
|  | __sync_fetch_and_add(&hweight_gen, 1); | 
|  |  | 
|  | if (runnable) | 
|  | cgrp_refresh_hweight(cgrp, cgc); | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_runnable, struct task_struct *p, u64 enq_flags) | 
|  | { | 
|  | struct cgroup *cgrp; | 
|  |  | 
|  | cgrp = scx_bpf_task_cgroup(p); | 
|  | update_active_weight_sums(cgrp, true); | 
|  | bpf_cgroup_release(cgrp); | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_running, struct task_struct *p) | 
|  | { | 
|  | struct cgroup *cgrp; | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  |  | 
|  | if (fifo_sched) | 
|  | return; | 
|  |  | 
|  | cgrp = scx_bpf_task_cgroup(p); | 
|  | cgc = find_cgrp_ctx(cgrp); | 
|  | if (cgc) { | 
|  | /* | 
|  | * @cgc->tvtime_now always progresses forward as tasks start | 
|  | * executing. The test and update can be performed concurrently | 
|  | * from multiple CPUs and thus racy. Any error should be | 
|  | * contained and temporary. Let's just live with it. | 
|  | */ | 
|  | if (vtime_before(cgc->tvtime_now, p->scx.dsq_vtime)) | 
|  | cgc->tvtime_now = p->scx.dsq_vtime; | 
|  | } | 
|  | bpf_cgroup_release(cgrp); | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_stopping, struct task_struct *p, bool runnable) | 
|  | { | 
|  | struct fcg_task_ctx *taskc; | 
|  | struct cgroup *cgrp; | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  |  | 
|  | /* | 
|  | * Scale the execution time by the inverse of the weight and charge. | 
|  | * | 
|  | * Note that the default yield implementation yields by setting | 
|  | * @p->scx.slice to zero and the following would treat the yielding task | 
|  | * as if it has consumed all its slice. If this penalizes yielding tasks | 
|  | * too much, determine the execution time by taking explicit timestamps | 
|  | * instead of depending on @p->scx.slice. | 
|  | */ | 
|  | if (!fifo_sched) | 
|  | p->scx.dsq_vtime += | 
|  | (SCX_SLICE_DFL - p->scx.slice) * 100 / p->scx.weight; | 
|  |  | 
|  | taskc = bpf_task_storage_get(&task_ctx, p, 0, 0); | 
|  | if (!taskc) { | 
|  | scx_bpf_error("task_ctx lookup failed"); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!taskc->bypassed_at) | 
|  | return; | 
|  |  | 
|  | cgrp = scx_bpf_task_cgroup(p); | 
|  | cgc = find_cgrp_ctx(cgrp); | 
|  | if (cgc) { | 
|  | __sync_fetch_and_add(&cgc->cvtime_delta, | 
|  | p->se.sum_exec_runtime - taskc->bypassed_at); | 
|  | taskc->bypassed_at = 0; | 
|  | } | 
|  | bpf_cgroup_release(cgrp); | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_quiescent, struct task_struct *p, u64 deq_flags) | 
|  | { | 
|  | struct cgroup *cgrp; | 
|  |  | 
|  | cgrp = scx_bpf_task_cgroup(p); | 
|  | update_active_weight_sums(cgrp, false); | 
|  | bpf_cgroup_release(cgrp); | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_cgroup_set_weight, struct cgroup *cgrp, u32 weight) | 
|  | { | 
|  | struct fcg_cgrp_ctx *cgc, *pcgc = NULL; | 
|  |  | 
|  | cgc = find_cgrp_ctx(cgrp); | 
|  | if (!cgc) | 
|  | return; | 
|  |  | 
|  | if (cgrp->level) { | 
|  | pcgc = find_ancestor_cgrp_ctx(cgrp, cgrp->level - 1); | 
|  | if (!pcgc) | 
|  | return; | 
|  | } | 
|  |  | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  | if (pcgc && cgc->nr_active) | 
|  | pcgc->child_weight_sum += (s64)weight - cgc->weight; | 
|  | cgc->weight = weight; | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  | } | 
|  |  | 
|  | static bool try_pick_next_cgroup(u64 *cgidp) | 
|  | { | 
|  | struct bpf_rb_node *rb_node; | 
|  | struct cgv_node_stash *stash; | 
|  | struct cgv_node *cgv_node; | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  | struct cgroup *cgrp; | 
|  | u64 cgid; | 
|  |  | 
|  | /* pop the front cgroup and wind cvtime_now accordingly */ | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  |  | 
|  | rb_node = bpf_rbtree_first(&cgv_tree); | 
|  | if (!rb_node) { | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  | stat_inc(FCG_STAT_PNC_NO_CGRP); | 
|  | *cgidp = 0; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | rb_node = bpf_rbtree_remove(&cgv_tree, rb_node); | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  |  | 
|  | if (!rb_node) { | 
|  | /* | 
|  | * This should never happen. bpf_rbtree_first() was called | 
|  | * above while the tree lock was held, so the node should | 
|  | * always be present. | 
|  | */ | 
|  | scx_bpf_error("node could not be removed"); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | cgv_node = container_of(rb_node, struct cgv_node, rb_node); | 
|  | cgid = cgv_node->cgid; | 
|  |  | 
|  | if (vtime_before(cvtime_now, cgv_node->cvtime)) | 
|  | cvtime_now = cgv_node->cvtime; | 
|  |  | 
|  | /* | 
|  | * If lookup fails, the cgroup's gone. Free and move on. See | 
|  | * fcg_cgroup_exit(). | 
|  | */ | 
|  | cgrp = bpf_cgroup_from_id(cgid); | 
|  | if (!cgrp) { | 
|  | stat_inc(FCG_STAT_PNC_GONE); | 
|  | goto out_free; | 
|  | } | 
|  |  | 
|  | cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0, 0); | 
|  | if (!cgc) { | 
|  | bpf_cgroup_release(cgrp); | 
|  | stat_inc(FCG_STAT_PNC_GONE); | 
|  | goto out_free; | 
|  | } | 
|  |  | 
|  | if (!scx_bpf_consume(cgid)) { | 
|  | bpf_cgroup_release(cgrp); | 
|  | stat_inc(FCG_STAT_PNC_EMPTY); | 
|  | goto out_stash; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Successfully consumed from the cgroup. This will be our current | 
|  | * cgroup for the new slice. Refresh its hweight. | 
|  | */ | 
|  | cgrp_refresh_hweight(cgrp, cgc); | 
|  |  | 
|  | bpf_cgroup_release(cgrp); | 
|  |  | 
|  | /* | 
|  | * As the cgroup may have more tasks, add it back to the rbtree. Note | 
|  | * that here we charge the full slice upfront and then exact later | 
|  | * according to the actual consumption. This prevents lowpri thundering | 
|  | * herd from saturating the machine. | 
|  | */ | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  | cgv_node->cvtime += cgrp_slice_ns * FCG_HWEIGHT_ONE / (cgc->hweight ?: 1); | 
|  | cgrp_cap_budget(cgv_node, cgc); | 
|  | bpf_rbtree_add(&cgv_tree, &cgv_node->rb_node, cgv_node_less); | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  |  | 
|  | *cgidp = cgid; | 
|  | stat_inc(FCG_STAT_PNC_NEXT); | 
|  | return true; | 
|  |  | 
|  | out_stash: | 
|  | stash = bpf_map_lookup_elem(&cgv_node_stash, &cgid); | 
|  | if (!stash) { | 
|  | stat_inc(FCG_STAT_PNC_GONE); | 
|  | goto out_free; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Paired with cmpxchg in cgrp_enqueued(). If they see the following | 
|  | * transition, they'll enqueue the cgroup. If they are earlier, we'll | 
|  | * see their task in the dq below and requeue the cgroup. | 
|  | */ | 
|  | __sync_val_compare_and_swap(&cgc->queued, 1, 0); | 
|  |  | 
|  | if (scx_bpf_dsq_nr_queued(cgid)) { | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  | bpf_rbtree_add(&cgv_tree, &cgv_node->rb_node, cgv_node_less); | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  | stat_inc(FCG_STAT_PNC_RACE); | 
|  | } else { | 
|  | cgv_node = bpf_kptr_xchg(&stash->node, cgv_node); | 
|  | if (cgv_node) { | 
|  | scx_bpf_error("unexpected !NULL cgv_node stash"); | 
|  | goto out_free; | 
|  | } | 
|  | } | 
|  |  | 
|  | return false; | 
|  |  | 
|  | out_free: | 
|  | bpf_obj_drop(cgv_node); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_dispatch, s32 cpu, struct task_struct *prev) | 
|  | { | 
|  | struct fcg_cpu_ctx *cpuc; | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  | struct cgroup *cgrp; | 
|  | u64 now = bpf_ktime_get_ns(); | 
|  | bool picked_next = false; | 
|  |  | 
|  | cpuc = find_cpu_ctx(); | 
|  | if (!cpuc) | 
|  | return; | 
|  |  | 
|  | if (!cpuc->cur_cgid) | 
|  | goto pick_next_cgroup; | 
|  |  | 
|  | if (vtime_before(now, cpuc->cur_at + cgrp_slice_ns)) { | 
|  | if (scx_bpf_consume(cpuc->cur_cgid)) { | 
|  | stat_inc(FCG_STAT_CNS_KEEP); | 
|  | return; | 
|  | } | 
|  | stat_inc(FCG_STAT_CNS_EMPTY); | 
|  | } else { | 
|  | stat_inc(FCG_STAT_CNS_EXPIRE); | 
|  | } | 
|  |  | 
|  | /* | 
|  | * The current cgroup is expiring. It was already charged a full slice. | 
|  | * Calculate the actual usage and accumulate the delta. | 
|  | */ | 
|  | cgrp = bpf_cgroup_from_id(cpuc->cur_cgid); | 
|  | if (!cgrp) { | 
|  | stat_inc(FCG_STAT_CNS_GONE); | 
|  | goto pick_next_cgroup; | 
|  | } | 
|  |  | 
|  | cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0, 0); | 
|  | if (cgc) { | 
|  | /* | 
|  | * We want to update the vtime delta and then look for the next | 
|  | * cgroup to execute but the latter needs to be done in a loop | 
|  | * and we can't keep the lock held. Oh well... | 
|  | */ | 
|  | bpf_spin_lock(&cgv_tree_lock); | 
|  | __sync_fetch_and_add(&cgc->cvtime_delta, | 
|  | (cpuc->cur_at + cgrp_slice_ns - now) * | 
|  | FCG_HWEIGHT_ONE / (cgc->hweight ?: 1)); | 
|  | bpf_spin_unlock(&cgv_tree_lock); | 
|  | } else { | 
|  | stat_inc(FCG_STAT_CNS_GONE); | 
|  | } | 
|  |  | 
|  | bpf_cgroup_release(cgrp); | 
|  |  | 
|  | pick_next_cgroup: | 
|  | cpuc->cur_at = now; | 
|  |  | 
|  | if (scx_bpf_consume(SCX_DSQ_GLOBAL)) { | 
|  | cpuc->cur_cgid = 0; | 
|  | return; | 
|  | } | 
|  |  | 
|  | bpf_repeat(CGROUP_MAX_RETRIES) { | 
|  | if (try_pick_next_cgroup(&cpuc->cur_cgid)) { | 
|  | picked_next = true; | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | /* | 
|  | * This only happens if try_pick_next_cgroup() races against enqueue | 
|  | * path for more than CGROUP_MAX_RETRIES times, which is extremely | 
|  | * unlikely and likely indicates an underlying bug. There shouldn't be | 
|  | * any stall risk as the race is against enqueue. | 
|  | */ | 
|  | if (!picked_next) | 
|  | stat_inc(FCG_STAT_PNC_FAIL); | 
|  | } | 
|  |  | 
|  | s32 BPF_STRUCT_OPS(fcg_init_task, struct task_struct *p, | 
|  | struct scx_init_task_args *args) | 
|  | { | 
|  | struct fcg_task_ctx *taskc; | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  |  | 
|  | /* | 
|  | * @p is new. Let's ensure that its task_ctx is available. We can sleep | 
|  | * in this function and the following will automatically use GFP_KERNEL. | 
|  | */ | 
|  | taskc = bpf_task_storage_get(&task_ctx, p, 0, | 
|  | BPF_LOCAL_STORAGE_GET_F_CREATE); | 
|  | if (!taskc) | 
|  | return -ENOMEM; | 
|  |  | 
|  | taskc->bypassed_at = 0; | 
|  |  | 
|  | if (!(cgc = find_cgrp_ctx(args->cgroup))) | 
|  | return -ENOENT; | 
|  |  | 
|  | p->scx.dsq_vtime = cgc->tvtime_now; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | int BPF_STRUCT_OPS_SLEEPABLE(fcg_cgroup_init, struct cgroup *cgrp, | 
|  | struct scx_cgroup_init_args *args) | 
|  | { | 
|  | struct fcg_cgrp_ctx *cgc; | 
|  | struct cgv_node *cgv_node; | 
|  | struct cgv_node_stash empty_stash = {}, *stash; | 
|  | u64 cgid = cgrp->kn->id; | 
|  | int ret; | 
|  |  | 
|  | /* | 
|  | * Technically incorrect as cgroup ID is full 64bit while dq ID is | 
|  | * 63bit. Should not be a problem in practice and easy to spot in the | 
|  | * unlikely case that it breaks. | 
|  | */ | 
|  | ret = scx_bpf_create_dsq(cgid, -1); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0, | 
|  | BPF_LOCAL_STORAGE_GET_F_CREATE); | 
|  | if (!cgc) { | 
|  | ret = -ENOMEM; | 
|  | goto err_destroy_dsq; | 
|  | } | 
|  |  | 
|  | cgc->weight = args->weight; | 
|  | cgc->hweight = FCG_HWEIGHT_ONE; | 
|  |  | 
|  | ret = bpf_map_update_elem(&cgv_node_stash, &cgid, &empty_stash, | 
|  | BPF_NOEXIST); | 
|  | if (ret) { | 
|  | if (ret != -ENOMEM) | 
|  | scx_bpf_error("unexpected stash creation error (%d)", | 
|  | ret); | 
|  | goto err_destroy_dsq; | 
|  | } | 
|  |  | 
|  | stash = bpf_map_lookup_elem(&cgv_node_stash, &cgid); | 
|  | if (!stash) { | 
|  | scx_bpf_error("unexpected cgv_node stash lookup failure"); | 
|  | ret = -ENOENT; | 
|  | goto err_destroy_dsq; | 
|  | } | 
|  |  | 
|  | cgv_node = bpf_obj_new(struct cgv_node); | 
|  | if (!cgv_node) { | 
|  | ret = -ENOMEM; | 
|  | goto err_del_cgv_node; | 
|  | } | 
|  |  | 
|  | cgv_node->cgid = cgid; | 
|  | cgv_node->cvtime = cvtime_now; | 
|  |  | 
|  | cgv_node = bpf_kptr_xchg(&stash->node, cgv_node); | 
|  | if (cgv_node) { | 
|  | scx_bpf_error("unexpected !NULL cgv_node stash"); | 
|  | ret = -EBUSY; | 
|  | goto err_drop; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | err_drop: | 
|  | bpf_obj_drop(cgv_node); | 
|  | err_del_cgv_node: | 
|  | bpf_map_delete_elem(&cgv_node_stash, &cgid); | 
|  | err_destroy_dsq: | 
|  | scx_bpf_destroy_dsq(cgid); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_cgroup_exit, struct cgroup *cgrp) | 
|  | { | 
|  | u64 cgid = cgrp->kn->id; | 
|  |  | 
|  | /* | 
|  | * For now, there's no way find and remove the cgv_node if it's on the | 
|  | * cgv_tree. Let's drain them in the dispatch path as they get popped | 
|  | * off the front of the tree. | 
|  | */ | 
|  | bpf_map_delete_elem(&cgv_node_stash, &cgid); | 
|  | scx_bpf_destroy_dsq(cgid); | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_cgroup_move, struct task_struct *p, | 
|  | struct cgroup *from, struct cgroup *to) | 
|  | { | 
|  | struct fcg_cgrp_ctx *from_cgc, *to_cgc; | 
|  | s64 vtime_delta; | 
|  |  | 
|  | /* find_cgrp_ctx() triggers scx_ops_error() on lookup failures */ | 
|  | if (!(from_cgc = find_cgrp_ctx(from)) || !(to_cgc = find_cgrp_ctx(to))) | 
|  | return; | 
|  |  | 
|  | vtime_delta = p->scx.dsq_vtime - from_cgc->tvtime_now; | 
|  | p->scx.dsq_vtime = to_cgc->tvtime_now + vtime_delta; | 
|  | } | 
|  |  | 
|  | void BPF_STRUCT_OPS(fcg_exit, struct scx_exit_info *ei) | 
|  | { | 
|  | UEI_RECORD(uei, ei); | 
|  | } | 
|  |  | 
|  | SCX_OPS_DEFINE(flatcg_ops, | 
|  | .select_cpu		= (void *)fcg_select_cpu, | 
|  | .enqueue			= (void *)fcg_enqueue, | 
|  | .dispatch		= (void *)fcg_dispatch, | 
|  | .runnable		= (void *)fcg_runnable, | 
|  | .running			= (void *)fcg_running, | 
|  | .stopping		= (void *)fcg_stopping, | 
|  | .quiescent		= (void *)fcg_quiescent, | 
|  | .init_task		= (void *)fcg_init_task, | 
|  | .cgroup_set_weight	= (void *)fcg_cgroup_set_weight, | 
|  | .cgroup_init		= (void *)fcg_cgroup_init, | 
|  | .cgroup_exit		= (void *)fcg_cgroup_exit, | 
|  | .cgroup_move		= (void *)fcg_cgroup_move, | 
|  | .exit			= (void *)fcg_exit, | 
|  | .flags			= SCX_OPS_HAS_CGROUP_WEIGHT | SCX_OPS_ENQ_EXITING, | 
|  | .name			= "flatcg"); |