12-子进程-child_process

243 阅读11分钟

子进程-child_process

const child_process = require("child_process");
const spawn = child_process.spawn;
const exec = child_process.exec;
const execFile = child_process.execFile;

// 模块概述
// 在node中,child_process这个模块非常重要,掌握它,等于在node世界开启了一扇新的大门

简单的例子

const ls = spawn("ls", ["-lh", "/usr"]);
ls.stdout.on("data", (data) => {
  console.log("stdot", data);
});
ls.stderr.on("data", (data) => {
  console.log("stderr", data);
});
ls.on("close", (code) => {
  console.log("child progress exited with code", code);
});
// 标准输入(stdin)、
// 标准输出(stdout)、
// 标准错误(stderr)

几种创建子进程的方式

// 下面列出的都是异步创建子进程的方式,每一种方式都用对应的版本
// .exec() .execFile() .fork() 底层都是通过 .spawn() 实现的
// .exec() .execFile() 额外提供了回调,当子进程停止的时候执行
// child_process.spawn(commad, args, options)
// child_process.exec(commad, args, callbaxk)
// child_process.execFile(file, args, options, callback)
// child_process.fork(modulePath, args, options)

child_process.exec(command, options, callback)

// 创建一个shell, 然后在shell里执行命令,执行完成后,将 stdout stderr 作为参数传入回调方法
// 例子如下,
// 1. 执行成功 error 为 null; 执行失败, error 为 Error 实例, error.code 为错误码
// 2. stdout stderr 为标准输出,标准错误,默认是字符串,除非 options.encoding 为 buffer

// 成功的例子
exec("node -v", (error, stdout, stderr) => {
  if (error) {
    console.log("error", error);
    return;
  }
  console.log(stdout); // v14.19.0
  console.log(typeof stderr); // string
});
// 失败的例子
exec("narm", (err, stdout, stderr) => {
  if (err) {
    console.log("error", err); // Error: Command failed: narm
    return;
  }
  console.log(stdout);
  console.log(stderr);
});

参数说明

// cmd        当前工作路径
// env        环境变量
// encoding   编码,默认是utf-8
// shell      用来执行命令的shell,unix上默认是/bin/sh windows上默认是 cmd.exe
// timeout    默认是0
// killSingal 默认是SIGTERM
// uid        执行进程的uid
// dig        执行进程的gid
// maxBuffer  number输出标准,错误输出最大允许的数量级别(单位字节),如果超出的话,子进程就会被杀死.默认是200*1024(就是200k)

// 备注
// 1.如果timeout大于0,那么,当子进程运行超过timeout毫秒,那么就会给进程发送 killSingal 指定的信号(比如 SIGTERM)
// 2.如果运行没有错误,那么 error 为 null, 如果运行出粗,那么, error.code 就会推出代码,error.signal会被设置成终止进程的信号,比如 CTRL+C 的时候发送 SIGINT

风险项

// 传入的命令,如果是用户输入的,有可能产生sql注入的风险 例如

exec("ls hello.txt; rm - rf *", function (error, stdout, stderr) {
  if (error) {
    console.log("error", error);
  }
  console.log("stdout", stdout);
  console.log("stderr", stderr);
});

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

// 和 .exec 类似,不同点在于,没有创建一个新shell,至少有两点影响
// 1. 比 child_process.exec() 效率高一些,
// 2. 一些操作, 比如I/O重定向, 文件glob等不支持
// file 是字符串,可执行文件的名字或者路径

const process = child_process.execFile("node", ["--version"]);
process.stdout.on("data", (chunk) => {
  console.log(chunk); // v14.19.0
});
process.stderr.on("data", (chunk) => {
  console.log(chunk);
});

const process1 = child_process.execFile("D:/software/NodeTools/nodejs", [
  "--version",
]);
process1.stdout.on("data", (chunk) => {
  console.log(chunk); // v14.19.0
});
process1.stderr.on("data", (chunk) => {
  console.log(chunk);
});

// 从node源码来看, .exec() .execFile() 最大的差别就是是否创建了shell, .execFile()内部 Options.shell = false , 那么可以设置shell,以下代码差不多是等价的,win下的shell设置有所不同,
// .execFile()内部最终还是通过 spawn() 实现的,如果有设置{shell: '/bin/bash'} 那么 spawn() 内部对命令的解析会有所不同, .execFile('ls -al .')会直接报错

exec("dir  /b", (error, stderr, stdout) => {
  console.log(stderr);
  // aaa.js
  // index.js
});
execFile("dir  /b", { shell: "/bin/bash" }, (error, stderr, stdout) => {
  console.log(stderr);
});

child_process.fork(modulePath, args, options);

// modulePath 子进程运行的模块
// execPath (String) 用来创建子进程的可执行文件,默认是 'user/local/bin/node' 也就是说 你可以通过, execPath 来指定具体的node可执行文件路径(比如多个node版本)
// execArgv (Array) 传给可执行文件饿字符串参数列表, 默认是 process.execArgv 跟父进程保持一致
// silent (Boolean) 默认是 false, 即子进程的 stdio 从父进程继承, 如果是 true 则直接 pipe 向子进程 child.stdin child.stdout 等
// stdio (Array) 如果声明了 stdio 则会覆盖 silent 的选项和的设置

例子 1: silent

// 例子一: 不会打印出 output from the child
// silent 为 false 子进程的 stdout 等
// 从父进程继承
// child.js console.log('output from the child');
const child1 = child_process.fork("./child.js", {
  silent: false,
});
child1.stdout.setEncoding("utf8");
child1.stdout.on("data", function (data) {
  console.log(data);
});

// 例子二 会打印出 output from the child
// silent 为 true 子进程的 stdout
// pipe 向父进程
const child2 = child_process.fork("./child.js", {
  silent: true,
});
child2.stdout.setEncoding("utf-8");
child2.stdout.on("data", (data) => {
  console.log(data); // output from the child
});

例子 2: ipc

const child = child_process.fork("./child1.js");
child.on("message", (message) => {
  console.log("message from child", JSON.stringify(message)); // message from child {"from":"child"}
});
child.send({ from: "parent" }); // message from parent: {"from":"parent"}

// child.js
// process.on("message", (message) => {
//   console.log("message from parent", JSON.stringify(message));
// });
// process.send({ from: "Child" });

例子 3: exceArgv

// 设置 process.execArgv 的目的一般在用于,让子进程跟父进程保持相同的执行环境, 比如父进程指定了 --harmony 如果子进程没有指定,那么就不行

console.log("parent execArgv", process.execArgv); // parent execArgv [ '-i', '--harmony' ]
child_process.fork("./child.js", {
  execArgv: process.execArgv, // child execArgv: -i,--harmony
});

// child.js
console.log("child execArgv: " + process.execArgv);

// 执行 node -i --harmony .\index.js
// 打印 parent execArgv [ '-i', '--harmony' ]
// 打印 child execArgv: -i,--harmony

child_process.spawn(command, args, options);

// command 要执行的命令
// argv[0] (String) 在 uninx 和 windows 上的表现不一样
// stdio (Array | String) 子进程的 stdio 参考[这里](https://nodejs.org/api/child_process.html#child_process_options_stdio)
// detached (Boolean) 让子进程独立于父进程之外运行,同样在不同的平台上表现有所差异,具体参考[这里](https://nodejs.org/api/child_process.html#child_process_options_detached)
// shell (Boolean | String) 如果是true,在shell里面运行程序,默认是false,(比如 可以通过 /bin/sh -c xxx 来实现 .exec() 这样的效果)

例子 1 基础例子

const ls = spawn("node", ["-v"]);
ls.stdout.on("data", (data) => {
  console.log("stdout data from child", data); // stdout data from child <Buffer 76 31 34 2e 31 39 2e 30 0d 0a>
});
ls.stderr.on("data", (data) => {
  console.log("stderr error from child", data);
});
ls.on("close", (code) => {
  console.log("child exit width code", code); // child exit width code 0
});

例子 2: 声明 stdio

const ls = spawn("node", ["-v"], {
  stdio: "inherit", // v14.19.0
});
ls.on("close", (code) => {
  console.log("child exit with code", code); // child exit with code 0
});

例子 3 声明 shell

const ls = spawn("bash", ["c", 'echo "hello nodejs" | wc'], {
  stdio: "inherit",
  shell: true,
});
ls.on("close", (code) => {
  console.log("child exit with code", code);
});

例子 4 错误处理 包含两个场景,这两个场景有不同的处理方式

// 场景1 命令本身不存在创建子进程失败
// 场景2 命令存在但是运行过程报错了

const child = spawn("bad_command");
child.on("error", (code) => {
  console.log("Failed to start child process 1"); // Failed to start child process 1
});

const child2 = spawn("ls", ["nonexistFile"]);
child2.stderr.setEncoding("utf8");
child2.stderr.on("data", (data) => {
  console.log("Error msg from process 2", data); // Error msg from process 2 ls: cannot access 'nonexistFile': No such file or directory
});
child2.on("error", (err) => {
  console.log("Failed to start child process 2");
});

例子 5 echo "hello nodejs" | grep "nodejs"

const echo = child_process.spawn("echo", ["hello nodejs"]);
const grep = child_process.spawn("grep", ["nodejs"]);
grep.stdout.setEncoding("utf-8");
grep.stdout.on("data", (data) => {
  grep.stdin.write(data);
});
echo.on("close", (code) => {
  if (code !== 0) {
    console.log("echo exit width code", code);
  }
  grep.stdin.end();
});
grep.stdout.on("data", (data) => {
  console.log("grep", data);
});
grep.on("close", (code) => {
  if (code !== 0) {
    console.log("grep exit width code", code);
  }
});

关于 options.stdio

// 默认值: ['pipe', 'pipe', 'pipe']这意味着,
// 1. child.stdin child.stdout 不是 undefined
// 2. 可以通过监听 data 事件来获取数据

基础例子

const ls = spawn("ls", ["-al"]);
ls.stdout.setEncoding("utf-8");
ls.stdout.on("data", (data) => {
  console.log("data", data);
  // data total 18
  // drwxr-xr-x 1 259776 1049089     0  3月  3 11:20 .
  // drwxr-xr-x 1 259776 1049089     0  2月 28 17:09 ..
  // -rw-r--r-- 1 259776 1049089    53  3月  3 11:45 child.js
  // -rw-r--r-- 1 259776 1049089   134  3月  3 11:20 child1.js
  // -rw-r--r-- 1 259776 1049089 11230  3月  7 15:38 index.js
});
ls.on("close", (code) => {
  console.log("code", code); // code 0
});

通过 child.stdin.write 写入

const grep = spawn("grep", ["nodejs"]);
setTimeout(() => {
  grep.stdin.write("hello nodejs \n hello js");
  grep.stdin.end();
}, 2000);
grep.stdout.setEncoding("utf-8");
grep.stdout.on("data", (data) => {
  console.log("data-->", data); // data--> hello nodejs
});
grep.on("close", (code) => {
  console.log("code--->", code); // code---> 0
});

异步 vs 同步

// 大部分时间,子进程的创建是异步的,也就是说,他不会阻塞当前的事件循环,这对于性能的提升有很大的帮助,
// 当然有的时候,同步的方式会更方便(阻塞事件循环),比如通过子进程的方式拉执行shell脚本时
// node同样提供同步的版本, 比如
// spawnSync()
// exceSync()
// execFileSync()

关于 options.detached

// 在 window上
// >在Windows上,将options.dependent设置为true可以使子进程在父进程退出后继续运行。孩子将有自己的控制台窗口。一旦为子进程启用,就不能禁用它。
// 在非 window 上
// >在非Windows平台上,如果options.dependent设置为true,则子进程将成为新进程组和会话的领导者。请注意,无论父进程是否分离,子进程都可以在父进程退出后继续运行。有关更多信息,请参阅setsid(2)。

默认情况: 父进程等待子进程结束

// child.js 代码
// const timeout = 0;
// setInterval(() => {
//   timeout++;
// }, 1000);
child_process.spawn("node", ["./child.js"]);
console.log(123);

// 因为 child.js 内存在定时器, 父进程后面的进程会被卡主, 123 无法打印出来

通过 child.unref() 将子进程从父进程的事件循环中剔除,于是父进程可以更快的退出,需要强调的

// 1. 设置 child.unref()
// 2. 设置 detached 为 true
// 3. 设置 stdio 为 ignore

const child = spawn("node", ["child.js"], {
  detached: true,
  stdio: "ignore", // 如果不设置为 ignore 那么 父进程和不会退出
});
child.unref();

将 stdio 重定向到文件

// 除了直接将 stdio 设置为 ignore, 还可以将它重定向到本地的文件

const fs = require("fs");
const out = fs.openSync("./out.log", "a");
const err = fs.openSync("./err.log", "a");
const child = child_process.spawn("node", ["child.js"], {
  detached: true,
  stdio: ["ignore", out, err],
});
child.unref();

// 执行后 out.log 写入了 child.js  里面的 console.log(456)

exec() 和 execFile() 之间的区别

// 首先 exec() 内部调用 execFile() 来实现的, 而 execFile() 内部调用 spawn() 来实现的
// 其次 execFile() 内部默认将 options.shell 设置为了 false, exce() 默认不是 false

Class:ChildProcess

// 通过 child_process.spawn() 等创建,一般不直接用构造函数创建
// 继承了 EventEmitters 所以有 .on() 等方法

各种事件

close

// 当 stdio 流关闭的时候触发, 这个事件跟 exit 不相同, 因为多个进程可以共享同个 stdio 流,参数 code(退出码, 如果子进程是自己退出的话), signal(结束子进程的信号),

exit

// 参数 code signal 如果子进程是自己退出的, 那么code就是退出码, 否则就是null, 如果子进程是通过信号结束的,那么 signal就是结束进程的信号,否则为null,这两者当中, 其中一个肯定不为null
// 注意事项: exit 事件触发, 子进程的 stdio stream 可能打开着, 此外nodejs 监听了 SIGIN 和 SIGTERM信号,也就是说,nodejs接收到这两个信号时候,不会立即退出,而是先做一些清理的工作,然后重新抛出这两个信号.(此时js可以做清理工作,比如关闭数据库等)
// SIGINT interrupt 程序终止信号 通常在用户 chrl + c 的时候发出,用来通知前台进程终止
// SIGTREM terminate 程序结束信号,该信号可以被阻塞和处理,通常用来要求程序自己正常退出
// shell命令kill缺省产生这个信号,如果信号终止不了,我们才会尝试SIGKILL(强制终止)
// >另外,请注意Node.js为SIGINT和SIGTERM建立信号处理程序,Node.js进程不会因为收到这些信号而立即终止。相反,Node.js将执行一系列清理操作,然后重新发出已处理的信号。

error

// 当发生下列事情是,error就会被触发,当error触发时候,exit可能触发,也可能不触发,
// 无法创建子进程
// 进程无法kill
// 向子进程发送信息失败

message

// 当采用 procee.send() 来发送信息时触发.
// 参数 message 为 json对象,或者 primitive value; sendHandle, net.Socket 对象,或者 net.Server对线VG,
// .connected 当调用 .disconnected() 时候,设为false,代表是否能够从子进程接受信息或者对子进程发送消息
// .disconnected() 关闭父进程,子进程之间的IPC通道,当这个方法被调用的时候,.disconnect 事件会被触发,如果子进程是node实例(通过 child_process.fork() 创建)那么在子进程内部也可以主动调用 process.disconnect() 来终止IPC通道,

非重要的备忘点

window 平台上的 cmd 和 bat

// >child_process.exec()和child_proccess.execFile()之间区别的重要性可能因平台而异。在Unix类型的操作系统(Unix、Linux、OSX)上,child_process.execFile()可以更高效,因为它不会生成shell。但是,在Windows上,.bat和.cmd文件在没有终端的情况下无法单独执行,因此无法使用child_process.execFile()启动。在Windows上运行时,可以使用设置了shell选项的child_proccess.spawn()调用.bat和.cmd文件,或者通过生成cmd.exe并将.bat或.cmd文件作为参数传递(这是shell选项和child_process.exe()所做的)。

// my.bat 内容 123
const bat = spawn("cmd.exe", ["/c", "my.bat"]);
bat.stdout.setEncoding("utf-8");
bat.stderr.setEncoding("utf-8");
bat.stdout.on("data", (data) => {
  console.log("data--->", data); // D:\Node-learning\nodejs-learning-guide-master\practice\child_process>123
});
bat.stderr.on("data", (error) => {
  console.log("error--->", error); // 123
});
bat.on("exit", (code) => {
  console.log("child exit width code", code); // child exit width code 0
});

进程标题

// 注意:某些平台(OS X、Linux)将使用argv[0]值作为进程标题,而其他平台(Windows、SunOS)将使用命令。
// 注意:Node.js当前在启动时使用process.execPath覆盖argv[0],因此Node.js子进程中的process.argv[0]将与从父进程传递来派生的argv0参数不匹配,请改用process.argv属性检索它。

代码运行次序的问题

const n = child_process.fork(`${__dirname}/sub.js`);
console.log("1");
n.on("message", (m) => {
  console.log("parent get 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');

// 1
// 2
// 3
// 4
// 5
// parent get message { foo: 'bar' }
// CHILD got message: { hello: 'world' }

const fork = child_process.fork;
console.log("p: 1");
fork("./c2.js");
console.log("p: 2");
const t = 70;
setTimeout(() => {
  console.log("p: 3 in %s", t);
}, t);

// p: 1
// p: 2
// p: 3 in 70
// c: 1

关于 NODE_CHANNEL_FD

// child_process.fork() 时候,如果指定了 eccePath 那么 父子进程间通过 NODE_CHANNEL_FD 进行 通信
// >使用自定义execPath启动的Node.js进程将使用子进程上的环境变量Node_CHANNEL_fd标识的文件描述符(fd)与父进程通信。此fd上的输入和输出应该是以行分隔的JSON对象。

相关文档

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