[PATCH v4 2/2] KVM: RISC-V: Add CPU PM notifier for non-retention idle states
Anup Patel
anup at brainfault.org
Sat Aug 8 09:38:36 PDT 2026
On Wed, Jul 22, 2026 at 11:22 AM Yong-Xuan Wang
<yongxuan.wang at sifive.com> wrote:
>
> Register a CPU_PM notifier to restore hypervisor CSR content during CPU
> non-retention idle states. When a CPU enters a deep idle state that
> powers off the CPU domain, hypervisor CSRs and VS CSRs lose their state
> and must be saved before entry and restored after exit.
>
> This completes KVM's power management coverage for RISC-V:
> - CPU hotplug: handled by kvm_online_cpu/kvm_offline_cpu (cpuhp callbacks)
> - System suspend: handled by kvm_suspend/kvm_resume (syscore ops)
> - CPU idle (retention): no action needed, CSRs are retained
> - CPU idle (non-retention): handled by this CPU_PM notifier
>
> Signed-off-by: Yong-Xuan Wang <yongxuan.wang at sifive.com>
> ---
> arch/riscv/include/asm/kvm_aia.h | 15 +++-
> arch/riscv/kvm/aia.c | 144 ++++++++++++++++++++++-----------------
> arch/riscv/kvm/main.c | 87 ++++++++++++++++++-----
> 3 files changed, 163 insertions(+), 83 deletions(-)
>
> diff --git a/arch/riscv/include/asm/kvm_aia.h b/arch/riscv/include/asm/kvm_aia.h
> index c67ec5ac0a14..7c1c3250598f 100644
> --- a/arch/riscv/include/asm/kvm_aia.h
> +++ b/arch/riscv/include/asm/kvm_aia.h
> @@ -165,8 +165,19 @@ int kvm_riscv_aia_alloc_hgei(int cpu, struct kvm_vcpu *owner,
> void __iomem **hgei_va, phys_addr_t *hgei_pa);
> void kvm_riscv_aia_free_hgei(int cpu, int hgei);
>
> -void kvm_riscv_aia_enable(void);
> -void kvm_riscv_aia_disable(void);
> +/**
> + * kvm_riscv_aia_enable() - Enable AIA support on current CPU
> + * @full_cleanup: true = full hardware init (CPU hotplug/module load),
> + * false = lightweight CSR restore (CPU non-retention idle resume)
> + */
> +void kvm_riscv_aia_enable(bool full_cleanup);
> +
> +/**
> + * kvm_riscv_aia_disable() - Disable AIA support on current CPU
> + * @full_cleanup: true = full hardware cleanup (CPU hotplug/module exit),
> + * false = lightweight CSR save (CPU non-retention idle entry)
> + */
> +void kvm_riscv_aia_disable(bool full_cleanup);
Instead of re-using kvm_riscv_aia_enable()/disable()
in PM entry/exit path, I suggest keeping these functions
as-is and introduce new kvm_riscv_aia_pm_enter()/exit()
functions.
> int kvm_riscv_aia_init(void);
> void kvm_riscv_aia_exit(void);
>
> diff --git a/arch/riscv/kvm/aia.c b/arch/riscv/kvm/aia.c
> index bafb009c5ce5..4cf351f91c64 100644
> --- a/arch/riscv/kvm/aia.c
> +++ b/arch/riscv/kvm/aia.c
> @@ -25,6 +25,7 @@ struct aia_hgei_control {
> unsigned long free_bitmap;
> struct kvm_vcpu *owners[BITS_PER_LONG];
> unsigned int nr_hgei;
> + unsigned long saved_value;
> };
> static DEFINE_PER_CPU(struct aia_hgei_control, aia_hgei);
> static int hgei_parent_irq;
> @@ -532,50 +533,59 @@ static void aia_hgei_exit(void)
> free_percpu_irq(hgei_parent_irq, &aia_hgei);
> }
>
> -void kvm_riscv_aia_enable(void)
> +void kvm_riscv_aia_enable(bool full_cleanup)
> {
> - const struct imsic_global_config *gc;
> - const struct imsic_local_config *lc;
> struct aia_hgei_control *hgctrl;
> unsigned long flags;
> - int aia_nr_hgei;
>
> if (!kvm_riscv_aia_available())
> return;
>
> - gc = imsic_get_global_config();
> - lc = (gc) ? this_cpu_ptr(gc->local) : NULL;
> hgctrl = this_cpu_ptr(&aia_hgei);
>
> - /* Figure-out number of bits in HGEIE */
> - csr_write(CSR_HGEIE, -1UL);
> - hgctrl->nr_hgei = fls_long(csr_read(CSR_HGEIE));
> - csr_write(CSR_HGEIE, 0);
> - if (hgctrl->nr_hgei)
> - hgctrl->nr_hgei--;
> + if (full_cleanup) {
> + const struct imsic_global_config *gc;
> + const struct imsic_local_config *lc;
> + int aia_nr_hgei;
>
> - /*
> - * Number of usable per-HART HGEI lines should be minimum of
> - * per-HART IMSIC guest files and number of bits in HGEIE.
> - */
> - if (lc)
> - hgctrl->nr_hgei = min((ulong)hgctrl->nr_hgei, lc->nr_guest_files);
> - else
> - hgctrl->nr_hgei = 0;
> + gc = imsic_get_global_config();
> + lc = (gc) ? this_cpu_ptr(gc->local) : NULL;
>
> - /* Update the number of IMSIC guest files across all HARTs */
> - aia_nr_hgei = atomic_read(&kvm_riscv_aia_nr_hgei);
> - do {
> - if (aia_nr_hgei <= hgctrl->nr_hgei)
> - break;
> - } while (!atomic_try_cmpxchg(&kvm_riscv_aia_nr_hgei, &aia_nr_hgei, hgctrl->nr_hgei));
> + /* Figure-out number of bits in HGEIE */
> + csr_write(CSR_HGEIE, -1UL);
> + hgctrl->nr_hgei = fls_long(csr_read(CSR_HGEIE));
> + csr_write(CSR_HGEIE, 0);
> + if (hgctrl->nr_hgei)
> + hgctrl->nr_hgei--;
>
> - raw_spin_lock_irqsave(&hgctrl->lock, flags);
> - if (!hgctrl->free_bitmap_initialized) {
> - hgctrl->free_bitmap = (hgctrl->nr_hgei) ? GENMASK_ULL(hgctrl->nr_hgei, 1) : 0;
> - hgctrl->free_bitmap_initialized = true;
> + /*
> + * Number of usable per-HART HGEI lines should be minimum of
> + * per-HART IMSIC guest files and number of bits in HGEIE.
> + */
> + if (lc)
> + hgctrl->nr_hgei = min((ulong)hgctrl->nr_hgei, lc->nr_guest_files);
> + else
> + hgctrl->nr_hgei = 0;
> +
> + /* Update the number of IMSIC guest files across all HARTs */
> + aia_nr_hgei = atomic_read(&kvm_riscv_aia_nr_hgei);
> + do {
> + if (aia_nr_hgei <= hgctrl->nr_hgei)
> + break;
> + } while (!atomic_try_cmpxchg(&kvm_riscv_aia_nr_hgei, &aia_nr_hgei,
> + hgctrl->nr_hgei));
> +
> + raw_spin_lock_irqsave(&hgctrl->lock, flags);
> + if (!hgctrl->free_bitmap_initialized) {
> + hgctrl->free_bitmap = (hgctrl->nr_hgei) ?
> + GENMASK_ULL(hgctrl->nr_hgei, 1) :
> + 0;
> + hgctrl->free_bitmap_initialized = true;
> + }
> + raw_spin_unlock_irqrestore(&hgctrl->lock, flags);
> + } else {
> + csr_write(CSR_HGEIE, hgctrl->saved_value);
> }
> - raw_spin_unlock_irqrestore(&hgctrl->lock, flags);
>
> csr_write(CSR_HVICTL, aia_hvictl_value(false));
> csr_write(CSR_HVIPRIO1, 0x0);
> @@ -587,16 +597,17 @@ void kvm_riscv_aia_enable(void)
> csr_write(CSR_HVIPRIO2H, 0x0);
> #endif
>
> - /* Enable per-CPU SGEI interrupt */
> - enable_percpu_irq(hgei_parent_irq,
> - irq_get_trigger_type(hgei_parent_irq));
> + if (full_cleanup)
> + enable_percpu_irq(hgei_parent_irq,
> + irq_get_trigger_type(hgei_parent_irq));
> csr_set(CSR_HIE, BIT(IRQ_S_GEXT));
> +
> /* Enable IRQ filtering for overflow interrupt only if sscofpmf is present */
> if (__riscv_isa_extension_available(NULL, RISCV_ISA_EXT_SSCOFPMF))
> csr_set(CSR_HVIEN, BIT(IRQ_PMU_OVF));
> }
>
> -void kvm_riscv_aia_disable(void)
> +void kvm_riscv_aia_disable(bool full_cleanup)
> {
> int i;
> unsigned long flags;
> @@ -609,42 +620,49 @@ void kvm_riscv_aia_disable(void)
>
> if (__riscv_isa_extension_available(NULL, RISCV_ISA_EXT_SSCOFPMF))
> csr_clear(CSR_HVIEN, BIT(IRQ_PMU_OVF));
> - /* Disable per-CPU SGEI interrupt */
> - csr_clear(CSR_HIE, BIT(IRQ_S_GEXT));
> - disable_percpu_irq(hgei_parent_irq);
>
> - csr_write(CSR_HVICTL, aia_hvictl_value(false));
> -
> - raw_spin_lock_irqsave(&hgctrl->lock, flags);
> -
> - for (i = 0; i <= hgctrl->nr_hgei; i++) {
> - vcpu = hgctrl->owners[i];
> - if (!vcpu)
> - continue;
> + if (full_cleanup) {
> + /* Disable per-CPU SGEI interrupt */
> + csr_clear(CSR_HIE, BIT(IRQ_S_GEXT));
> + disable_percpu_irq(hgei_parent_irq);
> + }
>
> - /*
> - * We release hgctrl->lock before notifying IMSIC
> - * so that we don't have lock ordering issues.
> - */
> - raw_spin_unlock_irqrestore(&hgctrl->lock, flags);
> + csr_write(CSR_HVICTL, aia_hvictl_value(false));
>
> - /* Notify IMSIC */
> - kvm_riscv_vcpu_aia_imsic_release(vcpu);
> + if (full_cleanup) {
> + raw_spin_lock_irqsave(&hgctrl->lock, flags);
>
> - /*
> - * Wakeup VCPU if it was blocked so that it can
> - * run on other HARTs
> - */
> - if (csr_read(CSR_HGEIE) & BIT(i)) {
> - csr_clear(CSR_HGEIE, BIT(i));
> - kvm_vcpu_kick(vcpu);
> + for (i = 0; i <= hgctrl->nr_hgei; i++) {
> + vcpu = hgctrl->owners[i];
> + if (!vcpu)
> + continue;
> +
> + /*
> + * We release hgctrl->lock before notifying IMSIC
> + * so that we don't have lock ordering issues.
> + */
> + raw_spin_unlock_irqrestore(&hgctrl->lock, flags);
> +
> + /* Notify IMSIC */
> + kvm_riscv_vcpu_aia_imsic_release(vcpu);
> +
> + /*
> + * Wakeup VCPU if it was blocked so that it can
> + * run on other HARTs
> + */
> + if (csr_read(CSR_HGEIE) & BIT(i)) {
> + csr_clear(CSR_HGEIE, BIT(i));
> + kvm_vcpu_kick(vcpu);
> + }
> +
> + raw_spin_lock_irqsave(&hgctrl->lock, flags);
> }
>
> - raw_spin_lock_irqsave(&hgctrl->lock, flags);
> + raw_spin_unlock_irqrestore(&hgctrl->lock, flags);
> + } else {
> + hgctrl->saved_value = csr_read(CSR_HGEIE);
> }
>
> - raw_spin_unlock_irqrestore(&hgctrl->lock, flags);
> -
> put_cpu_ptr(&aia_hgei);
> }
>
> diff --git a/arch/riscv/kvm/main.c b/arch/riscv/kvm/main.c
> index 350e4f097d6e..b48bbd1855c1 100644
> --- a/arch/riscv/kvm/main.c
> +++ b/arch/riscv/kvm/main.c
> @@ -10,6 +10,7 @@
> #include <linux/err.h>
> #include <linux/module.h>
> #include <linux/kvm_host.h>
> +#include <linux/cpu_pm.h>
> #include <asm/cpufeature.h>
> #include <asm/kvm_mmu.h>
> #include <asm/kvm_nacl.h>
> @@ -33,14 +34,9 @@ long kvm_arch_dev_ioctl(struct file *filp,
> return -EINVAL;
> }
>
> -int kvm_arch_enable_virtualization_cpu(void)
> +/* Initialize hypervisor CSRs - called during CPU online and non-retention idle resume */
> +static void kvm_riscv_csr_enable(void)
s/kvm_riscv_csr_enable/kvm_riscv_csr_init/
> {
> - int rc;
> -
> - rc = kvm_riscv_nacl_enable();
> - if (rc)
> - return rc;
> -
> csr_write(CSR_HEDELEG, 0);
> csr_write(CSR_HIDELEG, 0);
>
> @@ -48,16 +44,11 @@ int kvm_arch_enable_virtualization_cpu(void)
> csr_write(CSR_HCOUNTEREN, 0x02);
>
> csr_write(CSR_HVIP, 0);
> -
> - kvm_riscv_aia_enable();
> -
> - return 0;
> }
>
> -void kvm_arch_disable_virtualization_cpu(void)
> +/* Clear hypervisor CSRs - called during CPU offline and non-retention idle entry */
> +static void kvm_riscv_csr_disable(void)
s/kvm_riscv_csr_disable/kvm_riscv_csr_cleanup/
> {
> - kvm_riscv_aia_disable();
> -
> /*
> * After clearing the hideleg CSR, the host kernel will receive
> * spurious interrupts if hvip CSR has pending interrupts and the
> @@ -70,10 +61,52 @@ void kvm_arch_disable_virtualization_cpu(void)
> csr_write(CSR_HIDELEG, 0);
>
> kvm_riscv_clear_former_vcpu();
> +}
> +
> +int kvm_arch_enable_virtualization_cpu(void)
> +{
> + int rc;
> +
> + rc = kvm_riscv_nacl_enable();
> + if (rc)
> + return rc;
> +
> + kvm_riscv_csr_enable();
> + kvm_riscv_aia_enable(true);
>
> + return 0;
> +}
> +
> +void kvm_arch_disable_virtualization_cpu(void)
> +{
> + kvm_riscv_aia_disable(true);
> + kvm_riscv_csr_disable();
> kvm_riscv_nacl_disable();
> }
>
> +static int kvm_riscv_cpu_pm_notifier(struct notifier_block *self, unsigned long cmd, void *v)
> +{
> + switch (cmd) {
> + case CPU_PM_EXIT:
> + case CPU_PM_ENTER_FAILED:
> + kvm_riscv_csr_enable();
> + kvm_riscv_aia_enable(false);
> + return NOTIFY_OK;
> + case CPU_PM_ENTER:
> + kvm_riscv_aia_disable(false);
> + kvm_riscv_csr_disable();
> + return NOTIFY_OK;
> + default:
> + break;
> + }
> +
> + return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block kvm_riscv_cpu_pm_nb = {
> + .notifier_call = kvm_riscv_cpu_pm_notifier,
> +};
> +
> static void kvm_riscv_teardown(void)
> {
> kvm_riscv_aia_exit();
> @@ -174,22 +207,40 @@ static int __init riscv_kvm_init(void)
>
> kvm_register_perf_callbacks();
>
> - rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE);
> - if (rc) {
> - kvm_riscv_teardown();
> - return rc;
> + /* Register CPU PM notifier for CPU idle non-retention states */
> + if (IS_ENABLED(CONFIG_CPU_PM)) {
> + rc = cpu_pm_register_notifier(&kvm_riscv_cpu_pm_nb);
> + if (rc) {
> + kvm_err("Failed to register CPU PM notifier: %d\n", rc);
> + goto err_teardown;
> + }
> }
>
> + rc = kvm_init(sizeof(struct kvm_vcpu), 0, THIS_MODULE);
> + if (rc)
> + goto err_unregister_cpu_pm;
> +
> if (kvm_riscv_aia_available())
> kvm_info("AIA available with %d guest external interrupts\n",
> atomic_read(&kvm_riscv_aia_nr_hgei));
>
> return 0;
> +
> +err_unregister_cpu_pm:
> + if (IS_ENABLED(CONFIG_CPU_PM))
> + cpu_pm_unregister_notifier(&kvm_riscv_cpu_pm_nb);
> +err_teardown:
> + kvm_riscv_teardown();
> + return rc;
> }
> module_init(riscv_kvm_init);
>
> static void __exit riscv_kvm_exit(void)
> {
> + /* Unregister CPU PM notifier */
> + if (IS_ENABLED(CONFIG_CPU_PM))
> + cpu_pm_unregister_notifier(&kvm_riscv_cpu_pm_nb);
> +
> kvm_exit();
The sequence in riscv_kvm_exit() should be mirror image of
riscv_kvm_init() so cpu_pm_unregister_notifier() must be done
after kvm_exit().
>
> kvm_riscv_teardown();
>
> --
> 2.43.7
>
Regards,
Anup
More information about the kvm-riscv
mailing list