news 2026/10/9 3:42:31

Linux DPM设备电源管理框架:系统睡眠挂起恢复调度机制与调试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux DPM设备电源管理框架:系统睡眠挂起恢复调度机制与调试实战

1. 从一次待机唤醒失败说起:DPM框架到底管什么

去年冬天调一块嵌入式板子,设备进入待机后按电源键没反应,串口也死了,只能拔电重启。当时第一反应是电源管理芯片配置有问题,查了两天寄存器没找到毛病,最后把/sys/power/state的写入流程从头跟了一遍,才发现问题出在设备挂起顺序上——某个外设的->suspend回调里做了耗时操作,把整个系统睡眠流程卡在了中间态。这件事让我意识到,很多人做功耗优化时盯着CPU调频、盯着时钟门控,却忽略了系统睡眠这条链路上真正起调度作用的角色:DPM(Device Power Management,设备电源管理)框架。

DPM是Linux内核功耗子系统里负责"系统级睡眠"的核心机制。它要解决的问题很具体:当系统决定进入Suspend-to-RAM、Suspend-to-Disk或者更深度的低功耗状态时,成百上千个设备不能一窝蜂地同时挂起,也不能随便挑一个先挂。谁先谁后、父子设备怎么协调、挂起失败怎么回滚、唤醒时按什么顺序恢复,这些都需要一套统一的规则来管。DPM就是这套规则的执行者。

这篇文章适合三类人看:一是做嵌入式Linux功耗优化的工程师,你迟早要跟dpm_list打交道;二是写设备驱动的开发者,你的dev_pm_ops回调什么时候被调用、被谁调用,DPM说了算;三是想深入理解内核电源管理子系统的学习者,DPM是绕不开的一环。我会把DPM的链表结构、状态机、挂起/恢复的完整调用链、以及实际调试中踩过的坑都摊开讲,尽量让你看完能直接对着代码和/sys节点复现。

需要先说明一点:DPM不是孤立的,它和Runtime PM、System Sleep、Wakeup Source这几个机制互相咬合。本文聚焦在系统睡眠(System Sleep)场景下DPM的调度逻辑,Runtime PM只在必要的地方带一句,避免话题发散。

2. dpm_list与dpm_suspended_list:两条链表撑起整个调度

2.1 设备是怎么被挂到dpm_list上的

内核里每个struct device都有一个成员叫power,类型是struct dev_pm_info。这个结构体里藏着DPM调度需要的所有信息,其中最关键的是两个链表节点:

struct dev_pm_info { ... struct list_head entry; /* 挂在dpm_list或dpm_suspended_list上 */ struct list_head sibling; /* 挂在父设备的power.children上 */ ... pm_message_t power_state; unsigned int can_wakeup:1; unsigned int async_suspend:1; ... };

设备注册的时候,device_pm_add()会被调用,把设备的power.entry挂到全局链表dpm_list的尾部。注意这里是尾部插入,而且父设备一定比子设备先注册,所以dpm_list天然形成了一个"父在前、子在后的拓扑序"。这个顺序不是随便定的,它是后面挂起顺序的基础。

我见过有人问:为什么不用红黑树或者别的结构?答案很简单,设备挂起本质上是一个有依赖关系的线性序列问题,链表插入删除O(1),遍历O(n),对于几百上千个设备的系统完全够用,没必要上更复杂的数据结构。内核在这类地方一向务实。

2.2 dpm_suspended_list的登场时机

系统开始进入睡眠时,dpm_suspend()会遍历dpm_list,对每个设备调用其->suspend回调。每成功挂起一个设备,就把它从dpm_list摘下来,挂到dpm_suspended_list上。这个动作看着不起眼,但意义重大:

  • dpm_list上剩下的都是还没挂起的设备;
  • dpm_suspended_list上都是已经挂起的设备;
  • 如果中途某个设备挂起失败,回滚时只需要遍历dpm_suspended_list,把已经挂起的按相反顺序恢复,不用去碰还没挂起的部分。

这两条链表的关系,我用一个表格说清楚:

链表挂载内容遍历方向典型使用场景
dpm_list所有已注册设备(未挂起的)正向(父→子)系统睡眠挂起阶段
dpm_suspended_list已成功挂起的设备反向(子→父)挂起失败回滚、系统恢复阶段
dpm_prepared_list已完成prepare的设备反向prepare失败回滚
dpm_off_list已关闭的设备(关机路径)反向关机回滚

注意:dpm_list的遍历方向是"从链表头到链表尾",而由于父设备先注册,头部的设备往往是根设备(比如PCI host bridge),尾部的设备是叶子设备。所以挂起顺序是根先挂、叶子后挂,恢复顺序反过来。

2.3 为什么挂起顺序是"父先子后"

这个顺序初看有点反直觉。直觉上你会觉得,应该先把叶子设备(比如USB键盘)挂起,再挂它的父控制器(USB host controller),最后挂根总线。但DPM的设计恰恰相反。

原因在于:父设备在挂起时,可能需要访问子设备来完成某些操作。举个实际例子,一个PCIe网卡在挂起时,它的->suspend回调里可能要读网卡寄存器保存状态,而这个寄存器访问要经过PCIe控制器。如果控制器先挂了,网卡就读不了寄存器了。所以必须父设备先挂起,但父设备挂起时不能切断对子设备的访问通路,等所有子设备都挂完了,父设备才真正进入低功耗。

这个逻辑在代码里的体现是:dpm_suspend()正向遍历dpm_list,但每个设备的->suspend回调只是"准备挂起",真正的电源切断往往在->suspend_late或者->suspend_noirq阶段。而恢复时反向遍历,先恢复父设备,让访问通路先通,再恢复子设备。

3. 系统睡眠的完整调用链:从write到/sys/power/state开始

3.1 用户态触发路径

用户态让系统进入睡眠,最直接的方式是:

echo mem > /sys/power/state

这一行命令背后,内核走了一条相当长的路径。我把它拆成几个关键节点:

  1. state_store()接收写入的字符串"mem",解析成PM_SUSPEND_MEM;
  2. 调用pm_suspend(PM_SUSPEND_MEM);
  3. pm_suspend()先做合法性检查,然后调用enter_state();
  4. enter_state()是核心,它依次调用suspend_prepare()、suspend_devices_and_enter()、suspend_finish()。

其中suspend_devices_and_enter()是DPM真正干活的地方。它的调用序列大致是:

dpm_suspend_start(PMSG_SUSPEND); // prepare + suspend dpm_suspend_end(PMSG_SUSPEND); // suspend_late + suspend_noirq syscore_suspend(); // 系统核心设备挂起 // ... 进入平台相关的低功耗代码 ... syscore_resume(); dpm_resume_start(PMSG_RESUME); // resume_noirq + resume_early dpm_resume_end(PMSG_RESUME); // resume + complete

3.2 dpm_suspend_start做了什么

dpm_suspend_start()内部先调dpm_prepare(),再调dpm_suspend()。这两个阶段的分工是:

  • prepare阶段:调用每个设备的->prepare回调。这个回调的语义是"我要开始睡眠了,你检查一下自己能不能睡"。如果某个设备返回错误,整个睡眠流程在这里就中止,还没真正挂起任何设备,回滚成本最低。
  • suspend阶段:调用->suspend回调,设备开始保存状态、关闭部分功能。这个阶段失败的话,需要回滚已经挂起的设备。

我实际调试时发现,很多驱动作者会把耗时的操作放在->suspend里,比如写Flash、等硬件响应。这是不推荐的,因为->suspend阶段系统还在正常运行,中断还开着,耗时操作会拖长整个睡眠流程,还可能因为中断竞争出问题。正确的做法是把耗时操作放到->prepare里,或者用异步挂起(async_suspend标志)。

3.3 suspend_late与suspend_noirq的边界

dpm_suspend_end()调用dpm_suspend_late()和dpm_suspend_noirq()。这两个阶段的区别在于中断是否关闭:

  • suspend_late阶段:中断还开着,但其他设备可能已经挂了,不能依赖其他设备;
  • suspend_noirq阶段:本地中断已关闭,不能睡眠,不能调用可能睡眠的函数。

这个边界非常重要。我踩过一次坑:在某个I2C设备的->suspend_noirq回调里调用了i2c_transfer(),结果系统直接卡死。原因是i2c_transfer()内部会睡眠等完成量,而noirq阶段不允许睡眠。后来把这段逻辑挪到->suspend里才解决。

经验:->suspend_noirq回调里只能做寄存器读写、内存拷贝这类不会睡眠的操作。任何可能引起调度的函数调用都是禁忌。

4. 设备挂起回调的四种形态与选型逻辑

4.1 dev_pm_ops结构体全貌

一个设备驱动要参与DPM调度,需要提供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); ... };

看着很多,其实可以按两个维度分类:睡眠类型(suspend/freeze/poweroff)和阶段(普通/late/noirq)。系统睡眠(Suspend-to-RAM)走的是suspend系列,冬眠(Suspend-to-Disk)走的是freeze系列,关机走poweroff系列。

4.2 各阶段回调的职责划分

我把suspend系列四个阶段的职责整理成表:

回调中断状态能否睡眠典型职责
->prepare开能检查设备状态,申请资源,拒绝睡眠
->suspend开能保存寄存器,关闭非必要功能
->suspend_late开能关闭时钟,准备断电
->suspend_noirq关不能最后的寄存器操作,进入低功耗

恢复时顺序完全相反:->resume_noirq→->resume_early→->resume→->complete。

4.3 什么时候该用哪个回调

这是驱动开发者最常问的问题。我的经验是:

  • 如果只是保存几个寄存器,放->suspend就够了;
  • 如果要关时钟、关电源域,放->suspend_late;
  • 如果要在中断关闭后做最后的硬件操作,放->suspend_noirq;
  • 如果只是检查能不能睡,放->prepare。

有个反直觉的点:不是所有驱动都需要实现全部回调。很多简单设备只实现->suspend和->resume就能正常工作。内核的dpm_run_callback()会检查回调是否存在,不存在就跳过,不会报错。

但要注意,如果你实现了->suspend_noirq,就必须实现对应的->resume_noirq,否则恢复时状态对不上。这个对称性要求在内核文档里写得很清楚,但实际开发中还是有人漏掉。

5. 异步挂起:让睡眠流程快起来的双刃剑

5.1 async_suspend标志的作用

设备结构体里有个async_suspend标志,置位后该设备的->suspend回调会在一个工作队列里异步执行,不阻塞主流程。对于挂起耗时较长的设备(比如要等硬件响应的存储设备),这能显著缩短整体睡眠时间。

启用方式很简单,在驱动里:

device_enable_async_suspend(dev);

或者在设备树里配置。但异步挂起有个前提:该设备不能有子设备依赖它的挂起顺序。因为异步执行意味着它的挂起完成时间不确定,如果子设备需要等它挂完才能挂,就会出问题。

5.2 异步挂起的同步点

内核用dpm_suspend()里的async_synchronize_full()来等待所有异步挂起完成。这个调用点很关键:它保证在进入suspend_late阶段之前,所有异步挂起的设备都已经挂完了。

我实测过一个场景:一块板子上有8个USB设备,全部启用异步挂起后,整体睡眠时间从120ms降到了45ms。但代价是调试变难了——如果某个设备挂起失败,错误信息可能和主流程的日志交错,不容易定位。

经验:异步挂起适合挂起耗时且独立的设备。如果设备之间有依赖关系,老老实实用同步挂起,别为了省几十毫秒引入难查的竞态问题。

5.3 异步挂起的错误处理

异步挂起失败时,错误码会被记录在设备的power.async_error里,主流程在async_synchronize_full()之后检查这个值。如果非零,整个睡眠流程会中止并回滚。这个机制保证了异步挂起不会"悄悄失败"。

但有个坑:如果异步挂起的设备在回调里睡眠太久,async_synchronize_full()会一直等,表现为系统卡在睡眠流程里不动。这时候用echo w > /proc/sysrq-trigger能看到工作队列的堆栈,定位是哪个设备卡住了。

6. 唤醒源与DPM的交互:谁能把系统叫醒

6.1 wakeup source的注册与使能

不是所有设备都能唤醒系统。设备要成为唤醒源,需要:

device_init_wakeup(dev, true);

这会设置dev->power.can_wakeup标志,并把设备注册到唤醒源链表。用户态可以通过/sys/devices/.../power/wakeup查看和修改使能状态:

cat /sys/devices/platform/serial0/power/wakeup echo enabled > /sys/devices/platform/serial0/power/wakeup

6.2 唤醒源在挂起流程中的特殊处理

DPM在挂起设备时,对唤醒源有特殊照顾。如果一个设备是唤醒源且已使能,它的->suspend回调不应该完全关闭设备,而要保留唤醒能力。这通常通过enable_irq_wake()来实现:

static int my_suspend(struct device *dev) { if (device_may_wakeup(dev)) enable_irq_wake(dev->irq); else disable_irq(dev->irq); return 0; }

device_may_wakeup()会同时检查can_wakeup和wakeup使能状态。这个判断很重要,如果漏了,要么唤醒源失效,要么非唤醒源设备在睡眠中产生中断导致系统立即唤醒。

6.3 唤醒后的恢复顺序

系统被唤醒源叫醒后,DPM按dpm_suspended_list的反向顺序恢复设备。这里有个细节:唤醒源设备本身会最先被恢复,因为它的中断处理需要设备处于可用状态。内核在dpm_resume_noirq()里会优先处理唤醒源。

我遇到过一个问题:某个GPIO按键作为唤醒源,唤醒后按键事件丢失。排查发现是按键驱动的->resume_noirq回调里没有重新使能中断,导致唤醒后的第一次按键被漏掉。修复方法是在->resume_noirq里调用enable_irq()。

7. 调试DPM问题的实战手法

7.1 用ftrace跟踪挂起调用链

DPM的调用链很长,光看代码容易迷路。我习惯用ftrace抓实际执行路径:

cd /sys/kernel/debug/tracing echo function_graph > current_tracer echo dpm_* > set_ftrace_filter echo 1 > tracing_on echo mem > /sys/power/state # 唤醒后 cat trace > /tmp/dpm_trace.txt

这样能看到每个dpm_*函数的调用顺序和耗时。如果某个设备的回调特别慢,在function_graph输出里会显示为一大块,一眼就能看出来。

7.2 查看设备挂起状态

/sys/kernel/debug/pm_genpd/和/sys/kernel/debug/devices_deferred能提供一些线索,但最直接的还是看dpm_list。内核没有直接导出这个链表,但可以通过/sys/devices/.../power/下的文件间接判断。

更实用的方法是打开CONFIG_PM_DEBUG和CONFIG_PM_ADVANCED_DEBUG,然后:

cat /sys/power/pm_test # 输出类似:none core platform devices freezer

pm_test可以让你在睡眠流程的某个阶段停下来,不真正进入低功耗,方便调试。比如:

echo devices > /sys/power/pm_test echo mem > /sys/power/state

这样系统会走完设备挂起流程,但不进入平台低功耗代码,然后自动恢复。如果问题出在设备挂起阶段,用这个模式能稳定复现,不用担心唤醒失败。

7.3 常见问题与排查表

我把这些年遇到的DPM相关问题整理成表:

现象可能原因排查方法
系统卡在睡眠流程某个->suspend回调阻塞ftrace看哪个函数没返回
唤醒后设备不工作->resume回调缺失或顺序错检查dev_pm_ops对称性
睡眠后立即唤醒非唤醒源设备产生中断检查/sys/.../power/wakeup
唤醒源失效enable_irq_wake未调用检查device_may_wakeup分支
异步挂起超时工作队列被阻塞sysrq-trigger看堆栈

7.4 一个真实的排查案例

回到开头那个待机唤醒失败的问题。我用pm_test模式复现后,ftrace显示卡在某个I2C触摸屏驱动的->suspend回调里。进去看代码,发现它在->suspend里调用了一个自定义的msleep(200)等触摸屏固件进入低功耗。问题是这个msleep在中断开启的情况下被其他中断打断,实际等了超过500ms,而系统的睡眠超时是300ms,导致流程被中止。

修复方案是把这200ms的等待挪到->suspend_late里,并且改用usleep_range(),同时把触摸屏的async_suspend打开,让它在后台等,不阻塞主流程。改完后待机唤醒正常,整体睡眠时间还缩短了。

这个案例的教训是:DPM回调里的任何延时都要慎重,能用异步就用异步,能挪到后面的阶段就往后挪。

8. 写驱动时容易忽略的几个DPM细节

8.1 回调的返回值语义

->prepare返回0表示可以睡眠,返回-EBUSY表示拒绝。但->suspend返回非零会导致整个睡眠流程回滚,这个代价很大。所以->suspend里应该尽量避免返回错误,能容错就容错。

我见过一个驱动在->suspend里检查某个可选硬件状态,状态不对就返回-EIO,结果导致整个系统无法待机。正确的做法是打印警告但返回0,让睡眠继续。

8.2 恢复顺序与资源释放

恢复时设备按反向顺序恢复,这意味着子设备先恢复,父设备后恢复。如果子设备的->resume里需要访问父设备的资源,就会出问题。所以驱动设计时要注意:->resume里不要依赖父设备已经完全恢复。

8.3 电源域的协同

现代SoC普遍有电源域(Power Domain)概念,多个设备共享一个电源域。DPM在挂起设备时,如果电源域里还有设备没挂,就不能关这个域。这个逻辑由genpd框架处理,但驱动作者需要确保自己的设备正确加入了电源域,否则可能出现"设备挂了但电源域没关"的功耗泄漏。

检查方法:

cat /sys/kernel/debug/pm_genpd/pm_genpd_summary

输出会显示每个电源域的状态和其中的设备。

8.4 与Runtime PM的边界

系统睡眠时,DPM会先调用pm_runtime_resume()确保设备处于活跃状态,然后再走系统睡眠流程。这个设计是为了避免设备在Runtime PM挂起状态下再被系统睡眠挂起,导致状态混乱。

驱动里如果同时实现了Runtime PM和系统睡眠回调,要注意两者的状态机不要冲突。常见做法是在系统睡眠的->suspend里调用pm_runtime_disable(),在->resume里调用pm_runtime_enable()。

9. 关于DPM框架演进的一点个人观察

DPM框架从早期的简单链表调度,发展到今天支持异步挂起、电源域协同、唤醒源精细管理,代码量翻了好几倍,但核心思想没变:用一条有序链表把设备的挂起/恢复串起来,用阶段划分处理不同中断上下文的需求。理解了这两点,再看drivers/base/power/下的代码就不会迷路。

我个人的习惯是,拿到一个新平台做功耗调试,先看dpm_list的构建顺序,再看各设备的dev_pm_ops实现,最后用pm_test逐阶段验证。这套流程走下来,大部分睡眠问题都能定位。真正难查的是那些涉及硬件时序和竞态的边界情况,那种只能靠ftrace加打印一点点啃。

如果你正在调系统睡眠,建议先把CONFIG_PM_DEBUG和CONFIG_PM_ADVANCED_DEBUG打开,这两个配置能省你很多事。另外,Documentation/driver-api/pm/devices.rst这份文档值得反复读,里面把各阶段回调的约束讲得很清楚,比看代码猜要靠谱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:42:11

DOI号里藏着什么?一套从编号拆解到文献精读的高效检索流程

拿到一串看不懂的文献编号,很多人第一反应是直接复制进浏览器看能不能打开,打不开就丢给导师或者扔进收藏夹吃灰。我之前帮学生做文献检索梳理的时候,收到过一条只写了几个字样和一个DOI号的信息:[TDSC]DOI: 10.1109/JIOT.2024.33…

作者头像 李华
网站建设 2026/10/9 3:42:09

Claude Opus 5.5 与 Claude Code 实战:Sub-agent 编排与 CLAUDE.md 避坑指南

1. 这次“焚诀”到底更新了什么:从标题拆解到真实能力边界先把话说在前头,标题里那个“焚诀”是圈内人的戏称,指的是模型在长链路推理、代码生成、复杂任务编排上的一次集中能力释放。我第一时间拿到 Claude Opus 5.5 的访问权限后&#xff0…

作者头像 李华
网站建设 2026/10/9 3:41:55

计算机发展史怎么读?从系统结构视角梳理四大阶段与核心概念

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 3:41:01

一维数据升二维:从映射到可视化的完整实践指南

先说清楚一件事:我这篇讲的“升到二维”,指的是把一个本质上只有“顺序/一维结构”的东西,变成一个有“平面/邻域/空间分布”的二维表达。不管你是做数据分析、做信号处理、做可视化、做机器学习特征工程,还是做图像生成&#xff…

作者头像 李华
网站建设 2026/10/9 3:40:45

Claude Code 长期记忆工具 claude-mem:原理、配置与实战指南

1. Claude Code 的“失忆症”,到底有多痛我之前用 Claude Code 写代码时最崩溃的场景就是:让它在项目里帮我重构一个模块,它做得挺好,我夸了一句“不错”,顺手又让它去改另一个文件。结果同一会话还没结束,…

作者头像 李华
网站建设 2026/10/9 3:39:49

CNN卷积神经网络图像识别实战:Python+PyTorch从入门到CIFAR-10模型训练

说实话,每次有朋友问我图像识别怎么入门,我的答案都出奇一致:别一上来就抱着一堆论文死磕,先动手,拿Python把CNN卷积神经网络的完整流程跑一遍。只有亲眼看模型吃数据、出结果,你才会真正理解什么是图像识别…

作者头像 李华