
调试Linux功耗问题的日子久了你会慢慢形成一种条件反射系统睡不下去第一反应不是去翻调度器不是去查哪个进程不睡而是打开/sys/kernel/debug/wakeup_sources看一眼或者执行echo mem之后盯着dmesg里那句Wakeup source XXX。因为大多数睡不下去刚睡就醒待机功耗异常的问题最后都能追到一个活跃的wakeup source身上。wakeup source唤醒源是内核电源管理子系统里最基础、也最容易被误用的一层。很多驱动工程师知道在代码里调用pm_stay_awake和pm_wakeup_event但真要问它和suspend流程是怎么配合的、autosleep依赖它的什么状态、/sys/power/wakeup_count这个节点到底怎么用能说清楚的人其实不多。这篇文章基于我最近对内核kernel/power/wakeup.c和相关子系统的整理把wakeup source的前世今生、数据结构、激活路径、suspend检查点以及基于它的功耗排障方法完整过一遍。无论你是做驱动开发、BSP移植还是单纯想弄明白系统到底怎么决定自己能不能睡这篇都值得看完。1. 先搞清楚为什么Linux需要wakeup source这一层这个问题如果没想明白后面看代码很容易一头雾水。我先把需求场景摆出来。正常使用手机或者嵌入式设备时系统的状态是大部分时间在睡觉偶尔醒来处理一点事情然后又睡。这里有两个角色一方是“想要睡觉的系统”另一方是“可能随时产生事件的设备”。设备产生的事件比如触摸按键、插入USB、网络包到达、充电器插拔都需要系统从低功耗状态醒来处理。麻烦的地方在于设备和系统做决策的时间点是不同的——设备说“我马上要有事件”系统说“我准备睡了”这中间如果协调不好要么丢失事件要么系统反复睡醒。1.1 没有wakeup source之前唤醒管理是怎么做的在比较老的实现里设备唤醒主要依赖两样东西enable_irq_wake和平台相关的低功耗回调。驱动在suspend的时候把自己的中断配置成唤醒源然后系统进入睡眠设备产生中断芯片把CPU唤醒然后执行中断处理函数。这套机制的问题在于它只管“中断能把CPU拉起来”并没有一个统一的、事件级别的账本。比如某个设备的中断处理函数执行到一半需要系统继续处理后续工作又比如设备的事件在系统即将入睡的那一刻到达驱动已经响应了但系统的suspend流程并不知道“刚处理过一个事件”还是会继续睡。更麻烦的是当你想排查“昨晚设备被谁唤醒了三次”没有任何一个地方能给你答案。1.2 wakeup source的定位事件级抽象wakeup source做的事情是把“某个唤醒事件正在发生、正在被处理、或者刚刚发生过”这件事用一个统一的内核对象表达出来。任何子系统只要调用pm_stay_awake或者pm_wakeup_event就是在告诉电源管理核心“我这里有一个唤醒事件你别睡”或者“你刚醒再等一会儿再睡”。这样说可能有点抽象。你可以把wakeup source理解成宿舍楼门口的值班登记本每个设备相当于一个学生半夜想出门产生事件需要在登记本上写一笔“我出去了还没回来”。系统宿管想锁门睡觉前会翻一下登记本发现有人还没回来就不能锁门等所有登记项都销掉relax才允许锁门。这个登记本还记录了每个人出门的次数、在外面待了多久——这就是后面的统计数据。1.3 它要回答的四件事总结起来wakeup source这一层需要回答四个问题问题对应机制现在有没有唤醒事件正在处理ws-active、pm_wakeup_pending()最近一共发生过多少次唤醒事件event_count、active_count、wakeup_count每次唤醒持续了多久total_time、max_time、last_time是哪个设备、哪个驱动导致的name、dev、debugfs里的节点名这四个问题既服务于“系统能不能睡”的前置判断也服务于事后功耗排查。理解了这个定位后面读struct wakeup_source的每个字段就会非常顺畅。2. 数据结构与生命周期一个wakeup source从创建到注销wakeup source的主体代码在kernel/power/wakeup.c头文件在include/linux/pm_wakeup.h。别看文件不大里面做的事情不少。我先从结构体讲起。2.1 struct wakeup_source的每个字段都在记录什么直接看结构体以较新内核的注释为准struct wakeup_source { const char *name; struct list_head entry; int id; struct rcu_head rcu; bool active; bool autosleep_enabled; unsigned long total_time; unsigned long max_time; 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; struct device *dev; bool dev_ref; };一个字段一个字段说name这个唤醒源的名称调试的时候就是靠它区分谁是谁。常见的名字有设备名比如pmic、gpio-keys、touchscreen也有直接叫wakelock的。entry、id、rcu链表节点、唯一ID、RCU释放用。这些是框架管理用的驱动不用关心。active核心状态位。true表示当前正处于“活跃”状态也就是有一个唤醒事件正在处理或者被某个驱动“占着”。这个字段是pm_wakeup_pending()判断的重要依据。total_time这个wakeup source累计处于active状态的总时长单位纳秒。功耗排查最常用的字段。max_time单次active持续的最长时间。last_time最近一次状态变化的时间戳。start_prevent_time和prevent_sleep_time和autosleep配合使用的统计字段记录的是“因为我在active系统多等了多久没睡”。event_count事件计数。每次pm_wakeup_event或者pm_stay_awake触发都会增加这个值代表该唤醒源总共产生了多少次唤醒事件。active_countactive的次数relax_count通过pm_relax主动释放的次数expire_count通过超时机制自动释放的次数。wakeup_count这个字段比较特殊只在系统处于睡眠/唤醒过程中递增代表“作为实际唤醒来源”的次数。dev关联的设备指针。通过device_init_wakeup这类接口创建的唤醒源会绑到具体设备上。这里面注意一个容易混淆的点active不等于“正在唤醒系统”。它更准确的语义是“有一个未完成的事件或操作”。比如驱动在执行一个需要保持系统清醒的异步操作也会把一个wakeup source置为active。所以你在调试里看到一个wakeup source长期active不一定代表系统被疯狂唤醒很可能是某个驱动忘了释放。2.2 注册链路create、add、register之间的区别创建wakeup source有三级接口很多文档混着说实际语义完全不同struct wakeup_source *wakeup_source_create(const char *name); void wakeup_source_add(struct wakeup_source *ws); struct wakeup_source *wakeup_source_register(struct device *dev, const char *name);wakeup_source_create只做内存分配和基本初始化创建出来的对象还不在全局链表里不能被pm_wakeup_pending()看见。wakeup_source_add把对象挂进全局链表/rbtree并且会在/sys/kernel/debug/wakeup_sources里出现。从这一刻起它才是一个“有效”的wakeup source。wakeup_source_register是create add的组合同时可以传入一个struct device *建立关联。驱动里绝大多数场景用这个就行。struct wakeup_source *ws wakeup_source_register(dev, my-wakeup-source); if (!ws) return -ENOMEM;销毁对应的是wakeup_source_unregister它内部会先remove再destroy。注意如果一个wakeup source当前还是active状态就unregister内核会打印警告但不会帮你强制释放——这本身就是一个信号说明你的驱动逻辑有问题。2.3 设备和wakeup source是怎么挂上关系的设备模型这边有另外一套接口层次结构也容易绕device_init_wakeup(dev, enable)设置设备是否具有唤醒能力并初始化对应的wakeup source。device_set_wakeup_capable(dev, capable)只修改能力位wakeup_capable不创建wakeup source。device_wakeup_enable(dev)分配并注册wakeup source然后绑定到dev-power.wakeup。device_may_wakeup(dev)判断设备是否允许唤醒系统通常在suspend回调里做判断用。它们之间的关系是一个设备要先capable之后enable才有效。设备驱动在suspend回调里一般会先查device_may_wakeup(dev)为真才去配置唤醒中断。通过device_wakeup_enable创建的wakeup source名字默认取dev_name(dev)所以debugfs里会看到fe310000.serial这种设备地址开头的名字。我见过不少驱动把device_init_wakeup(dev, 1)和pm_stay_awake混着用但这两者解决的是不同的问题前者回答“这个设备能不能唤醒系统”后者回答“当前事件还没处理完系统先别睡”。3. 激活与去激活pm_stay_awake、pm_relax和pm_wakeup_event的完整语义这是驱动开发最常打交道的三个API。先记一个总原则凡是让wakeup source进入active状态的调用最终必须有一个对应的释放操作区别只在于是代码主动释放还是内核帮你定时释放。3.1 两种用法模型持续占用与一次性事件模型A持续占用。适用于异步操作典型的驱动代码是这样pm_stay_awake(dev); async_work ...; // 异步处理某件事 // 在异步工作的回调里 pm_relax(dev);pm_stay_awake底层调用__pm_stay_awake把设备的wakeup source置为active。pm_relax对应__pm_relax将其置为非active。这对接口必须成对而且要在同一逻辑里配对出现。很多人把pm_stay_awake写在中断处理里等异步工作做完后在另一个线程调pm_relax这是对的但要注意时序如果异步操作还没排上队而pm_relax被提前调了这个保护窗口就失效了。更好的做法是参考内核里drivers/usb或网络子系统的做法先pm_stay_awake再启动工作工作结束最后一步pm_relax。模型B一次性事件。适合“通知内核我这儿刚来了个事件”的场景用pm_wakeup_eventpm_wakeup_event(dev, jiffies_to_msecs(delay));或者直接操作wakeup source__pm_wakeup_event(ws, msecs);这个API的含义是立刻把这个唤醒源标记为active然后在msecs毫秒之后自动去激活。这个“延时释放”非常关键——它避免了一个经典的竞态中断处理程序刚上报完事件系统suspend流程正好执行到最后的检查点如果事件立刻释放suspend流程可能根本没检测到这次唤醒导致刚睡下去又被同一事件拉起来。延时让系统有机会“看见”这个事件从而取消本次睡眠。3.2 内核里的activate/deactivate到底做了什么pm_stay_awake和pm_wakeup_event最终都会调到wakeup_source_activate。这个函数做三件事置active标志、递增active_count、更新last_time时间戳如果autosleep开着还会记录start_prevent_time。然后是原子的全局统计更新wakeup_source_active_count。wakeup_source_deactivate则负责统计收尾now ktime_get_mono_fast_ns(); duration ktime_sub(now, ws-last_time); ws-total_time ktime_add(ws-total_time, duration); if (ktime_compare(duration, ws-max_time) 0) ws-max_time duration; ws-last_time now; ws-active false; wakeup_source_active_count--;它把这次active的持续时间累加到total_time同时更新max_time然后把全局活跃计数减一。后面我们在debugfs里看到的每一行统计数字就是这么一点点攒出来的。值得注意的是pm_wakeup_event(dev, X)的超时释放用的是内核定时器到期时自动调用deactivate。所以那种“设置了超时时间就能高枕无忧”的用法并不总是可靠——特别是事件处理时间超过了预设的超时值后半段就不能再阻止系统睡眠了。这也是为什么短事件用pm_wakeup_event、长事件务必手动管理pm_stay_awake/pm_relax。3.3 使用纪律和IRQ上下文注意事项从实际经验里总结几条都是踩过的坑第一不要在中断处理函数里长时间保持wakeup source active。中断上下文里调pm_wakeup_event是安全的因为__pm_wakeup_event可以用自旋锁保护而且有定时器兜底。但如果在中断里pm_stay_awake然后指望一个线程稍后pm_relax中间的时间窗口完全不受控一旦线程被调度延迟系统的睡眠被白白阻塞功耗问题就是这么来的。第二pm_relax 与 pm_stay_awake 要对称但不必在同一函数里。内核文档要求的是“最终对称”不是“成对出现”。有些驱动把release逻辑放在component unbind等收尾路径上虽然合法但很容易造成泄漏。调试阶段可以打开KASAN和lockdep主动检查这类不对称。第三小心wakeup_source_activate里的WARN_ONCE。如果内核日志里出现wakeup source XXX is already active的警告说明同一个wakeup source被连续两次置active而没有中间释放。这不是致命错误但几乎一定是驱动逻辑bug因为wakeup source在语义上不是一个计数器重复激活不会带来“叠加保护”的效果。4. suspend路径上的层层把关wakeup_count握手与pm_wakeup_pending很多人在这一块容易卡住到底什么时候系统会检查wakeup source为什么有时候明明有活跃的唤醒源系统照样睡了这就要把suspend流程完整看一遍。4.1 用户空间和内核的两次握手先说wakeup_count这个sysfs节点。它的存在是为了解决一个经典的竞态用户空间程序可能先决定“系统该睡了”然后做一系列准备工作比如通知其他进程、保存状态这些工作需要时间。在这段时间里如果某个设备产生了唤醒事件而系统没有感知到最终结果就是系统刚睡下去就被同一事件唤醒白白浪费一次功耗。握手过程是这样的用户空间读/sys/power/wakeup_count得到当前的值N。用户程序做准备工作。用户程序把N写回/sys/power/wakeup_count。内核比较当前wakeup_count和N是否一致。一致说明这段准备时间里没有新的唤醒事件内核记录saved_count N开启events_check_enabled写操作返回成功不一致说明期间有事件发生返回错误用户空间通常会重新从第1步开始。握手成功后用户空间再echo mem /sys/power/state真正进入睡眠。这套流程在Android的suspend系统中很常见。wakeup_count_store其实就是调pm_save_wakeup_countint pm_save_wakeup_count(unsigned long count) { ... if (count wakeup_count) { saved_count count; events_check_enabled true; ret true; } ... }而pm_wakeup_pending()的检查逻辑bool pm_wakeup_pending(void) { ... if (events_check_enabled) { if (wakeup_count ! saved_count) { /* 有事件发生过 */ ret true; goto out; } if (wakeup_source_active_count) { /* 有事件正在处理 */ ret true; goto out; } } ... }注意只有events_check_enabled为真检查才生效。内核在suspend路径上会主动通过pm_wakeup_clear()之类的调用开启事件检查让后续的pm_wakeup_pending()真正生效而应用层的wakeup_count握手可以让检查窗口和准备阶段严格同步这是最严谨的做法。4.2 睡眠过程中wakeup source在哪些节点被检查从echo mem /sys/power/state开始到真正进入平台睡眠中间有多个检查点。简单梳理一下路径pm_suspend入口做状态检查。suspend_prepare准备阶段冻结进程。try_to_freeze_tasks在冻结每个任务的过程中不断调用pm_wakeup_pending()一旦发现有唤醒事件冻结中止suspend 返回-EBUSY。suspend_devices_and_enter先dpm_suspend_start冻结设备再dpm_suspend执行设备suspend回调之后进入平台相关的suspend_enter。suspend_enter里在真正调用平台睡眠回调之前又会检查pm_wakeup_pending()。这里如果为真就不真正睡直接回退到resume路径。所以你看到的现象是一个事件如果在“进程冻结阶段”或者“设备suspend阶段”到达整个系统的入睡过程会被打断而不是等到睡下去以后才通过中断唤醒。这其实是一道保护避免了反复进出的无谓功耗。4.3 从睡到醒的路径还原系统真正睡下去之后唤醒的来源就是实实在在的硬件中断了。enable_irq_wake配置过的中断会把CPU从低功耗状态拉起来。中断子系统会把中断和对应的wakeup source挂钩这样系统在醒来后能知道“这次是被谁唤起的”。所以wakeup source的作用分成两个阶段入睡前它是“协议”——任何一个活跃的唤醒源都可以阻止系统入睡入睡后它是“记录”——中断来了要记账醒来后我们可以从统计里看到是谁干的。一个框架同时承担两个职责这正是它容易被搞混的地方。理解了这个双重身份再回头看pm_wakeup_pending以及wakeup_count的各种逻辑会清晰很多。5. autosleep与wakelock自动睡眠是怎么依赖wakeup source的如果你用过Android旧设备一定听过wakelock。今天的mainline内核里Android那套逻辑已经被重构成两层底层是wakeup source上层是wakelock接口。autosleep是另一个相当依赖wakeup source的机制。5.1 autosleep的基本逻辑没有活跃唤醒源就去睡autosleep的出发点很简单与其让用户空间程序反复检查空闲状态然后手动echo mem不如让内核自己盯着——如果所有wakeup source都是非active的说明系统是空闲的就尝试睡眠。内核里autosleep.c的核心逻辑就是在try_to_suspend中检查pm_wakeup_pending()没有pending事件才会真正发起suspend。当wakeup_source_active_count降到0autosleep就进入真正的suspend流程一旦有设备把wakeup source置activeautosleep会立即取消本轮suspend并且进入下一轮尝试。所以在开着autosleep的板子上你插上一个USB设备、触摸屏还亮着或者某个GPIO驱动的wakeup source一直不释放系统就会一直“睡不下去”。5.2 wakelock和wakeup source的继承关系用户空间的wakelock在/sys/power/wakelock节点。写入一个名字内核就创建一个对应的wakelock这个名字本质上对应一个内核wakeup source。旧的Androidwakelock语义是“我持有锁系统别睡”对应到wakeup source就是“我保持active系统别睡”。本质上wl_acquire对应__pm_stay_awakewl_release对应__pm_relax。区别在于wakelock还有引用计数同一个名字可以acquire多次release一次不会立刻释放只有计数归零才真正relax。而wakeup source本身没有这个“多重持有”的概念虽然有active_count统计但它不是一个锁。这个继承关系解释了为什么你在老的Android内核里看到大量“wakelock”相关分析而在新内核中却变成查找某个wakeup source——两者其实是同一个机制在不同层次上的表现。5.3 手动suspend和autosleep的差异手动suspend是“用户主动请求直接尝试进入睡眠”autosleep是“内核检测到空闲自动尝试睡眠”。两者的区别主要在于事件的检查窗口和重试机制autosleep每次尝试前都严格基于wakeup source的状态做判定一旦有活跃唤醒源就取消而且会反复尝试而手动suspend是单次请求检查的是发起请求前后那一小段时间的事件。这个差异也带来了一个常见困惑为什么我pm_stay_awake锁着手动echo mem系统还是能睡答案就在4.1节里的events_check_enabled。手动suspend路径如果没有走wakeup_count握手事件检查的开启时机可能已经错过了活跃窗口而autosleep每次都会重新开启检查所以活跃的wakeup source会被实打实地拦住。在做功耗验证时这两种路径的行为差异要特别留意否则你测出来的“能睡”和用户实际场景中的“睡不下去”会对不上。6. 用wakeup source排查功耗问题的实战方法最后这部分是实操。说一千道一万wakeup source最终要能帮我们定位问题否则全是空谈。6.1 debugfs里的wakeup_sources怎么读内核开启CONFIG_DEBUG_FS后可以直接看/sys/kernel/debug/wakeup_sources输出大概长这样name active_count event_count wakeup_count expire_count active_since total_time max_time last_change每一列的含义和结构体字段一一对应。排查时优先看三列active_since如果非0说明当前还在active、total_time累计阻塞时间、event_count事件次数。total_time特别大的通常就是“罪魁祸首”它可能一直在active或者频繁active很久。实际使用中可以用一行命令周期采样观察数值变化趋势cat /sys/kernel/debug/wakeup_sources | awk NR1 {print $1, $3, $7, $8}这样可以快速过滤出event_count在增长、total_time在增长的节点缩小嫌疑范围。6.2 一个典型的“睡不下去”排查过程我举个例子。某嵌入式板卡跑Android类系统测试发现待机电流比正常值大了50mA。第一步先确认系统到底睡没睡。打开/sys/kernel/debug/wakeup_sources发现一个名为rtc_alarm的wakeup sourceactive_since不为0event_count已经超过几万。这说明RTC驱动逻辑有问题它可能在每次中断处理时active但从未relax或者某条路径上定时器没取消。第二步进一步查驱动日志发现RTC alarm中断处理函数里调了pm_stay_awake但中断处理返回前没有pm_relax而另一个线程里的relax代码因为mutex被别的线程占住了迟迟执行不到。修改方案很直接把relax提前到中断处理函数的出口或者改用一个短超时的pm_wakeup_event(dev, 100)。改完再测active_since恢复为0待机电流立刻降下来。这类问题用工具扫描就能锁定方向。第三步是回归验证在autosleep开启的状态下确认所有wakeup source的active_since均为空系统能够正常进出多次睡眠电流曲线平滑才算真正闭环。6.3 驱动开发中减少wakeup source误用的一些建议最后给几条我从大量代码评审里总结出来的建议优先使用pm_wakeup_event(dev, timeout)而不是pm_stay_awake除非你确定操作需要的时间明显超过几个jiffies。如果你需要连续多次上报事件考虑复位超时同一wakeup source上多次调用__pm_wakeup_event超时时间会被刷新这在QoS类场景里很有用。wakeup source的名字要起得能一眼看出是谁。我在实际项目里见过大量wakeup_source_create(ws)、pmic_wakeup这种含糊名字出了问题完全没法过滤。直接叫i2c-0-touch、rtc-alarm后面找问题能省半天。调试阶段开CONFIG_PM_DEBUG加上debugfs比在驱动里到处加printk高效得多统计链路更直观。每次释放wakeup source前花几秒钟看一下total_time和max_time的数值能帮你发现隐藏的长期占用问题。我在实际项目里吃过一次亏某传感器的wakeup source每次active只有几十毫秒但一小时内event_count达到了上千次累计起来相当于系统每个唤醒周期都被“续费”了待机功耗就是这么上去的。