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) 348