Node被設計用來有效率的處理I/O操作,但你應該知道,有些類型的程式並不適合這種模式。例如,如果你打算用Node處理一個CPU密集的任務,你可能會堵塞事件循環,並因此降低了程式的回應。替代辦法是,把CPU密集的任務分配給一個單獨的程序來處理,從而釋放事件循環。 Node允許你產生進程,並把這個新進程做成它父進程的子進程。在Node裡,子進程可以和父進程進行雙向通信,而且在某種程度上,父進程還可以監控和管理子進程。
另一個需要使用子程序的情況是,當你想簡單地執行一個外部指令,並讓Node取得指令的回傳值時。例如,你可以執行一個UNIX指令、腳本或其他那些不能在Node裡直接執行的指令。
本章將向你展示如何執行外部命令,創建,並和子進程通信,以及終結子進程。重點是讓你了解如何在Node進程外完成一系列任務。
執行外部指令
當你需要執行一個外部shell指令或執行檔時,你可以使用child_process模組,像這樣導入它:
exec(command,callback);
//譯者註:若使用windows,可改為windows指令,如dir,後面不再贅述
});
如果有錯誤發生,第一個參數將會是一個Error類別的實例,如果第一個參數不包含錯誤,那麼第二個參數stdout將會包含指令的標準輸出。最後一個參數包含命令相關的錯誤輸出。
清單8-1 展示了一個複雜些的執行外部指令的範例
LISTING 8-1:執行外部指令(原始碼:chapter8/01_external_command.js)
第四行,我們把「cat *.js | wc -l」當作第一個參數傳遞給exec,你也可以嘗試任何其它指令,只要你在shell裡使用過的指令都可以。
然後將一個回呼函數作為第二個參數,它將會在錯誤發生或子程序終結的時候被呼叫。
還可以在回呼函數之前傳遞第三個可選參數,它包含一些配置選項,例如:
var options ={
timeout: 1000,
killSignal: ‘SIGKILL'
};
exec(‘cat *.js | wc –l ‘, options, function(err,stdout,stderr){
//…
});
可以使用的參數有:
1.cwd —— 目前目錄,可以指定目前工作目錄。
2.encoding —— 子進程輸出內容的編碼格式,預設值是”utf8”,也就是UTF-8編碼。如果子進程的輸出不是utf8,你可以用這個參數來設置,支援的編碼格式有:
如果你想了解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)
//設定一些自訂變數
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)
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)
console.log(typeof(number)); // → "string"
number = parseInt(number, 10);
console.log(typeof(number)); // → "number"
當你把這個檔案儲存為child.js後,就可以在這個目錄下執行下面的指令:
將會看到以下的輸出:
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)
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流,父進程會在這種錯誤發生時被通知。
父行程可以這樣監聽標準錯誤流:
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)
上面程式碼裡,我們等待來自stdin標準輸入流的數據,每當有數據可用,就假設它是個整數並把它解析到一個整型變數裡,然後加1,並把結果輸出到標準輸出流。
可以透過下面指令來執行這個程式:
執行後程式就開始等待輸入,如果你輸入一個整數然後按回車,就會看到一個被加1以後的數字被顯示到螢幕上。
可以按Ctrl-C來退出程式。
一個測試客戶端
現在你要建立一個Node進程來使用前面的「 1應用程式」提供的計算服務。
先建立一個名為plus_one_test.js的文件,內容見例8-7:
例 8-7: 測試 1應用(chapter8/07_plus_one_test.js)
從第一行到第四行啟動了一個用來運行「 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)
// 當子程序退出:
child.on('exit', function(code) {
console.log('child process terminated with code ' code);
});
最後幾行加黑的程式碼,父行程使用子行程的exit事件來監聽它的退出事件,當事件發生時,控制台顯示對應的輸出。子行程的退出碼會被當作第一個參數傳遞給回呼函數。有些程式使用一個非0的退出碼來代表某種失敗狀態。例如,如果你嘗試執行指令“ls –al click filename.txt”,但目前目錄沒有這個文件,你就會得到一個值為1的退出碼,見例8-9:
例8-9:取得子程序的退出碼 (chapter8/10_child_exit_code.js)
這個例子裡,exit事件觸發了回呼函數,並把子行程的退出碼當作第一個參數傳遞給它。如果子進程是被訊號殺死而導致的非正常退出,那麼對應的訊號碼會被當作第二個參數傳遞給回呼函數,如例8-10:
LISTING 8-10: 取得子程序的退出訊號(chapter8/11_child_exit_signal.js)
這個例子裡,啟動一個子進程來執行sleep 10秒的操作,但是還沒到10秒就發送了一個SIGKILL訊號給子進程,這將會導致如下的輸出:
發送訊號並殺死進程
在這部分,你將學習如何使用訊號來管理子進程。訊號是父進程用來跟子進程通信,甚至殺死子進程的簡單方式。
不同的訊號代碼代表不同的意義,有很多訊號,其中最常見的一些是用來殺死進程的。如果一個行程接收到一個它不知道如何處理的訊號,程式就會被異常中斷。有些訊號會被子進程處理,有些則只能由作業系統處理。
一般情況下,你可以使用child.kill方法來傳送一個訊號,預設發送SIGTERM訊號:
也可以透過傳入一個識別訊號的字串作為kill方法的唯一參數,來傳送某個特定的訊號:
要注意的是,雖然這個方法的名字叫做kill,但是發送的訊號不一定會殺死子程序。如果子程序處理了訊號,預設的訊號行為就會被覆蓋。用Node寫的子程序可以像下面這樣重寫訊號處理器的定義:
現在,你定義了SIGUSR2的訊號處理器,當你的行程再收到SIGUSR2訊號的時候就不會被殺死,而是輸出「Got a SIGUSR2 signal」這句話。使用這個機制,你可以設計一個簡單的方式來跟子行程溝通甚至命令它。雖然不像使用標準輸入功能那麼豐富,但這方式要簡單很多。
小結
這一章,學習了使用child_process.exec方法來執行外部命令,這種方式可以不使用命令列參數,而是透過定義環境變數的方式把參數傳遞給子進程。
也學習了透過呼叫child_process.spawn方法產生子進程的方式來呼叫外部命令,這種方式你可以使用輸入流,輸出流來跟子進程通信,或者使用信號來跟子進程通信以及殺死進程。