Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node是如何实现异步I/O的 #156

Open
wuxianqiang opened this issue May 16, 2019 · 0 comments
Open

Node是如何实现异步I/O的 #156

wuxianqiang opened this issue May 16, 2019 · 0 comments

Comments

@wuxianqiang
Copy link
Owner

wuxianqiang commented May 16, 2019

完成整个异步I/O环节的有事件循环、观察者和请求对象等。

事件循环

首先,我们着重强调一下Node自身的执行模型——事件循环,正是它使得回调函数十分普遍。在进程启动时,Node便会创建一个类似于while(true) 的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。流程图如图所示。

image

观察者

在每个Tick的过程中,如何判断是否有事件需要处理呢?这里必须要引入的概念是观察者。每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

浏览器采用了类似的机制。事件可能来自用户的点击或者加载某些文件时产生,而这些产生的事件都有对应的观察者。在Node中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等。观察者将事件进行了分类。

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

在Windows下,这个循环基于IOCP创建,而在*nix下则基于多线程创建。

请求对象

在这一节中,我们将通过解释Windows下异步I/O(利用IOCP实现)的简单例子来探寻从JavaScript代码到系统内核之间都发生了什么。对于一般的(非异步)回调函数,函数由我们自行调用,如下所示:

var forEach = function (list, callback) {
    for (var i = 0; i < list.length; i++) {
        callback(list[i], i, list);
    }
};

对于Node中的异步I/O调用而言,回调函数却不由开发者来调用。那么从我们发出调用后,到回调函数被执行,中间发生了什么呢?事实上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它叫做请求对象。

下面我们以最简单的 fs.open() 方法来作为例子,探索Node与底层之间是如何执行异步I/O调用以及回调函数究竟是如何被调用执行的:

fs.open = function(path, flags, mode, callback) {})

fs.open() 的作用是根据指定路径和参数去打开一个文件,从而得到一个文件描述符,这是后续所有I/O操作的初始操作。从前面的代码中可以看到,JavaScript层面的代码通过调用C++核心模块进行下层的操作。图为调用示意图。

image

从JavaScript调用Node的核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用,这是Node里经典的调用方式。这里libuv作为封装层,有两个平台的实现,实质上是调用了uv_fs_open() 方法。在 uv_fs_open() 的调用过程中,我们创建了一个 FSReqWrap 请求对象。从JavaScript层传入的参数和当前方法都被封装在这个请求对象中,其中我们最为关注的回调函数则被设置在这个对象的 oncomplete_sym 属性上:

req_wrap->object_->Set(oncomplete_sym, callback);

对象包装完毕后,在Windows下,则调用 QueueUserWorkItem() 方法将这个 FSReqWrap 对象推入线
程池中等待执行,该方法的代码如下所示:

QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTEDEFAULT)

QueueUserWorkItem() 方法接受3个参数:第一个参数是将要执行的方法的引用,这里引用的是 uv_fs_thread_proc ,第二个参数是 uv_fs_thread_proc 方法运行时所需要的参数;第三个参数是执行的标志。当线程池中有可用线程时,我们会调用 uv_fs_thread_proc() 方法。 uv_fs_thread_proc() 方法会根
据传入参数的类型调用相应的底层函数。以uv_fs_open() 为例,实际上调用 fs__open() 方法。

至此,JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段就此结束。

JavaScript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到JavaScript线程的后续执行,如此就达到了异步的目的。

请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。

执行回调

组装好请求对象、送入I/O线程池等待执行,实际上完成了异步I/O的第一部分,回调通知是第二部分。

线程池中的I/O操作调用完毕之后,会将获取的结果储存在 req->result 属性上,然后调用 PostQueuedCompletionStatus() 通知IOCP,告知当前对象操作已经完成:

PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))

PostQueuedCompletionStatus() 方法的作用是向IOCP提交执行状态,并将线程归还线程池。通过PostQueuedCompletionStatus() 方法提交的状态,可以通过 GetQueuedCompletionStatus() 提取。

在这个过程中,我们其实还动用了事件循环的I/O观察者。在每次Tick的执行中,它会调用IOCP相关的 GetQueuedCompletionStatus() 方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者的队列中,然后将其当做事件处理。

I/O观察者回调函数的行为就是取出请求对象的result 属性作为参数,取出 oncomplete_sym 属性作为方
法,然后调用执行,以此达到调用JavaScript中传入的回调函数的目的。

至此,整个异步I/O的流程完全结束,如图所示。

image

事件循环、观察者、请求对象、I/O线程池这四者共同构成了Node异步I/O模型的基本要素。

不同的是线程池在Windows下由内核(IOCP)直接提供,*nix系列下由libuv自行实现。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant