diff --git a/logging.h b/logging.h index 9d7cbfb..6f19997 100644 --- a/logging.h +++ b/logging.h @@ -81,6 +81,7 @@ typedef enum { LOGF_Rtc, LOGF_Regress, LOGF_Sys, + LOGF_SysGeneric, LOGF_SysLinux, LOGF_SysNetBSD, LOGF_SysSolaris, diff --git a/sys_generic.c b/sys_generic.c new file mode 100644 index 0000000..5232b76 --- /dev/null +++ b/sys_generic.c @@ -0,0 +1,278 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Miroslav Lichvar 2014 + * + * 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 "local.h" +#include "localp.h" +#include "logging.h" +#include "sched.h" +#include "util.h" + +/* ================================================== */ + +/* System clock frequency drivers */ +static lcl_ReadFrequencyDriver drv_read_freq; +static lcl_SetFrequencyDriver drv_set_freq; + +/* 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; + +/* 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; + +/* Maximum allowed slewing frequency */ +#define MAX_SLEW_FREQ (1 / 12.0) + +/* Time (raw) of last update of slewing frequency and offset */ +static struct timeval slew_start; + +/* Limits for the slew timeout */ +#define MIN_SLEW_TIMEOUT 1.0 +#define MAX_SLEW_TIMEOUT 1.0e4 + +/* Scheduler timeout ID and flag if the timer is currently running */ +static SCH_TimeoutID slew_timeout_id; +static int slew_timer_running; + +/* 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; + +/* ================================================== */ + +static void handle_end_of_slew(void *anything); + +/* ================================================== */ +/* Adjust slew_start on clock step */ + +static void +handle_step(struct timeval *raw, struct timeval *cooked, double dfreq, + double doffset, int is_step_change, void *anything) +{ + if (is_step_change) + UTI_AddDoubleToTimeval(&slew_start, -doffset, &slew_start); +} + +/* ================================================== */ +/* End currently running slew and start a new one */ + +static void +update_slew(void) +{ + struct timeval now, end_of_slew; + double old_slew_freq, total_freq, corr_freq, duration; + + /* Remove currently running timeout */ + if (slew_timer_running) + SCH_RemoveTimeout(slew_timeout_id); + + LCL_ReadRawTime(&now); + + /* Adjust the offset register by achieved slew */ + UTI_DiffTimevalsToDouble(&duration, &now, &slew_start); + offset_register -= slew_freq * duration; + + /* Estimate how long should the next slew take */ + if (fabs(offset_register) < MIN_OFFSET_CORRECTION) { + duration = MAX_SLEW_TIMEOUT; + } else { + duration = correction_rate / fabs(offset_register); + if (duration < MIN_SLEW_TIMEOUT) + duration = MIN_SLEW_TIMEOUT; + } + + /* 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_SLEW_FREQ) + corr_freq = -MAX_SLEW_FREQ; + else if (corr_freq > MAX_SLEW_FREQ) + corr_freq = MAX_SLEW_FREQ; + + /* Get the new real frequency and clamp it */ + total_freq = base_freq + corr_freq * (1.0e6 - base_freq); + if (total_freq > max_freq) + total_freq = max_freq; + else if (total_freq < -max_freq) + total_freq = -max_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); + 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), use maximum timeout and try again + on the next update. */ + if (offset_register * slew_freq <= 0.0) { + duration = MAX_SLEW_TIMEOUT; + } else { + duration = offset_register / slew_freq; + if (duration < MIN_SLEW_TIMEOUT) + duration = MIN_SLEW_TIMEOUT; + else if (duration > MAX_SLEW_TIMEOUT) + duration = MAX_SLEW_TIMEOUT; + } + + /* Restart timer for the next update */ + UTI_AddDoubleToTimeval(&now, duration, &end_of_slew); + slew_timeout_id = SCH_AddTimeout(&end_of_slew, handle_end_of_slew, NULL); + + slew_start = now; + slew_timer_running = 1; + + DEBUG_LOG(LOGF_SysGeneric, "slew offset=%e corr_rate=%e base_freq=%f total_freq=%f slew_freq=%e duration=%f", + offset_register, correction_rate, base_freq, total_freq, slew_freq, duration); +} + +/* ================================================== */ + +static void +handle_end_of_slew(void *anything) +{ + slew_timer_running = 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 timeval *raw, + double *corr, double *err) +{ + double duration; + + UTI_DiffTimevalsToDouble(&duration, raw, &slew_start); + + *corr = slew_freq * duration - offset_register; + if (err) + *err = fabs(duration) <= max_freq_change_delay ? slew_error : 0.0; +} + +/* ================================================== */ + +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, + lcl_SetLeapDriver sys_set_leap) +{ + 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; + + base_freq = (*drv_read_freq)(); + slew_freq = 0.0; + offset_register = 0.0; + + lcl_RegisterSystemDrivers(read_frequency, set_frequency, + accrue_offset, sys_apply_step_offset, + offset_convert, sys_set_leap); + + LCL_AddParameterChangeHandler(handle_step, NULL); +} + +/* ================================================== */ + +void +SYS_Generic_Finalise(void) +{ + /* Must *NOT* leave a slew running - clock could drift way off + if the daemon is not restarted */ + if (slew_timer_running) { + SCH_RemoveTimeout(slew_timeout_id); + slew_timer_running = 0; + } + + (*drv_set_freq)(base_freq); +} + +/* ================================================== */ diff --git a/sys_generic.h b/sys_generic.h new file mode 100644 index 0000000..45c14e2 --- /dev/null +++ b/sys_generic.h @@ -0,0 +1,42 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Miroslav Lichvar 2014 + * + * 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. + * + ********************************************************************** + + ======================================================================= + + Header file for generic driver + */ + +#ifndef GOT_SYS_GENERIC_H +#define GOT_SYS_GENERIC_H + +#include "localp.h" + +/* Register a completed driver that implements offset functions on top of + provided frequency functions */ +extern 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, + lcl_SetLeapDriver sys_set_leap); + +extern void SYS_Generic_Finalise(void); + +#endif /* GOT_SYS_GENERIC_H */