【Node 连载 4/9】深入理解 Node.js 底层原理

本文是 2021 年 12 月 26 日,第三十五届 - 前端早早聊【前端搞 Node.js】专场,来自字节跳动 Web Infra 前端团队 —— 陈跃标的分享。感谢 AI 的发展,借助 GPT 的能力,最近我们终于可以非常高效地将各位讲师的精彩分享文本化后,分享给大家。(完整版含演示请看录播视频和 PPT):https://www.zaozao.run/video/c35

完整版高清 PPT 请添加小助手「zzleva」获取


正文如下

大家好,我是来自字节跳动 Web Infra 团队的陈跃标。我今天分享的主题是《深入理解 Node.js 的底层原理》,今天分享的内容一共分为以下 5 个部分。

Node.js 的组成和代码架构

下面我们先来看一下 Node.js 的组成和代码架构。

组成

Node.js 主要由 V8 引擎、Libuv 和一些第三方库组成。

V8 是一个 JS 的引擎,不仅实现了 JS 的解析和执行,而且还支持一些自定义扩展能力。比如说我们可以通过 V8 提供的一些 C++ API,然后去定义一个全局的变量,这样的话我们就可以在 JS 层里面去访问到这个全局的变量。

Libuv 是一个跨平台的异步 I/O 库,主要封装了各个操作系统的一些 API,提供网络和文件等功能。因为我们知道在 JS 里面其实是没有网络和文件这些功能的,在前端这些功能是由浏览器去提供的。因此在 Node.js 里面,这些功能就由 Libuv 去实现。

另外 Node.js 里面还用了很多第三方库,比如说像 DNS 解析,用了 cares 这个异步的 DNS 解析库,还有像 HTTP 解析器、HTTP2 解析器,还有一些压缩解压、加密解密的借鉴库等等。

代码架构

接下来我们再看一下 Node.js 的代码的整体架构。

Node.js 的代码一共分为 3 个部分,分别是 JS、C++ 和 C语言。

第一部分 JS 的代码主要是 Node.js 本身提供的一些模块,比如说像我们平时使用的 net、fs、HTTP 这些模块。而对于 C++ 的代码,主要是封装了 Libuv 和一些第三方库的 C++ 代码,比如说像我们平时使用的 net、fs、HTTP 这些模块会对应到 C++ 层的一个模块。

第二部分的内容是关于不依赖 Libuv 和第三方库的 C++ 代码。例如像我们通常使用的 Buffer 模块,主要依赖于 V8 提供的一些 API。 C++ 代码,则是关于 V8 本身的实现,因为 V8 是一个纯 C++ 实现的库。

第三部分 C 语言代码,则包括了一些第三方库和 Libuv 的代码,因为这些库都是纯 C 语言实现的。

Node.js 中的 Libuv

在了解了 Node.js 的组成和代码架构之后,让我们来看看 Node.js 中一些核心实现。首先介绍一下 Libuv,这将分为三个部分。

  1. 关于 Libuv 的模型以及限制。

  2. 介绍了线程池如何解决问题以及带来的问题。

  3. 介绍了事件循环和微任务处理的内容。

Libuv 的模型和限制

Libuv 本质上是一个生产者消费者模型。

从图中的右下角可以看出,在 Libuv 中有许多种类型的生产者,例如在一个回调函数中,或者在一个 Node.js 初始化的时候,或者在线程池完成操作的时候,它们都会充当生产者的角色,向这个事件循环中生产一些任务。Libuv 会在这个事件循环中不断地消费这些任务,从而驱动整个系统的运行。

生产者消费者模型存在一个问题,那就是消费者和生产者之间如何进行同步?例如,如果当前系统没有任务需要消费,消费者应该做什么?

第一种方式是以一种轮询的方式,也就是说在这种情况下,消费者会睡眠一段时间,然后醒来后会判断当前系统是否有任务需要处理,如果有的话就会处理,如果没有的话,就会继续睡眠。但显然,这种方式效率较低。

第二种方式是当系统没有任务需要处理时,进程会挂起,直到有任务需要执行时,系统会唤醒一个进程,然后进程会继续处理这些任务。

Libuv 中使用的就是第二种方式,并且这种方式是通过事件驱动模块来实现的。每个操作系统基本上都提供了一个事件驱动的模块,例如在 Linux 下提供的是 Epoll,在 Mac 下提供的是 Kqueue,在 Windows 下提供的是IOCP。

下面我们来看一下这个事件驱动模块使用的过程。

首先,应用层的代码通过事件驱动模块订阅一个 fd 对应的事件。如果此时该 fd 对应的事件没有就绪,那么该进程会被挂起,等待事件的发生。一旦事件发生,操作系统会唤醒该进程,并通过事件驱动模块回调应用层的代码。

以下以 Linux 下的事件驱动模块 Epoll 为例,我们来看一下事件驱动模块的使用过程。

第一步,通过 epoll_create 创建一个 Epoll 实例,这是后续操作的基础对象。

第二步,通过 epoll_ctl 可以订阅、修改或取消订阅一个 fd 对应的事件。

第三步,通过 epoll_wait 来判断当前是否有事件发生。如果有事件发生,就会直接执行上层注册的回调函数。如果没有事件发生,可以选择是非阻塞、定时阻塞或一直阻塞直到有事件发生。是否阻塞以及阻塞的时间取决于系统当前的状态。例如,在 Node.js 里,如果有定时器,Node.js 会选择定时阻塞,以确保定时器能按时执行。而如果系统中只有一个监听的 Socket 的话,Node.js 会一直阻塞,直到有连接到来时才会被唤醒。

然而,Epoll 本身也存在一些限制:

  • Epoll 不支持文件操作,这是因为操作系统本身没有实现这个功能。

  • Epoll 不太适合执行一些耗时的任务,例如大量的 CPU 计算和可能导致进程阻塞的任务。因为 Epoll 通常是搭配单线程使用的,如果在单线程中执行耗时任务或可能导致进程阻塞的任务,后续的任务就无法进行。

线程池

针对这个问题,在 Node.js 中引入了解决方案,即引入了一个线程池。下面我们来看一下线程池和主线程的一个关系。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值