接了这么多年的功耗案子我发现一个规律真正让人头疼的往往不是硬件上哪里漏电而是软件层面“睡没睡踏实”。前阵子调一块嵌入式板子灭屏待机一晚上掉电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或者有正在进行的唤醒事件处理它就会返回 truesuspend 流程随即中止。注意是中止而不是等它释放后自动继续。用户空间通常需要重新发起 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 状态置 1active_since 从创建时刻开始计时。这个模拟实验特别适合新人快速建立“wakeup source 就是阻止系统睡眠的实体”这个直观认知。熟练之后再去分析真实设备问题就不会被一堆 debugfs 数字吓住。最后分享一点实际经验我在实际项目里真正让我少走弯路的不是读源码的速度而是“稍等再抓一轮数据”的耐心。wakeup source 的核心价值并不是阻止系统睡眠而是让每一次“决定不睡眠”都有据可查。当你看到 event_count 涨得飞快、prevent_suspend_time 又长时间不为 0多半不是硬件 bug而是驱动在使用唤醒源时的生命周期没管理好要么忘了 relax要么在错误的时机上报。处理这类问题先别急着打日志按wakeup_sources的表头和 ftrace 事件一步步来基本都能把元凶钉在台面上。这个框架后续还可以往深一层挖wakeup_count 与用户态电源管理服务比如 Android 的 PowerManagerService的交互值得单独写一篇完整流程。等手里这阵子项目收尾我再把睡眠请求从用户态到内核态的全链路补上。