接了这么多年的功耗案子,我发现一个规律:真正让人头疼的,往往不是硬件上哪里漏电,而是软件层面“睡没睡踏实”。前阵子调一块嵌入式板子,灭屏待机一晚上掉电15个点,我在/sys/kernel/debug/wakeup_sources里看到某个设备名的 active_count 一晚上涨了几十次,prevent_suspend_time 累计了四个多小时,瞬间心里就有谱了——系统的 suspend 流程反复被唤醒事件打断,压根没进去过深睡状态。今天这篇是这个系列的第十六篇,专门梳理 Linux 内核里的 wakeup source 唤醒源框架:它解决什么问题、核心数据结构长什么样、事件怎么在源码里走通,最后给出一套我常用的定位功耗问题的手法。如果你最近在追唤醒异常,或者刚开始系统学电源管理,这篇值得读完。
1. 搞清楚 wakeup source 的职责边界:它管的是“系统睡不睡”这件事
1.1 没有这套框架之前,suspend 流程有多乱
早年内核的 suspend 比现在粗暴得多。用户空间往/sys/power/state写一个 "mem",内核就开始冻结进程、冻结设备驱动,然后逐个执行设备驱动的 suspend 回调。问题在于:硬件唤醒事件可能在任何时刻到达,尤其是在驱动已经 suspend 到一半的时候。如果驱动在这个节骨眼上错过了一次中断,后面设备可能永远等不到宿主机的响应;如果驱动太“积极”,又会在整个系统还没准备好时贸然唤醒,导致状态错乱。
于是 Android 早期搞出了 wakelock 机制:谁持有锁,系统就不允许 suspend。驱动想防睡就 new 一个锁,想允许睡就释放。这招简单粗暴,确实让手机系统能像样地睡眠了,但坑也很明显——锁全在驱动手里,一旦漏释放,系统永远睡不着,而且没人知道是“谁”锁住了它。内核社区后来用 wakeup source 取代了那套原始 wakelock,把“阻止睡眠”和“记录谁阻止了睡眠”这两件事一起做掉了。
1.2 wakeup source 的三种角色
理解 wakeup source 最好从一个角度切入:它同时扮演三个角色。
第一个角色是“闸门”。当一个 wakeup source 处于 active 状态,内核的 suspend 流程就会被拦住,不允许直接进入低功耗状态。通俗点说,它在告诉电源管理核心:“有人还没处理完事,你先别睡。”
第二个角色是“账本”。它记录这个唤醒源被激活了多少次、上报了多少事件、累计阻止了多久的休眠。排查异常唤醒时,这些数字是主要的破案线索。第三个角色是“协调器”。通过 wakeup_count 这个计数机制,它能把“睡眠决策瞬间”和“唤醒事件发生瞬间”对齐,保证不会出现“刚决定要睡,事件就来了,然后被漏掉”的竞态。
打个比方:wakeup source 就像家里大门上的锁加门铃。锁负责不让门在有人靠近时关上,门铃记录每一次按压。你出门前看一眼门铃记录,就知道谁来过、来了几次。这个框架的核心价值不是“阻止系统睡眠”,而是让每一次“决定不睡眠”都有据可查。
2. 数据结构与 API 全解:struct wakeup_source 源码地图
2.1 struct wakeup_source 的内部世界
先说结论:所有唤醒源逻辑都挂在drivers/base/power/wakeup.c和include/linux/pm_wakeup.h,核心结构体就一个struct wakeup_source。下面这段按主流 5.x/6.x 内核注释整理,不同分支在统计字段上略有差异。
struct wakeup_source { const char *name; struct list_head entry; spinlock_t lock; struct wake_irq *wakeirq; struct timer_list timer; unsigned long timer_expires; ktime_t last_time; ktime_t start_prevent_time; ktime_t max_prevent_time; ktime_t total_prevent_time; u64 max_time; u64 total_time; unsigned int active_count; unsigned int event_count; unsigned int wakeup_count; unsigned int expire_count; bool active:1; bool autosleep_enabled:1; };每个字段都不是摆设,调试功耗问题时要能直接反应出含义。
| 字段 | 直观意义 | 调试时的用法 |
|---|---|---|
| name | 唤醒源名字 | 一眼看出“谁”注册的,命名规范非常重要 |
| entry | 全局链表节点 | 串在wakeup_sources_list上 |
| active | 当前是否激活 | 为 1 表示它正在阻止系统休眠 |
| active_count | 进入激活状态的次数 | 数值越大,说明该源越忙 |
| event_count | 上报唤醒事件次数 | 对比 active_count 可发现重复事件 |
| wakeup_count | 真正把系统唤醒的次数 | 这是“背锅”指标 |
| expire_count | 定时器到期自动释放的次数 | 驱动没主动 relax 的痕迹 |
| total_time / total_prevent_time | 累计激活时长 | 功耗问题第一大线索 |
| last_time / start_prevent_time | 最近失活时间 / 本次激活起点 | active_since 列的数据来源 |
这里有个容易踩的坑:很多人以为 event_count 就是唤醒次数。实际上中断驱动里随便调用一次pm_wakeup_event()都会event_count++,但只有当这个事件真正让系统的 suspend 中止,或者把 CPU 从低功耗状态拉回来,wakeup_count才会变化。所以排查“设备导致异常唤醒”时,先看 event_count 再交叉验证 wakeup_count,别被表面数字骗了。
2.2 注册与注销的四种姿势
内核给驱动提供了不同层次的接口,选错层级很容易造成逻辑混乱。我按使用频率排个序。
驱动开发中最常用的是device_init_wakeup(dev, true)。它同时做了两件事:把设备标记为“可唤醒”(can_wakeup),然后设置 enable。enable 之后,内核会给这个设备创建一个专属的 wakeup source,挂在dev->power.wakeup上,设备名就是唤醒源默认名。
底层一点的接口是wakeup_source_register(dev, name),相当于 create + add + 关联设备一步到位。适合独立模块或者不想被设备模型束缚的场景。如果你需要完全自己控制生命周期,可以手动走wakeup_source_create→wakeup_source_add→ 使用完 →wakeup_source_remove→wakeup_source_destroy这条链。
device_set_wakeup_enable(dev, enable)负责单独切换设备的唤醒能力开关,但它有个前提:设备必须已经被device_set_wakeup_capable()标记过。很多新手直接在 probe 里调用device_set_wakeup_enable(dev, true)却忘了先设置 capable,结果折腾半天设备根本没生成 wakeup source。我建议统一用device_init_wakeup,一步到位最省事。
注销的时候同样别省事。设备 suspend 失败、驱动卸载回滚,如果只调了device_init_wakeup(dev, true),卸载前记得对称调用device_init_wakeup(dev, false),否则 sysfs 里会残留一个 danglings wakeup source,虽然不一定崩,但会污染统计信息。
2.3 激活、失活、事件上报,三个基本动作
wakeup source 的使用就三个动作:激活、失活、上报事件。
__pm_stay_awake(ws):把 ws 标记为 active,更新active_count和start_prevent_time。__pm_relax(ws):把 active 清掉,累计total_time,刷新last_time。pm_wakeup_ws_event(ws, msec, hard):上报一个唤醒事件,内部会视情况先把 ws 激活,再视 msec 启动一个自动释放定时器。
驱动里最常见的是设备级封装:pm_stay_awake(dev)、pm_relax(dev)、pm_wakeup_event(dev, msec)。举个例子:
/* 在中断处理或 workqueue 中 */ pm_wakeup_event(dev, 100); /* 100ms 后自动释放 */ /* 或者按住不放 */ pm_stay_awake(dev); ... 处理数据 ... pm_relax(dev);很多驱动把pm_wakeup_event当成“立刻唤醒系统”的按钮,这是误解。它真正的语义是“报告给电源管理核心:有活要干,先别睡”,至于系统当前是处于 idle、suspend 前奏还是深睡中,内核会自行判断要中止休眠还是立即唤醒。这个理解一旦到位,调试时就不会乱打算。
参数msec也很关键。它表示“事件上报后最长保持 active 多久”。内核会启动一个 timer,到点后自动释放。这是一种防御机制:驱动如果忘了调用pm_relax(),wakeup source 也不会永远锁死系统。
3. 从事件上报到 suspend 决策:完整源码工作流
3.1 事件上报后内核内部发生了什么
假设一个按键中断触发,驱动中断处理函数里调用pm_wakeup_event(dev, 100),这条路径在源码里比想象中清晰。
第一步,pm_wakeup_event()会在dev->power.lock保护下拿到dev->power.wakeup这个 wakeup source 指针。第二步进入pm_wakeup_ws_event(),在这里做四件事:先ws->event_count++,然后如果参数带了 msec,就通过mod_timer更新定时器,接着检查 ws 当前是否 active,如果没激活就执行__pm_stay_awake(),把状态置为 active,并让active_count++。最后如果是在系统 suspend 过程中调用,计数变化还会影响pm_wakeup_pending()的判断。
这里有个细节:一个 wakeup source 被重复上报事件时,定时器不会随便重置。内核里有个timer_expires字段,只有当新请求的到期时间比当前记录的还要晚时,才会重新mod_timer。也就是说,如果一个事件说“我需要 hold 100ms”,另一个事件又说“我还需要 hold 200ms”,定时器会推到 200ms 后再触发;反过来,如果新事件只要 hold 50ms,定时器不会往后缩。这个设计避免了一个高频中断源把定时器越推越近,反而让唤醒源提前释放。
3.2 定时器与 expire 机制
wakeup source 的自动释放定时器到点后,回调函数会执行__pm_relax(),同时把expire_count加一。看/sys/kernel/debug/wakeup_sources时,如果你的设备expire_count大幅增长,基本可以断定驱动大量使用了pm_wakeup_event(dev, msec)却没有在业务处理完时主动调用pm_relax(dev),全靠在等定时器兜底。
那为什么内核用timer_list(基于 jiffies)而不是高精度定时器?因为 wakeup source 根本不需要微秒级精确。它的目的只是“在事件发生后一段时间内挡住 suspend”,晚个几十毫秒释放完全无所谓。用低精度 timer 还能避免和唤醒路径里的高精度时钟抢占逻辑纠缠在一起。这一点从设计上就给开发人员省了不少事。
3.3 suspend 主流程如何读取这些信息
系统真正进入 suspend 的开关路径在kernel/power/suspend.c。每次pm_suspend()被调用,内核会按顺序做冻结进程、冻结设备、执行 suspend 回调这些步骤,但每一步之前以及进入低功耗状态前,都会调用pm_wakeup_pending()检查。
这个函数的行为可以简化成一句话:只要当前存在任何一个 active 的 wakeup source,或者有正在进行的唤醒事件处理,它就会返回 true,suspend 流程随即中止。注意,是中止而不是等它释放后自动继续。用户空间通常需要重新发起 suspend,这也是为什么有些系统“明明看到设备在忙,却反复退出睡眠”。
3.4 wakeup_count 机制是怎么兜底的
大家常在 Android 手机上听到“wakeup_count”,它解决的是一个很隐蔽的竞态:用户空间准备写/sys/power/state进入 suspend,在读取状态和真正写入之间,设备可能刚好来了一个唤醒事件。如果内核不拦,这个事件会被白白吞掉,系统睡下去后设备却再也等不到响应。
wakeup_count 的流程是这样:用户空间先读/sys/power/wakeup_count,拿到的数字是当前累计的唤醒事件数;然后向/sys/power/state写 mem 之前,把这个数字原样写回/sys/power/wakeup_count。内核端pm_save_wakeup_count()会做一次原子比较:如果此刻的计数和写回的数字不一致,说明读完之后又发生了新事件,这次写回失败,suspend 必须重来;如果一致,内核会把刚才那个瞬间定义为“分水岭”,之后到来的事件才算真正的唤醒事件。
这个机制的价值在于:它把“睡眠决策”和“事件到来”这两个时刻对齐了,避免了“刚说完晚安,又有人敲门,却没人听到”的局面。调试时如果发现系统频繁写入 wakeup_count 失败,不要急着怀疑驱动,先查是不是有定时器或者轮询线程在不停上报事件,把计数搅混了。
4. 与 irq、wakeirq、autosleep 的配合作战手册
4.1 wakeirq:专门给“太能睡的设备”准备的根中断
线上设备里,很多 I2C/SPI 外设本身有省电模式,比如触摸屏的 controller 在 no data 时会关闭内部振荡器,驱动程序也会在 idle 后把它的普通中断关掉,以此降低功耗。问题来了:设备自己睡了,系统也睡了,外部事件来了该怎么把系统叫醒?
这就需要 wakeirq 机制。dev_pm_set_wake_irq(dev, irq)可以将一个独立的中断号挂到设备的 wakeup source 上。系统进入 suspend 时,内核会把这个 irq 重新配置为 wake irq,只要这条线上有电平变化,硬件就能把 SoC 从低功耗状态拉起来,同时触发pm_wakeup_event()上报事件。
这里踩过的坑不少。最典型的就是把普通数据中断误设成了 wakeirq。比如一个传感器平时每次数据准备好都触发中断,把它配成 wakeirq 后,设备在系统睡眠期间也会频繁上报,板子基本一整晚都在反复醒来干活。判断是不是这个问题,看/sys/kernel/debug/wakeup_sources里对应设备的 event_count 和 wakeup_count 是否同步暴涨就知道。经验法则:只有设备“自己进入低功耗并关闭主中断”的那一路唤醒引脚才适合作为 wakeirq,普通工作中断不要碰。
4.2 autosleep 和用户态 wakelock 的兼容层
现在很多 Android 内核都开了CONFIG_PM_AUTOSLEEP。autosleep 的思路是:内核自动监控 wakeup source 的状态,一旦发现没有 active 的唤醒源,就自己选择睡眠状态并触发 suspend,不需要用户空间反复往/sys/power/state写值。这样系统在没有任何活动时能迅速自动睡眠,对手机和平板体验很重要。
用户空间仍然可以用/sys/power/wake_lock和/sys/power/wake_unlock手工占锁。这两个 sysfs 接口在启用CONFIG_PM_WAKELOCKS时会把锁操作翻译成创建和销毁一个临时 wakeup source。所以你在/sys/kernel/debug/wakeup_sources里看到的很多名字其实不是设备名,而是某个 App 或者服务通过 wake_lock 创建的字符串。
我自己排查过最多的问题就是“某个 wake_lock 一直没释放,导致系统永远进不了 deep sleep”。处理办法很简单:cat /sys/kernel/debug/wakeup_sources找到 name 不为空且 active_since 一直在增长的项,再去用户空间定位是谁写的这个锁名。
4.3 runtime PM 和唤醒源的配合
设备驱动在 runtime PM 框架里经常做这样一套组合拳:设备进入 runtime suspend 前调用device_set_wakeup_enable(dev, true),把唤醒能力打开;退出 runtime suspend 后再device_set_wakeup_enable(dev, false)把唤醒关掉。这样可以让设备在没有数据的时候进入硬件休眠,同时保留一扇唤醒的“侧门”。
这套逻辑本身没问题,但调试时要特别留意设备在 enable 状态时恰好被系统 suspend的情形。常见的坑是:驱动在 runtime suspend 回调里先调了device_set_wakeup_enable(false),结果系统级别的 suspend 流程立刻认为这个设备不再唤醒敏感,就不再参与后续的唤醒仲裁。如果设备内部其实还有未处理完的数据包,它就成了“聋子”。排查这种问题,重点看设备power/wakeup的当前状态和驱动的 suspend 时序,别被 runtime PM 和系统 PM 两个独立流程绕晕。
5. 调试实录:三大典型唤醒异常与工具链
5.1 第一步:把 debugfs 里的数字翻译成人话
内核把所有已注册的 wakeup source 都汇总在一个文件里,/sys/kernel/debug/wakeup_sources。读取方式:
cat /sys/kernel/debug/wakeup_sources输出大致是这样:
name active_count event_count wakeup_count expire_count active_since total_time max_time last_change prevent_suspend_time event0 3 12 0 0 0 1200 800 0 1200 gpio-keys 24 32 12 0 0 482000 50000 0 482000 test_wakelock 2 2 0 0 0 150 100 0 150字段虽然多,但排查功耗问题时首要看两个:active_since和prevent_suspend_time。active_since非 0 表示该 wakeup source 此刻还处于激活状态,正在阻止系统睡眠;prevent_suspend_time是它累计阻止休眠的总时长。如果某一行这个值大得离谱,基本就是它把系统“钉”在浅睡状态。
另外提一句:不同内核版本的列名略有差异,老内核里可能叫prevent_suspend_time,新内核改成total_prevent_time,具体以cat输出的表头为准。建议第一时间把表头截图存档,后续分析数据都对照着看。
5.2 问题 A:系统想睡却睡不下去
这类问题的典型现象是:echo mem > /sys/power/state卡住不退,或者 flash 了屏后功耗依然很高。排查第一步:
watch -n 1 'cat /sys/kernel/debug/wakeup_sources | sort -k9 -rn | head'盯住active_since和prevent_suspend_time持续增长的项。找到嫌疑 wakeup source 后,下一步得知道是谁在不停调__pm_stay_awake()或者上报事件。这个用 ftrace 最直接:
echo 'wakeup_source:activate wakeup_source:deactivate' > /sys/kernel/tracing/set_event echo 1 > /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace_pipe看到 activate 和 deactivate 交替疯狂的记录,就去代码里搜那个 wakeup source 名字对应的驱动。我遇到最多的场景是中断线程里无条件调pm_wakeup_event(dev, 0),msec 传 0 意味着“没有自动释放机制”,而驱动逻辑又没在后续路径里调pm_relax(),于是锁死。
5.3 问题 B:设备到点了却醒不来
反过来的问题更难查。硬件中断明明到了,系统就是不从 suspend 中醒来。这时别急着怀疑 wakeup source,先确认两件事。
第一件,这个中断在 suspend 前有没有被 arm 成 wake irq。很多 SoC 的 GPIO 中断默认在睡眠期间是关闭的,需要驱动在suspend_noirq阶段重新配置。第二件,irq 号对应的 wakeup source 是否已经注册且 enable。如果设备根本没调device_init_wakeup(dev, true),那它压根不在唤醒仲裁名单里,中断到死也调不起系统。
还有一个隐蔽原因:用户空间写wakeup_count的时序不对。有些定制的 power 管理服务读回 count 后,没有在真正写/sys/power/state之前把 count 写回,导致内核认为“现在的事件是睡眠过程中该有的”,结果把设备中断当成了背景噪声。遇到这类问题,用串口看内核日志里的PM: suspend entry和PM: suspend exit前后有没有跟踪到 warn,配合echo 0 > /sys/kernel/debug/tracing/tracing_on抓一次完整的事件流,比盲目猜快得多。
5.4 问题 C:设备被异常唤醒,但统计里没有明显线索
有时候/sys/kernel/debug/wakeup_sources里数字并不多,可系统就是半夜被唤醒。这种大概率不是 wakeup source 本身的问题,而是唤醒源根本没被统计到。典型场景:某个设备在系统 suspend 后,它的唤醒事件走的不是常规路径,比如硬件直接拉了一个 SoC 的深层睡眠唤醒引脚,但没有软件层面对应的 wakeup source。
排查思路是反着来:先看/proc/interrupts,统计睡眠期间哪些 irq 计数在涨;找到 irq 后追到设备树或驱动,确认它有没有注册 wakeup source。没有注册的,在驱动里补上device_init_wakeup()和对应的上报逻辑即可。这里插一句:临时验证“某设备是否导致唤醒”时,可以把它在 sysfs 里的 wakeup 开关关掉再试一晚:
echo disabled > /sys/devices/platform/xxx/power/wakeup如果第二天发现唤醒次数明显下降,那元凶基本锁定。这个办法比反复改代码重编内核高效得多,我几乎每次都能用它先缩小范围。
5.5 快速验证技巧:用 wake_lock 模拟“万恶之源”
想验证自己对 wakeup source 的理解对不对,或者验证某套排查脚本好不好用,可以直接在用户态造一个“锁死系统”的假象:
echo test_wakelock > /sys/power/wake_lock sleep 10 cat /sys/kernel/debug/wakeup_sources | grep test_wakelock echo test_wakelock > /sys/power/wake_unlock执行期间你会看到一个名字为 test_wakelock 的唤醒源出现,active 状态置 1,active_since 从创建时刻开始计时。这个模拟实验特别适合新人快速建立“wakeup source 就是阻止系统睡眠的实体”这个直观认知。熟练之后再去分析真实设备问题,就不会被一堆 debugfs 数字吓住。
最后分享一点实际经验
我在实际项目里真正让我少走弯路的,不是读源码的速度,而是“稍等再抓一轮数据”的耐心。wakeup source 的核心价值并不是阻止系统睡眠,而是让每一次“决定不睡眠”都有据可查。当你看到 event_count 涨得飞快、prevent_suspend_time 又长时间不为 0,多半不是硬件 bug,而是驱动在使用唤醒源时的生命周期没管理好:要么忘了 relax,要么在错误的时机上报。处理这类问题,先别急着打日志,按wakeup_sources的表头和 ftrace 事件一步步来,基本都能把元凶钉在台面上。
这个框架后续还可以往深一层挖:wakeup_count 与用户态电源管理服务(比如 Android 的 PowerManagerService)的交互,值得单独写一篇完整流程。等手里这阵子项目收尾,我再把睡眠请求从用户态到内核态的全链路补上。