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

本文摘錄自《Nodejs學習筆記》,更多章節及更新,請訪問 github主頁地址。歡迎加羣交流,羣號 197339705javascript

模塊概覽

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

舉個簡單的例子:java

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

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

  • .exec().execFile().fork()底層都是經過.spawn()實現的。github

  • .exec()execFile()額外提供了回調,當子進程中止的時候執行。sql

child_process.spawn(command, args)
child_process.exec(command, options)
child_process.execFile(file, args[, callback])
child_process.fork(modulePath, args)shell

child_process.exec(command, options)

建立一個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.json

例子以下:

  1. 執行成功,errornull;執行失敗,errorError實例。error.code爲錯誤碼,

  2. stdoutstderr爲標準輸出、標準錯誤。默認是字符串,除非options.encodingbuffer

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:<Number> 標準輸出、錯誤輸出最大容許的數據量(單位爲字節),若是超出的話,子進程就會被殺死。默認是200*1024(就是200k啦)

備註:

  1. 若是timeout大於0,那麼,當子進程運行超過timeout毫秒,那麼,就會給進程發送killSignal指定的信號(好比SIGTERM)。

  2. 若是運行沒有出錯,那麼errornull。若是運行出錯,那麼,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[, 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:<String> 可執行文件的名字,或者路徑。

例子:

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)

modulePath:子進程運行的模塊。

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

  • execPath:<String> 用來建立子進程的可執行文件,默認是/usr/local/bin/node。也就是說,你可經過execPath來指定具體的node可執行文件路徑。(好比多個node版本)

  • execArgv:<Array> 傳給可執行文件的字符串參數列表。默認是process.execArgv,跟父進程保持一致。

  • silent:<Boolean> 默認是false,即子進程的stdio從父進程繼承。若是是true,則直接pipe向子進程的child.stdinchild.stdout等。

  • stdio:<Array> 若是聲明瞭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)

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'],這意味着:

  1. child.stdin、child.stdout 不是undefined

  2. 能夠經過監聽 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);
});

經過child.stdin.write()寫入

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(),將子進程從父進程的事件循環中剔除。因而父進程能夠愉快的退出。這裏有幾個要點

  1. 調用child.unref()

  2. 設置detachedtrue

  3. 設置stdioignore(這點容易忘)

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平臺上的cmdbat

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_...

相關文章
相關標籤/搜索