summaryrefslogtreecommitdiffstats
path: root/sys_generic.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sys_generic.c449
1 files changed, 449 insertions, 0 deletions
diff --git a/sys_generic.c b/sys_generic.c
new file mode 100644
index 0000000..5c42df1
--- /dev/null
+++ b/sys_generic.c
@@ -0,0 +1,449 @@
+/*
+ chronyd/chronyc - Programs for keeping computer clocks accurate.
+
+ **********************************************************************
+ * Copyright (C) Miroslav Lichvar 2014-2015
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * 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 GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ **********************************************************************
+
+ =======================================================================
+
+ Generic driver functions to complete system-specific drivers
+ */
+
+#include "config.h"
+
+#include "sysincl.h"
+
+#include "sys_generic.h"
+
+#include "conf.h"
+#include "local.h"
+#include "localp.h"
+#include "logging.h"
+#include "privops.h"
+#include "sched.h"
+#include "util.h"
+
+/* ================================================== */
+
+/* System clock drivers */
+static lcl_ReadFrequencyDriver drv_read_freq;
+static lcl_SetFrequencyDriver drv_set_freq;
+static lcl_SetSyncStatusDriver drv_set_sync_status;
+static lcl_AccrueOffsetDriver drv_accrue_offset;
+static lcl_OffsetCorrectionDriver drv_get_offset_correction;
+
+/* Current frequency as requested by the local module (in ppm) */
+static double base_freq;
+
+/* Maximum frequency that can be set by drv_set_freq (in ppm) */
+static double max_freq;
+
+/* Maximum expected delay in the actual frequency change (e.g. kernel ticks)
+ in local time */
+static double max_freq_change_delay;
+
+/* Maximum allowed frequency offset relative to the base frequency */
+static double max_corr_freq;
+
+/* Amount of outstanding offset to process */
+static double offset_register;
+
+/* Minimum offset to correct */
+#define MIN_OFFSET_CORRECTION 1.0e-9
+
+/* Current frequency offset between base_freq and the real clock frequency
+ as set by drv_set_freq (not in ppm) */
+static double slew_freq;
+
+/* Time (raw) of last update of slewing frequency and offset */
+static struct timespec slew_start;
+
+/* Limits for the slew length */
+#define MIN_SLEW_DURATION 1.0
+#define MAX_SLEW_DURATION 1.0e4
+
+/* Scheduler timeout ID for ending of the currently running slew */
+static SCH_TimeoutID slew_timeout_id;
+
+/* Scheduled duration of the currently running slew */
+static double slew_duration;
+
+/* Expected delay in ending of the slew due to process scheduling and
+ execution time, tracked as a decaying maximum value */
+static double slew_excess_duration;
+
+/* Maximum accepted excess duration to ignore large jumps after resuming
+ suspended system and other reasons (which should be handled in the
+ scheduler), a constant to determine the minimum slew duration to avoid
+ oscillations due to the excess, and the decay constant */
+#define MAX_SLEW_EXCESS_DURATION 100.0
+#define MIN_SLEW_DURATION_EXCESS_RATIO 5.0
+#define SLEW_EXCESS_DURATION_DECAY 0.9
+
+/* Suggested offset correction rate (correction time * offset) */
+static double correction_rate;
+
+/* Maximum expected offset correction error caused by delayed change in the
+ real frequency of the clock */
+static double slew_error;
+
+/* Minimum offset that the system driver can slew faster than the maximum
+ frequency offset that it allows to be set directly */
+static double fastslew_min_offset;
+
+/* Maximum slew rate of the system driver */
+static double fastslew_max_rate;
+
+/* Flag indicating that the system driver is currently slewing */
+static int fastslew_active;
+
+/* ================================================== */
+
+static void handle_end_of_slew(void *anything);
+static void update_slew(void);
+
+/* ================================================== */
+/* Adjust slew_start on clock step */
+
+static void
+handle_step(struct timespec *raw, struct timespec *cooked, double dfreq,
+ double doffset, LCL_ChangeType change_type, void *anything)
+{
+ if (change_type == LCL_ChangeStep) {
+ UTI_AddDoubleToTimespec(&slew_start, -doffset, &slew_start);
+ }
+}
+
+/* ================================================== */
+
+static void
+start_fastslew(void)
+{
+ if (!drv_accrue_offset)
+ return;
+
+ drv_accrue_offset(offset_register, 0.0);
+
+ DEBUG_LOG("fastslew offset=%e", offset_register);
+
+ offset_register = 0.0;
+ fastslew_active = 1;
+}
+
+/* ================================================== */
+
+static void
+stop_fastslew(struct timespec *now)
+{
+ double corr;
+
+ if (!drv_get_offset_correction || !fastslew_active)
+ return;
+
+ /* Cancel the remaining offset */
+ drv_get_offset_correction(now, &corr, NULL);
+ drv_accrue_offset(corr, 0.0);
+ offset_register -= corr;
+}
+
+/* ================================================== */
+
+static double
+clamp_freq(double freq)
+{
+ if (freq > max_freq)
+ return max_freq;
+ if (freq < -max_freq)
+ return -max_freq;
+ return freq;
+}
+
+/* ================================================== */
+/* End currently running slew and start a new one */
+
+static void
+update_slew(void)
+{
+ double old_slew_freq, total_freq, corr_freq, duration, excess_duration;
+ struct timespec now, end_of_slew;
+
+ /* Remove currently running timeout */
+ SCH_RemoveTimeout(slew_timeout_id);
+
+ LCL_ReadRawTime(&now);
+
+ /* Adjust the offset register by achieved slew */
+ duration = UTI_DiffTimespecsToDouble(&now, &slew_start);
+ offset_register -= slew_freq * duration;
+
+ stop_fastslew(&now);
+
+ /* Update the maximum excess duration, decaying even when the slew did
+ not time out (i.e. frequency was set or offset accrued), but add a small
+ value to avoid denormals */
+ slew_excess_duration = (slew_excess_duration + 1.0e-9) * SLEW_EXCESS_DURATION_DECAY;
+ excess_duration = duration - slew_duration;
+ if (slew_excess_duration < excess_duration &&
+ excess_duration <= MAX_SLEW_EXCESS_DURATION)
+ slew_excess_duration = excess_duration;
+
+ /* Calculate the duration of the new slew, considering the current correction
+ rate and previous delays in stopping of the slew */
+ if (fabs(offset_register) < MIN_OFFSET_CORRECTION) {
+ duration = MAX_SLEW_DURATION;
+ } else {
+ duration = correction_rate / fabs(offset_register);
+ if (duration < MIN_SLEW_DURATION)
+ duration = MIN_SLEW_DURATION;
+ if (duration < MIN_SLEW_DURATION_EXCESS_RATIO * slew_excess_duration)
+ duration = MIN_SLEW_DURATION_EXCESS_RATIO * slew_excess_duration;
+ }
+
+ /* Get frequency offset needed to slew the offset in the duration
+ and clamp it to the allowed maximum */
+ corr_freq = offset_register / duration;
+ if (corr_freq < -max_corr_freq)
+ corr_freq = -max_corr_freq;
+ else if (corr_freq > max_corr_freq)
+ corr_freq = max_corr_freq;
+
+ /* Let the system driver perform the slew if the requested frequency
+ offset is too large for the frequency driver */
+ if (drv_accrue_offset && fabs(corr_freq) >= fastslew_max_rate &&
+ fabs(offset_register) > fastslew_min_offset) {
+ start_fastslew();
+ corr_freq = 0.0;
+ }
+
+ /* Get the new real frequency and clamp it */
+ total_freq = clamp_freq(base_freq + corr_freq * (1.0e6 - base_freq));
+
+ /* Set the new frequency (the actual frequency returned by the call may be
+ slightly different from the requested frequency due to rounding) */
+ total_freq = (*drv_set_freq)(total_freq);
+
+ /* Compute the new slewing frequency, it's relative to the real frequency to
+ make the calculation in offset_convert() cheaper */
+ old_slew_freq = slew_freq;
+ slew_freq = (total_freq - base_freq) / (1.0e6 - total_freq);
+
+ /* Compute the dispersion introduced by changing frequency and add it
+ to all statistics held at higher levels in the system */
+ slew_error = fabs((old_slew_freq - slew_freq) * max_freq_change_delay);
+ if (slew_error >= MIN_OFFSET_CORRECTION)
+ lcl_InvokeDispersionNotifyHandlers(slew_error);
+
+ /* Compute the duration of the slew and clamp it. If the slewing frequency
+ is zero or has wrong sign (e.g. due to rounding in the frequency driver or
+ when base_freq is larger than max_freq, or fast slew is active), use the
+ maximum timeout and try again on the next update. */
+ if (fabs(offset_register) < MIN_OFFSET_CORRECTION ||
+ offset_register * slew_freq <= 0.0) {
+ duration = MAX_SLEW_DURATION;
+ } else {
+ duration = offset_register / slew_freq;
+ if (duration < MIN_SLEW_DURATION)
+ duration = MIN_SLEW_DURATION;
+ else if (duration > MAX_SLEW_DURATION)
+ duration = MAX_SLEW_DURATION;
+ }
+
+ /* Restart timer for the next update */
+ UTI_AddDoubleToTimespec(&now, duration, &end_of_slew);
+ slew_timeout_id = SCH_AddTimeout(&end_of_slew, handle_end_of_slew, NULL);
+ slew_start = now;
+ slew_duration = duration;
+
+ DEBUG_LOG("slew offset=%e corr_rate=%e base_freq=%f total_freq=%f slew_freq=%e"
+ " duration=%f excess=%f slew_error=%e",
+ offset_register, correction_rate, base_freq, total_freq, slew_freq,
+ slew_duration, slew_excess_duration, slew_error);
+}
+
+/* ================================================== */
+
+static void
+handle_end_of_slew(void *anything)
+{
+ slew_timeout_id = 0;
+ update_slew();
+}
+
+/* ================================================== */
+
+static double
+read_frequency(void)
+{
+ return base_freq;
+}
+
+/* ================================================== */
+
+static double
+set_frequency(double freq_ppm)
+{
+ base_freq = freq_ppm;
+ update_slew();
+
+ return base_freq;
+}
+
+/* ================================================== */
+
+static void
+accrue_offset(double offset, double corr_rate)
+{
+ offset_register += offset;
+ correction_rate = corr_rate;
+
+ update_slew();
+}
+
+/* ================================================== */
+/* Determine the correction to generate the cooked time for given raw time */
+
+static void
+offset_convert(struct timespec *raw,
+ double *corr, double *err)
+{
+ double duration, fastslew_corr, fastslew_err;
+
+ duration = UTI_DiffTimespecsToDouble(raw, &slew_start);
+
+ if (drv_get_offset_correction && fastslew_active) {
+ drv_get_offset_correction(raw, &fastslew_corr, &fastslew_err);
+ if (fastslew_corr == 0.0 && fastslew_err == 0.0)
+ fastslew_active = 0;
+ } else {
+ fastslew_corr = fastslew_err = 0.0;
+ }
+
+ *corr = slew_freq * duration + fastslew_corr - offset_register;
+
+ if (err) {
+ *err = fastslew_err;
+ if (fabs(duration) <= max_freq_change_delay)
+ *err += slew_error;
+ }
+}
+
+/* ================================================== */
+/* Positive means currently fast of true time, i.e. jump backwards */
+
+static int
+apply_step_offset(double offset)
+{
+ struct timespec old_time, new_time;
+ struct timeval new_time_tv;
+ double err;
+
+ LCL_ReadRawTime(&old_time);
+ UTI_AddDoubleToTimespec(&old_time, -offset, &new_time);
+ UTI_TimespecToTimeval(&new_time, &new_time_tv);
+
+ if (PRV_SetTime(&new_time_tv, NULL) < 0) {
+ DEBUG_LOG("settimeofday() failed");
+ return 0;
+ }
+
+ LCL_ReadRawTime(&old_time);
+ err = UTI_DiffTimespecsToDouble(&old_time, &new_time);
+
+ lcl_InvokeDispersionNotifyHandlers(fabs(err));
+
+ return 1;
+}
+
+/* ================================================== */
+
+static void
+set_sync_status(int synchronised, double est_error, double max_error)
+{
+ double offset;
+
+ offset = fabs(offset_register);
+ if (est_error < offset)
+ est_error = offset;
+ max_error += offset;
+
+ if (drv_set_sync_status)
+ drv_set_sync_status(synchronised, est_error, max_error);
+}
+
+/* ================================================== */
+
+void
+SYS_Generic_CompleteFreqDriver(double max_set_freq_ppm, double max_set_freq_delay,
+ lcl_ReadFrequencyDriver sys_read_freq,
+ lcl_SetFrequencyDriver sys_set_freq,
+ lcl_ApplyStepOffsetDriver sys_apply_step_offset,
+ double min_fastslew_offset, double max_fastslew_rate,
+ lcl_AccrueOffsetDriver sys_accrue_offset,
+ lcl_OffsetCorrectionDriver sys_get_offset_correction,
+ lcl_SetLeapDriver sys_set_leap,
+ lcl_SetSyncStatusDriver sys_set_sync_status)
+{
+ max_freq = max_set_freq_ppm;
+ max_freq_change_delay = max_set_freq_delay * (1.0 + max_freq / 1.0e6);
+ drv_read_freq = sys_read_freq;
+ drv_set_freq = sys_set_freq;
+ drv_accrue_offset = sys_accrue_offset;
+ drv_get_offset_correction = sys_get_offset_correction;
+ drv_set_sync_status = sys_set_sync_status;
+
+ base_freq = (*drv_read_freq)();
+ slew_freq = 0.0;
+ offset_register = 0.0;
+ slew_excess_duration = 0.0;
+
+ max_corr_freq = CNF_GetMaxSlewRate() / 1.0e6;
+
+ fastslew_min_offset = min_fastslew_offset;
+ fastslew_max_rate = max_fastslew_rate / 1.0e6;
+ fastslew_active = 0;
+
+ lcl_RegisterSystemDrivers(read_frequency, set_frequency,
+ accrue_offset, sys_apply_step_offset ?
+ sys_apply_step_offset : apply_step_offset,
+ offset_convert, sys_set_leap, set_sync_status);
+
+ LCL_AddParameterChangeHandler(handle_step, NULL);
+}
+
+/* ================================================== */
+
+void
+SYS_Generic_Finalise(void)
+{
+ struct timespec now;
+
+ /* Must *NOT* leave a slew running - clock could drift way off
+ if the daemon is not restarted */
+
+ SCH_RemoveTimeout(slew_timeout_id);
+ slew_timeout_id = 0;
+
+ (*drv_set_freq)(clamp_freq(base_freq));
+
+ LCL_ReadRawTime(&now);
+ stop_fastslew(&now);
+
+ LCL_RemoveParameterChangeHandler(handle_step, NULL);
+}
+
+/* ================================================== */