[PATCH v9 18/22] perf: arm_pmuv3: Handle IRQs for Partitioned PMU guest counters
From: Colton Lewis
Date: Thu Sep 24 2026 - 14:00:59 EST
Because ARM hardware is not yet capable of direct PPI injection into
guests, guest counters trigger physical PMU interrupts that must be
handled by the host PMU interrupt handler.
In armv8pmu_handle_irq(), clear the overflow flags in hardware for host
counters, restrict host perf event handling to bits in cpuc->cntr_mask,
and pass the overflow flags to kvm_pmu_handle_guest_irq(). The KVM hook
clears the guest counter overflow flags in hardware and records them in
the running vCPU's virtual PMOVSSET_EL0 register for subsequent guest
interrupt injection.
Additionally, provide kvm_pmu_host_start() and kvm_pmu_host_stop() so
that armv8pmu_start() and armv8pmu_stop() toggle MDCR_EL2.HPME rather
than PMCR_EL0.E when a partitioned guest's PMU state is loaded on the
CPU.
Signed-off-by: Colton Lewis <coltonlewis@xxxxxxxxxx>
---
arch/arm/include/asm/arm_pmuv3.h | 8 ++++
arch/arm64/include/asm/arm_pmuv3.h | 5 +++
arch/arm64/kvm/pmu-direct.c | 67 ++++++++++++++++++++++++++++++
drivers/perf/arm_pmuv3.c | 34 ++++++++-------
include/kvm/arm_pmu.h | 6 +++
5 files changed, 104 insertions(+), 16 deletions(-)
diff --git a/arch/arm/include/asm/arm_pmuv3.h b/arch/arm/include/asm/arm_pmuv3.h
index cec26c12bc009..9b8d6bbbe70cf 100644
--- a/arch/arm/include/asm/arm_pmuv3.h
+++ b/arch/arm/include/asm/arm_pmuv3.h
@@ -180,6 +180,11 @@ static inline void write_pmintenset(u32 val)
write_sysreg(val, PMINTENSET);
}
+static inline u32 read_pmintenset(void)
+{
+ return read_sysreg(PMINTENSET);
+}
+
static inline void write_pmintenclr(u32 val)
{
write_sysreg(val, PMINTENCLR);
@@ -235,6 +240,9 @@ static inline u64 kvm_pmu_host_counter_mask(void)
{
return ~0;
}
+static inline void kvm_pmu_handle_guest_irq(u64 pmovsr) {}
+static inline bool kvm_pmu_host_start(void) { return false; }
+static inline bool kvm_pmu_host_stop(void) { return false; }
/* PMU Version in DFR Register */
#define ARMV8_PMU_DFR_VER_NI 0
diff --git a/arch/arm64/include/asm/arm_pmuv3.h b/arch/arm64/include/asm/arm_pmuv3.h
index cf2b2212e00a2..9037a28365951 100644
--- a/arch/arm64/include/asm/arm_pmuv3.h
+++ b/arch/arm64/include/asm/arm_pmuv3.h
@@ -110,6 +110,11 @@ static inline void write_pmintenset(u64 val)
write_sysreg(val, pmintenset_el1);
}
+static inline u64 read_pmintenset(void)
+{
+ return read_sysreg(pmintenset_el1);
+}
+
static inline void write_pmintenclr(u64 val)
{
write_sysreg(val, pmintenclr_el1);
diff --git a/arch/arm64/kvm/pmu-direct.c b/arch/arm64/kvm/pmu-direct.c
index 1ada09d54f68f..95b5a64052762 100644
--- a/arch/arm64/kvm/pmu-direct.c
+++ b/arch/arm64/kvm/pmu-direct.c
@@ -512,3 +512,70 @@ void kvm_pmu_set_guest_owned(struct kvm_vcpu *vcpu)
kvm_pmu_load(vcpu);
}
}
+
+/**
+ * kvm_pmu_handle_guest_irq() - Record IRQs in guest counters
+ * @pmovsr: Overflow flags reported by driver
+ *
+ * Set overflow flags in guest-reserved counters in the VCPU register
+ * for the guest to clear later.
+ */
+void kvm_pmu_handle_guest_irq(u64 pmovsr)
+{
+ struct kvm_vcpu *vcpu = kvm_get_running_vcpu();
+ u64 mask = kvm_pmu_guest_counter_mask();
+ u64 govf = pmovsr & mask;
+ int i;
+
+ write_pmovsclr(govf);
+
+ if (!vcpu || !vcpu->arch.pmu.loaded_on_cpu)
+ return;
+
+ for_each_set_bit(i, (unsigned long *)&govf, 64)
+ set_bit(i, (unsigned long *)__ctxt_sys_reg(&vcpu->arch.ctxt, PMOVSSET_EL0));
+}
+
+/**
+ * kvm_pmu_host_start() - Enable host PMU counters while guest owns PMU
+ *
+ * When a partitioned guest owns the PMU, host counters (HPMN..N-1) are
+ * gated by MDCR_EL2.HPME rather than PMCR_EL0.E.
+ *
+ * Return: True if handled via MDCR_EL2.HPME, false if caller should use PMCR_EL0.E
+ */
+bool kvm_pmu_host_start(void)
+{
+ struct kvm_vcpu *vcpu = kvm_get_running_vcpu();
+
+ if (!vcpu || !kvm_pmu_is_partitioned(vcpu->kvm) ||
+ !vcpu->arch.pmu.loaded_on_cpu)
+ return false;
+
+ vcpu->arch.mdcr_el2 |= MDCR_EL2_HPME;
+ write_sysreg(vcpu->arch.mdcr_el2, mdcr_el2);
+ isb();
+ return true;
+}
+
+/**
+ * kvm_pmu_host_stop() - Disable host PMU counters while guest owns PMU
+ *
+ * When a partitioned guest owns the PMU, host counters (HPMN..N-1) are
+ * gated by MDCR_EL2.HPME rather than PMCR_EL0.E.
+ *
+ * Return: True if handled via MDCR_EL2.HPME, false if caller should use PMCR_EL0.E
+ */
+bool kvm_pmu_host_stop(void)
+{
+ struct kvm_vcpu *vcpu = kvm_get_running_vcpu();
+
+ if (!vcpu || !kvm_pmu_is_partitioned(vcpu->kvm) ||
+ !vcpu->arch.pmu.loaded_on_cpu)
+ return false;
+
+ vcpu->arch.mdcr_el2 &= ~MDCR_EL2_HPME;
+ write_sysreg(vcpu->arch.mdcr_el2, mdcr_el2);
+ isb();
+ return true;
+}
diff --git a/drivers/perf/arm_pmuv3.c b/drivers/perf/arm_pmuv3.c
index 4fcdae8021a56..3e5f8207a0fee 100644
--- a/drivers/perf/arm_pmuv3.c
+++ b/drivers/perf/arm_pmuv3.c
@@ -763,18 +763,9 @@ static void armv8pmu_disable_event_irq(struct perf_event *event)
armv8pmu_disable_intens(BIT(event->hw.idx));
}
-static u64 armv8pmu_getreset_flags(void)
+static u64 armv8pmu_getovf_flags(void)
{
- u64 value;
-
- /* Read */
- value = read_pmovsclr();
-
- /* Write to clear flags */
- value &= ARMV8_PMU_CNT_MASK_ALL;
- write_pmovsclr(value);
-
- return value;
+ return read_pmovsclr() & ARMV8_PMU_CNT_MASK_ALL;
}
static void update_pmuserenr(u64 val)
@@ -864,7 +855,8 @@ static void armv8pmu_start(struct arm_pmu *cpu_pmu)
brbe_enable(cpu_pmu);
/* Enable all counters */
- armv8pmu_pmcr_write(armv8pmu_pmcr_read() | ARMV8_PMU_PMCR_E);
+ if (!kvm_pmu_host_start())
+ armv8pmu_pmcr_write(armv8pmu_pmcr_read() | ARMV8_PMU_PMCR_E);
}
static void armv8pmu_stop(struct arm_pmu *cpu_pmu)
@@ -875,7 +867,8 @@ static void armv8pmu_stop(struct arm_pmu *cpu_pmu)
brbe_disable();
/* Disable all counters */
- armv8pmu_pmcr_write(armv8pmu_pmcr_read() & ~ARMV8_PMU_PMCR_E);
+ if (!kvm_pmu_host_stop())
+ armv8pmu_pmcr_write(armv8pmu_pmcr_read() & ~ARMV8_PMU_PMCR_E);
}
static void read_branch_records(struct pmu_hw_events *cpuc,
@@ -890,16 +883,16 @@ static void read_branch_records(struct pmu_hw_events *cpuc,
static irqreturn_t armv8pmu_handle_irq(struct arm_pmu *cpu_pmu)
{
- u64 pmovsr;
struct perf_sample_data data;
struct pmu_hw_events *cpuc = this_cpu_ptr(cpu_pmu->hw_events);
struct pt_regs *regs;
+ u64 pmovsr;
int idx;
/*
- * Get and reset the IRQ flags
+ * Get the IRQ flags
*/
- pmovsr = armv8pmu_getreset_flags();
+ pmovsr = armv8pmu_getovf_flags();
/*
* Did an overflow occur?
@@ -907,6 +900,12 @@ static irqreturn_t armv8pmu_handle_irq(struct arm_pmu *cpu_pmu)
if (!armv8pmu_has_overflowed(pmovsr))
return IRQ_NONE;
+ /*
+ * Guest flag reset is handled by the kvm hook at the bottom of
+ * this function.
+ */
+ write_pmovsclr(pmovsr & ~kvm_pmu_guest_counter_mask());
+
/*
* Handle the counter(s) overflow(s)
*/
@@ -948,6 +947,9 @@ static irqreturn_t armv8pmu_handle_irq(struct arm_pmu *cpu_pmu)
*/
perf_event_overflow(event, &data, regs);
}
+
+ kvm_pmu_handle_guest_irq(pmovsr);
+
armv8pmu_start(cpu_pmu);
return IRQ_HANDLED;
diff --git a/include/kvm/arm_pmu.h b/include/kvm/arm_pmu.h
index 0f2e39367d637..955f142a41b94 100644
--- a/include/kvm/arm_pmu.h
+++ b/include/kvm/arm_pmu.h
@@ -109,6 +109,9 @@ u64 kvm_pmu_guest_counter_mask(void);
void kvm_pmu_load(struct kvm_vcpu *vcpu);
void kvm_pmu_put(struct kvm_vcpu *vcpu);
void kvm_pmu_set_guest_owned(struct kvm_vcpu *vcpu);
+void kvm_pmu_handle_guest_irq(u64 pmovsr);
+bool kvm_pmu_host_start(void);
+bool kvm_pmu_host_stop(void);
void kvm_pmu_apply_single_event_filter(struct kvm_vcpu *vcpu, u8 idx);
#define kvm_pmu_get_access(vcpu) ((vcpu)->arch.pmu.access)
@@ -274,6 +277,9 @@ static inline u64 kvm_pmu_guest_counter_mask(void)
return 0;
}
+static inline void kvm_pmu_handle_guest_irq(u64 pmovsr) {}
+static inline bool kvm_pmu_host_start(void) { return false; }
+static inline bool kvm_pmu_host_stop(void) { return false; }
static inline void kvm_pmu_apply_single_event_filter(struct kvm_vcpu *vcpu, u8 idx) {}
static inline bool has_kvm_pmu_partition_support(void)
--
2.56.0.rc1.310.g51773c2048-goog