nodejs是如何实现非阻塞异步io的?

656

前言:nodejs的特点,我们首先会想到,非阻塞异步io

第一:怎样实现非阻塞的呢?

通过我们所熟知的“事件循环(event loop)”。

第二:怎么实现网络请求异步io的呢?

  • 2.1: 异步io是在(event loop)哪个阶段实现的呢? 如我们所知道的nodejs的(event loop)具有如下图六个阶段。所有异步的回调都会在对应的阶段中执行。“poll”阶段住要处理的是io的输入和输出是否就绪。并且有的情况下会在timeout的时间内轮训这个阶段。我们nodejs的异步io就在这个阶段实现的。
  • 2.2 既然是在“poll”阶段实现异步io的,那“poll”阶段是怎么做到的呢? 先解释下操作系统中三种io机制,

1: select

  • 特点1: 每次需要全量拷贝文件描述符到操作系统内核。(关键词:“全量拷贝”,是不是比较消耗性能
  • 特点2: 内核获取到要监听的文件描述符后就需要不断轮训,这个文件描述符列表,获取可用的文件描述符。(关键词:“轮训”,是不是比较消耗性能
  • 特点3: 每个进程内使用的文件描述符数量是有限制的。(这个在编译操作系统内核是就写死了所有进程能用的对打文件数了)。(关键词:“数量是有限制的”,每个进程能监听的io操作是不是被限制了

2: poll

  • select类似

3: epoll

  • 特点1: 采用红黑树的方式保存文件操作符。(关键词:“红黑树”,每次要新监听一个文件描述符,只需要new一个新的描述符,插入树中。而且没有大小限制
  • 特点2: 采用事件机制的方式通知文件描述符就绪,不需要轮训整棵红黑树的文件描述符。(关键词: 事件机制 ” 通过事件机制就能获取到哪个文件描素符就绪,不用轮训整棵树
  • 特点3: 采用链表的方式保存所有已经准备就绪的文件描述符。

这三种io机制虽然各有特点,但也有共同点,就是他们在io轮训阶段,都会有一个timeOut时间。

1: 若没有准备就绪的文件描述符,那三种机制都会不断在当前阶段轮训获取可以的文件描述符直到timeOut时间达到,才退出当前轮训

2: 虽然nodejs通过eventLoop是非阻塞的,但是在调用系统内核的io机制进行“io多路复用”(什么是io多路复用文章的下面👇会提到)的轮训时,系统内核是会阻塞当前调用进程的。

结论1: 从上面不同io机制,加粗显示的特点描述,我们可以得出epoll性能要优于select和poll

结论2:从上面不同io机制的相同点,在调用系统内核io轮训可用描述符时会阻塞我们当前进程

基于操作系统提供select,poll和poll三种io机制,我们怎么调用呢?

备注: linux,unix系统中一切皆文件,键盘显示器等都是文件,系统初始化就有三个标准文件描素符,标准输入(stdin),标准输出(stdout)和标准错误(stderr)。每一次io操作又会在操作系统的文件“描述符表”中新增一个文件描述符,这个文件描述符可以理解为指向当前io操作文件的一个指针。通过这个“文件描述符状态”可以知道当前io操作是否就绪。

什么是“文件描述符状态”

  • 文件描述符状态可以理解为io操作的3种状态。(io操作完成已经就绪,io操作未完成,io操作出错)
  • 第一:只采用异步io,那么nodejs进程与操作系统 (调用一次只能检查一个文件描述符)
  • 第二:采用io多路复用(其实不管是java也好,nodejs也好。都是采用的io多路复用),(通过一次系统调用,检查多个文件描述符的状态) ,这样可以最大程度的节省性能的消耗。 很明显io多路复用性能优于单个的异步io,可以减少进程或线程与操作系统内核间的调用,通过一次操作系统调用就能监听多个文件描述符。

通过上面得出

1: epoll性能要优于select和poll

2: io多路复用性能优于单个的异步io

因此:nodejs正是在“poll”阶段,采用了操作系统内核中epoll的io机制来实现io多路复用机制

思考题:nodejs的“poll”阶段为上面会有timeOut

我们上面提到nodejs采用的是epoll机制的io多路复用,那所有的io多路复用多有个共同点。

image.png

nodejs中(event loop)的“poll”阶段的timeOut正是来源自这里,假如设置操作系统内核io机制的timeOut时间为null的话,如果一直没有准备就绪的文件描素符时,那“poll”阶段将一直阻塞在这里,整个nodejs进程也会阻塞在这里。

补充

epoll方式中,采用事件内机制通知文件描述符是否准备就绪。那什么时候通知呢?

1: 水平触发

  • 只要是文件描述符对应的文件准备就绪后,
  • 进程会来读取准备就绪的文件,
  • 若是当前没有读取完文件内容,之后还会触发文件描述符准备就绪事件。

这样是不是会触发多次文件描素符准备就绪的事件。

2: 边缘触发

  • 让准备就绪的文件能一次就读取完成,
  • 不用再次触发文件描述符准备就绪事件。(就是减少准备就绪事件的触发,提升性能)

减少事件的触发,提升性能

第二:怎么实现文件的异步io的呢?

  • 调用的是libuv库的线程池

结尾

其实了解这样一个过程也挺有趣的,中间也和后端的java同学讨论过,java的io多路复用和我们nodejs中“poll”阶段io多路复用的原理是一样的