跳到主要内容

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' // 超时时发送的信号
});

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']
});

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').cppus().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'
});

同步方法

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);

方法

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(); // 父进程会等待子进程

事件

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. 编写一个日志监控工具,实时过滤错误日志