| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES | |
| */ | |
| #define _GNU_SOURCE | |
| #include <bpf/bpf.h> | |
| #include <errno.h> | |
| #include <linux/sched.h> | |
| #include <sched.h> | |
| #include <signal.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/prctl.h> | |
| #include <sys/wait.h> | |
| #include <unistd.h> | |
| #include <scx/common.h> | |
| #include "kick.bpf.skel.h" | |
| #include "kick_test.h" | |
| #include "scx_test.h" | |
| #include "util.h" | |
| #define WAIT_LOOPS 3000 | |
| struct observation { | |
| u64 slice_before; | |
| u64 slice_at_resched; | |
| u64 nr_wait_callbacks; | |
| s32 resched_tif; | |
| }; | |
| struct kick_ctx { | |
| cpu_set_t original_mask; | |
| int target_cpu; | |
| }; | |
| static bool enum_supported(const char *type, const char *name) | |
| { | |
| u64 value; | |
| return __COMPAT_read_enum(type, name, &value); | |
| } | |
| static enum scx_test_status setup_controller(void **ctx_ptr) | |
| { | |
| struct kick_ctx *ctx; | |
| cpu_set_t controller_mask; | |
| int cpu, controller_cpu = -1; | |
| ctx = calloc(1, sizeof(*ctx)); | |
| SCX_FAIL_IF(!ctx, "Failed to allocate context"); | |
| if (sched_getaffinity(0, sizeof(ctx->original_mask), &ctx->original_mask)) { | |
| free(ctx); | |
| SCX_FAIL("Failed to get affinity (%d)", errno); | |
| } | |
| for (cpu = 0; cpu < CPU_SETSIZE; cpu++) { | |
| if (!CPU_ISSET(cpu, &ctx->original_mask)) | |
| continue; | |
| if (controller_cpu < 0) { | |
| controller_cpu = cpu; | |
| } else { | |
| ctx->target_cpu = cpu; | |
| break; | |
| } | |
| } | |
| if (cpu == CPU_SETSIZE) { | |
| printf("SKIP: two allowed CPUs are required\n"); | |
| free(ctx); | |
| return SCX_TEST_SKIP; | |
| } | |
| CPU_ZERO(&controller_mask); | |
| CPU_SET(controller_cpu, &controller_mask); | |
| if (sched_setaffinity(0, sizeof(controller_mask), &controller_mask)) { | |
| free(ctx); | |
| SCX_FAIL("Failed to pin controller to CPU %d (%d)", controller_cpu, errno); | |
| } | |
| *ctx_ptr = ctx; | |
| return SCX_TEST_PASS; | |
| } | |
| static void cleanup_controller(void *ctx_ptr) | |
| { | |
| struct kick_ctx *ctx = ctx_ptr; | |
| sched_setaffinity(0, sizeof(ctx->original_mask), &ctx->original_mask); | |
| free(ctx); | |
| } | |
| static bool wait_for_state(struct kick *skel, u32 wanted) | |
| { | |
| int i; | |
| for (i = 0; i < WAIT_LOOPS; i++) { | |
| if (__atomic_load_n(&skel->bss->state, __ATOMIC_ACQUIRE) == wanted) | |
| return true; | |
| if (UEI_EXITED(skel, uei)) | |
| return false; | |
| usleep(1000); | |
| } | |
| return false; | |
| } | |
| static bool wait_for_counter(struct kick *skel, const u64 *counter, u64 wanted) | |
| { | |
| int i; | |
| for (i = 0; i < WAIT_LOOPS; i++) { | |
| if (__atomic_load_n(counter, __ATOMIC_ACQUIRE) >= wanted) | |
| return true; | |
| if (UEI_EXITED(skel, uei)) | |
| return false; | |
| usleep(1000); | |
| } | |
| return false; | |
| } | |
| static enum scx_test_status trace_one(struct kick_ctx *ctx, u32 scenario, u64 ops_flags, | |
| s32 lazy_resched_override, struct observation *obs) | |
| { | |
| struct bpf_link *ops_link = NULL; | |
| struct kick *skel = NULL; | |
| struct scx_test_gated_worker challenger = { .pid = -1, .start_fd = -1 }; | |
| struct scx_test_gated_worker victim; | |
| enum scx_test_status ret = SCX_TEST_FAIL; | |
| int cpu = ctx->target_cpu; | |
| victim = scx_test_spawn_gated_worker(cpu, false); | |
| if (victim.pid < 0) { | |
| SCX_ERR("Failed to spawn victim"); | |
| return SCX_TEST_FAIL; | |
| } | |
| if (scenario == ENQ_BOTH) { | |
| challenger = scx_test_spawn_gated_worker(cpu, false); | |
| if (challenger.pid < 0) { | |
| SCX_ERR("Failed to spawn enqueue challenger"); | |
| goto out; | |
| } | |
| } | |
| skel = kick__open(); | |
| if (!skel) { | |
| SCX_ERR("Failed to open scenario %u", scenario); | |
| goto out; | |
| } | |
| SCX_ENUM_INIT(skel); | |
| skel->rodata->scenario = scenario; | |
| skel->rodata->victim_pid = victim.pid; | |
| skel->rodata->challenger_pid = challenger.pid; | |
| skel->rodata->target_cpu = cpu; | |
| skel->rodata->lazy_resched_override = lazy_resched_override; | |
| skel->struct_ops.kick_ops->flags |= ops_flags; | |
| if (scenario != KICK_LAZY_WAIT) | |
| bpf_program__set_autoload(skel->progs.kick_wait_callback, false); | |
| if (kick__load(skel)) { | |
| SCX_ERR("Failed to load scenario %u", scenario); | |
| goto out; | |
| } | |
| bpf_map__set_autoattach(skel->maps.kick_ops, false); | |
| if (kick__attach(skel)) { | |
| SCX_ERR("Failed to attach __resched_curr tracer"); | |
| goto out; | |
| } | |
| skel->bss->state = KICK_STATE_ARMED; | |
| ops_link = bpf_map__attach_struct_ops(skel->maps.kick_ops); | |
| if (!ops_link) { | |
| SCX_ERR("Failed to attach scenario %u", scenario); | |
| goto out; | |
| } | |
| if (!scx_test_start_gated_worker(&victim)) { | |
| SCX_ERR("Failed to start victim"); | |
| goto out; | |
| } | |
| if (scenario == ENQ_BOTH) { | |
| if (!wait_for_state(skel, KICK_STATE_QUEUED)) { | |
| SCX_ERR("Enqueue scenario did not arm"); | |
| goto out; | |
| } | |
| if (!scx_test_start_gated_worker(&challenger)) { | |
| SCX_ERR("Failed to start enqueue challenger"); | |
| goto out; | |
| } | |
| } | |
| if (!wait_for_state(skel, KICK_STATE_DONE)) { | |
| SCX_ERR("Scenario %u stopped in state %u, exit kind %d", scenario, skel->bss->state, | |
| skel->data->uei.kind); | |
| goto out; | |
| } | |
| if (scenario == KICK_LAZY_WAIT && | |
| !wait_for_counter(skel, &skel->bss->nr_wait_callbacks, 1)) { | |
| SCX_ERR("WAIT callback did not run"); | |
| goto out; | |
| } | |
| obs->slice_before = skel->bss->slice_before; | |
| obs->slice_at_resched = skel->bss->slice_at_resched; | |
| obs->nr_wait_callbacks = skel->bss->nr_wait_callbacks; | |
| obs->resched_tif = skel->bss->resched_tif; | |
| ret = SCX_TEST_PASS; | |
| out: | |
| scx_test_stop_gated_worker(&victim); | |
| scx_test_stop_gated_worker(&challenger); | |
| if (ops_link) | |
| bpf_link__destroy(ops_link); | |
| if (skel) | |
| kick__destroy(skel); | |
| return ret; | |
| } | |
| static bool observation_valid(const struct observation *obs) | |
| { | |
| return obs->slice_before > 0 && !obs->slice_at_resched; | |
| } | |
| static bool trace_target_supported(const char *name, enum bpf_attach_type attach_type) | |
| { | |
| return libbpf_find_vmlinux_btf_id(name, attach_type) > 0; | |
| } | |
| static enum scx_test_status setup_immediate(void **ctx) | |
| { | |
| if (!enum_supported("scx_kick_flags", "SCX_KICK_PREEMPT")) { | |
| printf("SKIP: SCX_KICK_PREEMPT is not supported\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| if (!trace_target_supported("__resched_curr", BPF_TRACE_FEXIT)) { | |
| printf("SKIP: __resched_curr is not available in BTF\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| return setup_controller(ctx); | |
| } | |
| static enum scx_test_status setup_lazy(void **ctx) | |
| { | |
| if (!enum_supported("scx_kick_flags", "SCX_KICK_PREEMPT_LAZY")) { | |
| printf("SKIP: SCX_KICK_PREEMPT_LAZY is not supported\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| return setup_immediate(ctx); | |
| } | |
| static enum scx_test_status setup_tick(void **ctx) | |
| { | |
| if (!enum_supported("scx_ops_flags", "SCX_OPS_LAZY_RESCHED")) { | |
| printf("SKIP: SCX_OPS_LAZY_RESCHED is not supported\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| return setup_lazy(ctx); | |
| } | |
| static enum scx_test_status setup_coalesce(void **ctx) | |
| { | |
| if (!enum_supported("scx_enq_flags", "SCX_ENQ_PREEMPT_LAZY")) { | |
| printf("SKIP: SCX_ENQ_PREEMPT_LAZY is not supported\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| if (!trace_target_supported("kick_sync_wait_bal_cb", BPF_TRACE_FENTRY)) { | |
| printf("SKIP: kick_sync_wait_bal_cb is not available in BTF\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| return setup_lazy(ctx); | |
| } | |
| static enum scx_test_status setup_invalid(void **ctx) | |
| { | |
| if (!enum_supported("scx_kick_flags", "SCX_KICK_PREEMPT_LAZY")) { | |
| printf("SKIP: SCX_KICK_PREEMPT_LAZY is not supported\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| return setup_controller(ctx); | |
| } | |
| static enum scx_test_status run_immediate(void *ctx) | |
| { | |
| struct observation obs; | |
| enum scx_test_status status; | |
| status = trace_one(ctx, KICK_IMMEDIATE, 0, -1, &obs); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&obs)); | |
| return SCX_TEST_PASS; | |
| } | |
| static enum scx_test_status run_lazy(void *ctx) | |
| { | |
| struct observation immediate, lazy; | |
| enum scx_test_status status; | |
| int lazy_mode = scx_test_preempt_lazy_mode(); | |
| if (lazy_mode < 0) { | |
| printf("SKIP: running kernel preemption mode is unavailable\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| status = trace_one(ctx, KICK_IMMEDIATE, 0, -1, &immediate); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&immediate)); | |
| status = trace_one(ctx, KICK_LAZY, 0, -1, &lazy); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&lazy)); | |
| if (lazy_mode > 0) | |
| SCX_FAIL_IF(lazy.resched_tif == immediate.resched_tif, | |
| "Lazy mode used immediate TIF %d", lazy.resched_tif); | |
| else if (!lazy_mode) | |
| SCX_EQ(lazy.resched_tif, immediate.resched_tif); | |
| return SCX_TEST_PASS; | |
| } | |
| static enum scx_test_status run_coalesce(void *ctx) | |
| { | |
| struct observation immediate, lazy_first, immediate_first; | |
| struct observation plain_first, lazy_then_plain; | |
| struct observation both, lazy_wait, enq_both; | |
| enum scx_test_status status; | |
| if (scx_test_preempt_lazy_mode() != 1) { | |
| printf("SKIP: kick coalescing requires active lazy preemption\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| status = trace_one(ctx, KICK_IMMEDIATE, 0, -1, &immediate); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&immediate)); | |
| status = trace_one(ctx, KICK_LAZY_THEN_IMMEDIATE, 0, -1, &lazy_first); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&lazy_first)); | |
| status = trace_one(ctx, KICK_IMMEDIATE_THEN_LAZY, 0, -1, &immediate_first); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&immediate_first)); | |
| SCX_EQ(lazy_first.resched_tif, immediate.resched_tif); | |
| SCX_EQ(immediate_first.resched_tif, immediate.resched_tif); | |
| /* | |
| * A plain kick in the same batch doesn't clear the slice by itself. The | |
| * lazy preemption must still expire it, and the plain kick must still | |
| * reschedule immediately, whichever came first. | |
| */ | |
| status = trace_one(ctx, KICK_PLAIN_THEN_LAZY, 0, -1, &plain_first); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&plain_first)); | |
| status = trace_one(ctx, KICK_LAZY_THEN_PLAIN, 0, -1, &lazy_then_plain); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&lazy_then_plain)); | |
| SCX_EQ(plain_first.resched_tif, immediate.resched_tif); | |
| SCX_EQ(lazy_then_plain.resched_tif, immediate.resched_tif); | |
| /* Immediate kick and WAIT both take precedence over lazy preemption. */ | |
| status = trace_one(ctx, KICK_BOTH, 0, -1, &both); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&both)); | |
| status = trace_one(ctx, KICK_LAZY_WAIT, 0, -1, &lazy_wait); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&lazy_wait)); | |
| SCX_EQ(both.resched_tif, immediate.resched_tif); | |
| SCX_EQ(lazy_wait.resched_tif, immediate.resched_tif); | |
| SCX_GT(lazy_wait.nr_wait_callbacks, 0); | |
| /* Immediate enqueue preemption likewise takes precedence over lazy. */ | |
| status = trace_one(ctx, ENQ_BOTH, 0, -1, &enq_both); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&enq_both)); | |
| SCX_EQ(enq_both.resched_tif, immediate.resched_tif); | |
| return SCX_TEST_PASS; | |
| } | |
| /* | |
| * A slice running out at the tick reschedules immediately by default and lazily | |
| * with SCX_OPS_LAZY_RESCHED, the way fair.c expires a slice. | |
| */ | |
| static enum scx_test_status run_tick(void *ctx) | |
| { | |
| struct observation immediate, lazy, force_lazy, force_immediate; | |
| enum scx_test_status status; | |
| u64 lazy_flag; | |
| int lazy_mode = scx_test_preempt_lazy_mode(); | |
| bool found; | |
| if (lazy_mode < 0) { | |
| printf("SKIP: running kernel preemption mode is unavailable\n"); | |
| return SCX_TEST_SKIP; | |
| } | |
| found = __COMPAT_read_enum("scx_ops_flags", "SCX_OPS_LAZY_RESCHED", &lazy_flag); | |
| SCX_ASSERT(found); | |
| status = trace_one(ctx, TICK_EXPIRY, 0, -1, &immediate); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&immediate)); | |
| status = trace_one(ctx, TICK_EXPIRY, lazy_flag, -1, &lazy); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&lazy)); | |
| if (lazy_mode > 0) { | |
| SCX_FAIL_IF(lazy.resched_tif == immediate.resched_tif, | |
| "Lazy slice expiry used immediate TIF %d", lazy.resched_tif); | |
| status = trace_one(ctx, TICK_EXPIRY, 0, 1, &force_lazy); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&force_lazy)); | |
| status = trace_one(ctx, TICK_EXPIRY, lazy_flag, 0, &force_immediate); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| SCX_ASSERT(observation_valid(&force_immediate)); | |
| SCX_EQ(force_lazy.resched_tif, lazy.resched_tif); | |
| SCX_EQ(force_immediate.resched_tif, immediate.resched_tif); | |
| } else { | |
| SCX_EQ(lazy.resched_tif, immediate.resched_tif); | |
| } | |
| return SCX_TEST_PASS; | |
| } | |
| static enum scx_test_status invalid_one(struct kick_ctx *ctx, u32 scenario) | |
| { | |
| struct bpf_link *ops_link = NULL; | |
| struct kick *skel = NULL; | |
| struct scx_test_gated_worker victim; | |
| enum scx_test_status ret = SCX_TEST_FAIL; | |
| int cpu = ctx->target_cpu; | |
| int i; | |
| u64 error_kind; | |
| error_kind = EXIT_KIND(SCX_EXIT_ERROR); | |
| victim = scx_test_spawn_gated_worker(cpu, false); | |
| if (victim.pid < 0) | |
| return SCX_TEST_FAIL; | |
| skel = kick__open(); | |
| if (!skel) | |
| goto out; | |
| SCX_ENUM_INIT(skel); | |
| skel->rodata->scenario = scenario; | |
| bpf_program__set_autoload(skel->progs.kick_need_resched, false); | |
| bpf_program__set_autoload(skel->progs.kick_wait_callback, false); | |
| if (kick__load(skel)) | |
| goto out; | |
| ops_link = bpf_map__attach_struct_ops(skel->maps.kick_ops); | |
| if (!ops_link || !scx_test_start_gated_worker(&victim)) | |
| goto out; | |
| for (i = 0; i < WAIT_LOOPS; i++) { | |
| if (UEI_EXITED(skel, uei) == error_kind) { | |
| ret = SCX_TEST_PASS; | |
| break; | |
| } | |
| usleep(1000); | |
| } | |
| out: | |
| scx_test_stop_gated_worker(&victim); | |
| if (ops_link) | |
| bpf_link__destroy(ops_link); | |
| if (skel) | |
| kick__destroy(skel); | |
| return ret; | |
| } | |
| static enum scx_test_status run_invalid(void *ctx) | |
| { | |
| enum scx_test_status status; | |
| u32 scenario; | |
| for (scenario = INVALID_KICK_IDLE; | |
| scenario <= INVALID_KICK_UNKNOWN; scenario++) { | |
| status = invalid_one(ctx, scenario); | |
| SCX_EQ(status, SCX_TEST_PASS); | |
| } | |
| return SCX_TEST_PASS; | |
| } | |
| static struct scx_test kick_immediate = { | |
| .name = "kick_immediate", | |
| .description = "Trace immediate kick slice expiration and rescheduling", | |
| .setup = setup_immediate, | |
| .run = run_immediate, | |
| .cleanup = cleanup_controller, | |
| }; | |
| static struct scx_test kick_lazy = { | |
| .name = "kick_lazy", | |
| .description = "Trace lazy kick slice expiration and rescheduling", | |
| .setup = setup_lazy, | |
| .run = run_lazy, | |
| .cleanup = cleanup_controller, | |
| }; | |
| static struct scx_test kick_coalesce = { | |
| .name = "kick_coalesce", | |
| .description = "Verify lazy preemption coalesces with immediate requests", | |
| .setup = setup_coalesce, | |
| .run = run_coalesce, | |
| .cleanup = cleanup_controller, | |
| }; | |
| static struct scx_test kick_tick = { | |
| .name = "kick_tick", | |
| .description = "Trace slice expiry at the tick, immediate and lazy", | |
| .setup = setup_tick, | |
| .run = run_tick, | |
| .cleanup = cleanup_controller, | |
| }; | |
| static struct scx_test kick_invalid = { | |
| .name = "kick_invalid", | |
| .description = "Verify invalid kick flag combinations fail", | |
| .setup = setup_invalid, | |
| .run = run_invalid, | |
| .cleanup = cleanup_controller, | |
| }; | |
| __attribute__((constructor)) | |
| static void register_kick_tests(void) | |
| { | |
| scx_test_register(&kick_immediate); | |
| scx_test_register(&kick_lazy); | |
| scx_test_register(&kick_coalesce); | |
| scx_test_register(&kick_tick); | |
| scx_test_register(&kick_invalid); | |
| } |