ARTICLE DETAIL

资讯详情

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

Linux PM wakeup source框架详解:结构与驱动调试

Linux PM wakeup source框架详解:结构与驱动调试 Linux PM 这个系列写到第十六篇wakeup source 这个主题早就该单拎出来说清楚了。前面聊 autosleep、suspend 状态机、wakeup_count 的时候一直有人在问/sys/kernel/debug/wakeup_sources里的那些节点到底是怎么来的驱动上报唤醒事件走的是哪条路径又为什么有的设备明明中断能触发却叫不醒系统。这篇文章就把 wakeup source 框架整个剥开来看它的核心数据结构、链表怎么维护、驱动怎么接入、线上怎么排查以及我在实际项目里踩过的几个坑。先说结论wakeup source 是内核 PM core 用来统一管理“谁能唤醒系统”和“每次唤醒持续多久”的一层抽象。它把 RTC、GPIO、网卡、输入设备、传感器这些来源都归一到一张全局链表上suspend 过程中随时检查这张链表一旦有活跃的唤醒源就能决定是否中止睡眠同时还能统计每个源的活跃时长和事件次数帮助判断掉电异常。这篇内容适合正在做底层驱动、跑过 suspend 调试、或者被功耗问题折磨过的内核开发者。1. wakeup source 到底在管什么1.1 从一次“假睡”现象说起我最早接触 wakeup source 是在一块 ARM 嵌入式板卡上。现象很典型系统进入 suspend 之后按音量键屏幕能亮按电源键也能亮但板子上的一个 GPIO 唤醒按键怎么按都没反应。查了硬件原理图GPIO 连接的是 SoC 的一个外部唤醒管脚复用功能开好了中断也注册了suspend 状态下应该能唤醒才对。最后发现这个 GPIO 按键驱动里面没有向 PM core 注册任何 wakeup source中断触发后只是简单置了一个标志位系统此时已经关掉了大部分外设时钟中断处理函数能跑但整个 PM 流程没有收到“这个事件需要唤醒系统”的通知suspend 状态根本不会被打断。这就是 wakeup source 要解决的第一个问题让一个外部事件在系统深度睡眠时能够被 PM core 识别为合法唤醒源并走完整的唤醒退出流程。换句话说它不是单纯的中断开关而是中断与电源管理状态机之间的一个“身份登记册”。1.2 与 suspend 流程的关系理解 wakeup source 之前得先明白它在 suspend 流程里扮演什么角色。标准 Linux suspend 过程会经历 freeze、prepare、dpm_suspend、suspend_enter 等多个阶段每一层都在为真正睡下去做准备。如果某一步发现有外部事件需要响应系统就不能继续往下睡必须回到正常工作状态。PM core 判断该不该中止睡眠靠的是一系列 wakeup event 状态而 wakeup source 正是这些状态的来源。驱动上报一个唤醒事件后这个事件会被叠加到某个 wakeup source 上比如调用pm_wakeup_event(dev, 0)内核会去找到这个设备对应的 wakeup source更新它的活跃状态和事件计数。suspend 路径上会在多个检查点查看是否存在 pending 的唤醒事件只要发现有事件就放弃这次睡眠让系统回到 active。这里有个容易混淆的点不是所有中断都能中止 suspend只有在驱动里显式上报、并且这个上报被 PM core 记录到 wakeup source 的事件才会参与判断。1.3 一个真实场景夜间掉电 10% 的元凶再讲一个更常见的场景。用户反馈 Android 设备一夜待机掉电严重拔掉充电器放置 8 小时电量从 80% 掉到 67%。看cat /sys/kernel/debug/wakeup_sources发现一个名为wlan_rx_wake的源active_since一直不为 0event_count只有两次但total_time接近两小时。这个现象说明网卡在睡眠期间持续收到某些广播包或误触发中断wakeup source 一直保持活跃系统频繁被唤醒无法进入真正的低功耗状态。解决办法是调整网卡驱动的唤醒阈值或者在处理完事件后尽快调用去活接口让系统重新睡下去。这个例子点出了 wakeup source 的第二个核心职责统计。它不仅管能不能唤醒还管每次唤醒活跃了多久、一共触发过几次这些数据是判断功耗异常的直接证据。2. 框架内部数据结构与链表管理2.1 struct wakeup_source 的核心字段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; ktime_t total_time; ktime_t max_time; ktime_t last_time; unsigned long active_since; unsigned long expire_count; unsigned long event_count; unsigned long active_count; unsigned int active:1; bool autosleep_enabled; };不是每个成员都必须在驱动里手动控制。name建议起一个定死的字符串用来在 debugfs 里标识这个源entry是挂在全局链表wakeup_list上的节点lock是保护这个结构体的自旋锁timer用于超时自动去活剩下的total_time、max_time、event_count、active_count都是统计字段。从概念上讲这个结构体就像一本账本记录了一个唤醒源从注册到现在的所有活跃情况。active位表示当前是否正在活跃状态如果为 1且长时间没有去活就说明有唤醒源卡住了功耗问题多半就出在这里。2.2 注册、激活与去活全局链表由drivers/base/power/wakeup.c管理。一个 wakeup source 想要进入这张链表需要调用wakeup_source_add()或者通过wakeup_source_register()一步到位后者先创建并初始化结构体再加入链表并设置设备的power.wakeup指针。激活发生在事件上报时。驱动调用wakeup_source_report_event()内部会根据参数决定是立即激活还是带超时地激活。如果该 source 之前处于非活跃状态内核会把它标为 active记录active_since并增加event_count和active_count。去活则由驱动显式调用wakeup_source_deactivate()或者由超时定时器自动触发。整个流程可以简化为三段驱动在 probe 阶段注册一个 wakeup source挂到全局链表。中断/事件到来时上报source 进入 active 状态。事件处理完成或超过保持时间source 退出 active 状态统计字段被更新。如果某个源长期停在 active 状态active_since会持续累加后续排查就能一眼看到异常。2.3 定时器与自动去活机制为什么需要超时去活因为中断处理函数只负责上报事件后续真正处理可能需要很长时间甚至有时事件上报之后驱动忘记去活。如果 wakeup source 永远处于活跃状态suspend 会一直被中止系统根本睡不下去。内核用一个高精度定时器来解决这个问题。当驱动上报事件时可以同时指定一个超时时间例如pm_wakeup_event(dev, 500)表示这个事件在 500 毫秒后自动去活即使驱动没有显式处理也不会卡死。这个设计非常实用尤其是对那些较新的、有突发性的外围设备比如传感器数据线、无线上报等。但如果超时时间设得太短可能事件还在处理中就被强制去活导致系统在真正处理完成前又被其他事件拉起所以实际参数需要结合业务压测来定。3. 驱动接入API 用法与完整示例3.1 常用 API 速查驱动开发者最常用的不是wakeup_source_create()这种底层函数而是跟设备绑定的高层接口。我整理了一个速查表API作用典型调用场景device_init_wakeup(dev, true)初始化设备唤醒能力并创建关联的 wakeup sourceprobe 阶段启用设备唤醒device_init_wakeup(dev, false)禁用设备唤醒能力释放相关 wakeup source设备移除或不再需要唤醒device_may_wakeup(dev)判断设备是否具备唤醒能力中断处理时快速判断是否能上报pm_wakeup_event(dev, msec)上报一次唤醒事件带超时去活中断处理函数中调用wakeup_source_report_event(ws, msec)直接向指定 wakeup source 上报事件非设备场景按 source 管理dev_pm_set_wakeirq(dev, irq)指定一个 wake irq并参与唤醒管理有些设备支持独立唤醒中断dev_pm_arm_wake_irq(dev)使能唤醒中断suspend 阶段前调用dev_pm_disarm_wake_irq(dev)失能唤醒中断resume 之后调用注意device_init_wakeup(dev, true)不等于立即让设备可以唤醒系统它只是把设备对应的 wakeup source 建好真正是否允许唤醒还要看device_may_wakeup()的返回值。很多驱动在 probe 里调用了device_init_wakeup但实际没有处理suspend/resume中的配置导致系统里能看到设备但它上报的事件并不能正确唤醒。3.2 一个 RTC 唤醒驱动的接入示例拿 RTC 唤醒来讲一下完整接入方式这是最经典也是我验证平台必测的用例。RTC 有一个报警中断到了设定时间会触发系统需要从 suspend 中唤醒。驱动里的思路是这样的#include linux/pm_wakeup.h #include linux/interrupt.h static struct wakeup_source *rtc_ws; static irqreturn_t rtc_alarm_irq_handler(int irq, void *dev_id) { // 上报唤醒事件并设置 100ms 超时 wakeup_source_report_event(rtc_ws, 100); /* 其他 RTC 状态处理 */ return IRQ_HANDLED; } static int rtc_probe(struct platform_device *pdev) { int irq platform_get_irq(pdev, 0); rtc_ws devm_kzalloc(pdev-dev, sizeof(*rtc_ws), GFP_KERNEL); rtc_ws-name my_board_rtc; wakeup_source_add(rtc_ws); device_init_wakeup(pdev-dev, true); return devm_request_irq(pdev-dev, irq, rtc_alarm_irq_handler, 0, rtc-alarm, NULL); }如果走纯设备接口中断处理函数里也可以直接这样写pm_wakeup_event(pdev-dev, 0);pm_wakeup_event会自己去查找这个设备上绑定的 wakeup source。注意第二个参数为 0 时表示不设置超时由驱动自行决定去活时机。这里强烈建议非特殊场景都设一个合理的超时值防止驱动处理异常后出现 active 状态卡死。wakeup_source_add()只把 source 加入链表并不把一个struct wakeup_source自动绑定到某个设备上。如果之后还想用pm_wakeup_event(dev, ...)上报就需要在device_init_wakeup()之后设备内部已经创建了关联 source或者干脆直接用wakeup_source_report_event()操作你自己管理的 source。两种方法我都用过个人偏好是在复杂驱动里自己显式维护wakeup_source因为调试时更直观而且不会跟设备框架耦合太深。3.3 说说不那么直观的 wake irq近几版内核里dev_pm_set_wakeirq()提供了另一条路设备本身有一个专门用于唤醒的中断和普通功能中断共享或不共享同一个物理中断线。把这条 irq 设置成 wake irq 后PM core 会在 suspend 流程中帮你做 arm/disarm 操作不需要驱动在 suspend/resume 回调里反复enable_irq_wake()、disable_irq_wake()。这个设计对蓝牙、触摸屏、PMU 这类设备很友好。不过它不直接创建一个独立的 wakeup source而是关联到设备现有的 wakeup source 上并且依赖device_init_wakeup()先设置好电源管理元数据。新手上路容易漏掉顺序我见过不少驱动在 probe 里只调了dev_pm_set_wakeirq()没调device_init_wakeup()结果调试时 wakeup_source 列表里压根看不到对应节点。4. 调试手段从 debugfs 到 tracepoint4.1 用 /sys/kernel/debug/wakeup_sources 看状态实践中最常用的就是 debugfs 的wakeup_sources节点。Linux 默认编译时如果开了CONFIG_PM_DEBUG通常就能用。查看命令cat /sys/kernel/debug/wakeup_sources输出类似这样name active_count event_count active_since total_time max_time change_count my_board_rtc 1 12 0 0.000 120.000 12 wlan_rx_wake 5 34 0 12.500 300.000 5 gpio_key 0 1 0 0.000 0.000 1每一个字段都值得盯一下。active_count表示这个源进入 active 状态的次数event_count是上报事件的次数实际通常和 active_count 同步active_since如果是非 0说明它正处于 active 状态而且这个值表示活跃开始的 tick 时间两列之和可以换算成具体时刻total_time是累计活跃时长max_time是历史上单次最长活跃时长。做功耗基线时我会先cat一次清空后的空载状态再跑场景最后对比变化量定位是哪一路唤醒源在捣乱。如果/sys/kernel/debug节点不存在先检查内核是否开了CONFIG_DEBUG_FS同时看debugfs是否挂载在/sys/kernel/debug如果没挂载执行mount -t debugfs none /sys/kernel/debug4.2 查看 sysfs 下的 wakeup 设备属性除了 debugfs/sys/class/wakeup/wakeupN/目录下也有每个唤醒源的属性节点/sys/class/wakeup/wakeup0/name /sys/class/wakeup/wakeup0/active /sys/class/wakeup/wakeup0/event_count /sys/class/wakeup/wakeup0/active_count /sys/class/wakeup/wakeup0/total_time_ms这些节点的信息其实和 debugfs 里的同源但更适合脚本监控。比如压测时每秒抓一次active和total_time_ms批量看趋势比盯表单直观。有些设备会在wakeupN/device下暴露 sysfs 链接可以快速反查设备路径。在排查“某个网卡唤醒过多”这类问题时我一般先ls /sys/class/wakeup/再挨个cat name对需求不明确的唤醒源直接在驱动侧先关闭。4.3 用 ftrace 抓唤醒历史只靠统计信息有时候看不出来唤醒的因果关系比如到底是谁在这个时刻触发了一次 wakeup event。此时可以用内核的 power trace 事件。常见事件有wakeup_source_activate和wakeup_source_deactivate。在支持 tracefs 的系统上执行echo 0 /sys/kernel/tracing/tracing_on echo 1 /sys/kernel/tracing/events/power/wakeup_source_activate/enable echo 1 /sys/kernel/tracing/events/power/wakeup_source_deactivate/enable echo 1 /sys/kernel/tracing/tracing_on sleep 5 cat /sys/kernel/tracing/trace看到类似rtc_alarm_irq_handler - __pm_wakeup_event - wakeup_source_report_event的调用链后就能精确定位到是哪个驱动哪一行代码把系统拉起来的。如果是没有 tracepoint 的老内核可以退而求其次用echo name: wakeup_source_activate format:这种方式动态插入 kprobe但不推荐在量产版本上这么干。我通常只在本地调试时开 trace。5. 常见问题与排查思路实录5.1 一张问题速查表现象可能原因排查方向suspend 总是被立即中止某个 wakeup source 一直 activewakeup_sources看active_since和active_count某个唤醒源 event_count 狂涨设备中断频繁触发了上报驱动侧降低上报阈值调整中断配置设备注册后无法唤醒系统device_may_wakeup()为 false或中断没有使能唤醒检查device_init_wakeup和enable_irq_wakeactive_since 一直累加驱动上报后没有去活检查事件上报参数补调用去活接口卸载驱动 panicwakeup source 指针泄漏或重复释放使用devm_kzalloc管理生命周期total_time 异常偏大事件上报没带超时全局搜索pm_wakeup_event(x, 0)场景5.2 我在实际项目里踩过的三个坑第一个坑是命名不清晰。一开始项目里多个驱动共用同一个 namewakeup_irq线上看 wakeup_sources 根本分不清谁是谁。后来统一命名规范把模块名_管脚号这种细节写进去排查效率一下子提上来了。命名这件事看似小事真到现场抓问题的时候含含糊糊的 name 会浪费很多时间。第二个坑是事件上报太频繁导致功耗恶化。一个触屏驱动在中断处理函数里无条件调用pm_wakeup_event(dev, 0)结果每次触摸都让系统保持一段活跃时间息屏待机时只要偶尔有误触摸功耗就压不住。后来改成只有检测到有效手势才上报并且带 200ms 超时又加了一层去抖。实际上这种场景需要结合业务定义不能一刀切。第三个坑跟 driver 生命周期有关。早期我在 probe 里手动wakeup_source_register()在 remove 里只调了wakeup_source_unregister()但是注册时用的是静态分配的struct wakeup_source没注意 devm 资源管理模块反复加载卸载时出现 use-after-free。后来养成了习惯能用devm_wakeup_source_register()就用它让内核负责资源释放别再自己裸奔管内存。5.3 线上事故复盘一个“永远睡不着的”蓝牙设备最后分享一个完整的排查流程算是对前面内容的整体应用。有次带蓝牙耳机的平板电脑待机功耗异常看 wakeup_sources 里出现一个bt_host_wakeevent_count只有几次但active_since一直在涨。我先禁用蓝牙的 wakeirq问题消失说明事件来自蓝牙唤醒引脚。进一步查 code发现蓝牙驱动上报事件后如果蓝牙协议栈晚 10 秒才处理完wakeup source 这个期间一直 active系统在 autosleep 里反复尝试 suspend反复被打断。修复方式是在上报事件时带一个 1000ms 的超时同时把蓝牙 HCI 协议栈的处理线程优先级调高缩短处理时间。修改后重新看 wakeup_sourcesactive_since很快归零待机功耗也回到正常。这个案例再次验证了一个经验wakeup source 活跃时间越长系统睡眠窗口越少功耗问题越明显。要搞懂 wakeup source说到底就是搞懂“谁在睡眠期间还醒着”。它不像中断子系统那么显眼但所有被唤醒后的链路最终都要回到这份名单上。调试时多抓几组不同场景下的wakeup_sources输出建立起自己对设备唤醒行为的直觉比死记 API 结构有用得多。这套框架的细节虽然多但只要数据链路捋顺了遇到奇葩功耗问题就有抓手不会再一头扎进驱动代码里瞎翻。
返回列表