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.stdoutchild.stderrnull,父进程不能监听其 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() 更清晰。

detachedunref() 与后台进程

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)

若孙进程继承了描述符,exitclose 可能延迟。

常见错误

const child = spawn("tool", [], { stdio: "inherit" });

// 错误:inherit 时 stdout 为 null。
child.stdout?.on("data", () => undefined);

最佳实践

练习

  1. 分别用 pipeinheritignore 执行同一脚本。
  2. 将 stdout 与 stderr 写入两个文件。
  3. 解释为什么 detached: true 配合 pipe 仍可能拖住父进程。

验收清单