无法睡下、刚睡就醒、恢复后设备失效,是不同阶段的问题。系统睡眠与设备 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 也不等于“只能浅睡”:合适的平台可通过它达到较深的整机低功耗状态。
挂起为什么要分阶段
系统需要阻止新的工作不断进入设备,同时保留完成收尾所需的设施。设备通常要先停止新 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。恢复子设备需要其父依赖先可用;活跃子设备又能阻止父设备挂起。
{"id":"runtime-pm-usage-and-state","title":"使用计数归零,设备就已经睡了吗?","summary":"put 到零只移除一种阻止挂起的条件,不保证设备已经 suspend。恢复子设备需要其父依赖先可用;活跃子设备又能阻止父设备挂起。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"auto\" checked><span data-i18n=\"auto\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"busy\"><span data-i18n=\"busy\"></span></label><div class=\"actions\"><button id=\"getP\" data-i18n=\"getP\"></button><button id=\"putP\" data-i18n=\"putP\"></button></div><div class=\"actions\"><button id=\"getC\" data-i18n=\"getC\"></button><button id=\"putC\" data-i18n=\"putC\"></button></div><div class=\"track-row\"><span data-i18n=\"parent\"></span><div id=\"parent\" class=\"track\"></div></div><div class=\"dependency\">↑</div><div class=\"track-row\"><span data-i18n=\"child\"></span><div id=\"child\" class=\"track\"></div></div><div class=\"actions\"><button id=\"time\" data-i18n=\"time\"></button></div><div class=\"actions\"><button id=\"tryP\" data-i18n=\"tryP\"></button><button id=\"tryC\" data-i18n=\"tryC\"></button></div><div class=\"metrics\"><div><span data-i18n=\"clock\"></span><strong id=\"clock\"></strong></div></div><div class=\"card\"><header data-i18n=\"result\"></header><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"refused\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"delayed\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"childActive\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"driverBusy\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"disabled\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"done\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"already\"></p></div></div><p class=\"caption\" data-i18n=\"note\"></p><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.dependency{text-align:center;font-size:25px;color:var(--accent);height:28px}.metrics{grid-template-columns:1fr}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let usage=[0,1],active=[true,true],last=[0,0],time=0,message='initial';function draw(){for(let i=0;i<2;i++){cells(i?'child':'parent',[{text:'usage '+usage[i],cls:usage[i]?'on':'empty'},{text:t(active[i]?'active':'suspended'),cls:active[i]?'gold':'empty'}]);$('#get'+'PC'[i]).disabled=usage[i]===3;$('#put'+'PC'[i]).disabled=usage[i]===0;}set('clock',time);set('explanation',t(message));}function attempt(i){if(!active[i])message='already';else if(!$('#auto').checked)message='disabled';else if(usage[i])message='refused';else if(i===0&&active[1])message='childActive';else if(time-last[i]<100)message='delayed';else if(i===1&&$('#busy').checked)message='driverBusy';else{active[i]=false;message='done';}draw();}for(let i=0;i<2;i++){$('#get'+'PC'[i]).onclick=()=>{usage[i]++;active[i]=true;if(i===1)active[0]=true;draw();};$('#put'+'PC'[i]).onclick=()=>{usage[i]--;if(usage[i]===0)last[i]=time;draw();};$('#try'+'PC'[i]).onclick=()=>attempt(i);}$('#time').onclick=()=>{time+=100;draw();};$('#reset').onclick=()=>{usage=[0,1];active=[true,true];last=[0,0];time=0;message='initial';$('#auto').checked=true;$('#busy').checked=false;draw();};draw();","audio":false,"strings":{"active":"活动","already":"设备已经挂起,没有重复转换。","auto":"允许 runtime 自动挂起","busy":"子设备驱动返回 EBUSY","child":"子设备","childActive":"子设备仍活动,父设备不能先挂起。","clock":"模型时间 / ms","delayed":"自动挂起延迟还未达到。","disabled":"策略未允许 runtime 自动挂起。","done":"条件满足,设备进入 runtime suspend。","driverBusy":"驱动 EBUSY,设备保持活动。","getC":"子设备 get","getP":"父设备 get","heading":"使用计数归零,设备就已经睡了吗?","initial":"还没有尝试;子设备有一份使用引用。","intro":"取得或交还父/子设备引用,推进模型时间,再尝试处理自动挂起。观察计数、延迟、活跃子设备和驱动拒绝各自怎样阻止挂起。","note":"交还子设备引用并等待 100 ms,先尝试父设备,再尝试子设备。先后结果不同,原因来自依赖条件,而不是“计数为零就已断电”。","parent":"父设备","putC":"子设备 put","putP":"父设备 put","refused":"使用计数尚未归零,不能挂起。","reset":"重置","result":"最近的挂起尝试","scope":"一个父设备与一个子设备,使用计数各最多 3,auto 对两者生效,延迟固定 100 ms。get 折叠成功的同步恢复,最后 put 更新 busy 时间。自动挂起在按钮触发时检查;驱动 EBUSY 只用于子设备。省略错误恢复、设备链接和真实工作队列,不控制宿主电源。","scopeLabel":"模型范围与约定","suspended":"已挂起","time":"经过 100 ms","tryC":"尝试子设备 autosuspend","tryP":"尝试父设备 autosuspend"}}
唤醒:能力、策略和实际事件分开
| 条件 | 要确认什么 |
| 设备能力 | 是否支持产生唤醒事件 |
| 策略许可 | power/wakeup 等是否允许 |
| 实际传播 | 事件能否经过父设备和平台到达 CPU |
启用一个属性不能替代硬件接线、固件和整条依赖路径的条件。
睡眠准备期间新事件可能要求中止挂起。用户空间与内核可以通过 wakeup_count 等协议处理“检查完没有事件,到真正睡眠前又来事件”的竞态。pm_wakeup_irq 提供有限的 IRQ 线索,不是所有唤醒来源的完整账本。
wakeup_count 如何关闭“检查完再睡”的空窗
一次协调尝试可以理解为:先读取没有活动唤醒事件时的计数 n,完成用户空间准备,再把 n 写回;只有计数仍相同且没有正在处理的事件,写回才成功。写回成功还会启用后续检查,新事件可以使本次挂起中止。读取也可能等待正在处理的事件结束,它不是始终立即返回的普通统计读。
| 新事件出现的位置 | 为什么不能沿用原来的睡眠判断 |
| 读取后、写回前已处理结束 | 计数从 n 变成 n+1,写回 n 失败 |
| 写回时仍在处理 | 即使完成计数还没增长,活动事件也使写回失败 |
| 写回成功后、挂起检查前 | 检查发现新计数或活动事件,中止本次转换 |
下面模拟单次尝试中的原子检查点。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 匹配就认定根因。