Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

sched_ext: Add a selftest for scx_bpf_dsq_peek

This commit adds two tests. The first is the most basic unit test:
make sure an empty queue peeks as empty, and when we put one element
in the queue, make sure peek returns that element.

However, even this simple test is a little complicated by the different
behavior of scx_bpf_dsq_insert in different calling contexts:
- insert is for direct dispatch in enqueue
- insert is delayed when called from select_cpu

In this case we split the insert and the peek that verifies the
result between enqueue/dispatch.

Note: An alternative would be to call `scx_bpf_dsq_move_to_local` on an
empty queue, which in turn calls `flush_dispatch_buf`, in order to flush
the buffered insert. Unfortunately, this is not viable within the
enqueue path, as it attempts a voluntary context switch within an RCU
read-side critical section.

The second test is a stress test that performs many peeks on all DSQs
and records the observed tasks.

Signed-off-by: Ryan Newton <newton@meta.com>
Reviewed-by: Christian Loehle <christian.loehle@arm.com>
Signed-off-by: Tejun Heo <tj@kernel.org>

authored by

Ryan Newton and committed by
Tejun Heo
5aff3b31 44f5c8ec

+476
+1
tools/testing/selftests/sched_ext/Makefile
··· 174 174 minimal \ 175 175 numa \ 176 176 allowed_cpus \ 177 + peek_dsq \ 177 178 prog_run \ 178 179 reload_loop \ 179 180 select_cpu_dfl \
+251
tools/testing/selftests/sched_ext/peek_dsq.bpf.c
··· 1 + // SPDX-License-Identifier: GPL-2.0 2 + /* 3 + * A BPF program for testing DSQ operations and peek in particular. 4 + * 5 + * Copyright (c) 2025 Meta Platforms, Inc. and affiliates. 6 + * Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu> 7 + */ 8 + 9 + #include <scx/common.bpf.h> 10 + #include <scx/compat.bpf.h> 11 + 12 + char _license[] SEC("license") = "GPL"; 13 + 14 + UEI_DEFINE(uei); /* Error handling */ 15 + 16 + #define MAX_SAMPLES 100 17 + #define MAX_CPUS 512 18 + #define DSQ_POOL_SIZE 8 19 + int max_samples = MAX_SAMPLES; 20 + int max_cpus = MAX_CPUS; 21 + int dsq_pool_size = DSQ_POOL_SIZE; 22 + 23 + /* Global variables to store test results */ 24 + int dsq_peek_result1 = -1; 25 + long dsq_inserted_pid = -1; 26 + int insert_test_cpu = -1; /* Set to the cpu that performs the test */ 27 + long dsq_peek_result2 = -1; 28 + long dsq_peek_result2_pid = -1; 29 + long dsq_peek_result2_expected = -1; 30 + int test_dsq_id = 1234; /* Use a simple ID like create_dsq example */ 31 + int real_dsq_id = 1235; /* DSQ for normal operation */ 32 + int enqueue_count = -1; 33 + int dispatch_count = -1; 34 + bool debug_ksym_exists; 35 + 36 + /* DSQ pool for stress testing */ 37 + int dsq_pool_base_id = 2000; 38 + int phase1_complete = -1; 39 + long total_peek_attempts = -1; 40 + long successful_peeks = -1; 41 + 42 + /* BPF map for sharing peek results with userspace */ 43 + struct { 44 + __uint(type, BPF_MAP_TYPE_ARRAY); 45 + __uint(max_entries, MAX_SAMPLES); 46 + __type(key, u32); 47 + __type(value, long); 48 + } peek_results SEC(".maps"); 49 + 50 + static int get_random_dsq_id(void) 51 + { 52 + u64 time = bpf_ktime_get_ns(); 53 + 54 + return dsq_pool_base_id + (time % DSQ_POOL_SIZE); 55 + } 56 + 57 + static void record_peek_result(long pid) 58 + { 59 + u32 slot_key; 60 + long *slot_pid_ptr; 61 + int ix; 62 + 63 + if (pid <= 0) 64 + return; 65 + 66 + /* Find an empty slot or one with the same PID */ 67 + bpf_for(ix, 0, 10) { 68 + slot_key = (pid + ix) % MAX_SAMPLES; 69 + slot_pid_ptr = bpf_map_lookup_elem(&peek_results, &slot_key); 70 + if (!slot_pid_ptr) 71 + continue; 72 + 73 + if (*slot_pid_ptr == -1 || *slot_pid_ptr == pid) { 74 + *slot_pid_ptr = pid; 75 + break; 76 + } 77 + } 78 + } 79 + 80 + /* Scan all DSQs in the pool and try to move a task to local */ 81 + static int scan_dsq_pool(void) 82 + { 83 + struct task_struct *task; 84 + int moved = 0; 85 + int i; 86 + 87 + bpf_for(i, 0, DSQ_POOL_SIZE) { 88 + int dsq_id = dsq_pool_base_id + i; 89 + 90 + total_peek_attempts++; 91 + 92 + task = __COMPAT_scx_bpf_dsq_peek(dsq_id); 93 + if (task) { 94 + successful_peeks++; 95 + record_peek_result(task->pid); 96 + 97 + /* Try to move this task to local */ 98 + if (!moved && scx_bpf_dsq_move_to_local(dsq_id) == 0) { 99 + moved = 1; 100 + break; 101 + } 102 + } 103 + } 104 + return moved; 105 + } 106 + 107 + /* Struct_ops scheduler for testing DSQ peek operations */ 108 + void BPF_STRUCT_OPS(peek_dsq_enqueue, struct task_struct *p, u64 enq_flags) 109 + { 110 + struct task_struct *peek_result; 111 + int last_insert_test_cpu, cpu; 112 + 113 + enqueue_count++; 114 + cpu = bpf_get_smp_processor_id(); 115 + last_insert_test_cpu = __sync_val_compare_and_swap(&insert_test_cpu, -1, cpu); 116 + 117 + /* Phase 1: Simple insert-then-peek test (only on first task) */ 118 + if (last_insert_test_cpu == -1) { 119 + bpf_printk("peek_dsq_enqueue beginning phase 1 peek test on cpu %d", cpu); 120 + 121 + /* Test 1: Peek empty DSQ - should return NULL */ 122 + peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id); 123 + dsq_peek_result1 = (long)peek_result; /* Should be 0 (NULL) */ 124 + 125 + /* Test 2: Insert task into test DSQ for testing in dispatch callback */ 126 + dsq_inserted_pid = p->pid; 127 + scx_bpf_dsq_insert(p, test_dsq_id, 0, enq_flags); 128 + dsq_peek_result2_expected = (long)p; /* Expected the task we just inserted */ 129 + } else if (!phase1_complete) { 130 + /* Still in phase 1, use real DSQ */ 131 + scx_bpf_dsq_insert(p, real_dsq_id, 0, enq_flags); 132 + } else { 133 + /* Phase 2: Random DSQ insertion for stress testing */ 134 + int random_dsq_id = get_random_dsq_id(); 135 + 136 + scx_bpf_dsq_insert(p, random_dsq_id, 0, enq_flags); 137 + } 138 + } 139 + 140 + void BPF_STRUCT_OPS(peek_dsq_dispatch, s32 cpu, struct task_struct *prev) 141 + { 142 + dispatch_count++; 143 + 144 + /* Phase 1: Complete the simple peek test if we inserted a task but 145 + * haven't tested peek yet 146 + */ 147 + if (insert_test_cpu == cpu && dsq_peek_result2 == -1) { 148 + struct task_struct *peek_result; 149 + 150 + bpf_printk("peek_dsq_dispatch completing phase 1 peek test on cpu %d", cpu); 151 + 152 + /* Test 3: Peek DSQ after insert - should return the task we inserted */ 153 + peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id); 154 + /* Store the PID of the peeked task for comparison */ 155 + dsq_peek_result2 = (long)peek_result; 156 + dsq_peek_result2_pid = peek_result ? peek_result->pid : -1; 157 + 158 + /* Now consume the task since we've peeked at it */ 159 + scx_bpf_dsq_move_to_local(test_dsq_id); 160 + 161 + /* Mark phase 1 as complete */ 162 + phase1_complete = 1; 163 + bpf_printk("Phase 1 complete, starting phase 2 stress testing"); 164 + } else if (!phase1_complete) { 165 + /* Still in phase 1, use real DSQ */ 166 + scx_bpf_dsq_move_to_local(real_dsq_id); 167 + } else { 168 + /* Phase 2: Scan all DSQs in the pool and try to move a task */ 169 + if (!scan_dsq_pool()) { 170 + /* No tasks found in DSQ pool, fall back to real DSQ */ 171 + scx_bpf_dsq_move_to_local(real_dsq_id); 172 + } 173 + } 174 + } 175 + 176 + s32 BPF_STRUCT_OPS_SLEEPABLE(peek_dsq_init) 177 + { 178 + s32 err; 179 + int i; 180 + 181 + /* Always set debug values so we can see which version we're using */ 182 + debug_ksym_exists = bpf_ksym_exists(scx_bpf_dsq_peek) ? 1 : 0; 183 + 184 + /* Initialize state first */ 185 + insert_test_cpu = -1; 186 + enqueue_count = 0; 187 + dispatch_count = 0; 188 + phase1_complete = 0; 189 + total_peek_attempts = 0; 190 + successful_peeks = 0; 191 + 192 + /* Create the test and real DSQs */ 193 + err = scx_bpf_create_dsq(test_dsq_id, -1); 194 + if (err) { 195 + scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err); 196 + return err; 197 + } 198 + err = scx_bpf_create_dsq(real_dsq_id, -1); 199 + if (err) { 200 + scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err); 201 + return err; 202 + } 203 + 204 + /* Create the DSQ pool for stress testing */ 205 + bpf_for(i, 0, DSQ_POOL_SIZE) { 206 + int dsq_id = dsq_pool_base_id + i; 207 + 208 + err = scx_bpf_create_dsq(dsq_id, -1); 209 + if (err) { 210 + scx_bpf_error("Failed to create DSQ pool entry %d: %d", dsq_id, err); 211 + return err; 212 + } 213 + } 214 + 215 + /* Initialize the peek results map */ 216 + bpf_for(i, 0, MAX_SAMPLES) { 217 + u32 key = i; 218 + long pid = -1; 219 + 220 + bpf_map_update_elem(&peek_results, &key, &pid, BPF_ANY); 221 + } 222 + 223 + return 0; 224 + } 225 + 226 + void BPF_STRUCT_OPS(peek_dsq_exit, struct scx_exit_info *ei) 227 + { 228 + int i; 229 + 230 + /* Destroy the primary DSQs */ 231 + scx_bpf_destroy_dsq(test_dsq_id); 232 + scx_bpf_destroy_dsq(real_dsq_id); 233 + 234 + /* Destroy the DSQ pool */ 235 + bpf_for(i, 0, DSQ_POOL_SIZE) { 236 + int dsq_id = dsq_pool_base_id + i; 237 + 238 + scx_bpf_destroy_dsq(dsq_id); 239 + } 240 + 241 + UEI_RECORD(uei, ei); 242 + } 243 + 244 + SEC(".struct_ops.link") 245 + struct sched_ext_ops peek_dsq_ops = { 246 + .enqueue = (void *)peek_dsq_enqueue, 247 + .dispatch = (void *)peek_dsq_dispatch, 248 + .init = (void *)peek_dsq_init, 249 + .exit = (void *)peek_dsq_exit, 250 + .name = "peek_dsq", 251 + };
+224
tools/testing/selftests/sched_ext/peek_dsq.c
··· 1 + // SPDX-License-Identifier: GPL-2.0 2 + /* 3 + * Test for DSQ operations including create, destroy, and peek operations. 4 + * 5 + * Copyright (c) 2025 Meta Platforms, Inc. and affiliates. 6 + * Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu> 7 + */ 8 + #include <bpf/bpf.h> 9 + #include <scx/common.h> 10 + #include <sys/wait.h> 11 + #include <unistd.h> 12 + #include <pthread.h> 13 + #include <string.h> 14 + #include <sched.h> 15 + #include "peek_dsq.bpf.skel.h" 16 + #include "scx_test.h" 17 + 18 + #define NUM_WORKERS 4 19 + 20 + static bool workload_running = true; 21 + static pthread_t workload_threads[NUM_WORKERS]; 22 + 23 + /** 24 + * Background workload thread that sleeps and wakes rapidly to exercise 25 + * the scheduler's enqueue operations and ensure DSQ operations get tested. 26 + */ 27 + static void *workload_thread_fn(void *arg) 28 + { 29 + while (workload_running) { 30 + /* Sleep for a very short time to trigger scheduler activity */ 31 + usleep(1000); /* 1ms sleep */ 32 + /* Yield to ensure we go through the scheduler */ 33 + sched_yield(); 34 + } 35 + return NULL; 36 + } 37 + 38 + static enum scx_test_status setup(void **ctx) 39 + { 40 + struct peek_dsq *skel; 41 + 42 + skel = peek_dsq__open(); 43 + SCX_FAIL_IF(!skel, "Failed to open"); 44 + SCX_ENUM_INIT(skel); 45 + SCX_FAIL_IF(peek_dsq__load(skel), "Failed to load skel"); 46 + 47 + *ctx = skel; 48 + 49 + return SCX_TEST_PASS; 50 + } 51 + 52 + static int print_observed_pids(struct bpf_map *map, int max_samples, const char *dsq_name) 53 + { 54 + long count = 0; 55 + 56 + printf("Observed %s DSQ peek pids:\n", dsq_name); 57 + for (int i = 0; i < max_samples; i++) { 58 + long pid; 59 + int err; 60 + 61 + err = bpf_map_lookup_elem(bpf_map__fd(map), &i, &pid); 62 + if (err == 0) { 63 + if (pid == 0) { 64 + printf(" Sample %d: NULL peek\n", i); 65 + } else if (pid > 0) { 66 + printf(" Sample %d: pid %ld\n", i, pid); 67 + count++; 68 + } 69 + } else { 70 + printf(" Sample %d: error reading pid (err=%d)\n", i, err); 71 + } 72 + } 73 + printf("Observed ~%ld pids in the %s DSQ(s)\n", count, dsq_name); 74 + return count; 75 + } 76 + 77 + static enum scx_test_status run(void *ctx) 78 + { 79 + struct peek_dsq *skel = ctx; 80 + bool failed = false; 81 + int seconds = 3; 82 + int err; 83 + 84 + /* Enable the scheduler to test DSQ operations */ 85 + printf("Enabling scheduler to test DSQ insert operations...\n"); 86 + 87 + struct bpf_link *link = 88 + bpf_map__attach_struct_ops(skel->maps.peek_dsq_ops); 89 + 90 + if (!link) { 91 + SCX_ERR("Failed to attach struct_ops"); 92 + return SCX_TEST_FAIL; 93 + } 94 + 95 + printf("Starting %d background workload threads...\n", NUM_WORKERS); 96 + workload_running = true; 97 + for (int i = 0; i < NUM_WORKERS; i++) { 98 + err = pthread_create(&workload_threads[i], NULL, workload_thread_fn, NULL); 99 + if (err) { 100 + SCX_ERR("Failed to create workload thread %d: %s", i, strerror(err)); 101 + /* Stop already created threads */ 102 + workload_running = false; 103 + for (int j = 0; j < i; j++) 104 + pthread_join(workload_threads[j], NULL); 105 + bpf_link__destroy(link); 106 + return SCX_TEST_FAIL; 107 + } 108 + } 109 + 110 + printf("Waiting for enqueue events.\n"); 111 + sleep(seconds); 112 + while (skel->data->enqueue_count <= 0) { 113 + printf("."); 114 + fflush(stdout); 115 + sleep(1); 116 + seconds++; 117 + if (seconds >= 30) { 118 + printf("\n\u2717 Timeout waiting for enqueue events\n"); 119 + /* Stop workload threads and cleanup */ 120 + workload_running = false; 121 + for (int i = 0; i < NUM_WORKERS; i++) 122 + pthread_join(workload_threads[i], NULL); 123 + bpf_link__destroy(link); 124 + return SCX_TEST_FAIL; 125 + } 126 + } 127 + 128 + workload_running = false; 129 + for (int i = 0; i < NUM_WORKERS; i++) { 130 + err = pthread_join(workload_threads[i], NULL); 131 + if (err) { 132 + SCX_ERR("Failed to join workload thread %d: %s", i, strerror(err)); 133 + bpf_link__destroy(link); 134 + return SCX_TEST_FAIL; 135 + } 136 + } 137 + printf("Background workload threads stopped.\n"); 138 + 139 + SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE)); 140 + 141 + /* Detach the scheduler */ 142 + bpf_link__destroy(link); 143 + 144 + printf("Enqueue/dispatch count over %d seconds: %d / %d\n", seconds, 145 + skel->data->enqueue_count, skel->data->dispatch_count); 146 + printf("Debug: ksym_exists=%d\n", 147 + skel->bss->debug_ksym_exists); 148 + 149 + /* Check DSQ insert result */ 150 + printf("DSQ insert test done on cpu: %d\n", skel->data->insert_test_cpu); 151 + if (skel->data->insert_test_cpu != -1) 152 + printf("\u2713 DSQ insert succeeded !\n"); 153 + else { 154 + printf("\u2717 DSQ insert failed or not attempted\n"); 155 + failed = true; 156 + } 157 + 158 + /* Check DSQ peek results */ 159 + printf(" DSQ peek result 1 (before insert): %d\n", 160 + skel->data->dsq_peek_result1); 161 + if (skel->data->dsq_peek_result1 == 0) 162 + printf("\u2713 DSQ peek verification success: peek returned NULL!\n"); 163 + else { 164 + printf("\u2717 DSQ peek verification failed\n"); 165 + failed = true; 166 + } 167 + 168 + printf(" DSQ peek result 2 (after insert): %ld\n", 169 + skel->data->dsq_peek_result2); 170 + printf(" DSQ peek result 2, expected: %ld\n", 171 + skel->data->dsq_peek_result2_expected); 172 + if (skel->data->dsq_peek_result2 == 173 + skel->data->dsq_peek_result2_expected) 174 + printf("\u2713 DSQ peek verification success: peek returned the inserted task!\n"); 175 + else { 176 + printf("\u2717 DSQ peek verification failed\n"); 177 + failed = true; 178 + } 179 + 180 + printf(" Inserted test task -> pid: %ld\n", skel->data->dsq_inserted_pid); 181 + printf(" DSQ peek result 2 -> pid: %ld\n", skel->data->dsq_peek_result2_pid); 182 + 183 + int pid_count; 184 + 185 + pid_count = print_observed_pids(skel->maps.peek_results, 186 + skel->data->max_samples, "DSQ pool"); 187 + printf("Total non-null peek observations: %ld out of %ld\n", 188 + skel->data->successful_peeks, skel->data->total_peek_attempts); 189 + 190 + if (skel->bss->debug_ksym_exists && pid_count == 0) { 191 + printf("\u2717 DSQ pool test failed: no successful peeks in native mode\n"); 192 + failed = true; 193 + } 194 + if (skel->bss->debug_ksym_exists && pid_count > 0) 195 + printf("\u2713 DSQ pool test success: observed successful peeks in native mode\n"); 196 + 197 + if (failed) 198 + return SCX_TEST_FAIL; 199 + else 200 + return SCX_TEST_PASS; 201 + } 202 + 203 + static void cleanup(void *ctx) 204 + { 205 + struct peek_dsq *skel = ctx; 206 + 207 + if (workload_running) { 208 + workload_running = false; 209 + for (int i = 0; i < NUM_WORKERS; i++) 210 + pthread_join(workload_threads[i], NULL); 211 + } 212 + 213 + peek_dsq__destroy(skel); 214 + } 215 + 216 + struct scx_test peek_dsq = { 217 + .name = "peek_dsq", 218 + .description = 219 + "Test DSQ create/destroy operations and future peek functionality", 220 + .setup = setup, 221 + .run = run, 222 + .cleanup = cleanup, 223 + }; 224 + REGISTER_SCX_TEST(&peek_dsq)