目标与现状
一条 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 | 重启桌面壳(壁纸、任务栏、桌面图标) |
fresh | reset + 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是电源六域视图,不是全日志检索:要查非电源类日志直接走日志工具。- 手工验收项:真实合盖/开盖一轮后核对「触发、唤醒、电量、屏、外设」五项与肉眼一致。