用户登录
用户注册

分享至

Nodejs libuv运行原理详解

  • 作者: Lzy8023hd
  • 来源: 51数据库
  • 2021-08-16

前言

这应该是nodejs的运行原理的第7篇分享,这篇过后,短时间内不会再分享nodejs的运行原理,会停更一段时间,ps:不是不更,而是会开挖新的坑,最近有在研究rpg maker mv,区块链,云计算,可能会更新一些相关文章,或者相关教学。

回到正题,异步编程的难点在于请求与响应不是按顺序发生的。以http server 为例,异步编程赋予了server 高并发的品质,而且他可以以很小的资源代价,不断地接受和处理请求。但是快速处理请求不表示快速地返回请求=>高并发不等同于快速反馈。

在nodejs中,libuv则为异步编程的实现提供了可能。libuv为builtin modules 提供了api,这些api用来支撑请求和数据的返回的异步处理方式。

这一篇分享,我们主要讨论libuv的运行原理,从两个角度出发:

1) libuv的架构

2) 案例,从细节的角度看libuv是如何对待不同i/o请求,按照不同的方式来完成异步请求和数据返回的。

libuv的架构

从左往右可分为两部分,network i/o的相关请求,另一部分file i/o,dns ops和user code组成。

上图展示了libuv细节的流程,图中代码很简单,包括2个部分:

1. server.listen()是用来创建tcp server时,通常放在最后一步执行的代码。主要指定服务器工作的端口以及回调函数。

2. fs.open()是用异步的方式打开一个文件。

选择两个示例很简单,因为libuv架构图可视:libuv对 network i/o和 file i/o采用不同的机制。

上图右半部分,主要分成两个部分:

1. 主线程:主线程也是node启动时执行的现成。node启动时,会完成一系列的初始化动作,启动v8 engine,进入下一个循环。

2. 线程池:线程池的数量可以通过环境变量uv_threadpool_size配置,最大不超过128个,默认为4个。

network 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中。这样的好处是请求能够立即得到处理。中断处理机制里面的下半部分与数据处理操作相似,交由主线程去完成处理。

代码逻辑很简单,查看loop中是否包含handle,如果有遍历default loop。

file i/o

这里我们研究一下 file i/o。

同network i/o一样,我们的应用所依赖的fs模块,后面有一个builtin module node_file.cc作为支撑。 node_file.cc包含了各种我们常用的文件操作的接口,例如open, read, write, chmod,chown等。但同时,它们都支持异步模式。 我们通过node_file.cc中的open()函数来研究一下具体的实现细节。

如果你用类似source insight之类的代码阅读工具跟踪一下代码调用顺序,会很容易发现对于异步模式,open()函数会在一系列辅助操作之后,进入函数uv_fs_open(),并且传入了一个fsreqwrap的对象。

fsreqwrap(),从名字可以看得出来,这是一个wrap,且是与fs相关的请求。也就是说,它基于某一个现成的机制来实现与fs相关的请求操作。这个现成的机制就是reqwrap。好吧,它也是个wrap。乘你还没疯的时候,看一下图6吧。这里完整展示了fsreqwrap类继承关系。

除了fsreqwrap,还有其它wrap,例如pipeconnectwrap,tcpconnectwrap等等。每个wrap均为一种请求类型服务。 但是这些wrap,都是node自身的行为,而与libuv相关的是什么呢?上图中表示出了fsreqwrap关键的数据结构 uv_fs_s req__。

让我们把目光回到uv_fs_open()。在调用这个函数时, req__作为其一个重要的参数被传递进去。而在uv_fs_open()内部,req__则被添加到work queue的末尾中去。图3 thread pool中的thread会去领取这些request进行处理。 每个request很像一个粘贴板,它将event loop, work queue,每个请求的处理函数(work()),以及请求结束处理函数(done())绑定在一起。绑定的操作在uv__work_submit()中完成。 例如对于这里的req__,绑定在它身上的work()为uv__fs_work(), done()为uv__fs_done()。

这里有一个比较有意思的问题值得额外看一下。我们的thread pool是在什么时候建立的呢?

答案是:在第一次异步调用uv__work_submit()时。

每个thead的入口函数是 threadpool.c中的worker()。工作逻辑比较简单,依次取出work queue中的请求,执行绑定在该请求上的work()函数。 前面我们提到的绑定在请求上的done()函数在哪里执行呢?这也是一个比较有意思的操作。libuv通过uv_async_send()通知event loop去执行相应的callback函数,也即我们绑定在request上的done()函数。uv__work_done()用于完成这样的操作。

uv_async_send()与主线程之间通过pipe通信。

我在这一小节以一个fsreqwrap以及open()函数为例,描述了libuv处理这种file i/o请求时所涉及的各种操作:

建立thread pool(只建立一次) 在每个请求req__上绑定与其相关的event loop, work queue, work(), done() thread worker()用来处理work queue里面的每个请求,并执行work() 通过uv_async_send()通知event loop执行done()

以上就是关于本次相关的知识点内容,感谢大家对的支持。

软件
前端设计
程序设计
Java相关