sleep监管的终极再加强

再次失败,nixos的睡眠太随缘了

问题现象

  • 拔电合盖后顺利睡眠,6小时后被唤醒,然后每60秒系统睡眠并系统唤醒,无限循环
  • 拔电合盖后顺利睡眠,4小时后被唤醒,然后每60秒系统睡眠并系统唤醒,无限循环
  • hermes+deepseek排查没有任何线索,我只能求助deepseek chat,它的搜索能力特别强

deepseek chat提供线索

你遇到的这个“睡20秒醒60秒”的循环,在NixOS社区里并非孤例,而且确实有迹可循。**核心问题通常是某个硬件或驱动在系统进入睡眠后,不断地发出唤醒信号(Wakeup Event),导致系统刚睡下就被立刻叫醒,然后 logind 等组件又尝试让它重新睡眠,从而形成循环。**

### 🔍 第一步:找到“元凶”

盲目修改配置效率很低,建议先通过日志精确定位唤醒源。你的场景(合盖、拔电、无扩展)和社区一个详细案例高度吻合。

1.  **复现后立即抓取日志**:让电脑复现循环后,唤醒它,然后立即执行:
    ```bash
    journalctl -b -u systemd-suspend --no-pager
    ```
    查看每次睡眠和唤醒的精确时间点。

2.  **检查 ACPI 唤醒源**:这是最关键的线索。运行:
    ```bash
    cat /proc/acpi/wakeup
    ```
    关注 `Status` 为 `*enabled` 的设备。社区案例中,**`GPE 0x0B`** 在2.5小时内触发了660次,占SCI中断的95%,是典型的“元凶”。

3.  **检查内核唤醒中断**:
    ```bash
    cat /sys/power/pm_wakeup_irq
    ```
    社区案例显示该值为 **`7` (pinctrl_amd)**,指向了AMD平台的引脚控制器。

4.  **检查 USB 设备**:你的场景是“无扩展”,但笔记本内置的USB设备(如摄像头、蓝牙、指纹识别器)仍可能捣乱。社区案例中指纹设备`usb 3-1.3`反复断开/重连了158次,是循环的驱动因素之一。

### 🛠️ 第二步:针对性解决方案

#### 方案一:禁用特定设备的唤醒能力(最推荐)
找到元凶后,用 `udev` 规则永久禁用它。例如,如果发现是某个PCI设备(如NVMe、USB控制器),可以获取其 `vendor` 和 `device` ID:
```bash
udevadm info -a /sys/bus/pci/devices/<你的设备地址> | grep -E 'ATTR{vendor}|ATTR{device}'
```
然后在 `configuration.nix` 中添加:
```nix
services.udev.extraRules = ''
  ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x你的ID", ATTR{device}=="0x你的ID", ATTR{power/wakeup}="disabled"
'';
```
如果是USB设备,可将 `SUBSYSTEM=="pci"` 改为 `SUBSYSTEM=="usb"`。

#### 方案二:禁用所有 PCIe 唤醒(简单粗暴)
如果你不需要通过网络唤醒或外接PCIe设备唤醒电脑,可以一刀切禁用所有PCIe端口的唤醒能力:
```nix
services.udev.extraRules = ''
  ACTION=="add", SUBSYSTEM=="pci", DRIVER=="pcieport", ATTR{power/wakeup}="disabled"
'';
```
这能解决大部分因NVMe或PCIe控制器引起的异常唤醒。

#### 方案三:调整 NVIDIA 电源管理(如果有NVIDIA显卡)
如果你的笔记本有NVIDIA独显,其电源管理功能是已知的“睡眠循环”常见诱因。在 `configuration.nix` 中尝试:
```nix
hardware.nvidia.powerManagement.enable = false;
```
社区中多位用户确认这能解决 `suspend/resume cycles`。

#### 方案四:检查 logind 与合盖配置
确认合盖行为未被意外覆盖。检查 `configuration.nix` 中是否包含:
```nix
services.logind.extraConfig = ''
  HandleLidSwitch=suspend
  HandleLidSwitchExternalPower=suspend # 拔电时也睡眠
'';
```
同时确保没有设置 `HandleLidSwitch=ignore` 导致系统行为异常。

#### 方案五:排查 `suspend-then-hibernate` 陷阱
如果你配置了“睡眠一段时间后休眠”,但**交换分区(swap)未正确配置**(例如使用了加密swap但未正确设置),系统会在 `HibernateDelaySec` 超时后尝试休眠失败,然后直接唤醒,造成循环。检查 `/etc/systemd/sleep.conf` 或 NixOS 选项 `systemd.sleep.extraConfig`,看是否设置了不切实际的超时。

### 💡 排查建议
建议先从**方案三**(NVIDIA)和**方案五**(sleep配置)查起,因为它们配置简单且常见。如果无效,再通过第一步的日志锁定具体设备,应用**方案一**进行精准打击。修改配置后记得 `sudo nixos-rebuild switch` 并**完全重启**一次,以确保 `udev` 规则和内核参数生效。

hermes被提醒后的结论

A 睡眠成功(正常)
23:15:48→06:16:37 真睡 7 小时:监管日志 7 小时零 monitor 行,电量 100%→95%(0.7%/h)。真睡签名=墙钟 25249.0s/内核单调 1.4s。06:16:37 那批 Freezing 行是 monotonic 跨睡眠的假时间戳,不是又睡了一次。

B 唤醒(不正常)
唤醒记账=ACPI 电源键设备 PNP0C0C(\_SB_.PWRB):active/event 298、wakeup 286,与全机 suspend 298 次 1:1,醒着时零增长。盖子 PNP0C0D 仅 5 次、xhci/WiFi wakeup 全 disabled、指纹无 wakeup 节点;irq7=pinctrl_amd、GPE 0x0B 唯一活跃。
【更正1】「禁 PWRB wakeup 不可行」→ 错:sysfs 确有 /sys/devices/platform/PNP0C0C:00/power/wakeup。
【更正2】「内置键盘本来就叫不醒」→ 错:i8042 wakeup=disabled 是内核默认,只表示内核没把 PS/2 键盘登记为独立唤醒源。
【更正3·DSDT 实证】PWRB 根本不是唤醒源,见下节。

C 翻饼(不正常)
从未睡。一轮按内核单调:entry→sync 0.007s→1.48s→freeze 0.002s→EC blocked→unblocked 6ms→exit,全程 1.47s;墙钟却显示 19.7s。差的 18.2s 是墙钟跳变——80 次 Clock change detected、74 轮共跳 1535s≈25.6 分钟(事后已校正回准)。流产=进 s2idle 6ms 内被待处理唤醒打回。真实周期 1.5s 尝试+41.4s 等待=43s。本机 mem_sleep 只有 [s2idle],无 S3 可退。三次循环终止时刻全部是开盖(11-06 09:31:45 / 11-09 08:13:44 / 11-10 07:31:18)。

D 日志失真(已修,世代 powermanager-duration-verdict)
wake 行改为带时长与判定:真睡 `(墙钟25249.0s 单调1.4s 判定=真睡)`,流产 `(墙钟19.7s 单调1.5s 判定=流产未睡)`;阈值 60s。睡眠行 mode/电量改取上轮活值。

DSDT 反编译:唤醒链路的固件真相

取证:sudo cp /sys/firmware/acpi/tables/DSDT . && iasl -d DSDT.aml → 312,042 字节 ASL

1. PWRB 不是唤醒源,是固件伪造的通知接收者
   Device (PWRB):只有 _HID(PNP0C0C)/_UID(0xAA)/_STA,**没有 _PRW**
   全 DSDT 只有 2 处 GPRW:GPP0._PRW → GPRW(0x08,0x04);LID0.RHRW → GPRW(0x0E,0x03)(RHRW 无人调用)
   Method (_WAK,1) → SWAK(Arg0) → If(Arg0==3||4) Notify(\_SB.PWRB, 0x02) // Device Wake
   ⇒ 286 次 PWRB 记账 = 每次 resume 的固定产物,不是 286 次按键

2. 真入口是 EC 的 SCI(Scope(_GPE) 内无 _Lxx/_Exx → GPE 由内核 EC 驱动接管)
   EC0(PNP0C09) 事件表:_Q0A 电源插拔、_Q0B 电池、_Q0C/_Q0D 盖子、_Q54 电源键、_Q58 温度、_Q74-76 显示
   每个 _Qxx 往 P80H 写特征码(0xEC0A/0xEC0B/0xEC0C/0xEC0D/0xEC54)

3. 盖子/电源键/键盘全走 EC:_LID = EC0.LIDS & 1(读 EC RAM 位)
   ec_no_wakeup=1 源码行为:acpi_ec_suspend_noirq() → acpi_set_gpe(EC_GPE, ACPI_GPE_DISABLE),
   resume_noirq 再 ENABLE(ec.c 2116-2141)→ 睡眠期间 EC 事件全进不来
   ⇒ 开盖/电源键/键盘唤醒全失效 → 本机 ec_no_wakeup 与 acpi_mask_gpe 不可用

4. 「模拟合盖」OS 层无等价物:盖子状态是 EC RAM 位 + EC 物理输入,不能伪造

社区与上游:G1619-04 是已知缺陷

1. 内核有专门针对本机型的补丁(已生效)
   commit 805c74eac8cb「gpiolib: acpi: Ignore touchpad wakeup on GPD G1619-04」
   (2024-01-17,Mario Limonciello @ AMD,Cc stable)
   "Spurious wakeups are reported on the GPD G1619-04 which can be absolved by
    programming the GPIO to ignore wakeups."
   DMI_MATCH(SYS_VENDOR,"GPD") + DMI_MATCH(PRODUCT_NAME,"G1619-04"),.ignore_wake="PNP0C50:00@8"
   注释:Spurious wakeups from TP_ATTN# pin / Found in BIOS 0.35
   本机验证:pin #8(DSDT TPDD 的 GpioInt pin 0x0008)有中断但 S0i3/S3/S4S5 唤醒位全空
            (对照 pin #0 三个全亮)→ quirk 生效,触摸板这一路已被内核压掉

2. 上游 issue 就是这台机器
   AMD DRM #3073「GPD win max 2 AMD 7840u s2idle suspend inconsistency + suspend power drain
   + frequency stuck at 544mhz」(2023-12-24 开,2024-01-28 关,labels: Phoenix/Hawk Point, s0ix)
   其 s2idle.py 报告与本机逐项对上:
     Woke up from IRQ 7 (IR-IO-APIC 7-fasteoi pinctrl_amd)  ← 本机 pm_wakeup_irq=7
     gpe0B increased from 0 to 2                             ← 本机唯一活跃 GPE 0x0B
     In a hardware sleep state for 0:11:29 (17.38%)          ← 本机 74 轮真睡 0 次
     power drain nearly 5-10W (10-15%/h)                     ← 本机 9%/h
     还有「cpu fan still working even on suspend」「wake up randomly by itself」
   该机主 BIOS 0.35 / EC 0.14;本机 BIOS 0.41

3. 社区 workaround(aarron-lee/gpd-win-tricks,GPD 掌机 Linux 事实标准仓库)
   wm2-suspend-udev.sh:udev 置 i2c PNP0C50:00 的 power/wakeup=disabled(quirk 的用户态版)
   指纹必须彻底禁:SUBSYSTEM=="usb", ATTR{idVendor}=="2541", ATTR{idProduct}=="9711", ATTR{remove}="1"
     ← 正是本机 3-1.3(2541:9711),每次 resume 重枚举 ~19 行的那个
   wm2-wakeup-workaround:检测盖子仍合着就再 suspend 一次(用户态重试=治循环)
     README 原文:「the GPD WM2 (7840u, 8840u) has issues where it will wake up periodically」
     原始出处 SpookOz/spook-wm2
   同仓库另有 win4-suspend-mods、fuzzy-sound-suspend-fix、win2 等

4. nixos-hardware 有 gpd/win-max-2 profile(default.nix 只 import laptop/ssd/hidpi/ppt,无睡眠设置)

5. 本机 s2idle 期间仍 armed 的针脚(quirk 之后剩下的嫌疑)
   看 S0i3/S3/S4S5 列(不是 wake 能力列):
     pin #0  S0i3+S3+S4S5 全亮(edge/ActiveHigh/debounce 46875us,claimed)
     pin #58 S0i3+S4S5    pin #59 S0i3+S4S5    pin #61 S0i3+S3    pin #62 S0i3+S3
     pin #52/#54 仅 S3
   AMD 报告里「GPIOs active: ['0','58']」也正好是 0 和 58 → 这两个是 s2idle 活跃针
   要压只能靠内核 quirk(boot.kernelPatches 扩 ignore_wake 列表,需精确 device@index),高风险,未做

结论:不是「NixOS 在笔记本上睡不了」,而是 G1619-04 这块板子的 s2idle 唤醒缺陷;
触摸板那一路上游已修(本机已生效),剩余 0/58/59/61/62 是候选。

hermes修复方案实施

【一、已实施·D 日志失真】/etc/nixos/modules/system/service/power-battery-sleep.py
1. sleep_wake_events() 改 journalctl -o json,同时取墙钟与内核单调,配对算 dw/dm
2. 判定 est=dw-dm:≥60s「真睡」,否则「流产未睡」
3. wake 行追加 `(墙钟Xs 单调Ys 判定=…)`
4. sleep 行 mode/电量取上轮活值,删 upower_at()
5. 关联块移到 cond 之前;reported_ev 防重复
落地:世代 …10-39-21-…-duration-verdict(rv8xgpbv…)、提交 9dfc38e

【二、已实施·配套取证】wakeup_line() 唤醒陷阱、打摆子判定、每样本必记 monitor 行
提交:6c9d717、363e905

【三、已实施·治循环】提交 05797a0,世代 …14-36-20-…-flap-inhibitor(2hj74xaq…)
- 修回归:sleep_wake_events 改 4 元组后首圈仍 3 元组解包(一有事件即崩),已改整元组
- 单轮采样内「判定=流产未睡」≥3 次 且 DC+合盖 → systemd-run 起 pbs-sleep-hold
  → systemd-inhibit --what=sleep --mode=block 持锁 1800s → logind/PowerDevil 的 sleep 被拒
- 条件解除立即解锁;抑制期间本监管不再自己发 sleep 信号
- 实测:arm→active、inhibit 清单可见、release→inactive;历史窗回放 151 事件/74 流产/1 真睡
- 收益如实记:翻饼 43s 周期里只有 1.5s 在尝试睡,97% 时间本来醒着 → 停手几乎不省电,
  买到的是止损(墙钟不再每小时被拨 25.6min、3-1.3 不再每 43s 重枚举、日志干净)

【四、已实施·作业法】绕开:AC 合盖入睡 → 睡着后再拔电(已证 11-09 真睡 7h / 0.7%h)

【五、未实施·候选】
- 指纹 udev remove=1(社区明确要求,本机 3-1.3 每次 resume 重枚举)
- 内核 quirk 扩 ignore_wake 到 pin 0/58/59/61/62(高风险,需精确 device@index)
- GPD BIOS/EC 更新(本机 0.41;官网 429 限流未查到)

【六、回滚】git revert 9dfc38e / 05797a0 → rebuild;世代 --rollback;
日志删 power-battery-sleep-state/{log,state.json,force.lock} 复位