History

selftests/sched_ext: Verify nohz_full tick behavior

Finite-slice EXT tasks need the periodic scheduler tick to expire their slices even when nohz_full is enabled. Add a regression test that selects a nohz_full CPU and exercises both infinite-to-finite and finite-to-finite slice transitions across an idle interval. For each finite task, verify that its ops.tick() callback is invoked. Skip the test when an allowed nohz_full CPU and a separate housekeeping CPU are not available.

Andrea Righi committed 4 months ago

Commit cfe950d1 parent 4ec10f33 files · +413 −0Browse files

Unified · Split

3 files
  1. tools/testing/selftests/sched_ext/Makefile +1 −0
  2. tools/testing/selftests/sched_ext/nohz_tick.bpf.c +65 −0
  3. tools/testing/selftests/sched_ext/nohz_tick.c +347 −0
tools/testing/selftests/sched_ext/Makefile+1−0
@@ -176,6 +176,7 @@ auto-test-targets := \ · 175 unmodified lines
176176 maybe_null \
177177 minimal \
178178 non_scx_kfunc_deny \
179+ nohz_tick \
179180 numa \
180181 allowed_cpus \
181182 peek_dsq \
tools/testing/selftests/sched_ext/nohz_tick.bpf.c+65−0
@@ -0,0 +1,65 @@
1+// SPDX-License-Identifier: GPL-2.0
2+/*
3+ * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
4+ *
5+ * Exercise tick dependency transitions between infinite and finite slices.
6+ */
7+#include <scx/common.bpf.h>
8+
9+char _license[] SEC("license") = "GPL";
10+
11+const volatile s32 test_cpu;
12+bool finite_phase;
13+u64 nr_inf_running;
14+u64 nr_finite_running;
15+u64 nr_finite_ticks;
16+
17+UEI_DEFINE(uei);
18+
19+s32 BPF_STRUCT_OPS(nohz_tick_select_cpu, struct task_struct *p, s32 prev_cpu,
20+ u64 wake_flags)
21+{
22+ return prev_cpu;
23+}
24+
25+void BPF_STRUCT_OPS(nohz_tick_enqueue, struct task_struct *p, u64 enq_flags)
26+{
27+ u64 slice = finite_phase ? 1000000ULL : SCX_SLICE_INF;
28+
29+ scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, slice, enq_flags);
30+ if (enq_flags & SCX_ENQ_LAST)
31+ scx_bpf_kick_cpu(test_cpu, SCX_KICK_IDLE);
32+}
33+
34+void BPF_STRUCT_OPS(nohz_tick_running, struct task_struct *p)
35+{
36+ if (bpf_get_smp_processor_id() != test_cpu)
37+ return;
38+
39+ if (finite_phase)
40+ __sync_fetch_and_add(&nr_finite_running, 1);
41+ else
42+ __sync_fetch_and_add(&nr_inf_running, 1);
43+}
44+
45+void BPF_STRUCT_OPS(nohz_tick_tick, struct task_struct *p)
46+{
47+ if (bpf_get_smp_processor_id() == test_cpu && finite_phase)
48+ __sync_fetch_and_add(&nr_finite_ticks, 1);
49+}
50+
51+void BPF_STRUCT_OPS(nohz_tick_exit, struct scx_exit_info *ei)
52+{
53+ UEI_RECORD(uei, ei);
54+}
55+
56+SEC(".struct_ops.link")
57+struct sched_ext_ops nohz_tick_ops = {
58+ .select_cpu = (void *)nohz_tick_select_cpu,
59+ .enqueue = (void *)nohz_tick_enqueue,
60+ .running = (void *)nohz_tick_running,
61+ .tick = (void *)nohz_tick_tick,
62+ .exit = (void *)nohz_tick_exit,
63+ .name = "nohz_tick",
64+ .timeout_ms = 1000U,
65+};
tools/testing/selftests/sched_ext/nohz_tick.c+347−0
@@ -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, &param))
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)