【8.31】node 原理 - 异步 I/O - Node 的异步 I/O

171 阅读4分钟

这是我参与8月更文挑战的第29天,活动详情查看:8月更文挑战

这个系列第一篇文章从用户体验、资源分配,这两个方面介绍了我们为什么需要异步 I/O。这一篇文章主要讲异步 I/O 在操作系统层面的支持状况。第二篇文章介绍了异步 I/O 在操作系统层面的支持情况,异步和非阻塞着两个概念的区别。

本文主要介绍 nodejs 是如何实现异步 I/O 的。

事件循环

当进程启动时,nodejs 中就会有一个类似于 while(true) 的循环,每次执行循环体我们叫做一次 Tick,每次 Tick 的过程就是查看是否有事件要执行,如果有,就执行这个事件和关联的回调函数,然后进入下一个循环,如果没有,就退出进程。

image.png

观察者

那么如何判断有事件要处理呢?每个事件循环中都有一个或多个观察者,判断是否有事件要执行的过程,就是像观察者询问,是否有事件要处理。一个观察者可能有多个事件。

浏览器中的观察者会处理用户的点击,文件加载等,nodejs 中观察者主要是网络请求,文件 I/O 等。观察者将事件进行了分类。

事件循环是一个典型的生产者/消费者模式,异步 I/O、网络请求就是生产者,事件被传递到观察者,事件循环再从观察者那里取出文件并处理。

如前面提到的,在Windows下,这个循环基于IOCP创建,而在*nix下则基于多线程创建。

请求对象

接下来我们串一下,异步 I/O 中,JavaScript 代码到系统内核都发生了什么

我们以 fs.open() 作为例子,fs.open 是根据指定的路径和参数打开一个文件,得到一个文件描述符

fs.open = function(path, flags, mode, callback) {
    // ...
    binding.open(pathModule._makeLong(path), stringToFlags(flags),mode,callback); 
};

可以看到 fs.open 的实现中,在 JavaScript 代码中调用了 C++ 核心模块进行下面的操作,整体的流程是:

image.png

JavaScript 代码调用了 nodejs 核心模块,核心模块调用 c++ 内建模块,内建模块通过 libuv 进行系统调用,这是 nodejs 里经典的调用方式。

我们从 libuv 模块可以看到,最终都是调用了 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)

第一个参数是将要执行函数的引用,第二个参数是将要执行函数的参数,第三个是执行标识位。线程池有可用线程时,就会执行 uv_fs_thread_proc 方法,它会根据传入的参数,判断要执行哪个底层函数,我们例子中 uv_fs_open() 对应的方法就是 fs__open()

到这里的话,JavaScript 调用立即返回了,实际的操作在 I/O 线程池等待执行,不会影响到 JavaScript 线程继续执行,达到了异步的目的。

执行回调

前面我们把请求对象 FSReqWrap 放入了线程池等待执行,下面我们看一下回调如何通知。

线程池 I/O 调用完毕后,会将结果放到 req->result 上,然后会执行 PostQueuedCompletionStatus() 告诉 IOCP 当前的操作已经完成,可以把线程归还线程池了,通过 PostQueuedCompletionStatus() 提交的状态,可以通过 GetQueuedCompletionStatus() 获取到。

在每次 Tick 执行的过程中,会调用 GetQueuedCompletionStatus() 检查线程池中是否有执行完的请求,如果有的话,把它放到 I/O 观察者的队列中,I/O 观察者会取出 result 属性作为参数,取出 oncomplete_sym 作为方法,然后调用执行,就达到了执行回调函数的目的了~

整体的流程:

image.png

以上通过 windows 下的 IOCP 的看了下异步 I/O 的实现方式,线程池在Windows下由内核(IOCP)直接提供,*nix 系列下由libuv自行实现

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

本文是对深入浅出nodejs中异步I/O一章的学习总结,欢迎评论和点赞~