S4休眠功能简介
针对ARM64架构/Linux 4.19内核
S4全称Suspend-to-disk,即挂起至硬盘,又称休眠(Hibernation)。简单讲是将当前内存镜像保存起来,存储至硬盘,然后所有设备下电。重新上电时,系统会在late_initcall阶段将硬盘中保存的镜像数据恢复至内存中,从而还原睡眠前的现场。
对于S4来说,相当于重新走了一遍系统关机/启动流程,所有设备都会下电并重新初始化,只是会从镜像中还原之前保存的数据状态。
文章目录
1. 睡眠功能概述
1.1. S状态定义
Linux内核包含多种休眠状态,目前主要是4种,目前S4是最省电的一种休眠形式,但恢复速度最慢。
状态 | ACPI等级 | 动作 | 备注 |
---|---|---|---|
Suspend-to-idle | - | 冻结用户空间进程 停止timekeeping IO设为低功耗状态 |
通用,纯软件实现,支持所有平台,且可以用于下面的所有休眠之中 |
Standby | S1 | 包含Suspend-to-idle所有动作 非引导cpu offline |
待机,适度节能,较快恢复 |
Suspend-to-RAM | S3 | 包含Standby所有动作 内存/唤醒设备外,所有设备进入低功耗状态,内存自动刷新 |
睡眠,提供较大程度节能,恢复较慢 |
Hibernation Suspend-to-Disk |
S4 | 创建内存快照保存至disk 内核停止所有系统活动 所有设备进入低功耗状态 |
休眠,提供最大程度节能,恢复最慢 |
2. 休眠功能的配置和使用
2.1. 内核配置
必选:
- CONFIG_HIBERNATION,打开休眠功能。
可选:
- COFIG_PM_STD_PARTITION,设置默认的休眠镜像保存路径,也可以通过2.2节的启动参数来控制具体保存路径。
2.2. S4状态启动参数
可选:
- no_console_suspend,若包含此参数,在休眠挂起时允许控制台输出,否则控制台将挂起。
- resume=/dev/sdXn,包含此参数时,将忽略配置选项中的默认恢复分区。
- loglevel=8,设置此参数,用于在休眠恢复期间打出更具体的日志。
- nocompress,设置此参数,则在执行镜像存储前不进行压缩操作,否则默认使用lzo压缩算法压缩镜像。
2.3. sys接口使用
休眠功能使用非常便捷,linux在/sys/power目录下提供了相关的接口以供使用,主要使用以下两个接口。
-
/sys/power/state
控制进入哪一个S状态(s1-s4),包含下列参数输入值 系统进入状态 ACPI状态 命令 freeze suspend-to-idle - echo freeze > /sys/power/state standby standby S1 echo standby > /sys/power/state mem 由/sys/power/mem_sleep确定 S3 echo mem > /sys/power/state disk hibernation S4 echo disk > /sys/power/state -
/sys/power/disk
控制休眠时的行为,包含是否下电,是否重启,是否委托平台执行简化的休眠操作。输入值 含义 platform 执行固件平台提供的简化的休眠,如ACPI提供的休眠功能 shutdown 写入休眠镜像后关机 reboot 写入镜像后重新启动 suspend 混合休眠模式,写入镜像后将系统挂起至mem_state描述的S3休眠状态,恢复时会直接从内存恢复,但如果出现内存掉电的情况,可以从硬盘恢复 test_resume 镜像诊断,写入镜像后立即恢复镜像,不执行下电操作 -
/sys/power/mem_state
该接口主要是描述挂起时的行为,允许用户空间选择变量与上面的mem关联。
可能存在的名称:s2idle,shallow, deep,分别对应上面的suspend-to-idle, standby和suspend-to-RAM。输入值 系统进入状态 ACPI状态 命令 s2idle suspend-to-idle - echo s2idle > /sys/power/mem_state
echo mem > /sys/power/stateshallow standby S1 echo shallow > /sys/power/mem_state
echo mem > /sys/power/statedeep suspend-to-RAM S3 echo deep > /sys/power/mem_state
echo mem > /sys/power/state -
/sys/power/image_size
该接口描述了用户定义的最大镜像大小,内核会尽可能保证镜像大小不超过此值,但如果保证不了,那内核会尝试创建尽可能小的镜像。
3. 休眠功能的主要流程
3.1 休眠准备工作
系统进入正式休眠之前,需要许多前序工作,包括控制台管理
3.1.1. PM(Power managment)控制台迁移
执行休眠的第一个动作是挂起/重定向控制台,其会尝试寻找挂起可用的虚拟控制台(virtual console),名为SUSPEND_CONSOLE。
为什么这么做? 原有控制台所绑定的设备,比如显卡可能会在休眠期间挂起,迁移到的控制台在休眠期间不会被挂起,所以用户看到的是一个完整的休眠过程,而不是黑屏或卡死这种可能让用户疑惑的状态。
如果配置了no_suspend_console,则不会挂起当前控制台,也不会重定向控制台。
void pm_prepare_console(void)
{
if (!pm_vt_switch())
return;
orig_fgconsole = vt_move_to_console(SUSPEND_CONSOLE, 1);
if (orig_fgconsole < 0)
return;
orig_kmsg = vt_kmsg_redirect(SUSPEND_CONSOLE);
return;
}
3.1.2. PM 消息链管理
完成控制台转移挂起后,将通知PM notifier chain上所有注册的事件发生了PM_HIBERNATION_PREPARE准备事件。
int __pm_notifier_call_chain(unsigned long val, int nr_to_call, int *nr_calls)
{
int ret;
ret = __blocking_notifier_call_chain(&pm_chain_head, val, NULL,
nr_to_call, nr_calls);
return notifier_to_errno(ret);
}
3.1.3. 元数据同步
完成PM通知链之前,会调用ksys_sync来通知刷新线程,将未完成写入的数据/元数据强制刷到硬盘上,保持数据一致性。
void ksys_sync(void)
{
int nowait = 0, wait = 1;
wakeup_flusher_threads(WB_REASON_SYNC); /* 唤醒刷新线程 */
iterate_supers(sync_inodes_one_sb, NULL); /* sync超级块 */
iterate_supers(sync_fs_one_sb, &nowait); /* 异步sync超级块 */
iterate_supers(sync_fs_one_sb, &wait); /* 同步sync超级块 */
iterate_bdevs(fdatawrite_one_bdev, NULL); /* sync块设备数据到磁盘*/
iterate_bdevs(fdatawait_one_bdev, NULL); /* 等待上一步骤完成 */
if (unlikely(laptop_mode))
laptop_sync_completion(); /* 执行笔记本相关sync工作 */
}
3.1.4. 用户进程冻结
为避免镜像保存时,进程数据仍旧在变化,需要先执行线程冻结操作,随后会disable OOM killer。线程冻结过程如下:
- 通过fake_signal_wake_up发送一个假唤醒信号给所有正在休眠的用户进程/线程,并将其强制唤醒。
- 线程唤醒后实际上执行了一个信号捕获函数,其会调用try_to_freeze将自身冻结。
int freeze_processes(void)
{
int error;
error = __usermodehelper_disable(UMH_FREEZING);
if (error)
return error;
/* Make sure this task doesn't get frozen */
current->flags |= PF_SUSPEND_TASK;
if (!pm_freezing)
atomic_inc(&system_freezing_cnt);
pm_wakeup_clear(true);
pr_info("Freezing user space processes ... ");
pm_freezing = true;
error = try_to_freeze_tasks(true); /* 这里会调用kick_process唤醒线程 */
if (!error) {
__usermodehelper_set_disable_depth(UMH_DISABLED);
pr_cont("done.");
}
pr_cont("\n");
BUG_ON(in_atomic());
if (!error && !oom_killer_disable(msecs_to_jiffies(freeze_timeout_msecs)))
error = -EBUSY;
if (error)
thaw_processes();
return error;
}
static inline bool try_to_freeze(void)
{
if (!(current->flags & PF_NOFREEZE))
debug_check_no_locks_held();
return try_to_freeze_unsafe();
}
static inline bool try_to_freeze_unsafe(void)
{
might_sleep();
if (likely(!freezing(current)))
return false;
return __refrigerator(false);
}
bool __refrigerator(bool check_kthr_stop)
{
/* Hmm, should we be allowed to suspend when there are realtime
processes around? */
bool was_frozen = false;
unsigned int save = get_current_state();
pr_debug("%s entered refrigerator\n", current->comm);
for (;;) {
set_current_state(TASK_UNINTERRUPTIBLE);
spin_lock_irq(&freezer_lock);
current->flags |= PF_FROZEN;
if (!freezing(current) ||
(check_kthr_stop && kthread_should_stop()))
current->flags &= ~PF_FROZEN;
spin_unlock_irq(&freezer_lock);
if (!(current->flags & PF_FROZEN))
break;
was_frozen = true;
schedule();
}
pr_debug("%s left refrigerator\n", current->comm);
/*
* Restore saved task state before returning. The mb'd version
* needs to be used; otherwise, it might silently break
* synchronization which depends on ordered task state change.
*/
set_current_state(save);
return was_frozen;
}
3.1.5. 内存bitmap管理
在实际保存内存状态前,并将当前已使用内存和空闲内存,保存为两个位图。
- bm1, forbidden_pages_map,当前已使用内存的位图,这部分被禁止修改了。
- bm2, free_pages_map,当前内存的空闲位图,可用于生成内存镜像。
具体数据结构是使用一个radix tree(4.19版本是radix tree,忘了哪一个版本之后内核用于索引的结构变成了xarray)结构来高效索引bitmap,通过遍历当前内存中的每一个populated zone并检查其标志位,确定其使用情况,并以树节点的形式添加进radix tree中。
int create_basic_memory_bitmaps(void)
{
struct memory_bitmap *bm1, *bm2;
int error = 0;
if (forbidden_pages_map && free_pages_map)
return 0;
else
BUG_ON(forbidden_pages_map || free_pages_map);
bm1 = kzalloc(sizeof(struct memory_bitmap), GFP_KERNEL);
if (!bm1)
return -ENOMEM;
error = memory_bm_create(bm1, GFP_KERNEL, PG_ANY);
if (error)
goto Free_first_object;
bm2 = kzalloc(sizeof(struct memory_bitmap), GFP_KERNEL);
if (!bm2)
goto Free_first_bitmap;
error = memory_bm_create(bm2, GFP_KERNEL, PG_ANY);
if (error)
goto Free_second_object;
forbidden_pages_map = bm1;
free_pages_map = bm2;
mark_nosave_pages(forbidden_pages_map);
pr_debug("Basic memory bitmaps created\n");
return 0;
Free_second_object:
kfree(bm2);
Free_first_bitmap:
memory_bm_free(bm1, PG_UNSAFE_CLEAR);
Free_first_object:
kfree(bm1);
return -ENOMEM;
}
3.2 内存镜像创建
此部分是S4休眠的核心部分之一。其程序调用关系图如下:
这个程序比较特殊,其在休眠时会正常退出。但在休眠唤醒时,会直接将sp指针设置为__cpu_suspend_enter()保存的值,从而唤醒会继续走休眠后半部分的流程。
此部分的主要功能详细介绍如下:
3.2.1. 平台休眠回调
在执行镜像创建之前,会使用通知firmware执行平台回调,如在ACPI定义了平台相关休眠Method。
static int platform_begin(int platform_mode)
{
return (platform_mode && hibernation_ops) ?
hibernation_ops->begin(PMSG_FREEZE) : 0;
}
static const struct platform_hibernation_ops acpi_hibernation_ops = {
.begin = acpi_hibernation_begin,
.end = acpi_pm_end,
.pre_snapshot = acpi_pm_prepare,
.finish = acpi_pm_finish,
.prepare = acpi_pm_prepare,
.enter = acpi_hibernation_enter,
.leave = acpi_hibernation_leave,
.pre_restore = acpi_pm_freeze,
.restore_cleanup = acpi_pm_thaw,
};
static int acpi_hibernation_begin(pm_message_t stage)
{
if (!nvs_nosave) {
int error = suspend_nvs_alloc();
if (error)
return error;
}
if (stage.event == PM_EVENT_HIBERNATE)
pm_set_suspend_via_firmware();
acpi_pm_start(ACPI_STATE_S4);
return 0;
}
3.2.2. 镜像内存预分配
休眠镜像内存分配函数hibernate_preallocate_memory
,为内存镜像预分配内存空间。用户可以提供/sys/power/image_size
来调节image最大大小。内核会在此函数内尝试将分配的内存大小控制在这个范围内,实在满足不了的时候,内核会尽可能将镜像内存减小。
int hibernate_preallocate_memory(void)
{
struct zone *zone;
unsigned long saveable, size, max_size, count, highmem, pages = 0;
unsigned long alloc, save_highmem, pages_highmem, avail_normal;
ktime_t start, stop;
int error;
alloc_normal = 0;
alloc_highmem = 0;
/* Count the number of saveable data pages. */
save_highmem = count_highmem_pages(); //计算需要保存的high-mem page
saveable = count_data_pages(); //计算需要保存的normal-mem page
/*
* Compute the total number of page frames we can use (count) and the
* number of pages needed for image metadata (size).
*/
count = saveable;
saveable += save_highmem