做过嵌入式或者 Android BSP 的同学,大概率都有过类似经历:产品经理拿着一台功耗测试仪,丢下一句“灭屏后整机平均电流比竞品多了 5mA,你查一下”,然后转身离开。你很不服气,但最后还是打开内核源码,盯着 suspend/resume 的调用栈看了一下午。如果你正处于这个状态,那恭喜你,你已经摸到了 Linux 内核功耗子系统最核心的地带——PM Core。
这篇是系列文章的第一篇,我打算把“PM Core”这个听起来很玄乎的名字拆开,讲清楚它到底在功耗框架里扮演什么角色。我尽量不去贴大段大段的代码注释,而是从“为什么这套框架要这样设计”的角度入手,带着你从 dev_pm_ops 看到 dpm_suspend,再看到 wakeup_source。读完你应该能建立起一张完整的脑内地图:Linux 内核的功耗管理,哪些事情归 PM Core 管,哪些事情其实不归它管,以及你在驱动里写的那些 callback 到底是在哪一环被调用的。
1. PM Core 在功耗管理版图里的准确坐标
1.1 功耗管理的“三层楼”:硬件特性、驱动回调、框架编排
刚开始接触内核电源管理的人,很容易把“功耗子系统”理解成一个巨大的、无所不包的黑盒:CPUFreq、CPUIdle、Regulator、Clock、设备 Runtime PM、suspend/resume、hibernate……好像全都搅在一起。实际上,Linux 内核的功耗管理是有清晰层次的,我习惯把它分成三层来看。
最底层是硬件特性层。这一层关心的是芯片本身能提供什么:CPU 有多少个 idle state,DDR 能不能进入 self-refresh,GPU 有没有 retention 模式,外设寄存器能不能在时钟关断后保持上下文。这些能力由硬件 IP 决定,内核能做的只是“发现”并“管理”它们,对应到代码里就是 cpuidle driver、cpufreq driver、clock framework、regulator framework 这些模块。
中间层是设备驱动层。每一个设备驱动都需要回答一个问题:我的设备在系统睡眠前要把哪些寄存器保存下来?在唤醒后要重新初始化哪些逻辑?这些回答通过填充struct dev_pm_ops里的回调函数来实现。这是驱动开发者接触最多的地方。
最上层就是框架编排层。也就是本文要说的 PM Core。它不关心某个具体设备的寄存器细节,它只关心两件事:一是“整个系统现在应该处于什么功耗状态”,二是“在切换这个状态时,怎么有序地通知到每一个驱动、每一个子系统”。
这三层的关系,很像一个剧场。演员(驱动)知道自己要演什么,舞台设备(硬件)也知道自己能实现什么效果,但如果没有导演(PM Core)拿着剧本一页一页地喊“该谁上场、该谁退场”,整场戏肯定是乱套的。
1.2 PM Core 的三大职责与目录对应
既然叫“Core”,它的职责实际上非常收敛。从代码结构上可以看得很清楚,PM Core 不是某个单一文件,而是分布在三个目录下的协同体系:
| 目录/文件 | 职责 | 典型内容 |
|---|---|---|
include/linux/pm.h | 定义框架的“契约” | dev_pm_ops、pm_message_t、pm_event等数据结构和宏 |
kernel/power/ | 全局状态机管理 | suspend/resume/hibernate 的入口流程、状态转换、wakeup_count 协议 |
drivers/base/power/ | 设备层面的 PM 机制 | dpm_list 管理、runtime PM 框架、wakeup source 管理、sysfs 接口 |
第一块负责“定规矩”,第二块负责“管全局”,第三块负责“落实到设备”。三者合起来,才是完整意义上的 PM Core。很多人读代码时只盯着kernel/power/里的main.c,其实真正和设备驱动天天打交道的逻辑,都在drivers/base/power/下面。
1.3 什么时候你会需要关注 PM Core
坦率地说,如果你只是写一个普通的字符设备驱动,不关心功耗,你可能几年都碰不到 PM Core 的代码。但下面这几类场景,你就必须把 PM Core 的机制弄明白:
- 系统休眠唤醒后,某个设备工作不正常,怀疑是恢复顺序或保存/恢复不完整;
- 灭屏后整机电流降不下去,怀疑有设备没进入低功耗模式,或者被某个 wakeup source 反复唤醒;
- 你需要在设备驱动里正确注册 runtime PM 回调,并理解
pm_runtime_get_sync和pm_runtime_put_sync背后的引用计数机制; - 做 Android BSP,需要调试
wake_lock和autosleep的行为,追踪系统为什么迟迟不进入 suspend。
如果你中了其中任何一条,请继续往下看。这一篇把“分层设计”的骨架搭起来,后面几篇我们再逐个功能模块深入进去。
2. 分层设计的灵魂在 include/linux/pm.h 那几百行
2.1 dev_pm_ops:设备与功耗框架之间的“合同”
打开include/linux/pm.h,第一眼会看到struct dev_pm_ops。这个结构体可以说是整个设备功耗管理的“合同”:内核框架不关心你的设备怎么实现低功耗,它只要求你按照约定的回调函数把该做的事情做掉。
struct dev_pm_ops { int (*prepare)(struct device *dev); void (*complete)(struct device *dev); int (*suspend)(struct device *dev); int (*resume)(struct device *dev); int (*freeze)(struct device *dev); int (*thaw)(struct device *dev); int (*poweroff)(struct device *dev); int (*restore)(struct device *dev); int (*suspend_late)(struct device *dev); int (*resume_early)(struct device *dev); int (*freeze_late)(struct device *dev); int (*thaw_early)(struct device *dev); int (*poweroff_late)(struct device *dev); int (*restore_early)(struct device *dev); int (*suspend_noirq)(struct device *dev); int (*resume_noirq)(struct device *dev); int (*freeze_noirq)(struct device *dev); int (*thaw_noirq)(struct device *dev); int (*poweroff_noirq)(struct device *dev); int (*restore_noirq)(struct device *dev); int (*runtime_suspend)(struct device *dev); int (*runtime_resume)(struct device *dev); int (*runtime_idle)(struct device *dev); };第一次看到这么多回调,很多人会头皮发麻。但注意看,其实只有三大组:系统睡眠相关(suspend/resume)、休眠镜像相关(freeze/thaw、poweroff/restore)、运行时电源管理相关(runtime_*)。每一组内部又按“时机”拆成了四个阶段:普通阶段、late 阶段、noirq 阶段、以及挂在最前面的 prepare/complete。
这里我想强调一个容易被忽略的设计点:这些回调都是可选的。如果你的设备在系统睡眠时不需要做任何事,你就什么都不用填,框架在遍历设备时发现回调为空就直接跳过。这点非常重要,因为很多老驱动其实根本没有 PM 概念,它们的内核版本也比较老,上面没有.suspend.resume回调。PM Core 要保证老驱动在引入新框架后依然能正常工作,所以“回调缺省为空”是最基础、最关键的兼容性设计。
2.2 为什么是 PM_EVENT_* 而不是一个布尔值
在非常早期的 Linux 内核里,电源管理回调是非常朴素的,只有简单的suspend/resume两个函数,宣传上就一个标志位。后来为什么演化出了pm_message_t和PM_EVENT_*?
答案是:系统挂起不止一种原因。
- suspend to RAM(挂起到内存):系统保持内存供电,把 CPU 和大部分外设关掉,唤醒后恢复的速度比较快;
- suspend to disk(hibernate,挂起到磁盘):把内存镜像写到磁盘,然后整机断电,唤醒时需要从磁盘恢复镜像;
- freeze(冻结):不进入真正低功耗状态,只是把进程和设备冻结住,用于创建系统镜像等场景。
同样是“让设备停下来”,三种场景下设备需要做的事差别很大。比如 hibernate 前设备必须把 DMA 完全停掉,保证内存镜像一致性;而 freeze 场景下设备往往只需要把中断和任务停掉即可。如果只靠一个布尔标志位,驱动根本分不清自己到底处在哪个流程里。
pm_message_t和PM_EVENT_*的存在,让框架可以向设备回调传递“事件类型”,驱动也可以根据事件类型走不同的处理逻辑。另外注意,历史上pm_message_t曾经还承载过 event 之外的信息,但现在它的核心作用就是传递事件类型。
typedef struct pm_message { int event; } pm_message_t;常用的event值定义在同一个头文件里:PM_EVENT_SUSPEND、PM_EVENT_RESUME、PM_EVENT_FREEZE、PM_EVENT_THAW、PM_EVENT_POWEROFF、PM_EVENT_RESTORE等。在回调里你可以这样判断:
static int demo_suspend(struct device *dev, pm_message_t state) { if (state.event == PM_EVENT_SUSPEND) dev_dbg(dev, "suspend to RAM\n"); else if (state.event == PM_EVENT_FREEZE) dev_dbg(dev, "freeze for image\n"); return 0; }不过在实战中,大多数驱动不需要区分这么细,因为 PM Core 在调用具体回调前已经把事件类型“翻译”成了对应的回调函数(suspend 流程调.suspend,freeze 流程调.freeze),驱动只有在想兼顾两类场景时才会去检查pm_message_t.event。
2.3 回调分成四段:prepare/suspend/late/noirq 的工程意义
很多驱动开发者第一次看到.suspend、.suspend_late、.suspend_noirq时是懵的:这些不都是让设备睡觉吗?为什么要分三四个阶段?
这是理解功耗框架分层设计的一个绝佳切入口。
系统睡眠是一个非常敏感的全局操作。它像一支多支部队协同撤退:有人要先撤,有人要后撤,有人要在别人撤完后再负责放哨。如果一个驱动在中断仍然正常派发的时候做复杂操作,它可以用普通.suspend;如果一个驱动希望在绝大部分系统机制还在运转时完成收尾,它可以用.suspend_late;而一旦系统进入了“中断禁用”阶段,只剩.suspend_noirq能提供极其有限的操作窗口。
反过来,恢复流程就是反过来的顺序:resume_noirq→resume_early→resume→complete。
我用自己的理解做个类比:你在公司加班到很晚,准备锁门走人。.prepare相当于你先把桌面收拾干净、把重要文件备份,告诉保安“我快走了”;.suspend相当于你关掉办公室的灯和大功率设备;.suspend_late相当于你把门锁上但还在楼道里;.suspend_noirq相当于你走出大楼,把门禁系统也关掉——此时已经没有任何人能响应门铃了。
这种分阶段设计最大的工程价值,是让“对中断敏感”和“对时序敏感”的设备都能找到自己合适的操作窗口。比如 GPIO 控制器、中断控制器这类设备,必须等到几乎所有设备都 suspend 之后再 sleep,因为它们还要为别人服务;对应的,它们必须最早醒来。
对于普通驱动,我的建议是:能用.suspend/.resume解决的,绝不要往.suspend_late/.resume_early里塞逻辑。里面能做的事情少,出错的代价大,真需要在这个阶段跑代码的设备通常都是平台级设备,普通外设驱动没有理由去凑热闹。
3. 从 /sys/power/state 到 CPU 睡死:一次整机睡眠的完整旅程
3.1 suspend_state_t 与用户态接口的对应关系
用户态触发系统挂起最常用的方法,就是往/sys/power/state里写字符串:
echo mem > /sys/power/state这个“mem”在内核里对应的是PM_SUSPEND_MEM。内核里定义了几个典型的挂起状态:
| 状态 | 含义 | 低功耗程度 | 典型场景 |
|---|---|---|---|
PM_SUSPEND_ON | 正常工作 | 无 | 默认 |
PM_SUSPEND_STANDBY | 待机 | 低,通过空闲 CPU | 快速唤醒 |
PM_SUSPEND_MEM | 挂起到内存 | 高,内存自刷新 | 手机灭屏待机 |
PM_SUSPEND_DISK | 挂起到磁盘 | 极高,几乎断电 | 笔记本合盖休眠 |
注意,/sys/power/state支持哪些字符串,取决于平台的suspend_ops和内核配置。有时候你写入standby会返回EINVAL,那说明平台实际上没有实现这个状态。这个细节经常被测试人员当成 bug 报上来,其实是预期行为。
写一个自定义字符串进/sys/power/state后,调用链大致是:
write() → state_store() → pm_suspend(state)state_store()位于kernel/power/main.c。它会先做一次合法性检查,然后进入全局挂起流程。这里有个很容易被新手忽略的点:pm_suspend()只有在系统没有正在挂起、也没有被其他机制阻止时才会真正执行。比如你在一个已经处于 suspend 状态的系统上再写一次mem,大概率会得到一个错误。
3.2 enter_state() 里最重要的握手:wakeup_count 协议
从/sys/power/state到设备回调真正被执行,中间还有一个非常关键的保护机制,叫做wakeup_count 协议。
这个协议的背景是:用户态写入“让我睡觉”的瞬间,可能正好有人按了电源键,或者有网络唤醒包到达。如果系统直接睡过去,这次唤醒事件会丢失,或者系统睡下去之后立刻又被唤醒,造成资源浪费。为了避免这种竞态,内核设计了“写之前先确认”的握手流程。
标准操作是:
# 1. 读取当前 wakeup events 计数 cat /sys/power/wakeup_count 1024 # 2. 将该计数写回,表示“基于这个时间点开始挂起” echo 1024 > /sys/power/wakeup_count # 3. 如果写回成功,再执行挂起 echo mem > /sys/power/state如果第 2 步写回时,内核发现 wakeup event 计数已经变了,写回会失败(返回错误)。此时挂在用户态的脚本就知道“有新的唤醒事件发生”,于是中止挂起流程。
进入enter_state()后,整个流程可以概括为:
enter_state() ├── 通知 PM_SUSPEND_PREPARE ├── suspend_devices_and_enter() │ ├── dpm_suspend() /* 普通设备挂起 */ │ ├── suspend_enter() /* 平台相关操作,调用 suspend_ops->enter() */ │ ├── dpm_resume() /* 设备恢复 */ └── 通知 PM_POST_SUSPEND这里可以看到,PM Core 把流程切成了“设备挂起”和“平台进入”两个环节。设备挂起是可移植的、与 SoC 无关的部分;而真正让 CPU 进入 deep idle、让 DDR 自刷新这些硬件操作,则委托给平台相关的suspend_ops。这种“平台无关 + 平台相关”的拆分,就是分层设计在全局状态机上的体现。
3.3 dpm_suspend() 的设备排序逻辑与经典坑
dpm_suspend()在drivers/base/power/main.c里,它做的事情本质上是:遍历一个全局设备链表,逐一调用每个设备的 suspend 回调。但链表顺序不是随机的,它由设备的注册顺序、依赖关系和异步标志共同决定。
这里有一个关键点:被挂起的顺序和恢复的顺序是相反的。先挂起的设备后恢复,后挂起的设备先恢复。这个规则让设备之间的依赖关系能自然满足——比如一个 MIPI 接口的传感器依赖 I2C 控制器,那么 I2C 控制器应该后挂起、先恢复,这样传感器在恢复时总能找到可用的 I2C 总线。
真正让很多人掉坑里的是下面几种情况:
第一,-EBUSY不代表 PM Core 出错。如果你的设备在.suspend或.prepare回调里返回-EBUSY,内核会认为此刻系统不该挂起,从而中止整个流程。很多驱动在“忙”的时候不该睡,这个设计是刻意的,但新手容易以为返回错误就是代码写错了。
第二,异步挂起让打印顺序乱了。内核默认可以对带DPM_FLAG_SMART_SUSPEND或异步能力的设备进行并行挂起。并行之后,你看到驱动里的dev_info打印顺序和链表顺序不一致,这是正常的,不代表回调乱序。
第三,其实也是最重要的:电源设备必须最后挂起、最先恢复。这不是命令,而是插入链表的顺序决定的:设备在注册时会被插入到 dpm_list 的合适位置,power domain / regulator / clock 这类电源相关的设备通常在更晚的位置。如果你的驱动在挂起后还需要向外部设备操作,但它的供电设备已经没了,这就是典型的排序 bug。内核为此也提供了各种 device PM domain 机制来解决依赖问题,相关内容后面专门写一篇。
4. 系统级的“装睡”与“叫醒”:Wakeup Source 与 Runtime PM
4.1 wakeup_source:让深度睡眠的设备还能对外界作出响应
如果一个系统进入mem挂起,CPU 停掉,外设大多断电,那它凭什么能被叫醒?答案是:某些设备仍然保持着“最低限度的检测能力”,它们被称为wakeup source(唤醒源)。
用大白话说,系统中的唤醒源相当于值班室里的电话。整个公司都下班了,但值班室的电话必须通着电,一旦有人打进来,就得把人叫回来处理。
在驱动的标准做法里,设备要成为唤醒源,需要两步:
/* 1. 在 probe 里初始化 wakeup 能力 */ device_init_wakeup(dev, true); /* 2. 在需要时“持有”唤醒锁 */ __pm_stay_awake(wakeup_source); /* 3. 事情处理完后“释放”唤醒锁 */ __pm_relax(wakeup_source);wakeup_source在内核里是一个计数 + 标志的机制。只要还有 wakeup source 处于“active”状态,系统就不允许进入 suspend,或者一旦尝试进入就会被立刻拉起来。这就是为什么 Android 上wake_lock能防止系统睡着——它的底层实现就是标准的 wakeup_source。
设备可以在系统已经进入 suspend 之后产生唤醒事件,此时它通过中断唤醒 CPU,并在 resume 流程中被识别。所以如果你排查“系统总是无法进入休眠”的问题,第一件事就是看看/sys/kernel/debug/wakeup_sources里哪些 source 还处于 active 状态。这个文件列出了每个 wakeup source 的名字、活跃计数、上次活跃时间,是定位功耗问题最直接的入口之一。
4.2 runtime PM:设备级功耗管理与 PM Core 的关系
很多人会有个困惑:runtime PM(运行时电源管理)到底算不算 PM Core 的一部分?答案是:算,但它是独立于系统级 suspend/resume 的一套机制。
系统级挂起是“所有设备一起睡”,runtime PM 是“单个设备自己睡”。两者通过同一个dev_pm_ops里的回调来工作,但触发方式和状态管理完全不同。
runtime PM 的核心思想是:一个设备在没人使用的时候,可以自行进入低功耗状态;一旦有人要用(比如open()、发起传输),就把它唤醒。内核提供了一组标准 API:
pm_runtime_get_sync(dev); /* 使用前,增加引用计数并唤醒 */ pm_runtime_put_sync(dev); /* 使用完,减少引用计数并允许睡眠 */ pm_runtime_allow(dev); /* 启用 runtime PM */ pm_runtime_forbid(dev); /* 禁用 runtime PM */引用计数归零后,框架会自动调用dev_pm_ops里的.runtime_suspend回调;计数从零变正时,调用.runtime_resume。
这个机制和系统级 suspend 的交互关系很有讲究。设备可能正处于 runtime suspended 状态,此时系统进入全局挂起,PM Core 就不会再调用它的.suspend,因为它已经睡了。同样,系统恢复时,runtime suspended 的设备也不会被.resume叫醒,它会继续保持自己的低功耗状态,直到有用户来用。
这个设计保证了“系统挂起”和“设备运行状态”互不干扰,也让驱动的逻辑能保持简单。不过它也是一把双刃剑:如果你在.runtime_resume里做了很重的初始化,系统级 resume 时相关的设备突然需要 IO,你就得面对“设备在 runtime suspend 时被系统级唤醒”的复杂时序。
4.3 autosleep 与 wake_lock:移动设备待机的隐藏功臣
如果你做过 Android 或者智能手表类产品,你一定见过wake_lock(唤醒锁)和autosleep(自动睡眠)的概念。它们在用户态表现为/sys/power/wake_lock和/sys/power/autosleep。而在内核里,它们都是建立在 PM Core 的 wakeup source 机制之上的。
autosleep的思路很聪明:系统不主动睡眠,而是“没有活跃 wakeup source 的时候就自动挂起”。它由一个内核线程持续监控 wakeup source 的活跃状态,一旦发现所有 source 都处于 inactive,就自动执行 suspend;如果睡眠过程中出现新的 wakeup 事件,系统被唤醒后继续监控。
Android 的wake_lock本质上就是把用户态的名字注册成一个 wakeup source。App 持锁时,source 是 active 的,系统不睡;App 释放锁后,source 变 inactive,autosleep 线程就会找机会让系统进入 suspend。这套配合把“应用行为”和“内核休眠决策”优雅地解耦了,也是 PM Core 分层设计在用户态影响最大的一处。
5. 实战:代码里怎么追踪 PM Core 的行为
5.1 /sys/power 下的调试接口清单
纸上谈兵到此结束。下面列出我平时排查功耗问题时会用的调试接口,大部分都在/sys/power/下面:
| 接口 | 用途 |
|---|---|
/sys/power/state | 查看/触发挂起状态 |
/sys/power/wakeup_count | 唤醒计数握手协议 |
/sys/power/wake_lock | 用户态创建命名 wakeup source |
/sys/power/wake_unlock | 释放用户态 wakeup source |
/sys/power/autosleep | 启用/禁用自动睡眠 |
/sys/kernel/debug/wakeup_sources | 查看所有 wakeup source 状态 |
/sys/devices/.../power/control | 查看/控制设备 runtime PM 策略 |
/sys/devices/.../power/wakeup | 查看设备是否能用做唤醒源 |
使用/sys/kernel/debug/wakeup_sources是定位“系统睡不着”问题的第一步。你会看到类似这样的输出:
name active_count active_since total_time max_time event0 0 0 0 0 gpio-keys 0 0 0 0 pm8921-rtc 0 0 0 0大部分时候,问题就出在一个你没想到的设备上,active_count一直不为零。看到的那一刻,你就知道该去查哪个驱动了。
5.2 用 ftrace 的 suspend_resume 事件观测设备回调
只想在 shell 层面看设备回调顺序,又不想改代码加打印,那ftrace的suspend_resume事件组就是最好的工具。用法很简单:
# 挂载 tracefs mount -t tracefs tracefs /sys/kernel/tracing # 打开 suspend/resume 相关事件 echo 1 > /sys/kernel/tracing/events/suspend_resume/enable # 开始记录 echo 1 > /sys/kernel/tracing/tracing_on # 触发一次挂起 echo mem > /sys/power/state # 停止记录并查看 echo 0 > /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace你会在输出里看到类似下面的片段:
migration/0-11 [000] d... 123.456789: suspend_resume: suspend_start kthreadd-2 [000] d... 123.456801: suspend_resume: dpm_suspend_start kworker/0:1-5 [000] d... 123.456810: suspend_resume: device_suspend: platform kworker/0:1-5 [000] d... 123.456823: suspend_resume: device_suspend: I2C0 ...这里最实用的信息是device_suspend事件旁边的设备名列表,它直接展示了 dpm 链表的遍历顺序。如果某个设备长时间卡住,你也会在时间戳里看出异常——相邻两个设备之间隔了几百毫秒甚至更久,说明前一个设备的 suspend 回调做了太多不可控的事情。
5.3 从零写一个带 dev_pm_ops 的驱动来验证休眠时序
理解 PM Core 最快的方法,是自己写一个带有 PM 回调的驱动,然后看着它的打印在休眠流程里出现。下面给一个最小化的例子,注册一个平台驱动,在挂起/恢复时打印日志:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/pm.h> static int demo_suspend(struct device *dev) { dev_info(dev, "suspend callback called\n"); return 0; } static int demo_resume(struct device *dev) { dev_info(dev, "resume callback called\n"); return 0; } static const struct dev_pm_ops demo_pm_ops = { .suspend = demo_suspend, .resume = demo_resume, .suspend_late = demo_suspend, .resume_early = demo_resume, }; static struct platform_driver demo_driver = { .driver = { .name = "demo_pm", .pm = &demo_pm_ops, }, }; static int __init demo_init(void) { return platform_driver_register(&demo_driver); } static void __exit demo_exit(void) { platform_driver_unregister(&demo_driver); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL");如果你的平台上有设备树节点匹配demo_pm,加载模块后触发一次echo mem > /sys/power/state,你就能在dmesg里看到suspend callback called在哪个阶段出现。这个实验的最大价值是让你直观感受到“PM Core 遍历设备”这件事本身——你的驱动只是茫茫设备海中的一个。
注意,我故意同时注册了.suspend和.suspend_late,你会在日志里看到它们按阶段分别触发。如果只注册.suspend,内核也会默默跳过 late 阶段——这是“回调可选”设计最直接的验证。
5.4 调试中容易被忽略的三件事
第一件:在 suspend/resume 回调里千万不要长时间自旋。这些回调运行在系统挂起/恢复的关键路径上,耗时越长,整机休眠/唤醒就越慢。如果回调里有 200ms 的延时,产品经理立刻就会在功耗报告里发现“唤醒太慢”。把耗时操作放到.prepare里做,或者干脆用异步机制去处理。
第二件:注意dev_info在 noirq 阶段的可用性。.suspend_noirq阶段中断已经被禁用,打印可能不可靠。内核提供了一些特殊的打印函数,但调试时最好避免在这个阶段做任何输出,否则你可能会看到日志中途断掉,误以为驱动崩溃。
第三件:唤醒源的中断标志不能乱设。很多设备想当唤醒源,但它的中断没有被正确地注册为 wakeirq,或者中断触发方式有问题。结果就是系统永远无法进入深度睡眠,或者休眠后一旦有中断,立刻唤醒但驱动已经来不及完整恢复。内核里有个机制叫dev_pm_set_wake_irq(),专门把某个中断关联到设备的唤醒源管理上,这个接口比自己在驱动里裸玩 IRQ 要安全得多。
从 PM Core 的设计,到/sys/power/state的一次挂起流程,再到 wakeup source 和 runtime PM 的配合,我希望你已经对 Linux 内核功耗子系统的骨架有了一个整体印象。这套分层设计真正的精妙之处在于:它用严格的时序契约(prepare/late/noirq)承载了无数硬件平台的能力差异,让“设备驱动”和“平台电源策略”可以独立演进——驱动只负责把自己管好,平台负责把所有驱动兜起来。下一篇我会挑drivers/base/power/main.c里的dpm_suspend()展开,专门聊聊设备链表排序背后的依赖规则和那些让人抓狂的边界情况。