ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核wakeup source机制详解:从原理到调试实践

Linux内核wakeup source机制详解:从原理到调试实践 做低功耗调试这几年我打交道最多的机制之一就是 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.hstruct 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 也只会变成一次 trueactive_count 不会凭空多加。所有这些操作都在 ws-lock 自旋锁的保护下完成所以即使驱动在中断上下文调用激活接口也不会出现数据竞争。我不建议驱动开发者在自己的代码里再包一层锁去封装这些函数框架已经做完了这层工作多包一层反而容易引入死锁。这里埋着一个设计点last_time 记录的是这次激活的起始时刻deactivate 时用它来计算本次激活持续了多久并把结果累加到 total_time。这个耗时数据用的是纳秒精度最终在 debugfs 里展示时再换算成毫秒。不要小看这个耗时统计排查“某事件频繁拉高系统功耗”时它就是直接的定量证据。2.3 和 suspend 主流程怎么配合wakeup source 之所以能成为 suspend 的裁判是因为 suspend 流程在关键节点检查了它。核心函数是 pm_wakeup_pending()它内部维护一个全局唤醒事件计数并与 suspend 决策阶段记录下的快照做比对只要期间有新事件发生就返回 truesuspend 流程随即终止。这样做的效果等价于检查“这段时间内是否有唤醒源被激活过”任何驱动通过 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 读懂 debugfswakeup_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 里那张账本始终清晰睡眠问题就一定有迹可循。希望这篇文章能帮你少走点弯路多睡几个安稳觉。
返回列表