溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

詳解從Node.js的child_process模塊來學習父子進程之間的通信

發(fā)布時間:2020-10-16 05:21:36 來源:腳本之家 閱讀:829 作者:liangklfang 欄目:web開發(fā)

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的事件循環(huán),然而child-process.spawnSync方法是同步的,他會阻塞事件循環(huán)只到產生的進程退出或者終止。

child_process.exec:產生一個shell客戶端,然后使用shell來執(zhí)行程序,當完成的時候傳遞給回調函數一個stdout和stderr

child_process.execFile:和exec相似,但是他不會馬上產生一個shell

child_process.fork:產生一個新的Node.js進程,同時執(zhí)行一個特定的模塊來產生IPC通道,進而在父進程和子進程之間傳輸數據

child_process.execSync:和exec不同之處在于會阻塞Node.js的事件循環(huán),然而child-process

child_process.execFileSync:和execFile不同之處在于會阻塞Node.js的事件循環(huán),然而child-process在一些特殊情況下,例如自動化shell腳本,同步的方法可能更加有用。多數情況下,同步的方法會對性能產生重要的影響,因為他會阻塞事件循環(huán)

child_process.spawn(), child_process.fork(), child_process.exec(), and child_process.execFile()都是異步的API。每一個方法都會產生一個ChildProcess實例,而且這個對象實現了Node.js的EventEmitter這個API,于是父進程可以注冊監(jiān)聽函數,在子進程的特定事件觸發(fā)的時候被調用。 child_process.exec() 和 child_process.execFile()可以指定一個可選的callback函數,這個函數在子進程終止的時候被調用。

在windows平臺上執(zhí)行.bat和.cmd:

child_process.exec和child_process.execFile的不同之處可能隨著平臺不同而有差異。在Unit/Linux/OSX平臺上execFile更加高效,因為他不會產生shell。在windows上,.bat/.cmd在沒有終端的情況下是無法執(zhí)行的,因此就無法使用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執(zhí)行命令,同時對產生的結果進行緩存。其中回調函數中的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,那么父進程就會發(fā)送信號,這個信號通過killSignal指定,默認是"SIGTERM"來終結子進程,如果子進程超過了timeout指定的時間。注意:和POSIX系統上調用exec方法不一樣的是,child_process.exec不會替換當前線程,而是使用一個shell去執(zhí)行命令

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

其中file表示需要執(zhí)行的文件。 child_process.execFile()和exec很相似,但是這個方法不會產生一個shell。指定的可執(zhí)行文件會馬上產生一個新的線程,因此其效率比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表示要在子線程中執(zhí)行的模塊。其中options中的參數silent如果設置為true,那么子進程的stdin, stdout, stderr將會被傳遞給父進程,如果設置為false那么就會從父進程繼承。execArgv默認的值為process.execArgv,execPath表示用于創(chuàng)建子進程的可執(zhí)行文件。這個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產生的新的進程和父進程之間通過文件描述符(子進程的環(huán)境變量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中執(zhí)行命令。這個方法通過指定的command來產生一個新的進程,如果第二個參數沒有指定args那么默認就是一個空的數組,第三個參數默認是如下對象,這個參數也可以用于指定額外的參數:

{
 cwd: undefined, //產生這個進程的工作目錄,默認繼承當前的工作目錄
 env: process.env//這個參數用于指定對于新的進程可見的環(huán)境變量,默認是process.env
}

其中cwd用于指定子進程產生的工作目錄,如果沒有指定表示的就是當前工作目錄。env用于指定新進程的環(huán)境變量,默認為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]`); 
 } 
}); 

用下面的例子來檢查錯誤的執(zhí)行程序:

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的話,這時候如果父進程退出了那么子進程還會繼續(xù)運行,而且子進程有自己的console window。如果把子進程設置了這個為true,那么就不能設置為false了。在非window平臺上,如果把這個設置為true,子進程就會成為進程組合和session的leader,這時候子進程在父進程退出以后會繼續(xù)執(zhí)行,不管子進程是否detached。可以參見setsid(2)。

默認情況下,父進程會等待detached子進程,然后退出。要阻止父進程等待一個指定的子進程可以使用child.unref方法,這個方法會讓父進程的事件循環(huán)不包括子進程,這時候允許父進程獨立退出,除非在父進程和子進程之間有一個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選項去產生一個長期執(zhí)行的進程,這時候如果父進程退出了那么子進程就不會繼續(xù)執(zhí)行了,除非指定了一個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那么就會用于創(chuàng)建父進程和子進程之間的額外的管道,可以是下面的任何一個值:

'pipe':為子進程和父進程之間創(chuàng)建一個管道。父進程管道的末端會作為child_process對象的ChildProcess.stdio[fd]而存在。fds0-2創(chuàng)建的管道在ChildProcess.stdin,ChildProcess.stdout,ChildProcess.stderr也是存在的

'ipc':用于創(chuàng)建IPC通道用于在父進程和子進程之間傳輸消息或者文件描述符。ChildProcess對象最多有一個IPC stdio文件描述符,使用這個配置可以啟用ChildProcess的send方法,如果父進程在文件描述符里面寫入了JSON對象,那么ChildProcess.on("message")事件就會在父進程上觸發(fā)。如果子進程是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管道被創(chuàng)建(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通道的打開而持續(xù)運行。

Class: ChildProcess

這個類的實例是一個EventEmitters,用于代表產生的子進程。這個類的實例不能直接創(chuàng)建,必須使用 child_process.spawn(), child_process.exec(), child_process.execFile(), or child_process.fork()來完成

'close'事件:

其中code表示子進程退出的時候的退出碼;signal表示終止子進程發(fā)出的信號;這個事件當子進程的stdio stream被關閉的時候觸發(fā),和exit事件的區(qū)別是多個進程可能共享同一個stdio streams!(所以一個進程退出了也就是exit被觸發(fā)了,這時候close可能不會觸發(fā))

'exit'事件:

其中code表示子進程退出的時候的退出碼;signal表示終止子進程發(fā)出的信號。這個事件當子進程結束的時候觸發(fā),如果進程退出了那么code表示進程退出的exit code,否則沒有退出就是null。如果進程是由于收到一個信號而終止的,那么signal就是這個信號,是一個string,默認為null。

注意:如果exit事件被觸發(fā)了,子進程的stdio stream可能還是打開的;Node.js為SUGUBT,SIGTERM創(chuàng)建信號處理器,而且Node.js進程在收到信號的時候不會馬上停止。Node.js會進行一系列的清理工作,然后才re-raise handled signal。見waitpid(2)

'disconnect'事件:

在子進程或者父進程中調用ChildProcess.disconnect()方法的時候會觸發(fā)。這時候就不能再發(fā)送和接受信息了,這是ChildProcess.connected就是false了

'error'事件:

當進程無法產生的時候,進程無法殺死的時候,為子進程發(fā)送消息失敗的時候就會觸發(fā)。注意:當產生錯誤的時候exit事件可能會也可能不會觸發(fā)。如果你同時監(jiān)聽了exit和error事件,那么就要注意是否會無意中多次調用事件處理函數

'message'事件:

message參數表示一個解析后的JSON對象或者初始值;sendHandle可以是一個net.Socket或者net.Server對象或者undefined。當子進程調用process.send時候觸發(fā)

child.connected:

調用了disconnect方法后就會是false。表示是否可以在父進程和子進程之間發(fā)送和接受數據,當值為false就不能發(fā)送數據了
 child.disconnect()

關閉子進程和父進程之間的IPC通道,這時候子進程可以正常退出如果沒有其他的連接使得他保持活動。這時候父進程的child.connected和子進程的process.connected就會設置為false,這時候不能傳輸數據了。disconnect事件當進程沒有消息接收到的時候被觸發(fā),當調用child.disconnect時候會立即觸發(fā)。注意:當子進程為Node.js實例的時候如child_process.fork,這時候process.disconnect方法就會在子進程中調用然后關閉IPC通道。

child.kill([signal])

為子進程傳入消息,如果沒有指定參數那么就會發(fā)送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對象在無法傳輸信號的時候會觸發(fā)error事件。為一個已經退出的子進程發(fā)送信號雖然無法報錯但是可能導致無法預料的結果。特別的,如果這個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就會為子進程發(fā)送消息,當子進程為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方法為父進程發(fā)送消息。有一個特例,發(fā)送{cmd: 'NODE_foo'}。當一個消息在他的cmd屬性中包含一個NODE_前綴會被看做使用Node.js核心(被Node.js保留)。這時候不會觸發(fā)子進程的process.on('message')。而是使用process.on('internalMessage')事件,同時會被Node.js內部消費,一般不要使用這個方法。sendHandle這個用于給子進程傳入一個TCP Server或者一個socket,為process.on('message')回調的第二個參數接受。callback當消息已經發(fā)送,但是子進程還沒有接收到的時候觸發(fā),這個函數只有一個參數成功為null否則為Error對象。如果沒有指定callback同時消息也不能發(fā)送ChildProcess就會觸發(fā)error事件。當子進程已經退出就會出現這個情況。child.send返回false如果父子進程通道已經關閉,或者積壓的沒有傳輸的數據超過一定的限度,否則這個方法返回true。這個callback方法可以用于實現流控制:

下面是發(fā)送一個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那么就應該監(jiān)聽message事件而不是connection,使用server.bind而不是server.listen,但是當前只在UNIX平臺上可行。
下面的例子展示發(fā)送一個socket對象(產生兩個子進程,處理normal和special優(yōu)先級):

父進程為:

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被發(fā)送到子進程的時候那么父進程已經無法追蹤這個socket什么時候被銷毀的。這時候.connections屬性就會成為null,因此我們建議不要使用.maxConnections。注意:這個方法在內部JSON.stringify去序列化消息

child.stderr:

一個流對象,是一個可讀流表示子進程的stderr。他是child.stdio[2]的別名,兩者表示同樣的值。如果子進程是通過stdio[2]產生的,設置的不是pipe那么值就是undefined。

child.stdin:

一個可寫的流對象。注意:如果子進程等待讀取輸入,那么子進程會一直等到流調用了end方法來關閉的時候才會繼續(xù)讀取。如果子進程通過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]一樣

 以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI