Support multiple realtime clocks, and remove locking/sleeping restrictions

on clock drivers.

This tracks multiple concurrent realtime clock drivers in a list sorted by
clock resolution.  When system time changes (and periodically) the
clock_settime() methods of all registered clocks are invoked.

To initialize system time, each driver is tried in turn from best to worst
resolution, until one succesfully returns a valid time.

The code no longer holds a mutex while calling the clock_settime() and
clock_gettime() methods of the registered clocks. This allows clock drivers
to do whatever kind of locking or sleeping is necessary (this is especially
important for i2c clock chips since i2c drivers often need to sleep).

A new clock_register_flags() function allows the clock driver to pass
flags. The flags currently defined help support drivers that use their own
techniques to avoid roundoff errors (prevents the 4/5 rounding done by the
subr_rtc code). A driver which may need to wait for resources (such as bus
ownership) may pass a flag to indicate that it will obtain system time for
itself after waiting for resources; this is merely an optimization to avoid
the common code retrieving a timespec that will never get used.

Relnotes:	yes
Differential Revision:	https://reviews.freebsd.org/D11484
This commit is contained in:
ian 2017-07-12 02:53:54 +00:00
parent 242599409b
commit 91f22a6f6a
2 changed files with 202 additions and 80 deletions

View File

@ -63,8 +63,10 @@ __FBSDID("$FreeBSD$");
#include <sys/bus.h>
#include <sys/clock.h>
#include <sys/lock.h>
#include <sys/mutex.h>
#include <sys/malloc.h>
#include <sys/sx.h>
#include <sys/sysctl.h>
#include <sys/taskqueue.h>
#ifdef FFCLOCK
#include <sys/timeffc.h>
#endif
@ -72,116 +74,210 @@ __FBSDID("$FreeBSD$");
#include "clock_if.h"
static device_t clock_dev = NULL;
static long clock_res;
static struct timespec clock_adj;
struct mtx resettodr_lock;
MTX_SYSINIT(resettodr_init, &resettodr_lock, "tod2rl", MTX_DEF);
/* XXX: should be kern. now, it's no longer machdep. */
static int disable_rtc_set;
SYSCTL_INT(_machdep, OID_AUTO, disable_rtc_set, CTLFLAG_RW, &disable_rtc_set,
0, "Disallow adjusting time-of-day clock");
/*
* An instance of a realtime clock. A list of these tracks all the registered
* clocks in the system.
*
* The resadj member is used to apply a "resolution adjustment" equal to half
* the clock's resolution, which is useful mainly on clocks with a whole-second
* resolution. Because the clock truncates the fractional part, adding half the
* resolution performs 4/5 rounding. The same adjustment is applied to the
* times returned from clock_gettime(), because the fraction returned will
* always be zero, but on average the actual fraction at the time of the call
* should be about .5.
*/
struct rtc_instance {
device_t clockdev;
int resolution;
int flags;
struct timespec resadj;
LIST_ENTRY(rtc_instance)
rtc_entries;
};
/*
* Clocks are updated using a task running on taskqueue_thread.
*/
static void settime_task_func(void *arg, int pending);
static struct task settime_task = TASK_INITIALIZER(0, settime_task_func, NULL);
/*
* Registered clocks are kept in a list which is sorted by resolution; the more
* accurate clocks get the first shot at providing the time.
*/
LIST_HEAD(rtc_listhead, rtc_instance);
static struct rtc_listhead rtc_list = LIST_HEAD_INITIALIZER(rtc_list);
static struct sx rtc_list_lock;
SX_SYSINIT(rtc_list_lock_init, &rtc_list_lock, "rtc list");
/*
* On the task thread, invoke the clock_settime() method of each registered
* clock. Do so holding only an sxlock, so that clock drivers are free to do
* whatever kind of locking or sleeping they need to.
*/
static void
settime_task_func(void *arg, int pending)
{
struct timespec ts;
struct rtc_instance *rtc;
sx_xlock(&rtc_list_lock);
LIST_FOREACH(rtc, &rtc_list, rtc_entries) {
if (!(rtc->flags & CLOCKF_SETTIME_NO_TS)) {
getnanotime(&ts);
if (!(rtc->flags & CLOCKF_SETTIME_NO_ADJ)) {
ts.tv_sec -= utc_offset();
timespecadd(&ts, &rtc->resadj);
}
} else {
ts.tv_sec = 0;
ts.tv_nsec = 0;
}
CLOCK_SETTIME(rtc->clockdev, &ts);
}
sx_xunlock(&rtc_list_lock);
}
void
clock_register(device_t dev, long res) /* res has units of microseconds */
clock_register_flags(device_t clockdev, long resolution, int flags)
{
struct rtc_instance *rtc, *newrtc;
newrtc = malloc(sizeof(*newrtc), M_DEVBUF, M_WAITOK);
newrtc->clockdev = clockdev;
newrtc->resolution = (int)resolution;
newrtc->flags = flags;
newrtc->resadj.tv_sec = newrtc->resolution / 2 / 1000000;
newrtc->resadj.tv_nsec = newrtc->resolution / 2 % 1000000 * 1000;
sx_xlock(&rtc_list_lock);
if (LIST_EMPTY(&rtc_list)) {
LIST_INSERT_HEAD(&rtc_list, newrtc, rtc_entries);
} else {
LIST_FOREACH(rtc, &rtc_list, rtc_entries) {
if (rtc->resolution > newrtc->resolution) {
LIST_INSERT_BEFORE(rtc, newrtc, rtc_entries);
break;
} else if (LIST_NEXT(rtc, rtc_entries) == NULL) {
LIST_INSERT_AFTER(rtc, newrtc, rtc_entries);
break;
}
}
}
sx_xunlock(&rtc_list_lock);
device_printf(clockdev,
"registered as a time-of-day clock, resolution %d.%6.6ds\n",
newrtc->resolution / 1000000, newrtc->resolution % 1000000);
}
void
clock_register(device_t dev, long res)
{
if (clock_dev != NULL) {
if (clock_res <= res) {
if (bootverbose)
device_printf(dev, "not installed as "
"time-of-day clock: clock %s has higher "
"resolution\n", device_get_name(clock_dev));
return;
clock_register_flags(dev, res, 0);
}
void
clock_unregister(device_t clockdev)
{
struct rtc_instance *rtc, *tmp;
sx_xlock(&rtc_list_lock);
LIST_FOREACH_SAFE(rtc, &rtc_list, rtc_entries, tmp) {
if (rtc->clockdev == clockdev) {
LIST_REMOVE(rtc, rtc_entries);
free(rtc, M_DEVBUF);
}
if (bootverbose)
device_printf(clock_dev, "removed as "
"time-of-day clock: clock %s has higher "
"resolution\n", device_get_name(dev));
}
clock_dev = dev;
clock_res = res;
clock_adj.tv_sec = res / 2 / 1000000;
clock_adj.tv_nsec = res / 2 % 1000000 * 1000;
if (bootverbose)
device_printf(dev, "registered as a time-of-day clock "
"(resolution %ldus, adjustment %jd.%09jds)\n", res,
(intmax_t)clock_adj.tv_sec, (intmax_t)clock_adj.tv_nsec);
sx_xunlock(&rtc_list_lock);
}
/*
* inittodr and settodr derived from the i386 versions written
* by Christoph Robitschko <chmr@edvz.tu-graz.ac.at>, reintroduced and
* updated by Chris Stenton <chris@gnome.co.uk> 8/10/94
*/
/*
* Initialize the time of day register, based on the time base which is, e.g.
* from a filesystem.
* Initialize the system time. Must be called from a context which does not
* restrict any locking or sleeping that clock drivers may need to do.
*
* First attempt to get the time from a registered realtime clock. The clocks
* are queried in order of resolution until one provides the time. If no clock
* can provide the current time, use the 'base' time provided by the caller, if
* non-zero. The 'base' time is potentially highly inaccurate, such as the last
* known good value of the system clock, or even a filesystem last-updated
* timestamp. It is used to prevent system time from appearing to move
* backwards in logs.
*/
void
inittodr(time_t base)
{
struct timespec ts;
struct rtc_instance *rtc;
int error;
if (clock_dev == NULL) {
printf("warning: no time-of-day clock registered, system time "
"will not be set accurately\n");
goto wrong_time;
}
/* XXX: We should poll all registered RTCs in case of failure */
mtx_lock(&resettodr_lock);
error = CLOCK_GETTIME(clock_dev, &ts);
mtx_unlock(&resettodr_lock);
if (error != 0 && error != EINVAL) {
printf("warning: clock_gettime failed (%d), the system time "
"will not be set accurately\n", error);
goto wrong_time;
}
if (error == EINVAL || ts.tv_sec < 0) {
printf("Invalid time in real time clock.\n"
"Check and reset the date immediately!\n");
goto wrong_time;
error = ENXIO;
sx_xlock(&rtc_list_lock);
LIST_FOREACH(rtc, &rtc_list, rtc_entries) {
if ((error = CLOCK_GETTIME(rtc->clockdev, &ts)) != 0)
continue;
if (ts.tv_sec < 0 || ts.tv_nsec < 0) {
error = EINVAL;
continue;
}
if (!(rtc->flags & CLOCKF_GETTIME_NO_ADJ)) {
timespecadd(&ts, &rtc->resadj);
ts.tv_sec += utc_offset();
}
if (bootverbose)
device_printf(rtc->clockdev,
"providing initial system time\n");
break;
}
sx_xunlock(&rtc_list_lock);
ts.tv_sec += utc_offset();
timespecadd(&ts, &clock_adj);
tc_setclock(&ts);
#ifdef FFCLOCK
ffclock_reset_clock(&ts);
#endif
return;
wrong_time:
if (base > 0) {
ts.tv_sec = base;
/*
* Do not report errors from each clock; it is expected that some clocks
* cannot provide results in some situations. Only report problems when
* no clocks could provide the time.
*/
if (error != 0) {
switch (error) {
case ENXIO:
printf("Warning: no time-of-day clock registered, ");
break;
case EINVAL:
printf("Warning: bad time from time-of-day clock, ");
break;
default:
printf("Error reading time-of-day clock (%d), ", error);
break;
}
printf("system time will not be set accurately\n");
ts.tv_sec = (base > 0) ? base : -1;
ts.tv_nsec = 0;
}
if (ts.tv_sec >= 0) {
tc_setclock(&ts);
#ifdef FFCLOCK
ffclock_reset_clock(&ts);
#endif
}
}
/*
* Write system time back to RTC
* Write system time back to all registered clocks, unless disabled by admin.
* This can be called from a context that restricts locking and/or sleeping; the
* actual updating is done asynchronously on a task thread.
*/
void
resettodr(void)
{
struct timespec ts;
int error;
if (disable_rtc_set || clock_dev == NULL)
if (disable_rtc_set)
return;
getnanotime(&ts);
timespecadd(&ts, &clock_adj);
ts.tv_sec -= utc_offset();
/* XXX: We should really set all registered RTCs */
mtx_lock(&resettodr_lock);
error = CLOCK_SETTIME(clock_dev, &ts);
mtx_unlock(&resettodr_lock);
if (error != 0)
printf("warning: clock_settime failed (%d), time-of-day clock "
"not adjusted to system time\n", error);
taskqueue_enqueue(taskqueue_thread, &settime_task);
}

View File

@ -54,7 +54,6 @@
*/
extern int tz_minuteswest;
extern int tz_dsttime;
extern struct mtx resettodr_lock;
int utc_offset(void);
@ -76,7 +75,34 @@ struct clocktime {
int clock_ct_to_ts(struct clocktime *, struct timespec *);
void clock_ts_to_ct(struct timespec *, struct clocktime *);
void clock_register(device_t, long);
/*
* Time-of-day clock register/unregister functions, and associated flags. These
* functions can sleep. Upon return from unregister, the clock's methods are
* not running and will not be called again.
*
* Flags:
*
* CLOCKF_SETTIME_NO_TS
* Do not pass a timespec to clock_settime(), the driver obtains its own time
* and applies its own adjustments (this flag implies CLOCKF_SETTIME_NO_ADJ).
*
* CLOCKF_SETTIME_NO_ADJ
* Do not apply utc offset and resolution/accuracy adjustments to the value
* passed to clock_settime(), the driver applies them itself.
*
* CLOCKF_GETTIME_NO_ADJ
* Do not apply utc offset and resolution/accuracy adjustments to the value
* returned from clock_gettime(), the driver has already applied them.
*/
#define CLOCKF_SETTIME_NO_TS 0x00000001
#define CLOCKF_SETTIME_NO_ADJ 0x00000002
#define CLOCKF_GETTIME_NO_ADJ 0x00000004
void clock_register(device_t _clockdev, long _resolution_us);
void clock_register_flags(device_t _clockdev, long _resolution_us, int _flags);
void clock_unregister(device_t _clockdev);
/*
* BCD to decimal and decimal to BCD.