☰
Linux内核wakeup source机制详解:从原理到调试实践
2026/10/8 1:10:23 网站建设 项目流程

做低功耗调试这几年,我打交道最多的机制之一就是 wakeup source。不管是手机、平板还是路由器,只要“睡不下去”或者“睡一会儿自己醒过来”,排查对象里十有八九都有它。wakeup source 翻译过来就是唤醒源,是 Linux 内核功耗子系统里负责管理“谁能唤醒系统、谁正在阻止系统睡眠”的那套框架。这篇文章不绕弯子,直接把框架的设计思路、核心结构、接口用法、调试手段和常见坑一次讲清楚,适合正在接触电源管理(PM)、autosleep 和深浅睡眠调试的开发者参考。

先说一个很容易被忽视的事实:wakeup source 并不是 suspend 流程本身,而是 suspend 流程的“裁判”。系统想进 sleep 时,它负责告诉你现在能不能睡;系统睡下后,它负责记录是谁把你叫醒的。理解了这一点,后面所有细节都好办了。

1. 为什么需要 wakeup source:从一个“睡不着”的板子入手

1.1 一个典型的熬夜现场

想象一下这个场景:嵌入式板子跑着 Linux,外部有一个触摸屏和一个 RTC。你在用户态写了一个命令让系统进入 suspend,日志里却反复出现“suspending console”“PM: suspend entry”之后不到一秒钟,又出现“PM: resume”之类的信息。电流表测下来,系统根本没有进入低功耗状态,功耗和满负荷运行时差不多。

这种问题早期排查起来非常头疼。原因在于,进入 suspend 是一个多阶段过程:从用户态发指令,到内核执行 device suspend、打断 CPU 空闲、关闭外设电源,中间花费的时间可能在几百毫秒到几秒不等。就在这段窗口里,很可能有一个外部事件(一次触摸、一个 RTC 中断、一个网络包)突然到达。如果内核没有一个机制记录“这里有事还没处理完”,它就会白白执行完整个 suspend 流程,然后立刻又被中断唤醒,前功尽弃。

wakeup source 就是为解决这个乱局设计的。它相当于一份“事件账本”:任何可能唤醒系统的事件源,都对应一个 wakeup_source 对象;事件到来时,相关驱动把对象标记为激活(activate),事件处理完毕再标记为去激活(deactivate)。suspend 流程在关键节点上查账,只要账本里还有未结清的条目,就取消这次睡眠,不做无用功。这个“先记账、后结账”的模型,正是整套框架的底层逻辑。

1.2 老方案与新方案:从 wake_lock 到 wakeup source

早期 Android 内核里用的是一套叫 wake_lock 的机制,配合 early suspend 框架使用。wake_lock 的思路很直接:驱动程序或用户态进程在需要阻止睡眠时持有一把锁,持有锁期间系统不允许 suspend。但它有几个先天问题:一是锁的粒度太粗,谁都可以持锁,忘了解锁系统就永远睡不了;二是 early suspend 只在特定平台和 Android 内核对 mainline 的 suspend 流程做了深度定制,难以统一。

所以社区后来把这些能力重新整理,沉淀成现在的 wakeup source 框架,把它作为 PM core 的通用机制放进了主线代码,核心文件是 drivers/base/power/wakeup.c。现在的 __pm_stay_awake、__pm_relax 等接口,本质上是当年 wake_lock / wake_unlock 的“正规军”版本:不再是一把简单的锁,而是一个带状态机、带统计、带超时保护和 debugfs 导出能力的完整子系统。很多老代码里还能看到 wake_lock 的影子,但新驱动不应该再用了。

1.3 wakeup source 在整个功耗框架里的位置

要理解 wakeup source,得先知道它和系统里其他功耗机制的分工。CPU 在无任务时会通过 cpuidle 进入 idle 状态,这是微观层面的节能;设备在系统 suspend 时进入 D3 等低功耗状态,这是设备层面的措施;而 wakeup source 管的是系统级睡眠决策,它决定一次 suspend 请求是否合法、是否可继续执行。

打个比方:把系统比作一栋楼,cpuidle 是每个人在工位上打盹儿,设备 PM 是关掉各自办公室的灯,wakeup source 则是楼下保安手里的“出入登记簿”。任何访客(中断、事件)进出这栋楼,都要在登记簿上留痕。保安锁大门(进入 suspend)之前,得先看登记簿:是否还有人没出来?有人没出来,这门就锁不了。等人都出来了(所有 wakeup_source 都去激活),门才锁得上。理解这个职责边界,后面看代码时就不会把 wakeup source 和 cpuidle、devfreq 混在一起。

2. 框架核心:从结构体到状态流转

2.1 一张结构体看懂 wakeup source

先看最核心的数据结构,定义在 include/linux/pm_wakeup.h:

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; unsigned long last_time; unsigned long start_prevent_time; unsigned long prevent_sleep_time; unsigned long event_count; unsigned long active_count; unsigned long relax_count; unsigned long expire_count; unsigned long wakeup_count; bool active:1; bool autosleep_enabled:1; struct device *dev; };

不需要每个字段都背,但有几个必须看明白。name 是调试时第一个辨认的信息,它告诉你是谁在阻止睡眠,命名要尽量有辨识度,别用“driver-x”这种名字。active 表示当前是否处于激活状态,true 说明这个源正在阻止系统睡眠,这是排查问题时最先要看的字段。event_count 是累计触发次数,active_count 是累计激活次数,relax_count 是累计去激活次数,wakeup_count 是这个源作为唤醒原因被记录下来的次数。prevent_sleep_time 统计这个源累计阻止睡眠的时间,单位毫秒。timer 和 timer_expires 用于实现“超时自动去激活”,这是框架里最实用的保护机制之一。

为什么要统计这么多计数?因为只判断“现在能不能睡”是不够的。做功耗优化的产品,往往还要回答三类问题:这个源今天被触发了多少次?每次激活持续多久?它到底有没有真的唤醒过系统?这些计数正是为调试和长期统计准备的。特别是 wakeup_count,记录的是“真正导致系统从 sleep 状态醒来的事件次数”,和 event_count 区分开,这对滤掉无效的中断风暴非常关键。敢把一个统计体系设计得这么细,说明这套机制的定位从一开始就不是“临时方案”,而是可长期观测、可持续运维的基础设施。

2.2 激活与去激活状态机

wakeup source 的内部状态其实很朴素,只有两个:激活和不激活。重点在于状态跳转时的动作,以及锁的保护。

激活路径对应 wakeup_source_activate(),核心逻辑可以理解为:

static void wakeup_source_activate(struct wakeup_source *ws) { if (ws->active) return; ws->active = true; ws->active_count++; ws->last_time = ktime_get(); if (ws->autosleep_enabled) ws->start_prevent_time = ws->last_time; }

去激活路径对应 wakeup_source_deactivate():

static void wakeup_source_deactivate(struct wakeup_source *ws) { if (!ws->active) return; ws->relax_count++; ws->active = false; ws->total_time = ktime_add(ws->total_time, ktime_sub(ktime_get(), ws->last_time)); }

代码故意写成“如果已经激活就返回”这种防护式写法,是为了在并发场景下保证状态机的幂等性。即使两个 CPU 同时执行激活操作,active 也只会变成一次 true,active_count 不会凭空多加。所有这些操作都在 ws->lock 自旋锁的保护下完成,所以即使驱动在中断上下文调用激活接口,也不会出现数据竞争。我不建议驱动开发者在自己的代码里再包一层锁去封装这些函数,框架已经做完了这层工作,多包一层反而容易引入死锁。

这里埋着一个设计点:last_time 记录的是这次激活的起始时刻,deactivate 时用它来计算本次激活持续了多久,并把结果累加到 total_time。这个耗时数据用的是纳秒精度,最终在 debugfs 里展示时再换算成毫秒。不要小看这个耗时统计,排查“某事件频繁拉高系统功耗”时,它就是直接的定量证据。

2.3 和 suspend 主流程怎么配合

wakeup source 之所以能成为 suspend 的裁判,是因为 suspend 流程在关键节点检查了它。核心函数是 pm_wakeup_pending(),它内部维护一个全局唤醒事件计数,并与 suspend 决策阶段记录下的快照做比对,只要期间有新事件发生,就返回 true,suspend 流程随即终止。这样做的效果,等价于检查“这段时间内是否有唤醒源被激活过”,任何驱动通过 pm_stay_awake 记录的事件,最终都会反映到这个全局计数上。

挂起流程和唤醒源的关系,在 autosleep 里体现得最明显。autosleep 会反复调用 pm_wakeup_pending() 来判断当下是否适合自动挂起,再配合 /sys/power/wakeup_count 做计数同步。这套设计解决的是“suspend 决策窗口”里的竞态:系统开始挂起的瞬间刚好来了一个事件,如果不做快照比对,这次挂起就会变成一次无效操作,白白浪费电力又快速醒来。wakeup_count 机制把这个问题兜住了。

3. 接口怎么用:从注册到激活的完整路径

3.1 创建、注册与注销

和 wakeup source 打交道的入口其实不多。最基本的三个函数如下:

struct wakeup_source *wakeup_source_create(const char *name); void wakeup_source_register(struct wakeup_source *ws); void wakeup_source_unregister(struct wakeup_source *ws);

wakeup_source_create 只负责分配一个带名字的对象,wakeup_source_register 才把它挂到系统的全局链路上。如果驱动里只有局部使用场景,也可以只 create 不 register,但绝大多数驱动都会注册,因为不注册就进不了 debugfs 的全局列表,出问题你根本看不到它。

实际项目中,驱动更常用的是一步到位的封装:device_init_wakeup(dev, true)。这个函数把“设备具备唤醒能力”和“关联 wakeup_source 并注册”合并在一起完成,还会在设备的 power/wakeup sysfs 节点上创建控制属性。设备驱动在 probe 里调用它,是最标准的做法。注销时用 device_init_wakeup(dev, false) 或者 device_wakeup_disable() 都能把资源释放干净。

提醒一下:create 和 register 分离不是累赘,而是为了给驱动更大的控制空间。有些驱动希望先准备好唤醒源状态,再选择一个合适时机把它暴露给全局框架,比如热插拔设备。如果你只是写一个普通平台驱动,用 device_init_wakeup 就够了,别折腾两段式流程。有个细节值得注意:device_init_wakeup 返回 int,但真正的错误检查应该放在 device_wakeup_enable 的返回值上,很多驱动忽略了这个错误,结果后续调用 pm_wakeup_event 时因为 dev->power.wakeup 是 NULL 而静默失效。

3.2 激活与去激活的几个核心 API

抛开注册,驱动日常打交道最多的是下面这组接口:

API作用说明
pm_stay_awake(ws)激活唤醒源内部加锁,可在中断上下文使用
pm_relax(ws)去激活唤醒源内部加锁,可在中断上下文使用
__pm_stay_awake(ws)激活唤醒源不额外加锁,要求调用者已持锁
__pm_relax(ws)去激活唤醒源不额外加锁,要求调用者已持锁
pm_wakeup_event(dev, msecs)触发一次事件并自动去激活事件发生后 msecs 毫秒自动 relax
__pm_wakeup_event(ws, msecs)同上,基于 ws适合已持有 ws 对象的驱动

为什么要有带下划线的 _pm版本?核心在于锁的归属。驱动里如果已经持有 ws->lock,再去调用带锁版本就会重复加锁,所以框架把“锁外调用”和“锁内调用”拆成两组。普通驱动直接用不带下划线的版本最安全,尤其在中断上下文里,pm_stay_awake 和 pm_relax 内部使用的是 spin_lock_irqsave,不会因为中断嵌套而破坏状态。

pm_stay_awake 和 pm_relax 必须严格配对。这里说的配对不是简单的数量相等,而是语义上:一次事件开始时 stay,事件处理完要 relax。实际项目里最常见的功耗 bug 就是 stay 之后没有 relax,导致系统永远睡不下去,客观表现就是 debugfs 里某个 wakeup source 一直处于 active 状态。

3.3 超时自动去激活:防止忘锁锁死

光靠人工管理配对偶尔会疏漏,所以框架提供了超时保护。调用带超时的接口后,框架会使用高精度定时器,到点自动做 deactivate。最实用的接口是 pm_wakeup_event(),一个参数是设备指针,另一个参数是超时毫秒数。

我遇到过这样一个案例:驱动在中断处理函数里调用 pm_stay_awake 记录事件,但在某个异常分支里漏调了 pm_relax。由于这次漏调,系统从那一刻起再也无法 suspend,而现场代码层面找不到任何报错,因为 wakeup source 不会主动报错,它只是静静地挡在睡眠路径上。后来改成 pm_wakeup_event(dev, 100),在 100 毫秒后无论正常分支还是异常分支都会自动 relax,问题立刻消失。

不过,超时值不是越短越好。如果事件处理本身需要更长时间,比如一次 DMA 传输或一次 flash 擦写,超时过短会导致事件还没干完就被强制 relax,系统可能在错误的时间点进入睡眠。合理的做法是根据硬件操作最坏耗时来设置,一般留 1.5 到 2 倍余量。源码里很多驱动直接用 jiffies,比如设置 20 个 jiffies 的超时,但如果你能明确算出毫秒值,尽量用毫秒,调试时更直观。

3.4 sysfs 与设备唤醒属性

设备唤醒能力在 sysfs 里是开放的。对支持唤醒的设备,/sys/devices/.../power/wakeup 节点会显示 enabled 或 disabled 状态,写入 enable/disable 可以运行时控制。同时还有一个状态标识:active 或 inactive,表示这个设备当前是否处于唤醒源激活状态。调试时我经常先看这个节点,能快速判断某个外设是不是一直拉着不睡。

全局还有 /sys/power/wakeup_count 节点,和 autosleep 配合使用,用于协调用户态和内核态的唤醒计数。Android 的 sleep 脚本会先读出 wakeup_count 保存,再执行 echo mem > /sys/power/state,如果 echo 返回失败,说明执行期间有唤醒事件发生,需要重试。这个机制保证了 suspend 决策的原子性。如果你只是在嵌入式环境里手动测试,知道它存在即可,不必过度设计。我一直提醒团队:把 wakeup_count 当作系统级状态机的一部分来理解,而不是一个普通的计数器,这样才能真正读懂 autosleep 的工作逻辑。

3.5 设备唤醒与中断的组合关系

在真实产品里,wakeup source 几乎总是和“唤醒中断”绑定。标准做法是:驱动在 suspend 回调里,如果确认自己允许作为唤醒源,就调用 enable_irq_wake(irq);同时,在中断处理函数里调用 pm_stay_awake 或 pm_wakeup_event。唤醒中断负责把 CPU 从睡眠里拉起来,wakeup source 负责把事件记录进账本并打断本次挂起流程。

这里必须说清楚一个概念:普通中断并不等于唤醒中断。如果一个中断没有设置 IRQF_NO_SUSPEND 标志,也没有 enable_irq_wake 打开唤醒功能,系统 suspend 之后它根本不会被触发。因此,wakeup source 的激活和中断唤醒是两条独立但互补的路径:前者是软件层面的记账,后者是硬件层面的叫醒,两者缺一不可。很多新手上来就给所有中断都开唤醒,结果系统被无关事件反复拉起,功耗不降反升,这就是没想明白这两者的分工。

4. 接入实战:给自己的驱动挂一个唤醒源

4.1 一个最小可用的按键唤醒示例

理论讲完了,看一个实际接入的例子。假设有一个 GPIO 按键驱动,希望在按键按下时唤醒系统,并且在按键按下期间阻止系统重新进入睡眠。

probe 阶段最常见的初始化方式是:

static int key_probe(struct platform_device *pdev) { struct key_data *data = dev_get_drvdata(&pdev->dev); device_init_wakeup(&pdev->dev, true); >static irqreturn_t key_irq_handler(int irq, void *dev_id) { struct key_data *data = dev_id; pm_wakeup_event(&data->pdev->dev, 200); /* 上报 input 事件给上层,做消抖等 */ return IRQ_HANDLED; }

200 毫秒的超时,是为了确保按键消抖和事件上报流程完整执行完之后,唤醒源才自动去激活。你不需要手动调用 pm_relax,框架会在超时后自己把账结掉。这套写法在内核里非常常见,优点是接口清晰、异常分支安全、可读性强。如果需要长时间保持唤醒,比如 USB 插入后要维持供电协商,那么可以用 pm_stay_awake 配合自己控制的 pm_relax,但那样要多写不少错误处理代码,不是必要情况不建议。

4.2 autosleep 与 suspend 的协同节奏

如果你的产品开启了 autosleep(在 /sys/power/autosleep 里写入 mem),系统的睡眠决策会变成自动模式:内核里有一个内核线程反复检查,只要没有新的唤醒事件,就自动发起 suspend;一旦有唤醒源激活,就打断挂起流程。这样一来,你不用再靠用户态脚本手动 echo mem,而是由唤醒源的状态驱动整体睡眠节奏。

autosleep 这种模式对驱动的要求更高。它把“谁决定睡眠”的责任完全交给了各个驱动,要求驱动们对唤醒源的管理非常严谨。我在项目里总是强调:autosleep 模式下驱动唤醒源管理的一点小错误,就会被放大为“系统完全睡不着”的严重问题。反过来,如果你还没有把各驱动的唤醒源梳理清楚,建议先用手动 suspend 的方式做验证,确认每条唤醒路径都正常,再开 autosleep。

4.3 读懂 debugfs:wakeup_sources 节点逐列解析

系统跑起来后,如果怀疑有唤醒源问题,第一个要看的文件是 /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 key-0 3 5 2 0 128 123456 654321 987654321 0 rtc_alarm 1 1 1 0 0 0 0 223344556 0

这些列的维度不一样,需要分开读。active_since 是当前激活的起始时间,如果它长期保持非零且数值不再变化,那基本可以直接定位“挡着睡眠的源”。event_count 反映触发频率,如果它涨得很快但 wakeup_count 不涨,说明事件在挂起前就被消耗掉了,没有实际唤醒系统。total_time 和 max_time 分别累计激活总时长和单次最长激活时间,用于评估这个源是否长时间占用睡眠权。prevent_suspend_time 则是这个源累计阻止挂起的时间总量,数值越大的源越值得怀疑。

我排查“系统睡不下去”的标准动作是三步:先 cat 这个文件,看有没有 active 状态且 active_since 非零的源;再用设备唤醒节点确认是不是某设备反复唤醒;最后用 ftrace 看事件时间线。三步下来,绝大多数问题都能定位。注意 debugfs 需要 root 权限。嵌入式产品发布时如果想去掉调试信息,记得关闭 CONFIG_PM_DEBUG,否则 wakeup source 的统计和导出特性会一直存在,虽然不直接影响功耗,但会多占一点内存。

4.4 用 ftrace 抓住事件序列

wakeup source 框架在 drivers/base/power/wakeup.c 里埋了一组 tracepoint,名字类似 wakeup_source_activate、wakeup_source_deactivate 和 wakeup_source_idle。配合 ftrace,可以完整还原“某事件在什么时间激活、又在什么时间被 relax”的序列。

实际操作很简单:

echo 'power:wakeup_source_activate' > /sys/kernel/debug/tracing/set_event echo 'power:wakeup_source_deactivate' > /sys/kernel/debug/tracing/set_event echo 1 > /sys/kernel/debug/tracing/events/power/enable cat /sys/kernel/debug/tracing/trace

如果看到 activate 之后长时间没有对应的 deactivate,那说明有驱动忘了 relax。这个信息量远大于只看 debugfs 计数,尤其适合剖析一次完整挂起被打断的因果链。在我的调试习惯里,debugfs 是判案,ftrace 是取证,两者配合效果最好。实际使用中首选抓 suspend 前后的窗口,事件太多时用 trace-cmd 的环形缓冲,先跑场景再停下来分析,比一直开着 trace 更省事。

5. 常见问题与排查技巧实录

5.1 问题一:系统根本睡不下去,active 的源一直挂着

这是最经典的症状。处理思路是先拿数据,不要瞎猜。第一步查看 /sys/kernel/debug/wakeup_sources,找到 active_since 不为零、且一直存在的那一行,行首的 name 就是元凶。如果是某个驱动的名字,回到代码去查它调用 pm_stay_awake 或 pm_wakeup_event 的所有路径,看是否有分支没有执行对应的 pm_relax。

实际项目中,最常见的原因有三个。第一,硬件事件产生的中断处理函数里,某个条件分支漏了 relax。第二,某个轮询线程在循环体里频繁 stay 和 relax,偶发异常导致状态一直无法回到非激活。第三,驱动在不同 CPU 上调用 stay 和 relax,时序上出现先 relax 后 stay 的倒挂。遇到这些情况,光看代码很难发现,不如直接在 tracepoint 日志里拉时间线,几秒就能看出异常序列。

5.2 问题二:系统频繁被唤醒,但业务上并不需要

另一种常见状况是:系统能睡,但睡不了几分钟就醒来一次,而且醒来后要等半天才重新睡。这种问题的根源通常是“不必要的中断唤醒了系统”,而非真的漏了 relax。排查手段是记录各 wakeup source 的 wakeup_count,如果某个源 wakeup_count 不断增长,但业务逻辑上并没有真实事件需要处理,大概率是硬件中断布线、中断共享或者驱动误触发导致的。

我的经验是,对“睡眠-唤醒-再睡眠”的循环,不要只看内核,还要看硬件叠加上拉、去抖电容是否足够。软件侧能做的则是把不关心的中断唤醒能力关掉,或者把共享中断里不期望唤醒的子设备从唤醒源列表里摘掉,比如通过 device_init_wakeup(dev, false) 临时关闭。这里有一条重要经验:开发初期把唤醒源范围收窄,让误唤醒概率降到最低,比一开始就让所有设备都具备唤醒能力更稳妥,否则后面排查的时间成本会成倍增加。

5.3 问题三:pm_relax 时机不对导致的倒挂

很多驱动在中断上半部里调用 pm_stay_awake,在下半部里调用 pm_relax,但忽略了中断可能嵌套或延迟的情况。比如同一个设备的一个中断在上下半部之间又触发了第二次,第一次的 relax 可能把第二次事件记录的状态一起结算掉,导致第二次事件丢失。针对这种情况,标准解法是:用带超时的 pm_wakeup_event 替代手动的 stay/relax 配对,让框架帮你管理“最后一次事件后的延迟结算窗口”。

更深一层的技巧是:不要在一个事件处理完的瞬间立刻 relax,而应该留出一点“事件消化窗口”。比如网络驱动中,一个数据包到达后,你在唤醒源激活中把包取走,但上层协议栈还在继续处理它,此时立刻 relax 就可能让系统在协议栈还没消化完时尝试挂起。设置一个小的延迟,比如 10 到 20 毫秒,代价极小,却能显著减少协议层的竞态。“延迟 relax”这个思路在真实产品调试中救过我很多次,强烈建议在驱动设计阶段就把它考虑进去。

5.4 避坑清单:最好一次就记住的规矩

结合多年经验,整理几条高价值规约,建议贴在产品迭代清单上:

  • pm_stay_awake 和 pm_relax 必须在同一个“事件生命周期”内配对,不要把 relax 藏在某个可能被绕过的错误处理路径里。
  • 中断上下文里别用可能睡眠的锁去保护 wakeup source,框架已经用自旋锁做了保护,你只需要调用公开接口。
  • 设置超时时,宁可多留一点余量,也不要设得太小。超时撤早了,事件没处理完;超时撤晚了,睡眠被耽误,两者都是功耗和稳定性双重损失。
  • 所有新增的、能唤醒系统的驱动,开发阶段都应该在开启 CONFIG_PM_DEBUG 的情况下验证一次 wakeup_sources 的输出,确认激活和去激活的完整生命周期是成立的。
  • 发布产品时关闭调试接口,或者在有安全需求的设备上限制相关节点的读写权限,避免生产设备上被人随意读取功耗状态信息。

最后再分享一点我用下来的体会。wakeup source 框架并不复杂,复杂的是工程中各种事件之间的时序关系。我最开始调试时总想着把每个中断的唤醒路径都理清,后来发现更高效的做法是让框架的统计和 tracepoint 替我先画出全局图,再顺着 active 的源一个个往下游查。只要 debugfs 里那张账本始终清晰,睡眠问题就一定有迹可循。希望这篇文章能帮你少走点弯路,多睡几个安稳觉。

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

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

立即咨询