zoukankan      html  css  js  c++  java
  • node进程间通信

    作为一名合格的程序猿/媛,对于进程、线程还是有必要了解一点的,本文将从下面几个方向进行梳理,尽量做到知其然并知其所以然:

    • 进程和线程的概念和关系
    • 进程演进
    • 进程间通信
    • 理解底层基础,助力上层应用
    • 进程保护

    进程和线程的概念和关系

    用户下达运行程序的命令后,就会产生进程。同一程序可产生多个进程(一对多关系),以允许同时有多位用户运行同一程序,却不会相冲突。

    进程需要一些资源才能完成工作,如CPU使用时间、存储器、文件以及I/O设备,且为依序逐一进行,也就是每个CPU核心任何时间内仅能运行一项进程。

    进程与线程的区别:进程是计算机管理运行程序的一种方式,一个进程下可包含一个或者多个线程。线程可以理解为子进程。

    摘自wiki百科

    也就是说,进程是我们运行的程序代码和占用的资源总和,线程是进程的最小执行单位,当然也支持并发。可以说是把问题细化,分成一个个更小的问题,进而得以解决。

    并且进程内的线程是共享进程资源的,处于同一地址空间,所以切换和通信相对成本小,而进程可以理解为没有公共的包裹容器

    但是如果进程间需要通信的话,也需要一个公共环境或者一个媒介,这个就是操作系统。

    进程演进

    我们的计算机有单核的、多核的,也有多种的组合方式:

    1. 单进程

    因为是一个进程,所以某一时刻只能处理一个事务,后续需要等待,体验不好

    1. 多进程

    为了解决上面的问题,但是如果有很多请求的话,会产生很多进程,开销本身就是一个不小的问题,而进程占据独立的内存,这么多响应是的进程难免会有重复的状态和数据,会造成资源浪费。

    1. 多进程多线程

    由之前的进程处理事务,改成使用线程处理事务,解决了开销大,资源浪费的问题,还可以使用线程池,预先创建就绪线程,减少创建和销毁线程的开销。

    但是一个cpu某一时刻只能处理一个事务。像时间分片来调度线程的话,会导致线程切换频繁,是非常耗时的。

    1. 单进程单线程

    类似也就是v8,基于事件驱动,有效的避免了内存开销和上下文切换,只需要线程间通信,即可在适当的时刻进行事务结果等的反馈。

    但是遇到计算量很大的事务,会阻塞后续任务的执行。像这样:

    1. 单进程单线程(多进程架构)

    node提供了clusterchild_process两个模块进行进程的创建,也就是我们常说的主(Master)从(Worker)模式。Master负责任务调度和管理Worker进程,Worker进行事务处理。

    进程间通信

    node本身提供了cluster和child_process模块创建子进程,本质上cluster.fork()是child_process.fork()的上层实现,cluster带来的好处是可以监听共享端口,否则建议使用child_process。

    child_process

    child_process提供了异步和同步的操作方法,具体可查看文档

    常见的异步方法有:

    1. .exec
    2. .execFile
    3. .fork
    4. .spawn

    除了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

    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)大概有这几种:

    • 匿名管道
    • 命名管道
    • 信号量
    • 消息队列
    • 信号
    • 共享内存
    • 套接字

    从技术上划分又可以划分成以下四种:

    1. 消息传递(管道,FIFO,消息队列)
    2. 同步(互斥量,条件变量,读写锁等)
    3. 共享内存(匿名的,命名的)
    4. 远程过程调用

    文件描述符是什么?

    在linux中一切皆文件,linux会给每个文件分配一个id,这个id就是文件描述符,指针也是文件描述符的一种。这个很好理解,不过我们可以再往深了说,一个进程启动后,会在内核空间(虚拟空间的一部分)创建一个PCB控制块,PCB内部有一个文件描述符表,记录着当前进程所有可用的文件描述符(即当前进程所有打开的文件)。系统出了维护文件描述符表外,还需要维护打开文件表(Open file table)和i-node表(i-node table)。

    文件打开表(Open file table)包含文件偏移量,状态标志,i-node表指针等信息

    i-node表(i-node table)包括文件类型,文件大小,时间戳,文件锁等信息

    文件描述符不是一对一的,它可以:

    1. 同一进程的不同文件描述符指向同一文件
    2. 不同进程可以拥有相同的文件描述符(比如fork出的子进程拥有和父进程一样的文件描述符,或者不同进程打开同一文件)
    3. 不同进程的同一文件描述符也可以指向不同的文件
    4. 不同进程的不同文件描述符也可以指向同一个文件

    上面提及了很多可以实现进程间通信的方式,那node进程间通信是以什么为基础的呢?

    nodeIPC通过管道技术 加 事件循环方式进行通信,管道技术在windows下由命名管道实现,在*nix系统则由Unix Domain socket实现,提供给我们的是简单的message事件和send方法。

    那管道是什么呢?

    管道实际上是在内核中开辟一块缓冲区,它有一个读端一个写端,并传给用户程序两个文件描述符,一个指向读端,一个指向写端口,然后该缓存区存储不同进程间写入的内容,并供不同进程读取内容,进而达到通信的目的。

    管道又分为匿名管道和命名管道,匿名管道常见于一个进程fork出子进程,只能亲缘进程通信,而命名管道可以让非亲缘进程进行通信。

    其实本质上来说进程间通信是利用内核管理一块内存,不同进程可以读写这块内容,进而可以互相通信,当然,说起来简单,做起来难。有兴趣的朋友可以自行研究。

    进程保护

    可以用cluster建立主从进程架构,主进程调度管理和分发任务给子进程,并在子进程挂掉或断开连接后重启。

    pm2是对cluster的一种封装,提供了:

    • 内奸负载均衡
    • 后台运行
    • 停机重载
    • 具有Ubuntu、CentOS的启动脚本
    • 停止不稳定的进程
    • 控制台检测
    • 有好的可视化界面

    具体原理和细节以后有空再做分析。

    文中若有错误的地方,欢迎指出,我会及时更新。希望读者借鉴的阅读。

    部分图片来源网络,侵权立删

    参考链接

    进程、线程、协程

    文件描述符

    IPC

    IPC2

  • 相关阅读:
    POJ3693 Maximum repetition substring —— 后缀数组 重复次数最多的连续重复子串
    SPOJ
    POJ2774 Long Long Message —— 后缀数组 两字符串的最长公共子串
    POJ3261 Milk Patterns —— 后缀数组 出现k次且可重叠的最长子串
    POJ1743 Musical Theme —— 后缀数组 重复出现且不重叠的最长子串
    SPOJ
    AC自动机小结
    HDU3247 Resource Archiver —— AC自动机 + BFS最短路 + 状压DP
    POJ1625 Censored! —— AC自动机 + DP + 大数
    Herding
  • 原文地址:https://www.cnblogs.com/xiaoyuxy/p/12968082.html
Copyright © 2011-2022 走看看