Files
MeowOS-For-R3P2/patch_v2/meow_watchdog/wd_resguard.h
T
AskaEth 166b32d0e5 feat(watchdog): M52 资源压力可观测 + multi_font 日志去刷屏 + audiod bind 失败即退
- watchdog(新 wd_resguard.h + meow_watchdog.c):每 10s 采 /proc/meminfo
  (MemFree/MemAvailable)、进程总数、单父进程子进程数前 5 名;过阈值时向
  MeowOS/logs 打 RESGUARD 告警,带等级/命中规则/进程名+PID 名单。零 fork、零堆分配、
  不发任何信号(只报警)。阈值依据 = 真机实测 56872kB / 空闲 100 进程 / maxch=11,
  推导写在 wd_resguard.h 文件头;日志只在等级跃迁或同态每 5 分钟复述。
- launcherd:multi_font 那行「每 2s 无条件打一条」改为只在配置签名跃迁时打
  (新 mf_logsig.h 纯函数 + test_mf_logsig 宿主单测,接进 build_launcher.sh test)。
  真机 6603 行日志里该行占 5297 行(80%)。
- audiod:io bind 失败不再只记一行就 return(旧行为让主线程 watchdog 空转 720s、
  客户端全 ECONNREFUSED),改为明确报错 + g_stop 干净退出;仅动 io_thread 一个 hunk。
2026-11-12 08:02:22 +08:00

243 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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/<pid>/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/<pid> 个数(不含线程) */
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/<pid>/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 */