Node被設計用來高效的處理I/O操作,但是你應該知道,有些類型的程序並不適合這種模式。比如,如果你打算用Node處理一個CPU密集的任務,你可能會堵塞事件循環,並因此降低了程序的響應。替代辦法是,把CPU密集的任務分配給一個單獨的進程來處理,從而釋放事件循環。Node允許你產生進程,並把這個新進程做為它父進程的子進程。在Node裡,子進程可以和父進程進行雙向通信,而且在某種程度上,父進程還可以監控和管理子進程。
另外一種需要使用子進程的情況是,當你想簡單地執行一個外部命令,並讓Node獲取命令的返回值時。比如,你可以執行一個UNIX命令、腳本或者其他那些不能在Node裡直接執行的命令。
本章將向你展示如何執行外部命令,創建,並和子進程通信,以及終結子進程。重點是讓你了解如何在Node進程外完成一系列任務。
執行外部命令
當你需要執行一個外部shell命令或可執行文件時,你可以使用child_process模塊,像這樣導入它:
復制代碼 代碼如下:
var child_process = require(‘child_process')
然後可以用模塊內的exec函數來執行外部命令:
復制代碼 代碼如下:
var exec = child_process.exec;
exec(command,callback);
exec的第一個參數是你准備執行的shell命令字符串,第二個參數是一個回調函數。這個回調函數將會在exec執行完外部命令或者有錯誤發生時被調用。回調函數有三個參數:error,stdout,stderr,看下面的例子:
復制代碼 代碼如下:
exec(‘ls',function(err,stdout,stderr){
//譯者注:如果使用windows,可改為windows命令,比如dir,後面不再贅述
});
如果有錯誤發生,第一個參數將會是一個Error類的實例,如果第一個參數不包含錯誤,那麼第二個參數stdout將會包含命令的標准輸出。最後一個參數包含命令相關的錯誤輸出。
列表8-1 展示了一個復雜些的執行外部命令的例子
LISTING 8-1:執行外部命令(源碼:chapter8/01_external_command.js)
復制代碼 代碼如下:
//導入child_process模塊的exec函數
var exec = require(‘child_process').exec;
//調用“cat *.js | wc -l”命令
exec(‘cat *.js | wc –l ‘, function(err, stdout, stderr ){ //第四行
//命令退出或者調用失敗
if( err ){
//啟動外部進程失敗
console.log(‘child_process 退出,錯誤碼是:',err.code);
return;
}
}
第四行,我們把“cat *.js | wc -l”作為第一個參數傳遞給exec,你也可以嘗試任何其它命令,只要你在shell裡使用過的命令都可以。
然後將一個回調函數作為第二個參數,它將會在錯誤發生或者子進程終結的時候被調用。
還可以在回調函數之前傳遞第三個可選參數,它包含一些配置選項,比如:
復制代碼 代碼如下:
var exec = require(‘child_process').exec;
var options ={
timeout: 1000,
killSignal: ‘SIGKILL'
};
exec(‘cat *.js | wc –l ‘, options, function(err,stdout,stderr){
//…
});
可以使用的參數有:
1.cwd —— 當前目錄,可以指定當前工作目錄。
2.encoding —— 子進程輸出內容的編碼格式,默認值是”utf8”,也就是UTF-8編碼。如果子進程的輸出不是utf8,你可以用這個參數來設置,支持的編碼格式有:
復制代碼 代碼如下:
ascii
utf8
ucs2
base64
如果你想了解Node支持的這些編碼格式的更多信息,請參考第4章“使用Buffer處理,編碼,解碼二進制數據”。
1.timeout —— 以毫秒為單位的命令執行超時時間,默認是0,即無限制,一直等到子進程結束。
2.maxBuffer —— 指定stdout流和stderr流允許輸出的最大字節數,如果達到最大值,子進程會被殺死。默認值是200*1024。
3.killSignal —— 當超時或者輸出緩存達到最大值時發送給子進程的終結信號。默認值是“SIGTERM”,它將給子進程發送一個終結信號。通常都會使用這種有序的方式來結束進程。當用SIGTERM信號時,進程接收到以後還可以進行處理或者重寫信號處理器的默認行為。如果目標進程需要,你可以同時向他傳遞其它的信號(比如SIGUSR1)。你也可以選擇發送一個SIGKILL信號,它會被操作系統處理並強制立刻結束子進程,這樣的話,子進程的任何清理操作都不會被執行。
如果你想更進一步的控制進程的結束,可以使用child_process.spawn命令,後面會介紹。
1.evn —— 指定傳遞給子進程的環境變量,默認是null,也就是說子進程會繼承在它被創建之前的所有父進程的環境變量。
注意:使用killSignal選項,你可以以字符串的形式向目標進程發送信號。在Node裡信號以字符串的形式存在,下面是UNIX信號和對應默認操作的列表:
你可能想為子進程提供一組可擴展的父級環境變量。如果直接去修改process.env對象,你會改變Node進程內所有模塊的環境變量,這樣會惹很多麻煩。替代方案是,創建一個新對象,復制process.env裡的所有參數,見例子8-2:
LISTING 8-2:使用參數化的環境變量來執行命令(源碼:chapter8/02_env_vars_augment.js)
復制代碼 代碼如下:
var env = process.env,
varName,
envCopy = {},
exec = require(‘child_prcess').exec;
//將process.env復制到envCopy
for( vaName in ev){
envCopy[varName] = env[varName];
}
//設置一些自定義變量
envCopy[‘CUSTOM ENV VAR1'] = ‘some value';
envCopy[‘CUSTOM ENV VAR2'] = ‘some other value';
//使用process.env和自定義變量來執行命令
exec(‘ls –la',{env: envCopy}, function(err,stdout,stderr){
if(err){ throw err; }
console.log(‘stdout:', stdout);
console.log(‘stderr:',stderr);
}
上面例子,創建了一個用來保存環境變量的envCopy變量,它首先從process.env那裡復制Node進程的環境變量,然後又添加或替換了一些需要修改的環境變量,最後把envCopy作為環境變量參數傳遞給exec函數並執行外部命令。
記住,環境變量是通過操作系統在進程之間傳遞的,所有類型的環境變量值都是以字符串的形式到達子進程的。比如,如果父進程將數字123作為一個環境變量,子進程將會以字符串的形式接收“123”。
下面的例子,將在同一個目錄裡建立2個Node腳本:parent.js和child.js,第一個腳本將會調用第二個,下面我們來創建這兩個文件:
LISTING 8-3: 父進程設置環境變量(chapter8/03_environment_number_parent.js)
復制代碼 代碼如下:
var exec = require('child_process').exec;
exec('node child.js', {env: {number: 123}}, function(err, stdout, stderr) {
if (err) { throw err; }
console.log('stdout:\n', stdout);
console.log('stderr:\n', stderr);
});
把這段代碼保存到parent.js,下面是子進程的源碼,把它們保存到child.js(見例8-4)
例 8-4: 子進程解析環境變量(chapter8/04_environment_number_child.js)
復制代碼 代碼如下:
var number = process.env.number;
console.log(typeof(number)); // → "string"
number = parseInt(number, 10);
console.log(typeof(number)); // → "number"
當你把這個文件保存為child.js後,就可以在這個目錄下運行下面的命令:
復制代碼 代碼如下:
$ node parent.js
將會看到如下的輸出:
復制代碼 代碼如下:
sdtou:
string
number
stderr:
可以看到,盡管父進程傳遞了一個數字型的環境變量,但是子進程卻以字符串形式接收它(參見輸出的第二行),在第三行你把這個字符串解析成了一個數字。
生成子進程
如你所見,可以使用child_process.exec()函數來啟動外部進程,並在進程結束的時候調用你的回調函數,這樣用起來很簡單,不過也有一些缺點:
1.除了使用命令行參數和環境變量,使用exec()無法和子進程通信
2.子進程的輸出是被緩存的,因此你無法流化它,它可能會耗盡內存
幸運的是,Node的child_process模塊允許更細粒度的控制子進程的啟動,停止,及其它常規操作。你可以在應用程序裡啟動一個新的子進程,Node提供一個雙向的通信通道,可以讓父進程和子進程相互收發字符串數據。父進程還可以有些針對子進程的管理操作,給子進程發送信號,以及強制關閉子進程。
創建子進程
你可以使用child_process.spawn函數來創建一個新的子進程,見例8-5:
例 8-5: 生成子進程。 (chapter8/05_spawning_child.js)
復制代碼 代碼如下:
// 導入child_process模塊的spawn函數
var spawn = require('child_process').spawn;
// 生成用來執行 "tail -f /var/log/system.log"命令的子進程
var child = spawn('tail', ['-f', '/var/log/system.log']);
上面代碼生成了一個用來執行tail命令的子進程,並將“-f”和“/bar/log/system.log”作為參數。tail命令將會監控/var/log/system.og文件(如果存在的話),然後將所有追加的新數據輸出到stdout標准輸出流。spawn函數返回一個ChildProcess對象,它是一個指針對象,封裝了真實進程的訪問接口。這個例子裡我們把這個新的描述符賦值給一個叫做child的變量。
監聽來自子進程的數據
任何包含stdout屬性的子進程句柄,都會將子進程的標准輸出stdout作為一個流對象,你可以在這個流對象上綁定data事件,這樣每當有數據塊可用時,就會調用對應的回調函數,見下面的例子:
復制代碼 代碼如下:
//將子進程的輸出打印到控制台
child.stdout.on(‘data',function(data){
console.log(‘tail output: ‘ + data);
});
每當子進程將數據輸出到標准輸出stdout時,父進程就會得到通知並把數據打印到控制台。
除了標准輸出,進程還有另外一個默認輸出流:標准錯誤流,通常用這個流來輸出錯誤信息。
在這個例子裡,如果/var/log/system.log文件不存在,tail進程將會輸出類似下面的消息:“/var/log/system.log:No such file or directory”,通過監聽stderr流,父進程會在這種錯誤發生時得到通知。
父進程可以這樣監聽標准錯誤流:
復制代碼 代碼如下:
child.stderr.on('data', function(data) {
console.log('tail error output:', data);
});
stderr屬性和stdout一樣,也是只讀流,每當子進程往標准錯誤流裡輸出數據時,父進程就會得到通知,並輸出數據。
發送數據到子進程
除了從子進程的輸出流裡接收數據,父進程還可以通過childPoces.stdin屬性往子進程的標准輸入裡寫入數據,以此來往子進程發送數據。
子進程可以通過process.stdin只讀流來監聽標准輸入的數據,但是注意你首先必須得恢復(resume)標准輸入流,因為它默認處於暫停(paused)狀態。
例8-6將會創建一個包含如下功能的程序:
1.+1 應用:一個簡單的應用程序,可以從標准輸入接收整型,然後相加,再把相加以後的結果輸出到標准輸出流。這個應用作為一個簡單的計算服務, 把Node進程模擬成一個可以執行特定工作的外部服務。
2.測試+1應用的客戶端,發送隨機整型,然後輸出結果。用來演示Node進程如何生成一個子進程然後讓它執行特定的任務。
用下面例8-6的代碼創建一個名為plus_one.js的文件:
例 8-6: +1 應用程序(chapter8/06_plus_one.js)
復制代碼 代碼如下:
// 恢復默認是暫停狀態的標准輸入流
process.stdin.resume();
process.stdin.on('data', function(data) {
var number;
try {
// 將輸入數據解析為整型
number = parseInt(data.toString(), 10);
// +1
number += 1;
// 輸出結果
process.stdout.write(number + "\n");
} catch(err) {
process.stderr.write(err.message + "\n");
}
});
上面代碼裡,我們等待來自stdin標准輸入流的數據,每當有數據可用,就假設它是個整型並把它解析到一個整型變量裡,然後加1,並把結果輸出到標准輸出流。
可以通過下面命令來運行這個程序:
復制代碼 代碼如下:
$ node plus_one.js
運行後程序就開始等待輸入,如果你輸入一個整數然後按回車,就會看到一個被加1以後的數字被顯示到屏幕上。
可以通過按Ctrl-C來退出程序。
一個測試客戶端
現在你要創建一個Node進程來使用前面的“+1應用”提供的計算服務。
首先創建一個名為plus_one_test.js的文件,內容見例8-7:
例 8-7: 測試+1應用(chapter8/07_plus_one_test.js)
復制代碼 代碼如下:
var spawn = require('child_process').spawn;
// 生成一個子進程來執行+1應用
var child = spawn('node', ['plus_one.js']);
// 每一秒調用一次函數
setInterval(function() {
// Create a random number smaller than 10.000
var number = Math.floor(Math.random() * 10000);
// Send that number to the child process:
child.stdin.write(number + "\n");
// Get the response from the child process and print it:
child.stdout.once('data', function(data) {
console.log('child replied to ' + number + ' with: ' + data);
});
}, 1000);
child.stderr.on('data', function(data) {
process.stdout.write(data);
});
從第一行到第四行啟動了一個用來運行“+1應用”的子進程,然後使用setInterval函數每秒鐘執行一次下列操作:
1..新建一個小於10000的隨機數
2.將這個數字作為字符串傳遞給子進程
3.等待子進程回復一個字符串
4.因為你想每次只接收1個數字的計算結果,因此需要使用child.stdout.once而不是child.stdout.on。如果使用了後者,會每隔1秒注冊一個data事件的回調函數,每個被注冊的回調函數都會在子進程的stdout接收到數據時被執行,這樣你會發現同一個計算結果會被輸出多次,這種行為顯然是錯的。
在子進程退出時接收通知
當子進程退出時,exit事件會被觸發。例8-8展示了如何監聽它:
例 8-8: 監聽子進程的退出事件 (chapter8/09_listen_child_exit.js)
復制代碼 代碼如下:
var spawn = require('child_process').spawn;
// 生成子進程來執行 "ls -la"命令
var child = spawn('ls', ['-la']);
child.stdout.on('data', function(data) {
console.log('data from child: ' + data);
});
// 當子進程退出:
<strong>child.on('exit', function(code) {
console.log('child process terminated with code ' + code);
});</strong>
最後幾行加黑的代碼,父進程使用子進程的exit事件來監聽它的退出事件,當事件發生時,控制台顯示相應的輸出。子進程的退出碼會被作為第一個參數傳遞給回調函數。有些程序使用一個非0的退出碼來代表某種失敗狀態。比如,如果你嘗試執行命令“ls –al click filename.txt”,但是當前目錄沒有這個文件,你就會得到一個值為1的退出碼,見例8-9:
例8-9:獲得子進程的退出碼 (chapter8/10_child_exit_code.js)
復制代碼 代碼如下:
var spawn = require('child_process').spawn;
// 生成子進程,執行"ls does_not_exist.txt" 命令
var child = spawn('ls', ['does_not_exist.txt']);
// 當子進程退出
child.on('exit', function(code) {
console.log('child process terminated with code ' + code);
});
這個例子裡,exit事件觸發了回調函數,並把子進程的退出碼作為第一個參數傳遞給它。如果子進程是被信號殺死而導致的非正常退出,那麼相應的信號代碼會被當作第二個參數傳遞給回調函數,如例8-10:
LISTING 8-10: 獲得子進程的退出信號(chapter8/11_child_exit_signal.js)
復制代碼 代碼如下:
var spawn = require('child_process').spawn;
// 生成子進程,運行"sleep 10"命令
var child = spawn('sleep', ['10']);
setTimeout(function() {
child.kill();
}, 1000);
child.on('exit', function(code, signal) {
if (code) {
console.log('child process terminated with code ' + code);
} else if (signal) {
console.log('child process terminated because of signal ' + signal);
}
});
這個例子裡,啟動一個子進程來執行sleep 10秒的操作,但是還沒到10秒就發送了一個SIGKILL信號給子進程,這將會導致如下的輸出:
復制代碼 代碼如下:
child process terminated because of signal SIGTERM
發送信號並殺死進程
在這部分,你將學習如何使用信號來管理子進程。信號是父進程用來跟子進程通信,甚至殺死子進程的一種簡單方式。
不同的信號代碼代表不同的含義,有很多信號,其中最常見的一些是用來殺死進程的。如果一個進程接收到一個它不知道如何處理的信號,程序就會被異常中斷。有些信號會被子進程處理,而有些只能由操作系統處理。
一般情況下,你可以使用child.kill方法來向子進程發送一個信號,默認發送SIGTERM信號:
復制代碼 代碼如下:
var spawn = require('child_process').spawn;
var child = spawn('sleep', ['10']);
setTimeout(function() {
child.kill();
}, 1000);
還可以通過傳入一個標識信號的字符串作為kill方法的唯一參數,來發送某個特定的信號:
復制代碼 代碼如下:
child.kill(‘SIGUSR2');
需要注意的是,雖然這個方法的名字叫kill,但是發送的信號並不一定會殺死子進程。如果子進程處理了信號,默認的信號行為就會被覆蓋。用Node寫的子進程可以像下面這樣重寫信號處理器的定義:
復制代碼 代碼如下:
process.on('SIGUSR2', function() {
console.log('Got a SIGUSR2 signal');
});
現在,你定義了SIGUSR2的信號處理器,當你的進程再收到SIGUSR2信號的時候就不會被殺死,而是輸出“Got a SIGUSR2 signal”這句話。使用這種機制,你可以設計一種簡單的方式來跟子進程溝通甚至命令它。雖然不像使用標准輸入功能那麼豐富,但是這方式要簡單很多。
小結
這一章,學習了使用child_process.exec方法來執行外部命令,這種方式可以不使用命令行參數,而是通過定義環境變量的方式把參數傳遞給子進程。
還學習了通過調用child_process.spawn方法生成子進程的方式來調用外部命令,這種方式你可以使用輸入流,輸出流來跟子進程通信,或者使用信號來跟子進程通信以及殺死進程。