S4休眠及唤醒功能分析(Arm64/linux4.19)

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. 内核配置

必选:

  1. CONFIG_HIBERNATION,打开休眠功能。

可选:

  1. COFIG_PM_STD_PARTITION,设置默认的休眠镜像保存路径,也可以通过2.2节的启动参数来控制具体保存路径。

2.2. S4状态启动参数

可选:

  1. no_console_suspend,若包含此参数,在休眠挂起时允许控制台输出,否则控制台将挂起。
  2. resume=/dev/sdXn,包含此参数时,将忽略配置选项中的默认恢复分区。
  3. loglevel=8,设置此参数,用于在休眠恢复期间打出更具体的日志。
  4. nocompress,设置此参数,则在执行镜像存储前不进行压缩操作,否则默认使用lzo压缩算法压缩镜像。

2.3. sys接口使用

休眠功能使用非常便捷,linux在/sys/power目录下提供了相关的接口以供使用,主要使用以下两个接口。

  1. /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
  2. /sys/power/disk
    控制休眠时的行为,包含是否下电,是否重启,是否委托平台执行简化的休眠操作。

    输入值 含义
    platform 执行固件平台提供的简化的休眠,如ACPI提供的休眠功能
    shutdown 写入休眠镜像后关机
    reboot 写入镜像后重新启动
    suspend 混合休眠模式,写入镜像后将系统挂起至mem_state描述的S3休眠状态,恢复时会直接从内存恢复,但如果出现内存掉电的情况,可以从硬盘恢复
    test_resume 镜像诊断,写入镜像后立即恢复镜像,不执行下电操作
  3. /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/state
    shallow standby S1 echo shallow > /sys/power/mem_state
    echo mem > /sys/power/state
    deep suspend-to-RAM S3 echo deep > /sys/power/mem_state
    echo mem > /sys/power/state
  4. /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。线程冻结过程如下:

  1. 通过fake_signal_wake_up发送一个假唤醒信号给所有正在休眠的用户进程/线程,并将其强制唤醒。
  2. 线程唤醒后实际上执行了一个信号捕获函数,其会调用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管理

在实际保存内存状态前,并将当前已使用内存和空闲内存,保存为两个位图。

  1. bm1, forbidden_pages_map,当前已使用内存的位图,这部分被禁止修改了。
  2. 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
  • 19
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
/** * YDL_Hibernate概要 <br/> * (一)支持功能: 1.自动建表,支持属性来自继承类:可根据注解自动完成建表,并且对于继承类中的注解字段也支持自动建表. 2.自动支持增删改 * ,增改支持对象化操作:增删改是数据库操作的最基本单元,不用重复写这些增删改的代码,并且添加和更新支持类似于hibernate中的对象化操作. * 3.查询方式灵活:支持android框架提供的方式,也支持原生sql方式. * 4.查询结果对象化:对于查询结果可自动包装为实体对象,类似于hibernate框架. * 5.查询结果灵活:查询结果支持对象化,也支持结果为List<Map<String,String>>形式,这个方法在实际项目中很实用,且效率更好些. * 6.日志较详细:因为android开发不支持热部署调试,运行报错时可根据日志来定位错误,这样可以减少运行Android的次数. <br/> * (二)不足之处: <br/> * 1.id暂时只支持int类型,不支持uuid,在sqlite中不建议用uuid. * 2.现在每个方法都自己开启和关闭事务,暂时还不支持在一个事务中做多个操作然后统一提交事务. <br/> * (三)作者寄语:<br/> * 昔日有JavaScript借Java发展,今日也希望AHibernate借Hibernate之名发展. * 希望这个项目以后会成为开源社区的重要一员,更希望这个项目能给所有Android开发者带便利. * 欢迎访问我的博客:http://blog.csdn.net/linglongxin24, * 这里有这个框架的使用范例和源码,希望朋友们多多交流完善这个框架,共同推动中国开源事业的发展,YDL_Hibernate期待与您共创美好未来!!! */

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值