参考资料:Linux kernel 源码 drivers/base/power/main.c、include/linux/pm.h、include/linux/pm_runtime.h。主题:DPM(Device Power Management)框架如何在整机休眠时按依赖顺序遍历所有设备。
全景介绍
第七篇讲了 Runtime PM:单个设备 idle 了可以自己 suspend,忙了再 resume,和别的设备没关系,中断和调度器都正常工作。
但 system sleep 不一样。用户按下电源键或合上盖子,内核要把所有设备按正确顺序逐个 suspend,然后才能调平台固件进入深度低功耗。这个"按正确顺序遍历所有设备"的工作,由 DPM(Device Power Management) 框架完成。
DPM 是 PM Core 中管理设备 suspend/resume 遍历顺序的核心机制。它解决的问题很具体:给定一棵设备树,怎么保证父设备不比子设备先关掉?怎么在每个阶段(prepare → suspend → suspend_late → suspend_noirq)正确遍历?resume 时怎么反向恢复?
这不是 Runtime PM 能做的事。Runtime PM 关心的是"一个设备什么时候该 suspend",DPM 关心的是"所有设备按什么顺序 suspend"。两者通过 struct dev_pm_ops 共享同一套回调结构体,但遍历逻辑完全不同。
实际情况
DPM 的实现里,有两个设计决策直接影响驱动行为。
第一,遍历顺序是链表,不是树。DPM 维护全局链表 dpm_list,设备在 device_add() 时按拓扑序插入。suspend 时从链表尾部反向遍历,先 suspend 子设备再 suspend 父设备;resume 时正向遍历,先 resume 父设备再 resume 子设备。
第二,回调查找有优先级。设备的 system sleep 回调不只来自驱动,DPM 按 pm_domain > type > class > bus > driver 的优先级查找,找到第一个非 NULL 就调用。SoC 厂商可以在 pm_domain 层面统一处理所有设备的 suspend/resume,驱动自己的回调不会被调用。
抽象对象
dpm_list:全局设备链表
// drivers/base/power/main.c
static LIST_HEAD(dpm_list); // 所有待 suspend 的设备
static LIST_HEAD(dpm_prepared_list); // 已完成 prepare
static LIST_HEAD(dpm_suspended_list); // 已完成 suspend
static LIST_HEAD(dpm_late_early_list); // 已完成 suspend_late
static LIST_HEAD(dpm_noirq_list); // 已完成 suspend_noirq
设备在每个阶段完成后从当前链表移到下一条链表。这是 DPM 的核心数据结构——五条链表追踪设备当前所处的阶段。
设备在 device_add() 时插入 dpm_list,顺序由设备树拓扑决定。dpm_suspend() 遍历 dpm_prepared_list,对每个设备调用 suspend 回调,成功后移到 dpm_suspended_list。以此类推。
dev_pm_ops:回调结构体
// include/linux/pm.h
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 (*suspend_late)(struct device *dev);
int (*resume_early)(struct device *dev);
int (*suspend_noirq)(struct device *dev);
int (*resume_noirq)(struct device *dev);
int (*freeze)(struct device *dev);
int (*thaw)(struct device *dev);
int (*poweroff)(struct device *dev);
int (*restore)(struct device *dev);
int (*runtime_suspend)(struct device *dev);
int (*runtime_resume)(struct device *dev);
int (*runtime_idle)(struct device *dev);
};
看着多,实际分四组:
- • suspend/resume 组:用于 mem(suspend-to-RAM)路径
- • freeze/thaw 组:用于 freeze(s2idle)路径
- • poweroff/restore 组:用于 hibernation 路径
- • runtime 组:Runtime PM 路径
每组又分三个子阶段:suspend → suspend_late → suspend_noirq。驱动通常用 SET_SYSTEM_SLEEP_PM_OPS 宏来注册 suspend/resume 对。
dpm_suspend:遍历函数
// drivers/base/power/main.c
void dpm_suspend(pm_message_t state)
{
struct device *dev;
list_for_each_entry_reverse(dev, &dpm_prepared_list, power.entry) {
error = device_suspend(dev);
if (!error)
list_move(&dev->power.entry, &dpm_suspended_list);
}
}
dpm_suspend() 用 list_for_each_entry_reverse 从链表尾部开始遍历——因为 dpm_prepared_list 的顺序是父在前、子在后,反向遍历就是先 suspend 子设备再 suspend 父设备。

模型
多阶段遍历模型
DPM 把 suspend/resume 分成五个阶段,每个阶段遍历同一条链表,成功后把设备移到下一条链表。这个设计的好处是:每个阶段的上下文限制不同(中断是否屏蔽、是否可睡眠),驱动可以根据需要选择在哪个阶段做操作。
suspend 方向(从叶子到根):
dpm_prepare():
遍历 dpm_list(正向,父在前子在后)
对每个设备调用 dev->pm_domain->ops.prepare()
成功 → list_move_tail(dev, &dpm_prepared_list)
dpm_suspend():
遍历 dpm_prepared_list(反向,先遇到子设备)
对每个设备调用 .suspend()
成功 → list_move(dev, &dpm_suspended_list)
dpm_suspend_late():
遍历 dpm_suspended_list(反向)
对每个设备调用 .suspend_late()
成功 → list_move(dev, &dpm_late_early_list)
dpm_suspend_noirq():
遍历 dpm_late_early_list(反向)
对每个设备调用 .suspend_noirq()
成功 → list_move(dev, &dpm_noirq_list)
resume 方向(从根到叶子),每步正向遍历,回调反向调用:
dpm_resume_noirq(): dpm_noirq_list
→ .resume_noirq()
→ 移回 dpm_late_early_list
dpm_resume_early(): dpm_late_early_list
→ .resume_early()
→ 移回 dpm_suspended_list
dpm_resume(): dpm_suspended_list
→ .resume()
→ 移回 dpm_prepared_list
dpm_complete(): dpm_prepared_list
→ .complete()
→ 移回 dpm_list
上图展示了五条链表和设备在各阶段间的迁移。suspend 方向水平箭头表示从左到右依次迁移;resume 方向折线箭头表示反向迁移。
依赖顺序保证
设备在 dpm_list 中的顺序在 device_add() 时确定。如果设备 B 的 parent 是 A,那么 A 在 B 前面。DPM 的 suspend 遍历用 list_for_each_entry_reverse 从尾部开始,所以先遇到子设备 B,suspend B 之后才 suspend A。
resume 时用 list_for_each_entry 正向遍历,先遇到父设备 A,先 resume A,再 resume B。
这保证了:suspend 时子设备先关、父设备后关;resume 时父设备先开、子设备后开。子设备在 suspend 回调里访问父设备提供的资源(时钟、电源、总线)时,父设备一定还活着。
回调查找优先级
DPM 调用回调时,不直接用 dev->driver->pm,而是按优先级查找:
// drivers/base/power/main.c
static int dev_pm_get_callbacks(struct device *dev, ...)
{
if (dev->pm_domain) return 1; // 用 pm_domain->ops
if (dev->type && dev->type->pm) return 2;
if (dev->class && dev->class->pm) return 3;
if (dev->bus && dev->bus->pm) return 4;
if (dev->driver && dev->driver->pm) return 5;
return 0;
}
优先级:pm_domain > type > class > bus > driver。找到第一个非 NULL 就调用。这意味着:
- • 如果 SoC 厂商在
pm_domain 层面定义了统一的 suspend/resume,驱动自己的回调不会被调用 - • 如果总线层(如 PCI、I2C)定义了通用的 suspend/resume,驱动可以不实现——总线层的回调会覆盖
.suspend()/.resume只有在上面四层都没有回调时才会被调用上图展示了这个五级查找链:从 dev->pm_domain->ops
开始,逐级向下查找,找到第一个非 NULL 就停止。
数据流
从用户态写 /sys/power/state 到所有设备 suspend 完,再到唤醒恢复,完整的调用链如下。上图展示了这条调用链的全景——suspend 路径从上往下走,resume 路径从下往上走。
suspend 路径
enter_state(state)
│
├─ 1. suspend_prepare(state)
│ ├─ pm_prepare_console() // 切换到安全控制台
│ ├─ pm_notifier_call_chain(PM_SUSPEND_PREPARE)
│ │ // 通知所有注册了 PM notifier 的模块:"准备 suspend 了"
│ │ // 内核子系统(如 workqueue、RCU)在这里做准备工作
│ ├─ suspend_freeze_processes() // 冻结所有用户态进程
│ │ // 发送 fake signal,等待所有进程进入 frozen 状态
│ │ // 这一步之后,用户态不会再有新的 I/O 请求
│ └─ dpm_prepare(state) // 遍历 dpm_list,调用 prepare
│ └─ list_for_each_entry(dev, &dpm_list)
│ → device_prepare(dev) // 查找回调并调用 .prepare()
│ → 成功: list_move_tail(dev, &dpm_prepared_list)
│ → 失败: 跳过,继续下一个设备
│
├─ 2. suspend_devices_and_enter(state)
│ ├─ suspend_device_irqs() // 屏蔽所有非 wakeup IRQ
│ │ // 这一步之后,只有标记了 IRQF_NO_SUSPEND 的 IRQ 还能触发
│ │ // 设备驱动的中断处理函数不会再被调用
│ │
│ ├─ dpm_suspend(state) // 遍历 dpm_prepared_list(反向)
│ │ └─ list_for_each_entry_reverse(dev, &dpm_prepared_list)
│ │ → device_suspend(dev) // 查找回调并调用 .suspend()
│ │ → 成功: list_move(dev, &dpm_suspended_list)
│ │ → 失败: 中止 suspend,开始 resume 恢复
│ │
│ ├─ dpm_suspend_late(state) // 遍历 dpm_suspended_list(反向)
│ │ └─ 此时 IRQ 已屏蔽,回调不能做 I2C/SPI 等需要中断的操作
│ │ └→ .suspend_late() 通常用于关闭中断相关的资源
│ │
│ ├─ dpm_suspend_noirq(state) // 遍历 dpm_late_early_list(反向)
│ │ └─ 此时仅 NMI 还能触发,回调只能做最简单的硬件状态确认
│ │
│ ├─ disable_nonboot_cpus() // 关闭除 boot CPU 以外的所有 CPU
│ │ // 减少功耗,确保只有 boot CPU 在运行 suspend 代码
│ │
│ ├─ syscore_suspend() // 系统核心模块的 suspend
│ │ // 中断控制器、时钟源、timer 等核心模块在这里保存状态
│ │
│ └─ suspend_ops->enter() // 调用平台固件的 enter 接口
│ // ARM: PSCI firmware 调用,CPU 进入深度低功耗
│ // x86: ACPI _PTS + 写 PM1a_CNT 寄存器
│ // 到这里,CPU 已经停止执行,等待唤醒事件
resume 路径
唤醒事件(GPIO 中断、RTC alarm、Power 键等)触发 CPU 从低功耗状态恢复:
│ // --- CPU 被唤醒事件唤醒,从这里开始 resume ---
│
│ ├─ suspend_ops->wake() // 平台固件的 wake 接口
│ ├─ syscore_resume() // 恢复中断控制器、时钟源、timer
│ ├─ enable_nonboot_cpus() // 重新启动其他 CPU
│ │
│ ├─ dpm_resume_noirq(state) // 遍历 dpm_noirq_list(正向)
│ │ └→ .resume_noirq() 恢复硬件状态,为后续 resume 做准备
│ │
│ ├─ dpm_resume_early(state) // 遍历 dpm_late_early_list(正向)
│ │ └→ .resume_early() 恢复中断相关的资源
│ │
│ ├─ resume_device_irqs() // 重新启用设备 IRQ
│ │ // 这一步之后,设备中断处理函数可以正常触发
│ │
│ ├─ dpm_resume(state) // 遍历 dpm_suspended_list(正向)
│ │ └→ .resume() 恢复设备功能,重新初始化硬件
│ │ └→ 此时 IRQ 已启用,可以做 I2C/SPI 等操作
│ │
│ └─ dpm_complete(state) // 遍历 dpm_prepared_list(正向)
│ └→ .complete() 清理 prepare 阶段的标记
│ └→ list_move_tail(dev, &dpm_list) // 移回原始链表
│
└─ 3. suspend_finish(state)
├─ thaw_processes() // 解冻用户态进程
└─ pm_notifier_call_chain(PM_POST_SUSPEND)
// 通知所有模块:"suspend/resume 结束了"
关键设计点
这条调用链有几个值得注意的设计:

运行
回调阶段与上下文限制
DPM 的五个阶段,每个阶段的中断状态和可睡眠性不同。驱动必须根据回调里需要做什么操作,选择合适的阶段:
- I2C 设备驱动:需要通过 I2C 总线写寄存器 → 只能在
suspend 阶段(I2C传输依赖中断)
- 1. 失败回滚:任何阶段的 suspend 失败,都会触发 resume 路径恢复已 suspend 的设备。不会留下"半 suspend"的状态。
- 2. IRQ 屏蔽时机:
suspend_device_irqs() 在 dpm_suspend() 之前调用。这意味着 dpm_suspend() 阶段设备 IRQ 已经被屏蔽了——但 suspend 回调本身还可以做 I2C/SPI 操作(因为这些操作不依赖设备 IRQ,而是由 CPU 主动发起)。 - 3. CPU 下线顺序:
disable_nonboot_cpus() 在所有设备 suspend 完之后才调用。这保证了多核系统上,所有 CPU 都参与了设备 suspend 的工作。 - 4. dpm_complete 的角色:
dpm_complete() 是 prepare 的逆操作。prepare 阶段检查设备是否可以 suspend,complete 阶段清理这些检查标记,把设备移回 dpm_list,为下一次 suspend 做准备。
- • GPIO 中断控制器:需要关闭中断资源 → 在
suspend_late 阶段做(此时 IRQ 已屏蔽,不会出现"关到一半来了个新中断"的情况) - • PCI 设备:需要写 PCI 配置空间 → 在
suspend 阶段做(MMIO 可以在任何阶段,但 PCI config space 访问可能依赖中断) - • 平台时钟驱动:需要关闭 PLL → 在
suspend_noirq 阶段做(确保所有设备都 suspend 完了,没人再用这个时钟) SET_SYSTEM_SLEEP_PM_OPS 宏
内核提供了宏来简化驱动注册,避免直接填充 dev_pm_ops 结构体:
// 最常用:suspend/resume 对
#define SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \
.suspend = suspend_fn, .resume = resume_fn
// 如果需要在 late 阶段做操作(IRQ 屏蔽后)
#define SET_LATE_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \
.suspend_late = suspend_fn, .resume_early = resume_fn
// 如果需要在 noirq 阶段做操作
#define SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \
.suspend_noirq = suspend_fn, .resume_noirq = resume_fn
大多数驱动只用 SET_SYSTEM_SLEEP_PM_OPS。只有需要在 IRQ 屏蔽后做操作的驱动才用 SET_LATE_SYSTEM_SLEEP_PM_OPS。使用示例:
static conststruct dev_pm_ops my_device_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(my_device_suspend, my_device_resume)
};
DPM 与 Runtime PM 的交互
DPM 和 Runtime PM 通过 pm_runtime_force_suspend() / pm_runtime_force_resume() 交互。驱动在 system sleep 的 suspend 回调里调用 pm_runtime_force_suspend(),确保设备不是 runtime active 状态:
static int my_device_suspend(struct device *dev)
{
// 先确保设备不是 runtime active
pm_runtime_force_suspend(dev);
// 然后做设备特定的 suspend 操作
return 0;
}
这避免了"设备 runtime active 时被 system sleep 强制关掉"导致的状态不一致。如果设备在 system sleep 时还是 runtime active(比如用户态一直在用),pm_runtime_force_suspend() 会先把它 suspend 掉,再让 system sleep 继续。
DPM 的错误处理
DPM 对 suspend 失败的处理是"立即 resume 恢复已 suspend 的设备":
dpm_suspend() 失败:
1. 记录失败设备的错误码
2. 调用 dpm_resume_for_each_dev() 恢复已 suspend 的设备
3. 返回错误码,enter_state() 会调用 suspend_finish() 清理
这意味着驱动的 suspend 回调返回非零值时,DPM 会自动 resume 所有已经 suspend 的设备。驱动不需要自己做回滚——DPM 框架会处理。
上图展示了完整的 suspend/resume 调用链:suspend 路径从上到下(prepare → suspend → suspend_late → suspend_noirq → enter),resume 路径从下到上(wake → resume_noirq → resume_early → resume → complete)。左侧是 suspend 阶段,右侧是 resume 阶段,中间是各阶段的链表迁移方向。

I2C suspend 超时
一个 I2C 设备驱动在 suspend 回调里通过 I2C 写寄存器让设备进入低功耗模式。但 suspend 时报错 "i2c transfer timeout"。
从 ftrace 日志看到 DPM 遍历顺序:
dpm_suspend: i2c-adapter (i2c-0) → suspend OK
dpm_suspend: my-device (i2c-0:0x48) → suspend FAIL (i2c timeout)
问题:i2c-0 的 suspend 回调把 I2C 控制器的时钟关了,但 my-device 的 suspend 回调还在执行。因为 dpm_suspend() 用 list_for_each_entry_reverse 遍历,i2c-0 先被 suspend,然后才轮到 my-device——但此时 I2C 控制器已经关了。
修复:i2c-0 应该用 suspend_late 而不是 suspend,确保在所有 I2C 设备 suspend 完后才关控制器时钟。或者用 SET_LATE_SYSTEM_SLEEP_PM_OPS 宏注册 suspend_late 回调。
pm_domain 统一 suspend
SoC 平台有 20 多个设备,每个设备的 suspend 都需要写同一个 power domain 寄存器。如果每个驱动都自己写,代码重复且容易出错。
解决方案:在 pm_domain 层面实现统一的 suspend/resume:
static int my_soc_pd_suspend(struct device *dev)
{
return my_soc_pd_set_low_power(dev->pm_domain);
}
staticstruct dev_pm_domain my_soc_pd_domain = {
.ops = {
.suspend = my_soc_pd_suspend,
.resume = my_soc_pd_resume,
},
};
所有设备的 dev->pm_domain 指向同一个 domain。DPM 回调查找时,pm_domain->ops 优先级最高,驱动自己的 suspend 回调不会被调用。这实现了"SoC 厂商统一处理,驱动无需关心"的效果。
总结
DPM 是 System Sleep 的"骨架"——它负责在整机休眠时按依赖顺序遍历所有设备,调用每个阶段的回调。
核心机制:
- 1. 五条链表:
dpm_list → dpm_prepared_list → dpm_suspended_list → dpm_late_early_list → dpm_noirq_list,设备在每阶段完成后迁移。 - 2. 反向遍历:
dpm_suspend() 用 list_for_each_entry_reverse 从链表尾部开始,先 suspend 子设备再 suspend 父设备。 - 3. 回调查找优先级:
pm_domain > type > class > bus > driver,SoC 厂商可以在 pm_domain 层面统一处理。 - 4. 与 Runtime PM 共享:通过
struct dev_pm_ops 共享同一套回调结构体,但遍历逻辑完全不同。 - 5. 错误回滚:任何阶段 suspend 失败,DPM 自动 resume 已 suspend 的设备。
DPM 解决的是"所有设备按什么顺序 suspend",Runtime PM 解决的是"一个设备什么时候该 suspend"。理解了 DPM,System Sleep 设备遍历逻辑就清楚了。