likes
comments
collection
share

Node是如何实现异步IO的?

作者站长头像
站长
· 阅读数 4

异步I/O在Node中应用最为广泛,但是它并不是Node的原创。

操作系统内核对于I/O只有两种方式,阻塞与非阻塞,在调用阻塞I/O时,应用程序需要等待I/O完成才会返回结果,造成CPU等待IO,这显然不能充分利用CPU的处理能力。

而非阻塞I/O与阻塞I/O的差别在于调用之后会不带数据立即返回,要获取数据,就要通过文件描述符再次读取,非阻塞I/O为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成,这种重复调用判断操作是否完成的技术叫做轮询。

在Node中完整的异步I/O环节有事件循环观察者请求对象等。

事件循环

事件循环是Node自身的执行模型,在进程启动时,Node就会创建一个循环,每执行一次循环体便会查看是否有事件要处理,如果有,就取出事件及其相关的回调函数,并执行它们,依次循环,直到不再有事件要处理。

Node是如何实现异步IO的?

观察者

观察者这个概念就是在事件循环中判断是否还有事件要处理。每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

举个例子来理解,就好比饭店厨房中的厨师,厨师们在一轮一轮制作着菜肴,而具体要做什么菜肴,取决于收银员收到的客人下的订单。厨师们每做完一轮菜肴,就会去询问收银员是否还有要做的菜品,如果没有,就下班打样。

在这个例子中,收银员扮演的角色就是观察者,而饭店收到客人的点单就是关联的回调函数,饭店也可能有多个收银员,就好比如事件循环中有多个观察者,收到下单就是一个事件,一个观察者里可能有多个事件。

让我们回归到浏览器中,事件可能来自于用户的点击或者加载某些文件时产生,这些事件都有对应的观察者。而在Node中,事件主要来源于网络请求、文件I/O等,其对应的观察者有文件I/O观察者、网络I/O观察者等。

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

请求对象

请求对象是JavaScript发起调用到内核执行完I/O操作这一过渡过程中的中间产物。

fs.open()为例,JavaScript层面的代码通过调用C++核心模块进行底层数据的处理:

Node是如何实现异步IO的?

从JavaScript调用Node的核心模块,核心模块调用C++内建模块,内建模块通过 libuv 进行系统调用,这是Node里经典的调用方式。其中 libuv 作为中间封装层,实质上调用的是uv_fs_open()方法,具体调用过程这里不再追赘述,感兴趣的掘友可查看 《深入浅出Node.js》 书籍中的3.3.3小节。

JavaScript层面发起的异步调用的第一阶段到这里就结束了,JavaScript线程此时仍然可以执行当前任务的后续操作,当前的I/O操作在线程池中等待执行,不管是否阻塞I/O,都不会影响到JavaScript线程的后续执行,这也就达到了异步的目的。

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

执行回调

异步I/O的第一部分是组装好请求对象,送入I/O线程池等待执行,而回调通知便是异步的第二部分,共同构成整个异步I/O的流程。

Node是如何实现异步IO的?

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

在Window下主要是通过IOCP来向系统内核发送I/O调用和从内核获取已完成的I/O操作,配以事件循环,来完成异步I/O的过程。

在Linux下通过epoll实现这个过程,两者的区别在于线程池在Window下由内核(IOCP)直接提供,而*nix系列下由libuv自行实现。

参考资料: 书籍《深入浅出Node.js》