Node.js 子进程(Child Process)
child_process 模块提供了生成子进程的能力,可以在 Node.js 中执行外部命令、运行脚本或创建新的 Node.js 进程。
为什么需要子进程?
Node.js 是单线程的,但有时需要:
- 执行系统命令(如
ls、git) - 运行 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)第一个参数是命令,第二个是参数数组stdout和stderr是可读流,可以流式处理输出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 子进程
fork 是 spawn 的特殊版本,专门用于创建 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);
小结
本章我们学习了:
- 四种子进程方法:spawn、exec、execFile、fork
- spawn:流式处理,适合大数据量
- exec:缓冲输出,适合小数据量和 shell 特性
- execFile:直接执行文件,更安全
- fork:Node.js 子进程,支持 IPC 通信
- 同步方法:execSync、execFileSync、spawnSync
- ChildProcess 对象:属性、方法、事件
- 实战示例:Git 命令、进程池、日志监控
- 安全注意事项:命令注入、资源限制、错误处理
练习
- 编写函数执行
git status并解析输出 - 创建一个进程池来并行处理多个任务
- 实现一个简单的命令行工具包装器
- 使用 fork 实现主进程和工作进程的通信
- 编写一个日志监控工具,实时过滤错误日志