From: VMware, Inc <> Date: Mon, 26 Jul 2010 18:14:55 +0000 (-0700) Subject: Implement NTP style PLL time slewing X-Git-Tag: 2010.07.25-280253~97 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c708f55b8863d007ab162f498a1e97378b67ea8e;p=thirdparty%2Fopen-vm-tools.git Implement NTP style PLL time slewing This change extends tools time sync to use the core ideas (and system call) from NTP's time synchronization algorithm. I tested this using ATTS for these guests: SLES11-64, RHEL5.4-64, RHEL4.8-32, WinXP-32, Win7-64. All of the results describe are ignoring time spikes caused by apparent time falling behind and catching up (which are not expected to be corrected by tools timesync). * Win7-64 hit a bug in the testing framework. * WinXP-32 looked good (but unchaged from before). * RHEL4.8-32 looks good (I'm using a 1000Hz kernel using the TSC, so the lost tick compensation bug hits very severely and thus provides a good test case for timesync). * SLES11-64: Stayed within 1.5ms (no data for before). * RHEL5.4-64: Stayed within 300us (before change it was 25ms). Signed-off-by: Marcelo Vanzin --- diff --git a/open-vm-tools/services/plugins/timeSync/Makefile.am b/open-vm-tools/services/plugins/timeSync/Makefile.am index b80c8feb3..7d7ce6522 100644 --- a/open-vm-tools/services/plugins/timeSync/Makefile.am +++ b/open-vm-tools/services/plugins/timeSync/Makefile.am @@ -33,11 +33,14 @@ libtimeSync_la_SOURCES += timeSyncPosix.c if SOLARIS libtimeSync_la_SOURCES += slewAdjtime.c +libtimeSync_la_SOURCES += pllNone.c endif if FREEBSD libtimeSync_la_SOURCES += slewAdjtime.c +libtimeSync_la_SOURCES += pllNone.c endif if LINUX libtimeSync_la_SOURCES += slewLinux.c +libtimeSync_la_SOURCES += pllLinux.c endif diff --git a/open-vm-tools/services/plugins/timeSync/pllLinux.c b/open-vm-tools/services/plugins/timeSync/pllLinux.c new file mode 100644 index 000000000..feb3ed734 --- /dev/null +++ b/open-vm-tools/services/plugins/timeSync/pllLinux.c @@ -0,0 +1,188 @@ +/********************************************************* + * Copyright (C) 2010 VMware, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation version 2.1 and no later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Lesser GNU General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + *********************************************************/ + +/** + * @file pllLinux.c + * + * Implementation of the NTP PLL using Linux's adjtimex system call. + */ + +#include "timeSync.h" +#include "timeSyncPosix.h" + +#include +#include +#include +#include +#include +#include +#include +#include "vm_assert.h" + + +static void +TimeSyncLogPLLState(const char *prefix, struct timex *tx) +{ + g_debug("%s : off %ld freq %ld maxerr %ld esterr %ld status %d " + "const %ld precision %ld tolerance %ld tick %ld\n", + prefix, tx->offset, tx->freq, tx->maxerror, tx->esterror, + tx->status, tx->constant, tx->precision, tx->tolerance, tx->tick); +} + +/* + ****************************************************************************** + * TimeSync_PLLSupported -- */ /** + * + * Report whether the platform supports an NTP style Type-II Phase Locked + * Loop for correcting the time. + * + * @return TRUE iff NTP Phase Locked Loop is supported. + * + ****************************************************************************** + */ + +Bool +TimeSync_PLLSupported(void) +{ + return TRUE; +} + + +/* + ****************************************************************************** + * TimeSync_PLLSetFrequency -- */ /** + * + * Set the frequency of the PLL. + * + * @param[in] ppmCorrection The parts per million error that should be + * corrected. This value is the ppm shifted + * left by 16 to match NTP. + * + * @return TRUE on success. + * + ****************************************************************************** + */ + +Bool +TimeSync_PLLSetFrequency(int64 ppmCorrection) +{ + struct timex tx; + int error; + + tx.modes = ADJ_FREQUENCY; + tx.freq = ppmCorrection; + + error = adjtimex(&tx); + if (error == -1) { + g_debug("%s: adjtimex failed: %d %s\n", __FUNCTION__, + error, strerror(errno)); + return FALSE; + } + TimeSyncLogPLLState(__FUNCTION__, &tx); + + return TRUE; +} + + +/* + ****************************************************************************** + * TimeSync_PLLUpdate -- */ /** + * + * Updates the PLL with a new offset. + * + * @param[in] offset The offset between the host and the guest. + * + * @return TRUE on success. + * + ****************************************************************************** + */ + +Bool +TimeSync_PLLUpdate(int64 offset) +{ + struct timex tx; + int error; + + if (offset < -500000) { + offset = -500000; + g_debug("%s: clamped offset at -500000\n", __FUNCTION__); + } + if (offset > 500000) { + offset = 500000; + g_debug("%s: clamped offset at 500000\n", __FUNCTION__); + } + + + tx.modes = ADJ_OFFSET | ADJ_MAXERROR | ADJ_ESTERROR; + tx.offset = offset; + tx.esterror = 0; + tx.maxerror = 0; + + error = adjtimex(&tx); + if (error == -1) { + g_debug("%s: adjtimex set offset failed: %d %s\n", __FUNCTION__, + error, strerror(errno)); + return FALSE; + } + TimeSyncLogPLLState(__FUNCTION__, &tx); + + /* Ensure that the kernel discipline is in the right mode. STA_PLLs + * should be set and STA_UNSYNC should not be set. + * + * The time constant is a bit trickier. In "Computer Network Time + * Synchronization" the terms used are "time constant" and "poll + * exponent" where time constant = 2 ^ poll exponent. Valid values for + * the poll exponent are 4 through 17, corresponding to a range of 16s + * to 131072s (36 hours). On linux things are a bit different though: + * tx.constant appears to be the poll exponent and when trying to set + * the poll exponent, tx.constant should be set to poll exponent - 4. + * + * We want to set the time constant to as low a value as possible. The + * core NTP PLL that the kernel discipline implements is built assuming + * that there is a clock filter with a variable delay of up to 8. + * Since TimeSyncReadHostAndGuest retries if the error is large, we + * don't need to implement the clock filter. Hence we want a time + * constant of 60/8 = 7, but settle for the lowest available: 16. This + * allows us to react to changes relatively fast. + */ + if (tx.constant != 4) { + tx.modes = ADJ_TIMECONST; + tx.constant = 0; + error = adjtimex(&tx); + if (error == -1) { + g_debug("%s: adjtimex set time constant failed: %d %s\n", __FUNCTION__, + error, strerror(errno)); + return FALSE; + } + g_debug("Set PLL time constant\n"); + TimeSyncLogPLLState(__FUNCTION__, &tx); + } + if ((tx.status & STA_PLL) != STA_PLL || (tx.status & STA_UNSYNC) != 0) { + tx.modes = ADJ_STATUS; + tx.status = STA_PLL; + error = adjtimex(&tx); + if (error == -1) { + g_debug("%s: adjtimex set status failed: %d %s\n", __FUNCTION__, + error, strerror(errno)); + return FALSE; + } + g_debug("Set PLL status\n"); + TimeSyncLogPLLState(__FUNCTION__, &tx); + } + return TRUE; +} diff --git a/open-vm-tools/services/plugins/timeSync/pllNone.c b/open-vm-tools/services/plugins/timeSync/pllNone.c new file mode 100644 index 000000000..00bc8e77a --- /dev/null +++ b/open-vm-tools/services/plugins/timeSync/pllNone.c @@ -0,0 +1,90 @@ +/********************************************************* + * Copyright (C) 2010 VMware, Inc. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation version 2.1 and no later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Lesser GNU General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + *********************************************************/ + +/** + * @file pllNone.c + * + * Null implementation of the NTP PLL. + */ + +#include "timeSync.h" + +#include "vm_assert.h" + + +/* + ****************************************************************************** + * TimeSync_PLLSupported -- */ /** + * + * Report whether the platform supports an NTP style Type-II Phase Locked + * Loop for correcting the time. + * + * @return TRUE iff NTP Phase Locked Loop is supported. + * + ****************************************************************************** + */ + +Bool +TimeSync_PLLSupported(void) +{ + return FALSE; +} + + +/* + ****************************************************************************** + * TimeSync_PLLSetFrequency -- */ /** + * + * Set the frequency of the PLL. + * + * @param[in] ppmCorrection The parts per million error that should be + * corrected. This value is the ppm shifted + * left by 16 to match NTP. + * + * @return FALSE + * + ****************************************************************************** + */ + +Bool +TimeSync_PLLSetFrequency(int64 ppmCorrection) +{ + NOT_IMPLEMENTED(); + return FALSE; +} + + +/* + ****************************************************************************** + * TimeSync_PLLUpdate -- */ /** + * + * Updates the PLL with a new offset. + * + * @param[in] offset The offset between the host and the guest. + * + * @return FALSE + * + ****************************************************************************** + */ + +Bool +TimeSync_PLLUpdate(int64 offset) +{ + NOT_IMPLEMENTED(); + return FALSE; +} diff --git a/open-vm-tools/services/plugins/timeSync/slewAdjtime.c b/open-vm-tools/services/plugins/timeSync/slewAdjtime.c index 2a0f9018c..8f6534662 100644 --- a/open-vm-tools/services/plugins/timeSync/slewAdjtime.c +++ b/open-vm-tools/services/plugins/timeSync/slewAdjtime.c @@ -63,13 +63,15 @@ TimeSync_DisableTimeSlew(void) /* ****************************************************************************** - * TimeSync_EnableTimeSlew -- */ /** + * TimeSync_Slew -- */ /** * * Slew the clock, correcting 'delta' microseconds. timeSyncPeriod is - * ignored by this implementation. + * ignored by this implementation. Report the amount of the previous + * correction that has not been applied. * - * @param[in] delta Time difference in us. - * @param[in] timeSyncPeriod Time interval in us. + * @param[in] delta Correction to apply in us. + * @param[in] timeSyncPeriod Time interval in us. + * @param[out] remaining Amount of previous correction not applied. * * @return TRUE on success. * @@ -77,8 +79,9 @@ TimeSync_DisableTimeSlew(void) */ Bool -TimeSync_EnableTimeSlew(int64 delta, - int64 timeSyncPeriod) +TimeSync_Slew(int64 delta, + int64 timeSyncPeriod, + int64 *remaining) { struct timeval tx; struct timeval oldTx; @@ -92,5 +95,8 @@ TimeSync_EnableTimeSlew(int64 delta, return FALSE; } g_debug("time slew start.\n"); + + *remaining = (int64)oldTx.tv_sec * US_PER_SEC + (int64)oldTx.tv_usec; + return TRUE; } diff --git a/open-vm-tools/services/plugins/timeSync/slewLinux.c b/open-vm-tools/services/plugins/timeSync/slewLinux.c index 3b78b07a3..0cb3f44d9 100644 --- a/open-vm-tools/services/plugins/timeSync/slewLinux.c +++ b/open-vm-tools/services/plugins/timeSync/slewLinux.c @@ -82,17 +82,22 @@ TimeSync_DisableTimeSlew(void) /* ****************************************************************************** - * TimeSync_EnableTimeSlew -- */ /** + * TimeSync_Slew -- */ /** * * Slew the clock so that the time difference is covered within the - * timeSyncPeriod. timeSyncPeriod is the interval of the time sync loop and we - * intend to catch up delta us. + * timeSyncPeriod. timeSyncPeriod is the interval of the time sync loop + * and we intend to catch up delta us. Report the amount of the previous + * correction that has not been applied (this may be negative if more than + * timeSyncPeriod elapsed since the last call). * * This changes the tick frequency and hence needs to be reset after the time * sync is achieved. * - * @param[in] delta Time difference in us. - * @param[in] timeSyncPeriod Time interval in us. + * All times are in microseconds. + * + * @param[in] delta Correction to apply. + * @param[in] timeSyncPeriod Time interval. + * @param[out] remaining Amount of previous correction not applied. * * @return TRUE on success. * @@ -100,33 +105,54 @@ TimeSync_DisableTimeSlew(void) */ Bool -TimeSync_EnableTimeSlew(int64 delta, - int64 timeSyncPeriod) +TimeSync_Slew(int64 delta, + int64 timeSyncPeriod, + int64 *remaining) { + static int64 startTime = 0; + static int64 tickLength; + static int64 deltaRequested; + struct timex tx; int error; - int64 tick; + int64 now; ASSERT(timeSyncPeriod > 0); + if (!TimeSync_GetCurrentTime(&now)) { + return FALSE; + } + + if (startTime != 0) { + int64 ticksElapsed = (now - startTime) / tickLength; + int64 deltaApplied = ticksElapsed * (tickLength - TICK_INCR_NOMINAL); + *remaining = deltaRequested - deltaApplied; + } + /* - * Set the tick so that delta time is corrected in timeSyncPeriod period. - * tick is the number of microseconds added per clock tick. We adjust this - * so that we get the desired delta + the timeSyncPeriod in timeSyncPeriod - * interval. + * Set the tick length so that delta time is corrected in + * timeSyncPeriod period. tick is the number of microseconds added per + * clock tick. We adjust this so that we get the desired delta + the + * timeSyncPeriod in timeSyncPeriod interval. */ - tx.modes = ADJ_TICK; - tick = (timeSyncPeriod + delta) / - ((timeSyncPeriod / US_PER_SEC) * USER_HZ); - if (tick > TICK_INCR_MAX) { - tick = TICK_INCR_MAX; - } else if (tick < TICK_INCR_MIN) { - tick = TICK_INCR_MIN; + tickLength = + (timeSyncPeriod + delta) / ((timeSyncPeriod / US_PER_SEC) * USER_HZ); + if (tickLength > TICK_INCR_MAX) { + tickLength = TICK_INCR_MAX; + } else if (tickLength < TICK_INCR_MIN) { + tickLength = TICK_INCR_MIN; } - tx.tick = tick; + tx.tick = tickLength; + tx.modes = ADJ_TICK; + + ASSERT(delta != 0 || tickLength == TICK_INCR_NOMINAL); + + startTime = now; + deltaRequested = delta; error = adjtimex(&tx); if (error == -1) { + startTime = 0; g_debug("adjtimex failed: %s\n", strerror(errno)); return FALSE; } diff --git a/open-vm-tools/services/plugins/timeSync/timeSync.c b/open-vm-tools/services/plugins/timeSync/timeSync.c index cd4fea5bc..f2dffccce 100644 --- a/open-vm-tools/services/plugins/timeSync/timeSync.c +++ b/open-vm-tools/services/plugins/timeSync/timeSync.c @@ -115,23 +115,40 @@ VM_EMBED_VERSION(VMTOOLSD_VERSION_STRING); #define TIMESYNC_MAX_SAMPLES 4 #define TIMESYNC_GOOD_SAMPLE_THRESHOLD 2000 +/* Once the error drops below TIMESYNC_PLL_ACTIVATE, activate the PLL. + * 500ppm error acumulated over a 60 second interval can produce 30ms of + * error. */ +#define TIMESYNC_PLL_ACTIVATE (30 * 1000) /* 30ms. */ +/* If the error goes above TIMESYNC_PLL_UNSYNC, deactivate the PLL. */ +#define TIMESYNC_PLL_UNSYNC (2 * TIMESYNC_PLL_ACTIVATE) +/* Period during which the frequency error of guest time is measured. */ +#define TIMESYNC_CALIBRATION_DURATION (15 * 60 * US_PER_SEC) /* 15min. */ + typedef enum TimeSyncState { TIMESYNC_INITIALIZING, TIMESYNC_STOPPED, TIMESYNC_RUNNING, } TimeSyncState; +typedef enum TimeSyncSlewState { + TimeSyncUncalibrated, + TimeSyncCalibrating, + TimeSyncPLL, +} TimeSyncSlewState; + typedef struct TimeSyncData { - gboolean slewActive; - gboolean slewCorrection; - uint32 slewPercentCorrection; - uint32 timeSyncPeriod; /* In seconds. */ - TimeSyncState state; - GSource *timer; + gboolean slewActive; + gboolean slewCorrection; + uint32 slewPercentCorrection; + uint32 timeSyncPeriod; /* In seconds. */ + TimeSyncState state; + TimeSyncSlewState slewState; + GSource *timer; } TimeSyncData; static void TimeSyncSetSlewState(TimeSyncData *data, gboolean active); +static void TimeSyncResetSlew(TimeSyncData *data); /** * Read the time reported by the Host OS. @@ -371,8 +388,18 @@ TimeSyncStepTime(TimeSyncData *data, int64 adjustment) /** - * Slew the guest OS time advancement to correct the time. Only correct a - * portion of the error to avoid overcorrection. + * Slew the guest OS time advancement to correct the time. + * + * In addition to standard slewing (implemented via TimeSync_Slew), we + * also support using an NTP style PLL to slew the time. The PLL can take + * a while to end up with an accurate measurement of the frequency error, + * so before entering PLL mode we calibrate the frequency error over a + * period of TIMESYNC_PLL_ACTIVATE seconds. + * + * When using standard slewing, only correct slewPercentCorrection of the + * error. This is to avoid overcorrection when the error is mis-measured, + * or overcorrection caused by the daemon waking up later than it is + * supposed to leaving the slew in place for longer than anticpiated. * * @param[in] data Structure tracking time sync state. * @param[in] adjustment Amount to correct the guest time. @@ -381,10 +408,76 @@ TimeSyncStepTime(TimeSyncData *data, int64 adjustment) static gboolean TimeSyncSlewTime(TimeSyncData *data, int64 adjustment) { + static int64 calibrationStart; + static int64 calibrationAdjustment; + + int64 now; + int64 remaining = 0; int64 timeSyncPeriodUS = data->timeSyncPeriod * US_PER_SEC; int64 slewDiff = (adjustment * data->slewPercentCorrection) / 100; - return TimeSync_EnableTimeSlew(slewDiff, timeSyncPeriodUS); + if (!TimeSync_GetCurrentTime(&now)) { + return FALSE; + } + + if (adjustment > TIMESYNC_PLL_UNSYNC && + data->slewState != TimeSyncUncalibrated) { + g_debug("Adjustment too large (%"FMT64"d), resetting PLL state.\n", + adjustment); + data->slewState = TimeSyncUncalibrated; + } + + if (data->slewState == TimeSyncUncalibrated) { + g_debug("Slewing time: adjustment %"FMT64"d\n", adjustment); + if (!TimeSync_Slew(slewDiff, timeSyncPeriodUS, &remaining)) { + data->slewState = TimeSyncUncalibrated; + return FALSE; + } + if (adjustment < TIMESYNC_PLL_ACTIVATE && TimeSync_PLLSupported()) { + g_debug("Starting PLL calibration.\n"); + calibrationStart = now; + /* Starting out the calibration period we are adjustment behind, + * but have already requested to correct slewDiff of that. */ + calibrationAdjustment = slewDiff - adjustment; + data->slewState = TimeSyncCalibrating; + } + } else if (data->slewState == TimeSyncCalibrating) { + if (now > calibrationStart + TIMESYNC_CALIBRATION_DURATION) { + int64 ppmErr; + /* Reset slewing to nominal and find out remaining slew. */ + TimeSync_Slew(0, timeSyncPeriodUS, &remaining); + calibrationAdjustment += adjustment; + calibrationAdjustment -= remaining; + ppmErr = ((1000000 * calibrationAdjustment) << 16) / + (now - calibrationStart); + if (ppmErr >> 16 < 500 && ppmErr >> 16 > -500) { + g_debug("Activating PLL ppmEst=%"FMT64"d (%"FMT64"d)\n", + ppmErr >> 16, ppmErr); + TimeSync_PLLUpdate(adjustment); + TimeSync_PLLSetFrequency(ppmErr); + data->slewState = TimeSyncPLL; + } else { + /* PPM error is too large to try the PLL. */ + g_debug("PPM error too large: %"FMT64"d (%"FMT64"d) " + "not activating PLL\n", ppmErr >> 16, ppmErr); + data->slewState = TimeSyncUncalibrated; + } + } else { + g_debug("Calibrating error: adjustment %"FMT64"d\n", adjustment); + if (!TimeSync_Slew(slewDiff, timeSyncPeriodUS, &remaining)) { + return FALSE; + } + calibrationAdjustment += slewDiff; + calibrationAdjustment -= remaining; + } + } else { + ASSERT(data->slewState == TimeSyncPLL); + g_debug("Updating PLL: adjustment %"FMT64"d\n", adjustment); + if (!TimeSync_PLLUpdate(adjustment)) { + TimeSyncResetSlew(data); + } + } + return TRUE; } @@ -397,7 +490,14 @@ TimeSyncSlewTime(TimeSyncData *data, int64 adjustment) static void TimeSyncResetSlew(TimeSyncData *data) { - TimeSyncSlewTime(data, 0); + int64 remaining; + int64 timeSyncPeriodUS = data->timeSyncPeriod * US_PER_SEC; + data->slewState = TimeSyncUncalibrated; + TimeSync_Slew(0, timeSyncPeriodUS, &remaining); + if (TimeSync_PLLSupported()) { + TimeSync_PLLUpdate(0); + TimeSync_PLLSetFrequency(0); + } } @@ -817,6 +917,7 @@ ToolsOnLoad(ToolsAppCtx *ctx) data->slewCorrection = FALSE; data->slewPercentCorrection = TIMESYNC_PERCENT_CORRECTION; data->state = TIMESYNC_INITIALIZING; + data->slewState = TimeSyncUncalibrated; data->timeSyncPeriod = TIMESYNC_TIME; data->timer = NULL; diff --git a/open-vm-tools/services/plugins/timeSync/timeSync.h b/open-vm-tools/services/plugins/timeSync/timeSync.h index e44c9c8a7..e0d3a6916 100644 --- a/open-vm-tools/services/plugins/timeSync/timeSync.h +++ b/open-vm-tools/services/plugins/timeSync/timeSync.h @@ -37,11 +37,21 @@ Bool TimeSync_AddToCurrentTime(int64 delta); Bool -TimeSync_EnableTimeSlew(int64 delta, - int64 timeSyncPeriod); +TimeSync_Slew(int64 delta, + int64 timeSyncPeriod, + int64 *remaining); Bool TimeSync_DisableTimeSlew(void); +Bool +TimeSync_PLLUpdate(int64 offset); + +Bool +TimeSync_PLLSetFrequency(int64 ppmCorrection); + +Bool +TimeSync_PLLSupported(void); + #endif /* _TIMESYNC_INT_H_ */