From: "Scott J. Goldman" <scottjgo@gmail.com>
To: qemu-devel@nongnu.org
Cc: qemu-arm@nongnu.org, "Peter Maydell" <peter.maydell@linaro.org>,
"Alexander Graf" <agraf@csgraf.de>,
"Phil Dennis-Jordan" <phil@philjordan.eu>,
"Roman Bolshakov" <rbolshakov@ddn.com>,
"Philippe Mathieu-Daudé" <philmd@linaro.org>,
"Scott J. Goldman" <scottjgo@gmail.com>
Subject: [PATCH v3] target/arm/hvf: Fix WFI halting to stop idle vCPU spinning
Date: Mon, 27 Apr 2026 12:55:16 -0700 [thread overview]
Message-ID: <20260427195516.46256-1-scottjgo@gmail.com> (raw)
In-Reply-To: <20260410055045.63001-1-scottjgo@gmail.com>
Commit b5f8f77271 ("accel/hvf: Implement WFI without using pselect()")
changed hvf_wfi() from blocking the vCPU thread with pselect() to
returning EXCP_HLT, intending QEMU's main event loop to handle the
idle wait. However, cpu->halted was never set, so cpu_thread_is_idle()
always returns false and the vCPU thread spins at 100% CPU per core
while the guest is idle.
Fix this by:
1. Setting cpu->halted = 1 in hvf_wfi() so the vCPU thread sleeps on
halt_cond in qemu_process_cpu_events().
2. Arming a per-vCPU QEMU_CLOCK_VIRTUAL timer to fire when the guest's
virtual timer (CNTV_CVAL_EL0) would expire. This is necessary
because HVF only delivers HV_EXIT_REASON_VTIMER_ACTIVATED during
hv_vcpu_run(), which is not called while the CPU is halted. The
timer callback mirrors the VTIMER_ACTIVATED handler: it raises the
vtimer IRQ through the GIC and marks vtimer_masked, causing the
interrupt delivery chain to wake the vCPU via qemu_cpu_kick().
3. Clearing cpu->halted in hvf_arch_vcpu_exec() when cpu_has_work()
indicates a pending interrupt, and cancelling the WFI timer.
4. Re-arming the WFI timer from hvf_vm_state_change() on the resume
transition for any halted vCPU, since the QEMUTimer is per-instance
state and is not migrated. After cpu_synchronize_all_states() the
migrated vtimer state is mirrored in env, so we can read CNTV_CTL
and CNTV_CVAL from there. If the vtimer has already expired by the
time the destination resumes, hvf_wfi_timer_cb() is invoked
directly so the halted vCPU is woken up.
Fixes: b5f8f77271 ("accel/hvf: Implement WFI without using pselect()")
Signed-off-by: Scott J. Goldman <scottjgo@gmail.com>
---
Changes since v2:
- Use QEMU_CLOCK_VIRTUAL instead of QEMU_CLOCK_HOST so the timer
pauses with the VM and a halted vCPU isn't woken (or its IRQ
raised) while the user has stopped the guest. (Peter)
- Convert vtimer ticks to nanoseconds with muldiv64() to avoid
intermediate overflow. (Peter)
- Re-arm the WFI timer from hvf_vm_state_change() on the resume
transition so a halted vCPU on the migration destination is
woken when its vtimer expires (the QEMUTimer is per-instance
state and isn't migrated). (Peter)
v2: https://lore.kernel.org/qemu-devel/20260410055045.63001-1-scottjgo@gmail.com/
v1: https://lore.kernel.org/qemu-devel/20260410044726.61853-1-scottjgo@gmail.com/
include/system/hvf_int.h | 1 +
target/arm/hvf/hvf.c | 124 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 124 insertions(+), 1 deletion(-)
diff --git a/include/system/hvf_int.h b/include/system/hvf_int.h
index 2621164cb2..58fb865eba 100644
--- a/include/system/hvf_int.h
+++ b/include/system/hvf_int.h
@@ -48,6 +48,7 @@ struct AccelCPUState {
hv_vcpu_exit_t *exit;
bool vtimer_masked;
bool guest_debug_enabled;
+ struct QEMUTimer *wfi_timer;
#endif
};
diff --git a/target/arm/hvf/hvf.c b/target/arm/hvf/hvf.c
index 678afe5c8e..a19d7a5e1f 100644
--- a/target/arm/hvf/hvf.c
+++ b/target/arm/hvf/hvf.c
@@ -28,6 +28,7 @@
#include "hw/core/boards.h"
#include "hw/core/irq.h"
#include "qemu/main-loop.h"
+#include "qemu/timer.h"
#include "system/cpus.h"
#include "arm-powerctl.h"
#include "target/arm/cpu.h"
@@ -301,6 +302,8 @@ void hvf_arm_init_debug(void)
#define TMR_CTL_IMASK (1 << 1)
#define TMR_CTL_ISTATUS (1 << 2)
+static void hvf_wfi_timer_cb(void *opaque);
+
static uint32_t chosen_ipa_bit_size;
typedef struct HVFVTimer {
@@ -1214,6 +1217,9 @@ void hvf_arch_vcpu_destroy(CPUState *cpu)
{
hv_return_t ret;
+ timer_free(cpu->accel->wfi_timer);
+ cpu->accel->wfi_timer = NULL;
+
ret = hv_vcpu_destroy(cpu->accel->fd);
assert_hvf_ok(ret);
}
@@ -1352,6 +1358,9 @@ int hvf_arch_init_vcpu(CPUState *cpu)
arm_cpu->isar.idregs[ID_AA64MMFR0_EL1_IDX]);
assert_hvf_ok(ret);
+ cpu->accel->wfi_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+ hvf_wfi_timer_cb, cpu);
+
aarch64_add_sme_properties(OBJECT(cpu));
return 0;
}
@@ -2027,8 +2036,67 @@ static uint64_t hvf_vtimer_val_raw(void)
return mach_absolute_time() - hvf_state->vtimer_offset;
}
+static void hvf_wfi_timer_cb(void *opaque)
+{
+ CPUState *cpu = opaque;
+ ARMCPU *arm_cpu = ARM_CPU(cpu);
+
+ /*
+ * vtimer expired while the CPU was halted for WFI.
+ * Mirror HV_EXIT_REASON_VTIMER_ACTIVATED: raise the vtimer
+ * interrupt and mark as masked so hvf_sync_vtimer() will
+ * check and unmask when the guest handles it.
+ *
+ * The interrupt delivery chain (GIC -> cpu_interrupt ->
+ * qemu_cpu_kick) wakes the vCPU thread from halt_cond.
+ */
+ qemu_set_irq(arm_cpu->gt_timer_outputs[GTIMER_VIRT], 1);
+ cpu->accel->vtimer_masked = true;
+}
+
+/*
+ * Arm a host-side QEMU_CLOCK_VIRTUAL timer to fire when the guest's
+ * vtimer (CNTV_CVAL_EL0) is scheduled to expire. HVF only delivers
+ * HV_EXIT_REASON_VTIMER_ACTIVATED during hv_vcpu_run(), which we won't
+ * call while the vCPU is halted, so we need this to wake the vCPU.
+ *
+ * QEMU_CLOCK_VIRTUAL pauses while the VM is stopped, which keeps the
+ * timer in lockstep with the guest's view of vtime across pause/resume.
+ *
+ * Caller must supply the current CNTV_CTL_EL0 and CNTV_CVAL_EL0 values,
+ * since the appropriate source (HVF vs. env) depends on context.
+ *
+ * Returns 0 if the timer was armed (or if the vtimer is disabled/masked
+ * and the vCPU should still halt waiting on another event), or -1 if
+ * the vtimer has already expired.
+ */
+static int hvf_arm_wfi_timer(CPUState *cpu, uint64_t ctl, uint64_t cval)
+{
+ ARMCPU *arm_cpu = ARM_CPU(cpu);
+ uint64_t now;
+ int64_t delta_ns;
+
+ if (!(ctl & TMR_CTL_ENABLE) || (ctl & TMR_CTL_IMASK)) {
+ return 0;
+ }
+
+ now = hvf_vtimer_val_raw();
+ if (cval <= now) {
+ return -1;
+ }
+
+ delta_ns = muldiv64(cval - now, NANOSECONDS_PER_SECOND,
+ arm_cpu->gt_cntfrq_hz);
+ timer_mod(cpu->accel->wfi_timer,
+ qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + delta_ns);
+ return 0;
+}
+
static int hvf_wfi(CPUState *cpu)
{
+ uint64_t ctl, cval;
+ hv_return_t r;
+
if (cpu_has_work(cpu)) {
/*
* Don't bother to go into our "low power state" if
@@ -2037,6 +2105,22 @@ static int hvf_wfi(CPUState *cpu)
return 0;
}
+ /*
+ * Read the vtimer state directly from HVF. We're on the vCPU thread,
+ * just exited from hv_vcpu_run(), so HVF holds the authoritative
+ * values and env may be stale.
+ */
+ r = hv_vcpu_get_sys_reg(cpu->accel->fd, HV_SYS_REG_CNTV_CTL_EL0, &ctl);
+ assert_hvf_ok(r);
+ r = hv_vcpu_get_sys_reg(cpu->accel->fd, HV_SYS_REG_CNTV_CVAL_EL0, &cval);
+ assert_hvf_ok(r);
+
+ if (hvf_arm_wfi_timer(cpu, ctl, cval) < 0) {
+ /* vtimer already expired, don't halt */
+ return 0;
+ }
+
+ cpu->halted = 1;
return EXCP_HLT;
}
@@ -2332,7 +2416,11 @@ int hvf_arch_vcpu_exec(CPUState *cpu)
hv_return_t r;
if (cpu->halted) {
- return EXCP_HLT;
+ if (!cpu_has_work(cpu)) {
+ return EXCP_HLT;
+ }
+ cpu->halted = 0;
+ timer_del(cpu->accel->wfi_timer);
}
flush_cpu_state(cpu);
@@ -2376,11 +2464,45 @@ static const VMStateDescription vmstate_hvf_vtimer = {
static void hvf_vm_state_change(void *opaque, bool running, RunState state)
{
HVFVTimer *s = opaque;
+ CPUState *cpu;
if (running) {
/* Update vtimer offset on all CPUs */
hvf_state->vtimer_offset = mach_absolute_time() - s->vtimer_val;
cpu_synchronize_all_states();
+
+ /*
+ * After migration restore (or any resume), the wfi_timer is not
+ * scheduled on this QEMU instance, so re-arm it for any halted
+ * vCPU with a pending vtimer. For a non-migration resume the
+ * QEMU_CLOCK_VIRTUAL timer was already scheduled; recomputing the
+ * deadline produces the same value and is a harmless no-op.
+ *
+ * cpu_synchronize_all_states() above ensures env mirrors the
+ * authoritative vtimer state (whether that came from HVF or from
+ * the migration stream), so we can safely read it here from the
+ * iothread.
+ */
+ CPU_FOREACH(cpu) {
+ ARMCPU *arm_cpu;
+ uint64_t ctl, cval;
+
+ if (!cpu->accel || !cpu->halted) {
+ continue;
+ }
+
+ arm_cpu = ARM_CPU(cpu);
+ ctl = arm_cpu->env.cp15.c14_timer[GTIMER_VIRT].ctl;
+ cval = arm_cpu->env.cp15.c14_timer[GTIMER_VIRT].cval;
+
+ if (hvf_arm_wfi_timer(cpu, ctl, cval) < 0) {
+ /*
+ * vtimer already expired while we were paused; raise the
+ * IRQ now so the halted vCPU wakes up.
+ */
+ hvf_wfi_timer_cb(cpu);
+ }
+ }
} else {
/* Remember vtimer value on every pause */
s->vtimer_val = hvf_vtimer_val_raw();
--
2.50.1 (Apple Git-155)
next prev parent reply other threads:[~2026-04-27 19:59 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-10 4:47 [PATCH] target/arm/hvf: Fix WFI halting to stop idle vCPU spinning Scott J. Goldman
2026-04-10 5:06 ` Mohamed Mediouni
2026-04-10 5:28 ` Scott J. Goldman
2026-04-10 5:50 ` [PATCH v2] " Scott J. Goldman
2026-04-10 6:18 ` Mohamed Mediouni
2026-04-16 21:20 ` Scott J. Goldman
2026-04-17 9:57 ` Philippe Mathieu-Daudé
2026-04-17 20:30 ` Scott J. Goldman
2026-04-21 23:24 ` Scott J. Goldman
2026-04-27 11:15 ` Peter Maydell
2026-04-27 18:42 ` Scott J. Goldman
2026-04-27 19:55 ` Scott J. Goldman [this message]
2026-05-12 16:46 ` [PATCH v3] " Scott J. Goldman
2026-05-12 19:18 ` Peter Maydell
2026-05-12 17:54 ` Mohamed Mediouni
2026-05-12 20:36 ` Scott J. Goldman
2026-05-12 20:51 ` Mohamed Mediouni
2026-05-13 1:21 ` Mohamed Mediouni
2026-05-13 2:21 ` [PATCH v11.1+ v4] " Scott J. Goldman
2026-05-15 10:39 ` Peter Maydell
2026-05-13 2:25 ` [PATCH v3] " Scott J. Goldman
2026-05-13 7:14 ` Mohamed Mediouni
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=20260427195516.46256-1-scottjgo@gmail.com \
--to=scottjgo@gmail.com \
--cc=agraf@csgraf.de \
--cc=peter.maydell@linaro.org \
--cc=phil@philjordan.eu \
--cc=philmd@linaro.org \
--cc=qemu-arm@nongnu.org \
--cc=qemu-devel@nongnu.org \
--cc=rbolshakov@ddn.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.