10. 子进程 stdio 配置
0、1、2 是什么
操作系统约定:0 是 stdin,1 是 stdout,2 是 stderr。spawn() 的 stdio 决定子进程的这些通道连接到哪里。
pipe:由父进程读写
import { spawn } from "node:child_process";
const child = spawn(process.execPath, ["worker.js"], {
stdio: ["pipe", "pipe", "pipe"],
});
child.stdin.write("hello\n");
child.stdin.end();
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
父进程得到三个流。必须消费配置为 pipe 的输出,否则操作系统缓冲区填满后,子进程可能阻塞。
inherit:直接使用父进程终端
const child = spawn("npm", ["--version"], {
stdio: "inherit",
shell: false,
});
适合交互式 CLI 和本地调试。输出直接显示,child.stdout 和 child.stderr 是 null,父进程不能监听其 data。
ignore:不连接标准流
const child = spawn("python", ["maintenance.py"], {
stdio: ["ignore", "ignore", "ignore"],
});
这会丢失诊断信息。服务端一般至少保留 stderr。
输出写入文件
import { closeSync, openSync } from "node:fs";
import { spawn } from "node:child_process";
const stdoutFd = openSync("task.stdout.log", "a");
const stderrFd = openSync("task.stderr.log", "a");
try {
const child = spawn("python", ["long-task.py"], {
stdio: ["ignore", stdoutFd, stderrFd],
});
child.on("error", console.error);
} finally {
// 子进程已复制描述符,父进程关闭自己的副本。
closeSync(stdoutFd);
closeSync(stderrFd);
}
真实项目还要处理打开文件失败、日志轮转、权限和磁盘占满。
第四个通道:IPC
const child = spawn(process.execPath, ["worker.js"], {
stdio: ["ignore", "inherit", "inherit", "ipc"],
});
child.send?.({ type: "start", taskId: "task-42" });
child.on("message", (message: unknown) => {
console.log("子进程消息", message);
});
数组下标 3 不属于标准流,这里是 Node.js IPC 通道。通常直接使用 fork() 更清晰。
detached、unref() 与后台进程
const child = spawn(process.execPath, ["daemon.js"], {
detached: true,
stdio: "ignore",
});
child.unref();
只有 detached 不够:若仍连接父进程 stdio,父进程可能无法独立退出。即便如此,PID、日志、停止方式和崩溃重启仍需管理;生产环境优先使用 systemd、PM2 或容器平台。Windows 与 POSIX 的实现也不同。
时间线
子进程写 stdout
→ 操作系统管道缓冲
→ 父进程 child.stdout 读取
→ 子进程退出(exit)
→ stdio 关闭(close)
若孙进程继承了描述符,exit 后 close 可能延迟。
常见错误
const child = spawn("tool", [], { stdio: "inherit" });
// 错误:inherit 时 stdout 为 null。
child.stdout?.on("data", () => undefined);
- 使用
pipe却不消费输出。 - 使用
ignore后又期待错误详情。 - 合并 stdout 和 stderr,导致机器结果混入日志。
- 把
detached当作完整任务管理器。
最佳实践
- 服务集成通常使用
["ignore", "pipe", "pipe"],需要输入时再启用 stdin。 - 本地交互命令使用
inherit。 - stdout 保持机器可读,stderr 保存诊断,必要时分别落盘。
- 后台任务交给专门的进程管理器。
练习
- 分别用
pipe、inherit、ignore执行同一脚本。 - 将 stdout 与 stderr 写入两个文件。
- 解释为什么
detached: true配合pipe仍可能拖住父进程。
验收清单
- [ ] 能说出 stdio 数组下标 0、1、2 的含义。
- [ ] 知道何时
child.stdout是null。 - [ ] 理解
pipe必须被消费。 - [ ] 不把
detached当作完整生产方案。