diff options
Diffstat (limited to 'sys_generic.c')
-rw-r--r-- | sys_generic.c | 449 |
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); +} + +/* ================================================== */ |