ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux唤醒源框架深度解析:从数据结构到功耗调试实战

Linux唤醒源框架深度解析:从数据结构到功耗调试实战 1. 唤醒源框架到底在解决什么问题第一次接触 wakeup source 的人多半是在调试一个设备明明没人用电却掉得飞快的板子。你打开/sys/kernel/debug/wakeup_sources看到一长串名字和一堆数字却不知道从哪看起。这个框架存在的意义说白了就一句话让系统知道谁有资格把 CPU 从睡梦里叫醒并且记录下每一次唤醒的来龙去脉。在嵌入式 Linux 和移动设备上功耗是硬指标。系统进入 suspend 之后绝大部分硬件断电、时钟停摆只有少数几个模块还留着耳朵听外界动静——比如电源键、RTC 闹钟、USB 插入检测、WiFi 唤醒包。这些能触发唤醒的源头就是 wakeup source。如果不管住它们任何一个驱动随手申请一个中断就能把系统拽醒那省电就无从谈起。这套框架属于 PM Core电源管理核心的一部分向上服务于 autosleep自动休眠机制向下约束各个设备驱动。它要回答三个问题谁可以唤醒系统、当前有没有人正在阻止休眠、每次唤醒是谁干的。把这三点搞清楚你排查功耗问题就有了抓手。我见过不少做嵌入式 Linux 项目的同学能背出linux常用命令大全能熟练linux系统安装python但一碰到 suspend/resume 就抓瞎。原因就在于唤醒源这套东西平时不显山不露水只有出问题的时候才跳出来。所以这篇我打算把框架从头到尾捋一遍包括数据结构、注册流程、sysfs 接口、和 autosleep 的配合以及我自己踩过的几个坑。适合谁看做嵌入式 Linux 驱动开发的、搞功耗优化的、准备linux面试题里电源管理相关问题的以及想真正理解linux底层原理的运维和系统工程师。不需要你已经是内核高手但至少得能看懂 C 结构体和基本的设备模型概念。2. 核心数据结构与设计思路拆解2.1 为什么要有 wakeup source 这个概念在没有这套框架的年代驱动想唤醒系统怎么办直接调用enable_irq_wake()把中断设成唤醒源就完事了。问题是没人统计、没人管理、没人负责。A 驱动开了唤醒忘了关B 驱动也开了一个系统想休眠的时候根本不知道到底还有多少设备处于可唤醒状态只能硬着头皮睡结果就是频繁被无关中断吵醒。wakeup source 的核心设计思想是把唤醒能力抽象成一个可计数、可统计、可查询的对象。每个 wakeup source 有一个引用计数active_count谁需要它保持唤醒能力就__pm_stay_awake()用完了就__pm_relax()。当计数归零这个源就不再阻止系统休眠。同时框架还记录event_count唤醒事件次数、wakeup_count累计唤醒次数、last_change最后一次状态变化时间等统计信息方便事后分析。这个设计有点像引用计数管理内存谁用谁加用完就减减到零就释放。区别在于这里释放的是对系统休眠的阻止权。2.2 关键结构体逐字段拆解核心结构体是struct wakeup_source定义在include/linux/pm_wakeup.h。我挑几个最关键的字段说struct wakeup_source { const char *name; // 名字sysfs 里显示的就是它 struct list_head entry; // 挂到全局链表 struct wakeup_source *parent; // 父子关系用于聚合统计 unsigned long flags; // 状态标志 spinlock_t lock; // 保护计数的自旋锁 unsigned long active_count; // 活跃引用计数 unsigned long event_count; // 事件计数 unsigned long wakeup_count; // 唤醒次数 ktime_t active_time; // 累计活跃时长 ktime_t total_time; // 累计总时长 ktime_t max_time; // 单次最长活跃时长 ktime_t last_time; // 上次状态变化时间 ... };active_count是最重要的字段。它大于 0 表示这个源正在阻止系统休眠等于 0 表示它当前不活跃。注意区分active_count和event_count前者是引用计数后者是事件次数。一个源可能被唤醒了很多次event_count 高但当前没人引用它active_count 为 0。flags里有个WAKUP_SOURCE_IN_PROGRESS标志表示正在处理唤醒事件这个在调试竞态问题时很有用。还有WAKUP_SOURCE_AUTO_CREATED标记这个源是框架自动创建的比如设备注册时自动生成而不是驱动手动创建的。parent字段是后来加的用于处理设备树里的父子设备关系。子设备的唤醒统计可以聚合到父设备上这样你在 sysfs 里看父设备就能知道整个子系统的唤醒情况不用一个个翻。2.3 全局链表与锁的保护所有 wakeup source 都挂在全局链表wakeup_sources上由wakeup_sources_lock保护。这个锁是读写锁rwlock因为读操作比如 sysfs 查询远多于写操作注册/注销。static LIST_HEAD(wakeup_sources); static DEFINE_RWLOCK(wakeup_sources_lock);为什么用读写锁而不是普通自旋锁因为wakeup_sources的读取非常频繁——每次系统尝试进入 suspend都要遍历整个链表检查有没有活跃的源。用读写锁能让多个读者并发只有注册和注销时才需要写锁独占。这个选择在高并发场景下能明显减少锁竞争。不过要注意active_count的增减用的是每个源自己的lock自旋锁而不是全局读写锁。这样设计是为了减少锁粒度修改某个源的计数不需要锁住整个全局链表。全局锁只在链表结构变化增删节点时才需要。3. 唤醒源的注册、注销与生命周期管理3.1 手动注册wakeup_source_register驱动想创建一个唤醒源最直接的方式是调用wakeup_source_register()struct wakeup_source *ws; ws wakeup_source_register(dev, my_device_wakeup); if (!ws) return -ENOMEM;第一个参数是关联的设备指针可以为 NULL第二个是名字会出现在 sysfs 里。这个函数内部会分配内存、初始化结构体、把源挂到全局链表并在 sysfs 里创建对应的目录和属性文件。注册完之后驱动在需要阻止休眠时调用__pm_stay_awake(ws)处理完事件后调用__pm_relax(ws)。注意这两个函数是内部版本不会触发 wakeup_count 的更新对应的公开版本pm_stay_awake()和pm_relax()会额外处理一些统计和通知逻辑。在中断上下文里只能用带下划线的版本因为它们不睡眠。3.2 自动注册设备树与 dev_pm 的联动更多时候你不需要手动注册。当设备通过设备模型注册并且设备树里标记了wakeup-source属性PM Core 会自动为这个设备创建一个 wakeup source。名字通常就是设备名。my_device: my_device12340000 { compatible vendor,my-device; interrupt-parent gic; interrupts 0 50 4; wakeup-source; // 这一行让框架自动创建唤醒源 };这个自动机制的好处是省事坏处是名字可能不够直观而且你没法在驱动里直接拿到struct wakeup_source指针去精细控制。所以实际项目里如果需要对唤醒行为做精细管理我还是建议手动注册名字起得清楚一点比如wifi_wake、tp_irq_wake这种一看就懂的。3.3 注销与资源释放驱动卸载或者设备移除时必须调用wakeup_source_unregister(ws)。这个函数会把源从全局链表摘下来释放 sysfs 节点最后释放内存。这里有个坑如果注销时active_count还大于 0说明还有人在引用这个源。框架会打印警告但不会阻止注销。结果就是内存被释放了但别处还拿着指针后续__pm_relax()就会踩到已释放内存典型的 use-after-free。我遇到过好几次这种崩溃最后都是靠KASAN定位到某个驱动忘了配对 relax。注意注册和注销必须成对出现且要保证注销前所有 stay_awake 都已经 relax。可以在注销前加一句WARN_ON(ws-active_count)做自检。4. sysfs 接口与调试实战4.1 /sys/kernel/debug/wakeup_sources 怎么读这是排查功耗问题最重要的一个文件。它不在/sys/power下而在 debugfs 里所以得先挂载 debugfsmount -t debugfs none /sys/kernel/debug 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 power_button 0 3 3 0 0 1234 567 890 0 wifi_wake 1 15 15 0 12345 67890 2345 67890 12345各列含义列名含义name唤醒源名字active_count当前活跃引用计数大于 0 表示正在阻止休眠event_count事件触发次数wakeup_count实际唤醒系统次数expire_count超时次数配合 timeout 使用active_since当前已活跃的时长毫秒total_time累计活跃总时长max_time单次最长活跃时长last_change距上次状态变化的时间prevent_suspend_time阻止休眠的总时长排查思路很直接先看 active_count 非零的源。如果系统进不了 suspend八成就是某个源的 active_count 一直没归零。找到它再去看对应的驱动代码检查 stay_awake 和 relax 是否配对。4.2 /sys/power/wakeup_count 与 autosleep 的配合/sys/power/wakeup_count是另一个关键接口。它的值等于所有唤醒源event_count的总和。用户空间在写/sys/power/state进入 autosleep 之前会先读一次 wakeup_count然后把这个值写回去。如果写入的值和当前值不一致说明期间有唤醒事件发生写入会失败系统就不进入休眠。这个机制叫竞态窗口保护从用户空间决定休眠到内核真正开始休眠之间如果有中断进来wakeup_count 会变化写入失败避免刚决定睡就被吵醒的尴尬。# 典型的 autosleep 使用流程 echo mem /sys/power/state # 或者用 autosleep echo 1 /sys/power/autosleep # 开启自动休眠开启 autosleep 后只要没有活跃的 wakeup source系统就会自动尝试进入 suspend。这就是为什么active_count的管理如此重要——它直接决定了系统能不能睡。4.3 用 ftrace 追踪唤醒事件sysfs 只能看结果想看过程得用 ftrace。内核里有个wakeup_eventstracepoint可以追踪每次唤醒的细节cd /sys/kernel/debug/tracing echo 1 events/power/wakeup_source_activate/enable echo 1 events/power/wakeup_source_deactivate/enable cat trace_pipe这样你能实时看到哪个源被激活、哪个被释放以及调用栈。定位谁在偷偷唤醒系统特别有效。我一般会配合trace-cmd用把数据抓下来慢慢分析比盯着 trace_pipe 刷屏舒服多了。5. 与 autosleep、runtime PM 的协作关系5.1 autosleep 的判定逻辑autosleep 的核心逻辑在kernel/power/autosleep.c。它维护一个工作队列当系统空闲时尝试进入 suspend。判定能不能睡的条件之一就是所有 wakeup source 的 active_count 都为 0。static bool pm_wakeup_pending(void) { // 检查是否有活跃的唤醒源 // 检查 wakeup_count 是否变化 ... }如果某个源一直活跃autosleep 就会一直重试但每次都失败。这时候你会在 dmesg 里看到类似PM: Some devices failed to suspend或者active wakeup source: xxx的日志。看到这种日志直接去查那个源就对了。5.2 runtime PM 与系统 PM 的区别很多人会把 runtime PM 和系统 suspend 搞混。简单说runtime PM 是单个设备在不使用时进入低功耗系统还在运行系统 suspend 是整个系统进入低功耗。wakeup source 主要服务于后者但两者有交互。一个设备如果 runtime PM 处于 active 状态它可能会持有 wakeup source从而阻止系统 suspend。反过来系统 suspend 时所有设备都会被 runtime suspend。理解这个层次关系才能理清为什么设备明明 runtime suspend 了系统还是睡不下去这类问题。5.3 唤醒源与设备树的 wakeup-source 属性前面提过设备树里的wakeup-source属性。它的作用是在设备注册时自动创建唤醒源并把设备的power.wakeup指针指向它。驱动可以通过device_may_wakeup(dev)判断当前是否允许这个设备唤醒系统。if (device_may_wakeup(pdev-dev)) enable_irq_wake(irq);这个判断很重要用户空间可以通过/sys/devices/.../power/wakeup文件开关某个设备的唤醒能力。驱动必须尊重这个设置否则用户关了唤醒设备还在偷偷唤醒就是 bug。6. 常见问题与排查技巧实录6.1 系统无法进入 suspend 的排查流程这是最高频的问题。我的排查顺序是看 dmesg搜索wakeup source、PM:、suspend关键词通常会有明确提示。看 wakeup_sources找 active_count 非零的源。看 wakeup_count确认是否有事件在持续触发。用 ftrace追踪 activate/deactivate 事件看调用栈。有一次我遇到一个 WiFi 驱动active_count 一直是 1。查代码发现它在 probe 里调了pm_stay_awake()但在 remove 里才 relax。结果设备一直不 remove源就一直活跃。改成事件处理完就 relax 就好了。6.2 active_count 泄漏的典型场景active_count 泄漏基本就是 stay_awake 和 relax 不配对。常见场景错误路径忘了 relax函数中间 return 了没走到 relax。中断里 stay线程里 relax但线程没跑比如工作队列被取消。多次 stay 只 relax 一次引用计数是累加的stay 两次就得 relax 两次。排查方法在__pm_stay_awake和__pm_relax里加 printk 打印调用栈对比次数。或者用perf采样看谁在频繁调用。6.3 唤醒源名字冲突与统计混乱如果两个设备用了同一个名字注册唤醒源sysfs 里会出现两个同名条目统计就乱了。框架本身不检查重名所以起名字要带上设备标识比如wifi_wake和bt_wake别都用wake。另外自动创建的源名字就是设备名如果设备树里节点名起得随意sysfs 里也会很难看。建议设备树节点名规范一点。6.4 常见问题速查表现象可能原因排查方法系统无法 suspend某源 active_count 非零查 wakeup_sources频繁被唤醒某源 event_count 飙升ftrace 追踪事件唤醒后立即又睡唤醒源处理完就 relax 了检查是否需要保持活跃注销时崩溃active_count 未归零加 WARN_ON 自检统计数字异常名字冲突或父子关系错检查注册名字和 parent6.5 几个我踩过的坑坑一在原子上下文调用会睡眠的函数。pm_stay_awake()内部可能拿 mutex中断里只能用__pm_stay_awake()。我一开始没注意在中断处理里调了公开版本直接报 scheduling while atomic。坑二忘记处理 device_may_wakeup。用户通过 sysfs 关了唤醒驱动还在 enable_irq_wake结果就是用户以为关了实际没关。这个在功耗测试时特别容易被误判。坑三autosleep 和手动 suspend 混用。同时开 autosleep 又手动写 state行为会很奇怪。选一种方式就好我一般调试时用手动产品里用 autosleep。坑四wakeup_count 写入失败没处理。用户空间写 wakeup_count 失败是正常的说明有事件但有些脚本没检查返回值以为睡了实际没睡导致测试结果失真。7. 从框架到实战的几点体会把 wakeup source 框架吃透之后你会发现它其实是 PM Core 里设计得比较优雅的一块。引用计数 全局链表 sysfs 暴露三件套组合起来既保证了内核态的严谨又给用户态留了足够的观测和控制手段。实际做项目时我的习惯是每个可能唤醒系统的设备注册一个名字清晰的唤醒源在中断处理里 stay在处理完成后 relax并且严格检查 device_may_wakeup。同时在产品测试阶段定期 dump/sys/kernel/debug/wakeup_sources观察有没有异常的活跃源或事件计数。这套流程跑下来功耗问题基本能在早期发现不用等到用户投诉待机掉电快才去救火。最后分享一个小技巧如果你怀疑某个源在捣鬼但又不想改驱动重新编译可以直接在 sysfs 里操作。虽然不能直接改 active_count但可以通过/sys/devices/.../power/wakeup关掉设备的唤醒能力快速验证是不是它的问题。验证完再回去改代码效率高很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进