[PATCH] irqchip/riscv-imsic: Synchronize remote state with IRQ work
Anup Patel
anup at brainfault.org
Tue Nov 3 07:52:17 PST 2026
On Tue, Nov 3, 2026 at 1:34 PM Troy Mitchell <troy.mitchell at linux.dev> wrote:
>
> Remote IMSIC state updates are deferred to a timer on the target CPU.
> If that CPU has stopped its local clockevent and is waiting for a
> broadcast interrupt, enabling the broadcast IRQ can depend on delivery
> of that same interrupt. This can stall timer delivery when the broadcast
> IRQ is unmasked or moved to another CPU, including during CPU hotplug.
>
> Queue hard IRQ work to synchronize remote state independently of timer
> delivery. Keep synchronous updates on the local CPU and the timer retry
> for an unfinished vector move. Hard IRQ work also avoids depending on a
> schedulable worker when PREEMPT_RT is enabled.
Using timer retry for local CPU and IRQ work queue for remote CPUs looks
strange. Why not use IRQ work queue even for local CPU ??
>
> Fixes: 21a8f8a0eb35 ("irqchip: Add RISC-V incoming MSI controller early driver")
> Signed-off-by: Troy Mitchell <troy.mitchell at linux.dev>
> ---
> This was found while bringing up the SoC timer on SpacemiT K3 as a tick
> broadcast device.
>
> Without a real broadcast device, the tick code falls back to the hrtimer
> based broadcast, which keeps one CPU's local timer running. Once the SoC
> timer is registered it replaces the hrtimer broadcast, and every CPU may
> stop its local timer in idle.
>
> On K3 the SoC timer interrupt goes through APLIC in MSI mode to IMSIC.
> When the CPU owning it goes offline, the interrupt is moved to a CPU
> that may already be idle with its local timer stopped. IMSIC leaves the
> enable on the new target to a timer on that CPU, and that timer can only
> fire through the broadcast interrupt, which is not enabled yet. The
> system stalls.
>
> Tested on a K3 COM260 board by offlining the CPU owning the broadcast
> interrupt, and then each CPU in turn, under a timerfd load:
> - v7.4-rc1 with the SpacemiT SoC timer series and local timers forced
> to stop in idle
> - a vendor 6.18 kernel with SBI cpuidle states that stop the local timer
>
> Both stall without this patch and pass with it.
> ---
> drivers/irqchip/Kconfig | 1 +
> drivers/irqchip/irq-riscv-imsic-state.c | 19 +++++++++++++------
> drivers/irqchip/irq-riscv-imsic-state.h | 4 ++++
> 3 files changed, 18 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/irqchip/Kconfig b/drivers/irqchip/Kconfig
> index 136bab41f801..5cdc3f52198c 100644
> --- a/drivers/irqchip/Kconfig
> +++ b/drivers/irqchip/Kconfig
> @@ -651,6 +651,7 @@ config RISCV_IMSIC
> bool
> depends on RISCV
> select IRQ_DOMAIN_HIERARCHY
> + select IRQ_WORK if SMP
> select GENERIC_IRQ_MATRIX_ALLOCATOR
> select GENERIC_MSI_IRQ
> select IRQ_MSI_LIB
> diff --git a/drivers/irqchip/irq-riscv-imsic-state.c b/drivers/irqchip/irq-riscv-imsic-state.c
> index 01620373b43a..f6373e4019a2 100644
> --- a/drivers/irqchip/irq-riscv-imsic-state.c
> +++ b/drivers/irqchip/irq-riscv-imsic-state.c
> @@ -252,6 +252,11 @@ void imsic_local_delivery(bool enable)
> }
>
> #ifdef CONFIG_SMP
> +static void imsic_local_sync_work(struct irq_work *work)
> +{
> + imsic_local_sync_all(false);
> +}
> +
> static void imsic_local_timer_callback(struct timer_list *timer)
> {
> imsic_local_sync_all(false);
> @@ -268,18 +273,19 @@ static void __imsic_remote_sync(struct imsic_local_priv *lpriv, unsigned int cpu
> */
>
> /*
> - * We schedule a timer on the target CPU if the target CPU is not
> - * same as the current CPU. An offline CPU will unconditionally
> - * synchronize IDs through imsic_starting_cpu() when the
> - * CPU is brought up.
> + * A remote CPU may have stopped its local timer and be waiting for
> + * the broadcast interrupt whose IMSIC state needs updating. Use an
> + * IPI-backed work item so enabling that interrupt does not depend on
> + * its delivery. An offline CPU synchronizes IDs in imsic_starting_cpu().
> */
> if (cpu_online(cpu)) {
> if (cpu == smp_processor_id()) {
> if (__imsic_local_sync(lpriv))
> return;
> + __imsic_local_timer_start(lpriv, cpu);
> + } else {
> + irq_work_queue_on(&lpriv->sync_work, cpu);
> }
> -
> - __imsic_local_timer_start(lpriv, cpu);
> }
> }
> #else
> @@ -510,6 +516,7 @@ static int __init imsic_local_init(void)
> #ifdef CONFIG_SMP
> /* Setup lazy timer for synchronization */
> timer_setup(&lpriv->timer, imsic_local_timer_callback, TIMER_PINNED);
> + lpriv->sync_work = IRQ_WORK_INIT_HARD(imsic_local_sync_work);
> #endif
>
> /* Allocate vector array */
> diff --git a/drivers/irqchip/irq-riscv-imsic-state.h b/drivers/irqchip/irq-riscv-imsic-state.h
> index 189829b8673a..a36b9f0ef840 100644
> --- a/drivers/irqchip/irq-riscv-imsic-state.h
> +++ b/drivers/irqchip/irq-riscv-imsic-state.h
> @@ -9,6 +9,7 @@
>
> #include <linux/irqchip/riscv-imsic.h>
> #include <linux/irqdomain.h>
> +#include <linux/irq_work.h>
> #include <linux/fwnode.h>
> #include <linux/timer.h>
>
> @@ -34,6 +35,9 @@ struct imsic_local_priv {
> unsigned long *dirty_bitmap;
>
> #ifdef CONFIG_SMP
> + /* Remote synchronization must not depend on a timer interrupt. */
> + struct irq_work sync_work;
> +
> /* Local timer for synchronization */
> struct timer_list timer;
> #endif
>
> ---
> base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
> change-id: 20261103-imsic-remote-sync-irqwork-9cfec6c410de
>
> Best regards,
> --
> Troy Mitchell <troy.mitchell at linux.dev>
>
Regards,
Anup
More information about the linux-riscv
mailing list