Node.js 中 Child Process 模块详解:创建、通信与生命周期管理
本文深入解析 Node.js 中 child_process 模块的使用。涵盖 spawn、fork、exec 等异步创建方式及同步变体,详解 ChildProcess 类的生命周期事件(exit、close、error 等)。重点讲解标准流处理、ref/unref 机制以及通过 IPC 通道传递消息和句柄(如 TCP Server)的高级用法,帮助开发者高效管理 Node.js 子进程。
简介
Node.js 的主事件循环采用单线程模型,其内部维护了一个 Worker Pool 来处理耗时操作。此外,开发者还可以利用 nodejs 提供的 worker_threads 模块手动创建新线程以执行自定义任务。
本文将介绍另一种执行 Node.js 任务的机制:child process(子进程)。
child process
Node.js 通过 lib/child_process.js 提供的 child_process 模块来创建子进程。
需注意,worker_threads 用于创建子线程,而 child_process 用于创建子进程,两者概念不同。
在 child_process 模块中,既支持同步创建进程,也支持异步创建进程。同步方法通常是在对应异步方法名后添加 Sync 后缀。
创建的进程由 ChildProcess 类进行表示。
我们查看 ChildProcess 的定义:
interface ChildProcess extends events.EventEmitter {
stdin: Writable | null;
stdout: Readable | null;
stderr: Readable | null;
readonly channel?: Pipe | null;
readonly stdio: [
Writable | null, // stdin
Readable | null, // stdout
Readable | null, // stderr
Readable | Writable | null | undefined, // extra
Readable | Writable | null | undefined // extra
];
readonly killed: boolean;
readonly pid: number;
readonly connected: boolean;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly spawnargs: string[];
readonly spawnfile: string;
kill(signal?: NodeJS.Signals | number): boolean;
send(message: Serializable, callback?: (error: Error | null) => void): boolean;
send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean;
send(message: Serializable, sendHandle?: SendHandle, options?: MessageOptions, callback?: (error: Error | null) => void): boolean;
disconnect(): void;
unref(): void;
ref(): void;
/**
* events.EventEmitter
* 1. close
* 2. disconnect
* 3. error
* 4. exit
* 5. message
*/
...
}
从定义可见,ChildProcess 继承自 EventEmitter,因此它具备发送和接收事件的能力。
ChildProcess 可接收的事件包括五种:close、disconnect、error、exit 和 message。
当父进程调用 subprocess.disconnect() 或子进程调用 process.disconnect() 时,将触发 disconnect 事件。
若出现无法创建进程、无法终止进程或向子进程发送消息失败等情况,将触发 error 事件。
当子进程结束时,会触发 exit 事件。
当子进程的 stdio 流被关闭时,会触发 close 事件。需注意,close 事件与 exit 事件不同,因为多个进程可能共享同一个 stdio,所以触发 exit 事件并不一定会导致 close 事件。
以下是一个展示 close 和 exit 区别的例子:
const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.on('close', (code) => {
console.log(`子进程使用代码 $[code] 关闭所有 stdio`);
});
ls.on('exit', (code) => {
console.log(`子进程使用代码 $[code] 退出`);
});
最后是 message 事件,当子进程使用 process.send() 发送消息时触发。
ChildProcess 包含几个标准流属性,分别为 stderr、stdout、stdin 和 stdio。
其中,stderr、stdout 和 stdin 分别代表标准错误、标准输出和标准输入,易于理解。
以下是一个使用 stdout 的例子:
const { spawn } = require('child_process');
const subprocess = spawn('ls');
subprocess.stdout.on('data', (data) => {
console.log(`接收到数据块 ${data}`);
});
stdio 实际上是 stderr、stdout 和 stdin 的集合:
readonly stdio: [
Writable | null, // stdin
Readable | null, // stdout
Readable | null, // stderr
Readable | Writable | null | undefined, // extra
Readable | Writable | null | undefined // extra
];
其中,stdio[0] 表示 stdin,stdio[1] 表示 stdout,stdio[2] 表示 stderr。
如果在通过 stdio 创建子进程时,这三个标准流被设置为除 pipe 之外的其他值,那么 stdin、stdout 和 stderr 属性将为 null。
以下是一个使用 stdio 的例子:
const assert = require('assert');
const fs = require('fs');
const child_process = require('child_process');
const subprocess = child_process.spawn('ls', {
stdio: [
0, // 使用父进程的 stdin 用于子进程。
'pipe', // 把子进程的 stdout 通过管道传到父进程 。
fs.openSync('err.out', 'w') // 把子进程的 stderr 定向到一个文件。
]
});
assert.strictEqual(subprocess.stdio[0], null);
assert.strictEqual(subprocess.stdio[0], subprocess.stdin);
assert(subprocess.stdout);
assert.strictEqual(subprocess.stdio[1], subprocess.stdout);
assert.strictEqual(subprocess.stdio[2], null);
assert.strictEqual(subprocess.stdio[2], subprocess.stderr);
通常情况下,父进程会维护一个对子进程的引用计数,只有当所有子进程退出后,父进程才会退出。
这个引用即为 ref。如果调用了 unref 方法,则允许父进程独立于子进程退出。
const { spawn } = require('child_process');
const subprocess = spawn(process.argv[0], ['child_program.js'], {
detached: true,
stdio: 'ignore'
});
subprocess.unref();
最后,我们看一下如何通过 ChildProcess 来发送消息:
subprocess.send(message[, sendHandle[, options]][, callback])
其中 message 是要发送的消息,callback 是发送消息后的回调函数。
sendHandle 比较特殊,它可以是一个 TCP 服务器或 socket 对象。通过将这些 handle 传递给子进程,子进程将在 message 事件中将 handle 传递给回调函数,从而在子进程中进行处理。
以下是一个传递 TCP Server 的例子,首先看主进程:
const subprocess = require('child_process').fork('subprocess.js');
// 打开 server 对象,并发送该句柄。
const server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('由父进程处理');
});
server.listen(1337, () => {
subprocess.send('server', server);
});
再看子进程:
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', (socket) => {
socket.end('由子进程处理');
});
}
});
可以看到子进程接收到了 server handle,并在子进程中监听 connection 事件。
下面看一个传递 socket 对象的例子:
onst { fork } = require('child_process');
const normal = fork('subprocess.js', ['normal']);
const special = fork('subprocess.js', ['special']);
// 开启 server,并发送 socket 给子进程。
// 使用 `pauseOnConnect` 防止 socket 在被发送到子进程之前被读取。
const server = require('net').createServer({ pauseOnConnect: true });
server.on('connection', (socket) => {
// 特殊优先级。
if (socket.remoteAddress === '74.125.127.100') {
special.send('socket', socket);
return;
}
// 普通优先级。
normal.send('socket', socket);
});
server.listen(1337);
subprocess.js 的内容:
process.on('message', (m, socket) => {
if (m === 'socket') {
if (socket) {
// 检查客户端 socket 是否存在。
// socket 在被发送与被子进程接收这段时间内可被关闭。
socket.end(`请求使用 ${process.argv[2]} 优先级处理`);
}
}
});
主进程创建了两个 subprocess,一个处理特殊的优先级,另一个处理普通的优先级。
异步创建进程
child_process 模块有 4 种方式可以异步创建进程,分别是 child_process.spawn()、child_process.fork()、child_process.exec() 和 child_process.execFile()。
先查看各个方法的定义:
child_process.spawn(command[, args][, options])
child_process.fork(modulePath[, args][, options])
child_process.exec(command[, options][, callback])
child_process.execFile(file[, args][, options][, callback])
其中 child_process.spawn 是基础方法,它会异步生成一个新的进程,其他方法 fork、exec 和 execFile 都是基于 spawn 实现的。
fork 会生成新的 Node.js 进程。
exec 和 execFile 以新进程执行新命令,并带有回调。它们的区别在于:在 Windows 环境中,如果要执行 .bat 或 .cmd 文件,没有 shell 终端是无法执行的,此时只能使用 exec 启动,execFile 无法执行。
或者也可以使用 spawn。
以下是一个在 Windows 中使用 spawn 和 exec 的例子:
// 仅在 Windows 上。
const { spawn } = require('child_process');
const bat = spawn('cmd.exe', ['/c', 'my.bat']);
bat.stdout.on('data', (data) => {
console.log(data.toString());
});
bat.stderr.on('data', (data) => {
console.error(data.toString());
});
bat.on('exit', (code) => {
console.log(`子进程退出,退出码 $[code]`);
});
const { exec, spawn } = require('child_process');
exec('my.bat', (err, stdout, stderr) => {
if (err) {
console.error(err);
return;
}
console.log(stdout);
});
// 文件名中包含空格的脚本:
const bat = spawn('"my script.cmd"', ['a', 'b'], { shell: true });
// 或:
exec('"my script.cmd" a b', (err, stdout, stderr) => {
// ...
});
同步创建进程
同步创建进程可以使用 child_process.spawnSync()、child_process.execSync() 和 child_process.execFileSync()。同步方法会阻塞 Node.js 事件循环,暂停任何其他代码的执行,直到子进程退出。
通常对于一些脚本任务来说,使用同步创建进程会比较常用。
到此这篇关于在 nodejs 中创建 child process 的方法的文章就介绍到这了,更多相关 nodejs 中创建 child process 内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
Java和JavaScript的应用场景分工:后端企业级开发与Web前端交互
2026-11-05 15:57:34
除了浏览器和Node.js,还有哪些轻量级Js脚本运行工具?
2026-11-04 17:38:24
无需后端:用JavaScript实现简单网站的本地数据存储
2026-11-03 23:03:30
Node.js 下载慢或官网打不开怎么办
2026-11-03 18:19:37
JS解析大体积Excel文件内存溢出怎么办
2026-11-03 18:19:37
