1. 程式人生 > >從Node.js的child_process模組來學習父子程序之間的通訊

從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]一樣