Linux 里整机休眠实际有三种,功耗从低到高:
前两种醒过来之后,还是原来那个内核,DRAM 里的所有指针、任务、缓存都在原地。hibernation 不一样:下次开机时,是一个新内核从磁盘里读一张旧内核的完整照片,然后原地覆盖自己。
这个两个内核的架构决定了 hibernation 的整套设计。

图 1 · 三种睡下去横向对比:s2idle / s2ram / hibernation
s2ram 已经省到只剩 DRAM refresh 的电流(几十 mW),为什么还要 S4?三个理由:
代价也大:恢复时间从毫秒级变到秒级(要读几百 MB 到几 GB 数据);对内存布局稳定性要求极高(image kernel 必须能重建那张历史照片)。
suspend_ops->enter三种模式在 kernel/power/ 目录下并列:suspend.c(s2ram/s2idle)、hibernate.c(S4)、autosleep.c(Android 自动 s2ram)。
写驱动最容易混淆的是 hibernation 和 suspend 用的 是同一套struct dev_pm_ops 回调,但语义差别很大:
suspend() / resume():s2ram/s2idle 通用freeze() / thaw():hibernation 的第一阶段(造快照前后)poweroff() / restore():hibernation 的第二阶段(写盘 poweroff / 从盘恢复)只写 suspend()/resume() 的驱动,hibernation 时 PM Core 会自动回退用 suspend/resume。但硬件状态在 hibernation 里被彻底断电,如果驱动依赖某个不能持久化的资源(比如 GPU 上的 firmware image),必须在 restore() 里重新加载。这个 fallback 陷阱是 hibernation 相关 bug 的最大来源。
hibernation 有三个关键设计取舍决定了它今天的样子。
朴素想法:一次性把 DRAM 全部写盘。这不可行,因为你正在运行的内核也在 DRAM 里,写盘时它在改自己。
swsusp 的解法是 两阶段:
hibernation_snapshot(),把整个内存复制到另一半内存(叫 image pages)。快照过程中,freezer 保证用户态不动、GFP_ATOMIC 保证内核不产生新 pagehibernation_write_image(),从容地把 image pages 写盘,因为它们已经不再变化了这就要求 hibernation 前至少要有 50% 内存空闲,一半装原始数据、一半装快照。所以 image_size 内核参数默认是当前空闲内存的 2/5,超过就分批 shrink。
为什么 resume 不是接着当时那个状态继续跑?因为 hibernation 已经彻底 poweroff,DRAM 内容清空,下次开机必然是一个全新的内核(bootloader 加载的、和当时那个不同副本、内核版本/config 甚至可能不同)。
于是 resume 路径变成:
swsusp_arch_resume(),跳转到 image kernel 的入口,让它接管thaw 所有设备swsusp_arch_resume() 的那段代码这个从 A 内核跳到 B 内核的动作是 hibernation 里最魔幻的部分,它的实现是 arch 相关的汇编。arm64 平台的实现在 arch/arm64/kernel/hibernate-asm.S(x86_64 有一份对应的 arch/x86/power/hibernate_asm_64.S);本文的汇编片段都以 arm64 为准。
hibernation 开始前必须把所有用户态进程和大部分内核 kthread 冻住。这不是可选项:
freeze_processes() 用一个 flag PF_FROZEN + try_to_freeze() 的检查点让所有可冻结任务停在安全位置。任何一个进程冻结失败,hibernation 直接 abort,这就是为什么服务器上跑着某个顽固进程时,hibernate 会失败。
一句话概括:hibernation 是内存分区 → 快照容器 → 落盘协议三件套。
写盘时最先落盘的是一个 header:
// kernel/power/swsusp.c
struct swsusp_info {
struct new_utsname uts; // uname 信息,用于校验
u32 version_code; // 内核版本号
unsigned long num_physpages; // 快照前的物理页数
int cpus;
unsigned long image_pages; // image 中 page 数
unsigned long pages; // 加上元数据后的总 page 数
unsigned long size; // 字节大小
} __aligned(PAGE_SIZE);
这个 header 是这张照片属于哪个内核的身份证,resume 时 boot kernel 会先读这一页,看 uts 和 version_code 是不是自己(或至少是兼容的),不匹配就拒绝 resume。
实际的快照是一堆散乱的 page,用 handle 迭代访问:
// kernel/power/power.h
struct snapshot_handle {
unsigned int cur; // 当前 page 索引
void *buffer; // 当前 page 的内容
int sync_read; // 是否同步读
};
上层协议就是一个 snapshot_read_next(handle) / snapshot_write_next(handle) 循环,每次拿到下一页 4KB 数据。这个抽象让写盘和用户态 uswsusp 走同一套接口(/dev/snapshot 就是把这个 handle 封给了用户态)。
image page 落到 swap 分区里的哪些扇区?用一个大数组记下来:
// kernel/power/swap.c
static sector_t swsusp_last_page = 0;
struct swap_map_page {
sector_t entries[MAP_PAGE_ENTRIES];
sector_t next_swap; // 下一个 swap_map_page 的位置
};
swap_map_page 本身也占一个扇区,多个 map page 用链表串起来,read 时按链表一路遍历。第一个 swap_map_page 的位置存在 swap header 的 resume_offset 字段,bootloader 传给 boot kernel 后,kernel 就知道从哪读起。

图 2 · swap 分区 image 布局与 header/map/data 三段
/sys/power/state 和 /sys/power/disk用户态发起 hibernation 有两条路:
/sys/power/disk 决定/sys/power/disk 有五个可选值:
platform:调用 platform_ops->finish/enter 让固件把机器 poweroff(ACPI S4)shutdown:等价于 reboot -h,写盘完就 shutdownreboot:写盘完就 reboot,重启后 resumesuspend:hybrid sleep,先写盘,再 s2ram。电池撑得住就走 resume from RAM 快路径,撑不住就走 resume from disktest_resume / testproc:调试用resume 内核参数 / initramfs 里的 resume 工具resume 侧的入口有两个:
resume=/dev/sda2 resume_offset=12345 直接告诉内核到哪找 imageupdate-initramfs 会把 resume 逻辑打包进 initramfs,好处是可以选择 resume 前先 mount root、prompt 用户内核路径最后都会走到 software_resume():读 swap header → 找 signature → 分配 image pages → 读盘 → 调 hibernation_restore() 跳转到 image kernel。
dev_pm_ops 的第二组回调驱动为 hibernation 提供的是四个第二组回调:
struct dev_pm_ops {
// 第一组(s2ram/s2idle)
int (*suspend)(struct device *dev);
int (*resume)(struct device *dev);
// 第二组(hibernation 特有)
int (*freeze)(struct device *dev); // 快照前,停止硬件活动
int (*thaw)(struct device *dev); // 快照完成,恢复正常运行(image 已在内存中)
int (*poweroff)(struct device *dev); // 写盘完成,即将 poweroff
int (*restore)(struct device *dev); // 从盘恢复后,重建硬件状态
};
时序上,一次完整的 hibernate + resume 是:
freeze → snapshot → thaw → (系统正常运行几秒钟) → poweroff → 写盘 → 断电
...下一次开机...
boot kernel 起来 → 读盘 → jump 到 image kernel → restore
freeze 和 poweroff 的差别:freeze 阶段设备只是暂停(快照要包含设备当前寄存器),thaw 后设备恢复继续跑;poweroff 阶段设备要做实际的下电准备(因为快照已经写完,接下来断电了)。
hibernation 把物理内存 page 分成几类,落盘策略不同:
如果 image 太大装不下,shrink_all_memory() 会主动收缩 page cache、释放 slab、swap out 部分匿名页,image 太大是 hibernate 失败的第二大原因(第一大是 freezer 失败)。
hibernation 的核心模型有两个:快照生命周期 和 两内核 handoff。
一张快照从造出来到写盘到读回来到销毁,经历这些状态:
hibernation_snapshot()
[空闲] ──────────────────────────► [in-memory image]
│
hibernation_write_image()
▼
[written to disk]
│
系统 poweroff
│
▼
[持久化在磁盘上]
│
下次开机 boot kernel
software_resume()
│
▼
[in-memory image (in boot kernel)]
│
swsusp_arch_resume()
│
▼
[restored (in image kernel)]
关键点:image 在磁盘上可以躺任意长时间,只要 swap 分区不被写坏、内核版本兼容,一年后开机也能 resume(虽然实践中没人这么用)。

图 3 · 快照生命周期(in-memory → disk → in-memory → restored)
boot kernel 和 image kernel 共存的窗口只有几十毫秒,但是这段时间的语义最诡异:

图 4 · 两内核 handoff 双时序(boot kernel 与 image kernel)
[boot kernel] [image kernel]
│
software_resume():
读 image 到 safe pages
调用 arch handoff
│
─────► swsusp_arch_resume() ────► restore_registers()
│
切换 page table
│
恢复所有 CPU 寄存器
│
─────► [image kernel 完全接管]
│
thaw_processes()
resume_console()
...
image kernel 醒来时看到的时间戳、系统状态、进程列表都是当年那个,它完全不知道刚才发生了一次 poweroff 和一次重启。所以:
jiffies 会突然跳跃(RTC 会尝试修正,但精度有限)从触发 hibernate 到下次 resume 完成,数据流分成四段。

图 5 · hibernate + resume 完整数据流泳道
用户态写 /sys/power/state = disk 触发 hibernate():
// kernel/power/hibernate.c
int hibernate(void)
{
int error, nr_calls = 0;
// 1. 通知所有 PM notifier
error = __pm_notifier_call_chain(PM_HIBERNATION_PREPARE, -1, &nr_calls);
// 2. 拿 hibernation 独占锁(防并发)
lock_system_sleep();
// 3. 冻结用户态
error = freeze_processes();
if (error)
goto Thaw;
// 4. 冻结内核态 kthreads
error = freeze_kernel_threads();
if (error)
goto Thaw;
// 5. sync 磁盘(保证脏 page 落盘)
ksys_sync_helper();
// 6. 造快照(这是核心)
error = create_image(in_suspend);
...
}
create_image() 内部:
// kernel/power/hibernate.c
static int create_image(int platform_mode)
{
// 关中断、把 CPU 降到 non-boot CPU 全下线
error = disable_nonboot_cpus();
// suspend 所有设备的 noirq 阶段
error = dpm_suspend_end(PMSG_FREEZE);
// 开始真正的 snapshot
save_processor_state();
// 这里是关键:hibernation_snapshot 会造出 in-memory image
error = swsusp_arch_suspend();
// 从这里返回可能是两条路:
// - 造 image 结束(in_suspend == 1,接着 poweroff)
// - image kernel 醒来(in_suspend == 0,接着 restore)
restore_processor_state();
dpm_resume_start(in_suspend ? PMSG_QUIESCE : PMSG_RECOVER);
enable_nonboot_cpus();
return error;
}
swsusp_arch_suspend() 是 arch 相关的核心(arm64 版):
// arch/arm64/kernel/hibernate-asm.S 精简版
SYM_CODE_START(swsusp_arch_suspend)
// 把 callee-saved regs (x19-x30) + sp + ttbr1/tcr/sctlr 等
// 保存到 struct sleep_stack_data
save_cpu_regs
bl swsusp_save // C 函数:拷贝 saveable pages 到 image_pages
// 走到这里说明 snapshot 造好了,正常返回;C 侧 in_suspend 保持为 1
mov x0, #0
ldp x29, x30, [sp], #16
ret
SYM_CODE_END(swsusp_arch_suspend)
SYM_CODE_START(_cpu_resume) // resume 时 image kernel 从这里"醒过来"
// 由 boot kernel 的 swsusp_arch_resume 跳过来
// 恢复 ttbr1 / tcr / sctlr / vbar / callee-saved regs / sp
restore_cpu_regs
mov x0, #0 // 让返回值为 0,模拟 image kernel
// 从 swsusp_arch_suspend 返回
ret
SYM_CODE_END(_cpu_resume)
这段汇编的讲究之处:同一段代码有两个返回点,一次是 snapshot 时 swsusp_arch_suspend 自己 return(in_suspend=1),一次是 resume 时 image kernel 从 _cpu_resume 醒来、假装是 swsusp_arch_suspend 返回(in_suspend=0)。C 代码上层用 in_suspend 变量区分自己是哪条路径。
arm64 相比 x86_64 多一件事:resume 时 MMU 一开就必须让 ttbr1_el1 指向 image kernel 的 swapper_pg_dir,否则地址翻译立刻错乱。所以 restore_cpu_regs 里先切 TTBR、再 isb,再恢复寄存器,最后 eret 之前 sctlr 里的 M 位必须已经稳定。
snapshot 造好之后,hibernation_write_image() 遍历 image pages 写盘:
// kernel/power/swap.c
int swsusp_write(unsigned int flags)
{
struct swap_map_handle handle;
struct snapshot_handle snapshot;
struct swsusp_info *header;
// 拿到第一页(就是 swsusp_info header)
snapshot_read_next(&snapshot);
header = (struct swsusp_info *)snapshot.buffer;
// 找 swap 分区里连续/离散的空闲扇区
error = get_swap_writer(&handle);
// 先写 header(写到第一个分配的扇区)
error = swap_write_page(&handle, header, NULL);
// 循环写剩余的 image pages
while (1) {
error = snapshot_read_next(&snapshot);
if (error <= 0)
break;
error = swap_write_page(&handle, snapshot.buffer, NULL);
}
error = flush_swap_writer(&handle);
// 最后一步:把 swap header 里的 signature 改成 "S1SUSPEND"
// 这一步是"发布"——之前所有数据都已经落盘,signature 一改,boot kernel 就能识别
if (!error)
error = mark_swapfiles(&handle, flags);
return error;
}
mark_swapfiles() 就是 hibernation 里的 commit 点,像数据库事务一样,写完所有数据才最后一步提交。如果 poweroff 发生在这一步之前,image 无效;这一步之后,image 就是有效的。
如果开了 image 压缩(CONFIG_HIBERNATION_ENC 或 lzo/lz4/xz),中间会加一层压缩器,把 4KB page 压成几百字节再落盘,能省大约 50% 磁盘 IO。
image 写完、signature 打上,接下来 power_down():
// kernel/power/hibernate.c
static void power_down(void)
{
switch (hibernation_mode) {
case HIBERNATION_SUSPEND:
// hybrid:进 s2ram
error = suspend_devices_and_enter(PM_SUSPEND_MEM);
break;
case HIBERNATION_PLATFORM:
// 让固件(ACPI/PSCI)执行 S4
hibernation_platform_enter();
break;
case HIBERNATION_SHUTDOWN:
// 直接 shutdown
kernel_power_off();
break;
case HIBERNATION_REBOOT:
// 直接 reboot
kernel_restart(NULL);
break;
}
// 如果走到这里,说明 poweroff 失败——只能 halt
while (1);
}
HIBERNATION_PLATFORM 走 ACPI/PSCI 的意义:固件知道这次不是普通 poweroff,是 hibernation,BIOS/UEFI 里可以显示 resuming from hibernation 提示、跳过某些自检、加速下次启动。ACPI 的 S4 状态就是这个用途,PSCI 通过 SYSTEM_OFF 实现(没有专用 opcode,需要 platform 侧配合)。
下次开机,boot kernel 起来到 software_resume():
// kernel/power/hibernate.c
static int software_resume(void)
{
// 1. 拿 resume 设备(cmdline resume= 或 sysfs /sys/power/resume)
resume_device = ...;
swsusp_resume_device = resume_device;
// 2. 打开 swap 分区
error = swsusp_check();
if (error)
goto Unlock; // signature 不对,说明没有 image,正常启动
// 3. 冻结用户态、准备 image
error = freeze_processes();
// 4. 分配临时 pages 装 image(叫 safe pages)
error = swsusp_read(&flags);
// 5. arch 相关的 handoff
error = hibernation_restore(flags & SF_PLATFORM_MODE);
// 走到这里说明 restore 失败——上面 hibernation_restore
// 如果成功,控制权已经交给 image kernel,永远不会回来
...
}
swsusp_check() 里最有意思的一段:
// kernel/power/swap.c
int swsusp_check(void)
{
struct block_device *bdev;
bdev = blkdev_get_by_dev(swsusp_resume_device, FMODE_READ, ...);
// 读 swap 分区第一个 page(swap header)
hib_submit_io(REQ_OP_READ, 0, 0, swsusp_header);
// 看 signature
if (memcmp(HIBERNATE_SIG, swsusp_header->sig, 10) == 0) {
// 找到 hibernation image
// 把 signature 改回 swap 的原始值 "SWAPSPACE2"
// 这样下次开机(没有 hibernation image)时就是普通 swap
memcpy(swsusp_header->sig, swsusp_header->orig_sig, 10);
hib_submit_io(REQ_OP_WRITE, REQ_SYNC, 0, swsusp_header);
return 0;
}
return -EINVAL;
}
signature 一读到就立刻改回去的目的:一旦 boot kernel 开始 resume,就消费这个 image,如果 resume 中途失败(比如 image 损坏),下次不会再尝试同一张损坏的 image。
hibernation_restore() 里最关键的是把 image pages 从临时位置搬回原始物理地址,因为 image kernel 醒来时,指针、page 表都指向当年那个物理地址,boot kernel 读进来时不一定能占到那些地址(可能被 boot kernel 自己占了),所以先读到 safe pages,最后在关中断状态下一次性 memcpy 到原地址,然后跳转。
姿势一:一次性 hibernate(最常见)
# 系统层
sudo systemctl hibernate
# 或者直接写 sysfs
echo disk | sudo tee /sys/power/state
# 用 pm-utils(老系统)
sudo pm-hibernate
姿势二:hybrid sleep
# 先写盘再进 s2ram,电池够就走 s2ram 快恢复
echo suspend | sudo tee /sys/power/disk
sudo systemctl hybrid-sleep
hybrid 的好处:s2ram 期间如果电池撑住,resume 是毫秒级;一旦电池耗尽 SoC 断电,下次开机走 hibernate resume,状态不丢。这是笔记本厂商推荐的默认模式。
姿势三:uswsusp / userspace hibernate
老工具 uswsusp 通过 /dev/snapshot 让用户态完全接管 image 处理,好处是可以做加密、压缩策略、进度显示。现在基本被内核内建的 lz4 压缩取代。
/etc/default/grub 里 kernel cmdline 常见的三个参数:
resume=UUID=xxxxx-xxxx-xxxx # swap 分区(或 swap 文件所在设备)
resume_offset=12345 # swap 文件的偏移(用 filefrag 拿)
no_console_suspend # 保留 console 输出,方便调试
用 swap 文件 hibernate 时 resume_offset 是必须的,因为 swap 文件在文件系统里可以不连续,内核需要知道 image 的物理起点:
# 拿 swap 文件的物理起点
sudo filefrag -v /swapfile | awk 'NR==4 {print $4}' | tr -d '..'
# 假设输出 234567
# 写到 /etc/default/grub:
# GRUB_CMDLINE_LINUX_DEFAULT="... resume=UUID=xxx resume_offset=234567"
hibernation-safe 的驱动需要额外考虑三件事:
要点一:freeze/thaw 只是暂停,不要彻底 shutdown
static int mydev_freeze(struct device *dev)
{
struct mydev *d = dev_get_drvdata(dev);
// 停止硬件活动、保存需要持久化的寄存器状态
mydev_stop_dma(d);
mydev_save_regs(d);
// 不要 clk_disable / regulator_disable
// 因为 thaw 之后设备要立刻恢复运行
return 0;
}
static int mydev_thaw(struct device *dev)
{
struct mydev *d = dev_get_drvdata(dev);
mydev_restore_regs(d);
mydev_start_dma(d);
return 0;
}
要点二:poweroff 才做实际的下电
static int mydev_poweroff(struct device *dev)
{
struct mydev *d = dev_get_drvdata(dev);
// 这时候 image 已经在盘上了,即将断电
// 可以放心 clk_disable / regulator_disable
mydev_shutdown(d);
return 0;
}
static int mydev_restore(struct device *dev)
{
struct mydev *d = dev_get_drvdata(dev);
// 从磁盘恢复回来,硬件是"完全冷启动"状态
// 必须重新初始化——不能假设寄存器保留了任何 freeze 时的值
mydev_hw_init(d);
mydev_restore_regs(d);
mydev_start_dma(d);
return 0;
}
要点三:不能有内存里的 firmware handle
如果驱动的运行状态依赖某个当时用 request_firmware() 加载的 blob,restore 时必须重新调用 request_firmware(),因为 image 里的 firmware 副本理论上还在,但很多 GPU/DSP firmware 会被写到设备内 SRAM,磁盘 image 里的副本已经过期了。
Nvidia/AMD GPU、DSP、部分 WiFi 芯片是这一类问题的重灾区,所以 hibernation 在这些平台上一直被打上 experimental 标签。
hibernation 子系统做的三件事:造快照、写磁盘、下次开机让新内核加载旧内核状态。这三件事分别对应三层设计:
hibernation_snapshot + freezer + swsusp_arch_suspend 保存 CPU 状态 + swsusp_save 拷贝 saveable pagesswsusp_write 逐页写 → swap_map_page 记录扇区 → mark_swapfiles 提交 signaturesoftware_resume 读盘 → swsusp_arch_resume 跳转 image kernel → 恢复所有硬件hibernation 的机制不在任何单一函数,它的核心难点在于:同一份内存有两个内核在写、同一段代码有两次 return、同一批 page 落盘时可能被压缩、被加密、被 shrink。任何一处理解偏差都会体现为 resume 后崩溃或 resume 一半失败。
在实际部署上,hibernation 用得越来越少,SSD 普及后 image 写入更快,但用户对秒开的期待也高了。