mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Karl Mehltretter <kmehltretter@gmail.com>
To: Steven Rostedt <rostedt@goodmis.org>,
	Gabriele Monaco <gmonaco@redhat.com>
Cc: Karl Mehltretter <kmehltretter@gmail.com>,
	Masami Hiramatsu <mhiramat@kernel.org>,
	Mathieu Desnoyers <mathieu.desnoyers@efficios.com>,
	Thomas Gleixner <tglx@kernel.org>,
	Sebastian Andrzej Siewior <bigeasy@linutronix.de>,
	Clark Williams <clrkwllms@kernel.org>,
	Ingo Molnar <mingo@redhat.com>,
	Peter Zijlstra <peterz@infradead.org>,
	Andy Lutomirski <luto@kernel.org>,
	Juri Lelli <juri.lelli@redhat.com>,
	Vincent Guittot <vincent.guittot@linaro.org>,
	Dietmar Eggemann <dietmar.eggemann@arm.com>,
	Ben Segall <bsegall@google.com>, Mel Gorman <mgorman@suse.de>,
	Valentin Schneider <vschneid@redhat.com>,
	K Prateek Nayak <kprateek.nayak@amd.com>,
	Jonathan Corbet <corbet@lwn.net>,
	Shuah Khan <skhan@linuxfoundation.org>,
	Randy Dunlap <rdunlap@infradead.org>,
	Wen Yang <wen.yang@linux.dev>,
	linux-trace-kernel@vger.kernel.org,
	linux-rt-devel@lists.linux.dev, linux-doc@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: [RFC PATCH v2 8/9] rv: Test empty PREEMPT_RT softirq worker intervals
Date: Sat,  7 Nov 2026 18:12:51 +0100	[thread overview]
Message-ID: <20261107171252.25090-9-kmehltretter@gmail.com> (raw)
In-Reply-To: <20261107171252.25090-1-kmehltretter@gmail.com>

On PREEMPT_RT, a worker enters serving-softirq state before it checks for
pending work and leaves that state afterward. A worker with no pending
action therefore creates a valid interval that action-based tests miss.

Add KUnit-only helpers that exercise the worker boundaries directly and
force the real per-CPU ksoftirqd task through one invocation with no
pending softirqs. Preserve pending work that races with the test seam.
Verify that each path emits one context pair without an intervening softirq
action.

Keep the seam inactive outside KUnit tests. Gate its predicate and worker
branches with the existing kunit_running static key, and isolate the test
bookkeeping in a helper. Outside a KUnit run, both added worker checks take
only the disabled static-key path.

When another KUnit suite enables the global static key, check the per-CPU
phase before trying the atomic compare-exchange. Keep the compare-exchange
as the arbiter between the worker claim and timeout disarm.

Both tests attach to non-exported IRQ boundary tracepoints. Run them only
when the RV KUnit suite is built in and report them as skipped when it is a
module.

Signed-off-by: Karl Mehltretter <kmehltretter@gmail.com>
---
 include/linux/interrupt.h                     |   7 +
 kernel/softirq.c                              | 228 ++++++++++++++++-
 kernel/trace/rv/Kconfig                       |   6 +-
 .../rv/monitors/irqstate/irqstate_kunit.c     | 239 ++++++++++++++++++
 kernel/trace/rv/rv_monitors_test.c            |   2 +
 5 files changed, 479 insertions(+), 3 deletions(-)

diff --git a/include/linux/interrupt.h b/include/linux/interrupt.h
index ee2dd5349697d..e1a4090596609 100644
--- a/include/linux/interrupt.h
+++ b/include/linux/interrupt.h
@@ -599,6 +599,13 @@ struct softirq_action
 asmlinkage void do_softirq(void);
 asmlinkage void __do_softirq(void);
 
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+void softirq_kunit_run_empty_worker(void);
+bool softirq_kunit_empty_worker_active(void);
+int softirq_kunit_run_empty_ksoftirqd(struct task_struct **owner,
+				      int *owner_cpu, bool *armed);
+#endif
+
 #ifdef CONFIG_PREEMPT_RT
 extern void do_softirq_post_smp_call_flush(unsigned int was_pending);
 #else
diff --git a/kernel/softirq.c b/kernel/softirq.c
index 0d5fe265e354b..f6ed814f1f94a 100644
--- a/kernel/softirq.c
+++ b/kernel/softirq.c
@@ -10,6 +10,9 @@
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
 #define INSTANTIATE_EXPORTED_INTERRUPT_DISABLE
+#include <kunit/test-bug.h>
+#include <kunit/visibility.h>
+#include <linux/completion.h>
 #include <linux/export.h>
 #include <linux/kernel_stat.h>
 #include <linux/interrupt.h>
@@ -609,6 +612,190 @@ asmlinkage __visible void do_softirq(void)
 
 #endif /* !CONFIG_PREEMPT_RT */
 
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+enum softirq_kunit_empty_worker_phase {
+	SOFTIRQ_KUNIT_EMPTY_IDLE,
+	SOFTIRQ_KUNIT_EMPTY_PREPARING,
+	SOFTIRQ_KUNIT_EMPTY_ARMED,
+	SOFTIRQ_KUNIT_EMPTY_RUNNING,
+	SOFTIRQ_KUNIT_EMPTY_DONE,
+};
+
+struct softirq_kunit_empty_worker {
+	atomic_t phase;
+	struct completion done;
+	u32 held_pending;
+};
+
+static DEFINE_PER_CPU(struct softirq_kunit_empty_worker,
+			      softirq_kunit_empty_workers);
+
+/*
+ * Exercise the worker boundaries without entering handle_softirqs().
+ *
+ * The caller must be in preemptible task context with interrupts enabled and
+ * no active softirq context. Like run_ksoftirqd(), this returns with local
+ * interrupts enabled.
+ */
+void softirq_kunit_run_empty_worker(void)
+{
+	ksoftirqd_run_begin();
+	/* Preserve any pending work which raced with the test seam. */
+	wakeup_softirqd();
+	ksoftirqd_run_end();
+}
+EXPORT_SYMBOL_IF_KUNIT(softirq_kunit_run_empty_worker);
+
+bool softirq_kunit_empty_worker_active(void)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	worker = raw_cpu_ptr(&softirq_kunit_empty_workers);
+	return atomic_read(&worker->phase) == SOFTIRQ_KUNIT_EMPTY_RUNNING &&
+	       current == raw_cpu_read(ksoftirqd);
+}
+EXPORT_SYMBOL_IF_KUNIT(softirq_kunit_empty_worker_active);
+
+/*
+ * Force one real ksoftirqd invocation after its smpboot predicate observes
+ * no pending work. The run itself still uses run_ksoftirqd(). The caller must
+ * hold the CPU hotplug read lock across this call so the selected CPU and its
+ * ksoftirqd remain online. The returned task pointer carries no reference.
+ */
+int softirq_kunit_run_empty_ksoftirqd(struct task_struct **owner,
+				      int *owner_cpu, bool *armed)
+{
+	struct softirq_kunit_empty_worker *worker;
+	struct task_struct *tsk;
+	unsigned long completed;
+	int this_cpu, phase, ret;
+
+	if (!owner || !owner_cpu || !armed)
+		return -EINVAL;
+	WRITE_ONCE(*owner, NULL);
+	WRITE_ONCE(*owner_cpu, -1);
+	WRITE_ONCE(*armed, false);
+
+	lockdep_assert_cpus_held();
+	this_cpu = get_cpu();
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	if (atomic_cmpxchg(&worker->phase, SOFTIRQ_KUNIT_EMPTY_IDLE,
+			   SOFTIRQ_KUNIT_EMPTY_PREPARING) !=
+	    SOFTIRQ_KUNIT_EMPTY_IDLE) {
+		put_cpu();
+		return -EBUSY;
+	}
+
+	tsk = __this_cpu_read(ksoftirqd);
+	if (!tsk) {
+		atomic_set(&worker->phase, SOFTIRQ_KUNIT_EMPTY_IDLE);
+		put_cpu();
+		return -ENODEV;
+	}
+
+	init_completion(&worker->done);
+	worker->held_pending = 0;
+	WRITE_ONCE(*owner_cpu, this_cpu);
+	WRITE_ONCE(*owner, tsk);
+	/* Publish the owner before globally registered callbacks can match it. */
+	smp_store_release(armed, true);
+	atomic_set(&worker->phase, SOFTIRQ_KUNIT_EMPTY_ARMED);
+	wake_up_process(tsk);
+	put_cpu();
+
+	completed = wait_for_completion_timeout(&worker->done, 5 * HZ);
+	if (!completed) {
+		phase = atomic_cmpxchg(&worker->phase,
+				       SOFTIRQ_KUNIT_EMPTY_ARMED,
+				       SOFTIRQ_KUNIT_EMPTY_IDLE);
+		if (phase == SOFTIRQ_KUNIT_EMPTY_ARMED) {
+			ret = -ETIMEDOUT;
+			goto out_disarm;
+		}
+
+		/*
+		 * Once the predicate has claimed the request, synchronize with
+		 * complete() before making the completion reusable. CPU hotplug
+		 * remains excluded by the caller while this helper runs. RT lock
+		 * contention can delay the worker after it claims the request, so
+		 * wait for it to finish.
+		 */
+		if (phase != SOFTIRQ_KUNIT_EMPTY_RUNNING &&
+		    phase != SOFTIRQ_KUNIT_EMPTY_DONE) {
+			ret = -EIO;
+			goto out_disarm;
+		}
+		wait_for_completion(&worker->done);
+		phase = atomic_cmpxchg(&worker->phase,
+				       SOFTIRQ_KUNIT_EMPTY_DONE,
+				       SOFTIRQ_KUNIT_EMPTY_IDLE);
+		ret = phase == SOFTIRQ_KUNIT_EMPTY_DONE ? -ETIMEDOUT : -EIO;
+		goto out_disarm;
+	}
+
+	phase = atomic_cmpxchg(&worker->phase, SOFTIRQ_KUNIT_EMPTY_DONE,
+			       SOFTIRQ_KUNIT_EMPTY_IDLE);
+	ret = phase == SOFTIRQ_KUNIT_EMPTY_DONE ? 0 : -EIO;
+
+out_disarm:
+	WRITE_ONCE(*armed, false);
+	return ret;
+}
+EXPORT_SYMBOL_IF_KUNIT(softirq_kunit_run_empty_ksoftirqd);
+
+static bool softirq_kunit_force_empty_ksoftirqd(void)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	if (atomic_read(&worker->phase) != SOFTIRQ_KUNIT_EMPTY_ARMED ||
+	    current != __this_cpu_read(ksoftirqd))
+		return false;
+
+	return atomic_cmpxchg(&worker->phase, SOFTIRQ_KUNIT_EMPTY_ARMED,
+			      SOFTIRQ_KUNIT_EMPTY_RUNNING) ==
+			      SOFTIRQ_KUNIT_EMPTY_ARMED;
+}
+
+static bool softirq_kunit_begin_empty_ksoftirqd(void)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	if (atomic_read(&worker->phase) != SOFTIRQ_KUNIT_EMPTY_RUNNING)
+		return false;
+
+	worker->held_pending = local_softirq_pending();
+	set_softirq_pending(0);
+	return true;
+}
+
+static void softirq_kunit_restore_empty_ksoftirqd(bool active)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	if (!active)
+		return;
+
+	lockdep_assert_irqs_disabled();
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	or_softirq_pending(worker->held_pending);
+	worker->held_pending = 0;
+}
+
+static void softirq_kunit_finish_empty_ksoftirqd(bool active)
+{
+	struct softirq_kunit_empty_worker *worker;
+
+	if (!active)
+		return;
+
+	worker = this_cpu_ptr(&softirq_kunit_empty_workers);
+	atomic_set(&worker->phase, SOFTIRQ_KUNIT_EMPTY_DONE);
+	complete(&worker->done);
+}
+#endif
+
 /*
  * We restart softirq processing for at most MAX_SOFTIRQ_RESTART times,
  * but break the loop if need_resched() is set or after 2 ms.
@@ -1198,11 +1385,50 @@ void __init softirq_init(void)
 
 static int ksoftirqd_should_run(unsigned int cpu)
 {
-	return local_softirq_pending();
+	unsigned int pending = local_softirq_pending();
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+	if (!pending && static_branch_unlikely(&kunit_running) &&
+	    softirq_kunit_force_empty_ksoftirqd())
+		return 1;
+#endif
+	return pending;
+}
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+static void softirq_kunit_run_ksoftirqd(unsigned int cpu)
+{
+	bool kunit_empty;
+
+	ksoftirqd_run_begin();
+	kunit_empty = softirq_kunit_begin_empty_ksoftirqd();
+	if (local_softirq_pending()) {
+		/*
+		 * We can safely run softirq on inline stack, as we are not deep
+		 * in the task stack here.
+		 */
+		handle_softirqs(true);
+		softirq_kunit_restore_empty_ksoftirqd(kunit_empty);
+		ksoftirqd_run_end();
+		softirq_kunit_finish_empty_ksoftirqd(kunit_empty);
+		cond_resched();
+	} else {
+		softirq_kunit_restore_empty_ksoftirqd(kunit_empty);
+		ksoftirqd_run_end();
+		softirq_kunit_finish_empty_ksoftirqd(kunit_empty);
+	}
 }
+#endif
 
 static void run_ksoftirqd(unsigned int cpu)
 {
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+	if (static_branch_unlikely(&kunit_running)) {
+		softirq_kunit_run_ksoftirqd(cpu);
+		return;
+	}
+#endif
+
 	ksoftirqd_run_begin();
 	if (local_softirq_pending()) {
 		/*
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index 9cdc881b8da29..2dc97edc38b1e 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -139,7 +139,9 @@ config RV_MONITORS_KUNIT_TEST
 	  These tests verify that monitors correctly detect violations by
 	  triggering fake events and validating the expected reactions.
 
-	  Enabling this may slightly increase overhead of some monitors if any
-	  unrelated KUnit test is running.
+	  Building this option, including as a module, adds test-only hooks to
+	  some monitored paths. KUnit's static key keeps the hooks dormant while
+	  no test is running. Running this or another KUnit suite can temporarily
+	  add slight overhead to those paths.
 
 	  If unsure, say N.
diff --git a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
index e61c500a954a9..40fe9fd4b1fb4 100644
--- a/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
+++ b/kernel/trace/rv/monitors/irqstate/irqstate_kunit.c
@@ -885,6 +885,37 @@ struct irqstate_inline_bh_ctx {
 	bool exit_attached;
 };
 
+struct irqstate_empty_worker_ctx {
+	struct task_struct *owner;
+	int owner_cpu;
+	unsigned int entries;
+	unsigned int exits;
+	unsigned int action_entries;
+	unsigned int action_exits;
+	unsigned int event_index;
+	bool armed;
+	bool real_worker;
+	bool inside_pair;
+	bool bad_order;
+	bool bad_boundary_context;
+	bool bad_boundary_cpu;
+	bool actions_at_exit;
+	bool entry_attached;
+	bool exit_attached;
+	bool action_entry_attached;
+	bool action_exit_attached;
+};
+
+static bool irqstate_empty_worker_matches(struct irqstate_empty_worker_ctx *empty_ctx)
+{
+	/* Pairs with owner publication in both empty-worker test paths. */
+	if (!smp_load_acquire(&empty_ctx->armed) ||
+	    current != READ_ONCE(empty_ctx->owner))
+		return false;
+
+	return !empty_ctx->real_worker || softirq_kunit_empty_worker_active();
+}
+
 static void irqstate_inline_bh_entry(void *data, bool serving,
 				     bool from_hardirq)
 {
@@ -1154,6 +1185,202 @@ static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
 		   softirq_count() == saved_softirq);
 }
 
+static void irqstate_empty_worker_entry(void *data, bool serving,
+					bool from_hardirq)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx))
+		return;
+	empty_ctx->entries++;
+	if (empty_ctx->event_index != 0 || empty_ctx->inside_pair)
+		empty_ctx->bad_order = true;
+	empty_ctx->event_index++;
+	empty_ctx->inside_pair = true;
+	if (!serving || !in_serving_softirq() || !irqs_disabled())
+		empty_ctx->bad_boundary_context = true;
+	if (raw_smp_processor_id() != READ_ONCE(empty_ctx->owner_cpu))
+		empty_ctx->bad_boundary_cpu = true;
+}
+
+static void irqstate_empty_worker_exit(void *data, bool serving,
+				       bool from_hardirq)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx))
+		return;
+	empty_ctx->exits++;
+	if (empty_ctx->event_index != 1 || !empty_ctx->inside_pair)
+		empty_ctx->bad_order = true;
+	empty_ctx->event_index++;
+	if (!serving || !in_serving_softirq() || !irqs_disabled())
+		empty_ctx->bad_boundary_context = true;
+	if (raw_smp_processor_id() != READ_ONCE(empty_ctx->owner_cpu))
+		empty_ctx->bad_boundary_cpu = true;
+	empty_ctx->actions_at_exit = empty_ctx->action_entries ||
+				     empty_ctx->action_exits;
+	empty_ctx->inside_pair = false;
+}
+
+static void irqstate_empty_worker_action_entry(void *data, unsigned int vec_nr)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx) ||
+	    !empty_ctx->inside_pair)
+		return;
+	(void)vec_nr;
+	empty_ctx->action_entries++;
+	if (raw_smp_processor_id() != empty_ctx->owner_cpu)
+		empty_ctx->bad_boundary_cpu = true;
+}
+
+static void irqstate_empty_worker_action_exit(void *data, unsigned int vec_nr)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	if (!irqstate_empty_worker_matches(empty_ctx) ||
+	    !empty_ctx->inside_pair)
+		return;
+	(void)vec_nr;
+	empty_ctx->action_exits++;
+	if (raw_smp_processor_id() != empty_ctx->owner_cpu)
+		empty_ctx->bad_boundary_cpu = true;
+}
+
+static void irqstate_empty_worker_detach(void *data)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx = data;
+
+	WRITE_ONCE(empty_ctx->armed, false);
+	if (empty_ctx->action_exit_attached)
+		unregister_trace_softirq_exit(irqstate_empty_worker_action_exit,
+					      empty_ctx);
+	if (empty_ctx->action_entry_attached)
+		unregister_trace_softirq_entry(irqstate_empty_worker_action_entry,
+					       empty_ctx);
+	if (empty_ctx->exit_attached)
+		unregister_trace_softirq_context_exit_tp(irqstate_empty_worker_exit,
+							 empty_ctx);
+	if (empty_ctx->entry_attached)
+		unregister_trace_softirq_context_entry_tp(
+			irqstate_empty_worker_entry, empty_ctx);
+	tracepoint_synchronize_unregister();
+}
+
+static void run_irqstate_rt_empty_worker(struct kunit *test, bool real_worker)
+{
+	struct irqstate_empty_worker_ctx *empty_ctx;
+	struct rv_kunit_ctx *ctx = test->priv;
+	unsigned int saved_count, saved_softirq;
+	bool order_ok, restored;
+	int ret;
+
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
+		kunit_skip(test, "Requires RT worker serving boundaries");
+		return;
+	}
+
+	prepare_test(test, &rv_irqstate_ops.mon);
+	empty_ctx = kunit_kzalloc(test, sizeof(*empty_ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, empty_ctx);
+	empty_ctx->real_worker = real_worker;
+
+	ret = register_trace_softirq_context_entry_tp(
+		irqstate_empty_worker_entry, empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->entry_attached = true;
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test,
+						  irqstate_empty_worker_detach,
+						  empty_ctx));
+	ret = register_trace_softirq_context_exit_tp(irqstate_empty_worker_exit,
+						     empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->exit_attached = true;
+	ret = register_trace_softirq_entry(irqstate_empty_worker_action_entry,
+					   empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->action_entry_attached = true;
+	ret = register_trace_softirq_exit(irqstate_empty_worker_action_exit,
+					  empty_ctx);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	empty_ctx->action_exit_attached = true;
+	attach_irqstate_test_probes();
+	KUNIT_ASSERT_EQ(test, 0,
+			kunit_add_action_or_reset(test, detach_irqstate_test_probes,
+						  NULL));
+
+	KUNIT_ASSERT_TRUE(test, preemptible());
+	KUNIT_ASSERT_FALSE(test, irqs_disabled());
+	KUNIT_ASSERT_EQ(test, softirq_count(), 0U);
+	saved_count = preempt_count();
+	saved_softirq = softirq_count();
+	if (real_worker) {
+		cpus_read_lock();
+		ret = softirq_kunit_run_empty_ksoftirqd(&empty_ctx->owner,
+							&empty_ctx->owner_cpu,
+							&empty_ctx->armed);
+		cpus_read_unlock();
+	} else {
+		migrate_disable();
+		WRITE_ONCE(empty_ctx->owner_cpu, smp_processor_id());
+		WRITE_ONCE(empty_ctx->owner, current);
+		/* Publish the owner before globally registered callbacks match it. */
+		smp_store_release(&empty_ctx->armed, true);
+		softirq_kunit_run_empty_worker();
+		WRITE_ONCE(empty_ctx->armed, false);
+		migrate_enable();
+		ret = 0;
+	}
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_ASSERT_NOT_NULL(test, empty_ctx->owner);
+
+	order_ok = empty_ctx->event_index == 2 && !empty_ctx->inside_pair &&
+		   !empty_ctx->bad_order;
+	restored = preempt_count() == saved_count &&
+		   softirq_count() == saved_softirq && !irqs_disabled();
+	KUNIT_EXPECT_EQ(test, empty_ctx->entries, 1U);
+	KUNIT_EXPECT_EQ(test, empty_ctx->exits, 1U);
+	KUNIT_EXPECT_EQ(test, empty_ctx->action_entries, 0U);
+	KUNIT_EXPECT_EQ(test, empty_ctx->action_exits, 0U);
+	KUNIT_EXPECT_TRUE(test, order_ok);
+	KUNIT_EXPECT_FALSE(test, empty_ctx->bad_boundary_context);
+	KUNIT_EXPECT_FALSE(test, empty_ctx->bad_boundary_cpu);
+	KUNIT_EXPECT_FALSE(test, empty_ctx->actions_at_exit);
+	KUNIT_EXPECT_EQ(test, preempt_count(), saved_count);
+	KUNIT_EXPECT_EQ(test, softirq_count(), saved_softirq);
+	KUNIT_EXPECT_FALSE(test, irqs_disabled());
+	RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+	if (real_worker)
+		kunit_info(test,
+			   "RT smpboot empty worker events=%u/%u actions=%u/%u order=%u serving=%u same_cpu=%u restored=%u\n",
+			   empty_ctx->entries, empty_ctx->exits,
+			   empty_ctx->action_entries, empty_ctx->action_exits,
+			   order_ok, !empty_ctx->bad_boundary_context,
+			   !empty_ctx->bad_boundary_cpu, restored);
+	else
+		kunit_info(test,
+			   "RT empty worker events=%u/%u actions=%u/%u order=%u serving=%u same_cpu=%u restored=%u\n",
+			   empty_ctx->entries, empty_ctx->exits,
+			   empty_ctx->action_entries, empty_ctx->action_exits,
+			   order_ok, !empty_ctx->bad_boundary_context,
+			   !empty_ctx->bad_boundary_cpu, restored);
+}
+
+/* Exercise an RT worker interval which executes no softirq action. */
+static void rv_test_irqstate_rt_empty_worker(struct kunit *test)
+{
+	run_irqstate_rt_empty_worker(test, false);
+}
+
+/* Exercise the real ksoftirqd task and its smpboot predicate. */
+static void rv_test_irqstate_rt_smpboot_empty_worker(struct kunit *test)
+{
+	run_irqstate_rt_empty_worker(test, true);
+}
+
 static int irqstate_preempt_worker(void *arg)
 {
 	struct irqstate_preempt_ctx *preempt_ctx = arg;
@@ -1389,6 +1616,16 @@ static void rv_test_irqstate_rt_inline_bh(struct kunit *test)
 	kunit_skip(test, "Requires built-in RV tests");
 }
 
+static void rv_test_irqstate_rt_empty_worker(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqstate_rt_smpboot_empty_worker(struct kunit *test)
+{
+	kunit_skip(test, "Requires built-in RV tests");
+}
+
 static void rv_test_irqstate_preempt(struct kunit *test)
 {
 	kunit_skip(test, "Requires built-in RV tests");
@@ -1414,6 +1651,8 @@ static void rv_test_irqstate_rt_prepare_switch(struct kunit *test)
 #define rv_test_irqstate_live_toggle rv_test_stub
 #define rv_test_irqstate_irq_exit_inline rv_test_stub
 #define rv_test_irqstate_rt_inline_bh rv_test_stub
+#define rv_test_irqstate_rt_empty_worker rv_test_stub
+#define rv_test_irqstate_rt_smpboot_empty_worker rv_test_stub
 #define rv_test_irqstate_preempt rv_test_stub
 #define rv_test_irqstate_rt_prepare_irq rv_test_stub
 #define rv_test_irqstate_rt_prepare_switch rv_test_stub
diff --git a/kernel/trace/rv/rv_monitors_test.c b/kernel/trace/rv/rv_monitors_test.c
index 9eb37cbd2de22..be3aaae2613a8 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -169,6 +169,8 @@ static struct kunit_case rv_mon_test_cases[] = {
 	KUNIT_CASE(rv_test_irqstate_live_toggle),
 	KUNIT_CASE(rv_test_irqstate_irq_exit_inline),
 	KUNIT_CASE(rv_test_irqstate_rt_inline_bh),
+	KUNIT_CASE(rv_test_irqstate_rt_empty_worker),
+	KUNIT_CASE(rv_test_irqstate_rt_smpboot_empty_worker),
 	KUNIT_CASE(rv_test_irqstate_preempt),
 	KUNIT_CASE(rv_test_irqstate_rt_prepare_irq),
 	KUNIT_CASE(rv_test_irqstate_rt_prepare_switch),
-- 
2.39.5 (Apple Git-154)


  parent reply	other threads:[~2026-11-07 17:13 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-07 17:12 [RFC PATCH v2 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 1/9] tracing/irq: Add generic IRQ context boundary tracepoints Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 2/9] tracing/sched: Add IRQ preemption lifecycle tracepoints Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 5/9] docs/rv: Document the IRQ monitors Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
2026-11-07 17:12 ` [RFC PATCH v2 7/9] rv: Add KUnit coverage for IRQ-exit scheduling Karl Mehltretter
2026-11-07 17:12 ` Karl Mehltretter [this message]
2026-11-07 17:12 ` [RFC PATCH v2 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261107171252.25090-9-kmehltretter@gmail.com \
    --to=kmehltretter@gmail.com \
    --cc=bigeasy@linutronix.de \
    --cc=bsegall@google.com \
    --cc=clrkwllms@kernel.org \
    --cc=corbet@lwn.net \
    --cc=dietmar.eggemann@arm.com \
    --cc=gmonaco@redhat.com \
    --cc=juri.lelli@redhat.com \
    --cc=kprateek.nayak@amd.com \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-rt-devel@lists.linux.dev \
    --cc=linux-trace-kernel@vger.kernel.org \
    --cc=luto@kernel.org \
    --cc=mathieu.desnoyers@efficios.com \
    --cc=mgorman@suse.de \
    --cc=mhiramat@kernel.org \
    --cc=mingo@redhat.com \
    --cc=peterz@infradead.org \
    --cc=rdunlap@infradead.org \
    --cc=rostedt@goodmis.org \
    --cc=skhan@linuxfoundation.org \
    --cc=tglx@kernel.org \
    --cc=vincent.guittot@linaro.org \
    --cc=vschneid@redhat.com \
    --cc=wen.yang@linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®