child_process - 子进程

# child_process - 子进程

node:child_process模块提供了以类似于但不完全相同的方式生成子进程的能力popen(3) (opens new window)。此功能主要由函数提供child_process.spawn() (opens new window)

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

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

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

ls.on('close', (code) => {
  console.log(`子进程退出码:${code}`);
});

默认情况下,Node.js 的父进程与衍生的子进程之间会建立 stdinstdoutstderr 的管道。 数据能以非阻塞的方式在管道中流通。 有些程序会在内部使用行缓冲 I/O,虽然这并不影响 Node.js,但发送到子进程的数据可能无法被立即使用。

[child_process.spawn()] 函数会异步地衍生子进程,且不会阻塞 Node.js 事件循环。 [child_process.spawnSync()] 函数则以同步的方式提供同样的功能,但会阻塞事件循环,直到衍生的子进程退出或被终止。

child_process 模块还提供了其他一些同步和异步的可选函数。 每个函数都是基于 [child_process.spawn()] 或 [child_process.spawnSync()] 实现的。

  • [child_process.exec()]: 衍生一个 shell 并在 shell 上运行命令,当完成时会传入 stdoutstderr 到回调函数。
  • [child_process.execFile()]: 类似 [child_process.exec()],但直接衍生命令,且无需先衍生 shell。
  • [child_process.fork()]: 衍生一个新的 Node.js 进程,并通过建立 IPC 通讯通道来调用指定的模块,该通道允许父进程与子进程之间相互发送信息。
  • [child_process.execSync()]: [child_process.exec()] 的同步函数,会阻塞 Node.js 事件循环。
  • [child_process.execFileSync()]: [child_process.execFile()] 的同步函数,会阻塞 Node.js 事件循环。

对于某些特例,如自动化的 shell 脚本,同步的函数 (opens new window)可能更方便。 但大多数情况下,同步的函数会明显影响性能,因为它会拖延事件循环直到衍生进程完成。

# 创建异步进程

[child_process.spawn()]、[child_process.fork()]、[child_process.exec()] 和 [child_process.execFile()] 函数都遵循 Node.js API 惯用的异步编程模式。

每个函数都返回 [ChildProcess] 实例。 这些实例实现了 Node.js [EventEmitter] API,允许父进程注册监听器函数,在子进程生命周期期间,当特定的事件发生时会调用这些函数。

[child_process.exec()] 和 [child_process.execFile()] 函数可以额外指定一个可选的 callback 函数,当子进程结束时会被调用。

# 在 Windows 上衍生 .bat.cmd 文件

[child_process.exec()] 和 [child_process.execFile()] 之间的区别会因平台而不同。

在类 Unix 操作系统(Unix、 Linux、 macOS)上,[child_process.execFile()] 效率更高,因为它不需要衍生 shell。

但在 Windows 上,.bat.cmd 文件在没有终端的情况下是不可执行的,因此不能使用 [child_process.execFile()] 启动。

可以使用设置了 shell 选项的 [child_process.spawn()]、或使用 [child_process.exec()]、或衍生 cmd.exe 并将 .bat.cmd 文件作为参数传入(也就是 shell 选项和 [child_process.exec()] 所做的工作)。

如果脚本文件名包含空格,则需要加上引号。

// 仅限 Windows 系统
const { spawn } = require('child_process');
const bat = spawn('cmd.exe', ['/c', 'my.bat']);

bat.stdout.on('data', (data) => {
  console.log(data.toString());
});

bat.stderr.on('data', (data) => {
  console.log(data.toString());
});

bat.on('exit', (code) => {
  console.log(`子进程退出码:${code}`);
});
// 或
const { exec } = require('child_process');
exec('my.bat', (err, stdout, stderr) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(stdout);
});

// 文件名带有空格的脚本:
const bat = spawn('"my script.cmd"', ['a', 'b'], { shell: true });
// 或:
exec('"my script.cmd" a b', (err, stdout, stderr) => {
  // ...
});

# child_process.exec

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

衍生一个 shell 并在 shell 中执行 command,且缓冲任何产生的输出。

传入函数的 command 字符串会被 shell 直接处理,特殊字符(因shell而异 (opens new window))需要相应处理:

exec('"/path/to/test file/test.sh" arg1 arg2');
// 使用双引号使路径中的空格不会被理解为多个参数。

exec('echo "The \\$HOME variable is $HOME"');
// 第一个 $HOME 会被转义,而第二个不会。

注意:不要把未经检查的用户输入传入到该函数。 任何包括 shell 元字符的输入都可被用于触发任何命令的执行。

const { exec } = require('child_process');
exec('cat *.js bad_file | wc -l', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.log(`stderr: ${stderr}`);
});

如果提供了一个 callback 函数,则它被调用时会带上参数 (error, stdout, stderr)。 当成功时,error 会是 null。 当失败时,error 会是一个 [Error] 实例。 error.code 属性会是子进程的退出码,error.signal 会被设为终止进程的信号。 除 0 以外的任何退出码都被认为是一个错误。

传给回调的 stdoutstderr 参数会包含子进程的 stdout 和 stderr 的输出。 默认情况下,Node.js 会解码输出为 UTF-8,并将字符串传给回调。 encoding 选项可用于指定用于解码 stdout 和 stderr 输出的字符编码。 如果 encoding'buffer'、或一个无法识别的字符编码,则传入 Buffer 对象到回调函数。

options 参数可以作为第二个参数传入,用于自定义如何衍生进程。 默认的选项是:

const defaults = {
  encoding: 'utf8',
  timeout: 0,
  maxBuffer: 200 * 1024,
  killSignal: 'SIGTERM',
  cwd: null,
  env: null
};

如果 timeout 大于 0,当子进程运行超过 timeout 毫秒时,父进程就会发送由 killSignal 属性标识的信号(默认为 'SIGTERM')。

注意:不像 POSIX 系统调用中的 exec(3) (opens new window)child_process.exec() 不会替换现有的进程,且使用一个 shell 来执行命令。

如果调用该方法的 [util.promisify()][] 版本,将会返回一个包含 stdoutstderr 的 Promise 对象。在出现错误的情况下,将返回 rejected 状态的 promise,拥有与回调函数一样的 error 对象,但附加了 stdoutstderr 属性。

例子

const util = require('util');
const exec = util.promisify(require('child_process').exec);

async function lsExample() {
  const { stdout, stderr } = await exec('ls');
  console.log('stdout:', stdout);
  console.log('stderr:', stderr);
}
lsExample();

# child_process.execFile

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

  • file string 要运行的可执行文件的名称或路径。
  • args string 字符串参数列表。
  • options
    • cwd string 子进程的当前工作目录。
    • env object 环境变量键值对。
    • encoding string 默认为 'utf8'
    • timeout number 默认为 0
    • maxBuffer (opens new window) number stdout 或 stderr 允许的最大字节数。 默认为 200*1024。 如果超过限制,则子进程会被终止。 See caveat at [maxBuffer and Unicode][].
    • killSignal string | number 默认为 'SIGTERM'
    • uid number 设置该进程的用户标识。(详见 setuid(2) (opens new window)
    • gid number 设置该进程的组标识。(详见 setgid(2) (opens new window)
    • windowsHide boolean 是否隐藏在Windows系统下默认会弹出的子进程控制台窗口。 默认为: false
    • windowsVerbatimArguments boolean 决定在Windows系统下是否使用转义参数。 在Linux平台下会自动忽略,当指令 shell 存在的时该属性将自动被设置为true。默认为: false
  • callback 当进程终止时调用,并带上输出。
    • error
    • stdout
    • stderr
  • 返回: ChildProcess (opens new window)

child_process.execFile() 函数类似 [child_process.exec()],除了不衍生一个 shell。 而是,指定的可执行的 file 被直接衍生为一个新进程,这使得它比 [child_process.exec()] 更高效。

它支持和 [child_process.exec()] 一样的选项。 由于没有衍生 shell,因此不支持像 I/O 重定向和文件查找这样的行为。

const { execFile } = require('child_process');
const child = execFile('node', ['--version'], (error, stdout, stderr) => {
  if (error) {
    throw error;
  }
  console.log(stdout);
});

传给回调的 stdoutstderr 参数会包含子进程的 stdout 和 stderr 的输出。 默认情况下,Node.js 会解码输出为 UTF-8,并将字符串传给回调。 encoding 选项可用于指定用于解码 stdout 和 stderr 输出的字符编码。 如果 encoding'buffer'、或一个无法识别的字符编码,则传入 Buffer 对象到回调函数。

如果调用该方法的 [util.promisify()][] 版本, 它会返回一个拥有 stdoutstderr 属性的 Promise 对象. 在发生错误的情况下, 返回一个 rejected 状态的 promise, 拥有与回调 函数一样的 error 对象, 但是附加了 stdoutstderr 这两个属性.

const util = require('util');
const execFile = util.promisify(require('child_process').execFile);
async function getVersion() {
  const { stdout } = await execFile('node', ['--version']);
  console.log(stdout);
}
getVersion();

# child_process.fork

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

child_process.fork()方法是一种特殊情况, child_process.spawn() (opens new window)专门用于生成新的 Node.js 进程。像child_process.spawn() (opens new window)ChildProcess (opens new window)返回一个对象。返回的ChildProcess (opens new window)将有一个额外的内置通信通道,允许消息在父子之间来回传递。subprocess.send() (opens new window)详情请见。

请记住,生成的 Node.js 子进程独立于父进程,但在两者之间建立的 IPC 通信通道除外。每个进程都有自己的内存,有自己的 V8 实例。由于需要额外的资源分配,因此不建议生成大量子 Node.js 进程。

默认情况下,将使用父进程的child_process.fork()生成新的 Node.js 实例 。对象中的属性 process.execPath (opens new window)允许使用替代执行路径。execPath``options

使用自定义启动的 Node.js 进程将使用使用子进程上的execPath环境变量标识的文件描述符 (fd) 与父进程通信。NODE_CHANNEL_FD

不像fork(2) (opens new window)POSIX 系统调用,child_process.fork()不克隆当前进程。

shell中可用的选项不受支持child_process.spawn() (opens new window)child_process.fork()如果设置将被忽略。

如果signal启用该选项,调用.abort()相应的 AbortController类似于调用.kill()子进程,除了传递给回调的错误将是AbortError

if (process.argv[2] === 'child') {
  setTimeout(() => {
    console.log(`Hello from ${process.argv[2]}!`);
  }, 1_000);
} else {
  const { fork } = require('child_process');
  const controller = new AbortController();
  const { signal } = controller;
  const child = fork(__filename, ['child'], { signal });
  child.on('error', (err) => {
    // This will be called with err being an AbortError if the controller aborts
  });
  controller.abort(); // Stops the child process
}

# child_process.spawn

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

child_process.spawn()方法使用给定的生成一个新进程 command,命令行参数在args. 如果省略,args则默认为空数组。

如果shell启用该选项,请不要将未经过滤的用户输入传递给此函数。任何包含 shell 元字符的输入都可用于触发任意命令执行。

第三个参数可用于指定其他选项,具有以下默认值:

const defaults = {
  cwd: undefined,
  env: process.env
};

用于cwd指定生成进程的工作目录。如果没有给出,默认是继承当前工作目录。如果给定,但路径不存在,子进程会发出错误ENOENT并立即退出。ENOENT当命令不存在时也会发出。

用于env指定对新进程可见的环境变量,默认为process.env (opens new window).

undefined中的值env将被忽略。

运行ls -lh /usr、捕获stdoutstderr和退出代码的示例:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

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

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

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

例子:一个非常复杂的运行方式ps ax | grep ssh

const { spawn } = require('child_process');
const ps = spawn('ps', ['ax']);
const grep = spawn('grep', ['ssh']);

ps.stdout.on('data', (data) => {
  grep.stdin.write(data);
});

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

ps.on('close', (code) => {
  if (code !== 0) {
    console.log(`ps process exited with code ${code}`);
  }
  grep.stdin.end();
});

grep.stdout.on('data', (data) => {
  console.log(data.toString());
});

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

grep.on('close', (code) => {
  if (code !== 0) {
    console.log(`grep process exited with code ${code}`);
  }
});

检查失败的示例spawn

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

subprocess.on('error', (err) => {
  console.error('Failed to start subprocess.');
});

某些平台(macOS、Linux)将使用 的值argv[0]作为进程标题,而其他平台(Windows、SunOS)将使用command.

Node.js 当前会在启动时覆盖argv[0]process.execPath因此 process.argv[0]在 Node.js 子进程中不会匹配从父 进程argv0 传递给的参数,而是使用属性检索它。spawn``process.argv0

如果signal启用该选项,调用.abort()相应的 AbortController类似于调用.kill()子进程,除了传递给回调的错误将是AbortError

const { spawn } = require('child_process');
const controller = new AbortController();
const { signal } = controller;
const grep = spawn('grep', ['ssh'], { signal });
grep.on('error', (err) => {
  // This will be called with err being an AbortError if the controller aborts
});
controller.abort(); // Stops the child process
# options.detached

在 Windows 上,设置options.detachedtrue可以让子进程在父进程退出后继续运行。孩子将有自己的控制台窗口。一旦为子进程启用,它就不能被禁用。

在非 Windows 平台上,如果options.detached设置为true,子进程将成为新进程组和会话的领导者。子进程可以在父进程退出后继续运行,无论它们是否分离。setsid(2) (opens new window)有关详细信息,请参阅。

默认情况下,父母将等待分离的孩子退出。要防止父级等待给定subprocess退出,请使用该 subprocess.unref()方法。这样做会导致父事件循环不将子事件包括在其引用计数中,允许父事件独立于子事件退出,除非在子事件和父事件之间建立了 IPC 通道。

当使用该detached选项启动一个长时间运行的进程时,该进程将不会在父进程退出后继续在后台运行,除非为其提供stdio不连接到父进程的配置。如果父母的stdio是继承的,孩子将继续依附于控制终端。

一个长时间运行的进程的示例,通过分离并忽略其父 stdio文件描述符,以忽略父进程的终止:

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

const subprocess = spawn(process.argv[0], ['child_program.js'], {
  detached: true,
  stdio: 'ignore'
});

subprocess.unref();

或者,可以将子进程的输出重定向到文件中:

const fs = require('fs');
const { spawn } = require('child_process');
const out = fs.openSync('./out.log', 'a');
const err = fs.openSync('./out.log', 'a');

const subprocess = spawn('prg', [], {
  detached: true,
  stdio: [ 'ignore', out, err ]
});

subprocess.unref();
# options.stdio

options.stdio选项用于配置在父进程和子进程之间建立的管道。默认情况下,孩子的 stdin、stdout 和 stderr 被重定向到对象上相应的subprocess.stdin (opens new window)subprocess.stdout (opens new window)subprocess.stderr (opens new window)ChildProcess (opens new window)。这相当于设置options.stdio 等于['pipe', 'pipe', 'pipe']

为方便起见,options.stdio可以是以下字符串之一:

  • 'pipe': 相当于['pipe', 'pipe', 'pipe'](默认)
  • 'overlapped': 相当于['overlapped', 'overlapped', 'overlapped']
  • 'ignore': 相当于['ignore', 'ignore', 'ignore']
  • 'inherit': 相当于['inherit', 'inherit', 'inherit'][0, 1, 2]

否则,的值options.stdio是一个数组,其中每个索引对应于孩子中的一个 fd。fds 0、1 和 2 分别对应于 stdin、stdout 和 stderr。可以指定额外的 fds 以在父子之间创建额外的管道。该值为以下之一:

  1. 'pipe':在子进程和父进程之间创建管道。管道的父端作为对象的属性公开给父 child_processsubprocess.stdio[fd\] (opens new window)。为 fds 0、1 和 2 创建的管道也可分别用作subprocess.stdin (opens new window)subprocess.stdout (opens new window)subprocess.stderr (opens new window)

  2. 'overlapped'``'pipe':除了在FILE_FLAG_OVERLAPPED句柄上设置标志外,其他相同。这对于子进程的 stdio 句柄上的重叠 I/O 是必需的。 有关详细信息,请参阅 文档。 (opens new window)'pipe'这与非 Windows 系统完全相同。

  3. 'ipc':创建一个 IPC 通道,用于在父子之间传递消息/文件描述符。AChildProcess (opens new window)最多可以有一个 IPC stdio 文件描述符。设置此选项可启用该 subprocess.send() (opens new window)方法。如果子进程是 Node.js 进程,IPC 通道的存在将启用process.send() (opens new window)process.disconnect() (opens new window)方法,以及子进程中的事件'disconnect' (opens new window)'message' (opens new window)

    process.send() (opens new window) 不支持以任何方式访问 IPC 通道 fd或将 IPC 通道与非 Node.js 实例的子进程一起使用。

  4. 'ignore': 指示 Node.js 忽略子节点中的 fd。虽然 Node.js 将始终为其生成的进程打开 fds 0、1 和 2,但将 fd 设置为'ignore'将导致 Node.js 打开/dev/null并将其附加到子进程的 fd。

  5. 'inherit': 通过相应的 stdio 流传入/传出父进程。在前三个位置,这分别相当于 process.stdinprocess.stdout、 和process.stderr。在任何其他位置,等同于'ignore'.

  6. stream (opens new window)对象:与子进程共享引用 tty、文件、套接字或管道的可读或可写流。流的底层文件描述符在子进程中被复制到与数组中的索引对应的 fd stdio'open'流必须有一个底层描述符(文件流在事件发生之前没有)。

  7. 正整数:整数值被解释为当前在父进程中打开的文件描述符。它与子进程共享,类似于共享对象的方式。 (opens new window)Windows 不支持传递套接字。

  8. null, undefined: 使用默认值。对于 stdio fds 0、1 和 2(换句话说,stdin、stdout 和 stderr),创建了一个管道。对于 fd 3 及更高版本,默认值为'ignore'.

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

// Child will use parent's stdios.
spawn('prg', [], { stdio: 'inherit' });

// Spawn child sharing only stderr.
spawn('prg', [], { stdio: ['pipe', 'pipe', process.stderr] });

// Open an extra fd=4, to interact with programs presenting a
// startd-style interface.
spawn('prg', [], { stdio: ['pipe', null, null, null, 'pipe'] });

值得注意的是,当父子进程之间建立了IPC通道,并且子进程是Node.js进程时,子进程在未引用IPC通道的情况下启动(使用),直到子进程为事件注册事件unref()处理'disconnect' (opens new window)程序或'message' (opens new window)事件。这允许子进程正常退出,而进程不会被打开的 IPC 通道保持打开状态。

在类 Unix 操作系统上,该child_process.spawn() (opens new window)方法在将事件循环与子进程解耦之前同步执行内存操作。具有大量内存占用的应用程序可能会发现频繁 child_process.spawn() (opens new window)调用成为瓶颈。有关详细信息,请参阅V8 问题 7381 (opens new window)

另见:child_process.exec() (opens new window)child_process.fork() (opens new window)