Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Day376:说一下你对进程和线程的了解?Node 中进程间是如何进行通信的? #1210

Open
Genzhen opened this issue Aug 30, 2021 · 0 comments
Labels
Node teach_tag

Comments

@Genzhen
Copy link
Collaborator

Genzhen commented Aug 30, 2021

每日一题会在下午四点在交流群集中讨论,五点小程序中更新答案
欢迎大家在下方发表自己的优质见解

二维码加载失败可点击 小程序二维码

扫描下方二维码,收藏关注,及时获取答案以及详细解析,同时可解锁800+道前端面试题。


一、进程和线程

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

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

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

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

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

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

1.1 进程的演进

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

  • 单进程

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

  • 多进程

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

  • 多进程多线程

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

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

  • 单进程单线程

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

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

img

  • 单进程单线程(多进程架构)

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

img

1.2 进程间的通信

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

  • 1.2.1 child_process

child_process 提供了异步和同步的操作方法

常见的异步方法有:

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

除了 fork 出来的进程会长期驻存外,其他方式会在子进程任务完成后以流的方式返回并销毁进程。

异步方法会返回 ChildProcess 的实例,ChildProcess 不能直接创建,只能返回。

img

img

img

看个例子

有一个很长很长的循环,如果不开启子进程,会等循环之后才能执行之后的逻辑。

我们可以将耗时的循环放到子进程中,主进程会接受子进程的返回,不影响后续事物的处理。

// 主进程
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 进行监听和发送。

  • 1.2.2 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 吗?

其实不然,真正监听端口的是主进程,当前端请求到达时,会将句柄发送给某个子进程。

二、进程间的通信

2.1 进程间通信分类

每个进程都有各自不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一个缓冲区,进程 A 把数据从用户空间拷贝到内核缓冲区,进程 B 再从该缓冲区把数据读走,内核提供的这种机制称为进程间通信。

进程间通信(IPC)大概有这种

  • 匿名通道
    • 管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间调用。进程的亲缘关系通常是指父子进程关系。
  • 命名管道
    • 命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信
  • 信号量
    • 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间同步的手段。
  • 消息队列
    • 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号
    • 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存
    • 共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制,如信号量配合使用,来实现进程间的同步和通信。
  • 套接字
    • 套接子也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。

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

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

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

2.2 Node 进程间通信方式

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

这里提到了管道,那管道是什么?

2.3 什么是管道

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

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

img

其实本质上来说进程间通信是利用内核管理一块内存,不同进程可以读写这块内容,进而可以互相通信。

这里又提到了文件描述符,再来了解下文件描述符

2.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. 不同进程的不同文件描述符也可以指向同一个文件
@Genzhen Genzhen added the Node teach_tag label Aug 30, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Node teach_tag
Projects
None yet
Development

No branches or pull requests

1 participant