Nodejs進階:如何玩轉子進程(child_process)

模塊概覽

在node中,child_process這個模塊非常重要。掌握了它,等於在node的世界開啓了一扇新的大門。熟悉shell腳本的同學,可以用它來完成很多有意思的事情,比如文件壓縮、增量部署等,感興趣的同學,看文本文後可以嘗試下。

舉個簡單的例子:

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}`);
});

幾種創建子進程的方式

注意事項:

下面列出來的都是異步創建子進程的方式,每一種方式都有對應的同步版本。

  • .exec()、.execFile()、.fork()底層都是通過.spawn()實現的。
  • .exec()、execFile()額外提供了回調,當子進程停止的時候執行。
  • child_process.spawn(command[, args][, options])
  • child_process.exec(command[, options][, callback])
  • child_process.execFile(file[, args][, options][, callback])
  • child_process.fork(modulePath[, args][, options])
  • child_process.exec(command[, options][, callback])

創建一個shell,然後在shell裏執行命令。執行完成後,將stdout、stderr作爲參數傳入回調方法。

spawns a shell and runs a command within that shell, passing the stdout and stderr to a callback function when complete.

例子如下:

執行成功,error爲null;執行失敗,error爲Error實例。error.code爲錯誤碼,
stdout、stderr爲標準輸出、標準錯誤。默認是字符串,除非options.encoding爲buffer
var exec = require(‘child_process’).exec;

// 成功的例子
exec('ls -al', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + typeof stderr);
});
// 失敗的例子
exec('ls hello.txt', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + stderr);
});

參數說明:

  • cwd:當前工作路徑。
  • env:環境變量。
  • encoding:編碼,默認是utf8。
  • shell:用來執行命令的shell,unix上默認是/bin/sh,windows上默認是cmd.exe。
  • timeout:默認是0。
  • killSignal:默認是SIGTERM。
  • uid:執行進程的uid。
  • gid:執行進程的gid。
  • maxBuffer:標準輸出、錯誤輸出最大允許的數據量(單位爲字節),如果超出的話,子進程就會被殺死。默認是200*1024(就是200k啦)

備註:

如果timeout大於0,那麼,當子進程運行超過timeout毫秒,那麼,就會給進程發送killSignal指定的信號(比如SIGTERM)。
如果運行沒有出錯,那麼error爲null。如果運行出錯,那麼,error.code就是退出代碼(exist code),error.signal會被設置成終止進程的信號。(比如CTRL+C時發送的SIGINT)

風險項

傳入的命令,如果是用戶輸入的,有可能產生類似sql注入的風險,比如

exec('ls hello.txt; rm -rf *', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        // return;
    }
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + stderr);
});

備註事項

Note: Unlike the exec(3) POSIX system call, child_process.exec() does not replace the existing process and uses a shell to execute the command.
child_process.execFile(file[, args][, options][, callback])

跟.exec()類似,不同點在於,沒有創建一個新的shell。至少有兩點影響

  1. 比child_process.exec()效率高一些。(實際待測試)
  2. 一些操作,比如I/O重定向,文件glob等不支持。
similar to child_process.exec() except that it spawns the command directly without first spawning a shell.

file: 可執行文件的名字,或者路徑。

例子:

var child_process = require('child_process');

child_process.execFile('node', ['--version'], function(error, stdout, stderr){
    if(error){
        throw error;
    }
    console.log(stdout);
});

child_process.execFile('/Users/a/.nvm/versions/node/v6.1.0/bin/node', ['--version'], function(error, stdout, stderr){
    if(error){
        throw error;
    }
    console.log(stdout);
});

====== 擴展閱讀

從node源碼來看,exec()、execFile()最大的差別,就在於是否創建了shell。(execFile()內部,options.shell === false),那麼,可以手動設置shell。以下代碼差不多是等價的。win下的shell設置有所不同,感興趣的同學可以自己試驗下。

備註:execFile()內部最終還是通過spawn()實現的, 如果沒有設置 {shell: ‘/bin/bash’},那麼 spawm() 內部對命令的解析會有所不同,execFile(‘ls -al .’) 會直接報錯。

var child_process = require('child_process');
var execFile = child_process.execFile;
var exec = child_process.exec;

exec('ls -al .', function(error, stdout, stderr){
    if(error){
        throw error;
    }
    console.log(stdout);
});

execFile('ls -al .', {shell: '/bin/bash'}, function(error, stdout, stderr){
    if(error){
        throw error;
    }
    console.log(stdout);
});

child_process.fork(modulePath[, args][, options])

modulePath:子進程運行的模塊。

參數說明:(重複的參數說明就不在這裏列舉)

execPath: 用來創建子進程的可執行文件,默認是/usr/local/bin/node。也就是說,你可通過execPath來指定具體的node可執行文件路徑。(比如多個node版本)
execArgv: 傳給可執行文件的字符串參數列表。默認是process.execArgv,跟父進程保持一致。
silent: 默認是false,即子進程的stdio從父進程繼承。如果是true,則直接pipe向子進程的child.stdin、child.stdout等。
stdio: 如果聲明瞭stdio,則會覆蓋silent選項的設置。

例子1:silent

parent.js

var child_process = require(‘child_process’);

// 例子一:會打印出 output from the child
// 默認情況,silent 爲 false,子進程的 stdout 等
// 從父進程繼承

child_process.fork('./child.js', {
    silent: false
});

// 例子二:不會打印出 output from the silent child

// silent 爲 true,子進程的 stdout 等
// pipe 向父進程
child_process.fork('./silentChild.js', {
    silent: true
});

// 例子三:打印出 output from another silent child

var child = child_process.fork('./anotherSilentChild.js', {
    silent: true
});

child.stdout.setEncoding('utf8');
child.stdout.on('data', function(data){
    console.log(data);
});

child.js

console.log('output from the child');
silentChild.js

console.log('output from the silent child');
anotherSilentChild.js

console.log('output from another silent child');

例子二:ipc

parent.js

var child_process = require('child_process');

var child = child_process.fork('./child.js');

child.on('message', function(m){
    console.log('message from child: ' + JSON.stringify(m));
});

child.send({from: 'parent'});
process.on('message', function(m){
    console.log('message from parent: ' + JSON.stringify(m));
});

process.send({from: 'child'});

運行結果

➜  ipc git:(master) ✗ node parent.js
message from child: {"from":"child"}
message from parent: {"from":"parent"}

例子三:execArgv

首先,process.execArgv的定義,參考這裏。設置execArgv的目的一般在於,讓子進程跟父進程保持相同的執行環境。

比如,父進程指定了–harmony,如果子進程沒有指定,那麼就要跪了。

parent.js

var child_process = require('child_process');

console.log('parent execArgv: ' + process.execArgv);

child_process.fork('./child.js', {
    execArgv: process.execArgv
});

child.js

console.log('child execArgv: ' + process.execArgv);

運行結果

execArgv git:(master) ✗ node --harmony parent.js
parent execArgv: --harmony
child execArgv: --harmony

例子3:execPath(TODO 待舉例子)

child_process.spawn(command[, args][, options])

command:要執行的命令

options參數說明:

  • argv0:[String] 這貨比較詭異,在uninx、windows上表現不一樣。有需要再深究。
  • stdio:[Array] | [String] 子進程的stdio。參考這裏
  • detached:[Boolean] 讓子進程獨立於父進程之外運行。同樣在不同平臺上表現有差異,具體參考這裏
  • shell:[Boolean] | [String] 如果是true,在shell裏運行程序。默認是false。(很有用,比如 可以通過

/bin/sh -c xxx 來實現 .exec() 這樣的效果)
例子1:基礎例子

var spawn = require('child_process').spawn;
var ls = spawn('ls', ['-al']);

ls.stdout.on('data', function(data){
    console.log('data from child: ' + data);
});
ls.stderr.on('data', function(data){
    console.log('error from child: ' + data);
});

ls.on('close', function(code){
    console.log('child exists with code: ' + code);
});

例子2:聲明stdio

var spawn = require('child_process').spawn;
var ls = spawn('ls', ['-al'], {
    stdio: 'inherit'
});

ls.on('close', function(code){
    console.log('child exists with code: ' + code);
});

例子3:聲明使用shell

var spawn = require('child_process').spawn;

// 運行 echo "hello nodejs" | wc
var ls = spawn('bash', ['-c', 'echo "hello nodejs" | wc'], {
    stdio: 'inherit',
    shell: true
});

ls.on('close', function(code){
    console.log('child exists with code: ' + code);
});

例子4:錯誤處理,包含兩種場景,這兩種場景有不同的處理方式。

  • 場景1:命令本身不存在,創建子進程報錯。
  • 場景2:命令存在,但運行過程報錯。
var spawn = require('child_process').spawn;
var child = spawn('bad_command');

child.on('error', (err) => {
  console.log('Failed to start child process 1.');
});

var child2 = spawn('ls', ['nonexistFile']);

child2.stderr.on('data', function(data){
    console.log('Error msg from process 2: ' + data);
});

child2.on('error', (err) => {
  console.log('Failed to start child process 2.');
});

運行結果如下。

➜  spawn git:(master) ✗ node error/error.js
Failed to start child process 1.
Error msg from process 2: ls: nonexistFile: No such file or directory

例子5:echo “hello nodejs” | grep “nodejs”

// echo "hello nodejs" | grep "nodejs"
var child_process = require('child_process');

var echo = child_process.spawn('echo', ['hello nodejs']);
var grep = child_process.spawn('grep', ['nodejs']);

grep.stdout.setEncoding('utf8');

echo.stdout.on('data', function(data){
    grep.stdin.write(data);
});

echo.on('close', function(code){
    if(code!==0){
        console.log('echo exists with code: ' + code);
    }
    grep.stdin.end();
});

grep.stdout.on('data', function(data){
    console.log('grep: ' + data);
});

grep.on('close', function(code){
    if(code!==0){
        console.log('grep exists with code: ' + code);
    }
});

運行結果:

➜  spawn git:(master) ✗ node pipe/pipe.js
grep: hello nodejs

關於options.stdio

默認值:[‘pipe’, ‘pipe’, ‘pipe’],這意味着:

  • child.stdin、child.stdout 不是undefined
  • 可以通過監聽 data 事件,來獲取數據。

基礎例子

var spawn = require('child_process').spawn;
var ls = spawn('ls', ['-al']);

ls.stdout.on('data', function(data){
    console.log('data from child: ' + data);
});

ls.on('close', function(code){
    console.log('child exists with code: ' + code);
});

TODO 待舉例子

var spawn = require('child_process').spawn;
var grep = spawn('grep', ['nodejs']);

setTimeout(function(){
    grep.stdin.write('hello nodejs \n hello javascript');
    grep.stdin.end();
}, 2000);

grep.stdout.on('data', function(data){
    console.log('data from grep: ' + data);
});

grep.on('close', function(code){
    console.log('grep exists with code: ' + code);
});

**

異步 vs 同步

**
大部分時候,子進程的創建是異步的。也就是說,它不會阻塞當前的事件循環,這對於性能的提升很有幫助。

當然,有的時候,同步的方式會更方便(阻塞事件循環),比如通過子進程的方式來執行shell腳本時

node同樣提供同步的版本,比如:

  • spawnSync()
  • execSync()
  • execFileSync()

關於options.detached

由於木有在windows上做測試,於是先貼原文

On Windows, setting options.detached to true makes it possible for the child process to continue running after the parent exits. The child will have its own console window. Once enabled for a child process, it cannot be disabled.

在非window是平臺上的表現

On non-Windows platforms, if options.detached is set to true, the child process will be made the leader of a new process group and session. Note that child processes may continue running after the parent exits regardless of whether they are detached or not. See setsid(2) for more information.

默認情況:父進程等待子進程結束。

子進程。可以看到,有個定時器一直在跑

var times = 0;
setInterval(function(){
    console.log(++times);
}, 1000);

運行下面代碼,會發現父進程一直hold着不退出。

var child_process = require('child_process');
child_process.spawn('node', ['child.js'], {
    // stdio: 'inherit'
});

通過child.unref()讓父進程退出

調用child.unref(),將子進程從父進程的事件循環中剔除。於是父進程可以愉快的退出。這裏有幾個要點

  • 調用child.unref()
  • 設置detached爲true
  • 設置stdio爲ignore(這點容易忘)
var child_process = require('child_process');
var child = child_process.spawn('node', ['child.js'], {
    detached: true,
    stdio: 'ignore'  // 備註:如果不置爲 ignore,那麼 父進程還是不會退出
    // stdio: 'inherit'
});

child.unref();

將stdio重定向到文件

除了直接將stdio設置爲ignore,還可以將它重定向到本地的文件。

var child_process = require('child_process');
var fs = require('fs');

var out = fs.openSync('./out.log', 'a');
var err = fs.openSync('./err.log', 'a');

var child = child_process.spawn('node', ['child.js'], {
    detached: true,
    stdio: ['ignore', out, err]
});

child.unref();

exec()與execFile()之間的區別

首先,exec() 內部調用 execFile() 來實現,而 execFile() 內部調用 spawn() 來實現。

exec() -> execFile() -> spawn()

其次,execFile() 內部默認將 options.shell 設置爲false,exec() 默認不是false。

Class: ChildProcess

  • 通過child_process.spawn()等創建,一般不直接用構造函數創建。
  • 繼承了EventEmitters,所以有.on()等方法。

各種事件

close

當stdio流關閉時觸發。這個事件跟exit不同,因爲多個進程可以共享同個stdio流。
參數:code(退出碼,如果子進程是自己退出的話),signal(結束子進程的信號)
問題:code一定是有的嗎?(從對code的註解來看好像不是)比如用kill殺死子進程,那麼,code是?

exit

參數:code、signal,如果子進程是自己退出的,那麼code就是退出碼,否則爲null;如果子進程是通過信號結束的,那麼,signal就是結束進程的信號,否則爲null。這兩者中,一者肯定不爲null。
注意事項:exit事件觸發時,子進程的stdio stream可能還打開着。(場景?)此外,nodejs監聽了SIGINT和SIGTERM信號,也就是說,nodejs收到這兩個信號時,不會立刻退出,而是先做一些清理的工作,然後重新拋出這兩個信號。(目測此時js可以做清理工作了,比如關閉數據庫等。)

  • SIGINT:interrupt,程序終止信號,通常在用戶按下CTRL+C時發出,用來通知前臺進程終止進程。
  • SIGTERM:terminate,程序結束信號,該信號可以被阻塞和處理,通常用來要求程序自己正常退出。shell命令kill缺省產生這個信號。如果信號終止不了,我們纔會嘗試SIGKILL(強制終止)。
Also, note that Node.js establishes signal handlers for SIGINT and SIGTERM and Node.js processes will not terminate immediately due to receipt of those signals. Rather, Node.js will perform a sequence of cleanup actions and then will re-raise the handled signal.

error

當發生下列事情時,error就會被觸發。當error觸發時,exit可能觸發,也可能不觸發。(內心是崩潰的)

  • 無法創建子進程。
  • 進程無法kill。(TODO 舉例子)
  • 向子進程發送消息失敗。(TODO 舉例子)

message

當採用process.send()來發送消息時觸發。
參數:message,爲json對象,或者primitive value;sendHandle,net.Socket對象,或者net.Server對象(熟悉cluster的同學應該對這個不陌生)

  • .connected:當調用.disconnected()時,設爲false。代表是否能夠從子進程接收消息,或者對子進程發送消息。
  • .disconnect():關閉父進程、子進程之間的IPC通道。當這個方法被調用時,disconnect事件就會觸發。如果子進程是node實例(通過child_process.fork()創建),那麼在子進程內部也可以主動調用process.disconnect()來終止IPC通道。參考process.disconnect。

非重要的備忘點

windows平臺上的cmd、bat

The importance of the distinction between child_process.exec() and child_process.execFile() can vary based on platform. On Unix-type operating systems (Unix, Linux, OSX) child_process.execFile() can be more efficient because it does not spawn a shell. On Windows, however, .bat and .cmd files are not executable on their own without a terminal, and therefore cannot be launched using child_process.execFile(). When running on Windows, .bat and .cmd files can be invoked using child_process.spawn() with the shell option set, with child_process.exec(), or by spawning cmd.exe and passing the .bat or .cmd file as an argument (which is what the shell option and child_process.exec() do).
// On Windows Only ...
const spawn = require('child_process').spawn;
const bat = spawn('cmd.exe', ['/c', 'my.bat']);

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}`);
});

// OR...
const exec = require('child_process').exec;
exec('my.bat', (err, stdout, stderr) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(stdout);
});

進程標題

Note: Certain platforms (OS X, Linux) will use the value of argv[0] for the process title while others (Windows, SunOS) will use command.
Note: Node.js currently overwrites argv[0] with process.execPath on startup, so process.argv[0] in a Node.js child process will not match the argv0 parameter passed to spawn from the parent, retrieve it with the process.argv0 property instead.

代碼運行次序的問題

p.js

const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);

console.log('1');

n.on('message', (m) => {
  console.log('PARENT got message:', m);
});

console.log('2');

n.send({ hello: 'world' });

console.log('3');

sub.js

console.log('4');
process.on('message', (m) => {
  console.log('CHILD got message:', m);
});

process.send({ foo: 'bar' });
console.log('5');

運行node p.js,打印出來的內容如下

ch node p.js       
1
2
3
4
5
PARENT got message: { foo: 'bar' }
CHILD got message: { hello: 'world' }

再來個例子

// p2.js
var fork = require('child_process').fork;

console.log('p: 1');

fork('./c2.js');

console.log('p: 2');

// 從測試結果來看,同樣是70ms,有的時候,定時器回調比子進程先執行,有的時候比子進程慢執行。
const t = 70;
setTimeout(function(){
    console.log('p: 3 in %s', t);
}, t);


// c2.js
console.log('c: 1');

關於NODE_CHANNEL_FD

child_process.fork()時,如果指定了execPath,那麼父、子進程間通過NODE_CHANNEL_FD 進行通信。

Node.js processes launched with a custom execPath will communicate with the parent process using the file descriptor (fd) identified using the environment variable NODE_CHANNEL_FD on the child process. The input and output on this fd is expected to be line delimited JSON objects.

寫在後面

內容較多,如有錯漏及建議請指出。

相關鏈接

官方文檔:https://nodejs.org/api/child_process.html

github博客:https://github.com/chyingp/blog
新浪微博:http://weibo.com/chyingp
站酷主頁:http://www.zcool.com.cn/u/346408/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章