2、Suspend-to-RAM (STR) 核心流程:内核态冻结进程、设备暂停、CPU下线

好,咱们今天聊聊 Suspend-to-RAM,也就是常说的 STR。说白了,就是把系统挂起到内存,让大部分硬件都歇着,只给内存留口电。我刚开始接触这块时,觉得不就是个休眠嘛,能有多复杂?后来真动手调起来才发现,这里面的门道,比想象中深得多。

STR 的核心流程,我习惯把它拆成三大步:冻结用户空间进程暂停设备下线 CPU。这三步环环相扣,哪一步出问题,系统要么醒不来,要么醒来就崩。

2.1 内核态冻结进程:让所有活儿都停下来

你想想看,系统要休眠了,用户空间还有一堆进程在跑。如果直接断电,文件写到一半怎么办?网络请求还没处理完怎么办?所以第一步,就是要把所有进程都「冻住」。

内核里干这活的,是 freeze_processes() 这个函数。它会向所有用户空间进程发送一个 SIGSTOP 信号,让它们进入不可中断的睡眠状态。嗯,这里要注意,内核线程是不会被冻结的,它们得继续干活,比如帮我们处理设备的暂停。

核心要点:冻结进程不是杀死进程,而是让进程暂停执行。等系统唤醒后,再通过 thaw_processes() 让它们恢复运行。

我在项目中遇到过一个问题:某个进程持有了一个文件锁,冻结时它正好在写文件。结果系统唤醒后,那个文件锁没释放干净,导致其他进程一直卡着。后来怎么解决的?我加了个超时机制,如果进程在规定时间内没冻结成功,就强制把它杀掉。虽然粗暴了点,但至少系统能正常唤醒。

避坑指南:我曾经在调试一个嵌入式设备时,发现休眠后永远醒不过来。查了半天,原来是一个后台服务进程在收到 SIGSTOP 后,没有正确处理信号,直接退出了。所以,冻结进程时,一定要确保所有进程都能正确处理信号

2.2 设备暂停:让硬件也进入休眠

进程冻结完了,接下来就该处理设备了。这一步叫 suspend_devices_and_enter(),它会遍历系统中的所有设备,调用它们的 suspend 回调函数。

设备的暂停顺序是有讲究的。内核会按照设备的依赖关系,从最外层的设备开始暂停,一直深入到最核心的设备。比如,你先得暂停 USB 控制器,然后才能暂停 PCI 总线,最后才能暂停 CPU 本身。

每个设备的 suspend 回调函数,需要完成以下工作:

  • 保存设备当前的寄存器状态
  • 将设备切换到低功耗模式
  • 关闭设备的时钟和电源(如果可能的话)

我举个例子,一个网卡的 suspend 回调大概长这样:

static int eth_suspend(struct device *dev)
{
    struct net_device *ndev = dev_get_drvdata(dev);
    struct eth_priv *priv = netdev_priv(ndev);

    // 保存寄存器状态
    priv->saved_regs = readl(priv->regs + REG_STATUS);

    // 关闭网络接口
    netif_device_detach(ndev);

    // 进入低功耗模式
    writel(0, priv->regs + REG_POWER_CTRL);

    return 0;
}

这里有个坑:设备暂停时,不能依赖任何可能已经被暂停的设备。比如,你的网卡依赖一个 GPIO 控制器来拉低复位引脚,那 GPIO 控制器必须在网卡之后才暂停。否则,网卡还没暂停完,GPIO 先断电了,那网卡就卡死了。

警告:设备暂停的顺序由 dpm_list 控制,这个列表是在系统启动时根据设备依赖关系生成的。如果你发现某个设备在休眠时总是报错,先检查一下它的依赖关系是不是正确的。

2.3 CPU 下线:让处理器也歇一歇

设备都暂停了,接下来就该处理 CPU 了。这一步叫 disable_nonboot_cpus(),它会将除了 CPU0 之外的所有 CPU 都下线。

为什么要留一个 CPU0?因为 CPU0 要负责执行最后的休眠指令,比如把系统状态写入内存,然后执行 WFI(Wait For Interrupt)指令让 CPU 进入休眠状态。

CPU 下线的过程,说白了就是让其他 CPU 上的任务都迁移到 CPU0 上,然后关闭它们的中断,最后让它们进入 idle 状态。这个过程由 cpu_down() 函数完成,它会调用每个 CPU 的 cpuhp_teardown 回调。

我记得有一次,我在一个四核的 ARM 板子上调试休眠,结果每次执行到 CPU 下线这一步就崩溃。查了半天,发现是一个内核模块在 CPU2 上注册了一个定时器,CPU2 下线时,那个定时器没被正确迁移。后来我加了个 migrate_timers() 的调用,问题就解决了。

个人经验:如果你在调试 CPU 下线相关的问题,建议先看看 /sys/devices/system/cpu/cpuX/online 这个文件。手动把某个 CPU 下线,看看会不会出问题。如果手动下线没问题,那问题大概率出在休眠流程的其他地方。

2.4 最后的休眠指令

所有 CPU 都下线后,只剩下 CPU0 还在运行。这时候,内核会调用 arch_suspend_enable_irqs() 关闭中断,然后执行 cpu_suspend() 函数。

cpu_suspend() 会做两件事:

  1. 将 CPU 的上下文(寄存器、栈指针等)保存到内存中
  2. 执行 WFI 指令,让 CPU 进入休眠状态

执行完 WFI 后,整个系统就彻底安静了。只有内存还在工作,保持着系统状态。这时候,只要有一个唤醒事件(比如按下电源键、网络唤醒包),CPU 就会重新开始执行,从 WFI 的下一条指令继续运行。

嗯,这里有个细节:CPU 的上下文保存必须放在一个不会被掉电的内存区域。如果内存控制器在休眠时也断电了,那 CPU 醒来后连自己是谁都不知道了。所以,很多 SoC 会专门留一块「永远在线」的内存区域,用来保存 CPU 上下文。

总结一下:STR 的核心流程,就是「先冻进程,再停设备,最后下线 CPU」。每一步都有很多细节,任何一个环节出问题,系统都可能醒不过来。我做了这么多年嵌入式,最怕的就是休眠唤醒的问题,因为它不像其他 bug 那样容易复现。但只要你把每一步的原理搞清楚了,调试起来其实也没那么难。

好了,这一章就聊到这儿。下一章咱们聊聊唤醒流程,看看系统是怎么从休眠状态恢复过来的。