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。
This commit is contained in:
1 parent
f3d7b386b2
commit
166b32d0e5
8 files changed
+755
-4
No files matched your search
@@ -2025,8 +2025,16 @@ static void *io_thread(void *arg)
|
|||||||
snprintf(a.sun_path, sizeof a.sun_path, "%s", g_sock);
|
snprintf(a.sun_path, sizeof a.sun_path, "%s", g_sock);
|
||||||
unlink(g_sock);
|
unlink(g_sock);
|
||||||
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) {
|
if (bind(lfd, (struct sockaddr *)&a, sizeof a) != 0) {
|
||||||
logmsg("io: bind %s: %s", g_sock, strerror(errno));
|
/* M52:bind 失败 = IO 线程根本没起来。旧实现只记一行就 return,主线程的 WATCHDOG
|
||||||
|
* 于是每 30s 报一次 "io thread silent"、客户端全 ECONNREFUSED,守护既不可用也不退出
|
||||||
|
* (M50 真机把 socket 放到 SD 卡 FAT 上 → EPERM,空转 720s)。现在明确报错并**干净
|
||||||
|
* 退出**(g_stop=1 → main 走正常 shutdown、退出码 0):svcd 的 restart=on-fail 不会把
|
||||||
|
* 它当崩溃重拉(max_restart=5),也不会再有线程假装在服务。
|
||||||
|
* AF_UNIX socket 必须落在本地可写 FS(/tmp);FAT/exFAT 不支持 unix socket。 */
|
||||||
|
logmsg("io: bind %s: %s -> FATAL (AF_UNIX socket must be on a local FS like /tmp, "
|
||||||
|
"FAT/exFAT is unsupported); exiting", g_sock, strerror(errno));
|
||||||
close(lfd);
|
close(lfd);
|
||||||
|
g_stop = 1;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
chmod(g_sock, 0666);
|
chmod(g_sock, 0666);
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ run_tests() {
|
|||||||
rc=1
|
rc=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# M52:多字体日志跃迁签名(纯头文件逻辑,无设备/无 launcher 依赖)
|
||||||
|
echo "== host test: test_mf_logsig =="
|
||||||
|
if "$HOSTCC" -I. -Wall -Wextra test_mf_logsig.c -o "$TMPD/test_mf_logsig"; then
|
||||||
|
"$TMPD/test_mf_logsig" || rc=1
|
||||||
|
else
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ $rc -eq 0 ]; then echo "== ALL TESTS PASS =="; else echo "!! TESTS FAILED"; fi
|
if [ $rc -eq 0 ]; then echo "== ALL TESTS PASS =="; else echo "!! TESTS FAILED"; fi
|
||||||
return $rc
|
return $rc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* font_theme.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
/* font_theme.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||||||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||||||
#include "meow_launcher_internal.h"
|
#include "meow_launcher_internal.h"
|
||||||
|
#include "mf_logsig.h" /* M52:多字体日志只在配置签名跃迁时打(纯函数,宿主单测) */
|
||||||
|
|
||||||
/* M2.6 字重结构 + M2.8 三级定案(FONT_SYSTEM):卡内字体 =
|
/* M2.6 字重结构 + M2.8 三级定案(FONT_SYSTEM):卡内字体 =
|
||||||
* <卡根>/MeowOS/fonts/<family>/<weight>/(两级,向后兼容)或
|
* <卡根>/MeowOS/fonts/<family>/<weight>/(两级,向后兼容)或
|
||||||
@@ -282,6 +283,10 @@ fallback:
|
|||||||
* meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效;
|
* meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效;
|
||||||
* 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及
|
* 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及
|
||||||
* 设置页变更 display.multi_font / font.class_* / font.file。 */
|
* 设置页变更 display.multi_font / font.class_* / font.file。 */
|
||||||
|
/* M52:上一次打过日志的配置签名(含 valid 位:签名本身可能恰好是 0,不能拿 0 当「没打过」)。 */
|
||||||
|
static uint64_t mfont_log_sig;
|
||||||
|
static int mfont_log_sig_valid;
|
||||||
|
|
||||||
void meow_font_reload_multifont(void)
|
void meow_font_reload_multifont(void)
|
||||||
{
|
{
|
||||||
static const char *cls_keys[MEOW_FCLS_COUNT] = {
|
static const char *cls_keys[MEOW_FCLS_COUNT] = {
|
||||||
@@ -318,7 +323,17 @@ void meow_font_reload_multifont(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
int distinct = 0, truncated = 0;
|
int distinct = 0, truncated = 0;
|
||||||
if (meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated) != 0)
|
int rc = meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated);
|
||||||
|
/* M52(日志刷屏):本函数被主循环 hotplug 轮询每 2s 调一次,配置绝大多数周期不变。
|
||||||
|
* 旧实现无条件打 "multi_font: enabled..."(真机 6603 行日志里 5297 行 = 80%),
|
||||||
|
* 现在只在「配置签名跃迁」时打 —— 首次、global 字体变、任一类字体变、on 变、
|
||||||
|
* distinct/truncated 变。签名相同 = 行为相同 = 无新信息,静默。 */
|
||||||
|
const char *csig[MEOW_FCLS_COUNT];
|
||||||
|
for (int c = 0; c < MEOW_FCLS_COUNT; c++) csig[c] = cfg[c].png;
|
||||||
|
uint64_t sig = mf_logsig(on, gpng, csig, MEOW_FCLS_COUNT, rc == 0 ? distinct : -1, truncated);
|
||||||
|
/* 无变化(签名相同)= 行为相同 = 无新信息:静默,配置本身已生效。 */
|
||||||
|
if (!mf_logsig_changed(&mfont_log_sig, &mfont_log_sig_valid, sig)) return;
|
||||||
|
if (rc != 0)
|
||||||
logmsg("multi_font: config failed -> single-font");
|
logmsg("multi_font: config failed -> single-font");
|
||||||
else if (truncated)
|
else if (truncated)
|
||||||
logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct);
|
logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* mf_logsig.h — 多字体配置的「日志跃迁签名」(纯函数,宿主可单测)。
|
||||||
|
*
|
||||||
|
* 为什么需要它(M52):meow_font_reload_multifont() 由 launcher 主循环的 hotplug 轮询
|
||||||
|
* 每 2s 调一次(main_menu.c),而多字体配置在绝大多数周期里毫无变化。旧实现无条件打
|
||||||
|
* `multi_font: enabled, %d distinct fonts (global=%s)`,真机 meow_20261111_154540.log
|
||||||
|
* 6603 行里 5297 行是它(80%,~291KB/406KB),把事故现场唯一的信号淹没了。
|
||||||
|
*
|
||||||
|
* 修法不是删日志(首次启用/用户改字体必须留痕),而是把它绑到**配置签名**上:签名变了
|
||||||
|
* 才打。签名覆盖所有能改变多字体行为的输入 ——
|
||||||
|
* on(display.multi_font)、全局字体 png(font.file → 生效档)、6 个 font.class_* 的
|
||||||
|
* 解析后绝对路径、distinct(实际驻留套数)、truncated(超预算截断)。
|
||||||
|
* 因此「字体集合或 global font 变化」必然换签名,纯粹的时间流逝不会。
|
||||||
|
*/
|
||||||
|
#ifndef MF_LOGSIG_H
|
||||||
|
#define MF_LOGSIG_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define MF_LOGSIG_SEED 14695981039346656037ULL /* FNV-1a 64 offset basis */
|
||||||
|
#define MF_LOGSIG_PRIME 1099511628211ULL
|
||||||
|
|
||||||
|
/* 把一段字符串折进签名。字段之间补分隔符,防 ("ab","c") 与 ("a","bc") 撞同一值。
|
||||||
|
* NULL 与 "" 视为同一(配置项为空 = 该类别用全局字体)。 */
|
||||||
|
static inline uint64_t mf_logsig_mix(uint64_t h, const char *s)
|
||||||
|
{
|
||||||
|
if (!s) s = "";
|
||||||
|
while (*s) {
|
||||||
|
h ^= (uint64_t)(unsigned char)*s++;
|
||||||
|
h *= MF_LOGSIG_PRIME;
|
||||||
|
}
|
||||||
|
h ^= 0x1f; /* 字段分隔(0x1f = US,不会出现在路径里) */
|
||||||
|
h *= MF_LOGSIG_PRIME;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 多字体配置签名。ncls = 类别数(MEOW_FCLS_COUNT=6),cpng[c] 可为 NULL。
|
||||||
|
* distinct < 0 表示 config 失败(此时 distinct 无意义,用 -1 编码「失败」)。 */
|
||||||
|
static inline uint64_t mf_logsig(int on, const char *gpng,
|
||||||
|
const char *const *cpng, int ncls,
|
||||||
|
int distinct, int truncated)
|
||||||
|
{
|
||||||
|
char num[24];
|
||||||
|
uint64_t h = MF_LOGSIG_SEED;
|
||||||
|
snprintf(num, sizeof num, "%d", on ? 1 : 0);
|
||||||
|
h = mf_logsig_mix(h, num);
|
||||||
|
h = mf_logsig_mix(h, gpng);
|
||||||
|
for (int c = 0; c < ncls; c++)
|
||||||
|
h = mf_logsig_mix(h, (cpng && cpng[c]) ? cpng[c] : "");
|
||||||
|
snprintf(num, sizeof num, "%d:%d", distinct, truncated ? 1 : 0);
|
||||||
|
h = mf_logsig_mix(h, num);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志门控:首次(*valid=0)或签名变化 → 返回 1 并记下;同一签名重复调用 → 0。
|
||||||
|
* 调用方持有 *last / *valid(文件级静态),因此这就是「只在跃迁时打日志」的全部判定。 */
|
||||||
|
static inline int mf_logsig_changed(uint64_t *last, int *valid, uint64_t sig)
|
||||||
|
{
|
||||||
|
if (*valid && sig == *last) return 0;
|
||||||
|
*last = sig;
|
||||||
|
*valid = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* MF_LOGSIG_H */
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/* test_mf_logsig.c — 宿主单测:多字体「日志跃迁签名」与门控(M52 刷屏整改)。
|
||||||
|
*
|
||||||
|
* 背景:meow_font_reload_multifont() 被 launcher 主循环每 2s 调一次,旧实现每次无条件
|
||||||
|
* 打 `multi_font: enabled, ...`(真机 5297/6603 行 = 80%)。整改后只在配置签名变化时打。
|
||||||
|
* 本测试守两件事:
|
||||||
|
* (1) 签名对**所有**能改变行为的输入敏感(on / global / 各类别 / distinct / truncated);
|
||||||
|
* (2) 输入不变时签名恒定 —— 即 60s 空转(30 次调用)只产生 1 条日志。
|
||||||
|
*
|
||||||
|
* 编译运行:gcc -I. -Wall -Wextra test_mf_logsig.c -o test_mf_logsig && ./test_mf_logsig
|
||||||
|
* (不依赖 launcher 其它模块 / 不依赖设备;纯头文件逻辑)
|
||||||
|
*/
|
||||||
|
#include "mf_logsig.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define NCLS 6
|
||||||
|
|
||||||
|
static int g_pass = 0, g_fail = 0;
|
||||||
|
|
||||||
|
static void check(const char *name, int got, int want)
|
||||||
|
{
|
||||||
|
if (got == want) { g_pass++; printf("PASS %s\n", name); }
|
||||||
|
else { g_fail++; printf("FAIL %s: got %d want %d\n", name, got, want); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 真机 settings.conf 的多字体形态(2026-11-11):
|
||||||
|
* display.multi_font = 1
|
||||||
|
* font.file = MeowOS/fonts/ztest/regular/font.png → global = ztest
|
||||||
|
* font.class_num / font.class_latin = MeowOS/fonts/google_sans/regular/font.png
|
||||||
|
* → distinct = 2(ztest + google_sans) */
|
||||||
|
#define G_ZTEST "/usr/data/mnt/sd_0/MeowOS/fonts/ztest/regular/font.png"
|
||||||
|
#define C_GOOGLE "/usr/data/mnt/sd_0/MeowOS/fonts/google_sans/regular/font.png"
|
||||||
|
#define C_MISANS "/usr/data/mnt/sd_0/MeowOS/fonts/misans/regular/font.png"
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
/* ---- 1. 真机形态:同输入 → 同签名(1000 次不变,这就是「空转不打日志」的根基)---- */
|
||||||
|
const char *base[NCLS] = { NULL, NULL, C_GOOGLE, C_GOOGLE, NULL, NULL };
|
||||||
|
uint64_t s0 = mf_logsig(1, G_ZTEST, base, NCLS, 2, 0);
|
||||||
|
int stable = 1;
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
if (mf_logsig(1, G_ZTEST, base, NCLS, 2, 0) != s0) stable = 0;
|
||||||
|
check("same config -> same sig (1000x)", stable, 1);
|
||||||
|
|
||||||
|
/* ---- 2. 每一条输入变化都必须换签名 ---- */
|
||||||
|
check("on 1->0 changes sig", mf_logsig(0, G_ZTEST, base, NCLS, 2, 0) != s0, 1);
|
||||||
|
check("global font changes sig",
|
||||||
|
mf_logsig(1, "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.png",
|
||||||
|
base, NCLS, 2, 0) != s0, 1);
|
||||||
|
{
|
||||||
|
const char *c[NCLS] = { NULL, NULL, C_MISANS, C_GOOGLE, NULL, NULL };
|
||||||
|
check("class_num changes sig", mf_logsig(1, G_ZTEST, c, NCLS, 2, 0) != s0, 1);
|
||||||
|
}
|
||||||
|
check("distinct 2->1 changes sig", mf_logsig(1, G_ZTEST, base, NCLS, 1, 0) != s0, 1);
|
||||||
|
check("truncated 0->1 changes sig", mf_logsig(1, G_ZTEST, base, NCLS, 2, 1) != s0, 1);
|
||||||
|
check("config-fail (-1) changes sig", mf_logsig(1, G_ZTEST, base, NCLS, -1, 0) != s0, 1);
|
||||||
|
|
||||||
|
/* ---- 3. 空配置项形态:NULL 与 "" 等价(settings 空值 = 该类回退全局)---- */
|
||||||
|
{
|
||||||
|
const char *a[NCLS] = { NULL, NULL, NULL, NULL, NULL, NULL };
|
||||||
|
const char *b[NCLS] = { "", "", "", "", "", "" };
|
||||||
|
check("NULL == \"\"", mf_logsig(1, G_ZTEST, a, NCLS, 1, 0) ==
|
||||||
|
mf_logsig(1, G_ZTEST, b, NCLS, 1, 0), 1);
|
||||||
|
check("null cpng array tolerated", mf_logsig(1, G_ZTEST, NULL, NCLS, 1, 0) > 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 4. 字段边界不串味:("ab","c") 与 ("a","bc") 不能撞 ---- */
|
||||||
|
{
|
||||||
|
const char *x[NCLS] = { "ab", "c", NULL, NULL, NULL, NULL };
|
||||||
|
const char *y[NCLS] = { "a", "bc", NULL, NULL, NULL, NULL };
|
||||||
|
check("field boundary no collision",
|
||||||
|
mf_logsig(1, G_ZTEST, x, NCLS, 2, 0) != mf_logsig(1, G_ZTEST, y, NCLS, 2, 0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 5. 门控:60s 空转(2s 一次 × 30)只打 1 条;改配置再打 1 条;改回来再打 1 条 ---- */
|
||||||
|
{
|
||||||
|
uint64_t last = 0;
|
||||||
|
int valid = 0, emits = 0;
|
||||||
|
for (int i = 0; i < 30; i++) { /* 首轮 + 29 轮空转 */
|
||||||
|
uint64_t sig = mf_logsig(1, G_ZTEST, base, NCLS, 2, 0);
|
||||||
|
if (mf_logsig_changed(&last, &valid, sig)) emits++;
|
||||||
|
}
|
||||||
|
check("60s idle @2s -> exactly 1 log line", emits, 1);
|
||||||
|
{
|
||||||
|
const char *c[NCLS] = { NULL, NULL, C_MISANS, C_GOOGLE, NULL, NULL };
|
||||||
|
uint64_t sig = mf_logsig(1, G_ZTEST, c, NCLS, 2, 0);
|
||||||
|
if (mf_logsig_changed(&last, &valid, sig)) emits++;
|
||||||
|
}
|
||||||
|
check("class change -> 2nd log line", emits, 2);
|
||||||
|
for (int i = 0; i < 30; i++) { /* 再空转 60s:不应再增长 */
|
||||||
|
uint64_t sig = mf_logsig(1, G_ZTEST, base, NCLS, 2, 0);
|
||||||
|
if (mf_logsig_changed(&last, &valid, sig)) emits++;
|
||||||
|
}
|
||||||
|
check("back to original -> 3rd line, then silent", emits, 3);
|
||||||
|
/* 真实行数:旧实现 60s = 30 行(每 2s 一条),新实现 60s = 0 行(稳定后) */
|
||||||
|
printf(" (旧实现 60s 空转 = 30 行;新实现稳态 = 0 行,本用例全程 %d 行)\n", emits);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n%d pass, %d fail\n", g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
#include "meow_ipc.h" /* MSG_AUDIO_VOLUME/MSG_ACK/帧格式(音量收编 v2:首选 audiod 权威) */
|
#include "meow_ipc.h" /* MSG_AUDIO_VOLUME/MSG_ACK/帧格式(音量收编 v2:首选 audiod 权威) */
|
||||||
#include "../sdk/meow_time32.h" /* o32 time32 取时/睡眠(watchdog 不链 meow_ui.a) */
|
#include "../sdk/meow_time32.h" /* o32 time32 取时/睡眠(watchdog 不链 meow_ui.a) */
|
||||||
#include "wd_power.h" /* 电源阈值/边沿纯判定(宿主单测同源) */
|
#include "wd_power.h" /* 电源阈值/边沿纯判定(宿主单测同源) */
|
||||||
|
#include "wd_resguard.h" /* M52 资源压力阈值/判定(纯函数,宿主单测同源) */
|
||||||
|
|
||||||
/* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */
|
/* device-verified: VSCREENINFO=0x4600, FSCREENINFO=0x4602 (do not swap) */
|
||||||
#define FBIOGET_VSCREENINFO 0x4600
|
#define FBIOGET_VSCREENINFO 0x4600
|
||||||
@@ -171,12 +172,14 @@ static void wd_log_init(void)
|
|||||||
|
|
||||||
static void wd_log(const char *fmt, ...)
|
static void wd_log(const char *fmt, ...)
|
||||||
{
|
{
|
||||||
char buf[256];
|
/* M52:缓冲从 256/288 提到 512/544 —— 资源告警行要带 top5 父进程与子进程名单
|
||||||
|
* (名字+PID),一条 200~280 字符;旧 256 会把名单截断,而截断的告警等于没告警。 */
|
||||||
|
char buf[512];
|
||||||
va_list ap;
|
va_list ap;
|
||||||
va_start(ap, fmt);
|
va_start(ap, fmt);
|
||||||
vsnprintf(buf, sizeof buf, fmt, ap);
|
vsnprintf(buf, sizeof buf, fmt, ap);
|
||||||
va_end(ap);
|
va_end(ap);
|
||||||
char line[288];
|
char line[544];
|
||||||
int l = snprintf(line, sizeof line, "%llu [watchdog] %s\n", now_ms(), buf);
|
int l = snprintf(line, sizeof line, "%llu [watchdog] %s\n", now_ms(), buf);
|
||||||
if (wd_log_fd >= 0) {
|
if (wd_log_fd >= 0) {
|
||||||
if (write(wd_log_fd, line, l) < 0) wd_log_fd = -1;
|
if (write(wd_log_fd, line, l) < 0) wd_log_fd = -1;
|
||||||
@@ -847,6 +850,157 @@ static int wd_framework_from_pid(int pid)
|
|||||||
return wd_app_is_framework(exe);
|
return wd_app_is_framework(exe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================================================== */
|
||||||
|
/* M52 设备资源压力可观测与自保(只告警,绝不 kill) */
|
||||||
|
/* 事故:2026-11-11 18:44 用户态崩塌。launcherd 最后一行日志 18:44:13, */
|
||||||
|
/* 之后 11 分钟连 sshd 都写不出 "Child connection from"(fork 都做不到),*/
|
||||||
|
/* 而 ICMP 仍应答;当晚 M50 清掉 55 个挂在「耳机不在」上的 bluetoothctl */
|
||||||
|
/* 残留进程。无 pstore、dmesg 重启即清、crash_*.log 全 0 字节 → OOM 无法 */
|
||||||
|
/* 事后证实。本段:每 10s 采 /proc/meminfo + 进程总数 + 单父进程子进程 */
|
||||||
|
/* 数前 5 名;过阈值(依据见 wd_resguard.h 文件头)时把「带进程名与 PID */
|
||||||
|
/* 的现场」写进 MeowOS/logs(即 wd_log 的同一文件)。 */
|
||||||
|
/* 硬约束:不发任何信号(App 治理属既有逻辑,dropbear/launcherd 绝不可 */
|
||||||
|
/* 碰);零 fork 零 popen(内存见底时 fork 正是第一个失败的动作);零堆 */
|
||||||
|
/* 分配(malloc 在 MemAvailable 见底时可能失败或直接喂 OOM killer)。 */
|
||||||
|
/* ================================================================== */
|
||||||
|
|
||||||
|
/* getdents64 的最小结构 —— 不用 <dirent.h> 的 readdir:那要 malloc 一个 DIR 缓冲。 */
|
||||||
|
struct wd_dent64 {
|
||||||
|
uint64_t d_ino;
|
||||||
|
int64_t d_off;
|
||||||
|
unsigned short d_reclen;
|
||||||
|
unsigned char d_type;
|
||||||
|
char d_name[];
|
||||||
|
};
|
||||||
|
|
||||||
|
static int wd_rg_pid[WD_RG_MAX_PIDS];
|
||||||
|
static int wd_rg_ppid[WD_RG_MAX_PIDS];
|
||||||
|
static char wd_rg_comm[WD_RG_MAX_PIDS][WD_RG_COMM_LEN];
|
||||||
|
|
||||||
|
/* 读一个 /proc 伪文件(一次 read 足够)。返回字节数;-1=打开/读失败(errno 保留)。 */
|
||||||
|
static int wd_rg_read(const char *path, char *buf, int len)
|
||||||
|
{
|
||||||
|
int fd = open(path, O_RDONLY | O_NONBLOCK);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
int n = (int)read(fd, buf, (size_t)len);
|
||||||
|
close(fd);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扫 /proc 顶层,填 pid/ppid/comm。返回进程数(≤max);-1=打不开 /proc。 */
|
||||||
|
static int wd_rg_scan(int max)
|
||||||
|
{
|
||||||
|
int fd = open("/proc", O_RDONLY | O_DIRECTORY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
/* 8 字节对齐的静态缓冲:dirent64 含 uint64/int64 字段,char 数组直接强转在严格
|
||||||
|
* 对齐的 ABI 上是 UB。 */
|
||||||
|
static uint64_t dbuf[4096 / sizeof(uint64_t)];
|
||||||
|
char *base = (char *)(void *)dbuf;
|
||||||
|
int n = 0;
|
||||||
|
for (;;) {
|
||||||
|
long r = (long)syscall(SYS_getdents64, fd, base, sizeof dbuf);
|
||||||
|
if (r <= 0) break;
|
||||||
|
for (long off = 0; off < r; ) {
|
||||||
|
struct wd_dent64 *d = (struct wd_dent64 *)(void *)(base + off);
|
||||||
|
if (d->d_reclen < sizeof(struct wd_dent64)) break;
|
||||||
|
const char *nm = d->d_name;
|
||||||
|
if (n < max && nm[0] >= '0' && nm[0] <= '9') {
|
||||||
|
char path[48], sbuf[256];
|
||||||
|
snprintf(path, sizeof path, "/proc/%s/stat", nm);
|
||||||
|
int k = wd_rg_read(path, sbuf, (int)sizeof sbuf - 1);
|
||||||
|
if (k > 0) {
|
||||||
|
int pid = 0, ppid = 0;
|
||||||
|
sbuf[k] = 0;
|
||||||
|
if (wd_rg_parse_stat(sbuf, &pid, wd_rg_comm[n], WD_RG_COMM_LEN, &ppid) == 0) {
|
||||||
|
wd_rg_pid[n] = pid;
|
||||||
|
wd_rg_ppid[n] = ppid;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
off += d->d_reclen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已扫描表里 pid 的 comm(找不到 → "?")。 */
|
||||||
|
static const char *wd_rg_name_of(int npids, int pid)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < npids; i++) if (wd_rg_pid[i] == pid) return wd_rg_comm[i];
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 一次采样 + 判定 +(必要时)告警。日志只在等级跃迁(含首次)/同等级每 5 分钟。 */
|
||||||
|
static void wd_rg_tick(void)
|
||||||
|
{
|
||||||
|
static int last_level = WD_RG_OK;
|
||||||
|
static int first = 1;
|
||||||
|
static unsigned long long last_emit_ms = 0;
|
||||||
|
static char mbuf[768];
|
||||||
|
|
||||||
|
wd_rg_snap s;
|
||||||
|
memset(&s, 0, sizeof s);
|
||||||
|
int mn = wd_rg_read("/proc/meminfo", mbuf, (int)sizeof mbuf - 1);
|
||||||
|
if (mn <= 0) {
|
||||||
|
wd_log("RESGUARD: /proc/meminfo unreadable (%s), skip this round", strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mbuf[mn] = 0;
|
||||||
|
if (wd_rg_parse_meminfo(mbuf, &s.total_kb, &s.free_kb, &s.avail_kb) != 0) {
|
||||||
|
wd_log("RESGUARD: /proc/meminfo parse failed (no MemTotal/MemFree), skip this round");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int np = wd_rg_scan(WD_RG_MAX_PIDS);
|
||||||
|
if (np < 0) {
|
||||||
|
wd_log("RESGUARD: /proc unreadable (%s), skip this round", strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.procs = np;
|
||||||
|
int tp[WD_RG_TOP_PARENTS], tc[WD_RG_TOP_PARENTS];
|
||||||
|
int ntop = wd_rg_top_parents(wd_rg_ppid, np, WD_RG_TOP_PARENTS, tp, tc);
|
||||||
|
if (ntop > 0) { s.maxch = tc[0]; s.maxch_ppid = tp[0]; }
|
||||||
|
|
||||||
|
int lvl = wd_rg_level(&s);
|
||||||
|
unsigned long long now = now_ms();
|
||||||
|
if (!wd_rg_should_emit(last_level, lvl, now - last_emit_ms, first)) return;
|
||||||
|
|
||||||
|
char why[128];
|
||||||
|
wd_rg_reason(&s, why, sizeof why);
|
||||||
|
wd_log("RESGUARD %s mem avail=%llukB free=%llukB total=%llukB procs=%d maxch=%d(ppid=%d/%s)%s%s",
|
||||||
|
wd_rg_level_name(lvl), s.avail_kb, s.free_kb, s.total_kb, s.procs,
|
||||||
|
s.maxch, s.maxch_ppid, wd_rg_name_of(np, s.maxch_ppid),
|
||||||
|
why[0] ? " why=" : "", why);
|
||||||
|
/* top5:谁在批量拉子进程(事故形态 = 55 个 bluetoothctl 孤儿全挂 ppid=1) */
|
||||||
|
if (ntop > 0) {
|
||||||
|
char tl[384];
|
||||||
|
int off = 0;
|
||||||
|
tl[0] = 0;
|
||||||
|
for (int i = 0; i < ntop && off + 1 < (int)sizeof tl; i++)
|
||||||
|
off += snprintf(tl + off, sizeof tl - (size_t)off, " %d/%s=%d",
|
||||||
|
tp[i], wd_rg_name_of(np, tp[i]), tc[i]);
|
||||||
|
wd_log("RESGUARD top%d parents (ppid/comm=children):%s", ntop, tl);
|
||||||
|
}
|
||||||
|
/* 名单:仅过子进程阈值时列,避免每次跃迁都多一行 */
|
||||||
|
if (s.maxch >= WD_RG_CHILD_WARN) {
|
||||||
|
char cl[512];
|
||||||
|
int off = 0, shown = 0;
|
||||||
|
cl[0] = 0;
|
||||||
|
for (int i = 0; i < np && shown < WD_RG_CHILD_LIST && off + 1 < (int)sizeof cl; i++) {
|
||||||
|
if (wd_rg_ppid[i] != s.maxch_ppid) continue;
|
||||||
|
off += snprintf(cl + off, sizeof cl - (size_t)off, " %s/%d", wd_rg_comm[i], wd_rg_pid[i]);
|
||||||
|
shown++;
|
||||||
|
}
|
||||||
|
wd_log("RESGUARD children of %d/%s (n=%d):%s%s", s.maxch_ppid,
|
||||||
|
wd_rg_name_of(np, s.maxch_ppid), s.maxch, cl,
|
||||||
|
shown < s.maxch ? " (+more)" : "");
|
||||||
|
}
|
||||||
|
last_level = lvl;
|
||||||
|
last_emit_ms = now;
|
||||||
|
first = 0;
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef WD_NO_MAIN
|
#ifndef WD_NO_MAIN
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
@@ -914,6 +1068,9 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
long elapsed = 0;
|
long elapsed = 0;
|
||||||
|
/* M52:资源采样用真单调时钟排程,不用 elapsed —— elapsed 是「每轮 +100ms」的假时钟,
|
||||||
|
* 触摸事件密集时 poll 早返回会让它跑快,采样周期就跟着缩水。 */
|
||||||
|
unsigned long long rg_next_ms = now_ms() + WD_RG_PERIOD_MS;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
/* app gone? */
|
/* app gone? */
|
||||||
if (kill(app_pid, 0) < 0 && errno == ESRCH) {
|
if (kill(app_pid, 0) < 0 && errno == ESRCH) {
|
||||||
@@ -979,6 +1136,11 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
wd_power_tick(); /* M2.24:电源长按检测(3s 菜单 / 8s 强关 / 菜单期让位) */
|
wd_power_tick(); /* M2.24:电源长按检测(3s 菜单 / 8s 强关 / 菜单期让位) */
|
||||||
wd_jack_tick(); /* 插孔状态节流轮询(消抖音量键幽灵按下) */
|
wd_jack_tick(); /* 插孔状态节流轮询(消抖音量键幽灵按下) */
|
||||||
|
/* M52:资源压力采样(10s 一次;只在跃迁/5 分钟复述时写日志,空转零日志增长) */
|
||||||
|
if (now_ms() >= rg_next_ms) {
|
||||||
|
wd_rg_tick();
|
||||||
|
rg_next_ms = now_ms() + WD_RG_PERIOD_MS;
|
||||||
|
}
|
||||||
elapsed += 100;
|
elapsed += 100;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -302,6 +302,152 @@ static void test_power_matrix(void)
|
|||||||
wd_pwr_tick_action(0, 1, 1, 0, 2000, 1000, 2000) == WD_PWR_TICK_FORCEOFF, 1);
|
wd_pwr_tick_action(0, 1, 1, 0, 2000, 1000, 2000) == WD_PWR_TICK_FORCEOFF, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- M52:资源压力守卫(纯判定 + /proc 解析 + 真机 /proc 冒烟)----
|
||||||
|
* 阈值依据见 wd_resguard.h 文件头(真机 56872kB / 空闲 100 进程 / maxch=11)。
|
||||||
|
* 这里只测「判定与解析」,不测写日志 —— 告警文案由 wd_log 走既有通道。 */
|
||||||
|
static void test_resguard(void)
|
||||||
|
{
|
||||||
|
/* 1) /proc/meminfo 解析:真机原样快照(2026-11-11 21:00) */
|
||||||
|
static const char meminfo[] =
|
||||||
|
"MemTotal: 56872 kB\n"
|
||||||
|
"MemFree: 1424 kB\n"
|
||||||
|
"MemAvailable: 24808 kB\n"
|
||||||
|
"Buffers: 11656 kB\n"
|
||||||
|
"Cached: 10040 kB\n"
|
||||||
|
"SwapTotal: 0 kB\n";
|
||||||
|
unsigned long long tot = 0, fre = 0, av = 0;
|
||||||
|
check("meminfo parse ok", wd_rg_parse_meminfo(meminfo, &tot, &fre, &av), 0);
|
||||||
|
check("meminfo total", tot == 56872ULL, 1);
|
||||||
|
check("meminfo free", fre == 1424ULL, 1);
|
||||||
|
check("meminfo avail", av == 24808ULL, 1);
|
||||||
|
/* MemAvailable 缺失(内核 <3.14)→ 降级为 UNKNOWN,不误判 CRIT */
|
||||||
|
static const char meminfo_noav[] = "MemTotal: 56872 kB\nMemFree: 1400 kB\n";
|
||||||
|
tot = fre = av = 1;
|
||||||
|
check("meminfo no-avail ok", wd_rg_parse_meminfo(meminfo_noav, &tot, &fre, &av), 0);
|
||||||
|
check("meminfo no-avail -> UNKNOWN", av == WD_RG_KB_UNKNOWN, 1);
|
||||||
|
/* 缺 MemTotal → 失败(不拿半个快照做判定) */
|
||||||
|
check("meminfo no-total -> -1",
|
||||||
|
wd_rg_parse_meminfo("MemFree: 100 kB\n", &tot, &fre, &av), -1);
|
||||||
|
/* 前缀不误匹配("MemTotally:" 之类) */
|
||||||
|
check("meminfo bogus key -> -1",
|
||||||
|
wd_rg_parse_meminfo("MemTotally: 1 kB\nMemFree: 2 kB\n", &tot, &fre, &av), -1);
|
||||||
|
|
||||||
|
/* 2) /proc/<pid>/stat 解析:comm 含空格 / 含 ')' 都要能取到正确的 ppid */
|
||||||
|
{
|
||||||
|
int pid = 0, ppid = 0;
|
||||||
|
char comm[WD_RG_COMM_LEN];
|
||||||
|
check("stat plain",
|
||||||
|
wd_rg_parse_stat("1234 (meow_audiod) S 1 1234 1234 0 -1 4194560", &pid, comm, sizeof comm, &ppid), 0);
|
||||||
|
check("stat plain pid", pid == 1234, 1);
|
||||||
|
check("stat plain ppid", ppid == 1, 1);
|
||||||
|
check("stat plain comm", !strcmp(comm, "meow_audiod"), 1);
|
||||||
|
/* 内核线程名(comm 内核上限 15 字节):取最后一个 ')' 之后的 state/ppid */
|
||||||
|
check("stat comm-with-space",
|
||||||
|
wd_rg_parse_stat("77 (kworker/0:1H) S 2 0 0 0 -1", &pid, comm, sizeof comm, &ppid), 0);
|
||||||
|
check("stat comm-with-space comm", !strcmp(comm, "kworker/0:1H"), 1);
|
||||||
|
check("stat comm-with-space ppid", ppid == 2, 1);
|
||||||
|
check("stat comm-with-paren",
|
||||||
|
wd_rg_parse_stat("88 (a)b) R 42 88 88 0 -1", &pid, comm, sizeof comm, &ppid), 0);
|
||||||
|
check("stat comm-with-paren comm", !strcmp(comm, "a)b"), 1);
|
||||||
|
check("stat comm-with-paren ppid", ppid == 42, 1);
|
||||||
|
check("stat malformed -> -1", wd_rg_parse_stat("no-parens here", &pid, comm, sizeof comm, &ppid), -1);
|
||||||
|
check("stat pid<=0 -> -1", wd_rg_parse_stat("0 (x) S 1", &pid, comm, sizeof comm, &ppid), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3) 父进程资格:kthreadd(2) 必须排除,孤儿桶(1) 必须保留 */
|
||||||
|
check("parent eligible 1 (orphan bucket)", wd_rg_parent_eligible(1), 1);
|
||||||
|
check("parent eligible 2 (kthreadd) -> 0", wd_rg_parent_eligible(2), 0);
|
||||||
|
check("parent eligible 0 -> 0", wd_rg_parent_eligible(0), 0);
|
||||||
|
check("parent eligible 1369", wd_rg_parent_eligible(1369), 1);
|
||||||
|
|
||||||
|
/* 4) 前 5 名父进程:事故形态 = 55 个 bluetoothctl 孤儿挂 ppid=1,同时 kthreadd 有 70 个
|
||||||
|
* 内核线程 —— 内核线程不计入,ppid=1 必须排第一。 */
|
||||||
|
{
|
||||||
|
int ppids[140];
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < 70; i++) ppids[n++] = 2; /* 内核线程 */
|
||||||
|
for (int i = 0; i < 55; i++) ppids[n++] = 1; /* bluetoothctl 残留 */
|
||||||
|
for (int i = 0; i < 9; i++) ppids[n++] = 1369; /* meow_svcd 子服务 */
|
||||||
|
for (int i = 0; i < 4; i++) ppids[n++] = 1315; /* launcher 子进程 */
|
||||||
|
int tp[WD_RG_TOP_PARENTS], tc[WD_RG_TOP_PARENTS];
|
||||||
|
int k = wd_rg_top_parents(ppids, n, WD_RG_TOP_PARENTS, tp, tc);
|
||||||
|
check("top parents count", k, 3); /* 只有 3 个合格父进程 */
|
||||||
|
check("top[0] is ppid=1", tp[0] == 1, 1);
|
||||||
|
check("top[0] children=55", tc[0] == 55, 1);
|
||||||
|
check("top[1] is svcd=9", tp[1] == 1369 && tc[1] == 9, 1);
|
||||||
|
check("top[2] is launcher=4", tp[2] == 1315 && tc[2] == 4, 1);
|
||||||
|
check("count_children(ppid=1)", wd_rg_count_children(ppids, n, 1), 55);
|
||||||
|
}
|
||||||
|
/* 平手按 ppid 升序(可复现,不随机) */
|
||||||
|
{
|
||||||
|
int ppids[6] = { 500, 400, 300, 500, 400, 300 };
|
||||||
|
int tp[WD_RG_TOP_PARENTS], tc[WD_RG_TOP_PARENTS];
|
||||||
|
int k = wd_rg_top_parents(ppids, 6, WD_RG_TOP_PARENTS, tp, tc);
|
||||||
|
check("top tie count", k, 3);
|
||||||
|
check("top tie order", tp[0] == 300 && tp[1] == 400 && tp[2] == 500, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5) 等级判定:真机空闲值必须 OK;每条阈值两侧各测一次 */
|
||||||
|
{
|
||||||
|
wd_rg_snap s;
|
||||||
|
memset(&s, 0, sizeof s);
|
||||||
|
s.total_kb = 56872; s.free_kb = 1424; s.avail_kb = 24808; s.procs = 100; s.maxch = 11;
|
||||||
|
check("level idle-device = OK", wd_rg_level(&s), WD_RG_OK);
|
||||||
|
s.avail_kb = WD_RG_AVAIL_WARN_KB; check("level avail==10240 -> OK", wd_rg_level(&s), WD_RG_OK);
|
||||||
|
s.avail_kb = WD_RG_AVAIL_WARN_KB - 1; check("level avail<10240 -> WARN", wd_rg_level(&s), WD_RG_WARN);
|
||||||
|
s.avail_kb = WD_RG_AVAIL_CRIT_KB; check("level avail==5120 -> WARN", wd_rg_level(&s), WD_RG_WARN);
|
||||||
|
s.avail_kb = WD_RG_AVAIL_CRIT_KB - 1; check("level avail<5120 -> CRIT", wd_rg_level(&s), WD_RG_CRIT);
|
||||||
|
s.avail_kb = 24808;
|
||||||
|
s.procs = WD_RG_PROCS_WARN - 1; check("level procs 139 -> OK", wd_rg_level(&s), WD_RG_OK);
|
||||||
|
s.procs = WD_RG_PROCS_WARN; check("level procs 140 -> WARN", wd_rg_level(&s), WD_RG_WARN);
|
||||||
|
s.procs = WD_RG_PROCS_CRIT; check("level procs 180 -> CRIT", wd_rg_level(&s), WD_RG_CRIT);
|
||||||
|
s.procs = 100;
|
||||||
|
s.maxch = WD_RG_CHILD_WARN - 1; check("level maxch 19 -> OK", wd_rg_level(&s), WD_RG_OK);
|
||||||
|
s.maxch = WD_RG_CHILD_WARN; check("level maxch 20 -> WARN", wd_rg_level(&s), WD_RG_WARN);
|
||||||
|
s.maxch = WD_RG_CHILD_CRIT; check("level maxch 40 -> CRIT", wd_rg_level(&s), WD_RG_CRIT);
|
||||||
|
/* MemAvailable 缺失(UNKNOWN)不得因为 0<5120 就报 CRIT */
|
||||||
|
s.maxch = 11; s.avail_kb = WD_RG_KB_UNKNOWN;
|
||||||
|
check("level avail-unknown -> OK", wd_rg_level(&s), WD_RG_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6) 命中规则名(写进告警行,读日志不用回代码查阈值) */
|
||||||
|
{
|
||||||
|
wd_rg_snap s;
|
||||||
|
char why[128];
|
||||||
|
memset(&s, 0, sizeof s);
|
||||||
|
s.total_kb = 56872; s.free_kb = 1424; s.avail_kb = 24808; s.procs = 100; s.maxch = 11;
|
||||||
|
wd_rg_reason(&s, why, sizeof why);
|
||||||
|
check("reason idle empty", why[0] == 0, 1);
|
||||||
|
s.avail_kb = 9000;
|
||||||
|
wd_rg_reason(&s, why, sizeof why);
|
||||||
|
check("reason warn avail", !strcmp(why, "avail<10240kB"), 1);
|
||||||
|
s.avail_kb = 4000; s.procs = 190; s.maxch = 45;
|
||||||
|
wd_rg_reason(&s, why, sizeof why);
|
||||||
|
check("reason crit all",
|
||||||
|
!strcmp(why, "avail<5120kB,procs>=180,children>=40"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 7) 日志节流:首次必打;同等级 5 分钟内静默;跃迁必打;回到 OK 必打;告警态 5 分钟复述 */
|
||||||
|
check("emit first", wd_rg_should_emit(WD_RG_OK, WD_RG_OK, 0, 1), 1);
|
||||||
|
check("emit same ok -> 0", wd_rg_should_emit(WD_RG_OK, WD_RG_OK, 100000, 0), 0);
|
||||||
|
check("emit ok->warn", wd_rg_should_emit(WD_RG_OK, WD_RG_WARN, 0, 0), 1);
|
||||||
|
check("emit warn sustained <5min", wd_rg_should_emit(WD_RG_WARN, WD_RG_WARN, 299000, 0), 0);
|
||||||
|
check("emit warn sustained >=5min", wd_rg_should_emit(WD_RG_WARN, WD_RG_WARN, WD_RG_REALERT_MS, 0), 1);
|
||||||
|
check("emit warn->crit", wd_rg_should_emit(WD_RG_WARN, WD_RG_CRIT, 0, 0), 1);
|
||||||
|
check("emit crit->ok (recovery)", wd_rg_should_emit(WD_RG_CRIT, WD_RG_OK, 0, 0), 1);
|
||||||
|
|
||||||
|
/* 8) 真机 /proc 冒烟:本进程必须能被扫到,且自身 ppid 合理(Linux 宿主机也适用) */
|
||||||
|
{
|
||||||
|
int np = wd_rg_scan(WD_RG_MAX_PIDS);
|
||||||
|
check("scan /proc ok", np > 0, 1);
|
||||||
|
check("scan finds self", wd_rg_name_of(np, getpid())[0] != '?' || np > 0, 1);
|
||||||
|
int tp[WD_RG_TOP_PARENTS], tc[WD_RG_TOP_PARENTS];
|
||||||
|
int k = wd_rg_top_parents(wd_rg_ppid, np, WD_RG_TOP_PARENTS, tp, tc);
|
||||||
|
check("scan top parents >=0", k >= 0, 1);
|
||||||
|
if (k > 0) check("scan top[0] not kthreadd", tp[0] != 2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
signal(SIGUSR1, on_sigusr1); /* 捕获 handle_touch 可能发出的 SIGUSR1(测命中) */
|
signal(SIGUSR1, on_sigusr1); /* 捕获 handle_touch 可能发出的 SIGUSR1(测命中) */
|
||||||
@@ -353,6 +499,9 @@ int main(void)
|
|||||||
test_time32();
|
test_time32();
|
||||||
test_power_matrix();
|
test_power_matrix();
|
||||||
|
|
||||||
|
/* ---- M52:资源压力守卫(meminfo/stat 解析 + 父进程排名 + 阈值 + 节流)---- */
|
||||||
|
test_resguard();
|
||||||
|
|
||||||
/* 清理 */
|
/* 清理 */
|
||||||
{
|
{
|
||||||
char path[560];
|
char path[560];
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/* 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 */
|
||||||
Reference in new issue
Block a user