sched_ext: Fix SCX_KICK_WAIT deadlock
Subsystems (4)
Tejun Heo
reviewerPATCH 1/1 sched_ext: Fix SCX_KICK_WAIT deadlock
Hello,
SCX_KICK_WAIT busy-waits in kick_cpus_irq_workfn() until the target CPU reschedules. Because the irq_work runs in hardirq context, the waiting CPU's kick_sync never advances, and if multiple CPUs form a wait cycle, all deadlock. This was reported by Christian while testing on arm64.
0001 fixes the deadlock by deferring the wait to a balance callback which drops the rq lock and enables IRQs, allowing IPIs to be processed and kick_sync to keep advancing during the wait.
0002 adds a selftest that creates a 3-CPU kick_wait cycle to reproduce the issue.
Based on sched_ext/for-7.0-fixes (db08b1940f4b).
0001-sched_ext-Fix-SCX_KICK_WAIT-deadlock-by-deferring-wa.patch 0002-selftests-sched_ext-Add-cyclic-SCX_KICK_WAIT-stress-.patch
kernel/sched/ext.c | 95 +++++++--- kernel/sched/sched.h | 3 + tools/testing/selftests/sched_ext/Makefile | 1 + .../selftests/sched_ext/cyclic_kick_wait.bpf.c | 68 ++++++++ .../testing/selftests/sched_ext/cyclic_kick_wait.c | 194 +++++++++++++++++++++ 5 files changed, 336 insertions(+), 25 deletions(-)
--
tejun
Tejun Heo
reviewerPATCH 1/2 sched_ext: Fix SCX_KICK_WAIT deadlock by deferring wait to balance callback
SCX_KICK_WAIT busy-waits in kick_cpus_irq_workfn() using smp_cond_load_acquire() until the target CPU's kick_sync advances. Because the irq_work runs in hardirq context, the waiting CPU cannot reschedule and its own kick_sync never advances. If multiple CPUs form a wait cycle, all CPUs deadlock.
Replace the busy-wait in kick_cpus_irq_workfn() with resched_curr() to force the CPU through do_pick_task_scx(), which queues a balance callback to perform the wait. The balance callback drops the rq lock and enables IRQs following the sched_core_balance() pattern, so the CPU can process IPIs while waiting. The local CPU's kick_sync is advanced on entry to do_pick_task_scx() and continuously during the wait, ensuring any CPU that starts waiting for us sees the advancement and cannot form cyclic dependencies.
--- kernel/sched/ext.c | 95 ++++++++++++++++++++++++++++++++------------ kernel/sched/sched.h | 3 ++ 2 files changed, 73 insertions(+), 25 deletions(-)
kernel/sched/ext.c +70 -25
@@ -2404,7 +2404,7 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p, {struct scx_sched *sch = scx_root; -/* see kick_cpus_irq_workfn() */ +/* see kick_sync_wait_bal_cb() */ smp_store_release (&rq->scx.kick_sync, rq->scx.kick_sync +1 );update_curr_scx (rq);
@@ -2447,6 +2447,48 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,switch_class (rq, next); } +static void kick_sync_wait_bal_cb (struct rq *rq) +{ +struct scx_kick_syncs __rcu *ks =__this_cpu_read (scx_kick_syncs); +unsigned long *ksyncs =rcu_dereference_sched (ks)->syncs; +bool waited; +s32 cpu; + +/* +* Drop rq lock and enable IRQs while waiting. IRQs must be enabled +* — a target CPU may be waiting for us to process an IPI (e.g. TLB +* flush) while we wait for its kick_sync to advance. +* +* Also, keep advancing our own kick_sync so that new kick_sync waits +* targeting us, which can start after we drop the lock, cannot form +* cyclic dependencies. +*/ +retry: + waited = false; +for_each_cpu (cpu, rq->scx.cpus_to_sync) { +/* +* smp_load_acquire() pairs with smp_store_release() on +* kick_sync updates on the target CPUs. +*/ +if (cpu ==cpu_of (rq) || +smp_load_acquire (&cpu_rq (cpu)->scx.kick_sync) != ksyncs[cpu]) { +cpumask_clear_cpu (cpu, rq->scx.cpus_to_sync); +continue ; + } + +raw_spin_rq_unlock_irq (rq); +while (READ_ONCE (cpu_rq (cpu)->scx.kick_sync) == ksyncs[cpu]) { +smp_store_release (&rq->scx.kick_sync, rq->scx.kick_sync +1 ); +cpu_relax (); + } +raw_spin_rq_lock_irq (rq); + waited = true; + } + +if (waited) + goto retry; +} +static struct task_struct *first_local_task (struct rq *rq) { return list_first_entry_or_null(&rq->scx.local_dsq.list,
@@ -2460,7 +2502,7 @@ do_pick_task_scx(struct rq *rq, struct rq_flags *rf, bool force_scx)bool keep_prev;struct task_struct *p; -/* see kick_cpus_irq_workfn() */ +/* see kick_sync_wait_bal_cb() */ smp_store_release (&rq->scx.kick_sync, rq->scx.kick_sync +1 );rq_modified_begin (rq, &ext_sched_class);
@@ -2470,6 +2512,17 @@ do_pick_task_scx(struct rq *rq, struct rq_flags *rf, bool force_scx)rq_repin_lock (rq, rf);maybe_queue_balance_callback (rq); +/* +* Defer to a balance callback which can drop rq lock and enable +* IRQs. Waiting directly in the pick path would deadlock against +* CPUs sending us IPIs (e.g. TLB flushes) while we wait for them. +*/ +if (unlikely (rq->scx.kick_sync_pending)) { + rq->scx.kick_sync_pending = false; +queue_balance_callback (rq, &rq->scx.kick_sync_bal_cb, + kick_sync_wait_bal_cb); + } +/* * If any higher-priority sched class enqueued a runnable task on * this rq during balance_one(), abort and return RETRY_TASK, so
@@ -4713,6 +4766,9 @@ static void scx_dump_state(struct scx_exit_info *ei, size_t dump_len)if (!cpumask_empty (rq->scx.cpus_to_wait))dump_line (&ns," cpus_to_wait : %*pb" ,cpumask_pr_args (rq->scx.cpus_to_wait)); +if (!cpumask_empty (rq->scx.cpus_to_sync)) +dump_line (&ns," cpus_to_sync : %*pb" , +cpumask_pr_args (rq->scx.cpus_to_sync)); used =seq_buf_used (&ns);if (SCX_HAS_OP (sch, dump_cpu)) {
@@ -5610,11 +5666,11 @@ static bool kick_one_cpu(s32 cpu, struct rq *this_rq, unsigned long *ksyncs)if (cpumask_test_cpu (cpu, this_scx->cpus_to_wait)) {if (cur_class == &ext_sched_class) { +cpumask_set_cpu (cpu, this_scx->cpus_to_sync); ksyncs[cpu] = rq->scx.kick_sync; should_wait = true; - }else { -cpumask_clear_cpu (cpu, this_scx->cpus_to_wait); } +cpumask_clear_cpu (cpu, this_scx->cpus_to_wait); }resched_curr (rq);
@@ -5669,27 +5725,15 @@ static void kick_cpus_irq_workfn(struct irq_work *irq_work)cpumask_clear_cpu (cpu, this_scx->cpus_to_kick_if_idle); } -if (!should_wait) -return ; - -for_each_cpu (cpu, this_scx->cpus_to_wait) { -unsigned long *wait_kick_sync = &cpu_rq (cpu)->scx.kick_sync; - -/* -* Busy-wait until the task running at the time of kicking is no -* longer running. This can be used to implement e.g. core -* scheduling. -* -* smp_cond_load_acquire() pairs with store_releases in -* pick_task_scx() and put_prev_task_scx(). The former breaks -* the wait if SCX's scheduling path is entered even if the same -* task is picked subsequently. The latter is necessary to break -* the wait when $cpu is taken by a higher sched class. -*/ -if (cpu !=cpu_of (this_rq)) -smp_cond_load_acquire (wait_kick_sync,VAL != ksyncs[cpu]); - -cpumask_clear_cpu (cpu, this_scx->cpus_to_wait); +/* +* Can't wait in hardirq — kick_sync can't advance, deadlocking if +* CPUs wait for each other. Defer to kick_sync_wait_bal_cb(). +*/ +if (should_wait) { +raw_spin_rq_lock (this_rq); + this_scx->kick_sync_pending = true; +resched_curr (this_rq); +raw_spin_rq_unlock (this_rq); } }
@@ -5794,6 +5838,7 @@ void __init init_sched_ext_class(void)BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_kick_if_idle,GFP_KERNEL , n));BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_preempt,GFP_KERNEL , n));BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_wait,GFP_KERNEL , n)); +BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_sync,GFP_KERNEL , n)); rq->scx.deferred_irq_work =IRQ_WORK_INIT_HARD (deferred_irq_workfn); rq->scx.kick_cpus_irq_work =IRQ_WORK_INIT_HARD (kick_cpus_irq_workfn);
kernel/sched/sched.h +3 -0
@@ -805,9 +805,12 @@ struct scx_rq {cpumask_var_t cpus_to_kick_if_idle;cpumask_var_t cpus_to_preempt;cpumask_var_t cpus_to_wait; +cpumask_var_t cpus_to_sync; +bool kick_sync_pending;unsigned long kick_sync;local_t reenq_local_deferred;struct balance_callback deferred_bal_cb; +struct balance_callback kick_sync_bal_cb;struct irq_work deferred_irq_work;struct irq_work kick_cpus_irq_work;struct scx_dispatch_q bypass_dsq;
Signature · 1 line
2.53.0
Andrea Righi
reviewerRe: [PATCH 1/2] sched_ext: Fix SCX_KICK_WAIT deadlock by deferring wait to balance callback
Hi Tejun,
Quoted 42 lines · Tejun Heo
SCX_KICK_WAITbusy-waits inkick_cpus_irq_workfn()usingsmp_cond_load_acquire()until the target CPU'skick_syncadvances. Because theirq_workruns in hardirq context, the waiting CPU cannot reschedule and its ownkick_syncnever advances. If multiple CPUs form a wait cycle, all CPUs deadlock.Replace the busy-wait in
kick_cpus_irq_workfn()withresched_curr()to force the CPU throughdo_pick_task_scx(), which queues a balance callback to perform the wait. The balance callback drops the rq lock and enables IRQs following thesched_core_balance()pattern, so the CPU can process IPIs while waiting. The local CPU'skick_syncis advanced on entry todo_pick_task_scx()and continuously during the wait, ensuring any CPU that starts waiting for us sees the advancement and cannot form cyclic dependencies.Fixes 90e55164dad4 ("sched_ext: Implement SCX_KICK_WAIT")Cc [email protected] # v6.12+Reported-by Christian Loehle <[email protected]>Signed-off-by Tejun Heo <[email protected]>--- kernel/sched/ext.c | 95 ++++++++++++++++++++++++++++++++------------ kernel/sched/sched.h | 3 ++ 2 files changed, 73 insertions(+), 25 deletions(-)kernel/sched/ext.c +1
-1@@ -2404,7 +2404,7 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p, {struct scx_sched *sch = scx_root; -/* see kick_cpus_irq_workfn() */ +/* see kick_sync_wait_bal_cb() */ smp_store_release (&rq->scx.kick_sync, rq->scx.kick_sync +1 );update_curr_scx (rq);@@ -2447,6 +2447,48 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,switch_class (rq, next); }
kernel/sched/ext.c static void put_prev_task_scx(struct rq *rq, struct task_struct *p, · line 2459
Quoted 10 lines
kernel/sched/ext.c +10
-0+static void kick_sync_wait_bal_cb (struct rq *rq) +{ +struct scx_kick_syncs __rcu *ks =__this_cpu_read (scx_kick_syncs); +unsigned long *ksyncs =rcu_dereference_sched (ks)->syncs; +bool waited; +s32 cpu; + +/* +* Drop rq lock and enable IRQs while waiting. IRQs must be enabled +* — a target CPU may be waiting for us to process an IPI (e.g. TLB
nit: s/—/-/
Quoted 8 lines
kernel/sched/ext.c +8
-0+* flush) while we wait for its kick_sync to advance. +* +* Also, keep advancing our own kick_sync so that new kick_sync waits +* targeting us, which can start after we drop the lock, cannot form +* cyclic dependencies. +*/ +retry: + waited = false;
kernel/sched/ext.c static void put_prev_task_scx(struct rq *rq, struct task_struct *p, · line 2477
Quoted 10 lines
kernel/sched/ext.c +10
-0+for_each_cpu (cpu, rq->scx.cpus_to_sync) { +/* +* smp_load_acquire() pairs with smp_store_release() on +* kick_sync updates on the target CPUs. +*/ +if (cpu ==cpu_of (rq) || +smp_load_acquire (&cpu_rq (cpu)->scx.kick_sync) != ksyncs[cpu]) { +cpumask_clear_cpu (cpu, rq->scx.cpus_to_sync); +continue ; + }
Should we add something like:
if (cpu != cpu_of(rq) && !cpu_online(cpu)) {
cpumask_clear_cpu(cpu, rq->scx.cpus_to_sync);
continue;
}kernel/sched/ext.c static void put_prev_task_scx(struct rq *rq, struct task_struct *p, · line 2480
Quoted 3 lines
kernel/sched/ext.c +3
-0+ +raw_spin_rq_unlock_irq (rq); +while (READ_ONCE (cpu_rq (cpu)->scx.kick_sync) == ksyncs[cpu]) {
And here:
if (cpu != cpu_of(rq) && !cpu_online(cpu)) break;
(see below)
Quoted 26 lines
kernel/sched/ext.c +12
-1+smp_store_release (&rq->scx.kick_sync, rq->scx.kick_sync +1 ); +cpu_relax (); + } +raw_spin_rq_lock_irq (rq); + waited = true; + } + +if (waited) + goto retry; +} +static struct task_struct *first_local_task (struct rq *rq) { return list_first_entry_or_null(&rq->scx.local_dsq.list,@@ -2460,7 +2502,7 @@ do_pick_task_scx(struct rq *rq, struct rq_flags *rf, bool force_scx)bool keep_prev;struct task_struct *p; -/* see kick_cpus_irq_workfn() */ +/* see kick_sync_wait_bal_cb() */ smp_store_release (&rq->scx.kick_sync, rq->scx.kick_sync +1 );rq_modified_begin (rq, &ext_sched_class);@@ -2470,6 +2512,17 @@ do_pick_task_scx(struct rq *rq, struct rq_flags *rf, bool force_scx)rq_repin_lock (rq, rf);maybe_queue_balance_callback (rq);
kernel/sched/ext.c do_pick_task_scx(struct rq *rq, struct rq_flags *rf, bool force_scx) · line 2523
Quoted 10 lines
kernel/sched/ext.c +9
-0+/* +* Defer to a balance callback which can drop rq lock and enable +* IRQs. Waiting directly in the pick path would deadlock against +* CPUs sending us IPIs (e.g. TLB flushes) while we wait for them. +*/ +if (unlikely (rq->scx.kick_sync_pending)) { + rq->scx.kick_sync_pending = false; +queue_balance_callback (rq, &rq->scx.kick_sync_bal_cb, + kick_sync_wait_bal_cb);
queue_balance_callback() is a no-op if the rq is in balance_push, but I guess it's ok to just clear the kick_sync_pending if we add the checks above.
Quoted 94 lines
kernel/sched/ext.c +17
-23+ } +/* * If any higher-priority sched class enqueued a runnable task on * this rq during balance_one(), abort and return RETRY_TASK, so @@ -4713,6 +4766,9 @@ static void scx_dump_state(struct scx_exit_info *ei, size_t dump_len)if (!cpumask_empty (rq->scx.cpus_to_wait))dump_line (&ns," cpus_to_wait : %*pb" ,cpumask_pr_args (rq->scx.cpus_to_wait)); +if (!cpumask_empty (rq->scx.cpus_to_sync)) +dump_line (&ns," cpus_to_sync : %*pb" , +cpumask_pr_args (rq->scx.cpus_to_sync)); used =seq_buf_used (&ns);if (SCX_HAS_OP (sch, dump_cpu)) {@@ -5610,11 +5666,11 @@ static bool kick_one_cpu(s32 cpu, struct rq *this_rq, unsigned long *ksyncs)if (cpumask_test_cpu (cpu, this_scx->cpus_to_wait)) {if (cur_class == &ext_sched_class) { +cpumask_set_cpu (cpu, this_scx->cpus_to_sync); ksyncs[cpu] = rq->scx.kick_sync; should_wait = true; - }else { -cpumask_clear_cpu (cpu, this_scx->cpus_to_wait); } +cpumask_clear_cpu (cpu, this_scx->cpus_to_wait); }resched_curr (rq);@@ -5669,27 +5725,15 @@ static void kick_cpus_irq_workfn(struct irq_work *irq_work)cpumask_clear_cpu (cpu, this_scx->cpus_to_kick_if_idle); } -if (!should_wait) -return ; - -for_each_cpu (cpu, this_scx->cpus_to_wait) { -unsigned long *wait_kick_sync = &cpu_rq (cpu)->scx.kick_sync; - -/* -* Busy-wait until the task running at the time of kicking is no -* longer running. This can be used to implement e.g. core -* scheduling. -* -* smp_cond_load_acquire() pairs with store_releases in -* pick_task_scx() and put_prev_task_scx(). The former breaks -* the wait if SCX's scheduling path is entered even if the same -* task is picked subsequently. The latter is necessary to break -* the wait when $cpu is taken by a higher sched class. -*/ -if (cpu !=cpu_of (this_rq)) -smp_cond_load_acquire (wait_kick_sync,VAL != ksyncs[cpu]); - -cpumask_clear_cpu (cpu, this_scx->cpus_to_wait); +/* +* Can't wait in hardirq — kick_sync can't advance, deadlocking if +* CPUs wait for each other. Defer to kick_sync_wait_bal_cb(). +*/ +if (should_wait) { +raw_spin_rq_lock (this_rq); + this_scx->kick_sync_pending = true; +resched_curr (this_rq); +raw_spin_rq_unlock (this_rq); } }@@ -5794,6 +5838,7 @@ void __init init_sched_ext_class(void)BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_kick_if_idle,GFP_KERNEL , n));BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_preempt,GFP_KERNEL , n));BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_wait,GFP_KERNEL , n)); +BUG_ON (!zalloc_cpumask_var_node (&rq->scx.cpus_to_sync,GFP_KERNEL , n)); rq->scx.deferred_irq_work =IRQ_WORK_INIT_HARD (deferred_irq_workfn); rq->scx.kick_cpus_irq_work =IRQ_WORK_INIT_HARD (kick_cpus_irq_workfn);kernel/sched/sched.h +3
-0@@ -805,9 +805,12 @@ struct scx_rq {cpumask_var_t cpus_to_kick_if_idle;cpumask_var_t cpus_to_preempt;cpumask_var_t cpus_to_wait; +cpumask_var_t cpus_to_sync; +bool kick_sync_pending;unsigned long kick_sync;local_t reenq_local_deferred;struct balance_callback deferred_bal_cb; +struct balance_callback kick_sync_bal_cb;struct irq_work deferred_irq_work;struct irq_work kick_cpus_irq_work;struct scx_dispatch_q bypass_dsq;Signature · 1 line
2.53.0
Thanks,
-Andrea
Tejun Heo
reviewerPATCH 2/2 selftests/sched_ext: Add cyclic SCX_KICK_WAIT stress test
In reply to Tejun HeoAdd a test that creates a 3-CPU kick_wait cycle (A->B->C->A). A BPF scheduler kicks the next CPU in the ring with SCX_KICK_WAIT on every enqueue while userspace workers generate continuous scheduling churn via sched_yield(). Without the preceding fix, this hangs the machine within seconds.
--- tools/testing/selftests/sched_ext/Makefile | 1 + .../sched_ext/cyclic_kick_wait.bpf.c | 68 ++++++ .../selftests/sched_ext/cyclic_kick_wait.c | 194 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 tools/testing/selftests/sched_ext/cyclic_kick_wait.bpf.c create mode 100644 tools/testing/selftests/sched_ext/cyclic_kick_wait.c
tools/testing/selftests/sched_ext/Makefile +1 -0
@@ -188,6 +188,7 @@ auto-test-targets := \ rt_stall \ test_example \ total_bw \ + cyclic_kick_wait \ testcase-targets := $(addsuffix .o,$(addprefix $(SCXOBJ_DIR)/,$(auto-test-targets)))
tools/testing/selftests/sched_ext/cyclic_kick_wait.bpf.c new file +68 -0
68 lines not on this page · raw message
tools/testing/selftests/sched_ext/cyclic_kick_wait.c new file +194 -0
194 lines not on this page · raw message
Signature · 1 line
2.53.0
Re: [PATCH 2/2] selftests/sched_ext: Add cyclic SCX_KICK_WAIT stress test
Hi Tejun,
Quoted 4 lines · Tejun Heo
Add a test that creates a 3-CPU
kick_waitcycle (A->B->C->A). A BPF scheduler kicks the next CPU in the ring withSCX_KICK_WAITon every enqueue while userspace workers generate continuous scheduling churn viasched_yield(). Without the preceding fix, this hangs the machine within seconds.
I think it would be better to skip this test on older, unpatched kernels. Sometimes I use my local kernel for testing before the stable patches are fully integrated, so skipping the test would be a safer approach.
Otherwise, this test can stall the machine and make it impossible to exit the test runner.
Log:
$ sudo ./runner -t cyclic_kick_wait # on v6.14 ===== START ===== TEST: cyclic_kick_wait DESCRIPTION: Verify SCX_KICK_WAIT forward progress under a 3-CPU wait cycle OUTPUT: libbpf: struct_ops cyclic_kick_wait_ops: member cgroup_set_bandwidth not found in kernel, skipping it as it's set to zero libbpf: struct_ops cyclic_kick_wait_ops: member cgroup_set_idle not found in kernel, skipping it as it's set to zero libbpf: struct_ops cyclic_kick_wait_ops: member priv not found in kernel, skipping it as it's set to zero ERR: cyclic_kick_wait.c:169 Failed to join worker thread 0 (-110)
Thanks,
Cheng-Yang
From 6f09d2298e547aa7a519d4bff262fc6851a969ea Mon Sep 17 00:00:00 2001
From: Cheng-Yang Chou <[email protected]>
Date: Sun, 29 Mar 2026 16:07:05 +0800
Subject: [PATCH] selftests/sched_ext: Skip cyclic_kick_wait on kernels without
deadlock fix
The cyclic_kick_wait test triggers a deadlock on kernels lacking the SCX_KICK_WAIT fix, causing the machine to hang or worker threads to timeout (-110).
Use __COMPAT_struct_has_field() to probe vmlinux BTF for rq_scx.kick_sync_pending, a field introduced by the SCX_KICK_WAIT deadlock fix. Skip the test on older kernels that lack the fix rather than hanging the machine.
Example failure on unpatched kernels:
ERR: cyclic_kick_wait.c:169
Failed to join worker thread 0 (-110)
--- tools/testing/selftests/sched_ext/cyclic_kick_wait.c | 5 +++++ 1 file changed, 5 insertions(+)
tools/testing/selftests/sched_ext/cyclic_kick_wait.c +5 -0
@@ -88,6 +88,11 @@ static enum scx_test_status setup(void **ctx) {struct cyclic_kick_wait *skel; +if (!__COMPAT_struct_has_field ("rq_scx" ,"kick_sync_pending" )) { +fprintf (stderr,"Skipping test: kernel lacks SCX_KICK_WAIT deadlock fix\n" ); +return SCX_TEST_SKIP ; + } + skel =cyclic_kick_wait__open ();SCX_FAIL_IF (!skel,"Failed to open skel" );SCX_ENUM_INIT (skel);
Signature · 1 line
2.43.0
Andrea Righi
reviewerRe: [PATCH 2/2] selftests/sched_ext: Add cyclic SCX_KICK_WAIT stress test
Hi Cheng-Yang,
Quoted 8 lines · Cheng-Yang Chou
Hi Tejun,
Quoted 4 lines · Tejun Heo
Add a test that creates a 3-CPU
kick_waitcycle (A->B->C->A). A BPF scheduler kicks the next CPU in the ring withSCX_KICK_WAITon every enqueue while userspace workers generate continuous scheduling churn viasched_yield(). Without the preceding fix, this hangs the machine within seconds.
Quoted 6 lines
I think it would be better to skip this test on older, unpatched kernels. Sometimes I use my local kernel for testing before the stable patches are fully integrated, so skipping the test would be a safer approach.
Otherwise, this test can stall the machine and make it impossible to exit the test runner.
Actually, I disagree. The whole point of the kselftests is to expose kernel issues, including bugs in older kernels that may need fixes. So, I think we shouldn't skip the test.
-Andrea
Quoted 62 lines
Log:
$ sudo ./runner -t cyclic_kick_wait # on v6.14 ===== START ===== TEST: cyclic_kick_wait DESCRIPTION: Verify SCX_KICK_WAIT forward progress under a 3-CPU wait cycle OUTPUT: libbpf: struct_ops cyclic_kick_wait_ops: member cgroup_set_bandwidth not found in kernel, skipping it as it's set to zero libbpf: struct_ops cyclic_kick_wait_ops: member cgroup_set_idle not found in kernel, skipping it as it's set to zero libbpf: struct_ops cyclic_kick_wait_ops: member priv not found in kernel, skipping it as it's set to zero ERR: cyclic_kick_wait.c:169 Failed to join worker thread 0 (-110)Thanks,
Cheng-YangFrom
6f09d2298e547aa7a519d4bff262fc6851a969eaMon Sep 17 00:00:00 2001
From: Cheng-Yang Chou <[email protected]>
Date: Sun, 29 Mar 2026 16:07:05 +0800
Subject: [PATCH] selftests/sched_ext: Skipcyclic_kick_waiton kernels without
deadlock fixThe
cyclic_kick_waittest triggers a deadlock on kernels lacking theSCX_KICK_WAITfix, causing the machine to hang or worker threads to timeout (-110).Use
__COMPAT_struct_has_field()to probe vmlinux BTF for rq_scx.kick_sync_pending, a field introduced by theSCX_KICK_WAITdeadlock fix. Skip the test on older kernels that lack the fix rather than hanging the machine.Example failure on unpatched kernels:
ERR: cyclic_kick_wait.c:169
Failed to join worker thread 0 (-110)Fixes e9b990b76922 ("selftests/sched_ext: Add cyclic SCX_KICK_WAIT stress test")Signed-off-by Cheng-Yang Chou <[email protected]>--- tools/testing/selftests/sched_ext/cyclic_kick_wait.c | 5 +++++ 1 file changed, 5 insertions(+)tools/testing/selftests/sched_ext/cyclic_kick_wait.c +5
-0@@ -88,6 +88,11 @@ static enum scx_test_status setup(void **ctx) {struct cyclic_kick_wait *skel; +if (!__COMPAT_struct_has_field ("rq_scx" ,"kick_sync_pending" )) { +fprintf (stderr,"Skipping test: kernel lacks SCX_KICK_WAIT deadlock fix\n" ); +return SCX_TEST_SKIP ; + } + skel =cyclic_kick_wait__open ();SCX_FAIL_IF (!skel,"Failed to open skel" );SCX_ENUM_INIT (skel);Signature · 1 line
2.43.0
Re: [PATCH 2/2] selftests/sched_ext: Add cyclic SCX_KICK_WAIT stress test
Hi Andrea,
Quoted 15 lines · Andrea Righi
Hi Cheng-Yang,
Quoted 12 lines · Cheng-Yang Chou
Hi Tejun,
Quoted 4 lines · Tejun Heo
Add a test that creates a 3-CPU
kick_waitcycle (A->B->C->A). A BPF scheduler kicks the next CPU in the ring withSCX_KICK_WAITon every enqueue while userspace workers generate continuous scheduling churn viasched_yield(). Without the preceding fix, this hangs the machine within seconds.I think it would be better to skip this test on older, unpatched kernels. Sometimes I use my local kernel for testing before the stable patches are fully integrated, so skipping the test would be a safer approach.
Quoted 6 lines
Quoted 2 lines
Otherwise, this test can stall the machine and make it impossible to exit the test runner.
Actually, I disagree. The whole point of the kselftests is to expose kernel issues, including bugs in older kernels that may need fixes. So, I think we shouldn't skip the test.
I see your point. While the system stall is a bit painful during local machine testing on unpatched kernels, I agree that kselftests should remain an effective gatekeeper for kernel issues.
Or maybe return SCX_TEST_FAIL?
Since without this patch, this test won't pass anyway. Perhaps failing more gracefully is a better middle ground than stalling the entire machine.
Signature · 2 lines
Thanks, Cheng-Yang
Re: [PATCH 2/2] selftests/sched_ext: Add cyclic SCX_KICK_WAIT stress test
In reply to Tejun HeoQuoted 6 lines · Tejun Heo
Add a test that creates a 3-CPU
kick_waitcycle (A->B->C->A). A BPF scheduler kicks the next CPU in the ring withSCX_KICK_WAITon every enqueue while userspace workers generate continuous scheduling churn viasched_yield(). Without the preceding fix, this hangs the machine within seconds.Signed-off-by Tejun Heo <[email protected]>
Quoted 26 lines · Tejun Heo
Hello,
SCX_KICK_WAITbusy-waits inkick_cpus_irq_workfn()until the target CPU reschedules. Because theirq_workruns in hardirq context, the waiting CPU'skick_syncnever advances, and if multiple CPUs form a wait cycle, all deadlock. This was reported by Christian while testing on arm64.0001 fixes the deadlock by deferring the wait to a balance callback which drops the rq lock and enables IRQs, allowing IPIs to be processed and
kick_syncto keep advancing during the wait.0002 adds a selftest that creates a 3-CPU
kick_waitcycle to reproduce the issue.Based on sched_ext/for-7.0-fixes (
db08b1940f4b).0001-sched_ext-Fix-SCX_KICK_WAIT-deadlock-by-deferring-wa.patch 0002-selftests-sched_ext-Add-cyclic-SCX_KICK_WAIT-stress-.patch
kernel/sched/ext.c | 95 +++++++--- kernel/sched/sched.h | 3 + tools/testing/selftests/sched_ext/Makefile | 1 + .../selftests/sched_ext/cyclic_kick_wait.bpf.c | 68 ++++++++ .../testing/selftests/sched_ext/cyclic_kick_wait.c | 194 +++++++++++++++++++++ 5 files changed, 336 insertions(+), 25 deletions(-)
Quoted 2 lines
--
tejun
For both:
Thanks for fixing this!
Tejun Heo
reviewerHello,
Quoted 3 lines
Tejun Heo (2):
sched_ext: FixSCX_KICK_WAITdeadlock by deferring wait to balance callback selftests/sched_ext: Add cyclicSCX_KICK_WAITstress test
Applied 1-2 to sched_ext/for-7.0-fixes.
Thanks.
--
tejun