if (system(cmd) != 0) 判不出命令失败:它返回的是等待状态,不是退出码

一个用 C 写成的小工具,负责在部署流程里调用几条外部命令。判断逻辑很朴素:system 的返回值不是 0,就认为命令失败,打印错误退出。

这套判断跑了两个月,期间有一类失败从来没被拦下过:命令确实失败了,工具却报成功,后面的步骤继续往下走,直到产物校验阶段才炸。

排查时打了个断点,发现失败那次返回的值是 256。命令的退出码是 1,但返回值不是 1。

返回值到底是什么?

man 页在 RETURN VALUE 一节把几种情况分开写了,其中最关键的是这句:

If all system calls succeed, then the return value is the termination status of the child shell used to execute command. (The termination status of a shell is the termination status of the last command it executes.)

「一切系统调用成功时,返回值是执行命令那个子 shell 的终止状态。」注意是「终止状态」,不是「退出码」。

紧接着的一句把用法写死了:

In the last two cases, the return value is a "wait status" that can be examined using the macros described in waitpid(2). (i.e., WIFEXITED(), WEXITSTATUS(), and so on).

返回值是一个 wait status,要用 waitpid 那一套宏去解码。所以 256 不是错误码,而是「退出码 1」被编码到状态字高位之后的结果。

顺带说清楚一件事:system 并不直接执行你给的命令。它先 fork 一个子进程,再在子进程里 execl 一个 shell,把整个字符串交给 shell 解释。所以你等的从来不是那条命令,而是执行它的那个 shell 进程。

三种返回值分别从哪来?

文档列的是三种来源,套在一起看才完整:

返回值 来源 处理方式
-1 子进程没建起来,或状态取不到 看 errno,与命令本身无关
状态字 命令正常结束 WIFEXITED 判断,WEXITSTATUS 取值
状态字 命令被信号终止 WIFSIGNALED 判断,WTERMSIG 取值
状态字 子 shell 压根没起来 形状等同于「以 127 退出」

第一行最容易处理,因为它是负数,和正常状态字的范围不重叠。真正的坑在后三行------它们都长得像「一个正整数」,只有解码之后才知道是正常退出、被信号杀掉,还是 shell 根本没起来。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int run(const char *cmd) {
    int st = system(cmd);
    if (st == -1) {
        return -1;                    /* fork 或 wait 失败,看 errno */
    }
    if (WIFEXITED(st)) {
        return WEXITSTATUS(st);       /* 真正的退出码 */
    }
    if (WIFSIGNALED(st)) {
        return 128 + WTERMSIG(st);    /* 被信号终止,用常见约定编码 */
    }
    return -1;                        /* 其它情况:暂停、继续等 */
}

这段代码里最容易被省略的是最后那行兜底。WIFEXITED 和 WIFSIGNALED 都不成立时,状态字仍然是个合法值,直接当成功会漏掉一类失败。

为什么 127 有两种来源?

文档里有一句专门提醒这件事的实现后果:

It is possible for the shell command to terminate with a status of 127, which yields a system() return value that is indistinguishable from the case where a shell could not be executed in the child process.

命令自己返回 127,和子进程里连 shell 都没起来,这两种情况的返回值分不出来。

127 在 shell 约定里表示「命令找不到」,所以命令本身返回 127 是常见的事------脚本里写错一个命令名就会这样。而 shell 起不来(比如 /bin/sh 不存在、被 chroot 之后路径断掉)也编码成 127。

如果想区分,system 这个接口给不了你答案,得换成自己 fork + exec:exec 失败时子进程可以自己选一个约定的退出码,或者在 fork 之后、exec 之前用管道把错误回传。代价是要自己处理信号屏蔽和 waitpid。

文档还留了一个小口子:参数传 NULL 时,返回值只用来说明系统上有没有可用的 shell,非零表示有。适合在程序启动时做一次前置检查,把「shell 不可用」和「命令失败」在源头分开。不过这一条有个版本说明------glibc 2.1.3 之前并不会真的去检查,而是假定一定可用、直接返回 1。

循环里调用为什么按 Ctrl+C 不退出?

文档在 DESCRIPTION 里提到一句:

During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored, in the process that calls system().

调用方进程在执行命令期间,SIGCHLD 被阻塞,SIGINT 和 SIGQUIT 被忽略。这两个恰好是终端里 Ctrl+C 和 Ctrl+\ 发出的信号。

所以一个在循环里不断调用 system 的程序,按下 Ctrl+C 之后可能没有任何反应------信号被忽略了,而循环还在继续。文档随后给了一句更直白的提示:

As mentioned, system() ignores SIGINT and SIGQUIT. This may make programs that call it from a loop uninterruptible, unless they take care themselves to check the exit status of the child.

「可能在循环里变得不可中断,除非调用方自己检查子进程的退出状态。」文档给的写法是:在循环里检查 WIFSIGNALED 和 WTERMSIG,如果子进程是被 SIGINT 或 SIGQUIT 终止的,就跳出循环。

这条对交互式工具影响很大。想在长时间运行的循环里保留「按一下就能停」的能力,除了上面那个检查,另一条路是自己装信号处理器并置一个标志位,在每轮循环开头判断。

安全上还有哪几处?

文档的 Caveats 一节列了三条,都跟环境有关。

一条是关于特权程序的:

Do not use system() from a privileged program (a set-user-ID or set-group-ID program, or a program with capabilities) because strange values for some environment variables might be used to subvert system integrity.

带特殊权限的程序里不要用它,因为环境变量可以被篡改成指向别的可执行文件。文档推荐的替代是 exec 系列,但明确排除了 execlp 和 execvp------它们同样靠 PATH 找程序。

另一条是关于输入清洗的:命令里拼接了用户输入时必须严格过滤,否则可以注入额外的 shell 命令或选项。特权程序里这个风险更严重。

第三条在 BUGS 一节,比较冷:如果命令名本身以连字符开头,shell 会把它当成选项解释,行为变成未定义。文档给的绕法是前面加一个空格。

小结

system 的返回值是 wait status,不是退出码;!= 0 只能粗略区分「成功」和「非成功」,拿不到具体失败原因。要拿到退出码必须走 WEXITSTATUS,要区分「正常失败」和「被信号杀掉」必须走 WIFSIGNALED。

127 这个值有两义,想分辨得放弃 system 自己 fork。循环里调用会屏蔽 Ctrl+C,需要在循环体里自己检查。

这套解码表我整理进了项目的编码约定,按「返回值形态、判断宏、取值宏、处理动作」四列排,几个工具共用一份。底稿是用 AI墨衍 从 man 页和几次误判的排查记录里抽出来的,新增一条只需要改一处。

真正值得记的一点是:!= 0 在很多场景下「碰巧能用」,因为失败时状态字几乎不会是 0。这种碰巧能用的写法,往往要等到需要打印具体退出码、或者需要按 127 做分支的那天,才暴露出来。

相关推荐
艾迪的技术之路27 分钟前
16k 条慢查询,128 张分表,我这么解决
后端·github
dpharness27 分钟前
我踩过的四个记忆坑,从预算到换机
前端·后端
加宝的起点雷达27 分钟前
三个定时任务被删那天,系统一声不吭
人工智能·后端
泡海椒28 分钟前
jquick-pdf 热力图(HEATMAP)实战:24 格营收矩阵嵌入评级报告的模板逐行拆解
后端
对象存储与RustFS31 分钟前
Velero 接 RustFS 实操:K8s 集群备份落到自建对象存储
后端·docker·云原生
leo在掘金34 分钟前
2.8万亿MoE跑昇腾:0 Day适配改了啥
后端·架构
小兔子35 分钟前
proxy_pass 末尾写不写路径,是两套规则:上线后接口路径莫名多了一段
后端
IT枫斗者枫哥37 分钟前
用了BigDecimal还差一分?用四组Java代码查清原因
java·后端
时光不老杰40 分钟前
01 总体架构与模块边界
后端