1 /* SPDX-License-Identifier: GPL-2.0 */
2 /*
3 * A demo sched_ext user space scheduler which provides vruntime semantics
4 * using a simple ordered-list implementation.
5 *
6 * Each CPU in the system resides in a single, global domain. This precludes
7 * the need to do any load balancing between domains. The scheduler could
8 * easily be extended to support multiple domains, with load balancing
9 * happening in user space.
10 *
11 * Any task which has any CPU affinity is scheduled entirely in BPF. This
12 * program only schedules tasks which may run on any CPU.
13 *
14 * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
15 * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
16 * Copyright (c) 2022 David Vernet <dvernet@meta.com>
17 */
18 #include <stdio.h>
19 #include <unistd.h>
20 #include <sched.h>
21 #include <signal.h>
22 #include <assert.h>
23 #include <libgen.h>
24 #include <pthread.h>
25 #include <bpf/bpf.h>
26 #include <sys/mman.h>
27 #include <sys/queue.h>
28 #include <sys/syscall.h>
29
30 #include <scx/common.h>
31 #include "scx_userland.h"
32 #include "scx_userland.bpf.skel.h"
33
34 const char help_fmt[] =
35 "A minimal userland sched_ext scheduler.\n"
36 "\n"
37 "See the top-level comment in .bpf.c for more details.\n"
38 "\n"
39 "Try to reduce `sysctl kernel.pid_max` if this program triggers OOMs.\n"
40 "\n"
41 "Usage: %s [-b BATCH] [-v]\n"
42 "\n"
43 " -b BATCH The number of tasks to batch when dispatching (default: 8)\n"
44 " -v Print libbpf debug messages\n"
45 " -h Display this help and exit\n";
46
47 /* Defined in UAPI */
48 #define SCHED_EXT 7
49
50 /* Number of tasks to batch when dispatching to user space. */
51 static __u32 batch_size = 8;
52
53 static bool verbose;
54 static volatile int exit_req;
55 static volatile int stats_stop;
56 static int enqueued_fd, dispatched_fd;
57
58 static pthread_t stats_printer;
59 static struct scx_userland *skel;
60 static struct bpf_link *ops_link;
61
62 /* Stats collected in user space. */
63 static __u64 nr_vruntime_enqueues, nr_vruntime_dispatches, nr_vruntime_failed;
64
65 /* Number of tasks currently enqueued. */
66 static __u64 nr_curr_enqueued;
67
68 /* The data structure containing tasks that are enqueued in user space. */
69 struct enqueued_task {
70 LIST_ENTRY(enqueued_task) entries;
71 __u64 sum_exec_runtime;
72 double vruntime;
73 };
74
75 /*
76 * Use a vruntime-sorted list to store tasks. This could easily be extended to
77 * a more optimal data structure, such as an rbtree as is done in CFS. We
78 * currently elect to use a sorted list to simplify the example for
79 * illustrative purposes.
80 */
81 LIST_HEAD(listhead, enqueued_task);
82
83 /*
84 * A vruntime-sorted list of tasks. The head of the list contains the task with
85 * the lowest vruntime. That is, the task that has the "highest" claim to be
86 * scheduled.
87 */
88 static struct listhead vruntime_head = LIST_HEAD_INITIALIZER(vruntime_head);
89
90 /*
91 * The main array of tasks. The array is allocated all at once during
92 * initialization, based on /proc/sys/kernel/pid_max, to avoid having to
93 * dynamically allocate memory on the enqueue path, which could cause a
94 * deadlock. A more substantive user space scheduler could e.g. provide a hook
95 * for newly enabled tasks that are passed to the scheduler from the
96 * .prep_enable() callback to allows the scheduler to allocate on safe paths.
97 */
98 struct enqueued_task *tasks;
99 static int pid_max;
100
101 static double min_vruntime;
102
libbpf_print_fn(enum libbpf_print_level level,const char * format,va_list args)103 static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
104 {
105 if (level == LIBBPF_DEBUG && !verbose)
106 return 0;
107 return vfprintf(stderr, format, args);
108 }
109
sigint_handler(int userland)110 static void sigint_handler(int userland)
111 {
112 exit_req = 1;
113 }
114
get_pid_max(void)115 static int get_pid_max(void)
116 {
117 FILE *fp;
118 int pid_max;
119
120 fp = fopen("/proc/sys/kernel/pid_max", "r");
121 if (fp == NULL) {
122 fprintf(stderr, "Error opening /proc/sys/kernel/pid_max\n");
123 return -1;
124 }
125 if (fscanf(fp, "%d", &pid_max) != 1) {
126 fprintf(stderr, "Error reading from /proc/sys/kernel/pid_max\n");
127 fclose(fp);
128 return -1;
129 }
130 fclose(fp);
131
132 return pid_max;
133 }
134
init_tasks(void)135 static int init_tasks(void)
136 {
137 pid_max = get_pid_max();
138 if (pid_max < 0)
139 return pid_max;
140
141 tasks = calloc(pid_max, sizeof(*tasks));
142 if (!tasks) {
143 fprintf(stderr, "Error allocating tasks array\n");
144 return -ENOMEM;
145 }
146
147 return 0;
148 }
149
task_pid(const struct enqueued_task * task)150 static __u32 task_pid(const struct enqueued_task *task)
151 {
152 return ((uintptr_t)task - (uintptr_t)tasks) / sizeof(*task);
153 }
154
dispatch_task(__s32 pid)155 static int dispatch_task(__s32 pid)
156 {
157 int err;
158
159 err = bpf_map_update_elem(dispatched_fd, NULL, &pid, 0);
160 if (err) {
161 __atomic_add_fetch(&nr_vruntime_failed, 1, __ATOMIC_RELAXED);
162 } else {
163 __atomic_add_fetch(&nr_vruntime_dispatches, 1, __ATOMIC_RELAXED);
164 }
165
166 return err;
167 }
168
get_enqueued_task(__s32 pid)169 static struct enqueued_task *get_enqueued_task(__s32 pid)
170 {
171 if (pid >= pid_max)
172 return NULL;
173
174 return &tasks[pid];
175 }
176
calc_vruntime_delta(__u64 weight,__u64 delta)177 static double calc_vruntime_delta(__u64 weight, __u64 delta)
178 {
179 double weight_f = (double)weight / 100.0;
180 double delta_f = (double)delta;
181
182 return delta_f / weight_f;
183 }
184
update_enqueued(struct enqueued_task * enqueued,const struct scx_userland_enqueued_task * bpf_task)185 static void update_enqueued(struct enqueued_task *enqueued, const struct scx_userland_enqueued_task *bpf_task)
186 {
187 __u64 delta;
188
189 delta = bpf_task->sum_exec_runtime - enqueued->sum_exec_runtime;
190
191 enqueued->vruntime += calc_vruntime_delta(bpf_task->weight, delta);
192 if (min_vruntime > enqueued->vruntime)
193 enqueued->vruntime = min_vruntime;
194 enqueued->sum_exec_runtime = bpf_task->sum_exec_runtime;
195 }
196
vruntime_enqueue(const struct scx_userland_enqueued_task * bpf_task)197 static int vruntime_enqueue(const struct scx_userland_enqueued_task *bpf_task)
198 {
199 struct enqueued_task *curr, *enqueued, *prev;
200
201 curr = get_enqueued_task(bpf_task->pid);
202 if (!curr)
203 return ENOENT;
204
205 update_enqueued(curr, bpf_task);
206 __atomic_add_fetch(&nr_vruntime_enqueues, 1, __ATOMIC_RELAXED);
207 __atomic_add_fetch(&nr_curr_enqueued, 1, __ATOMIC_RELAXED);
208
209 /*
210 * Enqueue the task in a vruntime-sorted list. A more optimal data
211 * structure such as an rbtree could easily be used as well. We elect
212 * to use a list here simply because it's less code, and thus the
213 * example is less convoluted and better serves to illustrate what a
214 * user space scheduler could look like.
215 */
216
217 if (LIST_EMPTY(&vruntime_head)) {
218 LIST_INSERT_HEAD(&vruntime_head, curr, entries);
219 return 0;
220 }
221
222 LIST_FOREACH(enqueued, &vruntime_head, entries) {
223 if (curr->vruntime <= enqueued->vruntime) {
224 LIST_INSERT_BEFORE(enqueued, curr, entries);
225 return 0;
226 }
227 prev = enqueued;
228 }
229
230 LIST_INSERT_AFTER(prev, curr, entries);
231
232 return 0;
233 }
234
drain_enqueued_map(void)235 static void drain_enqueued_map(void)
236 {
237 while (1) {
238 struct scx_userland_enqueued_task task;
239 int err;
240
241 if (bpf_map_lookup_and_delete_elem(enqueued_fd, NULL, &task)) {
242 skel->bss->nr_queued = 0;
243 skel->bss->nr_scheduled = nr_curr_enqueued;
244 return;
245 }
246
247 err = vruntime_enqueue(&task);
248 if (err) {
249 fprintf(stderr, "Failed to enqueue task %d: %s\n",
250 task.pid, strerror(err));
251 exit_req = 1;
252 return;
253 }
254 }
255 }
256
dispatch_batch(void)257 static void dispatch_batch(void)
258 {
259 __u32 i;
260
261 for (i = 0; i < batch_size; i++) {
262 struct enqueued_task *task;
263 int err;
264 __s32 pid;
265
266 task = LIST_FIRST(&vruntime_head);
267 if (!task)
268 break;
269
270 min_vruntime = task->vruntime;
271 pid = task_pid(task);
272 LIST_REMOVE(task, entries);
273 err = dispatch_task(pid);
274 if (err) {
275 /*
276 * If we fail to dispatch, put the task back to the
277 * vruntime_head list and stop dispatching additional
278 * tasks in this batch.
279 */
280 LIST_INSERT_HEAD(&vruntime_head, task, entries);
281 break;
282 }
283 __atomic_sub_fetch(&nr_curr_enqueued, 1, __ATOMIC_RELAXED);
284 }
285 skel->bss->nr_scheduled = __atomic_load_n(&nr_curr_enqueued, __ATOMIC_RELAXED);
286 }
287
run_stats_printer(void * arg)288 static void *run_stats_printer(void *arg)
289 {
290 while (!stats_stop) {
291 __u64 nr_failed_enqueues, nr_kernel_enqueues, nr_user_enqueues, total;
292
293 nr_failed_enqueues = skel->bss->nr_failed_enqueues;
294 nr_kernel_enqueues = skel->bss->nr_kernel_enqueues;
295 nr_user_enqueues = skel->bss->nr_user_enqueues;
296 total = nr_failed_enqueues + nr_kernel_enqueues + nr_user_enqueues;
297
298 printf("o-----------------------o\n");
299 printf("| BPF ENQUEUES |\n");
300 printf("|-----------------------|\n");
301 printf("| kern: %10llu |\n", nr_kernel_enqueues);
302 printf("| user: %10llu |\n", nr_user_enqueues);
303 printf("| failed: %10llu |\n", nr_failed_enqueues);
304 printf("| -------------------- |\n");
305 printf("| total: %10llu |\n", total);
306 printf("| |\n");
307 printf("|-----------------------|\n");
308 printf("| VRUNTIME / USER |\n");
309 printf("|-----------------------|\n");
310 printf("| enq: %10llu |\n", __atomic_load_n(&nr_vruntime_enqueues, __ATOMIC_RELAXED));
311 printf("| disp: %10llu |\n", __atomic_load_n(&nr_vruntime_dispatches, __ATOMIC_RELAXED));
312 printf("| failed: %10llu |\n", __atomic_load_n(&nr_vruntime_failed, __ATOMIC_RELAXED));
313 printf("o-----------------------o\n");
314 printf("\n\n");
315 fflush(stdout);
316 sleep(1);
317 }
318
319 return NULL;
320 }
321
spawn_stats_thread(void)322 static int spawn_stats_thread(void)
323 {
324 return pthread_create(&stats_printer, NULL, run_stats_printer, NULL);
325 }
326
pre_bootstrap(int argc,char ** argv)327 static void pre_bootstrap(int argc, char **argv)
328 {
329 int err;
330 __s32 opt;
331 struct sched_param sched_param = {
332 .sched_priority = sched_get_priority_max(SCHED_EXT),
333 };
334
335 err = init_tasks();
336 if (err)
337 exit(err);
338
339 libbpf_set_print(libbpf_print_fn);
340 signal(SIGINT, sigint_handler);
341 signal(SIGTERM, sigint_handler);
342
343 /*
344 * Enforce that the user scheduler task is managed by sched_ext. The
345 * task eagerly drains the list of enqueued tasks in its main work
346 * loop, and then yields the CPU. The BPF scheduler only schedules the
347 * user space scheduler task when at least one other task in the system
348 * needs to be scheduled.
349 */
350 err = syscall(__NR_sched_setscheduler, getpid(), SCHED_EXT, &sched_param);
351 SCX_BUG_ON(err, "Failed to set scheduler to SCHED_EXT");
352
353 while ((opt = getopt(argc, argv, "b:vh")) != -1) {
354 switch (opt) {
355 case 'b':
356 batch_size = strtoul(optarg, NULL, 0);
357 break;
358 case 'v':
359 verbose = true;
360 break;
361 default:
362 fprintf(stderr, help_fmt, basename(argv[0]));
363 exit(opt != 'h');
364 }
365 }
366
367 /*
368 * It's not always safe to allocate in a user space scheduler, as an
369 * enqueued task could hold a lock that we require in order to be able
370 * to allocate.
371 */
372 err = mlockall(MCL_CURRENT | MCL_FUTURE);
373 SCX_BUG_ON(err, "Failed to prefault and lock address space");
374 }
375
bootstrap(char * comm)376 static void bootstrap(char *comm)
377 {
378 stats_stop = 0;
379 min_vruntime = 0.0;
380 __atomic_store_n(&nr_vruntime_enqueues, 0, __ATOMIC_RELAXED);
381 __atomic_store_n(&nr_vruntime_dispatches, 0, __ATOMIC_RELAXED);
382 __atomic_store_n(&nr_vruntime_failed, 0, __ATOMIC_RELAXED);
383 __atomic_store_n(&nr_curr_enqueued, 0, __ATOMIC_RELAXED);
384 memset(tasks, 0, pid_max * sizeof(*tasks));
385 LIST_INIT(&vruntime_head);
386
387 skel = SCX_OPS_OPEN(userland_ops, scx_userland);
388
389 skel->rodata->num_possible_cpus = libbpf_num_possible_cpus();
390 assert(skel->rodata->num_possible_cpus > 0);
391 skel->rodata->usersched_pid = getpid();
392 assert(skel->rodata->usersched_pid > 0);
393
394 SCX_OPS_LOAD(skel, userland_ops, scx_userland, uei);
395
396 enqueued_fd = bpf_map__fd(skel->maps.enqueued);
397 dispatched_fd = bpf_map__fd(skel->maps.dispatched);
398 assert(enqueued_fd > 0);
399 assert(dispatched_fd > 0);
400
401 SCX_BUG_ON(spawn_stats_thread(), "Failed to spawn stats thread");
402
403 ops_link = SCX_OPS_ATTACH(skel, userland_ops, scx_userland);
404 }
405
sched_main_loop(void)406 static void sched_main_loop(void)
407 {
408 while (!exit_req && !UEI_EXITED(skel, uei)) {
409 /*
410 * Perform the following work in the main user space scheduler
411 * loop:
412 *
413 * 1. Drain all tasks from the enqueued map, and enqueue them
414 * to the vruntime sorted list.
415 *
416 * 2. Dispatch a batch of tasks from the vruntime sorted list
417 * down to the kernel.
418 *
419 * 3. Yield the CPU back to the system. The BPF scheduler will
420 * reschedule the user space scheduler once another task has
421 * been enqueued to user space.
422 */
423 drain_enqueued_map();
424 dispatch_batch();
425 sched_yield();
426 }
427 }
428
main(int argc,char ** argv)429 int main(int argc, char **argv)
430 {
431 __u64 ecode;
432
433 pre_bootstrap(argc, argv);
434 restart:
435 bootstrap(argv[0]);
436 sched_main_loop();
437
438 stats_stop = 1;
439 bpf_link__destroy(ops_link);
440 pthread_join(stats_printer, NULL);
441 ecode = UEI_REPORT(skel, uei);
442 scx_userland__destroy(skel);
443
444 if (!exit_req && UEI_ECODE_RESTART(ecode))
445 goto restart;
446 return 0;
447 }
448