Node child process

606 阅读7分钟

child_process 模块提供了衍生子进程的能力。 此功能主要由 child_process.spawn() 函数提供,exec、execFile、fork 底层都是通过 spawn 实现的,它们都是异步衍生子进程的方式,同样 child_process 也提供了同步版本:

const { spawnSync, execSync, execFileSync } = require('child_process');

在讲述具体的创建子进程的方法前,先了解下父子进程分别指的是什么: Node 执行代码(此处为node ./parent.js)衍生的进程叫父进程(主进程),通过 child_process 衍生(此处为执行 exec)的进程叫子进程。

//parent.js
const { exec } = require('child_process');
exec('ls');

本文的例子详见

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

spawn 方法使用给定的 command 和 args 中的命令行参数衍生新进程。

  • command(必选)

    string 类型,表示要执行的命令,如果给的 command 不存在则报 ENOENT 错误。

  • args(可选)

    string[]类型,表示参数列表

  • options(可选)

    • cwd()

      string 或 URL 类型,表示子进程的工作目录,默认为 process.cwd()。如果给的 cwd 不存在则报 ENOENT 错误。

    • env

      Object 类型,表示环境变量的键值对,默认为 process.env。

      spawn('echo $ANSWER', {
        stdio: 'inherit',
        shell: true,
        env: { ANSWER: 20 },
      }); //20
      
    • argv0

      显式设置发送给子进程的 argv0 的值, 如果未指定,则设置为 command。

      //设置argv0为abc则子进程的argv0为abc,否则为node
      spawn('node', ['./child.js'], { argv0: 'abc' });
      
    • stdio

      Array string 类型,表示标准的输入输出,用来配置父子进程间的管道流。

      stdio 的可选值:

      • pipe

        等同于['pipe', 'pipe', 'pipe'](默认值),表示标准输入、标准输出和标准错误分别为 pipe、pipe、pipe。pipe 的父端作为子进程对象上的属性subprocess.stdio[fd]暴露给父进程,fd 为 0、1、2 分别对应 subprocess.stdin, subprocess.stdout 和 subprocess.stderr。 pipe 意味着,child.stdin、child.stdout 不是 undefined,可以通过监听 data 事件,来获取数据。

        const sub = spawn('ls', {
          stdio: 'pipe',
        });
        
        console.log(sub.stdio[0] === sub.stdin); //true
        console.log(sub.stdio[1] === sub.stdout); //true
        console.log(sub.stdio[2] === sub.stderr); //true
        
        sub.stdout.on('data', function (data) {
          console.log(`data from child: ${data}`);
        });
        

        子进程的 stdio 是可写流,而父进程的 stdio 是可读流: 执行node readable-pipe-writable.js,输入字符 abc(3 个字符),按ctrl+d(结束输入),父进程的输入会传递给子进程,wc 子进程计算输入字符的个数,输出 3:

        //readable-pipe-writable.js
        const { spawn } = require('child_process');
        
        const child = spawn('wc', ['-m']); //获取字符数
        
        process.stdin.pipe(child.stdin);
        
        child.stdout.on('data', (data) => {
          console.log(data); //3
        });
        

        在进程间同样可以 pipe:

        //spawn-pipe-each-other.js
        const { spawn } = require('child_process');
        
        const find = spawn('find', ['.', '-type', 'f']);
        const wc = spawn('wc', ['-l']); //计算行数
        
        find.stdout.pipe(wc.stdin);
        
        wc.stdout.on('data', (data) => {
          console.log(`Number of files ${data}`);
        });
        
      • overlapped

        等同于['overlapped', 'overlapped', 'overlapped'],和 pipe 在非 Windows 系统上是完全一样的,详见

      • ignore

        等同于['ignore', 'ignore', 'ignore'],Node.js 子进程忽略 fd,这意味着无法通过 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 获取数据。

      • inherit

        等同于['inherit', 'inherit', 'inherit'][0, 1, 2][process.stdin, process.stdout, process.stderr]。表示父进程的 stdios 由子进程控制,如下: 当不设置 stdio,控制台执行node spawn-inherit.js不会输出任何内容,当 stdio 设置为 inherit 时会输出 pwd 结果,因为 inherit 使得子进程可以使用父进程的 stdios,也就能在父进程的控制台输出内容。

        //spawn-inherit.js
        const { spawn } = require('child_process');
        spawn('pwd', {
          stdio: 'inherit',
        });
        

      前述已经知道 fd 的 0、1 和 2 分别对应 stdin、stdout 和 stderr。 额外的 fd 可以被指定来衍生父进程和子进程之间的额外管道。 该值是以下之一:

      • ipc

        衍生一个用于父子进程间传递消息或文件描述符的 IPC 通道符。设置该选项可启用 subprocess.send() 方法。 如果子进程是一个 Node.js 进程,则一个已存在的 IPC 通道会在子进程中启用 process.send()process.disconnect()process.on('disconnect')process.on('message')

        //spawn-ipc.js
        const { spawn } = require('child_process');
        //如果不开ipc是没有subprocess.send方法的
        const subprocess = spawn('node', ['./child.js'], {
          stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
        });
        
        subprocess.on('message', function (message) {
          console.log(`Receive message from child: ${JSON.stringify(message)}`);
        });
        
        subprocess.send({ from: 'parent' });
        /*********************分割线*******************/
        //如果不开ipc是没有process.send方法的
        process.on('message', function (message) {
          console.log(`Receive message from parent: ${JSON.stringify(message)}`);
        });
        
        process.send({ from: 'child' });
        
      • pipe

        同上述 pipe。

      • ignore

        同上述 ignore。

      • Stream

        共享一个指向子进程的 tty(文本终端)、文件、socket 或管道的可读或可写流。

      • 正整数

        表示一个正在父进程中打开的文件描述符, 它和子进程共享。

      • null, undefined

        默认值, 对于 stdio fd 0、1 和 2(换言之,stdin、stdout 和 stderr)而言是衍生了一个管道。 对于 fd 3 及以上而言,默认值为 'ignore'。

    • detached

      boolean 类型, 表示子进程独立于父进程执行,默认情况下父进程会等待子进程退出后再退出,如果要阻止父进程等待子进程,可以用 subprocess.unref()方法,如果不调用subprocess.unref(),父进程会等待子进程结束(10s 后)才会退出:

      //spawn-detached.js
      const subprocess = spawn('node', ['long-task.js'], {
        detached: true,
        stdio: 'ignore', //不设置成ignore,调用unref父进程仍然不会退出
      });
      
      subprocess.on('close', (code) => {
        console.log(`subprocess process exited with code ${code}`);
      });
      subprocess.unref();
      /*-------------------分割线---------------------*/
      //long-task.js
      setTimeout(() => {
        console.log('10 seconds');
      }, 10000);
      

      执行node spawn-detached.js后父进程退出,在控制台上查看 long-task.js:

       ps -ef | grep long-task.js
       =>  501 42821 1 0 10:02AM ?? 0:00.02 node long-task.js
      

      可以看到父进程退出后子进程仍然在独立运行。

    • uid

      number 类型,表示执行进程的 user id。

    • gid

      number 类型,表示执行进程分组 id。

    • serialization

      string 类型,表示进程间传递信息的序列化方法,可能的值是 json 和 advanced 默认值为 json。

    • shell

      string 或 boolean 类型,默认值为 false,当为 true 时,在 Unix 上的 shell 为/bin/sh,Windows 上为process.env.ComSpec。 默认情况下,spawn 并不会创建 Shell 来执行命令,因此通过 Shell 的方式执行命令会报错:

      const { spawn } = require('child_process');
      
      spawn('ls -a'); //Error: spawn ls -a ENOENT
      const child = spawn('ls -a', {
        shell: true,
      }); //通过Shell来执行,不会报错
      spawn('ls', ['-a']); //不会报错
      
    • windowsVerbatimArguments

      布尔类型, 在 Windows 上不为参数加上引号或转义,默认值为 false,只在 Windows 上起作用。

    • windowsHide

      boolean 类型,表示隐藏子进程的窗口(通常在 Windows 上会衍生),默认值为 false。

    • signal

      类型为AbortSignal,可以用来终止子进程的执行。

    • timeout

      number 类型,默认为 0。当改值大于 0 时,子进程的父进程会发送终止信号终止进程。

    • killSignal

      string 或 integer,默认值为'SIGTERM'。

  • Returns

    返回ChildProcess

基本使用方式:

const { spawn } = require('child_process');

const ls = spawn('ls', ['-lh']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('exit', (code) => {
  console.log(`child process exited with code ${code}`);
});

ls.on('close', (code) => {
  console.log(`child process closed with code ${code}`);
});

child_process.exec(command[, options][, callback])

创建一个 shell 然后在 shell 里执行命令。

  • command(必要参数)

    string 类型,表示要执行的命令,用空格分隔。

  • options(可选)

    选项 cwd、env、maxBuffer、killSignal、uid、gid、windowsHide、signal 同 spwan 一样,增加了:

    • encoding

      string 类型,表示编码方式,默认为 utf8。

    • shell

      string 类型,表示要执行命令的 Shell。Unix 上的默认值为/bin/sh,Windows 上为process.env.ComSpec

    • maxBuffer

      number 类型,stdout 和 stderr 允许输出的最大的字节数(bytes),超过这个限制子进程则会被终止,所以输出都会被截断。 默认值为 1024 * 1024,也就是 1M。

  • callback(可选)

    进程终止的回调函数,参数如下:

    • error

      Error 类型,如果执行成功则为 null,如果失败返回 Error,通常来讲此时 Error.code(子进程退出的 code) 不为 0。

    • stdout

      string 或 Buffer。

    • stderr

      string 或 Buffer。

  • Returns

    返回ChildProcess

基本使用方式:

const { exec } = require('child_process');
//省略options
exec('ls -a', function (error, stdout, stderr) {
  if (error) {
    console.error(`error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.log(`stderr: ${stderr}`);
});

exec('ls -a', { cwd: '/' }, function (error, stdout, stderr) {
  if (error) {
    console.error(`error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.log(`stderr: ${stderr}`);
});

因为 command 的参数是以空格分隔的,因此对于一些带有空格特殊的命令需要加上引号,如下有一个叫test folder的文件夹,文件夹名中间有空格,如果不加参数 ls 会去查找./test文件夹,导致错误。

const { exec } = require('child_process');
//输出text.txt,test folder下文件
exec('ls "./test folder"',

//ls: ./test: No such file or directory
//ls: folder: No such file or directory
exec('ls ./test folder',

通过 util.promisify 可以将 exec 转换为 Promise 版本:

const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const testPromiseExec = async () => {
  try {
    const { stdout, stderr } = await execPromise('ls "./test folder"');
    console.log('testPromiseExec stdout:', stdout);
    console.error('testPromiseExec stderr:', stderr);
  } catch (error) {
    //error多了stdout、stderr两个属性
    console.error('testPromiseExec error:', error);
  }
};

exec 可以通过 AbortController 来主动终止进程:

const abortExample = () => {
  const controller = new AbortController();
  const { signal } = controller;
  const process = exec('grep history', { signal }, (error) => {
    console.log(`grep ssh error:${error}`); // AbortError
  });
  setTimeout(() => {
    controller.abort();
  }, 1000);
};

abortExample();

如果执行的用户输入的命令可能带来风险,比如:

exec('rm -rf *');

child_process.execFile(file[, args][, options][, callback])

  • file

    string 类型,表示要执行文件的名称或路径。

  • args

    string[]类型,表示参数列表。

  • options

    选项 cwd、env、encoding、timeout、maxBuffer、killSignal、uid、gid、windowsHide、signal 同 exec 一样,只新增加了一项:

    • windowsVerbatimArguments

      布尔类型, 在 Windows 上不为参数加上引号或转义,默认值为 false,只在 Windows 上起作用。

  • callback(可选)

    同 exec 一样

  • Returns

    同 exec 一样

execFile 同 exec 基本一样,不同点在于 execFile 并不会衍生一个 Shell 来执行,而是直接开启新进程执行文件,这样效率会更高一些。

基本使用方式:

execFile('node', ['--version'], (error, stdout, stderr) => {
  if (error) {
    throw error;
  }
  console.log(stdout);
});
//路径
execFile(
  '/Users/xinghunm/.nvm/versions/node/v16.10.0/bin/node',
  ['--version'],
  (error, stdout, stderr) => {
    if (error) {
      throw error;
    }
    console.log(stdout);
  }
);

需要注意的是 execFile 开启 Shell 选项同样会衍生 Shell

//不添加shell选项ls -a会报错
execFile('ls -a', { shell: '/bin/bash' }, function (error, stdout, stderr) {
  if (error) {
    throw error;
  }
  console.log('ls -a output:', stdout);
});

同 exec 一样都能通过 util.promisify 来使用 Promise 版本。

const { execFile } = require('child_process');
const util = require('util');
const execFilePromise = util.promisify(execFile);

const testPromiseExec = async () => {
  try {
    const { stdout, stderr } = await execFilePromise('node', ['--version']);
    console.log('testPromiseExec stdout:', stdout);
    console.error('testPromiseExec stderr:', stderr);
  } catch (error) {
    console.error('testPromiseExec error:', error);
  }
};
testPromiseExec();

通过 AbortController 终止进程:

const controller = new AbortController();
const { signal } = controller;
const process = execFile('node', ['--version'], { signal }, (error) => {
  console.log(`node --version error:${error}`); // AbortError
});

controller.abort();

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

  • modulePath

    string 类型,表示要执行的模块名字或路径。

  • args

    string[]类型,表示参数列表。

  • options

    选项 cwd、env、timeout、killSignal、uid、gid、windowsVerbatimArguments、signal 同 execFile 一样,其他如下:

    • detached

      布尔类型, 表示子进程独立于父进程运行。

    • execPath

      string 类型,表示衍生子进程的可执行文件,默认是/usr/local/bin/node。也就是说,通过 execPath 可以指定具体的 node 可执行文件路径,比如多个 node 版本。

    • execArgv

      string[]类型,表示传递给可执行文件(execPath 指定)的参数列表。默认值是 process.execArgv。

    • serialization

      string 类型,表示进程间传递信息的序列化方法,可能的值是 json 和 advanced 默认值为 json。

    • silent

      boolean 类型,默认是 false,如果为 true,表示静默,子进程的 stdin, stdout 以及 stderr 会直接 pipe 到父进程:

      const { fork } = require('child_process');
      
      //例1:
      //不会打印message from child
      const child = fork('./child.js', {
        silent: true,
      });
      
      //例2:
      //打印出message from child
      const child = fork('./child.js', {
        silent: true,
      });
      child.stdout.on('data', function (data) {
        console.log(data.toString());
      });
      
      //例3:
      //打印出message from child
      fork('./child.js', {
        silent: false,
      });
      /*-------------------分割线---------------------*/
      //child.j
      console.log('message from child');
      
    • stdio

      <Array><string>类型,表示标准输入输出,如果设置了该项,silent 选项的设置会被覆盖。

  • callback(可选)

    同 exec 一样

  • Returns

    同 exec 一样

fork 是 child_process.spawn() 的一个特殊的实例,返回的ChildProcess添加了父子进程间的通讯通道:

//parent.js
const child = fork('./child.js');

child.on('message', function (message) {
  //输出Receive message from child: {"from":"child"}
  console.log(`Receive message from child: ${JSON.stringify(message)}`);
});

child.send({ from: 'parent' });
/*-------------------分割线---------------------*/
//child.js
process.on('message', function (message) {
  //输出Receive message from parent: {"from":"parent"}
  console.log(`Receive message from parent: ${JSON.stringify(message)}`);
});

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

ChildProcess 事件

  • exit

    close 事件的回调有两个参数:

    • code

      number 类型,如果子进程自己退出,则为退出码。

    • signal

      string 类型,表示终止子进程的信号。

    exit 事件在子进程结束后触发,如果进程正常退出了,则 code 是进程的最终退出码,否则为 null。 如果进程是因为收到信号而终止的,则 signal 是信号的字符串名称,否则为 null。 code 和 signal 总有一个是非空的。 需要注意的是当 exit 事件触发时,子进程的 stdio 流可能依然是打开的。此外,exit(以及 close)事件并不一定触发,如下: 当子进程文件 write-to-stderr.js 通过 stderr 写入数据超过一定大小,大概在 24kb(测试环境为 24906 字节)后,进程会挂起,不会触发 exit、close 等事件。

    //spawn-close-not-fire.js
    const { spawn } = require('child_process');
    // 24576
    const subProcess = spawn('node', ['write-to-stderr.js', '24906']);
    
    subProcess.on('error', function (error) {
      console.log(`error:${code}`);
    });
    
    subProcess.on('close', function (code) {
      console.log(`close:${code}`);
    });
    
    subProcess.on('exit', function (code) {
      console.log(`exit:${code}`);
    });
    
    /*********************分割线*******************/
    //write-to-stderr.js
    const count = process.argv.length > 2 ? process.argv[2] : 1000;
    for (let i = 0; i < count; i++) {
      process.stderr.write('0');
    }
    

    因为少量数据系统会缓存,但是数据大了就需要客户端进行处理,上述例子中的数据超过了限值需要等待客户端处理,而主进程没有处理子进程的数据,因此子进程不能够完成,也就不能被 close。通过以下两种方式处理就能够顺利退出了。

    • 方式一

      消费数据:

      //spawn-close-not-fire.js
      subProcess.stdout.on('data', function (data) {
        //回调代码体可以为空
        console.log('stdout' + data);
      });
      //或
      subProcess.stderr.on('data', function (data) {
        //回调代码体可以为空
        console.log('stderr' + data);
      });
      
    • 方式二

      设置 stdio 为 ignore,不关心输出:

      //spawn-close-not-fire.js
      const subProcess = spawn('node', { stdio: 'ignore' }, [
        'write-to-stderr.js',
        '24906',
      ]);
      
  • close

    close 事件的回调有两个参数:

    • code

      number 类型,如果子进程自己退出,则为退出码。

    • signal

      string 类型,表示终止子进程的信号。

    close 事件在进程已结束且子进程的标准输入输出流已关闭之后触发,这与 exit 事件不同,因为多个进程可能共享相同的标准输入输出流。 close 事件始终在 exit 或 error(如果子进程衍生失败)事件后触发。

    const { spawn } = require('child_process');
    
    const ls = spawn('ls', ['-lh']);
    
    ls.on('close', (code, signal) => {
      console.log(`child process closed with code ${code}`);
    });
    
  • error

    error 在以下三种情况被触发:

    • 进程无法被衍生

    • 进程无法被杀死

    • 向子进程发送消息失败

    需要注意的是 error 事件粗分后并不一定会触发 exit,当同时监听了 error 和 exit 时间时,需要防止处理函数被多次调用。

  • message

    当子进程通过 process.send()发送消息时触发。

  • disconnect

    在父进程中调用 subprocess.disconnect() 或在子进程中调用 process.disconnect() 后会触发 disconnect 事件, 断开后就不能再发送或接收信息。

  • spawn

    当子进程成功衍生时触发,如果衍生失败则会触发 error 事件,spawn 事件比其他事件都要先被触发。

总结

  • exec、execFile、fork 都是基于 spawn 实现
  • 当想通过 Shell 语法执行命令且该命令输出的内容较小(1M 以内)时 exec 是一个好的选择
  • exec 与 execFile 的区别在于 exec 会创建 Shell 来执行命令, execFile 效率会更高一些
  • fork 与 spawn 的最大的区别是 fork 衍生子进程时会创建通讯通道,使得父子进程间可以发送消息
  • 当预期要执行的命令输出较大时,使用 spawn 是一个好的选择
  • 通过 detached 可以创建一个独立于父进程的进程