1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
|
// SPDX-License-Identifier: GPL-2.0
/*
* Check that a task which leaves SCHED_EXT while it's waiting doesn't carry
* that wait over to its next stay on SCHED_EXT. If it did, the watchdog would
* count the time the task spent outside SCHED_EXT as waiting and abort the
* scheduler.
*
* Copyright (c) 2026 Google LLC.
*/
#define _GNU_SOURCE
#include <bpf/bpf.h>
#include <poll.h>
#include <sched.h>
#include <scx/common.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "class_switch_stall.bpf.skel.h"
#include "scx_test.h"
#ifndef SCHED_EXT
#define SCHED_EXT 7
#endif
/* watchdog timeout, the watchdog checks every half of it */
#define TIMEOUT_MS 3000
/* longer than the watchdog interval, but shorter than the timeout */
#define PARK_MS 2000
/* time spent outside of SCHED_EXT, a stale wait would exceed the timeout */
#define OUTSIDE_MS (TIMEOUT_MS + 1000)
static enum scx_test_status setup(void **ctx)
{
struct class_switch_stall *skel;
skel = class_switch_stall__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
skel->struct_ops.class_switch_stall_ops->timeout_ms = TIMEOUT_MS;
SCX_FAIL_IF(class_switch_stall__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
/* Echoes back each byte from @in_fd. */
static void child_main(int in_fd, int out_fd)
{
char c;
while (read(in_fd, &c, 1) == 1) {
if (write(out_fd, &c, 1) != 1)
_exit(1);
}
_exit(0);
}
static bool read_byte(int fd, int timeout_ms)
{
struct pollfd pfd = { .fd = fd, .events = POLLIN };
char c;
return poll(&pfd, 1, timeout_ms) == 1 && read(fd, &c, 1) == 1;
}
/* Waits for up to 1s for @pid to be in interruptible sleep. */
static bool wait_for_sleeping(pid_t pid)
{
char path[64], buf[512], *state;
bool sleeping;
FILE *f;
int i;
snprintf(path, sizeof(path), "/proc/%d/stat", pid);
for (i = 0; i < 1000; i++) {
f = fopen(path, "r");
if (!f)
return false;
/* The state follows "(comm)", and comm may contain ')'. */
state = fgets(buf, sizeof(buf), f) ? strrchr(buf, ')') : NULL;
sleeping = state && !strncmp(state, ") S", 3);
fclose(f);
if (sleeping)
return true;
usleep(1000);
}
return false;
}
/* Waits for up to 1s for the target to have been parked @cnt times in total. */
static bool wait_for_parked(struct class_switch_stall *skel, u64 cnt)
{
int i;
for (i = 0; i < 1000; i++) {
if (skel->bss->nr_parked >= cnt)
return true;
usleep(1000);
}
return false;
}
static bool set_policy(pid_t pid, int policy)
{
struct sched_param param = {};
return !sched_setscheduler(pid, policy, ¶m);
}
static enum scx_test_status run(void *ctx)
{
struct class_switch_stall *skel = ctx;
enum scx_test_status status = SCX_TEST_FAIL;
int to_child[2] = { -1, -1 }, from_child[2] = { -1, -1 };
struct bpf_link *link = NULL;
pid_t pid = -1;
int i;
if (pipe(to_child) || pipe(from_child)) {
SCX_ERR("Failed to create pipes");
goto out;
}
link = bpf_map__attach_struct_ops(skel->maps.class_switch_stall_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
goto out;
}
pid = fork();
if (pid == 0)
child_main(to_child[0], from_child[1]);
if (pid < 0) {
SCX_ERR("Failed to fork");
goto out;
}
skel->bss->target_pid = pid;
if (!wait_for_sleeping(pid) || !set_policy(pid, SCHED_EXT)) {
SCX_ERR("Failed to switch the child to SCHED_EXT");
goto out;
}
/*
* Keep the child waiting on the DSQ after it wakes up, and switch it to
* SCHED_OTHER while it's still waiting.
*/
skel->bss->park_ns = 0;
skel->bss->park = true;
if (write(to_child[1], "a", 1) != 1 || !wait_for_parked(skel, 1)) {
SCX_ERR("The child wasn't parked");
goto out;
}
if (!set_policy(pid, SCHED_OTHER) || !read_byte(from_child[0], 1000)) {
SCX_ERR("The child didn't run after leaving SCHED_EXT");
goto out;
}
/* Let time pass while the child sleeps outside of SCHED_EXT. */
if (!wait_for_sleeping(pid)) {
SCX_ERR("The child didn't block in read()");
goto out;
}
usleep(OUTSIDE_MS * 1000);
/*
* Bring the child back and keep it waiting for longer than the watchdog
* interval, but shorter than the timeout. Its wait must start now, not
* when it started waiting before it left SCHED_EXT.
*/
if (!set_policy(pid, SCHED_EXT)) {
SCX_ERR("Failed to switch the child back to SCHED_EXT");
goto out;
}
skel->bss->park_ns = PARK_MS * 1000000ULL;
skel->bss->park = true;
if (write(to_child[1], "b", 1) != 1 || !wait_for_parked(skel, 2)) {
SCX_ERR("The child wasn't parked again");
goto out;
}
/*
* A stall is detected up to a watchdog interval after the timeout. Wait
* long enough for it to be reported by the exit info below.
*/
if (!read_byte(from_child[0], 2 * TIMEOUT_MS)) {
SCX_ERR("The child didn't run after the second wait");
goto out;
}
/*
* Only the watchdog aborting the scheduler lets the child run before
* it's released. The exit info is recorded asynchronously, wait for it.
*/
if (skel->bss->nr_released != 1)
SCX_TIMED_WAIT_UEI(skel, uei, 1000);
if (UEI_EXITED(skel, uei)) {
SCX_ERR("Scheduler exited (kind=%llu): %s",
(unsigned long long)skel->data->uei.kind,
skel->data->uei.msg);
goto out;
}
if (skel->bss->nr_released != 1) {
SCX_ERR("The child was released %llu times, expected 1",
(unsigned long long)skel->bss->nr_released);
goto out;
}
status = SCX_TEST_PASS;
out:
if (pid > 0) {
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
}
if (link)
bpf_link__destroy(link);
for (i = 0; i < 2; i++) {
if (to_child[i] >= 0)
close(to_child[i]);
if (from_child[i] >= 0)
close(from_child[i]);
}
return status;
}
static void cleanup(void *ctx)
{
struct class_switch_stall *skel = ctx;
class_switch_stall__destroy(skel);
}
struct scx_test class_switch_stall = {
.name = "class_switch_stall",
.description = "Verify that leaving SCHED_EXT ends a task's wait",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&class_switch_stall)
|