4、系统休眠流程详解:从用户态触发到内核态执行的完整路径
系统休眠这件事,说白了就是让整个系统「睡过去」,再「醒过来」。很多工程师调参调了半天,其实连休眠的完整路径都没走通过。我个人习惯是,先把流程画在纸上,再动手改代码。今天我们就从用户态敲下那条命令开始,一路追到内核最深处。
核心要点:休眠流程分为三个阶段——freeze进程、suspend devices、suspend syscore。顺序不能乱,少一步系统就可能醒不过来。
4.1 从用户态到内核态的触发入口
你在终端里敲下 echo mem > /sys/power/state,这行命令就是整个休眠流程的起点。用户态的 write() 系统调用会触发内核的 state_store() 函数。
// 用户态触发
echo mem > /sys/power/state
// 内核态入口
static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t n)
{
suspend_state_t state = PM_SUSPEND_MEM;
int error;
error = enter_state(state);
return error ? error : n;
}
嗯,这里要注意:enter_state() 才是真正干活的函数。它会检查当前状态、通知看门狗、然后一步步往下走。
4.2 freeze进程阶段:让所有任务停下来
为什么要先冻结进程?你想想看,如果设备都 suspend 了,进程还在访问硬件,那不就崩了吗?所以第一步,必须让所有用户态进程和内核线程都停下来。
我在项目中遇到过一个问题:某个驱动线程没有注册 freezable,结果系统 suspend 到一半就卡死了。排查了半天才发现是线程没冻结住。
// 冻结进程的核心调用
static int suspend_freeze_processes(void)
{
int error;
error = freeze_processes(); // 冻结所有用户态进程
if (error)
goto out;
error = freeze_kernel_threads(); // 冻结内核线程
if (error) {
thaw_processes();
goto out;
}
out:
return error;
}
避坑指南:我曾经在调试一个多媒体平台时,发现音频驱动线程没加 try_to_freeze() 检查,导致 freeze 阶段永远超时。后来给每个内核线程都加上了 set_freezable(),问题才解决。
冻结完成后,系统会检查是否有进程拒绝冻结。如果有,整个 suspend 流程就会回滚。这个机制很关键——它保证了「要么全冻住,要么不睡」。
4.3 suspend devices阶段:按依赖顺序关闭设备
进程冻结后,就该处理设备了。设备 suspend 的顺序不是随机的,而是按照设备依赖树来走。简单说:先 suspend 那些不依赖其他设备的,再 suspend 被依赖的。
| 阶段 | 操作 | 说明 |
|---|---|---|
| suspend_prepare | 通知驱动准备进入休眠 | 驱动可以在这里保存状态 |
| suspend_late | 关闭中断、停止DMA | 此时不能再访问外设 |
| suspend_noirq | 最后一步设备操作 | IRQ已关闭,只能轮询 |
// 设备suspend的核心流程
static int suspend_devices_and_enter(suspend_state_t state)
{
int error;
error = dpm_suspend_start(PMSG_SUSPEND); // 开始suspend设备
if (error) {
dpm_resume_end(PMSG_RESUME);
return error;
}
error = dpm_suspend_end(PMSG_SUSPEND); // 结束suspend设备
if (error) {
dpm_resume_start(PMSG_RESUME);
return error;
}
return 0;
}
注意:设备 suspend 阶段如果超时(默认 5 秒),内核会打印 dpm_timeout 警告。我建议你在调试阶段把超时时间调大一点,比如 pm_print_times=1 打开时间戳,方便定位是哪个驱动卡住了。
4.4 suspend syscore阶段:最后一道防线
设备都 suspend 完了,是不是就可以睡了?还不行。系统核心组件——比如时钟、中断控制器、CPU idle——还需要做最后的关闭操作。这就是 syscore suspend 阶段。
这个阶段非常短,因为能做的事情很少。说白了,就是把最后几个不能通过标准驱动框架管理的核心模块关掉。
// syscore suspend调用链
void syscore_suspend(void)
{
struct syscore_ops *ops;
list_for_each_entry_reverse(ops, &syscore_ops_list, node) {
if (ops->suspend) {
ops->suspend();
}
}
}
我记得有一次调试,发现 syscore 阶段某个时钟驱动没处理好,导致 resume 后 PLL 锁不住。后来在 syscore_ops 里加了重锁逻辑才搞定。
4.5 完整流程可视化
下面这张图把整个流程串起来了。我建议你把它打印出来贴在工位上,调试时对照着看。
4.6 回滚机制:睡不成就要醒回来
任何一个阶段出问题,系统都不会硬睡。内核会按照相反的顺序把已经 suspend 的设备 resume 回来,把冻结的进程解冻。这个机制叫「回滚」。
我曾经在项目中遇到一个奇葩问题:某个 USB 控制器在 suspend_late 阶段返回了 -EBUSY,结果系统回滚时又把其他设备搞乱了。后来发现是驱动里没处理好 pm_runtime_get_sync() 的引用计数。
调试建议:打开 pm_debug_messages 内核参数,可以看到每个设备 suspend/resume 的耗时和状态。配合 dmesg 抓日志,基本能定位 90% 的问题。
4.7 小结
整个休眠流程,从用户态到内核态,其实就三步:冻住进程、关掉设备、收尾核心。每一步都有对应的回滚机制。你只要记住这个顺序,调试时按图索骥,就不会迷路。