sleep:电源睡眠排查

目标与现状

一条 sleep 命令回答四件事:谁能唤醒本机、怎么关掉某个唤醒源、最近几轮是怎么睡怎么醒的、醒来后外设/屏幕/桌面不正常怎么就地恢复。不接管睡眠时机——合盖仍由 logind 决定,命令只做事后消费与查询。

实现形态:bashrc 里一个同名函数转调家目录下的 sleep.sh;非子命令参数原样交给真正的 sleep,所以 sleep 5、sleep 0.5 行为不变(实测 sleep 0.2 = 0.204s)。脚本由 nix 声明式落到家目录,源文件在配置仓库 modules/system/service/bashrc_script/sleep.sh。

子命令作用
list [-a]列唤醒源,默认只列启用的;-a 展开已关闭项(标「—」不占序号)
disable / enable按序号或名称批量开关(逗号分隔),只动有运行时开关的项,其余逐项报原因
logs [-n N] [-f] [-r]最近 N 轮睡眠时间线(六域聚合);-f 轮询刷新;-r 退回原始内核行
reset重放一次「醒后钩子」(外设/屏幕/色深/wifi/布局),与真实唤醒逐字同路
restart重启桌面壳(壁纸、任务栏、桌面图标)
freshreset + restart

前置事实(实测)

判据实测值
可切唤醒源数量55 个(扫 <code>*/power/wakeup</code> 属性,不是扫 wakeup 类目录)
盖子/电源键/睡眠键/电池 的开关位置<code>/sys/devices/platform/PNP0C0D:00/power/wakeup</code> 等 platform 路径;ACPI 设备下没有 wakeup 属性
GPIO 唤醒针脚<code>/sys/kernel/debug/gpio</code> 里 S0i3/S3/S4S5 三列全 ⏰,trigger=edge,debounce≈4.7ms;不可切
唤醒中断<code>/sys/power/pm_wakeup_irq</code> = 7 → GPIO 控制器(pinctrl-amd),不是 EC 的 GPE
journal 里的电量事件近 3 天 0 条有效事件(只有 zram/sda 的 capacity 噪声)
UPower 电池历史只留外设电池(鼠标/键盘/指点杆),系统电池无 history 文件
硬件深睡统计<code>/sys/power/suspend_stats/last_hw_sleep</code> 只保留最近一次,占比按它/该轮总时长算
终局样本单轮 81 分 17 秒,硬件深睡 4874.7s / 总 4877s = 99.95%,期间无翻饼
journal 检索成本裸扫 30 天 74 秒;每采集点加条数上限后 3.5 秒
powerManagement.*Commands 的落点当前发行版里合并进 <code>systemd.services.sleep-actions</code>:powerDownCommands = 该单元主脚本,resumeCommands = preStop

实现过程

命令挂载

bashrc 片段里定义 sleep():首参命中子命令白名单才转调 ~/sleep.sh,否则 command sleep "$@"。不覆盖内建行为的唯一理由是「查睡眠」与「等 N 秒」在同一条命令上不冲突——参数形态本身就区分得开。

sleep() {
case "$1" in
list|disable|enable|logs|reset|restart|fresh) "$HOME/sleep.sh" "$@" ;;
*) command sleep "$@" ;;
esac
}

list / disable / enable:五类唤醒源

枚举来源分五类,各自的可切性不同,序号按稳定顺序生成:设备 → ACPI GPE → GPIO 针脚 → RTC 闹钟 → systemd 定时器,同类内按名称自然序。

类别来源可切
设备find /sys/devices -path '*/power/wakeup'可(写 enabled/disabled)
ACPI GPE/sys/firmware/acpi/interrupts/gpeXX(只列计数 >0 的)可,但部分写入被内核静默忽略
GPIO 针脚/sys/kernel/debug/gpio 带 ⏰ 的行否(只有启动期内核参数)
RTC 闹钟/sys/class/rtc/rtc0/wakealarm可清空;设定需写 epoch
systemd 定时器WakeSystem=yes 的 .timer否(需改单元属性)

序号必须与状态无关:不能按唤醒计数排序。计数每次都在涨,序号跟着变,disable 3 之后再 enable 3 就打到了别人身上(实测踩过)。-a 展开的已关闭项一律显示 —、不占号,用名称操作。

名称匹配:精确 → 子串;命中多项时要求改用序号。口语别名固定几条:盖子、电源键、睡眠键、电池、EC、RTC。

logs:六域采集 → 合并排序 → 组轮

只回答电源问题,不翻全部日志。每个域一个采集点,各自按条数取(-n),合并后按时间排序,再按内核的「进入/退出睡眠」行切成「轮」:

域采集依据
睡眠/唤醒内核 PM: suspend entry/exit
触发logind Lid closed. / The system will suspend now! / Power key
外接屏Connected output N to CRTC M(合成器后端)+ gpu bpc apply:
外设内核 usb X-Y: reset + 蓝牙控制器 Controller resume with wake event
锁屏锁屏器进程的 PAM 行(按 identifier 抓,消息体里没有它自己的名字)
电量电量账本(见下一节)——journal 里没有电量事件

输出一行一轮,字段用 | 分段:时刻 → 时刻 时长: 触发 | 唤醒 | 电量 | 屏 | 外设 | 钩子 锁屏。翻饼时行尾追加「距上轮醒来 41s(翻饼)」,判据是上一轮醒来 → 下一轮入睡 < 60s。

11-10 07:57:02 → 11-10 09:32:15  1时35分:  触发 盖子合上+logind判定 | 唤醒 盖子打开+systemd记录 | 电量 无账 | 屏 连接重探 4 次 / bpc 写 1 次 | 外设 USB重枚举 2 / 蓝牙 1 | 钩子 睡前 3 行 / 醒后 7 行  锁屏器 0 行
11-10 07:26:09 → 11-10 07:26:29 0分20秒: 触发 logind判定 | 唤醒 systemd记录 | 电量 无账 | 屏 连接重探 0 次 / bpc 写 1 次 | 外设 USB重枚举 2 / 蓝牙 1 | 钩子 睡前 3 行 / 醒后 7 行 锁屏器 0 行 ← 距上轮醒来 41s(翻饼)

-f 走同一套「采集 → 组轮」,每 3 秒一轮,只在某轮状态变化时打印,所以能看到「入睡 …」→「醒来 …」的推进,而不是 raw tail。-r 退回内核原始行。

电量账本:journal 里没有,只能自己记两个点

本机没有任何电量日志源,所以「合盖一晚掉了多少电」必须自己取两个快照相减。账本挂在已有的 powerManagement.powerDownCommands / resumeCommands 上(这两个选项在 sleep-actions 单元里生效,见前置事实),睡前、醒后各追加一行:

1794366676	suspend	100	Full	1
1794366676 resume 100 Full 1
# epoch / 事件 / 容量% / 状态 / 交流在线(1=插电)

账本文件跟模块放一起(不放 /var/lib、不放 /var/log),随模块目录一起被版本库忽略,1MB 滚动保留最近约 500KB。交流侧设备名不写死——按 type == Mains 找,因为本机的交流设备名不是 AC。

sleep logs 取「入睡前最后一条 suspend + 醒来后第一条 resume」算掉电,插电时追加「·插电」标记;没有账就显示「无账」,不做推算。

reset / restart / fresh:三层递进

唤醒后出问题分两种病:钩子没跑成(外设/屏幕/色深/wifi/布局),或桌面壳自身没恢复(壁纸/任务栏/图标)。

  • reset:不复制钩子,直接调系统自己的醒后钩子脚本(与真实唤醒同一条代码路径),再起一次布局重放。最轻。
  • restart:只重启桌面壳。
  • fresh:reset + restart,两者都做。

实测证据

翻饼样本(为什么需要「轮」这个聚合层)

同一份 journal,只看内核进出两行会得到一串「20 秒一轮」的怪账;按六域组轮后能直接看出这是连续 7 轮的翻饼(每轮醒后 41 秒又睡),且每轮都跑了完整的睡前/醒后钩子:

11-10 07:25:08 → 11-10 07:25:28  0分20秒  ← 首轮
11-10 07:26:09 → 11-10 07:26:29 0分20秒 ← 距上轮醒来 41s(翻饼)
11-10 07:27:10 → 11-10 07:27:30 0分20秒 ← 距上轮醒来 41s(翻饼)
… 连续 7 轮,随后 07:57:02 → 09:32:15 恢复正常 1时35分

终局样本(当前机制生效后)

11-11 08:04:16 → 11-11 09:25:33  1时21分:  触发 盖子合上+logind判定 | 唤醒 盖子打开+systemd记录 | 电量 无账 | 屏 连接重探 4 次 / bpc 写 1 次 | 外设 USB重枚举 2 / 蓝牙 1 | 钩子 睡前 3 行 / 醒后 7 行  锁屏器 5 行
累计 success=300 fail=1;窗口内翻饼 11 次;上次唤醒中断 IRQ 7 = GPIO 控制器
最近一轮硬件深睡 4874.7s / 总 4877s = 99.95%

账本自证

手工触发一次记账,读回文件确认字段齐全、交流在线为 1;随后清空,真实数据从下一次睡眠开始记。测试行不许留在账本里——它会被下一轮当成「入睡前最后一条」,把掉电算歪。

管理命令

用途命令
看谁能唤醒sleep list(-a 连已关闭一起看)
关掉某个源sleep disable 3 / sleep disable 盖子,电源键
恢复sleep enable 3 / sleep enable 盖子
最近怎么睡的sleep logs(默认 10 轮)、sleep logs -n 3
盯着下次睡眠sleep logs -f
只想看内核原始行sleep logs -r -n 50
唤醒后外设/屏幕不正常sleep reset
唤醒后桌面壳不正常sleep restart
两者都做sleep fresh
确认当前硬件深睡占比cat /sys/power/suspend_stats/last_hw_sleep 与该轮总时长相比

命令为什么这样写:reset 直接调系统钩子脚本而不复制一份——复制会漂移,真实唤醒与手动恢复必须是同一条代码路径。

踩坑记录

条数上限的结果是「最新在前」

现象:入睡时间晚于醒来时间,时长算出负 41 秒。

成因:限条数后服务端从尾部倒着读,输出顺序是倒序,顺序配对就把「进/出」配反了。

处置:按时间戳统一重排(数值排序,稳定),不依赖工具的输出顺序。

按消息体匹配抓不到进程名

现象:按进程名抓钩子行与锁屏器行,得到「无记录」,但日志里明明有。

成因:检索只匹配消息正文,进程名是单独字段。

处置:两条出路:① 按消息内容抓(wifi-suspend-unload: / gpu bpc apply: / Connected output);② 按 identifier 抓(锁屏器只能用这条)。

裸扫时间窗慢到不可用

现象:扫 30 天日志要 74 秒。

成因:日志体量大,服务端正则过滤也不走索引。

处置:每个采集点加条数上限,从尾部倒着读、够了就停,降到 3.5 秒。

一个钩子行同时属于两个域

现象:醒后钩子报了 7 行,但「屏」域的 bpc 写次是 0。

成因:各域计数写成 if/else 链,前一分支先命中就把后面的吃掉了。

处置:各域计数改成互不排斥的独立判断。

按条数采集会让最老那一轮缺尾巴

现象:展示的第一轮,钩子行数和屏事件数都是 0,而它后面的轮次正常。

成因:每域条数是硬截断,最早那一轮的尾部(醒后钩子)刚好被切在窗口外。

处置:多取 2 轮、只展示最后 N 轮,用垫进来的轮次兜住截断。

关掉唤醒后设备从列表里消失

现象:关掉某个指点杆键盘的唤醒能力后,它再也无法用序号打开。

成因:内核在禁用时会把这个设备从 wakeup 类目录里摘掉,枚举源就找不到它。

处置:枚举改扫每台设备的 power/wakeup 属性文件——它一直存在。

GPE 写入被静默忽略

现象:写 disable 返回 0,读回仍是 enabled。

成因:部分 GPE 不是运行时 GPE,内核不接受改动也不报错。

处置:写完必须回读校验,状态没变就报「未生效」,不当成功。

提权时命令找不到

现象:写 sysfs 时报找不到 tee / sh。

成因:提权通道传下去的环境变量极窄,PATH 里没有常用工具。

处置:脚本内部用绝对路径调工具,不留「依赖 PATH」的写法。

不做的事(用户裁定)

以下不是「暂未实现」,是明确冻结,后续不要再提议:

  • 不接管睡眠时机:合盖/超时仍由 logind 决定,本命令不做「n 秒后强制睡眠」这类动作,也不对已有触发机制做任何干涉。
  • 不加守护进程、不加定时器:查询是现算的,没有常驻采样。
  • 不落状态文件:每次按配置与 sysfs 现算;唯一的持久化是电量账本两行。
  • 电量只记睡前/醒后两个点:不做周期采样曲线。
  • 不做「监测后自动调整」类的自适应机制:那类机制把工作机当成实验场,高风险低收益。
  • 运行时产物不落 /var/lib、/var/log:nix 产生的配置与日志跟模块放一起。

边界

  • 「写了 disabled 是否真的不唤醒」只能靠一次真实睡眠验证:sysfs 写入成功 ≠ 硬件唤醒被切断(存在不经过设备的通知路径)。本档只保证状态可写、可读回。
  • GPIO 针脚与 WakeSystem=yes 的定时器不可切:前者只有启动期内核参数,后者需改单元属性,不在本命令职责内。
  • 电量只对「有账」的轮次成立:账本上线前的历史轮次永远显示「无账」,不去 journal 里凑近似值。
  • 不覆盖显示链路恢复:色深/布局/MST 的恢复属于另外两条机制,本命令只负责在需要时重放它们。
  • sleep logs 是电源六域视图,不是全日志检索:要查非电源类日志直接走日志工具。
  • 手工验收项:真实合盖/开盖一轮后核对「触发、唤醒、电量、屏、外设」五项与肉眼一致。