电源管理四

理论学习: Suspend/Resume 机制

  • 了解Linux 内核 suspend的调用流程

  • 使用ftrace进行suspend/resume调试

  • 解析dmesg输出,分析关键路径

Linux suspend的五个阶段

阶段

函数

作用

准备阶段

suspend_prepare()

检查系统状态,通知用户空间

设备冻结

dpm_suspend()

冻结用户态进程,调用设备驱动 suspend

架构相关

arch_suspend_disable_irqs()

关闭中断,调用 CPU suspend

进入睡眠

syscore_suspend()

关闭系统核心,进入低功耗状态

恢复阶段

dpm_resume() → suspend_finish()

重新开启设备、恢复状态

suspend_prepare() // kernel/power/suspend.c

static int suspend_prepare(suspend_state_t state)
{
    int error, nr_calls = 0;
    if (!sleep_state_supported(state))
        return -EPERM;
    pm_prepare_console();
    error = __pm_notifier_call_chain(PM_SUSPEND_PREPARE, -1, &nr_calls);
    if (error) {
        nr_calls--;
        goto Finish;
    }
    trace_suspend_resume(TPS("freeze_processes"), 0, true);
    error = suspend_freeze_processes();
    trace_suspend_resume(TPS("freeze_processes"), 0, false);
    if (!error)
        return 0;
    log_suspend_abort_reason("One or more tasks refusing to freeze");
    suspend_stats.failed_freeze++;
    dpm_save_failed_step(SUSPEND_FREEZE);
Finish:
    __pm_notifier_call_chain(PM_POST_SUSPEND, nr_calls, NULL);
    pm_restore_console();
    return error;
}

pm_prepare_console();

  • pm_prepare_console(): 这是一个函数调用,目的是为系统挂起做控制台相关的准备工作。这可能包括:

    • 保存当前的控制台设置。

    • 设置控制台进入特定的低功耗模式 (如果需要)。

    • 确保在挂起和恢复期间控制台的行为是可预测和稳定的。

  • 为什么需要准备控制台? 在系统挂起和恢复过程中,控制台的显示和行为可能会受到影响。为了确保用户在系统恢复后能够正常使用控制台,需要在挂起前进行准备,并在恢复后进行恢复(pm_restore_console() 将在后面看到)。

error = __pm_notifier_call_chain(PM_SUSPEND_PREPARE, -1, &nr_calls);

  • 作用: 调用电源管理通知链,通知注册的模块进行挂起准备。

  • 详细说明:

    • __pm_notifier_call_chain(...): 这是一个核心的电源管理框架函数,用于执行通知链 (notifier chain)。通知链是一种发布-订阅模式,允许不同的模块(例如设备驱动程序、子系统)注册对特定电源管理事件的兴趣,并在事件发生时收到通知。

    • PM_SUSPEND_PREPARE: 这是一个预定义的宏或枚举值,表示要触发的电源管理事件类型是 "挂起准备"。当这个事件被触发时,所有注册了对 PM_SUSPEND_PREPARE 事件感兴趣的通知器(notifier)都会被调用。

error = suspend_freeze_processes();

  • 作用: 冻结用户空间的所有进程。

  • 详细说明:

    • suspend_freeze_processes(): 这是一个关键函数,负责暂停所有用户空间进程的运行。 这是挂起准备过程中至关重要的一步,因为要将系统安全地挂起,必须确保用户空间进程停止活动,避免在系统状态保存期间发生数据修改或状态不一致。

    • 冻结进程通常通过发送信号 (如 SIGSTOP) 给所有用户空间进程来实现。 这些进程会被暂停执行,但其状态 (内存、寄存器等) 会被保存下来,以便在系统恢复后能够继续运行。

__pm_notifier_call_chain(PM_POST_SUSPEND, nr_calls, NULL);

  • 作用: 调用电源管理通知链,进行挂起 后处理 (post suspend) 或 错误恢复

  • 详细说明:

    • __pm_notifier_call_chain(PM_POST_SUSPEND, nr_calls, NULL);: 再次调用 __pm_notifier_call_chain,但这次触发的事件类型是 PM_POST_SUSPEND ("挂起后处理")。

    • PM_POST_SUSPEND: 表示要触发 "挂起后处理" 事件。 当发生错误导致挂起准备阶段失败,或者在挂起恢复后,需要进行一些清理或恢复操作时,会触发这个事件。

pm_restore_console();

  • 作用: 恢复控制台到挂起准备前的状态。

  • 详细说明:

    • pm_restore_console(): 与第 3 步的 pm_prepare_console() 相对应。 pm_restore_console() 函数用于 撤销 pm_prepare_console() 所做的控制台准备工作,将控制台恢复到挂起准备之前的状态。 这确保了即使挂起操作失败,控制台也能恢复到正常的工作状态。

/**
 * dpm_suspend - Execute "suspend" callbacks for all non-sysdev devices.
 * @state: PM transition of the system being carried out.
 */
int dpm_suspend(pm_message_t state)
{
	ktime_t starttime = ktime_get();
	int error = 0;
	trace_suspend_resume(TPS("dpm_suspend"), state.event, true);
	might_sleep();
	devfreq_suspend();
	cpufreq_suspend();
	mutex_lock(&dpm_list_mtx);
	pm_transition = state;
	async_error = 0;
	while (!list_empty(&dpm_prepared_list)) {
		struct device *dev = to_device(dpm_prepared_list.prev);
		get_device(dev);
		mutex_unlock(&dpm_list_mtx);
		error = device_suspend(dev);
		mutex_lock(&dpm_list_mtx);
		if (error) {
			pm_dev_err(dev, state, "", error);
			dpm_save_failed_dev(dev_name(dev));
			put_device(dev);
			break;
		}
		if (!list_empty(&dev->power.entry))
			list_move(&dev->power.entry, &dpm_suspended_list);
		put_device(dev);
		if (async_error)
			break;
	}
	mutex_unlock(&dpm_list_mtx);
	async_synchronize_full();
	if (!error)
		error = async_error;
	if (error) {
		suspend_stats.failed_suspend++;
		dpm_save_failed_step(SUSPEND_SUSPEND);
	}
	dpm_show_time(starttime, state, error, NULL);
	trace_suspend_resume(TPS("dpm_suspend"), state.event, false);
	return error;
}

devfreq_suspend(); & cpufreq_suspend();

  • devfreq_suspend(): 挂起设备频率管理(devfreq)子系统,降低设备频率。

  • cpufreq_suspend(): 挂起 CPU 频率管理(cpufreq)子系统,降低 CPU 频率。

设备挂起循环

mutex_lock(&dpm_list_mtx);
    pm_transition = state;
    async_error = 0;
    while (!list_empty(&dpm_prepared_list)) {
        struct device *dev = to_device(dpm_prepared_list.prev);
        get_device(dev);
        mutex_unlock(&dpm_list_mtx);
        error = device_suspend(dev);
        mutex_lock(&dpm_list_mtx);
        if (error) {
            pm_dev_err(dev, state, "", error);
            dpm_save_failed_dev(dev_name(dev));
            put_device(dev);
            break;
        }
        if (!list_empty(&dev->power.entry))
            list_move(&dev->power.entry, &dpm_suspended_list);
        put_device(dev);
        if (async_error)
            break;
    }
    mutex_unlock(&dpm_list_mtx);
  • mutex_lock(&dpm_list_mtx);: 获取互斥锁,保护设备列表的访问。

  • pm_transition = state;: 记录当前的电源管理状态。

  • async_error = 0;: 初始化异步错误码。

  • while (!list_empty(&dpm_prepared_list)): 循环遍历 dpm_prepared_list 列表中的所有设备。

  • struct device *dev = to_device(dpm_prepared_list.prev);: 从列表中获取下一个要挂起的设备。

  • get_device(dev);: 增加设备的引用计数。

  • mutex_unlock(&dpm_list_mtx);: 释放互斥锁,允许其他线程访问设备列表。

  • error = device_suspend(dev);: 调用设备的 device_suspend 函数,执行设备的挂起操作。

  • mutex_lock(&dpm_list_mtx);: 重新获取互斥锁。

  • if (error): 如果挂起操作失败,记录错误信息,并将设备名称保存到失败列表中。

  • if (!list_empty(&dev->power.entry)) list_move(&dev->power.entry, &dpm_suspended_list);: 如果设备成功挂起,将其从 dpm_prepared_list 移动到 dpm_suspended_list

  • put_device(dev);: 减少设备的引用计数。

  • if (async_error) break;: 如果出现异步错误,退出循环。

  • mutex_unlock(&dpm_list_mtx);: 释放互斥锁。

arch_suspend_disable_irqs() // kernel/power/suspend.c

/* default implementation */
void __weak arch_suspend_disable_irqs(void)
{
	local_irq_disable();
}
  • 这段代码定义了一个名为 arch_suspend_disable_irqs 的函数,其作用是在系统挂起过程中禁用本地 CPU 的中断。

  • __weak 是一个函数属性,表示该函数是一个弱符号。在链接过程中,如果存在同名的强符号函数定义,则会覆盖这个弱符号函数。这使得架构特定的代码可以提供自己的 arch_suspend_disable_irqs 实现,而无需修改通用代码。

  • local_irq_disable() 是一个内核函数,用于禁用本地 CPU 的中断。在 ARM 架构中,这个函数最终调用 arch_local_irq_disable(),通过汇编指令设置处理器的状态寄存器,以屏蔽中断。

syscore_suspend(); // drivers/base/syscore.c

/**
 * syscore_suspend - Execute all the registered system core suspend callbacks.
 *
 * This function is executed with one CPU on-line and disabled interrupts.
 */
int syscore_suspend(void)
{
	struct syscore_ops *ops;
	int ret = 0;
	trace_suspend_resume(TPS("syscore_suspend"), 0, true);
	pr_debug("Checking wakeup interrupts\n");
	/* Return error code if there are any wakeup interrupts pending. */
	if (pm_wakeup_pending())
		return -EBUSY;
	WARN_ONCE(!irqs_disabled(),
		"Interrupts enabled before system core suspend.\n");
	list_for_each_entry_reverse(ops, &syscore_ops_list, node)
		if (ops->suspend) {
			if (initcall_debug)
				pr_info("PM: Calling %pS\n", ops->suspend);
			ret = ops->suspend();
			if (ret)
				goto err_out;
			WARN_ONCE(!irqs_disabled(),
				"Interrupts enabled after %pS\n", ops->suspend);
		}
	trace_suspend_resume(TPS("syscore_suspend"), 0, false);
	return 0;
 err_out:
	log_suspend_abort_reason("System core suspend callback %pS failed",
		ops->suspend);
	pr_err("PM: System core suspend callback %pF failed.\n", ops->suspend);
	list_for_each_entry_continue(ops, &syscore_ops_list, node)
		if (ops->resume)
			ops->resume();
	return ret;
}
EXPORT_SYMBOL_GPL(syscore_suspend);

pm_wakeup_pending()

调用 pm_wakeup_pending() 检查是否有挂起的唤醒事件。如果存在,返回 -EBUSY 错误码,表示系统正忙于处理唤醒事件,无法进入挂起状态。

ret = ops->suspend();

使用 list_for_each_entry_reverse 宏以逆序方式遍历 syscore_ops_list 链表中的每个 syscore_ops 结构。对于每个元素:

  • 如果 suspend 回调存在:

    • 如果启用了初始化调用调试(initcall_debug),输出正在调用的挂起回调的地址。

    • 调用 ops->suspend() 执行挂起操作,并将返回值赋给 ret

    • 如果返回值不为 0,表示挂起操作失败,跳转到错误处理部分。

    • 再次使用 WARN_ONCE 检查中断状态,如果在挂起回调后中断被启用,输出警告信息。

err_out:

如果suspend某个core失败,那么回滚之前的操作,唤醒之前已经suspend的cores。

ops->resume();

继续遍历剩余的 syscore_ops,对于每个具有 resume 回调的元素,调用其 resume 回调,以恢复已挂起的系统核心组件。

dpm_resume(); // drivers/base/power/main.c

might_sleep()

  • 指示接下来的代码可能会导致睡眠,以确保在不允许睡眠的上下文中不会调用此函数。

/**
 * dpm_resume - Execute "resume" callbacks for non-sysdev devices.
 * @state: PM transition of the system being carried out.
 *
 * Execute the appropriate "resume" callback for all devices whose status
 * indicates that they are suspended.
 */
void dpm_resume(pm_message_t state)
{
	struct device *dev;
	ktime_t starttime = ktime_get();
	trace_suspend_resume(TPS("dpm_resume"), state.event, true);
	might_sleep();
	mutex_lock(&dpm_list_mtx);
	pm_transition = state;
	async_error = 0;
	list_for_each_entry(dev, &dpm_suspended_list, power.entry)
		dpm_async_fn(dev, async_resume);
	while (!list_empty(&dpm_suspended_list)) {
		dev = to_device(dpm_suspended_list.next);
		get_device(dev);
		if (!is_async(dev)) {
			int error;
			mutex_unlock(&dpm_list_mtx);
			error = device_resume(dev, state, false);
			if (error) {
				suspend_stats.failed_resume++;
				dpm_save_failed_step(SUSPEND_RESUME);
				dpm_save_failed_dev(dev_name(dev));
				pm_dev_err(dev, state, "", error);
			}
			mutex_lock(&dpm_list_mtx);
		}
		if (!list_empty(&dev->power.entry))
			list_move_tail(&dev->power.entry, &dpm_prepared_list);
		put_device(dev);
	}
	mutex_unlock(&dpm_list_mtx);
	async_synchronize_full();
	dpm_show_time(starttime, state, 0, NULL);
	cpufreq_resume();
	devfreq_resume();
	trace_suspend_resume(TPS("dpm_resume"), state.event, false);
}

你可能感兴趣的:(linux)