Nodejs 第十五章 child_process
child_process 子进程
child_process
模块是 Node.js 的一个核心模块,可以从 Node.js 应用程序中创建和管理子进程。子进程是从父进程(你的 Node.js 应用)中派生出来的新进程,它可以执行系统命令、运行其他应用或执行 JavaScript 文件。使用子进程,可以实现多任务处理,提高应用的性能和效率
什么是子进程
- 子进程是操作系统中由其他进程(父进程)创建的进程
- 使用子进程时需要注意安全问题,特别是当执行的命令包含来自用户输入的部分时,需要防止注入攻击。
- 过多的子进程可能会耗尽系统资源,需要合理管理子进程的数量
使用场景
- 执行系统命令:从 Node.js 应用中执行 Shell 命令时,如
ls
,grep
,mkdir
等。 - 运行其他应用程序:在 Node.js 应用中运行其他外部应用程序,比如 Python 脚本、Ruby 程序或其他可执行文件。
- 并行处理:需要并行执行多个任务以提高应用性能时,可以使用子进程。这在处理 CPU 密集型任务时尤其有用。
- 资源密集型操作:对于一些资源密集型或长时间运行的操作,使用子进程可以避免阻塞 Node.js 的事件循环。
创建子进程
-
Nodejs创建子进程共有
7个
常用的API:有加Sync后缀的是同步API 没加的是异步API
- 并且后面的所有Node的API都是这个规则
- 异步一般会给我们提供一个回调函数,返回一个buffer(Buffer相当于是一个字节的数组,数组中的每一项对应一个字节的大小),可以帮我们执行shell命令,或者跟软件交互
- spawn 执行命令
- exec 执行命令
- execFile 执行可执行文件
- fork 创建node子进程
execSync
执行命令 同步执行execFileSync
执行可执行文件 同步执行spawnSync
执行命令 同步执行
exec 执行命令
- 我们在终端可以输入类似node -v的命令来获取当前node的版本,那在代码中,我们要怎么获取?
-
答案是通过exec执行命令啦,然后会返回对应的内容,我们要用回调函数接收一下
- 回调的有三个参数,意思分别都写在下图的注释中了,需要注意的是输出的是buffer,我们要能看懂,需要使用toString()进行转化
-
std
在stdout
中代表的是 “standard” 的缩写,意为“标准”。因此,stdout
完整的英文是 “standard output”,指的是标准输出流。同理,stdin
表示 “standard input”(标准输入流),而stderr
表示 “standard error”(标准错误流)。这些术语来自 UNIX 和类 UNIX 系统的传统,现在被广泛应用于各种操作系统和编程环境中
-
- 上面是异步的写法,那其实还有同步的写法,会更简单一些,也更常用
const { execSync, spawn, fork } = require('child_process');
const node_version = execSync('node -v').toString()
console.log(node_version);
-
所以执行较小的shell命令,想要立马拿到结果的shell命令,就适合使用execSync去进行。
- exec是有字节上限的,不能超过200kb,否则会报错
- 只要是操作系统里面有的操作命令都可以执行
- 有返回值的话,就可以用变量接收一下,如果没有返回值就直接输出就ok了
const node_version = execSync('node -v').toString()//有返回值用变量接收 execSync('mkdir test')//没返回值就直接使用(创建test文件夹)
execSync案例
-
通过这个命令,其实我们就可以灵活进行运用。
- 在日常使用电脑的时候,作为程序员,我使用的软件是很多的。如果每天重新开机电脑,我都需要一个个打开软件,就会非常麻烦繁琐。那我能不能一次性打开我们想要使用的软件呢?
- 其实是有办法的,通过我们的execSync命令就可以实现
execSync('软件地址')//这样就可以打开我的软件
-
进行功能封装:
const { execSync } = require('child_process');
function openSoftwares(softwares) {
for (const [name, path] of Object.entries(softwares)) {
console.log(`打开了软件: ${name}...`);
execSync(path);
}
}
// 示例用法
const softwareMap = {
"记事本": "notepad.exe", // Windows上的记事本
"计算器": "calc" // Windows上的计算器
// 在这里添加更多的软件和它们的路径
};
openSoftwares(softwareMap);
- 如果再进行前端页面的可视化输入页面,我们就真的实现了一个小工具了,就像下面一样,这其实很简单,但很有成就感
- execSync用的还是比较多的,是个需要着重记忆的API
spawn
- spawnSync 的这个同步方法用得比较少
- spawn 没有字节上限的限制,因为返回的是一个流(buffer),实时返回
[!TIP]
netstat
(network statistics)是一个命令行工具,用于显示网络连接、路由表、接口统计等网络相关信息、帮助用户监控和问题定位网络连接在这种情况下,exec命令会一直卡在这个输出网络连接信息,直到输出完才会停止。而spawn则是有当下有多少就返回多少(实时返回,返回流形态)
- 通过上图用execSync进行输出,能看到内容其实一直没有加载出来,所以execSync这个命令就不适合执行这种实时获取类型的任务
const { execSync, spawn, fork} = require('child_process');
const {stdout} = spawn('netstat')
stdout.on('data',(msg)=>{
console.log(msg.toString());
})
stdout.on('close',(msg)=>{
console.log("结束了");
})
- 而通过spawn就能够实时获取该部分信息,同时需要注意的是,我们是从spawn中解构出stdout这个方法,这个单词的意思在前面也有说过,是标准流输出的意思,所以输出的buffer流要变成我们能看懂的形式,就要记得加上**toString()**进行转化
- spawn自带的就有close事件的,在结束的时候会抛出close事件监听,并返回状态码,通过状态码可以知道子进程是否顺利执行。exec是没有close事件的,只能通过返回的buffer去识别完成状态,识别起来较为麻烦
spawn和exec不同的传参方式
spawn
除了第一个参数是直接传入要执行的命令,还有第二个参数,第二参数是一个数组,用来执行我们额外的命令。这些参数是明确地分开传递的,有助于防止某些类型的注入攻击,并且让参数处理更加清晰
const { spawn } = require('child_process');
//'ls' 是命令,['-lh', '/usr'] 是一个参数数组,它们将被传递给新的进程
const child = spawn('ls', ['-lh', '/usr']); // 命令: ls, 参数: ['-lh', '/usr']
exec
的传参方式与spawn
不同,它通过单个字符串接收命令和参数。也就是前面说的手动将命令和参数拼接成一个字符串,在命令后面跟着继续写字符串- exec输出方式在前面已经有详细讲解,这里不在扩展
spawn和exec相同配置项
- 里面都有一个options,该对象是用来配置子进程的各种行为,总结在下面,有需求直接用就行了
配置项 | 类型 | 默认值 | 描述 |
---|---|---|---|
cwd | string | undefined | 子进程的当前工作目录。如果未指定,则继承父进程的当前目录。 |
env | Object | process.env | 为子进程指定环境变量。默认为父进程的环境变量。 |
shell | boolean 或 string | false | 如果为 true ,则在一个shell中运行命令。可以指定要使用的 shell。 |
stdio | string 或 Array | 'pipe' | 配置子进程的标准输入、标准输出和标准错误流。可以是 'pipe' (管道到父进程)、'ignore' (忽略输出)、'inherit' (继承自父进程)或数组(指定为每个流)。 |
timeout | number | 0 | 子进程的最长执行时间,单位为毫秒。超时后将会杀死子进程。0 表示无超时限制。 |
uid | number | undefined | 设置子进程的用户标识(UID),仅在 POSIX 平台上有效。 |
gid | number | undefined | 设置子进程的组标识(GID),仅在 POSIX 平台上有效。 |
detached | boolean | false | 如果为 true ,则使子进程在父进程退出后继续运行。 |
windowsHide | boolean | false | 在 Windows 平台上,如果设置为 true ,可以隐藏子进程的窗口。 |
execFile
- execFile 适合执行可执行文件,例如执行一个node脚本,或者shell文件(后缀.sh的文件,这是Mac的方式),windows可以编写cmd脚本,posix,可以编写sh脚本
- 这个方法也有对应的Sync同步方法,但还是异步方法使用的更多一点
execFile用法
const { execFile } = require('child_process');
execFile(file, [args], [options], callback);
//参数对应的意思
file:要运行的可执行文件的路径。
args(可选):一个数组,包含所有传递给可执行文件的命令行参数。
options(可选):一个可选的对象,用来配置执行环境及其它选项。
callback(可选):一个函数,当子进程停止时被调用,该函数接收参数 (error, stdout, stderr)。
参数详解
-
file:这是我们希望执行的可执行文件。例如,在 Windows 系统上可能是一个
.exe
文件,在 Unix-like 系统上可能是任何可执行的二进制文件。 -
args:一个字符串数组,包含将传递给执行文件的参数。例如
['-l', '-a']
。 -
options
:该参数有多种选项可以设置,例如:
cwd
:子进程的当前工作目录。env
:环境变量键值对。encoding
:输出的编码(默认为 'buffer')。timeout
:超时时间,超时后将会杀死子进程。maxBuffer
:stdout 或 stderr 上允许的最大数据量(以字节为单位),默认为1024 * 1024
。killSignal
:用于杀死子进程的信号(默认为 'SIGTERM')。uid
和gid
:设置进程的用户标识和群组标识。
-
callback:一个回调函数,它有三个参数:
error
、stdout
和stderr
。error
是如果进程遇到错误或退出码非 0 则会被设置。
execFile案例
- 使用命令mkdir创建一个文件test.js,cd进入目录,echo输出一段内容在控制台,最后执行
# 输出"开始",标志脚本执行的开始
echo '开始'
# 创建一个名为test的新目录
mkdir test
# 改变当前工作目录到新创建的test目录
cd ./test
# 创建一个名为test.js的新js文件,并写入一行代码(通过>符号实现写入):console.log("xiaoyu666")
echo console.log("xiaoyu666") >test.js
# 控制台输出"结束",标志脚本执行的结束
echo '结束'
# 使用Node.js执行test.js文件,输出其结果
node test.js
- 然后我们在使用execFile来执行这段cmd
const path = require('path')
const {execFile} = require('child_process');
//process.cwd作用和__dirname差不多,用哪个都是一样的,前面章节有说
execFile(path.resolve(process.cwd(),'./bat.cmd'),null,(err,stdout)=>{
console.log(stdout.toString())
})
底层实现顺序
- exec -> execFile -> spawn(左边基于右边实现)
fork
child_process
模块的 fork
方法是一个特别的功能,它用于在新的 Node.js 进程中运行模块文件,是 spawn
函数的一个变体。fork
主要用于创建新的 Node.js 进程,并允许父子进程之间有一个通信通道,可以通过这个通道相互发送消息
主要特性
- 创建 Node.js 子进程:
fork
直接运行 Node.js 模块(即.js
文件),不需要像spawn
或exec
那样指定 Node.js 可执行文件。 - IPC 通信:
fork
启动的进程之间会自动建立一个 IPC(进程间通信)通道,允许父进程和子进程通过消息传递进行通信。 - 隔离的 V8 实例:每个通过
fork
创建的进程都有自己的 V8 实例和独立的事件循环。
fork用法
const { fork } = require('child_process');
const child = fork(modulePath, [args], [options]);
//modulePath:要在子进程中运行的模块文件的路径。
//args(可选):一个字符串数组,包含传递给模块的参数。
//options(可选):一个配置对象,可以设置各种选项,如环境变量、工作目录等。
参数详解
-
modulePath:一个字符串,指向要运行的 Node.js 模块。
-
args:运行模块时要传递的参数数组。
-
options
:可以包括如下几个选项:
cwd
:子进程的当前工作目录。env
:环境变量键值对。execPath
:用于创建子进程的可执行文件路径。execArgv
:传递给可执行文件的字符串参数数组(通常用于传递 V8 选项)。silent
:如果设置为true
,子进程中的stdin
、stdout
和stderr
会被重定向到父进程。
事件和通信
- 父进程和子进程可以通过
send()
方法和message
事件来发送和接收消息。对此,我们可以做一个案例来实现一个- 但需要注意的是,只能接受js模块
- 在需要多核处理能力的应用中,可以使用
fork
来利用多核CPU。 - 当需要子进程处理一些耗时的计算时,使用
fork
可以避免阻塞主事件循环。 - 在进行大量的异步 I/O 操作时,可以通过多个子进程来分散处理压力。
//index.js文件(主线程)
const { fork } = require("child_process")
//和需要沟通的js文件建立联系
const indexProcess = fork('./xiaoyu01.js')
//发送消息
indexProcess.send('我是index')
//xiaoyu01.js文件(子线程)
process.on('message', (msg) => {
console.log("我是xiaoyu01,接收到了消息:", msg);
})
fork底层结构图
- 下图是Node.js 中子进程如何通过 IPC(进程间通信)进行通信的基本结构
- 主进程和子进程:图表的顶部和底部分别表示主进程和子进程,它们需要相互通信。
- IPC:位于图表中心,代表进程间通信机制,它是主进程与子进程或不同子进程之间交换数据的方式。
- libuv:位于 IPC 下方,它是一个跨平台的异步I/O库,Node.js 用它来抽象不同操作系统之间的差异,包括提供 IPC 的实现。
- Windows 和 POSIX:图表下方的两个分支代表了不同的操作系统平台。libuv 针对每个平台使用不同的技术来实现 IPC。
- 在 Windows 平台上,IPC 是通过“命名管道”(named pipe)实现的。
- 在 POSIX 兼容的系统上(如 Linux 和 macOS),IPC 是通过“Unix 域套接字”(unix domain socket)实现的。
转载自:https://juejin.cn/post/7356867483829600265