| // 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); |