Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node基础篇之子进程child_process #36

Open
kekobin opened this issue Sep 19, 2019 · 0 comments
Open

Node基础篇之子进程child_process #36

kekobin opened this issue Sep 19, 2019 · 0 comments

Comments

@kekobin
Copy link
Owner

kekobin commented Sep 19, 2019

简介

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模块解读

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant