* [RFC PATCH v1 0/2] genirq: add migratable NMI registration and show NMI counts
@ 2026-11-14 23:49 Ashok Raj
2026-11-14 23:49 ` [RFC PATCH v1 1/2] genirq: Add migratable NMI registration mode Ashok Raj
2026-11-14 23:49 ` [RFC PATCH v1 2/2] genirq/proc: Show NMI interrupt counts in /proc/interrupts Ashok Raj
0 siblings, 2 replies; 3+ messages in thread
From: Ashok Raj @ 2026-11-14 23:49 UTC (permalink / raw)
To: Linux ARM Kernel
Cc: Linux Kernel Mailing List, Vladimir Murzin, Catalin Marinas,
Will Deacon, Mark Rutland, Lorenzo Pieralisi, Jinjie Ruan,
Liaochang, Marc Zyngier, Radu Rendec, Jonathan Cameron,
Ashok Raj, Sathvika Vasireddy
request_nmi() currently models NMIs as per-CPU sources. This does not
represent a globally routed NMI source with an affinity-capable irqchip,
which needs to be rerouted when its target CPU is offlined.
This RFC adds IRQF_NMI_MIGRATABLE as a request_nmi() registration mode
for such a source. It remains excluded from normal IRQ balancing, but the
generic startup and CPU-hotplug paths may select an online target. The
second patch fixes the related /proc/interrupts display: NMI-safe flow
handling updates the per-CPU counters rather than irq_desc::tot_count.
Patch 1: Add the migratable-NMI registration mode, validate that it is
used only through request_nmi() with an affinity-capable non-per-CPU
irqchip, and add IRQ KUnit coverage for registration and CPU-offline
migration.
Patch 2: Emit the per-CPU counters for all NMI descriptors, matching
kstat_irqs_desc().
Testing:
- ARM64 changed-object builds with WERROR=1
- ARM64 !SMP changed-object build
- ARM64 IRQ KUnit run under QEMU with two CPUs, including registration
validation and CPU-offline migration
- local downstream GHES delivery and migration validation for the intended
consumer topology (not part of this series)
Base and dependencies:
This RFC was developed and tested on v7.4-rc2 plus the FEAT_NMI v2 series
[1]. It depends on that series' GIC NMI support and is therefore not directly
applicable to mainline or linux-next as posted. This RFC is for design
feedback; it will need rebasing onto the upstream form of those prerequisites
before submission.
[1] https://lore.kernel.org/linux-arm-kernel/20261112132042.129062-1-vladimir.murzin@arm.com/
A separate GHES Type-4 series is the intended first consumer and is not
included here.
Feedback is particularly welcome on the IRQF_NMI_MIGRATABLE API and its
placement in the generic IRQ API, as well as the NMI accounting presentation
in /proc/interrupts.
Ashok Raj (2):
genirq: Add migratable NMI registration mode
genirq/proc: Show NMI interrupt counts in /proc/interrupts
include/linux/interrupt.h | 2 +
kernel/irq/irq_test.c | 141 +++++++++++++++++++++++++++++++++++++-
kernel/irq/manage.c | 33 +++++++--
kernel/irq/proc.c | 7 +-
4 files changed, 172 insertions(+), 11 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [RFC PATCH v1 1/2] genirq: Add migratable NMI registration mode
2026-11-14 23:49 [RFC PATCH v1 0/2] genirq: add migratable NMI registration and show NMI counts Ashok Raj
@ 2026-11-14 23:49 ` Ashok Raj
2026-11-14 23:49 ` [RFC PATCH v1 2/2] genirq/proc: Show NMI interrupt counts in /proc/interrupts Ashok Raj
1 sibling, 0 replies; 3+ messages in thread
From: Ashok Raj @ 2026-11-14 23:49 UTC (permalink / raw)
To: Linux ARM Kernel
Cc: Linux Kernel Mailing List, Vladimir Murzin, Catalin Marinas,
Will Deacon, Mark Rutland, Lorenzo Pieralisi, Jinjie Ruan,
Liaochang, Marc Zyngier, Radu Rendec, Jonathan Cameron,
Ashok Raj, Sathvika Vasireddy
request_nmi() only supports per-CPU NMIs, so a globally routed NMI
source cannot use the generic CPU-offline IRQ migration path.
Add IRQF_NMI_MIGRATABLE for a non-per-CPU NMI. Require an
affinity-capable irqchip, preserve the NMI no-balancing policy, and
allow generic startup affinity and CPU-hotplug migration to route the
source. Keep local per-CPU NMIs unchanged.
Add KUnit coverage for registration validation and CPU-offline
migration.
Signed-off-by: Ashok Raj <ashok.raj@oss.qualcomm.com>
---
include/linux/interrupt.h | 2 +
kernel/irq/irq_test.c | 141 +++++++++++++++++++++++++++++++++++++-
kernel/irq/manage.c | 33 +++++++--
3 files changed, 168 insertions(+), 8 deletions(-)
diff --git a/include/linux/interrupt.h b/include/linux/interrupt.h
index ee2dd5349697..58c917ce7655 100644
--- a/include/linux/interrupt.h
+++ b/include/linux/interrupt.h
@@ -46,6 +46,7 @@
* IRQF_TIMER - Flag to mark this interrupt as timer interrupt
* IRQF_PERCPU - Interrupt is per cpu
* IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
+ * IRQF_NMI_MIGRATABLE - NMI source can be migrated during CPU hotplug
* IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
* registered first in a shared interrupt is considered for
* performance reasons)
@@ -86,6 +87,7 @@
#define IRQF_NO_AUTOEN 0x00080000
#define IRQF_NO_DEBUG 0x00100000
#define IRQF_COND_ONESHOT 0x00200000
+#define IRQF_NMI_MIGRATABLE 0x00400000
#define IRQF_TIMER (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
diff --git a/kernel/irq/irq_test.c b/kernel/irq/irq_test.c
index e2d31914b3c4..5cc3caed661c 100644
--- a/kernel/irq/irq_test.c
+++ b/kernel/irq/irq_test.c
@@ -25,7 +25,7 @@ static int noop_affinity(struct irq_data *data, const struct cpumask *dest,
{
irq_data_update_effective_affinity(data, dest);
- return 0;
+ return IRQ_SET_MASK_OK_DONE;
}
static struct irq_chip fake_irq_chip = {
@@ -41,6 +41,40 @@ static struct irq_chip fake_irq_chip = {
.flags = IRQCHIP_SKIP_SET_WAKE,
};
+static int noop_nmi_setup(struct irq_data *data)
+{
+ return 0;
+}
+
+static struct irq_chip fake_nmi_irq_chip = {
+ .name = "fake-nmi",
+ .irq_startup = noop_ret,
+ .irq_shutdown = noop,
+ .irq_enable = noop,
+ .irq_disable = noop,
+ .irq_ack = noop,
+ .irq_mask = noop,
+ .irq_unmask = noop,
+ .irq_set_affinity = noop_affinity,
+ .irq_nmi_setup = noop_nmi_setup,
+ .irq_nmi_teardown = noop,
+ .flags = IRQCHIP_SKIP_SET_WAKE | IRQCHIP_SUPPORTS_NMI,
+};
+
+static struct irq_chip fake_nmi_noaffinity_irq_chip = {
+ .name = "fake-nmi-noaffinity",
+ .irq_startup = noop_ret,
+ .irq_shutdown = noop,
+ .irq_enable = noop,
+ .irq_disable = noop,
+ .irq_ack = noop,
+ .irq_mask = noop,
+ .irq_unmask = noop,
+ .irq_nmi_setup = noop_nmi_setup,
+ .irq_nmi_teardown = noop,
+ .flags = IRQCHIP_SKIP_SET_WAKE | IRQCHIP_SUPPORTS_NMI,
+};
+
static int irq_test_setup_fake_irq(struct kunit *test, struct irq_affinity_desc *affd)
{
struct irq_desc *desc;
@@ -60,6 +94,22 @@ static int irq_test_setup_fake_irq(struct kunit *test, struct irq_affinity_desc
return virq;
}
+static int irq_test_setup_fake_nmi(struct kunit *test, struct irq_chip *chip)
+{
+ struct irq_desc *desc;
+ int virq;
+
+ virq = irq_domain_alloc_descs(-1, 1, 0, NUMA_NO_NODE, NULL);
+ KUNIT_ASSERT_GE(test, virq, 0);
+ irq_set_chip_and_handler(virq, chip, handle_simple_irq);
+
+ desc = irq_to_desc(virq);
+ KUNIT_ASSERT_PTR_NE(test, desc, NULL);
+ irq_settings_clr_norequest(desc);
+
+ return virq;
+}
+
static void irq_disable_depth_test(struct kunit *test)
{
struct irq_desc *desc;
@@ -164,6 +214,93 @@ static void irq_shutdown_depth_test(struct kunit *test)
free_irq(virq, NULL);
}
+static void irq_nmi_migratable_test(struct kunit *test)
+{
+ struct irq_desc *desc;
+ struct irq_data *data;
+ const struct cpumask *affinity;
+ int virq, ret;
+
+ if (!IS_ENABLED(CONFIG_SMP))
+ kunit_skip(test, "requires CONFIG_SMP for NMI migration");
+ if (!get_cpu_device(1))
+ kunit_skip(test, "requires more than 1 CPU for NMI migration");
+ if (!cpu_is_hotpluggable(1))
+ kunit_skip(test, "CPU 1 must be hotpluggable");
+ if (!cpu_online(1))
+ kunit_skip(test, "CPU 1 must be online");
+
+ virq = irq_test_setup_fake_nmi(test, &fake_nmi_irq_chip);
+ desc = irq_to_desc(virq);
+ KUNIT_ASSERT_PTR_NE(test, desc, NULL);
+
+ data = irq_desc_get_irq_data(desc);
+ KUNIT_ASSERT_PTR_NE(test, data, NULL);
+
+ ret = request_nmi(virq, noop_handler,
+ IRQF_NMI_MIGRATABLE | IRQF_NO_AUTOEN,
+ "test_nmi", NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_TRUE(test, irq_is_nmi(desc));
+ KUNIT_EXPECT_FALSE(test, irqd_is_per_cpu(data));
+ KUNIT_EXPECT_FALSE(test, irq_settings_is_per_cpu(desc));
+ /* Userspace affinity remains disabled by IRQF_NOBALANCING. */
+ KUNIT_EXPECT_FALSE(test, irq_can_set_affinity(virq));
+ KUNIT_EXPECT_EQ(test, desc->depth, 1);
+
+ enable_nmi(virq);
+ KUNIT_EXPECT_TRUE(test, irqd_is_started(data));
+ KUNIT_EXPECT_EQ(test, desc->depth, 0);
+
+ ret = irq_set_affinity(virq, cpumask_of(1));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ affinity = irq_data_get_effective_affinity_mask(data);
+ KUNIT_EXPECT_TRUE(test, cpumask_test_cpu(1, affinity));
+
+ ret = remove_cpu(1);
+ if (ret) {
+ KUNIT_FAIL(test, "failed to offline CPU 1: %d", ret);
+ goto out_disable_nmi;
+ }
+
+ affinity = irq_data_get_effective_affinity_mask(data);
+ KUNIT_EXPECT_FALSE(test, cpumask_test_cpu(1, affinity));
+ KUNIT_EXPECT_TRUE(test, cpumask_intersects(affinity, cpu_online_mask));
+
+ ret = add_cpu(1);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+out_disable_nmi:
+ disable_nmi_nosync(virq);
+ free_nmi(virq, NULL);
+}
+
+static void irq_nmi_migratable_invalid_test(struct kunit *test)
+{
+ int virq, ret;
+
+ virq = irq_test_setup_fake_nmi(test, &fake_nmi_irq_chip);
+ ret = request_nmi(virq, noop_handler, IRQF_NO_AUTOEN, "test_nmi", NULL);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ ret = request_nmi(virq, noop_handler,
+ IRQF_PERCPU | IRQF_NMI_MIGRATABLE | IRQF_NO_AUTOEN,
+ "test_nmi", NULL);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ ret = request_irq(virq, noop_handler,
+ IRQF_NMI_MIGRATABLE, "test_irq", NULL);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ virq = irq_test_setup_fake_nmi(test, &fake_nmi_noaffinity_irq_chip);
+ ret = request_nmi(virq, noop_handler,
+ IRQF_NMI_MIGRATABLE | IRQF_NO_AUTOEN,
+ "test_nmi", NULL);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+}
+
static void irq_cpuhotplug_test(struct kunit *test)
{
struct irq_desc *desc;
@@ -222,6 +359,8 @@ static struct kunit_case irq_test_cases[] = {
KUNIT_CASE(irq_disable_depth_test),
KUNIT_CASE(irq_free_disabled_test),
KUNIT_CASE(irq_shutdown_depth_test),
+ KUNIT_CASE(irq_nmi_migratable_test),
+ KUNIT_CASE(irq_nmi_migratable_invalid_test),
KUNIT_CASE(irq_cpuhotplug_test),
{}
};
diff --git a/kernel/irq/manage.c b/kernel/irq/manage.c
index 9c32607f1ac3..d9f28e40affc 100644
--- a/kernel/irq/manage.c
+++ b/kernel/irq/manage.c
@@ -155,6 +155,8 @@ EXPORT_SYMBOL(synchronize_irq);
#ifdef CONFIG_SMP
cpumask_var_t irq_default_affinity;
+static bool irq_is_migratable_nmi(struct irq_desc *desc);
+
static bool __irq_can_set_affinity(struct irq_desc *desc)
{
if (!desc || !irqd_can_balance(&desc->irq_data) ||
@@ -599,8 +601,8 @@ int irq_setup_affinity(struct irq_desc *desc)
static DEFINE_RAW_SPINLOCK(mask_lock);
static struct cpumask mask;
- /* Excludes PER_CPU and NO_BALANCE interrupts */
- if (!__irq_can_set_affinity(desc))
+ /* Excludes PER_CPU and NO_BALANCE interrupts. */
+ if (!__irq_can_set_affinity(desc) && !irq_is_migratable_nmi(desc))
return 0;
guard(raw_spinlock)(&mask_lock);
@@ -1384,6 +1386,15 @@ static bool irq_supports_nmi(struct irq_desc *desc)
return d->chip->flags & IRQCHIP_SUPPORTS_NMI;
}
+#ifdef CONFIG_SMP
+static bool irq_is_migratable_nmi(struct irq_desc *desc)
+{
+ return irq_is_nmi(desc) && desc->action &&
+ (desc->action->flags & IRQF_NMI_MIGRATABLE) &&
+ desc->irq_data.chip->irq_set_affinity;
+}
+#endif
+
static int irq_nmi_setup(struct irq_desc *desc)
{
struct irq_data *d = irq_desc_get_irq_data(desc);
@@ -2153,7 +2164,8 @@ int request_threaded_irq(unsigned int irq, irq_handler_t handler,
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
- if (((irqflags & IRQF_SHARED) && !dev_id) ||
+ if ((irqflags & IRQF_NMI_MIGRATABLE) ||
+ ((irqflags & IRQF_SHARED) && !dev_id) ||
((irqflags & IRQF_SHARED) && (irqflags & IRQF_NO_AUTOEN)) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
@@ -2275,8 +2287,9 @@ EXPORT_SYMBOL_GPL(request_any_context_irq);
* An interrupt line delivering NMIs cannot be shared and IRQ handling
* cannot be threaded.
*
- * Interrupt lines requested for NMI delivering must produce per cpu
- * interrupts and have auto enabling setting disabled.
+ * Interrupt lines requested for NMI delivery must either produce per CPU
+ * interrupts or be migratable during CPU hotplug, and must have auto enabling
+ * disabled.
*
* @dev_id must be globally unique. Normally the address of the device data
* structure is used as the cookie. Since the handler receives this value
@@ -2299,7 +2312,9 @@ int request_nmi(unsigned int irq, irq_handler_t handler,
if (irqflags & (IRQF_SHARED | IRQF_COND_SUSPEND | IRQF_IRQPOLL))
return -EINVAL;
- if (!(irqflags & IRQF_PERCPU))
+ /* Exactly one NMI topology must be specified. */
+ if (!!(irqflags & IRQF_PERCPU) ==
+ !!(irqflags & IRQF_NMI_MIGRATABLE))
return -EINVAL;
if (!handler)
@@ -2311,7 +2326,11 @@ int request_nmi(unsigned int irq, irq_handler_t handler,
!(irqflags & IRQF_NO_AUTOEN)) ||
!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)) ||
- !irq_supports_nmi(desc))
+ !irq_supports_nmi(desc) ||
+ ((irqflags & IRQF_NMI_MIGRATABLE) &&
+ (irqd_is_per_cpu(&desc->irq_data) ||
+ irq_settings_is_per_cpu(desc) ||
+ !desc->irq_data.chip->irq_set_affinity)))
return -EINVAL;
action = kzalloc_obj(struct irqaction);
--
2.43.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [RFC PATCH v1 2/2] genirq/proc: Show NMI interrupt counts in /proc/interrupts
2026-11-14 23:49 [RFC PATCH v1 0/2] genirq: add migratable NMI registration and show NMI counts Ashok Raj
2026-11-14 23:49 ` [RFC PATCH v1 1/2] genirq: Add migratable NMI registration mode Ashok Raj
@ 2026-11-14 23:49 ` Ashok Raj
1 sibling, 0 replies; 3+ messages in thread
From: Ashok Raj @ 2026-11-14 23:49 UTC (permalink / raw)
To: Linux ARM Kernel
Cc: Linux Kernel Mailing List, Vladimir Murzin, Catalin Marinas,
Will Deacon, Mark Rutland, Lorenzo Pieralisi, Jinjie Ruan,
Liaochang, Marc Zyngier, Radu Rendec, Jonathan Cameron,
Ashok Raj, Sathvika Vasireddy
NMI flow handlers update only the per-CPU counters because
irq_desc::tot_count is not safe to update in NMI context. As a result,
/proc/interrupts shows zero counts for a non-per-CPU NMI.
Display the per-CPU counters for NMI descriptors, matching
kstat_irqs_desc().
Signed-off-by: Ashok Raj <ashok.raj@oss.qualcomm.com>
---
kernel/irq/proc.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/kernel/irq/proc.c b/kernel/irq/proc.c
index 0c0df16de581..490b2df9c105 100644
--- a/kernel/irq/proc.c
+++ b/kernel/irq/proc.c
@@ -559,11 +559,12 @@ static int irq_seq_show(struct seq_file *p, void *v)
seq_putc(p, ':');
/*
- * Always output per CPU interrupts. Output device interrupts only when
- * desc::tot_count is not zero.
+ * Always output per CPU interrupts and NMIs, which only account in
+ * per-CPU counters. Output device interrupts only when desc::tot_count
+ * is not zero.
*/
if (irq_settings_is_per_cpu(desc) || irq_settings_is_per_cpu_devid(desc) ||
- data_race(desc->tot_count))
+ irq_is_nmi(desc) || data_race(desc->tot_count))
irq_proc_emit_counts(p, &desc->kstat_irqs->cnt);
else
irq_proc_emit_zero_counts(p, num_online_cpus());
--
2.43.0
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-14 23:50 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-14 23:49 [RFC PATCH v1 0/2] genirq: add migratable NMI registration and show NMI counts Ashok Raj
2026-11-14 23:49 ` [RFC PATCH v1 1/2] genirq: Add migratable NMI registration mode Ashok Raj
2026-11-14 23:49 ` [RFC PATCH v1 2/2] genirq/proc: Show NMI interrupt counts in /proc/interrupts Ashok Raj
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®