第四章:系统挂起与恢复(Suspend/Resume)

说到电源管理,挂起与恢复这块,我估计是大家在实际项目中打交道最多的功能之一。说白了,就是让系统睡个觉,再叫醒它。但这里面的门道,比你想象的要深得多。

我记得刚入行那会儿,第一次调试STR(Suspend-to-RAM),板子挂起后死活唤不醒。折腾了两天,最后发现是GPIO的唤醒配置少了一行。嗯,从那以后,我对这套机制就格外上心。

4.1 四种睡眠模式,你搞清楚了吗?

Linux内核支持多种挂起方式,但最常用的就四种。我习惯把它们分成「浅睡」和「深睡」两类。

模式 内核名称 功耗等级 恢复速度
Suspend-to-Idle freeze 最低(浅睡) 最快(毫秒级)
Suspend-to-RAM (STR) mem 中等 较快(秒级)
Suspend-to-Disk (Hibernate) disk 最高(完全断电) 最慢(取决于磁盘)
Standby (Power-on Suspend) standby 介于Idle和STR之间 较快

4.2 Suspend-to-Idle:最温柔的睡眠

这种模式说白了就是「假装睡觉」。CPU进入idle状态,但内存和外围设备基本保持供电。我在做平板电脑项目时,经常用这个模式来实现「熄屏听歌」功能。

它的核心原理是:

  • 冻结所有用户空间进程
  • 让CPU进入WFI(Wait For Interrupt)状态
  • 外设时钟降频或关闭

实际体验: 恢复速度极快,基本感觉不到延迟。但省电效果有限,大概比正常待机省20%-30%的电量。

4.3 Suspend-to-RAM (STR):真正的深度睡眠

这是嵌入式设备中最常用的挂起方式。我做过一个智能家居网关项目,STR就是标配。

STR的工作流程是这样的:

  1. 用户触发挂起请求
  2. 内核冻结所有进程和线程
  3. 关闭所有非唤醒源的外设
  4. 将系统状态保存到内存(DRAM自刷新)
  5. 切断CPU和大部分外设的电源
  6. 只有唤醒源(如RTC、GPIO)保持供电

我曾经踩过的坑: 有一次STR挂起后,发现WiFi模块的电源没切断,导致待机功耗高达50mA。排查了半天,原来是设备树里漏配了WiFi的电源控制GPIO。所以,STR调试时一定要先量电流!

4.4 Hibernate (S4):彻底断电

Hibernate,也叫Suspend-to-Disk。说白了就是把内存里的所有内容写到磁盘上,然后完全断电。下次开机时再从磁盘恢复。

我一般只在电池快没电、或者需要长时间待机的场景下用这个模式。比如一些工业数据采集器,可能一个月才唤醒一次。

关键配置参数:

# 查看当前支持的挂起模式
cat /sys/power/state

# 设置Hibernate的交换分区
# 需要有一个大小 >= 内存大小的swap分区
sudo swapon /dev/mmcblk0p3

# 触发Hibernate
echo disk > /sys/power/state

个人建议: Hibernate的恢复速度取决于磁盘读写速度。用eMMC的话大概5-10秒,用NVMe SSD可以做到2-3秒。但千万别用SD卡做Hibernate,速度慢不说,还容易损坏卡。

4.5 用户空间触发方式

最直接的触发方式就是写/sys/power/state文件。但实际项目中,我们通常不会让用户直接操作这个文件。

我常用的几种触发方式:

  • 按键触发: 长按电源键进入STR
  • 定时触发: 通过RTC闹钟,比如晚上10点自动挂起,早上6点唤醒
  • 事件触发: 比如盖子合上、无操作超时

代码示例(C语言实现):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

int trigger_suspend(const char *mode) {
    int fd;
    int ret;
    
    fd = open("/sys/power/state", O_WRONLY);
    if (fd < 0) {
        perror("Failed to open /sys/power/state");
        return -1;
    }
    
    ret = write(fd, mode, strlen(mode));
    if (ret < 0) {
        perror("Failed to write suspend mode");
        close(fd);
        return -1;
    }
    
    close(fd);
    return 0;
}

int main() {
    // 触发STR挂起
    printf("Triggering Suspend-to-RAM...\n");
    if (trigger_suspend("mem") == 0) {
        printf("System resumed successfully\n");
    } else {
        printf("Suspend failed\n");
    }
    return 0;
}

4.6 唤醒源配置

唤醒源是挂起恢复的关键。没有配置好唤醒源,系统挂起后就真的「睡死」了。

常见的唤醒源:

唤醒源类型 典型应用 配置方式
RTC闹钟 定时唤醒 /sys/class/rtc/rtc0/wakealarm
GPIO中断 按键唤醒 设备树中配置wakeup-source
USB 鼠标/键盘唤醒 /sys/bus/usb/devices/.../power/wakeup
网络 Wake-on-LAN ethtool -s eth0 wol g

调试技巧: 我习惯在挂起前先检查一下唤醒源是否生效:

# 查看哪些设备支持唤醒
cat /sys/devices/system/wakeup/wakeup* 2>/dev/null

# 测试RTC唤醒
echo +60 > /sys/class/rtc/rtc0/wakealarm
echo mem > /sys/power/state
# 系统会在60秒后自动唤醒

4.7 实战中的避坑指南

做挂起恢复调试,我总结了几条血泪教训:

  • 先测电流: 挂起后第一时间用万用表量整板电流。如果电流没降下来,说明有设备没正确挂起。
  • 日志要开全: 内核配置要打开PM_DEBUG和PM_SLEEP_DEBUG,否则出问题根本不知道卡在哪一步。
  • 注意DMA: 有些DMA控制器在挂起时不会自动释放总线,会导致系统卡死。我遇到过两次这种情况。
  • GPU/VPU要特殊处理: 多媒体模块的挂起恢复往往最复杂,建议单独测试。

我的调试三板斧:

  1. 先试Suspend-to-Idle,确认基本流程没问题
  2. 再试STR,重点排查外设电源控制
  3. 最后试Hibernate,主要看磁盘和内存的配合

每一步都要确认唤醒正常,再进入下一步。

好了,关于系统挂起与恢复,核心内容就这些。说白了,就是让系统睡个好觉,再把它叫醒。但要让这个「睡觉-醒来」的过程稳定可靠,需要你对硬件、内核、驱动都有足够的了解。下一章我们会深入讲一下设备树的电源管理配置,到时候再细聊。