Linux电源管理——CPU suspend/resume流程

关于整个系统的 suspend/resume 流程大家可看查看我另外一篇文章,点击这里可直达 ,本文的主要内容是分析在Linux系统中 CPU 的 suspend/resume 流程。因为CPU可以分为 Non-Boot CPU和 boot CPU,所以CPU 的suspend/resume我也将分为这两类进行介绍。

目录

一、Non-Boot CPU suspend流程

1. cpu_ops 初始化流程

2. cpu_get_ops 函数分析

3. PSCI 接口介绍

二、BOOT CPU suspend 流程

1 、BOOT CPU suspend概述

2、函数指针enter的初始化

3、psci_system_suspend_enter 函数

4、arm架构中的cpu_suspend函数

5、arm64架构中的cpu_suspend函数

6、arm64架构中的resume流程


一、Non-Boot CPU suspend流程

Non-Boot CPU 顾名思义就是非引导的 CPU ,也就是除去 CPU0 之外的所以CPU叫做非引导CPU,关于为什么是这样如果感兴趣可以去看一下PSCI cpu多核启动流程。

让我们回到 suspend_enter 函数,进入到 disable_nonboot_cpus 函数,该函数的定义如下:

static inline int disable_nonboot_cpus(void)
{
	return freeze_secondary_cpus(0);
}

进入 return freeze_secondary_cpus 函数

int freeze_secondary_cpus(int primary)
{
	int cpu, error = 0;
 
	cpu_maps_update_begin();
	if (!cpu_online(primary))
		primary = cpumask_first(cpu_online_mask);
	/*
	 * We take down all of the non-boot CPUs in one shot to avoid races
	 * with the userspace trying to use the CPU hotplug at the same time
	 */
	cpumask_clear(frozen_cpus);
 
	pr_info("Disabling non-boot CPUs ...\n");
	for_each_online_cpu(cpu) {
		if (cpu == primary)
			continue;
		trace_suspend_resume(TPS("CPU_OFF"), cpu, true);
		error = _cpu_down(cpu, 1, CPUHP_OFFLINE);
		trace_suspend_resume(TPS("CPU_OFF"), cpu, false);
		if (!error)
			cpumask_set_cpu(cpu, frozen_cpus);
		else {
			pr_err("Error taking CPU%d down: %d\n", cpu, error);
			break;
		}
	}
 
	if (!error)
		BUG_ON(num_online_cpus() > 1);
	else
		pr_err("Non-boot CPUs are not disabled\n");
 
	/*
	 * Make sure the CPUs won't be enabled by someone else. We need to do
	 * this even in case of failure as all disable_nonboot_cpus() users are
	 * supposed to do enable_nonboot_cpus() on the failure path.
	 */
	cpu_hotplug_disabled++;
 
	cpu_maps_update_done();
	return error;
}

if (!cpu_online(primary)) primary=cpumask_first(cpu_online_mask):如果指定的主 CPU 不在线,则选择在线的第一个 CPU 作为主 CPU。

for_each_online_cpu(cpu):遍历每个在线的 CPU。

if (cpu == primary) continue:如果当前 CPU 是主 CPU,则跳过。

_cpu_down(cpu, 1, CPUHP_OFFLINE):调用 _cpu_down 函数关闭 nonboot CPU。

这里最重要的一个函数就是 _cpu_down,该函数会将 CPU 设置为离线状态,CPUHP_OFFLIN E 是一个枚举值,用来标识 CPU 即将进入离线状,当 CPU 离线时,它不再参与任何调度工作,并且会将当前的所有任务都迁移到其它在线的 CPU 上。

现在CPU已经被标识为离线状态,然后进入到 cpu_startup_entry 函数,函数定义在 /kernel/sched/idle.c 文件中,其定义如下:

void cpu_startup_entry(enum cpuhp_state state)
{
	/*
	 * This #ifdef needs to die, but it's too late in the cycle to
	 * make this generic (arm and sh have never invoked the canary
	 * init for the non boot cpus!). Will be fixed in 3.11
	 */
#ifdef CONFIG_X86
	/*
	 * If we're the non-boot CPU, nothing set the stack canary up
	 * for us. The boot CPU already has it initialized but no harm
	 * in doing it again. This is a good place for updating it, as
	 * we wont ever return from this function (so the invalid
	 * canaries already on the stack wont ever trigger).
	 */
	boot_init_stack_canary();
#endif
	arch_cpu_idle_prepare();
	cpuhp_online_idle(state);
	while (1)
		do_idle();
}

在这里有一个无限循环函数 do_idle() ,它是 CPU 空闲时的主循环,当 CPU 没有任务需要执行时,它会执行这个函数,进入低功耗模式,等待被唤醒执行新任务,进入到 do_idle 函数:

static void do_idle(void)
{
    ......
 
	while (!need_resched()) {
		check_pgt_cache();
		rmb();
 
		local_irq_disable();
 
		if (cpu_is_offline(smp_processor_id())) {
			tick_nohz_idle_stop_tick();
			cpuhp_report_idle_dead();
			arch_cpu_idle_dead();
		}
        ......
}

由于do_idle函数的其它代码这里用不到,所以先省略,先来看上面这段代码

while(!need_resched()): 循环执行,直到有任务需要被调度,如果没有任务就会进入循环内。

check_pgt_cache(): 检查页表缓存。

rmb(): 读取内存屏障,确保之前的内存访问操作对后续操作可见。

local_irq_disable(): 禁用本地中断,防止在空闲循环中被中断打断。

if (cpu_is_offline(smp_processor_id())): 检查 CPU 是否处于离线状态,如果是,则执行一系列离线处理。

tick_nohz_idle_stop_tick(): 如果 CPU 处于离线状态,这个函数调用会停止滴答计时,当无滴答时,系统不会在固定的时间间隔内发出滴答中断,而是根据实际需要来调度任务。

cpuhp_report_idle_dead():  这个函数用于报告 CPU 的空闲状态或者无效。CPUHP(CPU Hotplug)是 Linux 内核中用于处理 CPU 热插拔事件的框架,该函数用来报告CPU 离线状态的某些变化。

arch_cpu_idle_dead():  这是一个和架构相关的函数,它主要负责处理 CPU 离线时与架构相关的工作。

在前面我们已经通过_cpu_down将CPU设置为离线状态,所以cpu_is_offline(smp_processor _id()) 函数为真,所以会执行到 arch_cpu_idle_dead()函数该函数定义在/arch/arm64/ kernel/ process.c 文件中,定义如下:

void arch_cpu_idle_dead(void)
{
       cpu_die();
}

进入cpu_die() 函数

/*
 * Called from the idle thread for the CPU which has been shutdown.
 *
 * Note that we disable IRQs here, but do not re-enable them
 * before returning to the caller. This is also the behaviour
 * of the other hotplug-cpu capable cores, so presumably coming
 * out of idle fixes this.
 */
void cpu_die(void)
{
	unsigned int cpu = smp_processor_id();

	/* Save the shadow stack pointer before exiting the idle task */
	scs_save(current);

	idle_task_exit();

	local_irq_disable();

	/* Tell __cpu_die() that this CPU is now safe to dispose of */
	(void)cpu_report_death();

	/*
	 * Actually shutdown the CPU. This must never fail. The specific hotplug
	 * mechanism must perform all required cache maintenance to ensure that
	 * no dirty lines are lost in the process of shutting down the CPU.
	 */
	cpu_ops[cpu]->cpu_die(cpu);

	BUG();
}

unsigned int cpu = smp_processor_id(): 获取当前处理器的标识符,并保存在变量 cpu 中。

idle_task_exit(): 退出空闲任务。

local_irq_disable(): 禁用本地中断,为了确保在 CPU 关闭过程中不会有中断打断该过程。

(void)cpu_report_death(): 报告 CPU 的死亡。

cpu_ops[cpu]->cpu_die(cpu): 调用特定的函数来实际关闭 CPU。

这里最重要的一段代码就是 cpu_ops[cpu]->cpu_die(cpu),下面对cpu_ops进行分析。

cpu_ops 定义在/arch/arm64/kernel/cpu_ops.c 文件中,定义如下:

const struct cpu_operations *cpu_ops[NR_CPUS] __ro_after_init;

cpu_ops 是一个 struct cpu_operations 类型的指针数组,cpu_operations 结构体定义在/arch/arm64/include/asm/cpu_ops.h文件中,定义如下:

struct cpu_operations {
	const char	*name;
	int		(*cpu_init)(unsigned int);
	int		(*cpu_prepare)(unsigned int);
	int		(*cpu_boot)(unsigned int);
	void		(*cpu_postboot)(void);
#ifdef CONFIG_HOTPLUG_CPU
	int		(*cpu_disable)(unsigned int cpu);
	void		(*cpu_die)(unsigned int cpu);
	int		(*cpu_kill)(unsigned int cpu);
#endif
#ifdef CONFIG_CPU_IDLE
	int		(*cpu_init_idle)(unsigned int);
	int		(*cpu_suspend)(unsigned long);
#endif
};

cpu_operations 结构体主要定义了和 CPU 操作相关的各种函数指针。这些函数指针用于执行 CPU 的初始化、启动、关闭等相关操作。

1. cpu_ops 初始化流程

cpu_operations 结构体的初始化是在内核的启动阶段进行的,具体来说是在 CPU 初始化的过程中通过 cpu_read_ops 函数进行获取并设置每个 CPU 的操作函数。

cpu_read_ops 函数定义在 /arch/arm64/kernel/cpu_ops.c 文件中,定义如下:

/*
 * Read a cpu's enable method and record it in cpu_ops.
 */
int __init cpu_read_ops(int cpu)
{
	const char *enable_method = cpu_read_enable_method(cpu);

	if (!enable_method)
		return -ENODEV;

	cpu_ops[cpu] = cpu_get_ops(enable_method);
	if (!cpu_ops[cpu]) {
		pr_warn("Unsupported enable-method: %s\n", enable_method);
		return -EOPNOTSUPP;
	}

	return 0;
}

这段代码先通过 cpu_read_enable_method 函数读取指定 CPU 的启用方法,并将cpu_get_ops函数的返回值放到 cpu_ops[cpu] 数组中,所以现在分析cpu_get_ops 函数。

cpu_get_ops 函数定义如下:

static const struct cpu_operations * __init cpu_get_ops(const char *name)
{
	const struct cpu_operations **ops;
 
	ops = acpi_disabled ? dt_supported_cpu_ops : acpi_supported_cpu_ops;
 
	while (*ops) {
		if (!strcmp(name, (*ops)->name))
			return *ops;
 
		ops++;
	}
 
	return NULL;
}

ops = acpi_disabled ? dt_supported_cpu_ops : acpi_supported_cpu_ops;

dt_supported_cpu_ops:使用 Device Tree

acpi_supported_cpu_ops:ACPI(高级配置和电源接口)

这里根据 acpi_disabled 的值选择两个不同的 CPU操作列表dt_supported_cpu_ops或 acpi_supported_cpu_ops如果 acpi_disabled 为真(非零),则选择dt_support ed_cpu_ops,反之选择 acpi_supported_cpu_ops在我这个系统中 acpi_disabled 宏定义为1,所以选择 dt_supported_cpu_ops

所以 cpu_ops 是通过cpu_get_ops 函数进行初始化的。

2. cpu_get_ops 函数分析

dt_supported_cpu_ops 的定义如下:

static const struct cpu_operations *dt_supported_cpu_ops[] __initconst = {
	&smp_spin_table_ops,
	&cpu_psci_ops,
	NULL,
};

这段代码定义了一个名为 dt_supported_cpu_ops 的指针数组,这个数组用来存储使用 Device Tree 支持的 CPU 操作。

smp_spin_table_ops :指向一个 cpu_operations 结构体的指针,也就是通过  spin_table 方式启动多核方式的操作指针。

cpu_psci_ops :指向一个 cpu_operations 结构体的指针,也就是 PSCI 方式启动多核的操作指针。

因为我目前使用的系统是通过 PSCI 启动多核的,所以我这里以 PSCI 为例。

cpu_psci_ops 定义如下:

const struct cpu_operations cpu_psci_ops = {
	.name		= "psci",
#ifdef CONFIG_CPU_IDLE
	.cpu_init_idle	= psci_cpu_init_idle,
	.cpu_suspend	= psci_cpu_suspend_enter,
#endif
	.cpu_init	= cpu_psci_cpu_init,
	.cpu_prepare	= cpu_psci_cpu_prepare,
	.cpu_boot	= cpu_psci_cpu_boot,
#ifdef CONFIG_HOTPLUG_CPU
	.cpu_disable	= cpu_psci_cpu_disable,
	.cpu_die	= cpu_psci_cpu_die,
	.cpu_kill	= cpu_psci_cpu_kill,
#endif
};

这段代码主要是定义了一个名为 cpu_psci_ops 的结构体,类型为cpu_operations ,并进行初始化,该实例包含了一系列与 PSCI 相关的 CPU 操作函数,如休眠、唤醒、关闭等操作。

所以

这里只说明CPU热插拔的接口,如下:

name = "psci":标识这个 cpu_operations 结构体的名称

#ifdef CONFIG_HOTPLUG_CPU: 如果定义了CPU热插拔

cpu_disable = cpu_psci_cpu_disable:当需要禁用某个 CPU 时,这个函数会被调用。

cpu_die    = cpu_psci_cpu_die:这个函数用于执行 CPU 关闭前的最后操作,确保 CPU 安全地关闭。

cpu_kill    = cpu_psci_cpu_kill:如果 CPU 没有正常关闭,这个函数会被调用来强制关闭 CPU。

cpu_psci_cpu_die 函数如下:

static void cpu_psci_cpu_die(unsigned int cpu)
{
	/*
	 * There are no known implementations of PSCI actually using the
	 * power state field, pass a sensible default for now.
	 */
	u32 state = PSCI_POWER_STATE_TYPE_POWER_DOWN <<
		    PSCI_0_2_POWER_STATE_TYPE_SHIFT;
 
	psci_ops.cpu_off(state);
}

cpu_psci_cpu_die函数用于关闭指定的 CPU,所以这个函数调用之后 指定的CPU就会关闭。

3. PSCI 接口介绍

这里使用的是 PSCI cpu_off 接口,psci_ops 定义在 /include/linux/psci.h 文件中,定义如下:

extern struct psci_operations psci_ops;

psci_operations 结构体定义如下:

struct psci_operations {
	u32 (*get_version)(void);
	int (*cpu_suspend)(u32 state, unsigned long entry_point);
	int (*cpu_off)(u32 state);
	int (*cpu_on)(unsigned long cpuid, unsigned long entry_point);
	int (*migrate)(unsigned long cpuid);
	int (*affinity_info)(unsigned long target_affinity,
			unsigned long lowest_affinity_level);
	int (*migrate_info_type)(void);
	enum psci_conduit conduit;
	enum smccc_version smccc_version;
};

那 psci_ops 结构体又是在什么时候初始化的呢?下面接着分析

进入 PSCI 的初始化函数 psci_0_1_init ,函数如下所示:

/*
 * PSCI < v0.2 get PSCI Function IDs via DT.
 */
static int __init psci_0_1_init(struct device_node *np)
{
	u32 id;
	int err;
 
	err = get_set_conduit_method(np);
 
	if (err)
		goto out_put_node;
 
	pr_info("Using PSCI v0.1 Function IDs from DT\n");
 
	if (!of_property_read_u32(np, "cpu_suspend", &id)) {
		psci_function_id[PSCI_FN_CPU_SUSPEND] = id;
		psci_ops.cpu_suspend = psci_cpu_suspend;
	}
 
	if (!of_property_read_u32(np, "cpu_off", &id)) {
		psci_function_id[PSCI_FN_CPU_OFF] = id;
		psci_ops.cpu_off = psci_cpu_off;
	}
 
	if (!of_property_read_u32(np, "cpu_on", &id)) {
		psci_function_id[PSCI_FN_CPU_ON] = id;
		psci_ops.cpu_on = psci_cpu_on;
	}
 
	if (!of_property_read_u32(np, "migrate", &id)) {
		psci_function_id[PSCI_FN_MIGRATE] = id;
		psci_ops.migrate = psci_migrate;
	}
 
out_put_node:
	of_node_put(np);
	return err;
}

在 psci_0_1_init 函数中首先会调用 get_set_conduit_method(np) 函数设置是使用 hvc 还是 smc 而陷入到不同的异常等级。

static int get_set_conduit_method(struct device_node *np)
{
	......
	
	if (!strcmp("hvc", method)) {
		set_conduit(PSCI_CONDUIT_HVC);
	} else if (!strcmp("smc", method)) {
		set_conduit(PSCI_CONDUIT_SMC);
	}
	
	......
}

set_conduit 函数实现如下:

static void set_conduit(enum psci_conduit conduit)
{
	switch (conduit) {
	case PSCI_CONDUIT_HVC:
		invoke_psci_fn = __invoke_psci_fn_hvc;
		break;
	case PSCI_CONDUIT_SMC:
		invoke_psci_fn = __invoke_psci_fn_smc;
		break;
	default:
		WARN(1, "Unexpected PSCI conduit %d\n", conduit);
	}
 
	psci_ops.conduit = conduit;
}

set_conduit函数用于设置 PSCI 的通信机制,通过不同的 conduitinvoke_psci_fn 进行不同的设置。

如果传入的 conduit 是 PSCI_CONDUIT_HVC,则将 invoke_psci_fn 设置为 __invoke_psci_fn_hvc,即使用 Hypervisor Call(HVC)作为通信机制。

如果传入的 conduit 是 PSCI_CONDUIT_SMC,则将 invoke_psci_fn 设置为 __invoke_psci_fn_smc,即使用 Secure Monitor Call(SMC)作为通信机制。

invoke_psci_fn 是一个函数指针,它指向实际用于调用 PSCI 函数的实现。

回到 psci_0_1_init 函数中,该函数会对 psci_ops 中的成员项进行相关设置,所以在cpu_psci_cpu_die 函数中 psci_ops.cpu_off(state) 调用的 cpu_off 函数就是 psci_0_1_init 中psci_ ops.cpu_off = psci_cpu_off设置的,所以会调用到 psci_cpu_off 函数。

psci_cpu_off 函数定义如下:

static int psci_cpu_off(u32 state)
{
	int err;
	u32 fn;
 
	fn = psci_function_id[PSCI_FN_CPU_OFF];
	err = invoke_psci_fn(fn, state, 0, 0);
	return psci_to_linux_errno(err);
}

这里会调用到 invoke_psci_fn 函数,由上面的分析可知 invoke_psci_fn 函数在 set_conduit 函数中就已经设置好了,即最后是调用 __invoke_psci_fn_smc陷入到 ATF 中去关闭Non-Booting CPU 。

所以 cpu_ops[cpu]->cpu_die(cpu) 的调用流程为:

因为数组的每一个参数都是 struct cpu_operations *cpu_ops,且 cpu_ops[cpu] = cpu_get_ops(en able_method) ,cpu_get_ops 函数的返回为 dt_supported_cpu_ops。

> cpu_ops[cpu]->cpu_die(cpu) 可以等同于 dt_supported_cpu_ops -> cpu_die(cpu)

> dt_supported_cpu_ops 中的 cpu_psci_ops

> cpu_ops[cpu]->cpu_die(cpu) 又可以等同于 cpu_psci_ops -> cpu_die(cpu)

> 在 cpu_psci_ops又将 cpu_psci_ops 中的 cpu_die = cpu_psci_cpu_die,

所以最后的调用为 cpu_psci_ops -> cpu_psci_cpu_die,然后 cpu_psci_cpu_die 再继续调用 psci_ ops.cpu_off接口,从而完成 CPU 的 suspend,这里是直接关闭CPU那为什么说是CPU suspend呢?我的理解是CPU1-CPU7 suspend 其实就是关闭CPU,只有CPU0 suspend才是真正的suspend,如果CPU0也关闭的就不应该叫suspend,那应该是关机了,resume时先是CPU0 恢复相关寄存器的值,然后CPU0再去启动其它CPU。

二、BOOT CPU suspend 流程

1 、BOOT CPU suspend概述

前面介绍了关于Non-Boot CPU的suspend,下面介绍一下Boot CPU 的suspend 流程,回到 /kern el/power/suspend.c 文件中的 suspend_enter 函数,系统是执行该函数中的 suspend_ops->enter(state) 语句完成系统的最终休眠的,那接下来就来分析一下这个语句到底做了些什么事情。

suspend_ops 是一个platform_suspend_ops类型的结构体指针,定义如下:

static const struct platform_suspend_ops *suspend_ops;

platform_suspend_ops 结构体如下:

struct platform_suspend_ops {
	int (*valid)(suspend_state_t state);
	int (*begin)(suspend_state_t state);
	int (*prepare)(void);
	int (*prepare_late)(void);
	int (*enter)(suspend_state_t state);
	void (*wake)(void);
	void (*finish)(void);
	bool (*suspend_again)(void);
	void (*end)(void);
	void (*recover)(void);
};

valid:验证是否支持给定的挂起状态。

begin:开始挂起操作前的准备工作
prepare:挂起前的准备工作,比如关闭不必要的设备或保存状态。

prepare_late:挂起前的后期准备工作,可能涉及更底层的硬件或系统状态保存。

enter:实际进入挂起状态的操作。

wake:从挂起状态唤醒后的早期恢复操作
finish:挂起唤醒后的恢复工作,比如恢复设备状态
suspend_again:检查是否需要再次挂起系统
end:挂起操作的结束工作,清理资源等
recover:如果挂起操作失败,执行恢复操作

platform_suspend_ops 结构体定义了一组用于挂起(suspend)操作的函数指针,这些函数指针提供了挂起和恢复系统状态所需的各种操作,这里重点关注 enter 。

2、函数指针enter的初始化

从上面的分析大概可以知道是调用 enter 进入实际的挂起操作,那 enter 又是在什么地方初始化的呢?下面接着分析

打开 /driver/firmware/目录下的psci.c 文件,可以看到以下一段代码

static const struct platform_suspend_ops psci_suspend_ops = {
	.valid          = suspend_valid_only_mem,
	.enter          = psci_system_suspend_enter,
};

这段代码主要是初始化一个 platform_suspend_ops 结构体,主要就是为CPU0 的 suspend,代码中有那么多的 enter 函数为什么说这个就是suspend函数呢?可能也有同学注意到了,这里定义的结构体名为 psci_suspend_ops 而前面的分析是 suspend_ops里面的enter函数这两个变量有什么关系呢?接着往下分析。

在psci.c 文件中还有这样一段代码

static void __init psci_init_system_suspend(void)
{
	int ret;

	if (!IS_ENABLED(CONFIG_SUSPEND))
		return;

	ret = psci_features(PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND));

	if (ret != PSCI_RET_NOT_SUPPORTED)
		suspend_set_ops(&psci_suspend_ops);
}

从函数名字大概都能够猜到这段代码主要是在系统初始化时设置相应的挂起操作函数。这样,当系统需要挂起时,就会使用这些 PSCI 相关的函数来进行操作,psci_init_system_suspend 函数的初始化是在 psci_probe 中进行的。

注意到该函数有这样一段代码

suspend_set_ops(&psci_suspend_ops);

这个函数就是用于设置系统挂起操作函数指针为 psci_suspend_ops 结构体中的函数,那suspe nd_set_ops函数定义在哪里呢?打开 /kernel/power/suspend.c 文件,也就是定义suspend_ops的文件可以看到以下定义:

/**
 * suspend_set_ops - Set the global suspend method table.
 * @ops: Suspend operations to use.
 */
void suspend_set_ops(const struct platform_suspend_ops *ops)
{
	lock_system_sleep();

	suspend_ops = ops;

	if (valid_state(PM_SUSPEND_STANDBY)) {
		mem_sleep_states[PM_SUSPEND_STANDBY] = mem_sleep_labels[PM_SUSPEND_STANDBY];
		pm_states[PM_SUSPEND_STANDBY] = pm_labels[PM_SUSPEND_STANDBY];
		if (mem_sleep_default == PM_SUSPEND_STANDBY)
			mem_sleep_current = PM_SUSPEND_STANDBY;
	}
	if (valid_state(PM_SUSPEND_MEM)) {
		mem_sleep_states[PM_SUSPEND_MEM] = mem_sleep_labels[PM_SUSPEND_MEM];
		if (mem_sleep_default >= PM_SUSPEND_MEM)
			mem_sleep_current = PM_SUSPEND_MEM;
	}

	unlock_system_sleep();
}
EXPORT_SYMBOL_GPL(suspend_set_ops);

在这个函数注意到这样一段代码

suspend_ops = ops;

这段代码就是将/driverfirmware/psci.c 文件中定义的psci_suspend_ops结构体地址传给 /kernel/ power/suspend.c 文件中定义的 suspend_ops 指针,这样 psci_suspend_opssuspend_ ops就联系起来了所以调用 suspend_ops->enter 就是调用的psci_suspend_ops->enter函数。

3、psci_system_suspend_enter 函数

根据前面的分析结果我们下面直接对 psci_system_suspend_enter 函数进行分析,函数如下:

static int psci_system_suspend(unsigned long unused)
{
	return invoke_psci_fn(PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND),
			      __pa_symbol(cpu_resume), 0, 0);
}
static int psci_system_suspend_enter(suspend_state_t state)
{
	return cpu_suspend(0, psci_system_suspend);
}

invoke_psci_fnPSCI 接口中的特定功能。它接受多个参数,包括要调用的 PSCI 函数的标识符和传递给该函数的参数。

PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND)用于生成表示 PSCI 1.0 版本中 SYSTEM_ SUSPEND 功能的标识符。

__pa_symbol(cpu_resume)用于获取 cpu_resume 符号的物理地址。

这里有两个函数,psci_system_suspend 这个函数是用来调用 PSCI 接口,用来将系统设置为 suspend,简单来说就是当调用 psci_system_suspend 函数之后系统就会调用PSCI接口陷入到ATF(Hypervisor 没有开的前提)去关核,从而达到suspend状态。

这里cpu_resume 是一个函数,用于在CPU Resume 时执行一些操作,这个在后面会提到。

psci_system_suspend_enter 函数里面调用了cpu_suspend 函数并且是以 psci_ system_suspend函数作为参数传递那么我们接下来就对 cpu_suspend 函数进行分析。

cpu_suspend 函数和架构也有关系,不同的架构实现也不同,这里将分别介绍arm和arm64两种架构中的 cpu_suspend 函数实现

4、arm架构中的cpu_suspend函数

打开 /arch/arm/kernel/suspend.c 文件,能够看到如下代码段

#ifdef CONFIG_MMU
int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))
{
	struct mm_struct *mm = current->active_mm;
	u32 __mpidr = cpu_logical_map(smp_processor_id());
	int ret;

	if (!idmap_pgd)
		return -EINVAL;

	/*
	 * Provide a temporary page table with an identity mapping for
	 * the MMU-enable code, required for resuming.  On successful
	 * resume (indicated by a zero return code), we need to switch
	 * back to the correct page tables.
	 */
	ret = __cpu_suspend(arg, fn, __mpidr);
	if (ret == 0) {
		cpu_switch_mm(mm->pgd, mm);
		local_flush_bp_all();
		local_flush_tlb_all();
		check_other_bugs();
	}

	return ret;
}
#else
int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))
{
	u32 __mpidr = cpu_logical_map(smp_processor_id());
	return __cpu_suspend(arg, fn, __mpidr);
}
#define	idmap_pgd	NULL
#endif

虽然这里有条件编译,但最后都是进入到了 __cpu_suspend 函数,我们直接进入该函数,函数定义在 /arch/arm/kernel/sleep.S 文件中,代码如下所示

/*
 * Save CPU state for a suspend.  This saves the CPU general purpose
 * registers, and allocates space on the kernel stack to save the CPU
 * specific registers and some other data for resume.
 *  r0 = suspend function arg0
 *  r1 = suspend function
 *  r2 = MPIDR value the resuming CPU will use
 */
ENTRY(__cpu_suspend)
	stmfd	sp!, {r4 - r11, lr}
#ifdef MULTI_CPU
	ldr	r10, =processor
	ldr	r4, [r10, #CPU_SLEEP_SIZE] @ size of CPU sleep state
#else
	ldr	r4, =cpu_suspend_size
#endif
	mov	r5, sp			@ current virtual SP
	add	r4, r4, #12		@ Space for pgd, virt sp, phys resume fn
	sub	sp, sp, r4		@ allocate CPU state on stack
	ldr	r3, =sleep_save_sp
	stmfd	sp!, {r0, r1}		@ save suspend func arg and pointer
	ldr	r3, [r3, #SLEEP_SAVE_SP_VIRT]
	ALT_SMP(ldr r0, =mpidr_hash)
	ALT_UP_B(1f)
	/* This ldmia relies on the memory layout of the mpidr_hash struct */
	ldmia	r0, {r1, r6-r8}	@ r1 = mpidr mask (r6,r7,r8) = l[0,1,2] shifts
	compute_mpidr_hash	r0, r6, r7, r8, r2, r1
	add	r3, r3, r0, lsl #2
1:	mov	r2, r5			@ virtual SP
	mov	r1, r4			@ size of save block
	add	r0, sp, #8		@ pointer to save block
	bl	__cpu_suspend_save
	badr	lr, cpu_suspend_abort
	ldmfd	sp!, {r0, pc}		@ call suspend fn
ENDPROC(__cpu_suspend)
	.ltorg

__cpu_suspend 其实是一个汇编函数,我刚开始看的时候这个地方给我看懵了,哈哈哈。

这个函数主要作用是保存CPU suspend 之前的准备工作,包括保存CPU的部分寄存器的状态,比如 stmfd sp!,{r4 - r11, lr} 就是将一系列寄存器(从r4到r11,以及lr即链接寄存器)保存到栈上。

那为什么要保存这些寄存器的值呢?简单理解就是为了 resume 的时候能够还原 suspend时的状态吧,suspend之前系统是什么状态,之后就应该恢复到原样,如果不保存那就不是休眠唤醒了,那应该叫开机关机了,哈哈哈。

这个函数中的其它代码就不一一介绍了,大家如果感兴趣可以去看相关源码,这里只介绍和函数调用有关的关键代码。

通过注释可以看到以下内容

r0 = suspend function arg0
r1 = suspend function
r2 = MPIDR value the resuming CPU will use                                                                      

简单的说就是r0寄存器保存的是休眠函数的参数,r2寄存器保存的是挂起函数的地址,在执行CPU挂起操作时,内核会跳转到这个地址来执行挂起操作。

好了,现在来分析一下关键代码,看到如下代码段:

stmfd    sp!, {r0, r1}    @ save suspend func arg and pointer
ldmfd    sp!, {r0, pc}    @ call suspend fn

tmfd:连续压栈,将 r0 和 r1 寄存器进行压栈操作,注意是r1先入栈(函数先入栈,参数后入栈)

ldmfd:连续出栈,将 sp 栈中的数据加载到 r0 和 PC 中,注意是先加载r0(先把参数出栈到 r0 ,再把 函数出栈到 PC)

通过后面的注释也可以看出这两条指令的大概功能,其实最后是把函数的地址给到了PC指针,所以 psci_system_suspend 函数将会被调用,从而陷入到ATF 进行关闭 CPU0。

5、arm64架构中的cpu_suspend函数

接下开介绍一下arm64架构下的cpu_suspend 函数,打开arch/arm64/kernel/suspend.c文件,可以找到下面这个函数

/*
 * cpu_suspend
 *
 * arg: argument to pass to the finisher function
 * fn: finisher function pointer
 *
 */
int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))
{
	int ret = 0;
	unsigned long flags;
	struct sleep_stack_data state;

	/*
	 * From this point debug exceptions are disabled to prevent
	 * updates to mdscr register (saved and restored along with
	 * general purpose registers) from kernel debuggers.
	 */
	local_dbg_save(flags);

	/*
	 * Function graph tracer state gets incosistent when the kernel
	 * calls functions that never return (aka suspend finishers) hence
	 * disable graph tracing during their execution.
	 */
	pause_graph_tracing();

	if (__cpu_suspend_enter(&state)) {
		/* Call the suspend finisher */
		ret = fn(arg);

		/*
		 * Never gets here, unless the suspend finisher fails.
		 * Successful cpu_suspend() should return from cpu_resume(),
		 * returning through this code path is considered an error
		 * If the return value is set to 0 force ret = -EOPNOTSUPP
		 * to make sure a proper error condition is propagated
		 */
		if (!ret)
			ret = -EOPNOTSUPP;
	} else {
		__cpu_suspend_exit();
	}

	unpause_graph_tracing();

	/*
	 * Restore pstate flags. OS lock and mdscr have been already
	 * restored, so from this point onwards, debugging is fully
	 * renabled if it was enabled when core started shutdown.
	 */
	local_dbg_restore(flags);

	return ret;
}

这个函数中也有很多的注释,这里就不在详细说明,直接进入__cpu_suspend_enter(&state) 这个函数,如下:

/*
 * Save CPU state in the provided sleep_stack_data area, and publish its
 * location for cpu_resume()'s use in sleep_save_stash.
 *
 * cpu_resume() will restore this saved state, and return. Because the
 * link-register is saved and restored, it will appear to return from this
 * function. So that the caller can tell the suspend/resume paths apart,
 * __cpu_suspend_enter() will always return a non-zero value, whereas the
 * path through cpu_resume() will return 0.
 *
 *  x0 = struct sleep_stack_data area
 */
ENTRY(__cpu_suspend_enter)
	stp	x29, lr, [x0, #SLEEP_STACK_DATA_CALLEE_REGS]
	stp	x19, x20, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+16]
	stp	x21, x22, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+32]
	stp	x23, x24, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+48]
	stp	x25, x26, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+64]
	stp	x27, x28, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+80]

	/* save the sp in cpu_suspend_ctx */
	mov	x2, sp
	str	x2, [x0, #SLEEP_STACK_DATA_SYSTEM_REGS + CPU_CTX_SP]

	/* find the mpidr_hash */
	ldr_l	x1, sleep_save_stash
	mrs	x7, mpidr_el1
	adr_l	x9, mpidr_hash
	ldr	x10, [x9, #MPIDR_HASH_MASK]
	/*
	 * Following code relies on the struct mpidr_hash
	 * members size.
	 */
	ldp	w3, w4, [x9, #MPIDR_HASH_SHIFTS]
	ldp	w5, w6, [x9, #(MPIDR_HASH_SHIFTS + 8)]
	compute_mpidr_hash x8, x3, x4, x5, x6, x7, x10
	add	x1, x1, x8, lsl #3

	str	x0, [x1]
	add	x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS
	stp	x29, lr, [sp, #-16]!
	bl	cpu_do_suspend
	ldp	x29, lr, [sp], #16
	mov	x0, #1
	ret
ENDPROC(__cpu_suspend_enter)

这个函数主要做的工作就是保存一下CPU当前的一些状态信息到sleep_stack_data,类似中断中的保存现场,具体是怎么保存的这里就不在说明,但在最后的代码段( mov    x0, #1   /  ret )中函数返回值为1,再回到cpu_sus pend函数中

if (__cpu_suspend_enter(&state)) {
	/* Call the suspend finisher */
	ret = fn(arg);
	/*
	 * Never gets here, unless the suspend finisher fails.
	 * Successful cpu_suspend() should return from cpu_resume(),
	 * returning through this code path is considered an error
	 * If the return value is set to 0 force ret = -EOPNOTSUPP
	 * to make sure a proper error condition is propagated
	 */
	if (!ret)
		ret = -EOPNOTSUPP;
} else {
	__cpu_suspend_exit();
}

当 __cpu_suspend_enter 函数返回1时,此时if 的判断为真,所以会执行ret = fn(arg) 语句,所以会调用到 psci_system_suspend 函数陷入到ATF中去suspend CPU0,如果CPU0被suspend那也就是整个系统的suspend,在这个函数调用之后系统就已经睡下去了,那系统resume又是怎么样的呢?

在这里会有一个很有趣的现象,那就是__cpu_suspend_enter 会返回两次,也就是if 和 else 都会进去,下面接着往下分析

6、arm64架构中的resume流程

这里会涉及到一个resume函数,这个函数在psci_system_suspend函数中有提到过,在这里就不在赘述,当系统resume的时候会进入到 resume 函数,函数定义如下:

ENTRY(cpu_resume)
	bl	el2_setup		// if in EL2 drop to EL1 cleanly
	bl	__cpu_setup
	/* enable the MMU early - so we can access sleep_save_stash by va */
	bl	__enable_mmu
	ldr	x8, =_cpu_resume
	br	x8
ENDPROC(cpu_resume)

el2_setup:如果 CPU 是在 EL2 下挂起的,那么在恢复之前需要执行一些特定的设置,

__cpu_setup:执行 CPU 恢复前的一些设置。

__enable_mmu:在 CPU 恢复之前启用 MMU 确保代码能够正确的通过虚拟地址访问物理地址。

_cpu_resume:CPU 恢复操作的函数入口点。

br x8 :使用 br指令跳转到 x8 寄存器中保存的地址,即 _cpu_resume,这时CPU 将开始执行实际的恢复操作。

这个函数主要是对系统进行一些必要的初始化设置,然后会跳转到_cpu_resume,进入到该函数,如下:

ENTRY(_cpu_resume)
	mrs	x1, mpidr_el1
	adr_l	x8, mpidr_hash		// x8 = struct mpidr_hash virt address

	/* retrieve mpidr_hash members to compute the hash */
	ldr	x2, [x8, #MPIDR_HASH_MASK]
	ldp	w3, w4, [x8, #MPIDR_HASH_SHIFTS]
	ldp	w5, w6, [x8, #(MPIDR_HASH_SHIFTS + 8)]
	compute_mpidr_hash x7, x3, x4, x5, x6, x1, x2

	/* x7 contains hash index, let's use it to grab context pointer */
	ldr_l	x0, sleep_save_stash
	ldr	x0, [x0, x7, lsl #3]
	add	x29, x0, #SLEEP_STACK_DATA_CALLEE_REGS
	add	x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS
	/* load sp from context */
	ldr	x2, [x0, #CPU_CTX_SP]
	mov	sp, x2
	/*
	 * cpu_do_resume expects x0 to contain context address pointer
	 */
	bl	cpu_do_resume

#ifdef CONFIG_KASAN
	mov	x0, sp
	bl	kasan_unpoison_task_stack_below
#endif

	ldp	x19, x20, [x29, #16]
	ldp	x21, x22, [x29, #32]
	ldp	x23, x24, [x29, #48]
	ldp	x25, x26, [x29, #64]
	ldp	x27, x28, [x29, #80]
	ldp	x29, lr, [x29]
	mov	x0, #0
	ret
ENDPROC(_cpu_resume)

该函数主要是将上面__cpu_suspend_enter函数中保存的信息恢复到相关的寄存器里面,类似于中断中的恢复现场,因为前面保存寄存器是在执行__cpu_suspend_enter(&state) 这个语句开始保存的,所以恢复这些寄存器之后会再次回到if语句中再判断一次,因为resume返回了0,所以就会执行else语句,从而调用到 __cpu_suspend_exit() 函数,完成系统的resume。

对于arm架构的resume流程本人还没有去看过,所以这里就不再介绍arm架构的resume流程,如果大家感兴趣可以去阅读 linux kernel 代码进行了解。

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值