ARTICLE DETAIL

资讯详情

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

Linux内核wakeup source唤醒源框架:从中断乱账到suspend竞态排查

Linux内核wakeup source唤醒源框架:从中断乱账到suspend竞态排查 半夜的线上设备一整块板子明明进了 suspend却总是被不知哪里冒出来的中断弄醒第二天拿 trace 一看唤醒源指向某个外设的 IRQ但驱动里翻遍了也没找到对应的处理逻辑。这种“系统醒了但没人认领”的诡异现场我在过去几年里见过太多次。而绝大多数这类问题的根子都落在 Linux 内核功耗管理里的一个不算起眼的组件上——wakeup source 唤醒源框架。这是内核功耗子系统系列的第 16 篇前面几篇我们梳理过 regulator、clock、runtime PM 这些相对“重”的框架这次轮到 wakeup source。说实话它是整个功耗管理体系里抽象最薄的一层核心数据结构就一个结构体API 也就那么十几个。但恰恰是这个最简单的组件承担了“谁在阻止系统睡眠”“谁把系统弄醒了”“这个唤醒事件是否可信”这几件最难说清楚的事。这篇我会从上层的“为什么需要它”开始再深入到struct wakeup_source的字段、状态流转、与 suspend 主流程的耦合方式最后给出一套可落地的驱动接入和问题排查思路希望对正在折腾内核睡眠唤醒问题的朋友有点帮助。1. 天降事件为什么 suspend 流程必须数“唤醒事件”的账1.1 老问题中断能唤醒 CPU但内核不知道这笔账该记在谁头上很多刚从驱动开发转到功耗调试的同事第一反应是系统被唤醒不就是因为收到了中断吗把中断处理函数执行完再睡回去不就行了理论上确实是这样但现实里有两个坑。第一个坑是中断只是导火索不是事件本身。按下电源键、插上充电器、网络包到达、传感器数据准备好这些才是真正意义上的“唤醒事件”。中断控制器在硬件层面把 CPU 从低功耗状态拉起来但软件层面需要有人告诉 PM 核心刚才发生的是一件需要处理的外部事件而不是一次毛刺、误触发或者残留电平导致的假唤醒。如果没有这层“认领”机制CPU 醒了中断处理函数跑一圈发现无事可做系统就又睡回去了——可如果这次事件其实是用户按了按键那后果就是用户觉得设备失灵。第二个坑更隐蔽事件到达的时间和中断处理的时机往往对不上。尤其是具备 wakeup 能力的中断在系统 suspend 的不同阶段中断处理函数的执行环境都不一样。在 noirq 阶段被唤醒时可能连进程上下文都还没有完全恢复有些驱动为了快速响应中断处理里只做 mask 和标记真正的业务逻辑丢给 workqueue 或者 kthread。这中间存在一个窗口事件已经发生了但设备驱动还没来得及真正处理它。如果内核在中断处理完后就尝试再次 suspend很可能把带着 pending 事件的系统重新压回睡眠事件丢失。wakeup source 框架解决的就是这个对不齐的问题它允许驱动在事件发生的第一时间哪怕是在中断处理函数里登记一笔“我有事”而 PM 核心看到这个登记后会拒绝立刻再次进入 suspend给驱动留出窗口去处理真正的业务。1.2 从“有没有事件”到“事件发生了多少次”wakeup_count 的账本逻辑如果只是简单地加一个全局标志has_event那只能回答“刚才有没有事件”。但对于 suspend 流程来说需要回答的问题更细“从我开始准备睡觉到真正要睡下去这段时间里有没有新事件”这不能用 bool 标志解决因为标志会在读取时被清掉一旦读完之后、写echo mem /sys/power/state之前又来了事件系统察觉不到。所以内核引入了唤醒事件计数wakeup_count的概念。每次有唤醒事件发生全局计数wakeup_count就加一。用户空间要 suspend 时采用一个“读计数、写计数、再进 sleep”的经典两段式握手读取/sys/power/wakeup_count拿到当前快照值 N执行用户空间的准备工作冻结业务、关闭服务等把 N 写回/sys/power/wakeup_count。如果写入成功说明从读取到现在没有发生新的唤醒事件于是继续写入/sys/power/state真正进入 suspend。如果步骤 1 到步骤 3 之间有任何唤醒事件插入wakeup_count的值已经变成 N1那么步骤 3 的写入就会失败/sys/power/wakeup_count返回错误用户空间应当放弃本次 suspend等事件处理完再重试。这个设计很多人第一次看会觉得绕但你对比一下旧的思路就明白了。如果没有两段式握手单纯靠“写入时看一下当前标志”等于把检查点和真正的睡眠点之间的所有窗口都赌运气。而计数的方式天然是单调递增的比较两个瞬间的计数值就能确定区间内是否有事件不需要锁、不需要清标志也不存在“读和写之间丢事件”的问题。1.3 wakeup source 之于 wakeup event既要记账也要记名有了全局计数系统能感知“有新事件”但还差一件事谁干的。于是就有了 wakeup source 这个按“源”粒度的记账单位。每个可能产生唤醒事件的设备或者子系统都应当有一个对应的 wakeup source 对象。它不是一个真实的硬件也不是设备树节点而纯粹是内核内存里的一个跟踪单元。驱动可以在事件发生时调用pm_wakeup_event()、__pm_stay_awake()等接口让对应的 wakeup source 进入 active 状态同时把全局唤醒计数累加。这样一来内核里同时存在两个视角全局的wakeup_count回答“有没有发生”每个 wakeup source 的event_count、active_count、wakeup_count等计数器回答“是谁、发生了什么”。这也是后面所有统计、调试、功耗归因分析的基础。2. 核心数据结构struct wakeup_source 逐字段拆解wakeup source 的核心数据结构定义在drivers/base/power/wakeup.c内核 5.x 以后基本稳定我直接以内核 5.15 左右的版本为例拆一遍。struct wakeup_source { const char *name; int id; struct list_head entry; spinlock_t lock; struct wake_irq *wakeirq; struct timer_list timer; unsigned long timer_expires; ktime_t total_time; ktime_t max_time; ktime_t last_time; ktime_t start_prevent_time; ktime_t 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唤醒源名称通常是设备名或驱动自定义字符串调试时区分源头debugfs里直接展示id内核为每个 wakeup source 分配的唯一编号用于排序和快速引用entry挂到全局wakeup_sources链表的节点让 PM 核心能遍历所有注册的唤醒源lock保护该结构体的自旋锁因为pm_wakeup_event可能在中断上下文调用锁必须能关中断wakeirq关联的专用唤醒中断有的设备使用 IRQF_NO_SUSPEND 专用唤醒中断这里记录其状态timer/timer_expires自动 relax 定时器及其到期 jiffies实现“最多再维持多少毫秒 active”的延迟放松机制total_time累计 active 时间统计该唤醒源总共阻止了多久睡眠max_time单次最长 active 时间判断是否存在异常长占用last_time最近一次状态切换时刻计算单次 active 时长和变化间隔start_prevent_time开始阻止睡眠的时刻配合 autosleep 计算阻止睡眠的累计时间prevent_sleep_time累计阻止自动睡眠的时间autosleep/Android 睡眠机制依赖它event_count事件发生总次数事件频率分析的核心指标active_count从 inactive 变为 active 的次数注意和 event_count 的区别下面细说relax_count主动 relax 的次数反映驱动主动性expire_count定时器到期而被动 relax 的次数反映“超时释放”情况数值过高说明驱动偷懒wakeup_count实际导致系统被唤醒或阻止系统睡眠的次数这是归因分析最重要的字段active当前是否处于 active 状态核心状态位autosleep_enabled是否处于 autosleep 模式影响 prevent_sleep_time 的统计方式dev关联的struct device设备驱动的便捷入口也有完全不关联设备的“裸 source”2.1 两个容易混淆的计数event_count 与 active_count很多人第一次读代码时会卡在event_count和active_count的区别上。简单说event_count是事件发生的次数active_count是状态从非 active 翻转为 active 的次数。驱动连续调用两次__pm_wakeup_event()且中间没有 relax那么event_count加 2但active_count只加 1。因为第二个事件发生时 wakeup source 还处于 active只是把定时器往后推了一下。理解这个区别对排查问题很重要。如果你看到event_count飙升但active_count变化不大说明驱动在频繁上报事件但每次事件之间没有真正“放松”过系统实际上被这个 source 一直占着。反过来说active_count大但event_count不大说明驱动经常在 active/inactive 之间反复切换但每次事件数量都不多。2.2 时间戳家族的用途total_time、max_time、prevent_sleep_timetotal_time、max_time和prevent_sleep_time都是为了回答“这个唤醒源到底拖了多少后腿”的问题。total_time是累计 active 时间用来衡量一个唤醒源长期占用的总时长。max_time用来找异常尖峰如果某个 source 的max_time高达几秒甚至几分钟那驱动里很可能有路径忘了 relax或者业务逻辑在 active 期间阻塞了。prevent_sleep_time和start_prevent_time是为 autosleep 准备的一组字段。autosleep 模式下内核会周期性尝试进入 suspend只要还有一个 wakeup source 处于 activeautosleep 就会被阻止。prevent_sleep_time累计了这个 source 阻止 autosleep 的总时长直接反映它对电池续航的负面影响。3. 状态流转注册、激活、放松的调用链剖析3.1 注册三兄弟create / add / registerwakeup source 的生命周期从创建开始。内核提供三个不同层次的接口wakeup_source_create(name)单纯分配并初始化一个struct wakeup_source不挂到全局链表wakeup_source_add(ws)把创建好的 source 挂入全局链表并分配 idwakeup_source_register(dev, name)一条龙完成 create add 与指定设备关联同时设置dev-power.wakeup指针。驱动开发时绝大多数情况用wakeup_source_register()就够了它会自动把 wakeup source 和 device 的 power 管理绑定在一起。之后驱动就可以用pm_stay_awake(dev)、pm_wakeup_event(dev, msec)这类以 device 为参数的快捷接口而不用直接操作 wakeup source 指针。要注意的是wakeup_source_unregister()会先执行wakeup_source_remove()再从链表摘除并释放对象。如果设备要卸载这里不能漏否则全局链表上会留下悬空指针。3.2 激活路径从 pm_stay_awake 到 wakeup_source_activate驱动让一个唤醒源进入 active 状态标准入口是__pm_stay_awake()它对外的设备级封装是pm_stay_awake(dev)。void __pm_stay_awake(struct wakeup_source *ws) { unsigned long flags; if (!ws) return; spin_lock_irqsave(ws-lock, flags); if (!ws-active) wakeup_source_activate(ws); spin_unlock_irqrestore(ws-lock, flags); }值得注意的是如果 source 已经 active__pm_stay_awake()什么都不会做不会重新计数也不会更新时间戳。也就是说它表达的是“请继续保持清醒”而不是“重新开始”。这正是pm_stay_awake(dev)和pm_wakeup_event(dev, msec)的一个关键区别后者重新激活并刷新定时器。真正干活的是wakeup_source_activate()。它做的事包括event_count和wakeup_countactive true记录last_time now更新全局唤醒计数wakeup_count的原子值使pm_wakeup_pending()能感知到变化如果autosleep_enabled更新start_prevent_time表示从此刻开始在阻止 autosleep。中断处理函数里调用pm_wakeup_event()时由于可能处在 IRQ 上下文所以全程用关中断的自旋锁保护这也解释了为什么wakeup_source的锁不能用普通 mutex。3.3 放松路径__pm_relax 与 wakeup_source_deactivate__pm_relax()是主动把唤醒源释放掉的接口设备级封装是pm_relax(dev)。它的核心是把 active 状态清掉同时做一堆统计static void wakeup_source_deactivate(struct wakeup_source *ws) { ktime_t duration; ktime_t now; ws-relax_count; if (ws-active) { now ktime_get(); duration ktime_sub(now, ws-last_time); ws-total_time ktime_add(ws-total_time, duration); if (ktime_to_ns(duration) ktime_to_ns(ws-max_time)) ws-max_time duration; if (ws-autosleep_enabled) ws-prevent_sleep_time ktime_add( ws-prevent_sleep_time, ktime_sub(now, ws-start_prevent_time)); ws-last_time now; } ws-active false; }这段逻辑里有两个细节值得留意。一是total_time的累加时机它是在 deactivate 时一次性累加的而不是实时累加。所以如果你在半路通过调试器查看total_time它代表的是“上一次 relax 之前”的累计值加上当前正在进行的这次 session 还没入账。别被这个假象误导。二是relax_count在ws-active判断之前就自增了。这意味着即使一次 relax 调用发生在已经非 active 的状态relax_count依然会涨。这在统计上是有意的它记录的是驱动“尝试放松”的次数而不是“成功 relax”的次数。如果驱动频繁调用pm_relax()但 source 本来就不 active说明驱动状态管理可能有问题也可以作为一种排查线索。3.4 定时器路径__pm_wakeup_event 与 expire_count驱动有时不想自己管理 relax 的时机而是希望“保持 active 最多多少毫秒”。这样做的好处是即使业务处理完忘了 relax内核也会兜底自动释放。这时应该用__pm_wakeup_event()或pm_wakeup_event(dev, msec)。void __pm_wakeup_event(struct wakeup_source *ws, unsigned int msec) { unsigned long flags; unsigned long expires; spin_lock_irqsave(ws-lock, flags); if (ws-active) wakeup_source_deactivate(ws); wakeup_source_activate(ws); if (!msec) { wakeup_source_deactivate(ws); goto unlock; } expires jiffies msecs_to_jiffies(msec); if (!expires) expires 1; if (!ws-timer_expires || time_after(expires, ws-timer_expires)) { mod_timer(ws-timer, expires); ws-timer_expires expires; } unlock: spin_unlock_irqrestore(ws-lock, flags); }这段代码有几个值得仔细品的地方。首先是当你传入msec为 0 时它实际是“激活后立刻去激活”相当于只登记一个事件但不持续占坑。这很常用RTC 闹钟、按键这类事件中断处理里只需要告诉内核“发生了事件”不需要让系统保持清醒几十毫秒因为事件本身已经处理完了。这时pm_wakeup_event(dev, 0)就是最合适的选择。其次是定时器策略是“取更晚的到期时间”不是“重新开始计时”。如果驱动在 10ms 内连续调用三次pm_wakeup_event(dev, 50)第一次设置定时器 50ms 后到期后面两次发现新的到期时间并没有比现有的更晚就不会移动定时器。这样避免了高频事件导致定时器被无限推迟的问题。当然如果新到期时间确实更晚比如第三次传的是 200ms定时器会重新安排。最后是expire_count每次定时器到期执行wakeup_source_deactivate()时如果检测到 source 还处于 active就会累加expire_count。我们在分析异常时如果一个 wakeup source 的expire_count远大于relax_count基本可以断言驱动没有认真做自主 relax全靠兜底机制撑场面这是不健康的写法。4. suspend 竞态窗口wakeup_count 与用户空间的两段式握手4.1 用户空间读写的完整语义前面提到过/sys/power/wakeup_count现在把它的底层逻辑讲透。用户空间流程的标准写法是# 读取当前唤醒计数 count$(cat /sys/power/wakeup_count) # 做各种用户态准备比如停掉业务进程、同步数据 ... # 回写同一计数写成功才允许进 mem if echo $count /sys/power/wakeup_count; then echo mem /sys/power/state else # 有唤醒事件发生放弃本次 suspend fi这个回写动作对应内核里的pm_save_wakeup_count(unsigned int count)。它会把用户空间读到的count保存为saved_count同时打开events_check_enabled。之后内核判断“是否有事件 pending”时比较的就是当前实时 wakeup count 与 saved_count 是否相等。如果两次比较之间来了一个 wakeup event实时值变大了那么pm_wakeup_pending()就会返回 true。这里就引出一个很多新手困惑的点回写之后的窗口怎么办毕竟echo $count /sys/power/wakeup_count和echo mem /sys/power/state是两个独立操作中间不还是有可能插入事件吗答案是中间这个窗口由内核内部的检查兜底。events_check_enabled一旦置位只要系统还在进 suspend 的路上pm_wakeup_pending()就会在多个检查点被杀出来任何新事件都会导致 suspend aborted。用户空间的握手只是第一步真正确保安全的是 suspend 路径里一层层的pm_wakeup_pending()检查。4.2 suspend 流程里的检查点分布以标准kernel/power/suspend.c为例pm_wakeup_pending()的检查大致分布在以下几个位置冻结进程之后保证冻结过程中没有新事件把进程状态搞乱suspend_enter 之前检查在准备阶段是否已经有事件插队device_suspend_noirq 之前和之后noirq 阶段中断已经被屏蔽此时的事件要么已被处理要么由 wakeup 中断事后登记唤醒恢复路径上系统醒来后同样要检查是否有事件 pending以决定是否还需要继续处理。为什么要在 noirq 阶段之后再查一次因为有些设备在进入 noirq 前设备驱动已经 suspend 了但 wakeup 中断仍然能使 CPU 从睡眠状态醒来。CPU 被唤醒后硬件中断控制器先把 IRQ 递给内核此时中断处理函数会运行并在里面调用pm_wakeup_event()。但系统 suspend 流程并不知道这个中断是不是“真正要处理业务”的事件所以要在下一个检查点看实时计数有没有变化。如果变了就取消这次的 suspend让系统完全苏醒去处理业务。4.3 autosleep 与 Android 的隐藏耦合autosleep 是 Android 进入睡眠的主要机制他的实现本质是内核定期尝试系统级 suspend只要还有任何一个 wakeup source 处于 active就不真正睡眠。在 autosleep 模式下wakeup source 的autosleep_enabled标志会被置位。此时 source 进入 active 时会记录start_prevent_time解除 active 时会累加prevent_sleep_time。这个机制让 autosleep 能精确回答“这次没睡成有多大责任”而不是笼统地记一个次数。驱动侧要注意的是在 autosleep 环境里不要滥用pm_stay_awake()。很多 Android 驱动习惯在“操作进行中”长时间持有一个 wakeup source这确实能防止系统睡过去但也会导致耗电。内核态没有一个强制手段阻止驱动持有 source 太长时间只靠max_time这样的统计指标事后暴露问题。5. 驱动侧集成从 device wakeup 到实际触发链路5.1 第一步让设备具备 wakeup 能力驱动要和 wakeup source 框架对接通常有三件事调用device_init_wakeup(dev, true)初始化设备的 wakeup 能力这个函数会自动创建并注册一个与dev关联的 wakeup source设置中断的唤醒能力比如irq_set_irq_wake(irq, 1)让中断控制器允许该中断在系统 suspend 时唤醒 CPU在中断处理函数里调用pm_wakeup_event(dev, msec)或pm_stay_awake(dev)登记唤醒事件。从概念上要分清第二件事和 wakeup source 的关系irq_set_irq_wake解决的是“这个中断能不能把 CPU 从硬件睡眠状态拉起来”属于中断控制器和 CPU 电源管理的范畴而pm_wakeup_event解决的是“拉起来之后系统软件层面要不要保持清醒、要不要把这笔账记下来”。两者是配合关系不是一个替代另一个。5.2 中断处理函数里的标准姿势以一个典型的 GPIO 按键驱动为例它的中断处理函数通常长这样static irqreturn_t button_irq_handler(int irq, void *data) { struct button_device *bdev data; /* 上报按键事件视为一次唤醒事件 */ pm_wakeup_event(bdev-dev, 0); /* 真正的业务上报 input 子系统启用线程化中断慢慢处理 */ return IRQ_WAKE_THREAD; }pm_wakeup_event(bdev-dev, 0)表示这次中断是一次真实的外部唤醒事件但事件本身在中断处理里已经标记过了不需要系统保持额外的清醒时间。于是内核会立即激活再撤销对应 wakeup source只留下一笔计数。如果业务处理比较耗时比如要等 IO 完成或者 workqueue 调度就应该给一个明确的毫秒数pm_wakeup_event(bdev-dev, 100);这样系统会至少保持 100ms 的清醒时间给驱动留出处理窗口100ms 后如果没其他 active sourceautosleep 可以再次尝试睡眠。5.3 长时间工作场景用 pm_stay_awake / pm_relax定时器方案适合“事件型”场景。但有些场景是“持续工作型”比如一个外设在 suspend 前还有 DMA 传输没收尾或者固件下载过程中不能睡。此时用定时器就不合适应该显式地 stay awake 和 relaxpm_stay_awake(dev); /* 执行耗时操作 */ ... pm_relax(dev);这里最常见的错误是忘了在错误路径里 relax。比如函数中途 return -EIO但pm_relax()写在了最后才执行于是唤醒源一直 active系统迟迟进不了睡眠。我见过不少这样的 bug建议的做法是在函数入口先 stay awake然后用 goto out 统一出口在 out 里 relax和锁的释放模式保持一致避免每条错误路径都单独处理一次。5.4 上下文约束中断上下文可用但不能为所欲为wakeup source 的接口设计得相当“轻”锁用的是spin_lock_irqsave所以__pm_stay_awake、__pm_wakeup_event、__pm_relax都允许在中断上下文调用包括 hardirq 和 softirq。这是它的优势。但也有两个约束第一不要在早期中断里依赖设备锁或者可能有睡眠行为的资源。虽然 wakeup source 本身的操作不会睡眠但你调用它的前后如果还摸了别的锁锁本身可能在中断上下文出问题。第二定时器到期的回调同样运行在软中断环境它内部调用wakeup_source_deactivate()只做统计和状态翻转不做任何回调设备驱动的动作。所以驱动不要指望通过 wakeup source 的定时器到期事件去通知自己“该 relax 了”那个定时器只是内核侧的兜底措施不是业务回调机制。5.5 删除有讲究active 状态的 source 不能直接销毁驱动卸载时如果 wakeup source 还在 active 状态直接调用wakeup_source_destroy()会触发 WARN。内核不愿意看到这种局面是因为一个 active 的 source 意味着系统仍然依赖它保持清醒你把它销毁了等于把系统的“清醒意愿”也一起推翻了会产生难以追踪的竞态。正确做法是卸载前先调用__pm_relax()或者pm_relax(dev)把 source 复位然后再 unregister/destroy。即使驱动认为自己已经处理完所有业务也应该在 remove 回调里做一次强制 relax 兜底。6. 调试与统计debugfs、统计量读法和排查套路6.1 一张表读懂 debugfs/wakeup_sources内核在 debugfs 下提供了全局唤醒源视图路径是/sys/kernel/debug/wakeup_sources。只要内核开启了CONFIG_DEBUG_FS和CONFIG_PM_SLEEP随时可以 cat 出来看。cat /sys/kernel/debug/wakeup_sources输出格式总共 11 列左右我摘一个典型场景模拟一下name active_count event_count wakeup_count expire_count active_since total_time max_time last_change prevent_suspend_time button 123 456 78 0 0 2.436s 456.278ms 123.456s 0各列的使用方法我在前面数据结构部分已经覆盖了这里重点说三个在排查时钟类问题时最常用的判据。判据一event_count 和 wakeup_count 的关系。event_count是唤醒源上报的所有事件次数wakeup_count是其中真正让系统从 suspend 状态翻醒或者阻止了 suspend 的次数。如果两者接近说明这个唤醒源几乎每次上报都实打实把系统弄醒了是“高干扰源”。如果wakeup_count远小于event_count说明多数事件发生在系统本来就没睡的状态干扰不大。判据二expire_count 和 relax_count 的比例。如果 expire_count 远大于 relax_count说明这个驱动从不主动 relax全部靠定时器兜底。虽然功能上没问题但这种驱动往往会在业务逻辑变化时产生长尾的 active 时间属于潜在风险。判据三prevent_suspend_time 是否持续增长。在 autosleep 环境下这个值只增不减异常上涨说明驱动在某段时间内长时间持有 wakeup source 不放。6.2 排查“系统半夜随机唤醒”的完整路径我们拿一个真实场景来演练设备在深夜每隔几十秒醒来一次但业务侧没有任何日志。第一步先看全局 stat缩小嫌疑范围cat /sys/kernel/debug/wakeup_sources重点比较几个候选设备的event_count增长率。假设能看到某个外设的event_count几十秒涨一次且wakeup_count同步上涨那基本可以锁定唤醒源来源。第二步确定唤醒中断的硬件路径。配合/proc/interrupts看对应中断号的中断次数增长情况cat /proc/interrupts如果/proc/interrupts里的增长和 debugfs 里 event_count 的增长节奏吻合就能把“唤醒源”和“中断源”对应上。第三步看中断发生在哪个阶段。如果系统在 noirq 阶段唤醒通常是硬件上的唤醒中断触发直接看该 GPIO 的中断配置和外部信号比如用示波器量引脚电平。如果是 suspend 开始前后的普通中断要回到驱动代码确认中断处理函数里是否调用了pm_wakeup_event。第四步如果/proc/interrupts没有增长但 wakeup source 还在涨说明事件不是通过传统 IRQ 路径上报的可能是轮询线程或者 timer 在驱动内部调用了pm_stay_awake。这种“软件唤醒”在生产环境里更难定位需要给 wakeup source 起一个足够明确的名字别让每个驱动都用wakeup这种千篇一律的名字否则 debugfs 上根本分不清是谁。6.3 几个我踩过、见别人踩过的坑坑一把pm_wakeup_event(dev, 0)理解成“不处理”。它是“处理完了不需要保持清醒”不是“忽略事件”。有人用它清 IRQ 却不在中断状态里汇报真实业务导致明明发生了按键系统也醒了但业务层没有任何响应。坑二在 IRQ handler 里用非线程中断且内部有耗时操作然后只给一个很短的 msec。比如 handler 里要访问 SPI 读取数据老式驱动直接写死在 hardirq 里然后pm_wakeup_event(dev, 10)。10ms 对于 SPI 操作经常不够系统可能提前再次 suspend导致 DMA 没读完就断了。这种情况应该改成线程化中断request_threaded_irq并把pm_wakeup_event的 msec 放宽到线程处理完的合理值或者干脆用独立的 wakeup source 手工 stay awake/relax。坑三设备 suspend 回调里没有主动 relax。device 的suspend()回调执行时wakeup source 可能还处于 active。驱动在 suspend 回调里应当评估当前是否有未完成业务如果有要么拒绝 suspend返回错误如-EBUSY要么强制 relax。否则 suspend 流程走到后面pm_wakeup_pending()突然发现一个尚在 active 的 source会把这次 suspend abort 掉而且是没有任何日志的 abort排查起来极其消耗时间。6.4 手动触发工具sysfs 和 debugfs 的联动调试时还经常需要“手动激活一个 wakeup source”来验证系统会不会睡。以设备关联的 wakeup source 为例可以往设备 sysfs 里的power/wakeup写值控制开关但更直接的方式是构造一次真实事件。以 RTC 为例# 设置 RTC 闹钟为 5 秒后 echo 0 /sys/class/rtc/rtc0/wakealarm echo 5 /sys/class/rtc/rtc0/wakealarm # 尝试进 suspend echo mem /sys/power/state如果 5 秒后系统准时醒来且 debugfs 里 rtc 对应的 wakeup source 的wakeup_count加一说明整条唤醒链路是通的。对于 autosleep 环境可以临时关闭echo off /sys/power/autosleep然后手动确认每个 wakeup source 的状态变化逐个设备地做排查比全系统开着 autosleep 摸黑定位要高效得多。几个留在最后的实际操作体会wakeup source 框架写起来很薄但用起来比大多数 PM 子系统都更需要“全局观”。它不像 regulator 有明确的电压状态机也不像 clock 有清晰的 tree 结构它本质上就是让内核在“要不要保持清醒”这个问题上有一个能做出正确决策的依据。我在实际项目中体会最深的一点是不要在中断里把所有事情都做完也不要只把事件往 wakeup source 里一丢就万事大吉。事件上报、业务处理、主动 relax 这三件事必须有一条清晰的边界。把业务处理丢给 workqueue 时要确保 workqueue 里的路径确实会调用pm_relax()而在硬实时场景下宁可多用一些pm_wakeup_event(dev, msec)定时器方案也不要依赖“我肯定会在另一个函数里 relax”的默契毕竟这种默契最容易在代码重构时断掉。另外建议每个驱动在创建 wakeup source 时把名字起得具体一点比如rtc0-alarm、bluetooth-host-wake不要用wakeup这种无法区分来源的名字。debugfs 里多几十个wakeup你真到排查问题那天会想抽当时的自己。如果你的系统已经出现疑似唤醒源异常我建议从cat /sys/kernel/debug/wakeup_sources开始把唤醒前后两次输出的event_count和wakeup_count差分一下再结合/proc/interrupts和业务日志做交叉验证。这套流程帮我定位过至少十几个真实设备的异常唤醒问题效率比猜代码要高得多。
返回列表