feat(m53): 自写 timeout(1) 并接入 devkit 本地源码构建路径
设备没有 timeout,busybox 1.31 也无该 applet;2026-11-11 18:44 一条挂在 硬件上的 bluetoothctl 累积 55 个残留进程耗尽进程表,dropbear 无法 fork, 整机在线却不可登录。本单给设备一个可用的 timeout。 - scripts/build/devkit-src/timeout.c(新,Apache-2.0,489 行) 独立实现,未拷贝 busybox/GPL 代码;fork+setpgid(0,0) 后 kill(-pgid), 孙进程一并终止;waitpid 回收直接子进程不留僵尸;-s/-k 两阶段终止; 退出码 124/137/128+N/125/126/127 对齐 coreutils 语义。 计时不用 timerfd(musl 的 timerfd_settime 在 4.4 上静默失效,实测 timeout 2 sleep 30 等满 30s 才返回),改用 setitimer(ITIMER_REAL)+SIGALRM, 与 SIGCHLD 一起进 signalfd,由一次 ppoll 等待,零轮询、不读时钟。 - build_devkit.sh:新增 build_local(B 路径,不碰上游 procps/lsof/ncurses 流程);TOOLS/TOOL_NAMES/MANIFEST/licenses/stamp/--with-sources 接入; 产物同时标注 Apache-2.0 并随附许可证原文。 - test_build_devkit.sh:T9(产物静态/架构/零 NEEDED/MANIFEST/license)、 T10(源码纪律:独立实现、不调 clock_gettime、不用 timerfd、进程组模型)。 - DEVKIT.md/README.md:产物表、PATH 实测(ssh 非登录 shell 无 opt/bin, 且设备无 bash → 用 sh -lc 或绝对路径)、禁裸跑纪律与 18:44 事故时间线、 timerfd 静默失效的第二类 time64 坑、2026-11-11 真机验收原始输出。
This commit is contained in:
1 parent
a41b61198e
commit
80c0bd4eb4
5 files changed
+855
-22
No files matched your search
+179
-5
@@ -19,6 +19,51 @@ busybox 1.31.1:`free / ps / top / lsof` 是 busybox 的软链(实测 `/bin/p
|
||||
|
||||
所以本工具集只围绕「这台机器的真实调试痛点」,不做大而全。
|
||||
|
||||
## 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`,
|
||||
@@ -33,11 +78,17 @@ busybox 1.31.1:`free / ps / top / lsof` 是 busybox 的软链(实测 `/bin/p
|
||||
| `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` 网络视图 |
|
||||
| | | **合计** | **2 141 764 ≈ 2.1 MB** | |
|
||||
| `timeout` | 本仓 `devkit-src/timeout.c` | **Apache-2.0**(自有独立实现,非 busybox/GPL 代码) | 47 708 | 给任何外部命令加"最多等 N 秒、然后杀掉整个进程组";busybox 1.31 无此 applet |
|
||||
| | | **合计** | **2 189 472 ≈ 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**)、
|
||||
`README.txt`(上卡说明)。`--with-sources` 会额外生成 `src/`(三份源码包 + `build_devkit.sh` + 本文件)。
|
||||
`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. 上卡后怎么用
|
||||
|
||||
@@ -59,10 +110,58 @@ fi
|
||||
| `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 8 bluetoothctl connect XX:XX:XX:XX:XX:XX # 挂死的外部命令的默认包装
|
||||
```
|
||||
|
||||
- 超时信号发给 COMMAND 的**整个进程组**(`setpgid(0,0)` + `kill(-pgid, SIG)`),
|
||||
命令自己 fork 的孙进程一并终止;直接子进程一定被 `waitpid` 回收,不留僵尸。
|
||||
- 两阶段:先 `-s`(默认 TERM),`-k` 到期再 KILL。不是忙等,计时全在内核。
|
||||
- 退出码:`124` 超时;`137` 超时且最终发了 KILL;`128+N` 命令自己死于信号 N;
|
||||
`125` timeout 自身参数/系统调用错;`126` 命令无法执行;`127` 命令不存在。
|
||||
- 不支持 `--preserve-status` / `-v` / `d` 后缀,给了会明确报 125(不静默忽略)。
|
||||
- 不跟踪 `setsid()` 逃出进程组的守护进程(coreutils 也不跟踪)。
|
||||
|
||||
## 4. exFAT 语义(权限 / 软链)
|
||||
|
||||
设备挂载:`/dev/mmcblk0 on /usr/data/mnt/sd_0 type exfat (rw, fmask=0022, dmask=0022, symlink=0, noexec 未设置)`。
|
||||
@@ -100,11 +199,19 @@ fi
|
||||
| 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` + 本文件**一起放进 `src/`(合计约 5.6 MB)。少一样都不算完整:
|
||||
没有构建脚本,收卡人拿不到本轮用的 `configure`/`-D` 参数(这正是 "corresponding source" 明确包含的)。
|
||||
**三份源码包 + `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 义务外还必须
|
||||
「显著提示使用了该库 + 附许可证副本 + 三选一」:
|
||||
@@ -208,6 +315,14 @@ fi
|
||||
| `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` /
|
||||
@@ -297,6 +412,65 @@ meowpanel 1395 root 3u inet 2587 0t0 TCP *:8080 (LISTEN)
|
||||
- 设备上 `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 它)。
|
||||
|
||||
原始输出(`T=.../devkit/bin/timeout`,`cnt()` 读 `/proc/<pid>/comm` 精确计数,
|
||||
不会像 `grep` 那样把命令行文本算进去):
|
||||
|
||||
```
|
||||
== [1] timeout 2 sleep 30
|
||||
rc=124 elapsed=2.0s bt=0 sleep=0
|
||||
== [2] timeout -s KILL 2 sleep 30
|
||||
rc=137 elapsed=2.0s
|
||||
== [3] timeout 2 sleep 1
|
||||
rc=0 elapsed=1.0s
|
||||
== [4] timeout 5 sh -c "sleep 100 & sleep 100 & wait"
|
||||
rc=124 elapsed=5.0s bt=0 sleep=0 ps w | grep "sleep 100" = 0
|
||||
== [prec] timeout 0.2 sleep 5
|
||||
rc=124 elapsed=0.2s
|
||||
== [6] timeout(无参数)
|
||||
用法: timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...
|
||||
noargs_rc=125
|
||||
```
|
||||
|
||||
第 4 条是重点:`sh` 自己 fork 的两个 `sleep 100` 随进程组一起被杀,
|
||||
`/proc` 里 `comm=sleep` 的进程数回到 **0**(不是"看起来没了",是逐 pid 核对)。
|
||||
|
||||
### 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`
|
||||
|
||||
```
|
||||
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 上限——但它证明了**调用方式可用且不留残留**;
|
||||
"到点必杀"的能力由上面第 1–4 条的 `sleep` 用例证明(同一套 kill 路径)。
|
||||
|
||||
## 12. 其它已知限制
|
||||
|
||||
- `pmap -x` 无 PSS(无 smaps),只有 maps 级别的段/文件信息。
|
||||
|
||||
Reference in new issue
Block a user