linux/arch/s390/kernel/wti.c

// SPDX-License-Identifier: GPL-2.0
/*
 * Support for warning track interruption
 *
 * Copyright IBM Corp. 2023
 */

#include <linux/cpu.h>
#include <linux/debugfs.h>
#include <linux/kallsyms.h>
#include <linux/smpboot.h>
#include <linux/irq.h>
#include <uapi/linux/sched/types.h>
#include <asm/debug.h>
#include <asm/diag.h>
#include <asm/sclp.h>

#define WTI_DBF_LEN 64

struct wti_debug {
	unsigned long	missed;
	unsigned long	addr;
	pid_t		pid;
};

struct wti_state {
	/* debug data for s390dbf */
	struct wti_debug	dbg;
	/*
	 * Represents the real-time thread responsible to
	 * acknowledge the warning-track interrupt and trigger
	 * preliminary and postliminary precautions.
	 */
	struct task_struct	*thread;
	/*
	 * If pending is true, the real-time thread must be scheduled.
	 * If not, a wake up of that thread will remain a noop.
	 */
	bool			pending;
};

static DEFINE_PER_CPU(struct wti_state, wti_state);

static debug_info_t *wti_dbg;

/*
 * During a warning-track grace period, interrupts are disabled
 * to prevent delays of the warning-track acknowledgment.
 *
 * Once the CPU is physically dispatched again, interrupts are
 * re-enabled.
 */

static void wti_irq_disable(void)
{
	unsigned long flags;
	struct ctlreg cr6;

	local_irq_save(flags);
	local_ctl_store(6, &cr6);
	/* disable all I/O interrupts */
	cr6.val &= ~0xff000000UL;
	local_ctl_load(6, &cr6);
	local_irq_restore(flags);
}

static void wti_irq_enable(void)
{
	unsigned long flags;
	struct ctlreg cr6;

	local_irq_save(flags);
	local_ctl_store(6, &cr6);
	/* enable all I/O interrupts */
	cr6.val |= 0xff000000UL;
	local_ctl_load(6, &cr6);
	local_irq_restore(flags);
}

static void store_debug_data(struct wti_state *st)
{
	struct pt_regs *regs = get_irq_regs();

	st->dbg.pid = current->pid;
	st->dbg.addr = 0;
	if (!user_mode(regs))
		st->dbg.addr = regs->psw.addr;
}

static void wti_interrupt(struct ext_code ext_code,
			  unsigned int param32, unsigned long param64)
{
	struct wti_state *st = this_cpu_ptr(&wti_state);

	inc_irq_stat(IRQEXT_WTI);
	wti_irq_disable();
	store_debug_data(st);
	st->pending = true;
	wake_up_process(st->thread);
}

static int wti_pending(unsigned int cpu)
{
	struct wti_state *st = per_cpu_ptr(&wti_state, cpu);

	return st->pending;
}

static void wti_dbf_grace_period(struct wti_state *st)
{
	struct wti_debug *wdi = &st->dbg;
	char buf[WTI_DBF_LEN];

	if (wdi->addr)
		snprintf(buf, sizeof(buf), "%d %pS", wdi->pid, (void *)wdi->addr);
	else
		snprintf(buf, sizeof(buf), "%d <user>", wdi->pid);
	debug_text_event(wti_dbg, 2, buf);
	wdi->missed++;
}

static int wti_show(struct seq_file *seq, void *v)
{
	struct wti_state *st;
	int cpu;

	cpus_read_lock();
	seq_puts(seq, "       ");
	for_each_online_cpu(cpu)
		seq_printf(seq, "CPU%-8d", cpu);
	seq_putc(seq, '\n');
	for_each_online_cpu(cpu) {
		st = per_cpu_ptr(&wti_state, cpu);
		seq_printf(seq, " %10lu", st->dbg.missed);
	}
	seq_putc(seq, '\n');
	cpus_read_unlock();
	return 0;
}
DEFINE_SHOW_ATTRIBUTE(wti);

static void wti_thread_fn(unsigned int cpu)
{
	struct wti_state *st = per_cpu_ptr(&wti_state, cpu);

	st->pending = false;
	/*
	 * Yield CPU voluntarily to the hypervisor. Control
	 * resumes when hypervisor decides to dispatch CPU
	 * to this LPAR again.
	 */
	if (diag49c(DIAG49C_SUBC_ACK))
		wti_dbf_grace_period(st);
	wti_irq_enable();
}

static struct smp_hotplug_thread wti_threads = {
	.store			= &wti_state.thread,
	.thread_should_run	= wti_pending,
	.thread_fn		= wti_thread_fn,
	.thread_comm		= "cpuwti/%u",
	.selfparking		= false,
};

static int __init wti_init(void)
{
	struct sched_param wti_sched_param = { .sched_priority = MAX_RT_PRIO - 1 };
	struct dentry *wti_dir;
	struct wti_state *st;
	int cpu, rc;

	rc = -EOPNOTSUPP;
	if (!sclp.has_wti)
		goto out;
	rc = smpboot_register_percpu_thread(&wti_threads);
	if (WARN_ON(rc))
		goto out;
	for_each_online_cpu(cpu) {
		st = per_cpu_ptr(&wti_state, cpu);
		sched_setscheduler(st->thread, SCHED_FIFO, &wti_sched_param);
	}
	rc = register_external_irq(EXT_IRQ_WARNING_TRACK, wti_interrupt);
	if (rc) {
		pr_warn("Couldn't request external interrupt 0x1007\n");
		goto out_thread;
	}
	irq_subclass_register(IRQ_SUBCLASS_WARNING_TRACK);
	rc = diag49c(DIAG49C_SUBC_REG);
	if (rc) {
		pr_warn("Failed to register warning track interrupt through DIAG 49C\n");
		rc = -EOPNOTSUPP;
		goto out_subclass;
	}
	wti_dir = debugfs_create_dir("wti", arch_debugfs_dir);
	debugfs_create_file("stat", 0400, wti_dir, NULL, &wti_fops);
	wti_dbg = debug_register("wti", 1, 1, WTI_DBF_LEN);
	if (!wti_dbg) {
		rc = -ENOMEM;
		goto out_debug_register;
	}
	rc = debug_register_view(wti_dbg, &debug_hex_ascii_view);
	if (rc)
		goto out_debug_register;
	goto out;
out_debug_register:
	debug_unregister(wti_dbg);
out_subclass:
	irq_subclass_unregister(IRQ_SUBCLASS_WARNING_TRACK);
	unregister_external_irq(EXT_IRQ_WARNING_TRACK, wti_interrupt);
out_thread:
	smpboot_unregister_percpu_thread(&wti_threads);
out:
	return rc;
}
late_initcall(wti_init);