/* wd_resguard.h — meow_watchdog 的设备资源压力采样与判定(纯函数,宿主可单测)。 * * 背景(2026-11-11 18:44 用户态崩塌):设备 55 MB 内存、单核、无 swap。launcherd 最后一行 * 日志 18:44:13,之后 11 分钟连 sshd 都写不出 "Child connection from"(fork 都做不到), * 而 ICMP 仍应答;同时 M50 清掉了 55 个挂在「耳机不在」上的 bluetoothctl 残留进程。 * 无 pstore(/sys/fs/pstore 不存在)、reboot 后 dmesg 清空、crash_*.log 全 0 字节 —— * OOM / 进程耗尽**无法事后证实**。所以只能事前留痕:周期采样本,过阈值时把带进程名与 * PID 的现场写进 MeowOS/logs(wd_log 写的同一文件)。 * * 阈值依据(真机实测 2026-11-11 21:00,uptime 25min,100 个进程): * MemTotal 56872 kB;MemFree 1424~1496 kB;MemAvailable 24808~24864 kB * (Buffers 11656 + Cached 10040 → MemFree 常年只有 1.5 MB,单看 MemFree 会天天误报) * 非内核进程里「单父进程子进程数」最大 = 11(ppid=1);ppid=2(kthreadd) 恒有 ~70 个 * 内核线程,必须排除,否则永远报最高档(见 wd_rg_parent_eligible)。 * * avail < 10240 kB(10 MiB ≈ 空闲可用 24.3 MB 的 40%) → WARN * 无 swap;一次 fork+exec(dropbear 子进程、bluetoothctl)的匿名页需求约 1–2 MiB。 * 剩 10 MB 时回收路径已经很紧,再叠加几十个 bluetoothctl 就进入实测的 fork 失败区。 * avail < 5120 kB(5 MiB ≈ MemTotal 的 9%) → CRIT(最后一次舒适余量) * procs >= 140(实测空闲 100 的 +40%) → WARN * procs >= 180(≈1.8× 空闲) → CRIT(事故当晚 ≈100+55=155) * maxch >= 20(实测空闲最大值 11 的 ~2×) → WARN * maxch >= 40(事故形态:55 个残留孤儿全挂 ppid=1) → CRIT * * 为什么 MemFree 不做阈值:内核本来就把空闲页压到 ~1.5 MB(实测),可回收性全在 * MemAvailable 里(Buffers+Cached ≈ 21 MB)。单用 MemFree 会在每次大文件读/写时误报。 * MemFree 只作为告警行里的现场数字保留。 * * 采样周期 10 s:事故从「launcherd 最后一行」到「sshd 完全静默」是分钟级,10 s 一次, * 一次事故至少留 6 条/分钟。开销 = 一次 /proc 扫描(约 100 个 stat 文件),真机 <10 ms, * 摊到 10 s 即 <0.1% CPU。日志只在**等级跃迁**(含首次)或同一告警态每 5 分钟复述一次, * 绝不「每采样一条」——那正是本单要消灭的刷屏形态。 * * 硬性约束(事故教训,本模块 + 调用点共同保证): * - 零 fork / 零 popen:内存紧张时 fork 正是最先失败的动作(sshd 就是这样死的), * 本模块只 read(2) /proc 与 getdents64(2) 列目录。 * - 零堆分配:malloc 在 MemAvailable 见底时可能失败或直接触发 OOM killer;所有缓冲 * 都是调用方给的静态/栈数组。 * - **绝不 kill**:本模块只产出「等级 + 原因」,不发任何信号。App 治理是既有逻辑, * 与本模块无关;dropbear / launcherd / audiod 尤其不可碰。 */ #ifndef WD_RESGUARD_H #define WD_RESGUARD_H #include #include #include #include #define WD_RG_PERIOD_MS 10000u /* 采样周期 */ #define WD_RG_REALERT_MS 300000u /* 同一告警态复述间隔(5 分钟) */ #define WD_RG_TOP_PARENTS 5 /* 记录「子进程数最多的前 5 名」父进程 */ #define WD_RG_CHILD_LIST 8 /* 告警里列多少个具体子进程(名字+PID) */ #define WD_RG_MAX_PIDS 256 /* /proc 扫描上限(实测 100,留 2.5× 余量) */ #define WD_RG_COMM_LEN 16 /* /proc//stat 的 comm(内核上限 15+1) */ /* 阈值(依据见文件头)。单位 kB 的用 unsigned,进程数用 int。 */ #define WD_RG_AVAIL_WARN_KB 10240u #define WD_RG_AVAIL_CRIT_KB 5120u #define WD_RG_PROCS_WARN 140 #define WD_RG_PROCS_CRIT 180 #define WD_RG_CHILD_WARN 20 #define WD_RG_CHILD_CRIT 40 #define WD_RG_KB_UNKNOWN 0u /* 0 = 该字段没读到(MemAvailable 缺失时用) */ enum { WD_RG_OK = 0, WD_RG_WARN = 1, WD_RG_CRIT = 2 }; typedef struct { unsigned long long total_kb; unsigned long long free_kb; unsigned long long avail_kb; /* WD_RG_KB_UNKNOWN = /proc/meminfo 无 MemAvailable */ int procs; /* /proc/ 个数(不含线程) */ int maxch; /* 单父进程子进程数最大值(已排除内核线程) */ int maxch_ppid; /* 该父进程 PID(0 = 没有合格父进程) */ } wd_rg_snap; /* 解析 /proc/meminfo 文本。成功(MemTotal + MemFree 都在)返回 0;MemAvailable 缺失时 * *avail_kb = WD_RG_KB_UNKNOWN(内核 3.14+ 都有,缺失只作降级,不作致命错)。 */ static inline int wd_rg_parse_meminfo(const char *buf, unsigned long long *total_kb, unsigned long long *free_kb, unsigned long long *avail_kb) { unsigned long long t = 0, f = 0, a = WD_RG_KB_UNKNOWN; int seen_t = 0, seen_f = 0; if (!buf || !total_kb || !free_kb || !avail_kb) return -1; const char *p = buf; while (*p) { if (strncmp(p, "MemTotal:", 9) == 0) { t = strtoull(p + 9, NULL, 10); seen_t = 1; } else if (strncmp(p, "MemFree:", 8) == 0) { f = strtoull(p + 8, NULL, 10); seen_f = 1; } else if (strncmp(p, "MemAvailable:", 13) == 0) { a = strtoull(p + 13, NULL, 10); } /* 下一行(/proc/meminfo 每行定长友好,但按行推进更稳) */ const char *nl = strchr(p, '\n'); if (!nl) break; p = nl + 1; } if (!seen_t || !seen_f) return -1; *total_kb = t; *free_kb = f; *avail_kb = a; return 0; } /* 解析一行 /proc//stat:"pid (comm) state ppid ..."。 * comm 里可以有空格与 ')'(例如 "kworker/0:1H"、自命名带括号的进程),所以取**最后一个** ')' * 再往后解析 state/ppid。成功返回 0 并填 out_pid / comm / out_ppid。 */ static inline int wd_rg_parse_stat(const char *buf, int *out_pid, char *comm, size_t commlen, int *out_ppid) { if (!buf || !out_pid || !comm || commlen == 0 || !out_ppid) return -1; const char *lp = strchr(buf, '('); const char *rp = strrchr(buf, ')'); if (!lp || !rp || rp < lp) return -1; long pid = strtol(buf, NULL, 10); if (pid <= 0) return -1; size_t n = (size_t)(rp - (lp + 1)); if (n >= commlen) n = commlen - 1; memcpy(comm, lp + 1, n); comm[n] = 0; /* rp+1 = ' ' state ' ' ppid ... */ const char *q = rp + 1; while (*q == ' ') q++; if (!*q) return -1; q++; /* state 单字符 */ while (*q == ' ') q++; if (!*q) return -1; long ppid = strtol(q, NULL, 10); if (ppid < 0) return -1; *out_pid = (int)pid; *out_ppid = (int)ppid; return 0; } /* 该 ppid 是否参与「子进程数最多」排名。 * 排除 0(无父)与 2(kthreadd:真机恒有 ~70 个内核线程,计入则每次采样都报最高档)。 * ppid=1 **要保留**——孤儿进程(父死掉后 reparent)正是本单要抓的形态。 */ static inline int wd_rg_parent_eligible(int ppid) { if (ppid <= 0) return 0; if (ppid == 2) return 0; /* kthreadd */ return 1; } /* 统计 ppids[0..n) 里父进程 = parent 的个数。 */ static inline int wd_rg_count_children(const int *ppids, int n, int parent) { int c = 0; if (!ppids || n <= 0) return 0; for (int i = 0; i < n; i++) if (ppids[i] == parent) c++; return c; } /* 取子进程数最多的前 k 个合格父进程(降序;同数按 ppid 升序,保证可复现)。 * out_ppid/out_cnt 各至少 k 项。返回实际填了几项(0..k)。 * O(n^2),n<=256 → 每次采样最多 6.5 万次比较,真机 <1 ms。 */ static inline int wd_rg_top_parents(const int *ppids, int n, int k, int *out_ppid, int *out_cnt) { int found = 0; if (!ppids || !out_ppid || !out_cnt || n <= 0 || k <= 0) return 0; for (int i = 0; i < n; i++) { int pid = ppids[i]; if (!wd_rg_parent_eligible(pid)) continue; int cnt = wd_rg_count_children(ppids, n, pid); /* 已记录过同一个 ppid? */ int seen = 0; for (int j = 0; j < found; j++) if (out_ppid[j] == pid) { seen = 1; break; } if (seen) continue; /* 插入排序进前 k */ int pos = found; while (pos > 0 && (out_cnt[pos - 1] < cnt || (out_cnt[pos - 1] == cnt && out_ppid[pos - 1] > pid))) { if (pos < k) { out_cnt[pos] = out_cnt[pos - 1]; out_ppid[pos] = out_ppid[pos - 1]; } pos--; } if (pos < k) { out_cnt[pos] = cnt; out_ppid[pos] = pid; } if (found < k) found++; } return found; } /* 资源压力等级:取所有命中规则里最严重的一档。 */ static inline int wd_rg_level(const wd_rg_snap *s) { int lvl = WD_RG_OK; if (!s) return WD_RG_OK; if ((s->avail_kb != WD_RG_KB_UNKNOWN && s->avail_kb < WD_RG_AVAIL_WARN_KB) || s->procs >= WD_RG_PROCS_WARN || s->maxch >= WD_RG_CHILD_WARN) lvl = WD_RG_WARN; if ((s->avail_kb != WD_RG_KB_UNKNOWN && s->avail_kb < WD_RG_AVAIL_CRIT_KB) || s->procs >= WD_RG_PROCS_CRIT || s->maxch >= WD_RG_CHILD_CRIT) lvl = WD_RG_CRIT; return lvl; } /* 命中的规则名(逗号分隔),用于告警行 —— 让读日志的人不用回代码查阈值。 * 当前等级的规则全列;WARN 档不列 CRIT 阈值(避免「写 5MB 却没到 5MB」的误读)。 */ static inline void wd_rg_reason(const wd_rg_snap *s, char *out, size_t olen) { size_t off = 0; if (!out || olen == 0) return; out[0] = 0; if (!s) return; int lvl = wd_rg_level(s); if (lvl == WD_RG_OK) return; #define WD_RG_ADD(txt) do { \ if (off + 1 < olen) \ off += (size_t)snprintf(out + off, olen - off, "%s%s", off ? "," : "", txt); \ } while (0) if (s->avail_kb != WD_RG_KB_UNKNOWN && s->avail_kb < WD_RG_AVAIL_WARN_KB) { if (lvl == WD_RG_CRIT && s->avail_kb < WD_RG_AVAIL_CRIT_KB) WD_RG_ADD("avail<5120kB"); else WD_RG_ADD("avail<10240kB"); } if (s->procs >= WD_RG_PROCS_WARN) { if (lvl == WD_RG_CRIT && s->procs >= WD_RG_PROCS_CRIT) WD_RG_ADD("procs>=180"); else WD_RG_ADD("procs>=140"); } if (s->maxch >= WD_RG_CHILD_WARN) { if (lvl == WD_RG_CRIT && s->maxch >= WD_RG_CHILD_CRIT) WD_RG_ADD("children>=40"); else WD_RG_ADD("children>=20"); } #undef WD_RG_ADD } /* 这一轮该不该写日志:首次 / 等级跃迁 / 同一告警态超过 REALERT → 1,否则 0。 * first=1 只在本进程第一次采样时为真(调用方用静态标志)。 */ static inline int wd_rg_should_emit(int prev_level, int cur_level, unsigned long long since_last_ms, int first) { if (first) return 1; if (cur_level != prev_level) return 1; if (cur_level != WD_RG_OK && since_last_ms >= WD_RG_REALERT_MS) return 1; return 0; } static inline const char *wd_rg_level_name(int lvl) { return (lvl == WD_RG_CRIT) ? "CRIT" : (lvl == WD_RG_WARN) ? "WARN" : "OK"; } #endif /* WD_RESGUARD_H */