507 lines
14 KiB
C
Executable file
507 lines
14 KiB
C
Executable file
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (c) 2014-2020 Samsung Electronics Co., Ltd.
|
|
* http://www.samsung.com
|
|
*
|
|
* sec_debug_hardlockup_info.c
|
|
*/
|
|
#include <linux/slab.h>
|
|
#include <linux/module.h>
|
|
#include <linux/sched/clock.h>
|
|
#include "sec_debug_internal.h"
|
|
#include <soc/samsung/debug-snapshot-log.h>
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_DEBUG_EHLD_INFO)
|
|
#include <soc/samsung/exynos-ehld.h>
|
|
#endif
|
|
#include <linux/hashtable.h>
|
|
#include <linux/kallsyms.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/irqdesc.h>
|
|
#include "../../pinctrl/samsung/pinctrl-samsung.h"
|
|
#include "../../pinctrl/samsung/pinctrl-exynos.h"
|
|
|
|
#define PRINT_LINE_MAX 512
|
|
#define TASK_COMM_LEN 16
|
|
#define BUSY_IRQ_SET_HASH_BITS 4
|
|
#define MAX_PR_AUTO 6
|
|
#define GPIO_NAME_LEN 10
|
|
|
|
#define init_vars(item, domain, start, len, max) \
|
|
do { \
|
|
struct item##_log *item; \
|
|
start = dss_get_first_##item##_log_idx(domain); \
|
|
item = dss_get_##item##_log_by_idx(domain, start); \
|
|
if (start == 0 && item->time == 0) { \
|
|
len = max = 0; \
|
|
} else if (item->time == 0) { \
|
|
start = 0; \
|
|
len = dss_get_last_##item##_log_idx(domain) + 1; \
|
|
max = dss_get_len_##item##_log_by_cpu(domain); \
|
|
} else { \
|
|
max = len = dss_get_len_##item##_log_by_cpu(domain); \
|
|
} \
|
|
} while (0)
|
|
|
|
enum hardlockup_type {
|
|
HL_TASK_STUCK = 1,
|
|
HL_IRQ_STUCK,
|
|
HL_IDLE_STUCK,
|
|
HL_SMC_CALL_STUCK,
|
|
HL_IRQ_STORM,
|
|
HL_HRTIMER_ERROR,
|
|
HL_UNKNOWN_STUCK
|
|
};
|
|
|
|
struct task_info {
|
|
char task_comm[TASK_COMM_LEN];
|
|
char group_leader[TASK_COMM_LEN];
|
|
};
|
|
|
|
struct cpuidle_info {
|
|
char *mode;
|
|
};
|
|
|
|
struct smc_info {
|
|
int cmd;
|
|
};
|
|
|
|
struct irq_info {
|
|
int irq;
|
|
void *fn;
|
|
unsigned long long avg_period;
|
|
};
|
|
|
|
struct hardlockup_info {
|
|
enum hardlockup_type hl_type;
|
|
unsigned long long time;
|
|
unsigned long long delay_time;
|
|
union {
|
|
struct task_info task_info;
|
|
struct cpuidle_info cpuidle_info;
|
|
struct smc_info smc_info;
|
|
struct irq_info irq_info;
|
|
};
|
|
unsigned int ehld_type;
|
|
};
|
|
|
|
struct busy_irq {
|
|
int irq;
|
|
unsigned int occurrences;
|
|
void *fn;
|
|
unsigned long long total_duration;
|
|
unsigned long long last_time;
|
|
struct hlist_node hlist;
|
|
};
|
|
|
|
static DEFINE_PER_CPU(struct hardlockup_info, percpu_hl_info);
|
|
static DEFINE_HASHTABLE(busy_irq_hash, BUSY_IRQ_SET_HASH_BITS);
|
|
static const char * const hl_to_name[] = {
|
|
"NONE", "TASK STUCK", "IRQ STUCK",
|
|
"IDLE STUCK", "SMCCALL STUCK", "IRQ STORM",
|
|
"HRTIMER ERROR", "UNKNOWN STUCK"
|
|
};
|
|
static char dss_freq_name[DSS_DOMAIN_NUM][SZ_8] = {
|
|
"LIT", "MID", "BIG", "INT", "MIF", "CAM",
|
|
"DISP", "INTCAM", "AUD", "MFC0", "NPU", "DSU",
|
|
"DNC", "CSIS", "ISP", "MFC1", "DSP", "ALIVE",
|
|
"CHUB", "VTS", "HSI0", "G3D"
|
|
};
|
|
|
|
extern struct atomic_notifier_head hardlockup_notifier_list;
|
|
extern unsigned long hardlockup_watchdog_get_thresh(void);
|
|
extern u64 hardlockup_watchdog_get_period(void);
|
|
|
|
static void secdbg_get_busiest_irq(struct hardlockup_info *hl_info, int cpu)
|
|
{
|
|
int start, len, max;
|
|
struct irq_log *irq;
|
|
struct busy_irq *b_irq;
|
|
struct busy_irq *busiest_irq = NULL;
|
|
int i, alloc;
|
|
|
|
init_vars(irq, cpu, start, len, max);
|
|
|
|
for_each_item_in_dss_by_cpu(irq, cpu, start, len, true) {
|
|
if (!irq || irq->time == 0)
|
|
break;
|
|
|
|
if (irq->en == DSS_FLAG_OUT)
|
|
continue;
|
|
|
|
alloc = 1;
|
|
|
|
hash_for_each_possible(busy_irq_hash, b_irq, hlist, irq->irq) {
|
|
if (b_irq->irq == irq->irq) {
|
|
b_irq->total_duration += (irq->time - b_irq->last_time);
|
|
b_irq->last_time = irq->time;
|
|
b_irq->occurrences++;
|
|
alloc = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (alloc) {
|
|
b_irq = kzalloc(sizeof(*b_irq), GFP_ATOMIC);
|
|
|
|
if (!b_irq)
|
|
break;
|
|
|
|
b_irq->irq = irq->irq;
|
|
b_irq->fn = irq->fn;
|
|
b_irq->occurrences = 0;
|
|
b_irq->total_duration = 0;
|
|
b_irq->last_time = irq->time;
|
|
hash_add(busy_irq_hash, &b_irq->hlist, irq->irq);
|
|
}
|
|
}
|
|
|
|
hash_for_each(busy_irq_hash, i, b_irq, hlist) {
|
|
if (!busiest_irq)
|
|
busiest_irq = b_irq;
|
|
else if (busiest_irq->occurrences < b_irq->occurrences)
|
|
busiest_irq = b_irq;
|
|
}
|
|
|
|
hl_info->irq_info.irq = busiest_irq->irq;
|
|
hl_info->irq_info.fn = busiest_irq->fn;
|
|
hl_info->irq_info.avg_period = (busiest_irq->occurrences == 0) ?
|
|
0 : busiest_irq->total_duration / busiest_irq->occurrences;
|
|
}
|
|
|
|
static int secdbg_hardlockup_get_info(unsigned int cpu, struct hardlockup_info *hl_info)
|
|
{
|
|
unsigned long long curr_time;
|
|
unsigned long long thresh = hardlockup_watchdog_get_thresh() * NSEC_PER_SEC - hardlockup_watchdog_get_period();
|
|
unsigned long long cpuidle_delay_time, irq_delay_time, task_delay_time;
|
|
struct cpuidle_log *last_cpuidle;
|
|
struct irq_log *last_irq;
|
|
struct task_log *last_task;
|
|
|
|
curr_time = local_clock();
|
|
last_cpuidle = dss_get_last_cpuidle_log(cpu);
|
|
|
|
if (last_cpuidle) {
|
|
cpuidle_delay_time = (curr_time > last_cpuidle->time) ? curr_time - last_cpuidle->time : 0;
|
|
|
|
if (last_cpuidle->en == DSS_FLAG_IN &&
|
|
cpuidle_delay_time > thresh) {
|
|
hl_info->time = last_cpuidle->time;
|
|
hl_info->delay_time = cpuidle_delay_time;
|
|
hl_info->cpuidle_info.mode = last_cpuidle->modes;
|
|
hl_info->hl_type = HL_IDLE_STUCK;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
last_irq = dss_get_last_irq_log(cpu);
|
|
|
|
if (!last_irq)
|
|
return -ENOENT;
|
|
|
|
irq_delay_time = (curr_time > last_irq->time) ? curr_time - last_irq->time : 0;
|
|
|
|
if (last_irq->en == DSS_FLAG_IN &&
|
|
irq_delay_time > thresh) {
|
|
hl_info->time = last_irq->time;
|
|
hl_info->delay_time = irq_delay_time;
|
|
hl_info->irq_info.irq = last_irq->irq;
|
|
hl_info->irq_info.fn = last_irq->fn;
|
|
hl_info->hl_type = HL_IRQ_STUCK;
|
|
return 0;
|
|
}
|
|
|
|
last_task = dss_get_last_task_log(cpu);
|
|
|
|
if (!last_task)
|
|
return -ENOENT;
|
|
|
|
task_delay_time = (curr_time > last_task->time) ? curr_time - last_task->time : 0;
|
|
|
|
if (last_task->time < curr_time &&
|
|
task_delay_time > thresh) {
|
|
hl_info->time = last_task->time;
|
|
hl_info->delay_time = task_delay_time;
|
|
|
|
if (irq_delay_time > thresh) {
|
|
strncpy(hl_info->task_info.task_comm,
|
|
last_task->task_comm,
|
|
TASK_COMM_LEN - 1);
|
|
hl_info->task_info.task_comm[TASK_COMM_LEN - 1] = '\0';
|
|
strncpy(hl_info->task_info.group_leader,
|
|
last_task->task->group_leader->comm,
|
|
TASK_COMM_LEN - 1);
|
|
hl_info->task_info.group_leader[TASK_COMM_LEN - 1] = '\0';
|
|
hl_info->hl_type = HL_TASK_STUCK;
|
|
} else {
|
|
secdbg_get_busiest_irq(hl_info, cpu);
|
|
hl_info->hl_type = HL_IRQ_STORM;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
hl_info->hl_type = HL_UNKNOWN_STUCK;
|
|
return 0;
|
|
}
|
|
|
|
static void secdbg_hardlockup_print_freq(int domain, unsigned long *freq_idx)
|
|
{
|
|
char buf[PRINT_LINE_MAX];
|
|
ssize_t offset = 0;
|
|
struct freq_log *freq;
|
|
int i;
|
|
|
|
if (!strcmp(dss_freq_name[domain], "")
|
|
|| (freq_idx[0] == ULONG_MAX && freq_idx[1] == ULONG_MAX))
|
|
return;
|
|
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, "%6s", dss_freq_name[domain]);
|
|
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
if (freq_idx[i] != ULONG_MAX) {
|
|
freq = dss_get_freq_log_by_idx(domain, freq_idx[i]);
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " [%16llu] %7u -> %7u[%c]",
|
|
freq->time, freq->old_freq, freq->target_freq, (freq->en == 1) ? 'i' : 'o');
|
|
secdbg_exin_set_hardlockup_freq(dss_freq_name[domain], freq);
|
|
}
|
|
}
|
|
|
|
if (domain < MAX_PR_AUTO)
|
|
pr_auto(ASL3, "%s\n", buf);
|
|
else
|
|
pr_info("%s\n", buf);
|
|
}
|
|
|
|
static void secdbg_hardlockup_show_freq(struct hardlockup_info *hl_info)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < DSS_DOMAIN_NUM; i++) {
|
|
unsigned long start, len, max;
|
|
unsigned long freq_idx[2] = {ULONG_MAX, ULONG_MAX};
|
|
struct freq_log *freq;
|
|
|
|
init_vars(freq, i, start, len, max);
|
|
|
|
for_each_item_in_dss_by_cpu(freq, i, start, len, true) {
|
|
if (freq->time > hl_info->time) {
|
|
freq_idx[0] = (start - 1) & (max - 1);
|
|
freq_idx[1] = start & (max - 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (max && !len)
|
|
freq_idx[0] = (start - 1) & (max - 1);
|
|
|
|
secdbg_hardlockup_print_freq(i, freq_idx);
|
|
}
|
|
}
|
|
|
|
static char *secdbg_hardlockup_get_gpio_name(int irq)
|
|
{
|
|
char symname[KSYM_NAME_LEN];
|
|
struct irq_desc *desc = irq_to_desc(irq);
|
|
|
|
snprintf(symname, KSYM_NAME_LEN, "%ps", desc->handle_irq);
|
|
|
|
if (strstr(symname, "exynos_irq_eint0_15")) {
|
|
struct exynos_weint_data *eintd = irq_desc_get_handler_data(desc);
|
|
struct samsung_pin_bank *bank;
|
|
char *gpio_name = NULL;
|
|
|
|
if (!eintd)
|
|
return "None";
|
|
|
|
bank = eintd->bank;
|
|
|
|
if (!bank)
|
|
return "None";
|
|
|
|
gpio_name = kmalloc(GPIO_NAME_LEN + 1, GFP_NOWAIT | __GFP_NOWARN | __GFP_NORETRY);
|
|
|
|
if (!gpio_name)
|
|
return "None";
|
|
|
|
snprintf(gpio_name, GPIO_NAME_LEN, "%s[%d]", bank->name, eintd->irq);
|
|
|
|
return gpio_name;
|
|
}
|
|
|
|
return "None";
|
|
}
|
|
|
|
static void secdbg_hardlockup_show_info(struct hardlockup_info *hl_info)
|
|
{
|
|
char buf[PRINT_LINE_MAX];
|
|
ssize_t offset = 0;
|
|
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, "BUG: Hardlockup stuck for %lluns from %lluns [%s",
|
|
hl_info->delay_time, hl_info->time, hl_to_name[hl_info->hl_type]);
|
|
|
|
switch (hl_info->hl_type) {
|
|
case HL_TASK_STUCK:
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " task=%s[%s]]", hl_info->task_info.task_comm, hl_info->task_info.group_leader);
|
|
secdbg_exin_set_hardlockup_type("TASK_%s[%s]", hl_info->task_info.task_comm, hl_info->task_info.group_leader);
|
|
break;
|
|
case HL_IRQ_STUCK:
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " irq=%d, hwirq=%lu, %s, %ps]",
|
|
hl_info->irq_info.irq, irq_get_irq_data(hl_info->irq_info.irq)->hwirq,
|
|
secdbg_hardlockup_get_gpio_name(hl_info->irq_info.irq), hl_info->irq_info.fn);
|
|
secdbg_exin_set_hardlockup_type("IRQ_%d_%ps", hl_info->irq_info.irq, hl_info->irq_info.fn);
|
|
break;
|
|
case HL_IDLE_STUCK:
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " mode=%s]", hl_info->cpuidle_info.mode);
|
|
secdbg_exin_set_hardlockup_type("IDLE_%s", hl_info->cpuidle_info.mode);
|
|
break;
|
|
case HL_SMC_CALL_STUCK:
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " cmd=%u]", hl_info->smc_info.cmd);
|
|
secdbg_exin_set_hardlockup_type("SMC_%s", hl_info->task_info.task_comm);
|
|
break;
|
|
case HL_IRQ_STORM:
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " irq=%d, hwirq=%lu, %s, %ps, avg_period=%lluns]",
|
|
hl_info->irq_info.irq, irq_get_irq_data(hl_info->irq_info.irq)->hwirq,
|
|
secdbg_hardlockup_get_gpio_name(hl_info->irq_info.irq), hl_info->irq_info.fn, hl_info->irq_info.avg_period);
|
|
secdbg_exin_set_hardlockup_type("IRQs_%d_%s_%lluns",
|
|
hl_info->irq_info.irq, hl_info->irq_info.fn, hl_info->irq_info.avg_period);
|
|
break;
|
|
default:
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, "]");
|
|
break;
|
|
}
|
|
|
|
pr_auto(ASL3, "%s\n", buf);
|
|
secdbg_exin_set_hardlockup_data(buf);
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_DEBUG_EHLD_INFO)
|
|
static void secdbg_hardlockup_print_ehld_type(void)
|
|
{
|
|
unsigned int cpu;
|
|
struct hardlockup_info *hl_info;
|
|
char buf[PRINT_LINE_MAX];
|
|
ssize_t offset = 0;
|
|
bool is_alive = true;
|
|
const char *str[] = {"NO INSTRET", "NO INSTRUN"};
|
|
int i;
|
|
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, "EHLD: ");
|
|
for_each_possible_cpu(cpu) {
|
|
hl_info = per_cpu_ptr(&percpu_hl_info, cpu);
|
|
if (hl_info->ehld_type != 0) {
|
|
is_alive = false;
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " [C%u]", cpu);
|
|
for (i = 0; i < MAX_ETYPES; i++) {
|
|
if ((hl_info->ehld_type & (1 << i)) != 0)
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " %s", str[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (is_alive)
|
|
offset += scnprintf(buf + offset, PRINT_LINE_MAX - offset, " ALL CORE NOT DETECTED");
|
|
|
|
pr_auto(ASL1, "%s\n", buf);
|
|
secdbg_exin_set_hardlockup_ehld(hl_info->ehld_type, cpu);
|
|
}
|
|
|
|
static bool secdbg_hardlockup_check_ehld_info(unsigned int cpu, unsigned long long *time, unsigned long long *inst, unsigned long index)
|
|
{
|
|
unsigned long long last;
|
|
int count, max;
|
|
unsigned long long curr = local_clock();
|
|
unsigned long long thresh = hardlockup_watchdog_get_thresh() * NSEC_PER_SEC - hardlockup_watchdog_get_period();
|
|
|
|
if (time[index] == 0 && inst[index] == 0)
|
|
return false;
|
|
|
|
count = max = NUM_TRACE;
|
|
last = inst[index];
|
|
|
|
while (--count) {
|
|
index = (index - 1) & (max - 1);
|
|
if (last != inst[index]
|
|
|| (time[index] == 0 && inst[index] == 0))
|
|
break;
|
|
}
|
|
|
|
index = (index + 1) & (NUM_TRACE - 1);
|
|
|
|
return (curr > time[index] && (curr - time[index] >= thresh)) ? true : false;
|
|
}
|
|
|
|
static void secdbg_hardlockup_save_ehld_type(unsigned int cpu)
|
|
{
|
|
struct ehld_data *data = ehld_get_ctrl_data(cpu);
|
|
struct hardlockup_info *hl_info = per_cpu_ptr(&percpu_hl_info, cpu);
|
|
unsigned long index;
|
|
|
|
if (!data)
|
|
return;
|
|
|
|
index = data->data_ptr & (NUM_TRACE - 1);
|
|
|
|
hl_info->ehld_type = 0;
|
|
|
|
if (secdbg_hardlockup_check_ehld_info(cpu, data->time, data->instret, index))
|
|
hl_info->ehld_type |= (1 << NO_INSTRET);
|
|
}
|
|
|
|
static void secdbg_hardlockup_show_ehld_info(void)
|
|
{
|
|
unsigned int cpu;
|
|
|
|
for_each_possible_cpu(cpu) {
|
|
secdbg_hardlockup_save_ehld_type(cpu);
|
|
}
|
|
|
|
secdbg_hardlockup_print_ehld_type();
|
|
}
|
|
#endif
|
|
|
|
static int secdbg_hardlockup_info_handler(struct notifier_block *nb,
|
|
unsigned long l, void *core)
|
|
{
|
|
unsigned int *cpu = (unsigned int *)core;
|
|
struct hardlockup_info *hl_info = per_cpu_ptr(&percpu_hl_info, *cpu);
|
|
int ret;
|
|
|
|
dbg_snapshot_set_item_enable("log_kevents", false);
|
|
|
|
ret = secdbg_hardlockup_get_info(*cpu, hl_info);
|
|
|
|
if (ret)
|
|
goto out;
|
|
|
|
secdbg_hardlockup_show_info(hl_info);
|
|
secdbg_hardlockup_show_freq(hl_info);
|
|
#if IS_ENABLED(CONFIG_SEC_DEBUG_EHLD_INFO)
|
|
secdbg_hardlockup_show_ehld_info();
|
|
#endif
|
|
out:
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
static struct notifier_block secdbg_hardlockup_info_block = {
|
|
.notifier_call = secdbg_hardlockup_info_handler,
|
|
};
|
|
|
|
static int __init secdbg_hardlockup_info_init(void)
|
|
{
|
|
pr_info("%s: init\n", __func__);
|
|
|
|
atomic_notifier_chain_register(&hardlockup_notifier_list,
|
|
&secdbg_hardlockup_info_block);
|
|
|
|
return 0;
|
|
}
|
|
module_init(secdbg_hardlockup_info_init);
|
|
|
|
static void __exit secdbg_hardlockup_info_exit(void)
|
|
{
|
|
}
|
|
module_exit(secdbg_hardlockup_info_exit);
|
|
|
|
MODULE_DESCRIPTION("Samsung Debug Watchdog debug driver");
|
|
MODULE_LICENSE("GPL v2");
|