进程、标准输入输出与退出码
理解脚本、子进程和命令行工具如何通过 stdin、stdout、stderr 协作。
进程、标准输入输出与退出码
npm scripts 经常把多个工具串在一起。进入 Python 和 C++ 后,进程的标准流、退出码和信号会直接影响数据管线和容器编排。把 CLI 当成 API,就要像设计 HTTP 接口一样定义输入格式、输出格式、退出码、超时和终止行为。父进程不能只看“子进程结束了”,还要知道它是成功退出、被信号终止,还是输出了不完整结果。
学习目标
- 能定义子进程的标准输入、标准输出、标准错误和退出码契约。
- 能处理超时、取消、输出上限和子进程回收,避免留下孤儿进程。
- 能区分机器可解析输出与人类诊断,并在跨语言流水线中稳定传递错误。
命令行程序也是一种 API
下面这段代码只保留同一个意图,重点观察输入边界、数据流和失败语义,而不是逐字符翻译。
const child = spawn("python", ["prepare.py"], { stdio: "pipe" });
child.stdout.on("data", chunk => process.stdout.write(chunk));
child.on("exit", code => process.exit(code ?? 1)); # Python
result = subprocess.run(["python", "prepare.py"],
input=payload, text=True, capture_output=True, check=False)
raise SystemExit(result.returncode)
// C++
int code = std::system("python prepare.py");
return WEXITSTATUS(code); 三条通道和退出码
stdout 用于机器可读结果,stderr 用于诊断信息,stdin 用于结构化输入或管道数据。约定 0 表示成功,2 表示参数/输入错误,其他非零值表示运行失败,并把约定写进文档。不要把调试文字混进 stdout,否则下游 JSON 解析会随机失败。大输出要考虑管道背压,父子进程都必须持续消费对方输出,避免缓冲区互相等待。
import json
import sys
def main() -> int:
try:
payload = json.load(sys.stdin)
result = transform(payload)
except (ValueError, json.JSONDecodeError) as exc:
print(f"invalid input: {exc}", file=sys.stderr)
return 2
json.dump(result, sys.stdout)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
子进程生命周期
启动子进程时使用参数数组而不是 shell 字符串,显式设置工作目录、环境和超时。父进程取消时要终止子进程并等待回收;子进程收到 SIGTERM 或 Windows 等价信号后应尽量关闭文件、刷新必要输出。C++ 的 std::system 难以区分细粒度状态,生产工具应使用更明确的进程 API 或封装层。
常见错误与排错思路
最常见的错误是把 stderr 当成失败、把非零退出码忽略,或子进程死锁后只等待不读输出。排错时同时记录命令名、pid、退出码、信号、运行时长和 stdout/stderr 字节数;先独立运行子命令,再在父进程中复现。若管道输出是空的,检查是否只读到缓冲区未刷新的内容,是否在进程结束前错误地关闭了流。
用退出码判断结果,而不是猜标准输出
父进程应把“命令完成”与“命令成功”区分开:子进程可以正常结束但返回非零;也可能在超时后没有机会输出完整 JSON。先约定 stdout 只包含数据、stderr 只包含诊断,再把退出码映射成调用方可处理的状态。
function interpretExit(code, signal) {
if (signal) return { ok: false, kind: "terminated", signal };
if (code === 0) return { ok: true };
if (code === 2) return { ok: false, kind: "invalid_input" };
return { ok: false, kind: "child_failed", code };
}
console.log(interpretExit(2, null));
预期得到 invalid_input,而不是把 stderr 文本当作唯一协议。标准错误可能本地化或随版本变化,因此业务判断应依据稳定退出码和结构化 stdout。启动进程时使用参数数组并关闭不必要的 shell 解释,避免字符串拼接导致注入。
超时与回收验证
用一个立即成功的子进程、一个返回非零的子进程、一个超时子进程和一个输出超限进程验证父进程行为。超时后终止子进程并等待其退出;取消后关闭管道,确认不会继续堆积后台任务。为 stdout/stderr 设字节上限,防止异常程序无限输出耗尽内存。
排查“命令卡住”时检查父进程是否只读取 stdout 却未排空 stderr,管道缓冲区可能因此被填满;再检查当前工作目录、环境变量、信号和平台差异。记录 pid、命令名、运行时长和退出类别,不记录秘密参数。
迁移练习
请完成:设计一个 CLI,使它把结果写 stdout,把日志写 stderr,并在输入无效、子进程超时和成功时返回不同退出码。说明父进程如何回收子进程。
进程、标准输入输出与退出码练习
设计一个 CLI,使它把结果写 stdout,把日志写 stderr,并在输入无效、子进程超时和成功时返回不同退出码。说明父进程如何回收子进程。
给我一点提示
先画出父进程、子进程和三条通道,再定义每种退出码;取消时先发终止信号再等待。
查看参考答案
结果只写 stdout;诊断写 stderr;参数或数据校验失败返回 2,子进程超时返回 124,未处理异常返回 1,成功返回 0。父进程设置 deadline,超时终止并 wait 回收子进程,再记录 pid、退出状态和 request_id。 本节结论
稳定的进程边界能把 JS/TS、Python 和 C++ 工具组合成一条可靠流水线。完成后,请用一条 JSONL 管道测试 stdout 可解析、stderr 不污染结果,以及超时后没有遗留子进程。
小结
子进程是独立故障边界:参数、流、退出码、超时和回收都属于公开契约。将它当成 API 管理,JS/TS、Python 和 C++ 工具才可以可靠地组合成流水线。
本节是阶段检查点。完成练习后,再进入下一阶段。