198 lines · 4.9 KB · C
RawDownload
// 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);