Linux 内核 System Suspend 代码流程深度解析suspend-to-idle 与平台相关挂起/恢复的完整流转【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文以 Linux 内核官方文档 Documentation/admin-guide/pm/suspend-flows.rst 为核心骨架结合本仓库中kernel/power/、drivers/base/power/的源码实现系统梳理系统级挂起system suspend与恢复system resume的内核代码流程。你将掌握suspend-to-idleS2Idle与 suspend-to-RAMS2RAM/ standby 两类流程的每一步做什么、内核在哪里执行这些步骤、平台驱动如何介入以及如何利用 sysfs 接口与调试工具观察和验证整个流转过程。背景系统挂起与恢复的基本概念Linux 内核支持的睡眠状态sleep states中除休眠hibernation需要多次状态转换外其余睡眠状态通常只需一次转换即可从工作状态进入目标睡眠状态这类状态统称为系统挂起system-wide suspend。相应地从工作状态进入睡眠状态的转换过程被称为system suspend从睡眠状态回到工作状态的转换被称为system resume参见 sleep-states.rst。在 kernel/power/suspend.c 中这些状态以suspend_state_t枚举表示并通过pm_labels[]映射为面向用户的字符串static const char * const pm_labels[] { [PM_SUSPEND_TO_IDLE] freeze, [PM_SUSPEND_STANDBY] standby, [PM_SUSPEND_MEM] mem, };用户空间写入/sys/power/state时state_store()kernel/power/main.c#L799通过decode_state()解析字符串最终调用pm_suspend(state)kernel/power/suspend.c#L636进入enter_state()主流程。suspend-flows.rst将挂起/恢复流程划分为两大主线suspend-to-idleS2Idle纯软件实现、无需平台支持的轻量挂起平台相关挂起状态platform-dependent包括 suspend-to-RAMS2RAM与 standby二者必须由平台驱动platform_suspend_ops提供挂钩支持挂起/恢复流程基本相同仅在平台特定动作上存在差异因此文档将二者合并讨论。一、Suspend-to-idle 挂起代码流程从工作状态进入 S2Idle 共经历 4 个步骤。在源码中这些步骤分别由enter_state()kernel/power/suspend.c#L576、suspend_prepare()kernel/power/suspend.c#L372与suspend_devices_and_enter()kernel/power/suspend.c#L504承载。1. 调用系统级挂起通知器system-wide suspend notifiers内核子系统可以通过register_pm_notifier()kernel/power/main.c#L156注册回调在挂起即将发生时与恢复完成之后被调用用于为系统状态变化做准备或做清理工作。通知类型定义在 include/linux/suspend.h#L436#define PM_HIBERNATION_PREPARE 0x0001 /* Going to hibernate */ #define PM_POST_HIBERNATION 0x0002 /* Hibernation finished */ #define PM_SUSPEND_PREPARE 0x0003 /* Going to suspend the system */ #define PM_POST_SUSPEND 0x0004 /* Suspend finished */ #define PM_RESTORE_PREPARE 0x0005 /* Going to restore a saved image */ #define PM_POST_RESTORE 0x0006 /* Restore failed */enter_state()调用suspend_prepare()时会先执行pm_notifier_call_chain_robust(PM_SUSPEND_PREPARE, PM_POST_SUSPEND)kernel/power/suspend.c#L381该接口实现在 kernel/power/main.c#L168通过blocking_notifier_call_chain_robust()保证若某个回调返回错误会反向调用已执行回调的PM_POST_SUSPEND通知以完成回滚。恢复流程结束时suspend_finish()kernel/power/suspend.c#L560再调用pm_notifier_call_chain(PM_POST_SUSPEND)。这也解释了文档所述“同一组回调会被再次调用但通知类型参数不同”。2. 冻结任务freezing tasks冻结任务的核心目的是防止用户空间通过直接映射到进程的 MMIO 区域或 I/O 寄存器进行不受控的硬件访问防止在后续挂起步骤进行期间用户空间进入内核。内核将所有用户空间任务像收到信号一样截获intercept使其进入不可中断睡眠直到恢复流程结束才被唤醒。内核线程中明确选择参与冻结的freezable kernel threads随后也被冻结但方式不同——它们不会被截获而是周期性自检是否需要冻结必要时自行进入不可中断睡眠。文档同时指出内核线程更推荐使用内核空间的锁与并发控制机制比冻结更精确来与系统挂起/恢复同步冻结并非内核线程的首选方案。源码实现位于 kernel/power/process.cfreeze_processes()L121通过try_to_freeze_tasks(true)让所有用户空间进程进入“冰箱”refrigerator并在此期间禁用 usermode helper 与 OOM killerfreeze_kernel_threads()L165冻结可冻结的内核线程thaw_processes()L179恢复阶段解除冻结。挂起路径中这两个函数被封装在kernel/power/power.h的suspend_freeze_processes()内联函数中power.h#L277由suspend_prepare()调用suspend.c#L387。若冻结失败代码会通过dpm_save_failed_step(SUSPEND_FREEZE)记录失败点并走filesystems_thaw()与PM_POST_SUSPEND通知的回滚路径。3. 挂起设备并重配置 IRQ设备挂起分为四个阶段prepare、suspend、late suspend、noirq suspend各阶段细节见 Documentation/driver-api/pm/devices.rst#L316。每个设备在每个阶段都会被访问但通常只在其中两个阶段被物理访问。源码中这四个阶段对应dpm_suspend_start()drivers/base/power/main.c#L2349、dpm_suspend_late()L1793与dpm_suspend_noirq()L1662等函数它们在suspend_devices_and_enter()suspend.c#L523与suspend_enter()suspend.c#L427中被依次调用error dpm_suspend_start(PMSG_SUSPEND); // prepare suspend ... error dpm_suspend_late(PMSG_SUSPEND); // late suspend ... error dpm_suspend_noirq(PMSG_SUSPEND); // noirq suspend关键行为late suspend 阶段对每个设备禁用 runtime PM APInoirq suspend 阶段之前禁止高层“action”中断处理程序被调用之后中断仍会被处理但只向中断控制器应答acknowledge不执行任何设备特定动作——这些动作被推迟到后续恢复流程与系统唤醒wakeup设备关联的 IRQ 被“武装”armed当其中一个 IRQ 发出事件时系统恢复流程被启动。4. 冻结调度器 tick 并挂起时间keeping当所有设备挂起完成后CPU 进入 idle 循环并进入可用的最深 idle 状态。每个 CPU 在进入 idle 时会“冻结”自己的调度器 tick使与 tick 关联的定时器事件不再触发直到 CPU 被其他中断源唤醒。最后一个进入 idle 状态的 CPU 还会停止 timekeeping——这会阻止高精度定时器hrtimer继续向前触发直到第一个被唤醒的 CPU 重启 timekeeping。这使 CPU 能够一次在深度 idle 状态停留较长时间。此后 CPU 只能被非定时器硬件中断唤醒。若唤醒中断来自已武装的系统唤醒 IRQ则启动系统恢复流程。这一过程在源码中体现为 suspend.c#L133 的s2idle_loop()与 suspend.c#L91 的s2idle_enter()static void s2idle_enter(void) { raw_spin_lock_irq(s2idle_lock); if (pm_wakeup_pending()) goto out; s2idle_state S2IDLE_STATE_ENTER; raw_spin_unlock_irq(s2idle_lock); /* Push all the CPUs into the idle loop. */ wake_up_all_idle_cpus(); /* Make the current CPU wait so it can enter the idle loop too. */ swait_event_exclusive(s2idle_wait_head, s2idle_state S2IDLE_STATE_WAKE); ... }s2idle_loop()的注释精炼地概括了 S2Idle 的本质冻结的进程 挂起的设备 空闲的处理器。循环内先检查pm_wakeup_pending()或平台s2idle_ops-wake()是否触发唤醒否则反复调用s2idle_enter()让所有 CPU 进入 idle。二、Suspend-to-idle 恢复代码流程从 S2Idle 回到工作状态共 4 个步骤与挂起流程严格镜像。1. 恢复 timekeeping 并解冻调度器 tick当某个 CPU 被非定时器硬件中断唤醒时它退出挂起流程最后一步进入的 idle 状态重启 timekeeping若此前未被更早唤醒的其他 CPU 重启过并解冻该 CPU 的调度器 tick。若唤醒该 CPU 的中断是已为系统唤醒武装的 IRQ则系统恢复流程正式开始。2. 恢复设备并恢复 IRQ 的工作状态配置设备按四个阶段恢复noirq resume、early resume、resume、complete见 Documentation/driver-api/pm/devices.rst#L428。每个设备在每阶段都会被访问但通常只被物理访问两次。源码对应 drivers/base/power/main.c 中的dpm_resume_noirq()L951、dpm_resume_early()L1047、dpm_resume()L1226与dpm_resume_end()L1371。在 suspend.c#L485-L497 的suspend_enter()退出路径中它们以platform_resume_noirq → dpm_resume_noirq → dpm_resume_early → platform_resume_finish的顺序执行。两个关键时间点noirq resume 阶段之后恢复 IRQ 的工作状态配置——dpm_resume_noirq()内部依次执行resume_device_irqs()与device_wakeup_disarm_wake_irqs()main.c#L955-L956early resume 阶段为每个驱动程序支持 runtime PM 的设备重新启用 runtime PM API。3. 解冻任务thawing tasks挂起流程第 2 步中被冻结的任务被“解冻”即从不可中断睡眠中唤醒用户空间任务被允许退出内核。对应suspend_finish()中的suspend_thaw_processes()suspend.c#L562底层实现即thaw_processes()。4. 调用系统级恢复通知器与挂起流程第 1 步对称调用同一组回调但传入的“通知类型”参数不同恢复阶段使用PM_POST_SUSPEND见上文suspend_finish()中的pm_notifier_call_chain(PM_POST_SUSPEND)。三、平台相关挂起代码流程S2RAM / Standby进入平台相关挂起状态共 6 个步骤。前两步与 S2Idle 完全相同。1. 调用系统级挂起通知器与 S2Idle 挂起流程第 1 步相同。2. 冻结任务与 S2Idle 挂起流程第 2 步相同。3. 挂起设备并重配置 IRQ与 S2Idle 的对应步骤类似但存在一个重要差异为系统唤醒而武装 IRQ 的操作在平台上通常不生效。文档指出两种平台情形有些平台在所有 CPU 进入足够深的 idle 状态、且所有 I/O 设备进入低功耗状态后自身能够进入非常深的内部低功耗状态——这类平台上 S2Idle 本身就能非常有效地降低系统功耗另一些平台则需要以平台特定方式关闭底层组件如中断控制器该动作由平台驱动提供的挂钩实现才能获得可比的功耗降低。后者通常意味着带内硬件中断无法再唤醒系统系统唤醒必须以平台特定方式完成。此时系统唤醒源的配置通常在唤醒设备被挂起时开始并由平台挂起挂钩在后续阶段收尾。4. 关闭非 boot CPU部分平台上挂起挂钩必须在单 CPU 配置下运行特别是硬件不能被与平台挂起挂钩并行执行的代码访问——这些挂钩常常会陷入平台固件以完成挂起转换。因此内核使用 CPU 热插拔CPU hotplug框架将系统中除一个boot CPU之外的所有 CPU 下线offline。被下线的 CPU 通常进入深度 idle 状态。这同时意味着所有任务从这些 CPU 上被迁移走所有 IRQ 被重新路由到唯一保持在线的 CPU。对应源码为suspend_enter()中的pm_sleep_disable_secondary_cpus()suspend.c#L453失败或测试模式下由pm_sleep_enable_secondary_cpus()恢复suspend.c#L483。只有state ! PM_SUSPEND_TO_IDLE时才会走到这一步suspend.c#L448-L453。5. 挂起核心系统组件为核心系统组件可能面临的断电做准备并挂起 timekeeping。对应suspend_enter()中的syscore_suspend()suspend.c#L462。在此之前还有arch_suspend_disable_irqs()默认实现即local_irq_disable()suspend.c#L401关闭本地中断并将system_state置为SYSTEM_SUSPENDsuspend.c#L460。6. 平台特定断电platform-specific power removal此步骤预期从除内存控制器与 RAM为保留其内容以及部分指定用于系统唤醒的设备之外的所有系统组件上移除电源。多数情况下控制权移交给平台固件由其按需完成挂起转换的收尾。对应源码为suspend_ops-enter(state)suspend.c#L468这是struct platform_suspend_opsinclude/linux/suspend.h#L121中必须实现的回调。在调用前会检查pm_wakeup_pending()若有未决唤醒事件则返回-EBUSY而非进入睡眠suspend.c#L464-L473。该调用被trace_suspend_resume(TPS(machine_suspend), ...)追踪这也是dmesg中观察machine_suspend事件的来源。四、平台相关恢复代码流程从平台相关挂起状态回到工作状态共 6 个步骤。1. 平台特定系统唤醒platform-specific system wakeup平台被来自某个指定系统唤醒设备的信号唤醒该信号不一定是带内硬件中断控制权交回内核平台固件可能需要在交回控制权之前恢复平台的工作配置。2. 恢复核心系统组件恢复核心系统组件的挂起时配置并恢复 timekeeping。对应syscore_resume()suspend.c#L474随后system_state恢复为SYSTEM_RUNNING、arch_suspend_enable_irqs()重新使能本地中断suspend.c#L477-L479。3. 重新使能非 boot CPU挂起流程第 4 步中被下线的 CPU 被重新上线并恢复它们的挂起时配置。对应pm_sleep_enable_secondary_cpus()。4. 恢复设备并恢复 IRQ 工作状态配置与 S2Idle 恢复流程第 2 步相同。5. 解冻任务与 S2Idle 恢复流程第 3 步相同。6. 调用系统级恢复通知器与 S2Idle 恢复流程第 4 步相同。五、源码级纵深两条流程在suspend_enter()中的分岔suspend_enter()kernel/power/suspend.c#L419是理解两条流程差异的最佳窗口。它在设备挂起完成后执行关键分岔点如下if (state PM_SUSPEND_TO_IDLE) { s2idle_loop(); goto Platform_wake; } error pm_sleep_disable_secondary_cpus(); ... arch_suspend_disable_irqs(); system_state SYSTEM_SUSPEND; error syscore_suspend(); ... error suspend_ops-enter(state); ...也就是说S2Idle 路径设备挂起后直接进入s2idle_loop()CPU 保持在线、中断保持使能不执行syscore_suspend()平台相关路径依次执行关闭非 boot CPU、关闭本地中断、syscore_suspend()、suspend_ops-enter()平台断电。平台挂钩platform_suspend_ops与platform_s2idle_ops平台驱动通过suspend_set_ops()suspend.c#L220注册platform_suspend_ops其中valid()决定平台支持哪些状态suspend_valid_only_mem()是仅支持内存挂起的通用实现suspend.c#L252enter()是必须实现的回调。suspend_enter()内通过platform_suspend_prepare()、platform_suspend_prepare_late()、platform_suspend_prepare_noirq()等包装函数suspend.c#L264-L329将挂钩插入设备挂起各阶段之间。S2Idle 也允许平台介入通过s2idle_set_ops()注册platform_s2idle_opsinclude/linux/suspend.h#L134提供begin/prepare/prepare_late/check/wake/restore_early/restore/end回调其中wake()与check()在s2idle_loop()中被调用suspend.c#L147-L155。设备挂起阶段的direct-complete优化Documentation/driver-api/pm/devices.rst 指出prepare阶段会检查设备是否适合direct-complete若设备在prepare时已处于 runtime suspend 状态PM core 可能跳过suspend、suspend_late、suspend_noirq及其对应的恢复阶段直接进入complete回调。这也印证了文档中“每个设备在每个阶段都会被访问但通常只被物理访问两次”的说法——direct-complete是减少物理访问的重要手段。六、调试与验证观察挂起/恢复流程的工具1. 使用/sys/power/pm_test分阶段测试内核在CONFIG_PM_SLEEP_DEBUG下提供pm_test接口将挂起流程拆分为可独立测试的级别枚举定义于 kernel/power/power.h#L255字符串映射于 kernel/power/main.c#L326写入值枚举对应流程阶段noneTEST_NONE不测试默认freezerTEST_FREEZER冻结/解冻任务devicesTEST_DEVICES设备挂起/恢复platformTEST_PLATFORM平台挂钩processorsTEST_CPUSCPU 热插拔coreTEST_CORE核心系统组件例如仅验证设备挂起阶段可以echo devices /sys/power/pm_test echo mem /sys/power/state echo none /sys/power/pm_test每个测试点会等待pm_test_delay默认 5 秒模块参数suspend.c#L338后自动恢复。注意S2Idle 不支持processors与core级别——enter_state()中会直接拒绝suspend.c#L582-L587。2. 使用wakeup_count避免唤醒事件竞争suspend-flows.rst所属的系统挂起框架在 kernel/power/main.c#L833 注释中说明了wakeup_count的用途用户空间应先在挂起前读取/sys/power/wakeup_count随后将该值写回若写回失败说明期间已发生唤醒事件不应继续写/sys/power/state。这为“写入 state 与唤醒事件之间的竞态”提供了非竞态的处理方式。3. 选择默认挂起变体mem_sleep_defaultmem_sleep_current决定写mem到/sys/power/state时实际进入的挂起变体。内核命令行参数mem_sleep_default解析于 suspend.c#L200可覆盖默认值例如mem_sleep_defaults2idle # 或 deep / shallow4. 跟踪dmesg关键日志与 tracepoint内核在流程关键点通过pm_pr_dbg()与trace_suspend_resume()输出信息常见序列包括PM: suspend entry (deep)→PM: Preparing system for sleep (deep)→PM: Suspending system (deep)→PM: suspend exitFreezing user space processes .../Restarting tasks: Doneprocess.c#L107-L108、process.c#L192-L211machine_suspend事件标识平台enter()挂钩的执行suspend.c#L466-L470借助这些输出可以精确确认系统实际走的是 S2Idle 还是平台相关路径、各阶段是否按预期执行。七、与系统睡眠状态的 sysfs 接口联动挂起流程的入口是/sys/power/state其内容由pm_states_init()初始化suspend.c#L188mem与freeze始终存在standby仅在平台通过suspend_set_ops()注册且valid()认可时出现。挂起变体选择通过/sys/power/mem_sleeps2idle/shallow/deep对应mem_sleep_labels[]suspend.c#L43完成详情见 sleep-states.rst。常见实操组合# 直接进入 suspend-to-idle echo freeze /sys/power/state # 或先选择变体再写 mem echo s2idle /sys/power/mem_sleep echo mem /sys/power/state # 进入 suspend-to-RAM需要平台支持 echo deep /sys/power/mem_sleep echo mem /sys/power/state结语从 suspend-flows.rst 的流程骨架出发结合 kernel/power/suspend.c、kernel/power/main.c、kernel/power/process.c 与 drivers/base/power/main.c 的源码可以清晰看到S2Idle 与平台相关挂起共享“通知器 → 冻结任务 → 挂起设备 → 恢复设备 → 解冻任务 → 通知器”的公共框架差异集中在设备挂起之后——S2Idle 让 CPU 留在 idle 循环中等待带内中断而平台相关挂起则通过 CPU 热插拔、syscore_suspend()与平台enter()挂钩切断底层电源。理解这条流转路径是排查挂起/恢复异常、优化唤醒延迟与开发平台电源管理驱动的基础。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
