mirror of
https://git.hardenedbsd.org/hardenedbsd/HardenedBSD.git
synced 2024-12-27 13:34:00 +01:00
…
|
||
---|---|---|
.. | ||
chuinit.c | ||
clkinit.c | ||
README.kern |
Precision Time and Frequency Synchronization Using Modified Kernels 1. Introduction This memo describes replacements for certain SunOS and Ultrix kernel routines that manage the system clock and timer functions. They provide improved accuracy and stability through the use of a disciplined clock interface for use with the Network Time Protocol (NTP) or similar time- synchronization protocol. In addition, for certain models of the DECstation 5000 product line, the new routines provide improved precision to +-1 microsecond (us) (SunOS 4.1.1 already does provide precision to +-1 us). The current public NTP distribution cooperates with these kernel routines to provide synchronization in principle to within a microsecond, but in practice this is limited by the short-term stability of the oscillator that drives the timer interrupt. This memo describes the principles behind the design and operation of the software. There are two versions of the software, one that operates with the SunOS 4.1.1 kernel and the other that operates with the Ultrix 4.2a kernel (and probably the 4.3 kernel, although this has not been tested). A detailed description of the variables and algorithms is given in the hope that similar improvements can be incorporated in Unix kernels for other machines. The software itself is not included in this memo, since it involves licensed code. Detailed instructions on where to obtain it for either SunOS or Ultrix will be given separately. The principle function added to the SunOS and Ultrix kernels is to change the way the system clock is controlled, in order to provide precision time and frequency adjustments. Another function utilizes an undocumented counter in the DECstation hardware to provide precise time to the microsecond. This function can be used only with the DECstation 5000/240 and possibly others that use the same input/output chipset. 2. Design Principles In order to understand how these routines work, it is useful to consider how most Unix systems maintain the system clock. In the original design a hardware timer interrupts the kernel at some fixed rate, such as 100 Hz in the SunOS kernel and 256 Hz in the Ultrix kernel. Since 256 does not evenly divide the second in microseconds, the kernel inserts 64 us once each second so that the system clock stays in step with real time. The time returned by the gettimeofday() routine is thus characterized by 255 advances of 3906 us plus one of 3970 us. Also in the original design it is possible to slew the system clock to a new offset using the adjtime() system call. To do this the clock frequency is changed by adding or subtracting a fixed amount (tickadj) at each timer interrupt (tick) for a calculated number of ticks. Since this calculation involves dividing the requested offset by tickadj, it is possible to slew to a new offset with a precision only of tickadj, which is usually in the neighborhood of 5 us, but sometimes much higher. In order to maintain the system clock within specified bounds with this scheme, it is necessary to call adjtime() on a regular basis. For instance, let the bound be set at 100 us, which is a reasonable value for NTP-synchronized hosts on a local network, and let the onboard oscillator tolerance be 100 ppm, which is a reasonably conservative assumption. This requires that adjtime() be called at intervals not exceeding 1 second (s), which is in fact what the unmodified NTP software daemon does. In the modified kernel routines this scheme is replaced by another that extends the low-order bits of the system clock to provide very precise clock adjustments. At each timer interrupt a precisely calibrated time adjustment is added to the composite time value and overflows handled as required. The quantity to add is computed from the adjtime() call and, in addition a frequency adjustment, which is automatically calculated from previous time adjustments. This implementation operates as an adaptive-parameter, first-order, type-II, phase-lock loop (PLL), which in principle provides precision control of the system clock phase to within +-1 us and frequency to within +-5 nanoseconds (ns) per day. This PLL model is identical to the one implemented in NTP, except that in NTP the software daemon has to simulate the PLL using only the original adjtime() system call. The daemon is considerably complicated by the need to parcel time adjustments at frequent intervals in order to maintain the accuracy to specified bounds. The kernel routines do this directly, allowing vast gobs of ugly daemon code to be avoided at the expense of only a small amount of new code in the kernel. In fact, the amount of code added to the kernel for the new scheme is about the amount removed for the old scheme. The new adjtime() routine needs to be called only as each new time update is determined, which in NTP occurs at intervals of from 64 s to 1024 s. In addition, doing the frequency correction in the kernel means that the system time runs true even if the daemon were to cease operation or the network paths to the primary reference source fail. Note that the degree to which the adjtime() adjustment can be made is limited to a specific maximum value, presently +-128 milliseconds (ms), in order to achieve microsecond resolution. It is the intent in the design that settimeofday() be used for changes in system time greater than +-128 ms. It has been the Internet experience that the need to change the system time in increments greater than +-128 milliseconds is extremely rare and is usually associated with a hardware or software malfunction. Nevertheless, the limit applies to each adjtime() call and it is possible, but not recommended, that this routine is called at intervals smaller than 64 seconds, which is the NTP lower limit. For the most accurate and stable operation, adjtime() should be called at specified intervals; however, the PLL is quite forgiving and neither moderate loss of updates nor variations in the length of the interval is serious. The current engineering parameters have been optimized for intervals not greater than about 64 s. For larger intervals the PLL time constant can be adjusted to optimize the dynamic response up to intervals of 1024 s. Normally, this is automatically done by NTP. In any case, if updates are suspended, the PLL coasts at the frequency last determinated, which usually results in errors increasing only to a few tens of milliseconds over a day. The new code needs to know the initial frequency offset and time constant for the PLL, and the daemon needs to know the current frequency offset computed by the kernel for monitoring purposes. This is provided by a small change in the second argument of the kernel adjtime() calling sequence, which is documented later in this memo. Ordinarily, only the daemon will call the adjtime() routine, so the modified calling sequence is easily accommodated. Other than this change, the operation of adjtime() is transparent to the original. In the DECstation 5000/240 and possibly other models there happens to be an undocumented hardware register that counts system bus cycles at a rate of 25 MHz. The new kernel routines test for the CPU type and, in the case of the '240, use this register to interpolate system time between hardware timer interrupts. This results in a precision of +-1 us for all time values obtained via the gettimeofday() system call. This routine calls the kernel routine microtime(), which returns the actual interpolated value, but does not change the kernel time variable. Therefore, other kernel routines that access the kernel time variable directly and do not call either gettimeofday() or microtime() will continue their present behavior. The new kernel routines include provisions for error statistics (maximum error and estimated error), leap seconds and system clock status. These are intended to support applications that need such things; however, there are no applications other than the time-synchronization daemon itself that presently use them. At issue is the manner in which these data can be provided to application clients, such as new system calls and data interfaces. While a proposed interface is described later in this memo, it has not yet been implemented. This is an area for further study. While any time-synchronization daemon can in principle be modified to use the new code, the most likely will be users of the xntp3 distribution of NTP. The code in the xntp3 distribution determines whether the new kernel code is in use and automatically reconfigures as required. When the new code is in use, the daemon reads the frequency offset from a file and provides it and the initial time constant via adjtime(). In subsequent calls to adjtime(), only the time adjustment and time constant are affected. The daemon reads the frequency from the kernel (returned as the second argument of adjtime()) at intervals of one hour and writes it to the file. 3. Technical Description Following is a technical description of how the new scheme works in terms of the variables and algorithms involved. These components are discussed as a distinct entity and do not involve coding details specific to the Ultrix kernel. The algorithms involve only minor changes to the system clock and interval timer routines, but do not in themselves provide a conduit for application programs to learn the system clock status or statistics of the time-synchronization process. In a later section a number of new system calls are proposed to do this, along with an interface specification. The new scheme works like the companion simulator called kern.c and included in this directory. This stand-alone simulator includes code fragments identical to those in the modified kernel routines and operates in the same way. The system clock is implemented in the kernel using a set of variables and algorithms defined below and in the simulator. The algorithms are driven by explicit calls from the synchronization protocol as each time update is computed. The clock is read and set using the gettimeofday() and settimeofday() system calls, which operate in the same way as the originals, but return a status word describing the state of the system clock. Once the system clock has been set, the adjtime() system call is used to provide periodic updates including the time offset and possibly frequency offset and time constant. With NTP this occurs at intervals of from 64 s to 1024 s, deending on the time constant value. The kernel implements an adaptive-parameter, first-order, type-II, phase-lock loop (PLL) in order to integrate this offset into the phase and frequency of the system clock. The kernel keeps track of the time of the last update and adjusts the maximum error to grow by an amount equal to the oscillator frequency tolerance times the elapsed time since the last update. Occasionally, it is necessary to adjust the PLL parameters in response to environmental conditions, such as leap-second warning and oscillator stability observations. While the interface to do this has not yet been implemented, proposals to to that are included in a later section. A system call (setloop()) is used on such occasions to communicate these data. In addition, a system call (getloop())) is used to extract these data from the kernel for monitoring purposes. All programs utilize the system clock status variable time_status, which records whether the clock is synchronized, waiting for a leap second, etc. The value of this variable is returned by each system call. It can be set explicitly by the setloop() system call and implicitly by the settimeofday() system call and in the timer-interrupt routine. Values presently defined in the header file timex.h are as follows: int time_status = TIME_BAD; /* clock synchronization status */ #define TIME_UNS 0 /* unspecified or unknown */ #define TIME_OK 1 /* operation succeeded */ #define TIME_INS 1 /* insert leap second at end of current day */ #define TIME_DEL 2 /* delete leap second at end of current day */ #define TIME_OOP 3 /* leap second in progress */ #define TIME_BAD 4 /* system clock is not synchronized */ #define TIME_ADR -1 /* operation failed: invalid address */ #define TIME_VAL -2 /* operation failed: invalid argument */ #define TIME_PRV -3 /* operation failed: priviledged operation */ In case of a negative result code, the operation has failed; however, some variables may have been modified before the error was detected. Note that the new system calls never return a value of zero, so it is possible to determine whether the old routines or the new ones are in use. The syntax of the modified adjtime() is as follows: /* * adjtime - adjuts system time */ #include <sys/timex.h> int gettimexofday(tp, fiddle) struct timeval *tp; /* system time adjustment*/ struct timeval *fiddle; /* sneak path */ On entry the "timeval" sneak path is coded: struct timeval { long tv_sec = time_constant; /* time constant */ long tv_usec = time_freq; /* new frequency offset */ } However, the sneak is ignored if fiddle is the null pointer and the new frequency offset is ignored if zero. The value returned on exit is the system clock status defined above. The "timeval" sneak path is modified as follows: struct timeval { long tv_sec = time_precision; /* system clock precision */ long tv_usec = time_freq; /* current frequency offset */ } 3.1. Kernel Variables The following variables are used by the new code: long time_offset = 0; /* time adjustment (us) */ This variable is used by the PLL to adjust the system time in small increments. It is scaled by (1 << SHIFT_UPDATE) in binary microseconds. The maximum value that can be represented is about +-130 ms and the minimum value or precision is about one nanosecond. long time_constant = SHIFT_TAU; /* pll time constant */ This variable determines the bandwidth or "stiffness" of the PLL. It is used as a shift, with the effective value in positive powers of two. The optimum value for this variable is equal to 1/64 times the update interval. The default value SHIFT_TAU (0) corresponds to a PLL time constant of about one hour or an update interval of about one minute, which is appropriate for typical uncompensated quartz oscillators used in most computing equipment. Values larger than four are not useful, unless the local clock timebase is derived from a precision oscillator. long time_tolerance = MAXFREQ; /* frequency tolerance (ppm) */ This variable represents the maximum frequency error or tolerance of the particular platform and is a property of the architecture. It is expressed as a positive number greater than zero in parts-per-million (ppm). The default MAXFREQ (100) is appropriate for conventional workstations. long time_precision = 1000000 / HZ; /* clock precision (us) */ This variable represents the maximum error in reading the system clock. It is expressed as a positive number greater than zero in microseconds and is usually based on the number of microseconds between timer interrupts, in the case of the Ultrix kernel, 3906. However, in cases where the time can be interpolated between timer interrupts with microsecond resolution, the precision is specified as 1. This variable is computed by the kernel for use by the time-synchronization daemon, but is otherwise not used by the kernel. struct timeval time_maxerror; /* maximum error */ This variable represents the maximum error, expressed as a Unix timeval, of the system clock. For NTP, it is computed as the synchronization distance, which is equal to one-half the root delay plus the root dispersion. It is increased by a small amount (time_tolerance) each second to reflect the clock frequency tolerance. This variable is computed by the time-synchronization daemon and the kernel for use by the application program, but is otherwise not used by the kernel. struct timeval time_esterror; /* estimated error */ This variable represents the best estimate of the actual error, expressed as a Unix timeval, of the system clock based on its past behavior, together with observations of multiple clocks within the peer group. This variable is computed by the time-synchronization daemon for use by the application program, but is otherwise not used by the kernel. The PLL itself is controlled by the following variables: long time_phase = 0; /* phase offset (scaled us) */ long time_freq = 0; /* frequency offset (scaled ppm) */long time_adj = 0; /* tick adjust (scaled 1 / HZ) */ These variables control the phase increment and the frequency increment of the system clock at each tick of the clock. The time_phase variable is scaled by (1 << SHIFT_SCALE) in binary microseconds, giving a minimum value (time resolution) of 9.3e-10 us. The time_freq variable is scaled by (1 << SHIFT_KF) in parts-per-million (ppm), giving it a maximum value of about +-130 ppm and a minimum value (frequency resolution) of 6e-8 ppm. The time_adj variable is the actual phase increment in scaled microseconds to add to time_phase once each tick. It is computed from time_phase and time_freq once per second. long time_reftime = 0; /* time at last adjustment (s) */ This variable is the second's portion of the system time on the last call to adjtime(). It is used to adjust the time_freq variable as the time since the last update increases. The HZ define establishes the timer interrupt frequency, 256 Hz for the Ultrix kernel and 100 Hz for the SunOS kernel. The SHIFT_HZ define expresses the same value as the nearest power of two in order to avoid hardware multiply operations. These are the only parameters that need to be changed for different timer interrupt rates. #define HZ 256 /* timer interrupt frequency (Hz) */ #define SHIFT_HZ 8 /* log2(HZ) */ The following defines establish the engineering parameters of the PLL model. They are chosen for an initial convergence time of about an hour, an overshoot of about seven percent and a final convergence time of several hours, depending on initial frequency error. #define SHIFT_KG 10 /* shift for phase increment */ #define SHIFT_KF 24 /* shift for frequency increment */ #define SHIFT_TAU 0 /* default time constant (shift) */ The SHIFT_SCALE define establishes the decimal point on the time_phase variable which serves as a an extension to the low-order bits of the system clock variable. The SHIFT_UPDATE define establishes the decimal point of the phase portion of the adjtime() update. The FINEUSEC define represents 1 us in scaled units. #define SHIFT_SCALE 28 /* shift for scale factor */ #define SHIFT_UPDATE 14 /* shift for offset scale factor */ #define FINEUSEC (1 << SHIFT_SCALE) /* 1 us in scaled units */ The FINETUNE define represents the residual, in ppm, to be added to the system clock variable in addition to the integral 1-us value given by tick. This allows a systematic frequency offset in cases where the timer interrupt frequency does not exactly divide the second in microseconds. #define FINETUNE (1000000 - (1000000 / HZ) * HZ) /* frequency adjustment * for non-isochronous HZ (ppm) */ The following four defines establish the performance envelope of the PLL, one to bound the maximum phase error, another to bound the maximum frequency error and the last two to bound the minimum and maximum time between updates. The intent of these bounds is to force the PLL to operate within predefined limits in order to conform to the correctness models assumed by time-synchronization protocols like NTP and DTSS. An excursion which exceeds these bounds is clamped to the bound and operation proceeds accordingly. In practice, this can occur only if something has failed or is operating out of tolerance, but otherwise the PLL continues to operate in a stable mode. Note that the MAXPHASE define conforms to the maximum offset allowed in NTP before the system time is reset, rather than incrementally adjusted. #define MAXPHASE 128000 /* max phase error (us) */ #define MINSEC 64 /* min interval between updates (s) */ #define MAXFREQ 100 /* max frequency error (ppm) */ #define MAXSEC 1024 /* max interval between updates (s) */ 3.2. Code Segments The code segments illustrated in the simulator should make clear the operations at various points in the code. These segments are not derived from any licensed code. The hardupdate() fragment is called by adjtime() to update the system clock phase and frequency. This is an implementation of an adaptive-parameter, first-order, type-II phase-lock loop. Note that the time constant is in units of powers of two, so that multiplies can be done by simple shifts. The phase variable is computed as the offset multiplied by the time constant. Then, the time since the last update is computed and clamped to a maximum (for robustness) and to zero if initializing. The offset is multiplied (sorry about the ugly multiply) by the result and by the square of the time constant and then added to the frequency variable. Finally, the frequency variable is clamped not to exceed the tolerance. Note that all shifts are assumed to be positive and that a shift of a signed quantity to the right requires a litle dance. With the defines given, the maximum time offset is determined by the size in bits of the long type (32) less the SHIFT_UPDATE (14) scale factor or 18 bits (signed). The scale factor is chosen so that there is no loss of significance in later steps, which may involve a right shift up to 14 bits. This results in a maximum offset of about +-130 ms. Since the time_constant must be greater than or equal to zero, the maximum frequency offset is determined by the SHIFT_KF (24) scale factor, or about +-130 ppm. In the addition step the value of offset * mtemp is represented in 18 + 10 = 28 bits, which will not overflow a long add. There could be a loss of precision due to the right shift of up to eight bits, since time_constant is bounded at four. This results in a net worst-case frequency error of about 2^-16 us or well down into the oscillator phase noise. While the time_offset value is assumed checked before entry, the time_phase variable is an accumulator, so is clamped to the tolerance on every call. This helps to damp transients before the oscillator frequency has been determined, as well as to satisfy the correctness assertions if the time-synchronization protocol comes unstuck. The hardclock() fragment is inserted in the hardware timer interrupt routine at the point the system clock is to be incremented. The phase adjustment (time_adj) is added to the clock phase (time_phase) and tested for overflow of the microsecond. If an overflow occurs, the microsecond (tick) in incremented or decremented. The second_overflow() fragment is inserted at the point where the microseconds field of the system time variable is being checked for overflow. On rollover of the second the maximum error is increased by the tolerance. The time offset is divided by the phase weight (SHIFT_KG) and time constant. The time offset is then reduced by the result and the result is scaled and becomes the value of the phase adjustment. The phase adjustment is then corrected for the calculated frequency offset and a fixed offset FINETUNE which is a property of the architecture. On rollover of the day the leap-warning indicator is checked and the apparent time adjusted +-1 s accordingly. The gettimeofday() routine insures that the reported time is always monotonically increasing. The simulator can be used to check the loop operation over the design range of +-128 ms in time error and +-100 ppm in frequency error. This confirms that no overflows occur and that the loop initially converges in about 50-60 minutes for timer interrupt rates from 50 Hz to 1024 Hz. The loop has a normal overshoot of about seven percent and a final convergence time of several hours, depending on the initional frequency error. 3.3. Leap Seconds The leap-warning condition is determined by the synchronization protocol (if remotely synchronized), by the timecode receiver (if available), or by the operator (if awake). The time_status value must be set on the day the leap event is to occur (30 June or 31 December) and is automatically reset after the event. If the value is TIME_DEL, the kernel adds one second to the system time immediately following second 23:59:58 and resets time_status to TIME_OK. If the value is TIME_INS, the kernel subtracts one second from the system time immediately following second 23:59:59 and resets time_status to TIME_OOP, in effect causing system time to repeat second 59. Immediately following the repeated second, the kernel resets time_status to TIME_OK. Depending upon the system call implementation, the reported time during a leap second may repeat (with a return code set to advertise that fact) or be monotonically adjusted until system time "catches up" to reported time. With the latter scheme the reported time will be correct before and after the leap second, but freeze or slowly advance during the leap second itself. However, Most programs will probably use the ctime() library routine to convert from timeval (seconds, microseconds) format to tm format (seconds, minutes,...). If this routine is modified to inspect the return code of the gettimeofday() routine, it could simply report the leap second as second 60. To determine local midnight without fuss, the kernel simply finds the residue of the time.tv_sec value mod 86,400, but this requires a messy divide. Probably a better way to do this is to initialize an auxiliary counter in the settimeofday() routine using an ugly divide and increment the counter at the same time the time.tv_sec is incremented in the timer interrupt routine. For future embellishment. 4. Proposed Application Program Interface Most programs read the system clock using the gettimeofday() system call, which returns the system time and time-zone data. In the modified 5000/240 kernel, the gettimeofday() routine calls the microtime() routine, which interpolates between hardware timer interrupts to a precision of +-1 microsecond. However, the synchronization protocol provides additional information that will be of interest in many applications. For some applications it is necessary to know the maximum error of the reported time due to all causes, including those due to the system clock reading error, oscillator frequency error and accumulated errors due to intervening time servers on the path to a primary reference source. However, for those protocols that adjust the system clock frequency as well as the time offset, the errors expected in actual use will almost always be much less than the maximum error. Therefore, it is useful to report the estimated error, as well as the maximum error. It does not seem useful to provide additional details private to the kernel and synchronization protocol, such as stratum, reference identifier, reference timestamp and so forth. It would in principle be possible for the application to independently evaluate the quality of time and project into the future how long this time might be "valid." However, to do that properly would duplicate the functionality of the synchronization protocol and require knowledge of many mundane details of the platform architecture, such as the tick value, reachability status and related variables. Therefore, the application interface does not reveal anything except the time, timezone and error data. With respect to NTP, the data maintained by the protocol include the roundtrip delay and total dispersion to the source of synchronization. In terms of the above, the maximum error is computed as half the delay plus the dispersion, while the estimated error is equal to the dispersion. These are reported in timeval structures. A new system call is proposed that includes all the data in the gettimeofday() plus the two new timeval structures. The proposed interface involves modifications to the gettimeofday(), settimeofday() and adjtime() system calls, as well as new system calls to get and set various system parameters. In order to minimize confusion, by convention the new system calls are named with an "x" following the "time"; e.g., adjtime() becomes adjtimex(). The operation of the modified gettimexofday(), settimexofday() and adjtimex() system calls is identical to that of their prototypes, except for the error quantities and certain other side effects, as documented below. By convention, a NULL pointer can be used in place of any argument, in which case the argument is ignored. The synchronization protocol daemon needs to set and adjust the system clock and certain other kernel variables. It needs to read these variables for monitoring purposes as well. The present list of these include a subset of the variables defined previously: long time_precision long time_timeconstant long time_tolerance long time_freq long time_status /* * gettimexofday, settimexofday - get/set date and time */ #include <sys/timex.h> int gettimexofday(tp, tzp, tmaxp, testp) struct timeval *tp; /* system time */ struct timezone *tzp; /* timezone */ struct timeval *tmaxp; /* maximum error */ struct timeval *testp; /* estimated error */ The settimeofday() syntax is identical. Note that a call to settimexofday() automatically results in the system being declared unsynchronized (TIME_BAD return code), since the synchronization condition can only be achieved by the synchronization daemon using an internal or external primary reference source and the adjtimex() system call. /* * adjtimex - adjust system time */ #include <sys/timex.h> int adjtimex(tp, tzp, freq, tc) struct timeval *tp; /* system time */ struct timezone *tzp; /* timezone */ long freq; /* frequency adjustment */ long tc; /* time constant */ /* * getloop, setloop - get/set kernel time variables */ #include <sys/timex.h> int getloop(code, argp) int code; /* operation code */ long *argp; /* argument pointer */ The paticular kernal variables affected by these routines are selected by the operation code. Values presently defined in the header file timex.h are as follows: #define TIME_PREC 1 /* precision (log2(sec)) */ #define TIME_TCON 2 /* time constant (log2(sec) */ #define TIME_FREQ 3 /* frequency tolerance */ #define TIME_FREQ 4 /* frequency offset (scaled) */ #define TIME_STAT 5 /* status (see return codes) */ The getloop() syntax is identical. Comments welcome, but very little support is available: David L. Mills Electrical Engineering Department University of Delaware Newark, DE 19716 302 831 8247 fax 302 831 4316 mills@udel.edu