[PATCH 2/2] tracing: fgraph: Add a cond_resched() to the shadow stack retry loop
From: Vineet Gupta
Date: Tue Sep 22 2026 - 18:56:50 EST
This is supplementary to previous patch and is effective only for
!PREEMPTION kernels.
The preceding patch cuts the number of sweeps over the thread list by
32x, but the loop is still O(N^2). Each pass walks from the same start
init_task under rcu_read_lock() with no reschedule point. On a kernel
that cannot preempt the walk, a large enough thread count lets one task
monopolize a CPU until the walk completes.
Add a cond_resched() between sweeps which is safe since it is outside
the RCU read-side critical section.
It is understood that this is effectively a no-op for CONFIG_PREEMPTION
builds, which is what default x86 and arm64 kernels are today.
A CONFIG_PREEMPT_DYNAMIC build with "preempt={none,voluntary}"
used to work but even that got inhibited since the commit
7dadeaa6e851 ("sched: Further restrict the preemption modes").
Measured on a 60-core machine with a PREEMPT_LAZY kernel and 400000
idle threads, this patch changes neither the runtime nor the yield
count, which stays at zero.
Where it does have effect is !CONFIG_PREEMPTION builds: architectures
that still offer PREEMPT_NONE or PREEMPT_VOLUNTARY, and the stable
kernels where x86 PREEMPT_NONE remains a build-time choice. Verified
on a 6.16 PREEMPT_NONE kernel, 240000 idle threads, three runs each,
with the loop instrumented to count passes and cond_resched() yields:
time yields soft lockup
unpatched 51.3-54.6 s - 3 of 3
cond_resched() 52.7-53.0 s 592-609 0 of 3
The runtime is unchanged and the soft lockups are gone. Every
need_resched() converts to a yield, giving an average of about 98 ms
between reschedules.
There is a second effect on those builds. When cond_resched() does not
reschedule it still calls rcu_all_qs() on !CONFIG_PREEMPT_RCU kernels,
reporting a quiescent state. So the two failures seen in the field are
addressed by two mechanisms: the soft lockup by yielding so the
watchdog can run, and the RCU stall by reporting a quiescent state
even on passes where no yield happens.
This is deliberately separable from the preceding patch. If the view
is that a no-op on the architectures anyone runs is not worth carrying,
dropping this one leaves the batch-size fix intact.
Signed-off-by: Vineet Gupta <vineet.gupta@xxxxxxxxx>
---
kernel/trace/fgraph.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/kernel/trace/fgraph.c b/kernel/trace/fgraph.c
index ed455b53513b..86d11217c584 100644
--- a/kernel/trace/fgraph.c
+++ b/kernel/trace/fgraph.c
@@ -1249,6 +1249,14 @@ static int start_graph_tracing(void)
do {
ret = alloc_retstack_tasklist(ret_stack_list);
+ /*
+ * Each pass rescans the thread list from the head, so the
+ * loop is O(threads^2) overall. The RCU read-side section
+ * ends with the pass, and both locks held here (ftrace_lock,
+ * and fprobe_mutex for fprobe users) are mutexes, so it is
+ * safe to give the CPU up in between.
+ */
+ cond_resched();
} while (ret == -EAGAIN);
if (!ret) {
--
2.53.0-Meta