阅读 627

玩转 node 子进程 — child_process

前言

在 node 中,child_process 模块的重要性不言而喻,特别如果你在前端工作中有编写一些前端工具,不可避免需要使用 child_process 这个模块,并且可以使用它进行文件压缩、脚本运行等操作,所以深入掌握它变得刻不容缓,希望通过本篇文章,你能和我一样彻底掌握 child_process 模块。

博客 github地址为:fengshi123/blog ,汇总了作者的所有博客,也欢迎关注及 star ~

一、创建子进程的方式

child_process 模块提供了以下 4 个方法用于创建子进程,并且每一种方法都有对应的同步版本

  • spawn: 启动一个子进程来执行命令;
  • exec:  启动一个子进程来执行命令,与 spawn 不同的是,它有一个回调函数获知子进程的状况;
  • execFile: 启动一个子进程来执行可执行文件;
  • fork:  与 spawn 类似,不同点在于它创建 Node 的子进程只需指定要执行的 JavaScript 文件模块即可;

基本用法和区分点如下:

const cp = require('child_process');

// spawn
cp.spawn('node', ['./dir/test1.js'],
  { stdio: 'inherit' }
);
// exec
cp.exec('node ./dir/test1.js', (err, stdout, stderr) => {
  console.log(stdout);
});
// execFile
cp.execFile('node', ['./dir/test1.js'],(err, stdout, stderr) => {
  console.log(stdout);
});
// fork
cp.fork('./dir/test1.js',
  { silent: false }
);

// ./dir/test1.js
console.log('test1 输出...');
复制代码

差异点:

  • spawn 与 exec、execFile 不同的是,后两者创建时可以指定 timeout 属性设置超时时间,一旦创建的进程运行超过设定的时间将会被杀死;
  • exec 与 execFile 不同的是,exec 适合执行已有的命令,execFile 适合执行文件;
  • exec、execFile、fork 都是 spawn 的延伸应用,底层都是通过 spawn 实现的;

差异列表如下:

类型回调/异常进程类型执行类型可设置超时
spawn不支持任意命令不支持
exec支持任意命令支持
execFile支持任意可执行文件支持
fork不支持NodeJavaScript 文件不支持

二、子进程列表

1、child_process.exec(command[, options][, callback])

创建一个 shell,然后在 shell 里执行命令。执行完成后,将 stdout、stderr 作为参数传入回调方法。 options 参数说明:

  • cwd:当前工作路径;
  • env:环境变量;
  • encoding:编码,默认是 utf8;
  • shell:用来执行命令的 shell,unix 上默认是 /bin/sh,windows 上默认是 cmd.exe;
  • timeout:默认是 0;
  • killSignal:默认是 SIGTERM;
  • uid:执行进程的 uid;
  • gid:执行进程的 gid;
  • maxBuffer: 标准输出、错误输出最大允许的数据量(单位为字节),如果超出的话,子进程就会被杀死;默认是 200*1024(即 200k )

备注:

  1. 如果 timeout 大于 0,那么,当子进程运行超过 timeout 毫秒,那么,就会给进程发送 killSignal 指定的信号(比如 SIGTERM)。
  2. 如果运行没有出错,那么 error 为 null。如果运行出错,那么,error.code 就是退出代码(exist code),error.signal 会被设置成终止进程的信号。(比如 CTRL+C 时发送的 SIGINT)

例子 1: 基本用法

  1. 执行成功,error 为 null;执行失败,error 为 Error 实例;error.code 为错误码;
  2. stdout、stderr 为标准输出、标准错误;默认是字符串,除非 options.encoding 为 buffer;注意:stdout、stderr 会默认在结尾加上换行符;
const { exec } = require('child_process');

exec('ls', (error, stdout, stderr) => {
  if (error) {
    console.error('error:', error);
    return;
  }
  console.log('stdout: ' + stdout);
  console.log('stderr: ' + stderr);
})


exec('ls', {cwd: __dirname + '/dir'}, (error, stdout, stderr) => {
  if (error) {
    console.error('error:', error);
    return;
  }
  console.log('stdout: ' + stdout);
  console.log('stderr: ' + stderr);
})
复制代码

例子 2: 子进程输出/错误监听

除了例子1 中支持回调函数获取子进程的输出和错误外,还提供 stdout 和 stderr 对输出和错误进行监听,示例如下所示

const child = exec('node ./dir/test1.js')

child.stdout.on('data', data => {
  console.log('stdout 输出:', data);
})
child.stderr.on('data', err => {
  console.log('error 输出:', err);
})
复制代码

2、child_process.execFile(file[, args][, options][, callback])

跟 .exec() 类似,不同点在于,没有创建一个新的 shell,options 参数与 exec 一样

例子 1:执行 node 文件

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

// 1、执行命令
execFile('node', ['./dir/test1.js'], (error, stdout, stderr) => {
  if (error) {
    console.error('error:', error);
    return;
  }
  console.log('stdout: ' + stdout); 
  console.log('stderr: ' + stderr);
})
复制代码

例子 2:执行 shell 脚本文件

需要注意的是,我们执行 shell 脚本的时候,并没有重新开一个 shell,即:我们在根目录下运行 execFile 命令执行 ./dir/test2.sh 脚本,我们在 ./dir/test2.sh 脚本中执行与 test2.sh 同目录的 test1..js 文件,我们不能直接写成 node .test1.js 会找不到文件,应该从根目录去寻找; 注意:shell 脚本文件中如果需要访问 node 环境中的变量,可以将变量赋值给 process.env,这样在 shell 脚本中就可以通过 $变量名 进行直接访问;

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

// 2、执行 shell 脚本
// 在 shell 脚本中可以访问到 process.env 的属性 
process.env.DIRNAME = __dirname;
execFile(`${__dirname}/dir/test2.sh`, (error, stdout, stderr) => {
  if (error) {
    console.error('error:', error);
    return;
  }
  console.log('stdout: ' + stdout); // stdout: 执行 test2.sh  test1 输出...
  console.log('stderr: ' + stderr);
})

// ./dir/test2.sh

#! /bin/bash
echo '执行 test2.sh'
node $DIRNAME/dir/test1.js


// ./dir/test1.js
console.log('test1 输出...');
复制代码

3、child_process.fork(modulePath[, args][, options])

(1)modulePath:子进程运行的模块; (2)args:字符串参数列表; (3)options 参数如下所示,其中与 exec 重复的参数就不重复介绍:

  • execPath: 用来创建子进程的可执行文件,默认是 /usr/local/bin/node。也就是说,你可通过 execPath 来指定具体的 node 可执行文件路径;(比如多个 node 版本)
  • execArgv: 传给可执行文件的字符串参数列表。默认是 process.execArgv,跟父进程保持一致;
  • silent: 默认是 false,即子进程的 stdio 从父进程继承。如果是 true,则直接 pipe 向子进程的child.stdin、child.stdout 等;
  • stdio: 选项用于配置在父进程和子进程之间建立的管道,如果声明了 stdio,则会覆盖 silent 选项的设置;

例子 1:silent

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

// 1、默认 silent 为 false,子进程会输出 output from the child3
fork('./dir/child3.js', {
  silent: false
});

// 2、设置 silent 为 true,则子进程不会输出
fork('./dir/child3.js', {
  silent: true
});

// 3、通过 stdout 属性,可以获取到子进程输出的内容
const child3 = fork('./dir/child3.js', {
  silent: true
});

child3.stdout.setEncoding('utf8');
child3.stdout.on('data', function (data) {
  console.log('stdout 中输出:');
  console.log(data);
});
复制代码

4、child_process.spawn(command[, args][, options])

(1)command:要执行的命令; (2)args:字符串参数列表; (2)options 参数说明,其它重复的参数不在重复:

  • argv0:显式地设置发送给子进程的 argv[0] 的值, 如果没有指定,则会被设置为 command 的值;
  • detached:[Boolean] 让子进程独立于父进程之外运行;

例子 1:基础例子

const spawn = require('child_process').spawn;
const ls = spawn('ls', ['-al']);

// 输出相关的数据
ls.stdout.on('data', function(data){
    console.log('data from child: ' + data);
});

// 错误的输出
ls.stderr.on('data', function(data){
    console.log('error from child: ' + data);
});

// 子进程结束时输出
ls.on('close', function(code){
    console.log('child exists with code: ' + code);
});
复制代码

结果截图如下: image.png

例子 2:声明 stdio

父子进程共用一个输出管道;

// 2、声明 stdio
var ls = spawn('ls', ['-al'], {
  stdio: 'inherit'
});

ls.on('close', function(code){
  console.log('child exists with code: ' + code);
});
复制代码

结果截图如下: image.png

例子 3:错误场景

// 3、错误处理
// 3.1、场景1: 命令本身不存在,创建子进程报错
const child = spawn('bad_command');

child.on('error', (err) => {
  console.log('Failed to start child process 1: ', err);
});

// 3.2、场景2: 命令存在,但运行过程报错
const child2 = spawn('ls', ['nonexistFile']);

child2.stderr.on('data', function(data){
    console.log('Error msg from process 2: ' + data);
});

child2.on('error', (err) => {
  console.log('Failed to start child process 2: ', err);
});
复制代码

三、常用事件

其继承自 EventEmitter 类, ChildProcess 的实例代表创建的子进程,其可以由以上介绍的几种创建子进程的方式创建得到。

3.1、常用的事件

(1)close 事件:子进程的 stdio 流关闭时触发; (2)disconnect 事件:事件在父进程手动调用 child.disconnect 函数时触发; (3)error 事件:产生错误时会触发; (4)exit 事件:子进程自行退出时触发; (5)message 事件:它在子进程使用 process.send() 函数来传递消息时触发;

3.2、示例

// 例子
const { fork } = require('child_process');

const child = fork('./dir/test5.js')

child.on('close', (code, signal) => {
  console.log('close 事件:', code, signal);
})

child.on('disconnect', () => {
  console.log('disconnect 事件...');
})

child.on('error', (code, signal) => {
  console.log('error 事件:', code, signal);
})

child.on('exit', (code, signal) => {
  console.log('exit 事件:', code, signal);
})

child.on('message', (val) => {
  console.log('message 事件:', val);
})

// ./dir/test5.js
console.log('event_test 输出...');
process.send('子进程发送给父进程的消息...')
复制代码

各个事件捕获的顺序如下所示: image.png

四、进程间通信

通过以上 4 种 API 创建子进程后,父进程与子进程之间将会创建 IPC(Inter-Process Communication)通道,通过 IPC 通道,父子进程之间通过 message 和 send 来通信。Node 中实现 IPC 通道的是管道(pipe)技术,具体实现细节由 libuv 实现,在 Windows 下由命名管道(named pipe)实现,*nix 系统则采用 Unix Domain Socket 实现,IPC 创建和实现的示意图如下:

父进程在实际创建子进程之前,会创建 IPC 通道并监听它,然后才真正创建出子进程,并通过环境变量(NODE_CHANNEL_FD)告诉子进程这个 IPC 通道的文件描述符。子进程在启动过程中,根据文件描述符去连接这个已存在的 IPC 通道,从而完成父子进程之间的连接,创建 IPC 通道的示意图如下:

建立连接之后的父子进程就可以自由通信了,由于 IPC 通道是用命名管道或 Domain Socket 创建的,它们与网络 socket 的行为比较类似,属于双向通信。但是由于它们在系统内核中就完成了进程间的通信,而不用经过实际的网络层,非常高效。

例子 1:基本例子

// 父进程
const child3 = fork('./dir/child3_1.js');

child3.on('message', (m)=>{
  console.log('message from child: ' + JSON.stringify(m));
});

child3.send({from: 'parent'});

// 子进程
process.on('message', function(m){
  console.log('message from parent: ' + JSON.stringify(m));
});

process.send({from: 'child'});
复制代码

五、总结

本文对比了 node 创建子进程的 4 种方式 exec、execfile、fork、spawn 的异同点,并且介绍了它们常用的事件、IPC 通信,如有不足,欢迎指出。

博客 github地址为:fengshi123/blog ,汇总了作者的所有博客,也欢迎关注及 star ~