☰
Linux唤醒源框架深度解析:从数据结构到suspend交互与功耗排查
2026/10/30 1:01:20 网站建设 项目流程

1. 唤醒源框架到底在解决什么问题

第一次接触wakeup source的人,十有八九是在调一个“设备明明进了 suspend,结果几秒钟又自己醒了”的问题。你查dmesg,看到一行PM: Some devices failed to suspend,或者更隐蔽的,系统睡了又醒、醒了又睡,电池一晚上掉百分之十几。这时候老手会告诉你:去看/sys/kernel/debug/wakeup_sources。这个文件就是唤醒源框架给运维和驱动开发者留的“账本”,谁把系统叫醒了、叫了几次、当前是不是还占着唤醒锁,全在里面。

wakeup source是 Linux 内核 PM Core(电源管理核心)里一个非常关键的子系统,它的职责可以概括成一句话:在系统进入低功耗状态之前,判断有没有“合法的理由”阻止睡眠;在系统睡眠期间,记录是谁把系统唤醒的。它不是一个孤立的模块,而是和runtime PM、system suspend、autosleep、PM QoS这些机制互相咬合的。你如果只盯着wakeup source本身看,很容易觉得它就是一堆计数器和链表;但把它放到整个功耗管理链路里,它其实是“睡眠决策”的最后一道闸门。

这篇文章面向三类人:一是正在做嵌入式 Linux 功耗优化的驱动工程师,二是被“睡不下去/醒不过来”折磨的运维和系统开发者,三是想从零理解内核功耗子系统脉络的学习者。我会把wakeup source框架的来龙去脉、核心数据结构、注册与使用流程、和 suspend 的交互、以及实际排查中踩过的坑,尽量用能复现的方式讲清楚。文中涉及的内核代码路径以主线 Linux 5.x/6.x 为参考,不同版本细节会有差异,但框架思想是一致的。

先给一个直观的类比。把系统睡眠想象成一栋大楼要关灯锁门。每个设备(网卡、触摸屏、RTC、USB 控制器)都可能需要“留一盏灯”,比如网卡要能收到网络唤醒包,触摸屏要能感知用户点击。wakeup source就是每个设备手里的“留灯申请单”。大楼管理员(PM Core)在锁门前会挨个检查:还有没有有效的申请单?如果有,这次锁门就得放弃或者推迟。等大楼真的锁了,谁按了门铃(中断触发唤醒),门铃记录仪(wakeup source 的计数和 active 状态)就会记下来。这个类比不严谨,但足够帮你建立第一层认知。

2. 核心数据结构与框架设计思路

2.1 struct wakeup_source 里到底存了什么

框架的核心结构体是struct wakeup_source,定义在include/linux/pm_wakeup.h。不同内核版本字段略有增减,但关键成员长期稳定。我按“它为什么需要这个字段”的角度来拆。

struct wakeup_source { const char *name; // 唤醒源名字,debugfs 里显示的就是它 struct list_head entry; // 挂到全局 wakeup_sources 链表 struct wakeup_source *parent; // 用于 wakeup source 的层级关系 ... unsigned long active:1; // 是否处于 active(持有唤醒锁) ... ktime_t active_time; // 本次 active 开始的时间 ktime_t total_time; // 累计 active 总时长 ktime_t max_time; // 单次 active 最长时长 ktime_t last_time; // 上一次变为 inactive 的时间 unsigned long event_count; // 唤醒事件计数 unsigned long active_count; // 进入 active 的次数 unsigned long wakeup_count; // 实际导致系统唤醒的次数 unsigned long expire_count; // 超时未释放的次数 ... };

name是排查时第一眼要看的东西,它决定了/sys/kernel/debug/wakeup_sources里那一行的标识。active是布尔量,表示当前这个源是否阻止系统睡眠。event_count和wakeup_count的区别很多人搞混:event_count是“报告唤醒事件”的次数,wakeup_count是“真正把系统从 suspend 里拉出来”的次数。一个源可能报告了很多事件,但如果系统当时没在 suspend,wakeup_count就不会增加。total_time、max_time、last_time这三个时间统计是定位“谁在长时间持有唤醒锁”的关键,尤其是max_time,能帮你抓到偶发的长持有。

active_count和expire_count是判断“有没有人忘记释放”的重要指标。active_count远大于wakeup_count通常说明这个源频繁地短暂持有唤醒锁,属于正常;但如果active长期为 1,expire_count还在涨,那基本可以断定有代码路径拿了锁没放。

2.2 全局链表与锁的设计考量

所有注册的wakeup_source会挂到全局链表wakeup_sources上,由wakeup_sources_lock保护。为什么用全局链表而不是每设备私有?因为 suspend 流程需要在极短时间内遍历所有源,判断“有没有 active 的”。全局链表让这个遍历变成一次线性扫描,实现简单、缓存友好。代价是源特别多时(比如几百个)遍历会有开销,所以内核里对wakeup_sources的遍历做了优化,比如在pm_suspend路径里只关心“有没有 active”,一旦发现一个 active 就可以提前退出。

这里有个设计上的取舍值得说:wakeup_source既可以静态定义(DEFINE_WAKEUP_SOURCE),也可以动态注册(wakeup_source_register)。静态定义适合生命周期贯穿整个系统运行的源,比如main_wakeup_source;动态注册适合设备驱动,在 probe 时注册、remove 时注销。两种方式共用同一套链表和统计逻辑,保证了 debugfs 输出的一致性。

2.3 和 runtime PM、system suspend 的关系

很多人分不清wakeup source和runtime PM。简单说:runtime PM管的是“设备在系统运行期间能不能单独进入低功耗”,粒度是设备;wakeup source管的是“系统整体能不能进入 suspend”,粒度是系统。但两者会交互:一个设备如果注册了wakeup source并且处于 active,它会阻止系统 suspend;同时它自己的runtime PM状态可能仍然是 suspended,因为“能唤醒系统”和“设备自己省电”是两回事。

在system suspend流程里,wakeup source的检查发生在dpm_suspend之后、真正进入平台 suspend 之前。具体来说,suspend_devices_and_enter里会调用pm_wakeup_pending之类的接口,检查是否有 active 的唤醒源或者 pending 的唤醒事件。如果有,suspend 会被中止,返回-EBUSY。这就是为什么你有时候看到PM: Some devices failed to suspend, or early wake event detected。

3. 唤醒源的注册、使用与释放全流程

3.1 静态定义与动态注册怎么选

静态定义用宏:

static DEFINE_WAKEUP_SOURCE(my_wakeup_source, "my_device");

这种方式在编译期就分配好结构体,不需要处理注册失败,适合那些“一定存在”的源。但它的名字是固定的,debugfs 里看到的就是my_device。动态注册用:

struct wakeup_source *ws; ws = wakeup_source_register(dev, "my_device"); if (!ws) return -ENOMEM;

wakeup_source_register的第一个参数是struct device *,可以为 NULL。传了设备指针,框架会把唤醒源和设备关联起来,方便在/sys/devices/.../power/wakeup里做统一管理。动态注册的源必须在 remove 或出错路径里调用wakeup_source_unregister(ws),否则就是内存泄漏加链表残留。

我个人的选择标准:如果这个源的生命周期和某个struct device强绑定,一律用动态注册并传dev;如果是全局性的、和具体设备无关的源(比如某些架构级的唤醒事件),用静态定义更省事。

3.2 持有与释放唤醒锁的正确姿势

持有唤醒锁的接口是__pm_stay_awake和pm_stay_awake。前者不会检查ws是否为 NULL,后者会。实际驱动里更常用的是封装好的pm_wakeup_event和__pm_wakeup_event,它们在报告事件的同时会短暂持有唤醒锁,然后在一段时间后自动释放。

// 报告一个唤醒事件,并保持唤醒锁一段时间 pm_wakeup_event(dev, 100); // 100ms

这个“一段时间”很关键。如果你只是报告事件但立刻释放,系统可能在事件处理完之前就睡了,导致事件丢失。pm_wakeup_event内部会调用__pm_wakeup_event,后者把ws->event_count加一,然后调用__pm_stay_awake,再启动一个定时器在指定毫秒后调用__pm_relax。这个定时器的存在意味着:即使你的中断处理已经返回,唤醒锁还会保持一段时间,给上层处理留出窗口。

释放用__pm_relax或pm_relax。注意pm_relax会检查ws是否为 NULL,__pm_relax不会。在中断上下文里通常用带下划线的版本,因为已经确认过ws有效,省一次判断。

注意:pm_stay_awake和pm_relax必须成对出现。我见过太多“拿了忘放”的代码,表现就是系统永远进不了 suspend,或者进了也马上醒。debugfs 里active一直是 1,expire_count缓慢增长,基本就是这个问题。

3.3 唤醒事件的报告与计数逻辑

wakeup_count的增加发生在pm_wakeup_pending检查到 pending 事件时。具体路径是:设备报告唤醒事件(pm_wakeup_event),框架设置ws->event_count++并可能设置ws->active = 1;当系统尝试 suspend 时,pm_wakeup_pending遍历所有源,如果发现有event_count变化或者active为真,就返回 true,suspend 中止。此时框架会把wakeup_count加一,表示“这次唤醒确实阻止了一次 suspend”。

这个计数逻辑有个容易误解的地方:wakeup_count不是“唤醒次数”,而是“成功阻止 suspend 的次数”。如果你的系统根本没尝试 suspend,wakeup_count永远是 0,但event_count可能已经很大了。排查时如果只看wakeup_count会漏掉很多信息。

4. 与 suspend 流程的交互细节

4.1 suspend 前如何检查唤醒源

系统 suspend 的入口是pm_suspend,它会依次调用suspend_prepare、suspend_devices_and_enter。在suspend_prepare里,dpm_suspend_start会先让所有设备进入 suspend 状态。之后,suspend_devices_and_enter会调用pm_wakeup_pending检查是否有唤醒事件。这个检查的时机很讲究:必须在设备都 suspend 之后、平台真正断电之前。因为设备 suspend 过程中可能产生新的唤醒事件(比如网卡在 suspend 时收到包),这些事件必须被捕获。

pm_wakeup_pending的实现大致是遍历wakeup_sources链表,检查每个源的event_count是否和上次快照不同,或者active是否为真。如果是,返回 true,并打印PM: Wakeup pending, aborting suspend。同时,它会调用pm_print_active_wakeup_sources把当前 active 的源打印出来,这就是你在dmesg里看到的那串名字的来源。

4.2 唤醒中断如何被记录

当系统已经进入 suspend,一个中断把系统唤醒,中断处理里通常会调用pm_wakeup_event或pm_system_wakeup。pm_system_wakeup会设置一个全局标志pm_wakeup_irq,并调用pm_wakeup_pending的变体来记录。具体是哪个源被记录,取决于中断处理里传的是哪个wakeup_source。如果驱动没有正确关联,框架可能只能记录一个通用的main_wakeup_source,这时候你就很难定位具体是哪个设备。

这就是为什么驱动里报告唤醒事件时,一定要传对struct device *。框架会通过dev->power.wakeup找到对应的wakeup_source。如果dev->power.wakeup是 NULL,说明这个设备没有注册唤醒源,事件会被记到默认源上,排查时就会抓瞎。

4.3 autosleep 与 wakeup source 的配合

autosleep是让系统在没有用户活动时自动进入 suspend 的机制。它的决策依据之一就是wakeup source的状态。autosleep会周期性检查pm_wakeup_pending,如果没有 pending 事件且没有 active 的源,就触发 suspend。这里有个细节:autosleep检查的是“当前是否允许睡眠”,而不是“有没有事件”。所以一个长期 active 的源会让autosleep一直不触发,表现就是系统永远不休眠。

如果你在用autosleep,建议同时打开wakeup_sources的 debugfs 统计,观察哪些源的active长期为 1。常见的是某些传感器或网络相关的源,在特定场景下忘记释放。

5. 用 debugfs 和 sysfs 做实战排查

5.1 /sys/kernel/debug/wakeup_sources 怎么读

这个文件是排查唤醒问题的第一现场。典型输出:

name active_count event_count wakeup_count expire_count active_since total_time max_time last_change my_device 12 34 2 0 0 1200 300 5000 main_wakeup 5 5 1 0 0 800 200 6000

active_since是当前 active 持续的毫秒数,如果这个值很大且active为 1,说明有源长期持有。total_time是累计 active 时间,max_time是单次最长。last_change是距离上次状态变化的时间。排查时优先看active_since和max_time大的源。

提示:这个文件需要内核开启CONFIG_PM_DEBUG和CONFIG_PM_SLEEP_DEBUG,并且 debugfs 已挂载。生产环境如果没开,可以临时通过echo 1 > /sys/power/pm_debug_messages打开部分日志。

5.2 常见异常模式与对应原因

现象可能原因排查方向
active长期为 1拿了唤醒锁没释放检查pm_stay_awake/pm_relax配对
expire_count持续增长定时器超时后仍未释放检查pm_wakeup_event的超时设置
wakeup_count远小于event_count事件报告了但没阻止 suspend正常,说明 suspend 没被触发
suspend 立即返回-EBUSY有 active 源或 pending 事件看dmesg里打印的 active 源名字
系统睡了立刻醒唤醒中断被误触发检查wakeup_count和中断源

5.3 一个真实的排查案例

之前遇到一个项目,设备在待机时每隔几分钟就醒一次,电池掉得很快。dmesg里看到PM: Wakeup pending, aborting suspend,但没打印具体源。打开 debugfs 后发现wakeup_sources里有一个rtc相关的源,event_count每隔几分钟加一,wakeup_count也在涨。查 RTC 驱动,发现它注册了一个周期性闹钟,但驱动里没有正确区分“用户设置的闹钟”和“内部周期性唤醒”。修复方法是在 RTC 驱动里判断闹钟来源,只有用户设置的才报告唤醒事件。这个案例说明:wakeup source的统计能帮你定位到设备,但根因往往在驱动逻辑里。

6. 驱动开发中的注意事项与避坑经验

6.1 注册时机与注销顺序

动态注册的唤醒源,注册时机通常在probe的早期,注销在remove的晚期。顺序很重要:如果先注销了唤醒源,但设备还在产生中断,中断处理里调用pm_wakeup_event会找不到源,可能触发警告或空指针。正确的顺序是:先停止设备产生中断(disable_irq或关闭设备),再注销唤醒源。

另外,wakeup_source_register在probe失败路径里也要记得注销。我见过不少驱动在probe中途失败直接 return,忘了注销已经注册的源,导致链表里残留一个无效条目,debugfs 里显示一个奇怪的名字。

6.2 中断上下文里的使用限制

pm_wakeup_event可以在中断上下文调用,因为它内部用的是spin_lock_irqsave和定时器。但pm_stay_awake和pm_relax在中断上下文里要用带下划线的版本,避免不必要的 NULL 检查。更重要的是,不要在中断上下文里做可能睡眠的操作,比如mutex_lock。wakeup_source框架本身不睡眠,但如果你在报告事件后同步等待某个资源,就可能出问题。

6.3 避免“唤醒风暴”

有些设备在短时间内产生大量中断,每次都调用pm_wakeup_event,导致event_count暴涨,total_time也很大。这本身不一定有问题,但如果这些事件频繁阻止 suspend,系统就永远进不了低功耗。解决办法是在驱动里做去重或限流:比如只在状态真正变化时报告事件,或者用pm_wakeup_event的超时参数控制持有时间。

经验:pm_wakeup_event的超时时间不要设得太长。我见过设 5000ms 的,结果系统每次 suspend 都被这个源挡住。一般 100ms 到 500ms 足够覆盖大多数事件处理。具体值要根据设备的中断频率和处理延迟来调。

6.4 和 device tree 的配合

在设备树里,wakeup-source属性可以让框架自动为设备注册唤醒源。但自动注册的源名字通常是设备节点名,统计信息可能不够直观。如果设备有多个唤醒路径,建议还是手动注册并起一个有意义的名字。另外,wakeup-source属性只影响device_init_wakeup的行为,不会自动调用pm_wakeup_event,事件报告还是要在驱动里做。

7. 从框架视角看功耗优化的整体思路

wakeup source框架的价值不只是“记录谁醒了”,它其实是功耗优化的一个抓手。通过分析wakeup_sources的统计,你可以回答几个关键问题:系统为什么进不了 suspend?是谁在频繁唤醒?哪个源持有时间最长?这些问题的答案直接指向优化点。

比如,如果发现某个传感器的active_count很高但wakeup_count很低,说明它频繁持有唤醒锁但很少真正唤醒系统。可以考虑降低它的报告频率,或者用runtime PM让它自己先进入低功耗。如果发现某个源的max_time特别大,说明有一次事件处理卡了很久,需要查那个时间点的驱动日志。

我个人的习惯是:在功耗调试阶段,每天收一次wakeup_sources的快照,对比event_count和total_time的变化。连续几天观察下来,哪些源是“惯犯”一目了然。这个方法比单次抓取有效得多,因为很多问题是偶发的,单次快照可能刚好错过。

最后分享一个小技巧:如果dmesg里 suspend 中止但没打印具体源,可以临时把pm_debug_messages打开,或者在内核启动参数里加pm_debug_messages。这样pm_wakeup_pending会把所有 active 的源都打印出来,包括那些名字很长的。另外,/sys/power/wakeup_count这个接口可以让你在用户空间参与唤醒决策:先读一个值,处理完事情后再写回,如果期间有唤醒事件,写回会失败,你就知道系统被唤醒了。这个机制在自动化测试里很好用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询