深入解析NodeJS中的进程管理

深入解析NodeJS中的进程管理

熟悉 js 的朋友都知道,js 是单线程的,在 Node 中,采用的是 多进程单线程 的模型。由于javascript单线程的限制,在多核服务器上,我们往往需要启动多个进程才能最大化服务器性能。

Node.js 进程集群可用于运行多个 Node.js 实例,这些实例可以在其应用程序线程之间分配工作负载。 当不需要进程隔离时,请改用 worker_threads 模块,它允许在单个 Node.js 实例中运行多个应用程序线程。

零、NodeJS多进程

进程总数,其中一个主进程,cpu 个数 x cpu 核数 个 子进程无论 child_process 还是 cluster,都不是多线程模型,而是多进程模型应对单线程问题,通常使用多进程的方式来模拟多线程

一、核心模块cluster集群

Node 在 V0.8 版本之后引入了 cluster模块,通过worker_threads

集群模块可以轻松创建共享服务器端口的子进程。

cluster 底层是 child_process 模块,除了可以发送普通消息,还可以发送底层对象 TCP、UDP 等, cluster 模块是 child_process 模块和 net 模块的组合应用。cluster 启动时,内部会启动 TCP 服务器,将这个 TCP 服务器端 socket 的文件描述符发给工作进程。

一个主进程 (master) 管理多个子进程 (worker) 的方式实现集群 模块应用中,cluster,其运作模式没有 一个主进程只能管理一组工作进程 模块那么灵活,但是更加稳定:

1.png

1.cluster配置详情

1.1 引入cluster

const cluster = require('cluster')复

1.2 cluster常用属性

child_process 标识主进程, Node<16.isMaster 标识主进程, Node>16.isPrimary 标识子进程.isWorker 对当前工作进程对象的引用【子进程中】.worker 存储活动工作进程对象的哈希,以 .workers 字段为键。 这样可以很容易地遍历所有工作进程。 它仅在主进程中可用。id【主进程中】cluster.wokers[id] === worker 只读, cluster配置项。在调用 .setupPrimary()或.fork()方法之后,此设置对象将包含设置,包括默认值。之前为空对象。此对象不应手动更改或设置。

.settings配置项详情:

- `execArgv` 传给 Node.js 可执行文件的字符串参数列表。 **默认值:**  `process.execArgv`。- `exec`  工作进程文件的文件路径。 **默认值:** `process.argv[1]`。- `args`  传给工作进程的字符串参数。 **默认值:**`process.argv.slice(2)`。- `cwd` 工作进程的当前工作目录。 **默认值:**  `undefined` (从父进程继承)。- `serialization` 指定用于在进程之间发送消息的序列化类型。 可能的值为 `'json'` 和 `'advanced'`。  **默认值:**  `false`。- `silent` 是否将输出发送到父进程的标准输入输出。 **默认值:**  `false`。- `stdio` 配置衍生进程的标准输入输出。 由于集群模块依赖 IPC 来运行,因此此配置必须包含 `'ipc'` 条目。 提供此选项时,它会覆盖 `silent`。- `uid` 设置进程的用户标识。 - `gid` 设置进程的群组标识。- `inspectPort`  |  设置工作进程的检查器端口。 这可以是数字,也可以是不带参数并返回数字的函数。 默认情况下,每个工作进程都有自己的端口,从主进程的 `process.debugPort` 开始递增。- `windowsHide`  隐藏通常在 Windows 系统上创建的衍生进程控制台窗口。 **默认值:**  `false`。

1.3 cluster常用方法

cluster.settings 衍生新的工作进程【主进程中】.fork([env]) Node>16.setupPrimary([settings]) 用于更改默认的 ‘fork’ 行为,用后设置将出现在 .setupMaster([settings]) 中。任何设置更改只会影响未来对 cluster.settings的调用,而不会影响已经运行的工作进程。上述默认值仅适用于第一次调用。Node 小于 16【主进程中】.fork() 当所有工作进程断开连接并关闭句柄时调用【主进程中】

1.4 cluster常用事件

为了让集群更加稳定和健壮,.disconnect([callback]) 模块也暴露了许多事件:

cluster 事件, 当集群主进程接收到来自任何工作进程的消息时触发。'message' 事件, 当任何工作进程死亡时,则集群模块将触发 'exit' 事件。

cluster.on('exit', (worker, code, signal) => {  console.log('worker %d died (%s). restarting...',              worker.process.pid, signal || code);  cluster.fork();});

'exit'事件,从工作进程调用 'listening' 后,当服务器上触发 listen() 事件时,则主进程中的 'listening' 也将触发 cluster 事件。

cluster.on('listening', (worker, address) => {  console.log(    `A worker is now connected to ${address.address}:${address.port}`);});

'listening' 事件,当新的工作进程被衍生时,则集群模块将触发 'fork' 事件。

cluster.on('fork', (worker) => {  timeouts[worker.id] = setTimeout(errorMsg, 2000);});

'fork' 事件,每次调用 'setup'时触发。.setupPrimary()事件,在工作进程 IPC 通道断开连接后触发。 当工作进程正常退出、被杀死、或手动断开连接时

cluster.on('disconnect', (worker) => {  console.log(`The worker #${worker.id} has disconnected`);});

1.5 Worker类

disconnect 对象包含了工作进程的所有公共的信息和方法。 在主进程中,可以使用 Worker 来获取它。 在工作进程中,可以使用 cluster.workers 来获取它。

1.5.1 worker常用属性

cluster.worker 工作进程标识,每个新的工作进程都被赋予了自己唯一的 id,此 id 存储在 .id。当工作进程存活时,这是在 id 中索引它的键。cluster.workers 所有工作进程都是使用 .process 创建,此函数返回的对象存储为 child_process.fork()。 在工作进程中,存储了全局的 .process

1.5.2 worker常用方法

process 向工作进程或主进程发送消息,可选择使用句柄。在主进程中,这会向特定的工作进程发送消息。 它与 .send(message[, sendHandle[, options]][, callback])相同。在工作进程中,这会向主进程发送消息。 它与 ChildProcess.send() 相同。process.send().destroy()此函数会杀死工作进程。.kill([signal]) 函数在不等待正常断开连接的情况下杀死工作进程,它与 kill() 具有相同的行为。为了向后兼容,此方法别名为 worker.process.kill()worker.destroy()发送给工作进程,使其调用自身的 .disconnect([callback])将关闭所有服务器,等待那些服务器上的 .disconnect() 事件,然后断开 IPC 通道。'close' 如果工作进程通过其 IPC 通道连接到其主进程,则此函数返回 .isConnect(),否则返回 true。 工作进程在创建后连接到其主进程。false如果工作进程已终止(由于退出或收到信号),则此函数返回 .isDead()。 否则,它返回 true

1.5.3 worker常用事件

为了让集群更加稳定和健壮,false 模块也暴露了许多事件:

cluster 事件, 在工作进程中。

cluster.workers[id].on('message', messageHandler);

'message' 事件, 当任何工作进程死亡时,则'exit'对象将触发 当前worker工作进程 事件。

if (cluster.isPrimary) {  const worker = cluster.fork();  worker.on('exit', (code, signal) => {    if (signal) {      console.log(`worker was killed by signal: ${signal}`);    } else if (code !== 0) {      console.log(`worker exited with error code: ${code}`);    } else {      console.log('worker success!');    }  });}

'exit'事件,从工作进程调用 'listening' ,对当前工作进程进行监听。

cluster.fork().on('listening', (address) => {  // 工作进程正在监听});

listen()事件,在工作进程 IPC 通道断开连接后触发。 当工作进程正常退出、被杀死、或手动断开连接时

cluster.fork().on('disconnect', () => {  //限定于当前worker对象触发});

2. 进程通信

Node中主进程和子进程之间通过进程间通信 (IPC) 实现进程间的通信,进程间通过 disconnect(a.send表示向a发送)方法发送消息,监听 .send() 事件收取信息,这是 message 通过集成 cluster模块 实现的。还是一个简单的官网的进程间通信例子

子进程:EventEmitterprocess.on('message')父进程:process.send()child.on('message')

# cluster.isMaster# cluster.fork()# cluster.workers# cluster.workers[id].on('message', messageHandler);# cluster.workers[id].send();# process.on('message', messageHandler); # process.send();const cluster = require('cluster');const http = require('http');# 主进程if (cluster.isMaster) {  // Keep track of http requests  console.log(`Primary ${process.pid} is running`);  let numReqs = 0;    // Count requests  function messageHandler(msg) {    if (msg.cmd && msg.cmd === 'notifyRequest') {      numReqs += 1;    }  }  // Start workers and listen for messages containing notifyRequest  // 开启多进程(cpu核心数)  // 衍生工作进程。  const numCPUs = require('os').cpus().length;  for (let i = 0; i  {    console.log(`worker ${worker.process.pid} died`);  });} else {  # 子进程  // 工作进程可以共享任何 TCP 连接  // 在本示例中,其是 HTTP 服务器  // Worker processes have a http server.  http.Server((req, res) => {    res.writeHead(200);    res.end('hello worldn');    //****** !!!!Notify master about the request !!!!!!*******    //****** 向process发送    process.send({ cmd: 'notifyRequest' });         //****** 监听从process来的    process.on('message', function(message) {         // xxxxxxx    })  }).listen(8000);  console.log(`Worker ${process.pid} started`);}

2.png

2.1 句柄发送与还原

NodeJS 进程之间通信只有消息传递,不会真正的传递对象。

child.send() 方法在发送消息前,会将消息组装成 handle 和 message,这个 message 会经过 send() 序列化,也就是说,传递句柄的时候,不会将整个对象传递过去,在 IPC 通道传输的都是字符串,传输后通过 JSON.stringify 还原成对象。

2.2 监听共同端口

代码里有 JSON.parse 在进行 fork 时,为什么多个进程可以监听同一个端口呢?

原因是主进程通过 send() 方法向多个子进程发送属于该主进程的一个服务对象的句柄,所以对于每一个子进程而言,它们在还原句柄之后,得到的服务对象是一样的,当网络请求向服务端发起时,进程服务是抢占式的,所以监听相同端口时不会引起异常。

看下端口被占用的情况:

# master.jsconst fork = require('child_process').fork;const cpus = require('os').cpus();for (let i=0; i<cpus.length; i++) {    const worker = fork('worker.js');    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);}
# worker.jsconst http = require('http');http.createServer((req, res) => {res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);}).listen(3000);

以上代码示例,控制台执行 app.listen(port) 只有一个 worker 可以监听到 3000 端口,其余将会抛出 node master.js 错误。

那么多进程模式下怎么实现多进程端口监听呢?答案还是有的,通过句柄传递 Node.js v0.5.9 版本之后支持进程间可Error: listen EADDRINUSE :::3000功能

/** * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback * message * sendHandle */subprocess.send(message, sendHandle)

当父子进程之间建立 IPC 通道之后,通过子进程对象的 send 方法发送消息,第发送句柄,为了解决上面多进程端口占用问题,我们将主进程的 socket 传递到子进程。

# master.jsconst fork = require('child_process').fork;const cpus = require('os').cpus();const server = require('net').createServer();server.listen(3000);process.title = 'node-master'for (let i=0; i<cpus.length; i++) {    const worker = fork('worker.js');        # 句柄传递    worker.send('server', server);    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);}
// worker.jslet worker;process.title = 'node-worker'process.on('message', function (message, sendHandle) {  if (message === 'server') {    worker = sendHandle;    worker.on('connection', function (socket) {      console.log('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid)    });  }});

验证一番,控制台执行 二个参数 sendHandle 就是句柄,可以是 TCP套接字、TCP服务器、UDP套接字等 

3.png

4.png

5.png

2.3 进程负载均衡

了解 node master.js 的话会知道,子进程是通过 cluster 创建的。在 linux 中,系统原生提供了 cluster.fork() 方法,那么为什么 Node 选择自己实现 fork ,而不是直接使用系统原生的方法?主要的原因是以下两点:

fork的进程监听同一端口会导致端口占用错误

fork的进程之间没有负载均衡,容易导致惊群现象

在 cluster模块 中,针对第一个问题,通过判断当前进程是否为 cluster模块,若是,则监听端口,若不是则表示为 fork 的 master进程,不监听端口。

针对第二个问题,worker进程 内置了负载均衡功能, cluster模块 负责监听端口接收请求,然后通过调度算法(默认为 Round-Robin,可以通过环境变量 master进程 修改调度算法)分配给对应的 NODE_CLUSTER_SCHED_POLICY

3. 异常捕获

3.1 未捕获异常

当代码抛出了异常没有被捕获到时,进程将会退出,此时 Node.js 提供了 worker进程 接口来捕获它,但是当一个 Worker 进程遇到未捕获的异常时,它已经处于一个不确定状态,此时我们应该让这个进程优雅退出:

关闭异常 Worker 进程所有的 TCP Server(将已有的连接快速断开,且不再接收新的连接),断开和 Master 的 IPC 通道,不再接受新的用户请求。Master 立刻 fork 一个新的 Worker 进程,保证在线的『工人』总数不变。异常 Worker 等待一段时间,处理完已经接受的请求后退出。

+---------+                 +---------+|  Worker |                 |  Master |+---------+                 +----+----+     | uncaughtException         |     +------------+              |     |            |              |                   +---------+     |  + ---------------------> |     |         wait...           |                        |     |          exit             |                        |     +-------------------------> |                        |     |                           |                        |    die                          |                        |                                 |                        |                                 |                        |

3.2 OOM、系统异常

当一个进程出现异常导致 crash 或者 OOM 被系统杀死时,不像未捕获异常发生时我们还有机会让进程继续执行,只能够让当前进程直接退出,Master 立刻 fork 一个新的 Worker。

二、子进程

1. child_process模块

child_process 模块提供了衍生子进程的能力, 简单来说就是process.on('uncaughtException', handler)。默认情况下, 执行cmd命令的能力。 这些管道具有有限的(且平台特定的)容量。 如果子进程写入 stdout 时超出该限制且没有捕获输出,则子进程会阻塞并等待管道缓冲区接受更多的数据。 这与 shell 中的管道的行为相同。 如果不消费输出,则使用 { stdio: ‘ignore’ } 选项。

1.1 引入child_process

const cp = require('child_process');

1.2 基本概念

通过 API 创建出来的子进程和父进程没有任何必然联系

4个异步方法,创建子进程:fork、exec、execFile、spawn

Node

stdin、 stdout 和 stderr 的管道会在父 Node.js 进程和衍生的子进程之间建立:想将一个 Node 进程作为一个独立的进程来运行的时候使用,使得计算处理和文件描述器脱离 Node 主进程(复制一个子进程)

非 Node

fork(modulePath, args):处理一些会有很多子进程 I/O 时、进程会有大量输出时使用spawn(command, args):只需执行一个外部程序的时候使用,执行速度快,处理用户输入相对安全execFile(file, args[, callback]):想直接访问线程的 shell 命令时使用,一定要注意用户输入

3个同步方法:exec(command, options)execSyncexecFileSync

6.png

其他三种方法都是 spawnSync 的延伸。

1.2.1 fork(modulePath, args)函数, 复制进程

fork 方法会开放一个 IPC 通道,不同的 Node 进程进行消息传送一个子进程消耗 30ms 启动时间和 10MB 内存

记住,衍生的 Node.js 子进程独立于父进程,但两者之间建立的 IPC 通信通道除外。 每个进程都有自己的内存,带有自己的 V8 实例

举个?

在一个目录下新建 worker.js 和 master.js 两个文件:

# child.jsconst t = JSON.parse(process.argv[2]);console.error(`子进程 t=${JSON.stringify(t)}`);process.send({hello:`儿子pid=${process.pid} 给爸爸进程pid=${process.ppid} 请安`});process.on('message', (msg)=>{    console.error(`子进程 msg=${JSON.stringify(msg)}`);});
# parent.jsconst {fork} = require('child_process');for(let i = 0; i  {        console.log(`messsgae from child msg=${JSON.stringify(msg)}`, );    });    p.send({hello:`来自爸爸${process.pid} 进程id=${i}的问候`});}

7.png

通过 spawn() 启动 parent.js,然后通过 node parent.js 查看进程的数量,我们可以发现,理想状况下,进程的数量等于 CPU 的核心数,每个进程各自利用一个 CPU 核心。

这是经典的 Master-Worker 模式(主从模式)

8.png

实际上,fork 进程是昂贵的,复制进程的目的是充分利用 CPU 资源,所以 NodeJS 在单线程上使用了事件驱动的方式来解决高并发的问题。

适用场景
一般用于比较耗时的场景,并且用node去实现的,比如下载文件;
fork可以实现多线程下载:将文件分成多块,然后每个进程下载一部分,最后拼起来;

1.2.2 execFile(file, args[, callback])

会把输出结果缓存好,通过回调返回最后结果或者异常信息

const cp = require('child_process');// 第一个参数,要运行的可执行文件的名称或路径。这里是echocp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => {  if (err) { console.error(err); }  console.log('stdout: ', stdout);  console.log('stderr: ', stderr);});

适用场景
比较适合开销小的任务,更关注结果,比如ls等;

1.2.3 exec(command, options)

主要用来执行一个shell方法,其内部还是调用了spawn ,不过他有最大缓存限制。

只有一个字符串命令和 shell 一模一样

const cp = require('child_process');cp.exec(`cat ${__dirname}/messy.txt | sort | uniq`, (err, stdout, stderr) => {  console.log(stdout);});

适用场景
比较适合开销小的任务,更关注结果,比如ls等;

1.2.4 spawn(command, args)

通过流可以使用有大量数据输出的外部应用,节约内存使用流提高数据响应效率spawn 方法返回一个 I/O 的流接口

单一任务

const cp = require('child_process');const child = cp.spawn('echo', ['hello', 'world']);child.on('error', console.error);# 输出是流,输出到主进程stdout,控制台child.stdout.pipe(process.stdout);child.stderr.pipe(process.stderr);

多任务串联

const cp = require('child_process');const path = require('path');const cat = cp.spawn('cat', [path.resolve(__dirname, 'messy.txt')]);const sort = cp.spawn('sort');const uniq = cp.spawn('uniq');# 输出是流cat.stdout.pipe(sort.stdin);sort.stdout.pipe(uniq.stdin);uniq.stdout.pipe(process.stdout);

适用场景
spawn是流式的,所以适合耗时任务,比如执行npm install,打印install的过程

1.3 各种事件

1.3.1 close

在进程已结束并且子进程的标准输入输出流(sdtio)已关闭之后,则触发 ps aux | grep worker.js 事件。这个事件跟'close'不同,因为多个进程可以共享同个stdio流。

参数:

钉钉 AI 助理 钉钉 AI 助理

钉钉AI助理汇集了钉钉AI产品能力,帮助企业迈入智能新时代。

钉钉 AI 助理 21 查看详情 钉钉 AI 助理 code(退出码,如果子进程是自己退出的话)signal(结束子进程的信号)

问题:code一定是有的吗?
(从对code的注解来看好像不是)比如用exit杀死子进程,那么,code是?

1.3.2 exit

参数:
code、signal,如果子进程是自己退出的,那么kill就是退出码,否则为null;
如果子进程是通过信号结束的,那么,code就是结束进程的信号,否则为null。
这两者中,一者肯定不为null。

注意事项
signal事件触发时,子进程的stdio stream可能还打开着。(场景?)此外,nodejs监听了SIGINT和SIGTERM信号,也就是说,nodejs收到这两个信号时,不会立刻退出,而是先做一些清理的工作,然后重新抛出这两个信号。(目测此时js可以做清理工作了,比如关闭数据库等。)

exit:interrupt,程序终止信号,通常在用户按下CTRL+C时发出,用来通知前台进程终止进程。
SIGINT:terminate,程序结束信号,该信号可以被阻塞和处理,通常用来要求程序自己正常退出。shell命令kill缺省产生这个信号。如果信号终止不了,我们才会尝试SIGKILL(强制终止)。

1.3.3 error

当发生下列事情时,error就会被触发。当error触发时,exit可能触发,也可能不触发。(内心是崩溃的)

无法衍生该进程。进程无法kill。向子进程发送消息失败。

1.3.4 message

当采用SIGTERM来发送消息时触发。

参数
process.send(),为json对象,或者primitive value;message,net.Socket对象,或者net.Server对象(熟悉cluster的同学应该对这个不陌生)

1.4 方法

.connected:当调用sendHandle时,设为false。代表是否能够从子进程接收消息,或者对子进程发送消息。

.disconnect() :关闭父进程、子进程之间的IPC通道。当这个方法被调用时,.disconnected()事件就会触发。如果子进程是node实例(通过child_process.fork()创建),那么在子进程内部也可以主动调用disconnect来终止IPC通道。

三、NodeJS多线程

应对单线程问题,通常使用多进程的方式来模拟多线程

1. 单线程问题

对 cpu 利用不足某个未捕获的异常可能会导致整个程序的退出

2. Node 线程

Node 进程占用了 7 个线程

Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的

主线程:编译、执行代码编译/优化线程:在主线程执行的时候,可以优化代码分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据垃圾回收的几个线程

JavaScript 的执行是process.disconnect()的,但 Javascript 的宿主环境,无论是 Node 还是浏览器都是多线程的。

Javascript 为什么是单线程?
这个问题需要从浏览器说起,在浏览器环境中对于 DOM 的操作,试想如果多个线程来对同一个 DOM 操作是不是就乱了呢,那也就意味着对于DOM的操作只能是单线程,避免 DOM 渲染冲突。在浏览器环境中 UI 渲染线程和 JS 执行引擎是互斥的,一方在执行时都会导致另一方被挂起,这是由 JS 引擎所决定的。

3. 异步 IO

Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集计算(Zlib,Crypto)会启用 Node 的线程池线程池默认大小为 4,可以手动更改线程池默认大小

process.env.UV_THREADPOOL_SIZE = 64

4. 真 Node 多线程

4.1 worker_threads核心模块

Node 10.5.0 的发布,给出了一个实验性质的模块 单线程 给 Node 提供真正的多线程能力worker_thread 模块中有 4 个对象和 2 个类isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。MessagePort: 用于线程之间的通信,继承自 EventEmitter。MessageChannel: 用于创建异步、双向通信的通道实例。threadId: 线程 ID。Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 nullworkerData: 用于在主进程中向子进程传递数据(data 副本)

const {  isMainThread,  parentPort,  workerData,  threadId,  MessageChannel,  MessagePort,  Worker} = require('worker_threads');function mainThread() {  for (let i = 0; i  { console.log(`main: worker stopped with exit code ${code}`); });    worker.on('message', msg => {      console.log(`main: receive ${msg}`);      worker.postMessage(msg + 1);    });  }}function workerThread() {  console.log(`worker: workerDate ${workerData}`);  parentPort.on('message', msg => {    console.log(`worker: receive ${msg}`);  }),  parentPort.postMessage(workerData);}if (isMainThread) {  mainThread();} else {  workerThread();}

4.2 线程通信

const assert = require('assert');const {  Worker,  MessageChannel,  MessagePort,  isMainThread,  parentPort} = require('worker_threads');if (isMainThread) {  const worker = new Worker(__filename);  const subChannel = new MessageChannel();  worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);  subChannel.port2.on('message', (value) => {    console.log('received:', value);  });} else {  parentPort.once('message', (value) => {    assert(value.hereIsYourPort instanceof MessagePort);    value.hereIsYourPort.postMessage('the worker is sending this');    value.hereIsYourPort.close();  });}

四、 多进程 vs 多线程

进程是资源分配的最小单位,线程是CPU调度的最小单位

五、 知识拓展

1. IPC

IPC (Inter-process communication) 即worker_threads,由于每个进程创建之后都有自己的独立地址空间,实现 IPC 的目的就是为了进程之间资源共享访问。

实现 IPC 的方式有多种:管道、消息队列、信号量、Domain Socket,Node.js 通过 pipe 来实现。

9.png

实际上,父进程会在创建子进程之前,会先创建 IPC 通道并监听这个 IPC,然后再创建子进程,通过环境变量(NODE_CHANNEL_FD)告诉子进程和 IPC 通道相关的文件描述符,子进程启动的时候根据文件描述符连接 IPC 通道,从而和父进程建立连接。

10.png

2. 句柄传递

句柄是一种可以用来标识资源的引用的,它的内部包含了指向对象的文件资源描述符。

一般情况下,当我们想要将多个进程监听到一个端口下,可能会考虑使用主进程代理的方式处理:

11.png

然而,这种代理方案会导致每次请求的接收和代理转发用掉两个文件描述符,而系统的文件描述符是有限的,这种方式会影响系统的扩展能力。

所以,为什么要使用句柄?原因是在实际应用场景下,建立 IPC 通信后可能会涉及到比较复杂的数据处理场景,句柄可以作为 进程间通信 方法的第二个可选参数传入,也就是说可以直接将资源的标识通过 IPC 传输,避免了上面所说的代理转发造成的文件描述符的使用。

12.png

以下是支持发送的句柄类型:

net.Socketnet.Servernet.Nativedgram.Socketdgram.Native

3.孤儿进程

父进程创建子进程之后,父进程退出了,但是父进程对应的一个或多个子进程还在运行,这些子进程会被系统的 init 进程收养,对应的进程 ppid 为 1,这就是孤儿进程。通过以下代码示例说明。

# worker.jsconst http = require('http');const server = http.createServer((req, res) => {res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);         // 记录当前工作进程 pid 及父进程 ppid});let worker;process.on('message', function (message, sendHandle) {if (message === 'server') {worker = sendHandle;worker.on('connection', function(socket) {server.emit('connection', socket);});}});
# master.jsconst fork = require('child_process').fork;const server = require('net').createServer();server.listen(3000);const worker = fork('worker.js');worker.send('server', server);console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);process.exit(0); // 创建子进程之后,主进程退出,此时创建的 worker 进程会成为孤儿进程

控制台进行测试,输出当前工作进程 pid 和 父进程 ppid

13.png

由于在 master.js 里退出了父进程,活动监视器所显示的也就只有工作进程。

14.png

再次验证,打开控制台调用接口,可以看到工作进程 5611 对应的 ppid 为 1(为 init 进程),此时已经成为了孤儿进程

15.png

4. 守护进程

守护进程运行在后台不受终端的影响,什么意思呢?
Node.js 开发的同学们可能熟悉,当我们打开终端执行 send() 开启一个服务进程之后,这个终端就会一直被占用,如果关掉终端,服务就会断掉,即node app.js
如果采用守护进程进程方式,这个终端我执行 前台运行模式 开启一个服务进程之后,我还可以在这个终端上做些别的事情,且不会相互影响。

4.1 创建步骤

创建子进程

在子进程中创建新会话(调用系统函数 setsid)

改变子进程工作目录(如:“/” 或 “/usr/ 等)

父进程终止

4.2 Node.js 编写守护进程 Demo及测试

index.js 文件里的处理逻辑使用 spawn 创建子进程完成了上面的第一步操作。设置 node app.js 为 true 可以使子进程在父进程退出后继续运行(系统层会调用 setsid 方法),这是第二步操作。options.cwd 指定当前子进程工作目录若不做设置默认继承当前工作目录,这是第三步操作。运行 daemon.unref() 退出父进程,这是第四步操作。

// index.jsconst spawn = require('child_process').spawn;function startDaemon() {    const daemon = spawn('node', ['daemon.js'], {        cwd: '/usr',        detached : true,        stdio: 'ignore',    });    console.log('守护进程开启 父进程 pid: %s, 守护进程 pid: %s', process.pid, daemon.pid);    daemon.unref();}startDaemon()

daemon.js 文件里处理逻辑开启一个定时器每 10 秒执行一次,使得这个资源不会退出,同时写入日志到子进程当前工作目录下

/usr/daemon.jsconst fs = require('fs');const { Console } = require('console');// custom simple loggerconst logger = new Console(fs.createWriteStream('./stdout.log'), fs.createWriteStream('./stderr.log'));setInterval(function() {logger.log('daemon pid: ', process.pid, ', ppid: ', process.ppid);}, 1000 * 10);

守护进程实现 Node.js 版本 源码地址

https://github.com/Q-Angelo/project-training/tree/master/nodejs/simple-daemon

17.png

16.png

4.3 守护进程总结

在实际工作中对于守护进程并不陌生,例如 PM2、Egg-Cluster 等,以上只是一个简单的 Demo 对守护进程做了一个说明,在实际工作中对守护进程的健壮性要求还是很高的,例如:进程的异常监听、工作进程管理调度、进程挂掉之后重启等等,这些还需要去不断思考。

5. 进程的当前工作目录

目录是什么?

进程的当前工作目录可以通过 options.detached 命令获取,默认为当前启动的目录,如果是创建子进程则继承于父进程的目录,可通过 process.cwd() 命令重置,例如通过 spawn 命令创建的子进程可以指定 cwd 选项设置子进程的工作目录。

有什么作用?

例如,通过 fs 读取文件,如果设置为相对路径则相对于当前进程启动的目录进行查找,所以,启动目录设置有误的情况下将无法得到正确的结果。还有一种情况程序里引用第三方模块也是根据当前进程启动的目录来进行查找的。

// 示例process.chdir('/Users/may/Documents/test/') // 设置当前进程目录console.log(process.cwd()); // 获取当前进程目录

更多node相关知识,请访问:nodejs 教程!

以上就是深入解析NodeJS中的进程管理的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/549072.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月9日 19:10:27
下一篇 2025年11月9日 19:15:23

相关推荐

  • Go语言垃圾回收机制:理解循环引用与可达性分析

    go语言的垃圾回收器采用基于可达性分析的标记-清除算法。即使对象之间存在循环引用,只要它们不再能从任何gc根(如全局变量、栈变量)访问到,go gc也能有效地识别并回收这些不可达的内存,从而避免因循环引用导致的内存泄漏。 在Go语言的内存管理中,理解垃圾回收器(GC)的工作原理对于编写高效且无内存泄…

    2025年12月16日
    000
  • Go语言垃圾回收机制深度解析:可达性与循环引用处理

    go语言的垃圾回收器采用可达性分析模型。即使对象之间存在循环引用(如双向链表),只要这些对象不再能从任何gc根(如全局变量、活跃的栈帧)被访问到,它们就会被视为不可达并被垃圾回收器回收。这意味着开发者通常无需手动打破循环引用以释放内存。 理解Go语言的垃圾回收机制 Go语言的垃圾回收(GC)机制是其…

    2025年12月16日
    000
  • 理解Go语言垃圾回收:循环引用对象如何被回收

    go语言的垃圾回收机制基于可达性分析,而非传统的引用计数。这意味着即使对象之间存在循环引用,只要它们从任何垃圾回收根(gc roots)都不可达,go运行时环境的垃圾收集器就会将其识别并回收。本文将通过具体代码示例,深入探讨go语言如何高效处理循环引用,确保内存资源的有效管理。 Go语言垃圾回收机制…

    2025年12月16日
    000
  • Go语言中高效提取HTML节点文本内容的教程

    在使用go语言进行web内容抓取和解析时,`golang.org/x/net/html`(原`code.google.com/p/go.net/html`)库是一个强大而基础的工具,它能将html文档解析成一个dom树结构。然而,对于初学者而言,一个常见的困惑是如何从一个`html.node`中提取…

    2025年12月16日
    000
  • 深入理解Go GC:如何处理循环引用与不可达性

    本文深入探讨go语言垃圾回收器如何处理包含循环引用的数据结构。go gc采用基于可达性分析的并发标记清除算法,这意味着即使对象间存在循环引用,只要它们从程序根节点变得不可达,gc便能有效回收这些内存,从而避免了传统引用计数机制中常见的循环引用导致的内存泄漏问题。通过一个链表示例,我们将详细阐述这一机…

    2025年12月16日
    000
  • 理解Go语言垃圾回收:如何处理循环引用对象

    go语言的垃圾回收器采用可达性分析而非引用计数。这意味着即使对象之间存在循环引用,只要它们不再被任何gc根引用而变得不可达,垃圾回收器就能自动将其回收。本文将通过示例代码深入解析go gc如何有效管理内存,避免循环引用导致的内存泄漏。 Go语言垃圾回收机制概述 Go语言内置的垃圾回收(GC)机制是其…

    2025年12月16日
    000
  • 如何在Golang中配置多版本管理

    使用gvm、asdf或手动方式可实现Go多版本管理。gvm支持快速安装与切换,如gvm use go1.20.7;asdf适用于多语言统一管理,通过asdf global/local设置版本;手动方案则通过别名切换GOROOT和PATH。选择依据工作流,关键确保环境变量正确指向目标版本。 在Gola…

    2025年12月16日
    000
  • Golang环境搭建中如何切换Go版本

    使用g工具可高效管理多版本Go,安装后通过g install、g use和g set命令切换或设置默认版本,gvm功能更全面但依赖shell配置,手动方式则需自定义目录并修改GOROOT和PATH环境变量,适合不用第三方工具的场景。 在Golang开发中,经常需要在不同项目中使用不同版本的Go。为了…

    2025年12月16日
    000
  • Go语言go.net/html库:深入解析与提取html.Node的文本内容

    本文详细介绍了如何使用go语言的`go.net/html`库从html文档中提取特定`html.node`的完整文本内容。当节点包含嵌套元素时,直接获取文本会遇到挑战。教程通过递归遍历子节点并收集所有`textnode`数据的方法,提供了一个高效且通用的解决方案,并附带了具体的代码示例。 在使用Go…

    2025年12月16日
    000
  • Go net/http:高效获取URL查询参数的FormValue方法

    本文详细介绍了在go语言的`net/http`包中如何高效地获取url查询参数,解答了node.js中`request.param`在go中的对应实现。核心是利用`*http.request`对象的`formvalue`方法,它能便捷地提取指定名称的参数值,并兼顾了post/put请求体参数的优先级…

    2025年12月16日
    000
  • Golang进程控制与信号处理:构建健壮的进程包装器

    本文深入探讨了go语言中实现进程管理和信号处理的多种方法。我们将详细介绍go中执行外部程序的不同途径,以及如何利用`os/signal`包捕获发送给go应用程序的系统信号,同时阐述如何向其他进程发送信号。通过理解这些机制,开发者能够构建出健壮的进程包装器,实现对子进程的有效监控与控制。 在Go语言中…

    2025年12月16日
    000
  • Go语言中利用go.net/html库高效提取HTML节点文本内容

    本教程详细讲解如何使用go语言的`go.net/html`库从html节点中提取纯文本内容。针对文本可能嵌套在多层子元素中的情况,文章提供了一种递归遍历节点树并收集所有文本节点的通用方法,并通过示例代码展示了如何将其集成到html解析和遍历流程中,帮助开发者准确获取所需数据。 理解go.net/ht…

    2025年12月16日
    000
  • 深入理解go.net/html:如何获取HTML节点的完整文本内容

    本教程详细介绍了如何使用go语言的`go.net/html`库解析html并准确提取html元素的内部文本内容。文章阐明了html节点树结构中`elementnode`与`textnode`的区别,并提供了一种通过递归遍历子节点来收集所有文本内容的通用方法,辅以示例代码和注意事项,帮助开发者高效处理…

    2025年12月16日
    000
  • Go语言中高效获取HTML节点文本内容的教程

    本文详细介绍了如何在go语言中使用`go.net/html`库高效地提取html节点的文本内容。针对文本可能嵌套在子元素中的复杂情况,文章提供了一种递归遍历节点树并收集所有文本节点的解决方案,并通过示例代码演示了如何准确获取链接等元素的可见文本,从而克服直接获取`elementnode`数据时的局限…

    2025年12月16日 好文分享
    000
  • 如何在Golang中实现网络数据加密传输

    答案:Golang中通过TLS实现网络加密传输,服务端使用ListenAndServeTLS启用HTTPS,客户端配置http.Transport支持安全连接,非HTTP场景可用crypto/tls封装TCP通信,开发可自签证书,生产需CA签发并正确配置根证书池以确保安全。 在Golang中实现网络…

    2025年12月16日
    000
  • Golang如何实现指针链表遍历

    定义ListNode结构体后,通过循环或递归遍历链表。循环方式更安全高效,从头节点开始逐个访问直至nil,避免栈溢出风险。 在Go语言中实现指针链表的遍历,核心是定义一个链表节点结构体,使用指针连接各个节点,然后通过循环或递归方式从头节点开始逐个访问每个节点的数据。 定义链表节点结构 链表由多个节点…

    2025年12月16日
    000
  • Golang go.mod文件内容如何理解

    go.mod是Go模块的核心配置文件,定义模块名、Go版本及依赖。module声明模块路径,作为导入包的前缀;go指定Go语言版本,影响语法特性和模块行为;require列出直接依赖及其版本,支持// indirect标记间接依赖;replace可替换依赖源,常用于本地调试;exclude用于排除特…

    2025年12月16日
    000
  • Go 服务部署策略:跨平台编译与自动化实践

    本文探讨了go语言服务的部署策略,重点介绍了其强大的跨平台编译能力,允许开发者在不同操作系统和架构上生成可执行文件,从而避免在生产环境进行编译。文章还强调了通过go语言或现有工具(如capistrano)进行自动化部署的重要性,并鼓励利用go社区资源获取最新实践和工具。 Go语言因其高性能和并发特性…

    2025年12月16日
    000
  • Go语言中进程管理与信号处理实战指南

    本文深入探讨了go语言中管理外部进程和处理系统信号的多种方法。我们将对比`syscall`、`os`和`os/exec`包在进程执行方面的差异,重点介绍如何使用`os/exec`启动子进程并利用`os/signal`捕获发送给go程序的信号。此外,文章还将指导读者如何向子进程发送信号以实现优雅的进程…

    2025年12月16日
    000
  • Go语言中实现进程包装器与信号处理

    本教程深入探讨了Go语言中实现进程包装器(process wrapper)的关键技术,包括如何正确启动和管理外部子进程,以及如何在Go程序中有效地捕获和响应系统信号。文章详细比较了Go中执行外部程序的多种方式,并着重介绍了`os/exec`包在构建健壮进程管理系统中的应用,同时提供了使用`os/si…

    2025年12月16日
    000

发表回复

登录后才能评论
关注微信