/* chronyd/chronyc - Programs for keeping computer clocks accurate. ********************************************************************** * Copyright (C) Miroslav Lichvar 2009-2011, 2013-2014, 2016-2019, 2022 * * 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. * ********************************************************************** ======================================================================= Routines implementing reference clocks. */ #include "config.h" #include "array.h" #include "refclock.h" #include "reference.h" #include "conf.h" #include "local.h" #include "memory.h" #include "util.h" #include "sources.h" #include "logging.h" #include "regress.h" #include "samplefilt.h" #include "sched.h" /* Maximum offset of locked reference as a fraction of the PPS interval */ #define PPS_LOCK_LIMIT 0.4 /* list of refclock drivers */ extern RefclockDriver RCL_SHM_driver; extern RefclockDriver RCL_SOCK_driver; extern RefclockDriver RCL_PPS_driver; extern RefclockDriver RCL_PHC_driver; struct FilterSample { double offset; double dispersion; struct timespec sample_time; }; struct RCL_Instance_Record { RefclockDriver *driver; void *data; char *driver_parameter; int driver_parameter_length; int driver_poll; int driver_polled; int poll; int leap_status; int local; int pps_forced; int pps_rate; int pps_active; int max_lock_age; int stratum; int tai; uint32_t ref_id; uint32_t lock_ref; double offset; double delay; double precision; double pulse_width; SPF_Instance filter; SCH_TimeoutID timeout_id; SRC_Instance source; }; /* Array of pointers to RCL_Instance_Record */ static ARR_Instance refclocks; static LOG_FileID logfileid; static int valid_sample_time(RCL_Instance instance, struct timespec *sample_time); static int pps_stratum(RCL_Instance instance, struct timespec *ts); static void poll_timeout(void *arg); static void slew_samples(struct timespec *raw, struct timespec *cooked, double dfreq, double doffset, LCL_ChangeType change_type, void *anything); static void add_dispersion(double dispersion, void *anything); static void log_sample(RCL_Instance instance, struct timespec *sample_time, int filtered, int pulse, double raw_offset, double cooked_offset, double dispersion); static RCL_Instance get_refclock(unsigned int index) { return *(RCL_Instance *)ARR_GetElement(refclocks, index); } void RCL_Initialise(void) { refclocks = ARR_CreateInstance(sizeof (RCL_Instance)); CNF_AddRefclocks(); if (ARR_GetSize(refclocks) > 0) { LCL_AddParameterChangeHandler(slew_samples, NULL); LCL_AddDispersionNotifyHandler(add_dispersion, NULL); } logfileid = CNF_GetLogRefclocks() ? LOG_FileOpen("refclocks", " Date (UTC) Time Refid DP L P Raw offset Cooked offset Disp.") : -1; } void RCL_Finalise(void) { unsigned int i; for (i = 0; i < ARR_GetSize(refclocks); i++) { RCL_Instance inst = get_refclock(i); if (inst->driver->fini) inst->driver->fini(inst); SPF_DestroyInstance(inst->filter); Free(inst->driver_parameter); SRC_DestroyInstance(inst->source); Free(inst); } if (ARR_GetSize(refclocks) > 0) { LCL_RemoveParameterChangeHandler(slew_samples, NULL); LCL_RemoveDispersionNotifyHandler(add_dispersion, NULL); } ARR_DestroyInstance(refclocks); } int RCL_AddRefclock(RefclockParameters *params) { RCL_Instance inst; inst = MallocNew(struct RCL_Instance_Record); *(RCL_Instance *)ARR_GetNewElement(refclocks) = inst; if (strcmp(params->driver_name, "SHM") == 0) { inst->driver = &RCL_SHM_driver; } else if (strcmp(params->driver_name, "SOCK") == 0) { inst->driver = &RCL_SOCK_driver; } else if (strcmp(params->driver_name, "PPS") == 0) { inst->driver = &RCL_PPS_driver; } else if (strcmp(params->driver_name, "PHC") == 0) { inst->driver = &RCL_PHC_driver; } else { LOG_FATAL("unknown refclock driver %s", params->driver_name); } if (!inst->driver->init && !inst->driver->poll) LOG_FATAL("refclock driver %s is not compiled in", params->driver_name); if (params->tai && !CNF_GetLeapSecTimezone()) LOG_FATAL("refclock tai option requires leapsectz"); inst->data = NULL; inst->driver_parameter = Strdup(params->driver_parameter); inst->driver_parameter_length = 0; inst->driver_poll = params->driver_poll; inst->poll = params->poll; inst->driver_polled = 0; inst->leap_status = LEAP_Normal; inst->local = params->local; inst->pps_forced = params->pps_forced; inst->pps_rate = params->pps_rate; inst->pps_active = 0; inst->max_lock_age = params->max_lock_age; inst->stratum = params->stratum; inst->tai = params->tai; inst->lock_ref = params->lock_ref_id; inst->offset = params->offset; inst->delay = params->delay; inst->precision = LCL_GetSysPrecisionAsQuantum(); inst->precision = MAX(inst->precision, params->precision); inst->pulse_width = params->pulse_width; inst->timeout_id = -1; inst->source = NULL; if (inst->driver_parameter) { int i; inst->driver_parameter_length = strlen(inst->driver_parameter); for (i = 0; i < inst->driver_parameter_length; i++) if (inst->driver_parameter[i] == ':') inst->driver_parameter[i] = '\0'; } if (inst->pps_rate < 1) inst->pps_rate = 1; if (params->ref_id) inst->ref_id = params->ref_id; else { unsigned char ref[5] = { 0, 0, 0, 0, 0 }; unsigned int index = ARR_GetSize(refclocks) - 1; snprintf((char *)ref, sizeof (ref), "%3.3s", params->driver_name); ref[3] = index % 10 + '0'; if (index >= 10) ref[2] = (index / 10) % 10 + '0'; inst->ref_id = (uint32_t)ref[0] << 24 | ref[1] << 16 | ref[2] << 8 | ref[3]; } if (inst->local) { inst->pps_forced = 1; inst->lock_ref = inst->ref_id; inst->leap_status = LEAP_Unsynchronised; inst->max_lock_age = MAX(inst->max_lock_age, 3); } if (inst->driver->poll) { int max_samples; if (inst->driver_poll > inst->poll) inst->driver_poll = inst->poll; max_samples = 1 << (inst->poll - inst->driver_poll); if (max_samples < params->filter_length) { if (max_samples < 4) { LOG(LOGS_WARN, "Setting filter length for %s to %d", UTI_RefidToString(inst->ref_id), max_samples); } params->filter_length = max_samples; } } if (inst->driver->init && !inst->driver->init(inst)) LOG_FATAL("refclock %s initialisation failed", params->driver_name); /* Require the filter to have at least 4 samples to produce a filtered sample, or be full for shorter lengths, and combine 60% of samples closest to the median */ inst->filter = SPF_CreateInstance(MIN(params->filter_length, 4), params->filter_length, params->max_dispersion, 0.6); inst->source = SRC_CreateNewInstance(inst->ref_id, SRC_REFCLOCK, 0, params->sel_options, NULL, params->min_samples, params->max_samples, 0.0, 0.0); DEBUG_LOG("refclock %s refid=%s poll=%d dpoll=%d filter=%d", params->driver_name, UTI_RefidToString(inst->ref_id), inst->poll, inst->driver_poll, params->filter_length); return 1; } void RCL_StartRefclocks(void) { unsigned int i, j, n, lock_index; n = ARR_GetSize(refclocks); for (i = 0; i < n; i++) { RCL_Instance inst = get_refclock(i); SRC_SetActive(inst->source); inst->timeout_id = SCH_AddTimeoutByDelay(0.0, poll_timeout, (void *)inst); /* Replace lock refid with the refclock's index, or -1 if not valid */ lock_index = -1; if (inst->lock_ref != 0) { for (j = 0; j < n; j++) { RCL_Instance inst2 = get_refclock(j); if (inst->lock_ref != inst2->ref_id) continue; if (inst->driver->poll && inst2->driver->poll && (double)inst->max_lock_age / inst->pps_rate < UTI_Log2ToDouble(inst2->driver_poll)) LOG(LOGS_WARN, "%s maxlockage too small for %s", UTI_RefidToString(inst->ref_id), UTI_RefidToString(inst2->ref_id)); lock_index = j; break; } if (lock_index == -1 || (lock_index == i && !inst->local)) LOG(LOGS_WARN, "Invalid lock refid %s", UTI_RefidToString(inst->lock_ref)); } inst->lock_ref = lock_index; } } void RCL_ReportSource(RPT_SourceReport *report, struct timespec *now) { unsigned int i; uint32_t ref_id; assert(report->ip_addr.family == IPADDR_INET4); ref_id = report->ip_addr.addr.in4; for (i = 0; i < ARR_GetSize(refclocks); i++) { RCL_Instance inst = get_refclock(i); if (inst->ref_id == ref_id) { report->poll = inst->poll; report->mode = RPT_LOCAL_REFERENCE; break; } } } void RCL_SetDriverData(RCL_Instance instance, void *data) { instance->data = data; } void * RCL_GetDriverData(RCL_Instance instance) { return instance->data; } char * RCL_GetDriverParameter(RCL_Instance instance) { return instance->driver_parameter; } static char * get_next_driver_option(RCL_Instance instance, char *option) { if (option == NULL) option = instance->driver_parameter; option += strlen(option) + 1; if (option >= instance->driver_parameter + instance->driver_parameter_length) return NULL; return option; } void RCL_CheckDriverOptions(RCL_Instance instance, const char **options) { char *option; int i, len; for (option = get_next_driver_option(instance, NULL); option; option = get_next_driver_option(instance, option)) { for (i = 0; options && options[i]; i++) { len = strlen(options[i]); if (!strncmp(options[i], option, len) && (option[len] == '=' || option[len] == '\0')) break; } if (!options || !options[i]) LOG_FATAL("Invalid refclock driver option %s", option); } } char * RCL_GetDriverOption(RCL_Instance instance, char *name) { char *option; int len; len = strlen(name); for (option = get_next_driver_option(instance, NULL); option; option = get_next_driver_option(instance, option)) { if (!strncmp(name, option, len)) { if (option[len] == '=') return option + len + 1; if (option[len] == '\0') return option + len; } } return NULL; } static int convert_tai_offset(struct timespec *sample_time, double *offset) { struct timespec tai_ts, utc_ts; int tai_offset; /* Get approximate TAI-UTC offset for the reference time in TAI */ UTI_AddDoubleToTimespec(sample_time, *offset, &tai_ts); tai_offset = REF_GetTaiOffset(&tai_ts); /* Get TAI-UTC offset for the reference time in UTC +/- 1 second */ UTI_AddDoubleToTimespec(&tai_ts, -tai_offset, &utc_ts); tai_offset = REF_GetTaiOffset(&utc_ts); if (!tai_offset) return 0; *offset -= tai_offset; return 1; } static int accumulate_sample(RCL_Instance instance, struct timespec *sample_time, double offset, double dispersion) { NTP_Sample sample; sample.time = *sample_time; sample.offset = offset; sample.peer_delay = instance->delay; sample.root_delay = instance->delay; sample.peer_dispersion = dispersion; sample.root_dispersion = dispersion; return SPF_AccumulateSample(instance->filter, &sample); } int RCL_AddSample(RCL_Instance instance, struct timespec *sample_time, struct timespec *ref_time, int leap) { double correction, dispersion, raw_offset, offset; struct timespec cooked_time; if (instance->pps_forced) return RCL_AddPulse(instance, sample_time, 1.0e-9 * (sample_time->tv_nsec - ref_time->tv_nsec)); raw_offset = UTI_DiffTimespecsToDouble(ref_time, sample_time); LCL_GetOffsetCorrection(sample_time, &correction, &dispersion); UTI_AddDoubleToTimespec(sample_time, correction, &cooked_time); dispersion += instance->precision; /* Make sure the timestamp and offset provided by the driver are sane */ if (!UTI_IsTimeOffsetSane(sample_time, raw_offset) || !valid_sample_time(instance, &cooked_time)) return 0; switch (leap) { case LEAP_Normal: case LEAP_InsertSecond: case LEAP_DeleteSecond: instance->leap_status = leap; break; default: DEBUG_LOG("refclock sample ignored bad leap %d", leap); return 0; } /* Calculate offset = raw_offset - correction + instance->offset in parts to avoid loss of precision if there are large differences */ offset = ref_time->tv_sec - sample_time->tv_sec - (time_t)correction + (time_t)instance->offset; offset += 1.0e-9 * (ref_time->tv_nsec - sample_time->tv_nsec) - (correction - (time_t)correction) + (instance->offset - (time_t)instance->offset); if (instance->tai && !convert_tai_offset(sample_time, &offset)) { DEBUG_LOG("refclock sample ignored unknown TAI offset"); return 0; } if (!accumulate_sample(instance, &cooked_time, offset, dispersion)) return 0; instance->pps_active = 0; log_sample(instance, &cooked_time, 0, 0, raw_offset, offset, dispersion); /* for logging purposes */ if (!instance->driver->poll) instance->driver_polled++; return 1; } int RCL_AddPulse(RCL_Instance instance, struct timespec *pulse_time, double second) { double correction, dispersion; struct timespec cooked_time; LCL_GetOffsetCorrection(pulse_time, &correction, &dispersion); UTI_AddDoubleToTimespec(pulse_time, correction, &cooked_time); second += correction; if (!UTI_IsTimeOffsetSane(pulse_time, 0.0)) return 0; return RCL_AddCookedPulse(instance, &cooked_time, second, dispersion, correction); } static int check_pulse_edge(RCL_Instance instance, double offset, double distance) { double max_error; if (instance->pulse_width <= 0.0) return 1; max_error = 1.0 / instance->pps_rate - instance->pulse_width; max_error = MIN(instance->pulse_width, max_error); max_error *= 0.5; if (fabs(offset) > max_error || distance > max_error) { DEBUG_LOG("refclock pulse ignored offset=%.9f distance=%.9f max_error=%.9f", offset, distance, max_error); return 0; } return 1; } int RCL_AddCookedPulse(RCL_Instance instance, struct timespec *cooked_time, double second, double dispersion, double raw_correction) { double offset; int rate; NTP_Leap leap; if (!UTI_IsTimeOffsetSane(cooked_time, second) || !valid_sample_time(instance, cooked_time)) return 0; leap = LEAP_Normal; dispersion += instance->precision; rate = instance->pps_rate; offset = -second + instance->offset; /* Adjust the offset to [-0.5/rate, 0.5/rate) interval */ offset -= (long)(offset * rate) / (double)rate; if (offset < -0.5 / rate) offset += 1.0 / rate; else if (offset >= 0.5 / rate) offset -= 1.0 / rate; if (instance->lock_ref != -1) { RCL_Instance lock_refclock; NTP_Sample ref_sample; double sample_diff, shift; lock_refclock = get_refclock(instance->lock_ref); if (!SPF_GetLastSample(lock_refclock->filter, &ref_sample)) { if (instance->local) { /* Make the first sample in order to lock to itself */ ref_sample.time = *cooked_time; ref_sample.offset = offset; ref_sample.peer_delay = ref_sample.peer_dispersion = 0; ref_sample.root_delay = ref_sample.root_dispersion = 0; } else { DEBUG_LOG("refclock pulse ignored no ref sample"); return 0; } } ref_sample.root_dispersion += SPF_GetAvgSampleDispersion(lock_refclock->filter); sample_diff = UTI_DiffTimespecsToDouble(cooked_time, &ref_sample.time); if (fabs(sample_diff) >= (double)instance->max_lock_age / rate) { DEBUG_LOG("refclock pulse ignored samplediff=%.9f", sample_diff); /* Restart the local mode */ if (instance->local) { LOG(LOGS_WARN, "Local refclock lost lock"); SPF_DropSamples(instance->filter); SRC_ResetInstance(instance->source); } return 0; } /* Align the offset to the reference sample */ shift = round((ref_sample.offset - offset) * rate) / rate; offset += shift; if (fabs(ref_sample.offset - offset) + ref_sample.root_dispersion + dispersion > PPS_LOCK_LIMIT / rate) { DEBUG_LOG("refclock pulse ignored offdiff=%.9f refdisp=%.9f disp=%.9f", ref_sample.offset - offset, ref_sample.root_dispersion, dispersion); return 0; } if (!check_pulse_edge(instance, ref_sample.offset - offset, 0.0)) return 0; leap = lock_refclock->leap_status; DEBUG_LOG("refclock pulse offset=%.9f offdiff=%.9f samplediff=%.9f", offset, ref_sample.offset - offset, sample_diff); } else { struct timespec ref_time; int is_synchronised, stratum; double root_delay, root_dispersion, distance; uint32_t ref_id; /* Ignore the pulse if we are not well synchronized and the local reference is not active */ REF_GetReferenceParams(cooked_time, &is_synchronised, &leap, &stratum, &ref_id, &ref_time, &root_delay, &root_dispersion); distance = fabs(root_delay) / 2 + root_dispersion; if (leap == LEAP_Unsynchronised || distance >= 0.5 / rate) { DEBUG_LOG("refclock pulse ignored offset=%.9f sync=%d dist=%.9f", offset, leap != LEAP_Unsynchronised, distance); /* Drop also all stored samples */ SPF_DropSamples(instance->filter); return 0; } if (!check_pulse_edge(instance, offset, distance)) return 0; } if (!accumulate_sample(instance, cooked_time, offset, dispersion)) return 0; instance->leap_status = leap; instance->pps_active = 1; log_sample(instance, cooked_time, 0, 1, offset + raw_correction - instance->offset, offset, dispersion); /* for logging purposes */ if (!instance->driver->poll) instance->driver_polled++; return 1; } double RCL_GetPrecision(RCL_Instance instance) { return instance->precision; } int RCL_GetDriverPoll(RCL_Instance instance) { return instance->driver_poll; } static int valid_sample_time(RCL_Instance instance, struct timespec *sample_time) { struct timespec now; double diff; LCL_ReadCookedTime(&now, NULL); diff = UTI_DiffTimespecsToDouble(&now, sample_time); if (diff < 0.0 || diff > UTI_Log2ToDouble(instance->poll + 1)) { DEBUG_LOG("%s refclock sample time %s not valid age=%.6f", UTI_RefidToString(instance->ref_id), UTI_TimespecToString(sample_time), diff); return 0; } return 1; } static int pps_stratum(RCL_Instance instance, struct timespec *ts) { struct timespec ref_time; int is_synchronised, stratum; unsigned int i; double root_delay, root_dispersion; NTP_Leap leap; uint32_t ref_id; RCL_Instance refclock; REF_GetReferenceParams(ts, &is_synchronised, &leap, &stratum, &ref_id, &ref_time, &root_delay, &root_dispersion); /* Don't change our stratum if the local reference is active or this is the current source */ if (ref_id == instance->ref_id || (!is_synchronised && leap != LEAP_Unsynchronised)) return stratum - 1; /* Or the current source is another PPS refclock */ for (i = 0; i < ARR_GetSize(refclocks); i++) { refclock = get_refclock(i); if (refclock->ref_id == ref_id && refclock->pps_active && refclock->lock_ref == -1) return stratum - 1; } return 0; } static void get_local_stats(RCL_Instance inst, struct timespec *ref, double *freq, double *offset) { double offset_sd, freq_sd, skew, root_delay, root_disp; SST_Stats stats = SRC_GetSourcestats(inst->source); if (SST_Samples(stats) < SST_GetMinSamples(stats)) { UTI_ZeroTimespec(ref); return; } SST_GetTrackingData(stats, ref, offset, &offset_sd, freq, &freq_sd, &skew, &root_delay, &root_disp); } static void follow_local(RCL_Instance inst, struct timespec *prev_ref_time, double prev_freq, double prev_offset) { SST_Stats stats = SRC_GetSourcestats(inst->source); double freq, dfreq, offset, doffset, elapsed; struct timespec now, ref_time; get_local_stats(inst, &ref_time, &freq, &offset); if (UTI_IsZeroTimespec(prev_ref_time) || UTI_IsZeroTimespec(&ref_time)) return; dfreq = (freq - prev_freq) / (1.0 - prev_freq); elapsed = UTI_DiffTimespecsToDouble(&ref_time, prev_ref_time); doffset = offset - elapsed * prev_freq - prev_offset; if (!REF_AdjustReference(doffset, dfreq)) return; LCL_ReadCookedTime(&now, NULL); SST_SlewSamples(stats, &now, dfreq, doffset); SPF_SlewSamples(inst->filter, &now, dfreq, doffset); /* Keep the offset close to zero to not lose precision */ if (fabs(offset) >= 1.0) { SST_CorrectOffset(stats, -round(offset)); SPF_CorrectOffset(inst->filter, -round(offset)); } } static void poll_timeout(void *arg) { NTP_Sample sample; int poll, stratum; RCL_Instance inst = (RCL_Instance)arg; poll = inst->poll; if (inst->driver->poll) { poll = inst->driver_poll; inst->driver->poll(inst); inst->driver_polled++; } if (!(inst->driver->poll && inst->driver_polled < (1 << (inst->poll - inst->driver_poll)))) { inst->driver_polled = 0; if (SPF_GetFilteredSample(inst->filter, &sample)) { double local_freq, local_offset; struct timespec local_ref_time; /* Handle special case when PPS is used with the local reference */ if (inst->pps_active && inst->lock_ref == -1) stratum = pps_stratum(inst, &sample.time); else stratum = inst->stratum; if (inst->local) { get_local_stats(inst, &local_ref_time, &local_freq, &local_offset); inst->leap_status = LEAP_Unsynchronised; } SRC_UpdateReachability(inst->source, 1); SRC_UpdateStatus(inst->source, stratum, inst->leap_status); SRC_AccumulateSample(inst->source, &sample); SRC_SelectSource(inst->source); if (inst->local) follow_local(inst, &local_ref_time, local_freq, local_offset); log_sample(inst, &sample.time, 1, 0, 0.0, sample.offset, sample.peer_dispersion); } else { SRC_UpdateReachability(inst->source, 0); } } inst->timeout_id = SCH_AddTimeoutByDelay(UTI_Log2ToDouble(poll), poll_timeout, arg); } static void slew_samples(struct timespec *raw, struct timespec *cooked, double dfreq, double doffset, LCL_ChangeType change_type, void *anything) { unsigned int i; for (i = 0; i < ARR_GetSize(refclocks); i++) { if (change_type == LCL_ChangeUnknownStep) SPF_DropSamples(get_refclock(i)->filter); else SPF_SlewSamples(get_refclock(i)->filter, cooked, dfreq, doffset); } } static void add_dispersion(double dispersion, void *anything) { unsigned int i; for (i = 0; i < ARR_GetSize(refclocks); i++) SPF_AddDispersion(get_refclock(i)->filter, dispersion); } static void log_sample(RCL_Instance instance, struct timespec *sample_time, int filtered, int pulse, double raw_offset, double cooked_offset, double dispersion) { char sync_stats[4] = {'N', '+', '-', '?'}; if (logfileid == -1) return; if (!filtered) { LOG_FileWrite(logfileid, "%s.%06d %-5s %3d %1c %1d %13.6e %13.6e %10.3e", UTI_TimeToLogForm(sample_time->tv_sec), (int)sample_time->tv_nsec / 1000, UTI_RefidToString(instance->ref_id), instance->driver_polled, sync_stats[instance->leap_status], pulse, raw_offset, cooked_offset, dispersion); } else { LOG_FileWrite(logfileid, "%s.%06d %-5s - %1c - - %13.6e %10.3e", UTI_TimeToLogForm(sample_time->tv_sec), (int)sample_time->tv_nsec / 1000, UTI_RefidToString(instance->ref_id), sync_stats[instance->leap_status], cooked_offset, dispersion); } }