P2-1 时长解析:后缀乘数只作用在整数部分,nsec 没跟着乘。 0.05m 被算成 50ms(应 3s)、1.5m 算成 60.5s(应 90s)、0.5h 算成 0.5s。 修法:sec 与 nsec 都乘 mult(各自带溢出检查,mult==1 行为不变)。 --help 与头注释的示例改为逐个标出等价值(2.5s=2.5s、1.5m=90s、0.5h=1800s), 不再留文档写着、实际算错的例子。 新增 T11:宿主上编译 parser 单测(27 例,含 0.01m/0.05m/1.5m/0.5h)真跑, 并单独点名评审抓到的三条,防止回归。 P2-2 文档数字:§2 产物表 47 708 → 49 676,合计 2 189 472 → 2 191 440; 全表按 MANIFEST 重核(其余七项未漂移),§11.1 补上新的体积/md5/sha256。 P2-3 信号失败不静默:新增 kill_child()(kill(-pgid) 失败时 ESRCH 视为组已消失, 退化为 kill(pid),EPERM 等明确告警)、verify_process_group()(发信号前用 getpgid 核对建组是否生效)、父进程 setpgid 的异常 errno 告警;连兜底 kill 都失败时退出码 125 而不是谎报 124。新增 -DTIMEOUT_TEST_SETPGID_FAIL 故障注入 与 T12(a-d)验证告警路径可达且兜底真的杀掉子进程。 真机复验(md5 aded39e0,49 676 B):124/137/0/孙进程清零/保证挂住的命令 3.01s 被 124 结束/0.01m=0.61s、0.05m=3.01s、2.5s=2.51s/无参 125;收工 bt=0 sleep=0, Music 199164544。回归 44 通过 0 失败。
535 lines
35 KiB
Markdown
535 lines
35 KiB
Markdown
# MeowOS devkit — 静态 MIPS32r2 调试工具集
|
||
|
||
TF 卡 `MeowOS/opt/devkit/bin/` 里那批静态二进制的构建说明、可行性实测、许可证留痕。
|
||
构建入口:`bash scripts/build/build_devkit.sh`;回归测试:`bash scripts/build/test_build_devkit.sh`
|
||
(热缓存约 15 秒;冷启动需先重建基线,1–2 分钟)。
|
||
产物在 `fw_apps/devkit/`,已被 `.gitignore` 的 `fw_apps/` 规则覆盖,不入库。
|
||
|
||
## 1. 为什么要有这个
|
||
|
||
设备(HiBy R3 Pro II / Ingenic X1600 / MIPS32r2 / Linux 4.4.94+ / 单核 / 56 MB)自带
|
||
busybox 1.31.1:`free / ps / top / lsof` 是 busybox 的软链(实测 `/bin/ps -> busybox`、
|
||
`/usr/bin/{free,lsof,top} -> ../../bin/busybox`),字段残缺;`vmstat`、`slabtop`、`pmap`、
|
||
`sysctl` **没有对应 applet**(`busybox --list` 实测:有 `pgrep`/`kill`/`killall`,无 `pkill`/`sysctl`)。
|
||
做内存分析时最缺的正是:
|
||
|
||
- `slabtop` —— Slab 总量 ~11 MB 到底被谁占了(buffer_head / dentry / squashfs_inode…)
|
||
- `vmstat` —— 系统级趋势(r/b、si/so、in/cs、us/sy/id/wa),busybox 无替代
|
||
- 真版 `lsof` —— busybox lsof 只有 pid/fd/设备号,没有 SIZE/OFF、TCP 状态、NAME
|
||
|
||
所以本工具集只围绕「这台机器的真实调试痛点」,不做大而全。
|
||
|
||
## 1.1 ⚠ 设备侧「禁裸跑」纪律 —— 2026-11-11 整机崩塌的直接教训
|
||
|
||
**给下一个会话的救命提示,不是官腔。先读完这一节再敲命令。**
|
||
|
||
### 发生了什么(已取证的硬事实)
|
||
|
||
| 时刻(2026-11-11) | 事件 |
|
||
|---|---|
|
||
| 15:45:40 | 设备开机,一切正常 |
|
||
| 18:44:13 | `meow_launcherd` 最后一行日志 |
|
||
| 18:44:14 | sshd 最后一个 child 正常退出 |
|
||
| 18:44:14 之后 **11 分钟** | `ssh.log` **一行都没有**(连 `Child connection from` 都没记)→ dropbear 连 `fork()` 都做不到 |
|
||
| 同一时段 | ICMP 仍应答、RTT 163ms → 网络栈活着,**是进程表被耗尽**,不是掉网 |
|
||
| 崩溃前累积 | **55 个挂死的 `bluetoothctl` 客户端进程** —— 历次对着一台**已关机**的耳机裸跑 `bluetoothctl connect` 的残留 |
|
||
|
||
因果链:**裸跑会挂在硬件上的外部命令 → 每个都永久阻塞并持有资源 → 55 个之后
|
||
内核无法再 fork → dropbear 无法派生会话 → 整机"在线但不可登录" → 只能重启。**
|
||
|
||
### 纪律(没有例外)
|
||
|
||
1. **设备侧禁止裸跑任何可能挂在硬件上的外部命令**:`bluetoothctl`、`hciconfig`、
|
||
`hcitool`、`wpa_cli`、`rfkill`,以及一切由 `popen`/`system()` 起的子进程。
|
||
一律加超时:
|
||
```sh
|
||
timeout 8 bluetoothctl connect <MAC> # 首选:连它 fork 的孙进程一起杀
|
||
bluetoothctl --timeout 8 connect <MAC> # 备选:bluetoothctl 5.54 自带 --timeout
|
||
```
|
||
2. **循环重试必须封顶次数**,并且每次迭代都用 `timeout` 包住。
|
||
`while true; do bluetoothctl connect ...; done` 是**事故配方**,不是重试。
|
||
3. 收工前必须核对残留:`ps w | grep -c bluetoothctl` 回到基线(正常 0),
|
||
`ps` 里不应有自己留下的 `sleep`/`bluetoothctl`。
|
||
4. 设备是**单核 1GHz / 56MB**:并发 3 个以上挂死进程就足以让整机不可用。
|
||
这不是"性能慢",是**进程表耗尽**。
|
||
|
||
### 为什么是 `timeout`(而不是别的)
|
||
|
||
设备原本**没有** `timeout(1)`:`command -v timeout` 为空,busybox 1.31.1 也无此 applet
|
||
(`busybox --list | grep -x timeout` 无匹配)。所以本 devkit 自带一个(见 §2、§3.2),
|
||
它就是这条纪律的可执行形态:
|
||
|
||
```sh
|
||
timeout 5 sh -c 'sleep 100 & sleep 100 & wait' # 孙进程也必须死,ps 里不留 sleep
|
||
```
|
||
|
||
|
||
## 2. 产物清单
|
||
|
||
全部:`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
|
||
`readelf -d` 零 `NEEDED`(musl libc 静态链入),可直接从 exFAT 卡上跑。
|
||
|
||
| 工具 | 来源版本 | 许可证 | 体积(B) | 用途 / 相对 busybox 的增量 |
|
||
|---|---|---|---|---|
|
||
| `slabtop` | procps-ng 4.0.4 | GPL-2.0-or-later(依赖 libproc2 LGPL-2.1+) | 394 408 | Slab 明细;`-o` 一次性输出不需要 TTY/terminfo |
|
||
| `vmstat` | procps-ng 4.0.4 | 同上 | 216 924 | 系统趋势;busybox 无;`-t` 时间戳列本工具链下为空,见 §9 |
|
||
| `ps` | procps-ng 4.0.4 | 同上 | 324 980 | `-eo pid,rss,vsz,comm --sort=-rss` 等完整字段 |
|
||
| `pmap` | procps-ng 4.0.4 | 同上 | 173 460 | 地址空间映射(读 `/proc/pid/maps`);busybox 无 |
|
||
| `sysctl` | procps-ng 4.0.4 | 同上 | 148 184 | 读写 `/proc/sys`(`-a` 列出 428 个键);busybox 1.31 无此 applet |
|
||
| `free` | procps-ng 4.0.4 | 同上 | 94 312 | 拆 buff/cache、available |
|
||
| `lsof` | lsof 4.99.4 | Purdue/BSD-style | 789 496 | SIZE/OFF、TCP 状态、NAME、`-i` 网络视图 |
|
||
| `timeout` | 本仓 `devkit-src/timeout.c` | **Apache-2.0**(自有独立实现,非 busybox/GPL 代码) | 49 676 | 给任何外部命令加"最多等 N 秒、然后杀掉整个进程组";busybox 1.31 无此 applet |
|
||
| | | **合计** | **2 191 440 ≈ 2.1 MB** | |
|
||
|
||
`timeout` 走的是**本地源码编译路径**(`scripts/build/devkit-src/*.c` → `build_local`),
|
||
与上游包(procps/lsof/ncurses)的「下载 + sha256 + configure/make」流程完全隔离:
|
||
不联网、不 patch、不打乱上游构建参数。新增本地工具只需往该目录丢一个 `.c`。
|
||
|
||
同目录还有 `MANIFEST.txt`(每个二进制的 sha256 + 版本 + 许可证 + 体积)、
|
||
`licenses/`(procps COPYING + COPYING.LIB、ncurses COPYING、lsof COPYING、**musl COPYRIGHT**、
|
||
**meowos-timeout-LICENSE**=Apache-2.0 原文)、
|
||
`README.txt`(上卡说明)。`--with-sources` 会额外生成 `src/`(三份源码包 + `build_devkit.sh` + 本文件 + `timeout.c`)。
|
||
|
||
## 3. 上卡后怎么用
|
||
|
||
```sh
|
||
# 临时(SSH 会话)
|
||
export PATH=/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin:$PATH
|
||
|
||
# 持久化:rootfs 的 /etc/profile.d/meow_card_path.sh 只前插了 MeowOS/opt/bin,
|
||
# devkit 是独立目录,需要再补一行(由塔/维护者决定是否进固件):
|
||
if [ -d /usr/data/mnt/sd_0/MeowOS/opt/devkit/bin ]; then
|
||
export PATH="/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin:$PATH"
|
||
fi
|
||
```
|
||
|
||
**与 busybox 同名命令的先后关系**(前插 PATH 时):
|
||
|
||
| 名字 | 现状(实测) | 前插后 |
|
||
|---|---|---|
|
||
| `free` `lsof` `top` | `/usr/bin/x -> ../../bin/busybox` | `free`/`lsof` 解析到 devkit 版;`top` 仍是 busybox(本工具集不提供) |
|
||
| `ps` | `/bin/ps -> busybox` | 解析到 devkit 版 |
|
||
| `vmstat` `slabtop` `pmap` `sysctl` | 不存在(busybox 无对应 applet) | 新增 |
|
||
| `timeout` | 不存在(busybox 1.31 无对应 applet;`command -v timeout` 为空) | 新增 |
|
||
|
||
要临时用回 busybox 版:`busybox free`、`busybox ps`、`busybox lsof`。
|
||
不要覆盖 `/usr/bin`、`/bin` 里的软链:rootfs 是 ubifs,随固件回刷即被还原,而且这些软链是基线的一部分。
|
||
|
||
### 3.1 ⚠ SSH 是非登录 shell:PATH 里没有 `MeowOS/opt/bin`
|
||
|
||
这是后续会话反复踩的坑,实测(2026-11-11):
|
||
|
||
```sh
|
||
ssh root@<ip> 'echo $PATH'
|
||
# /usr/sbin:/usr/bin:/sbin:/bin ← 没有 MeowOS/opt/bin,也没有 devkit/bin
|
||
ssh root@<ip> 'command -v timeout' # 空
|
||
ssh root@<ip> 'timeout 1 sleep 5' # sh: timeout: not found (rc=127)
|
||
```
|
||
|
||
`/etc/profile.d/meow_card_path.sh` 只对**登录 shell** 生效;`ssh host 'cmd'` 走的是
|
||
非登录 shell。所以卡上的工具必须用下面两种方式之一调用:
|
||
|
||
```sh
|
||
sh -lc 'timeout 1 sleep 5' # 登录 shell → PATH 带 opt/bin
|
||
/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout 1 sleep 5 # 绝对路径(最稳)
|
||
```
|
||
|
||
**设备上没有 `bash`**(实测 `command -v bash` → MISSING,只有 busybox sh),
|
||
所以写 `bash -lc` 会得到 `sh: bash: not found`;要用 `sh -lc`。
|
||
实测 `sh -lc 'echo $PATH'` → `/usr/data/mnt/sd_0/MeowOS/opt/bin:/bin:/sbin:/usr/bin:/usr/sbin`,
|
||
`command -v timeout` 解析到 `/usr/data/mnt/sd_0/MeowOS/opt/bin/timeout`(能跑)。
|
||
|
||
`MeowOS/opt/bin/timeout` 也放了同一份(launcher 的 PATH 会 prepend 它),
|
||
但**不要指望 ssh 一行命令里能直接命中**。
|
||
|
||
### 3.2 `timeout` 用法(本仓 Apache-2.0 实现)
|
||
|
||
```sh
|
||
timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARGS...]
|
||
|
||
timeout 2 sleep 30 # ~2s 返回,rc=124
|
||
timeout -s KILL 2 sleep 30 # rc=137
|
||
timeout -k 1 5 sh -c 'sleep 100 & sleep 100 & wait' # TERM 后 1s 仍在 → KILL,孙进程一起死
|
||
timeout 0.2 sleep 5 # 支持小数秒(~200ms 返回)
|
||
timeout 0.5m sleep 60 # 小数 + 后缀是叠加的:0.5m = 30 秒
|
||
timeout 8 bluetoothctl connect XX:XX:XX:XX:XX:XX # 挂死的外部命令的默认包装
|
||
```
|
||
|
||
- **时长解析**:`纯数字=秒`,可带小数,可带 `s/m/h`,**小数与后缀叠加**:
|
||
`2.5s`=2.5s、`0.05m`=3s、`1.5m`=90s、`0.5h`=1800s、`0.01m`=0.6s。
|
||
(r1 评审抓到的 P2:旧实现只把后缀乘到整数部分,`0.05m` 会变成 50ms——
|
||
「比要求更早被杀」,静默给错答案。现有 `test_build_devkit.sh` T11 真跑 parser 回归。)
|
||
- 超时信号发给 COMMAND 的**整个进程组**(`setpgid(0,0)` + `kill(-pgid, SIG)`),
|
||
命令自己 fork 的孙进程一并终止;直接子进程一定被 `waitpid` 回收,不留僵尸。
|
||
- 两阶段:先 `-s`(默认 TERM),`-k` 到期再 KILL。不是忙等,计时全在内核。
|
||
- **杀不动时不装作成功**(18:44 事故的形态就是"进程没被杀掉、越积越多"):
|
||
发信号前用 `getpgid()` 核对进程组;`kill(-pgid)` 返回 `ESRCH` 视为"组已不存在",
|
||
退化为 `kill(pid, sig)` 只杀直接子进程;`EPERM` 等真错误会打一行明确告警;
|
||
连兜底 kill 都失败时退出码是 **125**(不是 124)——不谎报"已超时处理"。
|
||
- 退出码:`124` 超时;`137` 超时且最终发了 KILL;`128+N` 命令自己死于信号 N;
|
||
`125` timeout 自身参数/系统调用错或信号完全发不出去;`126` 命令无法执行;`127` 命令不存在。
|
||
- 不支持 `--preserve-status` / `-v` / `d` 后缀,给了会明确报 125(不静默忽略)。
|
||
- 不跟踪 `setsid()` 逃出进程组的守护进程(coreutils 也不跟踪)。
|
||
|
||
**5 行自检(每次上卡后复跑,全绿才算 timeout 可用)**:
|
||
|
||
```sh
|
||
T=/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout
|
||
$T 2 sleep 30; echo "t1=$? (want 124)"; $T -s KILL 2 sleep 30; echo "t2=$? (want 137)"
|
||
$T 2 sleep 1; echo "t3=$? (want 0)"
|
||
$T 5 sh -c 'sleep 100 & sleep 100 & wait'; echo "t4=$? (want 124)"
|
||
for p in /proc/[0-9]*; do [ "$(cat $p/comm 2>/dev/null)" = sleep ] && echo "LEAK $p"; done
|
||
```
|
||
|
||
带后缀小数的自检(秒数用 `date` 前后的墙钟差核对,见 §11.1):
|
||
|
||
```sh
|
||
$T 0.01m sleep 5; echo "u1=$? (want 124, ~0.6s)"; $T 0.05m sleep 5; echo "u2=$? (want 124, ~3s)"
|
||
```
|
||
|
||
## 4. exFAT 语义(权限 / 软链)
|
||
|
||
设备挂载:`/dev/mmcblk0 on /usr/data/mnt/sd_0 type exfat (rw, fmask=0022, dmask=0022, symlink=0, noexec 未设置)`。
|
||
|
||
- **无符号链接**(`symlink=0`):本工具集全部是实体文件,不做 `argv[0]` 分派,也不提供 symlink
|
||
(不像 busybox 那样一个多合一二进制 + 一堆软链)。
|
||
- **无 Unix 权限位**:`fmask=0022` 让卡上新文件恒为 `0755`,因此**不需要 chmod**——
|
||
反过来说 `chmod` 在这里本身是 no-op;卡上也表达不了 setuid,任何需要 setuid 的工具都不适合放这。
|
||
- **可执行性**:挂载参数里没有 `noexec`,且卡上现有 `MeowOS/opt/MeowPanel/meowpanel` 就是 0755 直接执行的。
|
||
本轮真机验证是在设备 `/tmp`(同为 MIPS 目标、root 身份)做的,**没有往卡上写过任何东西**(约定:卡由塔推)。
|
||
|
||
## 5. 重跑代价与可复现性
|
||
|
||
- **联网次数**:冷启动 **3 次**(ncurses / procps-ng / lsof 各一个 tarball,共 5.5 MB);
|
||
源码包按 sha256 校验后落在 `$MEOW_DEVKIT_WORK/src/`,此后**重跑 0 次联网**。
|
||
- **耗时**(8 核宿主实测):冷启动(含 3 次下载)**80–86 秒**;源码已缓存时全量重编 **60–85 秒**;
|
||
`stamp` 命中(脚本、版本、zig 版本都没变)**2–3 秒**;回归测试热缓存 **约 15 秒**(冷启动基线另算)。
|
||
- **`--verify-only`**:只做静态/架构/NEEDED 检查 + MANIFEST 逐条 sha256/体积比对 +
|
||
MANIFEST/README/licenses 完整性检查。**不需要 zig、make、date、网络**;
|
||
实际依赖 `file`、`readelf`、`sha256sum`、`awk`、`sed`、`grep`、`wc`、`cut`、`basename`。
|
||
- **stamp 内容**:脚本 sha256 + zig 完整版本(0.17.0 / 0.17.1 会区分)+ target + 内核版本 + 三个上游版本。
|
||
- **可复现(逐字节)**:在两个不同工作区各建一次,七个二进制 **sha256 完全相同**
|
||
(`ps` 用 `-ffile-prefix-map` 抹掉绝对源码路径;`lsof` 用裸名 `CC=zigcc` + 相对源码路径,
|
||
不再把工具链/构建目录写进 `LSOF_CC/LSOF_CCFLAGS`)。`MANIFEST.txt` 里的 sha256 与设备上
|
||
`sha256sum` 的结果一致,可用来核对「跑的到底是哪个二进制」。
|
||
- **skip 的完整条件**:stamp 未变 **且** 七个 bin 通过静态/架构自检 **且** MANIFEST 与 bin 逐条一致
|
||
**且** MANIFEST/README/licenses 齐全。任一不满足就补(meta 缺失时直接重写 meta,不必重编)或重建。
|
||
|
||
## 6. 许可证与分发合规(重点)
|
||
|
||
| 上游 | 许可证 | 分发二进制时的义务 |
|
||
|---|---|---|
|
||
| procps-ng 4.0.4(apps) | **GPL-2.0-or-later** | **提供完整对应源码**(GPLv2 §3),含 "scripts used to control compilation and installation" |
|
||
| procps `library/`(libproc2) | **LGPL-2.1+** | `ps/vmstat/slabtop/free/pmap/sysctl` **静态链接**了它 → LGPL-2.1 §6 另有要求,见下 |
|
||
| ncurses 6.5 | MIT 风格(X11) | 保留版权与许可声明;只静态链入 slabtop,不单独分发 |
|
||
| lsof 4.99.4 | Purdue Research Foundation 许可(BSD 风格 4 条) | 文档与源码中保留作者/Purdue 署名;改过的版本要标注 |
|
||
| **musl**(zig 0.17 自带) | MIT | 七个产物都静态链入了它 → 随副本保留版权与许可声明(`licenses/musl-COPYRIGHT`) |
|
||
| **timeout** | **Apache-2.0**(本仓自有) | 源码 `scripts/build/devkit-src/timeout.c`,独立实现,**未拷贝 busybox/GPL 代码**;分发时附 `licenses/meowos-timeout-LICENSE`(Apache-2.0 原文) |
|
||
|
||
**GPLv2 §3**:`build_devkit.sh` 里的固定版本 + 下载 URL **不构成合规**。URL 作者可能撤、
|
||
镜像可能消失,而且 GPL 要求的是*分发者*提供的源码或书面要约。因此 `--with-sources` 会把
|
||
**三份源码包 + `build_devkit.sh` + 本文件 + `timeout.c`** 一起放进 `src/`(合计约 5.6 MB)。
|
||
少一样都不算完整:没有构建脚本,收卡人拿不到本轮用的 `configure`/`-D` 参数
|
||
(这正是 "corresponding source" 明确包含的)。
|
||
|
||
**timeout(Apache-2.0,本仓自有)**:它与上面这些 GPL/LGPL 组件**没有任何源码混合**,
|
||
不是从 busybox 改的(busybox 的 `coreutils/timeout.c` 是 GPL-2.0,搬进来会造成许可污染)。
|
||
`timeout.c` 是照着 fork/setpgid/kill/setitimer/signalfd 的系统调用语义从零写的。
|
||
Apache-2.0 §4 要求分发时附许可证原文 → `licenses/meowos-timeout-LICENSE`;
|
||
`src/timeout.c` 随 `--with-sources` 一起上卡(可复现性,不是 GPL 义务)。
|
||
|
||
**LGPL-2.1 §6(静态链接)**:六个 procps 工具静态链入 libproc2,除 GPL 义务外还必须
|
||
「显著提示使用了该库 + 附许可证副本 + 三选一」:
|
||
**(§6a)** 随作品提供该库的完整源码(含修改);
|
||
**(§6b)** 改用共享库机制(静态交付做不到);
|
||
**(§6c)** 书面要约(≥3 年)提供 §6a 的材料。
|
||
本工具集走 **§6a**:`src/procps-ng-4.0.4.tar.xz` 含 `library/` 全量源码,
|
||
`src/build_devkit.sh` 是本轮的唯一构建描述,`licenses/procps-ng-4.0.4-COPYING.LIB` 是许可证副本。
|
||
若想更保守(收卡人不想自己重编),可再额外提供可重链接的 `.o`(当前**未做**,属可选加固)。
|
||
|
||
**musl(MIT)**:MIT 要求在副本中保留版权与许可声明,因此 `licenses/musl-COPYRIGHT` 是必需项
|
||
(构建脚本从 `zig env` 的 `lib_dir/libc/musl/COPYRIGHT` 取,取不到直接失败)。
|
||
|
||
**结论**:内部自用不触发分发义务;但既然这批二进制可能被拷来拷去,建议一律
|
||
`--with-sources` 上卡。`licenses/` 只是声明留痕,**不能替代源码义务**。
|
||
|
||
## 7. 未交付清单(以及为什么)
|
||
|
||
| 候选 | 结论 | 原因 |
|
||
|---|---|---|
|
||
| `top`(procps 4.0.4) | **不交付** | 表头时钟被工具链 time64 问题打坏(显示 `-1202590843:00` 一类),无 TTY 时还会静默 `exit 1`。**不是 procps 的 bug**(r1 评审已更正我此前的错误归因),根因见 §9。卡上已有 htop,`top` 属重复 |
|
||
| `htop` | 不构建 | 卡上 `MeowOS/opt/bin/htop` 已是 **3.5.3**(673 744 B)。实测用 zig + 自编 ncursesw 也能编出 3.5.3,但 strip 后 **756 908 B**,比现有版本更大(静态链 musl+ncurses)。**未做运行时验证**;按代码路径,它的亚秒刷新走 ncurses `halfdelay()`(内部 `select()` + 32 位 `struct timeval`),**大概率不受 §9 的 time64 影响**,真正会受影响的是时钟显示(`localtime_r`/`clock_gettime`)与部分采样 |
|
||
| `watch` | 不构建 | busybox 1.31 已有 `watch`;procps 版 403 KB,收益不抵体积 |
|
||
| `pgrep` | 不构建 | busybox 已有 `pgrep`;procps 版 260 KB(链接整份 libproc),增量有限 |
|
||
| `pkill` | **不构建** | busybox **没有** `pkill` applet(只有 `kill`/`killall`,`killall` 只能部分替代);procps 版 260 KB,本轮未做 |
|
||
| `uptime`/`tload`/`pidof`/`pidwait`/`w` | 不构建 | busybox 已有 `uptime`/`pidof`;`tload`/`w` 在本机无调试价值 |
|
||
| `perf` | 不可行 | 依赖目标内核源码树(`tools/perf` 要按内核版本配 `perf_event_attr`),没有内核头,属硬阻塞 |
|
||
| `valgrind` | **未尝试** | valgrind **支持 MIPS32/Linux**(官方平台页有 MIPS32/MIPS64),所以不是「无后端」;但交叉构建需 autotools 工具链 + 目标内核/vDSO 细节,产物通常 10 MB+,在 56 MB 单核设备上不实用。本轮未做可行性验证 |
|
||
| `ltrace` | **未做(成本高)** | 需要 libelf + 复杂 autoconf 探测,且本机程序多为静态/精简;属投入产出判断,不是「不可能」 |
|
||
| `sysstat`(sar/iostat/mpstat) | **未做(成本高)** | autoconf 探测多、依赖 `gettext`/`libsensors` 等;本机单核、`/proc/diskstats` 对 TF 卡意义有限 |
|
||
| `gdb` | 不建议 | 设备已带 `gdbserver`,宿主侧可用交叉 gdb 连;在 56 MB 单核设备上跑 gdb 本体收益极低、体积巨大 |
|
||
| `smem` | 价值为零 | 依赖 `/proc/<pid>/smaps` 的 PSS,本机没有 smaps |
|
||
|
||
`pmap` 的低价值要说明白:本机 `CONFIG_PROC_PAGE_MONITOR=n`,`/proc/<pid>/smaps` 不存在,
|
||
所以 `pmap -x` 拿不到 PSS/私有脏页。它仍有用的一点是 `/proc/<pid>/maps` **存在**,
|
||
`pmap <pid>` 能列出地址段 + 映射文件名(busybox 没有等价物),排查「谁映射了这块内存」够用。
|
||
|
||
## 8. 可行性实测记录(编不出来的卡在哪)
|
||
|
||
宿主:Termux/Android aarch64,zig 0.17.0(自带 musl libc 与头文件),`make`、`curl`、`pkg-config`。
|
||
**没有** autoconf/automake/libtool,**没有**目标内核头,sysroot 里只有 zlib(本工具集不需要它)。
|
||
|
||
- **procps-ng 4.0.4** ✅ 可编(vmstat/slabtop/free/ps/pmap/sysctl 全部产出)。三个必须处理的坑:
|
||
1. `slabtop`(`src/slabtop.c` 直接 `#include <ncurses.h>`)硬依赖 ncurses;
|
||
`configure --without-ncurses` 会把它整个跳过 → 要 `slabtop` 就必须先交叉编 ncurses。
|
||
2. 交叉编译下 autoconf 的 `malloc(0)/realloc(0)` 运行期探测无法执行,不缓存
|
||
`ac_cv_func_malloc_0_nonnull=yes ac_cv_func_realloc_0_nonnull=yes` 会引入
|
||
`rpl_malloc/rpl_realloc` 未定义引用,链接失败。
|
||
3. 必须把 `PKG_CONFIG_LIBDIR` 收紧到自编 prefix,否则会捡到宿主 aarch64 的
|
||
`ncursesw.pc`,结果是「musl 头 + bionic 头」混编(`watch` 直接编不过)。脚本里有事后断言。
|
||
- **ncurses 6.5** ✅ 可编(`--without-shared --with-normal --enable-widec --without-progs` 等)。
|
||
**必须** `--with-default-terminfo-dir=/usr/share/terminfo --with-terminfo-dirs=...`:
|
||
否则交互式 `slabtop` 会 `Error opening terminal: xterm.`(默认值是编译期 prefix,设备上不存在)。
|
||
体积代价:`libncursesw.a` 3.3 MB(未 strip,不单独分发),链进 `slabtop` 后只增加约 100 KB。
|
||
- **lsof 4.99.4** ✅ 可编。坑:
|
||
1. 4.99 源码包**不带** `configure`(autotools),但带老式 `Configure` 脚本,`./Configure -n linux` 可用。
|
||
2. 版本探测读宿主 `/usr/include/linux/version.h` + `uname`,交叉时拿不到 4.4.94,
|
||
会退化成 `LINUXV=00000 / LSOF_VSTR="0.0.0"`(Configure 打 "unsupported dialect or version")。
|
||
这两个宏在 linux dialect 里只影响 `lsof -v` 显示,仍用 `-DLINUXV/LSOF_VSTR` 覆盖成 4.4.94。
|
||
3. zig 的 musl 头没有 glibc 侧经由 `linux/tcp.h` 间接带来的 `TCP_ESTABLISHED` 等 11 个
|
||
状态宏,`dsock.c` 会编不过 → 脚本里 `-D` 补齐(值取自 `linux/tcp.h`)。
|
||
4. zig cc **不带 `-O` 时按 Debug 编**,会插入 UBSan 桩(`__ubsan_handle_negate_overflow`)
|
||
导致链接失败 → 所有编译都显式 `-O2`。
|
||
- **htop 3.5.3** ✅ 可编(用自编 ncursesw),strip 后 756 908 B —— 比卡上现有版本大,
|
||
且未做运行时验证,故不交付(见 §7)。
|
||
- **perf** ❌ 不可行:依赖目标内核源码树/头文件。
|
||
- **valgrind** ⚠️ 未尝试(支持 MIPS32/Linux,但交叉构建与体积/内存代价不划算;见 §7)。
|
||
- **ltrace** / **sysstat** ⚠️ 未做(成本高,见 §7)。
|
||
- **smem** ⚠️ 价值为零:依赖 smaps,本机没有。
|
||
|
||
## 9. 已知限制:工具链的 time64 在 4.4 内核上不可用
|
||
|
||
**这不是 procps/htop 的 bug,是 zig 0.17 自带 musl 在 mipsel 上的 ABI 错配**(r1 评审定根因,
|
||
我此前把 `top` 的表头问题误判成 procps bug,此处更正)。
|
||
|
||
机制(三段,均可复验):
|
||
|
||
1. zig 0.17 的 `mipsel-linux-musleabi` 是 **time64**:`sizeof(time_t)=8`、`sizeof(long)=4`,
|
||
`struct timespec` 16 字节;同时 clang 前端给 mips o32 预定义了 `__ILP32__`。
|
||
2. zig 自带 musl(`libc/musl/src/time/clock_gettime.c`、`time/clock_nanosleep.c` 等)的守卫写成
|
||
`#if defined(SYS_*_time64) && !defined(__ILP32__)`;上游 musl 1.2.5 没有这个 `!defined(__ILP32__)`。
|
||
3. MIPS o32 是 ILP32、内核 4.4 又没有 time64 syscall → 走 `#else` 分支,把 **16 字节的 time64 结构**
|
||
直接交给 **32 位 time32 系统调用**。内核按 `{int32 tv_sec; int32 tv_nsec}` 解释这 16 字节:
|
||
|
||
- `clock_gettime`:内核只写前 8 字节 → 调用者的 `tv_sec`(offset 0..7)= `sec | (nsec << 32)`,
|
||
**高 32 位是内核写入的 nsec**;而 offset 8 的 `tv_nsec` **完全没被写,保持未初始化**
|
||
(评审 memset 0xAA 实测读到 `nsec=0xAAAAAAAA`)。
|
||
- `gettimeofday` 同理:`tv_sec` 高半字是内核写入的 usec,offset 8 的 `tv_usec` 未初始化。
|
||
- `nanosleep`:内核把 `tv_sec` 的**高半字当 nsec 读**(小秒数时为 0),调用者真正的 `tv_nsec`
|
||
(offset 8)被忽略 → 亚秒被抹平、整秒正常。
|
||
|
||
真机实测(本轮):
|
||
|
||
| 现象 | 结果 | 影响 |
|
||
|---|---|---|
|
||
| `clock_gettime` / `time()` / `gettimeofday()` | rc=0 但值是垃圾(评审另测:`sec=2823213556008072608`,`localtime_r` 返回 NULL) | 任何按墙钟格式化时间的功能 |
|
||
| `vmstat -t` | 表头有 `-----timestamp-----`,**数据行该列整列为空**(`localtime()` 返回 NULL) | `vmstat` 核心的 3 组计数**正常**;只有时间戳列不可用 |
|
||
| `free -s 0.5 -c 2` | **实测 0 秒**(应约 0.5 秒):`free.c` 用 `usleep(interval)`,亚秒被抹平 | **小数间隔失效**;`free -s 1 -c 2` 实测 1 秒,正常 |
|
||
| `top` 表头时钟 | 显示 `-1202590843:00` 一类 | §7 不交付 top 的原因 |
|
||
| `usleep(0.5s)` ×10(独立探针) | **实测 0 秒**(应 5 秒) | 亚秒轮询会退化成空转 |
|
||
| `sleep(1)`(整秒) | 正常:`vmstat 1 3` 墙钟实测 2 秒、`vmstat 1 2` 实测 1 秒 | 整秒采样周期不受影响 |
|
||
| `ps -eo lstart` | 正常 | 时间来自 `/proc/stat` 的 `btime` + jiffies,不走 libc 取时 |
|
||
| `slabtop`/`pmap`/`sysctl`/`lsof` | 不受影响 | 不显示墙钟、也不做亚秒等待 |
|
||
| `timerfd_settime()`(2026-11-11 新测) | **静默失效**:返回 0、`errno=0`,但计时器根本没启动(`timerfd_gettime` 显示 `0.000000000`,`poll` 3 秒等不到)。用 32 位 timespec 结构的原始 syscall(MIPS o32 `4323`)或 `setitimer` 都正常 | 任何用 `timerfd` 做超时的实现都会变成"永不超时"——`timeout` 第一版就栽在这里:`timeout 2 sleep 30` 等满 30 秒、`rc=0` |
|
||
| `setitimer(ITIMER_REAL)` + `SIGALRM`(2026-11-11 新测) | **正常**:1s / 200ms 都按点触发(musl 对 `setitimer` 做了 32 位 timeval 转换) | `timeout` 最终用它计时,配上 `signalfd`(SIGALRM + SIGCHLD)与 `ppoll`,零轮询 |
|
||
|
||
> **教训**:这台机器上"time64 家族"的坏法不止一种。`clock_gettime` 是返回垃圾值,
|
||
> `timerfd_settime` 是**返回成功但什么都不做**——后者更阴险,因为没有错误码可查。
|
||
> 新写任何计时/睡眠代码,先在设备上跑一个最小探针验证行为,不要假设 libc 正常。
|
||
> 仓里 M16 的 `patch_v2/sdk/meow_ui_time.c` 正是接管了 `__timerfd_*` 一族,
|
||
> 侧面印证了这条路径需要修。
|
||
|
||
**能否复用仓里已有的修复?** 有。M16 的 `patch_v2/sdk/meow_ui_time.c`(文档 `patch_v2/sdk/CLOCK_ABI.md`)
|
||
用「链接期强定义」接管了 `__clock_gettime` / `__clock_gettime64` / `__nanosleep_time64` /
|
||
`__clock_nanosleep(_time64)` / `__timedwait*` / `__timerfd_*` / `__clock_settime64`,
|
||
内部按内核 o32 time32 ABI 取数再升成 64 位。
|
||
|
||
- **可复用的证据(本轮实测)**:`zigcc -O2 -I. -Iinclude -c patch_v2/sdk/meow_ui_time.c` 单文件编译成功,
|
||
未定义符号只有 5 个 libc 符号(`__errno_location`、`__pthread_setcancelstate`、`__syscall_cp`、
|
||
`__udivdi3`、`syscall`),不依赖 SDK 其它目标文件。
|
||
- **代价**:必须把该 `.o` 放在 libc 之前参与链接(才会盖掉 libc 里那份),要给两套构建系统分别注入:
|
||
procps 走 libtool 的 `LDADD`、lsof 是手写 Makefile 的固定链接行;然后七个二进制全部重新真机验证。
|
||
估计 1–2 小时(含设备回归),产物每个 +几 KB。
|
||
- **本轮为什么不做**:收益是 `vmstat -t` 时间戳列 + `free -s <小数>` 两处(`top` 不交付、`ps` 本来正常、
|
||
其余不打时间),而这个改动会动到 libc 的取时/睡眠入口,风险面比收益大。
|
||
**结论:可复用,但需要单独一条 mission 做「接入 + 全量回归」**,不适合塞进修 P2 的补丁里。
|
||
- **不加 shim 的替代方案**:给 zig 的 musl 打补丁(去掉 `!defined(__ILP32__)`)或换上游 musl 工具链。
|
||
**这不是加一个 `-D` 能修的**:守卫在 libc 源码里,编译产物(libc.a)已经固定。
|
||
|
||
## 10. 路径清洁:lsof 里有一处宿主路径(工具链副作用)
|
||
|
||
`strings lsof` 有一处 `/data/data/com.termux/files/usr/etc/resolv.conf`(其余六个产物没有宿主路径)。
|
||
来源是 **Termux 打包的 zig 自带 musl**(`libc/musl/src/network/resolvconf.c` 被改成 Termux prefix),
|
||
不是 lsof 源码,也不是本脚本的路径泄漏(源码路径已用 `-ffile-prefix-map` 抹掉)。
|
||
|
||
- 设备上该路径不存在 → **真正的 DNS 反查会失败**;`lsof -i` 不带 `-n` 时会因此卡在超时(实测约 3 秒),
|
||
带 `-nP` 瞬时返回。
|
||
- 端口名/本机名不受影响(来自 `/etc/services`、`/etc/hosts`):实测 `lsof -i` 仍显示 `TCP *:ssh`、`UDP localhost:domain`。
|
||
- **结论**:不改。换成非 Termux 的 zig/上游 musl 需要重装工具链并重做全部验证,代价大而收益仅「反向 DNS」;
|
||
文档与 `README.txt` 已写明**用 `lsof -nP`**。
|
||
|
||
## 11. 真机验证证据(2026-11-06,root@10.76.208.50)
|
||
|
||
产物拷到设备 `/tmp`(tmpfs)执行,验证完已删除;**没有**往 TF 卡写过任何东西。
|
||
|
||
```
|
||
# slabtop -o | head -6
|
||
Active / Total Objects (% used) : 55852 / 61357 (91.0%)
|
||
Active / Total Slabs (% used) : 2794 / 2794 (100.0%)
|
||
Active / Total Caches (% used) : 57 / 81 (70.4%)
|
||
Active / Total Size (% used) : 11024.86K / 11593.48K (95.1%)
|
||
Minimum / Average / Maximum Object : 0.02K / 0.19K / 32768.00K
|
||
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
|
||
12474 10746 86% 0.06K 198 63 792K buffer_head
|
||
|
||
# vmstat 1 3 (墙钟实测 2 秒,采样间隔正常)
|
||
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
|
||
r b swpd free buff cache si so bi bo in cs us sy id wa st gu
|
||
1 0 0 2584 8764 19388 0 0 0 0 449 6 3 3 94 0 0 0
|
||
0 0 0 2644 8764 19388 0 0 0 0 340 566 1 5 94 0 0 0
|
||
0 0 0 2644 8764 19388 0 0 0 0 332 526 1 2 97 0 0 0
|
||
|
||
# free -m
|
||
total used free shared buff/cache available
|
||
Mem: 55 30 2 1 27 24
|
||
Swap: 0 0 0
|
||
|
||
# ps -eo pid,rss,vsz,comm --sort=-rss
|
||
PID RSS VSZ COMMAND
|
||
1368 4832 16516 meow_launcherd
|
||
4241 1608 3012 sh
|
||
...
|
||
|
||
# pmap 1
|
||
1: init
|
||
00400000 864K r-x-- busybox
|
||
004e8000 4K rw--- busybox
|
||
00969000 132K rwx-- [ anon ]
|
||
|
||
# sysctl -a | wc -l → 428(键数)
|
||
# sysctl kernel.hostname kernel.panic
|
||
kernel.hostname = meowos
|
||
kernel.panic = 10
|
||
# sysctl -n kernel.ostype → Linux
|
||
|
||
# lsof -nP -i (瞬时;不带 -n 约 3 秒,见 §10)
|
||
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
||
dropbear 1354 root 3u inet 2196 0t0 TCP *:22 (LISTEN)
|
||
meow_netm 1372 root 5u inet 2611 0t0 UDP 127.0.0.1:53
|
||
meow_mdns 1375 root 3u inet 2906 0t0 UDP *:5353
|
||
meowpanel 1395 root 3u inet 2587 0t0 TCP *:8080 (LISTEN)
|
||
```
|
||
|
||
- 七个二进制 `--help`/`-V` 全部正常(`lsof --help` 不是合法选项,`lsof -v` 打印 4.99.4)。
|
||
- 交互式 `slabtop`(无 `-o`)在 `TERM=xterm` 下正常初始化 curses(stdout 写出 1599 字节转义序列);
|
||
对照 `TERM=bogus-term` 报 `Error opening terminal: bogus-term.` —— 说明 terminfo 路径确实命中
|
||
`/usr/share/terminfo`,不是内嵌 fallback。
|
||
- 设备上 `sha256sum` 与 `MANIFEST.txt` 逐条一致(测的就是交付的那份字节)。
|
||
- 宿主侧静态/架构自检、MANIFEST 对账、licenses 完整性:`bash scripts/build/build_devkit.sh --verify-only`。
|
||
|
||
## 11.1 `timeout` 真机验收(2026-11-11,root@10.58.215.50,内核 4.4.94+)
|
||
|
||
部署位置(两份同一字节):
|
||
`/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout`(主位置,和 slabtop 那批一起)
|
||
与 `/usr/data/mnt/sd_0/MeowOS/opt/bin/timeout`(launcher 的 PATH 会 prepend 它)。
|
||
|
||
本轮交付的字节:**49 676 B**,md5 `aded39e0f47012fb5827b7cdb2b3b2e1`,
|
||
sha256 `5c4d35c63f246aab6bfa70a1fbfd182ac16e668831f69fff6104515a99795a22`
|
||
(`MANIFEST.txt` 里的就是这份;两份卡上副本 md5 与构建产物一致)。
|
||
|
||
原始输出(`T=.../devkit/bin/timeout`,`cnt()` 读 `/proc/<pid>/comm` 精确计数,
|
||
不会像 `grep` 那样把命令行文本算进去):
|
||
|
||
```
|
||
== [1] timeout 2 sleep 30
|
||
rc=124 elapsed=2.02s
|
||
== [2] timeout -s KILL 2 sleep 30
|
||
rc=137 elapsed=2.01s
|
||
== [3] timeout 2 sleep 1
|
||
rc=0 elapsed=1.01s
|
||
== [4] timeout 5 sh -c "sleep 100 & sleep 100 & wait"
|
||
rc=124 elapsed=5.01s after4: bt=0 sleep=0 sleep100=0
|
||
== [5] 保证会挂住的命令:timeout 3 sh -c "sleep 30"
|
||
rc=124 elapsed=3.01s
|
||
== [6] 小数 + 后缀(r2 修复的重点)
|
||
0.01m (want ~0.6s): rc=124 elapsed=0.61s
|
||
0.05m (want ~3.0s): rc=124 elapsed=3.01s
|
||
2.5s (want ~2.5s): rc=124 elapsed=2.51s
|
||
1m vs sleep 1 (want rc=0 ~1s): rc=0 elapsed=1.02s
|
||
== [7] timeout(无参数)
|
||
rc=125(stderr 打印用法)
|
||
== 收工: bt=0 sleep=0
|
||
```
|
||
|
||
第 4 条是重点:`sh` 自己 fork 的两个 `sleep 100` 随进程组一起被杀,
|
||
`/proc` 里 `comm=sleep` 的进程数回到 **0**(不是"看起来没了",是逐 pid 核对)。
|
||
第 5 条是对评审建议的补充:用一个**确定会挂住**的命令(`sh -c 'sleep 30'`)
|
||
证明"到点必杀",不再依赖 `bluetoothctl` 是否恰好秒退。
|
||
|
||
> r1 的旧数字(47 708 B / 48 012 B)与旧的 `0.05m≈0.06s` 输出已被本轮取代;
|
||
> 本文档、`MANIFEST.txt`、`README.txt` 与卡上两份副本均对齐到 49 676 B。
|
||
|
||
### PATH 实测(非登录 shell vs 绝对路径 vs 登录 shell)
|
||
|
||
```
|
||
ssh root@<ip> 'echo $PATH'
|
||
→ /usr/sbin:/usr/bin:/sbin:/bin
|
||
ssh root@<ip> 'timeout 1 sleep 5; echo rc=$?'
|
||
→ sh: timeout: not found / rc=127
|
||
绝对路径 /usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout 1 sleep 5
|
||
→ rc=124 elapsed=1.0s
|
||
sh -lc 'command -v timeout'
|
||
→ /usr/data/mnt/sd_0/MeowOS/opt/bin/timeout
|
||
command -v bash
|
||
→ MISSING(设备只有 busybox sh,没有 bash)
|
||
```
|
||
|
||
结论:**ssh 默认非登录 shell,PATH 不含 opt/bin;请用绝对路径或 `sh -lc`**(不是 `bash -lc`)。
|
||
|
||
### 真实场景:`timeout 8 bluetoothctl connect`(r1 记录,保留作对照)
|
||
|
||
```
|
||
before: bt=0 sleep=0
|
||
$T 8 bluetoothctl connect 33:DE:2E:21:AD:30
|
||
Attempting to connect to 33:DE:2E:21:AD:30
|
||
rc=0 elapsed=0.3s
|
||
after: bt=0 sleep=0 (ps w | grep bluetoothctl 无残留)
|
||
```
|
||
|
||
耳机当时是关机状态,`bluetoothctl` 5.54 在尝试后自己退出了(0.3s,rc=0),
|
||
所以这条**没跑到 8s 上限**。评审指出这没验证到关键点——已改用第 5 条
|
||
`timeout 3 sh -c "sleep 30"`(确定挂住)作为"到点必杀"的证据。
|
||
|
||
### 故障注入自证(setpgid 失败告警路径)
|
||
|
||
`timeout.c` 带一个只给测试用的编译开关 `-DTIMEOUT_TEST_SETPGID_FAIL`(生产构建不定义)。
|
||
宿主上编译后运行 `timeout-pgfail 1 sleep 30`,实测 stderr:
|
||
|
||
```
|
||
timeout: 警告: setpgid(16333) 失败: Operation not permitted —— 超时信号可能只能发给该进程本身
|
||
timeout: 警告: 子进程 16333 未建立独立进程组(实际 pgid=16322)——setpgid 未生效,超时信号将尽量只发给该进程本身,它 fork 的孙进程可能残留
|
||
timeout: 警告: 超时: 进程组 16333 不存在,已退化为只给直接子进程 16333 发信号 15
|
||
rc=124 elapsed=1084ms
|
||
```
|
||
|
||
三条告警都出现、兜底 `kill(pid)` 真的杀掉了 `sleep 30`、退出码仍是 124。
|
||
这条自证已固化进 `test_build_devkit.sh` 的 T12(T12a–T12d),每次跑回归都会执行。
|
||
|
||
## 12. 其它已知限制
|
||
|
||
- `pmap -x` 无 PSS(无 smaps),只有 maps 级别的段/文件信息。
|
||
- 不提供 `top`(§7/§9);需要交互式总览请用卡上已有的 `htop`(其时钟显示同样受 §9 影响,亚秒刷新走 `select()` 大概率正常)。
|
||
- 二进制按设备内核 4.4.94 编 `LINUXV`;该宏只影响 `lsof -v` 显示,换内核不影响运行。
|
||
- 产物是 32 位 MIPS32r2(o32 ABI),只适用于 X1600 这类 MIPS32r2 目标。
|
||
- 卡上 `src/` 是 `--with-sources` 的**生成目录**(每次整理会整体重建),不要往里手放文件。
|