内核休眠唤醒框架: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. 三阶段流程图

为了让你更直观地理解这三个阶段的关系,我画了一张流程图。它展示了从用户发起休眠,到系统唤醒的完整路径。

PM Core 三阶段流程图 PM Core 休眠唤醒三阶段流程 用户发起休眠 阶段1:suspend_prepare 冻结进程 → 暂停线程 → 准备设备 阶段2:suspend_enter 保存状态 → 调用 enter() → CPU 休眠 (此阶段不可返回) 阶段3:suspend_finish 恢复设备 → 恢复线程 → 解冻进程 系统唤醒完成 说明:suspend_enter 阶段是唯一真正让 CPU 休眠的阶段,其他阶段都在做准备工作或恢复工作

6. 实际调试中的经验

最后,分享几个我在项目中积累的调试经验:

  • 先确认 suspend_ops 注册成功:检查 /sys/power/state 文件,看是否出现了预期的休眠状态。
  • 用 ftrace 跟踪休眠路径:echo function > /sys/kernel/debug/tracing/current_tracer,然后抓取休眠时的函数调用。
  • 注意中断处理:休眠过程中,有些中断是不能屏蔽的,比如唤醒源中断。我曾经因为屏蔽了 RTC 中断,导致系统无法唤醒。
  • 多试几次:有些问题不是每次都出现。我习惯连续休眠唤醒几十次,看看系统会不会崩溃。

重要提醒:千万不要在产品上直接调试休眠唤醒。先用开发板验证,确认没问题了再移植。我曾经在客户现场调试,结果系统睡下去就醒不来了,只能硬重启。那场面,别提多尴尬了。