当前位置:首页 >> 网络编程

详解从Node.js的child_process模块来学习父子进程之间的通信

child_process模块提供了和popen(3)一样的方式来产生自进程,这个功能主要是通过child_process.spawn函数来提供的:

const spawn = require('child_process').spawn; 
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(`child process exited with code $[code]`); 
}); 

默认情况下,Node.js进程和子进程之间的stdin,stdout,stderr管道是已经存在的。通常情况下这个方法可以以一种非阻塞的方式来传递数据。(注意,有些程序在内部使用line-buffered I/O。因为这也不会影响到Node.js,这意味着传递给子进程的数据可能不会马上消费)。

chid-process的spawn方法是通过一种异步的方式来产生自进程的,因此不会阻塞Node.js的事件循环,然而child-process.spawnSync方法是同步的,他会阻塞事件循环只到产生的进程退出或者终止。

child_process.exec:产生一个shell客户端,然后使用shell来执行程序,当完成的时候传递给回调函数一个stdout和stderr

child_process.execFile:和exec相似,但是他不会马上产生一个shell

child_process.fork:产生一个新的Node.js进程,同时执行一个特定的模块来产生IPC通道,进而在父进程和子进程之间传输数据

child_process.execSync:和exec不同之处在于会阻塞Node.js的事件循环,然而child-process

child_process.execFileSync:和execFile不同之处在于会阻塞Node.js的事件循环,然而child-process在一些特殊情况下,例如自动化shell脚本,同步的方法可能更加有用。多数情况下,同步的方法会对性能产生重要的影响,因为他会阻塞事件循环

child_process.spawn(), child_process.fork(), child_process.exec(), and child_process.execFile()都是异步的API。每一个方法都会产生一个ChildProcess实例,而且这个对象实现了Node.js的EventEmitter这个API,于是父进程可以注册监听函数,在子进程的特定事件触发的时候被调用。 child_process.exec() 和 child_process.execFile()可以指定一个可选的callback函数,这个函数在子进程终止的时候被调用。

在windows平台上执行.bat和.cmd:

child_process.exec和child_process.execFile的不同之处可能随着平台不同而有差异。在Unit/Linux/OSX平台上execFile更加高效,因为他不会产生shell。在windows上,.bat/.cmd在没有终端的情况下是无法执行的,因此就无法使用execFile(child_process.spawn也无法使用)。在window上,.bat/.cmd可以使用spawn方法,同时指定一个shell选项;或者使用child_process.exec或者通过产生一个cmd.exe同时把.bat/.cmd文件传递给它作为参数(child_process.exec就是这么做的)。

const spawn = require('child_process').spawn; 
const bat = spawn('cmd.exe', ['/c', 'my.bat']);//使用shell方法指定一个shell选项 
bat.stdout.on('data', (data) => { 
 console.log(data); 
}); 
bat.stderr.on('data', (data) => { 
 console.log(data); 
}); 
 
bat.on('exit', (code) => { 
 console.log(`Child exited with code $[code]`); 
}); 

或者也可以使用如下的方式:

const exec = require('child_process').exec;//产生exec,同时传入.bat文件 
exec('my.bat', (err, stdout, stderr) => { 
 if (err) { 
  console.error(err); 
  return; 
 } 
 console.log(stdout); 
}); 

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

其中options中的maxBuffer参数表示stdout/stderr允许的最大的数据量,如果超过了数据量那么子进程就会被杀死,默认是200*1024比特;killSignal默认是'SIGTERM'。其中回调函数当进程结束时候调用,参数分别为error,stdout,stderr。这个方法返回的是一个ChildProcess对象。

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

上面的代码产生一个shell,然后使用这个shell执行命令,同时对产生的结果进行缓存。其中回调函数中的error.code属性表示子进程的exit code,error.signal表示结束这个进程的信号,任何非0的代码表示出现了错误。默认的options参数值如下:

{ 
 encoding: 'utf8', 
 timeout: 0, 
 maxBuffer: 200*1024,//stdout和stderr允许的最大的比特数据,超过她子进程就会被杀死 
 killSignal: 'SIGTERM', 
 cwd: null, 
 env: null 
} 

如果timeout非0,那么父进程就会发送信号,这个信号通过killSignal指定,默认是"SIGTERM"来终结子进程,如果子进程超过了timeout指定的时间。注意:和POSIX系统上调用exec方法不一样的是,child_process.exec不会替换当前线程,而是使用一个shell去执行命令

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

其中file表示需要执行的文件。 child_process.execFile()和exec很相似,但是这个方法不会产生一个shell。指定的可执行文件会马上产生一个新的线程,因此其效率比child_process.exec高。

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

因为不会产生shell,一些I/O redirection和file globbing这些行为不支持

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

其中modulePath表示要在子线程中执行的模块。其中options中的参数silent如果设置为true,那么子进程的stdin, stdout, stderr将会被传递给父进程,如果设置为false那么就会从父进程继承。execArgv默认的值为process.execArgv,execPath表示用于创建子进程的可执行文件。这个fork方法是child_process.spawn的一种特殊用法,用于产生一个Node.js的子进程,和spawn一样返回一个ChildProcess对象。返回的ChildProcess会有一个内置的传输通道用于在子进程和父进程之间传输数据(用ChildProcess的send方法完成)。我们必须注意,产生的Node.js进程和父进程之间是独立的,除了他们之间的IPC传输通道以外。每一个进程有独立的内存,有自己独立的V8引擎。由于产生一个子进程需要其他额外的资源分配,因此产生大量的子进程不被提倡。默认情况下,child_process.fork会使用父进程的process.execPath来产生一个Node.js实例,options中的execPath允许指定一个新的路径。通过指定execPath产生的新的进程和父进程之间通过文件描述符(子进程的环境变量NODE_CHANNEL_FD)来通信。这个文件描述符上的input/output应该是一个JSON对象。和POSIX系统调用fork不一样的是,child_process.fork不会克隆当前的进程

最后,我们来看看子进程和父进程之间是如何通信的:

服务器端的代码:

var http = require('http'); 
var cp = require('child_process'); 
var server = http.createServer(function(req, res) { 
  var child = cp.fork(__dirname + '/cal.js'); 
  //每个请求都单独生成一个新的子进程 
  child.on('message', function(m) { 
    res.end(m.result + '\n'); 
  }); 
  //为其指定message事件 
  var input = parseInt(req.url.substring(1)); 
  //和postMessage很类似,不过这里是通过send方法而不是postMessage方法来完成的 
  child.send({input : input}); 
}); 
server.listen(8000); 

子进程的代码:

function fib(n) { 
  if (n < 2) { 
    return 1; 
  } else { 
    return fib(n - 2) + fib(n - 1); 
  } 
} 
//接受到send传递过来的参数 
process.on('message', function(m) { 
  //console.log(m); 
  //打印{ input: 9 } 
  process.send({result: fib(m.input)}); 
}); 

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

其中options对象的stdio参数表示子进程的stdio配置;detached表示让子进程在父进程下独立运行,这个行为与平台有关;shell如果设置为true那么就会在shell中执行命令。这个方法通过指定的command来产生一个新的进程,如果第二个参数没有指定args那么默认就是一个空的数组,第三个参数默认是如下对象,这个参数也可以用于指定额外的参数:

{
 cwd: undefined, //产生这个进程的工作目录,默认继承当前的工作目录
 env: process.env//这个参数用于指定对于新的进程可见的环境变量,默认是process.env
}

其中cwd用于指定子进程产生的工作目录,如果没有指定表示的就是当前工作目录。env用于指定新进程的环境变量,默认为process.env。下面的例子展示了使用ls -lh/usr来获取stdout,stderr以及exit code:

const spawn = require('child_process').spawn; 
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(`child process exited with code $[code]`); 
}); 

下面是一个很详细的运行"ps ax|grep ssh"的例子:

const spawn = require('child_process').spawn; 
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 process exited with code $[code]`); 
 } 
 grep.stdin.end(); 
}); 
grep.stdout.on('data', (data) => { 
 console.log(`${data}`); 
}); 
grep.stderr.on('data', (data) => { 
 console.log(`grep stderr: ${data}`); 
}); 
grep.on('close', (code) => { 
 if (code !== 0) { 
  console.log(`grep process exited with code $[code]`); 
 } 
}); 

用下面的例子来检查错误的执行程序:

const spawn = require('child_process').spawn; 
const child = spawn('bad_command'); 
child.on('error', (err) => { 
 console.log('Failed to start child process.'); 
}); 

options.detached:

在windows上,把这个参数设置为true的话,这时候如果父进程退出了那么子进程还会继续运行,而且子进程有自己的console window。如果把子进程设置了这个为true,那么就不能设置为false了。在非window平台上,如果把这个设置为true,子进程就会成为进程组合和session的leader,这时候子进程在父进程退出以后会继续执行,不管子进程是否detached。可以参见setsid(2)。

默认情况下,父进程会等待detached子进程,然后退出。要阻止父进程等待一个指定的子进程可以使用child.unref方法,这个方法会让父进程的事件循环不包括子进程,这时候允许父进程独立退出,除非在父进程和子进程之间有一个IPC通道。下面是一个detach长期运行的进程然后把它的输出导向到一个文件:

const fs = require('fs'); 
const spawn = require('child_process').spawn; 
const out = fs.openSync('./out.log', 'a'); 
const err = fs.openSync('./out.log', 'a'); 
const child = spawn('prg', [], { 
 detached: true,//依赖于父进程 
 stdio: [ 'ignore', out, err ] 
}); 
child.unref();//允许父进程单独退出,不用等待子进程 

当使用了detached选项去产生一个长期执行的进程,这时候如果父进程退出了那么子进程就不会继续执行了,除非指定了一个stdio配置(不和父进程之间有联系)。如果父进程的stdio是继承的,那么子进程依然会和控制终端之间保持关系。

options.stdio

这个选项用于配置父进程和子进程之间的管道。默认情况下,子进程的stdin,stdout,stderr导向了ChildProcess这个对象的child.stdin,child.stdout,child.stderr流,这和设置stdio为['pipe','pipe','pipe']是一样的。stdio可以是下面的任何一个字符串:

'pipe':相当于['pipe','pipe','pipe'],为默认选项

'ignore':相当于['ignore','ignore','ignore']

'inherit':相当于[process.stdin,process.stdout,process.stderr]或者[0,1,2]

一般情况下,stdio是一个数组,每一个选项对应于子进程的fd。其中0,1,2分别对应于stdin,stdout,stderr。如果还设置了多于的fds那么就会用于创建父进程和子进程之间的额外的管道,可以是下面的任何一个值:

'pipe':为子进程和父进程之间创建一个管道。父进程管道的末端会作为child_process对象的ChildProcess.stdio[fd]而存在。fds0-2创建的管道在ChildProcess.stdin,ChildProcess.stdout,ChildProcess.stderr也是存在的

'ipc':用于创建IPC通道用于在父进程和子进程之间传输消息或者文件描述符。ChildProcess对象最多有一个IPC stdio文件描述符,使用这个配置可以启用ChildProcess的send方法,如果父进程在文件描述符里面写入了JSON对象,那么ChildProcess.on("message")事件就会在父进程上触发。如果子进程是Node.js进程,那么ipc配置就会启用子进程的process.send(), process.disconnect(), process.on('disconnect'), and process.on('message')方法。

'ignore':让Node.js子进程忽视文件描述符。因为Node.js总是会为子进程开启fds0-2,设置为ignore就会导致Node.js去开启/dev/null,同时把这个值设置到子进程的fd上面。

'strem':和子进程之间共享一个可读或者可写流,比如file,socket,pipe。这个stream的文件描述符和子进程的文件描述符fd是重复的。注意:流必须有自己的文件描述符

正整数:表示父进程的打开的文件描述符。和stream对象可以共享一样,这个文件描述符在父子进程之间也是共享的
null/undefined:使用默认值。stdio的fds0,1,2管道被创建(stdin,stdout,stderr)。对于fd3或者fdn,默认为'ignore'

const spawn = require('child_process').spawn; 
// Child will use parent's stdios 
//使用父进程的stdios 
spawn('prg', [], { stdio: 'inherit' }); 
//产生一个共享process.stderr的子进程 
// Spawn child sharing only stderr 
spawn('prg', [], { stdio: ['pipe', 'pipe', process.stderr] }); 
// Open an extra fd=4, to interact with programs presenting a 
// startd-style interface. 
spawn('prg', [], { stdio: ['pipe', null, null, null, 'pipe'] }); 

注意:当子进程和父进程之间建立了IPC通道,同时子进程为Node.js进程,这时候开启的具有IPC通道的子进程(使用unref)直到子进程注册了一个disconnect的事件处理句柄process.on('disconnect'),这样就会允许子进程正常退出而不会由于IPC通道的打开而持续运行。

Class: ChildProcess

这个类的实例是一个EventEmitters,用于代表产生的子进程。这个类的实例不能直接创建,必须使用 child_process.spawn(), child_process.exec(), child_process.execFile(), or child_process.fork()来完成

'close'事件:

其中code表示子进程退出的时候的退出码;signal表示终止子进程发出的信号;这个事件当子进程的stdio stream被关闭的时候触发,和exit事件的区别是多个进程可能共享同一个stdio streams!(所以一个进程退出了也就是exit被触发了,这时候close可能不会触发)

'exit'事件:

其中code表示子进程退出的时候的退出码;signal表示终止子进程发出的信号。这个事件当子进程结束的时候触发,如果进程退出了那么code表示进程退出的exit code,否则没有退出就是null。如果进程是由于收到一个信号而终止的,那么signal就是这个信号,是一个string,默认为null。

注意:如果exit事件被触发了,子进程的stdio stream可能还是打开的;Node.js为SUGUBT,SIGTERM创建信号处理器,而且Node.js进程在收到信号的时候不会马上停止。Node.js会进行一系列的清理工作,然后才re-raise handled signal。见waitpid(2)

'disconnect'事件:

在子进程或者父进程中调用ChildProcess.disconnect()方法的时候会触发。这时候就不能再发送和接受信息了,这是ChildProcess.connected就是false了

'error'事件:

当进程无法产生的时候,进程无法杀死的时候,为子进程发送消息失败的时候就会触发。注意:当产生错误的时候exit事件可能会也可能不会触发。如果你同时监听了exit和error事件,那么就要注意是否会无意中多次调用事件处理函数

'message'事件:

message参数表示一个解析后的JSON对象或者初始值;sendHandle可以是一个net.Socket或者net.Server对象或者undefined。当子进程调用process.send时候触发

child.connected:

调用了disconnect方法后就会是false。表示是否可以在父进程和子进程之间发送和接受数据,当值为false就不能发送数据了
 child.disconnect()

关闭子进程和父进程之间的IPC通道,这时候子进程可以正常退出如果没有其他的连接使得他保持活动。这时候父进程的child.connected和子进程的process.connected就会设置为false,这时候不能传输数据了。disconnect事件当进程没有消息接收到的时候被触发,当调用child.disconnect时候会立即触发。注意:当子进程为Node.js实例的时候如child_process.fork,这时候process.disconnect方法就会在子进程中调用然后关闭IPC通道。

child.kill([signal])

为子进程传入消息,如果没有指定参数那么就会发送SIGTERM信号,可以参见signal(7)来查看一系列信号

const spawn = require('child_process').spawn; 
const grep = spawn('grep', ['ssh']); 
grep.on('close', (code, signal) => { 
 console.log( 
  `child process terminated due to receipt of signal ${signal}`); 
}); 
// Send SIGHUP to process 
grep.kill('SIGHUP'); 

ChildProcess对象在无法传输信号的时候会触发error事件。为一个已经退出的子进程发送信号虽然无法报错但是可能导致无法预料的结果。特别的,如果这个PID已经被分配给另外一个进程那么这时候也会导致无法预料的结果。

child.pid:

返回进程的PID值

const spawn = require('child_process').spawn; 
const grep = spawn('grep', ['ssh']); 
console.log(`Spawned child pid: ${grep.pid}`); 
grep.stdin.end();//通过grep.stdin.end结束 

child.send(message[, sendHandle][, callback])

当父子进程之间有了IPC通道,child.send就会为子进程发送消息,当子进程为Node.js实例,那么可以用process.on('message')事件接收

父进程为:

const cp = require('child_process'); 
const n = cp.fork(`${__dirname}/sub.js`); 
n.on('message', (m) => { 
 console.log('PARENT got message:', m); 
}); 
n.send({ hello: 'world' }); 

子进程为:

process.on('message', (m) => { 
 console.log('CHILD got message:', m); 
}); 
process.send({ foo: 'bar' }); 

子进程使用process.send方法为父进程发送消息。有一个特例,发送{cmd: 'NODE_foo'}。当一个消息在他的cmd属性中包含一个NODE_前缀会被看做使用Node.js核心(被Node.js保留)。这时候不会触发子进程的process.on('message')。而是使用process.on('internalMessage')事件,同时会被Node.js内部消费,一般不要使用这个方法。sendHandle这个用于给子进程传入一个TCP Server或者一个socket,为process.on('message')回调的第二个参数接受。callback当消息已经发送,但是子进程还没有接收到的时候触发,这个函数只有一个参数成功为null否则为Error对象。如果没有指定callback同时消息也不能发送ChildProcess就会触发error事件。当子进程已经退出就会出现这个情况。child.send返回false如果父子进程通道已经关闭,或者积压的没有传输的数据超过一定的限度,否则这个方法返回true。这个callback方法可以用于实现流控制:

下面是发送一个Server的例子:

const child = require('child_process').fork('child.js'); 
// Open up the server object and send the handle. 
const server = require('net').createServer(); 
server.on('connection', (socket) => { 
 socket.end('handled by parent'); 
}); 
server.listen(1337, () => { 
 child.send('server', server); 
}); 

子进程接受这个消息:

process.on('message', (m, server) => { 
 if (m === 'server') { 
  server.on('connection', (socket) => { 
   socket.end('handled by child'); 
  }); 
 } 
}); 

这时候server就被子进程和父进程共享了,一些连接可以被父进程处理,一些被子进程处理。上面的例子如果使用dgram那么就应该监听message事件而不是connection,使用server.bind而不是server.listen,但是当前只在UNIX平台上可行。
下面的例子展示发送一个socket对象(产生两个子进程,处理normal和special优先级):

父进程为:

const normal = require('child_process').fork('child.js', ['normal']); 
const special = require('child_process').fork('child.js', ['special']); 
// Open up the server and send sockets to child 
const server = require('net').createServer(); 
server.on('connection', (socket) => { 
 // If this is special priority 
 if (socket.remoteAddress === '74.125.127.100') { 
  special.send('socket', socket); 
  return; 
 } 
 // This is normal priority 
 normal.send('socket', socket); 
}); 
server.listen(1337); 

子进程为:

process.on('message', (m, socket) => { 
 if (m === 'socket') { 
  socket.end(`Request handled with ${process.argv[2]} priority`); 
 } 
}); 

当socket被发送到子进程的时候那么父进程已经无法追踪这个socket什么时候被销毁的。这时候.connections属性就会成为null,因此我们建议不要使用.maxConnections。注意:这个方法在内部JSON.stringify去序列化消息

child.stderr:

一个流对象,是一个可读流表示子进程的stderr。他是child.stdio[2]的别名,两者表示同样的值。如果子进程是通过stdio[2]产生的,设置的不是pipe那么值就是undefined。

child.stdin:

一个可写的流对象。注意:如果子进程等待读取输入,那么子进程会一直等到流调用了end方法来关闭的时候才会继续读取。如果子进程通过stdio[0]产生,同时不是设置的pipe那么值就是undefined。child.stdin是child.stdio[0]的别名,表示同样的值。

const spawn = require('child_process').spawn;  
const grep = spawn('grep', ['ssh']);  
console.log(`Spawned child pid: ${grep.pid}`);  
grep.stdin.end();//通过grep.stdin.end结束  

child.stdio:

一个子进程管道的稀疏数组,是 child_process.spawn()函数的stdio选项,同时这个值被设置为pipe。child.stdio[0], child.stdio[1], 和 child.stdio[2]也可以通过child.stdin, child.stdout, 和 child.stderr访问。下面的例子中只有子进程的fd1(也就是stdout)被设置为管道,因此只有父进程的child.stdio[1]是一个流,其他的数组中对象都是null:

const assert = require('assert'); 
const fs = require('fs'); 
const child_process = require('child_process'); 
const child = child_process.spawn('ls', { 
  stdio: [ 
   0, // Use parents stdin for child 
   'pipe', // Pipe child's stdout to parent 
   fs.openSync('err.out', 'w') // Direct child's stderr to a file 
  ] 
}); 
assert.equal(child.stdio[0], null); 
assert.equal(child.stdio[0], child.stdin); 
assert(child.stdout); 
assert.equal(child.stdio[1], child.stdout); 
assert.equal(child.stdio[2], null); 
assert.equal(child.stdio[2], child.stderr); 

child.stdout:

一个可读流,代表子进程的stdout。如果子进程产生的时候吧stdio[1]设置为除了pipe以外的任何数,那么值就是undefined。其值和child.stdio[1]一样

 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。