NodeJS可以感知和控製自身進程的運行環境和狀態,也可以創建子進程並與其協同工作,這使得NodeJS可以把多個程序組合在一起共同完成某項工作,並在其中充當膠水和調度器的作用。本章除了介紹與之相關的NodeJS內置模塊外,還會重點介紹典型的使用場景。
開門紅
我們已經知道了NodeJS自帶的fs模塊比較基礎,把一個目錄裏的所有文件和子目錄都拷貝到另一個目錄裏需要寫不少代碼。另外我們也知道,終端下的cp命令比較好用,一條cp -r source/* target
命令就能搞定目錄拷貝。那我們首先看看如何使用NodeJS調用終端命令來簡化目錄拷貝,示例代碼如下:
var child_process = require('child_process');
var util = require('util');
function copy(source, target, callback) {
child_process.exec(
util.format('cp -r %s/* %s', source, target), callback);
}
copy('a', 'b', function (err) {
// ...
});
從以上代碼中可以看到,子進程是異步運行的,通過回調函數返回執行結果。
API走馬觀花
我們先大致看看NodeJS提供了哪些和進程管理有關的API。這裏並不逐一介紹每個API的使用方法,官方文檔已經做得很好了。
Process
官方文檔: http://nodejs.org/api/process.html
任何一個進程都有啟動進程時使用的命令行參數,有標準輸入標準輸出,有運行權限,有運行環境和運行狀態。在NodeJS中,可以通過process對象感知和控製NodeJS自身進程的方方麵麵。另外需要注意的是,process不是內置模塊,而是一個全局對象,因此在任何地方都可以直接使用。
Child Process
官方文檔: http://nodejs.org/api/child_process.html
使用child_process模塊可以創建和控製子進程。該模塊提供的API中最核心的是.spawn,其餘API都是針對特定使用場景對它的進一步封裝,算是一種語法糖。
Cluster
官方文檔: http://nodejs.org/api/cluster.html
cluster模塊是對child_process模塊的進一步封裝,專用於解決單進程NodeJS Web服務器無法充分利用多核CPU的問題。使用該模塊可以簡化多進程服務器程序的開發,讓每個核上運行一個工作進程,並統一通過主進程監聽端口和分發請求。
應用場景
和進程管理相關的API單獨介紹起來比較枯燥,因此這裏從一些典型的應用場景出發,分別介紹一些重要API的使用方法。
如何獲取命令行參數
在NodeJS中可以通過process.argv獲取命令行參數。但是比較意外的是,node執行程序路徑和主模塊文件路徑固定占據了argv[0]和argv[1]兩個位置,而第一個命令行參數從argv[2]開始。為了讓argv使用起來更加自然,可以按照以下方式處理。
function main(argv) {
// ...
}
main(process.argv.slice(2));
如何退出程序
通常一個程序做完所有事情後就正常退出了,這時程序的退出狀態碼為0。或者一個程序運行時發生了異常後就掛了,這時程序的退出狀態碼不等於0。如果我們在代碼中捕獲了某個異常,但是覺得程序不應該繼續運行下去,需要立即退出,並且需要把退出狀態碼設置為指定數字,比如1,就可以按照以下方式:
try {
// ...
} catch (err) {
// ...
process.exit(1);
}
如何控製輸入輸出
NodeJS程序的標準輸入流(stdin)、一個標準輸出流(stdout)、一個標準錯誤流(stderr)分別對應process.stdin、process.stdout和process.stderr,第一個是隻讀數據流,後邊兩個是隻寫數據流,對它們的操作按照對數據流的操作方式即可。例如,console.log可以按照以下方式實現。
function log() {
process.stdout.write(
util.format.apply(util, arguments) + '\n');
}
如何降權
在Linux係統下,我們知道需要使用root權限才能監聽1024以下端口。但是一旦完成端口監聽後,繼續讓程序運行在root權限下存在安全隱患,因此最好能把權限降下來。以下是這樣一個例子。
http.createServer(callback).listen(80, function () {
var env = process.env,
uid = parseInt(env['SUDO_UID'] || process.getuid(), 10),
gid = parseInt(env['SUDO_GID'] || process.getgid(), 10);
process.setgid(gid);
process.setuid(uid);
});
上例中有幾點需要注意:
如果是通過sudo獲取root權限的,運行程序的用戶的UID和GID保存在環境變量SUDO_UID和SUDO_GID裏邊。如果是通過chmod +s方式獲取root權限的,運行程序的用戶的UID和GID可直接通過process.getuid和process.getgid方法獲取。
process.setuid和process.setgid方法隻接受number類型的參數。
降權時必須先降GID再降UID,否則順序反過來的話就沒權限更改程序的GID了。
如何創建子進程
以下是一個創建NodeJS子進程的例子。
var child = child_process.spawn('node', [ 'xxx.js' ]);
child.stdout.on('data', function (data) {
console.log('stdout: ' + data);
});
child.stderr.on('data', function (data) {
console.log('stderr: ' + data);
});
child.on('close', function (code) {
console.log('child process exited with code ' + code);
});
上例中使用了.spawn(exec, args, options)方法,該方法支持三個參數。第一個參數是執行文件路徑,可以是執行文件的相對或絕對路徑,也可以是根據PATH環境變量能找到的執行文件名。第二個參數中,數組中的每個成員都按順序對應一個命令行參數。第三個參數可選,用於配置子進程的執行環境與行為。
另外,上例中雖然通過子進程對象的.stdout和.stderr訪問子進程的輸出,但通過options.stdio字段的不同配置,可以將子進程的輸入輸出重定向到任何數據流上,或者讓子進程共享父進程的標準輸入輸出流,或者直接忽略子進程的輸入輸出。
進程間如何通訊
在Linux係統下,進程之間可以通過信號互相通信。以下是一個例子。
/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ]);
child.kill('SIGTERM');
/* child.js */
process.on('SIGTERM', function () {
cleanUp();
process.exit(0);
});
在上例中,父進程通過.kill方法向子進程發送SIGTERM信號,子進程監聽process對象的SIGTERM事件響應信號。不要被.kill方法的名稱迷惑了,該方法本質上是用來給進程發送信號的,進程收到信號後具體要做啥,完全取決於信號的種類和進程自身的代碼。
另外,如果父子進程都是NodeJS進程,就可以通過IPC(進程間通訊)雙向傳遞數據。以下是一個例子。
/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ], {
stdio: [ 0, 1, 2, 'ipc' ]
});
child.on('message', function (msg) {
console.log(msg);
});
child.send({ hello: 'hello' });
/* child.js */
process.on('message', function (msg) {
msg.hello = msg.hello.toUpperCase();
process.send(msg);
});
可以看到,父進程在創建子進程時,在options.stdio字段中通過ipc開啟了一條IPC通道,之後就可以監聽子進程對象的message事件接收來自子進程的消息,並通過.send方法給子進程發送消息。在子進程這邊,可以在process對象上監聽message事件接收來自父進程的消息,並通過.send方法向父進程發送消息。數據在傳遞過程中,會先在發送端使用JSON.stringify方法序列化,再在接收端使用JSON.parse方法反序列化。
如何守護子進程
守護進程一般用於監控工作進程的運行狀態,在工作進程不正常退出時重啟工作進程,保障工作進程不間斷運行。以下是一種實現方式。
/* daemon.js */
function spawn(mainModule) {
var worker = child_process.spawn('node', [ mainModule ]);
worker.on('exit', function (code) {
if (code !== 0) {
spawn(mainModule);
}
});
}
spawn('worker.js');
可以看到,工作進程非正常退出時,守護進程立即重啟工作進程。
小結
本章介紹了使用NodeJS管理進程時需要的API以及主要的應用場景,總結起來有以下幾點:
-
使用
process
對象管理自身。 -
使用
child_process
模塊創建和管理子進程。