内核休眠唤醒框架:PM Core 三阶段与 suspend_ops
做嵌入式 Linux 休眠唤醒,说白了就是跟内核的 PM Core 打交道。我刚开始啃这部分代码时,也觉得它挺绕的。但后来发现,只要抓住三个关键阶段,整个框架就清晰了。
这三个阶段分别是:suspend_prepare、suspend_enter、suspend_finish。它们像一条流水线,把系统从正常工作状态,平稳地送入休眠,再安全地唤醒回来。
核心要点:PM Core 是休眠唤醒的调度中心,它不直接操作硬件,而是通过 suspend_ops 回调函数,让平台相关的代码去干具体的活。
1. suspend_prepare:冻结世界
这个阶段的任务,说白了就是「让系统安静下来」。内核会做以下几件事:
- 冻结用户空间进程:所有用户态进程都会被挂起,不能再干活了。
- 暂停内核线程:一些关键的内核线程也会被暂停。
- 注册并准备设备:通知所有设备驱动,准备进入休眠。
我记得有一次调试,系统总是在 suspend_prepare 阶段卡死。后来发现,是一个第三方驱动在冻结进程时,没有正确处理信号量。嗯,这里要注意:驱动里的锁和信号量,在休眠路径上特别容易出问题。
个人经验:我习惯在 suspend_prepare 阶段加一些调试打印,看看是哪个进程或驱动卡住了。用 echo 1 > /sys/power/pm_print_times 可以打开时间戳,定位问题会快很多。
2. suspend_enter:真正入睡
这是最核心的阶段。系统会调用 suspend_ops 里的回调函数,把硬件状态保存好,然后让 CPU 进入指定的休眠状态。
suspend_ops 是一个关键结构体,它定义了平台相关的休眠操作。我把它常用的回调列出来:
| 回调函数 | 作用 | 我在项目中遇到的坑 |
|---|---|---|
begin() |
进入休眠前的准备工作 | 有时会跟其他驱动抢资源,导致死锁 |
prepare() |
保存 CPU 和平台相关状态 | 寄存器保存不全,唤醒后系统跑飞 |
enter() |
真正让 CPU 进入休眠状态 | 这个函数不能返回,否则系统会乱掉 |
finish() |
唤醒后的恢复工作 | 恢复顺序错了,外设初始化失败 |
end() |
休眠流程结束后的清理 | 资源泄漏,多次休眠后系统变慢 |
你想想看,enter() 函数一旦执行,CPU 就真的睡过去了。这时候,任何代码都不能再跑。所以,所有需要保存的状态,必须在 enter() 之前完成。
曾经踩过的坑:有一次,我在 enter() 里调了一个打印函数,结果系统直接死机。后来才明白,休眠状态下,串口控制器可能已经断电了,打印函数根本没法工作。所以,enter() 里只能做最纯粹的硬件操作。
3. suspend_finish:唤醒世界
系统从休眠中醒来后,会执行 suspend_finish 阶段。它的工作跟 suspend_prepare 正好相反:
- 恢复设备状态:调用驱动的 resume 回调,把硬件恢复到休眠前的状态。
- 恢复内核线程:让之前暂停的内核线程继续运行。
- 解冻用户空间进程:所有进程恢复执行,系统回到正常工作状态。
我个人觉得,这个阶段最容易出问题的是设备恢复顺序。有些设备依赖其他设备先恢复,如果顺序错了,就会导致初始化失败。
4. suspend_ops 回调注册
说了这么多,那 suspend_ops 到底怎么注册呢?其实很简单,内核提供了一个全局结构体,平台代码只需要填充它就行。
static const struct platform_suspend_ops my_suspend_ops = {
.valid = my_valid,
.begin = my_begin,
.prepare = my_prepare,
.enter = my_enter,
.finish = my_finish,
.end = my_end,
};
static int __init my_pm_init(void)
{
suspend_set_ops(&my_suspend_ops);
return 0;
}
这里要注意,suspend_set_ops() 只能调用一次。如果多个驱动都想注册,那就会冲突。我建议在平台初始化代码里统一注册,不要分散到各个驱动里。
调试技巧:可以用 cat /sys/power/state 查看系统支持哪些休眠状态。如果注册成功,这里会显示对应的字符串,比如 "mem" 或 "standby"。
5. 三阶段流程图
为了让你更直观地理解这三个阶段的关系,我画了一张流程图。它展示了从用户发起休眠,到系统唤醒的完整路径。
6. 实际调试中的经验
最后,分享几个我在项目中积累的调试经验:
- 先确认 suspend_ops 注册成功:检查
/sys/power/state文件,看是否出现了预期的休眠状态。 - 用 ftrace 跟踪休眠路径:
echo function > /sys/kernel/debug/tracing/current_tracer,然后抓取休眠时的函数调用。 - 注意中断处理:休眠过程中,有些中断是不能屏蔽的,比如唤醒源中断。我曾经因为屏蔽了 RTC 中断,导致系统无法唤醒。
- 多试几次:有些问题不是每次都出现。我习惯连续休眠唤醒几十次,看看系统会不会崩溃。
重要提醒:千万不要在产品上直接调试休眠唤醒。先用开发板验证,确认没问题了再移植。我曾经在客户现场调试,结果系统睡下去就醒不来了,只能硬重启。那场面,别提多尴尬了。