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>,
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 7/9] rv: Add KUnit coverage for IRQ-exit scheduling
Date: Thu, 22 Oct 2026 07:07:10 +0200 [thread overview]
Message-ID: <20261022050712.75035-8-kmehltretter@gmail.com> (raw)
In-Reply-To: <20261022050712.75035-1-kmehltretter@gmail.com>
Add KUnit coverage for the irqexit generated state table, invalid
transitions, and enable-time synchronization. Test live
preempt_schedule_irq() invocations without switching, across a task switch,
and across CPU migration. The migration case runs when another CPU is
available. The live cases require the built-in suite because they invoke a
non-exported helper.
KUnit failure reporting may allocate with GFP_KERNEL. Record reaction
counts inside synthetic IRQ-disabled and hardirq intervals. Evaluate the
expectations after restoring normal task context to avoid secondary
atomic-sleep or lockdep diagnostics.
The live tests invoke the generic helper directly. They do not exercise
an architecture-specific physical interrupt-return path.
Assisted-by: LLM
---
Documentation/trace/rv/monitor_irqentry.rst | 21 +
kernel/trace/rv/monitors/irqexit/irqexit.c | 13 +
.../trace/rv/monitors/irqexit/irqexit_kunit.c | 537 ++++++++++++++++++
.../trace/rv/monitors/irqexit/irqexit_kunit.h | 21 +
kernel/trace/rv/rv_monitors_test.c | 13 +
5 files changed, 605 insertions(+)
create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
create mode 100644 kernel/trace/rv/monitors/irqexit/irqexit_kunit.h
diff --git a/Documentation/trace/rv/monitor_irqentry.rst b/Documentation/trace/rv/monitor_irqentry.rst
index ba9e78e7c4a08..f4cd34c1b6bf1 100644
--- a/Documentation/trace/rv/monitor_irqentry.rst
+++ b/Documentation/trace/rv/monitor_irqentry.rst
@@ -292,6 +292,27 @@ boundary probes require exactly one entry before the callback and one exit
afterward, both with local interrupts disabled and live serving state set;
the monitor must accept the pair and the original counts must be restored.
+With built-in RV KUnit tests, ``rv_test_irqexit_live`` enters the real
+``preempt_schedule_irq()`` helper with and without a task switch on one CPU.
+``rv_test_irqexit_migrate`` instead wakes a same-CPU FIFO worker which moves
+the switched-out owner to a different allowed online CPU. The test requires
+accepted entry and suspension on the source CPU and resumption and return
+on the destination. It also samples the CPU on which the helper actually
+returned, independently of the RV observer, and checks restoration of the
+owner's original affinity before the tested task exits.
+
+The two live helper cases use single-element parameter fixtures. Monitor
+slot allocation and destruction, and CPU-hotplug read locking, run in the
+suite runner, which owns the RV interface mutex. The case body and its
+cleanup can run in different KUnit threads. The tests retain a task
+reference until the observer and worker are stopped; the migration case
+checks restoration of the tested task's affinity before that task exits.
+
+The migration case skips when fewer than two allowed CPUs are online, or
+when the tests are modules: the helper is not exported. These tests exercise
+the real helper and scheduler, but invoke the helper directly. They are not
+evidence of an architecture's physical interrupt-return assembly path.
+
Coverage limits
---------------
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit.c b/kernel/trace/rv/monitors/irqexit/irqexit.c
index a088b28b62b17..baee718dbe83c 100644
--- a/kernel/trace/rv/monitors/irqexit/irqexit.c
+++ b/kernel/trace/rv/monitors/irqexit/irqexit.c
@@ -157,3 +157,16 @@ module_exit(unregister_irqexit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Karl Mehltretter <kmehltretter@gmail.com>");
MODULE_DESCRIPTION("irqexit: verify IRQ-exit scheduling ownership");
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+#include <kunit/visibility.h>
+#include "irqexit_kunit.h"
+
+const struct rv_irqexit_ops rv_irqexit_ops = {
+ .mon = RV_MON_OPS_INIT(),
+ .handle_resched_entry = handle_resched_entry,
+ .handle_resched_exit = handle_resched_exit,
+ .handle_sched_switch = handle_sched_switch,
+};
+EXPORT_SYMBOL_IF_KUNIT(rv_irqexit_ops);
+#endif
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
new file mode 100644
index 0000000000000..56a9442e2f704
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.c
@@ -0,0 +1,537 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/completion.h>
+#include <linux/cpu.h>
+#include <linux/hardirq.h>
+#include <linux/irqflags.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/kthread.h>
+#include <linux/rv.h>
+#include <linux/sched.h>
+#include <linux/sched/task.h>
+#include <rv/kunit.h>
+#include <rv_trace.h>
+#include <trace/events/sched.h>
+
+#include "irqexit_kunit.h"
+
+#if IS_REACHABLE(CONFIG_RV_MON_IRQEXIT)
+
+static void irqexit_test_switch(struct task_struct *prev, struct task_struct *next)
+{
+ rv_mock_current(prev);
+ rv_irqexit_ops.handle_sched_switch(NULL, true, prev, next, TASK_RUNNING);
+ rv_mock_current(next);
+}
+
+static void rv_test_irqexit(struct kunit *test)
+{
+ struct task_struct *owner = rv_kunit_alloc_mock_task(test);
+ struct task_struct *other = rv_kunit_alloc_mock_task(test);
+ struct rv_kunit_ctx *ctx = test->priv;
+ unsigned long flags;
+ int observed;
+
+ owner->pid = 101;
+ other->pid = 102;
+ prepare_test(test, &rv_irqexit_ops.mon);
+ rv_mock_current(owner);
+
+ local_irq_save(flags);
+ /* The scheduling loop may return without an actual task switch. */
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+
+ /* A different task's IRQ-exit loop cannot complete owner's loop. */
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ irqexit_test_switch(owner, other);
+ rv_irqexit_ops.handle_resched_entry(NULL, other);
+ __irq_enter_raw();
+ __irq_exit_raw();
+ rv_irqexit_ops.handle_resched_exit(NULL, other);
+ irqexit_test_switch(other, owner);
+
+ /* Both tasks can have suspended invocations at the same time. */
+ irqexit_test_switch(owner, other);
+ rv_irqexit_ops.handle_resched_entry(NULL, other);
+ irqexit_test_switch(other, owner);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ irqexit_test_switch(owner, other);
+ rv_irqexit_ops.handle_resched_exit(NULL, other);
+ observed = READ_ONCE(ctx->reactions);
+ local_irq_restore(flags);
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed, 0);
+}
+
+static void rv_test_irqexit_invalid(struct kunit *test)
+{
+ struct task_struct *owner = rv_kunit_alloc_mock_task(test);
+ struct task_struct *other = rv_kunit_alloc_mock_task(test);
+ struct rv_kunit_ctx *ctx = test->priv;
+ unsigned long flags;
+ int observed[14];
+ unsigned int i = 0;
+
+ owner->pid = 101;
+ other->pid = 102;
+ prepare_test(test, &rv_irqexit_ops.mon);
+ rv_mock_current(owner);
+
+ KUNIT_ASSERT_FALSE(test, irqs_disabled());
+ RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+
+ local_irq_save(flags);
+ /* Scheduling must not start before the hardirq count is removed. */
+ __irq_enter_raw();
+ observed[i++] = READ_ONCE(ctx->reactions);
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ __irq_exit_raw();
+
+ /* A return with hardirq state still active is invalid as well. */
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ __irq_enter_raw();
+ observed[i++] = READ_ONCE(ctx->reactions);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ __irq_exit_raw();
+
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+
+ /* The first exit synchronizes; the second unmatched exit must fail. */
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+
+ /* The state permits exit, but its task identity must still agree. */
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ rv_mock_current(other);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+
+ /* Matching current is insufficient if the invocation is suspended. */
+ rv_mock_current(owner);
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ irqexit_test_switch(owner, other);
+ rv_mock_current(owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ observed[i++] = READ_ONCE(ctx->reactions);
+
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ local_irq_restore(flags);
+ for (i = 0; i < ARRAY_SIZE(observed); i += 2) {
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed[i], 0);
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed[i + 1], 1);
+ }
+ RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+
+ /* Even an unsynchronized bad return must not be silently skipped. */
+ rv_mock_current(other);
+ RV_KUNIT_EXPECT_REACTION_HERE(test, ctx)
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+}
+
+static void rv_test_irqexit_sync(struct kunit *test)
+{
+ struct task_struct *owner = rv_kunit_alloc_mock_task(test);
+ struct task_struct *other = rv_kunit_alloc_mock_task(test);
+ struct rv_kunit_ctx *ctx = test->priv;
+ unsigned long flags;
+ int observed;
+
+ owner->pid = 101;
+ other->pid = 102;
+ prepare_test(test, &rv_irqexit_ops.mon);
+ rv_mock_current(owner);
+
+ local_irq_save(flags);
+ /* Switch events alone do not synchronize an already active loop. */
+ irqexit_test_switch(owner, other);
+ irqexit_test_switch(other, owner);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+
+ /* Model disable/reset/re-enable while that invocation is suspended. */
+ rv_irqexit_ops.mon.rv_this->enabled = 0;
+ irqexit_test_switch(owner, other);
+ rv_irqexit_ops.handle_resched_entry(NULL, other);
+ rv_irqexit_ops.handle_resched_exit(NULL, other);
+ rv_irqexit_ops.mon.task_reset(owner);
+ rv_irqexit_ops.mon.task_reset(other);
+ rv_irqexit_ops.mon.rv_this->enabled = 1;
+ irqexit_test_switch(other, owner);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ rv_irqexit_ops.handle_resched_entry(NULL, owner);
+ rv_irqexit_ops.handle_resched_exit(NULL, owner);
+ observed = READ_ONCE(ctx->reactions);
+ local_irq_restore(flags);
+ RV_KUNIT_EXPECT_OBSERVED(test, ctx, observed, 0);
+}
+
+#ifndef MODULE
+struct irqexit_live_ctx {
+ struct task_struct *owner;
+ struct task_struct *worker;
+ const char *skip_reason;
+ struct completion worker_done;
+ unsigned int entries;
+ unsigned int exits;
+ unsigned int switches_out;
+ unsigned int switches_in;
+ unsigned int entry_cpu;
+ unsigned int exit_cpu;
+ unsigned int switch_out_cpu;
+ unsigned int switch_in_cpu;
+ unsigned int target_cpu;
+ unsigned int raw_entries;
+ unsigned int raw_exits;
+ unsigned int bad_raw_context;
+ int affinity_result;
+ cpumask_t saved_mask;
+ cpumask_t test_mask;
+ bool migrate;
+ bool affinity_changed;
+ bool observer_attached;
+ bool raw_entry_attached;
+ bool raw_exit_attached;
+ bool hotplug_disabled;
+};
+
+/* Parent fixtures run in the suite runner, which owns rv_interface_lock. */
+static int irqexit_live_fixture_init(struct kunit *test)
+{
+ struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+ struct irqexit_live_ctx *live;
+ int ret;
+
+ if (mon->enabled)
+ return -EBUSY;
+ live = kunit_kzalloc(test, sizeof(*live), GFP_KERNEL);
+ if (!live)
+ return -ENOMEM;
+
+ /* Keep the online mask stable without nesting tracepoint registration. */
+ cpu_hotplug_disable();
+ live->hotplug_disabled = true;
+ ret = mon->enable();
+ if (ret) {
+ cpu_hotplug_enable();
+ live->hotplug_disabled = false;
+ kunit_kfree(test, live);
+ return ret;
+ }
+ kunit_register_params_array(test, live, 1, NULL);
+ return 0;
+}
+
+static void irqexit_live_fixture_exit(struct kunit *test)
+{
+ struct irqexit_live_ctx *live = (void *)test->params_array.params;
+ struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+
+ mon->enabled = 0;
+ mon->disable();
+ if (live->hotplug_disabled) {
+ cpu_hotplug_enable();
+ live->hotplug_disabled = false;
+ }
+ /* Retain the child's explicit skip reason on the parent case. */
+ if (live->skip_reason)
+ kunit_mark_skipped(test, "%s", live->skip_reason);
+}
+
+static int irqexit_live_worker(void *data)
+{
+ struct irqexit_live_ctx *live = data;
+
+ if (live->migrate)
+ live->affinity_result =
+ set_cpus_allowed_ptr(live->owner, cpumask_of(live->target_cpu));
+ complete(&live->worker_done);
+ while (!kthread_should_stop()) {
+ set_current_state(TASK_INTERRUPTIBLE);
+ if (!kthread_should_stop())
+ schedule();
+ }
+ __set_current_state(TASK_RUNNING);
+ return 0;
+}
+
+static void irqexit_observe_event(void *data, int id, char *state,
+ char *event, char *next_state, bool final_state)
+{
+ struct irqexit_live_ctx *live = data;
+
+ if (id != live->owner->pid)
+ return;
+ if (!strcmp(event, "resched_entry")) {
+ live->entry_cpu = raw_smp_processor_id();
+ live->entries++;
+ } else if (!strcmp(event, "resched_exit")) {
+ live->exit_cpu = raw_smp_processor_id();
+ live->exits++;
+ } else if (!strcmp(event, "switch_out") && !strcmp(state, "rescheduling")) {
+ if (!live->switches_out)
+ live->switch_out_cpu = raw_smp_processor_id();
+ live->switches_out++;
+ } else if (!strcmp(event, "switch_in") && !strcmp(state, "suspended")) {
+ if (!live->switches_in)
+ live->switch_in_cpu = raw_smp_processor_id();
+ live->switches_in++;
+ }
+}
+
+static void irqexit_observe_entry(void *data, struct task_struct *owner,
+ unsigned long pc, bool irq_off)
+{
+ struct irqexit_live_ctx *live = data;
+
+ if (owner != live->owner)
+ return;
+ live->raw_entries++;
+ if (pc || !irq_off)
+ live->bad_raw_context++;
+}
+
+static void irqexit_observe_exit(void *data, struct task_struct *owner,
+ unsigned long pc, bool irq_off)
+{
+ struct irqexit_live_ctx *live = data;
+
+ if (owner != live->owner)
+ return;
+ live->raw_exits++;
+ if (pc || !irq_off)
+ live->bad_raw_context++;
+}
+
+static int irqexit_observe_boundaries(struct irqexit_live_ctx *live)
+{
+ int ret;
+
+ ret = register_trace_sched_irq_preempt_entry(irqexit_observe_entry, live);
+ if (ret)
+ return ret;
+ live->raw_entry_attached = true;
+ ret = register_trace_sched_irq_preempt_exit(irqexit_observe_exit, live);
+ if (!ret)
+ live->raw_exit_attached = true;
+ return ret;
+}
+
+static void irqexit_live_cleanup(void *data)
+{
+ struct irqexit_live_ctx *live = data;
+ struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+
+ mon->enabled = 0;
+ if (live->observer_attached)
+ unregister_trace_event_irqexit(irqexit_observe_event, live);
+ if (live->raw_entry_attached)
+ unregister_trace_sched_irq_preempt_entry(irqexit_observe_entry, live);
+ if (live->raw_exit_attached)
+ unregister_trace_sched_irq_preempt_exit(irqexit_observe_exit, live);
+ tracepoint_synchronize_unregister();
+ if (live->worker)
+ kthread_stop(live->worker);
+ if (live->affinity_changed && !(READ_ONCE(live->owner->flags) & PF_EXITING))
+ KUNIT_EXPECT_EQ(kunit_get_current_test(),
+ set_cpus_allowed_ptr(live->owner, &live->saved_mask), 0);
+ put_task_struct(live->owner);
+ live->owner = NULL;
+
+ RV_KUNIT_EXPECT_NO_REACTION(kunit_get_current_test(), active_ctx);
+ mon->react = mon->reactor ? mon->reactor->react : NULL;
+ active_ctx = NULL;
+ rv_mock_current(NULL);
+}
+
+static void rv_test_irqexit_live(struct kunit *test)
+{
+ struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+ struct rv_kunit_ctx *ctx = test->priv;
+ struct irqexit_live_ctx *live = (void *)test->param_value;
+ struct task_struct *worker;
+ unsigned int entries, exits, switches_out, switches_in;
+ unsigned int first_entries, first_switches;
+ unsigned long flags;
+ bool worker_done;
+
+ KUNIT_ASSERT_FALSE(test, mon->enabled);
+ live->owner = current;
+ get_task_struct(live->owner);
+ init_completion(&live->worker_done);
+
+ active_ctx = ctx;
+ mon->react = rv_kunit_mock_react;
+ rv_mock_current(NULL);
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, irqexit_live_cleanup, live));
+ /* The worker must run on this CPU inside the explicit scheduling loop. */
+ guard(migrate)();
+ worker = kthread_create(irqexit_live_worker, live, "rv-irqexit-test");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, worker);
+ live->worker = worker;
+ kthread_bind(worker, smp_processor_id());
+ sched_set_fifo(worker);
+ KUNIT_ASSERT_EQ(test, 0,
+ register_trace_event_irqexit(irqexit_observe_event, live));
+ live->observer_attached = true;
+ KUNIT_ASSERT_EQ(test, irqexit_observe_boundaries(live), 0);
+
+ mon->enabled = 1;
+
+ KUNIT_ASSERT_EQ(test, preempt_count(), 0);
+ local_irq_save(flags);
+ preempt_schedule_irq();
+ first_entries = live->entries;
+ first_switches = live->switches_out;
+ wake_up_process(worker);
+ preempt_schedule_irq();
+ entries = live->entries;
+ exits = live->exits;
+ switches_out = live->switches_out;
+ switches_in = live->switches_in;
+ worker_done = completion_done(&live->worker_done);
+ local_irq_restore(flags);
+
+ /* Accepted model events prove that the real boundary probes ran. */
+ KUNIT_EXPECT_GT(test, entries, 0);
+ KUNIT_EXPECT_GT(test, first_entries, 0);
+ KUNIT_EXPECT_GT(test, entries, first_entries);
+ KUNIT_EXPECT_EQ(test, entries, exits);
+ KUNIT_EXPECT_EQ(test, switches_out, switches_in);
+ KUNIT_EXPECT_GT(test, switches_out, first_switches);
+ KUNIT_EXPECT_TRUE(test, worker_done);
+ KUNIT_EXPECT_GE(test, live->raw_entries, 2U);
+ KUNIT_EXPECT_GE(test, live->raw_exits, 2U);
+ KUNIT_EXPECT_EQ(test, live->bad_raw_context, 0U);
+ RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+ kunit_info(test, "live IRQ-exit entries=%u exits=%u switches=%u\n",
+ entries, exits, switches_out);
+ mon->enabled = 0;
+}
+
+static void rv_test_irqexit_migrate(struct kunit *test)
+{
+ struct rv_monitor *mon = rv_irqexit_ops.mon.rv_this;
+ struct rv_kunit_ctx *ctx = test->priv;
+ struct irqexit_live_ctx *live = (void *)test->param_value;
+ struct task_struct *worker;
+ unsigned int source_cpu, resumed_cpu;
+ unsigned long flags;
+ bool affinity_restored;
+ int restore_result;
+
+ KUNIT_ASSERT_FALSE(test, mon->enabled);
+ live->owner = current;
+ get_task_struct(live->owner);
+ init_completion(&live->worker_done);
+ active_ctx = ctx;
+ mon->react = rv_kunit_mock_react;
+ rv_mock_current(NULL);
+ KUNIT_ASSERT_EQ(test, 0,
+ kunit_add_action_or_reset(test, irqexit_live_cleanup, live));
+ if (num_online_cpus() < 2) {
+ live->skip_reason = "Requires two online CPUs";
+ kunit_skip(test, "%s", live->skip_reason);
+ return;
+ }
+ cpumask_copy(&live->saved_mask, current->cpus_ptr);
+ cpumask_and(&live->test_mask, &live->saved_mask, cpu_online_mask);
+ if (cpumask_weight(&live->test_mask) < 2) {
+ live->skip_reason = "Requires two allowed online CPUs";
+ kunit_skip(test, "%s", live->skip_reason);
+ return;
+ }
+ source_cpu = cpumask_first(&live->test_mask);
+ live->target_cpu = cpumask_any_but(&live->test_mask, source_cpu);
+ KUNIT_ASSERT_EQ(test, set_cpus_allowed_ptr(current, cpumask_of(source_cpu)), 0);
+ live->affinity_changed = true;
+ live->migrate = true;
+ worker = kthread_create(irqexit_live_worker, live, "rv-irqexit-mig");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, worker);
+ live->worker = worker;
+ kthread_bind(worker, source_cpu);
+ sched_set_fifo(worker);
+ KUNIT_ASSERT_EQ(test, 0,
+ register_trace_event_irqexit(irqexit_observe_event, live));
+ live->observer_attached = true;
+ KUNIT_ASSERT_EQ(test, irqexit_observe_boundaries(live), 0);
+ mon->enabled = 1;
+
+ KUNIT_ASSERT_EQ(test, preempt_count(), 0);
+ local_irq_save(flags);
+ live->entries = 0;
+ live->exits = 0;
+ live->switches_out = 0;
+ live->switches_in = 0;
+ /*
+ * The FIFO worker runs only after this task switches out inside the
+ * real helper. It moves the queued, non-running owner to another CPU.
+ * Do not migration-disable the owner: migration is the property tested.
+ */
+ wake_up_process(worker);
+ preempt_schedule_irq();
+ resumed_cpu = raw_smp_processor_id();
+ mon->enabled = 0;
+ local_irq_restore(flags);
+ KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&live->worker_done, 5 * HZ), 0UL);
+ KUNIT_EXPECT_EQ(test, live->affinity_result, 0);
+ KUNIT_EXPECT_EQ(test, live->entries, 1U);
+ KUNIT_EXPECT_EQ(test, live->exits, 1U);
+ KUNIT_EXPECT_GT(test, live->switches_out, 0U);
+ KUNIT_EXPECT_EQ(test, live->switches_out, live->switches_in);
+ KUNIT_EXPECT_EQ(test, live->entry_cpu, source_cpu);
+ KUNIT_EXPECT_EQ(test, live->switch_out_cpu, source_cpu);
+ KUNIT_EXPECT_EQ(test, live->switch_in_cpu, live->target_cpu);
+ KUNIT_EXPECT_EQ(test, live->exit_cpu, live->target_cpu);
+ /* Check actual resumed execution independently of the RV observer. */
+ KUNIT_EXPECT_EQ(test, resumed_cpu, live->target_cpu);
+ KUNIT_EXPECT_GT(test, live->raw_entries, 0U);
+ KUNIT_EXPECT_GT(test, live->raw_exits, 0U);
+ KUNIT_EXPECT_EQ(test, live->bad_raw_context, 0U);
+ restore_result = set_cpus_allowed_ptr(current, &live->saved_mask);
+ KUNIT_EXPECT_EQ(test, restore_result, 0);
+ affinity_restored = !restore_result &&
+ cpumask_equal(current->cpus_ptr, &live->saved_mask);
+ KUNIT_EXPECT_TRUE(test, affinity_restored);
+ if (affinity_restored)
+ live->affinity_changed = false;
+ RV_KUNIT_EXPECT_NO_REACTION(test, ctx);
+ kunit_info(test, "live IRQ-exit migration cpu=%u->%u actual=%u entries=%u exits=%u switches=%u\n",
+ live->entry_cpu, live->exit_cpu, resumed_cpu, live->entries, live->exits,
+ live->switches_out);
+ kunit_info(test, "live IRQ-exit affinity restored=%u\n", affinity_restored);
+}
+#else
+static void rv_test_irqexit_live(struct kunit *test)
+{
+ /* preempt_schedule_irq() is deliberately not exported to modules. */
+ kunit_skip(test, "Requires built-in RV tests");
+}
+
+static void rv_test_irqexit_migrate(struct kunit *test)
+{
+ kunit_skip(test, "Requires built-in RV tests");
+}
+#endif
+
+#else
+#define rv_test_irqexit rv_test_stub
+#define rv_test_irqexit_invalid rv_test_stub
+#define rv_test_irqexit_sync rv_test_stub
+#define rv_test_irqexit_live rv_test_stub
+#define rv_test_irqexit_migrate rv_test_stub
+#endif
diff --git a/kernel/trace/rv/monitors/irqexit/irqexit_kunit.h b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.h
new file mode 100644
index 0000000000000..b475388455c2a
--- /dev/null
+++ b/kernel/trace/rv/monitors/irqexit/irqexit_kunit.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __IRQEXIT_KUNIT_H
+#define __IRQEXIT_KUNIT_H
+
+#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
+
+#include <linux/rv.h>
+#include <rv/kunit.h>
+
+extern const struct rv_irqexit_ops {
+ struct rv_kunit_mon mon;
+ void (*handle_resched_entry)(void *data, struct task_struct *owner);
+ void (*handle_resched_exit)(void *data, struct task_struct *owner);
+ void (*handle_sched_switch)(void *data, bool preempt,
+ struct task_struct *prev,
+ struct task_struct *next,
+ unsigned int prev_state);
+} rv_irqexit_ops;
+#endif
+
+#endif /* __IRQEXIT_KUNIT_H */
diff --git a/kernel/trace/rv/rv_monitors_test.c b/kernel/trace/rv/rv_monitors_test.c
index 4fb611a9f8b85..9eb37cbd2de22 100644
--- a/kernel/trace/rv/rv_monitors_test.c
+++ b/kernel/trace/rv/rv_monitors_test.c
@@ -150,6 +150,7 @@ static void rv_test_dummy(struct kunit *test)
#include "monitors/sssw/sssw_kunit.c"
#include "monitors/sts/sts_kunit.c"
#include "monitors/irqstate/irqstate_kunit.c"
+#include "monitors/irqexit/irqexit_kunit.c"
#include "monitors/opid/opid_kunit.c"
#include "monitors/nomiss/nomiss_kunit.c"
#include "monitors/pagefault/pagefault_kunit.c"
@@ -171,6 +172,18 @@ static struct kunit_case rv_mon_test_cases[] = {
KUNIT_CASE(rv_test_irqstate_preempt),
KUNIT_CASE(rv_test_irqstate_rt_prepare_irq),
KUNIT_CASE(rv_test_irqstate_rt_prepare_switch),
+ KUNIT_CASE(rv_test_irqexit),
+ KUNIT_CASE(rv_test_irqexit_invalid),
+ KUNIT_CASE(rv_test_irqexit_sync),
+#if IS_REACHABLE(CONFIG_RV_MON_IRQEXIT) && !defined(MODULE)
+ KUNIT_CASE_PARAM_WITH_INIT(rv_test_irqexit_live, kunit_array_gen_params,
+ irqexit_live_fixture_init, irqexit_live_fixture_exit),
+ KUNIT_CASE_PARAM_WITH_INIT(rv_test_irqexit_migrate, kunit_array_gen_params,
+ irqexit_live_fixture_init, irqexit_live_fixture_exit),
+#else
+ KUNIT_CASE(rv_test_irqexit_live),
+ KUNIT_CASE(rv_test_irqexit_migrate),
+#endif
KUNIT_CASE(rv_test_opid),
KUNIT_CASE(rv_test_nomiss),
KUNIT_CASE(rv_test_pagefault),
--
2.39.5 (Apple Git-154)
next prev parent reply other threads:[~2026-10-22 5:07 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 5:07 [RFC PATCH 0/9] rv: Verify generic IRQ context transitions Karl Mehltretter
2026-10-22 5:07 ` [RFC PATCH 1/9] tracing/irq: Add generic IRQ context boundary events Karl Mehltretter
2026-10-22 5:44 ` sashiko-bot
2026-10-22 5:07 ` [RFC PATCH 2/9] tracing/sched: Add IRQ preemption lifecycle events Karl Mehltretter
2026-10-22 5:07 ` [RFC PATCH 3/9] rv: Add a monitor for generic IRQ context transitions Karl Mehltretter
2026-10-22 5:07 ` [RFC PATCH 4/9] rv: Add a monitor for IRQ-exit scheduling ownership Karl Mehltretter
2026-10-22 5:07 ` [RFC PATCH 5/9] docs/rv: Document the IRQ-entry monitors Karl Mehltretter
2026-10-22 5:07 ` [RFC PATCH 6/9] rv: Add KUnit coverage for IRQ context transitions Karl Mehltretter
2026-10-22 5:33 ` sashiko-bot
2026-10-22 5:07 ` Karl Mehltretter [this message]
2026-10-22 5:30 ` [RFC PATCH 7/9] rv: Add KUnit coverage for IRQ-exit scheduling sashiko-bot
2026-10-22 5:07 ` [RFC PATCH 8/9] rv: Test empty PREEMPT_RT softirq worker intervals Karl Mehltretter
2026-10-22 5:07 ` [RFC PATCH 9/9] rv: Test IRQ-exit softirq provenance and handoff Karl Mehltretter
2026-10-22 5:33 ` sashiko-bot
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=20261022050712.75035-8-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 \
/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®