Node.js 子进程管理:我是如何被 spawn 逼疯的
做 memo 之前,我对 Node.js 子进程的理解基本等于零。不就是 spawn 一下吗?能有多难?结果...还真挺难的。

做 memo 之前,我对 Node.js 子进程的理解基本等于零。
不就是在代码里调个 spawn 跑个命令吗?能有多难。
结果,真正上手才发现——这玩意儿的水比我想的深多了。
背景:为什么我需要 spawn
memo 是个 Agent,要替用户干活,核心能力之一就是执行 shell 命令。
最简单的方案是什么?child_process.exec 一把梭,输入命令,拿到输出,完事儿。
但随着功能变复杂,问题就来了:
- 命令执行时间太长,想实时看输出怎么办?
- 超时了想杀掉进程,怎么搞?
- 同时跑多个命令,怎么管理?
- 输出太多,怎么截断?
这些问题逼着我去认真看了 child_process 的 API,然后发现——原来 spawn 才是真·主角。
spawn 是什么
简单说,spawn 是 Node.js 用来启动子进程的 API:
但这只是最基础的用法。真正的挑战在于:你怎么管好这个进程。
第一层:获取输出
spawn 默认不返回输出,只给你流。你得自己监听:
这一步还好,主要是耐心。
第二层:输入交互
更麻烦的是,有些命令需要交互——比如 sudo、ftp、python 脚本等待用户输入。
这时候你得用 stdin 写数据进去:
但 timing 很容易出问题:命令还没准备好,你就开始写,数据就丢了。
memo 的做法是等命令输出特定的「ready signal」再写入,比如看到 password: 提示符再写。
第三层:超时与终止
这是最容易漏的地方。
我一开始写的代码大概是这样:
看起来没问题,但——如果这个命令跑 10 分钟怎么办?用户等的花儿都谢了。
memo 现在的做法是两层退出机制:
先 SIGTERM(温和退出,给程序清理资源的机会),再 SIGKILL(强制杀死)。
第四层:进程池与会话管理
跑一个命令还好,但如果同时跑几十个呢?
memo 用的方案是统一会话管理器:
每个子进程都有唯一 ID,可以:
- 查询状态
- 发送信号(kill)
- 获取历史输出
同时限制最大活跃数量,防止资源被耗尽。
第五层:输出截断
Agent 是按 token 收费的,子进程输出不能无限返回。
memo 做了截断处理:
默认最多返回 8000 字符,不够可以调。
血的教训
踩过的坑列几个:
- 忘了等进程退出:spawn 是异步的,进程可能还在跑,但你以为已经结束了
- 没处理 stderr:只看 stdout,错误信息全丢了
- 输入 timing:没等命令 ready 就写 stdin,数据被吞了
- 内存泄漏:进程退出了但没清理,长时间运行内存慢慢涨
- 信号传递:Ctrl+C 杀的是父进程,子进程还在后台跑
总结
spawn 看起来简单——一行代码就启动了。
但真正做一个「安全、可控、可观测」的子进程管理系统,需要考虑的细节比我想的多得多:
- 输出流处理
- 输入交互
- 超时终止
- 会话管理
- 资源限制
- 内存清理
好消息是,这些坑踩一遍之后就通了。memo 现在跑得很稳,这些经验也值得分享给你。
如果你也在做类似的 Agent 或 CLI 工具,希望这篇文章能帮你少踩几个坑。