Nodejs child_process 模块

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nodejs child_process 模块相关的知识,希望对你有一定的参考价值。

参考技术A

child_process.exec(command[, options][, callback])
开启一个子进程执行 shell 命令,在回调中获取输出信息

在 lib/child_process 源码中 exec 内部只是处理参数后调用了 execFile 方法。

exec 调用 execFile

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

execFile 源码
调用 spawn 创建进程 child
通过 child.stdout.on(\'data\') 收集输出 stdout
当 child 进程 close 时把 _stdout 传入 callback 内输出
同样当 child 进程 触发 error 事件会把 _stderr 传入 callback 中输出,同时会 destroy 进程 child 的 stdout 和 stderr

execFile 通过 spawn 创建子进程执行命令,是 nodejs 围绕 spawn 方法的封装, 调用 spawn 执行命令一次性的把输出结果和错误通过回调输出。
exec 底层调用了 execFile,不过 exec 通过 option.shell = true 的配置使 spawn 创建的进程通过 /bin/sh -c 执行传入的 shell 脚本
适合执行开销小的命令

wiki Spawn (computing)

使用 spawn 创建子进程执行 命令时 options.stdio 比较常用的配置项就是默认的 pipe 和 inherit , ignore 选项将会忽略子进程的输入输出

如果使用 inherit 选项,命令的执行内容将会直接在控制台输出,因为指定了输出的 fd 的 1 process.stdio 标准输出流直接会在控制台中输出。

如果使用 pipe 选项, child.stdout 也可以通过 pipe 输出流的方式输出到可写流中, 比如 fs.createWriteStream 简单的文件可写流。

通过执行 index.js 执行 spwan API 设置 detached: true 创建守护进程并解除父进程引用。守护进程将会一直向 stdout.log 中写入数据。

通过守护进程的例子会发现 stdout.log 写入的 父进程 ppid 与 index.js 打印出的父进程 pid 不一样。这里的 ppid:1 是 init 进程,当父进程退出后通过父进程创建还在执行的子进程将会被 init 进程收养成为 孤儿进程

ChildProcess.prototype.spawn 创建子进程源码

通过 ChildProcess.prototype.spawn 创建子进程,通过 c++ 的 Pipe 创建不同 pipe 实例的和 Process 创建进程,这里可以看下 stido 中 pipe 和 ipc 模式创建不同的 Pipe 实例
pipe ipc 创建 Pipe 实例的区别

setupChannel 添加 ipc send() 方法

child_process和IPC探究
【node源码】child_process源码阅读
线程和进程

node child_process模块

NodeJs是一个单进程的语言,不能像Java那样可以创建多线程来并发执行。当然在大部分情况下,NodeJs是不需要并发执行的,因为它是事件驱动性永不阻塞。但单进程也有个问题就是不能充分利用CPU的多核机制,根据前人的经验,可以通过创建多个进程来充分利用CPU多核,并且Node通过了child_process模块来创建完成多进程的操作。

child_process模块给予node任意创建子进程的能力,node官方文档对于child_proces模块给出了四种方法,映射到操作系统其实都是创建子进程。但对于开发者而已,这几种方法的api有点不同

child_process.exec(command[, options][, callback]) 启动
子进程来执行shell命令,可以通过回调参数来获取脚本shell执行结果

const { exec } = require(‘child_process‘);
exec(‘cat *.js bad_file | wc -l‘, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.log(`stderr: ${stderr}`);
});


child_process.execfile(file[, args][, options][, callback])
与exec类型不同的是,不衍生一个 shell,而是,指定的可执行的 file 被直接衍生为一个新进程,这使得它比 child_process.exec() 更高效。 由于没有衍生 shell,因此不支持像 I/O 重定向和文件查找这样的行为。

const { execFile } = require(‘child_process‘);
const child = execFile(‘node‘, [‘--version‘], (error, stdout, stderr) => {
  if (error) {
    throw error;
  }
  console.log(stdout);
});

 

与另外另个命令不同的是接受一个函数,如果提供了一个 callback 函数,则它被调用时会带上参数 (error, stdout, stderr)。 当成功时,error 会是 null。 当失败时,error 会是一个 Error实例。 error.code 属性会是子进程的退出码,error.signal 会被设为终止进程的信号。 除 0 以外的任何退出码都被认为是一个错误。 

exec()与execfile()在创建的时候可以指定timeout属性设置超时时间,一旦超时会被杀死 
如果使用execfile()执行可执行文件,那么头部一定是#!/usr/bin/env node

 

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

仅仅执行一个shell命令,不需要获取执行结果

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.log(`stderr: ${data}`);
});

ls.on(‘close‘, (code) => {
  console.log(`子进程退出码:${code}`);
});

// 例子,一种执行 ‘ps ax | grep ssh‘ 的方法:
const { spawn } = require(‘child_process‘);
const ps = spawn(‘ps‘, [‘ax‘]);
const grep = spawn(‘grep‘, [‘ssh‘]);

ps.stdout.on(‘data‘, (data) => {
  grep.stdin.write(data);
});

ps.stderr.on(‘data‘, (data) => {
  console.log(`ps stderr: ${data}`);
});

ps.on(‘close‘, (code) => {
  if (code !== 0) {
    console.log(`ps 进程退出码:${code}`);
  }
  grep.stdin.end();
});

grep.stdout.on(‘data‘, (data) => {
  console.log(data.toString());
});

grep.stderr.on(‘data‘, (data) => {
  console.log(`grep stderr: ${data}`);
});

grep.on(‘close‘, (code) => {
  if (code !== 0) {
    console.log(`grep 进程退出码:${code}`);
  }
});


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

与另外三个不同的是它开启的是一个node进程,执行的只能是js文件。并通过建立 IPC 通讯通道来调用指定的模块,该通道允许父进程与子进程之间相互发送信息。

进程间通信

node 与 子进程之间的通信是使用IPC管道机制完成。如果子进程 
也是node进程(使用fork),则可以使用监听message事件和使用send()来通信。

main.js

var cp = require(‘child_process‘);
//只有使用fork才可以使用message事件和send()方法
var n = cp.fork(‘./child.js‘);
n.on(‘message‘,function(m){
  console.log(m);
})

n.send({"message":"hello"});

child.js

var cp = require(‘child_process‘);
process.on(‘message‘,function(m){
 console.log(m);
})
process.send({"message":"hello I am child"})

父子进程之间会创建IPC通道,message事件和send()便利用IPC通道通信.

句柄传递

学会如何创建子进程后,我们创建一个HTTP服务并启动多个进程来共同 
做到充分利用CPU多核。 
worker.js

var http = require(‘http‘);
http.createServer(function(req,res){
  res.end(‘Hello,World‘);
  //监听随机端口
}).listen(Math.round((1+Math.random())*1000),‘127.0.0.1‘);

main.js

var fork = require(‘child_process‘).fork;
var cpus = require(‘os‘).cpus();
for(var i=0;i<cpus.length;i++){
  fork(‘./worker.js‘);
}

上述代码会根据你的cpu核数来创建对应数量的fork进程,每个进程监听一个随机端口来提供HTTP服务。

上述就完成了一个典型的Master-Worker主从复制模式。在分布式应用中用于并行处理业务,具备良好的收缩性和稳定性。这里需要注意,fork一个进程代价是昂贵的,node单进程事件驱动具有很好的性能。此例的多个fork进程是为了充分利用CPU的核,并非解决并发问题.
上述示例有个不太好的地方就是占有了太多端口,那么能不能对于多个子进程全部使用同一个端口从而对外提供http服务也只是使用这一个端口。尝试将上述的端口随机数改为8080,启动会发现抛出如下异常。

events.js:72
        throw er;//Unhandled ‘error‘ event
Error:listen EADDRINUSE
XXXX

抛出端口被占有的异常,这意味着只有一个worker.js才能监听8080端口,而其余的会抛出异常。
如果要解决对外提供一个端口的问题,可以参考nginx反向代理的做法。对于Master进程使用80端口对外提供服务,而对于fork的进程则使用随机端口,Master进程接受到请求就将其转发到fork进程中

对于刚刚所说的代理模式,由于进程每收到一个连接会使用掉一个文件描述符,因此代理模式中客户端连接到代理进程,代理进程再去连接fork进程会使用掉两个文件描述符,OS中文件描述符是有限的,为了解决这个问题,node引入进程间发送句柄的功能。
在node的IPC进程通讯API中,send(message,[sendHandle])的第二个参数就是句柄。
句柄就是一种标识资源的引用,它的内部包含了指向对象的文件描述符。句柄可以用来描述一个socket对象,一个UDP套接子,一个管道
主进程向工作进程发送句柄意味着当主进程接收到客户端的socket请求后则直接将这个socket发送给工作进程,而不需要再与工作进程建立socket连接,则文件描述符的浪费即可解决。我们来看示例代码:
main.js

var cp = require(‘child_process‘);
var child = cp.fork(‘./child.js‘);
var server = require(‘net‘).createServer();
//监听客户端的连接
server.on(‘connection‘,function(socket){
  socket.end(‘handled by parent‘);
});
//启动监听8080端口
server.listen(8080,function(){
//给子进程发送TCP服务器(句柄)
  child.send(‘server‘,server);
});

child.js

process.on(‘message‘,function(m,server){
  if(m===‘server‘){
    server.on(‘connection‘,function(socket){
      socket.end(‘handle by child‘);
    });
  }
});

使用telnet或curl都可以测试:

1 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
2 handled by parent
3 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
4 handle by child
5 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
6 handled by parent
7 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
8 handled by parent

测试结果是每次对于客户端的连接,有可能父进程处理也有可能被子进程处理。现在我们尝试仅提供http服务,并且为了让父进程更加轻量,仅让父进程传递句柄给子进程而不做请求处理:

main.js

var cp = require(‘child_process‘);
var child1 = cp.fork(‘./child.js‘);
var child2 = cp.fork(‘./child.js‘);
var child3 = cp.fork(‘./child.js‘);
var child4 = cp.fork(‘./child.js‘);
var server = require(‘net‘).createServer();
//父进程将接收到的请求分发给子进程
server.listen(8080,function(){
  child1.send(‘server‘,server);
  child2.send(‘server‘,server);
  child3.send(‘server‘,server);
  child4.send(‘server‘,server);
  //发送完句柄后关闭监听
  server.close();
});

child.js

var http = require(‘http‘);
var serverInChild = http.createServer(function(req,res){
 res.end(‘I am child.Id:‘+process.pid);
});
//子进程收到父进程传递的句柄(即客户端与服务器的socket连接对象)
process.on(‘message‘,function(m,serverInParent){
  if(m===‘server‘){
    //处理与客户端的连接
    serverInParent.on(‘connection‘,function(socket){
      //交给http服务来处理
      serverInChild.emit(‘connection‘,socket);
    });
  }
});

当运行上述代码,此时查看8080端口占有会有如下结果:

[email protected] ~/code/nodeStudy $ lsof -i:8080
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
node    5120 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)
node    5126 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)
node    5127 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)
node    5133 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)

运行curl查看结果:

[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5127
[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5133
[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5120
[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5126
[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5133
[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5126

 

以上是关于Nodejs child_process 模块的主要内容,如果未能解决你的问题,请参考以下文章

NodeJs之child_process

node child_process模块

nodejs 怎样检测子进程执行完成

浅析 NodeJS 多进程和集群

nodejs开发游戏服务器遇到的性能问题

nodejs child_process.spawnSync 或 child_process.spawn 包裹在 yieldable 生成器中,它返回输出