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 完整流程可视化

下面这张图把整个流程串起来了。我建议你把它打印出来贴在工位上,调试时对照着看。

系统休眠完整流程 用户态 echo mem enter_state() 阶段1: freeze进程 freeze_processes() + freeze_kernel_threads() 阶段2: suspend devices dpm_suspend_start() → dpm_suspend_end() suspend_prepare → suspend_late → suspend_noirq 阶段3: suspend syscore syscore_suspend() → 关闭时钟/中断控制器 CPU 进入休眠 关键说明 • 三个阶段顺序固定 • 任一阶段失败则回滚 • resume 顺序完全相反

4.6 回滚机制:睡不成就要醒回来

任何一个阶段出问题,系统都不会硬睡。内核会按照相反的顺序把已经 suspend 的设备 resume 回来,把冻结的进程解冻。这个机制叫「回滚」。

我曾经在项目中遇到一个奇葩问题:某个 USB 控制器在 suspend_late 阶段返回了 -EBUSY,结果系统回滚时又把其他设备搞乱了。后来发现是驱动里没处理好 pm_runtime_get_sync() 的引用计数。

调试建议:打开 pm_debug_messages 内核参数,可以看到每个设备 suspend/resume 的耗时和状态。配合 dmesg 抓日志,基本能定位 90% 的问题。

4.7 小结

整个休眠流程,从用户态到内核态,其实就三步:冻住进程、关掉设备、收尾核心。每一步都有对应的回滚机制。你只要记住这个顺序,调试时按图索骥,就不会迷路。