休眠与唤醒

本页目录

无法睡下、刚睡就醒、恢复后设备失效,是不同阶段的问题。系统睡眠与设备 runtime PM 又是不同状态机,应按平台能力、阶段依赖和唤醒协议定位。

查阅目标阅读位置
支持模式与设备阶段前两节
镜像恢复与按需挂起hibernate、runtime PM
事件竞争与诊断唤醒、失败阶段

先看当前平台支持什么

cat /sys/power/state
cat /sys/power/mem_sleep

state 列出可请求状态,例如 freeze、mem、disk;mem_sleep 列出 mem 对应的可选模式,方括号标明当前选中项。例如 s2idle [deep] 表示 mem 当前选择 deep,不代表每台机器都支持 deep。系统睡眠状态

方式恢复依赖的状态主要区别
suspend-to-idle(s2idle、freeze)RAM 中的现有状态软件协调冻结、设备挂起与 CPU 空闲,不要求传统 S3
suspend-to-RAM(常见 deep)RAM 保留状态平台进入更深系统睡眠;ACPI 平台通常对应 S3
hibernation(disk)持久存储中的内存镜像下次启动读回镜像,恢复旧内核执行状态

还有平台可选的 shallow 等状态。功耗与恢复时间由设备、固件、镜像规模等共同决定,不应把 1W、100ms 或 10 秒写成接口保证。s2idle 也不等于“只能浅睡”:合适的平台可通过它达到较深的整机低功耗状态。

挂起为什么要分阶段

系统挂起:建立静止点,再恢复运行 准备与冻结 协调用户空间、内核线程与唤醒事件 1 设备挂起各阶段 按依赖处理 suspend、late、noirq 等回调 2 进入选定系统睡眠状态 s2idle 与 deep 的平台进入方式不同 3 恢复设备与解冻 唤醒后恢复依赖、处理失败并让任务继续 4 示意不等于全部源码调用栈;设备顺序、平台步骤与失败回退都需要考虑。

系统需要阻止新的工作不断进入设备,同时保留完成收尾所需的设施。设备通常要先停止新 I/O、保存状态,再进入较晚阶段;恢复时先重建依赖,再让上层重新发起工作。

设备 PM 回调包括 prepare/complete、suspend/resume、late/early、noirq 等阶段。noirq 不是“驱动终于可以随意关所有中断”的同义词,而是 PM core 已对设备中断处理做协调的阶段。异步挂起和 device links 等还会影响依赖执行,不能仅按设备名称顺序推断。

冻结用户任务并不要求它们释放所有用户态互斥锁,也不会把任意业务事务变成一致性快照。对内核锁与可冻结线程,应避免形成“已冻结的一方持有恢复或挂起所需资源”的等待环。设备 PM

hibernate:保存镜像与恢复旧执行现场

休眠需要创建可恢复的内存镜像并持久写出。它不是简单逐字节复制“全部 RAM”:空闲页、不需保存的页和回收结果等影响镜像规模,镜像还可能压缩。也不能统一说所有 page cache 都无条件跳过。

为写出镜像,设备可能经历冻结、解冻再进入最终电源状态等不同路径。驱动的 freeze/thaw、poweroff/restore 回调因此有独立语义,不能把它们全部视为 suspend/resume 的别名。

恢复通常由新启动的内核或 initramfs 发现并加载镜像,再切回保存的内核状态。resume 设备、swap 文件偏移、加密设备解锁及启动阶段支持必须配合;不是加一个 resume= 就必定成功。也不能在镜像存在时从另一套运行环境改写相关文件系统,然后指望恢复后的旧缓存仍一致。swsusp

镜像包含内存中的敏感状态,存储和恢复链应遵循系统已有的加密与可信启动设计。掉电后是否还有整机待机功耗,又取决于硬件,不由 hibernate 一词保证绝对零瓦。

runtime PM:使用计数、依赖与驱动状态

runtime PM 在系统运行期间协调某个设备的活动和挂起。使用计数是防止设备在仍被需要时挂起的协议之一,还涉及子设备、依赖、驱动允许状态和未完成请求;autosuspend_delay_ms 只是其中一个条件。

驱动需要访问设备前,应使用适当的同步获取与恢复接口,例如 pm_runtime_resume_and_get,并检查失败;结束使用后再按协议标记忙碌时间和释放使用引用。不能忽略恢复失败后继续访问寄存器。runtime PM

power/control 的 auto 允许 runtime PM,on 通常禁止 runtime 挂起;这不等于系统睡眠是否允许。PCI D3hot 与 D3cold 也有不同的供电和配置访问条件,不能把 D3hot 简写成“设备完全断电但链路保持”。

使用引用覆盖哪一段工作

一次同步设备访问的顺序是:成功获取运行时引用并恢复设备,访问硬件,最后释放这次引用。恢复失败时没有获得可用设备,既不能继续 I/O,也不能把失败当成成功获取后再盲目释放。pm_runtime_resume_and_get() 与 pm_runtime_get_sync() 的错误路径计数语义不同,后者出错不自动撤销已增加的使用计数。runtime PM helper 语义

路径是否访问硬件当前调用应释放的使用引用
resume-and-get 失败否0
获取成功,I/O 成功是1
获取成功,I/O 失败已尝试1
获取成功,异步 I/O 已提交但未完成设备仍可能工作引用应由完成/取消路径按所有权协议释放

下面的模型只检查引用配对,恢复失败用返回值表示,传输失败用异常表示。PM 计数归零不直接执行 suspend,因此也不会错误地暗示“put 返回时设备已经断电”。

class Usage:
    def __init__(self, resume_ok=True):
        self.resume_ok = resume_ok
        self.count = 0
        self.accesses = 0

    def resume_and_get(self):
        if not self.resume_ok:
            return False
        self.count += 1
        return True

    def put(self):
        assert self.count > 0
        self.count -= 1

    def io(self, fail=False):
        assert self.count > 0
        self.accesses += 1
        if fail:
            raise OSError("transfer failed")
        return "ok"

def transfer(pm, fail=False):
    if not pm.resume_and_get():
        return "resume failed"
    try:
        return pm.io(fail)
    finally:
        pm.put()

pm = Usage(False)
assert transfer(pm) == "resume failed" and (pm.count, pm.accesses) == (0, 0)
pm = Usage()
assert transfer(pm) == "ok" and (pm.count, pm.accesses) == (0, 1)
try:
    transfer(pm, fail=True)
except OSError:
    pass
else:
    raise AssertionError("应传播传输错误")
assert (pm.count, pm.accesses) == (0, 2)
assert pm.resume_and_get() and pm.resume_and_get()
pm.put()
assert pm.count == 1  # 一个调用结束,不会吃掉另一个调用的引用
pm.put()
assert pm.count == 0

Linux 7.2 的 pm_runtime_put_autosuspend() 自身会更新 last_busy 再释放引用;带双下划线的 __pm_runtime_put_autosuspend() 不负责这次更新时间。阅读旧驱动中“mark_last_busy + put”的组合时,要按目标版本的 include/linux/pm_runtime.h 核对。autosuspend 还受延迟、活动子设备、PM QoS、驱动状态与待处理请求等条件约束。

若传输采用 DMA,函数返回“提交成功”时硬件可能还在访问内存。引用生命周期应延长到完成或已确认停止的取消路径,并保证两条路径只释放一次;仅用提交函数的 finally 就释放引用,会把这个同步示例错误地套用到异步设备上。

释放子设备使用引用后,先尝试挂起父设备,再推进子设备挂起。使用计数为零、等待期已过、子设备已安静和驱动允许,是需要分别满足的条件。

正在呈现知识画面
使用计数归零,设备就已经睡了吗?

put 到零只移除一种阻止挂起的条件,不保证设备已经 suspend。恢复子设备需要其父依赖先可用;活跃子设备又能阻止父设备挂起。

唤醒:能力、策略和实际事件分开

条件要确认什么
设备能力是否支持产生唤醒事件
策略许可power/wakeup 等是否允许
实际传播事件能否经过父设备和平台到达 CPU

启用一个属性不能替代硬件接线、固件和整条依赖路径的条件。

睡眠准备期间新事件可能要求中止挂起。用户空间与内核可以通过 wakeup_count 等协议处理“检查完没有事件,到真正睡眠前又来事件”的竞态。pm_wakeup_irq 提供有限的 IRQ 线索,不是所有唤醒来源的完整账本。

wakeup_count 如何关闭“检查完再睡”的空窗

一次协调尝试可以理解为:先读取没有活动唤醒事件时的计数 n,完成用户空间准备,再把 n 写回;只有计数仍相同且没有正在处理的事件,写回才成功。写回成功还会启用后续检查,新事件可以使本次挂起中止。读取也可能等待正在处理的事件结束,它不是始终立即返回的普通统计读。

新事件出现的位置为什么不能沿用原来的睡眠判断
读取后、写回前已处理结束计数从 n 变成 n+1,写回 n 失败
写回时仍在处理即使完成计数还没增长,活动事件也使写回失败
写回成功后、挂起检查前检查发现新计数或活动事件,中止本次转换
一次睡眠尝试需要前后两道事件检查 读计数 n 等待活动事件结束 写回 n 比较计数与活动状态 挂起期间检查 变化则中止本次转换 事件窗口 ① 事件窗口 ② ① 导致写回失败;② 导致后续挂起检查发现 pending 示意协调协议;真正睡眠入口及已进入睡眠后的事件还依赖平台唤醒机制。

下面模拟单次尝试中的原子检查点。finished 表示已结束的模型唤醒活动数量,active 表示正在处理的数量;它们不是原始中断计数。真实内核还包含锁、计数打包、阻塞等待与额外的中止来源;这里的 pending 只比较状态,不复刻内核发现事件后清除检查使能位的副作用。

class WakeupGate:
    def __init__(self):
        self.finished = 0
        self.active = 0
        self.saved = None

    def begin(self):
        self.active += 1

    def end(self):
        assert self.active > 0
        self.active -= 1
        self.finished += 1

    def read(self):
        if self.active:
            raise BlockingIOError("真实阻塞读取需要等待活动结束")
        return self.finished

    def save(self, observed):
        self.saved = None
        if self.active or observed != self.finished:
            return False
        self.saved = observed
        return True

    def pending(self):
        assert self.saved is not None
        return self.active > 0 or self.finished != self.saved

# 完成在读写之间:计数变化能被识别。
gate = WakeupGate()
n = gate.read()
gate.begin(); gate.end()
assert not gate.save(n)

# 仍在处理:不能只比较已完成计数。
gate = WakeupGate()
n = gate.read()
gate.begin()
assert gate.finished == n and not gate.save(n)
try:
    gate.read()
except BlockingIOError:
    pass
else:
    raise AssertionError("活动事件期间不能给出本模型的稳定读取")

# 写回之后:活动期间和处理结束后都能发现变化。
gate = WakeupGate()
assert gate.save(gate.read()) and not gate.pending()
gate.begin()
assert gate.pending()
gate.end()
assert gate.pending()
# 处理完新事件,重新开始一次协调尝试。
assert gate.save(gate.read()) and not gate.pending()

检查通过只说明这个检查点满足条件,不意味着此后不会再来中断。后续 PM 检查和平台唤醒机制还要处理靠近真正睡眠入口、甚至进入睡眠后的事件;能力、策略和传播路径仍必须成立。因而不能把模型里的 not pending() 写成“无条件关闭唤醒并睡下”。接口契约可在内核 Documentation/ABI/testing/sysfs-power 的 wakeup_count 条目查到,实现位于 drivers/base/power/wakeup.c。

按失败阶段定位

先记录实际 mem_sleep,再定位阶段:

现象排查起点
无法进入冻结、设备回调和依赖是否完成
刚进入就醒来实际唤醒事件与准备期间竞态
无法唤醒能力、策略及平台传播路径
恢复后单个设备失效对应恢复回调及资源重建

最后一条日志提到的设备,不一定就是根因。

journalctl -k -b
cat /sys/power/pm_wakeup_irq

属性是否存在和日志权限依赖环境。可结合 power:suspend_resume 等 tracepoint 观察阶段耗时;需要启用跟踪时应使用机器实际挂载的 tracefs 路径,而不是把 trace_pipe 当作任意当前目录中的文件。

支持 CONFIG_PM_DEBUG 的系统可通过 pm_test 分层测试 freezer、devices 等阶段,缩小平台真正进入睡眠前的故障范围。测试会实际冻结任务或挂起设备,应在有恢复手段的测试环境执行;本页列出的读取命令本身不会触发睡眠。调试文档

pm_trace 使用 RTC 存储追踪指纹,有影响时钟的副作用,不宜作为无说明的第一条调试命令。驱动错误应结合回调返回值、依赖关系与重复测试确认,不能仅凭一个 hash 匹配就认定根因。