跳到主要内容

Node.js 子进程(Child Process)

child_process 模块提供了生成子进程的能力,可以在 Node.js 中执行外部命令、运行脚本或创建新的 Node.js 进程。

为什么需要子进程?

Node.js 是单线程的,但有时需要:

  • 执行系统命令(如 lsgit
  • 运行 CPU 密集型任务(避免阻塞主线程)
  • 并行执行多个任务
  • 与其他语言编写的程序交互

四种创建子进程的方法

方法描述特点
spawn启动命令,流式返回结果最灵活,适合大数据
exec启动 shell 执行命令缓冲输出,适合小数据
execFile直接执行可执行文件不启动 shell,更安全
fork创建 Node.js 子进程支持 IPC 通信

spawn - 流式执行命令

spawn 是最基础的子进程创建方法,通过流的方式返回数据。

基本用法

const { spawn } = require('child_process');

// 启动 ls 命令
const ls = spawn('ls', ['-lh', '/usr']);

// 监听标准输出
ls.stdout.on('data', (data) => {
console.log(`输出: ${data}`);
});

// 监听标准错误
ls.stderr.on('data', (data) => {
console.error(`错误: ${data}`);
});

// 监听进程结束
ls.on('close', (code) => {
console.log(`子进程退出码: ${code}`);
});

// 监听进程错误
ls.on('error', (err) => {
console.error('启动子进程失败', err);
});

解释

  • spawn(command, args, options) 第一个参数是命令,第二个是参数数组
  • stdoutstderr 是可读流,可以流式处理输出
  • close 事件表示进程完全结束
  • error 事件表示启动失败,不是命令执行失败

spawn 选项

const { spawn } = require('child_process');

const child = spawn('node', ['script.js'], {
cwd: '/path/to/dir', // 工作目录
env: { // 环境变量
NODE_ENV: 'production',
PATH: process.env.PATH
},
stdio: 'pipe', // 标准输入输出配置
detached: false, // 是否独立运行
uid: 1000, // 用户 ID
gid: 1000, // 组 ID
timeout: 5000, // 超时时间(毫秒)
killSignal: 'SIGTERM', // 超时时发送的信号
shell: false, // 是否在 shell 中执行
argv0: 'custom-name', // 设置 argv[0]
windowsHide: true, // Windows 下隐藏控制台窗口
windowsVerbatimArguments: false // Windows 下不转义参数
});

选项详解

选项类型默认值说明
cwdstring/URLprocess.cwd()子进程的工作目录
envObjectprocess.env环境变量对象,undefined 值会被忽略
stdioArray/string'pipe'子进程的标准 IO 配置
detachedbooleanfalse子进程是否独立于父进程运行
uidnumber-设置进程的用户 ID
gidnumber-设置进程的组 ID
timeoutnumber0进程运行的最大时间(毫秒)
killSignalstring/number'SIGTERM'超时或取消时发送的信号
shellboolean/stringfalse是否在 shell 中执行命令
argv0stringcommand显式设置 argv[0] 的值
windowsHidebooleanfalseWindows 下隐藏子进程控制台窗口
windowsVerbatimArgumentsbooleanfalseWindows 下不对参数进行转义
signalAbortSignal-用于取消子进程的 AbortSignal

shell 选项

const { spawn } = require('child_process');

// 使用默认 shell
const child1 = spawn('echo $HOME', { shell: true });

// 指定特定的 shell
const child2 = spawn('echo %PATH%', {
shell: process.env.ComSpec // Windows CMD
});

const child3 = spawn('echo $PATH', {
shell: '/bin/bash' // 指定 bash
});

安全警告:当启用 shell 选项时,不要将未经验证的用户输入传递给命令,这可能导致命令注入攻击。

stdio 配置

stdio 选项用于配置子进程的标准输入输出,非常灵活:

const { spawn } = require('child_process');

// 方式一:字符串
// 'pipe' - 创建管道(默认)
// 'ignore' - 忽略
// 'inherit' - 继承父进程的 stdio
const child1 = spawn('ls', ['-la'], { stdio: 'inherit' });

// 方式二:数组,分别配置 stdin, stdout, stderr
const child2 = spawn('ls', ['-la'], {
stdio: ['pipe', 'pipe', 'pipe'] // [stdin, stdout, stderr]
});

// 实际应用:输出到文件
const fs = require('fs');
const out = fs.openSync('output.txt', 'w');
const child3 = spawn('ls', ['-la'], {
stdio: ['ignore', out, 'pipe']
});

stdio 可选值

说明
'pipe'创建管道,子进程的 stdio 流可用
'ignore'忽略,不创建管道,/dev/null 的效果
'inherit'继承父进程的对应 stdio
'overlapped'Windows 特有,与 'pipe' 类似但支持异步 I/O
Stream 对象共享一个可读或可写流
正整数使用父进程的文件描述符
null / undefined使用默认值(stdin 为 'pipe',其他为 'ignore'
'ipc'创建 IPC 通道(仅 fork 时使用)

使用文件描述符

const fs = require('fs');
const { spawn } = require('child_process');

// 使用文件描述符
const inputFile = fs.openSync('input.txt', 'r');
const outputFile = fs.openSync('output.txt', 'w');
const errorFile = fs.openSync('error.txt', 'w');

const child = spawn('node', ['process.js'], {
stdio: [inputFile, outputFile, errorFile]
});

// 或者直接传入 Stream
const child2 = spawn('node', ['process.js'], {
stdio: [
fs.createReadStream('input.txt'),
fs.createWriteStream('output.txt'),
'pipe'
]
});

创建额外的文件描述符

const { spawn } = require('child_process');
const fs = require('fs');

// 创建 fd=4 用于输出
const extraOut = fs.openSync('extra.log', 'w');

const child = spawn('node', ['script.js'], {
stdio: ['pipe', 'pipe', 'pipe', 'pipe', extraOut]
// 0: stdin, 1: stdout, 2: stderr, 3: 可选的 pipe, 4: 额外输出
});

// 访问额外的管道
child.stdio[3]; // fd=3 的流

detached 选项

detached 选项让子进程可以在父进程退出后继续运行:

const { spawn } = require('child_process');

// 创建独立运行的子进程
const child = spawn('node', ['long-running.js'], {
detached: true,
stdio: 'ignore' // 重要:必须忽略 stdio
});

// 父进程可以退出,子进程继续运行
child.unref();

console.log('父进程退出,子进程 PID:', child.pid);

使用场景

  • 启动后台服务
  • 运行守护进程
  • 启动长时间运行的任务

注意事项

  • 设置 detached: true 后,通常需要 stdio: 'ignore'child.unref()
  • 子进程会成为新进程组的组长
  • 在 Windows 上行为略有不同,子进程会有自己的控制台窗口

启动后台服务示例

const { spawn } = require('child_process');
const fs = require('fs');

// 启动后台服务,记录输出
const logFile = fs.openSync('service.log', 'a');

const service = spawn('node', ['service.js'], {
detached: true,
stdio: ['ignore', logFile, logFile]
});

service.unref();

console.log('服务已启动,PID:', service.pid);
// 父进程可以安全退出

Windows 平台注意事项

const { spawn } = require('child_process');

// Windows 下执行 .bat 或 .cmd 文件
const bat = spawn('cmd.exe', ['/c', 'my.bat']);

// 或者使用 shell 选项
const bat2 = spawn('my.bat', [], { shell: true });

// 跨平台兼容写法
const { platform } = require('os');
const command = platform() === 'win32' ? 'npm.cmd' : 'npm';
const child = spawn(command, ['install']);

exec - 缓冲执行命令

exec 会启动一个 shell 来执行命令,并缓冲所有输出。

基本用法

const { exec } = require('child_process');

// 执行命令,回调获取输出
exec('ls -lh /usr', (error, stdout, stderr) => {
if (error) {
console.error(`执行错误: ${error}`);
return;
}
console.log(`标准输出: ${stdout}`);
console.error(`标准错误: ${stderr}`);
});

解释

  • exec 会启动一个 shell,可以使用 shell 语法(管道、通配符等)
  • 所有输出被缓冲,一次性返回
  • 默认最大输出 200KB,超过会导致错误

exec 选项

const { exec } = require('child_process');

exec('ls -la', {
cwd: '/home/user', // 工作目录
env: { /* ... */ }, // 环境变量
encoding: 'utf8', // 编码
timeout: 10000, // 超时时间(毫秒)
maxBuffer: 1024 * 1024, // 最大缓冲区大小(字节)
killSignal: 'SIGTERM' // 超时信号
}, (error, stdout, stderr) => {
// ...
});

Promise 版本

const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);

async function listFiles() {
try {
const { stdout, stderr } = await execPromise('ls -la');
console.log('输出:', stdout);
if (stderr) {
console.error('错误:', stderr);
}
} catch (error) {
console.error('执行失败:', error.message);
}
}

listFiles();

使用 shell 特性

const { exec } = require('child_process');

// 使用管道
exec('cat file.txt | grep "error"', (err, stdout) => {
console.log(stdout);
});

// 使用通配符
exec('ls *.js', (err, stdout) => {
console.log(stdout);
});

// 使用环境变量展开
exec('echo $HOME', (err, stdout) => {
console.log('Home:', stdout.trim());
});

安全警告:不要将未经验证的用户输入传递给 exec,可能导致命令注入攻击。

execFile - 直接执行文件

execFile 直接执行可执行文件,不启动 shell,更安全高效。

基本用法

const { execFile } = require('child_process');

// 直接执行命令
execFile('node', ['--version'], (error, stdout, stderr) => {
if (error) {
console.error(error);
return;
}
console.log('Node 版本:', stdout.trim());
});

// 执行脚本文件
execFile('./script.sh', (error, stdout, stderr) => {
console.log(stdout);
});

与 exec 的区别

const { exec, execFile } = require('child_process');

// exec - 启动 shell
exec('echo $HOME', (err, stdout) => {
console.log('exec:', stdout.trim()); // 输出: /home/user
});

// execFile - 不启动 shell
execFile('echo', ['$HOME'], (err, stdout) => {
console.log('execFile:', stdout.trim()); // 输出: $HOME
});

// execFile 加 shell 选项
execFile('echo', ['$HOME'], { shell: true }, (err, stdout) => {
console.log('execFile with shell:', stdout.trim()); // 输出: /home/user
});

解释

  • execFile 不启动 shell,所以 $HOME 不会被展开
  • 这也意味着不能使用管道、通配符等 shell 特性
  • 但这更安全,避免了命令注入风险

fork - 创建 Node.js 子进程

forkspawn 的特殊版本,专门用于创建 Node.js 子进程,并建立 IPC 通信通道。

基本用法

父进程(parent.js)

const { fork } = require('child_process');

// 创建子进程
const child = fork('./child.js');

// 发送消息给子进程
child.send({ type: 'greet', name: 'World' });

// 接收子进程消息
child.on('message', (message) => {
console.log('收到子进程消息', message);
});

// 监听子进程退出
child.on('exit', (code) => {
console.log('子进程退出,码', code);
});

子进程(child.js)

// 接收父进程消息
process.on('message', (message) => {
console.log('收到父进程消息', message);

// 发送消息给父进程
process.send({ reply: `Hello, ${message.name}!` });
});

// 子进程也可以主动发送消息给父进程
process.send({ status: 'ready' });

处理 CPU 密集型任务

主进程

const { fork } = require('child_process');
const path = require('path');

// 创建工作进程
const workers = [];
const numCPUs = require('os').cpus().length;

for (let i = 0; i < numCPUs; i++) {
const worker = fork(path.join(__dirname, 'worker.js'));
workers.push(worker);

worker.on('message', (result) => {
console.log('计算结果:', result);
});
}

// 分发任务
workers.forEach((worker, index) => {
worker.send({
task: 'calculate',
data: index * 1000
});
});

工作进程(worker.js)

process.on('message', (msg) => {
if (msg.task === 'calculate') {
// 执行 CPU 密集型计算
const result = heavyCalculation(msg.data);

// 发送结果回主进程
process.send({ result });
}
});

function heavyCalculation(n) {
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i);
}
return result;
}

fork 选项

const { fork } = require('child_process');

const child = fork('./child.js', [], {
cwd: '/path/to/dir', // 工作目录
env: { /* ... */ }, // 环境变量
execPath: '/path/to/node', // Node.js 可执行文件路径
execArgv: ['--inspect'], // 传递给 Node.js 的参数
silent: false, // true 则子进程的 stdio 被管道连接
stdio: ['pipe', 'pipe', 'pipe', 'ipc'] // 必须包含 'ipc'
});

fork 选项详解

选项类型默认值说明
cwdstring/URL-子进程的工作目录
envObjectprocess.env环境变量键值对
execPathstringprocess.execPath用于创建子进程的可执行文件路径
execArgvstring[]process.execArgv传递给可执行文件的参数列表
gidnumber-设置进程的群组 ID
serializationstring'json'IPC 消息的序列化方式
signalAbortSignal-用于关闭子进程的 AbortSignal
killSignalstring/number'SIGTERM'终止进程时发送的信号
silentbooleanfalse是否将子进程的 stdio 管道连接到父进程
stdioArray/string-覆盖 silent 选项
uidnumber-设置进程的用户 ID
windowsVerbatimArgumentsbooleanfalseWindows 下不对参数转义
timeoutnumber-进程运行的最大时间(毫秒)

serialization 选项

const { fork } = require('child_process');

// 默认使用 JSON 序列化
const child1 = fork('./worker.js', [], {
serialization: 'json' // 只能传递 JSON 可序列化的数据
});

// 高级序列化 - 支持更多数据类型
const child2 = fork('./worker.js', [], {
serialization: 'advanced' // 支持 Buffer、TypedArray 等
});

serialization 选项说明

说明
'json'默认值,使用 JSON 序列化,只能传递 JSON 可序列化的数据
'advanced'高级序列化,支持 Buffer、TypedArray、Date、Map、Set 等

使用高级序列化发送 Buffer

// 父进程
const child = fork('./worker.js', [], {
serialization: 'advanced'
});

// 可以发送 Buffer
child.send({
data: Buffer.from('Hello')
});

// 子进程 (worker.js)
process.on('message', (msg) => {
console.log(msg.data instanceof Buffer); // true
console.log(msg.data.toString()); // 'Hello'
});

同步方法

execSync

const { execSync } = require('child_process');

try {
// 同步执行命令
const output = execSync('ls -la');
console.log(output.toString());

// 指定编码,直接返回字符串
const output2 = execSync('echo "hello"', { encoding: 'utf8' });
console.log(output2); // "hello\n"

// 捕获标准错误
const output3 = execSync('ls /nonexistent 2>&1', { encoding: 'utf8' });

} catch (error) {
console.error('命令执行失败:', error.status);
console.error('错误输出:', error.stderr.toString());
}

execFileSync

const { execFileSync } = require('child_process');

// 同步执行可执行文件
const version = execFileSync('node', ['--version'], { encoding: 'utf8' });
console.log('Node 版本:', version.trim());

spawnSync

const { spawnSync } = require('child_process');

const result = spawnSync('ls', ['-la'], {
encoding: 'utf8',
cwd: '/home/user'
});

console.log('状态码:', result.status);
console.log('标准输出:', result.stdout);
console.log('标准错误:', result.stderr);
console.log('错误:', result.error);

解释

  • 同步方法会阻塞主线程,只在必要场景使用(如启动脚本、构建过程)
  • 生产环境应优先使用异步方法

ChildProcess 对象

所有异步方法返回的都是 ChildProcess 对象。

属性

const { spawn } = require('child_process');
const child = spawn('node', ['script.js']);

// 进程 ID
console.log('PID:', child.pid);

// 标准流
child.stdin; // 可写流
child.stdout; // 可读流
child.stderr; // 可读流
child.stdio; // 所有标准流数组

// 进程状态
console.log('已杀死:', child.killed);
console.log('退出码:', child.exitCode);
console.log('信号:', child.signalCode);

// 连接状态(fork 创建的进程)
console.log('已连接:', child.connected);

// 启动参数
console.log('命令:', child.spawnfile); // 启动的命令/文件
console.log('参数:', child.spawnargs); // 完整的参数列表

// IPC 通道(fork 创建的进程)
console.log('通道:', child.channel); // IPC 通道对象

属性详解

属性说明
pid子进程的进程 ID
stdin可写流,用于向子进程写入数据
stdout可读流,子进程的标准输出
stderr可读流,子进程的标准错误
stdio包含所有标准流的数组 [stdin, stdout, stderr, ...]
killed布尔值,表示是否已调用 kill()
exitCode子进程的退出码,未退出时为 null
signalCode终止子进程的信号,正常退出时为 null
connected布尔值,表示 IPC 通道是否仍然连接
spawnfile子进程启动的命令或文件名
spawnargs启动子进程时的完整参数列表
channelIPC 通道的引用(仅 fork 创建的进程)

方法

const { spawn } = require('child_process');
const child = spawn('node', ['long-running.js']);

// 发送信号终止进程
child.kill(); // 默认 SIGTERM
child.kill('SIGKILL'); // 强制终止
child.kill('SIGINT'); // 中断信号

// 断开 IPC 连接(fork 创建的进程)
child.disconnect();

// 发送消息(fork 创建的进程)
child.send({ data: 'hello' });

// ref/unref 控制进程是否阻止父进程退出
child.unref(); // 父进程可以独立退出
child.ref(); // 父进程会等待子进程

send() 方法详解

const { fork } = require('child_process');
const child = fork('./worker.js');

// 发送普通消息
child.send({ type: 'task', data: 'hello' });

// 发送句柄(如 socket、server)
const net = require('net');
const server = net.createServer();
server.listen(8080, () => {
child.send('server', server); // 发送 server 句柄
});

// 完整语法
// child.send(message, sendHandle, options, callback)

send() 参数说明

参数类型说明
messageObject要发送的消息对象
sendHandleHandle可选,要传递的句柄(net.Socket、net.Server 等)
optionsObject可选,配置对象 { keepOpen: false }
callbackFunction可选,消息发送后的回调函数

传递句柄示例

// 父进程 - main.js
const { fork } = require('child_process');
const net = require('net');

// 创建 TCP 服务器
const server = net.createServer();
const worker = fork('./worker.js');

server.on('connection', (socket) => {
// 将连接传递给工作进程
worker.send('socket', socket);
});

server.listen(8080);
console.log('服务器监听 8080 端口');

// 子进程 - worker.js
process.on('message', (msg, socket) => {
if (msg === 'socket' && socket) {
// 处理客户端连接
socket.end('由工作进程处理\n');
}
});

kill() 方法说明

const { spawn } = require('child_process');
const child = spawn('node', ['long-running.js']);

// kill() 返回值
const killed = child.kill(); // true(表示信号发送成功)
console.log('信号已发送:', killed);

// kill() 是发送信号,不是强制终止
// 如果子进程忽略 SIGTERM,kill() 不会终止它
child.kill('SIGTERM'); // 发送终止信号(可被捕获)
child.kill('SIGKILL'); // 发送强制终止信号(无法被忽略)

// kill() 与进程状态
console.log(child.killed); // true(表示 kill() 已被调用)
console.log(child.exitCode); // 可能还是 null(进程可能还在退出中)

常用信号

信号说明
SIGTERM默认信号,优雅终止(可被捕获)
SIGKILL强制终止(无法被捕获或忽略)
SIGINT中断信号,通常是 Ctrl+C
SIGHUP挂起信号,通常在终端关闭时发送

事件

const { spawn } = require('child_process');
const child = spawn('node', ['script.js']);

// spawn - 进程启动成功
child.on('spawn', () => {
console.log('子进程已启动');
});

// exit - 进程退出
child.on('exit', (code, signal) => {
console.log(`退出码: ${code}, 信号: ${signal}`);
});

// close - 所有 stdio 流关闭
child.on('close', (code, signal) => {
console.log('流已关闭');
});

// disconnect - IPC 连接断开
child.on('disconnect', () => {
console.log('IPC 连接断开');
});

// error - 启动失败或其他错误
child.on('error', (err) => {
console.error('错误:', err);
});

// message - 收到子进程消息(fork 创建的进程)
child.on('message', (message, sendHandle) => {
console.log('消息:', message);
});

实战示例

执行 Git 命令

const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);

class Git {
constructor(repoPath) {
this.repoPath = repoPath;
}

async status() {
const { stdout } = await execPromise('git status --porcelain', {
cwd: this.repoPath
});
return stdout.trim().split('\n').filter(Boolean);
}

async log(count = 10) {
const { stdout } = await execPromise(
`git log --oneline -n ${count}`,
{ cwd: this.repoPath }
);
return stdout.trim().split('\n');
}

async branch() {
const { stdout } = await execPromise('git branch', {
cwd: this.repoPath
});
return stdout.trim().split('\n').map(b => b.replace(/^\*?\s*/, ''));
}

async diff(file) {
const { stdout } = await execPromise(
`git diff ${file}`,
{ cwd: this.repoPath }
);
return stdout;
}
}

// 使用
const git = new Git('/path/to/repo');
const status = await git.status();
console.log('变更文件:', status);

进程池

const { fork } = require('child_process');
const path = require('path');

class ProcessPool {
constructor(workerFile, poolSize) {
this.workerFile = workerFile;
this.poolSize = poolSize;
this.pool = [];
this.queue = [];

// 初始化进程池
for (let i = 0; i < poolSize; i++) {
this.createWorker();
}
}

createWorker() {
const worker = fork(this.workerFile);
worker.busy = false;

worker.on('message', (result) => {
worker.busy = false;
// 处理队列中的下一个任务
if (this.queue.length > 0) {
const { task, resolve } = this.queue.shift();
this.runTask(worker, task, resolve);
}
});

this.pool.push(worker);
}

runTask(worker, task, resolve) {
worker.busy = true;
worker.once('message', resolve);
worker.send(task);
}

execute(task) {
return new Promise((resolve) => {
// 找到空闲的工作进程
const worker = this.pool.find(w => !w.busy);

if (worker) {
this.runTask(worker, task, resolve);
} else {
// 所有进程都忙,加入队列
this.queue.push({ task, resolve });
}
});
}

killAll() {
this.pool.forEach(worker => worker.kill());
}
}

// 使用
const pool = new ProcessPool('./worker.js', 4);

// 并行执行任务
const tasks = [1, 2, 3, 4, 5, 6, 7, 8];
const results = await Promise.all(
tasks.map(n => pool.execute({ number: n }))
);
console.log('结果:', results);

pool.killAll();

命令行进度显示

const { spawn } = require('child_process');

function runWithProgress(command, args) {
return new Promise((resolve, reject) => {
const child = spawn(command, args);

child.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
lines.forEach(line => {
if (line.includes('%')) {
// 更新进度显示
process.stdout.write(`\r${line}`);
}
});
});

child.stderr.on('data', (data) => {
process.stderr.write(data);
});

child.on('close', (code) => {
process.stdout.write('\n');
if (code === 0) {
resolve();
} else {
reject(new Error(`进程退出码: ${code}`));
}
});
});
}

// 使用
await runWithProgress('npm', ['install']);
console.log('安装完成');

实时日志监控

const { spawn } = require('child_process');
const fs = require('fs');

class LogMonitor {
constructor(logFile) {
this.logFile = logFile;
this.tail = null;
this.listeners = [];
}

start() {
// 使用 tail -f 监控日志
this.tail = spawn('tail', ['-f', this.logFile]);

this.tail.stdout.on('data', (data) => {
const lines = data.toString().split('\n').filter(Boolean);
lines.forEach(line => {
this.listeners.forEach(fn => fn(line));
});
});

this.tail.stderr.on('data', (data) => {
console.error('tail 错误:', data.toString());
});

this.tail.on('error', (err) => {
console.error('启动 tail 失败:', err);
});
}

onLog(callback) {
this.listeners.push(callback);
}

stop() {
if (this.tail) {
this.tail.kill();
this.tail = null;
}
}
}

// 使用
const monitor = new LogMonitor('/var/log/app.log');
monitor.start();

monitor.onLog((line) => {
if (line.includes('ERROR')) {
console.log('发现错误:', line);
}
});

// 停止监控
// monitor.stop();

安全注意事项

1. 避免命令注入

// ❌ 危险:用户输入直接拼接到命令
const userInput = 'file.txt; rm -rf /';
exec(`cat ${userInput}`, callback); // 危险!

// ✅ 安全:使用 spawn 传递参数
const userInput = 'file.txt';
spawn('cat', [userInput], callback); // 安全

// ✅ 安全:验证用户输入
const safeName = userInput.replace(/[^a-zA-Z0-9._-]/g, '');
exec(`cat ${safeName}`, callback);

2. 限制资源使用

const { spawn } = require('child_process');

const child = spawn('node', ['script.js'], {
timeout: 30000, // 30秒超时
maxBuffer: 1024 * 1024, // 最大输出 1MB
killSignal: 'SIGTERM'
});

// 设置额外的内存限制(需要配合操作系统工具)

3. 正确处理错误

const { spawn } = require('child_process');

const child = spawn('some-command');

// 始终监听 error 事件
child.on('error', (err) => {
console.error('启动失败:', err);
});

// 处理非零退出码
child.on('exit', (code, signal) => {
if (code !== 0) {
console.error(`进程异常退出, code=${code}, signal=${signal}`);
}
});

最佳实践

1. 选择正确的方法

// 大量输出 → spawn
spawn('find', ['/', '-name', '*.js']);

// 小量输出、需要 shell 特性 → exec
exec('cat *.txt | sort | uniq');

// 执行可执行文件 → execFile
execFile('./build.sh');

// Node.js 子进程通信 → fork
fork('./worker.js');

2. 使用 AbortController

const { spawn } = require('child_process');

const controller = new AbortController();
const { signal } = controller;

const child = spawn('node', ['long-task.js'], { signal });

child.on('error', (err) => {
if (err.name === 'AbortError') {
console.log('任务已取消');
}
});

// 5秒后取消
setTimeout(() => controller.abort(), 5000);

3. 清理子进程

const children = [];

// 注册退出处理
process.on('exit', () => {
children.forEach(child => child.kill());
});

// 创建子进程
const child = spawn('node', ['script.js']);
children.push(child);

小结

本章我们学习了:

  1. 四种子进程方法:spawn、exec、execFile、fork
  2. spawn:流式处理,适合大数据量
  3. exec:缓冲输出,适合小数据量和需要 shell 特性
  4. execFile:直接执行文件,更安全
  5. fork:Node.js 子进程,支持 IPC 通信
  6. 同步方法:execSync、execFileSync、spawnSync
  7. ChildProcess 对象:属性、方法、事件
  8. 实战示例:Git 命令、进程池、日志监控
  9. 安全注意事项:命令注入、资源限制、错误处理

练习

  1. 编写函数执行 git status 并解析输出
  2. 创建一个进程池来并行处理多个任务
  3. 实现一个简单的命令行工具包装器
  4. 使用 fork 实现主进程和工作进程的通信
  5. 编写一个日志监控工具,实时过滤错误日志