bootloader: Implement TSC calibration via hypervisor CPUID leaf.

While debugging some problems on the HaikuPorts build VMs, mmlr
noticed their clocks had an alarming amount of drift. This prompted
an investigation into TSC calibration mechanisms, and the discovery
that there is a VM-specific one which we did not implement.

This mechanism is more accurate than counting cycles on VMs where
cycles can be "stolen" (the probable cause of the aforementioned
clock drift.)

Tested in VMware (works out of the box) and on QEMU/KVM
(may need TSC frequency specified or a host with invariant TSC.)

Change-Id: I4ccfdb2e4e2621404ec9026e7106c02bf96faf18
Reviewed-on: https://review.haiku-os.org/c/haiku/+/7063
Reviewed-by: waddlesplash <waddlesplash@gmail.com>
This commit is contained in:
Augustin Cavalier 2023-10-24 17:54:55 -04:00 committed by waddlesplash
parent b8fbb723ea
commit bb2808d615
6 changed files with 55 additions and 23 deletions

View File

@ -17,7 +17,7 @@ extern "C" {
#endif
void calculate_cpu_conversion_factor(uint8 channel);
void determine_cpu_conversion_factor(uint8 channel);
status_t boot_arch_cpu_init(void);
void arch_ucode_load(BootVolume& volume);

View File

@ -23,19 +23,6 @@
#include <string.h>
#if __GNUC__ > 2
#include <x86intrin.h>
#else
static inline uint64_t __rdtsc()
{
uint64 tsc;
asm volatile ("rdtsc\n" : "=A"(tsc));
return tsc;
}
#endif
uint32 gTimeConversionFactor;
@ -184,6 +171,22 @@ private:
#endif
static inline uint64_t
rdtsc_fenced()
{
uint64 tsc;
// RDTSC is not serializing, nor does it drain the instruction stream.
// RDTSCP does, but is not available everywhere. Other OSes seem to use
// "CPUID" rather than MFENCE/LFENCE for serializing here during boot.
asm volatile ("cpuid" : : : "eax", "ebx", "ecx", "edx");
asm volatile ("rdtsc" : "=A"(tsc));
return tsc;
}
static inline void
calibration_loop(uint8 desiredHighByte, uint8 channel, uint64& tscDelta,
double& conversionFactor, uint16& expired)
@ -214,7 +217,7 @@ calibration_loop(uint8 desiredHighByte, uint8 channel, uint64& tscDelta,
} while (startHigh != 255);
// Read in the first TSC value
uint64 startTSC = __rdtsc();
uint64 startTSC = rdtsc_fenced();
// Wait for the PIT to count down to our desired value
uint8 endLow;
@ -226,7 +229,7 @@ calibration_loop(uint8 desiredHighByte, uint8 channel, uint64& tscDelta,
} while (endHigh > desiredHighByte);
// And read the second TSC value
uint64 endTSC = __rdtsc();
uint64 endTSC = rdtsc_fenced();
tscDelta = endTSC - startTSC;
expired = ((startHigh << 8) | startLow) - ((endHigh << 8) | endLow);
@ -234,7 +237,7 @@ calibration_loop(uint8 desiredHighByte, uint8 channel, uint64& tscDelta,
}
void
static void
calculate_cpu_conversion_factor(uint8 channel)
{
// When using channel 2, enable the input and disable the speaker.
@ -317,6 +320,33 @@ slower_sample:
}
void
determine_cpu_conversion_factor(uint8 channel)
{
// Before using the calibration loop, check if we are on a hypervisor.
cpuid_info info;
if (get_current_cpuid(&info, 1, 0) == B_OK
&& (info.regs.ecx & IA32_FEATURE_EXT_HYPERVISOR) != 0) {
get_current_cpuid(&info, 0x40000000, 0);
const uint32 maxVMM = info.regs.eax;
if (maxVMM >= 0x40000010) {
get_current_cpuid(&info, 0x40000010, 0);
uint64 clockSpeed = uint64(info.regs.eax) * 1000;
gTimeConversionFactor = (uint64(1000) << 32) / info.regs.eax;
gKernelArgs.arch_args.system_time_cv_factor = gTimeConversionFactor;
gKernelArgs.arch_args.cpu_clock_speed = clockSpeed;
dprintf("TSC frequency read from hypervisor CPUID leaf\n");
return;
}
}
calculate_cpu_conversion_factor(channel);
}
void
ucode_load(BootVolume& volume)
{
@ -386,7 +416,7 @@ ucode_load(BootVolume& volume)
extern "C" bigtime_t
system_time()
{
uint64 tsc = __rdtsc();
uint64 tsc = rdtsc_fenced();
uint64 lo = (uint32)tsc;
uint64 hi = tsc >> 32;
return ((lo * gTimeConversionFactor) >> 32) + hi * gTimeConversionFactor;

View File

@ -65,7 +65,7 @@ cpu_init()
if (check_cpu_features() != B_OK)
panic("You need a Pentium or higher in order to boot!\n");
calculate_cpu_conversion_factor(0);
determine_cpu_conversion_factor(0);
gKernelArgs.num_cpus = 1;
// this will eventually be corrected later on

View File

@ -21,7 +21,7 @@ arch_timer_init(void)
{
// use PIT channel 2 for the calibration loop
// as channel 0 is reserved for the UEFI firmware
calculate_cpu_conversion_factor(2);
determine_cpu_conversion_factor(2);
hpet_init();
}

View File

@ -19,6 +19,6 @@
void
arch_timer_init(void)
{
calculate_cpu_conversion_factor(2);
determine_cpu_conversion_factor(2);
hpet_init();
}

View File

@ -1648,8 +1648,10 @@ init_tsc(kernel_args* args)
// try to find the TSC frequency with CPUID
uint32 conversionFactor = args->arch_args.system_time_cv_factor;
init_tsc_with_cpuid(args, &conversionFactor);
init_tsc_with_msr(args, &conversionFactor);
if (!x86_check_feature(IA32_FEATURE_EXT_HYPERVISOR, FEATURE_EXT)) {
init_tsc_with_cpuid(args, &conversionFactor);
init_tsc_with_msr(args, &conversionFactor);
}
uint64 conversionFactorNsecs = (uint64)conversionFactor * 1000;