kekobin / blog

blog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Node基础篇之子进程child_process

kekobin opened this issue · comments

简介

child_process提供生成子进程的能力,主要基于起spawn方法,官方示例:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

默认情况下,上面的stdin、stdout和stderr的管道在父node.js进程和派生的子进程之间建立。这些管道具有有限的(和特定于平台的)容量。如果子进程在未捕获输出的情况下向stdout写入的数据超过了该限制,则子进程将阻止等待管道缓冲区接受更多数据。这与管壳中管道的行为相同。如果输出不会被使用,请使用{stdio:'ignore'}选项。

创建子进程的方式

  • child_process.exec():生成一个shell并在该shell中运行命令,完成后将stdout和stderr传递给回调函数。
  • child_process.execfile():与child_process.exec()类似,只是它直接生成命令,默认情况下不首先生成shell。
  • child_process.fork():生成一个新的node.js进程并调用一个指定的模块,该模块建立了一个IPC通信通道,允许在父进程和子进程之间发送消息。
  • child_process.execsync():将阻止node.js事件循环的child_process.exec()的同步版本。
  • child_process.execfilesync():将阻止node.js事件循环的child_process.execfile()的同步版本。

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

其中,options可选参数为:

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

例子:

const exec = require('child_process').exec;

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

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

const execFile = require('child_process').execFile;

execFile('node', ['-v'], (error, stdout, stderr) => {
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + typeof stderr);
});

输出结果和上面的exec例子一模一样,只是在于是否创建了shell。

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

modulePath:子进程运行的模块。
args参数说明:

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

fork实例

silent

parent.js

var child_process = require('child_process');

// 例子一:会打印出 output from the child
// 默认情况,silent 为 false,子进程的 stdout 等
// 从父进程继承
child_process.fork('./child.js', {
    silent: false
});

// 例子二:不会打印出 output from the silent child
// silent 为 true,子进程的 stdout 等
// pipe 向父进程
child_process.fork('./silentChild.js', {
    silent: true
});

// 例子三:打印出 output from another silent child
var child = child_process.fork('./anotherSilentChild.js', {
    silent: true
});

child.stdout.setEncoding('utf8');
child.stdout.on('data', function(data){
    console.log(data);
});

child.js

console.log('output from the child');

silentChild.js

console.log('output from the silent child');

anotherSilentChild.js

console.log('output from another silent child');

结果是:
image

说明:

  • slient: false 指的是子进程的stdin、stdout和stderr从父进程继承,也即像父进程那样直接输入输出、报错等;
  • slient: true 指的是子进程的stdin、stdout和stderr将通过管道传输到父进程,也就是说子进程中的信息,不能直接输入输出、报错,必须从父进程中显示的接收,在父进程中才能被展示出来。

IPC进程间通信

  • parent.js
var child_process = require('child_process');

var child = child_process.fork('./child.js');

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

child.send({from: 'parent'});
  • child.js
process.on('message', function(m){
    console.log('message from parent: ' + JSON.stringify(m));
});

process.send({from: 'child'});

运行结果:

message from child: {"from":"child"}
message from parent: {"from":"parent"}

execArgv
设置execArgv的目的一般在于,让子进程跟父进程保持相同的执行环境。
比如,父进程指定了--harmony,如果子进程没有指定,那么就要跪了。

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

options参数说明:

  • argv0:[String] 这货比较诡异,在uninx、windows上表现不一样。有需要再深究。
  • stdio:[Array] | [String] 子进程的stdio。参考这里
  • detached:[Boolean] 让子进程独立于父进程之外运行。同样在不同平台上表现有差异。
  • shell:[Boolean] | [String] 如果是true,在shell里运行程序。默认是false。(很有用,比如 可以通过 /bin/sh -c xxx 来实现 .exec() 这样的效果)
var spawn = require('child_process').spawn;
var ls = spawn('ls', ['-al'], {
    stdio: 'inherit'
});

ls.on('close', function(code){
    console.log('child exists with code: ' + code);
});
var spawn = require('child_process').spawn;

// 运行 echo "hello nodejs" | wc
var ls = spawn('bash', ['-c', 'echo "hello nodejs" | wc'], {
    stdio: 'inherit',
    shell: true
});

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

错误处理,包含两种场景,这两种场景有不同的处理方式。

场景1:命令本身不存在,创建子进程报错。
场景2:命令存在,但运行过程报错。

// 场景1
child.on('error', (err) => {...
// 场景2
child2.stderr.on('data'...

exec()与execFile()之间的区别

首先,exec() 内部调用 execFile() 来实现,而 execFile() 内部调用 spawn() 来实现。

exec() -> execFile() -> spawn()
其次,execFile() 内部默认将 options.shell 设置为false,exec() 默认不是false。

事件

ChildProcess 实例注册的事件有 error,close,message

  • 如果进程不能被衍生(spawn)或者被 killed,error 事件被触发;
  • 当子进程使用 process.send() 函数发送信息的时候,message 事件会被触发。这就是父子进程相会交流的方式。

每一个子进程都会得到三个标准的输入输出流,我们可以通过 child.stdin,child.stdout 和 child.stderr 进入。

当那些流关闭之后,使用他们的子进程将会触发 close 事件。这个 close 事件和 exit 事件不同,因为多个子进程可能共享相同的 stdio 流,因此一个子进程退出不代表流关闭了。

// 主进程中   
child.stdout.on('data', (data) => {
    console.log(`child stdout: ${data}`)
});
child.stderr.on('data', (data) => {
    console.error(`stderror ${data}`);
});

示例

可以将密集计算的逻辑放到单独的js文件中,然后再通过fork的方式来计算,等计算完成时再通知主进程计算结果,这样避免主进程繁忙的情况了。

compute.js

const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};

process.on('message', (msg) => {
  const sum = longComputation();
  process.send(sum);
});

index.js

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

const server = http.createServer();

server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const compute = fork('compute.js');
    compute.send('start');
    compute.on('message', sum => {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

通过child.unref()让父进程退出(动态创建守护进程的方式)

调用child.unref(),将子进程从父进程的事件循环中剔除。于是父进程可以愉快的退出。这里有几个要点

调用child.unref()
设置detached为true
设置stdio为ignore(这点容易忘)

var child_process = require('child_process');
var child = child_process.spawn('node', ['child.js'], {
    detached: true,
    stdio: 'ignore'  // 备注:如果不置为 ignore,那么 父进程还是不会退出
    // stdio: 'inherit'
});

child.unref();

信号

SIGINT:interrupt,程序终止信号,通常在用户按下CTRL+C时发出,用来通知前台进程终止进程。
SIGTERM:terminate,程序结束信号,该信号可以被阻塞和处理,通常用来要求程序自己正常退出。shell命令kill缺省产生这个信号。如果信号终止不了,我们才会尝试SIGKILL(强制终止)。

spawn执行python脚本报“ImportError: No module named pandas”

问题的原因是python安装第三方包是通过全局安装的(即sudo安装),在运行时会通过全局的PYTHONPATH去搜索import到的这些第三方包,node是通过process.env加载环境变量的,有可能少了python运行需要的路径(一般node都是$用户启动,而不是#根用户启动),故需要在运行时手动添加上去:

const ls = spawn('python', ['test.py','-u', 'xxx'], {
    timeout: 10000,
    env: Object.assign({}, process.env, {PYTHONPATH: '/xxx/lib/python2.7/site-packages'})
});

这样,python通过node开启的子进程运行时就找得到它内部引用的第三方包了。

python一般通过pip(类似于npm)装包到具体包版本下的site-packages下,所以要引用到这个目录下的包,必须得把它的路径暴露给全局的环境变量

问题查询集合:
Python 模块搜索路径
Node.js child_process execution of Python script causing errors importing modules
Python won't find installed module when run as child process within Electron app

参考

child_process
Node.js child_process模块解读