Nodejs实践 -- 子进程

node 中, child_process 模块允许我们在自己的Node 程序中执行一些外部程序(如图片处理等)。这里简单介绍下 child_process 提供的四个异步方法(同步的可以去看文档)。

exexFile 

执行外部程序,并且需要提供一组参数,以及一个在进程退出后的缓冲输出的回调。

执行过程:exexFile 异步执行外部程序,把外部程序的输出使用buffer缓存起来,一旦外部程序退出,触发回调并传入相应的输出数据。

spawn 

执行外部程序,并且需要提供一组参数,以及一个在进程退出后的输入输出和事件的数据流接口。

在使用 spawn 时,添加 detached:true属性,可以让创建的子进程拥有和父进程一样的等级,即使父进程终结,子进程也会继续执行。

执行过程:spawn 异步执行外部程序,返回一个ChildProcess 类型的对象,包含了标准的输出和标准错误的可读流。适用于外部程序的输出很多,或者对输出结果进行实时分析的情况。

exec

在一个命令行窗口中执行一个或者多个命令,以及一个在进程退出后缓冲输出的回调。

执行过程:exec 是在一个子shell环境中异步我们写的命令,子shell的输出会存在内部的buffer, 当子shell 退出时会调用回调,并传入对应的输出数据。实际上也是调用 /bin/sh (Linux)或 cmd.exe (Win)来执行命令的。适用于需要一次性执行一组外部应用程序命令的情况。

fork

在一个独立的进程中执行一个node 模块,并且需要一组参数以及一个类似spwan 方法里的数据流和事件式的接口,同时设置好父子进程间的通信。

执行过程:fork 会作为一个分离的进程来执行Node 模块,并在父子进程中创建一个IPC 通信通道,并且子进程共享父进程的I/O。


实践1 --- 执行一个外部程序并且得到相应的输出

如:使用 echo 命令 输出 hello world

const cp = require('child_process');

cp.execFile('echo',['hello','world'], function(err, stdout, stderr){
	if(err) console.error(err);
	console.log('stdout',stdout)
        // 当外部程序的退出返回的状态码非0时,Node 会把返回的状态码作为异常对象输入到stderr
	console.log('stderr',stderr)
})
复制代码



实践2 ---- 使用 spawn 改写 实例1

const cp = require('child_process');

let child = cp.spawn('echo',['hello','world']);
child.on('error', console.error)
child.stdout.pipe(process.stdout)
child.stderr.pipe(process.stderr)复制代码
child  作为一个 ChildProcess 的对象,本身具有流的API,如  child.stdin、child.out、child.stderr


实践3  -----  外部应用程序的串联调用

实现一个这样的场景:

  1. 先使用 cat 去读取一个文件
  2. 再使用 sort 命令对文件内容进行排序
  3. 最后使用 uniq 命令接收 sort 输出的内容,并去除重复的行

我们准备一个这样的文本:

// test.txt
abcdefg
哈哈哈哈哈
gfedcba
871435612
henghengheng
hahahaha
7654321
哈哈哈哈哈
998976777
复制代码

代码

// index.js
const cp = require('child_process');

let cat = cp.spawn('cat',['test.txt']);
let sort = cp.spawn('sort');
let uniq = cp.spawn('uniq');

cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout)复制代码

在同级目录下执行node index.js后,看看输出:

// out
7654321
871435612
998976777
abcdefg
gfedcba
hahahaha
henghengheng
哈哈哈哈哈
复制代码


实践4 ---- 使用 exec 改写 实践3的串联调用

const cp = require('child_process');
cp.exec('cat test.txt | sort | uniq', function(err, stdout, stderr){
	console.log(stdout)
})
复制代码

使用 exec 就像在命令行一样。

exec 的安全性

由于exec 实际上是调用系统命令解析器进行执行的,并且输入的参数可能是用户自己提供的,就有可能被用户注入非法攻击字符。如:

//  req.query.schema = '; rm -rf / ;';
cp.exec('ls '+ req.query.schema);复制代码

上述命令就会删除根目录下所有可读写文件。而execFile 命令在执行的时候,恶意参数将无法成为外部程序的正确参数,从而程序会抛出异常。因此更推荐使用 execFile 命令来执行外部应用。


实践5 --- 分离子进程1 --  不依赖父进程

在某些情况下,我们需要让子进程独立于父进程运行,哪怕父进程崩溃了,子进程仍然在运行。比如:在一个web 的应用程序监控平台,哪怕应用程序崩溃了,这个平台仍然在运行。

示例代码:

// index.js
const cp = require('child_process');

cp.spawn('node',['child.js'], { 
        // 将子进程独立于父进程运行
	detached: true,
        // 让子进程可以继承父进程的终端I/O
	stdio: 'inherit'
})

let timer = setInterval(function(){
	console.log("主程序正在运行")
},500)

// 3秒后父进程以失败退出
setTimeout(function(){
	clearInterval(timer);
	console.error('主进程崩溃')
	process.exit(1)
},3000)
复制代码

// child.js

let timer = setInterval(function(){
	console.log("子程序--正在运行")
},500)

// 设置子进程定时结束 —— 防止一直在运行
setTimeout(function(){
	clearInterval(timer);
	console.error('结束子进程')
	process.exit(1);
},10000)
复制代码

使用 node index.js  可以看到 终端的输出 -- 在父进程结束后,子进程仍在运行,直到自结束。



实践6 --- 分离子进程2 --- 父子进程之间的 I/O 处理

在子进程的实现中,stdio 选项用于配置子进程与父进程之间建立的管道, 即对子进程的 stdinstdout stderr 一个配置。常用值如下:

  • inherit -- 等同于 [process.stdin, process.stdout, process.stderr][0,1,2]。意思是,子进程的 stdio 都重定向到 父进程的 stdio。 因此可以使用同一个终端进行输出。
  • pipe 默认值-- 等同于 ['pipe', 'pipe', 'pipe']  [0,1,2](I/O的文件描述符) 。即子进程对象上相应的 subprocess.stdinsubprocess.stdoutsubprocess.stderr 流。
  • ignore -- 等同于 ['ignore', 'ignore', 'ignore']。即放弃。

除了上述这些参数,还有其他的配置,详情见官方文档

简单的一个示例:把子进程的输出都存放到文件中

const fs = require('fs')
const cp = require('child_process')

// 使用文件描述符,让子进程的输出都重定向到文件中
let outFd = fs.openSync('./child.out', 'a')
let errFd = fs.openSync('./child.err', 'a')

let childProcess = cp.spawn('./child',[], {
	detached: true,
	stdio: ['ignore', outFd, errFd]
})
复制代码


实践7 --- 分离子进程3 --  清除子进程在父进程中的引用

通过实例5和实例6,我们做了两件事:

  1. 独立子进程运行,不依赖于父进程的应用状态
  2. 子进程的I/O输出到文件,中断了父子进程的I/O

但是在父进程中依然保存了一个队子进程的引用,只要子进程没有终结并且引用没有移除,父进程都不会终结。

可以使用 child.unref() 方法,让Node 不要将子进程的引用保存。

示例:

const fs = require('fs')
const cp = require('child_process')

let outFd = fs.openSync('./child.out', 'a')
let errFd = fs.openSync('./child.err', 'a')

let childProcess = cp.spawn('./child',[], {
	detached: true,
	stdio: ['ignore', outFd, errFd]
})

// 移除子进程在父进程中的引用
childProcess.unref();
复制代码



总结一下 如何分离一个子进程:

  1. 第一步,将 detached 设为 true, 让子进程升级为进程组头,不依赖父进程的运行
  2. 第二步,配置 stdio,让父进程和子进程的I/O终端
  3. 第三步,使用child.unref() 清除父进程中对子进程引用的计数,



实践8 --- 使用 fork 独立运行一个可通信的子进程任务

使用 child_process.fork () 会在父子进程间创建一个IPC通道 -- 进程间通信通道(根据操作成系统不同,使用域套接字或命名管道实现),这样就可以通过 process.on('message') child.send() 进行通讯。

示例:

// index.js

const fs = require('fs')
const child = require('child_process').fork('./child.js')


child.on('message', function(msg){
	console.log('子进程消息:'+ msg)
})

child.send('hello')
复制代码

// child.js

process.on('message', function(msg){
	process.send(msg + ' world')
})复制代码

执行后在终端输出:子进程消息:hello world

使用 fork 会打开一个IPC 通道,只要子进程不中断,父进程也会保持活动状态。如果要中断IPC通道,可以在父进程中使用下列代码实现:

child.disconnect()复制代码


实践9  --- 使用 fork 来创建一个工作池

当我们需要处理一些计算任务的时候,很容易想到fork。为了增强代码的复用性,我们想创建一个工作池来运行工作进程。如下代码:

const cp = require('child_process')

function doWork(job, callback){
	// TODO worker 是 工作进程,下一步进行实现
	let child = cp.fork('./worker')
	// 用来跟踪回调的执行状态,防止出现多次回调调用
	let cbTriggered = false;

	child.once('error', function(err){
		if(!cbTriggered){
			callback(err)
			cbTriggered = true;
		}
                // 异常错误时,杀死子进程
		child.kill();
	})
	.once('exit', function(code, signal){
		if(!cbTriggered){
			callback(new Error('Child exited with code: '+ code));
		}
	})
	.once('message', function(result){
		callback(null, result)
		cbTriggered = true;
	})
	.send(job);
}
复制代码

上述的doWork 在每次工作时都会创建一个子进程,但是这也需要代价。Node 官方文档说:

通过 fork 创建的子节点,仍然是一个V8的实例,每一个节点需要耗费约30毫秒的启动时间和10MB的内存。

因此我们需要增加一些约束:

  1. 仅根据机器的CPU来fork 子进程
  2. 确保一个新的任务可以拿到一个可用进程,而不是一个正在使用的进程
  3. 当没有工作进程时,维护一个工作队列,当有工作进程时,再排队处理。
  4. 按需fork 进程

修改后的代码如下:

// pooler.js

const cp = require('child_process')
// 取到cpu 数量
const cpus = require('os').cpus().length;

module.exports = function(workModule){

	// 当没有空闲的cpu时,用来存放任务队列
	let awaiting  = [];
	// 存放准备就绪的工作进程
	let readyPool = [];
	// 存放现有的工作者进程的数量
	let poolSize = 0;

	return function doWork(job, callback){

		// 如果没有空闲的工作进程并且工作进程达到了限制,就把后续任务放入队列等待执行
		if(!readyPool.length && poolSize > cpus){
			return awaiting.push([ doWork, job, callback ])
		}

		// 获取下一个可用的子进程或者fork一个新的进程并增加池子大小
		let child = readyPool.length
			? readyPool.shift()
			: (poolSize++, cp.fork(workModule));

		let cbTriggered = false;

		// 移除子进程上的所有监听,保证每一个子进程每次只有一个监听
		child
			.removeAllListeners()
			.once('error', function(err){
				if(!cbTriggered){
					callback(err)
					cbTriggered = true;
				}
				child.kill();
			})
			.once('exit', function(code, signal){

				if(!cbTriggered){
					callback(new Error('Child exited with code: '+ code));
				}

				poolSize--;
				// 如果子进程由于某种原因退出,保证在readyPool中也被移除了
				let childIdx = readyPool.indexOf(child);
				if(childIdx > -1) {
					readyPool.splice(childIdx, 1);
				}

			})
			.once('message', function(result){
				callback(null, result)

				cbTriggered = true;

				// 把空闲的子进程再次放入readyPool,等待接下来的任务
				readyPool.push(child);
				if(awaiting.length){
					setImmediate.apply(null, awaiting.shift());
				}
			})
			.send(job);
	}



}

复制代码


模拟一下一个密集的计算任务的处理:

// worker1.js

process.on('message', function(job){
	var i;
	for (i=0; i< 100000000;i++);
	process.send('Finished: ' + job +' '+ i);
})复制代码

创建http服务来根据每一次请求来运行任务:

// index.js

const http = require('http')
let makePool = require('./pooler')
let runJob = makePool('./worker1')

http.createServer(function(req,res){
	runJob('some shit job', function(err, data){
		if(err) return res.end('Error:'+ err.message);
		res.end(data)
	})
}).listen(3000)
复制代码

执行 node index.js 可在浏览器中 打来 localhost:3000 查看处理结果





转载于:https://juejin.im/post/5ab3b867f265da237b220133

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值