做为一名合格的程序猿/媛,对于进程、线程仍是有必要了解一点的,本文将从下面几个方向进行梳理,尽可能作到知其然并知其因此然:javascript
用户下达运行程序的命令后,就会产生进程。同一程序可产生多个进程(一对多关系),以容许同时有多位用户运行同一程序,却不会相冲突。html
进程须要一些资源才能完成工做,如CPU使用时间、存储器、文件以及I/O设备,且为依序逐一进行,也就是每一个CPU核心任什么时候间内仅能运行一项进程。前端
进程与线程的区别:进程是计算机管理运行程序的一种方式,一个进程下可包含一个或者多个线程。线程能够理解为子进程。java
摘自wiki百科node
也就是说,进程是咱们运行的程序代码和占用的资源总和,线程是进程的最小执行单位,固然也支持并发。能够说是把问题细化,分红一个个更小的问题,进而得以解决。linux
而且进程内的线程是共享进程资源的,处于同一地址空间,因此切换和通讯相对成本小,而进程能够理解为没有公共的包裹容器
。git
可是若是进程间须要通讯的话,也须要一个公共环境或者一个媒介,这个就是操做系统。github
咱们的计算机有单核的、多核的,也有多种的组合方式:chrome
由于是一个进程,因此某一时刻只能处理一个事务,后续须要等待,体验很差windows
为了解决上面的问题,可是若是有不少请求的话,会产生不少进程,开销自己就是一个不小的问题,而进程占据独立的内存,这么多响应是的进程不免会有重复的状态和数据,会形成资源浪费。
由以前的进程处理事务,改为使用线程处理事务,解决了开销大,资源浪费的问题,还可使用线程池,预先建立就绪线程,减小建立和销毁线程的开销。
可是一个cpu某一时刻只能处理一个事务。像时间分片来调度线程的话,会致使线程切换频繁,是很是耗时的。
相似也就是v8,基于事件驱动,有效的避免了内存开销和上下文切换,只须要线程间通讯,便可在适当的时刻进行事务结果等的反馈。
可是遇到计算量很大的事务,会阻塞后续任务的执行。像这样:
node提供了cluster和child_process两个模块进行进程的建立,也就是咱们常说的主(Master)从(Worker)模式。Master负责任务调度和管理Worker进程,Worker进行事务处理。
node自己提供了cluster和child_process模块建立子进程,本质上cluster.fork()是child_process.fork()的上层实现,cluster带来的好处是能够监听共享端口,不然建议使用child_process。
child_process提供了异步和同步的操做方法,具体可查看文档。
常见的异步方法有:
除了fork出来的进程会长期驻存外,其余方式会在子进程任务完成后以流的方式返回并销毁进程。
异步方法会返回ChildProcess的实例,ChildProcess不能直接建立,只能返回。
来看几张图吧:
有一个很长很长的循环,若是不开启子进程,会等循环以后才能执行以后的逻辑。
咱们能够将耗时的循环放到子进程中,主进程会接受子进程的返回,不影响后续事物的处理。
// 主进程 const execFile = require('child_process').execFile; execFile('./child.js', [], (err, stdout, stderr) => { if (err) { console.log(err); return; } console.log(`stdout: ${stdout}`); }); console.log('用户事务处理');
// 子进程 #!/usr/bin/env node for (let i = 0; i < 10000; i++) { process.stdout.write(`${i}`); }
而对于fork,它是专门用来生产子进程的,也能够说是主进程的拷贝,返回的ChildProcess中会内置额外的通讯通道,也就是IPC通道,容许消息在父子进程间传递,例如经过文件描述符,不过因为建立的是匿名通道,因此只有主进程能够与之通讯,其余进程没法进行通讯。但相对的还有命名通道,详见下一节。
看一个简单的例子:
//parent.js const cp = require('child_process'); const n = cp.fork(`${__dirname}/sub.js`); n.on('message', (m) => { console.log('PARENT got message:', m); }); n.send({ hello: 'world' }); //sub.js process.on('message', (m) => { console.log('CHILD got message:', m); }); process.send({ foo: 'bar' });
父进程经过fork返回的ChildProcess进行通讯的监听和发送,子进程经过全局变量process进行监听和发送。
cluster本质上也是经过child_process.fork建立子进程,他还能帮咱们合理的管理进程。
const cluster = require('cluster'); // 判断是否为主进程 if (cluster.isMaster) { const cpuNum = require('os').cpus().length; for (let i = 0; i < cpuNum; ++i) { cluster.fork(); } cluster.on('online', (worker) => { console.log('Create worker-' + worker.process.pid); }); cluster.on('exit', (worker, code, signal) => { console.log( '[Master] worker ' + worker.process.pid + ' died with code:' + code + ', and' + signal ); cluster.fork(); // 重启子进程 }); } else { const net = require('net'); net.createServer() .on('connection', (socket) => { setTimeout(() => { socket.end('Request handled by worker-' + process.pid); }, 10); }) .listen(8989); }
细心地你可能发现多个子进程监听
了同一个端口,这样不会EADDRIUNS吗?
其实否则,真正监听端口的是主进程,当前端请求到达时,会将句柄发送给某个子进程。
进程间通讯(IPC)大概有这几种:
从技术上划分又能够划分红如下四种:
文件描述符是什么?
在linux中一切皆文件,linux会给每一个文件分配一个id,这个id就是文件描述符,指针也是文件描述符的一种。这个很好理解,不过咱们能够再往深了说,一个进程启动后,会在内核空间(虚拟空间的一部分)建立一个PCB控制块,PCB内部有一个文件描述符表,记录着当前进程全部可用的文件描述符(即当前进程全部打开的文件)。系统出了维护文件描述符表外,还须要维护打开文件表(Open file table)和i-node表(i-node table)。
文件打开表(Open file table)包含文件偏移量,状态标志,i-node表指针等信息
i-node表(i-node table)包括文件类型,文件大小,时间戳,文件锁等信息
文件描述符不是一对一的,它能够:
上面说起了不少能够实现进程间通讯的方式,那node进程间通讯是以什么为基础的呢?
nodeIPC经过管道技术 加 事件循环方式进行通讯,管道技术在windows下由命名管道实现,在*nix系统则由Unix Domain socket实现,提供给咱们的是简单的message事件和send方法。
那管道是什么呢?
管道其实是在内核中开辟一块缓冲区,它有一个读端一个写端,并传给用户程序两个文件描述符,一个指向读端,一个指向写端口,而后该缓存区存储不一样进程间写入的内容,并供不一样进程读取内容,进而达到通讯的目的。
管道又分为匿名管道和命名管道,匿名管道常见于一个进程fork出子进程,只能亲缘进程通讯,而命名管道可让非亲缘进程进行通讯。
其实本质上来讲进程间通讯是利用内核管理一块内存,不一样进程能够读写这块内容,进而能够互相通讯,固然,提及来简单,作起来难。有兴趣的朋友能够自行研究。
能够用cluster创建主从进程架构,主进程调度管理和分发任务给子进程,并在子进程挂掉或断开链接后重启。
pm2是对cluster的一种封装,提供了:
具体原理和细节之后有空再作分析。
文中如有错误的地方,欢迎指出,我会及时更新。但愿读者借鉴的阅读。