本文你能学到:javascript
看完本文后,你应该能更好的去理解事件循环,知道事件是怎么来的,Node 究竟执行异步I/O调用。若是面试官再问事件循环还有Node与底层之间如何执行异步I/O,我以为你把本文的流程说清楚,应该能加分!本文对事件循环中的具体步骤没有详细讲解,每一个步骤看官方文档更佳。java
nodejs模块能够分为下面三类:node
好比 Node 源码lib目录下的 fs.js 就是 native 模块,而fs.js调用的 src 目录下的 node_fs.cc 就是内建模块。linux
Libuv是一个高性能的,事件驱动的异步I/O库,它自己是由C语言编写的,具备很高的可移植性。libuv封装了不一样平台底层对于异步IO模型的实现,libuv 的 API 包含有时间,非阻塞的网络,异步文件操做,子进程等等,因此它还自己具有着Windows, Linux均可使用的跨平台能力。webpack
经典libuv图(来源网上)c++
概念:输入输出完成端口(Input/Output Completion Port,IOCP), 是支持多个同时发生的异步I/O操做的应用程序编程接口,在Windows NT的3.5版本之后,或AIX5版之后或Solaris第十版之后,开始支持。git
我直接这么说概念你可能也不太懂。能够暂时知道 Windows 下注意经过 IOCP 来向系统内核发送 I/O 调用和从内核获取已完成的 I/O 操做,配以事件循环,完成异步I/O的过程。在 linux 下经过 epoll 实现这个过程,也就是由 libuv 自行实现。程序员
IOCP 的另外一个应用场景在以前Node.js进程与线程那篇文章也有写过。Mater 和 app worker 进程通讯使用到。github
线程池,是一种线程的使用模式,它为了下降线程使用中频繁的建立和销毁所带来的资源消耗与代价。 经过建立必定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束以后再从新回来继续待命。web
这就是线程池最核心的设计思路,「复用线程,平摊线程的建立与销毁的开销代价」。
本文使用到线程池的地方:在 Node 中,不管是 *nix 仍是 Window 平台。内部完成 I/O 任务的都有用到线程池。
libuv 目前使用了一个全局的线程池,全部的循环均可以往其中加入任务。目前有三种操做会在这个线程池中执行:
文件系统操做
DNS 函数(getaddrinfo 和 getnameinfo)
经过 uv_queue_work() 添加的用户代码
对比图中两段经典api代码(server.listen
和fs.open
,选择两种api的缘由:网络 I/O 表明和文件 I/O 表明)和以前 libuv 图片,咱们来一块儿理解异步I/O调用流程
server.listen() 是用来建立 TCP server 时,一般放在最后一步执行的代码。主要指定服务器工做的端口以及回调函数。
fs.open() 是用异步的方式打开一个文件。
选择两个示例很简单,由于 libuv 架构图可视:libuv 对 Network I/O和 File I/O 采用不一样的机制。
上图右半部分,主要分红两个部分:
主线程:主线程也是 node 启动时执行的现成。node 启动时,会完成一系列的初始化动做,启动 V8 engine,进入下一个循环。
线程池:线程池的数量能够经过环境变量 UV_THREADPOOL_SIZE 配置,最大不超过 128 个,默认为 4 个。
在Node.js 中经典的代码调用方式:都是从 JavaScript 调用 Node 核心模块,核心模块调用 C++ 内建模块,内建模块经过 libuv 进行系统调用。请记住这段话
不论是server.listen
仍是fs.open
,他们在开启一个 node 服务(进程)的时候,Node会建立一个while(true)的循环,这个循环就是事件循环。每执行一次循环体的过程,咱们称之为Tick。每一个Tick的过程就是查看是否有事件待处理,若是有,就取出事件及其相关的回调函数。若是存在关联的回调函数,就执行。而后进入下一个循环,若是再也不有事件处理,退出进程。
这里咱们知道事件循环已经建立了,上面加粗字体查看是否有事件待处理,去哪里查看?事件怎么进入事件循环的?什么状况会产生事件继续往下看。
继续看这张图,讲解一下事件产生基本流程,(注意网络I/O和文件I/O会有一些不一样)这里对c++代码调用简单提一下,有兴趣的小伙伴能够继续深刻研究。
(这里就用到了文初提到的模块分类知识)先是 javascript 代码,而后调用 lib/fs.js
核心模块代码 fs.open
,核心模块调用 C++ 内建模块 src/node_file.cc
,内建模块c++代码会有一个平台判断,而后经过 libuv 进行系统调用。
从前面到达 libuv ,会有一个参数,请求对象,也就是open函数前面整个流程传递进来的请求对象,它保存了全部状态,包括送入线程池等待执行以及I/O操做完毕后的回调处理。
请求对象组装完成后,送入 libuv 中建立的 I/O 线程池,线程池中的 I/O 操做完毕后,会将获取的结果存储到 req->result 属性上,而后通知某函数通知 IOCP ,告知当前对象操做已经完成。
在这整个过程当中,进程初期建立的事件循环中有一个 I/O 观察者,每次 Tick 的执行中,它会调用 IOCP 相关的方法检查线程池中是否有执行完成的请求,若是存在,会讲请求对象和以前绑定的 result 属性,加入到 I/O 观察者的队列中,而后将其看成事件处理。
看到这里,前面提到的**是否有事件待处理,去哪里查看?事件怎么进入事件循环的?**这两个问题是否是搞懂了。
文字配上图。更清晰!
V8 engine 执行从 server.listen()
开始,调用 builtin module Tcp_wrap
的过程。
在建立TCP连接的过程当中,libuv直接参与Tcp_wrap.cc
函数中的 TCPWrap::listen()
调用uv_listen()开始到执行uv_io_start()
结束。看起来很短暂的过程,实际上是相似linux kernel的中断处理机制。
uv_io_start()
负载将 handle 插入处处理的water queue
中。这样的好处是请求可以当即获得处理。中断处理机制里面的下半部分与数据处理操做类似,交由主线程去完成处理。
重要:虽然 libuv 的异步文件 I/O 操做是经过线程池实现的,可是网络 I/O 老是在单线程中执行的,注意最后仍是会把完成的内容做为事件加入事件循环,事件循环就和文件I/O相同了。
传统的服务器模型
Node就不同了!
看了文章前面的内容,Node 经过事件驱动的方式处理请求,无需为每一个请求建立额外的对应线程,能够省掉建立线程和销毁线程的开销,同时操做系统在调度任务时由于线程较少,上下文切换的代价很低。这也是 Node.js 高性能之一
Nginx 目前也采用了和 Node 相同的事件驱动方式,有兴趣的也去了解下,不过 Nginx 采用 c 语言编写。
做者简介:koala,专一完整的 Node.js 技术栈分享,从 JavaScript 到 Node.js,再到后端数据库,祝您成为优秀的高级 Node.js 工程师。【程序员成长指北】做者,Github 博客开源项目 github.com/koala-codin…
本文不少内容来自朴灵老师的 《深刻浅出Node.js》,这本书虽然出版好久了,给个人感受仍是越看越香,本身能够边看边扩展,推荐。
Libuv学习——文件处理 zhuanlan.zhihu.com/p/97789391
高性能异步 I/O 模型库 libuv 设计思路概述 blog.csdn.net/ababab12345…