Files
linux-stable-mirror/drivers/clocksource/hyperv_timer.c
T
Michael Kelley be0cfab740 clocksource: hyper-v: Remove support for stimer interrupts in message mode
In Hyper-V versions prior to WS2016/Win10, Hyper-V synthetic timers
interrupt the guest by delivering a message that is initially handled
by the Linux VMBus driver. Starting with WS2016/Win10, Hyper-V can
deliver stimer interrupts directly to an assigned interrupt vector
without involving the VMBus driver. This is called "Direct Mode".

With the overall removal of Linux support for running on Hyper-V
hosts earlier than WS2016 and Windows 10, it's no longer necessary
to support the legacy message-based delivery. Remove that delivery
mechanism and always use Direct Mode. If for some reason, the
Hyper-V host does not enumerate Direct Mode, output an error
message but continue to run using the LAPIC timer instead of an
stimer.

With these changes, the VMBus driver no longer calls the stimer
interrupt service routine. This removal has a broader benefit in
unblocking the disentangling of VMBus code and stimer code, as
they should be independent of each other. The final disentangling
will come as a follow-on patch set.

Signed-off-by: Michael Kelley <mhklinux@outlook.com>
Signed-off-by: Wei Liu <wei.liu@kernel.org>
2026-08-24 00:36:14 +00:00

559 lines
15 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Clocksource driver for the synthetic counter and timers
* provided by the Hyper-V hypervisor to guest VMs, as described
* in the Hyper-V Top Level Functional Spec (TLFS). This driver
* is instruction set architecture independent.
*
* Copyright (C) 2019, Microsoft, Inc.
*
* Author: Michael Kelley <mikelley@microsoft.com>
*/
#include <linux/percpu.h>
#include <linux/cpumask.h>
#include <linux/clockchips.h>
#include <linux/clocksource.h>
#include <linux/sched_clock.h>
#include <linux/mm.h>
#include <linux/cpuhotplug.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/acpi.h>
#include <linux/hyperv.h>
#include <linux/export.h>
#include <clocksource/hyperv_timer.h>
#include <hyperv/hvhdk.h>
#include <asm/mshyperv.h>
static struct clock_event_device __percpu *hv_clock_event;
/* Note: offset can hold negative values after hibernation. */
static u64 hv_sched_clock_offset __read_mostly;
static int stimer0_irq = -1;
static __maybe_unused DEFINE_PER_CPU(long, stimer0_evt);
static void hv_stimer0_isr(void)
{
struct clock_event_device *ce;
ce = this_cpu_ptr(hv_clock_event);
ce->event_handler(ce);
}
/*
* stimer0 interrupt handler for architectures that support
* per-cpu interrupts
*/
static irqreturn_t __maybe_unused hv_stimer0_percpu_isr(int irq, void *dev_id)
{
hv_stimer0_isr();
return IRQ_HANDLED;
}
static int hv_ce_set_next_event(unsigned long delta,
struct clock_event_device *evt)
{
u64 current_tick;
current_tick = hv_read_reference_counter();
current_tick += delta;
hv_set_msr(HV_MSR_STIMER0_COUNT, current_tick);
return 0;
}
static int hv_ce_shutdown(struct clock_event_device *evt)
{
hv_set_msr(HV_MSR_STIMER0_COUNT, 0);
hv_set_msr(HV_MSR_STIMER0_CONFIG, 0);
if (stimer0_irq >= 0)
disable_percpu_irq(stimer0_irq);
return 0;
}
static int hv_ce_set_oneshot(struct clock_event_device *evt)
{
union hv_stimer_config timer_cfg;
timer_cfg.as_uint64 = 0;
timer_cfg.enable = 1;
timer_cfg.auto_enable = 1;
/*
* When it expires, the timer will directly interrupt
* on the specified hardware vector/IRQ.
*/
timer_cfg.direct_mode = 1;
timer_cfg.apic_vector = HYPERV_STIMER0_VECTOR;
if (stimer0_irq >= 0)
enable_percpu_irq(stimer0_irq, IRQ_TYPE_NONE);
hv_set_msr(HV_MSR_STIMER0_CONFIG, timer_cfg.as_uint64);
return 0;
}
/*
* hv_stimer_init - Per-cpu initialization of the clockevent
*/
static int hv_stimer_init(unsigned int cpu)
{
struct clock_event_device *ce;
if (!hv_clock_event)
return 0;
ce = per_cpu_ptr(hv_clock_event, cpu);
ce->name = "Hyper-V clockevent";
ce->features = CLOCK_EVT_FEAT_ONESHOT;
ce->cpumask = cpumask_of(cpu);
/*
* Lower the rating of the Hyper-V timer in a TDX VM without paravisor,
* so the local APIC timer (lapic_clockevent) is the default timer in
* such a VM. The Hyper-V timer is not preferred in such a VM because
* it depends on the slow VM Reference Counter MSR (the Hyper-V TSC
* page is not enbled in such a VM because the VM uses Invariant TSC
* as a better clocksource and it's challenging to mark the Hyper-V
* TSC page shared in very early boot).
*/
if (!ms_hyperv.paravisor_present && hv_isolation_type_tdx())
ce->rating = 90;
else
ce->rating = 1000;
ce->set_state_shutdown = hv_ce_shutdown;
ce->set_state_oneshot = hv_ce_set_oneshot;
ce->set_next_event = hv_ce_set_next_event;
clockevents_config_and_register(ce,
HV_CLOCK_HZ,
HV_MIN_DELTA_TICKS,
HV_MAX_MAX_DELTA_TICKS);
return 0;
}
/*
* hv_stimer_cleanup - Per-cpu cleanup of the clockevent
*/
int hv_stimer_cleanup(unsigned int cpu)
{
struct clock_event_device *ce;
if (!hv_clock_event)
return 0;
ce = per_cpu_ptr(hv_clock_event, cpu);
hv_ce_shutdown(ce);
return 0;
}
EXPORT_SYMBOL_GPL(hv_stimer_cleanup);
/*
* These placeholders are overridden by arch specific code on
* architectures that need special setup of the stimer0 IRQ because
* they don't support per-cpu IRQs (such as x86/x64).
*/
void __weak hv_setup_stimer0_handler(void (*handler)(void))
{
};
void __weak hv_remove_stimer0_handler(void)
{
};
#ifdef CONFIG_ACPI
/* Called only on architectures with per-cpu IRQs (i.e., not x86/x64) */
static int hv_setup_stimer0_irq(void)
{
int ret;
ret = acpi_register_gsi(NULL, HYPERV_STIMER0_VECTOR,
ACPI_EDGE_SENSITIVE, ACPI_ACTIVE_HIGH);
if (ret < 0) {
pr_err("Can't register Hyper-V stimer0 GSI. Error %d", ret);
return ret;
}
stimer0_irq = ret;
ret = request_percpu_irq(stimer0_irq, hv_stimer0_percpu_isr,
"Hyper-V stimer0", &stimer0_evt);
if (ret) {
pr_err("Can't request Hyper-V stimer0 IRQ %d. Error %d",
stimer0_irq, ret);
acpi_unregister_gsi(stimer0_irq);
stimer0_irq = -1;
}
return ret;
}
static void hv_remove_stimer0_irq(void)
{
if (stimer0_irq == -1) {
hv_remove_stimer0_handler();
} else {
free_percpu_irq(stimer0_irq, &stimer0_evt);
acpi_unregister_gsi(stimer0_irq);
stimer0_irq = -1;
}
}
#else
static int hv_setup_stimer0_irq(void)
{
return 0;
}
static void hv_remove_stimer0_irq(void)
{
}
#endif
/* hv_stimer_alloc - Global initialization of the clockevent and stimer0 */
int hv_stimer_alloc(bool have_percpu_irqs)
{
int ret;
/*
* Synthetic timers are always available except on old versions of
* Hyper-V on x86. In that case, return as error as Linux will use a
* clockevent based on emulated LAPIC timer hardware.
*/
if (!(ms_hyperv.features & HV_MSR_SYNTIMER_AVAILABLE) ||
!(ms_hyperv.misc_features & HV_STIMER_DIRECT_MODE_AVAILABLE))
return -EINVAL;
hv_clock_event = alloc_percpu(struct clock_event_device);
if (!hv_clock_event)
return -ENOMEM;
if (have_percpu_irqs) {
ret = hv_setup_stimer0_irq();
if (ret)
goto free_clock_event;
} else {
hv_setup_stimer0_handler(hv_stimer0_isr);
}
ret = cpuhp_setup_state(CPUHP_AP_HYPERV_TIMER_STARTING,
"clockevents/hyperv/stimer:starting",
hv_stimer_init, hv_stimer_cleanup);
if (ret < 0) {
hv_remove_stimer0_irq();
goto free_clock_event;
}
return ret;
free_clock_event:
free_percpu(hv_clock_event);
hv_clock_event = NULL;
return ret;
}
EXPORT_SYMBOL_GPL(hv_stimer_alloc);
/*
* Do a global cleanup of clockevents for the cases of kexec and
* vmbus exit
*/
void hv_stimer_global_cleanup(void)
{
if (!hv_clock_event)
return;
cpuhp_remove_state(CPUHP_AP_HYPERV_TIMER_STARTING);
hv_remove_stimer0_irq();
stimer0_irq = -1;
free_percpu(hv_clock_event);
hv_clock_event = NULL;
}
EXPORT_SYMBOL_GPL(hv_stimer_global_cleanup);
static __always_inline u64 read_hv_clock_msr(void)
{
/*
* Read the partition counter to get the current tick count. This count
* is set to 0 when the partition is created and is incremented in 100
* nanosecond units.
*
* Use hv_raw_get_msr() because this function is used from
* noinstr. Notable; while HV_MSR_TIME_REF_COUNT is a synthetic
* register it doesn't need the GHCB path.
*/
return hv_raw_get_msr(HV_MSR_TIME_REF_COUNT);
}
/*
* Code and definitions for the Hyper-V clocksources. Two
* clocksources are defined: one that reads the Hyper-V defined MSR, and
* the other that uses the TSC reference page feature as defined in the
* TLFS. The MSR version is for compatibility with old versions of
* Hyper-V and 32-bit x86. The TSC reference page version is preferred.
*/
static union {
struct ms_hyperv_tsc_page page;
u8 reserved[PAGE_SIZE];
} tsc_pg __bss_decrypted __aligned(PAGE_SIZE);
static struct ms_hyperv_tsc_page *tsc_page = &tsc_pg.page;
static unsigned long tsc_pfn;
unsigned long hv_get_tsc_pfn(void)
{
return tsc_pfn;
}
EXPORT_SYMBOL_GPL(hv_get_tsc_pfn);
struct ms_hyperv_tsc_page *hv_get_tsc_page(void)
{
return tsc_page;
}
EXPORT_SYMBOL_GPL(hv_get_tsc_page);
static __always_inline u64 read_hv_clock_tsc(void)
{
u64 cur_tsc, time;
/*
* The Hyper-V Top-Level Function Spec (TLFS), section Timers,
* subsection Refererence Counter, guarantees that the TSC and MSR
* times are in sync and monotonic. Therefore we can fall back
* to the MSR in case the TSC page indicates unavailability.
*/
if (!hv_read_tsc_page_tsc(tsc_page, &cur_tsc, &time))
time = read_hv_clock_msr();
return time;
}
static u64 notrace read_hv_clock_tsc_cs(struct clocksource *arg)
{
return read_hv_clock_tsc();
}
static u64 notrace read_hv_clock_tsc_cs_snapshot(struct clocksource *arg,
struct clocksource_hw_snapshot *chs)
{
u64 time;
if (hv_read_tsc_page_tsc(tsc_page, &chs->hw_cycles, &time)) {
chs->hw_csid = CSID_X86_TSC;
} else {
chs->hw_cycles = 0;
chs->hw_csid = CSID_GENERIC;
time = read_hv_clock_msr();
}
return time;
}
static u64 noinstr read_hv_sched_clock_tsc(void)
{
return (read_hv_clock_tsc() - hv_sched_clock_offset) *
(NSEC_PER_SEC / HV_CLOCK_HZ);
}
static void suspend_hv_clock_tsc(struct clocksource *arg)
{
union hv_reference_tsc_msr tsc_msr;
/* Disable the TSC page */
tsc_msr.as_uint64 = hv_get_msr(HV_MSR_REFERENCE_TSC);
tsc_msr.enable = 0;
hv_set_msr(HV_MSR_REFERENCE_TSC, tsc_msr.as_uint64);
}
static void resume_hv_clock_tsc(struct clocksource *arg)
{
union hv_reference_tsc_msr tsc_msr;
/* Re-enable the TSC page */
tsc_msr.as_uint64 = hv_get_msr(HV_MSR_REFERENCE_TSC);
tsc_msr.enable = 1;
tsc_msr.pfn = tsc_pfn;
hv_set_msr(HV_MSR_REFERENCE_TSC, tsc_msr.as_uint64);
}
/*
* Called during resume from hibernation, from overridden
* x86_platform.restore_sched_clock_state routine. This is to adjust offsets
* used to calculate time for hv tsc page based sched_clock, to account for
* time spent before hibernation.
*/
void hv_adj_sched_clock_offset(u64 offset)
{
hv_sched_clock_offset -= offset;
}
#ifdef HAVE_VDSO_CLOCKMODE_HVCLOCK
static int hv_cs_enable(struct clocksource *cs)
{
vclocks_set_used(VDSO_CLOCKMODE_HVCLOCK);
return 0;
}
#endif
static struct clocksource hyperv_cs_tsc = {
.name = "hyperv_clocksource_tsc_page",
.rating = 500,
.read = read_hv_clock_tsc_cs,
.read_snapshot = read_hv_clock_tsc_cs_snapshot,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
.suspend = suspend_hv_clock_tsc,
.resume = resume_hv_clock_tsc,
#ifdef HAVE_VDSO_CLOCKMODE_HVCLOCK
.enable = hv_cs_enable,
.vdso_clock_mode = VDSO_CLOCKMODE_HVCLOCK,
#else
.vdso_clock_mode = VDSO_CLOCKMODE_NONE,
#endif
};
static u64 notrace read_hv_clock_msr_cs(struct clocksource *arg)
{
return read_hv_clock_msr();
}
static struct clocksource hyperv_cs_msr = {
.name = "hyperv_clocksource_msr",
.rating = 495,
.read = read_hv_clock_msr_cs,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
/*
* Reference to pv_ops must be inline so objtool
* detection of noinstr violations can work correctly.
*/
#ifdef CONFIG_GENERIC_SCHED_CLOCK
static __always_inline void hv_setup_sched_clock(void *sched_clock)
{
/*
* We're on an architecture with generic sched clock (not x86/x64).
* The Hyper-V sched clock read function returns nanoseconds, not
* the normal 100ns units of the Hyper-V synthetic clock.
*/
sched_clock_register(sched_clock, 64, NSEC_PER_SEC);
}
#elif defined CONFIG_PARAVIRT
#include <asm/timer.h>
static __always_inline void hv_setup_sched_clock(void *sched_clock)
{
/* We're on x86/x64 *and* using PV ops */
paravirt_set_sched_clock(sched_clock);
}
#else /* !CONFIG_GENERIC_SCHED_CLOCK && !CONFIG_PARAVIRT */
static __always_inline void hv_setup_sched_clock(void *sched_clock) {}
#endif /* CONFIG_GENERIC_SCHED_CLOCK */
static void __init hv_init_tsc_clocksource(void)
{
union hv_reference_tsc_msr tsc_msr;
/*
* When running as a guest partition:
*
* If Hyper-V offers TSC_INVARIANT, then the virtualized TSC correctly
* handles frequency and offset changes due to live migration,
* pause/resume, and other VM management operations. So lower the
* Hyper-V Reference TSC rating, causing the generic TSC to be used.
* TSC_INVARIANT is not offered on ARM64, so the Hyper-V Reference
* TSC will be preferred over the virtualized ARM64 arch counter.
*
* When running as the root partition:
*
* There is no HV_ACCESS_TSC_INVARIANT feature. Always lower the rating
* of the Hyper-V Reference TSC.
*/
if ((ms_hyperv.features & HV_ACCESS_TSC_INVARIANT) ||
hv_root_partition()) {
hyperv_cs_tsc.rating = 250;
hyperv_cs_msr.rating = 245;
}
if (!(ms_hyperv.features & HV_MSR_REFERENCE_TSC_AVAILABLE))
return;
hv_read_reference_counter = read_hv_clock_tsc;
/*
* TSC page mapping works differently in root compared to guest.
* - In guest partition the guest PFN has to be passed to the
* hypervisor.
* - In root partition it's other way around: it has to map the PFN
* provided by the hypervisor.
* But it can't be mapped right here as it's too early and MMU isn't
* ready yet. So, we only set the enable bit here and will remap the
* page later in hv_remap_tsc_clocksource().
*
* It worth mentioning, that TSC clocksource read function
* (read_hv_clock_tsc) has a MSR-based fallback mechanism, used when
* TSC page is zeroed (which is the case until the PFN is remapped) and
* thus TSC clocksource will work even without the real TSC page
* mapped.
*/
tsc_msr.as_uint64 = hv_get_msr(HV_MSR_REFERENCE_TSC);
if (hv_root_partition())
tsc_pfn = tsc_msr.pfn;
else
tsc_pfn = HVPFN_DOWN(virt_to_phys(tsc_page));
tsc_msr.enable = 1;
tsc_msr.pfn = tsc_pfn;
hv_set_msr(HV_MSR_REFERENCE_TSC, tsc_msr.as_uint64);
clocksource_register_hz(&hyperv_cs_tsc, NSEC_PER_SEC/100);
/*
* If TSC is invariant, then let it stay as the sched clock since it
* will be faster than reading the TSC page. But if not invariant, use
* the TSC page so that live migrations across hosts with different
* frequencies is handled correctly.
*/
if (!(ms_hyperv.features & HV_ACCESS_TSC_INVARIANT)) {
hv_sched_clock_offset = hv_read_reference_counter();
hv_setup_sched_clock(read_hv_sched_clock_tsc);
}
}
void __init hv_init_clocksource(void)
{
/*
* Try to set up the TSC page clocksource, then the MSR clocksource.
* At least one of these will always be available except on very old
* versions of Hyper-V on x86. In that case we won't have a Hyper-V
* clocksource, but Linux will still run with a clocksource based
* on the emulated PIT or LAPIC timer.
*
* Never use the MSR clocksource as sched clock. It's too slow.
* Better to use the native sched clock as the fallback.
*/
hv_init_tsc_clocksource();
if (ms_hyperv.features & HV_MSR_TIME_REF_COUNT_AVAILABLE)
clocksource_register_hz(&hyperv_cs_msr, NSEC_PER_SEC/100);
}
void __init hv_remap_tsc_clocksource(void)
{
if (!(ms_hyperv.features & HV_MSR_REFERENCE_TSC_AVAILABLE))
return;
if (!hv_root_partition()) {
WARN(1, "%s: attempt to remap TSC page in guest partition\n",
__func__);
return;
}
tsc_page = memremap(tsc_pfn << HV_HYP_PAGE_SHIFT, sizeof(tsc_pg),
MEMREMAP_WB);
if (!tsc_page)
pr_err("Failed to remap Hyper-V TSC page.\n");
}