| /* | |
| * kernel/power/wakeup_reason.c | |
| * | |
| * Logs the reasons which caused the kernel to resume from | |
| * the suspend mode. | |
| * | |
| * Copyright (C) 2020 Google, Inc. | |
| * This software is licensed under the terms of the GNU General Public | |
| * License version 2, as published by the Free Software Foundation, and | |
| * may be copied, distributed, and modified under those terms. | |
| * | |
| * This program is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU General Public License for more details. | |
| */ | |
| #include <linux/wakeup_reason.h> | |
| #include <linux/kernel.h> | |
| #include <linux/irq.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/io.h> | |
| #include <linux/kobject.h> | |
| #include <linux/sysfs.h> | |
| #include <linux/init.h> | |
| #include <linux/spinlock.h> | |
| #include <linux/notifier.h> | |
| #include <linux/suspend.h> | |
| #include <linux/slab.h> | |
| /* | |
| * struct wakeup_irq_node - stores data and relationships for IRQs logged as | |
| * either base or nested wakeup reasons during suspend/resume flow. | |
| * @siblings - for membership on leaf or parent IRQ lists | |
| * @irq - the IRQ number | |
| * @irq_name - the name associated with the IRQ, or a default if none | |
| */ | |
| struct wakeup_irq_node { | |
| struct list_head siblings; | |
| int irq; | |
| const char *irq_name; | |
| }; | |
| enum wakeup_reason_flag { | |
| RESUME_NONE = 0, | |
| RESUME_IRQ, | |
| RESUME_ABORT, | |
| RESUME_ABNORMAL, | |
| }; | |
| static DEFINE_SPINLOCK(wakeup_reason_lock); | |
| static LIST_HEAD(leaf_irqs); /* kept in ascending IRQ sorted order */ | |
| static LIST_HEAD(parent_irqs); /* unordered */ | |
| static struct kmem_cache *wakeup_irq_nodes_cache; | |
| static const char *default_irq_name = "(unnamed)"; | |
| static struct kobject *kobj; | |
| static bool capture_reasons; | |
| static int wakeup_reason; | |
| static char non_irq_wake_reason[MAX_SUSPEND_ABORT_LEN]; | |
| static ktime_t last_monotime; /* monotonic time before last suspend */ | |
| static ktime_t curr_monotime; /* monotonic time after last suspend */ | |
| static ktime_t last_stime; /* monotonic boottime offset before last suspend */ | |
| static ktime_t curr_stime; /* monotonic boottime offset after last suspend */ | |
| static void init_node(struct wakeup_irq_node *p, int irq) | |
| { | |
| struct irq_desc *desc; | |
| INIT_LIST_HEAD(&p->siblings); | |
| p->irq = irq; | |
| desc = irq_to_desc(irq); | |
| if (desc && desc->action && desc->action->name) | |
| p->irq_name = desc->action->name; | |
| else | |
| p->irq_name = default_irq_name; | |
| } | |
| static struct wakeup_irq_node *create_node(int irq) | |
| { | |
| struct wakeup_irq_node *result; | |
| result = kmem_cache_alloc(wakeup_irq_nodes_cache, GFP_ATOMIC); | |
| if (unlikely(!result)) | |
| pr_warn("Failed to log wakeup IRQ %d\n", irq); | |
| else | |
| init_node(result, irq); | |
| return result; | |
| } | |
| static void delete_list(struct list_head *head) | |
| { | |
| struct wakeup_irq_node *n; | |
| while (!list_empty(head)) { | |
| n = list_first_entry(head, struct wakeup_irq_node, siblings); | |
| list_del(&n->siblings); | |
| kmem_cache_free(wakeup_irq_nodes_cache, n); | |
| } | |
| } | |
| static bool add_sibling_node_sorted(struct list_head *head, int irq) | |
| { | |
| struct wakeup_irq_node *n = NULL; | |
| struct list_head *predecessor = head; | |
| if (unlikely(WARN_ON(!head))) | |
| return NULL; | |
| if (!list_empty(head)) | |
| list_for_each_entry(n, head, siblings) { | |
| if (n->irq < irq) | |
| predecessor = &n->siblings; | |
| else if (n->irq == irq) | |
| return true; | |
| else | |
| break; | |
| } | |
| n = create_node(irq); | |
| if (n) { | |
| list_add(&n->siblings, predecessor); | |
| return true; | |
| } | |
| return false; | |
| } | |
| static struct wakeup_irq_node *find_node_in_list(struct list_head *head, | |
| int irq) | |
| { | |
| struct wakeup_irq_node *n; | |
| if (unlikely(WARN_ON(!head))) | |
| return NULL; | |
| list_for_each_entry(n, head, siblings) | |
| if (n->irq == irq) | |
| return n; | |
| return NULL; | |
| } | |
| void log_irq_wakeup_reason(int irq) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&wakeup_reason_lock, flags); | |
| if (wakeup_reason == RESUME_ABNORMAL || wakeup_reason == RESUME_ABORT) { | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return; | |
| } | |
| if (!capture_reasons) { | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return; | |
| } | |
| if (find_node_in_list(&parent_irqs, irq) == NULL) | |
| add_sibling_node_sorted(&leaf_irqs, irq); | |
| wakeup_reason = RESUME_IRQ; | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| } | |
| void log_threaded_irq_wakeup_reason(int irq, int parent_irq) | |
| { | |
| struct wakeup_irq_node *parent; | |
| unsigned long flags; | |
| /* | |
| * Intentionally unsynchronized. Calls that come in after we have | |
| * resumed should have a fast exit path since there's no work to be | |
| * done, any any coherence issue that could cause a wrong value here is | |
| * both highly improbable - given the set/clear timing - and very low | |
| * impact (parent IRQ gets logged instead of the specific child). | |
| */ | |
| if (!capture_reasons) | |
| return; | |
| spin_lock_irqsave(&wakeup_reason_lock, flags); | |
| if (wakeup_reason == RESUME_ABNORMAL || wakeup_reason == RESUME_ABORT) { | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return; | |
| } | |
| if (!capture_reasons || (find_node_in_list(&leaf_irqs, irq) != NULL)) { | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return; | |
| } | |
| parent = find_node_in_list(&parent_irqs, parent_irq); | |
| if (parent != NULL) | |
| add_sibling_node_sorted(&leaf_irqs, irq); | |
| else { | |
| parent = find_node_in_list(&leaf_irqs, parent_irq); | |
| if (parent != NULL) { | |
| list_del_init(&parent->siblings); | |
| list_add_tail(&parent->siblings, &parent_irqs); | |
| add_sibling_node_sorted(&leaf_irqs, irq); | |
| } | |
| } | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| } | |
| EXPORT_SYMBOL_GPL(log_threaded_irq_wakeup_reason); | |
| static void __log_abort_or_abnormal_wake(bool abort, const char *fmt, | |
| va_list args) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&wakeup_reason_lock, flags); | |
| /* Suspend abort or abnormal wake reason has already been logged. */ | |
| if (wakeup_reason != RESUME_NONE) { | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return; | |
| } | |
| if (abort) | |
| wakeup_reason = RESUME_ABORT; | |
| else | |
| wakeup_reason = RESUME_ABNORMAL; | |
| vsnprintf(non_irq_wake_reason, MAX_SUSPEND_ABORT_LEN, fmt, args); | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| } | |
| void log_suspend_abort_reason(const char *fmt, ...) | |
| { | |
| va_list args; | |
| va_start(args, fmt); | |
| __log_abort_or_abnormal_wake(true, fmt, args); | |
| va_end(args); | |
| } | |
| EXPORT_SYMBOL_GPL(log_suspend_abort_reason); | |
| void log_abnormal_wakeup_reason(const char *fmt, ...) | |
| { | |
| va_list args; | |
| va_start(args, fmt); | |
| __log_abort_or_abnormal_wake(false, fmt, args); | |
| va_end(args); | |
| } | |
| EXPORT_SYMBOL_GPL(log_abnormal_wakeup_reason); | |
| void clear_wakeup_reasons(void) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&wakeup_reason_lock, flags); | |
| delete_list(&leaf_irqs); | |
| delete_list(&parent_irqs); | |
| wakeup_reason = RESUME_NONE; | |
| capture_reasons = true; | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| } | |
| static void print_wakeup_sources(void) | |
| { | |
| struct wakeup_irq_node *n; | |
| unsigned long flags; | |
| spin_lock_irqsave(&wakeup_reason_lock, flags); | |
| capture_reasons = false; | |
| if (wakeup_reason == RESUME_ABORT) { | |
| pr_info("Abort: %s\n", non_irq_wake_reason); | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return; | |
| } | |
| if (wakeup_reason == RESUME_IRQ && !list_empty(&leaf_irqs)) | |
| list_for_each_entry(n, &leaf_irqs, siblings) | |
| pr_info("Resume caused by IRQ %d, %s\n", n->irq, | |
| n->irq_name); | |
| else if (wakeup_reason == RESUME_ABNORMAL) | |
| pr_info("Resume caused by %s\n", non_irq_wake_reason); | |
| else | |
| pr_info("Resume cause unknown\n"); | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| } | |
| static ssize_t last_resume_reason_show(struct kobject *kobj, | |
| struct kobj_attribute *attr, char *buf) | |
| { | |
| ssize_t buf_offset = 0; | |
| struct wakeup_irq_node *n; | |
| unsigned long flags; | |
| spin_lock_irqsave(&wakeup_reason_lock, flags); | |
| if (wakeup_reason == RESUME_ABORT) { | |
| buf_offset = scnprintf(buf, PAGE_SIZE, "Abort: %s", | |
| non_irq_wake_reason); | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return buf_offset; | |
| } | |
| if (wakeup_reason == RESUME_IRQ && !list_empty(&leaf_irqs)) | |
| list_for_each_entry(n, &leaf_irqs, siblings) | |
| buf_offset += scnprintf(buf + buf_offset, | |
| PAGE_SIZE - buf_offset, | |
| "%d %s\n", n->irq, n->irq_name); | |
| else if (wakeup_reason == RESUME_ABNORMAL) | |
| buf_offset = scnprintf(buf, PAGE_SIZE, "-1 %s", | |
| non_irq_wake_reason); | |
| spin_unlock_irqrestore(&wakeup_reason_lock, flags); | |
| return buf_offset; | |
| } | |
| static ssize_t last_suspend_time_show(struct kobject *kobj, | |
| struct kobj_attribute *attr, char *buf) | |
| { | |
| struct timespec64 sleep_time; | |
| struct timespec64 total_time; | |
| struct timespec64 suspend_resume_time; | |
| /* | |
| * total_time is calculated from monotonic bootoffsets because | |
| * unlike CLOCK_MONOTONIC it include the time spent in suspend state. | |
| */ | |
| total_time = ktime_to_timespec64(ktime_sub(curr_stime, last_stime)); | |
| /* | |
| * suspend_resume_time is calculated as monotonic (CLOCK_MONOTONIC) | |
| * time interval before entering suspend and post suspend. | |
| */ | |
| suspend_resume_time = | |
| ktime_to_timespec64(ktime_sub(curr_monotime, last_monotime)); | |
| /* sleep_time = total_time - suspend_resume_time */ | |
| sleep_time = timespec64_sub(total_time, suspend_resume_time); | |
| /* Export suspend_resume_time and sleep_time in pair here. */ | |
| return sprintf(buf, "%llu.%09lu %llu.%09lu\n", | |
| (unsigned long long)suspend_resume_time.tv_sec, | |
| suspend_resume_time.tv_nsec, | |
| (unsigned long long)sleep_time.tv_sec, | |
| sleep_time.tv_nsec); | |
| } | |
| static struct kobj_attribute resume_reason = __ATTR_RO(last_resume_reason); | |
| static struct kobj_attribute suspend_time = __ATTR_RO(last_suspend_time); | |
| static struct attribute *attrs[] = { | |
| &resume_reason.attr, | |
| &suspend_time.attr, | |
| NULL, | |
| }; | |
| static struct attribute_group attr_group = { | |
| .attrs = attrs, | |
| }; | |
| /* Detects a suspend and clears all the previous wake up reasons*/ | |
| static int wakeup_reason_pm_event(struct notifier_block *notifier, | |
| unsigned long pm_event, void *unused) | |
| { | |
| switch (pm_event) { | |
| case PM_SUSPEND_PREPARE: | |
| /* monotonic time since boot */ | |
| last_monotime = ktime_get(); | |
| /* monotonic time since boot including the time spent in suspend */ | |
| last_stime = ktime_get_boottime(); | |
| clear_wakeup_reasons(); | |
| break; | |
| case PM_POST_SUSPEND: | |
| /* monotonic time since boot */ | |
| curr_monotime = ktime_get(); | |
| /* monotonic time since boot including the time spent in suspend */ | |
| curr_stime = ktime_get_boottime(); | |
| print_wakeup_sources(); | |
| break; | |
| default: | |
| break; | |
| } | |
| return NOTIFY_DONE; | |
| } | |
| static struct notifier_block wakeup_reason_pm_notifier_block = { | |
| .notifier_call = wakeup_reason_pm_event, | |
| }; | |
| static int __init wakeup_reason_init(void) | |
| { | |
| if (register_pm_notifier(&wakeup_reason_pm_notifier_block)) { | |
| pr_warn("[%s] failed to register PM notifier\n", __func__); | |
| goto fail; | |
| } | |
| kobj = kobject_create_and_add("wakeup_reasons", kernel_kobj); | |
| if (!kobj) { | |
| pr_warn("[%s] failed to create a sysfs kobject\n", __func__); | |
| goto fail_unregister_pm_notifier; | |
| } | |
| if (sysfs_create_group(kobj, &attr_group)) { | |
| pr_warn("[%s] failed to create a sysfs group\n", __func__); | |
| goto fail_kobject_put; | |
| } | |
| wakeup_irq_nodes_cache = | |
| kmem_cache_create("wakeup_irq_node_cache", | |
| sizeof(struct wakeup_irq_node), 0, 0, NULL); | |
| if (!wakeup_irq_nodes_cache) | |
| goto fail_remove_group; | |
| return 0; | |
| fail_remove_group: | |
| sysfs_remove_group(kobj, &attr_group); | |
| fail_kobject_put: | |
| kobject_put(kobj); | |
| fail_unregister_pm_notifier: | |
| unregister_pm_notifier(&wakeup_reason_pm_notifier_block); | |
| fail: | |
| return 1; | |
| } | |
| late_initcall(wakeup_reason_init); |