* [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES
@ 2026-09-12 1:32 Aaron Tomlin
2026-09-22 14:31 ` Petr Mladek
2026-09-22 14:40 ` Peter Zijlstra
0 siblings, 2 replies; 5+ messages in thread
From: Aaron Tomlin @ 2026-09-12 1:32 UTC (permalink / raw)
To: akpm, mingo, peterz, juri.lelli, vincent.guittot
Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid, feng.tang,
pmladek, kprateek.nayak, atomlin, rishil1999, linux-kernel
When investigating kernel panics, inspectability of per-CPU runqueues
and runnable task states is valuable for diagnosing CPU starvation
priority inversion, etc.
While debugfs (/sys/kernel/debug/sched/debug) exposes runqueue metrics
to userspace, these details are not captured during an automated kernel
panic or crash dump. Capturing per-CPU runqueue state directly into
log_buf fills this diagnostic gap for post-mortem crash analysis.
Introduce SYS_INFO_CPU_RUNQUEUES and its corresponding string token
"cpu_runqueues" to panic_sys_info. Add sched_show_runqueues(), modelled
on print_rq(), to emit per-CPU scheduler diagnostics to the kernel log.
Unlike /sys/kernel/debug/sched/debug which dumps all threads assigned to
a CPU, sched_show_runqueues() only emits threads that are actively
running or queued on the runqueue (via task_on_rq_queued() and
task_current()). This keeps the panic log concise, reflects the true
runqueue depth, and prevents overflowing the printk ring buffer on
systems with high thread counts.
Additionally, to guarantee deadlock and memory safety in panic context:
- Acquire the runqueue lock using raw_spin_rq_trylock() with
READ_ONCE() and rcu_dereference() fallback, marking contended
queues with " (contended)"
- Wrap the per-CPU inspection in rcu_read_lock() to protect the
sampled current task (comm and PID) against premature release
during pr_info() across other callers
- Omit cgroup group-path printing in print_rq() to avoid acquiring
cgroup_mutex and traversing kernfs dentries
Suggested-by: Rishil Sandip Shah <rishil1999@outlook.com>
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
Changes since v1:
- Resolved Sparse __rcu address space warnings by accessing rq->curr
via rcu_dereference() in sched_show_runqueues()
- Link to v1: https://lore.kernel.org/lkml/20260911022844.521413-1-atomlin@atomlin.com/
---
Documentation/admin-guide/sysctl/kernel.rst | 1 +
include/linux/sched/debug.h | 1 +
include/linux/sys_info.h | 1 +
kernel/sched/debug.c | 76 +++++++++++++++++----
lib/sys_info.c | 4 ++
5 files changed, 70 insertions(+), 13 deletions(-)
diff --git a/Documentation/admin-guide/sysctl/kernel.rst b/Documentation/admin-guide/sysctl/kernel.rst
index b6328cd0f43e..0962b031e035 100644
--- a/Documentation/admin-guide/sysctl/kernel.rst
+++ b/Documentation/admin-guide/sysctl/kernel.rst
@@ -939,6 +939,7 @@ locks print locks info if CONFIG_LOCKDEP is on
ftrace print ftrace buffer
all_bt print all CPUs backtrace (if available in the arch)
blocked_tasks print only tasks in uninterruptible (blocked) state
+cpu_runqueues print per-CPU runqueue depth and runnable tasks
============= ===================================================
diff --git a/include/linux/sched/debug.h b/include/linux/sched/debug.h
index 35ed4577a6cc..d4276d8ead29 100644
--- a/include/linux/sched/debug.h
+++ b/include/linux/sched/debug.h
@@ -39,6 +39,7 @@ struct seq_file;
extern void proc_sched_show_task(struct task_struct *p,
struct pid_namespace *ns, struct seq_file *m);
extern void proc_sched_set_task(struct task_struct *p);
+extern void sched_show_runqueues(void);
/* Attach to any functions which should be ignored in wchan output. */
#define __sched __section(".sched.text")
diff --git a/include/linux/sys_info.h b/include/linux/sys_info.h
index a5bc3ea3d44b..c571aad1e178 100644
--- a/include/linux/sys_info.h
+++ b/include/linux/sys_info.h
@@ -16,6 +16,7 @@
#define SYS_INFO_PANIC_CONSOLE_REPLAY 0x00000020
#define SYS_INFO_ALL_BT 0x00000040
#define SYS_INFO_BLOCKED_TASKS 0x00000080
+#define SYS_INFO_CPU_RUNQUEUES 0x00000100
void sys_info(unsigned long si_mask);
unsigned long sys_info_parse_param(char *str);
diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index 72236db67983..79f6b00974bb 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -968,7 +968,8 @@ static void task_group_path(struct task_group *tg, char *path, int plen)
#endif
static void
-print_task(struct seq_file *m, struct rq *rq, struct task_struct *p)
+print_task(struct seq_file *m, struct rq *rq, struct task_struct *p,
+ bool show_cgroup_path)
{
if (task_current(rq, p))
SEQ_printf(m, ">R");
@@ -996,13 +997,15 @@ print_task(struct seq_file *m, struct rq *rq, struct task_struct *p)
SEQ_printf(m, " %d %d", task_node(p), task_numa_group_id(p));
#endif
#ifdef CONFIG_CGROUP_SCHED
- SEQ_printf_task_group_path(m, task_group(p), " %s")
+ if (show_cgroup_path)
+ SEQ_printf_task_group_path(m, task_group(p), " %s")
#endif
SEQ_printf(m, "\n");
}
-static void print_rq(struct seq_file *m, struct rq *rq, int rq_cpu)
+static void print_rq(struct seq_file *m, struct rq *rq, int rq_cpu,
+ bool show_cgroup_path, bool queued_only)
{
struct task_struct *g, *p;
@@ -1010,31 +1013,36 @@ static void print_rq(struct seq_file *m, struct rq *rq, int rq_cpu)
SEQ_printf(m, "runnable tasks:\n");
SEQ_printf(m, " S task PID weight vruntime eligible "
"deadline slice sum-exec switches "
- "prio wait-time sum-sleep sum-block"
+ "prio wait-time sum-sleep sum-block");
#ifdef CONFIG_NUMA_BALANCING
- " node group-id"
+ SEQ_printf(m, " node group-id");
#endif
#ifdef CONFIG_CGROUP_SCHED
- " group-path"
+ if (show_cgroup_path)
+ SEQ_printf(m, " group-path");
#endif
- "\n");
+ SEQ_printf(m, "\n");
SEQ_printf(m, "-------------------------------------------------------"
"------------------------------------------------------"
- "------------------------------------------------------"
+ "------------------------------------------------------");
#ifdef CONFIG_NUMA_BALANCING
- "--------------"
+ SEQ_printf(m, "--------------");
#endif
#ifdef CONFIG_CGROUP_SCHED
- "--------------"
+ if (show_cgroup_path)
+ SEQ_printf(m, "--------------");
#endif
- "\n");
+ SEQ_printf(m, "\n");
rcu_read_lock();
for_each_process_thread(g, p) {
if (task_cpu(p) != rq_cpu)
continue;
- print_task(m, rq, p);
+ if (queued_only && !task_current(rq, p) && !task_on_rq_queued(p))
+ continue;
+
+ print_task(m, rq, p, show_cgroup_path);
}
rcu_read_unlock();
}
@@ -1234,7 +1242,7 @@ do { \
print_rt_stats(m, cpu);
print_dl_stats(m, cpu);
- print_rq(m, rq, cpu);
+ print_rq(m, rq, cpu, true, false);
SEQ_printf(m, "\n");
}
@@ -1322,6 +1330,48 @@ void sysrq_sched_debug_show(void)
}
}
+void sched_show_runqueues(void)
+{
+ int cpu;
+
+ pr_info("CPU Runqueues:\n");
+ for_each_online_cpu(cpu) {
+ struct rq *rq = cpu_rq(cpu);
+ struct task_struct *curr;
+ unsigned int nr_running;
+ u64 nr_switches;
+ unsigned long flags;
+ bool locked;
+
+ touch_nmi_watchdog();
+ touch_all_softlockup_watchdogs();
+
+ rcu_read_lock();
+ local_irq_save(flags);
+ locked = raw_spin_rq_trylock(rq);
+ if (locked) {
+ nr_running = rq->nr_running;
+ nr_switches = rq->nr_switches;
+ curr = rcu_dereference(rq->curr);
+ raw_spin_rq_unlock(rq);
+ } else {
+ nr_running = READ_ONCE(rq->nr_running);
+ nr_switches = READ_ONCE(rq->nr_switches);
+ curr = rcu_dereference(rq->curr);
+ }
+ local_irq_restore(flags);
+
+ pr_info("cpu#%d: nr_running:%u switches:%llu curr:%s[%d]%s\n",
+ cpu, nr_running, nr_switches,
+ curr ? curr->comm : "<none>",
+ curr ? task_pid_nr(curr) : -1,
+ locked ? "" : " (contended)");
+
+ print_rq(NULL, rq, cpu, false, true);
+ rcu_read_unlock();
+ }
+}
+
/*
* This iterator needs some explanation.
* It returns 1 for the header position.
diff --git a/lib/sys_info.c b/lib/sys_info.c
index f32a06ec9ed4..fc5bfcc121de 100644
--- a/lib/sys_info.c
+++ b/lib/sys_info.c
@@ -22,6 +22,7 @@ static const char * const si_names[] = {
[ilog2(SYS_INFO_PANIC_CONSOLE_REPLAY)] = "",
[ilog2(SYS_INFO_ALL_BT)] = "all_bt",
[ilog2(SYS_INFO_BLOCKED_TASKS)] = "blocked_tasks",
+ [ilog2(SYS_INFO_CPU_RUNQUEUES)] = "cpu_runqueues",
};
/*
@@ -158,6 +159,9 @@ static void __sys_info(unsigned long si_mask)
if (si_mask & SYS_INFO_BLOCKED_TASKS)
show_state_filter(TASK_UNINTERRUPTIBLE);
+
+ if (si_mask & SYS_INFO_CPU_RUNQUEUES)
+ sched_show_runqueues();
}
void sys_info(unsigned long si_mask)
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES
2026-09-12 1:32 [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES Aaron Tomlin
@ 2026-09-22 14:31 ` Petr Mladek
2026-09-22 16:38 ` Aaron Tomlin
2026-09-22 14:40 ` Peter Zijlstra
1 sibling, 1 reply; 5+ messages in thread
From: Petr Mladek @ 2026-09-22 14:31 UTC (permalink / raw)
To: Aaron Tomlin
Cc: akpm, mingo, peterz, juri.lelli, vincent.guittot,
dietmar.eggemann, rostedt, bsegall, mgorman, vschneid, feng.tang,
kprateek.nayak, rishil1999, linux-kernel
On Fri 2026-09-11 21:32:40, Aaron Tomlin wrote:
> When investigating kernel panics, inspectability of per-CPU runqueues
> and runnable task states is valuable for diagnosing CPU starvation
> priority inversion, etc.
>
> While debugfs (/sys/kernel/debug/sched/debug) exposes runqueue metrics
> to userspace, these details are not captured during an automated kernel
> panic or crash dump. Capturing per-CPU runqueue state directly into
> log_buf fills this diagnostic gap for post-mortem crash analysis.
>
> Introduce SYS_INFO_CPU_RUNQUEUES and its corresponding string token
> "cpu_runqueues" to panic_sys_info. Add sched_show_runqueues(), modelled
> on print_rq(), to emit per-CPU scheduler diagnostics to the kernel log.
>
> Unlike /sys/kernel/debug/sched/debug which dumps all threads assigned to
> a CPU, sched_show_runqueues() only emits threads that are actively
> running or queued on the runqueue (via task_on_rq_queued() and
> task_current()). This keeps the panic log concise, reflects the true
> runqueue depth, and prevents overflowing the printk ring buffer on
> systems with high thread counts.
>
> Additionally, to guarantee deadlock and memory safety in panic context:
> - Acquire the runqueue lock using raw_spin_rq_trylock() with
> READ_ONCE() and rcu_dereference() fallback, marking contended
> queues with " (contended)"
>
> - Wrap the per-CPU inspection in rcu_read_lock() to protect the
> sampled current task (comm and PID) against premature release
> during pr_info() across other callers
>
> - Omit cgroup group-path printing in print_rq() to avoid acquiring
> cgroup_mutex and traversing kernfs dentries
>
> --- a/Documentation/admin-guide/sysctl/kernel.rst
> +++ b/Documentation/admin-guide/sysctl/kernel.rst
> @@ -939,6 +939,7 @@ locks print locks info if CONFIG_LOCKDEP is on
> ftrace print ftrace buffer
> all_bt print all CPUs backtrace (if available in the arch)
> blocked_tasks print only tasks in uninterruptible (blocked) state
> +cpu_runqueues print per-CPU runqueue depth and runnable tasks
I would keep is short and call it "rq".
> ============= ===================================================
>
> --- a/include/linux/sys_info.h
> +++ b/include/linux/sys_info.h
> @@ -16,6 +16,7 @@
> #define SYS_INFO_PANIC_CONSOLE_REPLAY 0x00000020
> #define SYS_INFO_ALL_BT 0x00000040
> #define SYS_INFO_BLOCKED_TASKS 0x00000080
> +#define SYS_INFO_CPU_RUNQUEUES 0x00000100
Similar here: SYS_INFO_RQ
> void sys_info(unsigned long si_mask);
> unsigned long sys_info_parse_param(char *str);
> diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
> index 72236db67983..79f6b00974bb 100644
> --- a/kernel/sched/debug.c
> +++ b/kernel/sched/debug.c
> @@ -1322,6 +1330,48 @@ void sysrq_sched_debug_show(void)
> }
> }
>
> +void sched_show_runqueues(void)
> +{
> + int cpu;
> +
> + pr_info("CPU Runqueues:\n");
> + for_each_online_cpu(cpu) {
> + struct rq *rq = cpu_rq(cpu);
> + struct task_struct *curr;
> + unsigned int nr_running;
> + u64 nr_switches;
> + unsigned long flags;
> + bool locked;
> +
> + touch_nmi_watchdog();
> + touch_all_softlockup_watchdogs();
> +
> + rcu_read_lock();
> + local_irq_save(flags);
> + locked = raw_spin_rq_trylock(rq);
Is the trylock needed for all sys_info() callers or just in panic()?
If it is just panic() then I would use it only when oops_in_progress
is set and use raw_spin_rq_lock() otherwise.
> + if (locked) {
> + nr_running = rq->nr_running;
> + nr_switches = rq->nr_switches;
> + curr = rcu_dereference(rq->curr);
> + raw_spin_rq_unlock(rq);
> + } else {
> + nr_running = READ_ONCE(rq->nr_running);
> + nr_switches = READ_ONCE(rq->nr_switches);
> + curr = rcu_dereference(rq->curr);
> + }
> + local_irq_restore(flags);
> +
> + pr_info("cpu#%d: nr_running:%u switches:%llu curr:%s[%d]%s\n",
> + cpu, nr_running, nr_switches,
> + curr ? curr->comm : "<none>",
> + curr ? task_pid_nr(curr) : -1,
> + locked ? "" : " (contended)");
> +
> + print_rq(NULL, rq, cpu, false, true);
> + rcu_read_unlock();
> + }
> +}
IMHO, it might be a useful feature.
The main question is whether it is acceptable to scheduler
maintainers. It adds some churn. Also they would need to keep in mind
that it can be called in panic().
Best Regards,
Petr
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES
2026-09-12 1:32 [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES Aaron Tomlin
2026-09-22 14:31 ` Petr Mladek
@ 2026-09-22 14:40 ` Peter Zijlstra
2026-09-22 17:35 ` Aaron Tomlin
1 sibling, 1 reply; 5+ messages in thread
From: Peter Zijlstra @ 2026-09-22 14:40 UTC (permalink / raw)
To: Aaron Tomlin
Cc: akpm, mingo, juri.lelli, vincent.guittot, dietmar.eggemann,
rostedt, bsegall, mgorman, vschneid, feng.tang, pmladek,
kprateek.nayak, rishil1999, linux-kernel
On Fri, Sep 11, 2026 at 09:32:40PM -0400, Aaron Tomlin wrote:
> When investigating kernel panics, inspectability of per-CPU runqueues
> and runnable task states is valuable for diagnosing CPU starvation
> priority inversion, etc.
>
> While debugfs (/sys/kernel/debug/sched/debug) exposes runqueue metrics
> to userspace, these details are not captured during an automated kernel
> panic or crash dump. Capturing per-CPU runqueue state directly into
> log_buf fills this diagnostic gap for post-mortem crash analysis.
Uh, crash-dump preserves everything.
> rcu_read_lock();
> for_each_process_thread(g, p) {
> if (task_cpu(p) != rq_cpu)
> continue;
>
> - print_task(m, rq, p);
> + if (queued_only && !task_current(rq, p) && !task_on_rq_queued(p))
> + continue;
> +
> + print_task(m, rq, p, show_cgroup_path);
> }
> rcu_read_unlock();
> }
> @@ -1234,7 +1242,7 @@ do { \
> print_rt_stats(m, cpu);
> print_dl_stats(m, cpu);
>
> - print_rq(m, rq, cpu);
> + print_rq(m, rq, cpu, true, false);
> SEQ_printf(m, "\n");
> }
>
> @@ -1322,6 +1330,48 @@ void sysrq_sched_debug_show(void)
> }
> }
>
> +void sched_show_runqueues(void)
> +{
> + int cpu;
> +
> + pr_info("CPU Runqueues:\n");
> + for_each_online_cpu(cpu) {
> + struct rq *rq = cpu_rq(cpu);
> + struct task_struct *curr;
> + unsigned int nr_running;
> + u64 nr_switches;
> + unsigned long flags;
> + bool locked;
> +
> + touch_nmi_watchdog();
> + touch_all_softlockup_watchdogs();
> +
> + rcu_read_lock();
> + local_irq_save(flags);
> + locked = raw_spin_rq_trylock(rq);
> + if (locked) {
> + nr_running = rq->nr_running;
> + nr_switches = rq->nr_switches;
> + curr = rcu_dereference(rq->curr);
> + raw_spin_rq_unlock(rq);
> + } else {
> + nr_running = READ_ONCE(rq->nr_running);
> + nr_switches = READ_ONCE(rq->nr_switches);
> + curr = rcu_dereference(rq->curr);
> + }
> + local_irq_restore(flags);
This seems to want to avoid deadlocking on rq->lock, but then
print_rq()->print_cfs_stats() will unconditionally take rq->lock again.
So meh.
> +
> + pr_info("cpu#%d: nr_running:%u switches:%llu curr:%s[%d]%s\n",
> + cpu, nr_running, nr_switches,
> + curr ? curr->comm : "<none>",
> + curr ? task_pid_nr(curr) : -1,
> + locked ? "" : " (contended)");
> +
> + print_rq(NULL, rq, cpu, false, true);
> + rcu_read_unlock();
> + }
> +}
> +
> /*
> * This iterator needs some explanation.
> * It returns 1 for the header position.
> diff --git a/lib/sys_info.c b/lib/sys_info.c
> index f32a06ec9ed4..fc5bfcc121de 100644
> --- a/lib/sys_info.c
> +++ b/lib/sys_info.c
> @@ -22,6 +22,7 @@ static const char * const si_names[] = {
> [ilog2(SYS_INFO_PANIC_CONSOLE_REPLAY)] = "",
> [ilog2(SYS_INFO_ALL_BT)] = "all_bt",
> [ilog2(SYS_INFO_BLOCKED_TASKS)] = "blocked_tasks",
> + [ilog2(SYS_INFO_CPU_RUNQUEUES)] = "cpu_runqueues",
> };
>
> /*
> @@ -158,6 +159,9 @@ static void __sys_info(unsigned long si_mask)
>
> if (si_mask & SYS_INFO_BLOCKED_TASKS)
> show_state_filter(TASK_UNINTERRUPTIBLE);
> +
> + if (si_mask & SYS_INFO_CPU_RUNQUEUES)
> + sched_show_runqueues();
> }
I really don't know if this is worth the trouble. I have *never* needed
this.
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES
2026-09-22 14:31 ` Petr Mladek
@ 2026-09-22 16:38 ` Aaron Tomlin
0 siblings, 0 replies; 5+ messages in thread
From: Aaron Tomlin @ 2026-09-22 16:38 UTC (permalink / raw)
To: Petr Mladek
Cc: akpm, mingo, peterz, juri.lelli, vincent.guittot,
dietmar.eggemann, rostedt, bsegall, mgorman, vschneid, feng.tang,
kprateek.nayak, rishil1999, linux-kernel
On Tue, Sep 22, 2026 at 04:31:42PM +0200, Petr Mladek wrote:
> On Fri 2026-09-11 21:32:40, Aaron Tomlin wrote:
> > When investigating kernel panics, inspectability of per-CPU runqueues
> > and runnable task states is valuable for diagnosing CPU starvation
> > priority inversion, etc.
> >
> > While debugfs (/sys/kernel/debug/sched/debug) exposes runqueue metrics
> > to userspace, these details are not captured during an automated kernel
> > panic or crash dump. Capturing per-CPU runqueue state directly into
> > log_buf fills this diagnostic gap for post-mortem crash analysis.
> >
> > Introduce SYS_INFO_CPU_RUNQUEUES and its corresponding string token
> > "cpu_runqueues" to panic_sys_info. Add sched_show_runqueues(), modelled
> > on print_rq(), to emit per-CPU scheduler diagnostics to the kernel log.
> >
> > Unlike /sys/kernel/debug/sched/debug which dumps all threads assigned to
> > a CPU, sched_show_runqueues() only emits threads that are actively
> > running or queued on the runqueue (via task_on_rq_queued() and
> > task_current()). This keeps the panic log concise, reflects the true
> > runqueue depth, and prevents overflowing the printk ring buffer on
> > systems with high thread counts.
> >
> > Additionally, to guarantee deadlock and memory safety in panic context:
> > - Acquire the runqueue lock using raw_spin_rq_trylock() with
> > READ_ONCE() and rcu_dereference() fallback, marking contended
> > queues with " (contended)"
> >
> > - Wrap the per-CPU inspection in rcu_read_lock() to protect the
> > sampled current task (comm and PID) against premature release
> > during pr_info() across other callers
> >
> > - Omit cgroup group-path printing in print_rq() to avoid acquiring
> > cgroup_mutex and traversing kernfs dentries
> >
> > --- a/Documentation/admin-guide/sysctl/kernel.rst
> > +++ b/Documentation/admin-guide/sysctl/kernel.rst
> > @@ -939,6 +939,7 @@ locks print locks info if CONFIG_LOCKDEP is on
> > ftrace print ftrace buffer
> > all_bt print all CPUs backtrace (if available in the arch)
> > blocked_tasks print only tasks in uninterruptible (blocked) state
> > +cpu_runqueues print per-CPU runqueue depth and runnable tasks
>
> I would keep is short and call it "rq".
Hi Petr,
Thank you for your review.
Acknowledged, fair enough.
> > ============= ===================================================
> >
> > --- a/include/linux/sys_info.h
> > +++ b/include/linux/sys_info.h
> > @@ -16,6 +16,7 @@
> > #define SYS_INFO_PANIC_CONSOLE_REPLAY 0x00000020
> > #define SYS_INFO_ALL_BT 0x00000040
> > #define SYS_INFO_BLOCKED_TASKS 0x00000080
> > +#define SYS_INFO_CPU_RUNQUEUES 0x00000100
>
> Similar here: SYS_INFO_RQ
Acknowledged.
> > void sys_info(unsigned long si_mask);
> > unsigned long sys_info_parse_param(char *str);
> > diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
> > index 72236db67983..79f6b00974bb 100644
> > --- a/kernel/sched/debug.c
> > +++ b/kernel/sched/debug.c
> > @@ -1322,6 +1330,48 @@ void sysrq_sched_debug_show(void)
> > }
> > }
> >
> > +void sched_show_runqueues(void)
> > +{
> > + int cpu;
> > +
> > + pr_info("CPU Runqueues:\n");
I will drop this. In print_rq(), there is often no overarching banner line
emitted beforehand.
> > + for_each_online_cpu(cpu) {
> > + struct rq *rq = cpu_rq(cpu);
> > + struct task_struct *curr;
> > + unsigned int nr_running;
> > + u64 nr_switches;
> > + unsigned long flags;
> > + bool locked;
> > +
> > + touch_nmi_watchdog();
> > + touch_all_softlockup_watchdogs();
> > +
> > + rcu_read_lock();
> > + local_irq_save(flags);
> > + locked = raw_spin_rq_trylock(rq);
>
> Is the trylock needed for all sys_info() callers or just in panic()?
> If it is just panic() then I would use it only when oops_in_progress
> is set and use raw_spin_rq_lock() otherwise.
Yes, the trylock in sched_show_runqueues() is needed for all callers to
__sys_info(), not just panic().
Because __sys_info() is also called from NMI context (hardlockup), hardirq
context (softlockup), and khungtaskd, oops_in_progress is 0 in those paths.
Using unconditional raw_spin_rq_lock() there risks fatal self-deadlocks if
rq->lock is already held on the local CPU. Furthermore, locking arbitrary
runqueues in a loop risks AB-BA deadlocks with concurrent scheduler
load-balancing.
Using raw_spin_rq_trylock() ensures sched_show_runqueues() remains strictly
non-blocking across all __sys_info() contexts, and tagging contended queues
with " (contended)" adds useful diagnostic value.
As such, I believe the current implementation is sufficient.
>
> > + if (locked) {
> > + nr_running = rq->nr_running;
> > + nr_switches = rq->nr_switches;
> > + curr = rcu_dereference(rq->curr);
> > + raw_spin_rq_unlock(rq);
> > + } else {
> > + nr_running = READ_ONCE(rq->nr_running);
> > + nr_switches = READ_ONCE(rq->nr_switches);
> > + curr = rcu_dereference(rq->curr);
> > + }
> > + local_irq_restore(flags);
> > +
> > + pr_info("cpu#%d: nr_running:%u switches:%llu curr:%s[%d]%s\n",
> > + cpu, nr_running, nr_switches,
> > + curr ? curr->comm : "<none>",
> > + curr ? task_pid_nr(curr) : -1,
> > + locked ? "" : " (contended)");
> > +
> > + print_rq(NULL, rq, cpu, false, true);
> > + rcu_read_unlock();
> > + }
> > +}
>
> IMHO, it might be a useful feature.
>
> The main question is whether it is acceptable to scheduler
> maintainers. It adds some churn. Also they would need to keep in mind
> that it can be called in panic().
Thank you for your feedback.
Indeed. Regarding the scheduler maintainers (Cc'd Ingo Molnar, Peter
Zijlstra, Juri Lelli, and Vincent Guittot):
The churn was kept strictly confined to kernel/sched/debug.c:
1. It reuses existing print_rq()/print_task() helpers with two flags;
queued_only, to keep output concise and show_cgroup_path, to avoid
cgroup_mutex.
2. sched_show_runqueues() is completely read-only and non-blocking
(raw_spin_rq_trylock() with READ_ONCE() and rcu_dereference()
fallback), so it does not impose locking constraints or latency
risks on core scheduler code, even in panic() or NMI/hardirq
contexts.
Kind regards,
--
Aaron Tomlin
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES
2026-09-22 14:40 ` Peter Zijlstra
@ 2026-09-22 17:35 ` Aaron Tomlin
0 siblings, 0 replies; 5+ messages in thread
From: Aaron Tomlin @ 2026-09-22 17:35 UTC (permalink / raw)
To: Peter Zijlstra
Cc: akpm, mingo, juri.lelli, vincent.guittot, dietmar.eggemann,
rostedt, bsegall, mgorman, vschneid, feng.tang, pmladek,
kprateek.nayak, rishil1999, linux-kernel
On Tue, Sep 22, 2026 at 04:40:27PM +0200, Peter Zijlstra wrote:
> On Fri, Sep 11, 2026 at 09:32:40PM -0400, Aaron Tomlin wrote:
> > When investigating kernel panics, inspectability of per-CPU runqueues
> > and runnable task states is valuable for diagnosing CPU starvation
> > priority inversion, etc.
> >
> > While debugfs (/sys/kernel/debug/sched/debug) exposes runqueue metrics
> > to userspace, these details are not captured during an automated kernel
> > panic or crash dump. Capturing per-CPU runqueue state directly into
> > log_buf fills this diagnostic gap for post-mortem crash analysis.
>
> Uh, crash-dump preserves everything.
Hi Peter,
Yes, fair point. The intention here is to preserve this state directly
within the kernel log buffer, particularly for scenarios where kdump is not
configured, the vmcore was truncated or fails to complete.
In such circumstances, dmesg, whether captured via pstore or serial, is
frequently the sole surviving diagnostic artefact.
>
>
> > rcu_read_lock();
> > for_each_process_thread(g, p) {
> > if (task_cpu(p) != rq_cpu)
> > continue;
> >
> > - print_task(m, rq, p);
> > + if (queued_only && !task_current(rq, p) && !task_on_rq_queued(p))
> > + continue;
> > +
> > + print_task(m, rq, p, show_cgroup_path);
> > }
> > rcu_read_unlock();
> > }
> > @@ -1234,7 +1242,7 @@ do { \
> > print_rt_stats(m, cpu);
> > print_dl_stats(m, cpu);
> >
> > - print_rq(m, rq, cpu);
> > + print_rq(m, rq, cpu, true, false);
> > SEQ_printf(m, "\n");
> > }
> >
> > @@ -1322,6 +1330,48 @@ void sysrq_sched_debug_show(void)
> > }
> > }
> >
> > +void sched_show_runqueues(void)
> > +{
> > + int cpu;
> > +
> > + pr_info("CPU Runqueues:\n");
> > + for_each_online_cpu(cpu) {
> > + struct rq *rq = cpu_rq(cpu);
> > + struct task_struct *curr;
> > + unsigned int nr_running;
> > + u64 nr_switches;
> > + unsigned long flags;
> > + bool locked;
> > +
> > + touch_nmi_watchdog();
> > + touch_all_softlockup_watchdogs();
> > +
> > + rcu_read_lock();
> > + local_irq_save(flags);
> > + locked = raw_spin_rq_trylock(rq);
> > + if (locked) {
> > + nr_running = rq->nr_running;
> > + nr_switches = rq->nr_switches;
> > + curr = rcu_dereference(rq->curr);
> > + raw_spin_rq_unlock(rq);
> > + } else {
> > + nr_running = READ_ONCE(rq->nr_running);
> > + nr_switches = READ_ONCE(rq->nr_switches);
> > + curr = rcu_dereference(rq->curr);
> > + }
> > + local_irq_restore(flags);
>
> This seems to want to avoid deadlocking on rq->lock, but then
> print_rq()->print_cfs_stats() will unconditionally take rq->lock again.
>
> So meh.
No, print_rq() does not call print_cfs_stats(). It is print_cpu() (i.e.
used by debugfs and SysRq) that calls print_cfs_stats() and unconditionally
takes rq->lock via print_cfs_rq().
With sched_show_runqueues(), it specifically avoids print_cpu() and only
calls print_rq(NULL, rq, cpu, false, true). Inside print_rq(), it merely
iterates threads under rcu_read_lock() and calls print_task(), neither of
which acquires rq->lock. Hence, rq->lock is never taken again, and the
non-blocking guarantee of the trylock remains intact throughout the entire
dump.
>
> > +
> > + pr_info("cpu#%d: nr_running:%u switches:%llu curr:%s[%d]%s\n",
> > + cpu, nr_running, nr_switches,
> > + curr ? curr->comm : "<none>",
> > + curr ? task_pid_nr(curr) : -1,
> > + locked ? "" : " (contended)");
> > +
> > + print_rq(NULL, rq, cpu, false, true);
> > + rcu_read_unlock();
> > + }
> > +}
> > +
> > /*
> > * This iterator needs some explanation.
> > * It returns 1 for the header position.
> > diff --git a/lib/sys_info.c b/lib/sys_info.c
> > index f32a06ec9ed4..fc5bfcc121de 100644
> > --- a/lib/sys_info.c
> > +++ b/lib/sys_info.c
> > @@ -22,6 +22,7 @@ static const char * const si_names[] = {
> > [ilog2(SYS_INFO_PANIC_CONSOLE_REPLAY)] = "",
> > [ilog2(SYS_INFO_ALL_BT)] = "all_bt",
> > [ilog2(SYS_INFO_BLOCKED_TASKS)] = "blocked_tasks",
> > + [ilog2(SYS_INFO_CPU_RUNQUEUES)] = "cpu_runqueues",
> > };
> >
> > /*
> > @@ -158,6 +159,9 @@ static void __sys_info(unsigned long si_mask)
> >
> > if (si_mask & SYS_INFO_BLOCKED_TASKS)
> > show_state_filter(TASK_UNINTERRUPTIBLE);
> > +
> > + if (si_mask & SYS_INFO_CPU_RUNQUEUES)
> > + sched_show_runqueues();
> > }
>
> I really don't know if this is worth the trouble. I have *never* needed
> this.
I understand. However, the requirement primarily arises in production
support. In environments where kdump is not configured or the resulting
vmcore is truncated, dmesg is frequently the sole surviving diagnostic
artefact.
During an NMI-induced panic, capturing per-CPU runqueue depth in the log is
invaluable for identifying CPU starvation, etc. As with the remainder of
sys_info, this functionality is strictly opt-in and disabled by default,
imposing no overhead on systems that do not require it while providing
vital visibility where full memory dumps are unavailable.
Kind regards,
--
Aaron Tomlin
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-22 17:36 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-12 1:32 [PATCH v2] sched/debug, sys_info: Introduce SYS_INFO_CPU_RUNQUEUES Aaron Tomlin
2026-09-22 14:31 ` Petr Mladek
2026-09-22 16:38 ` Aaron Tomlin
2026-09-22 14:40 ` Peter Zijlstra
2026-09-22 17:35 ` Aaron Tomlin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®