├── .gitignore ├── .gitattributes ├── 6871657625761_.pic.jpg ├── docs ├── extra │ └── image.js ├── index.md ├── chapter00-前言.md ├── chapter26-vscode调试Node.js.md ├── chapter31-Node.js 的 perf_hooks.md ├── chapter28-Node.js底层原理(架构篇).md ├── chapter22-events模块.md ├── chapter20-拓展Node.js.md ├── chapter09-Unix域.md ├── chapter03-事件循环.md ├── chapter29-Node.js底层原理(实现篇).md ├── chapter11-setImmediate和nextTick.md ├── chapter02-Libuv数据结构和通用逻辑.md ├── chapter10-定时器.md ├── chapter04-线程池.md └── chapter27-深入理解 Node.js 的 Buffer.md ├── requirements.txt ├── .github ├── workflows │ └── ci.yml └── FUNDING.yml ├── README.md ├── LICENSE └── mkdocs.yml /.gitignore: -------------------------------------------------------------------------------- 1 | site/ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md diff 2 | -------------------------------------------------------------------------------- /6871657625761_.pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theanarkh/understand-nodejs/HEAD/6871657625761_.pic.jpg -------------------------------------------------------------------------------- /docs/extra/image.js: -------------------------------------------------------------------------------- 1 | const images = document.querySelectorAll('img'); 2 | 3 | for (const image of images) { 4 | image.setAttribute('referrerpolicy', 'no-referrer'); 5 | } 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | mkdocs-material 3 | mkdocs-minify-plugin>=0.3.0 4 | mkdocs-git-revision-date-localized-plugin>=0.8 5 | mkdocs-git-revision-date-plugin>=0.3.1 6 | mkdocs-awesome-pages-plugin>=2.5.0 7 | mkdocs-redirects>=1.0.1 8 | mkdocs-macros-plugin>=0.5.0 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | - docs 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.x 16 | - run: | 17 | pip install -r requirements.txt 18 | - run: | 19 | mkdocs gh-deploy --force 20 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 通过源码分析nodejs原理
2 | 3 | * 知乎:[Node.js源码分析](https://www.zhihu.com/column/c_1094251741922619392) 4 | * [为什么要读Node.js源码?](https://zhuanlan.zhihu.com/p/350625461)
5 | * [Node.js的底层原理](https://zhuanlan.zhihu.com/p/375276722)
6 | * [《Node.js源码解析1.0.0带标签版》](https://11111-1252105172.cos.ap-shanghai.myqcloud.com/understand-nodejs%EF%BC%88%E5%B8%A6%E6%A0%87%E7%AD%BE%E7%89%88%EF%BC%89.pdf)
7 | 8 | 9 | 让我们开始学习[Node.js](chapter00-前言.md)吧🔥 10 | 11 | --- 12 | 13 | 最后更新: {{ git.date.strftime('%Y-%m-%d') }} 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: https://github.com/theanarkh/understand-nodejs 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 通过源码分析 Node.js 原理
2 | 3 | * [官网](https://theanarkh.github.io/understand-nodejs)
4 | * [Node.js 源码分析系列文章](https://www.zhihu.com/column/c_1094251741922619392)
5 | * [深入剖析 Node.js 底层原理](https://juejin.cn/book/7171733571638738952) 和 [示例代码](https://github.com/theanarkh/nodejs-book)
6 | * [JavaScript 运行时 Demo](https://github.com/theanarkh/js-runtime-demo)
7 | * [No.js: 基于 V8 和 io_uring 的 JavaScript 运行时](https://github.com/theanarkh/No.js)
8 | * [Deer: 基于 V8 和 kqueue 的 JavaScript 运行时](https://github.com/theanarkh/Deer)
9 | * [No.js: 基于 V8 和 Libuv 的 JavaScript 运行时](https://github.com/theanarkh/nojs)
10 | * [Node.js V10.x 源码注释](https://github.com/theanarkh/read-nodejs-code)
11 | * [Libuv 源码注释](https://github.com/theanarkh/read-libuv-code)
12 | 13 | Thanks 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 theanarkh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Node.js 源码剖析 2 | site_url: https://github.com/theanarkh/understand-nodejs 3 | site_author: theanarkh 4 | site_description: 通过源码分析 Node.js 原理 5 | 6 | repo_name: understand-nodejs 7 | repo_url: https://github.com/theanarkh/understand-nodejs 8 | edit_uri: edit/master/docs/ 9 | 10 | copyright: Copyright © 2021 theanarkh 11 | 12 | theme: 13 | name: material 14 | language: zh 15 | icon: 16 | logo: material/book-open-page-variant 17 | repo: fontawesome/brands/github 18 | features: 19 | # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#adding-annotations 20 | - content.code.annotate 21 | - navigation.indexes 22 | - navigation.instant 23 | - navigation.tabs 24 | - navigation.tabs.sticky 25 | - navigation.sections 26 | - navigation.top 27 | - navigation.tracking 28 | - search.highlight 29 | - search.share 30 | - search.suggest 31 | palette: 32 | - scheme: default 33 | primary: indigo 34 | accent: indigo 35 | toggle: 36 | icon: material/toggle-switch-off-outline 37 | name: Switch to dark mode 38 | - scheme: slate 39 | primary: blue 40 | accent: blue 41 | toggle: 42 | icon: material/toggle-switch 43 | name: Switch to light mode 44 | 45 | markdown_extensions: 46 | - admonition 47 | - codehilite 48 | - footnotes 49 | - toc: 50 | permalink: true 51 | - pymdownx.arithmatex 52 | - pymdownx.betterem: 53 | smart_enable: all 54 | - pymdownx.caret 55 | - pymdownx.critic 56 | - pymdownx.details 57 | - pymdownx.highlight: 58 | linenums: true 59 | - pymdownx.superfences 60 | - pymdownx.emoji: 61 | emoji_index: !!python/name:materialx.emoji.twemoji 62 | emoji_generator: !!python/name:materialx.emoji.to_svg 63 | - pymdownx.inlinehilite 64 | - pymdownx.magiclink 65 | - pymdownx.mark 66 | - pymdownx.smartsymbols 67 | - pymdownx.superfences 68 | - pymdownx.tasklist: 69 | custom_checkbox: true 70 | - pymdownx.tabbed 71 | - pymdownx.tilde 72 | 73 | plugins: 74 | - search 75 | - minify: 76 | minify_html: true 77 | - awesome-pages 78 | - macros 79 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-tags/ 80 | # - tags: 81 | # tags_file: tags.md 82 | 83 | extra: 84 | 85 | social: 86 | - icon: fontawesome/brands/github 87 | link: https://github.com/theanarkh 88 | - icon: fontawesome/brands/zhihu 89 | link: https://www.zhihu.com/people/theanarkh 90 | # - icon: fontawesome/brands/blog 91 | # link: https://xkx9431.github.io/xkx_blog/ 92 | spark: 93 | version: 3.2.0 (RC2) 94 | github: https://github.com/apache/spark/tree/v3.2.0-rc2 95 | 96 | 97 | nav: 98 | - Home: index.md 99 | - 前言: chapter00-前言.md 100 | - Node.js基础和架构: 101 | - 01-Node.js组成和原理: chapter01-Node.js组成和原理.md 102 | - 02-Libuv数据结构和通用逻辑: chapter02-Libuv数据结构和通用逻辑.md 103 | - 03-事件循环: chapter03-事件循环.md 104 | - 04-线程池: chapter04-线程池.md 105 | - 05-Libuv流: chapter05-Libuv流.md 106 | - 06-C++层: chapter06-C++层.md 107 | - Node.js核心模块的实现: 108 | - 07-信号处理: chapter07-信号处理.md 109 | - 08-DNS: chapter08-DNS.md 110 | - 09-Unix域: chapter09-Unix域.md 111 | - 10-定时器: chapter10-定时器.md 112 | - 11-setImmediate和nextTick: chapter11-setImmediate和nextTick.md 113 | - 12-文件: chapter12-文件.md 114 | - 13-进程: chapter13-进程.md 115 | - 14-线程: chapter14-线程.md 116 | - 15-Cluster: chapter15-Cluster.md 117 | - 16-UDP: chapter16-UDP.md 118 | - 17-TCP: chapter17-TCP.md 119 | - 18-HTTP: chapter18-HTTP.md 120 | - 19-模块加载: chapter19-模块加载.md 121 | - 20-JS Stream: chapter21-JS Stream.md 122 | - 21-events模块: chapter22-events模块.md 123 | - 22-Async hooks: chapter23-Async hooks.md 124 | - 23-Inspector: chapter24-Inspector.md 125 | - 27-深入理解 Node.js 的 Buffer: chapter27-深入理解 Node.js 的 Buffer.md 126 | - 30-Node.js 的 trace events 架构: chapter30-Node.js 的 trace events 架构.md 127 | - 31-Node.js 的 perf_hooks: chapter31-Node.js 的 perf_hooks.md 128 | - 其他: 129 | - 24-拓展Node.js: chapter20-拓展Node.js.md 130 | - 25-Node.js子线程调试和诊断指南: chapter25-Node.js子线程调试和诊断指南 131 | - 26-vscode调试Node.js: chapter26-vscode调试Node.js.md 132 | - 28-Node.js底层原理(架构篇): chapter28-Node.js底层原理(架构篇).md 133 | - 29-Node.js底层原理(实现篇): chapter29-Node.js底层原理(实现篇).md 134 | 135 | extra_javascript: 136 | - path: extra/image.js 137 | defer: true -------------------------------------------------------------------------------- /docs/chapter00-前言.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 我很喜欢JS这门语言,感觉它和C语言一样,在C语言里,很多东西都需要自己实现,让我们可以发挥无限的创造力和想象力。在JS中,虽然很多东西在V8里已经提供,但是用JS,依然可以创造很多好玩的东西,还有好玩的写法。另外,JS应该我见过唯一的一门没有实现网络和文件功能的语言,网络和文件,是一个很重要的能力,对于程序员来说,也是很核心很基础的知识。很幸运,Node.js被创造出来了,Node.js在JS的基础上,使用V8和Libuv提供的能力,极大地拓展、丰富了JS的能力,尤其是网络和文件,这样我就不仅可以使用JS,还可以使用网络、文件等功能,这是我逐渐转向Node.js方向的原因之一,也是我开始研究Node.js源码的原因之一。虽然Node.js满足了我喜好和技术上的需求,不过一开始的时候,我并没有全身心地投入代码的研究,只是偶尔会看一下某些模块的实现,真正的开始,是为了做《Node.js是如何利用Libuv实现事件循环和异步》的分享,从那时候起,大部分业余时间和精力都投入源码的研究。 4 |     我首先从Libuv开始研究,因为Libuv是Node.js的核心之一。由于曾经研究过一些Linux的源码,也一直在学习操作系统的一些原理和实现,所以在阅读Libuv的时候,算是没有遇到太大的困难,C语言函数的使用和原理,基本都可以看明白,重点在于需要把各个逻辑捋清楚。我使用的方法就是注释和画图,我个人比较喜欢写注释。虽然说代码是最好的注释,但是我还是愿意花时间用注释去把代码的背景和意义阐述一下,而且注释会让大部分人更快地能读懂代码的含义。读Libuv的时候,也穿插地读了一些JS和C++层的代码。我阅读Node.js源码的方式是,选择一个模块,垂直地从JS层分析到C++层,然后到Libuv层。 5 |     读完Libuv,接下来读的是JS层的代码,JS虽然容易看懂,但是JS层的代码非常多,而且我感觉逻辑上也非常绕,所以至今,我还有很多没有细读,这个作为后续的计划。Node.js中,C++算是胶水层,很多时候,不会C++,其实也不影响Node.js源码的阅读,因为C++很多时候,只是一种透传的功能,它把JS层的请求,通过V8,传给Libuv,然后再反过来,所以C++层我是放到最后才细读。C++层我觉得是最难的,这时候,我又不得不开始读V8的源码了,理解V8非常难,我选取的几乎是最早的版本0.1.5,然后结合8.x版本。通过早期版本,先学习V8的大概原理和一些早期实现上的细节。因为后续的版本虽然变化很大,但是更多只是功能的增强和优化,有很多核心的概念还是没有变化的,这是我选取早期版本的原因,避免一开始就陷入无穷无尽的代码中,迷失了方向,失去了动力。但是哪怕是早期的版本,有很多内容依然非常复杂,结合新版本是因为有些功能在早期版本里没有实现,这时候要明白它的原理,就只能看新版的代码,有了早期版本的经验,阅读新版的代码也有一定的好处,多少也知道了一些阅读技巧。 6 |     Node.js的大部分代码都在C++和JS层,所以目前仍然是在不断地阅读这两层的代码。还是按照模块垂直分析。阅读Node.js代码,让我更了解Node.js的原理,也更了解JS。不过代码量非常大,需要源源不断的时间和精力投入。但是做技术,知其然知其所以然的感觉是非常美妙的,你靠着一门技术谋生,却对它知之甚少,这种感觉并不好。阅读源码,虽然不会为你带来直接的、迅速的收益,但是有几个好处是必然的。第一是它会决定你的高度,第二你写代码的时候,你看到的不再是一些冰冷冷、无生命的字符。这可能有点夸张,但是你了解了技术的原理,你在使用技术的时候,的确会有不同的体验,你的思维也会有了更多的变化。第三是提高了你的学习能力,当你对底层原理有了更多的了解和理解,你在学习其它技术的时候,就会更快地学会,比如你了解了epoll的原理,那你看Nginx、Redis、Libuv等源码的时候,关于事件驱动的逻辑,基本上很快就能看懂。很高兴有这些经历,也投入了很多时间和经精力,希望以后对Node.js有更多的理解和了解,也希望在Node.js方向有更多的实践。 7 | 8 | ## 本书的目的 9 |     阅读Node.js源码的初衷是让自己深入理解Node.js的原理,但是我发现有很多同学对Node.js原理也非常感兴趣,因为业余时间里也一直在写一些关于Node.js源码分析的文章(基于Node.js V10和V14),所以就打算把这些内容整理成一本有体系的书,让感兴趣的同学能系统地去了解和理解Node.js的原理。不过我更希望的是,读者从书中不仅学到Node.js的知识,而且也学到如何阅读Node.js源码,可以自己独立完成源码的研究。也希望更多同学分享自己的心得。本书不是Node.js的全部,但是尽量去讲得更多,源码非常多,错综复杂,理解上可能有不对之处,欢迎交流。因为看过Linux早期内核(0.11和1.2.13)和早期V8(0.1.5)的一些实现,文章会引用其中的一些代码,目的在于让读者可以更了解一个知识点的大致实现原理,如果读者有兴趣,可以自行阅读相关代码。 10 | 11 | ## 本书结构 12 | 本书共分为二十二章,讲解的代码都是基于Linux系统的。 13 | 14 | 1. 主要介绍了Node.js的组成和整体的工作原理,另外分析了Node.js启动的过程,最后介绍了服务器架构的演变和Node.js的所选取的架构。 15 | 2. 主要介绍了Node.js中的基础数据结构和通用的逻辑,在后面的章节会用到。 16 | 3. 主要介绍了Libuv的事件循环,这是Node.js的核心所在,本章具体介绍了事件循环中每个阶段的实现。 17 | 4. 主要分析了Libuv中线程池的实现,Libuv线程池对Node.js来说是非常重要的,Node.js中很多模块都需要使用线程池,包括crypto、fs、dns等。如果没有线程池,Node.js的功能将会大打折扣。同时分析了Libuv中子线程和主线程的通信机制。同样适合其它子线程和主线程通信。 18 | 5. 主要分析了Libuv中流的实现,流在Node.js源码中很多地方都用到,可以说是非常核心的概念。 19 | 6. 主要分析了Node.js中C++层的一些重要模块和通用逻辑。 20 | 7. 主要分析了Node.js的信号处理机制,信号是进程间通信的另一种方式。 21 | 8. 主要分析了Node.js的dns模块的实现,包括cares的使用和原理。 22 | 9. 主要分析了Node.js中pipe模块(Unix域)的实现和使用,Unix域是实现进程间通信的方式,它解决了没有继承的进程无法通信的问题。而且支持传递文件描述符,极大地增强了Node.js的能力。 23 | 10. 主要分析了Node.js中定时器模块的实现。定时器是定时处理任务的利器。 24 | 11. 主要分析了Node.js setImmediate和nextTick的实现。 25 | 12. 主要介绍了Node.js中文件模块的实现,文件操作是我们经常会用到的功能。 26 | 13. 主要介绍了Node.js中进程模块的实现,多进程使得Node.js可以利用多核能力。 27 | 14. 主要介绍了Node.js中线程模块的实现,多进程和多线程有类似的功能但是也有一些差异。 28 | 15. 主要介绍了Node.js中cluster模块的使用和实现原理,cluster模块封装了多进程能力,使得Node.j是可以使用多进程的服务器架构,利用了多核的能力。 29 | 16. 主要分析了Node.js中UDP的实现和相关内容。 30 | 17. 主要分析了Node.js中TCP模块的实现,TCP是Node.js的核心模块,我们常用的HTTP,HTTPS都是基于net模块。 31 | 18. 主要介绍了HTTP模块的实现以及HTTP协议的一些原理。 32 | 19. 主要分析了Node.js中各种模块加载的原理,深入理解Node.js的require函数所做的事情。 33 | 20. 主要介绍了一些拓展Node.js的方法,使用Node.js,拓展Node.js。 34 | 21. 主要介绍了JS层Stream的实现,Stream模块的逻辑很绕,大概讲解了一下。 35 | 22. 主要介绍了Node.js中event模块的实现,event模块虽然简单,但是是Node.js的核心模块。 36 | 37 | ## 面对的读者 38 | 本书面向有一定Node.js使用经验并对Node.js原理感兴趣的同学,因为本书是Node.js源码的角度去分析Node.js的原理,其中部分是C、C++,所以需要读者有一定的C、C++基础,另外,有一定的操作系统、计算机网络、V8基础会更好。 39 | 40 | ## 阅读建议 41 | 建议首先阅读前面几种基础和通用的内容,然后再阅读单个模块的实现,最后有兴趣的话,再阅读如何拓展Node.js章节。如果你已经比较熟悉Node.js,只是对某个模块或内容比较感兴趣,则可以直接阅读某个章节。刚开始阅读Node.js源码时,选取的是V10.x的版本,后来Node.js已经更新到了V14,所以书中的代码有的是V10有的是V14的。Libuv是V1.23。可以到我的github上获取。 42 | 43 | ## 源码阅读建议 44 | Node.js的源码由JS、C++、C组成。 45 | 1 Libuv是C语言编写。理解Libuv除了需要了解C语法外,更多的是对操作系统和网络的理解,有些经典的书籍可以参考,比如《Unix网络编程》1,2两册,《Linux系统编程手册》上下两册,《TCP/IP权威指南》等等。还有Linux的API文档以及网上优秀的文章都可以参考一下。 46 | 47 | 2 C++主要是利用V8提供的能力对JS进行拓展,也有一部分功能使用C++实现,总的来说C++的作用更多是胶水层,利用V8作为桥梁,连接Libuv和JS。不会C++,也不完全影响源码的阅读,但是会C++会更好。阅读C++层代码,除了语法外,还需要对V8的概念和使用有一定的了解和理解。 48 | 49 | 3 JS代码相信学习Node.js的同学都没什么问题。 50 | 51 | ## 其它资源 52 | 个人博客 53 | csdn [https://blog.csdn.net/THEANARKH](https://blog.csdn.net/THEANARKH) 54 | 知乎[https://www.zhihu.com/people/theanarkh](https://www.zhihu.com/people/theanarkh) 55 | github [https://github.com/theanarkh](https://github.com/theanarkh) 56 | 阅读Node.js源码时,所用到的基础知识、所作积累和记录几乎都在上面的博客中。 如果你有任何问题可以到[https://github.com/theanarkh/understand-nodejs](https://github.com/theanarkh/understand-nodejs)提issue或者联系我。 57 | -------------------------------------------------------------------------------- /docs/chapter26-vscode调试Node.js.md: -------------------------------------------------------------------------------- 1 | 前言:调试代码不管对于开发还是学习源码都是非常重要的技能,本文简单介绍vscode调试Node.js相关代码的调试技巧。 2 | # 1 调试业务JS 3 | 调试业务JS可能是普遍的场景,随着Node.js和调试工具的成熟,调试也变得越来越简单。下面是vscode的lauch.json配置。 4 | ```json 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "type": "node", 10 | "request": "attach", 11 | "name": "Attact Program", 12 | "port": 9229 13 | } 14 | ] 15 | } 16 | ``` 17 | 1 在JS里设置断点,执行node --inspect index.js 启动进程,会输出调试地址。 18 | ![](https://img-blog.csdnimg.cn/b1d67620b96c4c07a0b48b390ec940a6.png) 19 | 2 点击虫子,然后点击绿色的三角形。![在这里插入图片描述](https://img-blog.csdnimg.cn/2b64913e72c34405b903aaba3a3b1472.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 20 | 3 vscode会连接Node.js的WebSocket服务。 21 | 4 开始调试(或者使用Chrome Dev Tools调试)。 22 | 23 | # 2 调试Addon的C++ 24 | 写Addon的场景可能不多,但是当你需要的时候,你就会需要调试它。下面的配置只可以调试C++代码。 25 | ```json 26 | { 27 | "version": "0.2.0", 28 | "configurations": [ 29 | { 30 | "name": "Debug node C++ addon", 31 | "type": "lldb", 32 | "request": "launch", 33 | "program": "node", 34 | "args": ["${workspaceFolder}/node-addon-examples/1_hello_world/napi/hello.js"], 35 | "cwd": "${workspaceFolder}/node-addon-examples/1_hello_world/napi" 36 | }, 37 | ] 38 | } 39 | ``` 40 | 1 在C++代码设置断点。 41 | ![](https://img-blog.csdnimg.cn/71b5921b90254b8a8cdcd809bd1d1135.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBAdGhlYW5hcmto,size_49,color_FFFFFF,t_70,g_se,x_16) 42 | 2 执行node-gyp configure && node-gyp build --debug编译debug版本的Addon。 43 | 3 JS里加载debug版本的Addon。 44 | ![](https://img-blog.csdnimg.cn/2b5c8793e83342879a83f21499909283.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBAdGhlYW5hcmto,size_57,color_FFFFFF,t_70,g_se,x_16) 45 | 4 点击小虫子开始调试。 46 | ![](https://img-blog.csdnimg.cn/4591a59721e74f5693c67d5d45d11bfa.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBAdGhlYW5hcmto,size_52,color_FFFFFF,t_70,g_se,x_16) 47 | 48 | # 3 调试Addon的C++和JS 49 | Addon通常需要通过JS暴露出来使用,如果我们需要调试C++和JS,那么就可以使用以下配置。 50 | ```json 51 | { 52 | "version": "0.2.0", 53 | "configurations": [ 54 | { 55 | "name": "Debug node C++ addon", 56 | "type": "node", 57 | "request": "launch", 58 | "program": "${workspaceFolder}/node-addon-examples/1_hello_world/napi/hello.js", 59 | "cwd": "${workspaceFolder}/node-addon-examples/1_hello_world/napi" 60 | }, 61 | { 62 | "name": "Attach node C/C++ Addon", 63 | "type": "lldb", 64 | "request": "attach", 65 | "pid": "${command:pickMyProcess}" 66 | } 67 | ] 68 | } 69 | ``` 70 | 和2的过程类似,点三角形开始调试,再选择Attach node C/C++ Addon,然后再次点击三角形。![在这里插入图片描述](https://img-blog.csdnimg.cn/c97b14f3fb724cb4bf8beb79e7e0f0c5.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBAdGhlYW5hcmto,size_16,color_FFFFFF,t_70,g_se,x_16) 71 | 选择attach到hello.js中。 72 | ![](https://img-blog.csdnimg.cn/4e2fc471d1734cceb4f4e382057515d1.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBAdGhlYW5hcmto,size_62,color_FFFFFF,t_70,g_se,x_16) 73 | 开始调试。 74 | 75 | # 4 调试Node.js源码C++ 76 | 我们不仅用Node.js,我们可能还会学习Node.js源码,学习源码的时候就少不了调试。可以通过下面的方式调试Node.js的C++源码。 77 | ```text 78 | ./configure --debug && make 79 | ``` 80 | 使用以下配置 81 | ```json 82 | { 83 | "version": "0.2.0", 84 | "configurations": [ 85 | { 86 | "name": "(lldb) 启动", 87 | "type": "cppdbg", 88 | "request": "launch", 89 | "program": "${workspaceFolder}/out/Debug/node", 90 | "args": [], 91 | "stopAtEntry": false, 92 | "cwd": "${fileDirname}", 93 | "environment": [], 94 | "externalConsole": false, 95 | "MIMode": "lldb" 96 | } 97 | ] 98 | } 99 | ``` 100 | 在node_main.cc的main函数或任何C++代码里打断点,点击小虫子开始调试。 101 | 102 | # 5 调试Node.js源码C++和JS代码 103 | Node.js的源码不仅仅有C++,还有JS,如果我们想同时调试,那么就使用以下配置。 104 | ```json 105 | { 106 | "version": "0.2.0", 107 | "configurations": [ 108 | { 109 | "name": "(lldb) 启动", 110 | "type": "cppdbg", 111 | "request": "launch", 112 | "program": "${workspaceFolder}/out/Debug/node", 113 | "args": ["--inspect-brk", "${workspaceFolder}/out/Debug/index.js"], 114 | "stopAtEntry": false, 115 | "cwd": "${fileDirname}", 116 | "environment": [], 117 | "externalConsole": false, 118 | "MIMode": "lldb" 119 | } 120 | ] 121 | } 122 | ``` 123 | 1 点击调试。 124 | ![](https://img-blog.csdnimg.cn/674c5cfddfb64b04a98d532c7be09659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_Q1NETiBAdGhlYW5hcmto,size_62,color_FFFFFF,t_70,g_se,x_16) 125 | 2 在vscode调试C++,执行完Node.js启动的流程后会输出调试JS的地址。 126 | ![](https://img-blog.csdnimg.cn/cbe5de2a0c1e47d68d9db1c35183b287.png) 127 | 3 在浏览器连接WebSocket服务调试JS。 128 | ![](https://img-blog.csdnimg.cn/5642810b42734b1bbea7d2aa981798cf.png) 129 | ![](https://img-blog.csdnimg.cn/8f3ae91621444e5885b8fe52de291738.png) 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/chapter31-Node.js 的 perf_hooks.md: -------------------------------------------------------------------------------- 1 | 前言:perf_hooks 是 Node.js 中用于收集性能数据的模块,Node.js 本身基于 perf_hooks 提供了性能数据,同时也提供了机制给用户上报性能数据。文本介绍一下 perk_hooks。 2 | 3 | # 1 使用 4 | 首先看一下 perf_hooks 的基本使用。 5 | ```c 6 | const { PerformanceObserver } = require('perf_hooks'); 7 | const obs = new PerformanceObserver((items) => { 8 | // 9 | }; 10 | 11 | obs.observe({ type: 'http' }); 12 | ``` 13 | 通过 PerformanceObserver 可以创建一个观察者,然后调用 observe 可以订阅对哪种类型的性能数据感兴趣。 14 | 15 | 下面看一下 C++ 层的实现,C++ 层的实现首先是为了支持 C++ 层的代码进行数据的上报,同时也为了支持 JS 层的功能。 16 | # 2 C++ 层实现 17 | ## 2.1 PerformanceEntry 18 | PerformanceEntry 是 perf_hooks 里的一个核心数据结构,PerformanceEntry 代表一次性能数据。下面来看一下它的定义。 19 | ```c 20 | template 21 | struct PerformanceEntry { 22 | using Details = typename Traits::Details; 23 | std::string name; 24 | double start_time; 25 | double duration; 26 | Details details; 27 | 28 | static v8::MaybeLocal GetDetails( 29 | Environment* env, 30 | const PerformanceEntry& entry) { 31 | return Traits::GetDetails(env, entry); 32 | } 33 | }; 34 | ``` 35 | PerformanceEntry 里面记录了一次性能数据的信息,从定义中可以看到,里面记录了类型,开始时间,持续时间,比如一个 HTTP 请求的开始时间,处理耗时。除了这些信息之外,性能数据还包括一些额外的信息,由 details 字段保存,比如 HTTP 请求的 url,不过目前还不支持这个能力,不同的性能数据会包括不同的额外信息,所以 PerformanceEntry 是一个类模版,具体的 details 由具体的性能数据生产者实现。下面我们看一个具体的例子。 36 | ```c 37 | struct GCPerformanceEntryTraits { 38 | static constexpr PerformanceEntryType kType = NODE_PERFORMANCE_ENTRY_TYPE_GC; 39 | struct Details { 40 | PerformanceGCKind kind; 41 | PerformanceGCFlags flags; 42 | 43 | Details(PerformanceGCKind kind_, PerformanceGCFlags flags_) 44 | : kind(kind_), flags(flags_) {} 45 | }; 46 | 47 | static v8::MaybeLocal GetDetails( 48 | Environment* env, 49 | const PerformanceEntry& entry); 50 | }; 51 | 52 | using GCPerformanceEntry = PerformanceEntry; 53 | ``` 54 | 这是关于 gc 性能数据的实现,我们看到它的 details 里包括了 kind 和 flags。接下来看一下 perf_hooks 是如何收集 gc 的性能数据的。首先通过 InstallGarbageCollectionTracking 注册 gc 钩子。 55 | ```c 56 | static void InstallGarbageCollectionTracking(const FunctionCallbackInfo& args) { 57 | Environment* env = Environment::GetCurrent(args); 58 | 59 | env->isolate()->AddGCPrologueCallback(MarkGarbageCollectionStart, 60 | static_cast(env)); 61 | env->isolate()->AddGCEpilogueCallback(MarkGarbageCollectionEnd, 62 | static_cast(env)); 63 | env->AddCleanupHook(GarbageCollectionCleanupHook, env); 64 | } 65 | ``` 66 | InstallGarbageCollectionTracking 主要是使用了 V8 提供的两个函数注册了 gc 开始和 gc 结束阶段的钩子。我们看一下这两个钩子的逻辑。 67 | ```c 68 | void MarkGarbageCollectionStart( 69 | Isolate* isolate, 70 | GCType type, 71 | GCCallbackFlags flags, 72 | void* data) { 73 | Environment* env = static_cast(data); 74 | env->performance_state()->performance_last_gc_start_mark = PERFORMANCE_NOW(); 75 | } 76 | ``` 77 | MarkGarbageCollectionStart 在开始 gc 时被执行,逻辑很简单,主要是记录了 gc 的开始时间。接着看 MarkGarbageCollectionEnd。 78 | ```c 79 | void MarkGarbageCollectionEnd( 80 | Isolate* isolate, 81 | GCType type, 82 | GCCallbackFlags flags, 83 | void* data) { 84 | Environment* env = static_cast(data); 85 | PerformanceState* state = env->performance_state(); 86 | 87 | double start_time = state->performance_last_gc_start_mark / 1e6; 88 | double duration = (PERFORMANCE_NOW() / 1e6) - start_time; 89 | 90 | std::unique_ptr entry = 91 | std::make_unique( 92 | "gc", 93 | start_time, 94 | duration, 95 | GCPerformanceEntry::Details( 96 | static_cast(type), 97 | static_cast(flags))); 98 | 99 | env->SetImmediate([entry = std::move(entry)](Environment* env) { 100 | entry->Notify(env); 101 | }, CallbackFlags::kUnrefed); 102 | } 103 | ``` 104 | MarkGarbageCollectionEnd 根据刚才记录 gc 开始时间,计算出 gc 的持续时间。然后产生一个性能数据 GCPerformanceEntry。然后在事件循环的 check 阶段通过 Notify 进行上报。 105 | ```c 106 | void Notify(Environment* env) { 107 | v8::Local detail; 108 | if (!Traits::GetDetails(env, *this).ToLocal(&detail)) { 109 | // TODO(@jasnell): Handle the error here 110 | return; 111 | } 112 | 113 | v8::Local argv[] = { 114 | OneByteString(env->isolate(), name.c_str()), 115 | OneByteString(env->isolate(), GetPerformanceEntryTypeName(Traits::kType)), 116 | v8::Number::New(env->isolate(), start_time), 117 | v8::Number::New(env->isolate(), duration), 118 | detail 119 | }; 120 | 121 | node::MakeSyncCallback( 122 | env->isolate(), 123 | env->context()->Global(), 124 | env->performance_entry_callback(), 125 | arraysize(argv), 126 | argv); 127 | } 128 | }; 129 | ``` 130 | Notify 进行进一步的处理,然后执行 JS 的回调进行数据的上报。env->performance_entry_callback() 对应的回调在 JS 设置。 131 | 132 | ## 2.2 PerformanceState 133 | PerformanceState 是 perf_hooks 的另一个核心数据结构,负责管理 perf_hooks 模块的一些公共数据。 134 | ```c 135 | class PerformanceState { 136 | public: 137 | explicit PerformanceState(v8::Isolate* isolate, const SerializeInfo* info); 138 | AliasedUint8Array root; 139 | AliasedFloat64Array milestones; 140 | AliasedUint32Array observers; 141 | 142 | uint64_t performance_last_gc_start_mark = 0; 143 | 144 | void Mark(enum PerformanceMilestone milestone,uint64_t ts = PERFORMANCE_NOW()); 145 | 146 | private: 147 | struct performance_state_internal { 148 | // Node.js 初始化时的性能数据 149 | double milestones[NODE_PERFORMANCE_MILESTONE_INVALID]; 150 | // 记录对不同类型性能数据感兴趣的观察者个数 151 | uint32_t observers[NODE_PERFORMANCE_ENTRY_TYPE_INVALID]; 152 | }; 153 | }; 154 | ``` 155 | PerformanceState 主要是记录了 Node.js 初始化时的性能数据,比如 Node.js 初始化完毕的时间,事件循环的开始时间等。还有就是记录了观察者的数据结构,比如对 HTTP 性能数据感兴趣的观察者,主要用于控制要不要上报相关类型的性能数据。比如如果没有观察者的话,那么就不需要上报这个数据。 156 | 157 | # 3 JS 层实现 158 | 接下来看一下 JS 的实现。首先看一下观察者的实现。 159 | 160 | ```c 161 | class PerformanceObserver { 162 | constructor(callback) { 163 | // 性能数据 164 | this[kBuffer] = []; 165 | // 观察者订阅的性能数据类型 166 | this[kEntryTypes] = new SafeSet(); 167 | // 观察者对一个还是多个性能数据类型感兴趣 168 | this[kType] = undefined; 169 | // 观察者回调 170 | this[kCallback] = callback; 171 | } 172 | 173 | observe(options = {}) { 174 | const { 175 | entryTypes, 176 | type, 177 | buffered, 178 | } = { ...options }; 179 | // 清除之前的数据 180 | maybeDecrementObserverCounts(this[kEntryTypes]); 181 | this[kEntryTypes].clear(); 182 | // 重新订阅当前设置的类型 183 | for (let n = 0; n < entryTypes.length; n++) { 184 | if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryTypes[n])) { 185 | this[kEntryTypes].add(entryTypes[n]); 186 | maybeIncrementObserverCount(entryTypes[n]); 187 | } 188 | } 189 | // 插入观察者队列 190 | kObservers.add(this); 191 | } 192 | 193 | takeRecords() { 194 | const list = this[kBuffer]; 195 | this[kBuffer] = []; 196 | return list; 197 | } 198 | 199 | static get supportedEntryTypes() { 200 | return kSupportedEntryTypes; 201 | } 202 | // 产生性能数据时被执行的函数 203 | [kMaybeBuffer](entry) { 204 | if (!this[kEntryTypes].has(entry.entryType)) 205 | return; 206 | // 保存性能数据,迟点上报 207 | ArrayPrototypePush(this[kBuffer], entry); 208 | // 插入待上报队列 209 | kPending.add(this); 210 | if (kPending.size) 211 | queuePending(); 212 | } 213 | // 执行观察者回调 214 | [kDispatch]() { 215 | this[kCallback](new PerformanceObserverEntryList(this.takeRecords()), 216 | this); 217 | } 218 | } 219 | ``` 220 | 观察者的实现比较简单,首先有一个全局的变量记录了所有的观察者,然后每个观察者记录了自己订阅的类型。当产生性能数据时,生产者就会通知观察者,接着观察者执行回调。这里需要额外介绍的一个是 maybeDecrementObserverCounts 和 maybeIncrementObserverCount。 221 | ```c 222 | function getObserverType(type) { 223 | switch (type) { 224 | case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC; 225 | case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2; 226 | case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP; 227 | } 228 | } 229 | 230 | function maybeDecrementObserverCounts(entryTypes) { 231 | for (const type of entryTypes) { 232 | const observerType = getObserverType(type); 233 | 234 | if (observerType !== undefined) { 235 | observerCounts[observerType]--; 236 | 237 | if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC && 238 | observerCounts[observerType] === 0) { 239 | removeGarbageCollectionTracking(); 240 | gcTrackingInstalled = false; 241 | } 242 | } 243 | } 244 | } 245 | ``` 246 | maybeDecrementObserverCounts 主要用于操作 C++ 层的逻辑,首先根据订阅类型判断是不是 C++ 层支持的类型,因为 perf_hooks 在 C++ 和 JS 层都定义了不同的性能类型,如果是涉及到底层的类型,就会操作 observerCounts 记录当前类型的观察者数量,observerCounts 就是刚才分析 C++ 层的 observers 变量,它是一个数组,每个索引对应一个类型,数组元素的值是观察者的个数。另外如果订阅的是 gc 类型,并且是第一个订阅者,那就 JS 层就会操作 C++ 层往 V8 里注册 gc 回调。 247 | 248 | 了解了 perf_hooks 提供的机制后,我们来看一个具体的性能数据上报例子。这里以 HTTP Server 处理请求的耗时为例。 249 | ```c 250 | function emitStatistics(statistics) { 251 | const startTime = statistics.startTime; 252 | const diff = process.hrtime(startTime); 253 | const entry = new InternalPerformanceEntry( 254 | statistics.type, 255 | 'http', 256 | startTime[0] * 1000 + startTime[1] / 1e6, 257 | diff[0] * 1000 + diff[1] / 1e6, 258 | undefined, 259 | ); 260 | enqueue(entry); 261 | } 262 | ``` 263 | 下面是 HTTP Server 处理完一个请求时上报性能数据的逻辑。首先创建一个 InternalPerformanceEntry 对象,这个和刚才介绍的 C++ 对象是一样的,是表示一个性能数据的对象。接着调用 enqueue 函数。 264 | ```c 265 | function enqueue(entry) { 266 | // 通知观察者有性能数据,观察者自己判断是否订阅了这个类型的数据 267 | for (const obs of kObservers) { 268 | obs[kMaybeBuffer](entry); 269 | } 270 | // 如果是 mark 或 measure 类型,则插入一个全局队列。 271 | const entryType = entry.entryType; 272 | let buffer; 273 | if (entryType === 'mark') { 274 | buffer = markEntryBuffer; // mark 性能数据队列 275 | } else if (entryType === 'measure') { 276 | buffer = measureEntryBuffer; // measure 性能数据队列 277 | } else { 278 | return; 279 | } 280 | 281 | ArrayPrototypePush(buffer, entry); 282 | } 283 | ``` 284 | enqueue 会把性能数据上报到观察者,然后观察者如果订阅这个类型的数据则执行用户回调通知用户。我们看一下 obs[kMaybeBuffer] 的逻辑。 285 | ```c 286 | [kMaybeBuffer](entry) { 287 | if (!this[kEntryTypes].has(entry.entryType)) 288 | return; 289 | ArrayPrototypePush(this[kBuffer], entry); 290 | // this 是观察者实例 291 | kPending.add(this); 292 | if (kPending.size) 293 | queuePending(); 294 | } 295 | 296 | 297 | function queuePending() { 298 | if (isPending) return; 299 | isPending = true; 300 | setImmediate(() => { 301 | isPending = false; 302 | const pendings = ArrayFrom(kPending.values()); 303 | kPending.clear(); 304 | // 遍历观察者队列,执行 kDispatch 305 | for (const pending of pendings) 306 | pending[kDispatch](); 307 | }); 308 | } 309 | // 下面是观察者中的逻辑,观察者把当前保存的数据上报给用户 310 | [kDispatch]() { 311 | this[kCallback](new PerformanceObserverEntryList(this.takeRecords()),this); 312 | } 313 | ``` 314 | 另外 mark 和 measure 类型的性能数据比较特殊,它不仅会通知观察者,还会插入到全局的一个队列中。所以对于其他类型的性能数据,如果没有观察者的话就会被丢弃(通常在调用 enqueue 之前会先判断是否有观察者),对于 mark 和 measure 类型的性能数据,不管有没有观察者都会被保存下来,所以我们需要显式清除。 315 | 316 | # 4 总结 317 | 以上就是 perf_hooks 中核心的实现,除此之外,perf_hooks 还提供了其他的功能,本文就先不介绍了。可以看到 perf_hooks 的实现是一个订阅发布的模式,看起来貌似没什么特别的。但是它的强大之处在于是由 Node.js 内置实现的, 这样 Node.js 的其他模块就可以基于 perf_hooks 这个框架上报各种类型的性能数据。相比来说虽然我们也能在用户层实现这样的逻辑,但是我们拿不到或者没有办法优雅地方法拿到 Node.js 内核里面的数据,比如我们想拿到 gc 的性能数据,我们只能写 addon 实现。又比如我们想拿到 HTTP Server 处理请求的耗时,虽然可以通过监听 reqeust 或者 response 对象的事件实现,但是这样一来我们就会耦合到业务代码里,每个开发者都需要处理这样的逻辑,如果我们想收拢这个逻辑,就只能劫持 HTTP 模块来实现,这些不是优雅但是是不得已的解决方案。有了 perf_hooks 机制,我们就可以以一种结耦的方式来收集这些性能数据,实现写一个 SDK,大家只需要简单引入就行。 318 | 319 | 最近在研究 perf_hooks 代码的时候发现目前 perf_hooks 的功能还不算完善,很多性能数据并没有上报,目前只支持 HTTP Server 的请求耗时、HTTP 2 和 gc 耗时这些性能数据。所以最近提交了两个 PR 支持了更多性能数据的上报。第一个 PR 是用于支持收集 HTTP Client 的耗时,第二个 PR 是用于支持收集 TCP 连接和 DNS 解析的耗时。在第二个 PR 里,实现了两个通用的方法,方便后续其他模块做性能上报。另外后续有时间的话,希望可以去不断完善 perf_hooks 机制和性能数据收集这块的能力。在从事 Node.js 调试和诊断这个方向的这段时间里,深感到应用层能力的局限,因为我们不是业务方,而是基础能力的提供者,就像前面提到的,哪怕想提供一个收集 HTTP 请求耗时的数据都是非常困难的,而作为基础能力的提供者,我们一直希望我们的能力对业务来说是无感知,无侵入并且是稳定可靠的。所以我们需要不断深入地了解 Node.js 在这方面提供的能力,如果 Node.js 没有提供我们想要的功能,我们只能写 addon 或者尝试给社区提交 PR 来解决。另外我们也在慢慢了解和学习 ebpf,希望能利用 ebpf 从另外一个层面帮助我们解决所碰到的问题。 320 | -------------------------------------------------------------------------------- /docs/chapter28-Node.js底层原理(架构篇).md: -------------------------------------------------------------------------------- 1 | 前言:之前分享了 Node.js 的底层原理,主要是简单介绍了 Node.js 的一些基础原理和一些核心模块的实现,本文从 Node.js 整体方面介绍 Node.js 的底层原理。 2 | 3 | 内容主要包括五个部分。第一部分是首先介绍一下 Node.js 的组成和代码架构。然后介绍一下 Node.js 中的 Libuv, 还有 V8 和模块加载器。最后介绍一下 Node.js 的服务器架构。 4 | 5 | # 1 Node.js 的组成和代码架构 6 | 下面先来看一下Node.js 的组成。Node.js 主要是由 V8、Libuv 和一些第三方库组成。 7 | 1. V8 我们都比较熟悉,它是一个 JS 引擎。但是它不仅实现了 JS 解析和执行,它还是自定义拓展。比如说我们可以通过 V8 提供一些 C++ API 去定义一些全局变量,这样话我们在 JS 里面去使用这个变量了。正是因为 V8 支持这个自定义的拓展,所以才有了 Node.js 等 JS 运行时。 8 | 2. Libuv 是一个跨平台的异步 IO 库。它主要的功能是它封装了各个操作系统的一些 API, 提供网络还有文件进程的这些功能。我们知道在 JS 里面是没有网络文件这些功能的,在前端时,是由浏览器提供的,而在 Node.js 里,这些功能是由 Libuv 提供的。 9 | 3. 另外 Node.js 里面还引用了很多第三方库,比如 DNS 解析库,还有 HTTP 解析器等等。 10 | 11 | 接下来看一下 Node.js 代码整体的架构。 12 | ![](https://img-blog.csdnimg.cn/5918a986cceb41b0b34019012c45d666.png) 13 | Node.js 代码主要是分为三个部分,分别是C、C++ 和 JS。 14 | 1. JS 代码就是我们平时在使用的那些 JS 的模块,比方说像 http 和 fs 这些模块。 15 | 2. C++ 代码主要分为三个部分,第一部分主要是封装 Libuv 和第三方库的 C++ 代码,比如net 和 fs 这些模块都会对应一个 C++ 模块,它主要是对底层的一些封装。第二部分是不依赖 Libuv 和第三方库的 C++ 代码,比方像 Buffer 模块的实现。第三部分 C++ 代码是 V8 本身的代码。 16 | 3. C 语言代码主要是包括 Libuv 和第三方库的代码,它们都是纯 C 语言实现的代码。 17 | 18 | 了解了 Nodejs 的组成和代码架构之后,再来看一下 Node.js 中各个主要部分的实现。 19 | # 2 Node.js 中的 Libuv 20 | 首先来看一下 Node.js 中的 Libuv,下面从三个方面介绍 Libuv。 21 | 1. 介绍 Libuv 的模型和限制 22 | 2. 介绍线程池解决的问题和带来的问题 23 | 3. 介绍事件循环 24 | 25 | ## 2.1 Libuv 的模型和限制 26 | Libuv 本质上是一个生产者消费者的模型。 27 | ![](https://img-blog.csdnimg.cn/f087a85959244fdb95628b6b74623aa0.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAdGhlYW5hcmto,size_20,color_FFFFFF,t_70,g_se,x_16) 28 | 从上面这个图中,我们可以看到在 Libuv 中有很多种生产任务的方式,比如说在一个回调里,在 Node.js 初始化的时候,或者在线程池完成一些操作的时候,这些方式都可以生产任务。然后 Libuv 会不断的去消费这些任务,从而驱动着整个进程的运行,这就是我们一直说的事件循环。 29 | 30 | 但是生产者的消费者模型存在一个问题,就是消费者和生产者之间,怎么去同步?比如说在没有任务消费的时候,这个消费者他应该在干嘛?第一种方式是消费者可以睡眠一段时间,睡醒之后,他会去判断有没有任务需要消费,如果有的话就继续消费,如果没有的话他就继续睡眠。很显然这种方式其实是比较低效的。第二种方式是消费者会把自己挂起,也就是说这个消费所在的进程会被挂起,然后等到有任务的时候,操作系统就会唤醒它,相对来说,这种方式是更高效的,Libuv 里也正是使用这种方式。 31 | ![](https://img-blog.csdnimg.cn/868cde84cc324b4db7ef6056411b83dd.png) 32 | 这个逻辑主要是由事件驱动模块实现的,下面看一下事件驱动的大致的流程。 33 | ![](https://img-blog.csdnimg.cn/b510e13c3aff49d8a4be107d1d6e3b47.png) 34 | 应用层代码可以通过事件驱动模块订阅 fd 的事件,如果这个事件还没有准备好的话,那么这个进程就会被挂起。然后等到这个 fd 所对应的事件触发了之后,就会通过事件驱动模块回调应用层的代码。 35 | 36 | 下面以 Linux 的 事件驱动模块 epoll 为例,来看一下使用流程。 37 | 1. 首先通过 epoll_create 去创建一个epoll 实例。 38 | 2. 然后通过 epoll_ctl 这个函数订阅、修改或者取消订阅一个 fd 的一些事件。 39 | 3. 最后通过 epoll_wait 去判断当前订阅的事件有没有发生,如果有事情要发生的话,那么就直接执行上层回调,如果没有事件发生的话,这种时候可以选择不阻塞,定时阻塞或者一直阻塞,直到有事件发生。要不要阻塞或者说阻塞多久,是根据当前系统的情况。比如 Node.js 里面如果有定时器的节点的话,那么 Node.js 就会定时阻塞,这样就可以保证定时器可以按时执行。 40 | 41 | 接下来再深入一点去看一下 epoll 的大致的实现。 42 | ![](https://img-blog.csdnimg.cn/0d0d7ee0d61d463eb7c8fc9fbef86901.png) 43 | 当应用层代码调用事件驱动模块订阅 fd 的事件时,比如说这里是订阅一个可读事件。那么事件驱动模块它就会往这个 fd 的队列里面注册一个回调,如果当前这个事件还没有触发,这个进程它就会被阻塞。等到有一块数据写入了这个 fd 时,也就是说这个 fd 有可读事件了,操作系统就会执行事件驱动模块的回调,事件驱动模块就会相应的执行用层代码的回调。 44 | 45 | 但是 epoll 存在一些限制。首先第一个是不支持文件操作的,比方说文件读写这些,因为操作系统没有实现。第二个是不适合执行耗时操作,比如大量 CPU 计算、引起进程阻塞的任务,因为 epoll 通常是搭配单线程的,如果在单线程里执行耗时任务,就会导致后面的任务无法执行。 46 | ## 2.2 线程池解决的问题和带来的问题 47 | 针对这个问题,Libuv 提供的解决方案就是使用线程池。下面来看一下引入了线程池之后, 线程池和主线程的关系。 48 | ![](https://img-blog.csdnimg.cn/bb05210ad4dc4c56b286c8ac14281c82.png) 49 | 从这个图中我们可以看到,当应用层提交任务时,比方说像 CPU 计算还有文件操作,这种时候不是交给主线程去处理的,而是直接交给线程池处理的。线程池处理完之后它会通知主线程。 50 | 51 | 但是引入了多线程后会带来一个问题,就是怎么去保证上层代码跑在单个线程里面。因为我们知道 JS 它是单线程的,如果线程池处理完一个任务之后,直接执行上层回调,那么上层代码就会完全乱了。这种时候就需要一个异步通知的机制,也就是说当一个线程它处理完任务的时候,它不是直接去执行上程回调的,而是通过异步机制去通知主线程来执行这个回调。 52 | ![](https://img-blog.csdnimg.cn/0d15d92da6fd4d7a93f26c5ca6f7031f.png) 53 | Libuv 中具体通过 fd 的方式去实现的。当线程池完成任务时,它会以原子的方式去修改这个 fd 为可读的,然后在主线程事件循环的 Poll IO 阶段时,它就会执行这个可读事件的回调,从而执行上层的回调。可以看到,Node.js 虽然是跑在多线程上面的,但是所有的 JS 代码都是跑在单个线程里的,这也是我们经常讨论的 Node.js 是单线程还是多线程的,从不同的角度去看就会得到不同的答案。 54 | 55 | 56 | 下面的图就是异步任务处理的一个大致过程。 57 | ![](https://img-blog.csdnimg.cn/a5ef3102808a40289632878302601fb1.png) 58 | 比如我们想读一个文件的时候,这时候主线程会把这个任务直接提交到线程池里面去处理,然后主线程就可以继续去做自己的事情了。当在线程池里面的线程完成这个任务之后,它就会往这个主线程的队列里面插入一个节点,然后主线程在 Poll IO 阶段时,它就会去执行这个节点里面的回调。 59 | ## 2.3 事件循环 60 | 了解 Libuv 的一些核心实现之后,下面我们再看一下 Libuv 中一个著名的事件循环。事件循环主要分为七个阶段, 61 | 1. 第一是 timer 阶段,timer 阶段是处理定时器相关的一些任务,比如 Node.js 中的 setTimeout和 setInterval。 62 | 2. 第二是 pending 的阶段, pending 阶段主要处理 Poll IO 阶段执行回调时产生的回调。 63 | 3. 第三是 check、prepare 和 idle 三个阶段,这三个阶段主要处理一些自定义的任务。setImmediate 属于 check 阶段。 64 | 4. 第四是 Poll IO 阶段,Poll IO 阶段主要要处理跟文件描述符相关的一些事件。 65 | 5. 第五是 close 阶段, 它主要是处理,调用了 uv_close 时传入的回调。比如关闭一个 TCP 连接时传入的回调,它就会在这个阶段被执行。 66 | 67 | 下面这个图是各个阶段在事件循环的顺序图。 68 | ![](https://img-blog.csdnimg.cn/63cb6cefcde74d06b86ede0a9ebc68cf.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAdGhlYW5hcmto,size_16,color_FFFFFF,t_70,g_se,x_16) 69 | 下面我们来看一下每个阶段的实现。 70 | 71 | 1. 定时器![](https://img-blog.csdnimg.cn/900367cb0a0546218bd3012817a5b122.png) 72 | Libuv 在底层里面维护了一个最小堆,每个定时节点就是堆里面的一个节点(Node.js 只用了 Libuv 的一个定时器节点),越早超时的节点就在越上面。然后等到定时期阶段的时候, Libuv 就会从上往下去遍历这个最小堆判断当前节点有没有超时,如果没有到期的话,那么后面节点也不需要去判断了,因为最早到期的节点都没到期,那么它后面节点也显然不会到期。如果当前节点到期了,那么就会执行它的回调,并且把它移出这个最小堆。但是为了支持类似 setInterval 这种场景。如果这个节点设置了repeat 标记,那么这个节点它会被重新插入到最小堆中,等待下一次的超时。 73 | 74 | 2. check、idle、prepare 阶段和 pending、close 阶段。 75 | ![](https://img-blog.csdnimg.cn/e486f26d2c6d4aafb435fffb3d5fe31d.png) 76 | 这五个阶段的实现其实类似的,它们都对应自己的一个任务队列。当产生任务的时候,它就会往这个队列里面插入一个节点,等到相应的阶段时,它就会去遍历这个队列里面的每个节点,并且执行它的回调。但是 check idle 还有 prepare 阶段有一个比较特别的地方,就是当这些阶段的节点回调被执行之后,它还会重新插入队列里面,也是说这三个阶段它对应的任务在每一轮的事件循环都会被执行。 77 | 78 | 3. Poll IO 阶段 79 | Poll IO 本质上是对前面讲的事件驱动模块的封装。下面来看一下整体的流程。 80 | ![](https://img-blog.csdnimg.cn/119987209a27487780c1b824394e319f.png) 81 | 82 | 当我们订阅一个 fd 的事件时,Libuv 就会通过 epoll 去注册这个 fd 对应的事件。如果这时候事件没有就绪,那么进程就会阻塞在 epoll_wait 中。等到这事件触发的时候,进程就会被唤醒,唤醒之后,它就遍历 epoll 返回了事件列表,并执行上层回调。 83 | 84 | 现在有一个底层能力,那么这个底层能力是怎么暴露给上层的 JS 去使用呢?这种时候就需要用到 JS 引擎 V8了。 85 | 86 | # 3. Node.js 中的 V8 87 | 下面从三个方面介绍 V8。 88 | 1. 介绍 V8 在 Node.js 的作用和 V8 的一些基础概念 89 | 2. 介绍如何通过 V8 执行 JS 和拓展 JS 90 | 3. 介绍如何通过 V8 实现 JS 和 C++ 通信 91 | 92 | ## 3.1 V8 在 Node.js 的作用和基础概念 93 | V8 在 Node.js 里面主要是有两个作用,第一个是负责解析和执行 JS。第二个是支持拓展 JS 能力,作为这个 JS 和 C++ 的桥梁。下面我们先来看一下 V8 里面那些重要的概念。 94 | 95 | Isolate:首先第一个是 Isolate 它是代表一个 V8 的实例,它相当于这一个容器。通常一个线程里面会有一个这样的实例。比如说在 Node.js主线程里面,它就会有一个 Isolate 实例。 96 | 97 | Context:Context 是代表我们执行代码的一个上下文,它主要是保存像 Object,Function 这些我们平时经常会用到的内置的类型。如果我们想拓展 JS 功能,就可以通过这个对象实现。 98 | 99 | ObjectTemplate:ObjectTemplate 是用于定义对象的模板,然后我们就可以基于这个模板去创建对象。 100 | 101 | FunctionTemplate:FunctionTemplate 和 ObjectTemplate 是类似的,它主要是用于定义一个函数的模板,然后就可以基于这个函数模板去创建一个函数。 102 | 103 | FunctionCallbackInfo: 用于实现 JS 和 C++ 通信的对象。 104 | 105 | Handle:Handle 是用管理在 V8 堆里面那些对象,因为像我们平时定义的对象和数组,它是存在 V8 堆内存里面的。Handle 就是用于管理这些对象。 106 | 107 | HandleScope:HandleScope 是一个 Handle 容器,HandleScope 里面可以定义很多 Handle,它主要是利用自己的生命周期管理多个 Handle。 108 | 109 | 下面我们通过一个代码来看一下 HandleScope 和 Handle 它们之间的关系。 110 | ![](https://img-blog.csdnimg.cn/77c5c18b999a45b0a95b1815ac4e1264.png) 111 | 首先第一步新建一个 HandleScope,就会在一个栈里面定义一个 HandleScope 对象。然后第二步新建了一个 Handle 并且把它指向一个堆对象。这时候就会在栈里面分配一个叫 Local 对象,然后在堆里面分配一块 slot 所代表的内存和一个 Object 对象,并且建立关联关系。当执行完这个函数的时候,这个栈就会被清空,相应的这个 slot 代表的内存也会被释放,但是 Object 所代表这个对象,它是不会立马被释放的,它会等待 GC 的回收。 112 | 113 | ## 3.2 通过 V8 执行 JS 和拓展 JS 114 | 了解了 V8 的基础概念之后,来看一下怎么通过 V8 执行一段 JS 的代码。 115 | ![](https://img-blog.csdnimg.cn/64be290cd1bf425e86fbc4228d4ee3af.png) 116 | 首先第一步新建一个 Isolate,它这表示一个隔离的实例。第二步定义一个 HandleScope 对象,因为我们下面需要定义 Handle。第三步定义一个 Context,这是代码执行所在的上下文。第四步定义一些需要被执行的 JS 代码。第五步通过 Script 对象的 Compile 函数编译 JS 代码。编译完之后,我们会得到一个 Script 对象,然后执行这个对象的 Run 函数就可以完成代码的执行。 117 | 118 | 接下来再看一下怎么去拓展 JS 原有的一些能力。 119 | ![](https://img-blog.csdnimg.cn/06d18854d1e440cf9a5fbb2fcf952502.png) 120 | 首先第一步是通过 Context 上下文对象拿到一个全局的对象,类似于在前端里面的 window 对象。第二步通过 ObjectTemplate 新建一个对象的模板,然后接着会给这个对象模板设置一个 test 属性, 值是函数。接着通过这个对象模板新建一个对象,并且把这个对象设置到一个全局变量里面去。这样我们就可以在 JS 层去访问这个全局对象。 121 | 122 | 下面我们通过使用刚才定义那个全局对象来看一下 JS 和 C++ 是怎么通信的。 123 | 124 | ## 3.3 通过 V8 实现 JS 和 C++ 层通信 125 | ![](https://img-blog.csdnimg.cn/f891205f648f431699662e9f0c37c18b.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAdGhlYW5hcmto,size_20,color_FFFFFF,t_70,g_se,x_16) 126 | 当在 JS 层调用刚才定义 test 函数时,就会相应的执行 C++ 层的 test 函数。这个函数有一个入参是 FunctionCallbackInfo,在 C++ 中可以通过这个对象拿到 JS 传来一些参数,这样就完成了 JS 层到 C++ 层通信。经过一系列处理之后,还是可以通过这个对象给 JS 层设置需要返回给 JS 的内容,这样可以完成了 C++ 层到 JS 层的通信。 127 | 128 | 现在有了底层能力,有了这一层的接口,但是我们是怎么去加载后执行 JS 代码呢?这时候就需要模块加载器。 129 | # 4 Node.js 中的模块加载器 130 | Node.js 中有五种模块加载器。 131 | 1. JSON 模块加载器 132 | 2. 用户 JS 模块加载器 133 | 3. 原生 JS 模块加载器 134 | 4. 内置 C++ 模块加载器 135 | 5. Addon 模块加载器 136 | 137 | 现在来看下每种模块加载器。 138 | ## 4.1 JSON 模块加载器 139 | JSON 模块加载器实现比较简单,Node.js 从硬盘里面把 JSON 文件读到内存里面去,然后通过 JSON.parse 函数进行解析,就可以拿到里面的数据。 140 | ![](https://img-blog.csdnimg.cn/98e8e115e19148e49cd2d01df37ea56a.png) 141 | 142 | ## 4.2 用户 JS 模块 143 | ![](https://img-blog.csdnimg.cn/be9b854677bc4ef09475ada75490dda2.png) 144 | 用户 JS 模块就是我们平时写的一些 JS 代码。当通过 require 函数加载一个用户 JS 模块时,Node.js 就会从硬盘读取这个模块的内容到内存中,然后通过 V8 提供了一个函数叫 CompileFunctionInContext 把读取的代码封装成一个函数,接着新建立一个 Module 对象。这个对象里面有两个属性叫 exports 和 require 函数,这两个对象就是我们平时在代码里面所使用的变量,接着会把这个对象作为函数的参数,并且执行这个函数,执行完这个函数的时候,就可以通过 module.exports 拿到这个函数(模块)里面导出的内容。这里需要注意的是这里的 require 函数是可以加载原生 JS 模块和用户模块的,所以我们平时在我们代码里面,可以通过require 加载我们自己写的模块,或者 Node.js 本身提供的 JS 模块。 145 | 146 | ## 4.3 原生 JS 模块 147 | ![](https://img-blog.csdnimg.cn/1a80ee5c759f4a26ab3140637025d02c.png) 148 | 接下来看下原生 JS 模块加载器。原生JS 模块是 Node.js 本身提供了一些 JS 模块,比如经常使用的 http 和 fs。当通过 require 函数加载 http 这个模块的时候,Node.js 就会从内存里读取这个模块所对应内容。因为原生 JS 模块默认是打包进内存里面的,所以直接从内存里面读就可以了,不需要从硬盘里面去读。然后还是通过 V8 提供的 CompileFunctionInContext 这个函数把读取的代码封装成一个函数,接着新建一个 NativeModule 对象,同样这个对象里面也是有个 exports 属性,接着它会把这个对象传到这个函数里面去执行,执行完这函数之后,就可以通过 module.exports 拿到这个函数里面导出的内容。需要注意是这里传入的 require 函数是一个叫 NativeModuleRequire 函数,这个函数它就只能加载原生 JS 模块。另外这里还传了另外一个 internalBinding 函数,这个函数是用于加载 C++ 模块的,所以在原生 JS 模块里面,是可以加载 C++ 模块的。 149 | 150 | ## 4.4 C++ 模块 151 | ![](https://img-blog.csdnimg.cn/a9e0c30f337e4c9683700ec4a4f31375.png) 152 | Node.js 在初始化的时候会注册 C++ 模块,并且形成一个 C++ 模块链表。当加载 C++ 模块时,Node.js 就通过模块名,从这个链表里面找到对应的节点,然后去执行它里面的钩子函数,执行完之后就可以拿到 C++ 模块导出的内容。 153 | 154 | ## 4.5 Addon 模块 155 | ![](https://img-blog.csdnimg.cn/a6e840e5d19d42068d66a8264b43c809.png) 156 | 接着再来看一下 Addon 模块, Addon 模块本质上是一个动态链接库。当通过 require 加载Addon 模块的时候,Node.js 会通过 dlopen 这个函数去加载这个动态链接库。 157 | 下图是我们定义一个 Addon 模块时的一个标准格式。 158 | ![](https://img-blog.csdnimg.cn/c2ee20e79ac4434fbf210c82cabf11c3.png) 159 | 它里面有一些 C语言宏,宏展开之后里面内容像下图所示。 160 | ![](https://img-blog.csdnimg.cn/2f80ee58332c464891d9c000b1416051.png) 161 | 里面主要定义了一个结构体和一个函数,这个函数会把这个结构体赋值给 Node.js 的一个全局变量,然后 Nodejs 它就可以通过全局变量拿到这个结构体,并且执行它里面的一个钩子函数,执行完之后就可以拿到它里面要导出的一些内容。 162 | 163 | 现在有了底层的能力,也有了这一次层的接口,也有了代码加载器。最后我们来看一下 Node.js 作为一个服务器的时候,它的架构是怎么样的? 164 | # 5 Node.js 的服务器架构 165 | 下面从两个方面介绍 Node.js 的服务器架构 166 | 1. 介绍服务器处理 TCP 连接的模型 167 | 2. 介绍 Node.js 中的实现和存在的问题 168 | 169 | ## 5.1 处理 TCP 连接的模型 170 | 171 | 首先来看一下网络编程中怎么去创建一个 TCP 服务器。 172 | ```c 173 | int fd = socket(…); 174 | bind(fd, 监听地址); 175 | listen(fd); 176 | ``` 177 | 首先建一个 socket, 然后把需要监听的地址绑定到这个 socket 中,最后通过 listen 函数启动服务器。启动服务器之后,那么怎么去处理 TCP 连接呢? 178 | 179 | 1. 串行处理(accept 和 handle 都会引起进程阻塞) 180 | ![](https://img-blog.csdnimg.cn/5702b11159374458a7a477879a36a727.png) 181 | 第一种处理方式是串行处理,串行方式就是在一个 while 循环里面,通过 accept 函数不断地摘取 TCP 连接,然后处理它。这种方式的缺点就是它每次只能处理一个连接,处理完一个连接之后,才能继续处理下一个连接。 182 | 183 | 2. 多进程/多线程 184 | ![](https://img-blog.csdnimg.cn/f77fdca6ab334975abc9652599ed83c0.png) 185 | 第二种方式是多进程或者多线程的方式。这种方式主要是利用多个进程或者线程同时处理多个连接。但这种模式它的缺点就是当流量非常大的时候,进程数或者线程数它会成为这种架构下面的一个瓶颈,因为我们不能无限的创建进程或者线程,像 Apache 还有 PHP 就是这种架构的。 186 | 187 | 3. 单进程单线程 + 事件驱动( Reactor & Proactor ) 188 | 第三种就是单线程 + 事件驱动的模式。这种模式下有两种类型,第一种叫 Reactor, 第二种叫 Proactor。 189 | Reactor 模式就是应用程序可以通过事件驱动模块注册 fd 的读写事件,然后事件触发的时候,它就会通过事件驱动模块回调上层的代码。 190 | ![](https://img-blog.csdnimg.cn/fb8f0de4d3bb42f983084a87a217c56d.png) 191 | Proactor 模式就是应用程序可以通过事件驱动模块注册 fd 的读写完成事件,然后这个读写完成事件后就会通过事件驱动模块回调上层代码。 192 | ![](https://img-blog.csdnimg.cn/a3efc521413c4eb7837315b83a74aa6d.png) 193 | 我们看到这两种模式的区别是,数据读写是由内核完成的,还是由应用程序完成的。很显然,通过内核去完成是更高效的,但是因为 Proactor 这种模式它兼容性还不是很好,所以目前用的还不算太多,主要目前主流的一些服务器,它用的都是 Reactor 模式。比方说像 Node.js、Redis 和 Nginx 这些服务器用的都是这种模式。 194 | 195 | 刚才提到 Node.js 是单进程单线程加事件驱动的架构。那么单线程的架构它怎么去利用多核呢?这种时候就需要用到多进程的这种模式了,每一个进程里面会包含一个Reactor 模式。但是引入多进程之后,它会带来一个问题,就是多进程之间它怎么去监听同一个端口。 196 | 197 | ## 5.2 Node.js 的实现和问题 198 | 下面来看下针对多进程监听同一个端口的一些解决方式。 199 | 1. 主进程监听端口并接收请求,轮询分发(轮询模式) 200 | 2. 子进程竞争接收请求(共享模式) 201 | 3. 子进程负载均衡处理连接(SO_REUSEPORT 模式) 202 | 203 | 第一种方式就是主进程去监听这个端口,并且接收连接。它接收连接之后,通过一定的算法(比如轮询)分发给各个子进程。这种模式。它的一个缺点就是当流量非常大的时候,这个主进程就会成为瓶颈,因为它可能都来不及接收或者分发这个连接给子进程去处理。 204 | ![](https://img-blog.csdnimg.cn/9207d9dcac734019934a1597901aa995.png) 205 | 206 | 第二种就是主进程创建监听 socket, 然后子进程通过 fork 的方式继承这个监听的 socket, 当有一个连接到来的时候,操作系统就唤醒所有的子进程,所有子进程会以竞争的方式接收连接。这种模式,它的缺点主要是有两个,第一个就是负载均衡的问题,因为操作系统唤醒了所有的进程,可能会导致某一个进程一直在处理连接,其他其它进程都没机会处理连接。然后另外一个问题就是惊群的问题,因为操作系统唤起了所有的进程,但是只有一个进程它会处理这个连接,然后剩下进程就会被无效地唤醒。这种方式会造成一定的性能的损失。 207 | ![](https://img-blog.csdnimg.cn/124fdeabd33f4fe8afceff8f2668ca78.png) 208 | 209 | 第三种通过 SO_REUSEPORT 这个标记来解决刚才提到的两个问题。在这种模式下,每个子进程都会有一个独立的监听 socket 和连接队列。当有一个连接到来的时候,操作系统会把这个连接分发给某一个子进程并且唤醒它。这样就可以解决惊群的问题,因为它只会唤醒一个子进程。又因为操作系统分发这个连接的时候,内部是有一个负载均衡的算法。所以这样的话又可以解决负载均衡的问题。 210 | ![](https://img-blog.csdnimg.cn/2e26fafdfae44591947a1af4b87b1fed.png) 211 | 212 | 213 | 接下来我们看一下 Node.js 中的实现。 214 | 1. 轮询模式。 215 | 在这种模式下,主进程会 fork 多个子进程,然后每个子进程里面都会调用 listen 函数。但是 listen 函数不会监听一个端口,它会请求主进程监听这个端口,当有连接到来的时候,这个主进程就会接收这个连接,然后通过文件描述符的方式传给各个子进程去处理。 216 | ![](https://img-blog.csdnimg.cn/70b100b08aa54e87a05169fa321b9314.png) 217 | 2. 共享模式 218 | 共享模式下,主进程同样还是会 fork 多个子进程,然后每个子进程里面还是会执行 listen 函数,但同样的这个 listen 函数不会监听一个端口,它会请求主进程创建一个 socket 并绑定到一个需要监听的地址,接着主进程会把这个 socket 通过文件描述符传递的方式传给多个子进程,这样就可以达到多个子进程同时监听同一个端口的效果。 219 | ![](https://img-blog.csdnimg.cn/7115c5129432484ca0253898e8c189e8.png) 220 | 通过刚才介绍,我们可以知道 Node.js 的服务器架构存在的问题。如果我们使用轮询模式,当流量比较大的时候,那么这个主进程就会成为系统瓶颈。如果我们使用共享模式,就会存在惊群和负载均衡的问题。不过在 Libuv 里面,可以通过设置 UV_TCP_SINGLE_ACCEPT 环境变量来一定程度缓解这个问题。当我们设置了这个环境变量。Libuv 在接收完一个连接的时候,它就会休眠一会,让其它进程也有接收连接的机会。 221 | 222 | 最后来总结一下,本文的内容。Node.js 里面通过 Libuv 解决了操作系统相关的问题。通过 V8 解决了执行 JS 和拓展 JS 功能的问题。通过模块加载器解决了代码加载还有组织的问题。通过多进程的服务器架构,使得 Node.js 可以利用多核,并且解决了多个进程监听同一个端口的问题。 223 | 224 | 下面是一些资料,有兴趣的同学也可以看一下。 225 | 1. 基于 epoll + V8 的JS 运行时 Just: 226 | https://github.com/theanarkh/read-just-0.1.4-code 227 | 2. 基于 io_uring+ V8 的 JS 运行时 No.js: 228 | https://github.com/theanarkh/No.js 229 | 3. 理解 Node.js 原理: 230 | https://github.com/theanarkh/understand-nodejs 231 | -------------------------------------------------------------------------------- /docs/chapter22-events模块.md: -------------------------------------------------------------------------------- 1 | 2 | events模块是Node.js中比较简单但是却非常核心的模块,Node.js中,很多模块都继承于events模块,events模块是发布、订阅模式的实现。我们首先看一个如果使用events模块。 3 | 4 | ```js 5 | const { EventEmitter } = require('events'); 6 | class Events extends EventEmitter {} 7 | const events = new Events(); 8 | events.on('demo', () => { 9 | console.log('emit demo event'); 10 | }); 11 | events.emit('demo'); 12 | ``` 13 | 14 | 接下来我们看一下events模块的具体实现。 15 | ## 22.1 初始化 16 | 当new一个EventEmitter或者他的子类时,就会进入EventEmitter的逻辑。 17 | 18 | ```js 19 | function EventEmitter(opts) { 20 | EventEmitter.init.call(this, opts); 21 | } 22 | 23 | EventEmitter.init = function(opts) { 24 | // 如果是未初始化或者没有自定义_events,则初始化 25 | if (this._events === undefined || 26 | this._events === ObjectGetPrototypeOf(this)._events) { 27 | this._events = ObjectCreate(null); 28 | this._eventsCount = 0; 29 | } 30 | // 初始化处理函数个数的阈值 31 | this._maxListeners = this._maxListeners || undefined; 32 | 33 | // 是否开启捕获promise reject,默认false 34 | if (opts && opts.captureRejections) { 35 | this[kCapture] = Boolean(opts.captureRejections); 36 | } else { 37 | this[kCapture] = EventEmitter.prototype[kCapture]; 38 | } 39 | }; 40 | ``` 41 | 42 | EventEmitter的初始化主要是初始化了一些数据结构和属性。唯一支持的一个参数就是captureRejections,captureRejections表示当触发事件,执行处理函数时,EventEmitter是否捕获处理函数中的异常。后面我们会详细讲解。 43 | ## 22.2 订阅事件 44 | 初始化完EventEmitter之后,我们就可以开始使用订阅、发布的功能。我们可以通过addListener、prependListener、on、once订阅事件。addListener和on是等价的,prependListener的区别在于处理函数会被插入到队首,而默认是追加到队尾。once注册的处理函数,最多被执行一次。四个API都是通过_addListener函数实现的。下面我们看一下具体实现。 45 | 46 | ```js 47 | function _addListener(target, type, listener, prepend) { 48 | let m; 49 | let events; 50 | let existing; 51 | events = target._events; 52 | // 还没有初始化_events则初始化 53 | if (events === undefined) { 54 | events = target._events = ObjectCreate(null); 55 | target._eventsCount = 0; 56 | } else { 57 | /* 58 | 是否定义了newListener事件,是的话先触发,如果监听了newListener事件, 59 | 每次注册其他事件时都会触发newListener,相当于钩子 60 | */ 61 | if (events.newListener !== undefined) { 62 | target.emit('newListener', type, 63 | listener.listener ? listener.listener : listener); 64 | // 可能会修改_events,这里重新赋值 65 | events = target._events; 66 | } 67 | // 判断是否已经存在处理函数 68 | existing = events[type]; 69 | } 70 | // 不存在则以函数的形式存储,否则是数组 71 | if (existing === undefined) { 72 | events[type] = listener; 73 | ++target._eventsCount; 74 | } else { 75 | if (typeof existing === 'function') { 76 | existing = events[type] = 77 | prepend ? [listener, existing] : [existing, listener]; 78 | } else if (prepend) { 79 | existing.unshift(listener); 80 | } else { 81 | existing.push(listener); 82 | } 83 | 84 | // 处理告警,处理函数过多可能是因为之前的没有删除,造成内存泄漏 85 | m = _getMaxListeners(target); 86 | if (m > 0 && existing.length > m && !existing.warned) { 87 | existing.warned = true; 88 | const w = new Error('Possible EventEmitter memory leak detected. ' + 89 | `${existing.length} ${String(type)} listeners ` + 90 | `added to ${inspect(target, { depth: -1 })}. Use ` + 91 | 'emitter.setMaxListeners() to increase limit'); 92 | w.name = 'MaxListenersExceededWarning'; 93 | w.emitter = target; 94 | w.type = type; 95 | w.count = existing.length; 96 | process.emitWarning(w); 97 | } 98 | } 99 | 100 | return target; 101 | } 102 | ``` 103 | 104 | 接下来我们看一下once的实现,对比其他几种api,once的实现相对比较难,因为我们要控制处理函数最多执行一次,所以我们需要坚持用户定义的函数,保证在事件触发的时候,执行用户定义函数的同时,还需要删除注册的事件。 105 | 106 | ```js 107 | EventEmitter.prototype.once = function once(type, listener) { 108 | this.on(type, _onceWrap(this, type, listener)); 109 | return this; 110 | }; 111 | 112 | function onceWrapper() { 113 | // 还没有触发过 114 | if (!this.fired) { 115 | // 删除他 116 | this.target.removeListener(this.type, this.wrapFn); 117 | // 触发了 118 | this.fired = true; 119 | // 执行 120 | if (arguments.length === 0) 121 | return this.listener.call(this.target); 122 | return this.listener.apply(this.target, arguments); 123 | } 124 | } 125 | // 支持once api 126 | function _onceWrap(target, type, listener) { 127 | // fired是否已执行处理函数,wrapFn包裹listener的函数 128 | const state = { fired: false, wrapFn: undefined, target, type, listener }; 129 | // 生成一个包裹listener的函数 130 | const wrapped = onceWrapper.bind(state); 131 | // 把原函数listener也挂到包裹函数中,用于事件没有触发前,用户主动删除,见removeListener 132 | wrapped.listener = listener; 133 | // 保存包裹函数,用于执行完后删除,见onceWrapper 134 | state.wrapFn = wrapped; 135 | return wrapped; 136 | } 137 | ``` 138 | 139 | ## 22.3 触发事件 140 | 分析完事件的订阅,接着我们看一下事件的触发。 141 | 142 | ```js 143 | EventEmitter.prototype.emit = function emit(type, ...args) { 144 | // 触发的事件是否是error,error事件需要特殊处理 145 | let doError = (type === 'error'); 146 | 147 | const events = this._events; 148 | // 定义了处理函数(不一定是type事件的处理函数) 149 | if (events !== undefined) { 150 | // 如果触发的事件是error,并且监听了kErrorMonitor事件则触发kErrorMonitor事件 151 | if (doError && events[kErrorMonitor] !== undefined) 152 | this.emit(kErrorMonitor, ...args); 153 | // 触发的是error事件但是没有定义处理函数 154 | doError = (doError && events.error === undefined); 155 | } else if (!doError) // 没有定义处理函数并且触发的不是error事件则不需要处理, 156 | return false; 157 | 158 | // If there is no 'error' event listener then throw. 159 | // 触发的是error事件,但是没有定义处理error事件的函数,则报错 160 | if (doError) { 161 | let er; 162 | if (args.length > 0) 163 | er = args[0]; 164 | // 第一个入参是Error的实例 165 | if (er instanceof Error) { 166 | try { 167 | const capture = {}; 168 | /* 169 | 给capture对象注入stack属性,stack的值是执行Error.captureStackTrace 170 | 语句的当前栈信息,但是不包括emit的部分 171 | */ 172 | Error.captureStackTrace(capture, EventEmitter.prototype.emit); 173 | ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { 174 | value: enhanceStackTrace.bind(this, er, capture), 175 | configurable: true 176 | }); 177 | } catch {} 178 | throw er; // Unhandled 'error' event 179 | } 180 | 181 | let stringifiedEr; 182 | const { inspect } = require('internal/util/inspect'); 183 | try { 184 | stringifiedEr = inspect(er); 185 | } catch { 186 | stringifiedEr = er; 187 | } 188 | const err = new ERR_UNHANDLED_ERROR(stringifiedEr); 189 | err.context = er; 190 | throw err; // Unhandled 'error' event 191 | } 192 | // 获取type事件对应的处理函数 193 | const handler = events[type]; 194 | // 没有则不处理 195 | if (handler === undefined) 196 | return false; 197 | // 等于函数说明只有一个 198 | if (typeof handler === 'function') { 199 | // 直接执行 200 | const result = ReflectApply(handler, this, args); 201 | // 非空判断是不是promise并且是否需要处理,见addCatch 202 | if (result !== undefined && result !== null) { 203 | addCatch(this, result, type, args); 204 | } 205 | } else { 206 | // 多个处理函数,同上 207 | const len = handler.length; 208 | const listeners = arrayClone(handler, len); 209 | for (let i = 0; i < len; ++i) { 210 | const result = ReflectApply(listeners[i], this, args); 211 | if (result !== undefined && result !== null) { 212 | addCatch(this, result, type, args); 213 | } 214 | } 215 | } 216 | 217 | return true; 218 | } 219 | ``` 220 | 221 | 我们看到在Node.js中,对于error事件是特殊处理的,如果用户没有注册error事件的处理函数,可能会导致程序挂掉,另外我们看到有一个addCatch的逻辑,addCatch是为了支持事件处理函数为异步模式的情况,比如async函数或者返回Promise的函数。 222 | 223 | ```js 224 | function addCatch(that, promise, type, args) { 225 | // 没有开启捕获则不需要处理 226 | if (!that[kCapture]) { 227 | return; 228 | } 229 | // that throws on second use. 230 | try { 231 | const then = promise.then; 232 | 233 | if (typeof then === 'function') { 234 | // 注册reject的处理函数 235 | then.call(promise, undefined, function(err) { 236 | process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); 237 | }); 238 | } 239 | } catch (err) { 240 | that.emit('error', err); 241 | } 242 | } 243 | 244 | function emitUnhandledRejectionOrErr(ee, err, type, args) { 245 | // 用户实现了kRejection则执行 246 | if (typeof ee[kRejection] === 'function') { 247 | ee[kRejection](err, type, ...args); 248 | } else { 249 | // 保存当前值 250 | const prev = ee[kCapture]; 251 | try { 252 | /* 253 | 关闭然后触发error事件,意义 254 | 1 防止error事件处理函数也抛出error,导致死循环 255 | 2 如果用户处理了error,则进程不会退出,所以需要恢复kCapture的值 256 | 如果用户没有处理error,则nodejs会触发uncaughtException,如果用户 257 | 处理了uncaughtException则需要灰度kCapture的值 258 | */ 259 | ee[kCapture] = false; 260 | ee.emit('error', err); 261 | } finally { 262 | ee[kCapture] = prev; 263 | } 264 | } 265 | } 266 | ``` 267 | 268 | ## 22.4 取消订阅 269 | 我们接着看一下删除事件处理函数的逻辑。 270 | 271 | ```js 272 | function removeAllListeners(type) { 273 | const events = this._events; 274 | if (events === undefined) 275 | return this; 276 | 277 | // 没有注册removeListener事件,则只需要删除数据,否则还需要触发removeListener事件 278 | if (events.removeListener === undefined) { 279 | // 等于0说明是删除全部 280 | if (arguments.length === 0) { 281 | this._events = ObjectCreate(null); 282 | this._eventsCount = 0; 283 | } else if (events[type] !== undefined) { // 否则是删除某个类型的事件, 284 | // 是唯一一个处理函数,则重置_events,否则删除对应的事件类型 285 | if (--this._eventsCount === 0) 286 | this._events = ObjectCreate(null); 287 | else 288 | delete events[type]; 289 | } 290 | return this; 291 | } 292 | 293 | // 说明注册了removeListener事件,arguments.length === 0说明删除所有类型的事件 294 | if (arguments.length === 0) { 295 | // 逐个删除,除了removeListener事件,这里删除了非removeListener事件 296 | for (const key of ObjectKeys(events)) { 297 | if (key === 'removeListener') continue; 298 | this.removeAllListeners(key); 299 | } 300 | // 这里删除removeListener事件,见下面的逻辑 301 | this.removeAllListeners('removeListener'); 302 | // 重置数据结构 303 | this._events = ObjectCreate(null); 304 | this._eventsCount = 0; 305 | return this; 306 | } 307 | // 删除某类型事件 308 | const listeners = events[type]; 309 | 310 | if (typeof listeners === 'function') { 311 | this.removeListener(type, listeners); 312 | } else if (listeners !== undefined) { 313 | // LIFO order 314 | for (let i = listeners.length - 1; i >= 0; i--) { 315 | this.removeListener(type, listeners[i]); 316 | } 317 | } 318 | 319 | return this; 320 | } 321 | ``` 322 | 323 | removeAllListeners函数主要的逻辑有两点,第一个是removeListener事件需要特殊处理,这类似一个钩子,每次用户删除事件处理函数的时候都会触发该事件。第二是removeListener函数。removeListener是真正删除事件处理函数的实现。removeAllListeners是封装了removeListener的逻辑。 324 | 325 | ```js 326 | function removeListener(type, listener) { 327 | let originalListener; 328 | const events = this._events; 329 | // 没有东西可删除 330 | if (events === undefined) 331 | return this; 332 | 333 | const list = events[type]; 334 | // 同上 335 | if (list === undefined) 336 | return this; 337 | // list是函数说明只有一个处理函数,否则是数组,如果list.listener === listener说明是once注册的 338 | if (list === listener || list.listener === listener) { 339 | // type类型的处理函数就一个,并且也没有注册其他类型的事件,则初始化_events 340 | if (--this._eventsCount === 0) 341 | this._events = ObjectCreate(null); 342 | else { 343 | // 就一个执行完删除type对应的属性 344 | delete events[type]; 345 | // 注册了removeListener事件,则先注册removeListener事件 346 | if (events.removeListener) 347 | this.emit('removeListener', type, list.listener || listener); 348 | } 349 | } else if (typeof list !== 'function') { 350 | // 多个处理函数 351 | let position = -1; 352 | // 找出需要删除的函数 353 | for (let i = list.length - 1; i >= 0; i--) { 354 | if (list[i] === listener || list[i].listener === listener) { 355 | // 保存原处理函数,如果有的话 356 | originalListener = list[i].listener; 357 | position = i; 358 | break; 359 | } 360 | } 361 | 362 | if (position < 0) 363 | return this; 364 | // 第一个则出队,否则删除一个 365 | if (position === 0) 366 | list.shift(); 367 | else { 368 | if (spliceOne === undefined) 369 | spliceOne = require('internal/util').spliceOne; 370 | spliceOne(list, position); 371 | } 372 | // 如果只剩下一个,则值改成函数类型 373 | if (list.length === 1) 374 | events[type] = list[0]; 375 | // 触发removeListener 376 | if (events.removeListener !== undefined) 377 | this.emit('removeListener', type, originalListener || listener); 378 | } 379 | 380 | return this; 381 | }; 382 | ``` 383 | 384 | 以上就是events模块的核心逻辑,另外还有一些工具函数就不一一分析。 385 | -------------------------------------------------------------------------------- /docs/chapter20-拓展Node.js.md: -------------------------------------------------------------------------------- 1 | 2 | 拓展Node.js从宏观来说,有几种方式,包括直接修改Node.js内核重新编译分发、提供npm包。npm包又可以分为JS和C++拓展。本章主要是介绍修改Node.js内核和写C++插件。 3 | ## 20.1 修改Node.js内核 4 | 修改Node.js内核的方式也有很多种,我们可以修改JS层、C++、C语言层的代码,也可以新增一些功能或模块。本节分别介绍如何新增一个Node.js的C++模块和修改Node.js内核。相比修改Node.js内核代码,新增一个Node.js内置模块需要了解更多的知识。 5 | ### 20.1.1 新增一个内置C++模块 6 | 1.首先在src文件夹下新增两个文件。 7 | cyb.h 8 | 9 | ```cpp 10 | #ifndef SRC_CYB_H_ 11 | #define SRC_CYB_H_ 12 | #include "v8.h" 13 | 14 | namespace node { 15 | class Environment; 16 | class Cyb { 17 | public: 18 | static void Initialize(v8::Local target, 19 | v8::Local unused, 20 | v8::Local context, 21 | void* priv); 22 | private: 23 | static void Console(const v8::FunctionCallbackInfo& args); 24 | }; 25 | } // namespace node 26 | #endif 27 | ``` 28 | 29 | cyb.cc 30 | 31 | ```cpp 32 | #include "cyb.h" 33 | #include "env-inl.h" 34 | #include "util-inl.h" 35 | #include "node_internals.h" 36 | 37 | namespace node { 38 | using v8::Context; 39 | using v8::Function; 40 | using v8::FunctionCallbackInfo; 41 | using v8::FunctionTemplate; 42 | using v8::Local; 43 | using v8::Object; 44 | using v8::String; 45 | using v8::Value; 46 | 47 | void Cyb::Initialize(Local target, 48 | Local unused, 49 | Local context, 50 | void* priv) { 51 | Environment* env = Environment::GetCurrent(context); 52 | // 申请一个函数模块,模板函数是Console 53 | Local t = env->NewFunctionTemplate(Console); 54 | // 申请一个字符串 55 | Local str = FIXED_ONE_BYTE_STRING(env->isolate(), 56 | "console"); 57 | // 设置函数名 58 | t->SetClassName(str); 59 | // 导出函数,target即exports 60 | target->Set(env->context(), 61 | str, 62 | t->GetFunction(env->context()).ToLocalChecke 63 | d()).Check(); 64 | } 65 | 66 | void Cyb::Console(const FunctionCallbackInfo& args) { 67 | v8::Isolate* isolate = args.GetIsolate(); 68 | v8::Local str = String::NewFromUtf8(isolate, 69 | "hello world"); 70 | args.GetReturnValue().Set(str); 71 | } 72 | 73 | } // namespace node 74 | // 声明该模块 75 | NODE_MODULE_CONTEXT_AWARE_INTERNAL(cyb_wrap, node::Cyb::Initialize) 76 | ``` 77 | 78 | 我们新定义一个模块,是不能自动添加到Node.js内核的,我们还需要额外的操作。 79 | 1 首先我们需要修改node.gyp文件。把我们新增的文件加到配置里,否则编译的时候,不会编译这个新增的模块。我们可以在node.gyp文件中找到src/tcp_wrap.cc,然后在它后面加入我们的文件就行。 80 | 81 | ```text 82 | src/cyb_wrap.cc 83 | src/cyb_wrap.h 84 | ``` 85 | 86 | 这时候Node.js会编译我们的代码了。但是Node.js的内置模块有一定的机制,我们的代码加入了Node.js内核,不代表就可以使用了。Node.js在初始化的时候会调用RegisterBuiltinModules函数注册所有的内置C++模块。 87 | 88 | ```cpp 89 | void RegisterBuiltinModules() { 90 | #define V(modname) _register_##modname(); 91 | NODE_BUILTIN_MODULES(V) 92 | #undef V 93 | } 94 | ``` 95 | 96 | 我们看到该函数只有一个宏。我们看看这个宏。 97 | 98 | ```cpp 99 | void RegisterBuiltinModules() { 100 | #define V(modname) _register_##modname(); 101 | NODE_BUILTIN_MODULES(V) 102 | #undef V 103 | } 104 | #define NODE_BUILTIN_MODULES(V) \ 105 | NODE_BUILTIN_STANDARD_MODULES(V) \ 106 | NODE_BUILTIN_OPENSSL_MODULES(V) \ 107 | NODE_BUILTIN_ICU_MODULES(V) \ 108 | NODE_BUILTIN_REPORT_MODULES(V) \ 109 | NODE_BUILTIN_PROFILER_MODULES(V) \ 110 | NODE_BUILTIN_DTRACE_MODULES(V) 111 | ``` 112 | 113 | 宏里面又是一堆宏。我们要做的就是修改这个宏。因为我们是自定义的内置模块,所以我们可以增加一个宏。 114 | 115 | ```cpp 116 | #define NODE_BUILTIN_EXTEND_MODULES(V) \ 117 | V(cyb_wrap) 118 | ``` 119 | 然后把这个宏追加到那一堆宏后面。 120 | ```cpp 121 | #define NODE_BUILTIN_MODULES(V) \ 122 | NODE_BUILTIN_STANDARD_MODULES(V) \ 123 | NODE_BUILTIN_OPENSSL_MODULES(V) \ 124 | NODE_BUILTIN_ICU_MODULES(V) \ 125 | NODE_BUILTIN_REPORT_MODULES(V) \ 126 | NODE_BUILTIN_PROFILER_MODULES(V) \ 127 | NODE_BUILTIN_DTRACE_MODULES(V) \ 128 | NODE_BUILTIN_EXTEND_MODULES(V) 129 | ``` 130 | 131 | 这时候,Node.js不仅可以编译我们的代码,还会把我们代码中定义的模块注册到内置C++模块里了,接下来就是如何使用C++模块了。 132 | 2 在lib文件夹新建一个cyb.js,作为Node.js原生模块 133 | 134 | ```js 135 | const cyb = internalBinding('cyb_wrap'); 136 | module.exports = cyb; 137 | ``` 138 | 139 | 新增原生模块,我们也需要修改node.gyp文件,否则代码也不会被编译进node内核。我们找到node.gyp文件的lib/net.js,在后面追加lib/cyb.js。该配置下的文件是给js2c.py使用的,如果不修改,我们在require的时候,就会找不到该模块。最后我们在lib/internal/bootstrap/loader文件里找到internalBindingWhitelist变量,在数组最后增加cyb_wrap,这个配置是给process.binding函数使用的,如果不修改这个配置,通过process.binding就找不到我们的模块。process.binding是可以在用户JS里使用的。至此,我们完成了所有的修改工作,重新编译Node.js。然后编写测试程序。 140 | 3 新建一个测试文件testcyb.js 141 | 142 | ```js 143 | // const cyb = process.binding('cyb_wrap'); 144 | const cyb = require('cyb'); 145 | console.log(cyb.console()) 146 | ``` 147 | 148 | 可以看到,会输出hello world。 149 | ### 20.1.2 修改Node.js内核 150 | 本节介绍如何修改Node.js内核。修改的部分主要是为了完善Node.js的TCP keepalive功能。目前Node.js的keepalive只支持设置开关以及空闲多久后发送探测包。在新版Linux内核中,TCP keepalive包括以下配置。 151 | 152 | ``` 153 | 1 多久没有通信数据包,则开始发送探测包。 154 | 2 每隔多久,再次发送探测包。 155 | 3 发送多少个探测包后,就认为连接断开。 156 | 4 TCP_USER_TIMEOUT,发送了数据,多久没有收到ack后,认为连接断开。 157 | ``` 158 | 159 | Node.js只支持第一条,所以我们的目的是支持2,3,4。因为这个功能是操作系统提供的,所以首先需要修改Libuv的代码。 160 | 1 修改src/unix/tcp.c 161 | 在tcp.c加入以下代码 162 | 163 | ```js 164 | int uv_tcp_keepalive_ex(uv_tcp_t* handle, 165 | int on, 166 | unsigned int delay, 167 | unsigned int interval, 168 | unsigned int count) { 169 | int err; 170 | 171 | if (uv__stream_fd(handle) != -1) { 172 | err =uv__tcp_keepalive_ex(uv__stream_fd(handle), 173 | on, 174 | delay, 175 | interval, 176 | count); 177 | if (err) 178 | return err; 179 | } 180 | 181 | if (on) 182 | handle->flags |= UV_HANDLE_TCP_KEEPALIVE; 183 | else 184 | handle->flags &= ~UV_HANDLE_TCP_KEEPALIVE; 185 | return 0; 186 | } 187 | 188 | int uv_tcp_timeout(uv_tcp_t* handle, unsigned int timeout) { 189 | #ifdef TCP_USER_TIMEOUT 190 | int fd = uv__stream_fd(handle); 191 | if (fd != -1 && setsockopt(fd, 192 | IPPROTO_TCP, 193 | TCP_USER_TIMEOUT, 194 | &timeout, 195 | sizeof(timeout))) { 196 | return UV__ERR(errno); 197 | } 198 | #endif 199 | return 0; 200 | } 201 | 202 | int uv__tcp_keepalive_ex(int fd, 203 | int on, 204 | unsigned int delay, 205 | unsigned int interval, 206 | unsigned int count) { 207 | if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on))) 208 | return UV__ERR(errno); 209 | 210 | #ifdef TCP_KEEPIDLE 211 | if (on && delay &&setsockopt(fd, 212 | IPPROTO_TCP, 213 | TCP_KEEPIDLE, 214 | &delay, 215 | sizeof(delay))) 216 | return UV__ERR(errno); 217 | #endif 218 | #ifdef TCP_KEEPINTVL 219 | if (on && interval && setsockopt(fd, 220 | IPPROTO_TCP, 221 | TCP_KEEPINTVL, 222 | &interval, 223 | sizeof(interval))) 224 | return UV__ERR(errno); 225 | #endif 226 | #ifdef TCP_KEEPCNT 227 | if (on && count && setsockopt(fd, 228 | IPPROTO_TCP, 229 | TCP_KEEPCNT, 230 | &count, 231 | sizeof(count))) 232 | return UV__ERR(errno); 233 | #endif 234 | /* Solaris/SmartOS, if you don't support keep-alive, 235 | * then don't advertise it in your system headers... 236 | */ 237 | /* FIXME(bnoordhuis) That's possibly because sizeof(delay) should be 1. */ 238 | #if defined(TCP_KEEPALIVE) && !defined(__sun) 239 | if (on && setsockopt(fd, IPPROTO_TCP, TCP_KEEPALIVE, &delay, sizeof(delay))) 240 | return UV__ERR(errno); 241 | #endif 242 | 243 | return 0; 244 | } 245 | ``` 246 | 247 | 2 修改include/uv.h 248 | 把在tcp.c中加入的接口暴露出来。 249 | 250 | ```cpp 251 | UV_EXTERN int uv_tcp_keepalive_ex(uv_tcp_t* handle, 252 | int enable, 253 | unsigned int delay, 254 | unsigned int interval, 255 | unsigned int count); 256 | UV_EXTERN int uv_tcp_timeout(uv_tcp_t* handle, unsigned int timeout); 257 | ``` 258 | 259 | 至此,我们就修改完Libuv的代码,也对外暴露了设置的接口,接着我们修改上层的C++和JS代码,使得我们可以在JS层使用该功能。 260 | 3 修改src/tcp_wrap.cc 261 | 修改TCPWrap::Initialize函数的代码。 262 | 263 | ```cpp 264 | env->SetProtoMethod(t, "setKeepAliveEx", SetKeepAliveEx); 265 | env->SetProtoMethod(t, "setKeepAliveTimeout", SetKeepAliveTimeout); 266 | ``` 267 | 268 | 首先对JS层暴露两个新的API。我们看看这两个API的定义。 269 | 270 | ```cpp 271 | void TCPWrap::SetKeepAliveEx(const FunctionCallbackInfo& args) { 272 | TCPWrap* wrap; 273 | ASSIGN_OR_RETURN_UNWRAP(&wrap, 274 | args.Holder(), 275 | args.GetReturnValue().Set(UV_EBADF)); 276 | Environment* env = wrap->env(); 277 | int enable; 278 | if (!args[0]->Int32Value(env->context()).To(&enable)) return; 279 | unsigned int delay = static_cast(args[1].As()->Value()); 280 | unsigned int detal = static_cast(args[2].As()->Value()); 281 | unsigned int count = static_cast(args[3].As()->Value()); 282 | int err = uv_tcp_keepalive_ex(&wrap->handle_, enable, delay, detal, count); 283 | args.GetReturnValue().Set(err); 284 | } 285 | 286 | void TCPWrap::SetKeepAliveTimeout(const FunctionCallbackInfo& args) { 287 | TCPWrap* wrap; 288 | ASSIGN_OR_RETURN_UNWRAP(&wrap, 289 | args.Holder(), 290 | args.GetReturnValue().Set(UV_EBADF)); 291 | unsigned int time = static_cast(args[0].As()->Value()); 292 | int err = uv_tcp_timeout(&wrap->handle_, time); 293 | args.GetReturnValue().Set(err); 294 | } 295 | ``` 296 | 297 | 同时还需要在src/tcp_wrap.h中声明这两个函数。 298 | 299 | ```cpp 300 | static void SetKeepAliveEx(const v8::FunctionCallbackInfo& args); 301 | static void SetKeepAliveTimeout(const v8::FunctionCallbackInfo& args); 302 | ``` 303 | 304 | 305 | ```js 306 | // 修改lib/net.js 307 | Socket.prototype.setKeepAliveEx = function(setting, 308 | secs, 309 | interval, 310 | count) { 311 | if (!this._handle) { 312 | this.once('connect', () => this.setKeepAliveEx(setting, 313 | secs, 314 | interval, 315 | count)); 316 | return this; 317 | } 318 | 319 | if (this._handle.setKeepAliveEx) 320 | this._handle.setKeepAliveEx(setting, 321 | ~~secs > 0 ? ~~secs : 0, 322 | ~~interval > 0 ? ~~interval : 0, 323 | ~~count > 0 ? ~~count : 0); 324 | 325 | return this; 326 | }; 327 | 328 | Socket.prototype.setKeepAliveTimeout = function(timeout) { 329 | if (!this._handle) { 330 | this.once('connect', () => this.setKeepAliveTimeout(timeout)); 331 | return this; 332 | } 333 | 334 | if (this._handle.setKeepAliveTimeout) 335 | this._handle.setKeepAliveTimeout(~~timeout > 0 ? ~~timeout : 0); 336 | 337 | return this; 338 | }; 339 | ``` 340 | 341 | 重新编译Node.js,我们就可以使用这两个新的API更灵活地控制TCP的keepalive了。 342 | 343 | ```js 344 | const net = require('net'); 345 | net.createServer((socket) => { 346 | socket.setKeepAliveEx(true, 1,2,3); 347 | // socket.setKeepAliveTimeout(4); 348 | }).listen(1101); 349 | ``` 350 | 351 | ## 20.2 使用N-API编写C++插件 352 | 本小节介绍使用N_API编写C++插件知识。Node.js C++插件本质是一个动态链接库,写完编译后,生成一个.node文件。我们在Node.js里直接require使用,Node.js会为我们处理一切。 353 | 首先建立一个test.cc文件 354 | 355 | ```cpp 356 | // hello.cc using N-API 357 | #include 358 | 359 | namespace demo { 360 | 361 | napi_value Method(napi_env env, napi_callback_info args) { 362 | napi_value greeting; 363 | napi_status status; 364 | 365 | status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting); 366 | if (status != napi_ok) return nullptr; 367 | return greeting; 368 | } 369 | 370 | napi_value init(napi_env env, napi_value exports) { 371 | napi_status status; 372 | napi_value fn; 373 | 374 | status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn); 375 | if (status != napi_ok) return nullptr; 376 | 377 | status = napi_set_named_property(env, exports, "hello", fn); 378 | if (status != napi_ok) return nullptr; 379 | return exports; 380 | } 381 | 382 | NAPI_MODULE(NODE_GYP_MODULE_NAME, init) 383 | 384 | } // namespace demo 385 | ``` 386 | 387 | 我们不需要具体了解代码的意思,但是从代码中我们大致知道它做了什么事情。剩下的就是阅读N-API的API文档就可以。接着我们新建一个binding.gyp文件。gyp文件是node-gyp的配置文件。node-gyp可以帮助我们针对不同平台生产不同的编译配置文件。比如Linux下的makefile。 388 | 389 | ```json 390 | { 391 | "targets": [ 392 | { 393 | "target_name": "test", 394 | "sources": [ "./test.cc" ] 395 | } 396 | ] 397 | } 398 | ``` 399 | 400 | 语法和makefile有点像,就是定义我们编译后的目前文件名,依赖哪些源文件。然后我们安装node-gyp。 401 | 402 | ```sh 403 | npm install node-gyp -g 404 | ``` 405 | 406 | Node.js源码中也有一个node-gyp,它是帮助npm安装拓展模块时,就地编译用的。我们安装的node-gyp是帮助我们生成配置文件并编译用的,具体可以参考Node.js文档。一切准备就绪。我们开始编译。直接执行 407 | 408 | ```sh 409 | node-gyp configure 410 | node-gyp build 411 | ``` 412 | 413 | 在路径./build/Release/下生成了test.node文件。这就是我们的拓展模块。我们编写测试程序app.js。 414 | 415 | ```js 416 | var addon = require("./build/Release/test"); 417 | console.log(addon.hello()); 418 | ``` 419 | 420 | 执行 421 |   422 | 423 | ```text 424 | node app.js 425 | ``` 426 | 427 | 我们看到输出world。我们已经学会了如何编写一个Node.js的拓展模块。剩下的就是阅读N-API文档,根据自己的需求编写不同的模块。 428 | -------------------------------------------------------------------------------- /docs/chapter09-Unix域.md: -------------------------------------------------------------------------------- 1 | Unix域一种进程间通信的方式,Unix域不仅支持没有继承关系的进程间进行通信,而且支持进程间传递文件描述符。Unix域是Node.js中核心的功能,它是进程间通信的底层基础,child_process和cluster模块都依赖Unix域的能力。从实现和使用上来看,Unix域类似TCP,但是因为它是基于同主机进程的,不像TCP需要面临复杂的网络的问题,所以实现也没有TCP那么复杂。Unix域和传统的socket通信一样,遵循网络编程的那一套流程,由于在同主机内,就不必要使用IP和端口的方式。Node.js中,Unix域采用的是一个文件作为标记。大致原理如下。 2 | 1 服务器首先拿到一个socket。 3 | 2 服务器bind一个文件,类似bind一个IP和端口一样,对于操作系统来说,就是新建一个文件(不一定是在硬盘中创建,可以设置抽象路径名),然后把文件路径信息存在socket中。 4 | 3 调用listen修改socket状态为监听状态。 5 | 4 客户端通过同样的文件路径调用connect去连接服务器。这时候用于表示客户端的结构体插入服务器的连接队列,等待处理。 6 | 5 服务器调用accept摘取队列的节点,然后新建一个通信socket和客户端进行通信。 7 | Unix域通信本质还是基于内存之间的通信,客户端和服务器都维护一块内存,这块内存分为读缓冲区和写缓冲区。从而实现全双工通信,而Unix域的文件路径,只不过是为了让客户端进程可以找到服务端进程,后续就可以互相往对方维护的内存里写数据,从而实现进程间通信。 8 | ## 9.1 Unix域在Libuv中的使用 9 | 接下来我们看一下在Libuv中关于Unix域的实现和使用。 10 | ### 9.1.1 初始化 11 | Unix域使用uv_pipe_t结构体表示,使用之前首先需要初始化uv_pipe_t。下面看一下它的实现逻辑。 12 | 13 | ```cpp 14 | int uv_pipe_init(uv_loop_t* loop, uv_pipe_t* handle, int ipc) { 15 | uv__stream_init(loop, (uv_stream_t*)handle, UV_NAMED_PIPE); 16 | handle->shutdown_req = NULL; 17 | handle->connect_req = NULL; 18 | handle->pipe_fname = NULL; 19 | handle->ipc = ipc; 20 | return 0; 21 | } 22 | ``` 23 | 24 | uv_pipe_init逻辑很简单,就是初始化uv_pipe_t结构体的一些字段。uv_pipe_t继承于stream,uv__stream_init就是初始化stream(父类)的字段。uv_pipe_t中有一个字段ipc,该字段标记了是否允许在该Unix域通信中传递文件描述符。 25 | ### 9.1.2 绑定Unix域路径 26 | 开头说过,Unix域的实现类似TCP的实现。遵循网络socket编程那一套流程。服务端使用bind,listen等函数启动服务。 27 | 28 | ```cpp 29 | // name是unix路径名称 30 | int uv_pipe_bind(uv_pipe_t* handle, const char* name) { 31 | struct sockaddr_un saddr; 32 | const char* pipe_fname; 33 | int sockfd; 34 | int err; 35 | pipe_fname = NULL; 36 | pipe_fname = uv__strdup(name); 37 | name = NULL; 38 | // 流式Unix域套接字 39 | sockfd = uv__socket(AF_UNIX, SOCK_STREAM, 0); 40 | memset(&saddr, 0, sizeof saddr); 41 | strncpy(saddr.sun_path, pipe_fname, sizeof(saddr.sun_path) - 1); 42 | saddr.sun_path[sizeof(saddr.sun_path) - 1] = '\0'; 43 | saddr.sun_family = AF_UNIX; 44 | // 绑定到路径,TCP是绑定到IP和端口 45 | if (bind(sockfd, (struct sockaddr*)&saddr, sizeof saddr)) { 46 | // ... 47 | } 48 | 49 | // 设置绑定成功标记 50 | handle->flags |= UV_HANDLE_BOUND; 51 | // Unix域的路径 52 | handle->pipe_fname = pipe_fname; 53 | // 保存socket对应的fd 54 | handle->io_watcher.fd = sockfd; 55 | return 0; 56 | } 57 | ``` 58 | 59 | uv_pipe_bind函数首先申请一个socket,然后调用操作系统的bind函数把Unix域路径保存到socket中。最后标记已经绑定标记,并且保存Unix域的路径和socket对应的fd到handle中,后续需要使用。我们看到Node.js中Unix域的类型是SOCK_STREAM。Unix域支持两种数据模式。 60 | 1 流式( SOCK_STREAM),类似TCP,数据为字节流,需要应用层处理粘包问题。 61 | 2 数据报模式( SOCK_DGRAM ),类似UDP,不需要处理粘包问题。 62 | 通过Unix域虽然可以实现进程间的通信,但是我们拿到的数据可能是"乱的",这是为什么呢?一般情况下,客户端给服务器发送1个字节,然后服务器处理,如果是基于这种场景,那么数据就不会是乱的。因为每次就是一个需要处理的数据单位。但是如果客户端给服务器发送1个字节,服务器还没来得及处理,客户端又发送了一个字节,那么这时候服务器再处理的时候,就会有问题。因为两个字节混一起了。就好比在一个TCP连接上先后发送两个HTTP请求一样,如果服务器没有办法判断两个请求的数据边界,那么处理就会有问题。所以这时候,我们需要定义一个应用层协议,并且实现封包解包的逻辑,才能真正完成进程间通信。 63 | ### 9.1.3 启动服务 64 | 绑定了路径后,就可以调用listen函数使得socket处于监听状态。 65 | 66 | ```cpp 67 | int uv_pipe_listen(uv_pipe_t* handle, int backlog, uv_connection_cb cb) { 68 | // uv__stream_fd(handle)得到bind函数中获取的socket 69 | if (listen(uv__stream_fd(handle), backlog)) 70 | return UV__ERR(errno); 71 | // 保存回调,有进程调用connect的时候时触发,由uv__server_io函数触发 72 | handle->connection_cb = cb; 73 | // IO观察者的回调 74 | handle->io_watcher.cb = uv__server_io; 75 | // 注册IO观察者到Libuv,等待连接,即读事件到来 76 | uv__io_start(handle->loop, &handle->io_watcher, POLLIN); 77 | return 0; 78 | } 79 | ``` 80 | 81 | uv_pipe_listen执行操作系统的listen函数使得socket成为监听型的套接字。然后把socket对应的文件描述符和回调封装成IO观察者。注册到Libuv中。等到有读事件到来(有连接到来)。就会执行uv__server_io函数,摘下对应的客户端节点。最后执行connection_cb回调。 82 | ### 9.1.4 发起连接 83 | 这时候,我们已经成功启动了一个Unix域服务。接下来就是看客户端的逻辑。 84 | 85 | ```cpp 86 | void uv_pipe_connect(uv_connect_t* req, 87 | uv_pipe_t* handle, 88 | const char* name, 89 | uv_connect_cb cb) { 90 | struct sockaddr_un saddr; 91 | int new_sock; 92 | int err; 93 | int r; 94 | // 判断是否已经有socket了,没有的话需要申请一个,见下面 95 | new_sock = (uv__stream_fd(handle) == -1); 96 | // 客户端还没有对应的socket fd 97 | if (new_sock) { 98 | handle->io_watcher.fd= uv__socket(AF_UNIX, 99 | SOCK_STREAM, 100 | 0); 101 | } 102 | // 需要连接的服务器信息。主要是Unix域路径信息 103 | memset(&saddr, 0, sizeof saddr); 104 | strncpy(saddr.sun_path, name, sizeof(saddr.sun_path) - 1); 105 | saddr.sun_path[sizeof(saddr.sun_path) - 1] = '\0'; 106 | saddr.sun_family = AF_UNIX; 107 | // 非阻塞式连接服务器,Unix域路径是name 108 | do { 109 | r = connect(uv__stream_fd(handle), 110 | (struct sockaddr*)&saddr, sizeof saddr); 111 | } 112 | while (r == -1 && errno == EINTR); 113 | // 忽略错误处理逻辑 114 | err = 0; 115 | // 设置socket的可读写属性 116 | if (new_sock) { 117 | err = uv__stream_open((uv_stream_t*)handle, 118 | uv__stream_fd(handle), 119 | UV_HANDLE_READABLE | UV_HANDLE_WRITABLE); 120 | } 121 | // 把IO观察者注册到Libuv,等到连接成功或者可以发送请求 122 | if (err == 0) 123 | uv__io_start(handle->loop, 124 | &handle->io_watcher, 125 | POLLIN | POLLOUT); 126 | 127 | out: 128 | // 记录错误码,如果有的话 129 | handle->delayed_error = err; 130 | // 保存调用者信息 131 | handle->connect_req = req; 132 | uv__req_init(handle->loop, req, UV_CONNECT); 133 | req->handle = (uv_stream_t*)handle; 134 | req->cb = cb; 135 | QUEUE_INIT(&req->queue); 136 | /* 137 | 如果连接出错,在pending阶段会执行uv__stream_io, 138 | 从而执行req对应的回调。错误码是delayed_error 139 | */ 140 | if (err) 141 | uv__io_feed(handle->loop, &handle->io_watcher); 142 | } 143 | ``` 144 | 145 | uv_pipe_connect函数首先以非阻塞的方式调用操作系统的connect函数,调用connect后操作系统把客户端对应的socket直接插入服务器socket的待处理socket队列中,等待服务器处理。这时候socket是处于连接中的状态,当服务器调用accept函数处理连接时,会修改连接状态为已连接(这和TCP不一样,TCP是完成三次握手后就会修改为连接状态,而不是accept的时候),并且会触发客户端socket的可写事件。事件驱动模块就会执行相应的回调(uv__stream_io),从而执行C++和JS的回调。 146 | ### 9.1.5 关闭Unix域 147 | 我们可以通过uv_close关闭一个Unix域handle。uv_close中会调用uv__pipe_close。 148 | 149 | ```cpp 150 | void uv__pipe_close(uv_pipe_t* handle) { 151 | // 如果是Unix域服务器则需要删除Unix域路径并删除指向的堆内存 152 | if (handle->pipe_fname) { 153 | unlink(handle->pipe_fname); 154 | uv__free((void*)handle->pipe_fname); 155 | handle->pipe_fname = NULL; 156 | } 157 | // 关闭流相关的内容 158 | uv__stream_close((uv_stream_t*)handle); 159 | } 160 | ``` 161 | 162 | 关闭Unix域handle时,Libuv会自动删除Unix域路径对应的文件。但是如果进程异常退出时,该文件可能不会被删除,这样会导致下次监听的时候报错listen EADDRINUSE,所以安全起见,我们可以在进程退出或者监听之前判断该文件是否存在,存在的话则删除。另外还有一个问题是,如果两个不相关的进程使用了同一个文件则会导致误删,所以Unix域对应的文件,我们需要小心处理,最好能保证唯一性。 163 | 164 | Unix域大致的流程和网络编程一样。分为服务端和客户端两面。Libuv在操作系统提供的API的基础上。和Libuv的异步非阻塞结合。在Libuv中为进程间提供了一种通信方式。下面看一下在Node.js中是如何使用Libuv提供的功能的。 165 | ## 9.2 Unix域在Node.js中的使用 166 | ### 9.2.1 Unix域服务器 167 | 在Node.js中,我们可以通过以下代码创建一个Unix域服务器 168 | 169 | ```js 170 | const server = net.createServer((client) => { 171 | // 处理client 172 | }); 173 | server.listen('/tmp/test.sock', () => { 174 | console.log(`bind uinx domain success`); 175 | }); 176 | ``` 177 | 178 | 我们从listen函数开始分析这个过程。 179 | 180 | ```js 181 | Server.prototype.listen = function(...args) { 182 | const normalized = normalizeArgs(args); 183 | let options = normalized[0]; 184 | const cb = normalized[1]; 185 | // 调用底层的listen函数成功后执行的回调 186 | if (cb !== null) { 187 | this.once('listening', cb); 188 | } 189 | if (options.path && isPipeName(options.path)) { 190 | const pipeName = this._pipeName = options.path; 191 | backlog = options.backlog || backlogFromArgs; 192 | listenIncluster(this, pipeName, -1, -1, backlog, undefined, 193 | options.exclusive); 194 | /* 195 | Unix域使用文件实现的,客户端需要访问该文件的权限才能通信, 196 | 这里做权限控制 197 | */ 198 | let mode = 0; 199 | if (options.readableAll === true) 200 | mode |= PipeConstants.UV_READABLE; 201 | if (options.writableAll === true) 202 | mode |= PipeConstants.UV_WRITABLE; 203 | if (mode !== 0) { 204 | // 修改文件的访问属性 205 | const err = this._handle.fchmod(mode); 206 | if (err) { 207 | this._handle.close(); 208 | this._handle = null; 209 | throw errnoException(err, 'uv_pipe_chmod'); 210 | } 211 | } 212 | return this; 213 | } 214 | } 215 | ``` 216 | 217 | 这段代码中最主要的是listenIncluster函数。我们看一下该函数的逻辑。 218 | 219 | ```js 220 | function listenIncluster(server, address, port, addressType, 221 | backlog, fd, exclusive, flags) { 222 | exclusive = !!exclusive; 223 | if (cluster === undefined) cluster = require('cluster'); 224 | if (cluster.isMaster || exclusive) { 225 | server._listen2(address, port, addressType, backlog, fd, flags); 226 | return; 227 | } 228 | } 229 | ``` 230 | 231 | 直接调用_listen2(isMaster只有在cluster.fork创建的进程中才是false,其余情况都是true,包括child_process模块创建的子进程)。我们继续看listen函数。 232 | 233 | ```js 234 | Server.prototype._listen2 = setupListenHandle; 235 | 236 | function setupListenHandle(address, 237 | port, 238 | addressType, 239 | backlog, 240 | fd, 241 | flags) { 242 | this._handle = createServerHandle(address, 243 | port, 244 | addressType, 245 | fd, 246 | flags); 247 | // 有完成连接完成时触发 248 | this._handle.onconnection = onconnection; 249 | const err = this._handle.listen(backlog || 511); 250 | if (err) { 251 | // 触发error事件 252 | } 253 | // 下一个tick触发listen回调 254 | defaultTriggerAsyncIdScope(this[async_id_symbol], 255 | process.nextTick, 256 | emitListeningNT, 257 | this); 258 | } 259 | ``` 260 | 首先调用createServerHandle创建一个handle,然后执行listen函数。我们首先看一下createServerHandle。 261 | ```js 262 | function createServerHandle(address, 263 | port, 264 | addressType, 265 | fd, 266 | flags) { 267 | let handle = new Pipe(PipeConstants.SERVER); 268 | handle.bind(address, port); 269 | return handle; 270 | } 271 | ``` 272 | 273 | 创建了一个Pipe对象,然后调用它的bind和listen函数,我们看new Pipe的逻辑,从pipe_wrap.cc的导出逻辑,我们知道,这时候会新建一个C++对象,然后执行New函数,并且把新建的C++对象等信息作为入参。 274 | 275 | ```cpp 276 | void PipeWrap::New(const FunctionCallbackInfo& args) { 277 | Environment* env = Environment::GetCurrent(args); 278 | // 类型 279 | int type_value = args[0].As()->Value(); 280 | PipeWrap::SocketType type = static_cast(type_value); 281 | // 是否是用于IPC 282 | bool ipc; 283 | ProviderType provider; 284 | switch (type) { 285 | case SOCKET: 286 | provider = PROVIDER_PIPEWRAP; 287 | ipc = false; 288 | break; 289 | case SERVER: 290 | provider = PROVIDER_PIPESERVERWRAP; 291 | ipc = false; 292 | break; 293 | case IPC: 294 | provider = PROVIDER_PIPEWRAP; 295 | ipc = true; 296 | break; 297 | default: 298 | UNREACHABLE(); 299 | } 300 | 301 | new PipeWrap(env, args.This(), provider, ipc); 302 | } 303 | ``` 304 | 305 | New函数处理了参数,然后执行了new PipeWrap创建一个对象。 306 | ```cpp 307 | PipeWrap::PipeWrap(Environment* env, 308 | Local object, 309 | ProviderType provider, 310 | bool ipc) 311 | : ConnectionWrap(env, object, provider) { 312 | int r = uv_pipe_init(env->event_loop(), &handle_, ipc); 313 | } 314 | ``` 315 | new Pipe执行完后,就会通过该C++对象调用Libuv的bind和listen完成服务器的启动,就不再展开分析。 316 | ### 9.2.2 Unix域客户端 317 | 接着我们看一下Unix域作为客户端使用时的过程。 318 | 319 | ```js 320 | Socket.prototype.connect = function(...args) { 321 | const path = options.path; 322 | // Unix域路径 323 | var pipe = !!path; 324 | if (!this._handle) { 325 | // 创建一个C++层handle,即pipe_wrap.cc导出的Pipe类 326 | this._handle = pipe ? 327 | new Pipe(PipeConstants.SOCKET) : 328 | new TCP(TCPConstants.SOCKET); 329 | // 挂载onread方法到this中 330 | initSocketHandle(this); 331 | } 332 | 333 | if (cb !== null) { 334 | this.once('connect', cb); 335 | } 336 | // 执行internalConnect 337 | defaultTriggerAsyncIdScope( 338 | this[async_id_symbol], internalConnect, this, path 339 | ); 340 | return this; 341 | }; 342 | ``` 343 | 344 | 首先新建一个handle,值是new Pipe。接着执行了internalConnect,internalConnect函数的主要逻辑如下 345 | 346 | ```js 347 | const req = new PipeConnectWrap(); 348 | // address为Unix域路径 349 | req.address = address; 350 | req.oncomplete = afterConnect; 351 | // 调用C++层connect 352 | err = self._handle.connect(req, address, afterConnect); 353 | ``` 354 | 我们看C++层的connect函数, 355 | ```cpp 356 | void PipeWrap::Connect(const FunctionCallbackInfo& args) { 357 | Environment* env = Environment::GetCurrent(args); 358 | 359 | PipeWrap* wrap; 360 | ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); 361 | // PipeConnectWrap对象 362 | Local req_wrap_obj = args[0].As(); 363 | // Unix域路径 364 | node::Utf8Value name(env->isolate(), args[1]); 365 | /* 366 | 新建一个ConnectWrap对象,ConnectWrap是对handle进行一次连接请求 367 | 的封装,内部维护一个uv_connect_t结构体, req_wrap_obj的一个字段 368 | 指向ConnectWrap对象,用于保存对应的请求上下文 369 | */ 370 | ConnectWrap* req_wrap = 371 | new ConnectWrap(env, 372 | req_wrap_obj, 373 | AsyncWrap::PROVIDER_PIPECONNECTWRAP); 374 | // 调用Libuv的connect函数 375 | uv_pipe_connect(req_wrap->req(), 376 | &wrap->handle_, 377 | *name, 378 | AfterConnect); 379 | // req_wrap->req_.data = req_wrap;关联起来 380 | req_wrap->Dispatched(); 381 | // uv_pipe_connect() doesn't return errors. 382 | args.GetReturnValue().Set(0); 383 | } 384 | ``` 385 | 386 | uv_pipe_connect函数,第一个参数是uv_connect_t结构体(request),第二个是一个uv_pipe_t结构体(handle),handle是对Unix域客户端的封装,request是请求的封装,它表示基于handle发起一次连接请求。连接成功后会执行AfterConnect。由前面分析我们知道,当连接成功时,首先会执行回调Libuv的uv__stream_io,然后执行C++层的AfterConnect。 387 | 388 | ```cpp 389 | // 主动发起连接,成功/失败后的回调 390 | template = PipeWrap, uv_pipe_t 391 | void ConnectionWrap::AfterConnect(uv_connect_t* req 392 | ,int status) { 393 | // 在Connect函数里关联起来的 394 | ConnectWrap* req_wrap = static_cast(req->data); 395 | // 在uv_pipe_connect中完成关联的 396 | WrapType* wrap = static_cast(req->handle->data); 397 | Environment* env = wrap->env(); 398 | 399 | HandleScope handle_scope(env->isolate()); 400 | Context::Scope context_scope(env->context()); 401 | 402 | bool readable, writable; 403 | // 是否连接成功 404 | if (status) { 405 | readable = writable = 0; 406 | } else { 407 | readable = uv_is_readable(req->handle) != 0; 408 | writable = uv_is_writable(req->handle) != 0; 409 | } 410 | 411 | Local argv[5] = { 412 | Integer::New(env->isolate(), status), 413 | wrap->object(), 414 | req_wrap->object(), 415 | Boolean::New(env->isolate(), readable), 416 | Boolean::New(env->isolate(), writable) 417 | }; 418 | // 执行JS层的oncomplete回调 419 | req_wrap->MakeCallback(env->oncomplete_string(), 420 | arraysize(argv), 421 | argv); 422 | 423 | delete req_wrap; 424 | } 425 | ``` 426 | 427 | 我们再回到JS层的afterConnect 428 | 429 | ```js 430 | function afterConnect(status, handle, req, readable, writable) { 431 | var self = handle.owner; 432 | handle = self._handle; 433 | if (status === 0) { 434 | self.readable = readable; 435 | self.writable = writable; 436 | self._unrefTimer(); 437 | // 触发connect事件 438 | self.emit('connect'); 439 | // 可读并且没有处于暂停模式,则注册等待可读事件 440 | if (readable && !self.isPaused()) 441 | self.read(0); 442 | } 443 | } 444 | ``` 445 | 446 | 至此,作为客户端对服务器的连接就完成了。后续就可以进行通信。 447 | -------------------------------------------------------------------------------- /docs/chapter03-事件循环.md: -------------------------------------------------------------------------------- 1 | # 第三章 事件循环 2 | Node.js属于单线程事件循环架构,该事件循环由Libuv的uv_run函数实现,在该函数中执行while循环,然后不断地处理各个阶段(phase)的事件回调。事件循环的处理相当于一个消费者,消费由各种代码产生的任务。Node.js初始化完成后就开始陷入该事件循环中,事件循环的结束也就意味着Node.js的结束。下面看一下事件循环的核心代码。 3 | 4 | ```cpp 5 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { 6 | int timeout; 7 | int r; 8 | int ran_pending; 9 | // 在uv_run之前要先提交任务到loop 10 | r = uv__loop_alive(loop); 11 | // 事件循环没有任务执行,即将退出,设置一下当前循环的时间 12 | if (!r) 13 | uv__update_time(loop); 14 | // 没有任务需要处理或者调用了uv_stop则退出事件循环 15 | while (r != 0 && loop->stop_flag == 0) { 16 | // 更新loop的time字段 17 | uv__update_time(loop); 18 | // 执行超时回调 19 | uv__run_timers(loop); 20 | /* 21 | 执行pending回调,ran_pending代表pending队列是否为空, 22 | 即没有节点可以执行 23 | */ 24 | ran_pending = uv__run_pending(loop); 25 | // 继续执行各种队列 26 | uv__run_idle(loop); 27 | uv__run_prepare(loop); 28 | 29 | timeout = 0; 30 | /* 31 | 执行模式是UV_RUN_ONCE时,如果没有pending节点, 32 | 才会阻塞式Poll IO,默认模式也是 33 | */ 34 | if ((mode == UV_RUN_ONCE && !ran_pending) || 35 | mode == UV_RUN_DEFAULT) 36 | timeout = uv_backend_timeout(loop); 37 | // Poll IO timeout是epoll_wait的超时时间 38 | uv__io_poll(loop, timeout); 39 | // 处理check阶段 40 | uv__run_check(loop); 41 | // 处理close阶段 42 | uv__run_closing_handles(loop); 43 | /* 44 | 还有一次执行超时回调的机会,因为uv__io_poll可能是因为 45 | 定时器超时返回的。 46 | */ 47 | if (mode == UV_RUN_ONCE) { 48 | uv__update_time(loop); 49 | uv__run_timers(loop); 50 | } 51 | 52 | r = uv__loop_alive(loop); 53 | /* 54 | 只执行一次,退出循环,UV_RUN_NOWAIT表示在Poll IO阶段 55 | 不会阻塞并且循环只执行一次 56 | */ 57 | if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) 58 | break; 59 | } 60 | // 是因为调用了uv_stop退出的,重置flag 61 | if (loop->stop_flag != 0) 62 | loop->stop_flag = 0; 63 | /* 64 | 返回是否还有活跃的任务(handle或request), 65 | 业务代表可以再次执行uv_run 66 | */ 67 | return r; 68 | } 69 | ``` 70 | 71 | Libuv分为几个阶段,下面从先到后,分别分析各个阶段的相关代码。 72 | ## 3.1 事件循环之定时器 73 | Libuv中,定时器阶段是第一个被处理的阶段。定时器是以最小堆实现的,最快过期的节点是根节点。Libuv在每次事件循环开始的时候都会缓存当前的时间,在每一轮的事件循环中,使用的都是这个缓存的时间,必要的时候Libuv会显式更新这个时间,因为获取时间需要调用操作系统提供的接口,而频繁调用系统调用会带来一定的耗时,缓存时间可以减少操作系统的调用,提高性能。Libuv缓存了当前最新的时间后,就执行uv__run_timers,该函数就是遍历最小堆,找出当前超时的节点。因为堆的性质是父节点肯定比孩子小。并且根节点是最小的,所以如果一个根节点,它没有超时,则后面的节点也不会超时。对于超时的节点就执行它的回调。我们看一下具体的逻辑。 74 | 75 | ```cpp 76 | void uv__run_timers(uv_loop_t* loop) { 77 | struct heap_node* heap_node; 78 | uv_timer_t* handle; 79 | // 遍历二叉堆 80 | for (;;) { 81 | // 找出最小的节点 82 | heap_node = heap_min(timer_heap(loop)); 83 | // 没有则退出 84 | if (heap_node == NULL) 85 | break; 86 | // 通过结构体字段找到结构体首地址 87 | handle = container_of(heap_node, uv_timer_t, heap_node); 88 | // 最小的节点都没有超市,则后面的节点也不会超时 89 | if (handle->timeout > loop->time) 90 | break; 91 | // 删除该节点 92 | uv_timer_stop(handle); 93 | /* 94 | 重试插入二叉堆,如果需要的话(设置了repeat,比如 95 | setInterval) 96 | */ 97 | uv_timer_again(handle); 98 | // 执行回调 99 | handle->timer_cb(handle); 100 | } 101 | } 102 | ``` 103 | 104 | 执行完回调后,还有两个关键的操作,第一就是stop,第二就是again。stop的逻辑很简单,就是把handle从二叉堆中删除,并且修改handle的状态。那么again又是什么呢?again是为了支持setInterval这种场景,如果handle设置了repeat标记,则该handle在超时后,每repeat的时间后,就会继续执行超时回调。对于setInterval,就是超时时间是x,每x的时间后,执行回调。这就是Node.js里定时器的底层原理。但Node.js不是每次调setTimeout/setInterval的时候都往最小堆插入一个节点,Node.js里,只有一个关于uv_timer_s的handle,它在JS层维护了一个数据结构,每次计算出最早到期的节点,然后修改handle的超时时间,具体在定时器章节讲解。 105 |     另外timer阶段和Poll IO阶段也有一些联系,因为Poll IO可能会导致主线程阻塞,为了保证主线程可以尽快执行定时器的回调,Poll IO不能一直阻塞,所以这时候,阻塞的时长就是最快到期的定时器节点的时长(具体可参考libuv core.c中的uv_backend_timeout函数)。 106 | ## 3.2 pending阶段 107 | 官网对pending阶段的解释是在上一轮的Poll IO阶段没有执行的IO回调,会在下一轮循环的pending阶段被执行。从源码来看,Poll IO阶段处理任务时,在某些情况下,如果当前执行的操作失败需要执行回调通知调用方一些信息,该回调函数不会立刻执行,而是在下一轮事件循环的pending阶段执行(比如写入数据成功,或者TCP连接失败时回调C++层),我们先看pending阶段的处理。 108 | 109 | ```cpp 110 | static int uv__run_pending(uv_loop_t* loop) { 111 | QUEUE* q; 112 | QUEUE pq; 113 | uv__io_t* w; 114 | 115 | if (QUEUE_EMPTY(&loop->pending_queue)) 116 | return 0; 117 | // 把pending_queue队列的节点移到pq,即清空了pending_queue 118 | QUEUE_MOVE(&loop->pending_queue, &pq); 119 | 120 | // 遍历pq队列 121 | while (!QUEUE_EMPTY(&pq)) { 122 | // 取出当前第一个需要处理的节点,即pq.next 123 | q = QUEUE_HEAD(&pq); 124 | // 把当前需要处理的节点移出队列 125 | QUEUE_REMOVE(q); 126 | /* 127 | 重置一下prev和next指针,因为这时候这两个指针是 128 | 指向队列中的两个节点 129 | */ 130 | QUEUE_INIT(q); 131 | w = QUEUE_DATA(q, uv__io_t, pending_queue); 132 | w->cb(loop, w, POLLOUT); 133 | } 134 | 135 | return 1; 136 | } 137 | ``` 138 | 139 | pending阶段的处理逻辑就是把pending队列里的节点逐个执行。我们看一下pending队列的节点是如何生产出来的。 140 | 141 | ```cpp 142 | void uv__io_feed(uv_loop_t* loop, uv__io_t* w) { 143 | if (QUEUE_EMPTY(&w->pending_queue)) 144 | QUEUE_INSERT_TAIL(&loop->pending_queue, &w->pending_queue); 145 | } 146 | ``` 147 | 148 | Libuv通过uv__io_feed函数生产pending任务,从Libuv的代码中我们看到IO错误的时候会调这个函数(如tcp.c的uv__tcp_connect函数)。 149 | 150 | ```cpp 151 | if (handle->delayed_error) 152 | uv__io_feed(handle->loop, &handle->io_watcher); 153 | ``` 154 | 155 | 在写入数据成功后(比如TCP、UDP),也会往pending队列插入一个节点,等待回调。比如发送数据成功后执行的代码(udp.c的uv__udp_sendmsg函数) 156 | 157 | ```cpp 158 | // 发送完移出写队列 159 | QUEUE_REMOVE(&req->queue); 160 | // 加入写完成队列 161 | QUEUE_INSERT_TAIL(&handle->write_completed_queue, &req->queue); 162 | /* 163 | 有节点数据写完了,把IO观察者插入pending队列, 164 | pending阶段执行回调 165 | */ 166 | uv__io_feed(handle->loop, &handle->io_watcher); 167 | ``` 168 | 169 | 最后关闭IO的时候(如关闭一个TCP连接)会从pending队列移除对应的节点,因为已经关闭了,自然就不需要执行回调。 170 | 171 | ```cpp 172 | void uv__io_close(uv_loop_t* loop, uv__io_t* w) { 173 | uv__io_stop(loop, 174 | w, 175 | POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI); 176 | QUEUE_REMOVE(&w->pending_queue); 177 | } 178 | ``` 179 | 180 | ## 3.3 事件循环之prepare,check,idle 181 | prepare,check,idle是Libuv事件循环中属于比较简单的一个阶段,它们的实现是一样的(见loop-watcher.c)。本节只讲解prepare阶段,我们知道Libuv中分为handle和request,而prepare阶段的任务是属于handle类型。这意味着除非我们显式移除,否则prepare阶段的节点在每次事件循环中都会被执行。下面我们先看看怎么使用它。 182 | 183 | ```cpp 184 | void prep_cb(uv_prepare_t *handle) { 185 | printf("Prep callback\n"); 186 | } 187 | 188 | int main() { 189 | uv_prepare_t prep; 190 | // 初始化一个handle,uv_default_loop是事件循环的核心结构体 191 | uv_prepare_init(uv_default_loop(), &prep); 192 | // 注册handle的回调 193 | uv_prepare_start(&prep, prep_cb); 194 | // 开始事件循环 195 | uv_run(uv_default_loop(), UV_RUN_DEFAULT); 196 | return 0; 197 | } 198 | ``` 199 | 200 | 执行main函数,Libuv就会在prepare阶段执行回调prep_cb。我们分析一下这个过程。 201 | 202 | ```cpp 203 | int uv_prepare_init(uv_loop_t* loop, uv_prepare_t* handle) { 204 | uv__handle_init(loop, (uv_handle_t*)handle, UV_PREPARE); 205 | handle->prepare_cb = NULL; 206 | return 0; 207 | } 208 | ``` 209 | 210 | init函数主要是做一些初始化操作。我们继续要看start函数。 211 | 212 | ```cpp 213 | int uv_prepare_start(uv_prepare_t* handle, uv_prepare_cb cb) { 214 | // 如果已经执行过start函数则直接返回 215 | if (uv__is_active(handle)) return 0; 216 | if (cb == NULL) return UV_EINVAL; 217 | QUEUE_INSERT_HEAD(&handle->loop->prepare_handles, 218 | &handle->queue); 219 | handle->prepare_cb = cb; 220 | uv__handle_start(handle); 221 | return 0; 222 | } 223 | ``` 224 | 225 | uv_prepare_start函数主要的逻辑主要是设置回调,把handle插入loop的prepare_handles队列,prepare_handles队列保存了prepare阶段的任务。在事件循环的prepare阶段会逐个执行里面的节点的回调。然后我们看看Libuv在事件循环的prepare阶段是如何处理的。 226 | 227 | ```cpp 228 | void uv__run_prepare(uv_loop_t* loop) { 229 | uv_prepare_t* h; 230 | QUEUE queue; 231 | QUEUE* q; 232 | /* 233 | 把该类型对应的队列中所有节点摘下来挂载到queue变量, 234 | 相当于清空prepare_handles队列,因为如果直接遍历 235 | prepare_handles队列,在执行回调的时候一直往prepare_handles 236 | 队列加节点,会导致下面的while循环无法退出。 237 | 先移除的话,新插入的节点在下一轮事件循环才会被处理。 238 | */ 239 | QUEUE_MOVE(&loop->prepare_handles, &queue); 240 | // 遍历队列,执行每个节点里面的函数 241 | while (!QUEUE_EMPTY(&queue)) { 242 | // 取下当前待处理的节点,即队列的头 243 | q = QUEUE_HEAD(&queue); 244 | /* 245 | 取得该节点对应的整个结构体的基地址, 246 | 即通过结构体成员取得结构体首地址 247 | */ 248 | h = QUEUE_DATA(q, uv_prepare_t, queue); 249 | // 把该节点移出当前队列 250 | QUEUE_REMOVE(q); 251 | // 重新插入原来的队列 252 | QUEUE_INSERT_TAIL(&loop->prepare_handles, q); 253 | // 执行回调函数 254 | h->prepare_cb(h); 255 | } 256 | } 257 | ``` 258 | 259 | uv__run_prepare函数的逻辑很简单,但是有一个重点的地方就是执行完每一个节点,Libuv会把该节点重新插入队列中,所以prepare(包括idle、check)阶段的节点在每一轮事件循环中都会被执行。而像定时器、pending、closing阶段的节点是一次性的,被执行后就会从队列里删除。 260 |     我们回顾一开始的测试代码。因为它设置了Libuv的运行模式是默认模式。而prepare队列又一直有一个handle节点,所以它是不会退出的。它会一直执行回调。那如果我们要退出怎么办呢?或者说不要执行prepare队列的某个节点了。我们只需要stop一下就可以了。 261 | 262 | ```cpp 263 | int uv_prepare_stop(uv_prepare_t* handle) { 264 | if (!uv__is_active(handle)) return 0; 265 | // 把handle从prepare队列中移除,但还挂载到handle_queue中 266 | QUEUE_REMOVE(&handle->queue); 267 | // 清除active标记位并且减去loop中handle的active数 268 | uv__handle_stop(handle); 269 | return 0; 270 | } 271 | ``` 272 | 273 | stop函数和start函数是相反的作用,这就是Node.js中prepare、check、idle阶段的原理。 274 | ## 3.4 事件循环之Poll IO 275 | Poll IO是Libuv非常重要的一个阶段,文件IO、网络IO、信号处理等都在这个阶段处理,这也是最复杂的一个阶段。处理逻辑在core.c的uv__io_poll这个函数,这个函数比较复杂,我们分开分析。在开始分析Poll IO之前,先了解一下它相关的一些数据结构。
276 | 1 IO观察者uv__io_t。这个结构体是Poll IO阶段核心结构体。它主要是保存了IO相关的文件描述符、回 调、感兴趣的事件等信息。
277 | 2 watcher_queue观察者队列。所有需要Libuv处理的IO观察者都挂载在这个队列里,Libuv在Poll IO阶段会逐个处理。 278 | 279 | 下面我们开始分析Poll IO阶段。先看第一段逻辑。 280 | 281 | ```cpp 282 | // 没有IO观察者,则直接返回 283 | if (loop->nfds == 0) { 284 | assert(QUEUE_EMPTY(&loop->watcher_queue)); 285 | return; 286 | } 287 | // 遍历IO观察者队列 288 | while (!QUEUE_EMPTY(&loop->watcher_queue)) { 289 | // 取出当前头节点 290 | q = QUEUE_HEAD(&loop->watcher_queue); 291 | // 脱离队列 292 | QUEUE_REMOVE(q); 293 | // 初始化(重置)节点的前后指针 294 | QUEUE_INIT(q); 295 | // 通过结构体成功获取结构体首地址 296 | w = QUEUE_DATA(q, uv__io_t, watcher_queue); 297 | // 设置当前感兴趣的事件 298 | e.events = w->pevents; 299 | /* 300 | 这里使用了fd字段,事件触发后再通过fd从watchs 301 | 字段里找到对应的IO观察者,没有使用ptr指向IO观察者的方案 302 | */ 303 | e.data.fd = w->fd; 304 | // 如果w->events初始化的时候为0,则新增,否则修改 305 | if (w->events == 0) 306 | op = EPOLL_CTL_ADD; 307 | else 308 | op = EPOLL_CTL_MOD; 309 | // 修改epoll的数据 310 | epoll_ctl(loop->backend_fd, op, w->fd, &e) 311 | // 记录当前加到epoll时的状态 312 | w->events = w->pevents; 313 | } 314 | ``` 315 | 316 | 第一步首先遍历IO观察者,修改epoll的数据。然后准备进入等待。 317 | 318 | ```cpp 319 | psigset = NULL; 320 | if (loop->flags & UV_LOOP_BLOCK_SIGPROF) { 321 | sigemptyset(&sigset); 322 | sigaddset(&sigset, SIGPROF); 323 | psigset = &sigset; 324 | } 325 | /* 326 | http://man7.org/Linux/man-pages/man2/epoll_wait.2.html 327 | pthread_sigmask(SIG_SETMASK, &sigmask, &origmask); 328 | ready = epoll_wait(epfd, &events, maxevents, timeout); 329 | pthread_sigmask(SIG_SETMASK, &origmask, NULL); 330 | 即屏蔽SIGPROF信号,避免SIGPROF信号唤醒epoll_wait,但是却没 331 | 有就绪的事件 332 | */ 333 | nfds = epoll_pwait(loop->backend_fd, 334 | events, 335 | ARRAY_SIZE(events), 336 | timeout, 337 | psigset); 338 | // epoll可能阻塞,这里需要更新事件循环的时间 339 | uv__update_time(loop) ``` 340 | ``` 341 | epoll_wait可能会引起主线程阻塞,所以wait返回后需要更新当前的时间,否则在使用的时候时间差会比较大,因为Libuv会在每轮时间循环开始的时候缓存当前时间这个值。其它地方直接使用,而不是每次都去获取。下面我们接着看epoll返回后的处理(假设有事件触发)。 342 | 343 | ```cpp 344 | // 保存epoll_wait返回的一些数据,maybe_resize申请空间的时候+2了 345 | loop->watchers[loop->nwatchers] = (void*) events; 346 | loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds; 347 | for (i = 0; i < nfds; i++) { 348 | // 触发的事件和文件描述符 349 | pe = events + i; 350 | fd = pe->data.fd; 351 | // 根据fd获取IO观察者,见上面的图 352 | w = loop->watchers[fd]; 353 | // 会其它回调里被删除了,则从epoll中删除 354 | if (w == NULL) { 355 | epoll_ctl(loop->backend_fd, EPOLL_CTL_DEL, fd, pe); 356 | continue; 357 | } 358 | if (pe->events != 0) { 359 | /* 360 | 用于信号处理的IO观察者感兴趣的事件触发了, 361 | 即有信号发生。 362 | */ 363 | if (w == &loop->signal_io_watcher) 364 | have_signals = 1; 365 | else 366 | // 一般的IO观察者则执行回调 367 | w->cb(loop, w, pe->events); 368 | nevents++; 369 | } 370 | } 371 | // 有信号发生,触发回调 372 | if (have_signals != 0) 373 | loop->signal_io_watcher.cb(loop, 374 | &loop->signal_io_watcher, 375 | POLLIN); 376 | ``` 377 | 378 | 上面的代码处理IO事件并执行IO观察者里的回调,但是有一个特殊的地方就是信号处理的IO观察者需要单独判断,它是一个全局的IO观察者,和一般动态申请和销毁的IO观察者不一样,它是存在于Libuv运行的整个生命周期。这就是Poll IO的整个过程。 379 | ## 3.5 事件循环之close 380 | close是Libuv每轮事件循环中最后的一个阶段。uv_close用于关闭一个handle,并且执行一个回调。uv_close产生的任务会插入到close阶段的队列,然后在close阶段被处理。我们看一下uv_close函数的实现。 381 | 382 | ```cpp 383 | void uv_close(uv_handle_t* handle, uv_close_cb close_cb) { 384 | // 正在关闭,但是还没执行回调等后置操作 385 | handle->flags |= UV_HANDLE_CLOSING; 386 | handle->close_cb = close_cb; 387 | 388 | switch (handle->type) { 389 | case UV_PREPARE: 390 | uv__prepare_close((uv_prepare_t*)handle); 391 | break; 392 | case UV_CHECK: 393 | uv__check_close((uv_check_t*)handle); 394 | break; 395 | ... 396 | default: 397 | assert(0); 398 | } 399 | uv__make_close_pending(handle); 400 | } 401 | ``` 402 | 403 | uv_close设置回调和状态,然后根据handle类型调对应的close函数,一般就是stop这个handle,解除IO观察者注册的事件,从事件循环的handle队列移除该handle等等,比如prepare的close函数只是把handle从队列中移除。 404 | 405 | ```cpp 406 | void uv__prepare_close(uv_prepare_t* handle) { 407 | uv_prepare_stop(handle); 408 | } 409 | int uv_prepare_stop(uv_prepare__t* handle) { 410 | QUEUE_REMOVE(&handle->queue); 411 | uv__handle_stop(handle); 412 | return 0; 413 | } 414 | ``` 415 | 416 | 417 | 根据不同的handle做不同的处理后,接着执行uv__make_close_pending往close队列追加节点。 418 | 419 | ```cpp 420 | // 头插法插入closing队列,在closing阶段被执行 421 | void uv__make_close_pending(uv_handle_t* handle) { 422 | handle->next_closing = handle->loop->closing_handles; 423 | handle->loop->closing_handles = handle; 424 | } 425 | ``` 426 | 427 | 然后在close阶段逐个处理。我们看一下close阶段的处理逻辑 428 | 429 | ```cpp 430 | // 执行closing阶段的的回调 431 | static void uv__run_closing_handles(uv_loop_t* loop) { 432 | uv_handle_t* p; 433 | uv_handle_t* q; 434 | 435 | p = loop->closing_handles; 436 | loop->closing_handles = NULL; 437 | 438 | while (p) { 439 | q = p->next_closing; 440 | uv__finish_close(p); 441 | p = q; 442 | } 443 | } 444 | 445 | // 执行closing阶段的回调 446 | static void uv__finish_close(uv_handle_t* handle) { 447 | handle->flags |= UV_HANDLE_CLOSED; 448 | ... 449 | uv__handle_unref(handle); 450 | // 从handle队列里移除 451 | QUEUE_REMOVE(&handle->handle_queue); 452 | if (handle->close_cb) { 453 | handle->close_cb(handle); 454 | } 455 | } 456 | ``` 457 | 458 | uv__run_closing_handles会逐个执行每个任务节点的回调。 459 | ## 3.6 控制事件循环 460 | Libuv通过uv__loop_alive函数判断事件循环是否还需要继续执行。我们看看这个函数的定义。 461 | 462 | ```cpp 463 | static int uv__loop_alive(const uv_loop_t* loop) { 464 | return uv__has_active_handles(loop) || 465 | uv__has_active_reqs(loop) || 466 | loop->closing_handles != NULL; 467 | } 468 | ``` 469 | 470 | 为什么会有一个closing_handle的判断呢?从uv_run的代码来看,执行完close阶段后,会立刻执行uv__loop_alive,正常来说,close阶段的队列是空的,但是如果我们在close回调里又往close队列新增了一个节点,而该节点不会在本轮的close阶段被执行,这样会导致执行完close阶段,但是close队列依然有节点,如果直接退出,则无法执行对应的回调。 471 | 我们看到有三种情况,Libuv认为事件循环是存活的。如果我们控制这三种条件就可以控制事件循环的的退出。我们通过一个例子理解一下这个过程。 472 | 473 | ```js 474 | const timeout = setTimeout(() => { 475 | console.log('never console') 476 | }, 5000); 477 | timeout.unref(); 478 | ``` 479 | 480 | 上面的代码中,setTimeout的回调是不会执行的。除非超时时间非常短,短到第一轮事件循环的时候就到期了,否则在第一轮事件循环之后,由于unref的影响,事件循环直接退出了。unref影响的就是handle这个条件。这时候事件循环代码如下。 481 | 482 | ```cpp 483 | while (r != 0 && loop->stop_flag == 0) { 484 | uv__update_time(loop); 485 | uv__run_timers(loop); 486 | // ... 487 | // uv__loop_alive返回false,直接跳出while,从而退出事件循环 488 | r = uv__loop_alive(loop); 489 | } 490 | ``` 491 | -------------------------------------------------------------------------------- /docs/chapter29-Node.js底层原理(实现篇).md: -------------------------------------------------------------------------------- 1 | **前言:本文根据最近做的一次分享整理而成,希望能帮忙大家深入理解Node.js的一些原理和实现。** 2 | 3 | 大家好,我是一名Node.js爱好者,今天我分享的主题是Node.js的底层原理。在大前端的趋势下,Node.js不仅拓展了前端的技术范围,同时,扮演的角色也越来越重要,深入了解和理解技术的底层原理,才能更好地为业务赋能。 4 | 5 | 今天分享的内容主要分为两大部分,第一部分是Node.js的基础和架构,第二部分是Node.js核心模块的实现。 6 | 7 | - 一 Node.js基础和架构 8 | Node.js的组成 9 | Node.js代码架构 10 | Node.js启动过程 11 | Node.js事件循环 12 | - 二 Node.js核心模块的实现 13 | 进程和进程间通信 14 | 线程和线程间通信 15 | Cluster 16 | Libuv线程池 17 | 信号处理 18 | 文件 19 | TCP 20 | UDP 21 | DNS 22 | 23 | # Nodejs组成 24 | ![](https://img-blog.csdnimg.cn/20210526030110435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 25 | Node.js主要由V8、Libuv和第三方库组成。 26 | 27 | Libuv:跨平台的异步IO库,但它提供的功能不仅仅是IO,还 28 | 包括进程、线程、信号、定时器、进程间通信,线程池等。 29 | 30 | 第三方库:异步DNS解析(cares)、HTTP解析器(旧版使用 31 | http_parser,新版使用llhttp)、HTTP2解析器(nghttp2)、 32 | 解压压缩库(zlib)、加密解密库(openssl)等等。 33 | 34 | V8:实现JS解析和支持自定义的功能,得益于V8支持自定义拓展,才有了Node.js。 35 | 36 | # Node.js代码架构 37 | ![](https://img-blog.csdnimg.cn/20210526030134645.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 38 | 上图是Node.js的代码架构,Node.js的代码主要分为JS、C++、C三种。 39 | 40 | 1 JS是我们使用的那些模块。 41 | 42 | 2 C++代码分为三个部分,第一部分是封装了Libuv的功能,第二部分则是不依赖于Libuv(crypto部分api使用了Libuv线程池),比如Buffer模块。第三部分是V8的代码。 43 | 44 | 3 C语言层的代码主要是封装了操作系统的功能,比如TCP、UDP。 45 | 46 | 了解了Node.js的组成和架构后,我们看看Node.js启动的过程都做了什么。 47 | 48 | # Node.js启动过程 49 | ## 1 注册C++模块 50 | ![](https://img-blog.csdnimg.cn/20210526030230636.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 51 | 52 | 首先Node.js会调用registerBuiltinModules函数注册C++模块,这个函数会调用一系列registerxxx的函数,我们发现在Node.js源码里找不到这些函数,因为这些函数会在各个C++模块中,通过宏定义实现的。宏展开后就是上图黄色框的内容,每个registerxxx函数的作用就是往C++模块的链表了插入一个节点,最后会形成一个链表。 53 | 54 | 那么Node.js里是如何访问这些C++模块的呢?在Node.js中,是通过internalBinding访问C++模块的,internalBinding的逻辑很简单,就是根据模块名从模块队列中找到对应模块。但是这个函数只能在Node.js内部使用,不能在用户js模块使用。用户可以通过process.binding访问C++模块。 55 | 56 | ## 2 创建Environment对象,并绑定到Context 57 | 注册完C++模块后就开始创建Environment对象,Environment是Node.js执行时的环境对象,类似一个全局变量的作用,他记录了Node.js在运行时的一些公共数据。创建完Environment后,Node.js会把该对象绑定到V8的Context中,为什么要这样做呢?主要是为了在V8的执行上下文里拿到env对象,因为V8中只有Isolate、Context这些对象。如果我们想在V8的执行环境中获取Environment对象的内容,就可以通过Context获取Environment对象。 58 | ![](https://img-blog.csdnimg.cn/2021052603212184.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 59 | ![](https://img-blog.csdnimg.cn/20210526032145269.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 60 | ## 3 初始化模块加载器 61 | 1 Node.js首先传入c++模块加载器,执行loader.js,loader.js主要是封装了c++模块加载器和原生js模块加载器。并保存到env对象中。 62 | 2 接着传入c++和原生js模块加载器,执行run_main_module.js。 63 | 3 在run_main_module.js中传入js和原生js模块加载器,执行用户的js。 64 | 假设用户js如下 65 | ```c 66 | require('net') 67 | require('./myModule') 68 | ``` 69 | 分别加载了一个用户模块和原生js模块,我们看看加载过程,执行require的时候。 70 | 1 Node.js首先会判断是否是原生js模块,如果不是则直接加载用户模块,否则,会使用原生模块加载器加载原生js模块。 71 | 2 加载原生js模块的时候,如果用到了c++模块,则使用internalBinding去加载。 72 | 73 | ![](https://img-blog.csdnimg.cn/20210526032429787.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 74 | ## 4 执行用户JS代码,然后进入Libuv事件循环 75 | 接着Node.js就会执行用户的js,通常用户的js会给事件循环生产任务,然后就进入了事件循环系统,比如我们listen一个服务器的时候,就会在事件循环中新建一个tcp handle。Node.js就会在这个事件循环中一直运行。 76 | ```c 77 | net.createServer(() => {}).listen(80) 78 | ``` 79 | ![](https://img-blog.csdnimg.cn/20210526032807146.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 80 | # 事件循环 81 | 下面我们看一下事件循环的实现。事件循环主要分为7个阶段。timer阶段主要是处理定时器相关的任务,pending阶段主要是处理poll io阶段回调里产生的回调。check、prepare、idle阶段是自定义的阶段,这三个阶段的任务每次事件序循环都会被执行。Poll io阶段主要是处理网络IO、信号、线程池等等任务。closing阶段主要是处理关闭的handle,比如停止关闭服务器。 82 | ![](https://img-blog.csdnimg.cn/20210526032901236.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 83 | 1 timer阶段: 用二叉堆实现,最快过期的在根节点。 84 | 2 pending阶段:处理poll io阶段回调里产生的回调。 85 | 3 check、prepare、idle阶段:每次事件循环都会被执行。 86 | 4 poll io阶段:处理文件描述符相关事件。 87 | 5 closing阶段:执行调用uv_close函数时传入的回调。 88 | 89 | 下面我们详细看一下每个阶段的实现。 90 | ## 定时器阶段 91 | 定时器的底层数据结构是二叉堆,最快到期的节点在最上面。在定时器阶段的时候,就会逐个节点遍历,如果节点超时了,那么就执行他的回调,如果没有超时,那么后面的节点也不用判断了,因为当前节点是最快过期的,如果他都没有过期,说明其他节点也没有过期。节点的回调被执行后,就会被删除,为了支持setInterval的场景,如果设置repeat标记,那么这个节点会被重新插入到二叉堆。 92 | ![](https://img-blog.csdnimg.cn/2021052603300282.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 93 | 我们看到底层的实现稍微简单,但是Node.js的定时器模块实现就稍微复杂。 94 | ![](https://img-blog.csdnimg.cn/20210526033050104.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 95 | 1 Node.js在js层维护了一个二叉堆。 96 | 2 堆的每个节点维护了一个链表,这个链表中,最久超时的排到后面。 97 | 3 另外Node.js还维护了一个map,map的key是相对超时时间,值就是对应的二叉堆节点。 98 | 4 堆的所有节点对应底层的一个超时节点。 99 | 100 | 当我们调用setTimeout的时候,首先根据setTimeout的入参,从map中找到二叉堆节点,然后插入链表的尾部。必要的时候,Node.js会根据js二叉堆的最快超时时间来更新底层节点的超时时间。当事件循环处理定时器阶段的时候,Node.js会遍历js二叉堆,然后拿到过期的节点,再遍历过期节点中的链表,逐个判断是否需要执行回调。必要的时候调整js二叉堆和底层的超时时间。 101 | ## check、idle、prepare阶段 102 | check、idle、prepare阶段相对比较简单,每个阶段维护一个队列,然后在处理对应阶段的时候,执行队列中每个节点的回调,不过这三个阶段比较特殊的是,队列中的节点被执行后不会被删除,而是虎一直在队列里,除非显式删除。 103 | ![](https://img-blog.csdnimg.cn/20210526033121707.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 104 | ## pending、closing阶段 105 | pending阶段:在poll io回调里产生的回调。 106 | closing阶段:执行关闭handle的回调。 107 | pending和closing阶段也是维护了一个队列,然后在对应阶段的时候执行每个节点的回调,最后删除对应的节点。 108 | ![](https://img-blog.csdnimg.cn/20210526033156857.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 109 | ## Poll io阶段 110 | Poll io阶段是最重要和复杂的一个阶段,下面我们看一下实现。首先我们看一下poll io阶段核心的数据结构:io观察者。io观察者是对文件描述符、感兴趣事件和回调的封装。主要是用在epoll中。 111 | ![](https://img-blog.csdnimg.cn/20210526033217770.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 112 | 当我们有一个文件描述符需要被epoll监听的时候 113 | 1 我们可以创建一个io观察者。 114 | 2 调用uv__io_start往事件循环中插入一个io观察者队列。 115 | 3 Libuv会记录文件描述符和io观察者的映射关系。 116 | 4 在poll io阶段的时候就会遍历io观察者队列,然后操作epoll去做相应的处理。 117 | 5 等从epoll返回的时候,我们就可以拿到哪些文件描述符的事件触发了,最后根据文件描述符找到对应的io观察者并执行他的回调就行。 118 | ![](https://img-blog.csdnimg.cn/20210526033338343.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 119 | 120 | 另外我们看到,poll io阶段会可能会阻塞,是否阻塞和阻塞多久取决于事件循环系统当前的状态。当发生阻塞的时候,为了保证定时器阶段按时执行,epoll阻塞的时间需要设置为等于最快到期定时器节点的时间。 121 | # 进程和进程间通信 122 | ## 创建进程 123 | Node.js中的进程是使用fork+exec模式创建的,fork就是复制主进程的数据,exec是加载新的程序执行。Node.js提供了异步和同步创建进程两种模式。 124 | 125 | 1 异步方式 126 | 异步方式就是创建一个人子进程后,主进程和子进程独立执行,互不干扰。在主进程的数据结构中如图所示,主进程会记录子进程的信息,子进程退出的时候会用到 127 | ![](https://img-blog.csdnimg.cn/202105260334330.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 128 | 2 同步方式 129 | ![](https://img-blog.csdnimg.cn/2021052603345684.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 130 | 同步创建子进程会导致主进程阻塞,具体的实现是 131 | 1 主进程中会新建一个新的事件循环结构体,然后基于这个新的事件循环创建一个子进程。 132 | 2 然后主进程就在新的事件循环中执行,旧的事件循环就被阻塞了。 133 | 3 子进程结束的时候,新的事件循环也就结束了,从而回到旧的事件循环。 134 | ## 进程间通信 135 | 接下来我们看一下父子进程间怎么通信呢?在操作系统中,进程间的虚拟地址是独立的,所以没有办法基于进程内存直接通信,这时候需要借助内核提供的内存。进程间通信的方式有很多种,管道、信号、共享内存等等。 136 | ![](https://img-blog.csdnimg.cn/20210526033529361.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 137 | Node.js选取的进程间通信方式是Unix域,Node.js为什么会选取Unix域呢?因为只有Unix域支持文件描述符传递。文件描述符传递是一个非常重要的能力。 138 | 139 | 首先我们看一下文件系统和进程的关系,在操作系统中,当进程打开一个文件的时候,他就是形成一个fd file inode这样的关系,这种关系在fork子进程的时候会被继承。 140 | ![](https://img-blog.csdnimg.cn/20210526033557917.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 141 | 但是如果主进程在fork子进程之后,打开了一个文件,他想告诉子进程,那怎么办呢?如果仅仅是把文件描述符对应的数字传给子进程,子进程是没有办法知道这个数字对应的文件的。如果通过Unix域发送的话,系统会把文件描述符和文件的关系也复制到子进程中。 142 | ![](https://img-blog.csdnimg.cn/20210526044410934.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 143 | 144 | 具体实现 145 | 1 Node.js底层通过socketpair创建两个文件描述符,主进程拿到其中一个文件描述符,并且封装send和on meesage方法进行进程间通信。 146 | 2 接着主进程通过环境变量把另一个文件描述符传给子进程。 147 | 3 子进程同样基于文件描述符封装发送和接收数据的接口。 148 | 这样两个进程就可以进行通信了。 149 | ![](https://img-blog.csdnimg.cn/20210526033718305.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 150 | # 线程和线程间通信 151 | ## 线程架构 152 | Node.js是单线程的,为了方便用户处理耗时的操作,Node.js在支持多进程之后,又支持了多线程。Node.js中多线程的架构如下图所示。每个子线程本质上是一个独立的事件循环,但是所有的线程会共享底层的Libuv线程池。 153 | ![](https://img-blog.csdnimg.cn/20210526033743455.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 154 | ## 创建线程 155 | 接下来我们看看创建线程的过程。 156 | ![](https://img-blog.csdnimg.cn/20210526033810406.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 157 | 当我们调用new Worker创建线程的时候 158 | 1 主线程会首先创建创建两个通信的数据结构,接着往对端发送一个加载js文件的消息。 159 | 2 然后调用底层接口创建一个线程。 160 | 3 这时候子线程就被创建出来了,子线程被创建后首先初始化自己的执行环境和上下文。 161 | 4 接着从通信的数据结构中读取消息,然后加载对应的js文件执行,最后进入事件循环。 162 | ## 线程间通信 163 | 那么Node.js中的线程是如何通信的呢?线程和进程不一样,进程的地址空间是独立的,不能直接通信,但是线程的地址是共享的,所以可以基于进程的内存直接进行通信。 164 | ![](https://img-blog.csdnimg.cn/20210526033837869.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 165 | 下面我们看看Node.js是如何实现线程间通信的。了解Node.js线程间通信之前,我们先看一下一些核心数据结构。 166 | 1 Message代表一个消息。 167 | 2 MessagePortData是对操作Message的封装和对消息的承载。 168 | 3 MessagePort是代表通信的端点,是对MessagePortData的封装。 169 | 4 MessageChannel是代表通信的两端,即两个MessagePort。 170 | ![](https://img-blog.csdnimg.cn/20210526033910227.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 171 | 172 | 我们看到两个port是互相关联的,当需要给对端发送消息的时候,只需要往对端的消息队列插入一个节点就行。 173 | 174 | 我们来看看通信的具体过程 175 | 1 线程1调用postMessage发送消息。 176 | 2 postMessage会先对消息进行序列化。 177 | 3 然后拿到对端消息队列的锁,并把消息插入队列中。 178 | 4 成功发送消息后,还需要通知消息接收者所在的线程。 179 | 5 消息接收者会在事件循环的poll io阶段处理这个消息。![在这里插入图片描述](https://img-blog.csdnimg.cn/20210526033922118.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 180 | # Cluster 181 | 我们知道Node.js是单进程架构的,不能很好地利用多核,Cluster模块使得Node.js支持多进程的服务器架构。支持轮询(主进程accept)和共享(子进程accept)两种模式。可以通过环境变量进行设置。多进程的服务器架构通常有两种模式,第一种是主进程处理连接,然后分发给子进程处理,第二种是子进程共享socket,通过竞争的方式获取连接进行处理。 182 | ![](https://img-blog.csdnimg.cn/20210526034038349.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 183 | 我们看一下Cluster模块是如何使用的。 184 | ![](https://img-blog.csdnimg.cn/20210526034125900.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 185 | 这个是Cluster模块的使用例子 186 | 1 主进程调用fork创建子进程。 187 | 2 子进程启动一个服务器。 188 | 通常来说,多个进程监听同一个端口会报错,我们看看Node.js里是怎么处理这个问题的。 189 | 190 | ## 主进程accept 191 | ![](https://img-blog.csdnimg.cn/20210526034152940.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 192 | 我们先看一下主进程accept这种模式。 193 | 1 首先主进程fork多个子进程处理。 194 | 2 然后在每个子进程里调用listen。 195 | 3 调用listen函数的时候,子进程会给主进程发送一个消息。 196 | 4 这时候主进程就会创建一个socket,绑定地址,并置为监听状态。 197 | 5 当连接到来的时候,主进程负责接收连接,然后然后通过文件描述符传递的方式分发给子进程处理。 198 | ## 子进程accept 199 | ![](https://img-blog.csdnimg.cn/20210526034212740.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 200 | 我们再看一下子进程accept这种模式。 201 | 1 首先主进程fork多个子进程处理。 202 | 2 然后在每个子进程里调用listen。 203 | 3 调用listen函数的时候,子进程会给主进程发送一个消息。 204 | 4 这时候主进程就会创建一个socket,并绑定地址。但不会把它置为监听状态,而是把这个socket通过文件描述符的方式返回给子进程。 205 | 5 当连接到来的时候,这个连接会被某一个子进程处理。 206 | # Libuv线程池 207 | 为什么需要使用线程池?文件IO、DNS、CPU密集型不适合在Node.js主线程处理,需要把这些任务放到子线程处理。 208 | ![](https://img-blog.csdnimg.cn/20210526034232881.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 209 | 了解线程池实现之前我们先看看Libuv的异步通信机制,异步通信指的是Libuv主线程和其他子线程之间的通信机制。比如Libuv主线程正在执行回调,子线程同时完成了一个任务,那么如何通知主线程,这就需要用到异步通信机制。 210 | ![](https://img-blog.csdnimg.cn/20210526034309919.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 211 | 1 Libuv内部维护了一个异步通信的队列,需要异步通信的时候,就往里面插入一个async节点。 212 | 2 同时Libuv还维护了一个异步通信相关的io观察者。 213 | 3 当有异步任务完成的时候,就会设置对应async节点的pending字段为1,说明任务完成了。并且通知主线程。 214 | 4 主线程在poll io阶段就会执行处理异步通信的回调,在回调里会执行pending为1的节点的回调。 215 | 216 | 下面我们来看一下线程池的实现。 217 | 1 线程池维护了一个待处理任务队列,多个线程互斥地从队列中摘下任务进行处理。 218 | 2 当给线程池提交一个任务的时候,就是往这个队列里插入一个节点。 219 | 3 当子线程处理完任务后,就会把这个任务插入到事件循环本身维护到一个已完成任务队列中,并且通过异步通信的机制通知主线程。 220 | 4 主线程在poll io阶段就会执行任务对应的回调。 221 | ![](https://img-blog.csdnimg.cn/20210526034329529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 222 | # 信号 223 | ![](https://img-blog.csdnimg.cn/20210526034350155.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 224 | 上图是操作系统中信号的表示,操作系统使用一个long类型表示进程收到的信息,并且用一个数组来标记对应的处理函数。 225 | 226 | 我们看一下信号在Libuv中是如何实现的。 227 | ![](https://img-blog.csdnimg.cn/20210526034510342.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 228 | 1 Libuv中维护了一个红黑树,当我们监听一个新的信号时就会新插入一个节点。 229 | 2 在插入第一个节点时,Libuv会封装一个io观察者注册到epoll中,用来监听是否有信号需要处理。 230 | 3 当信号发生的时候,就会根据信号类型从红黑树中找到对应的handle,然后通知主线程。 231 | 4 主线程在poll io阶段就会逐个执行回调。 232 | 233 | Node.js中,是通过监听newListener事件来实现信号的监听的,newListener是一种hooks的机制。每次监听事件的时候,如果监听了该事件,那就会触发newListener事件。所以当执行process.on(’SIGINT’)时,就会调用startListeningIfSignal注册一个红黑树节点。 并在events模块保存了订阅关系,信号触发时,执行process.emit(‘SIGINT’)通知订阅者。 234 | ![](https://img-blog.csdnimg.cn/20210526034720312.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 235 | # 文件 236 | ## 文件操作 237 | Node.js中文件操作分为同步和异步模式,同步模式就是在主进程中直接调用文件系统的api,这种方式可能会引起进程的阻塞,异步方式是借助了Libuv线程池,把阻塞操作放到子线程中去处理,主线程可以继续处理其他操作。 238 | ![](https://img-blog.csdnimg.cn/20210526034829365.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 239 | ## 文件监听 240 | Node.js中文件监听提供了基于轮询和订阅发布两种模式。我们先看一下轮询模式的实现,轮询模式比较简单,他是使用定时器实现的,Node.js会定时执行回调,在回调中比较当前文件的元数据和上一次获取的是否不一样,如果是则说明文件改变了。 241 | ![](https://img-blog.csdnimg.cn/2021052603491188.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 242 | 第二种监听模式是更高效的inotify机制,inotify是基于订阅发布模式的,避免了无效的轮询。我们首先看一下操作系统的inotify机制,inotify和epoll的使用是类似的 243 | 1 首先通过接口获取一个inotify实例对应的文件描述符。 244 | 2 然后通过增删改查接口操作inotify实例,比如需要监听一个文件的时候,就调用接口往inotify实例中新增一个订阅关系。 245 | 3 当文件发生改变的时候,我们可以调用read接口获取哪些文件发生了改变,inotify通常结合epoll来使用。 246 | 247 | 接下来我们看看Node.js中是如何基于inotify机制 实现文件监听的。 248 | ![](https://img-blog.csdnimg.cn/20210526034934418.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 249 | 250 | 1 首先Node.js把inotify实例的文件描述符和回调封装成io观察者注册到epoll中。 251 | 2 当需要监听一个文件的时候,Node.js会调用系统函数往inotify实例中插入一个项,并且拿到一个id,接着Node.js把这个id和文件信息封装到一个结构体中,然后插入红黑树。 252 | 3 Node.js维护了一棵红黑树,红黑树的每个节点记录了被监听的文件或目录和事件触发时的回调列表。 253 | 4 如果有事件触发时,在poll io阶段就会执行对应的回调,回调里会判断哪些文件发生了变化,然后根据id从红黑树中找到对应的接口,从而执行对应的回调。 254 | 255 | # TCP 256 | 我们通常会调用http.createServer.listen启动一个服务器,那么这个过程到底做了什么呢?listen函数其实是对网络api的封装, 257 | 1 首先获取一个socket。 258 | 2 然后绑定地址到该socket中。 259 | 3 接着调用listen函数把该socket改成监听状态。 260 | 4 最后把该socket注册到epoll中,等待连接的到来。 261 | 262 | 那么Node.js是如何处理连接的呢?当建立了一个tcp连接后,Node.js会在poll io阶段执行对应的回调。 263 | 1 Node.js会调用accept摘下一个tcp连接。 264 | 2 接着会调c++层,c++层会新建一个对象表示和客户端通信的实例。 265 | 3 接着回调js层,js也会新建一个对象表示通信的实例,主要是给用户使用。 266 | 4 最后注册等待可读事件,等待客户端发送数据过来。 267 | 268 | 这就是Node.js处理一个连接的过程,处理完一个连接后,Node.js会判断是否设置了single_accept标记,如果有则睡眠一段时间,给其他进程处理剩下的连接,一定程度上避免负责不均衡,如果没有设置该标记,Node.js会继续尝试处理下一个连接。这就是Node.js处理连接的整个过程。 269 | ![](https://img-blog.csdnimg.cn/20210526034959572.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 270 | # UDP 271 | 因为udp是非连接、不可靠的协议,在实现和使用上相对比较简单,这里讲一下发送udp数据的过程,当我们发送一个udp数据包的时候,Libuv会把数据先插入等待发送队列,接着在epoll中注册等待可写事件,当可写事件触发的时候,Libuv会遍历等待发送队列,逐个节点发送,成功发送后,Libuv会把节点移到发送成功队列,并往pending阶段插入一个节点,在pending阶段,Libuv就会执行发送完成队列里每个节点的会调通知调用方发送结束。 272 | ![](https://img-blog.csdnimg.cn/20210526035019803.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 273 | # DNS 274 | 因为通过域名查找ip或通过ip查找域名的api是阻塞式的,所以这两个功能是借助了Libuv的线程池实现的。发起一个查找操作的时候,Node.js会往线程池提及一个任务,然后就继续处理其他事情,同时,线程池的子线程会调用库函数做dns查询,查询结束后,子线程会把结果交给主线程。这就是整个查找过程。 275 | ![](https://img-blog.csdnimg.cn/20210526035035931.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 276 | 其他的dns操作是通过cares实现的,cares是一个异步dns库,我们知道dns是一个应用层协议,cares就是实现了这个协议。我们看一下Node.js是怎么使用cares实现dns操作的。 277 | ![](https://img-blog.csdnimg.cn/20210526035052552.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 278 | 279 | 1 首先Node.js初始化的时候,会初始化cares库,其中最重要的是设置socket变更的回调。我们一会可以看到这个回调的作用。 280 | 2 当我们发起一个dns操作的时候,Node.js会调用cares的接口,cares接口会创建一个socket并发起一个dns查询,接着通过状态变更回调把socket传给Node.js。 281 | 3 Node.js把这个socket注册到epoll中,等待查询结果,当查询结果返回的时候,Node.js会调用cares的函数进行解析。最后调用js回调通知用户。 282 | 283 | 以上就是所有分享的内容,谢谢。 284 | 更多内容参考:https://github.com/theanarkh/understand-nodejs​ 285 | -------------------------------------------------------------------------------- /docs/chapter11-setImmediate和nextTick.md: -------------------------------------------------------------------------------- 1 | setImmediate对应Libuv的check阶段。所提交的任务会在Libuv事件循环的check阶段被执行,check阶段的任务会在每一轮事件循环中被执行,但是setImmediate提交的任务只会执行一次,下面我们会看到Node.js是怎么处理的,我们看一下具体的实现。 2 | ## 11.1 setImmediate 3 | ### 11.1.1设置处理immediate任务的函数 4 | 在Node.js初始化的时候,设置了处理immediate任务的函数 5 | 6 | ```js 7 | // runNextTicks用于处理nextTick产生的任务,这里不关注 8 | const { processImmediate, processTimers } = getTimerCallbacks(runNextTicks); 9 | setupTimers(processImmediate, processTimers); 10 | ``` 11 | 12 | 13 | 我们先看看一下setupTimers(timer.cc)的逻辑。 14 | 15 | ```cpp 16 | void SetupTimers(const FunctionCallbackInfo& args) { 17 | auto env = Environment::GetCurrent(args); 18 | env->set_immediate_callback_function(args[0].As()); 19 | env->set_timers_callback_function(args[1].As()); 20 | } 21 | ``` 22 | 23 | SetupTimers在env中保存了两个函数processImmediate, processTimers,processImmediate是处理immediate任务的,processTimers是处理定时器任务的,在定时器章节我们已经分析过。 24 | ### 11.1.2 注册check阶段的回调 25 | 在Node.js初始化的时候,同时初始化了immediate任务相关的数据结构和逻辑。 26 | 27 | ```cpp 28 | void Environment::InitializeLibuv(bool start_profiler_idle_notifier) { 29 | // 初始化immediate相关的handle 30 | uv_check_init(event_loop(), immediate_check_handle()); 31 | // 修改状态为unref,避免没有任务的时候,影响事件循环的退出 32 | uv_unref(reinterpret_cast(immediate_check_handle())); 33 | // 激活handle,设置回调 34 | uv_check_start(immediate_check_handle(), CheckImmediate); 35 | // 在idle阶段也插入一个相关的节点 36 | uv_idle_init(event_loop(), immediate_idle_handle()); 37 | } 38 | ``` 39 | 40 | Node.js默认会往check阶段插入一个节点,并设置回调为CheckImmediate,但是初始化状态是unref的,所以如果没有immediate任务的话,不会影响事件循环的退出。我们看一下CheckImmediate函数 41 | 42 | ```cpp 43 | void Environment::CheckImmediate(uv_check_t* handle) { 44 | // 省略部分代码 45 | // 没有Immediate节点需要处理 46 | if (env->immediate_info()->count() == 0 || 47 | !env->can_call_into_js()) 48 | return; 49 | do { 50 | // 执行JS层回调immediate_callback_function 51 | MakeCallback(env->isolate(), 52 | env->process_object(), 53 | env->immediate_callback_function(), 54 | 0, 55 | nullptr, 56 | {0, 0}).ToLocalChecked(); 57 | } while (env->immediate_info()->has_outstanding() && 58 | env->can_call_into_js()); 59 | /* 60 | 所有immediate节点都处理完了,置idle阶段对应节点为非激活状态, 61 | 允许Poll IO阶段阻塞和事件循环退出 62 | */ 63 | if (env->immediate_info()->ref_count() == 0) 64 | env->ToggleImmediateRef(false); 65 | } 66 | ``` 67 | 68 | 我们看到每一轮事件循环时,CheckImmediate都会被执行,但是如果没有需要处理的任务则直接返回。如果有任务,CheckImmediate函数执行immediate_callback_function函数,这正是Node.js初始化的时候设置的函数processImmediate。看完初始化和处理immediate任务的逻辑后,我们看一下如何产生一个immediate任务。 69 | ### 11.1.3 setImmediate生成任务 70 | 我们可以通过setImmediate生成一个任务。 71 | 72 | ```js 73 | function setImmediate(callback, arg1, arg2, arg3) { 74 | let i, args; 75 | switch (arguments.length) { 76 | case 1: 77 | break; 78 | case 2: 79 | args = [arg1]; 80 | break; 81 | case 3: 82 | args = [arg1, arg2]; 83 | break; 84 | default: 85 | args = [arg1, arg2, arg3]; 86 | for (i = 4; i < arguments.length; i++) { 87 | args[i - 1] = arguments[i]; 88 | } 89 | break; 90 | } 91 | 92 | return new Immediate(callback, args); 93 | } 94 | ``` 95 | 96 | setImmediate的代码比较简单,新建一个Immediate。我们看一下Immediate的类。 97 | 98 | ```js 99 | const Immediate = class Immediate { 100 | constructor(callback, args) { 101 | this._idleNext = null; 102 | this._idlePrev = null; 103 | this._onImmediate = callback; 104 | this._argv = args; 105 | this._destroyed = false; 106 | this[kRefed] = false; 107 | this.ref(); 108 | // Immediate链表的节点个数,包括ref和unref状态 109 | immediateInfo[kCount]++; 110 | // 加入链表中 111 | immediateQueue.append(this); 112 | } 113 | // 打上ref标记,往Libuv的idle链表插入一个激活状态的节点,如果还没有的话 114 | ref() { 115 | if (this[kRefed] === false) { 116 | this[kRefed] = true; 117 | if (immediateInfo[kRefCount]++ === 0) 118 | toggleImmediateRef(true); 119 | } 120 | return this; 121 | } 122 | // 和上面相反 123 | unref() { 124 | if (this[kRefed] === true) { 125 | this[kRefed] = false; 126 | if (--immediateInfo[kRefCount] === 0) 127 | toggleImmediateRef(false); 128 | } 129 | return this; 130 | } 131 | 132 | hasRef() { 133 | return !!this[kRefed]; 134 | } 135 | }; 136 | ``` 137 | 138 | Immediate类主要做了两个事情。 139 | 140 | 1 生成一个节点插入到链表。 141 | 142 | ```js 143 | const immediateQueue = new ImmediateList(); 144 | 145 | // 双向非循环的链表 146 | function ImmediateList() { 147 | this.head = null; 148 | this.tail = null; 149 | } 150 | ImmediateList.prototype.append = function(item) { 151 | // 尾指针非空,说明链表非空,直接追加在尾节点后面 152 | if (this.tail !== null) { 153 | this.tail._idleNext = item; 154 | item._idlePrev = this.tail; 155 | } else { 156 | // 尾指针是空说明链表是空的,头尾指针都指向item 157 | this.head = item; 158 | } 159 | this.tail = item; 160 | }; 161 | 162 | ImmediateList.prototype.remove = function(item) { 163 | // 如果item在中间则自己全身而退,前后两个节点连上 164 | if (item._idleNext !== null) { 165 | item._idleNext._idlePrev = item._idlePrev; 166 | } 167 | 168 | if (item._idlePrev !== null) { 169 | item._idlePrev._idleNext = item._idleNext; 170 | } 171 | // 是头指针,则需要更新头指针指向item的下一个,因为item被删除了,尾指针同理 172 | if (item === this.head) 173 | this.head = item._idleNext; 174 | if (item === this.tail) 175 | this.tail = item._idlePrev; 176 | // 重置前后指针 177 | item._idleNext = null; 178 | item._idlePrev = null; 179 | }; 180 | ``` 181 | 182 | 2 如果还没有往Libuv的idle链表里插入一个激活节点的话,则插入一个。从之前的分析,我们知道,Node.js在check阶段插入了一个unref节点,在每次check阶段都会执行该节点的回调,那么这个idle节点有什么用呢?答案在uv_backend_timeout函数中,uv_backend_timeout定义了Poll IO阻塞的时长,如果有ref状态的idle节点则Poll IO阶段不会阻塞(但是不会判断是否有check节点)。所以当有immediate任务时,Node.js会把这个idle插入idle阶段中,表示有任务处理,不能阻塞Poll IO阶段。没有immediate任务时,则移除idle节点。总的来说,idle节点的意义是标记是否有immediate任务需要处理,有的话就不能阻塞Poll IO阶段,并且不能退出事件循环。 183 | 184 | ```cpp 185 | void ToggleImmediateRef(const FunctionCallbackInfo& args) { 186 | Environment::GetCurrent(args)->ToggleImmediateRef(args[0]->IsTrue()) 187 | } 188 | 189 | void Environment::ToggleImmediateRef(bool ref) { 190 | if (started_cleanup_) return; 191 | // 改变handle的状态(激活或不激活),防止在Poll IO阶段阻塞 192 | if (ref) { 193 | uv_idle_start(immediate_idle_handle(), [](uv_idle_t*){ }); 194 | } else { 195 | // 不阻塞Poll IO,允许事件循环退出 196 | uv_idle_stop(immediate_idle_handle()); 197 | } 198 | } 199 | ``` 200 | 201 | 这是setImmediate函数的整个过程。和定时器一样,我们可以调用immediate任务的ref和unref函数,控制它对事件循环的影响。 202 | ### 11.1.4 处理setImmediate产生的任务 203 | 最后我们看一下在check阶段时,是如何处理immediate任务的。由前面分析我们知道processImmediate函数是处理immediate任务的函数,来自getTimerCallbacks(internal/timer.js)。 204 | 205 | ```js 206 | function processImmediate() { 207 | /* 208 | 上次执行processImmediate的时候如果由未捕获的异常, 209 | 则outstandingQueue保存了未执行的节点,下次执行processImmediate的时候, 210 | 优先执行outstandingQueue队列的节点 211 | */ 212 | const queue = outstandingQueue.head !== null ? 213 | outstandingQueue : immediateQueue; 214 | let immediate = queue.head; 215 | /* 216 | 在执行immediateQueue队列的话,先置空队列,避免执行回调 217 | 的时候一直往队列加节点,死循环。 所以新加的接口会插入新的队列, 218 | 不会在本次被执行。并打一个标记,全部immediateQueue节点都被执 219 | 行则清空,否则会再执行processImmediate一次,见Environment::CheckImmediate 220 | */ 221 | if (queue !== outstandingQueue) { 222 | queue.head = queue.tail = null; 223 | immediateInfo[kHasOutstanding] = 1; 224 | } 225 | 226 | let prevImmediate; 227 | let ranAtLeastOneImmediate = false; 228 | while (immediate !== null) { 229 | // 执行微任务 230 | if (ranAtLeastOneImmediate) 231 | runNextTicks(); 232 | else 233 | ranAtLeastOneImmediate = true; 234 | 235 | // 微任务把该节点删除了,则不需要指向它的回调了,继续下一个 236 | if (immediate._destroyed) { 237 | outstandingQueue.head = immediate = prevImmediate._idleNext; 238 | continue; 239 | } 240 | 241 | immediate._destroyed = true; 242 | // 执行完要修改个数 243 | immediateInfo[kCount]--; 244 | if (immediate[kRefed]) 245 | immediateInfo[kRefCount]--; 246 | immediate[kRefed] = null; 247 | // 见上面if (immediate._destroyed)的注释 248 | prevImmediate = immediate; 249 | // 执行回调,指向下一个节点 250 | try { 251 | const argv = immediate._argv; 252 | if (!argv) 253 | immediate._onImmediate(); 254 | else 255 | immediate._onImmediate(...argv); 256 | } finally { 257 | immediate._onImmediate = null; 258 | outstandingQueue.head = immediate = immediate._idleNext; 259 | } 260 | } 261 | // 当前执行的是outstandingQueue的话则把它清空 262 | if (queue === outstandingQueue) 263 | outstandingQueue.head = null; 264 | // 全部节点执行完 265 | immediateInfo[kHasOutstanding] = 0; 266 | } 267 | ``` 268 | 269 | processImmediate的逻辑就是逐个执行immediate任务队列的节点。Immediate分两个队列,正常情况下,插入的immediate节点插入到immediateQueue队列。如果执行的时候有异常,则未处理完的节点就会被插入到outstandingQueue队列,等下一次执行。另外我们看到runNextTicks。runNextTicks在每执行完immediate节点后,都先处理tick任务然后再处理下一个immediate节点。 270 | ### 11.1.5 Node.js的setTimeout(fn,0)和setImmediate谁先执行的问题 271 | 我们首先看一下下面这段代码 272 | 273 | ```js 274 | setTimeout(()=>{ console.log('setTimeout'); },0) 275 | setImmediate(()=>{ console.log('setImmedate');}) 276 | ``` 277 | 278 | 我们执行上面这段代码,会发现输出是不确定的。下面来看一下为什么。Node.js的事件循环分为几个阶段(phase)。setTimeout是属于定时器阶段,setImmediate是属于check阶段。顺序上定时器阶段是比check更早被执行的。其中setTimeout的实现代码里有一个很重要的细节。 279 | 280 | ```js 281 | after *= 1; // coalesce to number or NaN 282 | if (!(after >= 1 && after <= TIMEOUT_MAX)) { 283 | if (after > TIMEOUT_MAX) { 284 | process.emitWarning(`错误提示`); 285 | } 286 | after = 1; // schedule on next tick, follows browser behavior 287 | } 288 | ``` 289 | 290 | 我们发现虽然我们传的超时时间是0,但是0不是合法值,Node.js会把超时时间变成1。这就是导致上面的代码输出不确定的原因。我们分析一下这段代码的执行过程。Node.js启动的时候,会编译执行上面的代码,开始一个定时器,挂载一个setImmediate节点在队列。然后进入Libuv的事件循环,然后执行定时器阶段,Libuv判断从开启定时器到现在是否已经过去了1毫秒,是的话,执行定时器回调,否则执行下一个节点,执行完其它阶段后,会执行check阶段。这时候就会执行setImmediate的回调。所以,一开始的那段代码的输出结果是取决于启动定时器的时间到Libuv执行定时器阶段是否过去了1毫秒。 291 | ## 11.2 nextTick 292 | nextTick用于异步执行一个回调函数,和setTimeout、setImmediate类似,不同的地方在于他们的执行时机,setTimeout和setImmediate的任务属于事件循环的一部分,但是nextTick的任务不属于事件循环的一部分,具体的执行时机我们会在本节分析。 293 | ### 11.2.1 初始化nextTick 294 | nextTick函数是在Node.js启动过程中,在执行bootstrap/node.js时挂载到process对象中。 295 | 296 | ```js 297 | const { nextTick, runNextTicks } = setupTaskQueue(); 298 | process.nextTick = nextTick; 299 | // 真正的定义在task_queues.js。 300 | setupTaskQueue() { 301 | setTickCallback(processTicksAndRejections); 302 | return { 303 | nextTick, 304 | }; 305 | }, 306 | ``` 307 | 308 | nextTick接下来会讲,setTickCallback是注册处理tick任务的函数, 309 | 310 | ```cpp 311 | static void SetTickCallback(const FunctionCallbackInfo& args) { 312 | Environment* env = Environment::GetCurrent(args); 313 | CHECK(args[0]->IsFunction()); 314 | env->set_tick_callback_function(args[0].As()); 315 | } 316 | ``` 317 | 318 | 只是简单地保存处理tick任务的函数。后续会用到 319 | ### 11.2.2 nextTick生产任务 320 | 321 | ```js 322 | function nextTick(callback) { 323 | let args; 324 | switch (arguments.length) { 325 | case 1: break; 326 | case 2: args = [arguments[1]]; break; 327 | case 3: args = [arguments[1], arguments[2]]; break; 328 | case 4: args = [arguments[1], arguments[2], arguments[3]]; break; 329 | default: 330 | args = new Array(arguments.length - 1); 331 | for (let i = 1; i < arguments.length; i++) 332 | args[i - 1] = arguments[i]; 333 | } 334 | // 第一个任务,开启tick处理逻辑 335 | if (queue.isEmpty()) 336 | setHasTickScheduled(true); 337 | const asyncId = newAsyncId(); 338 | const triggerAsyncId = getDefaultTriggerAsyncId(); 339 | const tickObject = { 340 | [async_id_symbol]: asyncId, 341 | [trigger_async_id_symbol]: triggerAsyncId, 342 | callback, 343 | args 344 | }; 345 | // 插入队列 346 | queue.push(tickObject); 347 | } 348 | ``` 349 | 350 | 这就是我们执行nextTick时的逻辑。每次调用nextTick都会往队列中追加一个节点。 351 | ### 11.2.3 处理tick任务 352 | 我们再看一下处理的tick任务的逻辑。Nodejs在初始化时,通过执行setTickCallback(processTicksAndRejections)注册了处理tick任务的函数。Node.js在初始化时把处理tick任务的函数保存到env中。另外,Nodejs使用TickInfo类管理tick的逻辑。 353 | 354 | ```js 355 | class TickInfo : public MemoryRetainer { 356 | public: 357 | inline AliasedUint8Array& fields(); 358 | inline bool has_tick_scheduled() const; 359 | inline bool has_rejection_to_warn() const; 360 | private: 361 | inline explicit TickInfo(v8::Isolate* isolate); 362 | enum Fields { kHasTickScheduled = 0, kHasRejectionToWarn, kFieldsCount }; 363 | 364 | AliasedUint8Array fields_; 365 | }; 366 | ``` 367 | 368 | TickInfo主要是有两个标记位,kHasTickScheduled标记是否有tick任务需要处理。然后通过InternalCallbackScope类的对象方法Close函数执行tick_callback_function。当Nodejs底层需要执行一个js回调时,会调用AsyncWrap的MakeCallback。MakeCallback里面调用了InternalMakeCallback。 369 | 370 | ```cpp 371 | MaybeLocal InternalMakeCallback(Environment* env, Local recv, 372 | const Local callback, int argc, Local argv[], 373 | async_context asyncContext) { 374 | InternalCallbackScope scope(env, recv, asyncContext); 375 | // 执行用户层js回调 376 | scope.Close(); 377 | 378 | return ret; 379 | } 380 | ``` 381 | 382 | 我们看InternalCallbackScope 的Close 383 | 384 | ```cpp 385 | void InternalCallbackScope::Close() { 386 | // 省略部分代码 387 | TickInfo* tick_info = env_->tick_info(); 388 | // 没有tick任务则不需要往下走,在插入tick任务的时候会设置这个为true,没有任务时变成false 389 | if (!tick_info->has_tick_scheduled() && !tick_info->has_rejection_to_warn()) { 390 | return; 391 | } 392 | 393 | HandleScope handle_scope(env_->isolate()); 394 | Local process = env_->process_object(); 395 | 396 | if (!env_->can_call_into_js()) return; 397 | // 处理tick的函数 398 | Local tick_callback = env_->tick_callback_function(); 399 | // 处理tick任务 400 | if (tick_callback->Call(env_->context(), process, 0, nullptr).IsEmpty()) { 401 | failed_ = true; 402 | } 403 | } 404 | ``` 405 | 406 | 我们看到每次执行js层的回调的时候,就会处理tick任务。Close函数可以主动调用,或者在InternalCallbackScope对象析构的时候被调用。除了执行js回调时是主动调用Close外,一般处理tick任务的时间点就是在InternalCallbackScope对象被析构的时候。所以在定义了InternalCallbackScope对象的时候,一般就会在对象析构的时候,进行tick任务的处理。另外一种就是在执行的js回调里,调用runNextTicks处理tick任务。比如执行immediate任务的过程中。 407 | 408 | ```js 409 | function runNextTicks() { 410 | if (!hasTickScheduled() && !hasRejectionToWarn()) 411 | runMicrotasks(); 412 | if (!hasTickScheduled() && !hasRejectionToWarn()) 413 | return; 414 | processTicksAndRejections(); 415 | } 416 | ``` 417 | 418 | 我们看processTicksAndRejections是如何处理tick任务的。 419 | 420 | ```js 421 | function processTicksAndRejections() { 422 | let tock; 423 | do { 424 | while (tock = queue.shift()) { 425 | const asyncId = tock[async_id_symbol]; 426 | emitBefore(asyncId, tock[trigger_async_id_symbol]); 427 | 428 | try { 429 | const callback = tock.callback; 430 | if (tock.args === undefined) { 431 | callback(); 432 | } else { 433 | const args = tock.args; 434 | switch (args.length) { 435 | case 1: callback(args[0]); break; 436 | case 2: callback(args[0], args[1]); break; 437 | case 3: callback(args[0], args[1], args[2]); break; 438 | case 4: callback(args[0], args[1], args[2], args[3]); break; 439 | default: callback(...args); 440 | } 441 | } 442 | } finally { 443 | if (destroyHooksExist()) 444 | emitDestroy(asyncId); 445 | } 446 | 447 | emitAfter(asyncId); 448 | } 449 | runMicrotasks(); 450 | } while (!queue.isEmpty() || processPromiseRejections()); 451 | setHasTickScheduled(false); 452 | setHasRejectionToWarn(false); 453 | } 454 | ``` 455 | 456 | 从processTicksAndRejections代码中,我们可以看到,Node.js是实时从任务队列里取节点执行的,所以如果我们在nextTick的回调里一直调用nextTick的话,就会导致死循环。 457 | 458 | ```js 459 | function test() { 460 | process.nextTick(() => { 461 | console.log(1); 462 | test() 463 | }); 464 | } 465 | test(); 466 | 467 | setTimeout(() => { 468 | console.log(2) 469 | }, 10) 470 | ``` 471 | 472 | 上面的代码中,会一直输出1,不会输出2。而在Nodejs源码的很多地方都处理了这个问题,首先把要执行的任务队列移到一个变量q2中,清空之前的队列q1。接着遍历q2指向的队列,如果执行回调的时候又新增了节点,只会加入到q1中。q2不会导致死循环。 473 | ### 11.2.4 nextTick的使用 474 | 我们知道nextTick可用于延迟执行一些逻辑,我们看一下哪些场景下可以使用nextTick。 475 | 476 | ```js 477 | const { EventEmitter } = require('events'); 478 | class DemoEvents extends EventEmitter { 479 | constructor() { 480 | super(); 481 | this.emit('start'); 482 | } 483 | } 484 | 485 | const demoEvents = new DemoEvents(); 486 | demoEvents.on('start', () => { 487 | console.log('start'); 488 | }); 489 | ``` 490 | 491 | 以上代码在构造函数中会触发start事件,但是事件的注册却在构造函数之后执行,而在构造函数之前我们还没有拿到DemoEvents对象,无法完成事件的注册。这时候,我们就可以使用nextTick。 492 | 493 | ```js 494 | const { EventEmitter } = require('events'); 495 | class DemoEvents extends EventEmitter { 496 | constructor() { 497 | super(); 498 | process.nextTick(() => { 499 | this.emit('start'); 500 | }) 501 | } 502 | } 503 | 504 | const demoEvents = new DemoEvents(); 505 | demoEvents.on('start', () => { 506 | console.log('start'); 507 | }); 508 | ``` 509 | -------------------------------------------------------------------------------- /docs/chapter02-Libuv数据结构和通用逻辑.md: -------------------------------------------------------------------------------- 1 | # 第二章Libuv数据结构和通用逻辑 2 | ## 2.1 核心结构体uv_loop_s 3 | uv_loop_s是Libuv的核心数据结构,每一个事件循环对应一个uv_loop_s结构体。它记录了整个事件循环中的核心数据。我们来分析每一个字段的意义。 4 | 5 | ```cpp 6 | 1 用户自定义数据的字段 7 | void* data; 8 | 9 | 2活跃的handle个数,会影响使用循环的退出 10 | unsigned int active_handles; 11 | 12 | 3 handle队列,包括活跃和非活跃的 13 | void* handle_queue[2]; 14 | 15 | 4 request个数,会影响事件循环的退出 16 | union { void* unused[2]; unsigned int count; } active_reqs; 17 | 18 | 5事件循环是否结束的标记 19 | unsigned int stop_flag; 20 | 21 | 6 Libuv运行的一些标记,目前只有UV_LOOP_BLOCK_SIGPROF,主要是用于epoll_wait的时候屏蔽SIGPROF信号,提高性能,SIGPROF是调操作系统settimer函数设置从而触发的信号 22 | unsigned long flags; 23 | 24 | 7 epoll的fd 25 | int backend_fd; 26 | 27 | 8 pending阶段的队列 28 | void* pending_queue[2]; 29 | 30 | 9指向需要在epoll中注册事件的uv__io_t结构体队列 31 | void* watcher_queue[2]; 32 | 33 | 10 watcher_queue队列的节点中有一个fd字段,watchers以fd为索引,记录fd所在的uv__io_t结构体 34 | uv__io_t** watchers; 35 | 36 | 11 watchers相关的数量,在maybe_resize函数里设置 37 | unsigned int nwatchers; 38 | 39 | 12 watchers里fd个数,一般为watcher_queue队列的节点数 40 | unsigned int nfds; 41 | 42 | 13线程池的子线程处理完任务后把对应的结构体插入到wq队列 43 | void* wq[2]; 44 | 45 | 14控制wq队列互斥访问,否则多个子线程同时访问会有问题 46 | uv_mutex_t wq_mutex; 47 | 48 | 15用于线程池的子线程和主线程通信 49 | uv_async_t wq_async; 50 | 51 | 16用于读写锁的互斥变量 52 | uv_rwlock_t cloexec_lock; 53 | 54 | 17 事件循环close阶段的队列,由uv_close产生 55 | uv_handle_t* closing_handles; 56 | 57 | 18 fork出来的进程队列 58 | void* process_handles[2]; 59 | 60 | 19 事件循环的prepare阶段对应的任务队列 61 | void* prepare_handles[2]; 62 | 63 | 20 事件循环的check阶段对应的任务队列 64 | void* check_handles[2]; 65 | 66 | 21 事件循环的idle阶段对应的任务队列 67 | void* idle_handles[2]; 68 | 69 | 21 async_handles队列,Poll IO阶段执行uv__async_io中遍历async_handles队列处理里面pending为1的节点 70 | void* async_handles[2]; 71 | 72 | 22用于监听是否有async handle任务需要处理 73 | uv__io_t async_io_watcher; 74 | 75 | 23用于保存子线程和主线程通信的写端fd 76 | int async_wfd; 77 | 78 | 24保存定时器二叉堆结构 79 | struct { 80 | void* min; 81 | unsigned int nelts; 82 | } timer_heap; 83 | 84 | 25 管理定时器节点的id,不断叠加 85 | uint64_t timer_counter; 86 | 87 | 26当前时间,Libuv会在每次事件循环的开始和Poll IO阶段更新当前时间,然后在后续的各个阶段使用,减少对系统调用 88 | uint64_t time; 89 | 90 | 27用于fork出来的进程和主进程通信的管道,用于子进程收到信号的时候通知主进程,然后主进程执行子进程节点注册的回调 91 | int signal_pipefd[2]; 92 | 93 | 28类似async_io_watcher,signal_io_watcher保存了管道读端fd和回调,然后注册到epoll中,在子进程收到信号的时候,通过write写到管道,最后在Poll IO阶段执行回调 94 | uv__io_t signal_io_watcher; 95 | 29 用于管理子进程退出信号的handle 96 | uv_signal_t child_watcher; 97 | 98 | 30备用的fd 99 | int emfile_fd; 100 | ``` 101 | 102 | 103 | ## 2.2 uv_handle_t 104 | 在Libuv中,uv_handle_t类似C++中的基类,有很多子类继承于它,Libuv主要通过控制内存的布局得到继承的效果。handle代表生命周期比较长的对象。例如
105 | 1 一个处于active状态的prepare handle,它的回调会在每次事件循环化的时候被执行。
106 | 2 一个TCP handle在每次有连接到来时,执行它的回调。 107 | 108 | 我们看一下uv_handle_t的定义 109 | 110 | ```cpp 111 | 1 自定义数据,用于关联一些上下文,Node.js中用于关联handle所属的C++对象 112 | void* data; 113 | 114 | 2 所属的事件循环 115 | uv_loop_t* loop; 116 | 117 | 3 handle类型 118 | uv_handle_type type; 119 | 120 | 4 handle调用uv_close后,在closing阶段被执行的回调 121 | uv_close_cb close_cb; 122 | 123 | 5 用于组织handle队列的前置后置指针 124 | void* handle_queue[2]; 125 | 126 | 6 文件描述符 127 | union { 128 | int fd; 129 | void* reserved[4]; 130 | } u; 131 | 132 | 7 当handle在close队列时,该字段指向下一个close节点 133 | uv_handle_t* next_closing; 134 | 135 | 8 handle的状态和标记 136 | unsigned int flags; 137 | ``` 138 | 139 | ### 2.2.1 uv_stream_s 140 | uv_stream_s是表示流的结构体。除了继承uv_handle_t的字段外,它额外定义下面字段 141 | 142 | ```cpp 143 | 1 等待发送的字节数 144 | size_t write_queue_size; 145 | 146 | 2 分配内存的函数 147 | uv_alloc_cb alloc_cb; 148 | 149 | 3 读取数据成功时执行的回调 150 | uv_read_cb read_cb; 151 | 152 | 4 发起连接对应的结构体 153 | uv_connect_t *connect_req; 154 | 155 | 5 关闭写端对应的结构体 156 | uv_shutdown_t *shutdown_req; 157 | 158 | 6 用于插入epoll,注册读写事件 159 | uv__io_t io_watcher; 160 | 161 | 7 待发送队列 162 | void* write_queue[2]; 163 | 164 | 8 发送完成的队列 165 | void* write_completed_queue[2]; 166 | 167 | 9 收到连接时执行的回调 168 | uv_connection_cb connection_cb; 169 | 170 | 10 socket操作失败的错误码 171 | int delayed_error; 172 | 173 | 11 accept返回的fd 174 | int accepted_fd; 175 | 176 | 12 已经accept了一个fd,又有新的fd,暂存起来 177 | void* queued_fds; 178 | ``` 179 | 180 | ### 2.2.2 uv_async_s 181 | uv_async_s是Libuv中实现异步通信的结构体。继承于uv_handle_t,并额外定义了以下字段。 182 | 183 | ```cpp 184 | 1 异步事件触发时执行的回调 185 | uv_async_cb async_cb; 186 | 187 | 2 用于插入async-handles队列 188 | void* queue[2]; 189 | 190 | 3 async_handles队列中的节点pending字段为1说明对应的事件触发了 191 | int pending; 192 | ``` 193 | 194 | ### 2.2.3 uv_tcp_s 195 | uv_tcp_s继承uv_handle_s和uv_stream_s。 196 | ### 2.2.4 uv_udp_s 197 | 198 | ```cpp 199 | 1 发送字节数 200 | size_t send_queue_size; 201 | 202 | 2 写队列节点的个数 203 | size_t send_queue_count; 204 | 205 | 3 分配接收数据的内存 206 | uv_alloc_cb alloc_cb; 207 | 208 | 4 接收完数据后执行的回调 209 | uv_udp_recv_cb recv_cb; 210 | 211 | 5 插入epoll里的IO观察者,实现数据读写 212 | uv__io_t io_watcher; 213 | 6 待发送队列 214 | void* write_queue[2]; 215 | 216 | 7 发送完成的队列(发送成功或失败),和待发送队列相关 217 | void* write_completed_queue[2]; 218 | ``` 219 | 220 | ### 2.2.5 uv_tty_s 221 | uv_tty_s继承于uv_handle_t和uv_stream_t。额外定义了下面字段。 222 | 223 | ```cpp 224 | 1 终端的参数 225 | struct termios orig_termios; 226 | 227 | 2 终端的工作模式 228 | int mode; 229 | ``` 230 | 231 | ### 2.2.6 uv_pipe_s 232 | uv_pipe_s继承于uv_handle_t和uv_stream_t。额外定义了下面字段。 233 | 234 | ```cpp 235 | 1 标记管道是否可用于传递文件描述符 236 | int ipc; 237 | 238 | 2 用于Unix域通信的文件路径 239 | const char* pipe_fname; 240 | ``` 241 | 242 | ### 2.2.7 uv_prepare_s、uv_check_s、uv_idle_s 243 | 上面三个结构体定义是类似的,它们都继承uv_handle_t,额外定义了两个字段。 244 | 245 | ```cpp 246 | 1 prepare、check、idle阶段回调 247 | uv_xxx_cb xxx_cb; 248 | 249 | 2 用于插入prepare、check、idle队列 250 | void* queue[2]; 251 | ``` 252 | 253 | ### 2.2.8 uv_timer_s 254 | uv_timer_s继承uv_handle_t,额外定义了下面几个字段。 255 | 256 | ```cpp 257 | 1 超时回调 258 | uv_timer_cb timer_cb; 259 | 260 | 2 插入二叉堆的字段 261 | void* heap_node[3]; 262 | 263 | 3 超时时间 264 | uint64_t timeout; 265 | 266 | 4 超时后是否继续开始重新计时,是的话重新插入二叉堆 267 | uint64_t repeat; 268 | 269 | 5 id标记,用于插入二叉堆的时候对比 270 | uint64_t start_id 271 | ``` 272 | 273 | ### 2.2.9 uv_process_s 274 | uv_process_s继承uv_handle_t,额外定义了 275 | 276 | ```cpp 277 | 1 进程退出时执行的回调 278 | uv_exit_cb exit_cb; 279 | 280 | 2 进程id 281 | int pid; 282 | 283 | 3 用于插入队列,进程队列或者pending队列 284 | void* queue[2]; 285 | 286 | 4 退出码,进程退出时设置 287 | int status; 288 | ``` 289 | 290 | ### 2.2.10 uv_fs_event_s 291 | uv_fs_event_s用于监听文件改动。uv_fs_event_s继承uv_handle_t,额外定义了 292 | 293 | ```cpp 294 | 1 监听的文件路径(文件或目录) 295 | char* path; 296 | 297 | 2 文件改变时执行的回调 298 | uv_fs_event_cb cb; 299 | ``` 300 | 301 | ### 2.2.11 uv_fs_poll_s 302 | uv_fs_poll_s继承uv_handle_t,额外定义了 303 | 304 | ```cpp 305 | 1 poll_ctx指向poll_ctx结构体 306 | void* poll_ctx; 307 | 308 | struct poll_ctx { 309 | // 对应的handle 310 | uv_fs_poll_t* parent_handle; 311 | // 标记是否开始轮询和轮询时的失败原因 312 | int busy_polling; 313 | // 多久检测一次文件内容是否改变 314 | unsigned int interval; 315 | // 每一轮轮询时的开始时间 316 | uint64_t start_time; 317 | // 所属事件循环 318 | uv_loop_t* loop; 319 | // 文件改变时回调 320 | uv_fs_poll_cb poll_cb; 321 | // 定时器,用于定时超时后轮询 322 | uv_timer_t timer_handle; 323 | // 记录轮询的一下上下文信息,文件路径、回调等 324 | uv_fs_t fs_req; 325 | // 轮询时保存操作系统返回的文件信息 326 | uv_stat_t statbuf; 327 | // 监听的文件路径,字符串的值追加在结构体后面 328 | char path[1]; /* variable length */ 329 | }; 330 | ``` 331 | 332 | ### 2.2.12 uv_poll_s 333 | uv_poll_s继承于uv_handle_t,额外定义了下面字段。 334 | 335 | ```cpp 336 | 1 监听的fd有感兴趣的事件时执行的回调 337 | uv_poll_cb poll_cb; 338 | 339 | 2 保存了fd和回调的IO观察者,注册到epoll中 340 | uv__io_t io_watcher; 341 | ``` 342 | 343 | ### 2.1.13 uv_signal_s 344 | uv_signal_s继承uv_handle_t,额外定义了以下字段 345 | 346 | ```cpp 347 | 1 收到信号时的回调 348 | uv_signal_cb signal_cb; 349 | 350 | 2 注册的信号 351 | int signum; 352 | 353 | 3 用于插入红黑树,进程把感兴趣的信号和回调封装成uv_signal_s,然后插入到红黑树,信号到来时,进程在信号处理号中把通知写入管道,通知Libuv。Libuv在Poll IO阶段会执行进程对应的回调。红黑树节点的定义如下 354 | struct { 355 | struct uv_signal_s* rbe_left; 356 | struct uv_signal_s* rbe_right; 357 | struct uv_signal_s* rbe_parent; 358 | int rbe_color; 359 | } tree_entry; 360 | 361 | 4 收到的信号个数 362 | unsigned int caught_signals; 363 | 364 | 5 已经处理的信号个数 365 | unsigned int dispatched_signals; 366 | ``` 367 | 368 | ## 2.3 uv_req_s 369 | 在Libuv中,uv_req_s也类似C++基类的作用,有很多子类继承于它,request代表一次请求,比如读写一个文件,读写socket,查询DNS。任务完成后这个request就结束了。request可以和handle结合使用,比如在一个TCP服务器上(handle)写一个数据(request),也可以单独使用一个request,比如DNS查询或者文件读写。我们看一下uv_req_s的定义。 370 | 371 | ```cpp 372 | 1 自定义数据 373 | void* data; 374 | 375 | 2 request类型 376 | uv_req_type type; 377 | 378 | 3 保留字段 379 | void* reserved[6]; 380 | ``` 381 | 382 | ### 2.3.1 uv_shutdown_s 383 | uv_shutdown_s用于关闭流的写端,额外定义的字段 384 | 385 | ```cpp 386 | 1 要关闭的流,比如TCP 387 | uv_stream_t* handle; 388 | 389 | 2 关闭流的写端后执行的回调 390 | uv_shutdown_cb cb; 391 | ``` 392 | 393 | ### 2.3.2 uv_write_s 394 | uv_write_s表示一次写请求,比如在TCP流上发送数据,额外定义的字段 395 | 396 | ```cpp 397 | 1 写完后的回调 398 | uv_write_cb cb; 399 | 400 | 2 需要传递的文件描述符,在send_handle中 401 | uv_stream_t* send_handle; 402 | 403 | 3 关联的handle 404 | uv_stream_t* handle; 405 | 406 | 4 用于插入队列 407 | void* queue[2]; 408 | 409 | 5 保存需要写的数据相关的字段(写入的buffer个数,当前写成功的位置等) 410 | unsigned int write_index; 411 | uv_buf_t* bufs; 412 | unsigned int nbufs; 413 | uv_buf_t bufsml[4]; 414 | 415 | 6 写出错的错误码 416 | int error; 417 | ``` 418 | 419 | ### 2.3.3 uv_connect_s 420 | uv_connect_s表示发起连接请求,比如TCP连接,额外定义的字段 421 | 422 | ```cpp 423 | 1 连接成功后执行的回调 424 | uv_connect_cb cb; 425 | 426 | 2 对应的流,比如tcp 427 | uv_stream_t* handle; 428 | 429 | 3 用于插入队列 430 | void* queue[2]; 431 | ``` 432 | 433 | ### 2.3.4 uv_udp_send_s 434 | uv_udp_send_s表示一次发送UDP数据的请求 435 | 436 | ```cpp 437 | 1 所属udp的handle,udp_send_s代表一次发送 438 | uv_udp_t* handle; 439 | 440 | 2 回调 441 | uv_udp_send_cb cb; 442 | 443 | 3 用于插入待发送队列 444 | void* queue[2]; 445 | 446 | 4 发送的目的地址 447 | struct sockaddr_storage addr; 448 | 449 | 5 保存了发送数据的缓冲区和个数 450 | unsigned int nbufs; 451 | uv_buf_t* bufs; 452 | uv_buf_t bufsml[4]; 453 | 454 | 6 发送状态或成功发送的字节数 455 | ssize_t status; 456 | 457 | 7 发送完执行的回调(发送成功或失败) 458 | uv_udp_send_cb send_cb; 459 | ``` 460 | 461 | 462 | ### 2.3.5 uv_getaddrinfo_s 463 | uv_getaddrinfo_s表示一次通过域名查询IP的DNS请求,额外定义的字段 464 | 465 | ```cpp 466 | 1 所属事件循环 467 | uv_loop_t* loop; 468 | 469 | 2 用于异步DNS解析时插入线程池任务队列的节点 470 | struct uv__work work_req; 471 | 472 | 3 DNS解析完后执行的回调 473 | uv_getaddrinfo_cb cb; 474 | 475 | 4 DNS查询的配置 476 | struct addrinfo* hints; 477 | char* hostname; 478 | char* service; 479 | 480 | 5 DNS解析结果 481 | struct addrinfo* addrinfo; 482 | 483 | 6 DNS解析的返回码 484 | int retcode; 485 | ``` 486 | 487 | ### 2.3.6 uv_getnameinfo_s 488 | uv_getnameinfo_s表示一次通过IP查询域名的DNS查询请求,额外定义的字段 489 | 490 | ```cpp 491 | 1 所属事件循环 492 | uv_loop_t* loop; 493 | 494 | 2 用于异步DNS解析时插入线程池任务队列的节点 495 | struct uv__work work_req; 496 | 497 | 3 socket转域名完成的回调 498 | uv_getnameinfo_cb getnameinfo_cb; 499 | 500 | 4 需要转域名的socket结构体 501 | struct sockaddr_storage storage; 502 | 503 | 5 指示查询返回的信息 504 | int flags; 505 | 506 | 6 查询返回的信息 507 | char host[NI_MAXHOST]; 508 | char service[NI_MAXSERV]; 509 | 510 | 7 查询返回码 511 | int retcode; 512 | ``` 513 | 514 | ### 2.3.7 uv_work_s 515 | uv_work_s用于往线程池提交任务,额外定义的字段 516 | 517 | ```cpp 518 | 1 所属事件循环 519 | uv_loop_t* loop; 520 | 521 | 2 处理任务的函数 522 | uv_work_cb work_cb; 523 | 524 | 3 处理完任务后执行的函数 525 | uv_after_work_cb after_work_cb; 526 | 527 | 4封装一个work插入到线程池队列,work_req的work和done函数是对上面work_cb和after_work_cb的封装 528 | struct uv__work work_req; 529 | ``` 530 | 531 | ### uv_fs_s 532 | uv_fs_s表示一次文件操作请求,额外定义的字段 533 | 534 | ```cpp 535 | 1 文件操作类型 536 | uv_fs_type fs_type; 537 | 538 | 2 所属事件循环 539 | uv_loop_t* loop; 540 | 541 | 3文件操作完成的回调 542 | uv_fs_cb cb; 543 | 544 | 4 文件操作的返回码 545 | ssize_t result; 546 | 547 | 5 文件操作返回的数据 548 | void* ptr; 549 | 550 | 6 文件操作路径 551 | const char* path; 552 | 553 | 7 文件的stat信息 554 | uv_stat_t statbuf; 555 | 556 | 8 文件操作涉及到两个路径时,保存目的路径 557 | const char *new_path; 558 | 559 | 9 文件描述符 560 | uv_file file; 561 | 562 | 10 文件标记 563 | int flags; 564 | 565 | 11 操作模式 566 | mode_t mode; 567 | 568 | 12 写文件时传入的数据和个数 569 | unsigned int nbufs; 570 | uv_buf_t* bufs; 571 | 572 | 13 文件偏移 573 | off_t off; 574 | 575 | 14 保存需要设置的uid和gid,例如chmod的时候 576 | uv_uid_t uid; 577 | uv_gid_t gid; 578 | 579 | 15 保存需要设置的文件修改、访问时间,例如fs.utimes的时候 580 | double atime; 581 | double mtime; 582 | 583 | 16 异步的时候用于插入任务队列,保存工作函数,回调函数 584 | struct uv__work work_req; 585 | 586 | 17 保存读取数据或者长度。例如read和sendfile 587 | uv_buf_t bufsml[4]; 588 | ``` 589 | 590 | ## 2.4 IO观察者 591 | IO观察者是Libuv中的核心概念和数据结构。我们看一下它的定义 592 | 593 | ```cpp 594 | 1. struct uv__io_s { 595 | 2. // 事件触发后的回调 596 | 3. uv__io_cb cb; 597 | 4. // 用于插入队列 598 | 5. void* pending_queue[2]; 599 | 6. void* watcher_queue[2]; 600 | 7. // 保存本次感兴趣的事件,在插入IO观察者队列时设置 601 | 8. unsigned int pevents; 602 | 9. // 保存当前感兴趣的事件 603 | 10. unsigned int events; 604 | 11. int fd; 605 | 12. }; 606 | ``` 607 | 608 | IO观察者封装了文件描述符、事件和回调,然后插入到loop维护的IO观察者队列,在Poll IO阶段,Libuv会根据IO观察者描述的信息,往底层的事件驱动模块注册文件描述符感兴趣的事件。当注册的事件触发的时候,IO观察者的回调就会被执行。我们看如何初IO观察者的一些逻辑。 609 | ### 2.4.1 初始化IO观察者 610 | 611 | ```cpp 612 | 1. void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd) { 613 | 2. // 初始化队列,回调,需要监听的fd 614 | 3. QUEUE_INIT(&w->pending_queue); 615 | 4. QUEUE_INIT(&w->watcher_queue); 616 | 5. w->cb = cb; 617 | 6. w->fd = fd; 618 | 7. // 上次加入epoll时感兴趣的事件,在执行完epoll操作函数后设置 619 | 8. w->events = 0; 620 | 9. // 当前感兴趣的事件,在再次执行epoll函数之前设置 621 | 10. w->pevents = 0; 622 | 11. } 623 | ``` 624 | 625 | ### 2.4.2注册一个IO观察者到Libuv。 626 | 627 | ```cpp 628 | 1. void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) { 629 | 2. // 设置当前感兴趣的事件 630 | 3. w->pevents |= events; 631 | 4. // 可能需要扩容 632 | 5. maybe_resize(loop, w->fd + 1); 633 | 6. // 事件没有变化则直接返回 634 | 7. if (w->events == w->pevents) 635 | 8. return; 636 | 9. // IO观察者没有挂载在其它地方则插入Libuv的IO观察者队列 637 | 10. if (QUEUE_EMPTY(&w->watcher_queue)) 638 | 11. QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue); 639 | 12. // 保存映射关系 640 | 13. if (loop->watchers[w->fd] == NULL) { 641 | 14. loop->watchers[w->fd] = w; 642 | 15. loop->nfds++; 643 | 16. } 644 | 17. } 645 | ``` 646 | 647 | uv__io_start函数就是把一个IO观察者插入到Libuv的观察者队列中,并且在watchers数组中保存一个映射关系。Libuv在Poll IO阶段会处理IO观察者队列。 648 | 649 | ### 2.4.3 撤销IO观察者或者事件 650 | uv__io_stop修改IO观察者感兴趣的事件,如果还有感兴趣的事件的话,IO观察者还会在队列里,否则移出 651 | 652 | ```cpp 653 | 1. void uv__io_stop(uv_loop_t* loop, 654 | 2. uv__io_t* w, 655 | 3. unsigned int events) { 656 | 4. if (w->fd == -1) 657 | 5. return; 658 | 6. assert(w->fd >= 0); 659 | 7. if ((unsigned) w->fd >= loop->nwatchers) 660 | 8. return; 661 | 9. // 清除之前注册的事件,保存在pevents里,表示当前感兴趣的事件 662 | 10. w->pevents &= ~events; 663 | 11. // 对所有事件都不感兴趣了 664 | 12. if (w->pevents == 0) { 665 | 13. // 移出IO观察者队列 666 | 14. QUEUE_REMOVE(&w->watcher_queue); 667 | 15. // 重置 668 | 16. QUEUE_INIT(&w->watcher_queue); 669 | 17. // 重置 670 | 18. if (loop->watchers[w->fd] != NULL) { 671 | 19. assert(loop->watchers[w->fd] == w); 672 | 20. assert(loop->nfds > 0); 673 | 21. loop->watchers[w->fd] = NULL; 674 | 22. loop->nfds--; 675 | 23. w->events = 0; 676 | 24. } 677 | 25. } 678 | 26. /* 679 | 27. 之前还没有插入IO观察者队列,则插入, 680 | 28. 等到Poll IO时处理,否则不需要处理 681 | 29. */ 682 | 30. else if (QUEUE_EMPTY(&w->watcher_queue)) 683 | 31. QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue); 684 | 32. } 685 | ``` 686 | 687 | ## 2.5 Libuv通用逻辑 688 | ### 2.5.1 uv__handle_init 689 | uv__handle_init初始化handle的类型,设置REF标记,插入handle队列。 690 | 691 | ```cpp 692 | 1. #define uv__handle_init(loop_, h, type_) 693 | 2. do { 694 | 3. (h)->loop = (loop_); 695 | 4. (h)->type = (type_); 696 | 5. (h)->flags = UV_HANDLE_REF; 697 | 6. QUEUE_INSERT_TAIL(&(loop_)->handle_queue, &(h)->handle_queue); 698 | 7. (h)->next_closing = NULL 699 | 8. } 700 | 9. while (0) 701 | ``` 702 | 703 | ### 2.5.2. uv__handle_start 704 | uv__handle_start设置标记handle为ACTIVE,如果设置了REF标记,则active handle的个数加一,active handle数会影响事件循环的退出。 705 | 706 | ```cpp 707 | 1. #define uv__handle_start(h) 708 | 2. do { 709 | 3. if (((h)->flags & UV_HANDLE_ACTIVE) != 0) break; 710 | 4. (h)->flags |= UV_HANDLE_ACTIVE; 711 | 5. if (((h)->flags & UV_HANDLE_REF) != 0) 712 | 6. (h)->loop->active_handles++; 713 | 7. } 714 | 8. while (0) 715 | ``` 716 | 717 | ### 2.5.3. uv__handle_stop 718 | uv__handle_stop和uv__handle_start相反。 719 | 720 | ```cpp 721 | 1. #define uv__handle_stop(h) 722 | 2. do { 723 | 3. if (((h)->flags & UV_HANDLE_ACTIVE) == 0) break; 724 | 4. (h)->flags &= ~UV_HANDLE_ACTIVE; 725 | 5. if (((h)->flags & UV_HANDLE_REF) != 0) uv__active_handle_rm(h); 726 | 6. } 727 | 7. while (0) 728 | ``` 729 | 730 | Libuv中handle有REF和ACTIVE两个状态。当一个handle调用xxx_init函数的时候,它首先被打上REF标记,并且插入loop->handle队列。当handle调用xxx_start函数的时候,它被打上ACTIVE标记,并且记录active handle的个数加一。只有REF并且ACTIVE状态的handle才会影响事件循环的退出。 731 | 732 | ### 2.5.4. uv__req_init 733 | uv__req_init初始化请求的类型,记录请求的个数,会影响事件循环的退出。 734 | 735 | ```cpp 736 | 1. #define uv__req_init(loop, req, typ) 737 | 2. do { 738 | 3. (req)->type = (typ); 739 | 4. (loop)->active_reqs.count++; 740 | 5. } 741 | 6. while (0) 742 | ``` 743 | 744 | 745 | ### 2.5.5. uv__req_register 746 | 请求的个数加一 747 | 748 | ```cpp 749 | 1. #define uv__req_register(loop, req) 750 | 2. do { 751 | 3. (loop)->active_reqs.count++; 752 | 4. } 753 | 5. while (0) 754 | ``` 755 | 756 | ### 2.5.6. uv__req_unregister 757 | 请求个数减一 758 | 759 | ```cpp 760 | 1. #define uv__req_unregister(loop, req) 761 | 2. do { 762 | 3. assert(uv__has_active_reqs(loop)); 763 | 4. (loop)->active_reqs.count--; 764 | 5. } 765 | 6. while (0) 766 | ``` 767 | 768 | ### 2.5.7. uv__handle_ref 769 | uv__handle_ref标记handle为REF状态,如果handle是ACTIVE状态,则active handle数加一 770 | 771 | ```cpp 772 | 1. #define uv__handle_ref(h) 773 | 2. do { 774 | 3. if (((h)->flags & UV_HANDLE_REF) != 0) break; 775 | 4. (h)->flags |= UV_HANDLE_REF; 776 | 5. if (((h)->flags & UV_HANDLE_CLOSING) != 0) break; 777 | 6. if (((h)->flags & UV_HANDLE_ACTIVE) != 0) uv__active_handle_add(h); 778 | 7. } 779 | 8. while (0) 780 | 9. uv__handle_unref 781 | ``` 782 | 783 | uv__handle_unref去掉handle的REF状态,如果handle是ACTIVE状态,则active handle数减一 784 | 785 | ```cpp 786 | 1. #define uv__handle_unref(h) 787 | 2. do { 788 | 3. if (((h)->flags & UV_HANDLE_REF) == 0) break; 789 | 4. (h)->flags &= ~UV_HANDLE_REF; 790 | 5. if (((h)->flags & UV_HANDLE_CLOSING) != 0) break; 791 | 6. if (((h)->flags & UV_HANDLE_ACTIVE) != 0) uv__active_handle_rm(h); 792 | 7. } 793 | 8. while (0) 794 | ``` 795 | -------------------------------------------------------------------------------- /docs/chapter10-定时器.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Node.js V14对定时器模块进行了重构,之前版本的实现是用一个map,以超时时间为键,每个键对应一个队列。即有同样超时时间的节点在同一个队列。每个队列对应一个底层的一个节点(二叉堆里的节点),Node.js在事件循环的timer阶段会从二叉堆里找出超时的节点,然后执行回调,回调里会遍历队列,判断哪个节点超时了。14重构后,只使用了一个二叉堆的节点。我们看一下它的实现,首先看下定时器模块的整体关系图,如图10-1所示。 4 | ![](https://img-blog.csdnimg.cn/2834e17d10244f93861a062f659afa28.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 5 | 图10-1 6 | 7 | 下面我们先看一下定时器模块的几个重要的数据结构。 8 | ## 10.1 Libuv的实现 9 | Libuv中使用二叉堆实现了定时器。最快到期的节点是根节点。 10 | ### 10.1.1 Libuv中维护定时器的数据结构 11 | 12 | ```cpp 13 | // 取出loop中的计时器堆指针 14 | static struct heap *timer_heap(const uv_loop_t* loop) { 15 | return (struct heap*) &loop->timer_heap; 16 | } 17 | ``` 18 | 19 | ### 10.1.2 比较函数 20 | 因为Libuv使用二叉堆实现定时器,这就涉及到节点插入堆的时候的规则。 21 | 22 | ```cpp 23 | static int timer_less_than(const struct heap_node* ha, 24 | const struct heap_node* hb) { 25 | const uv_timer_t* a; 26 | const uv_timer_t* b; 27 | // 通过结构体成员找到结构体首地址 28 | a = container_of(ha, uv_timer_t, heap_node); 29 | b = container_of(hb, uv_timer_t, heap_node); 30 | // 比较两个结构体中的超时时间 31 | if (a->timeout < b->timeout) 32 | return 1; 33 | if (b->timeout < a->timeout) 34 | return 0; 35 | // 超时时间一样的话,看谁先创建 36 | if (a->start_id < b->start_id) 37 | return 1; 38 | if (b->start_id < a->start_id) 39 | return 0; 40 | 41 | return 0; 42 | } 43 | ``` 44 | 45 | ### 10.1.3 初始化定时器结构体 46 | 如果需要使用定时器,首先要对定时器的结构体进行初始化。 47 | 48 | ```cpp 49 | // 初始化uv_timer_t结构体 50 | int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) { 51 | uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER); 52 | handle->timer_cb = NULL; 53 | handle->repeat = 0; 54 | return 0; 55 | } 56 | ``` 57 | 58 | ### 10.1.4 插入一个定时器 59 | 60 | ```cpp 61 | // 启动一个计时器 62 | int uv_timer_start(uv_timer_t* handle, 63 | uv_timer_cb cb, 64 | uint64_t timeout, 65 | uint64_t repeat) { 66 | uint64_t clamped_timeout; 67 | 68 | if (cb == NULL) 69 | return UV_EINVAL; 70 | // 重新执行start的时候先把之前的停掉 71 | if (uv__is_active(handle)) 72 | uv_timer_stop(handle); 73 | // 超时时间,为绝对值 74 | clamped_timeout = handle->loop->time + timeout; 75 | if (clamped_timeout < timeout) 76 | clamped_timeout = (uint64_t) -1; 77 | // 初始化回调,超时时间,是否重复计时,赋予一个独立无二的id 78 | handle->timer_cb = cb; 79 | handle->timeout = clamped_timeout; 80 | handle->repeat = repeat; 81 | // 用于超时时间一样的时候,比较定时器在二叉堆的位置,见cmp函数 82 | handle->start_id = handle->loop->timer_counter++; 83 | // 插入最小堆 84 | heap_insert(timer_heap(handle->loop), 85 | (struct heap_node*) &handle->heap_node, 86 | timer_less_than); 87 | // 激活该handle 88 | uv__handle_start(handle); 89 | 90 | return 0; 91 | } 92 | ``` 93 | 94 | ### 10.1.5 停止一个定时器 95 | 96 | ```cpp 97 | // 停止一个计时器 98 | int uv_timer_stop(uv_timer_t* handle) { 99 | if (!uv__is_active(handle)) 100 | return 0; 101 | // 从最小堆中移除该计时器节点 102 | heap_remove(timer_heap(handle->loop), 103 | (struct heap_node*) &handle->heap_node, 104 | timer_less_than); 105 | // 清除激活状态和handle的active数减一 106 | uv__handle_stop(handle); 107 | 108 | return 0; 109 | } 110 | ``` 111 | 112 | ### 10.1.6 重新设置定时器 113 | 重新设置定时器类似插入一个定时器,它首先需要把之前的定时器从二叉堆中移除,然后重新插入二叉堆。 114 | 115 | ```cpp 116 | // 重新启动一个计时器,需要设置repeat标记 117 | int uv_timer_again(uv_timer_t* handle) { 118 | if (handle->timer_cb == NULL) 119 | return UV_EINVAL; 120 | // 如果设置了repeat标记说明计时器是需要重复触发的 121 | if (handle->repeat) { 122 | // 先把旧的节点从最小堆中移除,然后再重新开启一个计时器 123 | uv_timer_stop(handle); 124 | uv_timer_start(handle, 125 | handle->timer_cb, 126 | handle->repeat, 127 | handle->repeat); 128 | } 129 | 130 | return 0; 131 | } 132 | ``` 133 | 134 | ### 10.1.7 计算二叉堆中超时时间最小值 135 | 超时时间最小值,主要用于判断Poll IO节点是阻塞的最长时间。 136 | 137 | ```cpp 138 | // 计算最小堆中最小节点的超时时间,即最小的超时时间 139 | int uv__next_timeout(const uv_loop_t* loop) { 140 | const struct heap_node* heap_node; 141 | const uv_timer_t* handle; 142 | uint64_t diff; 143 | // 取出堆的根节点,即超时时间最小的 144 | heap_node = heap_min(timer_heap(loop)); 145 | if (heap_node == NULL) 146 | return -1; /* block indefinitely */ 147 | 148 | handle = container_of(heap_node, uv_timer_t, heap_node); 149 | // 如果最小的超时时间小于当前时间,则返回0,说明已经超时 150 | if (handle->timeout <= loop->time) 151 | return 0; 152 | // 否则计算还有多久超时,返回给epoll,epoll的timeout不能大于diff 153 | diff = handle->timeout - loop->time; 154 | if (diff > INT_MAX) 155 | diff = INT_MAX; 156 | 157 | return diff; 158 | } 159 | ``` 160 | 161 | ### 10.1.8 处理定时器 162 | 处理超时定时器就是遍历二叉堆,判断哪个节点超时了。 163 | 164 | ```cpp 165 | // 找出已经超时的节点,并且执行里面的回调 166 | void uv__run_timers(uv_loop_t* loop) { 167 | struct heap_node* heap_node; 168 | uv_timer_t* handle; 169 | 170 | for (;;) { 171 | heap_node = heap_min(timer_heap(loop)); 172 | if (heap_node == NULL) 173 | break; 174 | 175 | handle = container_of(heap_node, uv_timer_t, heap_node); 176 | // 如果当前节点的时间大于当前时间则返回,说明后面的节点也没有超时 177 | if (handle->timeout > loop->time) 178 | break; 179 | // 移除该计时器节点,重新插入最小堆,如果设置了repeat的话 180 | uv_timer_stop(handle); 181 | uv_timer_again(handle); 182 | // 执行超时回调 183 | handle->timer_cb(handle); 184 | } 185 | } 186 | ``` 187 | 188 | ## 10.2 核心数据结构 189 | ### 10.2.1 TimersList 190 | 相对超时时间一样的定时器会被放到同一个队列,比如当前执行setTimeout(()=>{}, 10000})和5秒后执行setTimeout(()=>{}, 10000}),这两个任务就会在同一个List中,这个队列由TimersList来管理。对应图1中的List那个队列。 191 | 192 | ```js 193 | function TimersList(expiry, msecs) { 194 | // 用于链表 195 | this._idleNext = this; 196 | this._idlePrev = this; 197 | this.expiry = expiry; 198 | this.id = timerListId++; 199 | this.msecs = msecs; 200 | // 在优先队列里的位置 201 | this.priorityQueuePosition = null; 202 | } 203 | ``` 204 | 205 | expiry记录的是链表中最快超时的节点的绝对时间。每次执行定时器阶段时会动态更新,msecs是超时时间的相对值(相对插入时的当前时间)。用于计算该链表中的节点是否超时。后续我们会看到具体的用处。 206 | ### 10.2.2 优先队列 207 | 208 | ```js 209 | const timerListQueue = new PriorityQueue(compareTimersLists, setPosition) 210 | ``` 211 | 212 | Node.js用优先队列对所有TimersList链表进行管理,优先队列本质是一个二叉堆(小根堆),每个TimersList链表在二叉堆里对应一个节点。根据TimersList的结构,我们知道每个链表都保存链表中最快到期的节点的过期时间。二叉堆以该时间为依据,即最快到期的list对应二叉堆中的根节点。根节点的到期时间就是整个Node.js定时器最快到期的时间,Node.js把Libuv中定时器节点的超时时间设置为该值,在事件循环的定时器阶段就会处理定时的节点,并且不断遍历优先队列,判断当前节点是否超时,如果超时了,就需要处理,如果没有超时,说明整个二叉堆的节点都没有超时。然后重新设置Libuv定时器节点新的到期时间。 213 | 214 | 另外,Node.js中用一个map保存了超时时间到TimersList链表的映射关系。 这样就可以根据相对超时时间快速找到对应的列表,利用空间换时间。了解完定时器整体的组织和核心数据结构,我们可以开始进入真正的源码分析了。 215 | ## 10.3 设置定时器处理函数 216 | Node.js在初始化的时候设置了处理定时器的函数。 217 | setupTimers(processImmediate, processTimers); 218 | setupTimers对应的C++函数是 219 | 220 | ```cpp 221 | void SetupTimers(const FunctionCallbackInfo& args) { 222 | auto env = Environment::GetCurrent(args); 223 | env->set_immediate_callback_function(args[0].As()); 224 | env->set_timers_callback_function(args[1].As()); 225 | } 226 | ``` 227 | 228 | SetupTimers在env中保存了两个函数,processImmediate是处理setImmediate的,processTimers是处理定时器的。当有节点超时时,Node.js会执行该函数处理超时的节点,后续会看到该函数的具体处理逻辑。下面我们看一下如何设置一个定时器。 229 | ## 10.4 设置定时器 230 | 231 | ```js 232 | function setTimeout(callback, after, arg1, arg2, arg3) { 233 | // 忽略处理参数args逻辑 234 | // 新建一个Timeout对象 235 | const timeout = new Timeout(callback, 236 | after, 237 | args, 238 | false, 239 | true); 240 | insert(timeout, timeout._idleTimeout); 241 | return timeout; 242 | } 243 | ``` 244 | 245 | setTimeout主要包含两个操作,new Timeout和insert。我们逐个分析一下。 246 | 1 setTimeout 247 | 248 | ```js 249 | function Timeout(callback, after, args, isRepeat, isRefed) { 250 | after *= 1; // Coalesce to number or NaN 251 | // 关于setTimeout的超时时间为0的问题在这里可以揭开迷雾 252 | if (!(after >= 1 && after <= TIMEOUT_MAX)) { 253 | after = 1; 254 | } 255 | // 超时时间相对值 256 | this._idleTimeout = after; 257 | // 前后指针,用于链表 258 | this._idlePrev = this; 259 | this._idleNext = this; 260 | // 定时器的开始时间 261 | this._idleStart = null; 262 | // 超时回调 263 | this._onTimeout = callback; 264 | // 执行回调时传入的参数 265 | this._timerArgs = args; 266 | // 是否定期触发超时,用于setInterval 267 | this._repeat = isRepeat ? after : null; 268 | this._destroyed = false; 269 | // this._idleStart = now(); 270 | // 激活底层的定时器节点(二叉堆的节点),说明有定时节点需要处理 271 | if (isRefed) 272 | incRefCount(); 273 | // 记录状态 274 | this[kRefed] = isRefed; 275 | } 276 | ``` 277 | 278 | Timeout主要是新建一个对象记录一些定时器的相对超时时间(用于支持setInterval,重新插入队列时找到所属队列)、开始时间(用于计算定时器是否超时)等上下文信息。这里有一个关键的逻辑是isRefed的值。Node.js支持ref和unref状态的定时器(setTimeout 和setUnrefTimeout),unref状态的定时器,不会影响事件循环的退出。即当只有unref状态的定时器时,事件循环会结束。当isRefed为true时会执行incRefCount(); 279 | 280 | ```cpp 281 | function incRefCount() { 282 | if (refCount++ === 0) 283 | toggleTimerRef(true); 284 | } 285 | 286 | void ToggleTimerRef(const FunctionCallbackInfo& args) { 287 | Environment::GetCurrent(args)->ToggleTimerRef(args[0]->IsTrue()); 288 | } 289 | 290 | void Environment::ToggleTimerRef(bool ref) { 291 | if (started_cleanup_) return; 292 | // 打上ref标记, 293 | if (ref) { 294 | uv_ref(reinterpret_cast(timer_handle())); 295 | } else { 296 | uv_unref(reinterpret_cast(timer_handle())); 297 | } 298 | } 299 | ``` 300 | 301 | 我们看到最终会调用Libuv的uv_ref或uv_unref修改定时器相关handle的状态,因为Node.js只会在Libuv中注册一个定时器handle并且是常驻的,如果JS层当前没有设置定时器,则需要修改定时器handle的状态为unref,否则会影响事件循环的退出。refCount值便是记录JS层ref状态的定时器个数的。所以当我们第一次执行setTimeout的时候,Node.js会激活Libuv的定时器节点。接着我们看一下insert。 302 | 303 | ```js 304 | let nextExpiry = Infinity; 305 | function insert(item, msecs, start = getLibuvNow()) { 306 | msecs = MathTrunc(msecs); 307 | // 记录定时器的开始时间,见Timeout函数的定义 308 | item._idleStart = start; 309 | // 该相对超时时间是否已经存在对应的链表 310 | let list = timerListMap[msecs]; 311 | // 还没有 312 | if (list === undefined) { 313 | // 算出绝对超时时间,第一个节点是该链表中最早到期的节点 314 | const expiry = start + msecs; 315 | // 新建一个链表 316 | timerListMap[msecs] = list = new TimersList(expiry, msecs); 317 | // 插入优先队列 318 | timerListQueue.insert(list); 319 | /* 320 | nextExpiry记录所有超时节点中最快到期的节点, 321 | 如果有更快到期的,则修改底层定时器节点的过期时间 322 | */ 323 | if (nextExpiry > expiry) { 324 | // 修改底层超时节点的超时时间 325 | scheduleTimer(msecs); 326 | nextExpiry = expiry; 327 | } 328 | } 329 | // 把当前节点加到链表里 330 | L.append(list, item); 331 | } 332 | ``` 333 | 334 | Insert的主要逻辑如下 335 | 1 如果该超时时间还没有对应的链表,则新建一个链表,每个链表都会记录该链表中最快到期的节点的值,即第一个插入的值。然后把链表插入优先队列,优先队列会根据该链表的最快过期时间的值,把链表对应的节点调整到相应的位置。 336 | 2 如果当前设置的定时器,比之前所有的定时器都快到期,则需要修改底层的定时器节点,使得更快触发超时。 337 | 3 把当前的定时器节点插入对应的链表尾部。即该链表中最久超时的节点。 338 | 假设我们在0s的时候插入一个节点,下面是插入第一个节点时的结构图如图10-2所示。 339 | ![](https://img-blog.csdnimg.cn/8088834776f84585a4d5ef050c73fbee.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 340 | 341 | 图10-2 342 | 343 | 下面我们看一下多个节点的情况。假设0s的时候插入两个节点10s过期和11s过期。如图10-3所示。 344 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/0e5e072cd20f40ba9ef60780d254ca7b.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 345 | 346 | 图10-3 347 | 348 | 然后在1s的时候,插入一个新的11s过期的节点,9s的时候插入一个新的10s过期节点。我们看一下这时候的关系图如图10-4所示。 349 | ![](https://img-blog.csdnimg.cn/d32f8c30193b4123b7cd8415caee82bc.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 350 | 351 | 图10-4 352 | 353 | 我们看到优先队列中,每一个节点是一个链表,父节点对应的链表的第一个元素是比子节点链表的第一个元素先超时的,但是链表中后续节点的超时就不一定。比如子节点1s开始的节点就比父节点9s开始的节点先超时。因为同一队列,只是相对超时时间一样,而还有一个重要的因素是开始的时间。虽然某节点的相对超时时间长,但是如果它比另一个节点开始的早,那么就有可能比它先超时。后续我们会看到具体是怎么实现的。 354 | ## 10.5 处理定时器 355 | 前面我们讲到了设置定时器处理函数和设置一个定时器,但是在哪里触发这个处理定时器的函数呢?答案在scheduleTimer函数。Node.js的实现中,所有JS层设置的定时器对应Libuv的一个定时器节点,Node.js维护了JS层所有定时器的超时最小值。在第一个设置定时器或者设置一个新的定时器时,如果新设置的定时器比当前的最小值小,则会通过scheduleTimer修改超时时间。超时的时候,就会执行回调。scheduleTimer函数是对C++函数的封装。 356 | 357 | ```cpp 358 | void ScheduleTimer(const FunctionCallbackInfo& args) { 359 | auto env = Environment::GetCurrent(args); 360 | env->ScheduleTimer(args[0]->IntegerValue(env->context()).FromJust()); 361 | } 362 | 363 | void Environment::ScheduleTimer(int64_t duration_ms) { 364 | if (started_cleanup_) return; 365 | uv_timer_start(timer_handle(), RunTimers, duration_ms, 0); 366 | } 367 | ``` 368 | 369 | uv_timer_start就是开启底层计时,即往Libuv的二叉堆插入一个节点(如果该handle已经存在二叉堆,则先删除)。超时时间是duration_ms,就是最快到期的时间,超时回调是RunTimers,在timer阶段会判断是否过期。是的话执行RunTimers函数。我们先看一下RunTimers函数的主要代码。 370 | 371 | ```cpp 372 | Local cb = env->timers_callback_function(); 373 | ret = cb->Call(env->context(), process, 1, &arg); 374 | ``` 375 | 376 | RunTimers会执行timers_callback_function。timers_callback_function是在Node.js初始化的时候设置的processTimers函数。现在我们知道了Node.js是如何设置超时的处理函数,也知道了什么时候会执行该回调。那我们就来看一下回调时具体处理逻辑。 377 | 378 | ```cpp 379 | void Environment::RunTimers(uv_timer_t* handle) { 380 | Local cb = env->timers_callback_function(); 381 | MaybeLocal ret; 382 | Local arg = env->GetNow(); 383 | 384 | do { 385 | // 执行js回调processTimers函数 386 | ret = cb->Call(env->context(), process, 1, &arg); 387 | } while (ret.IsEmpty() && env->can_call_into_js()); 388 | 389 | // 如果还有未超时的节点,则ret为第一个未超时的节点的超时时间 390 | int64_t expiry_ms = ret.ToLocalChecked()->IntegerValue(env->context()).FromJust(); 391 | uv_handle_t* h = reinterpret_cast(handle); 392 | 393 | /* 394 | 1 等于0说明所有节点都执行完了,但是定时器节点还是在Libuv中, 395 | 不过改成非激活状态,即不会影响Libuv退出,因为当前没有需要处理的节点了(handle), 396 | 2 不等于0说明没有还要节点需要处理,这种情况又分为两种 397 | 1 还有激活状态的定时器,即不允许事件循环退出 398 | 2 定时器都是非激活状态的,允许事件循环退出 399 | 具体见Timeout的unref和ref方法 400 | */ 401 | if (expiry_ms != 0) { 402 | // 算出下次超时的相对值 403 | int64_t duration_ms = 404 | llabs(expiry_ms) - (uv_now(env->event_loop()) - env->timer_base()); 405 | // 重新把handle插入Libuv的二叉堆 406 | env->ScheduleTimer(duration_ms > 0 ? duration_ms : 1); 407 | /* 408 | 见internal/timer.js的processTimers 409 | 1 大于0说明还有节点没超时,并且不允许事件循环退出, 410 | 需要保持定时器的激活状态(如果之前是激活状态则不影响), 411 | 2 小于0说明定时器不影响Libuv的事件循环的结束,改成非激活状态 412 | */ 413 | if (expiry_ms > 0) 414 | uv_ref(h); 415 | else 416 | uv_unref(h); 417 | } else { 418 | uv_unref(h); 419 | } 420 | } 421 | ``` 422 | 423 | 该函数主要是执行回调,然后如果还有没超时的节点,重新设置Libuv定时器的时间。看看JS层面。 424 | 425 | ```js 426 | function processTimers(now) { 427 | nextExpiry = Infinity; 428 | let list; 429 | let ranAtLeastOneList = false; 430 | // 取出优先队列的根节点,即最快到期的节点 431 | while (list = timerListQueue.peek()) { 432 | // 还没过期,则取得下次到期的时间,重新设置超时时间 433 | if (list.expiry > now) { 434 | nextExpiry = list.expiry; 435 | // 返回下一次过期的时间,负的说明允许事件循环退出 436 | return refCount > 0 ? nextExpiry : -nextExpiry; 437 | } 438 | 439 | // 处理超时节点 440 | listOnTimeout(list, now); 441 | } 442 | // 所有节点都处理完了 443 | return 0; 444 | } 445 | 446 | function listOnTimeout(list, now) { 447 | const msecs = list.msecs; 448 | let ranAtLeastOneTimer = false; 449 | let timer; 450 | // 遍历具有统一相对过期时间的队列 451 | while (timer = L.peek(list)) { 452 | // 算出已经过去的时间 453 | const diff = now - timer._idleStart; 454 | // 过期的时间比超时时间小,还没过期 455 | if (diff < msecs) { 456 | /* 457 | 整个链表节点的最快过期时间等于当前 458 | 还没过期节点的值,链表是有序的 459 | */ 460 | list.expiry = MathMax(timer._idleStart + msecs, 461 | now + 1); 462 | // 更新id,用于决定在优先队列里的位置 463 | list.id = timerListId++; 464 | /* 465 | 调整过期时间后,当前链表对应的节点不一定是优先队列 466 | 里的根节点了,可能有它更快到期,即当前链表对应的节 467 | 点可能需要往下沉 468 | */ 469 | timerListQueue.percolateDown(1); 470 | return; 471 | } 472 | 473 | // 准备执行用户设置的回调,删除这个节点 474 | L.remove(timer); 475 | 476 | let start; 477 | if (timer._repeat) 478 | start = getLibuvNow(); 479 | try { 480 | const args = timer._timerArgs; 481 | // 执行用户设置的回调 482 | if (args === undefined) 483 | timer._onTimeout(); 484 | else 485 | timer._onTimeout(...args); 486 | } finally { 487 | /* 488 | 设置了重复执行回调,即来自setInterval。 489 | 则需要重新加入链表。 490 | */ 491 | if (timer._repeat && 492 | timer._idleTimeout !== -1) { 493 | // 更新超时时间,一样的时间间隔 494 | timer._idleTimeout = timer._repeat; 495 | // 重新插入链表 496 | insert(timer, timer._idleTimeout, start); 497 | } else if (!timer._idleNext && 498 | !timer._idlePrev && 499 | !timer._destroyed) { 500 | timer._destroyed = true; 501 | // 是ref类型,则减去一个,防止阻止事件循环退出 502 | if (timer[kRefed]) 503 | refCount--; 504 | } 505 | // 为空则删除 506 | if (list === timerListMap[msecs]) { 507 | delete timerListMap[msecs]; 508 | // 从优先队列中删除该节点,并调整队列结构 509 | timerListQueue.shift(); 510 | } 511 | } 512 | ``` 513 | 514 | 上面的代码主要是遍历优先队列 515 | 1 如果当前节点超时,则遍历它对应的链表。遍历链表的时候如果遇到超时的节点则执行。如果遇到没有超时的节点,则说明后面的节点也不会超时了,因为链表是有序的,接着重新计算出最快超时时间,修改链表的expiry字段。调整在优先队列的位置。因为修改后的expiry可能会导致位置发生变化。如果链表的节点全部都超时了,则从优先队列中删除链表对应的节点。重新调整优先队列的节点。 516 | 2 如果当前节点没有超时则说明后面的节点也不会超时了。因为当前节点是优先队列中最快到期(最小的)的节点。接着设置Libuv的定时器时间为当前节点的时间。等待下一次超时处理。 517 | ## 10.6 ref和unref 518 | setTimeout返回的是一个Timeout对象,该提供了ref和unref接口,刚才提到了关于定时器影响事件循环退出的内容,我们看一下这个原理。刚才说到Node.js定时器模块在Libuv中只对应一个定时器节点。在Node.js初始化的时候,初始化了该节点。 519 | 520 | ```cpp 521 | void Environment::InitializeLibuv(bool start_profiler_idle_notifier) { 522 | // 初始化定时器 523 | CHECK_EQ(0, uv_timer_init(event_loop(), timer_handle())); 524 | // 置unref状态 525 | uv_unref(reinterpret_cast(timer_handle())); 526 | } 527 | ``` 528 | 529 | 我们看到底层定时器节点默认是unref状态的,所以不会影响事件循环的退出。因为初始化时JS层没有定时节点。可以通过Node.js提供的接口修改该状态。Node.js支持ref状态的Timeout(setTimeout)和unref状态的Timeout(setUnrefTimeout)。 530 | 531 | ```js 532 | function Timeout(callback, after, args, isRepeat, isRefed) { 533 | if (isRefed) 534 | incRefCount(); 535 | this[kRefed] = isRefed; 536 | } 537 | ``` 538 | 539 | 最后一个参数就是控制ref还是unref的。我们继续看一下如果isRefed为true的时候的逻辑 540 | 541 | ```js 542 | function incRefCount() { 543 | if (refCount++ === 0) 544 | toggleTimerRef(true); 545 | } 546 | ``` 547 | 548 | refCount初始化的时候是1,所以在新加第一个Timeout的时候,if成立。我们接着看toggleTimerRef,该函数对应的代码如下 549 | 550 | ```cpp 551 | void Environment::ToggleTimerRef(bool ref) { 552 | // 打上ref标记, 553 | if (ref) { 554 | uv_ref(reinterpret_cast(timer_handle())); 555 | } else { 556 | uv_unref(reinterpret_cast(timer_handle())); 557 | } 558 | } 559 | ``` 560 | 561 | 该函数正是给定时器对应的handle设置状态的。setTimeout的时候,isRefed的值是true的,Node.js还提供了另外一个函数setUnrefTimeout。 562 | 563 | ```js 564 | function setUnrefTimeout(callback, after) { 565 | const timer = new Timeout(callback, after, undefined, false, false); 566 | insert(timer, timer._idleTimeout); 567 | return timer; 568 | } 569 | ``` 570 | 571 | 该函数和setTimeout最主要的区别是new Timeout的时候,最后一个参数是false(isRefed变量的值),所以setUnrefTimeout设置的定时器是不会影响Libuv事件循环退出的。另外除了Node.js直接提供的api后。我们还可以通过Timeout对象提供的ref和unref手动控制这个状态。 572 | 现在通过一个例子具体来看一下。 573 | 574 | ```js 575 | const timeout = setTimeout(() => { 576 | console.log(1) 577 | }, 10000); 578 | timeout.unref(); 579 | // timeout.ref(); 加这一句会输出1 580 | ``` 581 | 582 | 上面的代码中,1是不会输出,除非把注释去掉。Unref和ref是相反的参数,即把定时器模块对应的Libuv handle改成unref状态。 583 | -------------------------------------------------------------------------------- /docs/chapter04-线程池.md: -------------------------------------------------------------------------------- 1 | # 第四章 线程池 2 | Libuv是单线程事件驱动的异步IO库,对于阻塞式或耗时的操作,如果在Libuv的主循环里执行的话,就会阻塞后面的任务执行,所以Libuv里维护了一个线程池,它负责处理Libuv中耗时或者导致阻塞的操作,比如文件IO、DNS、自定义的耗时任务。线程池在Libuv架构中的位置如图4-1所示。 3 | 4 | ![](https://img-blog.csdnimg.cn/20210420234827155.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 5 | 6 | 7 | Libuv主线程通过线程池提供的接口把任务提交给线程池,然后立刻返回到事件循环中继续执行,线程池维护了一个任务队列,多个子线程会互斥地从中摘下任务节点执行,当子线程执行任务完毕后会通知主线程,主线程在事件循环的Poll IO阶段就会执行对应的回调。下面我们看一下线程池在Libuv中的实现。 8 | 9 | ## 4.1主线程和子线程间通信 10 | Libuv子线程和主线程的通信是使用uv_async_t结构体实现的。Libuv使用loop->async_handles队列记录所有的uv_async_t结构体,使用loop->async_io_watcher作为所有uv_async_t结构体的IO观察者,即loop-> async_handles队列上所有的handle都是共享async_io_watcher这个IO观察者的。第一次插入一个uv_async_t结构体到async_handle队列时,会初始化IO观察者,如果再次注册一个async_handle,只会在loop->async_handle队列和handle队列插入一个节点,而不会新增一个IO观察者。当uv_async_t结构体对应的任务完成时,子线程会设置IO观察者为可读。Libuv在事件循环的Poll IO阶段就会处理IO观察者。下面我们看一下uv_async_t在Libuv中的使用。 11 | ### 4.1.1 初始化 12 | 使用uv_async_t之前首先需要执行uv_async_init进行初始化。 13 | 14 | ```cpp 15 | int uv_async_init(uv_loop_t* loop, 16 | uv_async_t* handle, 17 | uv_async_cb async_cb) { 18 | int err; 19 | // 给Libuv注册一个观察者io 20 | err = uv__async_start(loop); 21 | if (err) 22 | return err; 23 | // 设置相关字段,给Libuv插入一个handle 24 | uv__handle_init(loop, (uv_handle_t*)handle, UV_ASYNC); 25 | // 设置回调 26 | handle->async_cb = async_cb; 27 | // 初始化标记字段,0表示没有任务完成 28 | handle->pending = 0; 29 | // 把uv_async_t插入async_handle队列 30 | QUEUE_INSERT_TAIL(&loop->async_handles, &handle->queue); 31 | uv__handle_start(handle); 32 | return 0; 33 | } 34 | ``` 35 | 36 | uv_async_init函数主要初始化结构体uv_async_t的一些字段,然后执行QUEUE_INSERT_TAIL给Libuv的async_handles队列追加一个节点。我们看到还有一个uv__async_start函数。我们看一下uv__async_start的实现。 37 | 38 | ```cpp 39 | static int uv__async_start(uv_loop_t* loop) { 40 | int pipefd[2]; 41 | int err; 42 | // uv__async_start只执行一次,有fd则不需要执行了 43 | if (loop->async_io_watcher.fd != -1) 44 | return 0; 45 | // 获取一个用于进程间通信的fd(Linux的eventfd机制) 46 | err = uv__async_eventfd(); 47 | /* 48 | 成功则保存fd,失败说明不支持eventfd, 49 | 则使用管道通信作为进程间通信 50 | */ 51 | if (err >= 0) { 52 | pipefd[0] = err; 53 | pipefd[1] = -1; 54 | } 55 | else if (err == UV_ENOSYS) { 56 | // 不支持eventfd则使用匿名管道 57 | err = uv__make_pipe(pipefd, UV__F_NONBLOCK); 58 | #if defined(__Linux__) 59 | if (err == 0) { 60 | char buf[32]; 61 | int fd; 62 | snprintf(buf, sizeof(buf), "/proc/self/fd/%d", pipefd[0]); // 通过一个fd就可以实现对管道的读写,高级用法 63 | fd = uv__open_cloexec(buf, O_RDWR); 64 | if (fd >= 0) { 65 | // 关掉旧的 66 | uv__close(pipefd[0]); 67 | uv__close(pipefd[1]); 68 | // 赋值新的 69 | pipefd[0] = fd; 70 | pipefd[1] = fd; 71 | } 72 | } 73 | #endif 74 | } 75 | // err大于等于0说明拿到了通信的读写两端 76 | if (err < 0) 77 | return err; 78 | /* 79 | 初始化IO观察者async_io_watcher, 80 | 把读端文件描述符保存到IO观察者 81 | */ 82 | uv__io_init(&loop->async_io_watcher, uv__async_io, pipefd[0]); 83 | // 注册IO观察者到loop里,并注册感兴趣的事件POLLIN,等待可读 84 | uv__io_start(loop, &loop->async_io_watcher, POLLIN); 85 | // 保存写端文件描述符 86 | loop->async_wfd = pipefd[1]; 87 | return 0; 88 | } 89 | ``` 90 | 91 | uv__async_start只会执行一次,时机在第一次执行uv_async_init的时候。uv__async_start主要的逻辑如下 92 | 1 获取通信描述符(通过eventfd生成一个通信的fd(充当读写两端)或者管道生成线程间通信的两个fd表示读端和写端)。 93 | 2 封装感兴趣的事件和回调到IO观察者然后追加到watcher_queue队列,在Poll IO阶段,Libuv会注册到epoll里面,如果有任务完成,也会在Poll IO阶段执行回调。 94 | 3 保存写端描述符。任务完成时通过写端fd通知主线程。 95 | 我们看到uv__async_start函数里有很多获取通信文件描述符的逻辑,总的来说,是为了完成两端通信的功能。初始化async结构体后,Libuv结构如图4-2所示。 96 | 97 | ![](https://img-blog.csdnimg.cn/20210420234949238.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 98 | 99 | 100 | ### 4.1.2 通知主线程 101 | 初始化async结构体后,如果async结构体对应的任务完成后,就会通知主线程,子线程通过设置这个handle的pending为1标记任务完成,然后再往管道写端写入标记,通知主线程有任务完成了。 102 | 103 | ```cpp 104 | int uv_async_send(uv_async_t* handle) { 105 | /* Do a cheap read first. */ 106 | if (ACCESS_ONCE(int, handle->pending) != 0) 107 | return 0; 108 | /* 109 | 如pending是0,则设置为1,返回0,如果是1则返回1, 110 | 所以如果多次调用该函数是会被合并的 111 | */ 112 | if (cmpxchgi(&handle->pending, 0, 1) == 0) 113 | uv__async_send(handle->loop); 114 | return 0; 115 | } 116 | 117 | static void uv__async_send(uv_loop_t* loop) { 118 | const void* buf; 119 | ssize_t len; 120 | int fd; 121 | int r; 122 | 123 | buf = ""; 124 | len = 1; 125 | fd = loop->async_wfd; 126 | 127 | #if defined(__Linux__) 128 | // 说明用的是eventfd而不是管道,eventfd时读写两端对应同一个fd 129 | if (fd == -1) { 130 | static const uint64_t val = 1; 131 | buf = &val; 132 | len = sizeof(val); 133 | // 见uv__async_start 134 | fd = loop->async_io_watcher.fd; /* eventfd */ 135 | } 136 | #endif 137 | // 通知读端 138 | do 139 | r = write(fd, buf, len); 140 | while (r == -1 && errno == EINTR); 141 | 142 | if (r == len) 143 | return; 144 | 145 | if (r == -1) 146 | if (errno == EAGAIN || errno == EWOULDBLOCK) 147 | return; 148 | 149 | abort(); 150 | } 151 | ``` 152 | 153 | uv_async_send首先拿到写端对应的fd,然后调用write函数,此时,往管道的写端写入数据,标记有任务完成。有写则必然有读。读的逻辑是在uv__io_poll中实现的。uv__io_poll函数即Libuv中Poll IO阶段执行的函数。在uv__io_poll中会发现管道可读,然后执行对应的回调uv__async_io。 154 | ### 4.1.3 主线程处理回调 155 | 156 | ```cpp 157 | static void uv__async_io(uv_loop_t* loop, 158 | uv__io_t* w, 159 | unsigned int events) { 160 | char buf[1024]; 161 | ssize_t r; 162 | QUEUE queue; 163 | QUEUE* q; 164 | uv_async_t* h; 165 | 166 | for (;;) { 167 | // 消费所有的数据 168 | r = read(w->fd, buf, sizeof(buf)); 169 | // 数据大小大于buf长度(1024),则继续消费 170 | if (r == sizeof(buf)) 171 | continue; 172 | // 成功消费完毕,跳出消费的逻辑 173 | if (r != -1) 174 | break; 175 | // 读繁忙 176 | if (errno == EAGAIN || errno == EWOULDBLOCK) 177 | break; 178 | // 读被中断,继续读 179 | if (errno == EINTR) 180 | continue; 181 | abort(); 182 | } 183 | // 把async_handles队列里的所有节点都移到queue变量中 184 | QUEUE_MOVE(&loop->async_handles, &queue); 185 | while (!QUEUE_EMPTY(&queue)) { 186 | // 逐个取出节点 187 | q = QUEUE_HEAD(&queue); 188 | // 根据结构体字段获取结构体首地址 189 | h = QUEUE_DATA(q, uv_async_t, queue); 190 | // 从队列中移除该节点 191 | QUEUE_REMOVE(q); 192 | // 重新插入async_handles队列,等待下次事件 193 | QUEUE_INSERT_TAIL(&loop->async_handles, q); 194 | /* 195 | 将第一个参数和第二个参数进行比较,如果相等, 196 | 则将第三参数写入第一个参数,返回第二个参数的值, 197 | 如果不相等,则返回第一个参数的值。 198 | */ 199 | /* 200 | 判断触发了哪些async。pending在uv_async_send里设置成1, 201 | 如果pending等于1,则清0,返回1.如果pending等于0,则返回0 202 | */ 203 | if (cmpxchgi(&h->pending, 1, 0) == 0) 204 | continue; 205 | 206 | if (h->async_cb == NULL) 207 | continue; 208 | // 执行上层回调 209 | h->async_cb(h); 210 | } 211 | } 212 | ``` 213 | 214 | uv__async_io会遍历async_handles队列,pending等于1的话说明任务完成,然后执行对应的回调并清除标记位。 215 | ## 4.2 线程池的实现 216 | 了解了Libuv中子线程和主线程的通信机制后,我们来看一下线程池的实现。 217 | ### 4.2.1 线程池的初始化 218 | 线程池是懒初始化的,Node.js启动的时候,并没有创建子线程,而是在提交第一个任务给线程池时,线程池才开始初始化。我们先看线程池的初始化逻辑,然后再看它的使用。 219 | 220 | ```cpp 221 | static void init_threads(void) { 222 | unsigned int i; 223 | const char* val; 224 | // 默认线程数4个,static uv_thread_t default_threads[4]; 225 | nthreads = ARRAY_SIZE(default_threads); 226 | // 判断用户是否在环境变量中设置了线程数,是的话取用户定义的 227 | val = getenv("UV_THREADPOOL_SIZE"); 228 | if (val != NULL) 229 | nthreads = atoi(val); 230 | if (nthreads == 0) 231 | nthreads = 1; 232 | // #define MAX_THREADPOOL_SIZE 128最多128个线程 233 | if (nthreads > MAX_THREADPOOL_SIZE) 234 | nthreads = MAX_THREADPOOL_SIZE; 235 | 236 | threads = default_threads; 237 | // 超过默认大小,重新分配内存 238 | if (nthreads > ARRAY_SIZE(default_threads)) { 239 | threads = uv__malloc(nthreads * sizeof(threads[0])); 240 | } 241 | // 初始化条件变量,用于有任务时唤醒子线程,没有任务时挂起子线程 242 | if (uv_cond_init(&cond)) 243 | abort(); 244 | // 初始化互斥变量,用于多个子线程互斥访问任务队列 245 | if (uv_mutex_init(&mutex)) 246 | abort(); 247 | 248 | // 初始化三个队列 249 | QUEUE_INIT(&wq); 250 | QUEUE_INIT(&slow_io_pending_wq); 251 | QUEUE_INIT(&run_slow_work_message); 252 | 253 | // 创建多个线程,工作函数为worker,sem为worker入参 254 | for (i = 0; i < nthreads; i++) 255 | if (uv_thread_create(threads + i, worker, &sem)) 256 | abort(); 257 | } 258 | ``` 259 | 260 | 线程池初始化时,会根据配置的子线程数创建对应数量的线程。默认是4个,最大128个子线程(不同版本的Libuv可能会不一样),我们也可以通过环境变量设置自定义的大小。线程池的初始化主要是初始化一些数据结构,然后创建多个线程,接着在每个线程里执行worker函数处理任务。后面我们会分析worker的逻辑。 261 | ### 4.2.2 提交任务到线程池 262 | 了解线程池的初始化之后,我们看一下如何给线程池提交任务 263 | 264 | ```cpp 265 | // 给线程池提交一个任务 266 | void uv__work_submit(uv_loop_t* loop, 267 | struct uv__work* w, 268 | enum uv__work_kind kind, 269 | void (*work)(struct uv__work* w), 270 | void (*done)(struct uv__work* w, int status)){ 271 | /* 272 | 保证已经初始化线程,并只执行一次,所以线程池是在提交第一个 273 | 任务的时候才被初始化,init_once -> init_threads 274 | */ 275 | uv_once(&once, init_once); 276 | w->loop = loop; 277 | w->work = work; 278 | w->done = done; 279 | post(&w->wq, kind); 280 | } 281 | ``` 282 | 283 | 这里把业务相关的函数和任务完成后的回调函数封装到uv__work结构体中。uv__work结构定义如下。 284 | 285 | ```cpp 286 | struct uv__work { 287 | void (*work)(struct uv__work *w); 288 | void (*done)(struct uv__work *w, int status); 289 | struct uv_loop_s* loop; 290 | void* wq[2]; 291 | }; 292 | ``` 293 | 294 | 然后调调用post函数往线程池的队列中加入一个新的任务。Libuv把任务分为三种类型,慢IO(DNS解析)、快IO(文件操作)、CPU密集型等,kind就是说明任务的类型的。我们接着看post函数。 295 | 296 | ```cpp 297 | static void post(QUEUE* q, enum uv__work_kind kind) { 298 | // 加锁访问任务队列,因为这个队列是线程池共享的 299 | uv_mutex_lock(&mutex); 300 | // 类型是慢IO 301 | if (kind == UV__WORK_SLOW_IO) { 302 | /* 303 | 插入慢IO对应的队列,Libuv这个版本把任务分为几种类型, 304 | 对于慢IO类型的任务,Libuv是往任务队列里面插入一个特殊的节点 305 | run_slow_work_message,然后用slow_io_pending_wq维护了一个慢IO 306 | 任务的队列,当处理到run_slow_work_message这个节点的时候, 307 | Libuv会从slow_io_pending_wq队列里逐个取出任务节点来执行。 308 | */ 309 | QUEUE_INSERT_TAIL(&slow_io_pending_wq, q); 310 | /* 311 | 有慢IO任务的时候,需要给主队列wq插入一个消息节点 312 | run_slow_work_message,说明有慢IO任务,所以如果 313 | run_slow_work_message是空,说明还没有插入主队列。需要进行 314 | q = &run_slow_work_message;赋值,然后把 315 | run_slow_work_message插入主队列。如果run_slow_work_message 316 | 非空,说明已经插入线程池的任务队列了。解锁然后直接返回。 317 | */ 318 | if (!QUEUE_EMPTY(&run_slow_work_message)) { 319 | uv_mutex_unlock(&mutex); 320 | return; 321 | } 322 | // 说明run_slow_work_message还没有插入队列,准备插入队列 323 | q = &run_slow_work_message; 324 | } 325 | // 把节点插入主队列,可能是慢IO消息节点或者一般任务 326 | QUEUE_INSERT_TAIL(&wq, q); 327 | /* 328 | 有空闲线程则唤醒它,如果大家都在忙, 329 | 则等到它忙完后就会重新判断是否还有新任务 330 | */ 331 | if (idle_threads > 0) 332 | uv_cond_signal(&cond); 333 | // 操作完队列,解锁 334 | uv_mutex_unlock(&mutex); 335 | } 336 | ``` 337 | 338 | 这就是Libuv中线程池的生产者逻辑。任务队列的架构如图4-3所示。 339 | 340 | ![](https://img-blog.csdnimg.cn/20210420235058824.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 341 | 342 | 343 | 344 | 除了上面提到的,Libuv还提供了另外一种生产任务的方式,即uv_queue_work函数,它只提交CPU密集型的任务(在Node.js的crypto模块中使用)。下面我们看uv_queue_work的实现。 345 | 346 | ```cpp 347 | int uv_queue_work(uv_loop_t* loop, 348 | uv_work_t* req, 349 | uv_work_cb work_cb, 350 | uv_after_work_cb after_work_cb) { 351 | if (work_cb == NULL) 352 | return UV_EINVAL; 353 | 354 | uv__req_init(loop, req, UV_WORK); 355 | req->loop = loop; 356 | req->work_cb = work_cb; 357 | req->after_work_cb = after_work_cb; 358 | uv__work_submit(loop, 359 | &req->work_req, 360 | UV__WORK_CPU, 361 | uv__queue_work, 362 | uv__queue_done); 363 | return 0; 364 | } 365 | ``` 366 | 367 | uv_queue_work函数其实也没有太多的逻辑,它保存用户的工作函数和回调到request中。然后把uv__queue_work和uv__queue_done封装到uv__work中,接着提交任务到线程池中。所以当这个任务被执行的时候。它会执行工作函数uv__queue_work。 368 | 369 | ```cpp 370 | static void uv__queue_work(struct uv__work* w) { 371 | // 通过结构体某字段拿到结构体地址 372 | uv_work_t* req = container_of(w, uv_work_t, work_req); 373 | req->work_cb(req); 374 | } 375 | ``` 376 | 377 | 我们看到uv__queue_work其实就是对用户定义的任务函数进行了封装。这时候我们可以猜到,uv__queue_done也只是对用户回调的简单封装,即它会执行用户的回调。 378 | ### 4.2.3 处理任务 379 | 我们提交了任务后,线程自然要处理,初始化线程池的时候我们分析过,worker函数是负责处理任务。我们看一下worker函数的逻辑。 380 | 381 | ```cpp 382 | static void worker(void* arg) { 383 | struct uv__work* w; 384 | QUEUE* q; 385 | int is_slow_work; 386 | // 线程启动成功 387 | uv_sem_post((uv_sem_t*) arg); 388 | arg = NULL; 389 | // 加锁互斥访问任务队列 390 | uv_mutex_lock(&mutex); 391 | for (;;) { 392 | /* 393 | 1 队列为空 394 | 2 队列不为空,但是队列中只有慢IO任务且正在执行的慢IO任务 395 | 个数达到阈值则空闲线程加一,防止慢IO占用过多线程,导致 396 | 其它快的任务无法得到执行 397 | */ 398 | while (QUEUE_EMPTY(&wq) || 399 | (QUEUE_HEAD(&wq) == &run_slow_work_message && 400 | QUEUE_NEXT(&run_slow_work_message) == &wq && 401 | slow_io_work_running >= slow_work_thread_threshold())) { 402 | idle_threads += 1; 403 | // 阻塞,等待唤醒 404 | uv_cond_wait(&cond, &mutex); 405 | // 被唤醒,开始干活,空闲线程数减一 406 | idle_threads -= 1; 407 | } 408 | // 取出头结点,头指点可能是退出消息、慢IO,一般请求 409 | q = QUEUE_HEAD(&wq); 410 | // 如果头结点是退出消息,则结束线程 411 | if (q == &exit_message) { 412 | /* 413 | 唤醒其它因为没有任务正阻塞等待任务的线程, 414 | 告诉它们准备退出 415 | */ 416 | uv_cond_signal(&cond); 417 | uv_mutex_unlock(&mutex); 418 | break; 419 | } 420 | // 移除节点 421 | QUEUE_REMOVE(q); 422 | // 重置前后指针 423 | QUEUE_INIT(q); 424 | is_slow_work = 0; 425 | /* 426 | 如果当前节点等于慢IO节点,上面的while只判断了是不是只有慢 427 | IO任务且达到阈值,这里是任务队列里肯定有非慢IO任务,可能有 428 | 慢IO,如果有慢IO并且正在执行的个数达到阈值,则先不处理该慢 429 | IO任务,继续判断是否还有非慢IO任务可执行。 430 | */ 431 | if (q == &run_slow_work_message) { 432 | // 达到阈值,该节点重新入队,因为刚才被删除了 433 | if (slow_io_work_running >= slow_work_thread_threshold()) { 434 | QUEUE_INSERT_TAIL(&wq, q); 435 | continue; 436 | } 437 | /* 438 | 没有慢IO任务则继续,这时候run_slow_work_message 439 | 已经从队列中被删除,下次有慢IO的时候重新入队 440 | */ 441 | if (QUEUE_EMPTY(&slow_io_pending_wq)) 442 | continue; 443 | // 有慢IO,开始处理慢IO任务 444 | is_slow_work = 1; 445 | /* 446 | 正在处理慢IO任务的个数累加,用于其它线程判断慢IO任务个 447 | 数是否达到阈值, slow_io_work_running是多个线程共享的变量 448 | */ 449 | slow_io_work_running++; 450 | // 摘下一个慢IO任务 451 | q = QUEUE_HEAD(&slow_io_pending_wq); 452 | // 从慢IO队列移除 453 | QUEUE_REMOVE(q); 454 | QUEUE_INIT(q); 455 | /* 456 | 取出一个任务后,如果还有慢IO任务则把慢IO标记节点重新入 457 | 队,表示还有慢IO任务,因为上面把该标记节点出队了 458 | */ 459 | if (!QUEUE_EMPTY(&slow_io_pending_wq)) { 460 | QUEUE_INSERT_TAIL(&wq, &run_slow_work_message); 461 | // 有空闲线程则唤醒它,因为还有任务处理 462 | if (idle_threads > 0) 463 | uv_cond_signal(&cond); 464 | } 465 | } 466 | // 不需要操作队列了,尽快释放锁 467 | uv_mutex_unlock(&mutex); 468 | // q是慢IO或者一般任务 469 | w = QUEUE_DATA(q, struct uv__work, wq); 470 | // 执行业务的任务函数,该函数一般会阻塞 471 | w->work(w); 472 | // 准备操作loop的任务完成队列,加锁 473 | uv_mutex_lock(&w->loop->wq_mutex); 474 | // 置空说明执行完了,见cancel逻辑 475 | w->work = NULL; 476 | /* 477 | 执行完任务,插入到loop的wq队列,在uv__work_done的时候会 478 | 执行该队列的节点 479 | */ 480 | QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq); 481 | // 通知loop的wq_async节点 482 | uv_async_send(&w->loop->wq_async); 483 | uv_mutex_unlock(&w->loop->wq_mutex); 484 | // 为下一轮操作任务队列加锁 485 | uv_mutex_lock(&mutex); 486 | /* 487 | 执行完慢IO任务,记录正在执行的慢IO个数变量减1, 488 | 上面加锁保证了互斥访问这个变量 489 | */ 490 | if (is_slow_work) { 491 | slow_io_work_running--; 492 | } 493 | } 494 | } 495 | ``` 496 | 497 | 我们看到消费者的逻辑似乎比较复杂,对于慢IO类型的任务,Libuv限制了处理慢IO任务的线程数,避免耗时比较少的任务得不到处理。其余的逻辑和一般的线程池类似,就是互斥访问任务队列,然后取出节点执行,执行完后通知主线程。结构如图4-4所示。 498 | 499 | ![](https://img-blog.csdnimg.cn/20210420235148855.png) 500 | 501 | 502 | 503 | ### 4.2.4 通知主线程 504 | 线程执行完任务后,并不是直接执行用户回调,而是通知主线程,由主线程统一处理,这是Node.js单线程事件循环的要求,也避免了多线程带来的复杂问题,我们看一下这块的逻辑。一切要从Libuv的初始化开始 505 | 506 | ```cpp 507 | uv_default_loop();-> uv_loop_init();-> uv_async_init(loop, &loop->wq_async, uv__work_done); 508 | ``` 509 | 510 | 刚才我们已经分析过主线程和子线程的通信机制,wq_async是用于线程池中子线程和主线程通信的async handle,它对应的回调是uv__work_done。所以当一个线程池的线程任务完成时,通过uv_async_send(&w->loop->wq_async)设置loop->wq_async.pending = 1,然后通知IO观察者,Libuv在Poll IO阶段就会执行该handle对应的回调uv__work_done函数。那么我们就看看这个函数的逻辑。 511 | 512 | ```cpp 513 | void uv__work_done(uv_async_t* handle) { 514 | struct uv__work* w; 515 | uv_loop_t* loop; 516 | QUEUE* q; 517 | QUEUE wq; 518 | int err; 519 | // 通过结构体字段获得结构体首地址 520 | loop = container_of(handle, uv_loop_t, wq_async); 521 | // 准备处理队列,加锁 522 | uv_mutex_lock(&loop->wq_mutex); 523 | /* 524 | loop->wq是已完成的任务队列。把loop->wq队列的节点全部移到 525 | wp变量中,这样一来可以尽快释放锁 526 | */ 527 | QUEUE_MOVE(&loop->wq, &wq); 528 | // 不需要使用了,解锁 529 | uv_mutex_unlock(&loop->wq_mutex); 530 | // wq队列的节点来自子线程插入 531 | while (!QUEUE_EMPTY(&wq)) { 532 | q = QUEUE_HEAD(&wq); 533 | QUEUE_REMOVE(q); 534 | w = container_of(q, struct uv__work, wq); 535 | // 等于uv__canceled说明这个任务被取消了 536 | err = (w->work == uv__cancelled) ? UV_ECANCELED : 0; 537 | // 执行回调 538 | w->done(w, err); 539 | } 540 | } 541 | ``` 542 | 543 | 该函数的逻辑比较简单,逐个处理已完成的任务节点,执行回调,在Node.js中,这里的回调是C++层,然后再到JS层。结构图如图4-5所示。 544 | 545 | ![](https://img-blog.csdnimg.cn/20210420235212281.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RIRUFOQVJLSA==,size_16,color_FFFFFF,t_70) 546 | 547 | 548 | ### 4.2.5 取消任务 549 | 线程池的设计中,取消任务是一个比较重要的能力,因为在线程里执行的都是一些耗时或者引起阻塞的操作,如果能及时取消一个任务,将会减轻很多没必要的处理。不过Libuv实现中,只有当任务还在等待队列中才能被取消,如果一个任务正在被线程处理,则无法取消了。我们先看一下Libuv中是如何实现取消任务的。Libuv提供了uv__work_cancel函数支持用户取消提交的任务。我们看一下它的逻辑。 550 | 551 | ```cpp 552 | static int uv__work_cancel(uv_loop_t* loop, uv_req_t* req, struct uv__work* w) { 553 | int cancelled; 554 | // 加锁,为了把节点移出队列 555 | uv_mutex_lock(&mutex); 556 | // 加锁,为了判断w->wq是否为空 557 | uv_mutex_lock(&w->loop->wq_mutex); 558 | /* 559 | cancelled为true说明任务还在线程池队列等待处理 560 | 1 处理完,w->work == NULL 561 | 2 处理中,QUEUE_EMPTY(&w->wq)为true,因 562 | 为worker在摘下一个任务的时候,重置prev和next指针 563 | 3 未处理,!QUEUE_EMPTY(&w->wq)是true 且w->work != NULL 564 | */ 565 | cancelled = !QUEUE_EMPTY(&w->wq) && w->work != NULL; 566 | // 从线程池任务队列中删除该节点 567 | if (cancelled) 568 | QUEUE_REMOVE(&w->wq); 569 | 570 | uv_mutex_unlock(&w->loop->wq_mutex); 571 | uv_mutex_unlock(&mutex); 572 | // 正在执行或者已经执行完了,则不能取消 573 | if (!cancelled) 574 | return UV_EBUSY; 575 | // 打取消标记,Libuv执行回调的时候用到 576 | w->work = uv__cancelled; 577 | 578 | uv_mutex_lock(&loop->wq_mutex); 579 | /* 580 | 插入loop的wq队列,对于取消的动作,Libuv认为是任务执行完了。 581 | 所以插入已完成的队列,执行回调的时候会通知用户该任务的执行结果 582 | 是取消,错误码是UV_ECANCELED 583 | */ 584 | QUEUE_INSERT_TAIL(&loop->wq, &w->wq); 585 | // 通知主线程有任务完成 586 | uv_async_send(&loop->wq_async); 587 | uv_mutex_unlock(&loop->wq_mutex); 588 | 589 | return 0; 590 | } 591 | ``` 592 | 593 | 在Libuv中,取消任务的方式就是把节点从线程池待处理队列中删除,然后打上取消的标记(w->work = uv__cancelled),接着把该节点插入已完成队列,Libuv在处理已完成队列的节点时,判断如果w->work == uv__cancelled则在执行用户回调时,传入错误码UV_ECANCELED,我们看到uv__work_cancel这个函数定义前面加了一个static,说明这个函数是只在本文件内使用的,Libuv对外提供的取消任务的接口是uv_cancel。 594 | 595 | 596 | -------------------------------------------------------------------------------- /docs/chapter27-深入理解 Node.js 的 Buffer.md: -------------------------------------------------------------------------------- 1 | 前言:Buffer 模块是 Node.js 非常重要的模块,很多模块都依赖它,本文介绍一下 Buffer 模块底层的原理,包括 Buffer 的核心实现和 V8 堆外内存等内容。 2 | 3 | # 1 Buffer 的实现 4 | ## 1.1 Buffer 的 JS 层实现 5 | Buffer 模块的实现虽然非常复杂,代码也非常多,但是很多都是编码解码以及内存分配管理的逻辑,我们从常用的使用方式 Buffer.from 来看看 Buffer 的核心实现。 6 | ```c 7 | Buffer.from = function from(value, encodingOrOffset, length) { 8 | return fromString(value, encodingOrOffset); 9 | }; 10 | 11 | function fromString(string, encoding) { 12 | return fromStringFast(string, ops); 13 | } 14 | 15 | function fromStringFast(string, ops) { 16 | const length = ops.byteLength(string); 17 | // 长度太长,从 C++ 层分配 18 | if (length >= (Buffer.poolSize >>> 1)) 19 | return createFromString(string, ops.encodingVal); 20 | // 剩下的不够了,扩容 21 | if (length > (poolSize - poolOffset)) 22 | createPool(); 23 | // 从 allocPool (ArrayBuffer)中分配内存 24 | let b = new FastBuffer(allocPool, poolOffset, length); 25 | const actual = ops.write(b, string, 0, length); 26 | poolOffset += actual; 27 | alignPool(); 28 | return b; 29 | } 30 | ``` 31 | from 的逻辑如下: 32 | 1. 如果长度大于 Node.js 设置的阈值,则调用 createFromString 通过 C++ 层直接分配内存。 33 | 2. 否则判断之前剩下的内存是否足够,足够则直接分配。Node.js 初始化时会首先分配一大块内存由 JS 管理,每次从这块内存了切分一部分给使用方,如果不够则扩容。 34 | 我们看看 createPool。 35 | ```c 36 | // 分配一个内存池 37 | function createPool() { 38 | poolSize = Buffer.poolSize; 39 | // 拿到底层的 ArrayBuffer 40 | allocPool = createUnsafeBuffer(poolSize).buffer; 41 | poolOffset = 0; 42 | } 43 | 44 | function createUnsafeBuffer(size) { 45 | zeroFill[0] = 0; 46 | try { 47 | return new FastBuffer(size); 48 | } finally { 49 | zeroFill[0] = 1; 50 | } 51 | } 52 | 53 | class FastBuffer extends Uint8Array {} 54 | ``` 55 | 我们看到最终调用 Uint8Array 实现了内存分配。 56 | 3. 通过 new FastBuffer(allocPool, poolOffset, length) 从内存池中分配一块内存。如下图所示。 57 | ![](https://img-blog.csdnimg.cn/926ba056c76f417d8a515dfbbe4bfe36.png) 58 | ## 1.2 Buffer 的 C++ 层实现 59 | 分析 C++ 层之前我们先看一下 V8 里下面几个对象的关系图。 60 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/a50fdf50695b453e94fbba2a86802cd7.png)接着来看看通过 createFromString 直接从 C++ 申请内存的实现。 61 | ```c 62 | void CreateFromString(const FunctionCallbackInfo& args) { 63 | enum encoding enc = static_cast(args[1].As()->Value()); 64 | Local buf; 65 | if (New(args.GetIsolate(), args[0].As(), enc).ToLocal(&buf)) 66 | args.GetReturnValue().Set(buf); 67 | } 68 | 69 | MaybeLocal New(Isolate* isolate, 70 | Local string, 71 | enum encoding enc) { 72 | EscapableHandleScope scope(isolate); 73 | 74 | size_t length; 75 | // 计算长度 76 | if (!StringBytes::Size(isolate, string, enc).To(&length)) 77 | return Local(); 78 | size_t actual = 0; 79 | char* data = nullptr; 80 | // 直接通过 realloc 在进程堆上申请一块内存 81 | data = UncheckedMalloc(length); 82 | // 按照编码转换数据 83 | actual = StringBytes::Write(isolate, data, length, string, enc); 84 | return scope.EscapeMaybe(New(isolate, data, actual)); 85 | } 86 | 87 | MaybeLocal New(Isolate* isolate, char* data, size_t length) { 88 | EscapableHandleScope handle_scope(isolate); 89 | Environment* env = Environment::GetCurrent(isolate); 90 | Local obj; 91 | if (Buffer::New(env, data, length).ToLocal(&obj)) 92 | return handle_scope.Escape(obj); 93 | return Local(); 94 | } 95 | 96 | MaybeLocal New(Environment* env, 97 | char* data, 98 | size_t length) { 99 | // JS 层变量释放后使得这块内存没人用了,GC 时在回调里释放这块内存 100 | auto free_callback = [](char* data, void* hint) { free(data); }; 101 | return New(env, data, length, free_callback, nullptr); 102 | } 103 | 104 | MaybeLocal New(Environment* env, 105 | char* data, 106 | size_t length, 107 | FreeCallback callback, 108 | void* hint) { 109 | EscapableHandleScope scope(env->isolate()); 110 | // 创建一个 ArrayBuffer 111 | Local ab = 112 | CallbackInfo::CreateTrackedArrayBuffer(env, data, length, callback, hint); 113 | /* 114 | 创建一个 Uint8Array 115 | Buffer::New => Local ui = Uint8Array::New(ab, byte_offset, length) 116 | */ 117 | MaybeLocal maybe_ui = Buffer::New(env, ab, 0, length); 118 | 119 | Local ui; 120 | if (!maybe_ui.ToLocal(&ui)) 121 | return MaybeLocal(); 122 | 123 | return scope.Escape(ui); 124 | } 125 | ``` 126 | 通过一系列的调用,最后通过 CreateTrackedArrayBuffer 创建了一个 ArrayBuffer,再通过 ArrayBuffer 创建了一个 Uint8Array。接着看一下 CreateTrackedArrayBuffer 的实现。 127 | ```c 128 | Local CallbackInfo::CreateTrackedArrayBuffer( 129 | Environment* env, 130 | char* data, 131 | size_t length, 132 | FreeCallback callback, 133 | void* hint) { 134 | // 管理回调 135 | CallbackInfo* self = new CallbackInfo(env, callback, data, hint); 136 | // 用自己申请的内存创建一个 BackingStore,并设置 GC 回调 137 | std::unique_ptr bs = 138 | ArrayBuffer::NewBackingStore(data, length, [](void*, size_t, void* arg) { 139 | static_cast(arg)->OnBackingStoreFree(); 140 | }, self); 141 | // 通过 BackingStore 创建 ArrayBuffer 142 | Local ab = ArrayBuffer::New(env->isolate(), std::move(bs)); 143 | return ab; 144 | } 145 | ``` 146 | 看一下 NewBackingStore 的实现。 147 | ```c 148 | std::unique_ptr v8::ArrayBuffer::NewBackingStore( 149 | void* data, size_t byte_length, v8::BackingStore::DeleterCallback deleter, 150 | void* deleter_data) { 151 | std::unique_ptr backing_store = i::BackingStore::WrapAllocation(data, byte_length, deleter, deleter_data, 152 | i::SharedFlag::kNotShared); 153 | return std::unique_ptr( 154 | static_cast(backing_store.release())); 155 | } 156 | 157 | std::unique_ptr BackingStore::WrapAllocation( 158 | void* allocation_base, size_t allocation_length, 159 | v8::BackingStore::DeleterCallback deleter, void* deleter_data, 160 | SharedFlag shared) { 161 | bool is_empty_deleter = (deleter == v8::BackingStore::EmptyDeleter); 162 | // 新建一个 BackingStore 163 | auto result = new BackingStore(allocation_base, // start 164 | allocation_length, // length 165 | allocation_length, // capacity 166 | shared, // shared 167 | false, // is_wasm_memory 168 | true, // free_on_destruct 169 | false, // has_guard_regions 170 | // 说明释放内存由调用方执行 171 | true, // custom_deleter 172 | is_empty_deleter); // empty_deleter 173 | // 保存回调需要的信息 174 | result->type_specific_data_.deleter = {deleter, deleter_data}; 175 | return std::unique_ptr(result); 176 | } 177 | ``` 178 | NewBackingStore 最终是创建了一个 BackingStore 对象。我们再看一下 GC 时 BackingStore 的析构函数里都做了什么。 179 | ```c 180 | BackingStore::~BackingStore() { 181 | if (custom_deleter_) { 182 | type_specific_data_.deleter.callback(buffer_start_, byte_length_, 183 | type_specific_data_.deleter.data); 184 | Clear(); 185 | return; 186 | } 187 | } 188 | ``` 189 | 析构的时候会执行创建 BackingStore 时保存的回调。我们看一下管理回调的 CallbackInfo 的实现。 190 | ```c 191 | CallbackInfo::CallbackInfo(Environment* env, 192 | FreeCallback callback, 193 | char* data, 194 | void* hint) 195 | : callback_(callback), 196 | data_(data), 197 | hint_(hint), 198 | env_(env) { 199 | env->AddCleanupHook(CleanupHook, this); 200 | env->isolate()->AdjustAmountOfExternalAllocatedMemory(sizeof(*this)); 201 | } 202 | ``` 203 | CallbackInfo 的实现很简单,主要的地方是 AdjustAmountOfExternalAllocatedMemory。该函数告诉 V8 堆外内存增加了多少个字节,V8 会根据内存的数据做适当的 GC。CallbackInfo 主要是保存了回调和内存地址。接着在 GC 的时候会回调 CallbackInfo 的 OnBackingStoreFree。 204 | ```c 205 | void CallbackInfo::OnBackingStoreFree() { 206 | std::unique_ptr self { this }; 207 | Mutex::ScopedLock lock(mutex_); 208 | // check 阶段执行 CallAndResetCallback 209 | env_->SetImmediateThreadsafe([self = std::move(self)](Environment* env) { 210 | self->CallAndResetCallback(); 211 | }); 212 | } 213 | 214 | void CallbackInfo::CallAndResetCallback() { 215 | FreeCallback callback; 216 | { 217 | Mutex::ScopedLock lock(mutex_); 218 | callback = callback_; 219 | callback_ = nullptr; 220 | } 221 | if (callback != nullptr) { 222 | // 堆外内存减少了这么多个字节 223 | int64_t change_in_bytes = -static_cast(sizeof(*this)); 224 | env_->isolate()->AdjustAmountOfExternalAllocatedMemory(change_in_bytes); 225 | // 执行回调,通常是释放内存 226 | callback(data_, hint_); 227 | } 228 | } 229 | ``` 230 | ## 1.3 Buffer C++ 层的另一种实现 231 | 刚才介绍的 C++ 实现中内存是由自己分配并释放的,下面介绍另一种内存的分配和释放由 V8 管理的场景。以 Buffer 的提供的 EncodeUtf8String 函数为例,该函数实现字符串的编码。 232 | ```c 233 | static void EncodeUtf8String(const FunctionCallbackInfo& args) { 234 | Environment* env = Environment::GetCurrent(args); 235 | Isolate* isolate = env->isolate(); 236 | // 被编码的字符串 237 | Local str = args[0].As(); 238 | size_t length = str->Utf8Length(isolate); 239 | // 分配内存 240 | AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, length); 241 | // 编码 242 | str->WriteUtf8(isolate, 243 | buf.data(), 244 | -1, // We are certain that `data` is sufficiently large 245 | nullptr, 246 | String::NO_NULL_TERMINATION | String::REPLACE_INVALID_UTF8); 247 | // 基于上面申请的 buf 内存新建一个 Uint8Array 248 | auto array = Uint8Array::New(buf.ToArrayBuffer(), 0, length); 249 | args.GetReturnValue().Set(array); 250 | } 251 | ``` 252 | 我们重点分析 AllocatedBuffer::AllocateManaged。 253 | ```c 254 | AllocatedBuffer AllocatedBuffer::AllocateManaged( 255 | Environment* env, 256 | size_t size) { 257 | NoArrayBufferZeroFillScope no_zero_fill_scope(env->isolate_data()); 258 | std::unique_ptr bs = v8::ArrayBuffer::NewBackingStore(env->isolate(), size); 259 | return AllocatedBuffer(env, std::move(bs)); 260 | } 261 | ``` 262 | AllocateManaged 调用 NewBackingStore 申请了内存。 263 | ```c 264 | std::unique_ptr v8::ArrayBuffer::NewBackingStore( 265 | Isolate* isolate, size_t byte_length) { 266 | 267 | i::Isolate* i_isolate = reinterpret_cast(isolate); 268 | std::unique_ptr backing_store = 269 | i::BackingStore::Allocate(i_isolate, byte_length, 270 | i::SharedFlag::kNotShared, 271 | i::InitializedFlag::kZeroInitialized); 272 | 273 | return std::unique_ptr( 274 | static_cast(backing_store.release())); 275 | } 276 | ``` 277 | 继续看 BackingStore::Allocate。 278 | ```c 279 | std::unique_ptr BackingStore::Allocate( 280 | Isolate* isolate, size_t byte_length, SharedFlag shared, 281 | InitializedFlag initialized) { 282 | void* buffer_start = nullptr; 283 | // ArrayBuffer 内存分配器,可以自定义,V8 默认提供的是使用平台相关的堆内存分析函数,比如 malloc 284 | auto allocator = isolate->array_buffer_allocator(); 285 | if (byte_length != 0) { 286 | auto allocate_buffer = [allocator, initialized](size_t byte_length) { 287 | // 分配内存 288 | void* buffer_start = allocator->Allocate(byte_length); 289 | return buffer_start; 290 | }; 291 | // 同步执行 allocate_buffer 分配内存 292 | buffer_start = isolate->heap()->AllocateExternalBackingStore(allocate_buffer, byte_length); 293 | } 294 | // 新建 BackingStore 管理内存 295 | auto result = new BackingStore(buffer_start, // start 296 | byte_length, // length 297 | byte_length, // capacity 298 | shared, // shared 299 | false, // is_wasm_memory 300 | true, // free_on_destruct 301 | false, // has_guard_regions 302 | false, // custom_deleter 303 | false); // empty_deleter 304 | 305 | return std::unique_ptr(result); 306 | } 307 | 308 | ``` 309 | BackingStore::Allocate 分配一块内存并新建 BackingStore 对象管理这块内存,内存分配器是在初始化 V8 的时候设置的。这里我们再看一下 AllocateExternalBackingStore 函数的逻辑。 310 | ```c 311 | void* Heap::AllocateExternalBackingStore( 312 | const std::function& allocate, size_t byte_length) { 313 | // 可能需要触发 GC 314 | if (!always_allocate()) { 315 | size_t new_space_backing_store_bytes = 316 | new_space()->ExternalBackingStoreBytes(); 317 | if (new_space_backing_store_bytes >= 2 * kMaxSemiSpaceSize && 318 | new_space_backing_store_bytes >= byte_length) { 319 | CollectGarbage(NEW_SPACE, 320 | GarbageCollectionReason::kExternalMemoryPressure); 321 | } 322 | } 323 | // 分配内存 324 | void* result = allocate(byte_length); 325 | // 成功则返回 326 | if (result) return result; 327 | // 失败则进行 GC 328 | if (!always_allocate()) { 329 | for (int i = 0; i < 2; i++) { 330 | CollectGarbage(OLD_SPACE, 331 | GarbageCollectionReason::kExternalMemoryPressure); 332 | result = allocate(byte_length); 333 | if (result) return result; 334 | } 335 | isolate()->counters()->gc_last_resort_from_handles()->Increment(); 336 | CollectAllAvailableGarbage( 337 | GarbageCollectionReason::kExternalMemoryPressure); 338 | } 339 | // 再次分配,失败则返回失败 340 | return allocate(byte_length); 341 | } 342 | ``` 343 | 我们看到通过 BackingStore 申请内存失败时会触发 GC 来腾出更多的可用内存。分配完内存后,最终以 BackingStore 对象为参数,返回一个 AllocatedBuffer 对象。 344 | ```c 345 | AllocatedBuffer::AllocatedBuffer( 346 | Environment* env, std::unique_ptr bs) 347 | : env_(env), backing_store_(std::move(bs)) {} 348 | ``` 349 | 接着把 AllocatedBuffer 对象转成 ArrayBuffer 对象。 350 | ```c 351 | v8::Local AllocatedBuffer::ToArrayBuffer() { 352 | return v8::ArrayBuffer::New(env_->isolate(), std::move(backing_store_)); 353 | } 354 | ``` 355 | 最后把 ArrayBuffer 对象传入 Uint8Array 返回一个 Uint8Array 对象返回给调用方。 356 | # 2 Uint8Array 的使用和实现 357 | 从前面的实现中可以看到 C++ 层的实现中,内存都是从进程的堆中分配的,那么 JS 层通过 Uint8Array 申请的内存是否也是在进程堆中申请的呢?下面我们看看 V8 中 Uint8Array 的实现。Uint8Array 有多种创建方式,我们只看 new Uint8Array(length) 的实现。 358 | ```c 359 | transitioning macro ConstructByLength(implicit context: Context)( 360 | map: Map, lengthObj: JSAny, 361 | elementsInfo: typed_array::TypedArrayElementsInfo): JSTypedArray { 362 | try { 363 | // 申请的内存大小 364 | const length: uintptr = ToIndex(lengthObj); 365 | // 拿到创建 ArrayBuffer 的函数 366 | const defaultConstructor: Constructor = GetArrayBufferFunction(); 367 | const initialize: constexpr bool = true; 368 | return TypedArrayInitialize( 369 | initialize, map, length, elementsInfo, defaultConstructor) 370 | otherwise RangeError; 371 | } 372 | } 373 | 374 | transitioning macro TypedArrayInitialize(implicit context: Context)( 375 | initialize: constexpr bool, map: Map, length: uintptr, 376 | elementsInfo: typed_array::TypedArrayElementsInfo, 377 | bufferConstructor: JSReceiver): JSTypedArray labels IfRangeError { 378 | 379 | const byteLength = elementsInfo.CalculateByteLength(length); 380 | const byteLengthNum = Convert(byteLength); 381 | const defaultConstructor = GetArrayBufferFunction(); 382 | const byteOffset: uintptr = 0; 383 | 384 | try { 385 | // 创建 JSArrayBuffer 386 | const buffer = AllocateEmptyOnHeapBuffer(byteLength); 387 | const isOnHeap: constexpr bool = true; 388 | // 通过 buffer 创建 TypedArray 389 | const typedArray = AllocateTypedArray( 390 | isOnHeap, map, buffer, byteOffset, byteLength, length); 391 | // 内存置 0 392 | if constexpr (initialize) { 393 | const backingStore = typedArray.data_ptr; 394 | typed_array::CallCMemset(backingStore, 0, byteLength); 395 | } 396 | 397 | return typedArray; 398 | } 399 | } 400 | ``` 401 | 主要逻辑分为两步,首先通过 AllocateEmptyOnHeapBuffer 申请一个 JSArrayBuffer,然后以 JSArrayBuffer 创建一个 TypedArray。我们先看一下 AllocateEmptyOnHeapBuffer。 402 | ```c 403 | TNode TypedArrayBuiltinsAssembler::AllocateEmptyOnHeapBuffer( 404 | TNode context, TNode byte_length) { 405 | 406 | TNode native_context = LoadNativeContext(context); 407 | TNode map = CAST(LoadContextElement(native_context, Context::ARRAY_BUFFER_MAP_INDEX)); 408 | TNode empty_fixed_array = EmptyFixedArrayConstant(); 409 | // 申请一个 JSArrayBuffer 对象所需要的内存 410 | TNode buffer = UncheckedCast(Allocate(JSArrayBuffer::kSizeWithEmbedderFields)); 411 | // 初始化对象的属性 412 | StoreMapNoWriteBarrier(buffer, map); 413 | StoreObjectFieldNoWriteBarrier(buffer, JSArray::kPropertiesOrHashOffset, empty_fixed_array); 414 | StoreObjectFieldNoWriteBarrier(buffer, JSArray::kElementsOffset, empty_fixed_array); 415 | int32_t bitfield_value = (1 << JSArrayBuffer::IsExternalBit::kShift) | 416 | (1 << JSArrayBuffer::IsDetachableBit::kShift); 417 | StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kBitFieldOffset, Int32Constant(bitfield_value)); 418 | StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kByteLengthOffset, byte_length); 419 | // 设置 buffer 为 nullptr 420 | StoreJSArrayBufferBackingStore(buffer, EncodeExternalPointer(ReinterpretCast(IntPtrConstant(0)))); 421 | StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kExtensionOffset, IntPtrConstant(0)); 422 | for (int offset = JSArrayBuffer::kHeaderSize; offset < JSArrayBuffer::kSizeWithEmbedderFields; offset += kTaggedSize) { 423 | StoreObjectFieldNoWriteBarrier(buffer, offset, SmiConstant(0)); 424 | } 425 | return buffer; 426 | } 427 | ``` 428 | AllocateEmptyOnHeapBuffer 申请了一个空的 JSArrayBuffer 对象,空的意思是说没有存储数据的内存。接着看基于 JSArrayBuffer 对象 通过 AllocateTypedArray 创建一个 TypedArray。 429 | ```c 430 | transitioning macro AllocateTypedArray(implicit context: Context)( 431 | isOnHeap: constexpr bool, map: Map, buffer: JSArrayBuffer, 432 | byteOffset: uintptr, byteLength: uintptr, length: uintptr): JSTypedArray { 433 | // 从 V8 堆中申请存储数据的内存 434 | let elements: ByteArray = AllocateByteArray(byteLength); 435 | // 申请一个 JSTypedArray 对象 436 | const typedArray = UnsafeCast(AllocateFastOrSlowJSObjectFromMap(map)); 437 | // 初始化属性 438 | typedArray.elements = elements; 439 | typedArray.buffer = buffer; 440 | typedArray.byte_offset = byteOffset; 441 | typedArray.byte_length = byteLength; 442 | typedArray.length = length; 443 | typed_array::SetJSTypedArrayOnHeapDataPtr(typedArray, elements, byteOffset); 444 | SetupTypedArrayEmbedderFields(typedArray); 445 | return typedArray; 446 | } 447 | ``` 448 | 我们发现 Uint8Array 申请的内存是基于 V8 堆的,而不是 V8 的堆外内存,这难道和 C++ 层的实现不一样?Uint8Array 的内存的确是基于 V8 堆的,比如我像下面这样使用的时候。 449 | ```c 450 | const arr = new Uint8Array(1); 451 | arr[0] = 65; 452 | ``` 453 | 但是如果我们使用 arr.buffer 的时候,情况就不一样了。我们看看具体的实现。 454 | ```c 455 | BUILTIN(TypedArrayPrototypeBuffer) { 456 | HandleScope scope(isolate); 457 | CHECK_RECEIVER(JSTypedArray, typed_array, 458 | "get %TypedArray%.prototype.buffer"); 459 | return *typed_array->GetBuffer(); 460 | } 461 | ``` 462 | 接着看 GetBuffer 的实现。 463 | ```c 464 | Handle JSTypedArray::GetBuffer() { 465 | Isolate* isolate = GetIsolate(); 466 | Handle self(*this, isolate); 467 | // 拿到 TypeArray 对应的 JSArrayBuffer 对象 468 | Handle array_buffer(JSArrayBuffer::cast(self->buffer()), isolate); 469 | // 分配过了直接返回 470 | if (!is_on_heap()) { 471 | return array_buffer; 472 | } 473 | size_t byte_length = self->byte_length(); 474 | // 申请 byte_length 字节内存存储数据 475 | auto backing_store = BackingStore::Allocate(isolate, byte_length, SharedFlag::kNotShared, InitializedFlag::kUninitialized); 476 | // 关联 backing_store 到 array_buffer 477 | array_buffer->Setup(SharedFlag::kNotShared, std::move(backing_store)); 478 | return array_buffer; 479 | } 480 | ``` 481 | 我们看到当使用 buffer 的时候,V8 会在 V8 堆外申请内存来替代初始化 Uint8Array 时在 V8 堆内分配的内存,并且把原来的数据复制过来。看一下下面的例子。 482 | ```c 483 | console.log(process.memoryUsage().arrayBuffers) 484 | let a = new Uint8Array(10); 485 | a[0] = 65; 486 | console.log(process.memoryUsage().arrayBuffers) 487 | ``` 488 | 我们会发现 arrayBuffers 的值是一样的,说明 Uint8Array 初始化时没有通过 arrayBuffers 申请堆外内存。接着再看下一个例子。 489 | ```c 490 | console.log(process.memoryUsage().arrayBuffers) 491 | let a = new Uint8Array(1); 492 | a[0] = 65; 493 | a.buffer 494 | console.log(process.memoryUsage().arrayBuffers) 495 | console.log(new Uint8Array(a.buffer)) 496 | ``` 497 | 我们看到输出的内存增加了一个字节,输出的 a.buffer 是 [ 65 ](申请内存大于 64 字节会在堆外内存分配)。 498 | # 3 堆外内存的管理 499 | 从之前的分析中我们看到,Node.js Buffer 是基于堆外内存实现的(自己申请进程堆内存或者使用 V8 默认的内存分配器),我们知道,平时使用的变量都是由 V8 负责管理内存的,那么 Buffer 所代表的堆外内存是怎么管理的呢?Buffer 的内存释放也是由 V8 跟踪的,不过释放的逻辑和堆内内存不太一样。我们通过一些例子来分析一下。 500 | ```c 501 | function forceGC() { 502 | new ArrayBuffer(1024 * 1024 * 1024); 503 | } 504 | setTimeout(() => { 505 | /* 506 | 从 C++ 层调用 V8 对象创建内存 507 | let a = process.binding('buffer').createFromString("你好", 1); 508 | */ 509 | /* 510 | 直接使用 V8 内置对象 511 | let a = new ArrayBuffer(10); 512 | */ 513 | // 从 C++ 层自己管理内存 514 | let a = process.binding('buffer').encodeUtf8String("你好"); 515 | // 置空等待 GC 516 | a = null; 517 | // 分配一块大内存触发 GC 518 | process.nextTick(forceGC); 519 | }, 1000); 520 | const net = require('net'); 521 | net.createServer((socket) => {}).listen() 522 | ``` 523 | 在 V8 的代码打断点,然后调试以上代码。 524 | ![](https://img-blog.csdnimg.cn/325620e7d4764644a11fe23d514fecf3.png) 525 | 我们看到在超时回调里 V8 分配了一个 ArrayBufferExtension 对象并记录到 ArrayBufferSweeper 中。 接着看一下触发 GC 时的逻辑。 526 | ![](https://img-blog.csdnimg.cn/0a66808e50c94fd9a322cc8b2597a780.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAdGhlYW5hcmto,size_20,color_FFFFFF,t_70,g_se,x_16) 527 | ![](https://img-blog.csdnimg.cn/1e43fda36dc94892832758693bc761f4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAdGhlYW5hcmto,size_20,color_FFFFFF,t_70,g_se,x_16) 528 | V8 在 GC 中会调用 heap_->array_buffer_sweeper()->RequestSweepYoung() 回收堆外内存,另外 Node.js 本身似乎也使用线程去回收 堆外内存。我们再看一下自己管理内存的情况下回调的触发。 529 | ![](https://img-blog.csdnimg.cn/f0e63e78f0fe4ec7abdb50bf32c60997.png) 530 | 如果这样写是不会触发 BackingStore::~BackingStore 执行的,再次验证了 Uint8Array 初始化时没有使用 BackingStore。 531 | ```c 532 | setTimeout(() => { 533 | let a = new Uint8Array(1); 534 | // a.buffer; 535 | a = null; 536 | process.nextTick(forceGC); 537 | }); 538 | ``` 539 | 但是如果把注释打开就可以。 540 | 541 | # 4 总结 542 | Buffer 平时用起来可能比较简单,但是如果深入研究它的实现就会发现涉及的内容不仅多,而且还复杂,不过深入理解了它的底层实现后,会有种豁然开朗的感觉,另外 Buffer 的内存是堆外内存,如果我们发现进程的内存不断增长但是 V8 堆快照大小变化不大,那可能是 Buffer 变量没有释放,理解实现能帮助我们更好地思考问题和解决问题。 543 | --------------------------------------------------------------------------------