Files
linux/kernel/time/clocksource-wdtest.c
Thomas Gleixner 763aacf86f clocksource: Rewrite watchdog code completely
The clocksource watchdog code has over time reached the state of an
impenetrable maze of duct tape and staples. The original design, which was
made in the context of systems far smaller than today, is based on the
assumption that the to be monitored clocksource (TSC) can be trivially
compared against a known to be stable clocksource (HPET/ACPI-PM timer).

Over the years it turned out that this approach has major flaws:

  - Long delays between watchdog invocations can result in wrap arounds
    of the reference clocksource

  - Scalability of the reference clocksource readout can degrade on large
    multi-socket systems due to interconnect congestion

This was addressed with various heuristics which degraded the accuracy of
the watchdog to the point that it fails to detect actual TSC problems on
older hardware which exposes slow inter CPU drifts due to firmware
manipulating the TSC to hide SMI time.

To address this and bring back sanity to the watchdog, rewrite the code
completely with a different approach:

  1) Restrict the validation against a reference clocksource to the boot
     CPU, which is usually the CPU/Socket closest to the legacy block which
     contains the reference source (HPET/ACPI-PM timer). Validate that the
     reference readout is within a bound latency so that the actual
     comparison against the TSC stays within 500ppm as long as the clocks
     are stable.

  2) Compare the TSCs of the other CPUs in a round robin fashion against
     the boot CPU in the same way the TSC synchronization on CPU hotplug
     works. This still can suffer from delayed reaction of the remote CPU
     to the SMP function call and the latency of the control variable cache
     line. But this latency is not affecting correctness. It only affects
     the accuracy. With low contention the readout latency is in the low
     nanoseconds range, which detects even slight skews between CPUs. Under
     high contention this becomes obviously less accurate, but still
     detects slow skews reliably as it solely relies on subsequent readouts
     being monotonically increasing. It just can take slightly longer to
     detect the issue.

  3) Rewrite the watchdog test so it tests the various mechanisms one by
     one and validating the result against the expectation.

Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Tested-by: Borislav Petkov (AMD) <bp@alien8.de>
Tested-by: Daniel J Blueman <daniel@quora.org>
Reviewed-by: Jiri Wiesner <jwiesner@suse.de>
Reviewed-by: Daniel J Blueman <daniel@quora.org>
Link: https://patch.msgid.link/20260123231521.926490888@kernel.org
Link: https://patch.msgid.link/87h5qeomm5.ffs@tglx
2026-03-20 13:36:32 +01:00

199 lines
4.9 KiB
C

// SPDX-License-Identifier: GPL-2.0+
/*
* Unit test for the clocksource watchdog.
*
* Copyright (C) 2021 Facebook, Inc.
* Copyright (C) 2026 Intel Corp.
*
* Author: Paul E. McKenney <paulmck@kernel.org>
* Author: Thomas Gleixner <tglx@kernel.org>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/clocksource.h>
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include "tick-internal.h"
#include "timekeeping_internal.h"
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Clocksource watchdog unit test");
MODULE_AUTHOR("Paul E. McKenney <paulmck@kernel.org>");
MODULE_AUTHOR("Thomas Gleixner <tglx@kernel.org>");
enum wdtest_states {
WDTEST_INJECT_NONE,
WDTEST_INJECT_DELAY,
WDTEST_INJECT_POSITIVE,
WDTEST_INJECT_NEGATIVE,
WDTEST_INJECT_PERCPU = 0x100,
};
static enum wdtest_states wdtest_state;
static unsigned long wdtest_test_count;
static ktime_t wdtest_last_ts, wdtest_offset;
#define SHIFT_4000PPM 8
static ktime_t wdtest_get_offset(struct clocksource *cs)
{
if (wdtest_state < WDTEST_INJECT_PERCPU)
return wdtest_test_count & 0x1 ? 0 : wdtest_offset >> SHIFT_4000PPM;
/* Only affect the readout of the "remote" CPU */
return cs->wd_cpu == smp_processor_id() ? 0 : NSEC_PER_MSEC;
}
static u64 wdtest_ktime_read(struct clocksource *cs)
{
ktime_t now = ktime_get_raw_fast_ns();
ktime_t intv = now - wdtest_last_ts;
/*
* Only increment the test counter once per watchdog interval and
* store the interval for the offset calculation of this step. This
* guarantees a consistent behaviour even if the other side needs
* to repeat due to a watchdog read timeout.
*/
if (intv > (NSEC_PER_SEC / 4)) {
WRITE_ONCE(wdtest_test_count, wdtest_test_count + 1);
wdtest_last_ts = now;
wdtest_offset = intv;
}
switch (wdtest_state & ~WDTEST_INJECT_PERCPU) {
case WDTEST_INJECT_POSITIVE:
return now + wdtest_get_offset(cs);
case WDTEST_INJECT_NEGATIVE:
return now - wdtest_get_offset(cs);
case WDTEST_INJECT_DELAY:
udelay(500);
return now;
default:
return now;
}
}
#define KTIME_FLAGS (CLOCK_SOURCE_IS_CONTINUOUS | \
CLOCK_SOURCE_CALIBRATED | \
CLOCK_SOURCE_MUST_VERIFY | \
CLOCK_SOURCE_WDTEST)
static struct clocksource clocksource_wdtest_ktime = {
.name = "wdtest-ktime",
.rating = 10,
.read = wdtest_ktime_read,
.mask = CLOCKSOURCE_MASK(64),
.flags = KTIME_FLAGS,
.list = LIST_HEAD_INIT(clocksource_wdtest_ktime.list),
};
static void wdtest_clocksource_reset(enum wdtest_states which, bool percpu)
{
clocksource_unregister(&clocksource_wdtest_ktime);
pr_info("Test: State %d percpu %d\n", which, percpu);
wdtest_state = which;
if (percpu)
wdtest_state |= WDTEST_INJECT_PERCPU;
wdtest_test_count = 0;
wdtest_last_ts = 0;
clocksource_wdtest_ktime.rating = 10;
clocksource_wdtest_ktime.flags = KTIME_FLAGS;
if (percpu)
clocksource_wdtest_ktime.flags |= CLOCK_SOURCE_WDTEST_PERCPU;
clocksource_register_khz(&clocksource_wdtest_ktime, 1000 * 1000);
}
static bool wdtest_execute(enum wdtest_states which, bool percpu, unsigned int expect,
unsigned long calls)
{
wdtest_clocksource_reset(which, percpu);
for (; READ_ONCE(wdtest_test_count) < calls; msleep(100)) {
unsigned int flags = READ_ONCE(clocksource_wdtest_ktime.flags);
if (kthread_should_stop())
return false;
if (flags & CLOCK_SOURCE_UNSTABLE) {
if (expect & CLOCK_SOURCE_UNSTABLE)
return true;
pr_warn("Fail: Unexpected unstable\n");
return false;
}
if (flags & CLOCK_SOURCE_VALID_FOR_HRES) {
if (expect & CLOCK_SOURCE_VALID_FOR_HRES)
return true;
pr_warn("Fail: Unexpected valid for highres\n");
return false;
}
}
if (!expect)
return true;
pr_warn("Fail: Timed out\n");
return false;
}
static bool wdtest_run(bool percpu)
{
if (!wdtest_execute(WDTEST_INJECT_NONE, percpu, CLOCK_SOURCE_VALID_FOR_HRES, 8))
return false;
if (!wdtest_execute(WDTEST_INJECT_DELAY, percpu, 0, 4))
return false;
if (!wdtest_execute(WDTEST_INJECT_POSITIVE, percpu, CLOCK_SOURCE_UNSTABLE, 8))
return false;
if (!wdtest_execute(WDTEST_INJECT_NEGATIVE, percpu, CLOCK_SOURCE_UNSTABLE, 8))
return false;
return true;
}
static int wdtest_func(void *arg)
{
clocksource_register_khz(&clocksource_wdtest_ktime, 1000 * 1000);
if (wdtest_run(false)) {
if (wdtest_run(true))
pr_info("Success: All tests passed\n");
}
clocksource_unregister(&clocksource_wdtest_ktime);
if (!IS_MODULE(CONFIG_TEST_CLOCKSOURCE_WATCHDOG))
return 0;
while (!kthread_should_stop())
schedule_timeout_interruptible(3600 * HZ);
return 0;
}
static struct task_struct *wdtest_thread;
static int __init clocksource_wdtest_init(void)
{
struct task_struct *t = kthread_run(wdtest_func, NULL, "wdtest");
if (IS_ERR(t)) {
pr_warn("Failed to create wdtest kthread.\n");
return PTR_ERR(t);
}
wdtest_thread = t;
return 0;
}
module_init(clocksource_wdtest_init);
static void clocksource_wdtest_cleanup(void)
{
if (wdtest_thread)
kthread_stop(wdtest_thread);
}
module_exit(clocksource_wdtest_cleanup);