| @@ -0,0 +1,347 @@ |
| 1 | +// SPDX-License-Identifier: GPL-2.0 |
| 2 | +/* |
| 3 | + * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES |
| 4 | + * |
| 5 | + * Validate that a finite-slice EXT task restarts the scheduler tick when it |
| 6 | + * follows an infinite-slice EXT task and an idle interval on a NOHZ_FULL CPU. |
| 7 | + */ |
| 8 | +#define _GNU_SOURCE |
| 9 | + |
| 10 | +#include <bpf/bpf.h> |
| 11 | +#include <errno.h> |
| 12 | +#include <sched.h> |
| 13 | +#include <signal.h> |
| 14 | +#include <stdbool.h> |
| 15 | +#include <stdio.h> |
| 16 | +#include <stdlib.h> |
| 17 | +#include <sys/prctl.h> |
| 18 | +#include <sys/wait.h> |
| 19 | +#include <unistd.h> |
| 20 | + |
| 21 | +#include <scx/common.h> |
| 22 | + |
| 23 | +#include "nohz_tick.bpf.skel.h" |
| 24 | +#include "scx_test.h" |
| 25 | + |
| 26 | +#ifndef SCHED_EXT |
| 27 | +#define SCHED_EXT 7 |
| 28 | +#endif |
| 29 | + |
| 30 | +#define MIN_FINITE_TICKS 3 |
| 31 | +#define PHASE_TIMEOUT_MS 1000 |
| 32 | + |
| 33 | +struct nohz_tick_ctx { |
| 34 | + struct nohz_tick *skel; |
| 35 | + cpu_set_t original_mask; |
| 36 | + int test_cpu; |
| 37 | +}; |
| 38 | + |
| 39 | +static int first_allowed_cpu(const cpu_set_t *mask, int first, int last) |
| 40 | +{ |
| 41 | + int cpu; |
| 42 | + |
| 43 | + for (cpu = first; cpu <= last && cpu < CPU_SETSIZE; cpu++) |
| 44 | + if (CPU_ISSET(cpu, mask)) |
| 45 | + return cpu; |
| 46 | + |
| 47 | + return -1; |
| 48 | +} |
| 49 | + |
| 50 | +static int find_nohz_full_cpu(const cpu_set_t *allowed) |
| 51 | +{ |
| 52 | + char buf[4096], *cur, *end; |
| 53 | + FILE *file; |
| 54 | + |
| 55 | + file = fopen("/sys/devices/system/cpu/nohz_full", "r"); |
| 56 | + if (!file) |
| 57 | + return -1; |
| 58 | + if (!fgets(buf, sizeof(buf), file)) { |
| 59 | + fclose(file); |
| 60 | + return -1; |
| 61 | + } |
| 62 | + fclose(file); |
| 63 | + |
| 64 | + cur = buf; |
| 65 | + while (*cur) { |
| 66 | + long first, last; |
| 67 | + int cpu; |
| 68 | + |
| 69 | + while (*cur == ' ' || *cur == '\t' || *cur == ',') |
| 70 | + cur++; |
| 71 | + if (*cur < '0' || *cur > '9') |
| 72 | + break; |
| 73 | + |
| 74 | + errno = 0; |
| 75 | + first = strtol(cur, &end, 10); |
| 76 | + if (errno || end == cur || first < 0 || first >= CPU_SETSIZE) |
| 77 | + return -1; |
| 78 | + cur = end; |
| 79 | + last = first; |
| 80 | + if (*cur == '-') { |
| 81 | + cur++; |
| 82 | + errno = 0; |
| 83 | + last = strtol(cur, &end, 10); |
| 84 | + if (errno || end == cur || last < first) |
| 85 | + return -1; |
| 86 | + cur = end; |
| 87 | + } |
| 88 | + |
| 89 | + cpu = first_allowed_cpu(allowed, first, last); |
| 90 | + if (cpu >= 0) |
| 91 | + return cpu; |
| 92 | + } |
| 93 | + |
| 94 | + return -1; |
| 95 | +} |
| 96 | + |
| 97 | +static pid_t start_worker(int cpu) |
| 98 | +{ |
| 99 | + struct sched_param param = {}; |
| 100 | + cpu_set_t mask; |
| 101 | + pid_t parent; |
| 102 | + pid_t pid; |
| 103 | + |
| 104 | + parent = getpid(); |
| 105 | + pid = fork(); |
| 106 | + if (pid != 0) |
| 107 | + return pid; |
| 108 | + if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent) |
| 109 | + _exit(1); |
| 110 | + |
| 111 | + /* |
| 112 | + * Become EXT before touching the target so it stays idle until wakeup. |
| 113 | + */ |
| 114 | + if (sched_setscheduler(0, SCHED_EXT, ¶m)) |
| 115 | + _exit(1); |
| 116 | + |
| 117 | + CPU_ZERO(&mask); |
| 118 | + CPU_SET(cpu, &mask); |
| 119 | + if (sched_setaffinity(0, sizeof(mask), &mask)) |
| 120 | + _exit(1); |
| 121 | + |
| 122 | + for (;;) |
| 123 | + asm volatile("" ::: "memory"); |
| 124 | +} |
| 125 | + |
| 126 | +static void stop_worker(pid_t pid) |
| 127 | +{ |
| 128 | + if (pid <= 0) |
| 129 | + return; |
| 130 | + |
| 131 | + kill(pid, SIGKILL); |
| 132 | + waitpid(pid, NULL, 0); |
| 133 | +} |
| 134 | + |
| 135 | +static int pause_worker(pid_t pid) |
| 136 | +{ |
| 137 | + int status; |
| 138 | + |
| 139 | + if (kill(pid, SIGSTOP)) |
| 140 | + return -errno; |
| 141 | + if (waitpid(pid, &status, WUNTRACED) != pid) |
| 142 | + return -errno; |
| 143 | + if (!WIFSTOPPED(status)) |
| 144 | + return -ECHILD; |
| 145 | + |
| 146 | + return 0; |
| 147 | +} |
| 148 | + |
| 149 | +static bool wait_for_counter(const u64 *counter, u64 value, int timeout_ms) |
| 150 | +{ |
| 151 | + int elapsed; |
| 152 | + |
| 153 | + for (elapsed = 0; elapsed < timeout_ms; elapsed++) { |
| 154 | + if (__atomic_load_n(counter, __ATOMIC_RELAXED) >= value) |
| 155 | + return true; |
| 156 | + usleep(1000); |
| 157 | + } |
| 158 | + |
| 159 | + return false; |
| 160 | +} |
| 161 | + |
| 162 | +static enum scx_test_status setup(void **ctx_ptr) |
| 163 | +{ |
| 164 | + struct nohz_tick_ctx *ctx; |
| 165 | + cpu_set_t controller_mask; |
| 166 | + int cpu; |
| 167 | + |
| 168 | + ctx = calloc(1, sizeof(*ctx)); |
| 169 | + SCX_FAIL_IF(!ctx, "Failed to allocate context"); |
| 170 | + if (sched_getaffinity(0, sizeof(ctx->original_mask), |
| 171 | + &ctx->original_mask)) { |
| 172 | + free(ctx); |
| 173 | + SCX_FAIL("Failed to get affinity (%d)", errno); |
| 174 | + } |
| 175 | + |
| 176 | + cpu = find_nohz_full_cpu(&ctx->original_mask); |
| 177 | + if (cpu < 0) { |
| 178 | + fprintf(stderr, "SKIP: no allowed NOHZ_FULL CPU\n"); |
| 179 | + free(ctx); |
| 180 | + return SCX_TEST_SKIP; |
| 181 | + } |
| 182 | + |
| 183 | + controller_mask = ctx->original_mask; |
| 184 | + CPU_CLR(cpu, &controller_mask); |
| 185 | + if (CPU_COUNT(&controller_mask) == 0) { |
| 186 | + fprintf(stderr, "SKIP: no housekeeping CPU available\n"); |
| 187 | + free(ctx); |
| 188 | + return SCX_TEST_SKIP; |
| 189 | + } |
| 190 | + |
| 191 | + ctx->test_cpu = cpu; |
| 192 | + ctx->skel = nohz_tick__open(); |
| 193 | + if (!ctx->skel) { |
| 194 | + free(ctx); |
| 195 | + SCX_FAIL("Failed to open skeleton"); |
| 196 | + } |
| 197 | + |
| 198 | + SCX_ENUM_INIT(ctx->skel); |
| 199 | + ctx->skel->rodata->test_cpu = cpu; |
| 200 | + ctx->skel->struct_ops.nohz_tick_ops->flags |= SCX_OPS_SWITCH_PARTIAL | |
| 201 | + SCX_OPS_ENQ_LAST; |
| 202 | + if (nohz_tick__load(ctx->skel)) { |
| 203 | + nohz_tick__destroy(ctx->skel); |
| 204 | + free(ctx); |
| 205 | + SCX_FAIL("Failed to load skeleton"); |
| 206 | + } |
| 207 | + |
| 208 | + if (sched_setaffinity(0, sizeof(controller_mask), &controller_mask)) { |
| 209 | + nohz_tick__destroy(ctx->skel); |
| 210 | + free(ctx); |
| 211 | + SCX_FAIL("Failed to move controller off CPU %d (%d)", cpu, errno); |
| 212 | + } |
| 213 | + |
| 214 | + *ctx_ptr = ctx; |
| 215 | + return SCX_TEST_PASS; |
| 216 | +} |
| 217 | + |
| 218 | +static enum scx_test_status run(void *ctx_ptr) |
| 219 | +{ |
| 220 | + struct nohz_tick_ctx *ctx = ctx_ptr; |
| 221 | + struct nohz_tick *skel = ctx->skel; |
| 222 | + struct bpf_link *link = NULL; |
| 223 | + enum scx_test_status status = SCX_TEST_FAIL; |
| 224 | + pid_t finite_worker = -1; |
| 225 | + pid_t inf_worker = -1; |
| 226 | + u64 finite_running; |
| 227 | + u64 finite_ticks; |
| 228 | + int ret; |
| 229 | + |
| 230 | + link = bpf_map__attach_struct_ops(skel->maps.nohz_tick_ops); |
| 231 | + if (!link) { |
| 232 | + SCX_ERR("Failed to attach scheduler"); |
| 233 | + goto out; |
| 234 | + } |
| 235 | + |
| 236 | + /* |
| 237 | + * Establish SCX_RQ_CAN_STOP_TICK with an infinite-slice task. |
| 238 | + */ |
| 239 | + inf_worker = start_worker(ctx->test_cpu); |
| 240 | + if (inf_worker < 0) { |
| 241 | + SCX_ERR("Failed to start infinite-slice worker (%d)", errno); |
| 242 | + goto out; |
| 243 | + } |
| 244 | + if (!wait_for_counter(&skel->bss->nr_inf_running, 1, |
| 245 | + PHASE_TIMEOUT_MS)) { |
| 246 | + SCX_ERR("Infinite-slice worker was not scheduled"); |
| 247 | + goto out; |
| 248 | + } |
| 249 | + |
| 250 | + /* Block without exiting so the rq retains the infinite-slice state. */ |
| 251 | + ret = pause_worker(inf_worker); |
| 252 | + if (ret) { |
| 253 | + SCX_ERR("Failed to stop infinite-slice worker (%d)", ret); |
| 254 | + goto out; |
| 255 | + } |
| 256 | + |
| 257 | + /* Let the target enter idle with its tick stopped. */ |
| 258 | + usleep(100000); |
| 259 | + |
| 260 | + /* |
| 261 | + * The next EXT task receives a finite slice and must restart the tick. |
| 262 | + */ |
| 263 | + __atomic_store_n(&skel->bss->finite_phase, true, __ATOMIC_RELEASE); |
| 264 | + finite_worker = start_worker(ctx->test_cpu); |
| 265 | + if (finite_worker < 0) { |
| 266 | + SCX_ERR("Failed to start finite-slice worker (%d)", errno); |
| 267 | + goto out; |
| 268 | + } |
| 269 | + if (!wait_for_counter(&skel->bss->nr_finite_running, 1, |
| 270 | + PHASE_TIMEOUT_MS)) { |
| 271 | + SCX_ERR("Finite-slice worker was not scheduled"); |
| 272 | + goto out; |
| 273 | + } |
| 274 | + if (!wait_for_counter(&skel->bss->nr_finite_ticks, MIN_FINITE_TICKS, |
| 275 | + PHASE_TIMEOUT_MS)) { |
| 276 | + SCX_ERR("Finite-slice worker received only %llu scheduler ticks", |
| 277 | + (unsigned long long)skel->bss->nr_finite_ticks); |
| 278 | + goto out; |
| 279 | + } |
| 280 | + stop_worker(finite_worker); |
| 281 | + finite_worker = -1; |
| 282 | + |
| 283 | + /* |
| 284 | + * Leave the CPU idle after a finite-slice task. The next finite-slice |
| 285 | + * task must restart the tick even though the slice type is unchanged. |
| 286 | + */ |
| 287 | + usleep(100000); |
| 288 | + finite_running = __atomic_load_n(&skel->bss->nr_finite_running, |
| 289 | + __ATOMIC_RELAXED); |
| 290 | + finite_ticks = __atomic_load_n(&skel->bss->nr_finite_ticks, |
| 291 | + __ATOMIC_RELAXED); |
| 292 | + |
| 293 | + finite_worker = start_worker(ctx->test_cpu); |
| 294 | + if (finite_worker < 0) { |
| 295 | + SCX_ERR("Failed to start second finite-slice worker (%d)", errno); |
| 296 | + goto out; |
| 297 | + } |
| 298 | + if (!wait_for_counter(&skel->bss->nr_finite_running, |
| 299 | + finite_running + 1, PHASE_TIMEOUT_MS)) { |
| 300 | + SCX_ERR("Second finite-slice worker was not scheduled"); |
| 301 | + goto out; |
| 302 | + } |
| 303 | + if (!wait_for_counter(&skel->bss->nr_finite_ticks, |
| 304 | + finite_ticks + MIN_FINITE_TICKS, |
| 305 | + PHASE_TIMEOUT_MS)) { |
| 306 | + SCX_ERR("Second finite-slice worker received only %llu scheduler ticks", |
| 307 | + (unsigned long long)(skel->bss->nr_finite_ticks - |
| 308 | + finite_ticks)); |
| 309 | + goto out; |
| 310 | + } |
| 311 | + |
| 312 | + if (skel->data->uei.kind != EXIT_KIND(SCX_EXIT_NONE)) { |
| 313 | + SCX_ERR("Scheduler exited unexpectedly (kind=%llu code=%lld)", |
| 314 | + (unsigned long long)skel->data->uei.kind, |
| 315 | + (long long)skel->data->uei.exit_code); |
| 316 | + goto out; |
| 317 | + } |
| 318 | + |
| 319 | + fprintf(stderr, "CPU %d received %llu finite-slice ticks\n", |
| 320 | + ctx->test_cpu, |
| 321 | + (unsigned long long)skel->bss->nr_finite_ticks); |
| 322 | + status = SCX_TEST_PASS; |
| 323 | +out: |
| 324 | + stop_worker(finite_worker); |
| 325 | + stop_worker(inf_worker); |
| 326 | + if (link) |
| 327 | + bpf_link__destroy(link); |
| 328 | + return status; |
| 329 | +} |
| 330 | + |
| 331 | +static void cleanup(void *ctx_ptr) |
| 332 | +{ |
| 333 | + struct nohz_tick_ctx *ctx = ctx_ptr; |
| 334 | + |
| 335 | + sched_setaffinity(0, sizeof(ctx->original_mask), &ctx->original_mask); |
| 336 | + nohz_tick__destroy(ctx->skel); |
| 337 | + free(ctx); |
| 338 | +} |
| 339 | + |
| 340 | +struct scx_test nohz_tick = { |
| 341 | + .name = "nohz_tick", |
| 342 | + .description = "Verify finite EXT slices restart the NOHZ_FULL tick", |
| 343 | + .setup = setup, |
| 344 | + .run = run, |
| 345 | + .cleanup = cleanup, |
| 346 | +}; |
| 347 | +REGISTER_SCX_TEST(&nohz_tick) |