/* * timeout.c — MeowOS devkit: a small, self-contained timeout(1) * * SPDX-License-Identifier: Apache-2.0 * Copyright (c) 2026 MeowOS contributors * * 独立实现,未拷贝任何 GPL 代码。 * busybox 的 coreutils/timeout.c 与本仓 Apache-2.0 不兼容,因此本文件从零编写, * 只依据 POSIX/Linux 系统调用语义(fork/setpgid/kill/timerfd/ppoll/signalfd)。 * * 为什么设备需要它(2026-11-11 18:44 整机进程耗尽事故):设备没有 timeout(1), * busybox 1.31.1 也无该 applet,一条挂在硬件上的外部命令(bluetoothctl 等) * 会永久阻塞并持续 fork,最终 dropbear 连 fork 都做不到。本工具让"最多等 N 秒 * 然后杀掉整个进程组"成为设备侧可用的默认动作。 * * 支持的子集(coreutils timeout 的最小可用子集): * timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]... * timeout --help * * DURATION 纯数字=秒;支持小数(0.2、1.5m)与 s/m/h 后缀(大小写均可)。 * 不接受 coreutils 的 d(天)后缀,也不接受 0(与本工具用途矛盾)。 * -s SIGNAL 超时后发给整个进程组的信号,默认 TERM。可用 SIGTERM/TERM/15。 * -k DURATION 发完 -s 信号后再等 DURATION,仍未退出则 SIGKILL(两阶段)。 * * 退出码(对齐 coreutils 语义): * 124 超时(本工具发出的信号不是 KILL,或 -k 未到期) * 137 超时且本工具最终发了 SIGKILL(-s KILL,或 -k 到期) * 128+N 命令自己死于信号 N(未超时;不提供 --preserve-status) * 125 timeout 自身失败(参数错、timerfd/fork 失败、命令状态不可得) * 126 命令存在但无法执行(EACCES 等) * 127 命令不存在(ENOENT) * 其它 透传命令自身的退出码 * * 进程模型(本工具最容易做错的部分): * fork 后子进程 setpgid(0,0) 自成一个进程组,父进程也 setpgid(pid,pid) * 关掉 fork/setpgid 之间的竞态。超时后 kill(-pgid, SIG),因此命令自己 fork * 出来的孙进程(bluetoothctl 的形态、sh -c 'a & b & wait')一并收到信号。 * 直接子进程一定被 waitpid 回收(不留僵尸);孙进程在组内被信号终止后 * 由 init 收养并回收。 * * 计时(为什么不读时钟、也不用 timerfd): * zig/musl 的 time64 头在 4.4 内核上 ABI 错配,clock_gettime/time 会返回垃圾值 * (本仓 M11/M16/M40/M43 的既有结论)。实测还有第二个陷阱:musl 的 * timerfd_settime() 在这台机上是**静默失效**的——返回 0、不报错,但计时器 * 根本不启动(/proc 侧 gettime 为 0,poll 永远等不到)。用普通 timerfd 写法的 * 第一版因此表现为「timeout 2 sleep 30 等满 30 秒才返回、退出码 0」。 * 本工具改用 setitimer(ITIMER_REAL) + SIGALRM:musl 对 setitimer 做了 * 32 位 timeval 转换(实测 1s/200ms 都能按点触发),不经过坏掉的 timerfd 路径。 * 计时完全交给内核,进程只做两件事:等 SIGALRM(超时)与等 SIGCHLD(子进程退出), * 两者都进同一个 signalfd,由一次 ppoll() 等待。没有轮询循环,没有忙等, * 也没有任何"读时钟算差值"的代码。 * * 已知限制: * - 不跟踪 setsid()/双重 fork 逃出进程组的守护进程(coreutils 也不跟踪)。 * - 不提供 --preserve-status / -v;给出这些参数会明确报 125 而不是静默忽略。 * - 精度是"毫秒级够用":单核 1GHz 上真实误差来自调度延迟(负载高时数十毫秒), * 本工具不谎称微秒级精度。 * - 若直接子进程在超时前自行退出,其留在组内的孙进程不会被本工具清理 * (与 coreutils 一致;想要组清理请让子进程活到超时)。 */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define EXIT_TIMEOUT 124 #define EXIT_FAILURE_125 125 #define EXIT_CANNOT_EXEC 126 #define EXIT_NOT_FOUND 127 #define NSEC_PER_SEC 1000000000ULL /* 被转发给子进程组的信号:父进程收到后原样转给组,避免 Ctrl-C 只打断 * timeout 自己、把子进程留成孤儿。 */ static const int forward_sigs[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2 }; static volatile sig_atomic_t g_child_pgid = 0; static volatile sig_atomic_t g_pending_sig = 0; static void forward_signal(int sig) { pid_t pgid = (pid_t)g_child_pgid; g_pending_sig = sig; if (pgid > 0) { (void)kill(-pgid, sig); g_pending_sig = 0; } } struct sig_name { const char *name; int num; }; static const struct sig_name sig_names[] = { { "HUP", SIGHUP }, { "INT", SIGINT }, { "QUIT", SIGQUIT }, { "ILL", SIGILL }, { "TRAP", SIGTRAP }, { "ABRT", SIGABRT }, { "BUS", SIGBUS }, { "FPE", SIGFPE }, { "KILL", SIGKILL }, { "USR1", SIGUSR1 }, { "SEGV", SIGSEGV }, { "USR2", SIGUSR2 }, { "PIPE", SIGPIPE }, { "ALRM", SIGALRM }, { "TERM", SIGTERM }, { "CHLD", SIGCHLD }, { "CONT", SIGCONT }, { "STOP", SIGSTOP }, { "TSTP", SIGTSTP }, { "TTIN", SIGTTIN }, { "TTOU", SIGTTOU }, { "URG", SIGURG }, { "XCPU", SIGXCPU }, { "XFSZ", SIGXFSZ }, { "VTALRM", SIGVTALRM }, { "PROF", SIGPROF }, { "WINCH", SIGWINCH }, { "IO", SIGIO }, { "SYS", SIGSYS }, { "PWR", SIGPWR }, }; static void usage(FILE *fp) { fputs( "用法: timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...\n" "\n" " DURATION 等待时长:纯数字为秒,可带小数,后缀 s/m/h(如 2、0.2、1.5m)\n" " -s SIGNAL 超时后发给整个进程组的信号,默认 TERM(可写 SIGTERM/TERM/15)\n" " -k DURATION 发出 -s 信号后再等这么久,仍未退出则发 KILL(两阶段终止)\n" " -h, --help 显示本帮助\n" "\n" "退出码: 124=超时 137=超时且发了 KILL 128+N=命令死于信号 N\n" " 125=timeout 自身出错 126=命令无法执行 127=命令不存在\n" "\n" "超时信号发给 COMMAND 所在的整个进程组,命令自己 fork 的孙进程也会被终止。\n", fp); } /* 解析信号:数字(1..64)、NAME、SIGNAME(大小写不敏感) */ static int parse_signal(const char *s) { const char *name = s; size_t i; if (s == NULL || *s == '\0') return -1; if (isdigit((unsigned char)*s)) { char *end = NULL; long v = strtol(s, &end, 10); if (end == s || *end != '\0' || v < 1 || v > 64) return -1; return (int)v; } if (strncasecmp(name, "SIG", 3) == 0) name += 3; for (i = 0; i < sizeof(sig_names) / sizeof(sig_names[0]); i++) { if (strcasecmp(name, sig_names[i].name) == 0) return sig_names[i].num; } return -1; } /* 解析时长 → 纳秒。整数秒 + 最多 9 位小数 + s/m/h 后缀;0 视为非法。 */ static int parse_duration(const char *s, uint64_t *out_ns) { uint64_t sec = 0, nsec = 0, mult = 1; size_t i = 0; int int_digits = 0, frac_digits = 0; if (s == NULL || *s == '\0') return -1; while (isdigit((unsigned char)s[i])) { if (sec > (UINT64_MAX - 9) / 10) return -1; sec = sec * 10 + (uint64_t)(s[i] - '0'); i++; int_digits++; } if (s[i] == '.') { i++; while (isdigit((unsigned char)s[i])) { if (frac_digits < 9) { nsec = nsec * 10 + (uint64_t)(s[i] - '0'); frac_digits++; } i++; } /* 小数位补足 9 位;"5." 这类也接受 */ while (frac_digits < 9) { nsec *= 10; frac_digits++; } } if (int_digits == 0 && frac_digits == 0) return -1; switch (s[i]) { case '\0': break; case 's': case 'S': mult = 1; i++; break; case 'm': case 'M': mult = 60; i++; break; case 'h': case 'H': mult = 3600; i++; break; default: return -1; } if (s[i] != '\0') return -1; if (mult != 1) { if (sec > UINT64_MAX / mult) return -1; sec *= mult; } if (sec > (UINT64_MAX - nsec) / NSEC_PER_SEC) return -1; *out_ns = sec * NSEC_PER_SEC + nsec; return (*out_ns == 0) ? -1 : 0; } static void arm_timer(uint64_t ns) { struct itimerval it; memset(&it, 0, sizeof(it)); it.it_value.tv_sec = (time_t)(ns / NSEC_PER_SEC); it.it_value.tv_usec = (suseconds_t)((ns % NSEC_PER_SEC) / 1000); /* 亚毫秒向上取整到 1µs:setitimer 的 0 表示"不启动",会把小超时变成永不超时 */ if (ns != 0 && it.it_value.tv_sec == 0 && it.it_value.tv_usec == 0) it.it_value.tv_usec = 1; /* 失败也无从补救:下面的 ppoll 会一直等,用户看得见卡住 */ (void)setitimer(ITIMER_REAL, &it, NULL); } static void disarm_timer(void) { struct itimerval it; memset(&it, 0, sizeof(it)); (void)setitimer(ITIMER_REAL, &it, NULL); } static int duration_arg(const char *opt, const char *val, uint64_t *ns) { if (parse_duration(val, ns) != 0) { fprintf(stderr, "timeout: 无效时长 '%s'(%s):支持纯数字秒、小数与 s/m/h 后缀\n", val, opt); return -1; } return 0; } int main(int argc, char **argv) { int kill_signal = SIGTERM; uint64_t kill_after_ns = 0; int have_kill_after = 0; int i = 1; uint64_t timeout_ns = 0; char **cmd; pid_t pid; int sfd = -1; struct sigaction sa; sigset_t wait_mask, orig_mask; struct pollfd pfd; int status = 0; int timed_out = 0, killed_by_us = 0, reaped = 0, done = 0; while (i < argc) { const char *a = argv[i]; if (strcmp(a, "--") == 0) { i++; break; } if (a[0] != '-' || strcmp(a, "-") == 0) break; if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) { usage(stdout); return 0; } else if (strcmp(a, "-s") == 0 || strcmp(a, "--signal") == 0) { if (i + 1 >= argc) { fprintf(stderr, "timeout: %s 缺少信号参数\n", a); return EXIT_FAILURE_125; } kill_signal = parse_signal(argv[++i]); if (kill_signal < 0) { fprintf(stderr, "timeout: 无效信号 '%s'\n", argv[i]); return EXIT_FAILURE_125; } } else if (strncmp(a, "--signal=", 9) == 0) { kill_signal = parse_signal(a + 9); if (kill_signal < 0) { fprintf(stderr, "timeout: 无效信号 '%s'\n", a + 9); return EXIT_FAILURE_125; } } else if (strcmp(a, "-k") == 0 || strcmp(a, "--kill-after") == 0) { if (i + 1 >= argc) { fprintf(stderr, "timeout: %s 缺少时长参数\n", a); return EXIT_FAILURE_125; } if (duration_arg(a, argv[++i], &kill_after_ns) != 0) return EXIT_FAILURE_125; have_kill_after = 1; } else if (strncmp(a, "--kill-after=", 13) == 0) { if (duration_arg("--kill-after", a + 13, &kill_after_ns) != 0) return EXIT_FAILURE_125; have_kill_after = 1; } else if (strcmp(a, "--preserve-status") == 0 || strcmp(a, "-v") == 0 || strcmp(a, "--verbose") == 0) { fprintf(stderr, "timeout: 不支持 %s(本工具是 coreutils 的最小可用子集)\n", a); return EXIT_FAILURE_125; } else { fprintf(stderr, "timeout: 未知选项 '%s'\n", a); usage(stderr); return EXIT_FAILURE_125; } i++; } if (i >= argc) { usage(stderr); return EXIT_FAILURE_125; } if (parse_duration(argv[i], &timeout_ns) != 0) { fprintf(stderr, "timeout: 无效时长 '%s':支持纯数字秒、小数与 s/m/h 后缀\n", argv[i]); return EXIT_FAILURE_125; } i++; if (i >= argc) { fprintf(stderr, "timeout: 缺少要执行的命令\n"); usage(stderr); return EXIT_FAILURE_125; } cmd = &argv[i]; /* SIGCHLD(子进程退出)与 SIGALRM(超时)都屏蔽后交给同一个 signalfd; * 这样一次 ppoll 就能同时等两件事,不需要轮询,也不需要 timerfd。 */ sigemptyset(&wait_mask); sigaddset(&wait_mask, SIGCHLD); sigaddset(&wait_mask, SIGALRM); if (sigprocmask(SIG_BLOCK, &wait_mask, &orig_mask) != 0) { perror("timeout: sigprocmask"); return EXIT_FAILURE_125; } sfd = signalfd(-1, &wait_mask, SFD_CLOEXEC | SFD_NONBLOCK); if (sfd < 0) { perror("timeout: signalfd"); return EXIT_FAILURE_125; } memset(&sa, 0, sizeof(sa)); sa.sa_handler = forward_signal; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; /* 不 SA_RESTART:要让 ppoll 返回 EINTR */ for (size_t k = 0; k < sizeof(forward_sigs) / sizeof(forward_sigs[0]); k++) (void)sigaction(forward_sigs[k], &sa, NULL); pid = fork(); if (pid < 0) { perror("timeout: fork"); return EXIT_FAILURE_125; } if (pid == 0) { /* 自成进程组:超时信号 kill(-pgid) 才能覆盖孙进程 */ (void)setpgid(0, 0); for (size_t k = 0; k < sizeof(forward_sigs) / sizeof(forward_sigs[0]); k++) (void)signal(forward_sigs[k], SIG_DFL); (void)sigprocmask(SIG_SETMASK, &orig_mask, NULL); execvp(cmd[0], cmd); /* exec 失败:说清原因并用 coreutils 的 127/126 区分 */ { int saved = errno; char msg[512]; int n = snprintf(msg, sizeof(msg), "timeout: 无法执行 '%s': %s\n", cmd[0], strerror(saved)); if (n > 0) (void)write(STDERR_FILENO, msg, (size_t)n); _exit(saved == ENOENT ? EXIT_NOT_FOUND : EXIT_CANNOT_EXEC); } } g_child_pgid = pid; /* fork 与子进程 setpgid 之间的竞态由父进程这一侧兜住(任一侧成功即可) */ (void)setpgid(pid, pid); /* 若在 fork 与 g_child_pgid 赋值之间收到过转发信号,这里补发一次 */ if (g_pending_sig != 0) { int sig = (int)g_pending_sig; (void)kill(-pid, sig); g_pending_sig = 0; } arm_timer(timeout_ns); pfd.fd = sfd; pfd.events = POLLIN; pfd.revents = 0; for (;;) { int r = ppoll(&pfd, 1, NULL, NULL); if (r < 0) { if (errno == EINTR) continue; /* 转发信号的处理函数已完成转发 */ perror("timeout: ppoll"); return EXIT_FAILURE_125; } if (!(pfd.revents & POLLIN)) continue; for (;;) { struct signalfd_siginfo si; ssize_t got = read(sfd, &si, sizeof(si)); pid_t w; if (got != (ssize_t)sizeof(si)) break; /* EAGAIN:这一轮通知读完了 */ if (si.ssi_signo == SIGALRM) { if (!timed_out) { timed_out = 1; /* 先发温和信号(默认 TERM),发给整个进程组 */ (void)kill(-pid, kill_signal); if (kill_signal == SIGKILL) { killed_by_us = 1; disarm_timer(); } else if (have_kill_after) { arm_timer(kill_after_ns); /* 第二阶段:-k 到期 */ } else { disarm_timer(); } } else { /* -k 到期:KILL 整个组,仍继续 waitpid 回收直接子进程 */ (void)kill(-pid, SIGKILL); killed_by_us = 1; disarm_timer(); } continue; } if (si.ssi_signo != SIGCHLD) continue; do { w = waitpid(pid, &status, WNOHANG); } while (w < 0 && errno == EINTR); if (w == pid) { reaped = 1; break; } if (w < 0 && errno == ECHILD) { /* 子进程被别人回收(极不寻常),状态不可得 */ reaped = 0; done = 1; break; } /* w == 0:还有其它子进程状态变化(本工具没有),继续等 */ } if (reaped || done) break; } if (!reaped) return EXIT_FAILURE_125; if (timed_out) { /* 超时:最终发了 KILL 才报 137,否则 124(coreutils 语义) */ return killed_by_us ? 128 + SIGKILL : EXIT_TIMEOUT; } if (WIFEXITED(status)) return WEXITSTATUS(status); if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); return EXIT_FAILURE_125; }